Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ValidationRunner.php
Go to the documentation of this file.
1<?php
12namespace MediaWiki\Extension\Translate\Validation;
13
14use Exception;
15use FormatJson;
16use InvalidArgumentException;
21use RuntimeException;
22
41 protected $validators = [];
43 protected $groupId;
45 private static $ignorePatterns;
46
47 public function __construct( string $groupId ) {
48 if ( self::$ignorePatterns === null ) {
49 // TODO: Review if this logic belongs in this class.
50 self::reloadIgnorePatterns();
51 }
52
53 $this->groupId = $groupId;
54 }
55
57 protected static function foldValue( string $value ): string {
58 return str_replace( ' ', '_', strtolower( $value ) );
59 }
60
69 public function setValidators( array $validatorConfigs ): void {
70 $this->validators = [];
71 foreach ( $validatorConfigs as $config ) {
72 $this->addValidator( $config );
73 }
74 }
75
77 public function addValidator( array $validatorConfig ): void {
78 $validatorId = $validatorConfig['id'] ?? null;
79 $className = $validatorConfig['class'] ?? null;
80
81 if ( $validatorId !== null ) {
82 $validator = ValidatorFactory::get(
83 $validatorId,
84 $validatorConfig['params'] ?? null
85 );
86 } elseif ( $className !== null ) {
87 $validator = ValidatorFactory::loadInstance(
88 $className,
89 $validatorConfig['params'] ?? null
90 );
91 } else {
92 throw new InvalidArgumentException(
93 'Validator configuration does not specify the \'class\' or \'id\'.'
94 );
95 }
96
97 $isInsertable = $validatorConfig['insertable'] ?? false;
98 if ( $isInsertable && !$validator instanceof InsertablesSuggester ) {
99 $actualClassName = get_class( $validator );
100 throw new InvalidArgumentException(
101 "Insertable validator $actualClassName does not implement InsertablesSuggester interface."
102 );
103 }
104
105 $this->validators[] = [
106 'instance' => $validator,
107 'insertable' => $isInsertable,
108 'enforce' => $validatorConfig['enforce'] ?? false,
109 'include' => $validatorConfig['keymatch'] ?? $validatorConfig['include'] ?? false,
110 'exclude' => $validatorConfig['exclude'] ?? false
111 ];
112 }
113
119 public function getValidators(): array {
120 return array_column( $this->validators, 'instance' );
121 }
122
129 public function getInsertableValidators(): array {
130 $insertableValidators = [];
131 foreach ( $this->validators as $validator ) {
132 if ( $validator['insertable'] === true ) {
133 $insertableValidators[] = $validator['instance'];
134 }
135 }
136
137 return $insertableValidators;
138 }
139
145 public function validateMessage(
146 Message $message,
147 string $code,
148 bool $ignoreWarnings = false
150 $errors = new ValidationIssues();
151 $warnings = new ValidationIssues();
152
153 foreach ( $this->validators as $validator ) {
154 $this->runValidation(
155 $validator,
156 $message,
157 $code,
158 $errors,
159 $warnings,
160 $ignoreWarnings
161 );
162 }
163
164 $errors = $this->filterValidations( $message->key(), $errors, $code );
165 $warnings = $this->filterValidations( $message->key(), $warnings, $code );
166
167 return new ValidationResult( $errors, $warnings );
168 }
169
171 public function quickValidate(
172 Message $message,
173 string $code,
174 bool $ignoreWarnings = false
176 $errors = new ValidationIssues();
177 $warnings = new ValidationIssues();
178
179 foreach ( $this->validators as $validator ) {
180 $this->runValidation(
181 $validator,
182 $message,
183 $code,
184 $errors,
185 $warnings,
186 $ignoreWarnings
187 );
188
189 $errors = $this->filterValidations( $message->key(), $errors, $code );
190 $warnings = $this->filterValidations( $message->key(), $warnings, $code );
191
192 if ( $warnings->hasIssues() || $errors->hasIssues() ) {
193 break;
194 }
195 }
196
197 return new ValidationResult( $errors, $warnings );
198 }
199
201 public static function reloadIgnorePatterns(): void {
202 $validationExclusionFile = Services::getInstance()->getConfigHelper()->getValidationExclusionFile();
203
204 if ( $validationExclusionFile === false ) {
205 self::$ignorePatterns = [];
206 return;
207 }
208
209 $list = PHPVariableLoader::loadVariableFromPHPFile(
210 $validationExclusionFile,
211 'validationExclusionList'
212 );
213 $keys = [ 'group', 'check', 'subcheck', 'code', 'message' ];
214
215 if ( $list && !is_array( $list ) ) {
216 throw new InvalidArgumentException(
217 "validationExclusionList defined in $validationExclusionFile must be an array"
218 );
219 }
220
221 foreach ( $list as $key => $pattern ) {
222 foreach ( $keys as $checkKey ) {
223 if ( !isset( $pattern[$checkKey] ) ) {
224 $list[$key][$checkKey] = '#';
225 } elseif ( is_array( $pattern[$checkKey] ) ) {
226 $list[$key][$checkKey] =
227 array_map(
228 [ self::class, 'foldValue' ],
229 $pattern[$checkKey]
230 );
231 } else {
232 $list[$key][$checkKey] = self::foldValue( $pattern[$checkKey] );
233 }
234 }
235 }
236
237 self::$ignorePatterns = $list;
238 }
239
241 private function filterValidations(
242 string $messageKey,
243 ValidationIssues $issues,
244 string $targetLanguage
245 ): ValidationIssues {
246 $filteredIssues = new ValidationIssues();
247
248 foreach ( $issues as $issue ) {
249 foreach ( self::$ignorePatterns as $pattern ) {
250 if ( $this->shouldIgnore( $messageKey, $issue, $this->groupId, $targetLanguage, $pattern ) ) {
251 continue 2;
252 }
253 }
254 $filteredIssues->add( $issue );
255 }
256
257 return $filteredIssues;
258 }
259
260 private function shouldIgnore(
261 string $messageKey,
262 ValidationIssue $issue,
263 string $messageGroupId,
264 string $targetLanguage,
265 array $pattern
266 ): bool {
267 return $this->matchesIgnorePattern( $pattern['group'], $messageGroupId )
268 && $this->matchesIgnorePattern( $pattern['check'], $issue->type() )
269 && $this->matchesIgnorePattern( $pattern['subcheck'], $issue->subType() )
270 && $this->matchesIgnorePattern( $pattern['message'], $messageKey )
271 && $this->matchesIgnorePattern( $pattern['code'], $targetLanguage );
272 }
273
281 private function matchesIgnorePattern( $pattern, string $value ): bool {
282 if ( $pattern === '#' ) {
283 return true;
284 } elseif ( is_array( $pattern ) ) {
285 return in_array( strtolower( $value ), $pattern, true );
286 } else {
287 return strtolower( $value ) === $pattern;
288 }
289 }
290
299 protected function doesKeyMatch( string $key, array $keyMatches ): bool {
300 $normalizedKey = lcfirst( $key );
301 foreach ( $keyMatches as $match ) {
302 if ( is_string( $match ) ) {
303 if ( lcfirst( $match ) === $normalizedKey ) {
304 return true;
305 }
306 continue;
307 }
308
309 // The value is neither a string nor an array, should never happen but still handle it.
310 if ( !is_array( $match ) ) {
311 throw new InvalidArgumentException(
312 "Invalid key matcher configuration passed. Expected type: array or string. " .
313 "Received: " . gettype( $match ) . ". match value: " . FormatJson::encode( $match )
314 );
315 }
316
317 $matcherType = $match['type'];
318 $pattern = $match['pattern'];
319
320 // If regex matches, or wildcard matches return true, else continue processing.
321 if (
322 ( $matcherType === 'regex' && preg_match( $pattern, $normalizedKey ) === 1 ) ||
323 ( $matcherType === 'wildcard' && fnmatch( $pattern, $normalizedKey ) )
324 ) {
325 return true;
326 }
327 }
328
329 return false;
330 }
331
337 private function runValidation(
338 array $validatorData,
339 Message $message,
340 string $targetLanguage,
341 ValidationIssues $errors,
342 ValidationIssues $warnings,
343 bool $ignoreWarnings
344 ): void {
345 // Check if key match has been specified, and then check if the key matches it.
347 $validator = $validatorData['instance'];
348
349 $definition = $message->definition();
350 if ( $definition === null ) {
351 // This should NOT happen, but add a check since it seems to be happening
352 // See: https://phabricator.wikimedia.org/T255669
353 return;
354 }
355
356 try {
357 $includedKeys = $validatorData['include'];
358 if ( $includedKeys !== false && !$this->doesKeyMatch( $message->key(), $includedKeys ) ) {
359 return;
360 }
361
362 $excludedKeys = $validatorData['exclude'];
363 if ( $excludedKeys !== false && $this->doesKeyMatch( $message->key(), $excludedKeys ) ) {
364 return;
365 }
366
367 if ( $validatorData['enforce'] === true ) {
368 $errors->merge( $validator->getIssues( $message, $targetLanguage ) );
369 } elseif ( !$ignoreWarnings ) {
370 $warnings->merge( $validator->getIssues( $message, $targetLanguage ) );
371 }
372 // else: caller does not want warnings, skip running the validator
373 } catch ( Exception $e ) {
374 throw new RuntimeException(
375 'An error occurred while validating message: ' . $message->key() . '; group: ' .
376 $this->groupId . "; validator: " . get_class( $validator ) . "\n. Exception: $e"
377 );
378 }
379 }
380}
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'));}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore($services->getDBLoadBalancerFactory());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'),);}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
Interface for message objects used by MessageCollection.
Definition Message.php:13
Minimal service container.
Definition Services.php:44
Container for validation issues returned by MessageValidator.
Message validator is used to run validators to find common mistakes so that translators can fix them ...
quickValidate(Message $message, string $code, bool $ignoreWarnings=false)
Validate a message, and return as soon as any validation fails.
static foldValue(string $value)
Normalise validator keys.
getValidators()
Return the currently set validators for this group.
addValidator(array $validatorConfig)
Add a validator for this group.
setValidators(array $validatorConfigs)
Set the validators for this group.
getInsertableValidators()
Return currently set validators that are insertable.
doesKeyMatch(string $key, array $keyMatches)
Check if key matches validator's key patterns.
validateMessage(Message $message, string $code, bool $ignoreWarnings=false)
Validate a translation of a message.
Stuff for handling configuration files in PHP format.