diff options
author | Brian Evans <grknight@gentoo.org> | 2020-10-02 15:24:06 -0400 |
---|---|---|
committer | Brian Evans <grknight@gentoo.org> | 2020-10-02 15:24:06 -0400 |
commit | 60dd5fd95847643eab04ce173f0774c9c584e795 (patch) | |
tree | 52299ac4e3c5c69df75997bfd7d62b71ef9e0089 /MLEB/Translate/MessageValidator.php | |
parent | Update Widgets to 1.35 (diff) | |
download | extensions-60dd5fd95847643eab04ce173f0774c9c584e795.tar.gz extensions-60dd5fd95847643eab04ce173f0774c9c584e795.tar.bz2 extensions-60dd5fd95847643eab04ce173f0774c9c584e795.zip |
Update MLEB to 2020.07
Signed-off-by: Brian Evans <grknight@gentoo.org>
Diffstat (limited to 'MLEB/Translate/MessageValidator.php')
-rw-r--r-- | MLEB/Translate/MessageValidator.php | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/MLEB/Translate/MessageValidator.php b/MLEB/Translate/MessageValidator.php new file mode 100644 index 00000000..e3711321 --- /dev/null +++ b/MLEB/Translate/MessageValidator.php @@ -0,0 +1,345 @@ +<?php +/** + * Message validation framework. + * + * @file + * @defgroup MessageValidator Message Validators + * @author Abijeet Patro + * @author Niklas Laxström + * @license GPL-2.0-or-later + */ + +use MediaWiki\Extensions\Translate\MessageValidator\ValidationResult; +use MediaWiki\Extensions\Translate\MessageValidator\ValidatorFactory; +use MediaWiki\Extensions\Translate\Validation\MessageValidator as MessageValidatorInterface; +use MediaWiki\Extensions\Translate\Validation\ValidationIssue; +use MediaWiki\Extensions\Translate\Validation\ValidationIssues; + +/** + * Message validator is used to run validators to find common mistakes so that + * translators can fix them quickly. This is an improvement over the old Message + * Checker framework because it allows maintainers to enforce a validation so + * that translations that do not pass validation are not saved. + * + * To create your own validator, implement the following interface, + * @see MediaWiki\Extensions\Translate\Validation\MessageValidator + * + * In addition you can use the following Trait to reuse some pre-existing methods, + * @see MediaWiki\Extensions\Translate\MessageValidator\ValidatorHelper + * + * There are two types of notices - error and warning. + * + * @link https://www.mediawiki.org/wiki/Help:Extension:Translate/Group_configuration#VALIDATORS + * @link https://www.mediawiki.org/wiki/Help:Extension:Translate/Validators + * + * @ingroup MessageValidator + * @since 2019.06 + */ +class MessageValidator { + /** @var array List of validator data */ + protected $validators = []; + + /** @var string Message group id */ + protected $groupId; + + /** @var string[][] */ + private static $ignorePatterns; + + public function __construct( string $groupId ) { + if ( self::$ignorePatterns === null ) { + // TODO: Review if this logic belongs in this class. + self::reloadIgnorePatterns(); + } + + $this->groupId = $groupId; + } + + /** Normalise validator keys. */ + protected static function foldValue( string $value ): string { + return str_replace( ' ', '_', strtolower( $value ) ); + } + + /** + * Set the validators for this group. + * + * Removes the existing validators. + * + * @see addValidator() + * @param array $validatorConfigs List of Validator configurations + */ + public function setValidators( array $validatorConfigs ): void { + $this->validators = []; + foreach ( $validatorConfigs as $config ) { + $this->addValidator( $config ); + } + } + + /** Add a validator for this group. */ + public function addValidator( array $validatorConfig ): void { + $validatorId = $validatorConfig['id'] ?? null; + $className = $validatorConfig['class'] ?? null; + + if ( $validatorId !== null ) { + $validator = ValidatorFactory::get( + $validatorId, + $validatorConfig['params'] ?? null + ); + } elseif ( $className !== null ) { + $validator = ValidatorFactory::loadInstance( $className, + $validatorConfig['params'] ?? null ); + } else { + throw new InvalidArgumentException( + 'Validator configuration does not specify the \'class\' or \'id\'.' + ); + } + + $isInsertable = $validatorConfig['insertable'] ?? false; + if ( $isInsertable && !$validator instanceof InsertablesSuggester ) { + throw new InvalidArgumentException( + "Insertable validator does not implement InsertablesSuggester interface." + ); + } + + $this->validators[] = [ + 'instance' => $validator, + 'insertable' => $isInsertable, + 'enforce' => $validatorConfig['enforce'] ?? false, + 'keymatch' => $validatorConfig['keymatch'] ?? false + ]; + } + + /** + * Return the currently set validators for this group. + * + * @return MessageValidatorInterface[] List of validators + */ + public function getValidators(): array { + return array_map( function ( $validator ) { + return $validator['instance']; + }, $this->validators ); + } + + /** + * Return currently set validators that are insertable. + * + * @return MessageValidatorInterface[] List of insertable + * validators + */ + public function getInsertableValidators(): array { + $insertableValidators = []; + foreach ( $this->validators as $validator ) { + if ( $validator['insertable'] === true ) { + $insertableValidators[] = $validator['instance']; + } + } + + return $insertableValidators; + } + + /** + * Validate a translation of a message. + * + * Returns a ValidationResult that contains methods to print the issues. + */ + public function validateMessage( + TMessage $message, string $code, bool $ignoreWarnings = false + ): ValidationResult { + $errors = new ValidationIssues(); + $warnings = new ValidationIssues(); + + foreach ( $this->validators as $validator ) { + $this->runValidation( $validator, $message, $code, $errors, $warnings, $ignoreWarnings ); + } + + $errors = $this->filterValidations( $errors, $code ); + $warnings = $this->filterValidations( $warnings, $code ); + + return new ValidationResult( $errors, $warnings ); + } + + /** Validate a message, and return as soon as any validation fails. */ + public function quickValidate( + TMessage $message, string $code, bool $ignoreWarnings = false + ): ValidationResult { + $errors = new ValidationIssues(); + $warnings = new ValidationIssues(); + + foreach ( $this->validators as $validator ) { + $this->runValidation( $validator, $message, $code, $errors, $warnings, $ignoreWarnings ); + + $errors = $this->filterValidations( $errors, $code ); + $warnings = $this->filterValidations( $warnings, $code ); + + if ( $warnings->hasIssues() || $errors->hasIssues() ) { + break; + } + } + + return new ValidationResult( $errors, $warnings ); + } + + /** @internal Should only be used by tests and inside this class. */ + public static function reloadIgnorePatterns(): void { + global $wgTranslateCheckBlacklist; + + if ( $wgTranslateCheckBlacklist === false ) { + self::$ignorePatterns = []; + return; + } + + $list = PHPVariableLoader::loadVariableFromPHPFile( + $wgTranslateCheckBlacklist, 'checkBlacklist' + ); + $keys = [ 'group', 'check', 'subcheck', 'code', 'message' ]; + + foreach ( $list as $key => $pattern ) { + foreach ( $keys as $checkKey ) { + if ( !isset( $pattern[$checkKey] ) ) { + $list[$key][$checkKey] = '#'; + } elseif ( is_array( $pattern[$checkKey] ) ) { + $list[$key][$checkKey] = + array_map( 'MessageValidator::foldValue', $pattern[$checkKey] ); + } else { + $list[$key][$checkKey] = self::foldValue( $pattern[$checkKey] ); + } + } + } + + self::$ignorePatterns = $list; + } + + /** Filter validations based on a ignore list. */ + private function filterValidations( + ValidationIssues $issues, + string $targetLanguage + ): ValidationIssues { + $filteredIssues = new ValidationIssues(); + + foreach ( $issues as $issue ) { + foreach ( self::$ignorePatterns as $pattern ) { + if ( $this->shouldIgnore( $issue, $this->groupId, $targetLanguage, $pattern ) ) { + continue 2; + } + } + $filteredIssues->add( $issue ); + } + + return $filteredIssues; + } + + private function shouldIgnore( + ValidationIssue $issue, + string $messageGroupId, + string $targetLanguage, + array $pattern + ): bool { + return $this->match( $pattern['group'], $messageGroupId ) + && $this->match( $pattern['check'], $issue->type() ) + && $this->match( $pattern['subcheck'], $issue->subType() ) + && $this->match( $pattern['message'], $issue->messageKey() ) + && $this->match( $pattern['code'], $targetLanguage ); + } + + /** + * Match validation information against a ignore pattern. + * + * @param string|array $pattern + * @param string $value The actual value in the validation produced by the validator + * @return bool True if the pattern matches the value. + */ + protected function match( $pattern, string $value ): bool { + if ( $pattern === '#' ) { + return true; + } elseif ( is_array( $pattern ) ) { + return in_array( strtolower( $value ), $pattern, true ); + } else { + return strtolower( $value ) === $pattern; + } + } + + /** + * Check if key matches validator's key patterns. + * + * Only relevant if the 'keymatch' option is specified in the validator. + * + * @param string $key + * @param string[] $keyMatches + * @return bool True if the key matches one of the matchers, false otherwise. + */ + protected function doesKeyMatch( string $key, array $keyMatches ): bool { + $normalizedKey = lcfirst( $key ); + foreach ( $keyMatches as $match ) { + if ( is_string( $match ) ) { + if ( lcfirst( $match ) === $normalizedKey ) { + return true; + } + continue; + } + + // The value is neither a string nor an array, should never happen but still handle it. + if ( !is_array( $match ) ) { + throw new InvalidArgumentException( + "Invalid key matcher configuration passed. Expected type: array or string. " . + "Recieved: " . gettype( $match ) . ". match value: " . FormatJson::encode( $match ) + ); + } + + $matcherType = $match['type']; + $pattern = $match['pattern']; + + // If regex matches, or wildcard matches return true, else continue processing. + if ( + ( $matcherType === 'regex' && preg_match( $pattern, $normalizedKey ) === 1 ) || + ( $matcherType === 'wildcard' && fnmatch( $pattern, $normalizedKey ) ) + ) { + return true; + } + } + + return false; + } + + /** + * Run the validator to produce warnings and errors. + * + * May also skip validation depending on validator configuration and $ignoreWarnings. + */ + private function runValidation( + array $validatorData, + TMessage $message, + string $targetLanguage, + ValidationIssues $errors, + ValidationIssues $warnings, + bool $ignoreWarnings + ): void { + // Check if key match has been specified, and then check if the key matches it. + /** @var MessageValidatorInterface $validator */ + $validator = $validatorData['instance']; + + $definition = $message->definition(); + if ( $definition === null ) { + // This should NOT happen, but add a check since it seems to be happening + // See: https://phabricator.wikimedia.org/T255669 + return; + } + + try { + $keyMatches = $validatorData['keymatch']; + if ( $keyMatches !== false && !$this->doesKeyMatch( $message->key(), $keyMatches ) ) { + return; + } + + if ( $validatorData['enforce'] === true ) { + $errors->merge( $validator->getIssues( $message, $targetLanguage ) ); + } elseif ( !$ignoreWarnings ) { + $warnings->merge( $validator->getIssues( $message, $targetLanguage ) ); + } + // else: caller does not want warnings, skip running the validator + } catch ( Exception $e ) { + throw new \RuntimeException( + 'An error occurred while validating message: ' . $message->key() . '; group: ' . + $this->groupId . "; validator: " . get_class( $validator ) . "\n. Exception: $e" + ); + } + } +} |