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