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