MediaWiki
REL1_23
|
00001 <?php 00023 define( 'MW_LC_VERSION', 2 ); 00024 00037 class LocalisationCache { 00039 private $conf; 00040 00046 private $manualRecache = false; 00047 00051 private $forceRecache = false; 00052 00059 protected $data = array(); 00060 00066 private $store; 00067 00075 private $loadedItems = array(); 00076 00081 private $loadedSubitems = array(); 00082 00088 private $initialisedLangs = array(); 00089 00095 private $shallowFallbacks = array(); 00096 00100 private $recachedLangs = array(); 00101 00105 static public $allKeys = array( 00106 'fallback', 'namespaceNames', 'bookstoreList', 00107 'magicWords', 'messages', 'rtl', 'capitalizeAllNouns', 'digitTransformTable', 00108 'separatorTransformTable', 'fallback8bitEncoding', 'linkPrefixExtension', 00109 'linkTrail', 'linkPrefixCharset', 'namespaceAliases', 00110 'dateFormats', 'datePreferences', 'datePreferenceMigrationMap', 00111 'defaultDateFormat', 'extraUserToggles', 'specialPageAliases', 00112 'imageFiles', 'preloadedMessages', 'namespaceGenderAliases', 00113 'digitGroupingPattern', 'pluralRules', 'pluralRuleTypes', 'compiledPluralRules', 00114 ); 00115 00120 static public $mergeableMapKeys = array( 'messages', 'namespaceNames', 00121 'dateFormats', 'imageFiles', 'preloadedMessages' 00122 ); 00123 00127 static public $mergeableListKeys = array( 'extraUserToggles' ); 00128 00133 static public $mergeableAliasListKeys = array( 'specialPageAliases' ); 00134 00140 static public $optionalMergeKeys = array( 'bookstoreList' ); 00141 00145 static public $magicWordKeys = array( 'magicWords' ); 00146 00150 static public $splitKeys = array( 'messages' ); 00151 00155 static public $preloadedKeys = array( 'dateFormats', 'namespaceNames' ); 00156 00161 private $pluralRules = null; 00162 00175 private $pluralRuleTypes = null; 00176 00177 private $mergeableKeys = null; 00178 00187 function __construct( $conf ) { 00188 global $wgCacheDirectory; 00189 00190 $this->conf = $conf; 00191 $storeConf = array(); 00192 if ( !empty( $conf['storeClass'] ) ) { 00193 $storeClass = $conf['storeClass']; 00194 } else { 00195 switch ( $conf['store'] ) { 00196 case 'files': 00197 case 'file': 00198 $storeClass = 'LCStoreCDB'; 00199 break; 00200 case 'db': 00201 $storeClass = 'LCStoreDB'; 00202 break; 00203 case 'accel': 00204 $storeClass = 'LCStoreAccel'; 00205 break; 00206 case 'detect': 00207 $storeClass = $wgCacheDirectory ? 'LCStoreCDB' : 'LCStoreDB'; 00208 break; 00209 default: 00210 throw new MWException( 00211 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' ); 00212 } 00213 } 00214 00215 wfDebugLog( 'caches', get_class( $this ) . ": using store $storeClass" ); 00216 if ( !empty( $conf['storeDirectory'] ) ) { 00217 $storeConf['directory'] = $conf['storeDirectory']; 00218 } 00219 00220 $this->store = new $storeClass( $storeConf ); 00221 foreach ( array( 'manualRecache', 'forceRecache' ) as $var ) { 00222 if ( isset( $conf[$var] ) ) { 00223 $this->$var = $conf[$var]; 00224 } 00225 } 00226 } 00227 00234 public function isMergeableKey( $key ) { 00235 if ( $this->mergeableKeys === null ) { 00236 $this->mergeableKeys = array_flip( array_merge( 00237 self::$mergeableMapKeys, 00238 self::$mergeableListKeys, 00239 self::$mergeableAliasListKeys, 00240 self::$optionalMergeKeys, 00241 self::$magicWordKeys 00242 ) ); 00243 } 00244 00245 return isset( $this->mergeableKeys[$key] ); 00246 } 00247 00257 public function getItem( $code, $key ) { 00258 if ( !isset( $this->loadedItems[$code][$key] ) ) { 00259 wfProfileIn( __METHOD__ . '-load' ); 00260 $this->loadItem( $code, $key ); 00261 wfProfileOut( __METHOD__ . '-load' ); 00262 } 00263 00264 if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) { 00265 return $this->shallowFallbacks[$code]; 00266 } 00267 00268 return $this->data[$code][$key]; 00269 } 00270 00278 public function getSubitem( $code, $key, $subkey ) { 00279 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) && 00280 !isset( $this->loadedItems[$code][$key] ) 00281 ) { 00282 wfProfileIn( __METHOD__ . '-load' ); 00283 $this->loadSubitem( $code, $key, $subkey ); 00284 wfProfileOut( __METHOD__ . '-load' ); 00285 } 00286 00287 if ( isset( $this->data[$code][$key][$subkey] ) ) { 00288 return $this->data[$code][$key][$subkey]; 00289 } else { 00290 return null; 00291 } 00292 } 00293 00306 public function getSubitemList( $code, $key ) { 00307 if ( in_array( $key, self::$splitKeys ) ) { 00308 return $this->getSubitem( $code, 'list', $key ); 00309 } else { 00310 $item = $this->getItem( $code, $key ); 00311 if ( is_array( $item ) ) { 00312 return array_keys( $item ); 00313 } else { 00314 return false; 00315 } 00316 } 00317 } 00318 00324 protected function loadItem( $code, $key ) { 00325 if ( !isset( $this->initialisedLangs[$code] ) ) { 00326 $this->initLanguage( $code ); 00327 } 00328 00329 // Check to see if initLanguage() loaded it for us 00330 if ( isset( $this->loadedItems[$code][$key] ) ) { 00331 return; 00332 } 00333 00334 if ( isset( $this->shallowFallbacks[$code] ) ) { 00335 $this->loadItem( $this->shallowFallbacks[$code], $key ); 00336 00337 return; 00338 } 00339 00340 if ( in_array( $key, self::$splitKeys ) ) { 00341 $subkeyList = $this->getSubitem( $code, 'list', $key ); 00342 foreach ( $subkeyList as $subkey ) { 00343 if ( isset( $this->data[$code][$key][$subkey] ) ) { 00344 continue; 00345 } 00346 $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey ); 00347 } 00348 } else { 00349 $this->data[$code][$key] = $this->store->get( $code, $key ); 00350 } 00351 00352 $this->loadedItems[$code][$key] = true; 00353 } 00354 00361 protected function loadSubitem( $code, $key, $subkey ) { 00362 if ( !in_array( $key, self::$splitKeys ) ) { 00363 $this->loadItem( $code, $key ); 00364 00365 return; 00366 } 00367 00368 if ( !isset( $this->initialisedLangs[$code] ) ) { 00369 $this->initLanguage( $code ); 00370 } 00371 00372 // Check to see if initLanguage() loaded it for us 00373 if ( isset( $this->loadedItems[$code][$key] ) || 00374 isset( $this->loadedSubitems[$code][$key][$subkey] ) 00375 ) { 00376 return; 00377 } 00378 00379 if ( isset( $this->shallowFallbacks[$code] ) ) { 00380 $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey ); 00381 00382 return; 00383 } 00384 00385 $value = $this->store->get( $code, "$key:$subkey" ); 00386 $this->data[$code][$key][$subkey] = $value; 00387 $this->loadedSubitems[$code][$key][$subkey] = true; 00388 } 00389 00397 public function isExpired( $code ) { 00398 if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) { 00399 wfDebug( __METHOD__ . "($code): forced reload\n" ); 00400 00401 return true; 00402 } 00403 00404 $deps = $this->store->get( $code, 'deps' ); 00405 $keys = $this->store->get( $code, 'list' ); 00406 $preload = $this->store->get( $code, 'preload' ); 00407 // Different keys may expire separately, at least in LCStoreAccel 00408 if ( $deps === null || $keys === null || $preload === null ) { 00409 wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" ); 00410 00411 return true; 00412 } 00413 00414 foreach ( $deps as $dep ) { 00415 // Because we're unserializing stuff from cache, we 00416 // could receive objects of classes that don't exist 00417 // anymore (e.g. uninstalled extensions) 00418 // When this happens, always expire the cache 00419 if ( !$dep instanceof CacheDependency || $dep->isExpired() ) { 00420 wfDebug( __METHOD__ . "($code): cache for $code expired due to " . 00421 get_class( $dep ) . "\n" ); 00422 00423 return true; 00424 } 00425 } 00426 00427 return false; 00428 } 00429 00435 protected function initLanguage( $code ) { 00436 if ( isset( $this->initialisedLangs[$code] ) ) { 00437 return; 00438 } 00439 00440 $this->initialisedLangs[$code] = true; 00441 00442 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback 00443 if ( !Language::isValidBuiltInCode( $code ) ) { 00444 $this->initShallowFallback( $code, 'en' ); 00445 00446 return; 00447 } 00448 00449 # Recache the data if necessary 00450 if ( !$this->manualRecache && $this->isExpired( $code ) ) { 00451 if ( Language::isSupportedLanguage( $code ) ) { 00452 $this->recache( $code ); 00453 } elseif ( $code === 'en' ) { 00454 throw new MWException( 'MessagesEn.php is missing.' ); 00455 } else { 00456 $this->initShallowFallback( $code, 'en' ); 00457 } 00458 00459 return; 00460 } 00461 00462 # Preload some stuff 00463 $preload = $this->getItem( $code, 'preload' ); 00464 if ( $preload === null ) { 00465 if ( $this->manualRecache ) { 00466 // No Messages*.php file. Do shallow fallback to en. 00467 if ( $code === 'en' ) { 00468 throw new MWException( 'No localisation cache found for English. ' . 00469 'Please run maintenance/rebuildLocalisationCache.php.' ); 00470 } 00471 $this->initShallowFallback( $code, 'en' ); 00472 00473 return; 00474 } else { 00475 throw new MWException( 'Invalid or missing localisation cache.' ); 00476 } 00477 } 00478 $this->data[$code] = $preload; 00479 foreach ( $preload as $key => $item ) { 00480 if ( in_array( $key, self::$splitKeys ) ) { 00481 foreach ( $item as $subkey => $subitem ) { 00482 $this->loadedSubitems[$code][$key][$subkey] = true; 00483 } 00484 } else { 00485 $this->loadedItems[$code][$key] = true; 00486 } 00487 } 00488 } 00489 00496 public function initShallowFallback( $primaryCode, $fallbackCode ) { 00497 $this->data[$primaryCode] =& $this->data[$fallbackCode]; 00498 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode]; 00499 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode]; 00500 $this->shallowFallbacks[$primaryCode] = $fallbackCode; 00501 } 00502 00510 protected function readPHPFile( $_fileName, $_fileType ) { 00511 wfProfileIn( __METHOD__ ); 00512 // Disable APC caching 00513 wfSuppressWarnings(); 00514 $_apcEnabled = ini_set( 'apc.cache_by_default', '0' ); 00515 wfRestoreWarnings(); 00516 00517 include $_fileName; 00518 00519 wfSuppressWarnings(); 00520 ini_set( 'apc.cache_by_default', $_apcEnabled ); 00521 wfRestoreWarnings(); 00522 00523 if ( $_fileType == 'core' || $_fileType == 'extension' ) { 00524 $data = compact( self::$allKeys ); 00525 } elseif ( $_fileType == 'aliases' ) { 00526 $data = compact( 'aliases' ); 00527 } else { 00528 wfProfileOut( __METHOD__ ); 00529 throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" ); 00530 } 00531 wfProfileOut( __METHOD__ ); 00532 00533 return $data; 00534 } 00535 00542 public function readJSONFile( $fileName ) { 00543 wfProfileIn( __METHOD__ ); 00544 00545 if ( !is_readable( $fileName ) ) { 00546 wfProfileOut( __METHOD__ ); 00547 00548 return array(); 00549 } 00550 00551 $json = file_get_contents( $fileName ); 00552 if ( $json === false ) { 00553 wfProfileOut( __METHOD__ ); 00554 00555 return array(); 00556 } 00557 00558 $data = FormatJson::decode( $json, true ); 00559 if ( $data === null ) { 00560 wfProfileOut( __METHOD__ ); 00561 00562 throw new MWException( __METHOD__ . ": Invalid JSON file: $fileName" ); 00563 } 00564 00565 // Remove keys starting with '@', they're reserved for metadata and non-message data 00566 foreach ( $data as $key => $unused ) { 00567 if ( $key === '' || $key[0] === '@' ) { 00568 unset( $data[$key] ); 00569 } 00570 } 00571 00572 wfProfileOut( __METHOD__ ); 00573 00574 // The JSON format only supports messages, none of the other variables, so wrap the data 00575 return array( 'messages' => $data ); 00576 } 00577 00582 public function getCompiledPluralRules( $code ) { 00583 $rules = $this->getPluralRules( $code ); 00584 if ( $rules === null ) { 00585 return null; 00586 } 00587 try { 00588 $compiledRules = CLDRPluralRuleEvaluator::compile( $rules ); 00589 } catch ( CLDRPluralRuleError $e ) { 00590 wfDebugLog( 'l10n', $e->getMessage() ); 00591 00592 return array(); 00593 } 00594 00595 return $compiledRules; 00596 } 00597 00603 public function getPluralRules( $code ) { 00604 if ( $this->pluralRules === null ) { 00605 $this->loadPluralFiles(); 00606 } 00607 if ( !isset( $this->pluralRules[$code] ) ) { 00608 return null; 00609 } else { 00610 return $this->pluralRules[$code]; 00611 } 00612 } 00613 00619 public function getPluralRuleTypes( $code ) { 00620 if ( $this->pluralRuleTypes === null ) { 00621 $this->loadPluralFiles(); 00622 } 00623 if ( !isset( $this->pluralRuleTypes[$code] ) ) { 00624 return null; 00625 } else { 00626 return $this->pluralRuleTypes[$code]; 00627 } 00628 } 00629 00633 protected function loadPluralFiles() { 00634 global $IP; 00635 $cldrPlural = "$IP/languages/data/plurals.xml"; 00636 $mwPlural = "$IP/languages/data/plurals-mediawiki.xml"; 00637 // Load CLDR plural rules 00638 $this->loadPluralFile( $cldrPlural ); 00639 if ( file_exists( $mwPlural ) ) { 00640 // Override or extend 00641 $this->loadPluralFile( $mwPlural ); 00642 } 00643 } 00644 00649 protected function loadPluralFile( $fileName ) { 00650 $doc = new DOMDocument; 00651 $doc->load( $fileName ); 00652 $rulesets = $doc->getElementsByTagName( "pluralRules" ); 00653 foreach ( $rulesets as $ruleset ) { 00654 $codes = $ruleset->getAttribute( 'locales' ); 00655 $rules = array(); 00656 $ruleTypes = array(); 00657 $ruleElements = $ruleset->getElementsByTagName( "pluralRule" ); 00658 foreach ( $ruleElements as $elt ) { 00659 $ruleType = $elt->getAttribute( 'count' ); 00660 if ( $ruleType === 'other' ) { 00661 // Don't record "other" rules, which have an empty condition 00662 continue; 00663 } 00664 $rules[] = $elt->nodeValue; 00665 $ruleTypes[] = $ruleType; 00666 } 00667 foreach ( explode( ' ', $codes ) as $code ) { 00668 $this->pluralRules[$code] = $rules; 00669 $this->pluralRuleTypes[$code] = $ruleTypes; 00670 } 00671 } 00672 } 00673 00679 protected function readSourceFilesAndRegisterDeps( $code, &$deps ) { 00680 global $IP; 00681 wfProfileIn( __METHOD__ ); 00682 00683 00684 // This reads in the PHP i18n file with non-messages l10n data 00685 $fileName = Language::getMessagesFileName( $code ); 00686 if ( !file_exists( $fileName ) ) { 00687 $data = array(); 00688 } else { 00689 $deps[] = new FileDependency( $fileName ); 00690 $data = $this->readPHPFile( $fileName, 'core' ); 00691 } 00692 00693 # Load CLDR plural rules for JavaScript 00694 $data['pluralRules'] = $this->getPluralRules( $code ); 00695 # And for PHP 00696 $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code ); 00697 # Load plural rule types 00698 $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code ); 00699 00700 $deps['plurals'] = new FileDependency( "$IP/languages/data/plurals.xml" ); 00701 $deps['plurals-mw'] = new FileDependency( "$IP/languages/data/plurals-mediawiki.xml" ); 00702 00703 wfProfileOut( __METHOD__ ); 00704 00705 return $data; 00706 } 00707 00715 protected function mergeItem( $key, &$value, $fallbackValue ) { 00716 if ( !is_null( $value ) ) { 00717 if ( !is_null( $fallbackValue ) ) { 00718 if ( in_array( $key, self::$mergeableMapKeys ) ) { 00719 $value = $value + $fallbackValue; 00720 } elseif ( in_array( $key, self::$mergeableListKeys ) ) { 00721 $value = array_unique( array_merge( $fallbackValue, $value ) ); 00722 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) { 00723 $value = array_merge_recursive( $value, $fallbackValue ); 00724 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) { 00725 if ( !empty( $value['inherit'] ) ) { 00726 $value = array_merge( $fallbackValue, $value ); 00727 } 00728 00729 if ( isset( $value['inherit'] ) ) { 00730 unset( $value['inherit'] ); 00731 } 00732 } elseif ( in_array( $key, self::$magicWordKeys ) ) { 00733 $this->mergeMagicWords( $value, $fallbackValue ); 00734 } 00735 } 00736 } else { 00737 $value = $fallbackValue; 00738 } 00739 } 00740 00745 protected function mergeMagicWords( &$value, $fallbackValue ) { 00746 foreach ( $fallbackValue as $magicName => $fallbackInfo ) { 00747 if ( !isset( $value[$magicName] ) ) { 00748 $value[$magicName] = $fallbackInfo; 00749 } else { 00750 $oldSynonyms = array_slice( $fallbackInfo, 1 ); 00751 $newSynonyms = array_slice( $value[$magicName], 1 ); 00752 $synonyms = array_values( array_unique( array_merge( 00753 $newSynonyms, $oldSynonyms ) ) ); 00754 $value[$magicName] = array_merge( array( $fallbackInfo[0] ), $synonyms ); 00755 } 00756 } 00757 } 00758 00772 protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) { 00773 $used = false; 00774 foreach ( $codeSequence as $code ) { 00775 if ( isset( $fallbackValue[$code] ) ) { 00776 $this->mergeItem( $key, $value, $fallbackValue[$code] ); 00777 $used = true; 00778 } 00779 } 00780 00781 return $used; 00782 } 00783 00790 public function recache( $code ) { 00791 global $wgExtensionMessagesFiles, $wgMessagesDirs; 00792 wfProfileIn( __METHOD__ ); 00793 00794 if ( !$code ) { 00795 wfProfileOut( __METHOD__ ); 00796 throw new MWException( "Invalid language code requested" ); 00797 } 00798 $this->recachedLangs[$code] = true; 00799 00800 # Initial values 00801 $initialData = array_combine( 00802 self::$allKeys, 00803 array_fill( 0, count( self::$allKeys ), null ) ); 00804 $coreData = $initialData; 00805 $deps = array(); 00806 00807 # Load the primary localisation from the source file 00808 $data = $this->readSourceFilesAndRegisterDeps( $code, $deps ); 00809 if ( $data === false ) { 00810 wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" ); 00811 $coreData['fallback'] = 'en'; 00812 } else { 00813 wfDebug( __METHOD__ . ": got localisation for $code from source\n" ); 00814 00815 # Merge primary localisation 00816 foreach ( $data as $key => $value ) { 00817 $this->mergeItem( $key, $coreData[$key], $value ); 00818 } 00819 } 00820 00821 # Fill in the fallback if it's not there already 00822 if ( is_null( $coreData['fallback'] ) ) { 00823 $coreData['fallback'] = $code === 'en' ? false : 'en'; 00824 } 00825 if ( $coreData['fallback'] === false ) { 00826 $coreData['fallbackSequence'] = array(); 00827 } else { 00828 $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) ); 00829 $len = count( $coreData['fallbackSequence'] ); 00830 00831 # Ensure that the sequence ends at en 00832 if ( $coreData['fallbackSequence'][$len - 1] !== 'en' ) { 00833 $coreData['fallbackSequence'][] = 'en'; 00834 } 00835 00836 # Load the fallback localisation item by item and merge it 00837 foreach ( $coreData['fallbackSequence'] as $fbCode ) { 00838 # Load the secondary localisation from the source file to 00839 # avoid infinite cycles on cyclic fallbacks 00840 $fbData = $this->readSourceFilesAndRegisterDeps( $fbCode, $deps ); 00841 if ( $fbData === false ) { 00842 continue; 00843 } 00844 00845 foreach ( self::$allKeys as $key ) { 00846 if ( !isset( $fbData[$key] ) ) { 00847 continue; 00848 } 00849 00850 if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) { 00851 $this->mergeItem( $key, $coreData[$key], $fbData[$key] ); 00852 } 00853 } 00854 } 00855 } 00856 00857 $codeSequence = array_merge( array( $code ), $coreData['fallbackSequence'] ); 00858 00859 # Load core messages and the extension localisations. 00860 wfProfileIn( __METHOD__ . '-extensions' ); 00861 $allData = $initialData; 00862 foreach ( $wgMessagesDirs as $dirs ) { 00863 foreach ( (array)$dirs as $dir ) { 00864 foreach ( $codeSequence as $csCode ) { 00865 $fileName = "$dir/$csCode.json"; 00866 $data = $this->readJSONFile( $fileName ); 00867 00868 foreach ( $data as $key => $item ) { 00869 $this->mergeItem( $key, $allData[$key], $item ); 00870 } 00871 00872 $deps[] = new FileDependency( $fileName ); 00873 } 00874 } 00875 } 00876 00877 foreach ( $wgExtensionMessagesFiles as $extension => $fileName ) { 00878 if ( isset( $wgMessagesDirs[$extension] ) ) { 00879 # Already loaded the JSON files for this extension; skip the PHP shim 00880 continue; 00881 } 00882 00883 $data = $this->readPHPFile( $fileName, 'extension' ); 00884 $used = false; 00885 00886 foreach ( $data as $key => $item ) { 00887 if ( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) { 00888 $used = true; 00889 } 00890 } 00891 00892 if ( $used ) { 00893 $deps[] = new FileDependency( $fileName ); 00894 } 00895 } 00896 00897 # Merge core data into extension data 00898 foreach ( $coreData as $key => $item ) { 00899 $this->mergeItem( $key, $allData[$key], $item ); 00900 } 00901 wfProfileOut( __METHOD__ . '-extensions' ); 00902 00903 # Add cache dependencies for any referenced globals 00904 $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' ); 00905 $deps['wgMessagesDirs'] = new GlobalDependency( 'wgMessagesDirs' ); 00906 $deps['version'] = new ConstantDependency( 'MW_LC_VERSION' ); 00907 00908 # Add dependencies to the cache entry 00909 $allData['deps'] = $deps; 00910 00911 # Replace spaces with underscores in namespace names 00912 $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] ); 00913 00914 # And do the same for special page aliases. $page is an array. 00915 foreach ( $allData['specialPageAliases'] as &$page ) { 00916 $page = str_replace( ' ', '_', $page ); 00917 } 00918 # Decouple the reference to prevent accidental damage 00919 unset( $page ); 00920 00921 # If there were no plural rules, return an empty array 00922 if ( $allData['pluralRules'] === null ) { 00923 $allData['pluralRules'] = array(); 00924 } 00925 if ( $allData['compiledPluralRules'] === null ) { 00926 $allData['compiledPluralRules'] = array(); 00927 } 00928 # If there were no plural rule types, return an empty array 00929 if ( $allData['pluralRuleTypes'] === null ) { 00930 $allData['pluralRuleTypes'] = array(); 00931 } 00932 00933 # Set the list keys 00934 $allData['list'] = array(); 00935 foreach ( self::$splitKeys as $key ) { 00936 $allData['list'][$key] = array_keys( $allData[$key] ); 00937 } 00938 # Run hooks 00939 $purgeBlobs = true; 00940 wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData, &$purgeBlobs ) ); 00941 00942 if ( is_null( $allData['namespaceNames'] ) ) { 00943 wfProfileOut( __METHOD__ ); 00944 throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' . 00945 'Check that your languages/messages/MessagesEn.php file is intact.' ); 00946 } 00947 00948 # Set the preload key 00949 $allData['preload'] = $this->buildPreload( $allData ); 00950 00951 # Save to the process cache and register the items loaded 00952 $this->data[$code] = $allData; 00953 foreach ( $allData as $key => $item ) { 00954 $this->loadedItems[$code][$key] = true; 00955 } 00956 00957 # Save to the persistent cache 00958 wfProfileIn( __METHOD__ . '-write' ); 00959 $this->store->startWrite( $code ); 00960 foreach ( $allData as $key => $value ) { 00961 if ( in_array( $key, self::$splitKeys ) ) { 00962 foreach ( $value as $subkey => $subvalue ) { 00963 $this->store->set( "$key:$subkey", $subvalue ); 00964 } 00965 } else { 00966 $this->store->set( $key, $value ); 00967 } 00968 } 00969 $this->store->finishWrite(); 00970 wfProfileOut( __METHOD__ . '-write' ); 00971 00972 # Clear out the MessageBlobStore 00973 # HACK: If using a null (i.e. disabled) storage backend, we 00974 # can't write to the MessageBlobStore either 00975 if ( $purgeBlobs && !$this->store instanceof LCStoreNull ) { 00976 MessageBlobStore::clear(); 00977 } 00978 00979 wfProfileOut( __METHOD__ ); 00980 } 00981 00990 protected function buildPreload( $data ) { 00991 $preload = array( 'messages' => array() ); 00992 foreach ( self::$preloadedKeys as $key ) { 00993 $preload[$key] = $data[$key]; 00994 } 00995 00996 foreach ( $data['preloadedMessages'] as $subkey ) { 00997 if ( isset( $data['messages'][$subkey] ) ) { 00998 $subitem = $data['messages'][$subkey]; 00999 } else { 01000 $subitem = null; 01001 } 01002 $preload['messages'][$subkey] = $subitem; 01003 } 01004 01005 return $preload; 01006 } 01007 01013 public function unload( $code ) { 01014 unset( $this->data[$code] ); 01015 unset( $this->loadedItems[$code] ); 01016 unset( $this->loadedSubitems[$code] ); 01017 unset( $this->initialisedLangs[$code] ); 01018 unset( $this->shallowFallbacks[$code] ); 01019 01020 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) { 01021 if ( $fbCode === $code ) { 01022 $this->unload( $shallowCode ); 01023 } 01024 } 01025 } 01026 01030 public function unloadAll() { 01031 foreach ( $this->initialisedLangs as $lang => $unused ) { 01032 $this->unload( $lang ); 01033 } 01034 } 01035 01039 public function disableBackend() { 01040 $this->store = new LCStoreNull; 01041 $this->manualRecache = false; 01042 } 01043 } 01044 01062 interface LCStore { 01068 function get( $code, $key ); 01069 01074 function startWrite( $code ); 01075 01079 function finishWrite(); 01080 01087 function set( $key, $value ); 01088 } 01089 01095 class LCStoreAccel implements LCStore { 01096 private $currentLang; 01097 private $keys; 01098 01099 public function __construct() { 01100 $this->cache = wfGetCache( CACHE_ACCEL ); 01101 } 01102 01103 public function get( $code, $key ) { 01104 $k = wfMemcKey( 'l10n', $code, 'k', $key ); 01105 $r = $this->cache->get( $k ); 01106 01107 return $r === false ? null : $r; 01108 } 01109 01110 public function startWrite( $code ) { 01111 $k = wfMemcKey( 'l10n', $code, 'l' ); 01112 $keys = $this->cache->get( $k ); 01113 if ( $keys ) { 01114 foreach ( $keys as $k ) { 01115 $this->cache->delete( $k ); 01116 } 01117 } 01118 $this->currentLang = $code; 01119 $this->keys = array(); 01120 } 01121 01122 public function finishWrite() { 01123 if ( $this->currentLang ) { 01124 $k = wfMemcKey( 'l10n', $this->currentLang, 'l' ); 01125 $this->cache->set( $k, array_keys( $this->keys ) ); 01126 } 01127 $this->currentLang = null; 01128 $this->keys = array(); 01129 } 01130 01131 public function set( $key, $value ) { 01132 if ( $this->currentLang ) { 01133 $k = wfMemcKey( 'l10n', $this->currentLang, 'k', $key ); 01134 $this->keys[$k] = true; 01135 $this->cache->set( $k, $value ); 01136 } 01137 } 01138 } 01139 01144 class LCStoreDB implements LCStore { 01145 private $currentLang; 01146 private $writesDone = false; 01147 01151 private $dbw; 01152 private $batch; 01153 private $readOnly = false; 01154 01155 public function get( $code, $key ) { 01156 if ( $this->writesDone ) { 01157 $db = wfGetDB( DB_MASTER ); 01158 } else { 01159 $db = wfGetDB( DB_SLAVE ); 01160 } 01161 $row = $db->selectRow( 'l10n_cache', array( 'lc_value' ), 01162 array( 'lc_lang' => $code, 'lc_key' => $key ), __METHOD__ ); 01163 if ( $row ) { 01164 return unserialize( $db->decodeBlob( $row->lc_value ) ); 01165 } else { 01166 return null; 01167 } 01168 } 01169 01170 public function startWrite( $code ) { 01171 if ( $this->readOnly ) { 01172 return; 01173 } 01174 01175 if ( !$code ) { 01176 throw new MWException( __METHOD__ . ": Invalid language \"$code\"" ); 01177 } 01178 01179 $this->dbw = wfGetDB( DB_MASTER ); 01180 try { 01181 $this->dbw->begin( __METHOD__ ); 01182 $this->dbw->delete( 'l10n_cache', array( 'lc_lang' => $code ), __METHOD__ ); 01183 } catch ( DBQueryError $e ) { 01184 if ( $this->dbw->wasReadOnlyError() ) { 01185 $this->readOnly = true; 01186 $this->dbw->rollback( __METHOD__ ); 01187 01188 return; 01189 } else { 01190 throw $e; 01191 } 01192 } 01193 01194 $this->currentLang = $code; 01195 $this->batch = array(); 01196 } 01197 01198 public function finishWrite() { 01199 if ( $this->readOnly ) { 01200 return; 01201 } 01202 01203 if ( $this->batch ) { 01204 $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ ); 01205 } 01206 01207 $this->dbw->commit( __METHOD__ ); 01208 $this->currentLang = null; 01209 $this->dbw = null; 01210 $this->batch = array(); 01211 $this->writesDone = true; 01212 } 01213 01214 public function set( $key, $value ) { 01215 if ( $this->readOnly ) { 01216 return; 01217 } 01218 01219 if ( is_null( $this->currentLang ) ) { 01220 throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' ); 01221 } 01222 01223 $this->batch[] = array( 01224 'lc_lang' => $this->currentLang, 01225 'lc_key' => $key, 01226 'lc_value' => $this->dbw->encodeBlob( serialize( $value ) ) ); 01227 01228 if ( count( $this->batch ) >= 100 ) { 01229 $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ ); 01230 $this->batch = array(); 01231 } 01232 } 01233 } 01234 01247 class LCStoreCDB implements LCStore { 01249 private $readers; 01250 01252 private $writer; 01253 01255 private $currentLang; 01256 01258 private $directory; 01259 01260 function __construct( $conf = array() ) { 01261 global $wgCacheDirectory; 01262 01263 if ( isset( $conf['directory'] ) ) { 01264 $this->directory = $conf['directory']; 01265 } else { 01266 $this->directory = $wgCacheDirectory; 01267 } 01268 } 01269 01270 public function get( $code, $key ) { 01271 if ( !isset( $this->readers[$code] ) ) { 01272 $fileName = $this->getFileName( $code ); 01273 01274 $this->readers[$code] = false; 01275 if ( file_exists( $fileName ) ) { 01276 try { 01277 $this->readers[$code] = CdbReader::open( $fileName ); 01278 } catch ( CdbException $e ) { 01279 wfDebug( __METHOD__ . ": unable to open cdb file for reading\n" ); 01280 } 01281 } 01282 } 01283 01284 if ( !$this->readers[$code] ) { 01285 return null; 01286 } else { 01287 $value = false; 01288 try { 01289 $value = $this->readers[$code]->get( $key ); 01290 } catch ( CdbException $e ) { 01291 wfDebug( __METHOD__ . ": CdbException caught, error message was " 01292 . $e->getMessage() . "\n" ); 01293 } 01294 if ( $value === false ) { 01295 return null; 01296 } 01297 01298 return unserialize( $value ); 01299 } 01300 } 01301 01302 public function startWrite( $code ) { 01303 if ( !file_exists( $this->directory ) ) { 01304 if ( !wfMkdirParents( $this->directory, null, __METHOD__ ) ) { 01305 throw new MWException( "Unable to create the localisation store " . 01306 "directory \"{$this->directory}\"" ); 01307 } 01308 } 01309 01310 // Close reader to stop permission errors on write 01311 if ( !empty( $this->readers[$code] ) ) { 01312 $this->readers[$code]->close(); 01313 } 01314 01315 try { 01316 $this->writer = CdbWriter::open( $this->getFileName( $code ) ); 01317 } catch ( CdbException $e ) { 01318 throw new MWException( $e->getMessage() ); 01319 } 01320 $this->currentLang = $code; 01321 } 01322 01323 public function finishWrite() { 01324 // Close the writer 01325 try { 01326 $this->writer->close(); 01327 } catch ( CdbException $e ) { 01328 throw new MWException( $e->getMessage() ); 01329 } 01330 $this->writer = null; 01331 unset( $this->readers[$this->currentLang] ); 01332 $this->currentLang = null; 01333 } 01334 01335 public function set( $key, $value ) { 01336 if ( is_null( $this->writer ) ) { 01337 throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' ); 01338 } 01339 try { 01340 $this->writer->set( $key, serialize( $value ) ); 01341 } catch ( CdbException $e ) { 01342 throw new MWException( $e->getMessage() ); 01343 } 01344 } 01345 01346 protected function getFileName( $code ) { 01347 if ( strval( $code ) === '' || strpos( $code, '/' ) !== false ) { 01348 throw new MWException( __METHOD__ . ": Invalid language \"$code\"" ); 01349 } 01350 01351 return "{$this->directory}/l10n_cache-$code.cdb"; 01352 } 01353 } 01354 01358 class LCStoreNull implements LCStore { 01359 public function get( $code, $key ) { 01360 return null; 01361 } 01362 01363 public function startWrite( $code ) { 01364 } 01365 01366 public function finishWrite() { 01367 } 01368 01369 public function set( $key, $value ) { 01370 } 01371 } 01372 01377 class LocalisationCacheBulkLoad extends LocalisationCache { 01382 private $fileCache = array(); 01383 01389 private $mruLangs = array(); 01390 01394 private $maxLoadedLangs = 10; 01395 01401 protected function readPHPFile( $fileName, $fileType ) { 01402 $serialize = $fileType === 'core'; 01403 if ( !isset( $this->fileCache[$fileName][$fileType] ) ) { 01404 $data = parent::readPHPFile( $fileName, $fileType ); 01405 01406 if ( $serialize ) { 01407 $encData = serialize( $data ); 01408 } else { 01409 $encData = $data; 01410 } 01411 01412 $this->fileCache[$fileName][$fileType] = $encData; 01413 01414 return $data; 01415 } elseif ( $serialize ) { 01416 return unserialize( $this->fileCache[$fileName][$fileType] ); 01417 } else { 01418 return $this->fileCache[$fileName][$fileType]; 01419 } 01420 } 01421 01427 public function getItem( $code, $key ) { 01428 unset( $this->mruLangs[$code] ); 01429 $this->mruLangs[$code] = true; 01430 01431 return parent::getItem( $code, $key ); 01432 } 01433 01440 public function getSubitem( $code, $key, $subkey ) { 01441 unset( $this->mruLangs[$code] ); 01442 $this->mruLangs[$code] = true; 01443 01444 return parent::getSubitem( $code, $key, $subkey ); 01445 } 01446 01450 public function recache( $code ) { 01451 parent::recache( $code ); 01452 unset( $this->mruLangs[$code] ); 01453 $this->mruLangs[$code] = true; 01454 $this->trimCache(); 01455 } 01456 01460 public function unload( $code ) { 01461 unset( $this->mruLangs[$code] ); 01462 parent::unload( $code ); 01463 } 01464 01468 protected function trimCache() { 01469 while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) { 01470 reset( $this->mruLangs ); 01471 $code = key( $this->mruLangs ); 01472 wfDebug( __METHOD__ . ": unloading $code\n" ); 01473 $this->unload( $code ); 01474 } 01475 } 01476 }