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:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager( $services->getTitleFactory());}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, '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:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, '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:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getDBLoadBalancer());}, '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->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance( 'Translate.MessageGroupSubscription'), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):MessageGroupSubscriptionHookHandler { return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getDBLoadBalancerFactory());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance( 'Translate'), $services->getMainObjectStash(), $services->getDBLoadBalancerFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, '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'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getDBLoadBalancer());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, '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(), $services->getNamespaceInfo(), $services->getTitleFactory());}, '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->getDBLoadBalancerFactory(), $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:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getDBLoadBalancer(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'));}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, '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'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getDBLoadBalancerFactory(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getDBLoadBalancer(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, '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:58
Stuff for handling configuration files in PHP format.
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.