MediaWiki  master
LocalisationCache.php
Go to the documentation of this file.
1 <?php
23 use CLDRPluralRuleParser\Evaluator;
24 use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
27 use Psr\Log\LoggerInterface;
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  public 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  'rest' => "$IP/includes/Rest/i18n",
830  'oojs-ui' => "$IP/resources/lib/ooui/i18n",
831  'paramvalidator' => "$IP/includes/libs/ParamValidator/i18n",
832  ] + $this->options->get( 'MessagesDirs' );
833  }
834 
841  public function recache( $code ) {
842  if ( !$code ) {
843  throw new MWException( "Invalid language code requested" );
844  }
845  $this->recachedLangs[ $code ] = true;
846 
847  # Initial values
848  $initialData = array_fill_keys( self::$allKeys, null );
849  $coreData = $initialData;
850  $deps = [];
851 
852  # Load the primary localisation from the source file
853  $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
854  $this->logger->debug( __METHOD__ . ": got localisation for $code from source" );
855 
856  # Merge primary localisation
857  foreach ( $data as $key => $value ) {
858  $this->mergeItem( $key, $coreData[ $key ], $value );
859  }
860 
861  # Fill in the fallback if it's not there already
862  if ( ( is_null( $coreData['fallback'] ) || $coreData['fallback'] === false ) && $code === 'en' ) {
863  $coreData['fallback'] = false;
864  $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'] = [];
865  } else {
866  if ( !is_null( $coreData['fallback'] ) ) {
867  $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
868  } else {
869  $coreData['fallbackSequence'] = [];
870  }
871  $len = count( $coreData['fallbackSequence'] );
872 
873  # Before we add the 'en' fallback for messages, keep a copy of
874  # the original fallback sequence
875  $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'];
876 
877  # Ensure that the sequence ends at 'en' for messages
878  if ( !$len || $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
879  $coreData['fallbackSequence'][] = 'en';
880  }
881  }
882 
883  $codeSequence = array_merge( [ $code ], $coreData['fallbackSequence'] );
884  $messageDirs = $this->getMessagesDirs();
885 
886  # Load non-JSON localisation data for extensions
887  $extensionData = array_fill_keys( $codeSequence, $initialData );
888  foreach ( $this->options->get( 'ExtensionMessagesFiles' ) as $extension => $fileName ) {
889  if ( isset( $messageDirs[$extension] ) ) {
890  # This extension has JSON message data; skip the PHP shim
891  continue;
892  }
893 
894  $data = $this->readPHPFile( $fileName, 'extension' );
895  $used = false;
896 
897  foreach ( $data as $key => $item ) {
898  foreach ( $codeSequence as $csCode ) {
899  if ( isset( $item[$csCode] ) ) {
900  $this->mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] );
901  $used = true;
902  }
903  }
904  }
905 
906  if ( $used ) {
907  $deps[] = new FileDependency( $fileName );
908  }
909  }
910 
911  # Load the localisation data for each fallback, then merge it into the full array
912  $allData = $initialData;
913  foreach ( $codeSequence as $csCode ) {
914  $csData = $initialData;
915 
916  # Load core messages and the extension localisations.
917  foreach ( $messageDirs as $dirs ) {
918  foreach ( (array)$dirs as $dir ) {
919  $fileName = "$dir/$csCode.json";
920  $data = $this->readJSONFile( $fileName );
921 
922  foreach ( $data as $key => $item ) {
923  $this->mergeItem( $key, $csData[$key], $item );
924  }
925 
926  $deps[] = new FileDependency( $fileName );
927  }
928  }
929 
930  # Merge non-JSON extension data
931  if ( isset( $extensionData[$csCode] ) ) {
932  foreach ( $extensionData[$csCode] as $key => $item ) {
933  $this->mergeItem( $key, $csData[$key], $item );
934  }
935  }
936 
937  if ( $csCode === $code ) {
938  # Merge core data into extension data
939  foreach ( $coreData as $key => $item ) {
940  $this->mergeItem( $key, $csData[$key], $item );
941  }
942  } else {
943  # Load the secondary localisation from the source file to
944  # avoid infinite cycles on cyclic fallbacks
945  $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps );
946  # Only merge the keys that make sense to merge
947  foreach ( self::$allKeys as $key ) {
948  if ( !isset( $fbData[ $key ] ) ) {
949  continue;
950  }
951 
952  if ( is_null( $coreData[ $key ] ) || $this->isMergeableKey( $key ) ) {
953  $this->mergeItem( $key, $csData[ $key ], $fbData[ $key ] );
954  }
955  }
956  }
957 
958  # Allow extensions an opportunity to adjust the data for this
959  # fallback
960  Hooks::run( 'LocalisationCacheRecacheFallback', [ $this, $csCode, &$csData ] );
961 
962  # Merge the data for this fallback into the final array
963  if ( $csCode === $code ) {
964  $allData = $csData;
965  } else {
966  foreach ( self::$allKeys as $key ) {
967  if ( !isset( $csData[$key] ) ) {
968  continue;
969  }
970 
971  if ( is_null( $allData[$key] ) || $this->isMergeableKey( $key ) ) {
972  $this->mergeItem( $key, $allData[$key], $csData[$key] );
973  }
974  }
975  }
976  }
977 
978  # Add cache dependencies for any referenced globals
979  $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
980  // The 'MessagesDirs' config setting is used in LocalisationCache::getMessagesDirs().
981  // We use the key 'wgMessagesDirs' for historical reasons.
982  $deps['wgMessagesDirs'] = new MainConfigDependency( 'MessagesDirs' );
983  $deps['version'] = new ConstantDependency( 'LocalisationCache::VERSION' );
984 
985  # Add dependencies to the cache entry
986  $allData['deps'] = $deps;
987 
988  # Replace spaces with underscores in namespace names
989  $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
990 
991  # And do the same for special page aliases. $page is an array.
992  foreach ( $allData['specialPageAliases'] as &$page ) {
993  $page = str_replace( ' ', '_', $page );
994  }
995  # Decouple the reference to prevent accidental damage
996  unset( $page );
997 
998  # If there were no plural rules, return an empty array
999  if ( $allData['pluralRules'] === null ) {
1000  $allData['pluralRules'] = [];
1001  }
1002  if ( $allData['compiledPluralRules'] === null ) {
1003  $allData['compiledPluralRules'] = [];
1004  }
1005  # If there were no plural rule types, return an empty array
1006  if ( $allData['pluralRuleTypes'] === null ) {
1007  $allData['pluralRuleTypes'] = [];
1008  }
1009 
1010  # Set the list keys
1011  $allData['list'] = [];
1012  foreach ( self::$splitKeys as $key ) {
1013  $allData['list'][$key] = array_keys( $allData[$key] );
1014  }
1015  # Run hooks
1016  $unused = true; // Used to be $purgeBlobs, removed in 1.34
1017  Hooks::run( 'LocalisationCacheRecache', [ $this, $code, &$allData, &$unused ] );
1018 
1019  if ( is_null( $allData['namespaceNames'] ) ) {
1020  throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
1021  'Check that your languages/messages/MessagesEn.php file is intact.' );
1022  }
1023 
1024  # Set the preload key
1025  $allData['preload'] = $this->buildPreload( $allData );
1026 
1027  # Save to the process cache and register the items loaded
1028  $this->data[$code] = $allData;
1029  foreach ( $allData as $key => $item ) {
1030  $this->loadedItems[$code][$key] = true;
1031  }
1032 
1033  # Save to the persistent cache
1034  $this->store->startWrite( $code );
1035  foreach ( $allData as $key => $value ) {
1036  if ( in_array( $key, self::$splitKeys ) ) {
1037  foreach ( $value as $subkey => $subvalue ) {
1038  $this->store->set( "$key:$subkey", $subvalue );
1039  }
1040  } else {
1041  $this->store->set( $key, $value );
1042  }
1043  }
1044  $this->store->finishWrite();
1045 
1046  # Clear out the MessageBlobStore
1047  # HACK: If using a null (i.e. disabled) storage backend, we
1048  # can't write to the MessageBlobStore either
1049  if ( !$this->store instanceof LCStoreNull ) {
1050  foreach ( $this->clearStoreCallbacks as $callback ) {
1051  $callback();
1052  }
1053  }
1054  }
1055 
1064  protected function buildPreload( $data ) {
1065  $preload = [ 'messages' => [] ];
1066  foreach ( self::$preloadedKeys as $key ) {
1067  $preload[$key] = $data[$key];
1068  }
1069 
1070  foreach ( $data['preloadedMessages'] as $subkey ) {
1071  $subitem = $data['messages'][$subkey] ?? null;
1072  $preload['messages'][$subkey] = $subitem;
1073  }
1074 
1075  return $preload;
1076  }
1077 
1083  public function unload( $code ) {
1084  unset( $this->data[$code] );
1085  unset( $this->loadedItems[$code] );
1086  unset( $this->loadedSubitems[$code] );
1087  unset( $this->initialisedLangs[$code] );
1088  unset( $this->shallowFallbacks[$code] );
1089 
1090  foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
1091  if ( $fbCode === $code ) {
1092  $this->unload( $shallowCode );
1093  }
1094  }
1095  }
1096 
1100  public function unloadAll() {
1101  foreach ( $this->initialisedLangs as $lang => $unused ) {
1102  $this->unload( $lang );
1103  }
1104  }
1105 
1109  public function disableBackend() {
1110  $this->store = new LCStoreNull;
1111  $this->manualRecache = false;
1112  }
1113 }
FileDependency
Definition: FileDependency.php:27
LocalisationCache\loadSubitem
loadSubitem( $code, $key, $subkey)
Load a subitem into the cache.
Definition: LocalisationCache.php:398
LocalisationCache\$initialisedLangs
$initialisedLangs
An array where presence of a key indicates that that language has been initialised.
Definition: LocalisationCache.php:101
LocalisationCache\$manualRecache
$manualRecache
True if recaching should only be done on an explicit call to recache().
Definition: LocalisationCache.php:52
LocalisationCache\initLanguage
initLanguage( $code)
Initialise a language in this object.
Definition: LocalisationCache.php:472
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:35
LocalisationCache\isExpired
isExpired( $code)
Returns true if the cache identified by $code is missing or expired.
Definition: LocalisationCache.php:434
LocalisationCache\getSubitemList
getSubitemList( $code, $key)
Get the list of subitem keys for a given item.
Definition: LocalisationCache.php:343
LocalisationCache\$logger
LoggerInterface $logger
Definition: LocalisationCache.php:72
LocalisationCache\$recachedLangs
$recachedLangs
An array where the keys are codes that have been recached by this instance.
Definition: LocalisationCache.php:113
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:148
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:713
LocalisationCache\getItem
getItem( $code, $key)
Get a cache item.
Definition: LocalisationCache.php:302
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:841
LocalisationCache\readJSONFile
readJSONFile( $fileName)
Read a JSON file containing localisation messages.
Definition: LocalisationCache.php:576
LocalisationCache\$pluralRules
$pluralRules
Associative array of cached plural rules.
Definition: LocalisationCache.php:176
MediaWiki\Languages\LanguageNameUtils
Definition: LanguageNameUtils.php:44
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:803
LocalisationCache\$options
ServiceOptions $options
Definition: LocalisationCache.php:45
LocalisationCache\$preloadedKeys
static $preloadedKeys
Keys which are loaded automatically by initLanguage()
Definition: LocalisationCache.php:170
LocalisationCache\getPluralRules
getPluralRules( $code)
Get the plural rules for a given language from the XML files.
Definition: LocalisationCache.php:631
FormatJson\decode
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:174
MWException
MediaWiki exception.
Definition: MWException.php:26
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:1083
LocalisationCache\getMessagesDirs
getMessagesDirs()
Gets the combined list of messages dirs from core and extensions.
Definition: LocalisationCache.php:822
LocalisationCache\mergeMagicWords
mergeMagicWords(&$value, $fallbackValue)
Definition: LocalisationCache.php:776
LocalisationCache\getStoreFromConf
static getStoreFromConf(array $conf, $fallbackCacheDir)
Return a suitable LCStore as specified by the given configuration.
Definition: LocalisationCache.php:202
LocalisationCache\disableBackend
disableBackend()
Disable the storage backend.
Definition: LocalisationCache.php:1109
LocalisationCache\getCompiledPluralRules
getCompiledPluralRules( $code)
Get the compiled plural rules for a given language from the XML files.
Definition: LocalisationCache.php:608
$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:155
LocalisationCache\$loadedItems
$loadedItems
A 2-d associative array, code/key, where presence indicates that the item is loaded.
Definition: LocalisationCache.php:88
LocalisationCache\readPHPFile
readPHPFile( $_fileName, $_fileType)
Read a PHP file containing localisation data.
Definition: LocalisationCache.php:547
LocalisationCache\$mergeableMapKeys
static $mergeableMapKeys
Keys for items which consist of associative arrays, which may be merged by a fallback sequence.
Definition: LocalisationCache.php:135
LocalisationCache\buildPreload
buildPreload( $data)
Build the preload item from the given pre-cache data.
Definition: LocalisationCache.php:1064
LocalisationCache\$mergeableKeys
$mergeableKeys
Definition: LocalisationCache.php:192
LocalisationCache\__construct
__construct(ServiceOptions $options, LCStore $store, LoggerInterface $logger, array $clearStoreCallbacks, LanguageNameUtils $langNameUtils)
For constructor parameters, see the documentation in DefaultSettings.php for $wgLocalisationCacheConf...
Definition: LocalisationCache.php:254
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:674
LocalisationCache
Class for caching the contents of localisation files, Messages*.php and *.i18n.php.
Definition: LocalisationCache.php:41
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:361
CacheDependency
Definition: CacheDependency.php:27
LocalisationCache\$pluralRuleTypes
$pluralRuleTypes
Associative array of cached plural rule types.
Definition: LocalisationCache.php:190
LocalisationCache\initShallowFallback
initShallowFallback( $primaryCode, $fallbackCode)
Create a fallback from one language to another, without creating a complete persistent cache.
Definition: LocalisationCache.php:533
CacheDependency\isExpired
isExpired()
Returns true if the dependency is expired, false otherwise.
LocalisationCache\VERSION
const VERSION
Definition: LocalisationCache.php:42
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:279
LocalisationCache\mergeItem
mergeItem( $key, &$value, $fallbackValue)
Merge two localisation values, a primary and a fallback, overwriting the primary value in place.
Definition: LocalisationCache.php:745
$keys
$keys
Definition: testCompression.php:69
LocalisationCache\unloadAll
unloadAll()
Unload all data.
Definition: LocalisationCache.php:1100
MediaWiki\Config\ServiceOptions\get
get( $key)
Definition: ServiceOptions.php:84
LocalisationCache\getPluralRuleTypes
getPluralRuleTypes( $code)
Get the plural rule types for a given language from the XML files.
Definition: LocalisationCache.php:645
LocalisationCache\$data
$data
The cache data.
Definition: LocalisationCache.php:60
LocalisationCache\$splitKeys
static $splitKeys
Keys for items where the subitems are stored in the backend separately.
Definition: LocalisationCache.php:165
LocalisationCache\$mergeableListKeys
static $mergeableListKeys
Keys for items which are a numbered array.
Definition: LocalisationCache.php:142
LocalisationCache\getSubitem
getSubitem( $code, $key, $subkey)
Get a subitem, for instance a single message for a given language.
Definition: LocalisationCache.php:321
LocalisationCache\$clearStoreCallbacks
callable[] $clearStoreCallbacks
See comment for parameter in constructor.
Definition: LocalisationCache.php:75
$IP
$IP
Definition: WebStart.php:41
LocalisationCache\$shallowFallbacks
$shallowFallbacks
An array mapping non-existent pseudo-languages to fallback languages.
Definition: LocalisationCache.php:108
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
LocalisationCache\$loadedSubitems
$loadedSubitems
A 3-d associative array, code/key/subkey, where presence indicates that the subitem is loaded.
Definition: LocalisationCache.php:94
LocalisationCache\loadPluralFiles
loadPluralFiles()
Load the plural XML files.
Definition: LocalisationCache.php:655
LocalisationCache\$langNameUtils
LanguageNameUtils $langNameUtils
Definition: LocalisationCache.php:78
LocalisationCache\$allKeys
static $allKeys
All item keys.
Definition: LocalisationCache.php:118
LocalisationCache\$magicWordKeys
static $magicWordKeys
Keys for items that are formatted like $magicWords.
Definition: LocalisationCache.php:160
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:67