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;
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',
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_fill_keys( array_merge(
289  self::$mergeableMapKeys,
290  self::$mergeableListKeys,
291  self::$mergeableAliasListKeys,
292  self::$optionalMergeKeys,
293  self::$magicWordKeys
294  ), true );
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  foreach ( $this->getPluralFiles() as $fileName ) {
665  $this->loadPluralFile( $fileName );
666  }
667  }
668 
669  private function getPluralFiles(): array {
670  global $IP;
671  return [
672  // Load CLDR plural rules
673  "$IP/languages/data/plurals.xml",
674  // Override or extend with MW-specific rules
675  "$IP/languages/data/plurals-mediawiki.xml",
676  ];
677  }
678 
686  protected function loadPluralFile( $fileName ) {
687  // Use file_get_contents instead of DOMDocument::load (T58439)
688  $xml = file_get_contents( $fileName );
689  if ( !$xml ) {
690  throw new MWException( "Unable to read plurals file $fileName" );
691  }
692  $doc = new DOMDocument;
693  $doc->loadXML( $xml );
694  $rulesets = $doc->getElementsByTagName( "pluralRules" );
695  foreach ( $rulesets as $ruleset ) {
696  $codes = $ruleset->getAttribute( 'locales' );
697  $rules = [];
698  $ruleTypes = [];
699  $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
700  foreach ( $ruleElements as $elt ) {
701  $ruleType = $elt->getAttribute( 'count' );
702  if ( $ruleType === 'other' ) {
703  // Don't record "other" rules, which have an empty condition
704  continue;
705  }
706  $rules[] = $elt->nodeValue;
707  $ruleTypes[] = $ruleType;
708  }
709  foreach ( explode( ' ', $codes ) as $code ) {
710  $this->pluralRules[$code] = $rules;
711  $this->pluralRuleTypes[$code] = $ruleTypes;
712  }
713  }
714  }
715 
725  protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
726  // This reads in the PHP i18n file with non-messages l10n data
727  $fileName = $this->langNameUtils->getMessagesFileName( $code );
728  if ( !file_exists( $fileName ) ) {
729  $data = [];
730  } else {
731  $deps[] = new FileDependency( $fileName );
732  $data = $this->readPHPFile( $fileName, 'core' );
733  }
734 
735  // Load CLDR plural rules for JavaScript
736  $data['pluralRules'] = $this->getPluralRules( $code );
737  // And for PHP
738  $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
739  // Load plural rule types
740  $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code );
741 
742  foreach ( $this->getPluralFiles() as $fileName ) {
743  $deps[] = new FileDependency( $fileName );
744  }
745 
746  return $data;
747  }
748 
756  protected function mergeItem( $key, &$value, $fallbackValue ) {
757  if ( $value !== null ) {
758  if ( $fallbackValue !== null ) {
759  if ( in_array( $key, self::$mergeableMapKeys ) ) {
760  $value += $fallbackValue;
761  } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
762  $value = array_unique( array_merge( $fallbackValue, $value ) );
763  } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
764  $value = array_merge_recursive( $value, $fallbackValue );
765  } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
766  if ( !empty( $value['inherit'] ) ) {
767  $value = array_merge( $fallbackValue, $value );
768  }
769 
770  unset( $value['inherit'] );
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 validation 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\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:725
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:849
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:43
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:639
FormatJson\decode
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:146
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: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: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: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:686
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:756
$keys
$keys
Definition: testCompression.php:72
LocalisationCache\unloadAll
unloadAll()
Unload all data.
Definition: LocalisationCache.php:1110
LocalisationCache\getPluralFiles
getPluralFiles()
Definition: LocalisationCache.php:669
MediaWiki\Config\ServiceOptions\get
get( $key)
Definition: ServiceOptions.php:93
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:554
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:71
LocalisationCache\$store
LCStore $store
The persistent store object.
Definition: LocalisationCache.php:69