MediaWiki  master
LocalisationCache.php
Go to the documentation of this file.
1 <?php
24 use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
28 
42  const VERSION = 4;
43 
45  private $options;
46 
52  private $manualRecache = false;
53 
60  protected $data = [];
61 
67  private $store;
68 
72  private $logger;
73 
76 
78  private $langNameUtils;
79 
88  private $loadedItems = [];
89 
94  private $loadedSubitems = [];
95 
101  private $initialisedLangs = [];
102 
108  private $shallowFallbacks = [];
109 
113  private $recachedLangs = [];
114 
118  public static $allKeys = [
119  'fallback', 'namespaceNames', 'bookstoreList',
120  'magicWords', 'messages', 'rtl', 'capitalizeAllNouns',
121  'digitTransformTable', 'separatorTransformTable',
122  'minimumGroupingDigits', 'fallback8bitEncoding',
123  'linkPrefixExtension', 'linkTrail', 'linkPrefixCharset',
124  'namespaceAliases', 'dateFormats', 'datePreferences',
125  'datePreferenceMigrationMap', 'defaultDateFormat',
126  'specialPageAliases', 'imageFiles', 'preloadedMessages',
127  'namespaceGenderAliases', 'digitGroupingPattern', 'pluralRules',
128  'pluralRuleTypes', 'compiledPluralRules',
129  ];
130 
135  public static $mergeableMapKeys = [ 'messages', 'namespaceNames',
136  'namespaceAliases', 'dateFormats', 'imageFiles', 'preloadedMessages'
137  ];
138 
142  public static $mergeableListKeys = [];
143 
148  public static $mergeableAliasListKeys = [ 'specialPageAliases' ];
149 
155  public static $optionalMergeKeys = [ 'bookstoreList' ];
156 
160  public static $magicWordKeys = [ 'magicWords' ];
161 
165  public static $splitKeys = [ 'messages' ];
166 
170  public static $preloadedKeys = [ 'dateFormats', 'namespaceNames' ];
171 
176  private $pluralRules = null;
177 
190  private $pluralRuleTypes = null;
191 
192  private $mergeableKeys = null;
193 
202  public static function getStoreFromConf( array $conf, $fallbackCacheDir ) : LCStore {
203  $storeArg = [];
204  $storeArg['directory'] =
205  $conf['storeDirectory'] ?: $fallbackCacheDir;
206 
207  if ( !empty( $conf['storeClass'] ) ) {
208  $storeClass = $conf['storeClass'];
209  } elseif ( $conf['store'] === 'files' || $conf['store'] === 'file' ||
210  ( $conf['store'] === 'detect' && $storeArg['directory'] )
211  ) {
212  $storeClass = LCStoreCDB::class;
213  } elseif ( $conf['store'] === 'db' || $conf['store'] === 'detect' ) {
214  $storeClass = LCStoreDB::class;
215  $storeArg['server'] = $conf['storeServer'] ?? [];
216  } elseif ( $conf['store'] === 'array' ) {
217  $storeClass = LCStoreStaticArray::class;
218  } else {
219  throw new MWException(
220  'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
221  );
222  }
223 
224  return new $storeClass( $storeArg );
225  }
226 
231  public const CONSTRUCTOR_OPTIONS = [
232  // True to treat all files as expired until they are regenerated by this object.
233  'forceRecache',
234  'manualRecache',
235  'ExtensionMessagesFiles',
236  'MessagesDirs',
237  ];
238 
254  function __construct(
256  LCStore $store,
257  LoggerInterface $logger,
258  array $clearStoreCallbacks,
260  ) {
261  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
262 
263  $this->options = $options;
264  $this->store = $store;
265  $this->logger = $logger;
266  $this->clearStoreCallbacks = $clearStoreCallbacks;
267  $this->langNameUtils = $langNameUtils;
268 
269  // Keep this separate from $this->options so it can be mutable
270  $this->manualRecache = $options->get( 'manualRecache' );
271  }
272 
279  public function isMergeableKey( $key ) {
280  if ( $this->mergeableKeys === null ) {
281  $this->mergeableKeys = array_flip( array_merge(
282  self::$mergeableMapKeys,
283  self::$mergeableListKeys,
284  self::$mergeableAliasListKeys,
285  self::$optionalMergeKeys,
286  self::$magicWordKeys
287  ) );
288  }
289 
290  return isset( $this->mergeableKeys[$key] );
291  }
292 
302  public function getItem( $code, $key ) {
303  if ( !isset( $this->loadedItems[$code][$key] ) ) {
304  $this->loadItem( $code, $key );
305  }
306 
307  if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
308  return $this->shallowFallbacks[$code];
309  }
310 
311  return $this->data[$code][$key];
312  }
313 
321  public function getSubitem( $code, $key, $subkey ) {
322  if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
323  !isset( $this->loadedItems[$code][$key] )
324  ) {
325  $this->loadSubitem( $code, $key, $subkey );
326  }
327 
328  return $this->data[$code][$key][$subkey] ?? null;
329  }
330 
343  public function getSubitemList( $code, $key ) {
344  if ( in_array( $key, self::$splitKeys ) ) {
345  return $this->getSubitem( $code, 'list', $key );
346  } else {
347  $item = $this->getItem( $code, $key );
348  if ( is_array( $item ) ) {
349  return array_keys( $item );
350  } else {
351  return false;
352  }
353  }
354  }
355 
361  protected function loadItem( $code, $key ) {
362  if ( !isset( $this->initialisedLangs[$code] ) ) {
363  $this->initLanguage( $code );
364  }
365 
366  // Check to see if initLanguage() loaded it for us
367  if ( isset( $this->loadedItems[$code][$key] ) ) {
368  return;
369  }
370 
371  if ( isset( $this->shallowFallbacks[$code] ) ) {
372  $this->loadItem( $this->shallowFallbacks[$code], $key );
373 
374  return;
375  }
376 
377  if ( in_array( $key, self::$splitKeys ) ) {
378  $subkeyList = $this->getSubitem( $code, 'list', $key );
379  foreach ( $subkeyList as $subkey ) {
380  if ( isset( $this->data[$code][$key][$subkey] ) ) {
381  continue;
382  }
383  $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey );
384  }
385  } else {
386  $this->data[$code][$key] = $this->store->get( $code, $key );
387  }
388 
389  $this->loadedItems[$code][$key] = true;
390  }
391 
398  protected function loadSubitem( $code, $key, $subkey ) {
399  if ( !in_array( $key, self::$splitKeys ) ) {
400  $this->loadItem( $code, $key );
401 
402  return;
403  }
404 
405  if ( !isset( $this->initialisedLangs[$code] ) ) {
406  $this->initLanguage( $code );
407  }
408 
409  // Check to see if initLanguage() loaded it for us
410  if ( isset( $this->loadedItems[$code][$key] ) ||
411  isset( $this->loadedSubitems[$code][$key][$subkey] )
412  ) {
413  return;
414  }
415 
416  if ( isset( $this->shallowFallbacks[$code] ) ) {
417  $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
418 
419  return;
420  }
421 
422  $value = $this->store->get( $code, "$key:$subkey" );
423  $this->data[$code][$key][$subkey] = $value;
424  $this->loadedSubitems[$code][$key][$subkey] = true;
425  }
426 
434  public function isExpired( $code ) {
435  if ( $this->options->get( 'forceRecache' ) && !isset( $this->recachedLangs[$code] ) ) {
436  $this->logger->debug( __METHOD__ . "($code): forced reload" );
437 
438  return true;
439  }
440 
441  $deps = $this->store->get( $code, 'deps' );
442  $keys = $this->store->get( $code, 'list' );
443  $preload = $this->store->get( $code, 'preload' );
444  // Different keys may expire separately for some stores
445  if ( $deps === null || $keys === null || $preload === null ) {
446  $this->logger->debug( __METHOD__ . "($code): cache missing, need to make one" );
447 
448  return true;
449  }
450 
451  foreach ( $deps as $dep ) {
452  // Because we're unserializing stuff from cache, we
453  // could receive objects of classes that don't exist
454  // anymore (e.g. uninstalled extensions)
455  // When this happens, always expire the cache
456  if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
457  $this->logger->debug( __METHOD__ . "($code): cache for $code expired due to " .
458  get_class( $dep ) );
459 
460  return true;
461  }
462  }
463 
464  return false;
465  }
466 
472  protected function initLanguage( $code ) {
473  if ( isset( $this->initialisedLangs[$code] ) ) {
474  return;
475  }
476 
477  $this->initialisedLangs[$code] = true;
478 
479  # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
480  if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
481  $this->initShallowFallback( $code, 'en' );
482 
483  return;
484  }
485 
486  # Recache the data if necessary
487  if ( !$this->manualRecache && $this->isExpired( $code ) ) {
488  if ( $this->langNameUtils->isSupportedLanguage( $code ) ) {
489  $this->recache( $code );
490  } elseif ( $code === 'en' ) {
491  throw new MWException( 'MessagesEn.php is missing.' );
492  } else {
493  $this->initShallowFallback( $code, 'en' );
494  }
495 
496  return;
497  }
498 
499  # Preload some stuff
500  $preload = $this->getItem( $code, 'preload' );
501  if ( $preload === null ) {
502  if ( $this->manualRecache ) {
503  // No Messages*.php file. Do shallow fallback to en.
504  if ( $code === 'en' ) {
505  throw new MWException( 'No localisation cache found for English. ' .
506  'Please run maintenance/rebuildLocalisationCache.php.' );
507  }
508  $this->initShallowFallback( $code, 'en' );
509 
510  return;
511  } else {
512  throw new MWException( 'Invalid or missing localisation cache.' );
513  }
514  }
515  $this->data[$code] = $preload;
516  foreach ( $preload as $key => $item ) {
517  if ( in_array( $key, self::$splitKeys ) ) {
518  foreach ( $item as $subkey => $subitem ) {
519  $this->loadedSubitems[$code][$key][$subkey] = true;
520  }
521  } else {
522  $this->loadedItems[$code][$key] = true;
523  }
524  }
525  }
526 
533  public function initShallowFallback( $primaryCode, $fallbackCode ) {
534  $this->data[$primaryCode] =& $this->data[$fallbackCode];
535  $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
536  $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
537  $this->shallowFallbacks[$primaryCode] = $fallbackCode;
538  }
539 
547  protected function readPHPFile( $_fileName, $_fileType ) {
548  include $_fileName;
549 
550  $data = [];
551  if ( $_fileType == 'core' || $_fileType == 'extension' ) {
552  foreach ( self::$allKeys as $key ) {
553  // Not all keys are set in language files, so
554  // check they exist first
555  if ( isset( $$key ) ) {
556  $data[$key] = $$key;
557  }
558  }
559  } elseif ( $_fileType == 'aliases' ) {
560  if ( isset( $aliases ) ) {
561  $data['aliases'] = $aliases;
562  }
563  } else {
564  throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
565  }
566 
567  return $data;
568  }
569 
576  public function readJSONFile( $fileName ) {
577  if ( !is_readable( $fileName ) ) {
578  return [];
579  }
580 
581  $json = file_get_contents( $fileName );
582  if ( $json === false ) {
583  return [];
584  }
585 
586  $data = FormatJson::decode( $json, true );
587  if ( $data === null ) {
588  throw new MWException( __METHOD__ . ": Invalid JSON file: $fileName" );
589  }
590 
591  // Remove keys starting with '@', they're reserved for metadata and non-message data
592  foreach ( $data as $key => $unused ) {
593  if ( $key === '' || $key[0] === '@' ) {
594  unset( $data[$key] );
595  }
596  }
597 
598  // The JSON format only supports messages, none of the other variables, so wrap the data
599  return [ 'messages' => $data ];
600  }
601 
608  public function getCompiledPluralRules( $code ) {
609  $rules = $this->getPluralRules( $code );
610  if ( $rules === null ) {
611  return null;
612  }
613  try {
614  $compiledRules = Evaluator::compile( $rules );
615  } catch ( CLDRPluralRuleError $e ) {
616  $this->logger->debug( $e->getMessage() );
617 
618  return [];
619  }
620 
621  return $compiledRules;
622  }
623 
631  public function getPluralRules( $code ) {
632  if ( $this->pluralRules === null ) {
633  $this->loadPluralFiles();
634  }
635  return $this->pluralRules[$code] ?? null;
636  }
637 
645  public function getPluralRuleTypes( $code ) {
646  if ( $this->pluralRuleTypes === null ) {
647  $this->loadPluralFiles();
648  }
649  return $this->pluralRuleTypes[$code] ?? null;
650  }
651 
655  protected function loadPluralFiles() {
656  global $IP;
657  $cldrPlural = "$IP/languages/data/plurals.xml";
658  $mwPlural = "$IP/languages/data/plurals-mediawiki.xml";
659  // Load CLDR plural rules
660  $this->loadPluralFile( $cldrPlural );
661  if ( file_exists( $mwPlural ) ) {
662  // Override or extend
663  $this->loadPluralFile( $mwPlural );
664  }
665  }
666 
674  protected function loadPluralFile( $fileName ) {
675  // Use file_get_contents instead of DOMDocument::load (T58439)
676  $xml = file_get_contents( $fileName );
677  if ( !$xml ) {
678  throw new MWException( "Unable to read plurals file $fileName" );
679  }
680  $doc = new DOMDocument;
681  $doc->loadXML( $xml );
682  $rulesets = $doc->getElementsByTagName( "pluralRules" );
683  foreach ( $rulesets as $ruleset ) {
684  $codes = $ruleset->getAttribute( 'locales' );
685  $rules = [];
686  $ruleTypes = [];
687  $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
688  foreach ( $ruleElements as $elt ) {
689  $ruleType = $elt->getAttribute( 'count' );
690  if ( $ruleType === 'other' ) {
691  // Don't record "other" rules, which have an empty condition
692  continue;
693  }
694  $rules[] = $elt->nodeValue;
695  $ruleTypes[] = $ruleType;
696  }
697  foreach ( explode( ' ', $codes ) as $code ) {
698  $this->pluralRules[$code] = $rules;
699  $this->pluralRuleTypes[$code] = $ruleTypes;
700  }
701  }
702  }
703 
713  protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
714  global $IP;
715 
716  // This reads in the PHP i18n file with non-messages l10n data
717  $fileName = $this->langNameUtils->getMessagesFileName( $code );
718  if ( !file_exists( $fileName ) ) {
719  $data = [];
720  } else {
721  $deps[] = new FileDependency( $fileName );
722  $data = $this->readPHPFile( $fileName, 'core' );
723  }
724 
725  # Load CLDR plural rules for JavaScript
726  $data['pluralRules'] = $this->getPluralRules( $code );
727  # And for PHP
728  $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
729  # Load plural rule types
730  $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code );
731 
732  $deps['plurals'] = new FileDependency( "$IP/languages/data/plurals.xml" );
733  $deps['plurals-mw'] = new FileDependency( "$IP/languages/data/plurals-mediawiki.xml" );
734 
735  return $data;
736  }
737 
745  protected function mergeItem( $key, &$value, $fallbackValue ) {
746  if ( !is_null( $value ) ) {
747  if ( !is_null( $fallbackValue ) ) {
748  if ( in_array( $key, self::$mergeableMapKeys ) ) {
749  $value = $value + $fallbackValue;
750  } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
751  // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
752  $value = array_unique( array_merge( $fallbackValue, $value ) );
753  } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
754  $value = array_merge_recursive( $value, $fallbackValue );
755  } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
756  if ( !empty( $value['inherit'] ) ) {
757  $value = array_merge( $fallbackValue, $value );
758  }
759 
760  if ( isset( $value['inherit'] ) ) {
761  unset( $value['inherit'] );
762  }
763  } elseif ( in_array( $key, self::$magicWordKeys ) ) {
764  $this->mergeMagicWords( $value, $fallbackValue );
765  }
766  }
767  } else {
768  $value = $fallbackValue;
769  }
770  }
771 
776  protected function mergeMagicWords( &$value, $fallbackValue ) {
777  foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
778  if ( !isset( $value[$magicName] ) ) {
779  $value[$magicName] = $fallbackInfo;
780  } else {
781  $oldSynonyms = array_slice( $fallbackInfo, 1 );
782  $newSynonyms = array_slice( $value[$magicName], 1 );
783  $synonyms = array_values( array_unique( array_merge(
784  $newSynonyms, $oldSynonyms ) ) );
785  $value[$magicName] = array_merge( [ $fallbackInfo[0] ], $synonyms );
786  }
787  }
788  }
789 
803  protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
804  $used = false;
805  foreach ( $codeSequence as $code ) {
806  if ( isset( $fallbackValue[$code] ) ) {
807  $this->mergeItem( $key, $value, $fallbackValue[$code] );
808  $used = true;
809  }
810  }
811 
812  return $used;
813  }
814 
822  public function getMessagesDirs() {
823  global $IP;
824 
825  return [
826  'core' => "$IP/languages/i18n",
827  'exif' => "$IP/languages/i18n/exif",
828  'api' => "$IP/includes/api/i18n",
829  'oojs-ui' => "$IP/resources/lib/ooui/i18n",
830  ] + $this->options->get( 'MessagesDirs' );
831  }
832 
839  public function recache( $code ) {
840  if ( !$code ) {
841  throw new MWException( "Invalid language code requested" );
842  }
843  $this->recachedLangs[ $code ] = true;
844 
845  # Initial values
846  $initialData = array_fill_keys( self::$allKeys, null );
847  $coreData = $initialData;
848  $deps = [];
849 
850  # Load the primary localisation from the source file
851  $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
852  $this->logger->debug( __METHOD__ . ": got localisation for $code from source" );
853 
854  # Merge primary localisation
855  foreach ( $data as $key => $value ) {
856  $this->mergeItem( $key, $coreData[ $key ], $value );
857  }
858 
859  # Fill in the fallback if it's not there already
860  if ( ( is_null( $coreData['fallback'] ) || $coreData['fallback'] === false ) && $code === 'en' ) {
861  $coreData['fallback'] = false;
862  $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'] = [];
863  } else {
864  if ( !is_null( $coreData['fallback'] ) ) {
865  $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
866  } else {
867  $coreData['fallbackSequence'] = [];
868  }
869  $len = count( $coreData['fallbackSequence'] );
870 
871  # Before we add the 'en' fallback for messages, keep a copy of
872  # the original fallback sequence
873  $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'];
874 
875  # Ensure that the sequence ends at 'en' for messages
876  if ( !$len || $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
877  $coreData['fallbackSequence'][] = 'en';
878  }
879  }
880 
881  $codeSequence = array_merge( [ $code ], $coreData['fallbackSequence'] );
882  $messageDirs = $this->getMessagesDirs();
883 
884  # Load non-JSON localisation data for extensions
885  $extensionData = array_fill_keys( $codeSequence, $initialData );
886  foreach ( $this->options->get( 'ExtensionMessagesFiles' ) as $extension => $fileName ) {
887  if ( isset( $messageDirs[$extension] ) ) {
888  # This extension has JSON message data; skip the PHP shim
889  continue;
890  }
891 
892  $data = $this->readPHPFile( $fileName, 'extension' );
893  $used = false;
894 
895  foreach ( $data as $key => $item ) {
896  foreach ( $codeSequence as $csCode ) {
897  if ( isset( $item[$csCode] ) ) {
898  $this->mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] );
899  $used = true;
900  }
901  }
902  }
903 
904  if ( $used ) {
905  $deps[] = new FileDependency( $fileName );
906  }
907  }
908 
909  # Load the localisation data for each fallback, then merge it into the full array
910  $allData = $initialData;
911  foreach ( $codeSequence as $csCode ) {
912  $csData = $initialData;
913 
914  # Load core messages and the extension localisations.
915  foreach ( $messageDirs as $dirs ) {
916  foreach ( (array)$dirs as $dir ) {
917  $fileName = "$dir/$csCode.json";
918  $data = $this->readJSONFile( $fileName );
919 
920  foreach ( $data as $key => $item ) {
921  $this->mergeItem( $key, $csData[$key], $item );
922  }
923 
924  $deps[] = new FileDependency( $fileName );
925  }
926  }
927 
928  # Merge non-JSON extension data
929  if ( isset( $extensionData[$csCode] ) ) {
930  foreach ( $extensionData[$csCode] as $key => $item ) {
931  $this->mergeItem( $key, $csData[$key], $item );
932  }
933  }
934 
935  if ( $csCode === $code ) {
936  # Merge core data into extension data
937  foreach ( $coreData as $key => $item ) {
938  $this->mergeItem( $key, $csData[$key], $item );
939  }
940  } else {
941  # Load the secondary localisation from the source file to
942  # avoid infinite cycles on cyclic fallbacks
943  $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps );
944  # Only merge the keys that make sense to merge
945  foreach ( self::$allKeys as $key ) {
946  if ( !isset( $fbData[ $key ] ) ) {
947  continue;
948  }
949 
950  if ( is_null( $coreData[ $key ] ) || $this->isMergeableKey( $key ) ) {
951  $this->mergeItem( $key, $csData[ $key ], $fbData[ $key ] );
952  }
953  }
954  }
955 
956  # Allow extensions an opportunity to adjust the data for this
957  # fallback
958  Hooks::run( 'LocalisationCacheRecacheFallback', [ $this, $csCode, &$csData ] );
959 
960  # Merge the data for this fallback into the final array
961  if ( $csCode === $code ) {
962  $allData = $csData;
963  } else {
964  foreach ( self::$allKeys as $key ) {
965  if ( !isset( $csData[$key] ) ) {
966  continue;
967  }
968 
969  if ( is_null( $allData[$key] ) || $this->isMergeableKey( $key ) ) {
970  $this->mergeItem( $key, $allData[$key], $csData[$key] );
971  }
972  }
973  }
974  }
975 
976  # Add cache dependencies for any referenced globals
977  $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
978  // The 'MessagesDirs' config setting is used in LocalisationCache::getMessagesDirs().
979  // We use the key 'wgMessagesDirs' for historical reasons.
980  $deps['wgMessagesDirs'] = new MainConfigDependency( 'MessagesDirs' );
981  $deps['version'] = new ConstantDependency( 'LocalisationCache::VERSION' );
982 
983  # Add dependencies to the cache entry
984  $allData['deps'] = $deps;
985 
986  # Replace spaces with underscores in namespace names
987  $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
988 
989  # And do the same for special page aliases. $page is an array.
990  foreach ( $allData['specialPageAliases'] as &$page ) {
991  $page = str_replace( ' ', '_', $page );
992  }
993  # Decouple the reference to prevent accidental damage
994  unset( $page );
995 
996  # If there were no plural rules, return an empty array
997  if ( $allData['pluralRules'] === null ) {
998  $allData['pluralRules'] = [];
999  }
1000  if ( $allData['compiledPluralRules'] === null ) {
1001  $allData['compiledPluralRules'] = [];
1002  }
1003  # If there were no plural rule types, return an empty array
1004  if ( $allData['pluralRuleTypes'] === null ) {
1005  $allData['pluralRuleTypes'] = [];
1006  }
1007 
1008  # Set the list keys
1009  $allData['list'] = [];
1010  foreach ( self::$splitKeys as $key ) {
1011  $allData['list'][$key] = array_keys( $allData[$key] );
1012  }
1013  # Run hooks
1014  $unused = true; // Used to be $purgeBlobs, removed in 1.34
1015  Hooks::run( 'LocalisationCacheRecache', [ $this, $code, &$allData, &$unused ] );
1016 
1017  if ( is_null( $allData['namespaceNames'] ) ) {
1018  throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
1019  'Check that your languages/messages/MessagesEn.php file is intact.' );
1020  }
1021 
1022  # Set the preload key
1023  $allData['preload'] = $this->buildPreload( $allData );
1024 
1025  # Save to the process cache and register the items loaded
1026  $this->data[$code] = $allData;
1027  foreach ( $allData as $key => $item ) {
1028  $this->loadedItems[$code][$key] = true;
1029  }
1030 
1031  # Save to the persistent cache
1032  $this->store->startWrite( $code );
1033  foreach ( $allData as $key => $value ) {
1034  if ( in_array( $key, self::$splitKeys ) ) {
1035  foreach ( $value as $subkey => $subvalue ) {
1036  $this->store->set( "$key:$subkey", $subvalue );
1037  }
1038  } else {
1039  $this->store->set( $key, $value );
1040  }
1041  }
1042  $this->store->finishWrite();
1043 
1044  # Clear out the MessageBlobStore
1045  # HACK: If using a null (i.e. disabled) storage backend, we
1046  # can't write to the MessageBlobStore either
1047  if ( !$this->store instanceof LCStoreNull ) {
1048  foreach ( $this->clearStoreCallbacks as $callback ) {
1049  $callback();
1050  }
1051  }
1052  }
1053 
1062  protected function buildPreload( $data ) {
1063  $preload = [ 'messages' => [] ];
1064  foreach ( self::$preloadedKeys as $key ) {
1065  $preload[$key] = $data[$key];
1066  }
1067 
1068  foreach ( $data['preloadedMessages'] as $subkey ) {
1069  $subitem = $data['messages'][$subkey] ?? null;
1070  $preload['messages'][$subkey] = $subitem;
1071  }
1072 
1073  return $preload;
1074  }
1075 
1081  public function unload( $code ) {
1082  unset( $this->data[$code] );
1083  unset( $this->loadedItems[$code] );
1084  unset( $this->loadedSubitems[$code] );
1085  unset( $this->initialisedLangs[$code] );
1086  unset( $this->shallowFallbacks[$code] );
1087 
1088  foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
1089  if ( $fbCode === $code ) {
1090  $this->unload( $shallowCode );
1091  }
1092  }
1093  }
1094 
1098  public function unloadAll() {
1099  foreach ( $this->initialisedLangs as $lang => $unused ) {
1100  $this->unload( $lang );
1101  }
1102  }
1103 
1107  public function disableBackend() {
1108  $this->store = new LCStoreNull;
1109  $this->manualRecache = false;
1110  }
1111 }
unloadAll()
Unload all data.
static $mergeableAliasListKeys
Keys for items which contain an array of arrays of equivalent aliases for each subitem.
readSourceFilesAndRegisterDeps( $code, &$deps)
Read the data from the source files for a given language, and register the relevant dependencies in t...
getItem( $code, $key)
Get a cache item.
getSubitem( $code, $key, $subkey)
Get a subitem, for instance a single message for a given language.
mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue)
Given an array mapping language code to localisation value, such as is found in extension *...
mergeItem( $key, &$value, $fallbackValue)
Merge two localisation values, a primary and a fallback, overwriting the primary value in place...
$IP
Definition: WebStart.php:41
unload( $code)
Unload the data for a given language from the object cache.
LCStore $store
The persistent store object.
if(!isset( $args[0])) $lang
callable [] $clearStoreCallbacks
See comment for parameter in constructor.
$shallowFallbacks
An array mapping non-existent pseudo-languages to fallback languages.
readJSONFile( $fileName)
Read a JSON file containing localisation messages.
initLanguage( $code)
Initialise a language in this object.
$initialisedLangs
An array where presence of a key indicates that that language has been initialised.
LanguageNameUtils $langNameUtils
static $allKeys
All item keys.
isExpired()
Returns true if the dependency is expired, false otherwise.
getMessagesDirs()
Gets the combined list of messages dirs from core and extensions.
mergeMagicWords(&$value, $fallbackValue)
$manualRecache
True if recaching should only be done on an explicit call to recache().
disableBackend()
Disable the storage backend.
A class for passing options to services.
static $optionalMergeKeys
Keys for items which contain an associative array, and may be merged if the primary value contains th...
LoggerInterface $logger
$recachedLangs
An array where the keys are codes that have been recached by this instance.
getSubitemList( $code, $key)
Get the list of subitem keys for a given item.
buildPreload( $data)
Build the preload item from the given pre-cache data.
recache( $code)
Load localisation data for a given language for both core and extensions and save it to the persisten...
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:174
ServiceOptions $options
$pluralRules
Associative array of cached plural rules.
static $preloadedKeys
Keys which are loaded automatically by initLanguage()
loadPluralFile( $fileName)
Load a plural XML file with the given filename, compile the relevant rules, and save the compiled rul...
__construct(ServiceOptions $options, LCStore $store, LoggerInterface $logger, array $clearStoreCallbacks, LanguageNameUtils $langNameUtils)
For constructor parameters, see the documentation in DefaultSettings.php for $wgLocalisationCacheConf...
$data
The cache data.
getPluralRules( $code)
Get the plural rules for a given language from the XML files.
initShallowFallback( $primaryCode, $fallbackCode)
Create a fallback from one language to another, without creating a complete persistent cache...
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys, without regard for order.
static $mergeableListKeys
Keys for items which are a numbered array.
loadPluralFiles()
Load the plural XML files.
static getStoreFromConf(array $conf, $fallbackCacheDir)
Return a suitable LCStore as specified by the given configuration.
Interface for the persistence layer of LocalisationCache.
Definition: LCStore.php:38
getPluralRuleTypes( $code)
Get the plural rule types for a given language from the XML files.
static $magicWordKeys
Keys for items that are formatted like $magicWords.
$loadedItems
A 2-d associative array, code/key, where presence indicates that the item is loaded.
static $splitKeys
Keys for items where the subitems are stored in the backend separately.
getCompiledPluralRules( $code)
Get the compiled plural rules for a given language from the XML files.
$loadedSubitems
A 3-d associative array, code/key/subkey, where presence indicates that the subitem is loaded...
Null store backend, used to avoid DB errors during install.
Definition: LCStoreNull.php:24
readPHPFile( $_fileName, $_fileType)
Read a PHP file containing localisation data.
static $mergeableMapKeys
Keys for items which consist of associative arrays, which may be merged by a fallback sequence...
$pluralRuleTypes
Associative array of cached plural rule types.
loadSubitem( $code, $key, $subkey)
Load a subitem into the cache.
A service that provides utilities to do with language names and codes.
loadItem( $code, $key)
Load an item into the cache.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
isMergeableKey( $key)
Returns true if the given key is mergeable, that is, if it is an associative array which can be merge...
isExpired( $code)
Returns true if the cache identified by $code is missing or expired.