MediaWiki  master
LocalisationCache.php
Go to the documentation of this file.
1 <?php
23 use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
24 use CLDRPluralRuleParser\Evaluator;
30 use Psr\Log\LoggerInterface;
31 
45  public const VERSION = 4;
46 
48  private $options;
49 
55  private $manualRecache;
56 
63  protected $data = [];
64 
70  private $store;
71 
75  private $logger;
76 
78  private $hookRunner;
79 
82 
84  private $langNameUtils;
85 
94  private $loadedItems = [];
95 
100  private $loadedSubitems = [];
101 
107  private $initialisedLangs = [];
108 
114  private $shallowFallbacks = [];
115 
119  private $recachedLangs = [];
120 
124  public static $allKeys = [
125  'fallback', 'namespaceNames', 'bookstoreList',
126  'magicWords', 'messages', 'rtl',
127  'digitTransformTable', 'separatorTransformTable',
128  'minimumGroupingDigits', 'fallback8bitEncoding',
129  'linkPrefixExtension', 'linkTrail', 'linkPrefixCharset',
130  'namespaceAliases', 'dateFormats', 'datePreferences',
131  'datePreferenceMigrationMap', 'defaultDateFormat',
132  'specialPageAliases', 'imageFiles', 'preloadedMessages',
133  'namespaceGenderAliases', 'digitGroupingPattern', 'pluralRules',
134  'pluralRuleTypes', 'compiledPluralRules',
135  ];
136 
141  public static $mergeableMapKeys = [ 'messages', 'namespaceNames',
142  'namespaceAliases', 'dateFormats', 'imageFiles', 'preloadedMessages'
143  ];
144 
148  public static $mergeableListKeys = [];
149 
154  public static $mergeableAliasListKeys = [ 'specialPageAliases' ];
155 
161  public static $optionalMergeKeys = [ 'bookstoreList' ];
162 
166  public static $magicWordKeys = [ 'magicWords' ];
167 
171  public static $splitKeys = [ 'messages' ];
172 
176  public static $preloadedKeys = [ 'dateFormats', 'namespaceNames' ];
177 
182  private $pluralRules = null;
183 
196  private $pluralRuleTypes = null;
197 
198  private $mergeableKeys = null;
199 
208  public static function getStoreFromConf( array $conf, $fallbackCacheDir ): LCStore {
209  $storeArg = [];
210  $storeArg['directory'] =
211  $conf['storeDirectory'] ?: $fallbackCacheDir;
212 
213  if ( !empty( $conf['storeClass'] ) ) {
214  $storeClass = $conf['storeClass'];
215  } elseif ( $conf['store'] === 'files' || $conf['store'] === 'file' ||
216  ( $conf['store'] === 'detect' && $storeArg['directory'] )
217  ) {
218  $storeClass = LCStoreCDB::class;
219  } elseif ( $conf['store'] === 'db' || $conf['store'] === 'detect' ) {
220  $storeClass = LCStoreDB::class;
221  $storeArg['server'] = $conf['storeServer'] ?? [];
222  } elseif ( $conf['store'] === 'array' ) {
223  $storeClass = LCStoreStaticArray::class;
224  } else {
225  throw new MWException(
226  'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
227  );
228  }
229 
230  return new $storeClass( $storeArg );
231  }
232 
236  public const CONSTRUCTOR_OPTIONS = [
237  // True to treat all files as expired until they are regenerated by this object.
238  'forceRecache',
239  'manualRecache',
240  MainConfigNames::ExtensionMessagesFiles,
241  MainConfigNames::MessagesDirs,
242  ];
243 
260  public function __construct(
261  ServiceOptions $options,
262  LCStore $store,
263  LoggerInterface $logger,
264  array $clearStoreCallbacks,
265  LanguageNameUtils $langNameUtils,
266  HookContainer $hookContainer
267  ) {
268  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
269 
270  $this->options = $options;
271  $this->store = $store;
272  $this->logger = $logger;
273  $this->clearStoreCallbacks = $clearStoreCallbacks;
274  $this->langNameUtils = $langNameUtils;
275  $this->hookRunner = new HookRunner( $hookContainer );
276 
277  // Keep this separate from $this->options so it can be mutable
278  $this->manualRecache = $options->get( 'manualRecache' );
279  }
280 
287  public function isMergeableKey( $key ) {
288  if ( $this->mergeableKeys === null ) {
289  $this->mergeableKeys = array_fill_keys( array_merge(
290  self::$mergeableMapKeys,
291  self::$mergeableListKeys,
292  self::$mergeableAliasListKeys,
293  self::$optionalMergeKeys,
294  self::$magicWordKeys
295  ), true );
296  }
297 
298  return isset( $this->mergeableKeys[$key] );
299  }
300 
310  public function getItem( $code, $key ) {
311  if ( !isset( $this->loadedItems[$code][$key] ) ) {
312  $this->loadItem( $code, $key );
313  }
314 
315  if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
316  return $this->shallowFallbacks[$code];
317  }
318 
319  return $this->data[$code][$key];
320  }
321 
329  public function getSubitem( $code, $key, $subkey ) {
330  if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
331  !isset( $this->loadedItems[$code][$key] )
332  ) {
333  $this->loadSubitem( $code, $key, $subkey );
334  }
335 
336  return $this->data[$code][$key][$subkey] ?? null;
337  }
338 
351  public function getSubitemList( $code, $key ) {
352  if ( in_array( $key, self::$splitKeys ) ) {
353  return $this->getSubitem( $code, 'list', $key );
354  } else {
355  $item = $this->getItem( $code, $key );
356  if ( is_array( $item ) ) {
357  return array_keys( $item );
358  } else {
359  return false;
360  }
361  }
362  }
363 
369  protected function loadItem( $code, $key ) {
370  if ( !isset( $this->initialisedLangs[$code] ) ) {
371  $this->initLanguage( $code );
372  }
373 
374  // Check to see if initLanguage() loaded it for us
375  if ( isset( $this->loadedItems[$code][$key] ) ) {
376  return;
377  }
378 
379  if ( isset( $this->shallowFallbacks[$code] ) ) {
380  $this->loadItem( $this->shallowFallbacks[$code], $key );
381 
382  return;
383  }
384 
385  if ( in_array( $key, self::$splitKeys ) ) {
386  $subkeyList = $this->getSubitem( $code, 'list', $key );
387  foreach ( $subkeyList as $subkey ) {
388  if ( isset( $this->data[$code][$key][$subkey] ) ) {
389  continue;
390  }
391  $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey );
392  }
393  } else {
394  $this->data[$code][$key] = $this->store->get( $code, $key );
395  }
396 
397  $this->loadedItems[$code][$key] = true;
398  }
399 
406  protected function loadSubitem( $code, $key, $subkey ) {
407  if ( !in_array( $key, self::$splitKeys ) ) {
408  $this->loadItem( $code, $key );
409 
410  return;
411  }
412 
413  if ( !isset( $this->initialisedLangs[$code] ) ) {
414  $this->initLanguage( $code );
415  }
416 
417  // Check to see if initLanguage() loaded it for us
418  if ( isset( $this->loadedItems[$code][$key] ) ||
419  isset( $this->loadedSubitems[$code][$key][$subkey] )
420  ) {
421  return;
422  }
423 
424  if ( isset( $this->shallowFallbacks[$code] ) ) {
425  $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
426 
427  return;
428  }
429 
430  $value = $this->store->get( $code, "$key:$subkey" );
431  $this->data[$code][$key][$subkey] = $value;
432  $this->loadedSubitems[$code][$key][$subkey] = true;
433  }
434 
442  public function isExpired( $code ) {
443  if ( $this->options->get( 'forceRecache' ) && !isset( $this->recachedLangs[$code] ) ) {
444  $this->logger->debug( __METHOD__ . "($code): forced reload" );
445 
446  return true;
447  }
448 
449  $deps = $this->store->get( $code, 'deps' );
450  $keys = $this->store->get( $code, 'list' );
451  $preload = $this->store->get( $code, 'preload' );
452  // Different keys may expire separately for some stores
453  if ( $deps === null || $keys === null || $preload === null ) {
454  $this->logger->debug( __METHOD__ . "($code): cache missing, need to make one" );
455 
456  return true;
457  }
458 
459  foreach ( $deps as $dep ) {
460  // Because we're unserializing stuff from cache, we
461  // could receive objects of classes that don't exist
462  // anymore (e.g. uninstalled extensions)
463  // When this happens, always expire the cache
464  if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
465  $this->logger->debug( __METHOD__ . "($code): cache for $code expired due to " .
466  get_class( $dep ) );
467 
468  return true;
469  }
470  }
471 
472  return false;
473  }
474 
480  protected function initLanguage( $code ) {
481  if ( isset( $this->initialisedLangs[$code] ) ) {
482  return;
483  }
484 
485  $this->initialisedLangs[$code] = true;
486 
487  # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
488  if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
489  $this->initShallowFallback( $code, 'en' );
490 
491  return;
492  }
493 
494  # Recache the data if necessary
495  if ( !$this->manualRecache && $this->isExpired( $code ) ) {
496  if ( $this->langNameUtils->isSupportedLanguage( $code ) ) {
497  $this->recache( $code );
498  } elseif ( $code === 'en' ) {
499  throw new MWException( 'MessagesEn.php is missing.' );
500  } else {
501  $this->initShallowFallback( $code, 'en' );
502  }
503 
504  return;
505  }
506 
507  # Preload some stuff
508  $preload = $this->getItem( $code, 'preload' );
509  if ( $preload === null ) {
510  if ( $this->manualRecache ) {
511  // No Messages*.php file. Do shallow fallback to en.
512  if ( $code === 'en' ) {
513  throw new MWException( 'No localisation cache found for English. ' .
514  'Please run maintenance/rebuildLocalisationCache.php.' );
515  }
516  $this->initShallowFallback( $code, 'en' );
517 
518  return;
519  } else {
520  throw new MWException( 'Invalid or missing localisation cache.' );
521  }
522  }
523  $this->data[$code] = $preload;
524  foreach ( $preload as $key => $item ) {
525  if ( in_array( $key, self::$splitKeys ) ) {
526  foreach ( $item as $subkey => $subitem ) {
527  $this->loadedSubitems[$code][$key][$subkey] = true;
528  }
529  } else {
530  $this->loadedItems[$code][$key] = true;
531  }
532  }
533  }
534 
541  public function initShallowFallback( $primaryCode, $fallbackCode ) {
542  $this->data[$primaryCode] =& $this->data[$fallbackCode];
543  $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
544  $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
545  $this->shallowFallbacks[$primaryCode] = $fallbackCode;
546  }
547 
555  protected function readPHPFile( $_fileName, $_fileType ) {
556  include $_fileName;
557 
558  $data = [];
559  if ( $_fileType == 'core' || $_fileType == 'extension' ) {
560  foreach ( self::$allKeys as $key ) {
561  // Not all keys are set in language files, so
562  // check they exist first
563  if ( isset( $$key ) ) {
564  $data[$key] = $$key;
565  }
566  }
567  } elseif ( $_fileType == 'aliases' ) {
568  // @phan-suppress-next-line PhanImpossibleCondition May be set in included file
569  if ( isset( $aliases ) ) {
570  $data['aliases'] = $aliases;
571  }
572  } else {
573  throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
574  }
575 
576  return $data;
577  }
578 
585  public function readJSONFile( $fileName ) {
586  if ( !is_readable( $fileName ) ) {
587  return [];
588  }
589 
590  $json = file_get_contents( $fileName );
591  if ( $json === false ) {
592  return [];
593  }
594 
595  $data = FormatJson::decode( $json, true );
596  if ( $data === null ) {
597  throw new MWException( __METHOD__ . ": Invalid JSON file: $fileName" );
598  }
599 
600  // Remove keys starting with '@', they're reserved for metadata and non-message data
601  foreach ( $data as $key => $unused ) {
602  if ( $key === '' || $key[0] === '@' ) {
603  unset( $data[$key] );
604  }
605  }
606 
607  // The JSON format only supports messages, none of the other variables, so wrap the data
608  return [ 'messages' => $data ];
609  }
610 
617  public function getCompiledPluralRules( $code ) {
618  $rules = $this->getPluralRules( $code );
619  if ( $rules === null ) {
620  return null;
621  }
622  try {
623  $compiledRules = Evaluator::compile( $rules );
624  } catch ( CLDRPluralRuleError $e ) {
625  $this->logger->debug( $e->getMessage() );
626 
627  return [];
628  }
629 
630  return $compiledRules;
631  }
632 
640  public function getPluralRules( $code ) {
641  if ( $this->pluralRules === null ) {
642  $this->loadPluralFiles();
643  }
644  return $this->pluralRules[$code] ?? null;
645  }
646 
654  public function getPluralRuleTypes( $code ) {
655  if ( $this->pluralRuleTypes === null ) {
656  $this->loadPluralFiles();
657  }
658  return $this->pluralRuleTypes[$code] ?? null;
659  }
660 
664  protected function loadPluralFiles() {
665  foreach ( $this->getPluralFiles() as $fileName ) {
666  $this->loadPluralFile( $fileName );
667  }
668  }
669 
670  private function getPluralFiles(): array {
671  global $IP;
672  return [
673  // Load CLDR plural rules
674  "$IP/languages/data/plurals.xml",
675  // Override or extend with MW-specific rules
676  "$IP/languages/data/plurals-mediawiki.xml",
677  ];
678  }
679 
687  protected function loadPluralFile( $fileName ) {
688  // Use file_get_contents instead of DOMDocument::load (T58439)
689  $xml = file_get_contents( $fileName );
690  if ( !$xml ) {
691  throw new MWException( "Unable to read plurals file $fileName" );
692  }
693  $doc = new DOMDocument;
694  $doc->loadXML( $xml );
695  $rulesets = $doc->getElementsByTagName( "pluralRules" );
696  foreach ( $rulesets as $ruleset ) {
697  $codes = $ruleset->getAttribute( 'locales' );
698  $rules = [];
699  $ruleTypes = [];
700  $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
701  foreach ( $ruleElements as $elt ) {
702  $ruleType = $elt->getAttribute( 'count' );
703  if ( $ruleType === 'other' ) {
704  // Don't record "other" rules, which have an empty condition
705  continue;
706  }
707  $rules[] = $elt->nodeValue;
708  $ruleTypes[] = $ruleType;
709  }
710  foreach ( explode( ' ', $codes ) as $code ) {
711  $this->pluralRules[$code] = $rules;
712  $this->pluralRuleTypes[$code] = $ruleTypes;
713  }
714  }
715  }
716 
726  protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
727  // This reads in the PHP i18n file with non-messages l10n data
728  $fileName = $this->langNameUtils->getMessagesFileName( $code );
729  if ( !is_file( $fileName ) ) {
730  $data = [];
731  } else {
732  $deps[] = new FileDependency( $fileName );
733  $data = $this->readPHPFile( $fileName, 'core' );
734  }
735 
736  // Load CLDR plural rules for JavaScript
737  $data['pluralRules'] = $this->getPluralRules( $code );
738  // And for PHP
739  $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
740  // Load plural rule types
741  $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code );
742 
743  foreach ( $this->getPluralFiles() as $fileName ) {
744  $deps[] = new FileDependency( $fileName );
745  }
746 
747  return $data;
748  }
749 
757  protected function mergeItem( $key, &$value, $fallbackValue ) {
758  if ( $value !== null ) {
759  if ( $fallbackValue !== null ) {
760  if ( in_array( $key, self::$mergeableMapKeys ) ) {
761  $value += $fallbackValue;
762  } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
763  $value = array_unique( array_merge( $fallbackValue, $value ) );
764  } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
765  $value = array_merge_recursive( $value, $fallbackValue );
766  } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
767  if ( !empty( $value['inherit'] ) ) {
768  $value = array_merge( $fallbackValue, $value );
769  }
770 
771  unset( $value['inherit'] );
772  } elseif ( in_array( $key, self::$magicWordKeys ) ) {
773  $this->mergeMagicWords( $value, $fallbackValue );
774  }
775  }
776  } else {
777  $value = $fallbackValue;
778  }
779  }
780 
785  protected function mergeMagicWords( &$value, $fallbackValue ) {
786  foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
787  if ( !isset( $value[$magicName] ) ) {
788  $value[$magicName] = $fallbackInfo;
789  } else {
790  $oldSynonyms = array_slice( $fallbackInfo, 1 );
791  $newSynonyms = array_slice( $value[$magicName], 1 );
792  $synonyms = array_values( array_unique( array_merge(
793  $newSynonyms, $oldSynonyms ) ) );
794  $value[$magicName] = array_merge( [ $fallbackInfo[0] ], $synonyms );
795  }
796  }
797  }
798 
812  protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
813  $used = false;
814  foreach ( $codeSequence as $code ) {
815  if ( isset( $fallbackValue[$code] ) ) {
816  $this->mergeItem( $key, $value, $fallbackValue[$code] );
817  $used = true;
818  }
819  }
820 
821  return $used;
822  }
823 
831  public function getMessagesDirs() {
832  global $IP;
833 
834  return [
835  'core' => "$IP/languages/i18n",
836  'exif' => "$IP/languages/i18n/exif",
837  'api' => "$IP/includes/api/i18n",
838  'rest' => "$IP/includes/Rest/i18n",
839  'oojs-ui' => "$IP/resources/lib/ooui/i18n",
840  'paramvalidator' => "$IP/includes/libs/ParamValidator/i18n",
841  ] + $this->options->get( MainConfigNames::MessagesDirs );
842  }
843 
850  public function recache( $code ) {
851  if ( !$code ) {
852  throw new MWException( "Invalid language code requested" );
853  }
854  $this->recachedLangs[ $code ] = true;
855 
856  # Initial values
857  $initialData = array_fill_keys( self::$allKeys, null );
858  $coreData = $initialData;
859  $deps = [];
860 
861  # Load the primary localisation from the source file
862  $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
863  $this->logger->debug( __METHOD__ . ": got localisation for $code from source" );
864 
865  # Merge primary localisation
866  foreach ( $data as $key => $value ) {
867  $this->mergeItem( $key, $coreData[ $key ], $value );
868  }
869 
870  # Fill in the fallback if it's not there already
871  // @phan-suppress-next-line PhanSuspiciousValueComparison
872  if ( ( $coreData['fallback'] === null || $coreData['fallback'] === false ) && $code === 'en' ) {
873  $coreData['fallback'] = false;
874  $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'] = [];
875  } else {
876  if ( $coreData['fallback'] !== null ) {
877  $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
878  } else {
879  $coreData['fallbackSequence'] = [];
880  }
881  $len = count( $coreData['fallbackSequence'] );
882 
883  # Before we add the 'en' fallback for messages, keep a copy of
884  # the original fallback sequence
885  $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'];
886 
887  # Ensure that the sequence ends at 'en' for messages
888  if ( !$len || $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
889  $coreData['fallbackSequence'][] = 'en';
890  }
891  }
892 
893  $codeSequence = array_merge( [ $code ], $coreData['fallbackSequence'] );
894  $messageDirs = $this->getMessagesDirs();
895 
896  # Load non-JSON localisation data for extensions
897  $extensionData = array_fill_keys( $codeSequence, $initialData );
898  foreach ( $this->options->get( MainConfigNames::ExtensionMessagesFiles ) as $extension => $fileName ) {
899  if ( isset( $messageDirs[$extension] ) ) {
900  # This extension has JSON message data; skip the PHP shim
901  continue;
902  }
903 
904  $data = $this->readPHPFile( $fileName, 'extension' );
905  $used = false;
906 
907  foreach ( $data as $key => $item ) {
908  foreach ( $codeSequence as $csCode ) {
909  if ( isset( $item[$csCode] ) ) {
910  $this->mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] );
911  $used = true;
912  }
913  }
914  }
915 
916  if ( $used ) {
917  $deps[] = new FileDependency( $fileName );
918  }
919  }
920 
921  # Load the localisation data for each fallback, then merge it into the full array
922  $allData = $initialData;
923  foreach ( $codeSequence as $csCode ) {
924  $csData = $initialData;
925 
926  # Load core messages and the extension localisations.
927  foreach ( $messageDirs as $dirs ) {
928  foreach ( (array)$dirs as $dir ) {
929  $fileName = "$dir/$csCode.json";
930  $data = $this->readJSONFile( $fileName );
931 
932  foreach ( $data as $key => $item ) {
933  $this->mergeItem( $key, $csData[$key], $item );
934  }
935 
936  $deps[] = new FileDependency( $fileName );
937  }
938  }
939 
940  # Merge non-JSON extension data
941  if ( isset( $extensionData[$csCode] ) ) {
942  foreach ( $extensionData[$csCode] as $key => $item ) {
943  $this->mergeItem( $key, $csData[$key], $item );
944  }
945  }
946 
947  if ( $csCode === $code ) {
948  # Merge core data into extension data
949  foreach ( $coreData as $key => $item ) {
950  $this->mergeItem( $key, $csData[$key], $item );
951  }
952  } else {
953  # Load the secondary localisation from the source file to
954  # avoid infinite cycles on cyclic fallbacks
955  $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps );
956  # Only merge the keys that make sense to merge
957  foreach ( self::$allKeys as $key ) {
958  if ( !isset( $fbData[ $key ] ) ) {
959  continue;
960  }
961 
962  if ( ( $coreData[ $key ] ) === null || $this->isMergeableKey( $key ) ) {
963  $this->mergeItem( $key, $csData[ $key ], $fbData[ $key ] );
964  }
965  }
966  }
967 
968  # Allow extensions an opportunity to adjust the data for this
969  # fallback
970  $this->hookRunner->onLocalisationCacheRecacheFallback( $this, $csCode, $csData );
971 
972  # Merge the data for this fallback into the final array
973  if ( $csCode === $code ) {
974  $allData = $csData;
975  } else {
976  foreach ( self::$allKeys as $key ) {
977  if ( !isset( $csData[$key] ) ) {
978  continue;
979  }
980 
981  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
982  if ( $allData[$key] === null || $this->isMergeableKey( $key ) ) {
983  $this->mergeItem( $key, $allData[$key], $csData[$key] );
984  }
985  }
986  }
987  }
988 
989  # Add cache dependencies for any referenced globals
990  $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
991  // The 'MessagesDirs' config setting is used in LocalisationCache::getMessagesDirs().
992  // We use the key 'wgMessagesDirs' for historical reasons.
993  $deps['wgMessagesDirs'] = new MainConfigDependency( MainConfigNames::MessagesDirs );
994  $deps['version'] = new ConstantDependency( 'LocalisationCache::VERSION' );
995 
996  # Add dependencies to the cache entry
997  $allData['deps'] = $deps;
998 
999  # Replace spaces with underscores in namespace names
1000  $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
1001 
1002  # And do the same for special page aliases. $page is an array.
1003  foreach ( $allData['specialPageAliases'] as &$page ) {
1004  $page = str_replace( ' ', '_', $page );
1005  }
1006  # Decouple the reference to prevent accidental damage
1007  unset( $page );
1008 
1009  # If there were no plural rules, return an empty array
1010  if ( $allData['pluralRules'] === null ) {
1011  $allData['pluralRules'] = [];
1012  }
1013  if ( $allData['compiledPluralRules'] === null ) {
1014  $allData['compiledPluralRules'] = [];
1015  }
1016  # If there were no plural rule types, return an empty array
1017  if ( $allData['pluralRuleTypes'] === null ) {
1018  $allData['pluralRuleTypes'] = [];
1019  }
1020 
1021  # Set the list keys
1022  $allData['list'] = [];
1023  foreach ( self::$splitKeys as $key ) {
1024  $allData['list'][$key] = array_keys( $allData[$key] );
1025  }
1026  # Run hooks
1027  $unused = true; // Used to be $purgeBlobs, removed in 1.34
1028  $this->hookRunner->onLocalisationCacheRecache( $this, $code, $allData, $unused );
1029 
1030  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
1031  if ( $allData['namespaceNames'] === null ) {
1032  throw new MWException( __METHOD__ . ': Localisation data failed validation check! ' .
1033  'Check that your languages/messages/MessagesEn.php file is intact.' );
1034  }
1035 
1036  # Set the preload key
1037  $allData['preload'] = $this->buildPreload( $allData );
1038 
1039  # Save to the process cache and register the items loaded
1040  $this->data[$code] = $allData;
1041  foreach ( $allData as $key => $item ) {
1042  $this->loadedItems[$code][$key] = true;
1043  }
1044 
1045  # Save to the persistent cache
1046  $this->store->startWrite( $code );
1047  foreach ( $allData as $key => $value ) {
1048  if ( in_array( $key, self::$splitKeys ) ) {
1049  foreach ( $value as $subkey => $subvalue ) {
1050  $this->store->set( "$key:$subkey", $subvalue );
1051  }
1052  } else {
1053  $this->store->set( $key, $value );
1054  }
1055  }
1056  $this->store->finishWrite();
1057 
1058  # Clear out the MessageBlobStore
1059  # HACK: If using a null (i.e. disabled) storage backend, we
1060  # can't write to the MessageBlobStore either
1061  if ( !$this->store instanceof LCStoreNull ) {
1062  foreach ( $this->clearStoreCallbacks as $callback ) {
1063  $callback();
1064  }
1065  }
1066  }
1067 
1076  protected function buildPreload( $data ) {
1077  $preload = [ 'messages' => [] ];
1078  foreach ( self::$preloadedKeys as $key ) {
1079  $preload[$key] = $data[$key];
1080  }
1081 
1082  foreach ( $data['preloadedMessages'] as $subkey ) {
1083  $subitem = $data['messages'][$subkey] ?? null;
1084  $preload['messages'][$subkey] = $subitem;
1085  }
1086 
1087  return $preload;
1088  }
1089 
1095  public function unload( $code ) {
1096  unset( $this->data[$code] );
1097  unset( $this->loadedItems[$code] );
1098  unset( $this->loadedSubitems[$code] );
1099  unset( $this->initialisedLangs[$code] );
1100  unset( $this->shallowFallbacks[$code] );
1101 
1102  foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
1103  if ( $fbCode === $code ) {
1104  $this->unload( $shallowCode );
1105  }
1106  }
1107  }
1108 
1112  public function unloadAll() {
1113  foreach ( $this->initialisedLangs as $lang => $unused ) {
1114  $this->unload( $lang );
1115  }
1116  }
1117 
1121  public function disableBackend() {
1122  $this->store = new LCStoreNull;
1123  $this->manualRecache = false;
1124  }
1125 }
if(!defined( 'MEDIAWIKI')) if(ini_get( 'mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition: Setup.php:90
isExpired()
Returns true if the dependency is expired, false otherwise.
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:146
Null store backend, used to avoid DB errors during install.
Definition: LCStoreNull.php:24
Class for caching the contents of localisation files, Messages*.php and *.i18n.php.
$manualRecache
True if recaching should only be done on an explicit call to recache().
static $magicWordKeys
Keys for items that are formatted like $magicWords.
buildPreload( $data)
Build the preload item from the given pre-cache data.
initLanguage( $code)
Initialise a language in this object.
static $mergeableMapKeys
Keys for items which consist of associative arrays, which may be merged by a fallback sequence.
readSourceFilesAndRegisterDeps( $code, &$deps)
Read the data from the source files for a given language, and register the relevant dependencies in t...
isMergeableKey( $key)
Returns true if the given key is mergeable, that is, if it is an associative array which can be merge...
loadPluralFile( $fileName)
Load a plural XML file with the given filename, compile the relevant rules, and save the compiled rul...
getPluralRules( $code)
Get the plural rules for a given language from the XML files.
$loadedItems
A 2-d associative array, code/key, where presence indicates that the item is loaded.
readPHPFile( $_fileName, $_fileType)
Read a PHP file containing localisation data.
$initialisedLangs
An array where presence of a key indicates that that language has been initialised.
readJSONFile( $fileName)
Read a JSON file containing localisation messages.
loadPluralFiles()
Load the plural XML files.
unload( $code)
Unload the data for a given language from the object cache.
unloadAll()
Unload all data.
disableBackend()
Disable the storage backend.
static $mergeableAliasListKeys
Keys for items which contain an array of arrays of equivalent aliases for each subitem.
getSubitemList( $code, $key)
Get the list of subitem keys for a given item.
$recachedLangs
An array where the keys are codes that have been recached by this instance.
$loadedSubitems
A 3-d associative array, code/key/subkey, where presence indicates that the subitem is loaded.
$data
The cache data.
__construct(ServiceOptions $options, LCStore $store, LoggerInterface $logger, array $clearStoreCallbacks, LanguageNameUtils $langNameUtils, HookContainer $hookContainer)
For constructor parameters, see the documentation for the LocalisationCacheConf setting in docs/Confi...
static $mergeableListKeys
Keys for items which are a numbered array.
recache( $code)
Load localisation data for a given language for both core and extensions and save it to the persisten...
isExpired( $code)
Returns true if the cache identified by $code is missing or expired.
mergeItem( $key, &$value, $fallbackValue)
Merge two localisation values, a primary and a fallback, overwriting the primary value in place.
getSubitem( $code, $key, $subkey)
Get a subitem, for instance a single message for a given language.
mergeMagicWords(&$value, $fallbackValue)
$shallowFallbacks
An array mapping non-existent pseudo-languages to fallback languages.
ServiceOptions $options
initShallowFallback( $primaryCode, $fallbackCode)
Create a fallback from one language to another, without creating a complete persistent cache.
static getStoreFromConf(array $conf, $fallbackCacheDir)
Return a suitable LCStore as specified by the given configuration.
LanguageNameUtils $langNameUtils
LoggerInterface $logger
static $allKeys
All item keys.
getCompiledPluralRules( $code)
Get the compiled plural rules for a given language from the XML files.
static $optionalMergeKeys
Keys for items which contain an associative array, and may be merged if the primary value contains th...
getMessagesDirs()
Gets the combined list of messages dirs from core and extensions.
callable[] $clearStoreCallbacks
See comment for parameter in constructor.
static $preloadedKeys
Keys which are loaded automatically by initLanguage()
getItem( $code, $key)
Get a cache item.
static $splitKeys
Keys for items where the subitems are stored in the backend separately.
loadItem( $code, $key)
Load an item into the cache.
$pluralRuleTypes
Associative array of cached plural rule types.
getPluralRuleTypes( $code)
Get the plural rule types for a given language from the XML files.
LCStore $store
The persistent store object.
loadSubitem( $code, $key, $subkey)
Load a subitem into the cache.
$pluralRules
Associative array of cached plural rules.
mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue)
Given an array mapping language code to localisation value, such as is found in extension *....
MediaWiki exception.
Definition: MWException.php:29
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:562
A service that provides utilities to do with language names and codes.
A class containing constants representing the names of configuration variables.
Interface for the persistence layer of LocalisationCache.
Definition: LCStore.php:38
if(!isset( $args[0])) $lang