]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/LocalisationCache.php
MediaWiki 1.17.1
[autoinstallsdev/mediawiki.git] / includes / LocalisationCache.php
1 <?php
2
3 define( 'MW_LC_VERSION', 1 );
4
5 /**
6  * Class for caching the contents of localisation files, Messages*.php
7  * and *.i18n.php.
8  *
9  * An instance of this class is available using Language::getLocalisationCache().
10  *
11  * The values retrieved from here are merged, containing items from extension 
12  * files, core messages files and the language fallback sequence (e.g. zh-cn -> 
13  * zh-hans -> en ). Some common errors are corrected, for example namespace
14  * names with spaces instead of underscores, but heavyweight processing, such
15  * as grammatical transformation, is done by the caller.
16  */
17 class LocalisationCache {
18         /** Configuration associative array */
19         var $conf;
20
21         /**
22          * True if recaching should only be done on an explicit call to recache().
23          * Setting this reduces the overhead of cache freshness checking, which
24          * requires doing a stat() for every extension i18n file.
25          */
26         var $manualRecache = false;
27
28         /**
29          * True to treat all files as expired until they are regenerated by this object.
30          */
31         var $forceRecache = false;
32
33         /**
34          * The cache data. 3-d array, where the first key is the language code,
35          * the second key is the item key e.g. 'messages', and the third key is
36          * an item specific subkey index. Some items are not arrays and so for those
37          * items, there are no subkeys.
38          */
39         var $data = array();
40
41         /**
42          * The persistent store object. An instance of LCStore.
43          */
44         var $store;
45
46         /**
47          * A 2-d associative array, code/key, where presence indicates that the item
48          * is loaded. Value arbitrary.
49          *
50          * For split items, if set, this indicates that all of the subitems have been
51          * loaded.
52          */
53         var $loadedItems = array();
54
55         /**
56          * A 3-d associative array, code/key/subkey, where presence indicates that
57          * the subitem is loaded. Only used for the split items, i.e. messages.
58          */
59         var $loadedSubitems = array();
60
61         /**
62          * An array where presence of a key indicates that that language has been
63          * initialised. Initialisation includes checking for cache expiry and doing
64          * any necessary updates.
65          */
66         var $initialisedLangs = array();
67
68         /**
69          * An array mapping non-existent pseudo-languages to fallback languages. This
70          * is filled by initShallowFallback() when data is requested from a language
71          * that lacks a Messages*.php file.
72          */
73         var $shallowFallbacks = array();
74
75         /**
76          * An array where the keys are codes that have been recached by this instance.
77          */
78         var $recachedLangs = array();
79
80         /**
81          * Data added by extensions using the deprecated $wgMessageCache->addMessages() 
82          * interface.
83          */
84         var $legacyData = array();
85
86         /**
87          * All item keys
88          */
89         static public $allKeys = array(
90                 'fallback', 'namespaceNames', 'mathNames', 'bookstoreList',
91                 'magicWords', 'messages', 'rtl', 'capitalizeAllNouns', 'digitTransformTable',
92                 'separatorTransformTable', 'fallback8bitEncoding', 'linkPrefixExtension',
93                 'defaultUserOptionOverrides', 'linkTrail', 'namespaceAliases',
94                 'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
95                 'defaultDateFormat', 'extraUserToggles', 'specialPageAliases',
96                 'imageFiles', 'preloadedMessages',
97         );
98
99         /**
100          * Keys for items which consist of associative arrays, which may be merged
101          * by a fallback sequence.
102          */
103         static public $mergeableMapKeys = array( 'messages', 'namespaceNames', 'mathNames',
104                 'dateFormats', 'defaultUserOptionOverrides', 'imageFiles',
105                 'preloadedMessages',
106         );
107
108         /**
109          * Keys for items which are a numbered array.
110          */
111         static public $mergeableListKeys = array( 'extraUserToggles' );
112
113         /**
114          * Keys for items which contain an array of arrays of equivalent aliases
115          * for each subitem. The aliases may be merged by a fallback sequence.
116          */
117         static public $mergeableAliasListKeys = array( 'specialPageAliases' );
118
119         /**
120          * Keys for items which contain an associative array, and may be merged if
121          * the primary value contains the special array key "inherit". That array
122          * key is removed after the first merge.
123          */
124         static public $optionalMergeKeys = array( 'bookstoreList' );
125         
126         /**
127          * Keys for items that are formatted like $magicWords
128          */
129         static public $magicWordKeys = array( 'magicWords' );
130
131         /**
132          * Keys for items where the subitems are stored in the backend separately.
133          */
134         static public $splitKeys = array( 'messages' );
135
136         /**
137          * Keys which are loaded automatically by initLanguage()
138          */
139         static public $preloadedKeys = array( 'dateFormats', 'namespaceNames',
140                 'defaultUserOptionOverrides' );
141
142         /**
143          * Constructor.
144          * For constructor parameters, see the documentation in DefaultSettings.php 
145          * for $wgLocalisationCacheConf.
146          */
147         function __construct( $conf ) {
148                 global $wgCacheDirectory;
149
150                 $this->conf = $conf;
151                 $storeConf = array();
152                 if ( !empty( $conf['storeClass'] ) ) {
153                         $storeClass = $conf['storeClass'];
154                 } else {
155                         switch ( $conf['store'] ) {
156                                 case 'files':
157                                 case 'file':
158                                         $storeClass = 'LCStore_CDB';
159                                         break;
160                                 case 'db':
161                                         $storeClass = 'LCStore_DB';
162                                         break;
163                                 case 'detect':
164                                         $storeClass = $wgCacheDirectory ? 'LCStore_CDB' : 'LCStore_DB';
165                                         break;
166                                 default:
167                                         throw new MWException( 
168                                                 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' );
169                         }
170                 }
171
172                 wfDebug( get_class( $this ) . ": using store $storeClass\n" );
173                 if ( !empty( $conf['storeDirectory'] ) ) {
174                         $storeConf['directory'] = $conf['storeDirectory'];
175                 }
176
177                 $this->store = new $storeClass( $storeConf );
178                 foreach ( array( 'manualRecache', 'forceRecache' ) as $var ) {
179                         if ( isset( $conf[$var] ) ) {
180                                 $this->$var = $conf[$var];
181                         }
182                 }
183         }
184
185         /**
186          * Returns true if the given key is mergeable, that is, if it is an associative
187          * array which can be merged through a fallback sequence.
188          */
189         public function isMergeableKey( $key ) {
190                 if ( !isset( $this->mergeableKeys ) ) {
191                         $this->mergeableKeys = array_flip( array_merge(
192                                 self::$mergeableMapKeys,
193                                 self::$mergeableListKeys,
194                                 self::$mergeableAliasListKeys,
195                                 self::$optionalMergeKeys,
196                                 self::$magicWordKeys
197                         ) );
198                 }
199                 return isset( $this->mergeableKeys[$key] );
200         }
201
202         /**
203          * Get a cache item.
204          *
205          * Warning: this may be slow for split items (messages), since it will
206          * need to fetch all of the subitems from the cache individually.
207          */
208         public function getItem( $code, $key ) {
209                 if ( !isset( $this->loadedItems[$code][$key] ) ) {
210                         wfProfileIn( __METHOD__.'-load' );
211                         $this->loadItem( $code, $key );
212                         wfProfileOut( __METHOD__.'-load' );
213                 }
214                 if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
215                         return $this->shallowFallbacks[$code];
216                 }
217                 return $this->data[$code][$key];
218         }
219
220         /**
221          * Get a subitem, for instance a single message for a given language.
222          */
223         public function getSubitem( $code, $key, $subkey ) {
224                 if ( isset( $this->legacyData[$code][$key][$subkey] ) ) {
225                         return $this->legacyData[$code][$key][$subkey];
226                 }
227                 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) 
228                         && !isset( $this->loadedItems[$code][$key] ) ) 
229                 {
230                         wfProfileIn( __METHOD__.'-load' );
231                         $this->loadSubitem( $code, $key, $subkey );
232                         wfProfileOut( __METHOD__.'-load' );
233                 }
234                 if ( isset( $this->data[$code][$key][$subkey] ) ) {
235                         return $this->data[$code][$key][$subkey];
236                 } else {
237                         return null;
238                 }
239         }
240
241         /**
242          * Get the list of subitem keys for a given item.
243          *
244          * This is faster than array_keys($lc->getItem(...)) for the items listed in 
245          * self::$splitKeys.
246          *
247          * Will return null if the item is not found, or false if the item is not an 
248          * array.
249          */
250         public function getSubitemList( $code, $key ) {
251                 if ( in_array( $key, self::$splitKeys ) ) {
252                         return $this->getSubitem( $code, 'list', $key );
253                 } else {
254                         $item = $this->getItem( $code, $key );
255                         if ( is_array( $item ) ) {
256                                 return array_keys( $item );
257                         } else {
258                                 return false;
259                         }
260                 }
261         }
262
263         /**
264          * Load an item into the cache.
265          */
266         protected function loadItem( $code, $key ) {
267                 if ( !isset( $this->initialisedLangs[$code] ) ) {
268                         $this->initLanguage( $code );
269                 }
270                 // Check to see if initLanguage() loaded it for us
271                 if ( isset( $this->loadedItems[$code][$key] ) ) {
272                         return;
273                 }
274                 if ( isset( $this->shallowFallbacks[$code] ) ) {
275                         $this->loadItem( $this->shallowFallbacks[$code], $key );
276                         return;
277                 }
278                 if ( in_array( $key, self::$splitKeys ) ) {
279                         $subkeyList = $this->getSubitem( $code, 'list', $key );
280                         foreach ( $subkeyList as $subkey ) {
281                                 if ( isset( $this->data[$code][$key][$subkey] ) ) {
282                                         continue;
283                                 }
284                                 $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey );
285                         }
286                 } else {
287                         $this->data[$code][$key] = $this->store->get( $code, $key );
288                 }
289                 $this->loadedItems[$code][$key] = true;
290         }
291
292         /**
293          * Load a subitem into the cache
294          */
295         protected function loadSubitem( $code, $key, $subkey ) {
296                 if ( !in_array( $key, self::$splitKeys ) ) {
297                         $this->loadItem( $code, $key );
298                         return;
299                 }
300                 if ( !isset( $this->initialisedLangs[$code] ) ) {
301                         $this->initLanguage( $code );
302                 }
303                 // Check to see if initLanguage() loaded it for us
304                 if ( isset( $this->loadedItems[$code][$key] )
305                         || isset( $this->loadedSubitems[$code][$key][$subkey] ) )
306                 {
307                         return;
308                 }
309                 if ( isset( $this->shallowFallbacks[$code] ) ) {
310                         $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
311                         return;
312                 }
313                 $value = $this->store->get( $code, "$key:$subkey" );
314                 $this->data[$code][$key][$subkey] = $value;
315                 $this->loadedSubitems[$code][$key][$subkey] = true;
316         }
317
318         /**
319          * Returns true if the cache identified by $code is missing or expired.
320          */
321         public function isExpired( $code ) {
322                 if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
323                         wfDebug( __METHOD__."($code): forced reload\n" );
324                         return true;
325                 }
326
327                 $deps = $this->store->get( $code, 'deps' );
328                 if ( $deps === null ) {
329                         wfDebug( __METHOD__."($code): cache missing, need to make one\n" );
330                         return true;
331                 }
332                 foreach ( $deps as $dep ) {
333                         // Because we're unserializing stuff from cache, we
334                         // could receive objects of classes that don't exist
335                         // anymore (e.g. uninstalled extensions)
336                         // When this happens, always expire the cache
337                         if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
338                                 wfDebug( __METHOD__."($code): cache for $code expired due to " . 
339                                         get_class( $dep ) . "\n" );
340                                 return true;
341                         }
342                 }
343                 return false;
344         }
345
346         /**
347          * Initialise a language in this object. Rebuild the cache if necessary.
348          */
349         protected function initLanguage( $code ) {
350                 if ( isset( $this->initialisedLangs[$code] ) ) {
351                         return;
352                 }
353                 $this->initialisedLangs[$code] = true;
354
355                 # Recache the data if necessary
356                 if ( !$this->manualRecache && $this->isExpired( $code ) ) {
357                         if ( file_exists( Language::getMessagesFileName( $code ) ) ) {
358                                 $this->recache( $code );
359                         } elseif ( $code === 'en' ) {
360                                 throw new MWException( 'MessagesEn.php is missing.' );
361                         } else {
362                                 $this->initShallowFallback( $code, 'en' );
363                         }
364                         return;
365                 }
366
367                 # Preload some stuff
368                 $preload = $this->getItem( $code, 'preload' );
369                 if ( $preload === null ) {
370                         if ( $this->manualRecache ) {
371                                 // No Messages*.php file. Do shallow fallback to en.
372                                 if ( $code === 'en' ) {
373                                         throw new MWException( 'No localisation cache found for English. ' . 
374                                                 'Please run maintenance/rebuildLocalisationCache.php.' );
375                                 }
376                                 $this->initShallowFallback( $code, 'en' );
377                                 return;
378                         } else {
379                                 throw new MWException( 'Invalid or missing localisation cache.' );
380                         }
381                 }
382                 $this->data[$code] = $preload;
383                 foreach ( $preload as $key => $item ) {
384                         if ( in_array( $key, self::$splitKeys ) ) {
385                                 foreach ( $item as $subkey => $subitem ) {
386                                         $this->loadedSubitems[$code][$key][$subkey] = true;
387                                 }
388                         } else {
389                                 $this->loadedItems[$code][$key] = true;
390                         }
391                 }
392         }
393
394         /**
395          * Create a fallback from one language to another, without creating a 
396          * complete persistent cache.
397          */
398         public function initShallowFallback( $primaryCode, $fallbackCode ) {
399                 $this->data[$primaryCode] =& $this->data[$fallbackCode];
400                 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
401                 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
402                 $this->shallowFallbacks[$primaryCode] = $fallbackCode;
403         }
404
405         /**
406          * Read a PHP file containing localisation data.
407          */
408         protected function readPHPFile( $_fileName, $_fileType ) {
409                 // Disable APC caching
410                 $_apcEnabled = ini_set( 'apc.cache_by_default', '0' );
411                 include( $_fileName );
412                 ini_set( 'apc.cache_by_default', $_apcEnabled );
413
414                 if ( $_fileType == 'core' || $_fileType == 'extension' ) {
415                         $data = compact( self::$allKeys );
416                 } elseif ( $_fileType == 'aliases' ) {
417                         $data = compact( 'aliases' );
418                 } else {
419                         throw new MWException( __METHOD__.": Invalid file type: $_fileType" );
420                 }
421                 return $data;
422         }
423
424         /**
425          * Merge two localisation values, a primary and a fallback, overwriting the 
426          * primary value in place.
427          */
428         protected function mergeItem( $key, &$value, $fallbackValue ) {
429                 if ( !is_null( $value ) ) {
430                         if ( !is_null( $fallbackValue ) ) {
431                                 if ( in_array( $key, self::$mergeableMapKeys ) ) {
432                                         $value = $value + $fallbackValue;
433                                 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
434                                         $value = array_unique( array_merge( $fallbackValue, $value ) );
435                                 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
436                                         $value = array_merge_recursive( $value, $fallbackValue );
437                                 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
438                                         if ( !empty( $value['inherit'] ) )  {
439                                                 $value = array_merge( $fallbackValue, $value );
440                                         }
441                                         if ( isset( $value['inherit'] ) ) {
442                                                 unset( $value['inherit'] );
443                                         }
444                                 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
445                                         $this->mergeMagicWords( $value, $fallbackValue );
446                                 }
447                         }
448                 } else {
449                         $value = $fallbackValue;
450                 }
451         }
452
453         protected function mergeMagicWords( &$value, $fallbackValue ) {
454                 foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
455                         if ( !isset( $value[$magicName] ) ) {
456                                 $value[$magicName] = $fallbackInfo;
457                         } else {
458                                 $oldSynonyms = array_slice( $fallbackInfo, 1 );
459                                 $newSynonyms = array_slice( $value[$magicName], 1 );
460                                 $synonyms = array_values( array_unique( array_merge( 
461                                         $newSynonyms, $oldSynonyms ) ) );
462                                 $value[$magicName] = array_merge( array( $fallbackInfo[0] ), $synonyms );
463                         }
464                 }
465         }
466
467         /**
468          * Given an array mapping language code to localisation value, such as is
469          * found in extension *.i18n.php files, iterate through a fallback sequence
470          * to merge the given data with an existing primary value.
471          *
472          * Returns true if any data from the extension array was used, false 
473          * otherwise.
474          */
475         protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
476                 $used = false;
477                 foreach ( $codeSequence as $code ) {
478                         if ( isset( $fallbackValue[$code] ) ) {
479                                 $this->mergeItem( $key, $value, $fallbackValue[$code] );
480                                 $used = true;
481                         }
482                 }
483                 return $used;
484         }
485
486         /**
487          * Load localisation data for a given language for both core and extensions
488          * and save it to the persistent cache store and the process cache
489          */
490         public function recache( $code ) {
491                 static $recursionGuard = array();
492                 global $wgExtensionMessagesFiles, $wgExtensionAliasesFiles;
493                 wfProfileIn( __METHOD__ );
494
495                 if ( !$code ) {
496                         throw new MWException( "Invalid language code requested" );
497                 }
498                 $this->recachedLangs[$code] = true;
499
500                 # Initial values
501                 $initialData = array_combine(
502                         self::$allKeys, 
503                         array_fill( 0, count( self::$allKeys ), null ) );
504                 $coreData = $initialData;
505                 $deps = array();
506
507                 # Load the primary localisation from the source file
508                 $fileName = Language::getMessagesFileName( $code );
509                 if ( !file_exists( $fileName ) ) {
510                         wfDebug( __METHOD__.": no localisation file for $code, using fallback to en\n" );
511                         $coreData['fallback'] = 'en';
512                 } else {
513                         $deps[] = new FileDependency( $fileName );
514                         $data = $this->readPHPFile( $fileName, 'core' );
515                         wfDebug( __METHOD__.": got localisation for $code from source\n" );
516
517                         # Merge primary localisation
518                         foreach ( $data as $key => $value ) {
519                                 $this->mergeItem( $key, $coreData[$key], $value );
520                         }
521                 }
522
523                 # Fill in the fallback if it's not there already
524                 if ( is_null( $coreData['fallback'] ) ) {
525                         $coreData['fallback'] = $code === 'en' ? false : 'en';
526                 }
527
528                 if ( $coreData['fallback'] !== false ) {
529                         # Guard against circular references
530                         if ( isset( $recursionGuard[$code] ) ) {
531                                 throw new MWException( "Error: Circular fallback reference in language code $code" );
532                         }
533                         $recursionGuard[$code] = true;
534
535                         # Load the fallback localisation item by item and merge it
536                         $deps = array_merge( $deps, $this->getItem( $coreData['fallback'], 'deps' ) );
537                         foreach ( self::$allKeys as $key ) {
538                                 if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) {
539                                         $fallbackValue = $this->getItem( $coreData['fallback'], $key );
540                                         $this->mergeItem( $key, $coreData[$key], $fallbackValue );
541                                 }
542                         }
543                         $fallbackSequence = $this->getItem( $coreData['fallback'], 'fallbackSequence' );
544                         array_unshift( $fallbackSequence, $coreData['fallback'] );
545                         $coreData['fallbackSequence'] = $fallbackSequence;
546                         unset( $recursionGuard[$code] );
547                 } else {
548                         $coreData['fallbackSequence'] = array();
549                 }
550                 $codeSequence = array_merge( array( $code ), $coreData['fallbackSequence'] );
551
552                 # Load the extension localisations
553                 # This is done after the core because we know the fallback sequence now.
554                 # But it has a higher precedence for merging so that we can support things 
555                 # like site-specific message overrides.
556                 $allData = $initialData;
557                 foreach ( $wgExtensionMessagesFiles as $fileName ) {
558                         $data = $this->readPHPFile( $fileName, 'extension' );
559                         $used = false;
560                         foreach ( $data as $key => $item ) {
561                                 if( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
562                                         $used = true;
563                                 }
564                         }
565                         if ( $used ) {
566                                 $deps[] = new FileDependency( $fileName );
567                         }
568                 }
569
570                 # Load deprecated $wgExtensionAliasesFiles
571                 foreach ( $wgExtensionAliasesFiles as $fileName ) {
572                         $data = $this->readPHPFile( $fileName, 'aliases' );
573                         if ( !isset( $data['aliases'] ) ) {
574                                 continue;
575                         }
576                         $used = $this->mergeExtensionItem( $codeSequence, 'specialPageAliases',
577                                 $allData['specialPageAliases'], $data['aliases'] );
578                         if ( $used ) {
579                                 $deps[] = new FileDependency( $fileName );
580                         }
581                 }
582
583                 # Merge core data into extension data
584                 foreach ( $coreData as $key => $item ) {
585                         $this->mergeItem( $key, $allData[$key], $item );
586                 }
587
588                 # Add cache dependencies for any referenced globals
589                 $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
590                 $deps['wgExtensionAliasesFiles'] = new GlobalDependency( 'wgExtensionAliasesFiles' );
591                 $deps['version'] = new ConstantDependency( 'MW_LC_VERSION' );
592
593                 # Add dependencies to the cache entry
594                 $allData['deps'] = $deps;
595
596                 # Replace spaces with underscores in namespace names
597                 $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
598
599                 # And do the same for special page aliases. $page is an array.
600                 foreach ( $allData['specialPageAliases'] as &$page ) {
601                         $page = str_replace( ' ', '_', $page );
602                 }
603                 # Decouple the reference to prevent accidental damage
604                 unset($page);
605         
606                 # Fix broken defaultUserOptionOverrides
607                 if ( !is_array( $allData['defaultUserOptionOverrides'] ) ) {
608                         $allData['defaultUserOptionOverrides'] = array();
609                 }
610
611                 # Set the list keys
612                 $allData['list'] = array();
613                 foreach ( self::$splitKeys as $key ) {
614                         $allData['list'][$key] = array_keys( $allData[$key] );
615                 }
616
617                 # Run hooks
618                 wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) );
619
620                 if ( is_null( $allData['defaultUserOptionOverrides'] ) ) {
621                         throw new MWException( __METHOD__.': Localisation data failed sanity check! ' . 
622                                 'Check that your languages/messages/MessagesEn.php file is intact.' );
623                 }
624
625                 # Set the preload key
626                 $allData['preload'] = $this->buildPreload( $allData );
627
628                 # Save to the process cache and register the items loaded
629                 $this->data[$code] = $allData;
630                 foreach ( $allData as $key => $item ) {
631                         $this->loadedItems[$code][$key] = true;
632                 }
633
634                 # Save to the persistent cache
635                 $this->store->startWrite( $code );
636                 foreach ( $allData as $key => $value ) {
637                         if ( in_array( $key, self::$splitKeys ) ) {
638                                 foreach ( $value as $subkey => $subvalue ) {
639                                         $this->store->set( "$key:$subkey", $subvalue );
640                                 }
641                         } else {
642                                 $this->store->set( $key, $value );
643                         }
644                 }
645                 $this->store->finishWrite();
646                 
647                 # Clear out the MessageBlobStore
648                 # HACK: If using a null (i.e. disabled) storage backend, we
649                 # can't write to the MessageBlobStore either
650                 if ( !$this->store instanceof LCStore_Null ) {
651                         MessageBlobStore::clear();
652                 }
653
654                 wfProfileOut( __METHOD__ );
655         }
656
657         /**
658          * Build the preload item from the given pre-cache data.
659          *
660          * The preload item will be loaded automatically, improving performance
661          * for the commonly-requested items it contains.
662          */
663         protected function buildPreload( $data ) {
664                 $preload = array( 'messages' => array() );
665                 foreach ( self::$preloadedKeys as $key ) {
666                         $preload[$key] = $data[$key];
667                 }
668                 foreach ( $data['preloadedMessages'] as $subkey ) {
669                         if ( isset( $data['messages'][$subkey] ) ) {
670                                 $subitem = $data['messages'][$subkey];
671                         } else {
672                                 $subitem = null;
673                         }
674                         $preload['messages'][$subkey] = $subitem;
675                 }
676                 return $preload;
677         }
678
679         /**
680          * Unload the data for a given language from the object cache. 
681          * Reduces memory usage.
682          */
683         public function unload( $code ) {
684                 unset( $this->data[$code] );
685                 unset( $this->loadedItems[$code] );
686                 unset( $this->loadedSubitems[$code] );
687                 unset( $this->initialisedLangs[$code] );
688                 // We don't unload legacyData because there's no way to get it back 
689                 // again, it's not really a cache
690                 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
691                         if ( $fbCode === $code ) {
692                                 $this->unload( $shallowCode );
693                         }
694                 }
695         }
696
697         /**
698          * Unload all data
699          */
700         public function unloadAll() {
701                 foreach ( $this->initialisedLangs as $lang => $unused ) {
702                         $this->unload( $lang );
703                 }
704         }
705
706         /**
707          * Add messages to the cache, from an extension that has not yet been 
708          * migrated to $wgExtensionMessages or the LocalisationCacheRecache hook. 
709          * Called by deprecated function $wgMessageCache->addMessages(). 
710          */
711         public function addLegacyMessages( $messages ) {
712                 foreach ( $messages as $lang => $langMessages ) {
713                         if ( isset( $this->legacyData[$lang]['messages'] ) ) {
714                                 $this->legacyData[$lang]['messages'] = 
715                                         $langMessages + $this->legacyData[$lang]['messages'];
716                         } else {
717                                 $this->legacyData[$lang]['messages'] = $langMessages;
718                         }
719                 }
720         }
721
722         /**
723          * Disable the storage backend
724          */
725         public function disableBackend() {
726                 $this->store = new LCStore_Null;
727                 $this->manualRecache = false;
728         }
729 }
730
731 /**
732  * Interface for the persistence layer of LocalisationCache.
733  *
734  * The persistence layer is two-level hierarchical cache. The first level
735  * is the language, the second level is the item or subitem.
736  *
737  * Since the data for a whole language is rebuilt in one operation, it needs 
738  * to have a fast and atomic method for deleting or replacing all of the 
739  * current data for a given language. The interface reflects this bulk update
740  * operation. Callers writing to the cache must first call startWrite(), then 
741  * will call set() a couple of thousand times, then will call finishWrite() 
742  * to commit the operation. When finishWrite() is called, the cache is 
743  * expected to delete all data previously stored for that language.
744  *
745  * The values stored are PHP variables suitable for serialize(). Implementations 
746  * of LCStore are responsible for serializing and unserializing.
747  */
748 interface LCStore {
749         /**
750          * Get a value.
751          * @param $code Language code
752          * @param $key Cache key
753          */
754         public function get( $code, $key );
755
756         /**
757          * Start a write transaction.
758          * @param $code Language code
759          */
760         public function startWrite( $code );
761
762         /**
763          * Finish a write transaction.
764          */
765         public function finishWrite();
766
767         /**
768          * Set a key to a given value. startWrite() must be called before this
769          * is called, and finishWrite() must be called afterwards.
770          */
771         public function set( $key, $value );
772
773 }
774
775 /**
776  * LCStore implementation which uses the standard DB functions to store data. 
777  * This will work on any MediaWiki installation.
778  */
779 class LCStore_DB implements LCStore {
780         var $currentLang;
781         var $writesDone = false;
782         var $dbw, $batch;
783         var $readOnly = false;
784
785         public function get( $code, $key ) {
786                 if ( $this->writesDone ) {
787                         $db = wfGetDB( DB_MASTER );
788                 } else {
789                         $db = wfGetDB( DB_SLAVE );
790                 }
791                 $row = $db->selectRow( 'l10n_cache', array( 'lc_value' ),
792                         array( 'lc_lang' => $code, 'lc_key' => $key ), __METHOD__ );
793                 if ( $row ) {
794                         return unserialize( $row->lc_value );
795                 } else {
796                         return null;
797                 }
798         }
799
800         public function startWrite( $code ) {
801                 if ( $this->readOnly ) {
802                         return;
803                 }
804                 if ( !$code ) {
805                         throw new MWException( __METHOD__.": Invalid language \"$code\"" );
806                 }
807                 $this->dbw = wfGetDB( DB_MASTER );
808                 try {
809                         $this->dbw->begin();
810                         $this->dbw->delete( 'l10n_cache', array( 'lc_lang' => $code ), __METHOD__ );
811                 } catch ( DBQueryError $e ) {
812                         if ( $this->dbw->wasReadOnlyError() ) {
813                                 $this->readOnly = true;
814                                 $this->dbw->rollback();
815                                 $this->dbw->ignoreErrors( false );
816                                 return;
817                         } else {
818                                 throw $e;
819                         }
820                 }
821                 $this->currentLang = $code;
822                 $this->batch = array();
823         }
824
825         public function finishWrite() {
826                 if ( $this->readOnly ) {
827                         return;
828                 }
829                 if ( $this->batch ) {
830                         $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
831                 }
832                 $this->dbw->commit();
833                 $this->currentLang = null;
834                 $this->dbw = null;
835                 $this->batch = array();
836                 $this->writesDone = true;
837         }
838
839         public function set( $key, $value ) {
840                 if ( $this->readOnly ) {
841                         return;
842                 }
843                 if ( is_null( $this->currentLang ) ) {
844                         throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
845                 }
846                 $this->batch[] = array(
847                         'lc_lang' => $this->currentLang,
848                         'lc_key' => $key,
849                         'lc_value' => serialize( $value ) );
850                 if ( count( $this->batch ) >= 100 ) {
851                         $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
852                         $this->batch = array();
853                 }
854         }
855 }
856
857 /**
858  * LCStore implementation which stores data as a collection of CDB files in the
859  * directory given by $wgCacheDirectory. If $wgCacheDirectory is not set, this
860  * will throw an exception.
861  *
862  * Profiling indicates that on Linux, this implementation outperforms MySQL if 
863  * the directory is on a local filesystem and there is ample kernel cache 
864  * space. The performance advantage is greater when the DBA extension is 
865  * available than it is with the PHP port.
866  *
867  * See Cdb.php and http://cr.yp.to/cdb.html
868  */
869 class LCStore_CDB implements LCStore {
870         var $readers, $writer, $currentLang, $directory;
871
872         function __construct( $conf = array() ) {
873                 global $wgCacheDirectory;
874                 if ( isset( $conf['directory'] ) ) {
875                         $this->directory = $conf['directory'];
876                 } else {
877                         $this->directory = $wgCacheDirectory;
878                 }
879         }
880
881         public function get( $code, $key ) {
882                 if ( !isset( $this->readers[$code] ) ) {
883                         $fileName = $this->getFileName( $code );
884                         if ( !file_exists( $fileName ) ) {
885                                 $this->readers[$code] = false;
886                         } else {
887                                 $this->readers[$code] = CdbReader::open( $fileName );
888                         }
889                 }
890                 if ( !$this->readers[$code] ) {
891                         return null;
892                 } else {
893                         $value = $this->readers[$code]->get( $key );
894                         if ( $value === false ) {
895                                 return null;
896                         }
897                         return unserialize( $value );
898                 }
899         }
900
901         public function startWrite( $code ) {
902                 if ( !file_exists( $this->directory ) ) {
903                         if ( !wfMkdirParents( $this->directory ) ) {
904                                 throw new MWException( "Unable to create the localisation store " . 
905                                         "directory \"{$this->directory}\"" );
906                         }
907                 }
908                 // Close reader to stop permission errors on write
909                 if( !empty($this->readers[$code]) ) {
910                         $this->readers[$code]->close();
911                 }
912                 $this->writer = CdbWriter::open( $this->getFileName( $code ) );
913                 $this->currentLang = $code;
914         }
915
916         public function finishWrite() {
917                 // Close the writer
918                 $this->writer->close();
919                 $this->writer = null;
920                 unset( $this->readers[$this->currentLang] );
921                 $this->currentLang = null;
922         }
923
924         public function set( $key, $value ) {
925                 if ( is_null( $this->writer ) ) {
926                         throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
927                 }
928                 $this->writer->set( $key, serialize( $value ) );
929         }
930
931         protected function getFileName( $code ) {
932                 if ( !$code || strpos( $code, '/' ) !== false ) {
933                         throw new MWException( __METHOD__.": Invalid language \"$code\"" );
934                 }
935                 return "{$this->directory}/l10n_cache-$code.cdb";
936         }
937 }
938
939 /**
940  * Null store backend, used to avoid DB errors during install
941  */
942 class LCStore_Null implements LCStore {
943         public function get( $code, $key ) {
944                 return null;
945         }
946
947         public function startWrite( $code ) {}
948         public function finishWrite() {}
949         public function set( $key, $value ) {}
950 }
951
952 /**
953  * A localisation cache optimised for loading large amounts of data for many 
954  * languages. Used by rebuildLocalisationCache.php.
955  */
956 class LocalisationCache_BulkLoad extends LocalisationCache {
957         /**
958          * A cache of the contents of data files.
959          * Core files are serialized to avoid using ~1GB of RAM during a recache.
960          */
961         var $fileCache = array();
962
963         /**
964          * Most recently used languages. Uses the linked-list aspect of PHP hashtables
965          * to keep the most recently used language codes at the end of the array, and 
966          * the language codes that are ready to be deleted at the beginning.
967          */
968         var $mruLangs = array();
969
970         /**
971          * Maximum number of languages that may be loaded into $this->data
972          */
973         var $maxLoadedLangs = 10;
974
975         protected function readPHPFile( $fileName, $fileType ) {
976                 $serialize = $fileType === 'core';
977                 if ( !isset( $this->fileCache[$fileName][$fileType] ) ) {
978                         $data = parent::readPHPFile( $fileName, $fileType );
979                         if ( $serialize ) {
980                                 $encData = serialize( $data );
981                         } else {
982                                 $encData = $data;
983                         }
984                         $this->fileCache[$fileName][$fileType] = $encData;
985                         return $data;
986                 } elseif ( $serialize ) {
987                         return unserialize( $this->fileCache[$fileName][$fileType] );
988                 } else {
989                         return $this->fileCache[$fileName][$fileType];
990                 }
991         }
992
993         public function getItem( $code, $key ) {
994                 unset( $this->mruLangs[$code] );
995                 $this->mruLangs[$code] = true;
996                 return parent::getItem( $code, $key );
997         }
998
999         public function getSubitem( $code, $key, $subkey ) {
1000                 unset( $this->mruLangs[$code] );
1001                 $this->mruLangs[$code] = true;
1002                 return parent::getSubitem( $code, $key, $subkey );
1003         }
1004
1005         public function recache( $code ) {
1006                 parent::recache( $code );
1007                 unset( $this->mruLangs[$code] );
1008                 $this->mruLangs[$code] = true;
1009                 $this->trimCache();
1010         }
1011
1012         public function unload( $code ) {
1013                 unset( $this->mruLangs[$code] );
1014                 parent::unload( $code );
1015         }
1016
1017         /**
1018          * Unload cached languages until there are less than $this->maxLoadedLangs
1019          */
1020         protected function trimCache() {
1021                 while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) {
1022                         reset( $this->mruLangs );
1023                         $code = key( $this->mruLangs );
1024                         wfDebug( __METHOD__.": unloading $code\n" );
1025                         $this->unload( $code );
1026                 }
1027         }
1028 }