Add 2 new options to importImages.php
[mediawiki.git] / includes / LocalisationCache.php
blobd8e5d3a368648e9e5851c1cbf4c69339ba2cb11a
1 <?php
2 /**
3 * Cache of the contents of localisation files.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
20 * @file
23 define( 'MW_LC_VERSION', 2 );
25 /**
26 * Class for caching the contents of localisation files, Messages*.php
27 * and *.i18n.php.
29 * An instance of this class is available using Language::getLocalisationCache().
31 * The values retrieved from here are merged, containing items from extension
32 * files, core messages files and the language fallback sequence (e.g. zh-cn ->
33 * zh-hans -> en ). Some common errors are corrected, for example namespace
34 * names with spaces instead of underscores, but heavyweight processing, such
35 * as grammatical transformation, is done by the caller.
37 class LocalisationCache {
38 /** Configuration associative array */
39 var $conf;
41 /**
42 * True if recaching should only be done on an explicit call to recache().
43 * Setting this reduces the overhead of cache freshness checking, which
44 * requires doing a stat() for every extension i18n file.
46 var $manualRecache = false;
48 /**
49 * True to treat all files as expired until they are regenerated by this object.
51 var $forceRecache = false;
53 /**
54 * The cache data. 3-d array, where the first key is the language code,
55 * the second key is the item key e.g. 'messages', and the third key is
56 * an item specific subkey index. Some items are not arrays and so for those
57 * items, there are no subkeys.
59 var $data = array();
61 /**
62 * The persistent store object. An instance of LCStore.
64 * @var LCStore
66 var $store;
68 /**
69 * A 2-d associative array, code/key, where presence indicates that the item
70 * is loaded. Value arbitrary.
72 * For split items, if set, this indicates that all of the subitems have been
73 * loaded.
75 var $loadedItems = array();
77 /**
78 * A 3-d associative array, code/key/subkey, where presence indicates that
79 * the subitem is loaded. Only used for the split items, i.e. messages.
81 var $loadedSubitems = array();
83 /**
84 * An array where presence of a key indicates that that language has been
85 * initialised. Initialisation includes checking for cache expiry and doing
86 * any necessary updates.
88 var $initialisedLangs = array();
90 /**
91 * An array mapping non-existent pseudo-languages to fallback languages. This
92 * is filled by initShallowFallback() when data is requested from a language
93 * that lacks a Messages*.php file.
95 var $shallowFallbacks = array();
97 /**
98 * An array where the keys are codes that have been recached by this instance.
100 var $recachedLangs = array();
103 * All item keys
105 static public $allKeys = array(
106 'fallback', 'namespaceNames', 'bookstoreList',
107 'magicWords', 'messages', 'rtl', 'capitalizeAllNouns', 'digitTransformTable',
108 'separatorTransformTable', 'fallback8bitEncoding', 'linkPrefixExtension',
109 'linkTrail', 'namespaceAliases',
110 'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
111 'defaultDateFormat', 'extraUserToggles', 'specialPageAliases',
112 'imageFiles', 'preloadedMessages', 'namespaceGenderAliases',
113 'digitGroupingPattern', 'pluralRules', 'compiledPluralRules',
117 * Keys for items which consist of associative arrays, which may be merged
118 * by a fallback sequence.
120 static public $mergeableMapKeys = array( 'messages', 'namespaceNames',
121 'dateFormats', 'imageFiles', 'preloadedMessages'
125 * Keys for items which are a numbered array.
127 static public $mergeableListKeys = array( 'extraUserToggles' );
130 * Keys for items which contain an array of arrays of equivalent aliases
131 * for each subitem. The aliases may be merged by a fallback sequence.
133 static public $mergeableAliasListKeys = array( 'specialPageAliases' );
136 * Keys for items which contain an associative array, and may be merged if
137 * the primary value contains the special array key "inherit". That array
138 * key is removed after the first merge.
140 static public $optionalMergeKeys = array( 'bookstoreList' );
143 * Keys for items that are formatted like $magicWords
145 static public $magicWordKeys = array( 'magicWords' );
148 * Keys for items where the subitems are stored in the backend separately.
150 static public $splitKeys = array( 'messages' );
153 * Keys which are loaded automatically by initLanguage()
155 static public $preloadedKeys = array( 'dateFormats', 'namespaceNames' );
158 * Associative array of cached plural rules. The key is the language code,
159 * the value is an array of plural rules for that language.
161 var $pluralRules = null;
163 var $mergeableKeys = null;
166 * Constructor.
167 * For constructor parameters, see the documentation in DefaultSettings.php
168 * for $wgLocalisationCacheConf.
170 * @param $conf Array
172 function __construct( $conf ) {
173 global $wgCacheDirectory;
175 $this->conf = $conf;
176 $storeConf = array();
177 if ( !empty( $conf['storeClass'] ) ) {
178 $storeClass = $conf['storeClass'];
179 } else {
180 switch ( $conf['store'] ) {
181 case 'files':
182 case 'file':
183 $storeClass = 'LCStore_CDB';
184 break;
185 case 'db':
186 $storeClass = 'LCStore_DB';
187 break;
188 case 'accel':
189 $storeClass = 'LCStore_Accel';
190 break;
191 case 'detect':
192 $storeClass = $wgCacheDirectory ? 'LCStore_CDB' : 'LCStore_DB';
193 break;
194 default:
195 throw new MWException(
196 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' );
200 wfDebug( get_class( $this ) . ": using store $storeClass\n" );
201 if ( !empty( $conf['storeDirectory'] ) ) {
202 $storeConf['directory'] = $conf['storeDirectory'];
205 $this->store = new $storeClass( $storeConf );
206 foreach ( array( 'manualRecache', 'forceRecache' ) as $var ) {
207 if ( isset( $conf[$var] ) ) {
208 $this->$var = $conf[$var];
214 * Returns true if the given key is mergeable, that is, if it is an associative
215 * array which can be merged through a fallback sequence.
216 * @param $key
217 * @return bool
219 public function isMergeableKey( $key ) {
220 if ( $this->mergeableKeys === null ) {
221 $this->mergeableKeys = array_flip( array_merge(
222 self::$mergeableMapKeys,
223 self::$mergeableListKeys,
224 self::$mergeableAliasListKeys,
225 self::$optionalMergeKeys,
226 self::$magicWordKeys
227 ) );
229 return isset( $this->mergeableKeys[$key] );
233 * Get a cache item.
235 * Warning: this may be slow for split items (messages), since it will
236 * need to fetch all of the subitems from the cache individually.
237 * @param $code
238 * @param $key
239 * @return mixed
241 public function getItem( $code, $key ) {
242 if ( !isset( $this->loadedItems[$code][$key] ) ) {
243 wfProfileIn( __METHOD__ . '-load' );
244 $this->loadItem( $code, $key );
245 wfProfileOut( __METHOD__ . '-load' );
248 if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
249 return $this->shallowFallbacks[$code];
252 return $this->data[$code][$key];
256 * Get a subitem, for instance a single message for a given language.
257 * @param $code
258 * @param $key
259 * @param $subkey
260 * @return null
262 public function getSubitem( $code, $key, $subkey ) {
263 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
264 !isset( $this->loadedItems[$code][$key] ) ) {
265 wfProfileIn( __METHOD__ . '-load' );
266 $this->loadSubitem( $code, $key, $subkey );
267 wfProfileOut( __METHOD__ . '-load' );
270 if ( isset( $this->data[$code][$key][$subkey] ) ) {
271 return $this->data[$code][$key][$subkey];
272 } else {
273 return null;
278 * Get the list of subitem keys for a given item.
280 * This is faster than array_keys($lc->getItem(...)) for the items listed in
281 * self::$splitKeys.
283 * Will return null if the item is not found, or false if the item is not an
284 * array.
285 * @param $code
286 * @param $key
287 * @return bool|null|string
289 public function getSubitemList( $code, $key ) {
290 if ( in_array( $key, self::$splitKeys ) ) {
291 return $this->getSubitem( $code, 'list', $key );
292 } else {
293 $item = $this->getItem( $code, $key );
294 if ( is_array( $item ) ) {
295 return array_keys( $item );
296 } else {
297 return false;
303 * Load an item into the cache.
304 * @param $code
305 * @param $key
307 protected function loadItem( $code, $key ) {
308 if ( !isset( $this->initialisedLangs[$code] ) ) {
309 $this->initLanguage( $code );
312 // Check to see if initLanguage() loaded it for us
313 if ( isset( $this->loadedItems[$code][$key] ) ) {
314 return;
317 if ( isset( $this->shallowFallbacks[$code] ) ) {
318 $this->loadItem( $this->shallowFallbacks[$code], $key );
319 return;
322 if ( in_array( $key, self::$splitKeys ) ) {
323 $subkeyList = $this->getSubitem( $code, 'list', $key );
324 foreach ( $subkeyList as $subkey ) {
325 if ( isset( $this->data[$code][$key][$subkey] ) ) {
326 continue;
328 $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey );
330 } else {
331 $this->data[$code][$key] = $this->store->get( $code, $key );
334 $this->loadedItems[$code][$key] = true;
338 * Load a subitem into the cache
339 * @param $code
340 * @param $key
341 * @param $subkey
342 * @return
344 protected function loadSubitem( $code, $key, $subkey ) {
345 if ( !in_array( $key, self::$splitKeys ) ) {
346 $this->loadItem( $code, $key );
347 return;
350 if ( !isset( $this->initialisedLangs[$code] ) ) {
351 $this->initLanguage( $code );
354 // Check to see if initLanguage() loaded it for us
355 if ( isset( $this->loadedItems[$code][$key] ) ||
356 isset( $this->loadedSubitems[$code][$key][$subkey] ) ) {
357 return;
360 if ( isset( $this->shallowFallbacks[$code] ) ) {
361 $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
362 return;
365 $value = $this->store->get( $code, "$key:$subkey" );
366 $this->data[$code][$key][$subkey] = $value;
367 $this->loadedSubitems[$code][$key][$subkey] = true;
371 * Returns true if the cache identified by $code is missing or expired.
372 * @return bool
374 public function isExpired( $code ) {
375 if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
376 wfDebug( __METHOD__ . "($code): forced reload\n" );
377 return true;
380 $deps = $this->store->get( $code, 'deps' );
381 $keys = $this->store->get( $code, 'list', 'messages' );
382 $preload = $this->store->get( $code, 'preload' );
383 // Different keys may expire separately, at least in LCStore_Accel
384 if ( $deps === null || $keys === null || $preload === null ) {
385 wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" );
386 return true;
389 foreach ( $deps as $dep ) {
390 // Because we're unserializing stuff from cache, we
391 // could receive objects of classes that don't exist
392 // anymore (e.g. uninstalled extensions)
393 // When this happens, always expire the cache
394 if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
395 wfDebug( __METHOD__ . "($code): cache for $code expired due to " .
396 get_class( $dep ) . "\n" );
397 return true;
401 return false;
405 * Initialise a language in this object. Rebuild the cache if necessary.
406 * @param $code
408 protected function initLanguage( $code ) {
409 if ( isset( $this->initialisedLangs[$code] ) ) {
410 return;
413 $this->initialisedLangs[$code] = true;
415 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
416 if ( !Language::isValidBuiltInCode( $code ) ) {
417 $this->initShallowFallback( $code, 'en' );
418 return;
421 # Recache the data if necessary
422 if ( !$this->manualRecache && $this->isExpired( $code ) ) {
423 if ( file_exists( Language::getMessagesFileName( $code ) ) ) {
424 $this->recache( $code );
425 } elseif ( $code === 'en' ) {
426 throw new MWException( 'MessagesEn.php is missing.' );
427 } else {
428 $this->initShallowFallback( $code, 'en' );
430 return;
433 # Preload some stuff
434 $preload = $this->getItem( $code, 'preload' );
435 if ( $preload === null ) {
436 if ( $this->manualRecache ) {
437 // No Messages*.php file. Do shallow fallback to en.
438 if ( $code === 'en' ) {
439 throw new MWException( 'No localisation cache found for English. ' .
440 'Please run maintenance/rebuildLocalisationCache.php.' );
442 $this->initShallowFallback( $code, 'en' );
443 return;
444 } else {
445 throw new MWException( 'Invalid or missing localisation cache.' );
448 $this->data[$code] = $preload;
449 foreach ( $preload as $key => $item ) {
450 if ( in_array( $key, self::$splitKeys ) ) {
451 foreach ( $item as $subkey => $subitem ) {
452 $this->loadedSubitems[$code][$key][$subkey] = true;
454 } else {
455 $this->loadedItems[$code][$key] = true;
461 * Create a fallback from one language to another, without creating a
462 * complete persistent cache.
463 * @param $primaryCode
464 * @param $fallbackCode
466 public function initShallowFallback( $primaryCode, $fallbackCode ) {
467 $this->data[$primaryCode] =& $this->data[$fallbackCode];
468 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
469 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
470 $this->shallowFallbacks[$primaryCode] = $fallbackCode;
474 * Read a PHP file containing localisation data.
475 * @param $_fileName
476 * @param $_fileType
477 * @return array
479 protected function readPHPFile( $_fileName, $_fileType ) {
480 // Disable APC caching
481 $_apcEnabled = ini_set( 'apc.cache_by_default', '0' );
482 include( $_fileName );
483 ini_set( 'apc.cache_by_default', $_apcEnabled );
485 if ( $_fileType == 'core' || $_fileType == 'extension' ) {
486 $data = compact( self::$allKeys );
487 } elseif ( $_fileType == 'aliases' ) {
488 $data = compact( 'aliases' );
489 } else {
490 throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
492 return $data;
496 * Get the compiled plural rules for a given language from the XML files.
497 * @since 1.20
499 public function getCompiledPluralRules( $code ) {
500 $rules = $this->getPluralRules( $code );
501 if ( $rules === null ) {
502 return null;
504 try {
505 $compiledRules = CLDRPluralRuleEvaluator::compile( $rules );
506 } catch( CLDRPluralRuleError $e ) {
507 wfDebugLog( 'l10n', $e->getMessage() . "\n" );
508 return array();
510 return $compiledRules;
514 * Get the plural rules for a given language from the XML files.
515 * Cached.
516 * @since 1.20
518 public function getPluralRules( $code ) {
519 if ( $this->pluralRules === null ) {
520 $cldrPlural = __DIR__ . "/../languages/data/plurals.xml";
521 $mwPlural = __DIR__ . "/../languages/data/plurals-mediawiki.xml";
522 // Load CLDR plural rules
523 $this->loadPluralFile( $cldrPlural );
524 if ( file_exists( $mwPlural ) ) {
525 // Override or extend
526 $this->loadPluralFile( $mwPlural );
529 if ( !isset( $this->pluralRules[$code] ) ) {
530 return null;
531 } else {
532 return $this->pluralRules[$code];
538 * Load a plural XML file with the given filename, compile the relevant
539 * rules, and save the compiled rules in a process-local cache.
541 protected function loadPluralFile( $fileName ) {
542 $doc = new DOMDocument;
543 $doc->load( $fileName );
544 $rulesets = $doc->getElementsByTagName( "pluralRules" );
545 foreach ( $rulesets as $ruleset ) {
546 $codes = $ruleset->getAttribute( 'locales' );
547 $rules = array();
548 $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
549 foreach ( $ruleElements as $elt ) {
550 $rules[] = $elt->nodeValue;
552 foreach ( explode( ' ', $codes ) as $code ) {
553 $this->pluralRules[$code] = $rules;
559 * Read the data from the source files for a given language, and register
560 * the relevant dependencies in the $deps array. If the localisation
561 * exists, the data array is returned, otherwise false is returned.
563 protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
564 $fileName = Language::getMessagesFileName( $code );
565 if ( !file_exists( $fileName ) ) {
566 return false;
569 $deps[] = new FileDependency( $fileName );
570 $data = $this->readPHPFile( $fileName, 'core' );
572 # Load CLDR plural rules for JavaScript
573 $data['pluralRules'] = $this->getPluralRules( $code );
574 # And for PHP
575 $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
577 $deps['plurals'] = new FileDependency( __DIR__ . "/../languages/data/plurals.xml" );
578 $deps['plurals-mw'] = new FileDependency( __DIR__ . "/../languages/data/plurals-mediawiki.xml" );
579 return $data;
583 * Merge two localisation values, a primary and a fallback, overwriting the
584 * primary value in place.
585 * @param $key
586 * @param $value
587 * @param $fallbackValue
589 protected function mergeItem( $key, &$value, $fallbackValue ) {
590 if ( !is_null( $value ) ) {
591 if ( !is_null( $fallbackValue ) ) {
592 if ( in_array( $key, self::$mergeableMapKeys ) ) {
593 $value = $value + $fallbackValue;
594 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
595 $value = array_unique( array_merge( $fallbackValue, $value ) );
596 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
597 $value = array_merge_recursive( $value, $fallbackValue );
598 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
599 if ( !empty( $value['inherit'] ) ) {
600 $value = array_merge( $fallbackValue, $value );
603 if ( isset( $value['inherit'] ) ) {
604 unset( $value['inherit'] );
606 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
607 $this->mergeMagicWords( $value, $fallbackValue );
610 } else {
611 $value = $fallbackValue;
616 * @param $value
617 * @param $fallbackValue
619 protected function mergeMagicWords( &$value, $fallbackValue ) {
620 foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
621 if ( !isset( $value[$magicName] ) ) {
622 $value[$magicName] = $fallbackInfo;
623 } else {
624 $oldSynonyms = array_slice( $fallbackInfo, 1 );
625 $newSynonyms = array_slice( $value[$magicName], 1 );
626 $synonyms = array_values( array_unique( array_merge(
627 $newSynonyms, $oldSynonyms ) ) );
628 $value[$magicName] = array_merge( array( $fallbackInfo[0] ), $synonyms );
634 * Given an array mapping language code to localisation value, such as is
635 * found in extension *.i18n.php files, iterate through a fallback sequence
636 * to merge the given data with an existing primary value.
638 * Returns true if any data from the extension array was used, false
639 * otherwise.
640 * @param $codeSequence
641 * @param $key
642 * @param $value
643 * @param $fallbackValue
644 * @return bool
646 protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
647 $used = false;
648 foreach ( $codeSequence as $code ) {
649 if ( isset( $fallbackValue[$code] ) ) {
650 $this->mergeItem( $key, $value, $fallbackValue[$code] );
651 $used = true;
655 return $used;
659 * Load localisation data for a given language for both core and extensions
660 * and save it to the persistent cache store and the process cache
661 * @param $code
663 public function recache( $code ) {
664 global $wgExtensionMessagesFiles;
665 wfProfileIn( __METHOD__ );
667 if ( !$code ) {
668 throw new MWException( "Invalid language code requested" );
670 $this->recachedLangs[$code] = true;
672 # Initial values
673 $initialData = array_combine(
674 self::$allKeys,
675 array_fill( 0, count( self::$allKeys ), null ) );
676 $coreData = $initialData;
677 $deps = array();
679 # Load the primary localisation from the source file
680 $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
681 if ( $data === false ) {
682 wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
683 $coreData['fallback'] = 'en';
684 } else {
685 wfDebug( __METHOD__ . ": got localisation for $code from source\n" );
687 # Merge primary localisation
688 foreach ( $data as $key => $value ) {
689 $this->mergeItem( $key, $coreData[$key], $value );
694 # Fill in the fallback if it's not there already
695 if ( is_null( $coreData['fallback'] ) ) {
696 $coreData['fallback'] = $code === 'en' ? false : 'en';
698 if ( $coreData['fallback'] === false ) {
699 $coreData['fallbackSequence'] = array();
700 } else {
701 $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
702 $len = count( $coreData['fallbackSequence'] );
704 # Ensure that the sequence ends at en
705 if ( $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
706 $coreData['fallbackSequence'][] = 'en';
709 # Load the fallback localisation item by item and merge it
710 foreach ( $coreData['fallbackSequence'] as $fbCode ) {
711 # Load the secondary localisation from the source file to
712 # avoid infinite cycles on cyclic fallbacks
713 $fbData = $this->readSourceFilesAndRegisterDeps( $fbCode, $deps );
714 if ( $fbData === false ) {
715 continue;
718 foreach ( self::$allKeys as $key ) {
719 if ( !isset( $fbData[$key] ) ) {
720 continue;
723 if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) {
724 $this->mergeItem( $key, $coreData[$key], $fbData[$key] );
730 $codeSequence = array_merge( array( $code ), $coreData['fallbackSequence'] );
732 # Load the extension localisations
733 # This is done after the core because we know the fallback sequence now.
734 # But it has a higher precedence for merging so that we can support things
735 # like site-specific message overrides.
736 $allData = $initialData;
737 foreach ( $wgExtensionMessagesFiles as $fileName ) {
738 $data = $this->readPHPFile( $fileName, 'extension' );
739 $used = false;
741 foreach ( $data as $key => $item ) {
742 if ( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
743 $used = true;
747 if ( $used ) {
748 $deps[] = new FileDependency( $fileName );
752 # Merge core data into extension data
753 foreach ( $coreData as $key => $item ) {
754 $this->mergeItem( $key, $allData[$key], $item );
757 # Add cache dependencies for any referenced globals
758 $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
759 $deps['version'] = new ConstantDependency( 'MW_LC_VERSION' );
761 # Add dependencies to the cache entry
762 $allData['deps'] = $deps;
764 # Replace spaces with underscores in namespace names
765 $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
767 # And do the same for special page aliases. $page is an array.
768 foreach ( $allData['specialPageAliases'] as &$page ) {
769 $page = str_replace( ' ', '_', $page );
771 # Decouple the reference to prevent accidental damage
772 unset( $page );
774 # If there were no plural rules, return an empty array
775 if ( $allData['pluralRules'] === null ) {
776 $allData['pluralRules'] = array();
778 if ( $allData['compiledPluralRules'] === null ) {
779 $allData['compiledPluralRules'] = array();
782 # Set the list keys
783 $allData['list'] = array();
784 foreach ( self::$splitKeys as $key ) {
785 $allData['list'][$key] = array_keys( $allData[$key] );
787 # Run hooks
788 wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) );
790 if ( is_null( $allData['namespaceNames'] ) ) {
791 throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
792 'Check that your languages/messages/MessagesEn.php file is intact.' );
795 # Set the preload key
796 $allData['preload'] = $this->buildPreload( $allData );
798 # Save to the process cache and register the items loaded
799 $this->data[$code] = $allData;
800 foreach ( $allData as $key => $item ) {
801 $this->loadedItems[$code][$key] = true;
804 # Save to the persistent cache
805 $this->store->startWrite( $code );
806 foreach ( $allData as $key => $value ) {
807 if ( in_array( $key, self::$splitKeys ) ) {
808 foreach ( $value as $subkey => $subvalue ) {
809 $this->store->set( "$key:$subkey", $subvalue );
811 } else {
812 $this->store->set( $key, $value );
815 $this->store->finishWrite();
817 # Clear out the MessageBlobStore
818 # HACK: If using a null (i.e. disabled) storage backend, we
819 # can't write to the MessageBlobStore either
820 if ( !$this->store instanceof LCStore_Null ) {
821 MessageBlobStore::clear();
824 wfProfileOut( __METHOD__ );
828 * Build the preload item from the given pre-cache data.
830 * The preload item will be loaded automatically, improving performance
831 * for the commonly-requested items it contains.
832 * @param $data
833 * @return array
835 protected function buildPreload( $data ) {
836 $preload = array( 'messages' => array() );
837 foreach ( self::$preloadedKeys as $key ) {
838 $preload[$key] = $data[$key];
841 foreach ( $data['preloadedMessages'] as $subkey ) {
842 if ( isset( $data['messages'][$subkey] ) ) {
843 $subitem = $data['messages'][$subkey];
844 } else {
845 $subitem = null;
847 $preload['messages'][$subkey] = $subitem;
850 return $preload;
854 * Unload the data for a given language from the object cache.
855 * Reduces memory usage.
856 * @param $code
858 public function unload( $code ) {
859 unset( $this->data[$code] );
860 unset( $this->loadedItems[$code] );
861 unset( $this->loadedSubitems[$code] );
862 unset( $this->initialisedLangs[$code] );
864 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
865 if ( $fbCode === $code ) {
866 $this->unload( $shallowCode );
872 * Unload all data
874 public function unloadAll() {
875 foreach ( $this->initialisedLangs as $lang => $unused ) {
876 $this->unload( $lang );
881 * Disable the storage backend
883 public function disableBackend() {
884 $this->store = new LCStore_Null;
885 $this->manualRecache = false;
890 * Interface for the persistence layer of LocalisationCache.
892 * The persistence layer is two-level hierarchical cache. The first level
893 * is the language, the second level is the item or subitem.
895 * Since the data for a whole language is rebuilt in one operation, it needs
896 * to have a fast and atomic method for deleting or replacing all of the
897 * current data for a given language. The interface reflects this bulk update
898 * operation. Callers writing to the cache must first call startWrite(), then
899 * will call set() a couple of thousand times, then will call finishWrite()
900 * to commit the operation. When finishWrite() is called, the cache is
901 * expected to delete all data previously stored for that language.
903 * The values stored are PHP variables suitable for serialize(). Implementations
904 * of LCStore are responsible for serializing and unserializing.
906 interface LCStore {
908 * Get a value.
909 * @param $code string Language code
910 * @param $key string Cache key
912 function get( $code, $key );
915 * Start a write transaction.
916 * @param $code Language code
918 function startWrite( $code );
921 * Finish a write transaction.
923 function finishWrite();
926 * Set a key to a given value. startWrite() must be called before this
927 * is called, and finishWrite() must be called afterwards.
928 * @param $key
929 * @param $value
931 function set( $key, $value );
935 * LCStore implementation which uses PHP accelerator to store data.
936 * This will work if one of XCache, WinCache or APC cacher is configured.
937 * (See ObjectCache.php)
939 class LCStore_Accel implements LCStore {
940 var $currentLang;
941 var $keys;
943 public function __construct() {
944 $this->cache = wfGetCache( CACHE_ACCEL );
947 public function get( $code, $key ) {
948 $k = wfMemcKey( 'l10n', $code, 'k', $key );
949 $r = $this->cache->get( $k );
950 return $r === false ? null : $r;
953 public function startWrite( $code ) {
954 $k = wfMemcKey( 'l10n', $code, 'l' );
955 $keys = $this->cache->get( $k );
956 if ( $keys ) {
957 foreach ( $keys as $k ) {
958 $this->cache->delete( $k );
961 $this->currentLang = $code;
962 $this->keys = array();
965 public function finishWrite() {
966 if ( $this->currentLang ) {
967 $k = wfMemcKey( 'l10n', $this->currentLang, 'l' );
968 $this->cache->set( $k, array_keys( $this->keys ) );
970 $this->currentLang = null;
971 $this->keys = array();
974 public function set( $key, $value ) {
975 if ( $this->currentLang ) {
976 $k = wfMemcKey( 'l10n', $this->currentLang, 'k', $key );
977 $this->keys[$k] = true;
978 $this->cache->set( $k, $value );
984 * LCStore implementation which uses the standard DB functions to store data.
985 * This will work on any MediaWiki installation.
987 class LCStore_DB implements LCStore {
988 var $currentLang;
989 var $writesDone = false;
992 * @var DatabaseBase
994 var $dbw;
995 var $batch;
996 var $readOnly = false;
998 public function get( $code, $key ) {
999 if ( $this->writesDone ) {
1000 $db = wfGetDB( DB_MASTER );
1001 } else {
1002 $db = wfGetDB( DB_SLAVE );
1004 $row = $db->selectRow( 'l10n_cache', array( 'lc_value' ),
1005 array( 'lc_lang' => $code, 'lc_key' => $key ), __METHOD__ );
1006 if ( $row ) {
1007 return unserialize( $row->lc_value );
1008 } else {
1009 return null;
1013 public function startWrite( $code ) {
1014 if ( $this->readOnly ) {
1015 return;
1018 if ( !$code ) {
1019 throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
1022 $this->dbw = wfGetDB( DB_MASTER );
1023 try {
1024 $this->dbw->begin( __METHOD__ );
1025 $this->dbw->delete( 'l10n_cache', array( 'lc_lang' => $code ), __METHOD__ );
1026 } catch ( DBQueryError $e ) {
1027 if ( $this->dbw->wasReadOnlyError() ) {
1028 $this->readOnly = true;
1029 $this->dbw->rollback( __METHOD__ );
1030 $this->dbw->ignoreErrors( false );
1031 return;
1032 } else {
1033 throw $e;
1037 $this->currentLang = $code;
1038 $this->batch = array();
1041 public function finishWrite() {
1042 if ( $this->readOnly ) {
1043 return;
1046 if ( $this->batch ) {
1047 $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
1050 $this->dbw->commit( __METHOD__ );
1051 $this->currentLang = null;
1052 $this->dbw = null;
1053 $this->batch = array();
1054 $this->writesDone = true;
1057 public function set( $key, $value ) {
1058 if ( $this->readOnly ) {
1059 return;
1062 if ( is_null( $this->currentLang ) ) {
1063 throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
1066 $this->batch[] = array(
1067 'lc_lang' => $this->currentLang,
1068 'lc_key' => $key,
1069 'lc_value' => serialize( $value ) );
1071 if ( count( $this->batch ) >= 100 ) {
1072 $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
1073 $this->batch = array();
1079 * LCStore implementation which stores data as a collection of CDB files in the
1080 * directory given by $wgCacheDirectory. If $wgCacheDirectory is not set, this
1081 * will throw an exception.
1083 * Profiling indicates that on Linux, this implementation outperforms MySQL if
1084 * the directory is on a local filesystem and there is ample kernel cache
1085 * space. The performance advantage is greater when the DBA extension is
1086 * available than it is with the PHP port.
1088 * See Cdb.php and http://cr.yp.to/cdb.html
1090 class LCStore_CDB implements LCStore {
1091 var $readers, $writer, $currentLang, $directory;
1093 function __construct( $conf = array() ) {
1094 global $wgCacheDirectory;
1096 if ( isset( $conf['directory'] ) ) {
1097 $this->directory = $conf['directory'];
1098 } else {
1099 $this->directory = $wgCacheDirectory;
1103 public function get( $code, $key ) {
1104 if ( !isset( $this->readers[$code] ) ) {
1105 $fileName = $this->getFileName( $code );
1107 if ( !file_exists( $fileName ) ) {
1108 $this->readers[$code] = false;
1109 } else {
1110 $this->readers[$code] = CdbReader::open( $fileName );
1114 if ( !$this->readers[$code] ) {
1115 return null;
1116 } else {
1117 $value = $this->readers[$code]->get( $key );
1119 if ( $value === false ) {
1120 return null;
1122 return unserialize( $value );
1126 public function startWrite( $code ) {
1127 if ( !file_exists( $this->directory ) ) {
1128 if ( !wfMkdirParents( $this->directory, null, __METHOD__ ) ) {
1129 throw new MWException( "Unable to create the localisation store " .
1130 "directory \"{$this->directory}\"" );
1134 // Close reader to stop permission errors on write
1135 if ( !empty( $this->readers[$code] ) ) {
1136 $this->readers[$code]->close();
1139 $this->writer = CdbWriter::open( $this->getFileName( $code ) );
1140 $this->currentLang = $code;
1143 public function finishWrite() {
1144 // Close the writer
1145 $this->writer->close();
1146 $this->writer = null;
1147 unset( $this->readers[$this->currentLang] );
1148 $this->currentLang = null;
1151 public function set( $key, $value ) {
1152 if ( is_null( $this->writer ) ) {
1153 throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
1155 $this->writer->set( $key, serialize( $value ) );
1158 protected function getFileName( $code ) {
1159 if ( !$code || strpos( $code, '/' ) !== false ) {
1160 throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
1162 return "{$this->directory}/l10n_cache-$code.cdb";
1167 * Null store backend, used to avoid DB errors during install
1169 class LCStore_Null implements LCStore {
1170 public function get( $code, $key ) {
1171 return null;
1174 public function startWrite( $code ) {}
1175 public function finishWrite() {}
1176 public function set( $key, $value ) {}
1180 * A localisation cache optimised for loading large amounts of data for many
1181 * languages. Used by rebuildLocalisationCache.php.
1183 class LocalisationCache_BulkLoad extends LocalisationCache {
1185 * A cache of the contents of data files.
1186 * Core files are serialized to avoid using ~1GB of RAM during a recache.
1188 var $fileCache = array();
1191 * Most recently used languages. Uses the linked-list aspect of PHP hashtables
1192 * to keep the most recently used language codes at the end of the array, and
1193 * the language codes that are ready to be deleted at the beginning.
1195 var $mruLangs = array();
1198 * Maximum number of languages that may be loaded into $this->data
1200 var $maxLoadedLangs = 10;
1203 * @param $fileName
1204 * @param $fileType
1205 * @return array|mixed
1207 protected function readPHPFile( $fileName, $fileType ) {
1208 $serialize = $fileType === 'core';
1209 if ( !isset( $this->fileCache[$fileName][$fileType] ) ) {
1210 $data = parent::readPHPFile( $fileName, $fileType );
1212 if ( $serialize ) {
1213 $encData = serialize( $data );
1214 } else {
1215 $encData = $data;
1218 $this->fileCache[$fileName][$fileType] = $encData;
1220 return $data;
1221 } elseif ( $serialize ) {
1222 return unserialize( $this->fileCache[$fileName][$fileType] );
1223 } else {
1224 return $this->fileCache[$fileName][$fileType];
1229 * @param $code
1230 * @param $key
1231 * @return mixed
1233 public function getItem( $code, $key ) {
1234 unset( $this->mruLangs[$code] );
1235 $this->mruLangs[$code] = true;
1236 return parent::getItem( $code, $key );
1240 * @param $code
1241 * @param $key
1242 * @param $subkey
1243 * @return
1245 public function getSubitem( $code, $key, $subkey ) {
1246 unset( $this->mruLangs[$code] );
1247 $this->mruLangs[$code] = true;
1248 return parent::getSubitem( $code, $key, $subkey );
1252 * @param $code
1254 public function recache( $code ) {
1255 parent::recache( $code );
1256 unset( $this->mruLangs[$code] );
1257 $this->mruLangs[$code] = true;
1258 $this->trimCache();
1262 * @param $code
1264 public function unload( $code ) {
1265 unset( $this->mruLangs[$code] );
1266 parent::unload( $code );
1270 * Unload cached languages until there are less than $this->maxLoadedLangs
1272 protected function trimCache() {
1273 while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) {
1274 reset( $this->mruLangs );
1275 $code = key( $this->mruLangs );
1276 wfDebug( __METHOD__ . ": unloading $code\n" );
1277 $this->unload( $code );