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