MediaWiki
REL1_24
|
00001 <?php 00035 class LocalisationCache { 00036 const VERSION = 2; 00037 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 'detect': 00204 $storeClass = $wgCacheDirectory ? 'LCStoreCDB' : 'LCStoreDB'; 00205 break; 00206 default: 00207 throw new MWException( 00208 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' ); 00209 } 00210 } 00211 00212 wfDebugLog( 'caches', get_class( $this ) . ": using store $storeClass" ); 00213 if ( !empty( $conf['storeDirectory'] ) ) { 00214 $storeConf['directory'] = $conf['storeDirectory']; 00215 } 00216 00217 $this->store = new $storeClass( $storeConf ); 00218 foreach ( array( 'manualRecache', 'forceRecache' ) as $var ) { 00219 if ( isset( $conf[$var] ) ) { 00220 $this->$var = $conf[$var]; 00221 } 00222 } 00223 } 00224 00231 public function isMergeableKey( $key ) { 00232 if ( $this->mergeableKeys === null ) { 00233 $this->mergeableKeys = array_flip( array_merge( 00234 self::$mergeableMapKeys, 00235 self::$mergeableListKeys, 00236 self::$mergeableAliasListKeys, 00237 self::$optionalMergeKeys, 00238 self::$magicWordKeys 00239 ) ); 00240 } 00241 00242 return isset( $this->mergeableKeys[$key] ); 00243 } 00244 00254 public function getItem( $code, $key ) { 00255 if ( !isset( $this->loadedItems[$code][$key] ) ) { 00256 wfProfileIn( __METHOD__ . '-load' ); 00257 $this->loadItem( $code, $key ); 00258 wfProfileOut( __METHOD__ . '-load' ); 00259 } 00260 00261 if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) { 00262 return $this->shallowFallbacks[$code]; 00263 } 00264 00265 return $this->data[$code][$key]; 00266 } 00267 00275 public function getSubitem( $code, $key, $subkey ) { 00276 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) && 00277 !isset( $this->loadedItems[$code][$key] ) 00278 ) { 00279 wfProfileIn( __METHOD__ . '-load' ); 00280 $this->loadSubitem( $code, $key, $subkey ); 00281 wfProfileOut( __METHOD__ . '-load' ); 00282 } 00283 00284 if ( isset( $this->data[$code][$key][$subkey] ) ) { 00285 return $this->data[$code][$key][$subkey]; 00286 } else { 00287 return null; 00288 } 00289 } 00290 00303 public function getSubitemList( $code, $key ) { 00304 if ( in_array( $key, self::$splitKeys ) ) { 00305 return $this->getSubitem( $code, 'list', $key ); 00306 } else { 00307 $item = $this->getItem( $code, $key ); 00308 if ( is_array( $item ) ) { 00309 return array_keys( $item ); 00310 } else { 00311 return false; 00312 } 00313 } 00314 } 00315 00321 protected function loadItem( $code, $key ) { 00322 if ( !isset( $this->initialisedLangs[$code] ) ) { 00323 $this->initLanguage( $code ); 00324 } 00325 00326 // Check to see if initLanguage() loaded it for us 00327 if ( isset( $this->loadedItems[$code][$key] ) ) { 00328 return; 00329 } 00330 00331 if ( isset( $this->shallowFallbacks[$code] ) ) { 00332 $this->loadItem( $this->shallowFallbacks[$code], $key ); 00333 00334 return; 00335 } 00336 00337 if ( in_array( $key, self::$splitKeys ) ) { 00338 $subkeyList = $this->getSubitem( $code, 'list', $key ); 00339 foreach ( $subkeyList as $subkey ) { 00340 if ( isset( $this->data[$code][$key][$subkey] ) ) { 00341 continue; 00342 } 00343 $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey ); 00344 } 00345 } else { 00346 $this->data[$code][$key] = $this->store->get( $code, $key ); 00347 } 00348 00349 $this->loadedItems[$code][$key] = true; 00350 } 00351 00358 protected function loadSubitem( $code, $key, $subkey ) { 00359 if ( !in_array( $key, self::$splitKeys ) ) { 00360 $this->loadItem( $code, $key ); 00361 00362 return; 00363 } 00364 00365 if ( !isset( $this->initialisedLangs[$code] ) ) { 00366 $this->initLanguage( $code ); 00367 } 00368 00369 // Check to see if initLanguage() loaded it for us 00370 if ( isset( $this->loadedItems[$code][$key] ) || 00371 isset( $this->loadedSubitems[$code][$key][$subkey] ) 00372 ) { 00373 return; 00374 } 00375 00376 if ( isset( $this->shallowFallbacks[$code] ) ) { 00377 $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey ); 00378 00379 return; 00380 } 00381 00382 $value = $this->store->get( $code, "$key:$subkey" ); 00383 $this->data[$code][$key][$subkey] = $value; 00384 $this->loadedSubitems[$code][$key][$subkey] = true; 00385 } 00386 00394 public function isExpired( $code ) { 00395 if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) { 00396 wfDebug( __METHOD__ . "($code): forced reload\n" ); 00397 00398 return true; 00399 } 00400 00401 $deps = $this->store->get( $code, 'deps' ); 00402 $keys = $this->store->get( $code, 'list' ); 00403 $preload = $this->store->get( $code, 'preload' ); 00404 // Different keys may expire separately for some stores 00405 if ( $deps === null || $keys === null || $preload === null ) { 00406 wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" ); 00407 00408 return true; 00409 } 00410 00411 foreach ( $deps as $dep ) { 00412 // Because we're unserializing stuff from cache, we 00413 // could receive objects of classes that don't exist 00414 // anymore (e.g. uninstalled extensions) 00415 // When this happens, always expire the cache 00416 if ( !$dep instanceof CacheDependency || $dep->isExpired() ) { 00417 wfDebug( __METHOD__ . "($code): cache for $code expired due to " . 00418 get_class( $dep ) . "\n" ); 00419 00420 return true; 00421 } 00422 } 00423 00424 return false; 00425 } 00426 00432 protected function initLanguage( $code ) { 00433 if ( isset( $this->initialisedLangs[$code] ) ) { 00434 return; 00435 } 00436 00437 $this->initialisedLangs[$code] = true; 00438 00439 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback 00440 if ( !Language::isValidBuiltInCode( $code ) ) { 00441 $this->initShallowFallback( $code, 'en' ); 00442 00443 return; 00444 } 00445 00446 # Recache the data if necessary 00447 if ( !$this->manualRecache && $this->isExpired( $code ) ) { 00448 if ( Language::isSupportedLanguage( $code ) ) { 00449 $this->recache( $code ); 00450 } elseif ( $code === 'en' ) { 00451 throw new MWException( 'MessagesEn.php is missing.' ); 00452 } else { 00453 $this->initShallowFallback( $code, 'en' ); 00454 } 00455 00456 return; 00457 } 00458 00459 # Preload some stuff 00460 $preload = $this->getItem( $code, 'preload' ); 00461 if ( $preload === null ) { 00462 if ( $this->manualRecache ) { 00463 // No Messages*.php file. Do shallow fallback to en. 00464 if ( $code === 'en' ) { 00465 throw new MWException( 'No localisation cache found for English. ' . 00466 'Please run maintenance/rebuildLocalisationCache.php.' ); 00467 } 00468 $this->initShallowFallback( $code, 'en' ); 00469 00470 return; 00471 } else { 00472 throw new MWException( 'Invalid or missing localisation cache.' ); 00473 } 00474 } 00475 $this->data[$code] = $preload; 00476 foreach ( $preload as $key => $item ) { 00477 if ( in_array( $key, self::$splitKeys ) ) { 00478 foreach ( $item as $subkey => $subitem ) { 00479 $this->loadedSubitems[$code][$key][$subkey] = true; 00480 } 00481 } else { 00482 $this->loadedItems[$code][$key] = true; 00483 } 00484 } 00485 } 00486 00493 public function initShallowFallback( $primaryCode, $fallbackCode ) { 00494 $this->data[$primaryCode] =& $this->data[$fallbackCode]; 00495 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode]; 00496 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode]; 00497 $this->shallowFallbacks[$primaryCode] = $fallbackCode; 00498 } 00499 00507 protected function readPHPFile( $_fileName, $_fileType ) { 00508 wfProfileIn( __METHOD__ ); 00509 // Disable APC caching 00510 wfSuppressWarnings(); 00511 $_apcEnabled = ini_set( 'apc.cache_by_default', '0' ); 00512 wfRestoreWarnings(); 00513 00514 include $_fileName; 00515 00516 wfSuppressWarnings(); 00517 ini_set( 'apc.cache_by_default', $_apcEnabled ); 00518 wfRestoreWarnings(); 00519 00520 if ( $_fileType == 'core' || $_fileType == 'extension' ) { 00521 $data = compact( self::$allKeys ); 00522 } elseif ( $_fileType == 'aliases' ) { 00523 $data = compact( 'aliases' ); 00524 } else { 00525 wfProfileOut( __METHOD__ ); 00526 throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" ); 00527 } 00528 wfProfileOut( __METHOD__ ); 00529 00530 return $data; 00531 } 00532 00539 public function readJSONFile( $fileName ) { 00540 wfProfileIn( __METHOD__ ); 00541 00542 if ( !is_readable( $fileName ) ) { 00543 wfProfileOut( __METHOD__ ); 00544 00545 return array(); 00546 } 00547 00548 $json = file_get_contents( $fileName ); 00549 if ( $json === false ) { 00550 wfProfileOut( __METHOD__ ); 00551 00552 return array(); 00553 } 00554 00555 $data = FormatJson::decode( $json, true ); 00556 if ( $data === null ) { 00557 wfProfileOut( __METHOD__ ); 00558 00559 throw new MWException( __METHOD__ . ": Invalid JSON file: $fileName" ); 00560 } 00561 00562 // Remove keys starting with '@', they're reserved for metadata and non-message data 00563 foreach ( $data as $key => $unused ) { 00564 if ( $key === '' || $key[0] === '@' ) { 00565 unset( $data[$key] ); 00566 } 00567 } 00568 00569 wfProfileOut( __METHOD__ ); 00570 00571 // The JSON format only supports messages, none of the other variables, so wrap the data 00572 return array( 'messages' => $data ); 00573 } 00574 00581 public function getCompiledPluralRules( $code ) { 00582 $rules = $this->getPluralRules( $code ); 00583 if ( $rules === null ) { 00584 return null; 00585 } 00586 try { 00587 $compiledRules = CLDRPluralRuleEvaluator::compile( $rules ); 00588 } catch ( CLDRPluralRuleError $e ) { 00589 wfDebugLog( 'l10n', $e->getMessage() ); 00590 00591 return array(); 00592 } 00593 00594 return $compiledRules; 00595 } 00596 00604 public function getPluralRules( $code ) { 00605 if ( $this->pluralRules === null ) { 00606 $this->loadPluralFiles(); 00607 } 00608 if ( !isset( $this->pluralRules[$code] ) ) { 00609 return null; 00610 } else { 00611 return $this->pluralRules[$code]; 00612 } 00613 } 00614 00622 public function getPluralRuleTypes( $code ) { 00623 if ( $this->pluralRuleTypes === null ) { 00624 $this->loadPluralFiles(); 00625 } 00626 if ( !isset( $this->pluralRuleTypes[$code] ) ) { 00627 return null; 00628 } else { 00629 return $this->pluralRuleTypes[$code]; 00630 } 00631 } 00632 00636 protected function loadPluralFiles() { 00637 global $IP; 00638 $cldrPlural = "$IP/languages/data/plurals.xml"; 00639 $mwPlural = "$IP/languages/data/plurals-mediawiki.xml"; 00640 // Load CLDR plural rules 00641 $this->loadPluralFile( $cldrPlural ); 00642 if ( file_exists( $mwPlural ) ) { 00643 // Override or extend 00644 $this->loadPluralFile( $mwPlural ); 00645 } 00646 } 00647 00654 protected function loadPluralFile( $fileName ) { 00655 $doc = new DOMDocument; 00656 $doc->load( $fileName ); 00657 $rulesets = $doc->getElementsByTagName( "pluralRules" ); 00658 foreach ( $rulesets as $ruleset ) { 00659 $codes = $ruleset->getAttribute( 'locales' ); 00660 $rules = array(); 00661 $ruleTypes = array(); 00662 $ruleElements = $ruleset->getElementsByTagName( "pluralRule" ); 00663 foreach ( $ruleElements as $elt ) { 00664 $ruleType = $elt->getAttribute( 'count' ); 00665 if ( $ruleType === 'other' ) { 00666 // Don't record "other" rules, which have an empty condition 00667 continue; 00668 } 00669 $rules[] = $elt->nodeValue; 00670 $ruleTypes[] = $ruleType; 00671 } 00672 foreach ( explode( ' ', $codes ) as $code ) { 00673 $this->pluralRules[$code] = $rules; 00674 $this->pluralRuleTypes[$code] = $ruleTypes; 00675 } 00676 } 00677 } 00678 00688 protected function readSourceFilesAndRegisterDeps( $code, &$deps ) { 00689 global $IP; 00690 wfProfileIn( __METHOD__ ); 00691 00692 // This reads in the PHP i18n file with non-messages l10n data 00693 $fileName = Language::getMessagesFileName( $code ); 00694 if ( !file_exists( $fileName ) ) { 00695 $data = array(); 00696 } else { 00697 $deps[] = new FileDependency( $fileName ); 00698 $data = $this->readPHPFile( $fileName, 'core' ); 00699 } 00700 00701 # Load CLDR plural rules for JavaScript 00702 $data['pluralRules'] = $this->getPluralRules( $code ); 00703 # And for PHP 00704 $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code ); 00705 # Load plural rule types 00706 $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code ); 00707 00708 $deps['plurals'] = new FileDependency( "$IP/languages/data/plurals.xml" ); 00709 $deps['plurals-mw'] = new FileDependency( "$IP/languages/data/plurals-mediawiki.xml" ); 00710 00711 wfProfileOut( __METHOD__ ); 00712 00713 return $data; 00714 } 00715 00723 protected function mergeItem( $key, &$value, $fallbackValue ) { 00724 if ( !is_null( $value ) ) { 00725 if ( !is_null( $fallbackValue ) ) { 00726 if ( in_array( $key, self::$mergeableMapKeys ) ) { 00727 $value = $value + $fallbackValue; 00728 } elseif ( in_array( $key, self::$mergeableListKeys ) ) { 00729 $value = array_unique( array_merge( $fallbackValue, $value ) ); 00730 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) { 00731 $value = array_merge_recursive( $value, $fallbackValue ); 00732 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) { 00733 if ( !empty( $value['inherit'] ) ) { 00734 $value = array_merge( $fallbackValue, $value ); 00735 } 00736 00737 if ( isset( $value['inherit'] ) ) { 00738 unset( $value['inherit'] ); 00739 } 00740 } elseif ( in_array( $key, self::$magicWordKeys ) ) { 00741 $this->mergeMagicWords( $value, $fallbackValue ); 00742 } 00743 } 00744 } else { 00745 $value = $fallbackValue; 00746 } 00747 } 00748 00753 protected function mergeMagicWords( &$value, $fallbackValue ) { 00754 foreach ( $fallbackValue as $magicName => $fallbackInfo ) { 00755 if ( !isset( $value[$magicName] ) ) { 00756 $value[$magicName] = $fallbackInfo; 00757 } else { 00758 $oldSynonyms = array_slice( $fallbackInfo, 1 ); 00759 $newSynonyms = array_slice( $value[$magicName], 1 ); 00760 $synonyms = array_values( array_unique( array_merge( 00761 $newSynonyms, $oldSynonyms ) ) ); 00762 $value[$magicName] = array_merge( array( $fallbackInfo[0] ), $synonyms ); 00763 } 00764 } 00765 } 00766 00780 protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) { 00781 $used = false; 00782 foreach ( $codeSequence as $code ) { 00783 if ( isset( $fallbackValue[$code] ) ) { 00784 $this->mergeItem( $key, $value, $fallbackValue[$code] ); 00785 $used = true; 00786 } 00787 } 00788 00789 return $used; 00790 } 00791 00798 public function recache( $code ) { 00799 global $wgExtensionMessagesFiles, $wgMessagesDirs; 00800 wfProfileIn( __METHOD__ ); 00801 00802 if ( !$code ) { 00803 wfProfileOut( __METHOD__ ); 00804 throw new MWException( "Invalid language code requested" ); 00805 } 00806 $this->recachedLangs[$code] = true; 00807 00808 # Initial values 00809 $initialData = array_combine( 00810 self::$allKeys, 00811 array_fill( 0, count( self::$allKeys ), null ) ); 00812 $coreData = $initialData; 00813 $deps = array(); 00814 00815 # Load the primary localisation from the source file 00816 $data = $this->readSourceFilesAndRegisterDeps( $code, $deps ); 00817 if ( $data === false ) { 00818 wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" ); 00819 $coreData['fallback'] = 'en'; 00820 } else { 00821 wfDebug( __METHOD__ . ": got localisation for $code from source\n" ); 00822 00823 # Merge primary localisation 00824 foreach ( $data as $key => $value ) { 00825 $this->mergeItem( $key, $coreData[$key], $value ); 00826 } 00827 } 00828 00829 # Fill in the fallback if it's not there already 00830 if ( is_null( $coreData['fallback'] ) ) { 00831 $coreData['fallback'] = $code === 'en' ? false : 'en'; 00832 } 00833 if ( $coreData['fallback'] === false ) { 00834 $coreData['fallbackSequence'] = array(); 00835 } else { 00836 $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) ); 00837 $len = count( $coreData['fallbackSequence'] ); 00838 00839 # Ensure that the sequence ends at en 00840 if ( $coreData['fallbackSequence'][$len - 1] !== 'en' ) { 00841 $coreData['fallbackSequence'][] = 'en'; 00842 } 00843 } 00844 00845 $codeSequence = array_merge( array( $code ), $coreData['fallbackSequence'] ); 00846 00847 wfProfileIn( __METHOD__ . '-fallbacks' ); 00848 00849 # Load non-JSON localisation data for extensions 00850 $extensionData = array_combine( 00851 $codeSequence, 00852 array_fill( 0, count( $codeSequence ), $initialData ) ); 00853 foreach ( $wgExtensionMessagesFiles as $extension => $fileName ) { 00854 if ( isset( $wgMessagesDirs[$extension] ) ) { 00855 # This extension has JSON message data; skip the PHP shim 00856 continue; 00857 } 00858 00859 $data = $this->readPHPFile( $fileName, 'extension' ); 00860 $used = false; 00861 00862 foreach ( $data as $key => $item ) { 00863 foreach ( $codeSequence as $csCode ) { 00864 if ( isset( $item[$csCode] ) ) { 00865 $this->mergeItem( $key, $extensionData[$csCode][$key], $item[$csCode] ); 00866 $used = true; 00867 } 00868 } 00869 } 00870 00871 if ( $used ) { 00872 $deps[] = new FileDependency( $fileName ); 00873 } 00874 } 00875 00876 # Load the localisation data for each fallback, then merge it into the full array 00877 $allData = $initialData; 00878 foreach ( $codeSequence as $csCode ) { 00879 $csData = $initialData; 00880 00881 # Load core messages and the extension localisations. 00882 foreach ( $wgMessagesDirs as $dirs ) { 00883 foreach ( (array)$dirs as $dir ) { 00884 $fileName = "$dir/$csCode.json"; 00885 $data = $this->readJSONFile( $fileName ); 00886 00887 foreach ( $data as $key => $item ) { 00888 $this->mergeItem( $key, $csData[$key], $item ); 00889 } 00890 00891 $deps[] = new FileDependency( $fileName ); 00892 } 00893 } 00894 00895 # Merge non-JSON extension data 00896 if ( isset( $extensionData[$csCode] ) ) { 00897 foreach ( $extensionData[$csCode] as $key => $item ) { 00898 $this->mergeItem( $key, $csData[$key], $item ); 00899 } 00900 } 00901 00902 if ( $csCode === $code ) { 00903 # Merge core data into extension data 00904 foreach ( $coreData as $key => $item ) { 00905 $this->mergeItem( $key, $csData[$key], $item ); 00906 } 00907 } else { 00908 # Load the secondary localisation from the source file to 00909 # avoid infinite cycles on cyclic fallbacks 00910 $fbData = $this->readSourceFilesAndRegisterDeps( $csCode, $deps ); 00911 if ( $fbData !== false ) { 00912 # Only merge the keys that make sense to merge 00913 foreach ( self::$allKeys as $key ) { 00914 if ( !isset( $fbData[$key] ) ) { 00915 continue; 00916 } 00917 00918 if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) { 00919 $this->mergeItem( $key, $csData[$key], $fbData[$key] ); 00920 } 00921 } 00922 } 00923 } 00924 00925 # Allow extensions an opportunity to adjust the data for this 00926 # fallback 00927 wfRunHooks( 'LocalisationCacheRecacheFallback', array( $this, $csCode, &$csData ) ); 00928 00929 # Merge the data for this fallback into the final array 00930 if ( $csCode === $code ) { 00931 $allData = $csData; 00932 } else { 00933 foreach ( self::$allKeys as $key ) { 00934 if ( !isset( $csData[$key] ) ) { 00935 continue; 00936 } 00937 00938 if ( is_null( $allData[$key] ) || $this->isMergeableKey( $key ) ) { 00939 $this->mergeItem( $key, $allData[$key], $csData[$key] ); 00940 } 00941 } 00942 } 00943 } 00944 00945 wfProfileOut( __METHOD__ . '-fallbacks' ); 00946 00947 # Add cache dependencies for any referenced globals 00948 $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' ); 00949 $deps['wgMessagesDirs'] = new GlobalDependency( 'wgMessagesDirs' ); 00950 $deps['version'] = new ConstantDependency( 'LocalisationCache::VERSION' ); 00951 00952 # Add dependencies to the cache entry 00953 $allData['deps'] = $deps; 00954 00955 # Replace spaces with underscores in namespace names 00956 $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] ); 00957 00958 # And do the same for special page aliases. $page is an array. 00959 foreach ( $allData['specialPageAliases'] as &$page ) { 00960 $page = str_replace( ' ', '_', $page ); 00961 } 00962 # Decouple the reference to prevent accidental damage 00963 unset( $page ); 00964 00965 # If there were no plural rules, return an empty array 00966 if ( $allData['pluralRules'] === null ) { 00967 $allData['pluralRules'] = array(); 00968 } 00969 if ( $allData['compiledPluralRules'] === null ) { 00970 $allData['compiledPluralRules'] = array(); 00971 } 00972 # If there were no plural rule types, return an empty array 00973 if ( $allData['pluralRuleTypes'] === null ) { 00974 $allData['pluralRuleTypes'] = array(); 00975 } 00976 00977 # Set the list keys 00978 $allData['list'] = array(); 00979 foreach ( self::$splitKeys as $key ) { 00980 $allData['list'][$key] = array_keys( $allData[$key] ); 00981 } 00982 # Run hooks 00983 $purgeBlobs = true; 00984 wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData, &$purgeBlobs ) ); 00985 00986 if ( is_null( $allData['namespaceNames'] ) ) { 00987 wfProfileOut( __METHOD__ ); 00988 throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' . 00989 'Check that your languages/messages/MessagesEn.php file is intact.' ); 00990 } 00991 00992 # Set the preload key 00993 $allData['preload'] = $this->buildPreload( $allData ); 00994 00995 # Save to the process cache and register the items loaded 00996 $this->data[$code] = $allData; 00997 foreach ( $allData as $key => $item ) { 00998 $this->loadedItems[$code][$key] = true; 00999 } 01000 01001 # Save to the persistent cache 01002 wfProfileIn( __METHOD__ . '-write' ); 01003 $this->store->startWrite( $code ); 01004 foreach ( $allData as $key => $value ) { 01005 if ( in_array( $key, self::$splitKeys ) ) { 01006 foreach ( $value as $subkey => $subvalue ) { 01007 $this->store->set( "$key:$subkey", $subvalue ); 01008 } 01009 } else { 01010 $this->store->set( $key, $value ); 01011 } 01012 } 01013 $this->store->finishWrite(); 01014 wfProfileOut( __METHOD__ . '-write' ); 01015 01016 # Clear out the MessageBlobStore 01017 # HACK: If using a null (i.e. disabled) storage backend, we 01018 # can't write to the MessageBlobStore either 01019 if ( $purgeBlobs && !$this->store instanceof LCStoreNull ) { 01020 MessageBlobStore::getInstance()->clear(); 01021 } 01022 01023 wfProfileOut( __METHOD__ ); 01024 } 01025 01034 protected function buildPreload( $data ) { 01035 $preload = array( 'messages' => array() ); 01036 foreach ( self::$preloadedKeys as $key ) { 01037 $preload[$key] = $data[$key]; 01038 } 01039 01040 foreach ( $data['preloadedMessages'] as $subkey ) { 01041 if ( isset( $data['messages'][$subkey] ) ) { 01042 $subitem = $data['messages'][$subkey]; 01043 } else { 01044 $subitem = null; 01045 } 01046 $preload['messages'][$subkey] = $subitem; 01047 } 01048 01049 return $preload; 01050 } 01051 01057 public function unload( $code ) { 01058 unset( $this->data[$code] ); 01059 unset( $this->loadedItems[$code] ); 01060 unset( $this->loadedSubitems[$code] ); 01061 unset( $this->initialisedLangs[$code] ); 01062 unset( $this->shallowFallbacks[$code] ); 01063 01064 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) { 01065 if ( $fbCode === $code ) { 01066 $this->unload( $shallowCode ); 01067 } 01068 } 01069 } 01070 01074 public function unloadAll() { 01075 foreach ( $this->initialisedLangs as $lang => $unused ) { 01076 $this->unload( $lang ); 01077 } 01078 } 01079 01083 public function disableBackend() { 01084 $this->store = new LCStoreNull; 01085 $this->manualRecache = false; 01086 } 01087 } 01088 01106 interface LCStore { 01112 function get( $code, $key ); 01113 01118 function startWrite( $code ); 01119 01123 function finishWrite(); 01124 01131 function set( $key, $value ); 01132 } 01133 01138 class LCStoreDB implements LCStore { 01139 private $currentLang; 01140 private $writesDone = false; 01141 01143 private $dbw; 01145 private $batch = array(); 01146 01147 private $readOnly = false; 01148 01149 public function get( $code, $key ) { 01150 if ( $this->writesDone ) { 01151 $db = wfGetDB( DB_MASTER ); 01152 } else { 01153 $db = wfGetDB( DB_SLAVE ); 01154 } 01155 $row = $db->selectRow( 'l10n_cache', array( 'lc_value' ), 01156 array( 'lc_lang' => $code, 'lc_key' => $key ), __METHOD__ ); 01157 if ( $row ) { 01158 return unserialize( $db->decodeBlob( $row->lc_value ) ); 01159 } else { 01160 return null; 01161 } 01162 } 01163 01164 public function startWrite( $code ) { 01165 if ( $this->readOnly ) { 01166 return; 01167 } elseif ( !$code ) { 01168 throw new MWException( __METHOD__ . ": Invalid language \"$code\"" ); 01169 } 01170 01171 $this->dbw = wfGetDB( DB_MASTER ); 01172 01173 $this->currentLang = $code; 01174 $this->batch = array(); 01175 } 01176 01177 public function finishWrite() { 01178 if ( $this->readOnly ) { 01179 return; 01180 } elseif ( is_null( $this->currentLang ) ) { 01181 throw new MWException( __CLASS__ . ': must call startWrite() before finishWrite()' ); 01182 } 01183 01184 $this->dbw->begin( __METHOD__ ); 01185 try { 01186 $this->dbw->delete( 'l10n_cache', 01187 array( 'lc_lang' => $this->currentLang ), __METHOD__ ); 01188 foreach ( array_chunk( $this->batch, 500 ) as $rows ) { 01189 $this->dbw->insert( 'l10n_cache', $rows, __METHOD__ ); 01190 } 01191 $this->writesDone = true; 01192 } catch ( DBQueryError $e ) { 01193 if ( $this->dbw->wasReadOnlyError() ) { 01194 $this->readOnly = true; // just avoid site down time 01195 } else { 01196 throw $e; 01197 } 01198 } 01199 $this->dbw->commit( __METHOD__ ); 01200 01201 $this->currentLang = null; 01202 $this->batch = array(); 01203 } 01204 01205 public function set( $key, $value ) { 01206 if ( $this->readOnly ) { 01207 return; 01208 } elseif ( is_null( $this->currentLang ) ) { 01209 throw new MWException( __CLASS__ . ': must call startWrite() before set()' ); 01210 } 01211 01212 $this->batch[] = array( 01213 'lc_lang' => $this->currentLang, 01214 'lc_key' => $key, 01215 'lc_value' => $this->dbw->encodeBlob( serialize( $value ) ) ); 01216 } 01217 } 01218 01231 class LCStoreCDB implements LCStore { 01233 private $readers; 01234 01236 private $writer; 01237 01239 private $currentLang; 01240 01242 private $directory; 01243 01244 function __construct( $conf = array() ) { 01245 global $wgCacheDirectory; 01246 01247 if ( isset( $conf['directory'] ) ) { 01248 $this->directory = $conf['directory']; 01249 } else { 01250 $this->directory = $wgCacheDirectory; 01251 } 01252 } 01253 01254 public function get( $code, $key ) { 01255 if ( !isset( $this->readers[$code] ) ) { 01256 $fileName = $this->getFileName( $code ); 01257 01258 $this->readers[$code] = false; 01259 if ( file_exists( $fileName ) ) { 01260 try { 01261 $this->readers[$code] = CdbReader::open( $fileName ); 01262 } catch ( CdbException $e ) { 01263 wfDebug( __METHOD__ . ": unable to open cdb file for reading\n" ); 01264 } 01265 } 01266 } 01267 01268 if ( !$this->readers[$code] ) { 01269 return null; 01270 } else { 01271 $value = false; 01272 try { 01273 $value = $this->readers[$code]->get( $key ); 01274 } catch ( CdbException $e ) { 01275 wfDebug( __METHOD__ . ": CdbException caught, error message was " 01276 . $e->getMessage() . "\n" ); 01277 } 01278 if ( $value === false ) { 01279 return null; 01280 } 01281 01282 return unserialize( $value ); 01283 } 01284 } 01285 01286 public function startWrite( $code ) { 01287 if ( !file_exists( $this->directory ) ) { 01288 if ( !wfMkdirParents( $this->directory, null, __METHOD__ ) ) { 01289 throw new MWException( "Unable to create the localisation store " . 01290 "directory \"{$this->directory}\"" ); 01291 } 01292 } 01293 01294 // Close reader to stop permission errors on write 01295 if ( !empty( $this->readers[$code] ) ) { 01296 $this->readers[$code]->close(); 01297 } 01298 01299 try { 01300 $this->writer = CdbWriter::open( $this->getFileName( $code ) ); 01301 } catch ( CdbException $e ) { 01302 throw new MWException( $e->getMessage() ); 01303 } 01304 $this->currentLang = $code; 01305 } 01306 01307 public function finishWrite() { 01308 // Close the writer 01309 try { 01310 $this->writer->close(); 01311 } catch ( CdbException $e ) { 01312 throw new MWException( $e->getMessage() ); 01313 } 01314 $this->writer = null; 01315 unset( $this->readers[$this->currentLang] ); 01316 $this->currentLang = null; 01317 } 01318 01319 public function set( $key, $value ) { 01320 if ( is_null( $this->writer ) ) { 01321 throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' ); 01322 } 01323 try { 01324 $this->writer->set( $key, serialize( $value ) ); 01325 } catch ( CdbException $e ) { 01326 throw new MWException( $e->getMessage() ); 01327 } 01328 } 01329 01330 protected function getFileName( $code ) { 01331 if ( strval( $code ) === '' || strpos( $code, '/' ) !== false ) { 01332 throw new MWException( __METHOD__ . ": Invalid language \"$code\"" ); 01333 } 01334 01335 return "{$this->directory}/l10n_cache-$code.cdb"; 01336 } 01337 } 01338 01342 class LCStoreNull implements LCStore { 01343 public function get( $code, $key ) { 01344 return null; 01345 } 01346 01347 public function startWrite( $code ) { 01348 } 01349 01350 public function finishWrite() { 01351 } 01352 01353 public function set( $key, $value ) { 01354 } 01355 } 01356 01361 class LocalisationCacheBulkLoad extends LocalisationCache { 01366 private $fileCache = array(); 01367 01373 private $mruLangs = array(); 01374 01378 private $maxLoadedLangs = 10; 01379 01385 protected function readPHPFile( $fileName, $fileType ) { 01386 $serialize = $fileType === 'core'; 01387 if ( !isset( $this->fileCache[$fileName][$fileType] ) ) { 01388 $data = parent::readPHPFile( $fileName, $fileType ); 01389 01390 if ( $serialize ) { 01391 $encData = serialize( $data ); 01392 } else { 01393 $encData = $data; 01394 } 01395 01396 $this->fileCache[$fileName][$fileType] = $encData; 01397 01398 return $data; 01399 } elseif ( $serialize ) { 01400 return unserialize( $this->fileCache[$fileName][$fileType] ); 01401 } else { 01402 return $this->fileCache[$fileName][$fileType]; 01403 } 01404 } 01405 01411 public function getItem( $code, $key ) { 01412 unset( $this->mruLangs[$code] ); 01413 $this->mruLangs[$code] = true; 01414 01415 return parent::getItem( $code, $key ); 01416 } 01417 01424 public function getSubitem( $code, $key, $subkey ) { 01425 unset( $this->mruLangs[$code] ); 01426 $this->mruLangs[$code] = true; 01427 01428 return parent::getSubitem( $code, $key, $subkey ); 01429 } 01430 01434 public function recache( $code ) { 01435 parent::recache( $code ); 01436 unset( $this->mruLangs[$code] ); 01437 $this->mruLangs[$code] = true; 01438 $this->trimCache(); 01439 } 01440 01444 public function unload( $code ) { 01445 unset( $this->mruLangs[$code] ); 01446 parent::unload( $code ); 01447 } 01448 01452 protected function trimCache() { 01453 while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) { 01454 reset( $this->mruLangs ); 01455 $code = key( $this->mruLangs ); 01456 wfDebug( __METHOD__ . ": unloading $code\n" ); 01457 $this->unload( $code ); 01458 } 01459 } 01460 }