(bug 33471) compare detectProtocol to 'https'
[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', 'pluralRules', 'compiledPluralRules',
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 /**
158 * Associative array of cached plural rules. The key is the language code,
159 * the value is an array of plural rules for that language.
160 */
161 var $pluralRules = null;
162
163 var $mergeableKeys = null;
164
165 /**
166 * Constructor.
167 * For constructor parameters, see the documentation in DefaultSettings.php
168 * for $wgLocalisationCacheConf.
169 *
170 * @param $conf Array
171 */
172 function __construct( $conf ) {
173 global $wgCacheDirectory;
174
175 $this->conf = $conf;
176 $storeConf = array();
177 if ( !empty( $conf['storeClass'] ) ) {
178 $storeClass = $conf['storeClass'];
179 } else {
180 switch ( $conf['store'] ) {
181 case 'files':
182 case 'file':
183 $storeClass = 'LCStore_CDB';
184 break;
185 case 'db':
186 $storeClass = 'LCStore_DB';
187 break;
188 case 'accel':
189 $storeClass = 'LCStore_Accel';
190 break;
191 case 'detect':
192 $storeClass = $wgCacheDirectory ? 'LCStore_CDB' : 'LCStore_DB';
193 break;
194 default:
195 throw new MWException(
196 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' );
197 }
198 }
199
200 wfDebug( get_class( $this ) . ": using store $storeClass\n" );
201 if ( !empty( $conf['storeDirectory'] ) ) {
202 $storeConf['directory'] = $conf['storeDirectory'];
203 }
204
205 $this->store = new $storeClass( $storeConf );
206 foreach ( array( 'manualRecache', 'forceRecache' ) as $var ) {
207 if ( isset( $conf[$var] ) ) {
208 $this->$var = $conf[$var];
209 }
210 }
211 }
212
213 /**
214 * Returns true if the given key is mergeable, that is, if it is an associative
215 * array which can be merged through a fallback sequence.
216 * @param $key
217 * @return bool
218 */
219 public function isMergeableKey( $key ) {
220 if ( $this->mergeableKeys === null ) {
221 $this->mergeableKeys = array_flip( array_merge(
222 self::$mergeableMapKeys,
223 self::$mergeableListKeys,
224 self::$mergeableAliasListKeys,
225 self::$optionalMergeKeys,
226 self::$magicWordKeys
227 ) );
228 }
229 return isset( $this->mergeableKeys[$key] );
230 }
231
232 /**
233 * Get a cache item.
234 *
235 * Warning: this may be slow for split items (messages), since it will
236 * need to fetch all of the subitems from the cache individually.
237 * @param $code
238 * @param $key
239 * @return mixed
240 */
241 public function getItem( $code, $key ) {
242 if ( !isset( $this->loadedItems[$code][$key] ) ) {
243 wfProfileIn( __METHOD__ . '-load' );
244 $this->loadItem( $code, $key );
245 wfProfileOut( __METHOD__ . '-load' );
246 }
247
248 if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
249 return $this->shallowFallbacks[$code];
250 }
251
252 return $this->data[$code][$key];
253 }
254
255 /**
256 * Get a subitem, for instance a single message for a given language.
257 * @param $code
258 * @param $key
259 * @param $subkey
260 * @return null
261 */
262 public function getSubitem( $code, $key, $subkey ) {
263 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
264 !isset( $this->loadedItems[$code][$key] ) ) {
265 wfProfileIn( __METHOD__ . '-load' );
266 $this->loadSubitem( $code, $key, $subkey );
267 wfProfileOut( __METHOD__ . '-load' );
268 }
269
270 if ( isset( $this->data[$code][$key][$subkey] ) ) {
271 return $this->data[$code][$key][$subkey];
272 } else {
273 return null;
274 }
275 }
276
277 /**
278 * Get the list of subitem keys for a given item.
279 *
280 * This is faster than array_keys($lc->getItem(...)) for the items listed in
281 * self::$splitKeys.
282 *
283 * Will return null if the item is not found, or false if the item is not an
284 * array.
285 * @param $code
286 * @param $key
287 * @return bool|null|string
288 */
289 public function getSubitemList( $code, $key ) {
290 if ( in_array( $key, self::$splitKeys ) ) {
291 return $this->getSubitem( $code, 'list', $key );
292 } else {
293 $item = $this->getItem( $code, $key );
294 if ( is_array( $item ) ) {
295 return array_keys( $item );
296 } else {
297 return false;
298 }
299 }
300 }
301
302 /**
303 * Load an item into the cache.
304 * @param $code
305 * @param $key
306 */
307 protected function loadItem( $code, $key ) {
308 if ( !isset( $this->initialisedLangs[$code] ) ) {
309 $this->initLanguage( $code );
310 }
311
312 // Check to see if initLanguage() loaded it for us
313 if ( isset( $this->loadedItems[$code][$key] ) ) {
314 return;
315 }
316
317 if ( isset( $this->shallowFallbacks[$code] ) ) {
318 $this->loadItem( $this->shallowFallbacks[$code], $key );
319 return;
320 }
321
322 if ( in_array( $key, self::$splitKeys ) ) {
323 $subkeyList = $this->getSubitem( $code, 'list', $key );
324 foreach ( $subkeyList as $subkey ) {
325 if ( isset( $this->data[$code][$key][$subkey] ) ) {
326 continue;
327 }
328 $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey );
329 }
330 } else {
331 $this->data[$code][$key] = $this->store->get( $code, $key );
332 }
333
334 $this->loadedItems[$code][$key] = true;
335 }
336
337 /**
338 * Load a subitem into the cache
339 * @param $code
340 * @param $key
341 * @param $subkey
342 * @return
343 */
344 protected function loadSubitem( $code, $key, $subkey ) {
345 if ( !in_array( $key, self::$splitKeys ) ) {
346 $this->loadItem( $code, $key );
347 return;
348 }
349
350 if ( !isset( $this->initialisedLangs[$code] ) ) {
351 $this->initLanguage( $code );
352 }
353
354 // Check to see if initLanguage() loaded it for us
355 if ( isset( $this->loadedItems[$code][$key] ) ||
356 isset( $this->loadedSubitems[$code][$key][$subkey] ) ) {
357 return;
358 }
359
360 if ( isset( $this->shallowFallbacks[$code] ) ) {
361 $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
362 return;
363 }
364
365 $value = $this->store->get( $code, "$key:$subkey" );
366 $this->data[$code][$key][$subkey] = $value;
367 $this->loadedSubitems[$code][$key][$subkey] = true;
368 }
369
370 /**
371 * Returns true if the cache identified by $code is missing or expired.
372 * @return bool
373 */
374 public function isExpired( $code ) {
375 if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
376 wfDebug( __METHOD__ . "($code): forced reload\n" );
377 return true;
378 }
379
380 $deps = $this->store->get( $code, 'deps' );
381 $keys = $this->store->get( $code, 'list', 'messages' );
382 $preload = $this->store->get( $code, 'preload' );
383 // Different keys may expire separately, at least in LCStore_Accel
384 if ( $deps === null || $keys === null || $preload === null ) {
385 wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" );
386 return true;
387 }
388
389 foreach ( $deps as $dep ) {
390 // Because we're unserializing stuff from cache, we
391 // could receive objects of classes that don't exist
392 // anymore (e.g. uninstalled extensions)
393 // When this happens, always expire the cache
394 if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
395 wfDebug( __METHOD__ . "($code): cache for $code expired due to " .
396 get_class( $dep ) . "\n" );
397 return true;
398 }
399 }
400
401 return false;
402 }
403
404 /**
405 * Initialise a language in this object. Rebuild the cache if necessary.
406 * @param $code
407 */
408 protected function initLanguage( $code ) {
409 if ( isset( $this->initialisedLangs[$code] ) ) {
410 return;
411 }
412
413 $this->initialisedLangs[$code] = true;
414
415 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
416 if ( !Language::isValidBuiltInCode( $code ) ) {
417 $this->initShallowFallback( $code, 'en' );
418 return;
419 }
420
421 # Recache the data if necessary
422 if ( !$this->manualRecache && $this->isExpired( $code ) ) {
423 if ( file_exists( Language::getMessagesFileName( $code ) ) ) {
424 $this->recache( $code );
425 } elseif ( $code === 'en' ) {
426 throw new MWException( 'MessagesEn.php is missing.' );
427 } else {
428 $this->initShallowFallback( $code, 'en' );
429 }
430 return;
431 }
432
433 # Preload some stuff
434 $preload = $this->getItem( $code, 'preload' );
435 if ( $preload === null ) {
436 if ( $this->manualRecache ) {
437 // No Messages*.php file. Do shallow fallback to en.
438 if ( $code === 'en' ) {
439 throw new MWException( 'No localisation cache found for English. ' .
440 'Please run maintenance/rebuildLocalisationCache.php.' );
441 }
442 $this->initShallowFallback( $code, 'en' );
443 return;
444 } else {
445 throw new MWException( 'Invalid or missing localisation cache.' );
446 }
447 }
448 $this->data[$code] = $preload;
449 foreach ( $preload as $key => $item ) {
450 if ( in_array( $key, self::$splitKeys ) ) {
451 foreach ( $item as $subkey => $subitem ) {
452 $this->loadedSubitems[$code][$key][$subkey] = true;
453 }
454 } else {
455 $this->loadedItems[$code][$key] = true;
456 }
457 }
458 }
459
460 /**
461 * Create a fallback from one language to another, without creating a
462 * complete persistent cache.
463 * @param $primaryCode
464 * @param $fallbackCode
465 */
466 public function initShallowFallback( $primaryCode, $fallbackCode ) {
467 $this->data[$primaryCode] =& $this->data[$fallbackCode];
468 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
469 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
470 $this->shallowFallbacks[$primaryCode] = $fallbackCode;
471 }
472
473 /**
474 * Read a PHP file containing localisation data.
475 * @param $_fileName
476 * @param $_fileType
477 * @return array
478 */
479 protected function readPHPFile( $_fileName, $_fileType ) {
480 // Disable APC caching
481 $_apcEnabled = ini_set( 'apc.cache_by_default', '0' );
482 include( $_fileName );
483 ini_set( 'apc.cache_by_default', $_apcEnabled );
484
485 if ( $_fileType == 'core' || $_fileType == 'extension' ) {
486 $data = compact( self::$allKeys );
487 } elseif ( $_fileType == 'aliases' ) {
488 $data = compact( 'aliases' );
489 } else {
490 throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
491 }
492 return $data;
493 }
494
495 /**
496 * Get the compiled plural rules for a given language from the XML files.
497 * @since 1.20
498 */
499 public function getCompiledPluralRules( $code ) {
500 $rules = $this->getPluralRules( $code );
501 if ( $rules === null ) {
502 return null;
503 }
504 try {
505 $compiledRules = CLDRPluralRuleEvaluator::compile( $rules );
506 } catch( CLDRPluralRuleError $e ) {
507 wfDebugLog( 'l10n', $e->getMessage() . "\n" );
508 return array();
509 }
510 return $compiledRules;
511 }
512
513 /**
514 * Get the plural rules for a given language from the XML files.
515 * Cached.
516 * @since 1.20
517 */
518 public function getPluralRules( $code ) {
519 if ( $this->pluralRules === null ) {
520 $cldrPlural = __DIR__ . "/../languages/data/plurals.xml";
521 $mwPlural = __DIR__ . "/../languages/data/plurals-mediawiki.xml";
522 // Load CLDR plural rules
523 $this->loadPluralFile( $cldrPlural );
524 if ( file_exists( $mwPlural ) ) {
525 // Override or extend
526 $this->loadPluralFile( $mwPlural );
527 }
528 }
529 if ( !isset( $this->pluralRules[$code] ) ) {
530 return null;
531 } else {
532 return $this->pluralRules[$code];
533 }
534 }
535
536
537 /**
538 * Load a plural XML file with the given filename, compile the relevant
539 * rules, and save the compiled rules in a process-local cache.
540 */
541 protected function loadPluralFile( $fileName ) {
542 $doc = new DOMDocument;
543 $doc->load( $fileName );
544 $rulesets = $doc->getElementsByTagName( "pluralRules" );
545 foreach ( $rulesets as $ruleset ) {
546 $codes = $ruleset->getAttribute( 'locales' );
547 $rules = array();
548 $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
549 foreach ( $ruleElements as $elt ) {
550 $rules[] = $elt->nodeValue;
551 }
552 foreach ( explode( ' ', $codes ) as $code ) {
553 $this->pluralRules[$code] = $rules;
554 }
555 }
556 }
557
558 /**
559 * Read the data from the source files for a given language, and register
560 * the relevant dependencies in the $deps array. If the localisation
561 * exists, the data array is returned, otherwise false is returned.
562 */
563 protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
564 $fileName = Language::getMessagesFileName( $code );
565 if ( !file_exists( $fileName ) ) {
566 return false;
567 }
568
569 $deps[] = new FileDependency( $fileName );
570 $data = $this->readPHPFile( $fileName, 'core' );
571
572 # Load CLDR plural rules for JavaScript
573 $data['pluralRules'] = $this->getPluralRules( $code );
574 # And for PHP
575 $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
576
577 $deps['plurals'] = new FileDependency( __DIR__ . "/../languages/data/plurals.xml" );
578 $deps['plurals-mw'] = new FileDependency( __DIR__ . "/../languages/data/plurals-mediawiki.xml" );
579 return $data;
580 }
581
582 /**
583 * Merge two localisation values, a primary and a fallback, overwriting the
584 * primary value in place.
585 * @param $key
586 * @param $value
587 * @param $fallbackValue
588 */
589 protected function mergeItem( $key, &$value, $fallbackValue ) {
590 if ( !is_null( $value ) ) {
591 if ( !is_null( $fallbackValue ) ) {
592 if ( in_array( $key, self::$mergeableMapKeys ) ) {
593 $value = $value + $fallbackValue;
594 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
595 $value = array_unique( array_merge( $fallbackValue, $value ) );
596 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
597 $value = array_merge_recursive( $value, $fallbackValue );
598 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
599 if ( !empty( $value['inherit'] ) ) {
600 $value = array_merge( $fallbackValue, $value );
601 }
602
603 if ( isset( $value['inherit'] ) ) {
604 unset( $value['inherit'] );
605 }
606 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
607 $this->mergeMagicWords( $value, $fallbackValue );
608 }
609 }
610 } else {
611 $value = $fallbackValue;
612 }
613 }
614
615 /**
616 * @param $value
617 * @param $fallbackValue
618 */
619 protected function mergeMagicWords( &$value, $fallbackValue ) {
620 foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
621 if ( !isset( $value[$magicName] ) ) {
622 $value[$magicName] = $fallbackInfo;
623 } else {
624 $oldSynonyms = array_slice( $fallbackInfo, 1 );
625 $newSynonyms = array_slice( $value[$magicName], 1 );
626 $synonyms = array_values( array_unique( array_merge(
627 $newSynonyms, $oldSynonyms ) ) );
628 $value[$magicName] = array_merge( array( $fallbackInfo[0] ), $synonyms );
629 }
630 }
631 }
632
633 /**
634 * Given an array mapping language code to localisation value, such as is
635 * found in extension *.i18n.php files, iterate through a fallback sequence
636 * to merge the given data with an existing primary value.
637 *
638 * Returns true if any data from the extension array was used, false
639 * otherwise.
640 * @param $codeSequence
641 * @param $key
642 * @param $value
643 * @param $fallbackValue
644 * @return bool
645 */
646 protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
647 $used = false;
648 foreach ( $codeSequence as $code ) {
649 if ( isset( $fallbackValue[$code] ) ) {
650 $this->mergeItem( $key, $value, $fallbackValue[$code] );
651 $used = true;
652 }
653 }
654
655 return $used;
656 }
657
658 /**
659 * Load localisation data for a given language for both core and extensions
660 * and save it to the persistent cache store and the process cache
661 * @param $code
662 */
663 public function recache( $code ) {
664 global $wgExtensionMessagesFiles;
665 wfProfileIn( __METHOD__ );
666
667 if ( !$code ) {
668 throw new MWException( "Invalid language code requested" );
669 }
670 $this->recachedLangs[$code] = true;
671
672 # Initial values
673 $initialData = array_combine(
674 self::$allKeys,
675 array_fill( 0, count( self::$allKeys ), null ) );
676 $coreData = $initialData;
677 $deps = array();
678
679 # Load the primary localisation from the source file
680 $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
681 if ( $data === false ) {
682 wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
683 $coreData['fallback'] = 'en';
684 } else {
685 wfDebug( __METHOD__ . ": got localisation for $code from source\n" );
686
687 # Merge primary localisation
688 foreach ( $data as $key => $value ) {
689 $this->mergeItem( $key, $coreData[$key], $value );
690 }
691
692 }
693
694 # Fill in the fallback if it's not there already
695 if ( is_null( $coreData['fallback'] ) ) {
696 $coreData['fallback'] = $code === 'en' ? false : 'en';
697 }
698 if ( $coreData['fallback'] === false ) {
699 $coreData['fallbackSequence'] = array();
700 } else {
701 $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
702 $len = count( $coreData['fallbackSequence'] );
703
704 # Ensure that the sequence ends at en
705 if ( $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
706 $coreData['fallbackSequence'][] = 'en';
707 }
708
709 # Load the fallback localisation item by item and merge it
710 foreach ( $coreData['fallbackSequence'] as $fbCode ) {
711 # Load the secondary localisation from the source file to
712 # avoid infinite cycles on cyclic fallbacks
713 $fbData = $this->readSourceFilesAndRegisterDeps( $fbCode, $deps );
714 if ( $fbData === false ) {
715 continue;
716 }
717
718 foreach ( self::$allKeys as $key ) {
719 if ( !isset( $fbData[$key] ) ) {
720 continue;
721 }
722
723 if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) {
724 $this->mergeItem( $key, $coreData[$key], $fbData[$key] );
725 }
726 }
727 }
728 }
729
730 $codeSequence = array_merge( array( $code ), $coreData['fallbackSequence'] );
731
732 # Load the extension localisations
733 # This is done after the core because we know the fallback sequence now.
734 # But it has a higher precedence for merging so that we can support things
735 # like site-specific message overrides.
736 $allData = $initialData;
737 foreach ( $wgExtensionMessagesFiles as $fileName ) {
738 $data = $this->readPHPFile( $fileName, 'extension' );
739 $used = false;
740
741 foreach ( $data as $key => $item ) {
742 if ( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
743 $used = true;
744 }
745 }
746
747 if ( $used ) {
748 $deps[] = new FileDependency( $fileName );
749 }
750 }
751
752 # Merge core data into extension data
753 foreach ( $coreData as $key => $item ) {
754 $this->mergeItem( $key, $allData[$key], $item );
755 }
756
757 # Add cache dependencies for any referenced globals
758 $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
759 $deps['version'] = new ConstantDependency( 'MW_LC_VERSION' );
760
761 # Add dependencies to the cache entry
762 $allData['deps'] = $deps;
763
764 # Replace spaces with underscores in namespace names
765 $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
766
767 # And do the same for special page aliases. $page is an array.
768 foreach ( $allData['specialPageAliases'] as &$page ) {
769 $page = str_replace( ' ', '_', $page );
770 }
771 # Decouple the reference to prevent accidental damage
772 unset( $page );
773
774 # If there were no plural rules, return an empty array
775 if ( $allData['pluralRules'] === null ) {
776 $allData['pluralRules'] = array();
777 }
778 if ( $allData['compiledPluralRules'] === null ) {
779 $allData['compiledPluralRules'] = array();
780 }
781
782 # Set the list keys
783 $allData['list'] = array();
784 foreach ( self::$splitKeys as $key ) {
785 $allData['list'][$key] = array_keys( $allData[$key] );
786 }
787 # Run hooks
788 wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) );
789
790 if ( is_null( $allData['namespaceNames'] ) ) {
791 throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
792 'Check that your languages/messages/MessagesEn.php file is intact.' );
793 }
794
795 # Set the preload key
796 $allData['preload'] = $this->buildPreload( $allData );
797
798 # Save to the process cache and register the items loaded
799 $this->data[$code] = $allData;
800 foreach ( $allData as $key => $item ) {
801 $this->loadedItems[$code][$key] = true;
802 }
803
804 # Save to the persistent cache
805 $this->store->startWrite( $code );
806 foreach ( $allData as $key => $value ) {
807 if ( in_array( $key, self::$splitKeys ) ) {
808 foreach ( $value as $subkey => $subvalue ) {
809 $this->store->set( "$key:$subkey", $subvalue );
810 }
811 } else {
812 $this->store->set( $key, $value );
813 }
814 }
815 $this->store->finishWrite();
816
817 # Clear out the MessageBlobStore
818 # HACK: If using a null (i.e. disabled) storage backend, we
819 # can't write to the MessageBlobStore either
820 if ( !$this->store instanceof LCStore_Null ) {
821 MessageBlobStore::clear();
822 }
823
824 wfProfileOut( __METHOD__ );
825 }
826
827 /**
828 * Build the preload item from the given pre-cache data.
829 *
830 * The preload item will be loaded automatically, improving performance
831 * for the commonly-requested items it contains.
832 * @param $data
833 * @return array
834 */
835 protected function buildPreload( $data ) {
836 $preload = array( 'messages' => array() );
837 foreach ( self::$preloadedKeys as $key ) {
838 $preload[$key] = $data[$key];
839 }
840
841 foreach ( $data['preloadedMessages'] as $subkey ) {
842 if ( isset( $data['messages'][$subkey] ) ) {
843 $subitem = $data['messages'][$subkey];
844 } else {
845 $subitem = null;
846 }
847 $preload['messages'][$subkey] = $subitem;
848 }
849
850 return $preload;
851 }
852
853 /**
854 * Unload the data for a given language from the object cache.
855 * Reduces memory usage.
856 * @param $code
857 */
858 public function unload( $code ) {
859 unset( $this->data[$code] );
860 unset( $this->loadedItems[$code] );
861 unset( $this->loadedSubitems[$code] );
862 unset( $this->initialisedLangs[$code] );
863
864 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
865 if ( $fbCode === $code ) {
866 $this->unload( $shallowCode );
867 }
868 }
869 }
870
871 /**
872 * Unload all data
873 */
874 public function unloadAll() {
875 foreach ( $this->initialisedLangs as $lang => $unused ) {
876 $this->unload( $lang );
877 }
878 }
879
880 /**
881 * Disable the storage backend
882 */
883 public function disableBackend() {
884 $this->store = new LCStore_Null;
885 $this->manualRecache = false;
886 }
887 }
888
889 /**
890 * Interface for the persistence layer of LocalisationCache.
891 *
892 * The persistence layer is two-level hierarchical cache. The first level
893 * is the language, the second level is the item or subitem.
894 *
895 * Since the data for a whole language is rebuilt in one operation, it needs
896 * to have a fast and atomic method for deleting or replacing all of the
897 * current data for a given language. The interface reflects this bulk update
898 * operation. Callers writing to the cache must first call startWrite(), then
899 * will call set() a couple of thousand times, then will call finishWrite()
900 * to commit the operation. When finishWrite() is called, the cache is
901 * expected to delete all data previously stored for that language.
902 *
903 * The values stored are PHP variables suitable for serialize(). Implementations
904 * of LCStore are responsible for serializing and unserializing.
905 */
906 interface LCStore {
907 /**
908 * Get a value.
909 * @param $code string Language code
910 * @param $key string Cache key
911 */
912 function get( $code, $key );
913
914 /**
915 * Start a write transaction.
916 * @param $code Language code
917 */
918 function startWrite( $code );
919
920 /**
921 * Finish a write transaction.
922 */
923 function finishWrite();
924
925 /**
926 * Set a key to a given value. startWrite() must be called before this
927 * is called, and finishWrite() must be called afterwards.
928 * @param $key
929 * @param $value
930 */
931 function set( $key, $value );
932 }
933
934 /**
935 * LCStore implementation which uses PHP accelerator to store data.
936 * This will work if one of XCache, WinCache or APC cacher is configured.
937 * (See ObjectCache.php)
938 */
939 class LCStore_Accel implements LCStore {
940 var $currentLang;
941 var $keys;
942
943 public function __construct() {
944 $this->cache = wfGetCache( CACHE_ACCEL );
945 }
946
947 public function get( $code, $key ) {
948 $k = wfMemcKey( 'l10n', $code, 'k', $key );
949 $r = $this->cache->get( $k );
950 return $r === false ? null : $r;
951 }
952
953 public function startWrite( $code ) {
954 $k = wfMemcKey( 'l10n', $code, 'l' );
955 $keys = $this->cache->get( $k );
956 if ( $keys ) {
957 foreach ( $keys as $k ) {
958 $this->cache->delete( $k );
959 }
960 }
961 $this->currentLang = $code;
962 $this->keys = array();
963 }
964
965 public function finishWrite() {
966 if ( $this->currentLang ) {
967 $k = wfMemcKey( 'l10n', $this->currentLang, 'l' );
968 $this->cache->set( $k, array_keys( $this->keys ) );
969 }
970 $this->currentLang = null;
971 $this->keys = array();
972 }
973
974 public function set( $key, $value ) {
975 if ( $this->currentLang ) {
976 $k = wfMemcKey( 'l10n', $this->currentLang, 'k', $key );
977 $this->keys[$k] = true;
978 $this->cache->set( $k, $value );
979 }
980 }
981 }
982
983 /**
984 * LCStore implementation which uses the standard DB functions to store data.
985 * This will work on any MediaWiki installation.
986 */
987 class LCStore_DB implements LCStore {
988 var $currentLang;
989 var $writesDone = false;
990
991 /**
992 * @var DatabaseBase
993 */
994 var $dbw;
995 var $batch;
996 var $readOnly = false;
997
998 public function get( $code, $key ) {
999 if ( $this->writesDone ) {
1000 $db = wfGetDB( DB_MASTER );
1001 } else {
1002 $db = wfGetDB( DB_SLAVE );
1003 }
1004 $row = $db->selectRow( 'l10n_cache', array( 'lc_value' ),
1005 array( 'lc_lang' => $code, 'lc_key' => $key ), __METHOD__ );
1006 if ( $row ) {
1007 return unserialize( $row->lc_value );
1008 } else {
1009 return null;
1010 }
1011 }
1012
1013 public function startWrite( $code ) {
1014 if ( $this->readOnly ) {
1015 return;
1016 }
1017
1018 if ( !$code ) {
1019 throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
1020 }
1021
1022 $this->dbw = wfGetDB( DB_MASTER );
1023 try {
1024 $this->dbw->begin( __METHOD__ );
1025 $this->dbw->delete( 'l10n_cache', array( 'lc_lang' => $code ), __METHOD__ );
1026 } catch ( DBQueryError $e ) {
1027 if ( $this->dbw->wasReadOnlyError() ) {
1028 $this->readOnly = true;
1029 $this->dbw->rollback( __METHOD__ );
1030 $this->dbw->ignoreErrors( false );
1031 return;
1032 } else {
1033 throw $e;
1034 }
1035 }
1036
1037 $this->currentLang = $code;
1038 $this->batch = array();
1039 }
1040
1041 public function finishWrite() {
1042 if ( $this->readOnly ) {
1043 return;
1044 }
1045
1046 if ( $this->batch ) {
1047 $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
1048 }
1049
1050 $this->dbw->commit( __METHOD__ );
1051 $this->currentLang = null;
1052 $this->dbw = null;
1053 $this->batch = array();
1054 $this->writesDone = true;
1055 }
1056
1057 public function set( $key, $value ) {
1058 if ( $this->readOnly ) {
1059 return;
1060 }
1061
1062 if ( is_null( $this->currentLang ) ) {
1063 throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
1064 }
1065
1066 $this->batch[] = array(
1067 'lc_lang' => $this->currentLang,
1068 'lc_key' => $key,
1069 'lc_value' => serialize( $value ) );
1070
1071 if ( count( $this->batch ) >= 100 ) {
1072 $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
1073 $this->batch = array();
1074 }
1075 }
1076 }
1077
1078 /**
1079 * LCStore implementation which stores data as a collection of CDB files in the
1080 * directory given by $wgCacheDirectory. If $wgCacheDirectory is not set, this
1081 * will throw an exception.
1082 *
1083 * Profiling indicates that on Linux, this implementation outperforms MySQL if
1084 * the directory is on a local filesystem and there is ample kernel cache
1085 * space. The performance advantage is greater when the DBA extension is
1086 * available than it is with the PHP port.
1087 *
1088 * See Cdb.php and http://cr.yp.to/cdb.html
1089 */
1090 class LCStore_CDB implements LCStore {
1091 var $readers, $writer, $currentLang, $directory;
1092
1093 function __construct( $conf = array() ) {
1094 global $wgCacheDirectory;
1095
1096 if ( isset( $conf['directory'] ) ) {
1097 $this->directory = $conf['directory'];
1098 } else {
1099 $this->directory = $wgCacheDirectory;
1100 }
1101 }
1102
1103 public function get( $code, $key ) {
1104 if ( !isset( $this->readers[$code] ) ) {
1105 $fileName = $this->getFileName( $code );
1106
1107 if ( !file_exists( $fileName ) ) {
1108 $this->readers[$code] = false;
1109 } else {
1110 $this->readers[$code] = CdbReader::open( $fileName );
1111 }
1112 }
1113
1114 if ( !$this->readers[$code] ) {
1115 return null;
1116 } else {
1117 $value = $this->readers[$code]->get( $key );
1118
1119 if ( $value === false ) {
1120 return null;
1121 }
1122 return unserialize( $value );
1123 }
1124 }
1125
1126 public function startWrite( $code ) {
1127 if ( !file_exists( $this->directory ) ) {
1128 if ( !wfMkdirParents( $this->directory, null, __METHOD__ ) ) {
1129 throw new MWException( "Unable to create the localisation store " .
1130 "directory \"{$this->directory}\"" );
1131 }
1132 }
1133
1134 // Close reader to stop permission errors on write
1135 if ( !empty( $this->readers[$code] ) ) {
1136 $this->readers[$code]->close();
1137 }
1138
1139 $this->writer = CdbWriter::open( $this->getFileName( $code ) );
1140 $this->currentLang = $code;
1141 }
1142
1143 public function finishWrite() {
1144 // Close the writer
1145 $this->writer->close();
1146 $this->writer = null;
1147 unset( $this->readers[$this->currentLang] );
1148 $this->currentLang = null;
1149 }
1150
1151 public function set( $key, $value ) {
1152 if ( is_null( $this->writer ) ) {
1153 throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
1154 }
1155 $this->writer->set( $key, serialize( $value ) );
1156 }
1157
1158 protected function getFileName( $code ) {
1159 if ( !$code || strpos( $code, '/' ) !== false ) {
1160 throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
1161 }
1162 return "{$this->directory}/l10n_cache-$code.cdb";
1163 }
1164 }
1165
1166 /**
1167 * Null store backend, used to avoid DB errors during install
1168 */
1169 class LCStore_Null implements LCStore {
1170 public function get( $code, $key ) {
1171 return null;
1172 }
1173
1174 public function startWrite( $code ) {}
1175 public function finishWrite() {}
1176 public function set( $key, $value ) {}
1177 }
1178
1179 /**
1180 * A localisation cache optimised for loading large amounts of data for many
1181 * languages. Used by rebuildLocalisationCache.php.
1182 */
1183 class LocalisationCache_BulkLoad extends LocalisationCache {
1184 /**
1185 * A cache of the contents of data files.
1186 * Core files are serialized to avoid using ~1GB of RAM during a recache.
1187 */
1188 var $fileCache = array();
1189
1190 /**
1191 * Most recently used languages. Uses the linked-list aspect of PHP hashtables
1192 * to keep the most recently used language codes at the end of the array, and
1193 * the language codes that are ready to be deleted at the beginning.
1194 */
1195 var $mruLangs = array();
1196
1197 /**
1198 * Maximum number of languages that may be loaded into $this->data
1199 */
1200 var $maxLoadedLangs = 10;
1201
1202 /**
1203 * @param $fileName
1204 * @param $fileType
1205 * @return array|mixed
1206 */
1207 protected function readPHPFile( $fileName, $fileType ) {
1208 $serialize = $fileType === 'core';
1209 if ( !isset( $this->fileCache[$fileName][$fileType] ) ) {
1210 $data = parent::readPHPFile( $fileName, $fileType );
1211
1212 if ( $serialize ) {
1213 $encData = serialize( $data );
1214 } else {
1215 $encData = $data;
1216 }
1217
1218 $this->fileCache[$fileName][$fileType] = $encData;
1219
1220 return $data;
1221 } elseif ( $serialize ) {
1222 return unserialize( $this->fileCache[$fileName][$fileType] );
1223 } else {
1224 return $this->fileCache[$fileName][$fileType];
1225 }
1226 }
1227
1228 /**
1229 * @param $code
1230 * @param $key
1231 * @return mixed
1232 */
1233 public function getItem( $code, $key ) {
1234 unset( $this->mruLangs[$code] );
1235 $this->mruLangs[$code] = true;
1236 return parent::getItem( $code, $key );
1237 }
1238
1239 /**
1240 * @param $code
1241 * @param $key
1242 * @param $subkey
1243 * @return
1244 */
1245 public function getSubitem( $code, $key, $subkey ) {
1246 unset( $this->mruLangs[$code] );
1247 $this->mruLangs[$code] = true;
1248 return parent::getSubitem( $code, $key, $subkey );
1249 }
1250
1251 /**
1252 * @param $code
1253 */
1254 public function recache( $code ) {
1255 parent::recache( $code );
1256 unset( $this->mruLangs[$code] );
1257 $this->mruLangs[$code] = true;
1258 $this->trimCache();
1259 }
1260
1261 /**
1262 * @param $code
1263 */
1264 public function unload( $code ) {
1265 unset( $this->mruLangs[$code] );
1266 parent::unload( $code );
1267 }
1268
1269 /**
1270 * Unload cached languages until there are less than $this->maxLoadedLangs
1271 */
1272 protected function trimCache() {
1273 while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) {
1274 reset( $this->mruLangs );
1275 $code = key( $this->mruLangs );
1276 wfDebug( __METHOD__ . ": unloading $code\n" );
1277 $this->unload( $code );
1278 }
1279 }
1280
1281 }