MediaWiki  master
LocalisationCache.php
Go to the documentation of this file.
1 <?php
24 use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
26 
40  const VERSION = 4;
41 
43  private $conf;
44 
50  private $manualRecache = false;
51 
55  private $forceRecache = false;
56 
63  protected $data = [];
64 
70  private $store;
71 
79  private $loadedItems = [];
80 
85  private $loadedSubitems = [];
86 
92  private $initialisedLangs = [];
93 
99  private $shallowFallbacks = [];
100 
104  private $recachedLangs = [];
105 
109  public static $allKeys = [
110  'fallback', 'namespaceNames', 'bookstoreList',
111  'magicWords', 'messages', 'rtl', 'capitalizeAllNouns',
112  'digitTransformTable', 'separatorTransformTable',
113  'minimumGroupingDigits', 'fallback8bitEncoding',
114  'linkPrefixExtension', 'linkTrail', 'linkPrefixCharset',
115  'namespaceAliases', 'dateFormats', 'datePreferences',
116  'datePreferenceMigrationMap', 'defaultDateFormat',
117  'specialPageAliases', 'imageFiles', 'preloadedMessages',
118  'namespaceGenderAliases', 'digitGroupingPattern', 'pluralRules',
119  'pluralRuleTypes', 'compiledPluralRules',
120  ];
121 
126  public static $mergeableMapKeys = [ 'messages', 'namespaceNames',
127  'namespaceAliases', 'dateFormats', 'imageFiles', 'preloadedMessages'
128  ];
129 
133  public static $mergeableListKeys = [];
134 
139  public static $mergeableAliasListKeys = [ 'specialPageAliases' ];
140 
146  public static $optionalMergeKeys = [ 'bookstoreList' ];
147 
151  public static $magicWordKeys = [ 'magicWords' ];
152 
156  public static $splitKeys = [ 'messages' ];
157 
161  public static $preloadedKeys = [ 'dateFormats', 'namespaceNames' ];
162 
167  private $pluralRules = null;
168 
182 
183  private $mergeableKeys = null;
184 
192  function __construct( $conf ) {
193  global $wgCacheDirectory;
194 
195  $this->conf = $conf;
196 
197  $directory = !empty( $conf['storeDirectory'] ) ? $conf['storeDirectory'] : $wgCacheDirectory;
198  $storeArg = [];
199  $storeArg['directory'] = $directory;
200 
201  if ( !empty( $conf['storeClass'] ) ) {
202  $storeClass = $conf['storeClass'];
203  } else {
204  switch ( $conf['store'] ) {
205  case 'files':
206  case 'file':
207  $storeClass = LCStoreCDB::class;
208  break;
209  case 'db':
210  $storeClass = LCStoreDB::class;
211  $storeArg['server'] = $conf['storeServer'] ?? [];
212  break;
213  case 'array':
214  $storeClass = LCStoreStaticArray::class;
215  break;
216  case 'detect':
217  if ( $directory ) {
218  $storeClass = LCStoreCDB::class;
219  } else {
220  $storeClass = LCStoreDB::class;
221  $storeArg['server'] = $conf['storeServer'] ?? [];
222  }
223  break;
224  default:
225  throw new MWException(
226  'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
227  );
228  }
229  }
230 
231  wfDebugLog( 'caches', static::class . ": using store $storeClass" );
232 
233  $this->store = new $storeClass( $storeArg );
234  foreach ( [ 'manualRecache', 'forceRecache' ] as $var ) {
235  if ( isset( $conf[$var] ) ) {
236  $this->$var = $conf[$var];
237  }
238  }
239  }
240 
247  public function isMergeableKey( $key ) {
248  if ( $this->mergeableKeys === null ) {
249  $this->mergeableKeys = array_flip( array_merge(
250  self::$mergeableMapKeys,
251  self::$mergeableListKeys,
252  self::$mergeableAliasListKeys,
253  self::$optionalMergeKeys,
254  self::$magicWordKeys
255  ) );
256  }
257 
258  return isset( $this->mergeableKeys[$key] );
259  }
260 
270  public function getItem( $code, $key ) {
271  if ( !isset( $this->loadedItems[$code][$key] ) ) {
272  $this->loadItem( $code, $key );
273  }
274 
275  if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
276  return $this->shallowFallbacks[$code];
277  }
278 
279  return $this->data[$code][$key];
280  }
281 
289  public function getSubitem( $code, $key, $subkey ) {
290  if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
291  !isset( $this->loadedItems[$code][$key] )
292  ) {
293  $this->loadSubitem( $code, $key, $subkey );
294  }
295 
296  return $this->data[$code][$key][$subkey] ?? null;
297  }
298 
311  public function getSubitemList( $code, $key ) {
312  if ( in_array( $key, self::$splitKeys ) ) {
313  return $this->getSubitem( $code, 'list', $key );
314  } else {
315  $item = $this->getItem( $code, $key );
316  if ( is_array( $item ) ) {
317  return array_keys( $item );
318  } else {
319  return false;
320  }
321  }
322  }
323 
329  protected function loadItem( $code, $key ) {
330  if ( !isset( $this->initialisedLangs[$code] ) ) {
331  $this->initLanguage( $code );
332  }
333 
334  // Check to see if initLanguage() loaded it for us
335  if ( isset( $this->loadedItems[$code][$key] ) ) {
336  return;
337  }
338 
339  if ( isset( $this->shallowFallbacks[$code] ) ) {
340  $this->loadItem( $this->shallowFallbacks[$code], $key );
341 
342  return;
343  }
344 
345  if ( in_array( $key, self::$splitKeys ) ) {
346  $subkeyList = $this->getSubitem( $code, 'list', $key );
347  foreach ( $subkeyList as $subkey ) {
348  if ( isset( $this->data[$code][$key][$subkey] ) ) {
349  continue;
350  }
351  $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey );
352  }
353  } else {
354  $this->data[$code][$key] = $this->store->get( $code, $key );
355  }
356 
357  $this->loadedItems[$code][$key] = true;
358  }
359 
366  protected function loadSubitem( $code, $key, $subkey ) {
367  if ( !in_array( $key, self::$splitKeys ) ) {
368  $this->loadItem( $code, $key );
369 
370  return;
371  }
372 
373  if ( !isset( $this->initialisedLangs[$code] ) ) {
374  $this->initLanguage( $code );
375  }
376 
377  // Check to see if initLanguage() loaded it for us
378  if ( isset( $this->loadedItems[$code][$key] ) ||
379  isset( $this->loadedSubitems[$code][$key][$subkey] )
380  ) {
381  return;
382  }
383 
384  if ( isset( $this->shallowFallbacks[$code] ) ) {
385  $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
386 
387  return;
388  }
389 
390  $value = $this->store->get( $code, "$key:$subkey" );
391  $this->data[$code][$key][$subkey] = $value;
392  $this->loadedSubitems[$code][$key][$subkey] = true;
393  }
394 
402  public function isExpired( $code ) {
403  if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
404  wfDebug( __METHOD__ . "($code): forced reload\n" );
405 
406  return true;
407  }
408 
409  $deps = $this->store->get( $code, 'deps' );
410  $keys = $this->store->get( $code, 'list' );
411  $preload = $this->store->get( $code, 'preload' );
412  // Different keys may expire separately for some stores
413  if ( $deps === null || $keys === null || $preload === null ) {
414  wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" );
415 
416  return true;
417  }
418 
419  foreach ( $deps as $dep ) {
420  // Because we're unserializing stuff from cache, we
421  // could receive objects of classes that don't exist
422  // anymore (e.g. uninstalled extensions)
423  // When this happens, always expire the cache
424  if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
425  wfDebug( __METHOD__ . "($code): cache for $code expired due to " .
426  get_class( $dep ) . "\n" );
427 
428  return true;
429  }
430  }
431 
432  return false;
433  }
434 
440  protected function initLanguage( $code ) {
441  if ( isset( $this->initialisedLangs[$code] ) ) {
442  return;
443  }
444 
445  $this->initialisedLangs[$code] = true;
446 
447  # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
448  if ( !Language::isValidBuiltInCode( $code ) ) {
449  $this->initShallowFallback( $code, 'en' );
450 
451  return;
452  }
453 
454  # Recache the data if necessary
455  if ( !$this->manualRecache && $this->isExpired( $code ) ) {
456  if ( Language::isSupportedLanguage( $code ) ) {
457  $this->recache( $code );
458  } elseif ( $code === 'en' ) {
459  throw new MWException( 'MessagesEn.php is missing.' );
460  } else {
461  $this->initShallowFallback( $code, 'en' );
462  }
463 
464  return;
465  }
466 
467  # Preload some stuff
468  $preload = $this->getItem( $code, 'preload' );
469  if ( $preload === null ) {
470  if ( $this->manualRecache ) {
471  // No Messages*.php file. Do shallow fallback to en.
472  if ( $code === 'en' ) {
473  throw new MWException( 'No localisation cache found for English. ' .
474  'Please run maintenance/rebuildLocalisationCache.php.' );
475  }
476  $this->initShallowFallback( $code, 'en' );
477 
478  return;
479  } else {
480  throw new MWException( 'Invalid or missing localisation cache.' );
481  }
482  }
483  $this->data[$code] = $preload;
484  foreach ( $preload as $key => $item ) {
485  if ( in_array( $key, self::$splitKeys ) ) {
486  foreach ( $item as $subkey => $subitem ) {
487  $this->loadedSubitems[$code][$key][$subkey] = true;
488  }
489  } else {
490  $this->loadedItems[$code][$key] = true;
491  }
492  }
493  }
494 
501  public function initShallowFallback( $primaryCode, $fallbackCode ) {
502  $this->data[$primaryCode] =& $this->data[$fallbackCode];
503  $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
504  $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
505  $this->shallowFallbacks[$primaryCode] = $fallbackCode;
506  }
507 
515  protected function readPHPFile( $_fileName, $_fileType ) {
516  // Disable APC caching
517  Wikimedia\suppressWarnings();
518  $_apcEnabled = ini_set( 'apc.cache_by_default', '0' );
519  Wikimedia\restoreWarnings();
520 
521  include $_fileName;
522 
523  Wikimedia\suppressWarnings();
524  ini_set( 'apc.cache_by_default', $_apcEnabled );
525  Wikimedia\restoreWarnings();
526 
527  $data = [];
528  if ( $_fileType == 'core' || $_fileType == 'extension' ) {
529  foreach ( self::$allKeys as $key ) {
530  // Not all keys are set in language files, so
531  // check they exist first
532  if ( isset( $$key ) ) {
533  $data[$key] = $$key;
534  }
535  }
536  } elseif ( $_fileType == 'aliases' ) {
537  if ( isset( $aliases ) ) {
538  $data['aliases'] = $aliases;
539  }
540  } else {
541  throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
542  }
543 
544  return $data;
545  }
546 
553  public function readJSONFile( $fileName ) {
554  if ( !is_readable( $fileName ) ) {
555  return [];
556  }
557 
558  $json = file_get_contents( $fileName );
559  if ( $json === false ) {
560  return [];
561  }
562 
563  $data = FormatJson::decode( $json, true );
564  if ( $data === null ) {
565  throw new MWException( __METHOD__ . ": Invalid JSON file: $fileName" );
566  }
567 
568  // Remove keys starting with '@', they're reserved for metadata and non-message data
569  foreach ( $data as $key => $unused ) {
570  if ( $key === '' || $key[0] === '@' ) {
571  unset( $data[$key] );
572  }
573  }
574 
575  // The JSON format only supports messages, none of the other variables, so wrap the data
576  return [ 'messages' => $data ];
577  }
578 
585  public function getCompiledPluralRules( $code ) {
586  $rules = $this->getPluralRules( $code );
587  if ( $rules === null ) {
588  return null;
589  }
590  try {
591  $compiledRules = Evaluator::compile( $rules );
592  } catch ( CLDRPluralRuleError $e ) {
593  wfDebugLog( 'l10n', $e->getMessage() );
594 
595  return [];
596  }
597 
598  return $compiledRules;
599  }
600 
608  public function getPluralRules( $code ) {
609  if ( $this->pluralRules === null ) {
610  $this->loadPluralFiles();
611  }
612  return $this->pluralRules[$code] ?? null;
613  }
614 
622  public function getPluralRuleTypes( $code ) {
623  if ( $this->pluralRuleTypes === null ) {
624  $this->loadPluralFiles();
625  }
626  return $this->pluralRuleTypes[$code] ?? null;
627  }
628 
632  protected function loadPluralFiles() {
633  global $IP;
634  $cldrPlural = "$IP/languages/data/plurals.xml";
635  $mwPlural = "$IP/languages/data/plurals-mediawiki.xml";
636  // Load CLDR plural rules
637  $this->loadPluralFile( $cldrPlural );
638  if ( file_exists( $mwPlural ) ) {
639  // Override or extend
640  $this->loadPluralFile( $mwPlural );
641  }
642  }
643 
651  protected function loadPluralFile( $fileName ) {
652  // Use file_get_contents instead of DOMDocument::load (T58439)
653  $xml = file_get_contents( $fileName );
654  if ( !$xml ) {
655  throw new MWException( "Unable to read plurals file $fileName" );
656  }
657  $doc = new DOMDocument;
658  $doc->loadXML( $xml );
659  $rulesets = $doc->getElementsByTagName( "pluralRules" );
660  foreach ( $rulesets as $ruleset ) {
661  $codes = $ruleset->getAttribute( 'locales' );
662  $rules = [];
663  $ruleTypes = [];
664  $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
665  foreach ( $ruleElements as $elt ) {
666  $ruleType = $elt->getAttribute( 'count' );
667  if ( $ruleType === 'other' ) {
668  // Don't record "other" rules, which have an empty condition
669  continue;
670  }
671  $rules[] = $elt->nodeValue;
672  $ruleTypes[] = $ruleType;
673  }
674  foreach ( explode( ' ', $codes ) as $code ) {
675  $this->pluralRules[$code] = $rules;
676  $this->pluralRuleTypes[$code] = $ruleTypes;
677  }
678  }
679  }
680 
690  protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
691  global $IP;
692 
693  // This reads in the PHP i18n file with non-messages l10n data
694  $fileName = Language::getMessagesFileName( $code );
695  if ( !file_exists( $fileName ) ) {
696  $data = [];
697  } else {
698  $deps[] = new FileDependency( $fileName );
699  $data = $this->readPHPFile( $fileName, 'core' );
700  }
701 
702  # Load CLDR plural rules for JavaScript
703  $data['pluralRules'] = $this->getPluralRules( $code );
704  # And for PHP
705  $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
706  # Load plural rule types
707  $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code );
708 
709  $deps['plurals'] = new FileDependency( "$IP/languages/data/plurals.xml" );
710  $deps['plurals-mw'] = new FileDependency( "$IP/languages/data/plurals-mediawiki.xml" );
711 
712  return $data;
713  }
714 
722  protected function mergeItem( $key, &$value, $fallbackValue ) {
723  if ( !is_null( $value ) ) {
724  if ( !is_null( $fallbackValue ) ) {
725  if ( in_array( $key, self::$mergeableMapKeys ) ) {
726  $value = $value + $fallbackValue;
727  } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
728  $value = array_unique( array_merge( $fallbackValue, $value ) );
729  } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
730  $value = array_merge_recursive( $value, $fallbackValue );
731  } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
732  if ( !empty( $value['inherit'] ) ) {
733  $value = array_merge( $fallbackValue, $value );
734  }
735 
736  if ( isset( $value['inherit'] ) ) {
737  unset( $value['inherit'] );
738  }
739  } elseif ( in_array( $key, self::$magicWordKeys ) ) {
740  $this->mergeMagicWords( $value, $fallbackValue );
741  }
742  }
743  } else {
744  $value = $fallbackValue;
745  }
746  }
747 
752  protected function mergeMagicWords( &$value, $fallbackValue ) {
753  foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
754  if ( !isset( $value[$magicName] ) ) {
755  $value[$magicName] = $fallbackInfo;
756  } else {
757  $oldSynonyms = array_slice( $fallbackInfo, 1 );
758  $newSynonyms = array_slice( $value[$magicName], 1 );
759  $synonyms = array_values( array_unique( array_merge(
760  $newSynonyms, $oldSynonyms ) ) );
761  $value[$magicName] = array_merge( [ $fallbackInfo[0] ], $synonyms );
762  }
763  }
764  }
765 
779  protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
780  $used = false;
781  foreach ( $codeSequence as $code ) {
782  if ( isset( $fallbackValue[$code] ) ) {
783  $this->mergeItem( $key, $value, $fallbackValue[$code] );
784  $used = true;
785  }
786  }
787 
788  return $used;
789  }
790 
798  public function getMessagesDirs() {
799  global $IP;
800 
801  $config = MediaWikiServices::getInstance()->getMainConfig();
802  $messagesDirs = $config->get( 'MessagesDirs' );
803  return [
804  'core' => "$IP/languages/i18n",
805  'exif' => "$IP/languages/i18n/exif",
806  'api' => "$IP/includes/api/i18n",
807  'oojs-ui' => "$IP/resources/lib/ooui/i18n",
808  ] + $messagesDirs;
809  }
810 
817  public function recache( $code ) {
819 
820  if ( !$code ) {
821  throw new MWException( "Invalid language code requested" );
822  }
823  $this->recachedLangs[$code] = true;
824 
825  # Initial values
826  $initialData = array_fill_keys( self::$allKeys, null );
827  $coreData = $initialData;
828  $deps = [];
829 
830  # Load the primary localisation from the source file
831  $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
832  if ( $data === false ) {
833  wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
834  $coreData['fallback'] = 'en';
835  } else {
836  wfDebug( __METHOD__ . ": got localisation for $code from source\n" );
837 
838  # Merge primary localisation
839  foreach ( $data as $key => $value ) {
840  $this->mergeItem( $key, $coreData[$key], $value );
841  }
842  }
843 
844  # Fill in the fallback if it's not there already
845  if ( ( is_null( $coreData['fallback'] ) || $coreData['fallback'] === false ) && $code === 'en' ) {
846  $coreData['fallback'] = false;
847  $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'] = [];
848  } else {
849  if ( !is_null( $coreData['fallback'] ) ) {
850  $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
851  } else {
852  $coreData['fallbackSequence'] = [];
853  }
854  $len = count( $coreData['fallbackSequence'] );
855 
856  # Before we add the 'en' fallback for messages, keep a copy of
857  # the original fallback sequence
858  $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'];
859 
860  # Ensure that the sequence ends at 'en' for messages
861  if ( !$len || $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
862  $coreData['fallbackSequence'][] = 'en';
863  }
864  }
865 
866  $codeSequence = array_merge( [ $code ], $coreData['fallbackSequence'] );
867  $messageDirs = $this->getMessagesDirs();
868 
869  # Load non-JSON localisation data for extensions
870  $extensionData = array_fill_keys( $codeSequence, $initialData );
871  foreach ( $wgExtensionMessagesFiles as $extension => $fileName ) {
872  if ( isset( $messageDirs[$extension] ) ) {
873  # This extension has JSON message data; skip the PHP shim
874  continue;
875  }
876 
877  $data = $this->readPHPFile( $fileName, 'extension' );
878  $used = false;
879 
880  foreach ( $data as $key => $item ) {
881  foreach ( $codeSequence as $csCode ) {
882  if ( isset( $item[$csCode] ) ) {
883  $this->mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] );
884  $used = true;
885  }
886  }
887  }
888 
889  if ( $used ) {
890  $deps[] = new FileDependency( $fileName );
891  }
892  }
893 
894  # Load the localisation data for each fallback, then merge it into the full array
895  $allData = $initialData;
896  foreach ( $codeSequence as $csCode ) {
897  $csData = $initialData;
898 
899  # Load core messages and the extension localisations.
900  foreach ( $messageDirs as $dirs ) {
901  foreach ( (array)$dirs as $dir ) {
902  $fileName = "$dir/$csCode.json";
903  $data = $this->readJSONFile( $fileName );
904 
905  foreach ( $data as $key => $item ) {
906  $this->mergeItem( $key, $csData[$key], $item );
907  }
908 
909  $deps[] = new FileDependency( $fileName );
910  }
911  }
912 
913  # Merge non-JSON extension data
914  if ( isset( $extensionData[$csCode] ) ) {
915  foreach ( $extensionData[$csCode] as $key => $item ) {
916  $this->mergeItem( $key, $csData[$key], $item );
917  }
918  }
919 
920  if ( $csCode === $code ) {
921  # Merge core data into extension data
922  foreach ( $coreData as $key => $item ) {
923  $this->mergeItem( $key, $csData[$key], $item );
924  }
925  } else {
926  # Load the secondary localisation from the source file to
927  # avoid infinite cycles on cyclic fallbacks
928  $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps );
929  if ( $fbData !== false ) {
930  # Only merge the keys that make sense to merge
931  foreach ( self::$allKeys as $key ) {
932  if ( !isset( $fbData[$key] ) ) {
933  continue;
934  }
935 
936  if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) {
937  $this->mergeItem( $key, $csData[$key], $fbData[$key] );
938  }
939  }
940  }
941  }
942 
943  # Allow extensions an opportunity to adjust the data for this
944  # fallback
945  Hooks::run( 'LocalisationCacheRecacheFallback', [ $this, $csCode, &$csData ] );
946 
947  # Merge the data for this fallback into the final array
948  if ( $csCode === $code ) {
949  $allData = $csData;
950  } else {
951  foreach ( self::$allKeys as $key ) {
952  if ( !isset( $csData[$key] ) ) {
953  continue;
954  }
955 
956  if ( is_null( $allData[$key] ) || $this->isMergeableKey( $key ) ) {
957  $this->mergeItem( $key, $allData[$key], $csData[$key] );
958  }
959  }
960  }
961  }
962 
963  # Add cache dependencies for any referenced globals
964  $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
965  // The 'MessagesDirs' config setting is used in LocalisationCache::getMessagesDirs().
966  // We use the key 'wgMessagesDirs' for historical reasons.
967  $deps['wgMessagesDirs'] = new MainConfigDependency( 'MessagesDirs' );
968  $deps['version'] = new ConstantDependency( 'LocalisationCache::VERSION' );
969 
970  # Add dependencies to the cache entry
971  $allData['deps'] = $deps;
972 
973  # Replace spaces with underscores in namespace names
974  $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
975 
976  # And do the same for special page aliases. $page is an array.
977  foreach ( $allData['specialPageAliases'] as &$page ) {
978  $page = str_replace( ' ', '_', $page );
979  }
980  # Decouple the reference to prevent accidental damage
981  unset( $page );
982 
983  # If there were no plural rules, return an empty array
984  if ( $allData['pluralRules'] === null ) {
985  $allData['pluralRules'] = [];
986  }
987  if ( $allData['compiledPluralRules'] === null ) {
988  $allData['compiledPluralRules'] = [];
989  }
990  # If there were no plural rule types, return an empty array
991  if ( $allData['pluralRuleTypes'] === null ) {
992  $allData['pluralRuleTypes'] = [];
993  }
994 
995  # Set the list keys
996  $allData['list'] = [];
997  foreach ( self::$splitKeys as $key ) {
998  $allData['list'][$key] = array_keys( $allData[$key] );
999  }
1000  # Run hooks
1001  $purgeBlobs = true;
1002  Hooks::run( 'LocalisationCacheRecache', [ $this, $code, &$allData, &$purgeBlobs ] );
1003 
1004  if ( is_null( $allData['namespaceNames'] ) ) {
1005  throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
1006  'Check that your languages/messages/MessagesEn.php file is intact.' );
1007  }
1008 
1009  # Set the preload key
1010  $allData['preload'] = $this->buildPreload( $allData );
1011 
1012  # Save to the process cache and register the items loaded
1013  $this->data[$code] = $allData;
1014  foreach ( $allData as $key => $item ) {
1015  $this->loadedItems[$code][$key] = true;
1016  }
1017 
1018  # Save to the persistent cache
1019  $this->store->startWrite( $code );
1020  foreach ( $allData as $key => $value ) {
1021  if ( in_array( $key, self::$splitKeys ) ) {
1022  foreach ( $value as $subkey => $subvalue ) {
1023  $this->store->set( "$key:$subkey", $subvalue );
1024  }
1025  } else {
1026  $this->store->set( $key, $value );
1027  }
1028  }
1029  $this->store->finishWrite();
1030 
1031  # Clear out the MessageBlobStore
1032  # HACK: If using a null (i.e. disabled) storage backend, we
1033  # can't write to the MessageBlobStore either
1034  if ( $purgeBlobs && !$this->store instanceof LCStoreNull ) {
1035  $blobStore = MediaWikiServices::getInstance()->getResourceLoader()->getMessageBlobStore();
1036  $blobStore->clear();
1037  }
1038  }
1039 
1048  protected function buildPreload( $data ) {
1049  $preload = [ 'messages' => [] ];
1050  foreach ( self::$preloadedKeys as $key ) {
1051  $preload[$key] = $data[$key];
1052  }
1053 
1054  foreach ( $data['preloadedMessages'] as $subkey ) {
1055  $subitem = $data['messages'][$subkey] ?? null;
1056  $preload['messages'][$subkey] = $subitem;
1057  }
1058 
1059  return $preload;
1060  }
1061 
1067  public function unload( $code ) {
1068  unset( $this->data[$code] );
1069  unset( $this->loadedItems[$code] );
1070  unset( $this->loadedSubitems[$code] );
1071  unset( $this->initialisedLangs[$code] );
1072  unset( $this->shallowFallbacks[$code] );
1073 
1074  foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
1075  if ( $fbCode === $code ) {
1076  $this->unload( $shallowCode );
1077  }
1078  }
1079  }
1080 
1084  public function unloadAll() {
1085  foreach ( $this->initialisedLangs as $lang => $unused ) {
1086  $this->unload( $lang );
1087  }
1088  }
1089 
1093  public function disableBackend() {
1094  $this->store = new LCStoreNull;
1095  $this->manualRecache = false;
1096  }
1097 
1098 }
unloadAll()
Unload all data.
static $mergeableAliasListKeys
Keys for items which contain an array of arrays of equivalent aliases for each subitem.
magicword txt Magic Words are some phrases used in the wikitext They are used for two that looks like templates but that don t accept any parameter *Parser functions(like {{fullurl:...}}, {{#special:...}}) $wgExtensionMessagesFiles['ExtensionNameMagic']
Definition: magicword.txt:43
and how to run hooks for an and one after Each event has a preferably in CamelCase For ArticleDelete hook A clump of code and data that should be run when an event happens This can be either a function and a chunk of data
Definition: hooks.txt:6
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
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2159
unload( $code)
Unload the data for a given language from the object cache.
LCStore $store
The persistent store object.
if(!isset( $args[0])) $lang
$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.
$value
$conf
Configuration associative array.
static $allKeys
All item keys.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
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().
$wgCacheDirectory
Directory for caching data in the local filesystem.
disableBackend()
Disable the storage backend.
static isValidBuiltInCode( $code)
Returns true if a language code is of a valid form for the purposes of internal customisation of Medi...
Definition: Language.php:413
static $optionalMergeKeys
Keys for items which contain an associative array, and may be merged if the primary value contains th...
$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...
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:174
$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...
$forceRecache
True to treat all files as expired until they are regenerated by this object.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
Class for caching the contents of localisation files, Messages*.php and *.i18n.php.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition: hooks.txt:780
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
$data
The cache data.
getPluralRules( $code)
Get the plural rules for a given language from the XML files.
static getMessagesFileName( $code)
Definition: Language.php:4466
initShallowFallback( $primaryCode, $fallbackCode)
Create a fallback from one language to another, without creating a complete persistent cache...
static $mergeableListKeys
Keys for items which are a numbered array.
loadPluralFiles()
Load the plural XML files.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
static isSupportedLanguage( $code)
Checks whether any localisation is available for that language tag in MediaWiki (MessagesXx.php exists).
Definition: Language.php:306
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.
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
$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.
__construct( $conf)
For constructor parameters, see the documentation in DefaultSettings.php for $wgLocalisationCacheConf...
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...
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might include
Definition: hooks.txt:780
isExpired( $code)
Returns true if the cache identified by $code is missing or expired.