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 
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  'ExtensionMessagesFiles',
241  'MessagesDirs',
242  ];
243 
260  public function __construct(
262  LCStore $store,
263  LoggerInterface $logger,
264  array $clearStoreCallbacks,
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_flip( array_merge(
290  self::$mergeableMapKeys,
291  self::$mergeableListKeys,
292  self::$mergeableAliasListKeys,
293  self::$optionalMergeKeys,
294  self::$magicWordKeys
295  ) );
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  global $IP;
666  $cldrPlural = "$IP/languages/data/plurals.xml";
667  $mwPlural = "$IP/languages/data/plurals-mediawiki.xml";
668  // Load CLDR plural rules
669  $this->loadPluralFile( $cldrPlural );
670  if ( file_exists( $mwPlural ) ) {
671  // Override or extend
672  $this->loadPluralFile( $mwPlural );
673  }
674  }
675 
683  protected function loadPluralFile( $fileName ) {
684  // Use file_get_contents instead of DOMDocument::load (T58439)
685  $xml = file_get_contents( $fileName );
686  if ( !$xml ) {
687  throw new MWException( "Unable to read plurals file $fileName" );
688  }
689  $doc = new DOMDocument;
690  $doc->loadXML( $xml );
691  $rulesets = $doc->getElementsByTagName( "pluralRules" );
692  foreach ( $rulesets as $ruleset ) {
693  $codes = $ruleset->getAttribute( 'locales' );
694  $rules = [];
695  $ruleTypes = [];
696  $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
697  foreach ( $ruleElements as $elt ) {
698  $ruleType = $elt->getAttribute( 'count' );
699  if ( $ruleType === 'other' ) {
700  // Don't record "other" rules, which have an empty condition
701  continue;
702  }
703  $rules[] = $elt->nodeValue;
704  $ruleTypes[] = $ruleType;
705  }
706  foreach ( explode( ' ', $codes ) as $code ) {
707  $this->pluralRules[$code] = $rules;
708  $this->pluralRuleTypes[$code] = $ruleTypes;
709  }
710  }
711  }
712 
722  protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
723  global $IP;
724 
725  // This reads in the PHP i18n file with non-messages l10n data
726  $fileName = $this->langNameUtils->getMessagesFileName( $code );
727  if ( !file_exists( $fileName ) ) {
728  $data = [];
729  } else {
730  $deps[] = new FileDependency( $fileName );
731  $data = $this->readPHPFile( $fileName, 'core' );
732  }
733 
734  # Load CLDR plural rules for JavaScript
735  $data['pluralRules'] = $this->getPluralRules( $code );
736  # And for PHP
737  $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
738  # Load plural rule types
739  $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code );
740 
741  $deps['plurals'] = new FileDependency( "$IP/languages/data/plurals.xml" );
742  $deps['plurals-mw'] = new FileDependency( "$IP/languages/data/plurals-mediawiki.xml" );
743 
744  return $data;
745  }
746 
754  protected function mergeItem( $key, &$value, $fallbackValue ) {
755  if ( $value !== null ) {
756  if ( $fallbackValue !== null ) {
757  if ( in_array( $key, self::$mergeableMapKeys ) ) {
758  $value += $fallbackValue;
759  } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
760  $value = array_unique( array_merge( $fallbackValue, $value ) );
761  } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
762  $value = array_merge_recursive( $value, $fallbackValue );
763  } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
764  if ( !empty( $value['inherit'] ) ) {
765  $value = array_merge( $fallbackValue, $value );
766  }
767 
768  if ( isset( $value['inherit'] ) ) {
769  unset( $value['inherit'] );
770  }
771  } elseif ( in_array( $key, self::$magicWordKeys ) ) {
772  $this->mergeMagicWords( $value, $fallbackValue );
773  }
774  }
775  } else {
776  $value = $fallbackValue;
777  }
778  }
779 
784  protected function mergeMagicWords( &$value, $fallbackValue ) {
785  foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
786  if ( !isset( $value[$magicName] ) ) {
787  $value[$magicName] = $fallbackInfo;
788  } else {
789  $oldSynonyms = array_slice( $fallbackInfo, 1 );
790  $newSynonyms = array_slice( $value[$magicName], 1 );
791  $synonyms = array_values( array_unique( array_merge(
792  $newSynonyms, $oldSynonyms ) ) );
793  $value[$magicName] = array_merge( [ $fallbackInfo[0] ], $synonyms );
794  }
795  }
796  }
797 
811  protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
812  $used = false;
813  foreach ( $codeSequence as $code ) {
814  if ( isset( $fallbackValue[$code] ) ) {
815  $this->mergeItem( $key, $value, $fallbackValue[$code] );
816  $used = true;
817  }
818  }
819 
820  return $used;
821  }
822 
830  public function getMessagesDirs() {
831  global $IP;
832 
833  return [
834  'core' => "$IP/languages/i18n",
835  'exif' => "$IP/languages/i18n/exif",
836  'api' => "$IP/includes/api/i18n",
837  'rest' => "$IP/includes/Rest/i18n",
838  'oojs-ui' => "$IP/resources/lib/ooui/i18n",
839  'paramvalidator' => "$IP/includes/libs/ParamValidator/i18n",
840  ] + $this->options->get( 'MessagesDirs' );
841  }
842 
849  public function recache( $code ) {
850  if ( !$code ) {
851  throw new MWException( "Invalid language code requested" );
852  }
853  $this->recachedLangs[ $code ] = true;
854 
855  # Initial values
856  $initialData = array_fill_keys( self::$allKeys, null );
857  $coreData = $initialData;
858  $deps = [];
859 
860  # Load the primary localisation from the source file
861  $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
862  $this->logger->debug( __METHOD__ . ": got localisation for $code from source" );
863 
864  # Merge primary localisation
865  foreach ( $data as $key => $value ) {
866  $this->mergeItem( $key, $coreData[ $key ], $value );
867  }
868 
869  # Fill in the fallback if it's not there already
870  // @phan-suppress-next-line PhanSuspiciousValueComparison
871  if ( ( $coreData['fallback'] === null || $coreData['fallback'] === false ) && $code === 'en' ) {
872  $coreData['fallback'] = false;
873  $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'] = [];
874  } else {
875  if ( $coreData['fallback'] !== null ) {
876  $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
877  } else {
878  $coreData['fallbackSequence'] = [];
879  }
880  $len = count( $coreData['fallbackSequence'] );
881 
882  # Before we add the 'en' fallback for messages, keep a copy of
883  # the original fallback sequence
884  $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'];
885 
886  # Ensure that the sequence ends at 'en' for messages
887  if ( !$len || $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
888  $coreData['fallbackSequence'][] = 'en';
889  }
890  }
891 
892  $codeSequence = array_merge( [ $code ], $coreData['fallbackSequence'] );
893  $messageDirs = $this->getMessagesDirs();
894 
895  # Load non-JSON localisation data for extensions
896  $extensionData = array_fill_keys( $codeSequence, $initialData );
897  foreach ( $this->options->get( 'ExtensionMessagesFiles' ) as $extension => $fileName ) {
898  if ( isset( $messageDirs[$extension] ) ) {
899  # This extension has JSON message data; skip the PHP shim
900  continue;
901  }
902 
903  $data = $this->readPHPFile( $fileName, 'extension' );
904  $used = false;
905 
906  foreach ( $data as $key => $item ) {
907  foreach ( $codeSequence as $csCode ) {
908  if ( isset( $item[$csCode] ) ) {
909  $this->mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] );
910  $used = true;
911  }
912  }
913  }
914 
915  if ( $used ) {
916  $deps[] = new FileDependency( $fileName );
917  }
918  }
919 
920  # Load the localisation data for each fallback, then merge it into the full array
921  $allData = $initialData;
922  foreach ( $codeSequence as $csCode ) {
923  $csData = $initialData;
924 
925  # Load core messages and the extension localisations.
926  foreach ( $messageDirs as $dirs ) {
927  foreach ( (array)$dirs as $dir ) {
928  $fileName = "$dir/$csCode.json";
929  $data = $this->readJSONFile( $fileName );
930 
931  foreach ( $data as $key => $item ) {
932  $this->mergeItem( $key, $csData[$key], $item );
933  }
934 
935  $deps[] = new FileDependency( $fileName );
936  }
937  }
938 
939  # Merge non-JSON extension data
940  if ( isset( $extensionData[$csCode] ) ) {
941  foreach ( $extensionData[$csCode] as $key => $item ) {
942  $this->mergeItem( $key, $csData[$key], $item );
943  }
944  }
945 
946  if ( $csCode === $code ) {
947  # Merge core data into extension data
948  foreach ( $coreData as $key => $item ) {
949  $this->mergeItem( $key, $csData[$key], $item );
950  }
951  } else {
952  # Load the secondary localisation from the source file to
953  # avoid infinite cycles on cyclic fallbacks
954  $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps );
955  # Only merge the keys that make sense to merge
956  foreach ( self::$allKeys as $key ) {
957  if ( !isset( $fbData[ $key ] ) ) {
958  continue;
959  }
960 
961  if ( ( $coreData[ $key ] ) === null || $this->isMergeableKey( $key ) ) {
962  $this->mergeItem( $key, $csData[ $key ], $fbData[ $key ] );
963  }
964  }
965  }
966 
967  # Allow extensions an opportunity to adjust the data for this
968  # fallback
969  $this->hookRunner->onLocalisationCacheRecacheFallback( $this, $csCode, $csData );
970 
971  # Merge the data for this fallback into the final array
972  if ( $csCode === $code ) {
973  $allData = $csData;
974  } else {
975  foreach ( self::$allKeys as $key ) {
976  if ( !isset( $csData[$key] ) ) {
977  continue;
978  }
979 
980  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
981  if ( $allData[$key] === null || $this->isMergeableKey( $key ) ) {
982  $this->mergeItem( $key, $allData[$key], $csData[$key] );
983  }
984  }
985  }
986  }
987 
988  # Add cache dependencies for any referenced globals
989  $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
990  // The 'MessagesDirs' config setting is used in LocalisationCache::getMessagesDirs().
991  // We use the key 'wgMessagesDirs' for historical reasons.
992  $deps['wgMessagesDirs'] = new MainConfigDependency( 'MessagesDirs' );
993  $deps['version'] = new ConstantDependency( 'LocalisationCache::VERSION' );
994 
995  # Add dependencies to the cache entry
996  $allData['deps'] = $deps;
997 
998  # Replace spaces with underscores in namespace names
999  $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
1000 
1001  # And do the same for special page aliases. $page is an array.
1002  foreach ( $allData['specialPageAliases'] as &$page ) {
1003  $page = str_replace( ' ', '_', $page );
1004  }
1005  # Decouple the reference to prevent accidental damage
1006  unset( $page );
1007 
1008  # If there were no plural rules, return an empty array
1009  if ( $allData['pluralRules'] === null ) {
1010  $allData['pluralRules'] = [];
1011  }
1012  if ( $allData['compiledPluralRules'] === null ) {
1013  $allData['compiledPluralRules'] = [];
1014  }
1015  # If there were no plural rule types, return an empty array
1016  if ( $allData['pluralRuleTypes'] === null ) {
1017  $allData['pluralRuleTypes'] = [];
1018  }
1019 
1020  # Set the list keys
1021  $allData['list'] = [];
1022  foreach ( self::$splitKeys as $key ) {
1023  $allData['list'][$key] = array_keys( $allData[$key] );
1024  }
1025  # Run hooks
1026  $unused = true; // Used to be $purgeBlobs, removed in 1.34
1027  $this->hookRunner->onLocalisationCacheRecache( $this, $code, $allData, $unused );
1028 
1029  if ( $allData['namespaceNames'] === null ) {
1030  throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
1031  'Check that your languages/messages/MessagesEn.php file is intact.' );
1032  }
1033 
1034  # Set the preload key
1035  $allData['preload'] = $this->buildPreload( $allData );
1036 
1037  # Save to the process cache and register the items loaded
1038  $this->data[$code] = $allData;
1039  foreach ( $allData as $key => $item ) {
1040  $this->loadedItems[$code][$key] = true;
1041  }
1042 
1043  # Save to the persistent cache
1044  $this->store->startWrite( $code );
1045  foreach ( $allData as $key => $value ) {
1046  if ( in_array( $key, self::$splitKeys ) ) {
1047  foreach ( $value as $subkey => $subvalue ) {
1048  $this->store->set( "$key:$subkey", $subvalue );
1049  }
1050  } else {
1051  $this->store->set( $key, $value );
1052  }
1053  }
1054  $this->store->finishWrite();
1055 
1056  # Clear out the MessageBlobStore
1057  # HACK: If using a null (i.e. disabled) storage backend, we
1058  # can't write to the MessageBlobStore either
1059  if ( !$this->store instanceof LCStoreNull ) {
1060  foreach ( $this->clearStoreCallbacks as $callback ) {
1061  $callback();
1062  }
1063  }
1064  }
1065 
1074  protected function buildPreload( $data ) {
1075  $preload = [ 'messages' => [] ];
1076  foreach ( self::$preloadedKeys as $key ) {
1077  $preload[$key] = $data[$key];
1078  }
1079 
1080  foreach ( $data['preloadedMessages'] as $subkey ) {
1081  $subitem = $data['messages'][$subkey] ?? null;
1082  $preload['messages'][$subkey] = $subitem;
1083  }
1084 
1085  return $preload;
1086  }
1087 
1093  public function unload( $code ) {
1094  unset( $this->data[$code] );
1095  unset( $this->loadedItems[$code] );
1096  unset( $this->loadedSubitems[$code] );
1097  unset( $this->initialisedLangs[$code] );
1098  unset( $this->shallowFallbacks[$code] );
1099 
1100  foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
1101  if ( $fbCode === $code ) {
1102  $this->unload( $shallowCode );
1103  }
1104  }
1105  }
1106 
1110  public function unloadAll() {
1111  foreach ( $this->initialisedLangs as $lang => $unused ) {
1112  $this->unload( $lang );
1113  }
1114  }
1115 
1119  public function disableBackend() {
1120  $this->store = new LCStoreNull;
1121  $this->manualRecache = false;
1122  }
1123 }
FileDependency
@newable
Definition: FileDependency.php:28
LocalisationCache\loadSubitem
loadSubitem( $code, $key, $subkey)
Load a subitem into the cache.
Definition: LocalisationCache.php:406
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:480
$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:442
LocalisationCache\getSubitemList
getSubitemList( $code, $key)
Get the list of subitem keys for a given item.
Definition: LocalisationCache.php:351
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:722
LocalisationCache\getItem
getItem( $code, $key)
Get a cache item.
Definition: LocalisationCache.php:310
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:849
LocalisationCache\readJSONFile
readJSONFile( $fileName)
Read a JSON file containing localisation messages.
Definition: LocalisationCache.php:585
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:811
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:640
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:25
LocalisationCache\unload
unload( $code)
Unload the data for a given language from the object cache.
Definition: LocalisationCache.php:1093
LocalisationCache\getMessagesDirs
getMessagesDirs()
Gets the combined list of messages dirs from core and extensions.
Definition: LocalisationCache.php:830
LocalisationCache\mergeMagicWords
mergeMagicWords(&$value, $fallbackValue)
Definition: LocalisationCache.php:784
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:1119
LocalisationCache\getCompiledPluralRules
getCompiledPluralRules( $code)
Get the compiled plural rules for a given language from the XML files.
Definition: LocalisationCache.php:617
$dirs
$dirs
Definition: mergeMessageFileList.php:192
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:555
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:1074
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:683
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:369
CacheDependency
Stable to extend.
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:541
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:287
LocalisationCache\mergeItem
mergeItem( $key, &$value, $fallbackValue)
Merge two localisation values, a primary and a fallback, overwriting the primary value in place.
Definition: LocalisationCache.php:754
$keys
$keys
Definition: testCompression.php:72
LocalisationCache\unloadAll
unloadAll()
Unload all data.
Definition: LocalisationCache.php:1110
MediaWiki\Config\ServiceOptions\get
get( $key)
Definition: ServiceOptions.php:84
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:44
LocalisationCache\getPluralRuleTypes
getPluralRuleTypes( $code)
Get the plural rule types for a given language from the XML files.
Definition: LocalisationCache.php:654
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:570
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:329
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:664
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:260
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:62
LocalisationCache\$store
LCStore $store
The persistent store object.
Definition: LocalisationCache.php:69