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