MediaWiki  REL1_24
LocalisationCache.php
Go to the documentation of this file.
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 }