MediaWiki REL1_39
LocalisationCache.php
Go to the documentation of this file.
1<?php
21use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
22use CLDRPluralRuleParser\Evaluator;
28use Psr\Log\LoggerInterface;
29
47 public const VERSION = 5;
48
50 private $options;
51
57 private $manualRecache;
58
65 protected $data = [];
66
70 protected $sourceLanguage = [];
71
77 private $store;
78
82 private $logger;
83
85 private $hookRunner;
86
88 private $clearStoreCallbacks;
89
91 private $langNameUtils;
92
101 private $loadedItems = [];
102
107 private $loadedSubitems = [];
108
114 private $initialisedLangs = [];
115
121 private $shallowFallbacks = [];
122
126 private $recachedLangs = [];
127
131 public static $allKeys = [
132 'fallback', 'namespaceNames', 'bookstoreList',
133 'magicWords', 'messages', 'rtl',
134 'digitTransformTable', 'separatorTransformTable',
135 'minimumGroupingDigits', 'fallback8bitEncoding',
136 'linkPrefixExtension', 'linkTrail', 'linkPrefixCharset',
137 'namespaceAliases', 'dateFormats', 'datePreferences',
138 'datePreferenceMigrationMap', 'defaultDateFormat',
139 'specialPageAliases', 'imageFiles', 'preloadedMessages',
140 'namespaceGenderAliases', 'digitGroupingPattern', 'pluralRules',
141 'pluralRuleTypes', 'compiledPluralRules',
142 ];
143
148 public static $mergeableMapKeys = [ 'messages', 'namespaceNames',
149 'namespaceAliases', 'dateFormats', 'imageFiles', 'preloadedMessages'
150 ];
151
155 public static $mergeableListKeys = [];
156
161 public static $mergeableAliasListKeys = [ 'specialPageAliases' ];
162
168 public static $optionalMergeKeys = [ 'bookstoreList' ];
169
173 public static $magicWordKeys = [ 'magicWords' ];
174
178 public static $splitKeys = [ 'messages' ];
179
184 public static $sourcePrefixKeys = [ 'messages' ];
185
189 protected const SOURCEPREFIX_SEPARATOR = ':';
190
194 public static $preloadedKeys = [ 'dateFormats', 'namespaceNames' ];
195
200 private $pluralRules = null;
201
214 private $pluralRuleTypes = null;
215
216 private $mergeableKeys = null;
217
226 public static function getStoreFromConf( array $conf, $fallbackCacheDir ): LCStore {
227 $storeArg = [];
228 $storeArg['directory'] =
229 $conf['storeDirectory'] ?: $fallbackCacheDir;
230
231 if ( !empty( $conf['storeClass'] ) ) {
232 $storeClass = $conf['storeClass'];
233 } elseif ( $conf['store'] === 'files' || $conf['store'] === 'file' ||
234 ( $conf['store'] === 'detect' && $storeArg['directory'] )
235 ) {
236 $storeClass = LCStoreCDB::class;
237 } elseif ( $conf['store'] === 'db' || $conf['store'] === 'detect' ) {
238 $storeClass = LCStoreDB::class;
239 $storeArg['server'] = $conf['storeServer'] ?? [];
240 } elseif ( $conf['store'] === 'array' ) {
241 $storeClass = LCStoreStaticArray::class;
242 } else {
243 throw new MWException(
244 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
245 );
246 }
247
248 return new $storeClass( $storeArg );
249 }
250
254 public const CONSTRUCTOR_OPTIONS = [
255 // True to treat all files as expired until they are regenerated by this object.
256 'forceRecache',
257 'manualRecache',
258 MainConfigNames::ExtensionMessagesFiles,
259 MainConfigNames::MessagesDirs,
260 ];
261
278 public function __construct(
279 ServiceOptions $options,
280 LCStore $store,
281 LoggerInterface $logger,
282 array $clearStoreCallbacks,
283 LanguageNameUtils $langNameUtils,
284 HookContainer $hookContainer
285 ) {
286 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
287
288 $this->options = $options;
289 $this->store = $store;
290 $this->logger = $logger;
291 $this->clearStoreCallbacks = $clearStoreCallbacks;
292 $this->langNameUtils = $langNameUtils;
293 $this->hookRunner = new HookRunner( $hookContainer );
294
295 // Keep this separate from $this->options so it can be mutable
296 $this->manualRecache = $options->get( 'manualRecache' );
297 }
298
305 public function isMergeableKey( $key ) {
306 if ( $this->mergeableKeys === null ) {
307 $this->mergeableKeys = array_fill_keys( array_merge(
308 self::$mergeableMapKeys,
309 self::$mergeableListKeys,
310 self::$mergeableAliasListKeys,
311 self::$optionalMergeKeys,
312 self::$magicWordKeys
313 ), true );
314 }
315
316 return isset( $this->mergeableKeys[$key] );
317 }
318
328 public function getItem( $code, $key ) {
329 if ( !isset( $this->loadedItems[$code][$key] ) ) {
330 $this->loadItem( $code, $key );
331 }
332
333 if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
334 return $this->shallowFallbacks[$code];
335 }
336
337 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
338 return $this->data[$code][$key];
339 }
340
348 public function getSubitem( $code, $key, $subkey ) {
349 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
350 !isset( $this->loadedItems[$code][$key] )
351 ) {
352 $this->loadSubitem( $code, $key, $subkey );
353 }
354
355 return $this->data[$code][$key][$subkey] ?? null;
356 }
357
367 public function getSubitemWithSource( $code, $key, $subkey ) {
368 $subitem = $this->getSubitem( $code, $key, $subkey );
369 // Undefined in the backend.
370 if ( $subitem === null ) {
371 return null;
372 }
373
374 // The source language should have been set, but to avoid Phan error and be double sure.
375 return [ $subitem, $this->sourceLanguage[$code][$key][$subkey] ?? $code ];
376 }
377
390 public function getSubitemList( $code, $key ) {
391 if ( in_array( $key, self::$splitKeys ) ) {
392 return $this->getSubitem( $code, 'list', $key );
393 } else {
394 $item = $this->getItem( $code, $key );
395 if ( is_array( $item ) ) {
396 return array_keys( $item );
397 } else {
398 return false;
399 }
400 }
401 }
402
408 protected function loadItem( $code, $key ) {
409 if ( !isset( $this->initialisedLangs[$code] ) ) {
410 $this->initLanguage( $code );
411 }
412
413 // Check to see if initLanguage() loaded it for us
414 if ( isset( $this->loadedItems[$code][$key] ) ) {
415 return;
416 }
417
418 if ( isset( $this->shallowFallbacks[$code] ) ) {
419 $this->loadItem( $this->shallowFallbacks[$code], $key );
420
421 return;
422 }
423
424 if ( in_array( $key, self::$splitKeys ) ) {
425 $subkeyList = $this->getSubitem( $code, 'list', $key );
426 foreach ( $subkeyList as $subkey ) {
427 if ( isset( $this->data[$code][$key][$subkey] ) ) {
428 continue;
429 }
430 $this->loadSubitem( $code, $key, $subkey );
431 }
432 } else {
433 $this->data[$code][$key] = $this->store->get( $code, $key );
434 }
435
436 $this->loadedItems[$code][$key] = true;
437 }
438
445 protected function loadSubitem( $code, $key, $subkey ) {
446 if ( !in_array( $key, self::$splitKeys ) ) {
447 $this->loadItem( $code, $key );
448
449 return;
450 }
451
452 if ( !isset( $this->initialisedLangs[$code] ) ) {
453 $this->initLanguage( $code );
454 }
455
456 // Check to see if initLanguage() loaded it for us
457 if ( isset( $this->loadedItems[$code][$key] ) ||
458 isset( $this->loadedSubitems[$code][$key][$subkey] )
459 ) {
460 return;
461 }
462
463 if ( isset( $this->shallowFallbacks[$code] ) ) {
464 $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
465
466 return;
467 }
468
469 $value = $this->store->get( $code, "$key:$subkey" );
470 if ( $value !== null && in_array( $key, self::$sourcePrefixKeys ) ) {
471 [
472 $this->sourceLanguage[$code][$key][$subkey],
473 $this->data[$code][$key][$subkey]
474 ] = explode( self::SOURCEPREFIX_SEPARATOR, $value, 2 );
475 } else {
476 $this->data[$code][$key][$subkey] = $value;
477 }
478
479 $this->loadedSubitems[$code][$key][$subkey] = true;
480 }
481
489 public function isExpired( $code ) {
490 if ( $this->options->get( 'forceRecache' ) && !isset( $this->recachedLangs[$code] ) ) {
491 $this->logger->debug( __METHOD__ . "($code): forced reload" );
492
493 return true;
494 }
495
496 $deps = $this->store->get( $code, 'deps' );
497 $keys = $this->store->get( $code, 'list' );
498 $preload = $this->store->get( $code, 'preload' );
499 // Different keys may expire separately for some stores
500 if ( $deps === null || $keys === null || $preload === null ) {
501 $this->logger->debug( __METHOD__ . "($code): cache missing, need to make one" );
502
503 return true;
504 }
505
506 foreach ( $deps as $dep ) {
507 // Because we're unserializing stuff from cache, we
508 // could receive objects of classes that don't exist
509 // anymore (e.g. uninstalled extensions)
510 // When this happens, always expire the cache
511 if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
512 $this->logger->debug( __METHOD__ . "($code): cache for $code expired due to " .
513 get_class( $dep ) );
514
515 return true;
516 }
517 }
518
519 return false;
520 }
521
527 protected function initLanguage( $code ) {
528 if ( isset( $this->initialisedLangs[$code] ) ) {
529 return;
530 }
531
532 $this->initialisedLangs[$code] = true;
533
534 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
535 if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
536 $this->initShallowFallback( $code, 'en' );
537
538 return;
539 }
540
541 # Recache the data if necessary
542 if ( !$this->manualRecache && $this->isExpired( $code ) ) {
543 if ( $this->langNameUtils->isSupportedLanguage( $code ) ) {
544 $this->recache( $code );
545 } elseif ( $code === 'en' ) {
546 throw new MWException( 'MessagesEn.php is missing.' );
547 } else {
548 $this->initShallowFallback( $code, 'en' );
549 }
550
551 return;
552 }
553
554 # Preload some stuff
555 $preload = $this->getItem( $code, 'preload' );
556 if ( $preload === null ) {
557 if ( $this->manualRecache ) {
558 // No Messages*.php file. Do shallow fallback to en.
559 if ( $code === 'en' ) {
560 throw new MWException( 'No localisation cache found for English. ' .
561 'Please run maintenance/rebuildLocalisationCache.php.' );
562 }
563 $this->initShallowFallback( $code, 'en' );
564
565 return;
566 } else {
567 throw new MWException( 'Invalid or missing localisation cache.' );
568 }
569 }
570
571 foreach ( self::$sourcePrefixKeys as $key ) {
572 if ( !isset( $preload[$key] ) ) {
573 continue;
574 }
575 foreach ( $preload[$key] as $subkey => $value ) {
576 if ( $value !== null ) {
577 [
578 $this->sourceLanguage[$code][$key][$subkey],
579 $preload[$key][$subkey]
580 ] = explode( self::SOURCEPREFIX_SEPARATOR, $value, 2 );
581 } else {
582 $preload[$key][$subkey] = null;
583 }
584 }
585 }
586
587 $this->data[$code] = $preload;
588 foreach ( $preload as $key => $item ) {
589 if ( in_array( $key, self::$splitKeys ) ) {
590 foreach ( $item as $subkey => $subitem ) {
591 $this->loadedSubitems[$code][$key][$subkey] = true;
592 }
593 } else {
594 $this->loadedItems[$code][$key] = true;
595 }
596 }
597 }
598
605 public function initShallowFallback( $primaryCode, $fallbackCode ) {
606 $this->data[$primaryCode] =& $this->data[$fallbackCode];
607 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
608 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
609 $this->shallowFallbacks[$primaryCode] = $fallbackCode;
610 }
611
619 protected function readPHPFile( $_fileName, $_fileType ) {
620 include $_fileName;
621
622 $data = [];
623 if ( $_fileType == 'core' || $_fileType == 'extension' ) {
624 foreach ( self::$allKeys as $key ) {
625 // Not all keys are set in language files, so
626 // check they exist first
627 if ( isset( $$key ) ) {
628 $data[$key] = $$key;
629 }
630 }
631 } elseif ( $_fileType == 'aliases' ) {
632 // @phan-suppress-next-line PhanImpossibleCondition May be set in included file
633 if ( isset( $aliases ) ) {
634 $data['aliases'] = $aliases;
635 }
636 } else {
637 throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
638 }
639
640 return $data;
641 }
642
649 public function readJSONFile( $fileName ) {
650 if ( !is_readable( $fileName ) ) {
651 return [];
652 }
653
654 $json = file_get_contents( $fileName );
655 if ( $json === false ) {
656 return [];
657 }
658
659 $data = FormatJson::decode( $json, true );
660 if ( $data === null ) {
661 throw new MWException( __METHOD__ . ": Invalid JSON file: $fileName" );
662 }
663
664 // Remove keys starting with '@', they're reserved for metadata and non-message data
665 foreach ( $data as $key => $unused ) {
666 if ( $key === '' || $key[0] === '@' ) {
667 unset( $data[$key] );
668 }
669 }
670
671 return $data;
672 }
673
680 public function getCompiledPluralRules( $code ) {
681 $rules = $this->getPluralRules( $code );
682 if ( $rules === null ) {
683 return null;
684 }
685 try {
686 $compiledRules = Evaluator::compile( $rules );
687 } catch ( CLDRPluralRuleError $e ) {
688 $this->logger->debug( $e->getMessage() );
689
690 return [];
691 }
692
693 return $compiledRules;
694 }
695
703 public function getPluralRules( $code ) {
704 if ( $this->pluralRules === null ) {
705 $this->loadPluralFiles();
706 }
707 return $this->pluralRules[$code] ?? null;
708 }
709
717 public function getPluralRuleTypes( $code ) {
718 if ( $this->pluralRuleTypes === null ) {
719 $this->loadPluralFiles();
720 }
721 return $this->pluralRuleTypes[$code] ?? null;
722 }
723
727 protected function loadPluralFiles() {
728 foreach ( $this->getPluralFiles() as $fileName ) {
729 $this->loadPluralFile( $fileName );
730 }
731 }
732
733 private function getPluralFiles(): array {
734 global $IP;
735 return [
736 // Load CLDR plural rules
737 "$IP/languages/data/plurals.xml",
738 // Override or extend with MW-specific rules
739 "$IP/languages/data/plurals-mediawiki.xml",
740 ];
741 }
742
750 protected function loadPluralFile( $fileName ) {
751 // Use file_get_contents instead of DOMDocument::load (T58439)
752 $xml = file_get_contents( $fileName );
753 if ( !$xml ) {
754 throw new MWException( "Unable to read plurals file $fileName" );
755 }
756 $doc = new DOMDocument;
757 $doc->loadXML( $xml );
758 $rulesets = $doc->getElementsByTagName( "pluralRules" );
759 foreach ( $rulesets as $ruleset ) {
760 $codes = $ruleset->getAttribute( 'locales' );
761 $rules = [];
762 $ruleTypes = [];
763 $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
764 foreach ( $ruleElements as $elt ) {
765 $ruleType = $elt->getAttribute( 'count' );
766 if ( $ruleType === 'other' ) {
767 // Don't record "other" rules, which have an empty condition
768 continue;
769 }
770 $rules[] = $elt->nodeValue;
771 $ruleTypes[] = $ruleType;
772 }
773 foreach ( explode( ' ', $codes ) as $code ) {
774 $this->pluralRules[$code] = $rules;
775 $this->pluralRuleTypes[$code] = $ruleTypes;
776 }
777 }
778 }
779
789 protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
790 // This reads in the PHP i18n file with non-messages l10n data
791 $fileName = $this->langNameUtils->getMessagesFileName( $code );
792 if ( !is_file( $fileName ) ) {
793 $data = [];
794 } else {
795 $deps[] = new FileDependency( $fileName );
796 $data = $this->readPHPFile( $fileName, 'core' );
797 }
798
799 // Load CLDR plural rules for JavaScript
800 $data['pluralRules'] = $this->getPluralRules( $code );
801 // And for PHP
802 $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
803 // Load plural rule types
804 $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code );
805
806 foreach ( $this->getPluralFiles() as $fileName ) {
807 $deps[] = new FileDependency( $fileName );
808 }
809
810 return $data;
811 }
812
820 protected function mergeItem( $key, &$value, $fallbackValue ) {
821 if ( $value !== null ) {
822 if ( $fallbackValue !== null ) {
823 if ( in_array( $key, self::$mergeableMapKeys ) ) {
824 $value += $fallbackValue;
825 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
826 $value = array_unique( array_merge( $fallbackValue, $value ) );
827 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
828 $value = array_merge_recursive( $value, $fallbackValue );
829 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
830 if ( !empty( $value['inherit'] ) ) {
831 $value = array_merge( $fallbackValue, $value );
832 }
833
834 unset( $value['inherit'] );
835 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
836 $this->mergeMagicWords( $value, $fallbackValue );
837 }
838 }
839 } else {
840 $value = $fallbackValue;
841 }
842 }
843
848 protected function mergeMagicWords( &$value, $fallbackValue ) {
849 foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
850 if ( !isset( $value[$magicName] ) ) {
851 $value[$magicName] = $fallbackInfo;
852 } else {
853 $oldSynonyms = array_slice( $fallbackInfo, 1 );
854 $newSynonyms = array_slice( $value[$magicName], 1 );
855 $synonyms = array_values( array_unique( array_merge(
856 $newSynonyms, $oldSynonyms ) ) );
857 $value[$magicName] = array_merge( [ $fallbackInfo[0] ], $synonyms );
858 }
859 }
860 }
861
875 protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
876 $used = false;
877 foreach ( $codeSequence as $code ) {
878 if ( isset( $fallbackValue[$code] ) ) {
879 $this->mergeItem( $key, $value, $fallbackValue[$code] );
880 $used = true;
881 }
882 }
883
884 return $used;
885 }
886
894 public function getMessagesDirs() {
895 global $IP;
896
897 return [
898 'core' => "$IP/languages/i18n",
899 'exif' => "$IP/languages/i18n/exif",
900 'api' => "$IP/includes/api/i18n",
901 'rest' => "$IP/includes/Rest/i18n",
902 'oojs-ui' => "$IP/resources/lib/ooui/i18n",
903 'paramvalidator' => "$IP/includes/libs/ParamValidator/i18n",
904 ] + $this->options->get( MainConfigNames::MessagesDirs );
905 }
906
913 public function recache( $code ) {
914 if ( !$code ) {
915 throw new MWException( "Invalid language code requested" );
916 }
917 $this->recachedLangs[ $code ] = true;
918
919 # Initial values
920 $initialData = array_fill_keys( self::$allKeys, null );
921 $coreData = $initialData;
922 $deps = [];
923
924 # Load the primary localisation from the source file
925 $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
926 $this->logger->debug( __METHOD__ . ": got localisation for $code from source" );
927
928 # Merge primary localisation
929 foreach ( $data as $key => $value ) {
930 $this->mergeItem( $key, $coreData[ $key ], $value );
931 }
932
933 # Fill in the fallback if it's not there already
934 // @phan-suppress-next-line PhanSuspiciousValueComparison
935 if ( ( $coreData['fallback'] === null || $coreData['fallback'] === false ) && $code === 'en' ) {
936 $coreData['fallback'] = false;
937 $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'] = [];
938 } else {
939 if ( $coreData['fallback'] !== null ) {
940 $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
941 } else {
942 $coreData['fallbackSequence'] = [];
943 }
944 $len = count( $coreData['fallbackSequence'] );
945
946 # Before we add the 'en' fallback for messages, keep a copy of
947 # the original fallback sequence
948 $coreData['originalFallbackSequence'] = $coreData['fallbackSequence'];
949
950 # Ensure that the sequence ends at 'en' for messages
951 if ( !$len || $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
952 $coreData['fallbackSequence'][] = 'en';
953 }
954 }
955
956 $codeSequence = array_merge( [ $code ], $coreData['fallbackSequence'] );
957 $messageDirs = $this->getMessagesDirs();
958
959 # Load non-JSON localisation data for extensions
960 $extensionData = array_fill_keys( $codeSequence, $initialData );
961 foreach ( $this->options->get( MainConfigNames::ExtensionMessagesFiles ) as $extension => $fileName ) {
962 if ( isset( $messageDirs[$extension] ) ) {
963 # This extension has JSON message data; skip the PHP shim
964 continue;
965 }
966
967 $data = $this->readPHPFile( $fileName, 'extension' );
968 $used = false;
969
970 foreach ( $data as $key => $item ) {
971 foreach ( $codeSequence as $csCode ) {
972 if ( isset( $item[$csCode] ) ) {
973 // Keep the behaviour the same as for json messages.
974 // TODO: Consider deprecating using a PHP file for messages.
975 if ( in_array( $key, self::$sourcePrefixKeys ) ) {
976 foreach ( $item[$csCode] as $subkey => $_ ) {
977 $this->sourceLanguage[$code][$key][$subkey] ??= $csCode;
978 }
979 }
980 $this->mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] );
981 $used = true;
982 }
983 }
984 }
985
986 if ( $used ) {
987 $deps[] = new FileDependency( $fileName );
988 }
989 }
990
991 # Load the localisation data for each fallback, then merge it into the full array
992 $allData = $initialData;
993 foreach ( $codeSequence as $csCode ) {
994 $csData = $initialData;
995
996 # Load core messages and the extension localisations.
997 foreach ( $messageDirs as $dirs ) {
998 foreach ( (array)$dirs as $dir ) {
999 $fileName = "$dir/$csCode.json";
1000 $messages = $this->readJSONFile( $fileName );
1001
1002 foreach ( $messages as $subkey => $_ ) {
1003 $this->sourceLanguage[$code]['messages'][$subkey] ??= $csCode;
1004 }
1005 $this->mergeItem( 'messages', $csData['messages'], $messages );
1006
1007 $deps[] = new FileDependency( $fileName );
1008 }
1009 }
1010
1011 # Merge non-JSON extension data
1012 if ( isset( $extensionData[$csCode] ) ) {
1013 foreach ( $extensionData[$csCode] as $key => $item ) {
1014 $this->mergeItem( $key, $csData[$key], $item );
1015 }
1016 }
1017
1018 if ( $csCode === $code ) {
1019 # Merge core data into extension data
1020 foreach ( $coreData as $key => $item ) {
1021 $this->mergeItem( $key, $csData[$key], $item );
1022 }
1023 } else {
1024 # Load the secondary localisation from the source file to
1025 # avoid infinite cycles on cyclic fallbacks
1026 $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps );
1027 # Only merge the keys that make sense to merge
1028 foreach ( self::$allKeys as $key ) {
1029 if ( !isset( $fbData[ $key ] ) ) {
1030 continue;
1031 }
1032
1033 if ( ( $coreData[ $key ] ) === null || $this->isMergeableKey( $key ) ) {
1034 $this->mergeItem( $key, $csData[ $key ], $fbData[ $key ] );
1035 }
1036 }
1037 }
1038
1039 # Allow extensions an opportunity to adjust the data for this
1040 # fallback
1041 $this->hookRunner->onLocalisationCacheRecacheFallback( $this, $csCode, $csData );
1042
1043 # Merge the data for this fallback into the final array
1044 if ( $csCode === $code ) {
1045 $allData = $csData;
1046 } else {
1047 foreach ( self::$allKeys as $key ) {
1048 if ( !isset( $csData[$key] ) ) {
1049 continue;
1050 }
1051
1052 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
1053 if ( $allData[$key] === null || $this->isMergeableKey( $key ) ) {
1054 $this->mergeItem( $key, $allData[$key], $csData[$key] );
1055 }
1056 }
1057 }
1058 }
1059
1060 if ( !isset( $allData['rtl'] ) ) {
1061 throw new MWException( __METHOD__ . ': Localisation data failed validation check! ' .
1062 'Check that your languages/messages/MessagesEn.php file is intact.' );
1063 }
1064
1065 # Add cache dependencies for any referenced globals
1066 $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
1067 // The 'MessagesDirs' config setting is used in LocalisationCache::getMessagesDirs().
1068 // We use the key 'wgMessagesDirs' for historical reasons.
1069 $deps['wgMessagesDirs'] = new MainConfigDependency( MainConfigNames::MessagesDirs );
1070 $deps['version'] = new ConstantDependency( 'LocalisationCache::VERSION' );
1071
1072 # Add dependencies to the cache entry
1073 $allData['deps'] = $deps;
1074
1075 # Replace spaces with underscores in namespace names
1076 $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
1077
1078 # And do the same for special page aliases. $page is an array.
1079 foreach ( $allData['specialPageAliases'] as &$page ) {
1080 $page = str_replace( ' ', '_', $page );
1081 }
1082 # Decouple the reference to prevent accidental damage
1083 unset( $page );
1084
1085 # If there were no plural rules, return an empty array
1086 if ( $allData['pluralRules'] === null ) {
1087 $allData['pluralRules'] = [];
1088 }
1089 if ( $allData['compiledPluralRules'] === null ) {
1090 $allData['compiledPluralRules'] = [];
1091 }
1092 # If there were no plural rule types, return an empty array
1093 if ( $allData['pluralRuleTypes'] === null ) {
1094 $allData['pluralRuleTypes'] = [];
1095 }
1096
1097 # Set the list keys
1098 $allData['list'] = [];
1099 foreach ( self::$splitKeys as $key ) {
1100 $allData['list'][$key] = array_keys( $allData[$key] );
1101 }
1102 # Run hooks
1103 $unused = true; // Used to be $purgeBlobs, removed in 1.34
1104 $this->hookRunner->onLocalisationCacheRecache( $this, $code, $allData, $unused );
1105
1106 # Save to the process cache and register the items loaded
1107 $this->data[$code] = $allData;
1108 foreach ( $allData as $key => $item ) {
1109 $this->loadedItems[$code][$key] = true;
1110 }
1111
1112 # Prefix each item with its source language code before save
1113 foreach ( self::$sourcePrefixKeys as $key ) {
1114 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
1115 foreach ( $allData[$key] as $subKey => $value ) {
1116 // The source language should have been set, but to avoid Phan error and be double sure.
1117 $allData[$key][$subKey] = ( $this->sourceLanguage[$code][$key][$subKey] ?? $code ) .
1118 self::SOURCEPREFIX_SEPARATOR . $value;
1119 }
1120 }
1121
1122 # Set the preload key
1123 $allData['preload'] = $this->buildPreload( $allData );
1124
1125 # Save to the persistent cache
1126 $this->store->startWrite( $code );
1127 foreach ( $allData as $key => $value ) {
1128 if ( in_array( $key, self::$splitKeys ) ) {
1129 foreach ( $value as $subkey => $subvalue ) {
1130 $this->store->set( "$key:$subkey", $subvalue );
1131 }
1132 } else {
1133 $this->store->set( $key, $value );
1134 }
1135 }
1136 $this->store->finishWrite();
1137
1138 # Clear out the MessageBlobStore
1139 # HACK: If using a null (i.e. disabled) storage backend, we
1140 # can't write to the MessageBlobStore either
1141 if ( !$this->store instanceof LCStoreNull ) {
1142 foreach ( $this->clearStoreCallbacks as $callback ) {
1143 $callback();
1144 }
1145 }
1146 }
1147
1156 protected function buildPreload( $data ) {
1157 $preload = [ 'messages' => [] ];
1158 foreach ( self::$preloadedKeys as $key ) {
1159 $preload[$key] = $data[$key];
1160 }
1161
1162 foreach ( $data['preloadedMessages'] as $subkey ) {
1163 $subitem = $data['messages'][$subkey] ?? null;
1164 $preload['messages'][$subkey] = $subitem;
1165 }
1166
1167 return $preload;
1168 }
1169
1175 public function unload( $code ) {
1176 unset( $this->data[$code] );
1177 unset( $this->loadedItems[$code] );
1178 unset( $this->loadedSubitems[$code] );
1179 unset( $this->initialisedLangs[$code] );
1180 unset( $this->shallowFallbacks[$code] );
1181 unset( $this->sourceLanguage[$code] );
1182
1183 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
1184 if ( $fbCode === $code ) {
1185 $this->unload( $shallowCode );
1186 }
1187 }
1188 }
1189
1193 public function unloadAll() {
1194 foreach ( $this->initialisedLangs as $lang => $unused ) {
1195 $this->unload( $lang );
1196 }
1197 }
1198
1202 public function disableBackend() {
1203 $this->store = new LCStoreNull;
1204 $this->manualRecache = false;
1205 }
1206}
if(!defined( 'MEDIAWIKI')) if(ini_get('mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition Setup.php:91
Base class to represent dependencies for LocalisationCache entries.
isExpired()
Returns true if the dependency is expired, false otherwise.
Depend on a PHP constant.
Depend on a file.
Depend on a PHP global variable.
Null store backend, used to avoid DB errors during install.
Caching for the contents of localisation files.
static $magicWordKeys
Keys for items that are formatted like $magicWords.
buildPreload( $data)
Build the preload item from the given pre-cache data.
getSubitemWithSource( $code, $key, $subkey)
Get a subitem with its source language.
initLanguage( $code)
Initialise a language in this object.
static $mergeableMapKeys
Keys for items which consist of associative arrays, which may be merged by a fallback sequence.
readSourceFilesAndRegisterDeps( $code, &$deps)
Read the data from the source files for a given language, and register the relevant dependencies in t...
static $sourcePrefixKeys
Keys for items that will be prefixed with its source language code, which should be stripped out when...
isMergeableKey( $key)
Returns true if the given key is mergeable, that is, if it is an associative array which can be merge...
loadPluralFile( $fileName)
Load a plural XML file with the given filename, compile the relevant rules, and save the compiled rul...
getPluralRules( $code)
Get the plural rules for a given language from the XML files.
const SOURCEPREFIX_SEPARATOR
Separator for the source language prefix.
readPHPFile( $_fileName, $_fileType)
Read a PHP file containing localisation data.
readJSONFile( $fileName)
Read a JSON file containing localisation messages.
loadPluralFiles()
Load the plural XML files.
unload( $code)
Unload the data for a given language from the object cache.
unloadAll()
Unload all data.
disableBackend()
Disable the storage backend.
static $mergeableAliasListKeys
Keys for items which contain an array of arrays of equivalent aliases for each subitem.
getSubitemList( $code, $key)
Get the list of subitem keys for a given item.
__construct(ServiceOptions $options, LCStore $store, LoggerInterface $logger, array $clearStoreCallbacks, LanguageNameUtils $langNameUtils, HookContainer $hookContainer)
For constructor parameters, see the documentation for the LocalisationCacheConf setting in docs/Confi...
static $mergeableListKeys
Keys for items which are a numbered array.
recache( $code)
Load localisation data for a given language for both core and extensions and save it to the persisten...
isExpired( $code)
Returns true if the cache identified by $code is missing or expired.
mergeItem( $key, &$value, $fallbackValue)
Merge two localisation values, a primary and a fallback, overwriting the primary value in place.
$sourceLanguage
The source language of cached data items.
getSubitem( $code, $key, $subkey)
Get a subitem, for instance a single message for a given language.
mergeMagicWords(&$value, $fallbackValue)
initShallowFallback( $primaryCode, $fallbackCode)
Create a fallback from one language to another, without creating a complete persistent cache.
static getStoreFromConf(array $conf, $fallbackCacheDir)
Return a suitable LCStore as specified by the given configuration.
static $allKeys
All item keys.
getCompiledPluralRules( $code)
Get the compiled plural rules for a given language from the XML files.
static $optionalMergeKeys
Keys for items which contain an associative array, and may be merged if the primary value contains th...
getMessagesDirs()
Gets the combined list of messages dirs from core and extensions.
static $preloadedKeys
Keys which are loaded automatically by initLanguage()
getItem( $code, $key)
Get a cache item.
static $splitKeys
Keys for items where the subitems are stored in the backend separately.
loadItem( $code, $key)
Load an item into the cache.
getPluralRuleTypes( $code)
Get the plural rule types for a given language from the XML files.
loadSubitem( $code, $key, $subkey)
Load a subitem into the cache.
mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue)
Given an array mapping language code to localisation value, such as is found in extension *....
MediaWiki exception.
Depend on a MW configuration variable.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A service that provides utilities to do with language names and codes.
A class containing constants representing the names of configuration variables.
Interface for the persistence layer of LocalisationCache.
Definition LCStore.php:40
if(!isset( $args[0])) $lang