41 protected $validators = [];
45 private static $ignorePatterns;
47 public function __construct(
string $groupId ) {
48 if ( self::$ignorePatterns ===
null ) {
50 self::reloadIgnorePatterns();
53 $this->groupId = $groupId;
57 protected static function foldValue(
string $value ): string {
58 return str_replace(
' ',
'_', strtolower( $value ) );
70 $this->validators = [];
71 foreach ( $validatorConfigs as $config ) {
72 $this->addValidator( $config );
78 $validatorId = $validatorConfig[
'id'] ?? null;
79 $className = $validatorConfig[
'class'] ??
null;
81 if ( $validatorId !==
null ) {
82 $validator = ValidatorFactory::get(
84 $validatorConfig[
'params'] ??
null
86 } elseif ( $className !==
null ) {
87 $validator = ValidatorFactory::loadInstance(
89 $validatorConfig[
'params'] ??
null
92 throw new InvalidArgumentException(
93 'Validator configuration does not specify the \'class\' or \'id\'.'
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."
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
121 static function ( $validator ) {
122 return $validator[
'instance'];
135 $insertableValidators = [];
136 foreach ( $this->validators as $validator ) {
137 if ( $validator[
'insertable'] ===
true ) {
138 $insertableValidators[] = $validator[
'instance'];
142 return $insertableValidators;
153 bool $ignoreWarnings =
false
158 foreach ( $this->validators as $validator ) {
159 $this->runValidation(
169 $errors = $this->filterValidations( $message->
key(), $errors, $code );
170 $warnings = $this->filterValidations( $message->
key(), $warnings, $code );
179 bool $ignoreWarnings =
false
184 foreach ( $this->validators as $validator ) {
185 $this->runValidation(
194 $errors = $this->filterValidations( $message->
key(), $errors, $code );
195 $warnings = $this->filterValidations( $message->
key(), $warnings, $code );
197 if ( $warnings->hasIssues() || $errors->hasIssues() ) {
202 return new ValidationResult( $errors, $warnings );
206 public static function reloadIgnorePatterns(): void {
207 $validationExclusionFile = Services::getInstance()->getConfigHelper()->getValidationExclusionFile();
209 if ( $validationExclusionFile ===
false ) {
210 self::$ignorePatterns = [];
214 $list = PHPVariableLoader::loadVariableFromPHPFile(
215 $validationExclusionFile,
216 'validationExclusionList'
218 $keys = [
'group',
'check',
'subcheck',
'code',
'message' ];
220 if ( $list && !is_array( $list ) ) {
221 throw new InvalidArgumentException(
222 "validationExclusionList defined in $validationExclusionFile must be an array"
226 foreach ( $list as $key => $pattern ) {
227 foreach ( $keys as $checkKey ) {
228 if ( !isset( $pattern[$checkKey] ) ) {
229 $list[$key][$checkKey] =
'#';
230 } elseif ( is_array( $pattern[$checkKey] ) ) {
231 $list[$key][$checkKey] =
233 [ self::class,
'foldValue' ],
237 $list[$key][$checkKey] = self::foldValue( $pattern[$checkKey] );
242 self::$ignorePatterns = $list;
246 private function filterValidations(
248 ValidationIssues $issues,
249 string $targetLanguage
250 ): ValidationIssues {
251 $filteredIssues = new ValidationIssues();
253 foreach ( $issues as $issue ) {
254 foreach ( self::$ignorePatterns as $pattern ) {
255 if ( $this->shouldIgnore( $messageKey, $issue, $this->groupId, $targetLanguage, $pattern ) ) {
259 $filteredIssues->add( $issue );
262 return $filteredIssues;
265 private function shouldIgnore(
267 ValidationIssue $issue,
268 string $messageGroupId,
269 string $targetLanguage,
272 return $this->matchesIgnorePattern( $pattern[
'group'], $messageGroupId )
273 && $this->matchesIgnorePattern( $pattern[
'check'], $issue->type() )
274 && $this->matchesIgnorePattern( $pattern[
'subcheck'], $issue->subType() )
275 && $this->matchesIgnorePattern( $pattern[
'message'], $messageKey )
276 && $this->matchesIgnorePattern( $pattern[
'code'], $targetLanguage );
286 private function matchesIgnorePattern( $pattern,
string $value ): bool {
287 if ( $pattern ===
'#' ) {
289 } elseif ( is_array( $pattern ) ) {
290 return in_array( strtolower( $value ), $pattern,
true );
292 return strtolower( $value ) === $pattern;
304 protected function doesKeyMatch(
string $key, array $keyMatches ): bool {
305 $normalizedKey = lcfirst( $key );
306 foreach ( $keyMatches as $match ) {
307 if ( is_string( $match ) ) {
308 if ( lcfirst( $match ) === $normalizedKey ) {
315 if ( !is_array( $match ) ) {
316 throw new InvalidArgumentException(
317 "Invalid key matcher configuration passed. Expected type: array or string. " .
318 "Received: " . gettype( $match ) .
". match value: " . FormatJson::encode( $match )
322 $matcherType = $match[
'type'];
323 $pattern = $match[
'pattern'];
327 ( $matcherType ===
'regex' && preg_match( $pattern, $normalizedKey ) === 1 ) ||
328 ( $matcherType ===
'wildcard' && fnmatch( $pattern, $normalizedKey ) )
342 private function runValidation(
343 array $validatorData,
345 string $targetLanguage,
346 ValidationIssues $errors,
347 ValidationIssues $warnings,
352 $validator = $validatorData[
'instance'];
355 if ( $definition ===
null ) {
362 $includedKeys = $validatorData[
'include'];
363 if ( $includedKeys !==
false && !$this->doesKeyMatch( $message->
key(), $includedKeys ) ) {
367 $excludedKeys = $validatorData[
'exclude'];
368 if ( $excludedKeys !==
false && $this->doesKeyMatch( $message->
key(), $excludedKeys ) ) {
372 if ( $validatorData[
'enforce'] ===
true ) {
373 $errors->merge( $validator->getIssues( $message, $targetLanguage ) );
374 } elseif ( !$ignoreWarnings ) {
375 $warnings->merge( $validator->getIssues( $message, $targetLanguage ) );
378 }
catch ( Exception $e ) {
379 throw new RuntimeException(
380 'An error occurred while validating message: ' . $message->
key() .
'; group: ' .
381 $this->groupId .
"; validator: " . get_class( $validator ) .
"\n. Exception: $e"
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'), MessageIndex::singleton());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore(new RevTagStore(), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReview'=> static function(MediaWikiServices $services):MessageGroupReview { return new MessageGroupReview($services->getDBLoadBalancer(), $services->getHookContainer());}, '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: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:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, '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: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(), new RevTagStore(), $services->getDBLoadBalancer());}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnectionRef(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());}, '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