MediaWiki  REL1_22
LocalisationCache.php
Go to the documentation of this file.
00001 <?php
00023 define( 'MW_LC_VERSION', 2 );
00024 
00037 class LocalisationCache {
00039     var $conf;
00040 
00046     var $manualRecache = false;
00047 
00051     var $forceRecache = false;
00052 
00059     var $data = array();
00060 
00066     var $store;
00067 
00075     var $loadedItems = array();
00076 
00081     var $loadedSubitems = array();
00082 
00088     var $initialisedLangs = array();
00089 
00095     var $shallowFallbacks = array();
00096 
00100     var $recachedLangs = array();
00101 
00105     static public $allKeys = array(
00106         'fallback', 'namespaceNames', 'bookstoreList',
00107         'magicWords', 'messages', 'rtl', 'capitalizeAllNouns', 'digitTransformTable',
00108         'separatorTransformTable', 'fallback8bitEncoding', 'linkPrefixExtension',
00109         'linkTrail', '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     var $pluralRules = null;
00162 
00175     var $pluralRuleTypes = null;
00176 
00177     var $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 = 'LCStore_CDB';
00199                     break;
00200                 case 'db':
00201                     $storeClass = 'LCStore_DB';
00202                     break;
00203                 case 'accel':
00204                     $storeClass = 'LCStore_Accel';
00205                     break;
00206                 case 'detect':
00207                     $storeClass = $wgCacheDirectory ? 'LCStore_CDB' : 'LCStore_DB';
00208                     break;
00209                 default:
00210                     throw new MWException(
00211                         'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' );
00212             }
00213         }
00214 
00215         wfDebug( get_class( $this ) . ": using store $storeClass\n" );
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         return isset( $this->mergeableKeys[$key] );
00245     }
00246 
00256     public function getItem( $code, $key ) {
00257         if ( !isset( $this->loadedItems[$code][$key] ) ) {
00258             wfProfileIn( __METHOD__ . '-load' );
00259             $this->loadItem( $code, $key );
00260             wfProfileOut( __METHOD__ . '-load' );
00261         }
00262 
00263         if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
00264             return $this->shallowFallbacks[$code];
00265         }
00266 
00267         return $this->data[$code][$key];
00268     }
00269 
00277     public function getSubitem( $code, $key, $subkey ) {
00278         if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
00279              !isset( $this->loadedItems[$code][$key] ) ) {
00280             wfProfileIn( __METHOD__ . '-load' );
00281             $this->loadSubitem( $code, $key, $subkey );
00282             wfProfileOut( __METHOD__ . '-load' );
00283         }
00284 
00285         if ( isset( $this->data[$code][$key][$subkey] ) ) {
00286             return $this->data[$code][$key][$subkey];
00287         } else {
00288             return null;
00289         }
00290     }
00291 
00304     public function getSubitemList( $code, $key ) {
00305         if ( in_array( $key, self::$splitKeys ) ) {
00306             return $this->getSubitem( $code, 'list', $key );
00307         } else {
00308             $item = $this->getItem( $code, $key );
00309             if ( is_array( $item ) ) {
00310                 return array_keys( $item );
00311             } else {
00312                 return false;
00313             }
00314         }
00315     }
00316 
00322     protected function loadItem( $code, $key ) {
00323         if ( !isset( $this->initialisedLangs[$code] ) ) {
00324             $this->initLanguage( $code );
00325         }
00326 
00327         // Check to see if initLanguage() loaded it for us
00328         if ( isset( $this->loadedItems[$code][$key] ) ) {
00329             return;
00330         }
00331 
00332         if ( isset( $this->shallowFallbacks[$code] ) ) {
00333             $this->loadItem( $this->shallowFallbacks[$code], $key );
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             return;
00362         }
00363 
00364         if ( !isset( $this->initialisedLangs[$code] ) ) {
00365             $this->initLanguage( $code );
00366         }
00367 
00368         // Check to see if initLanguage() loaded it for us
00369         if ( isset( $this->loadedItems[$code][$key] ) ||
00370              isset( $this->loadedSubitems[$code][$key][$subkey] ) ) {
00371             return;
00372         }
00373 
00374         if ( isset( $this->shallowFallbacks[$code] ) ) {
00375             $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
00376             return;
00377         }
00378 
00379         $value = $this->store->get( $code, "$key:$subkey" );
00380         $this->data[$code][$key][$subkey] = $value;
00381         $this->loadedSubitems[$code][$key][$subkey] = true;
00382     }
00383 
00391     public function isExpired( $code ) {
00392         if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
00393             wfDebug( __METHOD__ . "($code): forced reload\n" );
00394             return true;
00395         }
00396 
00397         $deps = $this->store->get( $code, 'deps' );
00398         $keys = $this->store->get( $code, 'list' );
00399         $preload = $this->store->get( $code, 'preload' );
00400         // Different keys may expire separately, at least in LCStore_Accel
00401         if ( $deps === null || $keys === null || $preload === null ) {
00402             wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" );
00403             return true;
00404         }
00405 
00406         foreach ( $deps as $dep ) {
00407             // Because we're unserializing stuff from cache, we
00408             // could receive objects of classes that don't exist
00409             // anymore (e.g. uninstalled extensions)
00410             // When this happens, always expire the cache
00411             if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
00412                 wfDebug( __METHOD__ . "($code): cache for $code expired due to " .
00413                     get_class( $dep ) . "\n" );
00414                 return true;
00415             }
00416         }
00417 
00418         return false;
00419     }
00420 
00426     protected function initLanguage( $code ) {
00427         if ( isset( $this->initialisedLangs[$code] ) ) {
00428             return;
00429         }
00430 
00431         $this->initialisedLangs[$code] = true;
00432 
00433         # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
00434         if ( !Language::isValidBuiltInCode( $code ) ) {
00435             $this->initShallowFallback( $code, 'en' );
00436             return;
00437         }
00438 
00439         # Recache the data if necessary
00440         if ( !$this->manualRecache && $this->isExpired( $code ) ) {
00441             if ( file_exists( Language::getMessagesFileName( $code ) ) ) {
00442                 $this->recache( $code );
00443             } elseif ( $code === 'en' ) {
00444                 throw new MWException( 'MessagesEn.php is missing.' );
00445             } else {
00446                 $this->initShallowFallback( $code, 'en' );
00447             }
00448             return;
00449         }
00450 
00451         # Preload some stuff
00452         $preload = $this->getItem( $code, 'preload' );
00453         if ( $preload === null ) {
00454             if ( $this->manualRecache ) {
00455                 // No Messages*.php file. Do shallow fallback to en.
00456                 if ( $code === 'en' ) {
00457                     throw new MWException( 'No localisation cache found for English. ' .
00458                         'Please run maintenance/rebuildLocalisationCache.php.' );
00459                 }
00460                 $this->initShallowFallback( $code, 'en' );
00461                 return;
00462             } else {
00463                 throw new MWException( 'Invalid or missing localisation cache.' );
00464             }
00465         }
00466         $this->data[$code] = $preload;
00467         foreach ( $preload as $key => $item ) {
00468             if ( in_array( $key, self::$splitKeys ) ) {
00469                 foreach ( $item as $subkey => $subitem ) {
00470                     $this->loadedSubitems[$code][$key][$subkey] = true;
00471                 }
00472             } else {
00473                 $this->loadedItems[$code][$key] = true;
00474             }
00475         }
00476     }
00477 
00484     public function initShallowFallback( $primaryCode, $fallbackCode ) {
00485         $this->data[$primaryCode] =& $this->data[$fallbackCode];
00486         $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
00487         $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
00488         $this->shallowFallbacks[$primaryCode] = $fallbackCode;
00489     }
00490 
00498     protected function readPHPFile( $_fileName, $_fileType ) {
00499         wfProfileIn( __METHOD__ );
00500         // Disable APC caching
00501         $_apcEnabled = ini_set( 'apc.cache_by_default', '0' );
00502         include $_fileName;
00503         ini_set( 'apc.cache_by_default', $_apcEnabled );
00504 
00505         if ( $_fileType == 'core' || $_fileType == 'extension' ) {
00506             $data = compact( self::$allKeys );
00507         } elseif ( $_fileType == 'aliases' ) {
00508             $data = compact( 'aliases' );
00509         } else {
00510             wfProfileOut( __METHOD__ );
00511             throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
00512         }
00513         wfProfileOut( __METHOD__ );
00514         return $data;
00515     }
00516 
00521     public function getCompiledPluralRules( $code ) {
00522         $rules = $this->getPluralRules( $code );
00523         if ( $rules === null ) {
00524             return null;
00525         }
00526         try {
00527             $compiledRules = CLDRPluralRuleEvaluator::compile( $rules );
00528         } catch ( CLDRPluralRuleError $e ) {
00529             wfDebugLog( 'l10n', $e->getMessage() . "\n" );
00530             return array();
00531         }
00532         return $compiledRules;
00533     }
00534 
00540     public function getPluralRules( $code ) {
00541         if ( $this->pluralRules === null ) {
00542             $this->loadPluralFiles();
00543         }
00544         if ( !isset( $this->pluralRules[$code] ) ) {
00545             return null;
00546         } else {
00547             return $this->pluralRules[$code];
00548         }
00549     }
00550 
00556     public function getPluralRuleTypes( $code ) {
00557         if ( $this->pluralRuleTypes === null ) {
00558             $this->loadPluralFiles();
00559         }
00560         if ( !isset( $this->pluralRuleTypes[$code] ) ) {
00561             return null;
00562         } else {
00563             return $this->pluralRuleTypes[$code];
00564         }
00565     }
00566 
00570     protected function loadPluralFiles() {
00571         global $IP;
00572         $cldrPlural = "$IP/languages/data/plurals.xml";
00573         $mwPlural = "$IP/languages/data/plurals-mediawiki.xml";
00574         // Load CLDR plural rules
00575         $this->loadPluralFile( $cldrPlural );
00576         if ( file_exists( $mwPlural ) ) {
00577             // Override or extend
00578             $this->loadPluralFile( $mwPlural );
00579         }
00580     }
00581 
00586     protected function loadPluralFile( $fileName ) {
00587         $doc = new DOMDocument;
00588         $doc->load( $fileName );
00589         $rulesets = $doc->getElementsByTagName( "pluralRules" );
00590         foreach ( $rulesets as $ruleset ) {
00591             $codes = $ruleset->getAttribute( 'locales' );
00592             $rules = array();
00593             $ruleTypes = array();
00594             $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
00595             foreach ( $ruleElements as $elt ) {
00596                 $ruleType = $elt->getAttribute( 'count' );
00597                 if ( $ruleType === 'other' ) {
00598                     // Don't record "other" rules, which have an empty condition
00599                     continue;
00600                 }
00601                 $rules[] = $elt->nodeValue;
00602                 $ruleTypes[] = $ruleType;
00603             }
00604             foreach ( explode( ' ', $codes ) as $code ) {
00605                 $this->pluralRules[$code] = $rules;
00606                 $this->pluralRuleTypes[$code] = $ruleTypes;
00607             }
00608         }
00609     }
00610 
00616     protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
00617         global $IP;
00618         wfProfileIn( __METHOD__ );
00619 
00620         $fileName = Language::getMessagesFileName( $code );
00621         if ( !file_exists( $fileName ) ) {
00622             wfProfileOut( __METHOD__ );
00623             return false;
00624         }
00625 
00626         $deps[] = new FileDependency( $fileName );
00627         $data = $this->readPHPFile( $fileName, 'core' );
00628 
00629         # Load CLDR plural rules for JavaScript
00630         $data['pluralRules'] = $this->getPluralRules( $code );
00631         # And for PHP
00632         $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
00633         # Load plural rule types
00634         $data['pluralRuleTypes'] = $this->getPluralRuleTypes( $code );
00635 
00636         $deps['plurals'] = new FileDependency( "$IP/languages/data/plurals.xml" );
00637         $deps['plurals-mw'] = new FileDependency( "$IP/languages/data/plurals-mediawiki.xml" );
00638 
00639         wfProfileOut( __METHOD__ );
00640         return $data;
00641     }
00642 
00650     protected function mergeItem( $key, &$value, $fallbackValue ) {
00651         if ( !is_null( $value ) ) {
00652             if ( !is_null( $fallbackValue ) ) {
00653                 if ( in_array( $key, self::$mergeableMapKeys ) ) {
00654                     $value = $value + $fallbackValue;
00655                 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
00656                     $value = array_unique( array_merge( $fallbackValue, $value ) );
00657                 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
00658                     $value = array_merge_recursive( $value, $fallbackValue );
00659                 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
00660                     if ( !empty( $value['inherit'] ) ) {
00661                         $value = array_merge( $fallbackValue, $value );
00662                     }
00663 
00664                     if ( isset( $value['inherit'] ) ) {
00665                         unset( $value['inherit'] );
00666                     }
00667                 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
00668                     $this->mergeMagicWords( $value, $fallbackValue );
00669                 }
00670             }
00671         } else {
00672             $value = $fallbackValue;
00673         }
00674     }
00675 
00680     protected function mergeMagicWords( &$value, $fallbackValue ) {
00681         foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
00682             if ( !isset( $value[$magicName] ) ) {
00683                 $value[$magicName] = $fallbackInfo;
00684             } else {
00685                 $oldSynonyms = array_slice( $fallbackInfo, 1 );
00686                 $newSynonyms = array_slice( $value[$magicName], 1 );
00687                 $synonyms = array_values( array_unique( array_merge(
00688                     $newSynonyms, $oldSynonyms ) ) );
00689                 $value[$magicName] = array_merge( array( $fallbackInfo[0] ), $synonyms );
00690             }
00691         }
00692     }
00693 
00707     protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
00708         $used = false;
00709         foreach ( $codeSequence as $code ) {
00710             if ( isset( $fallbackValue[$code] ) ) {
00711                 $this->mergeItem( $key, $value, $fallbackValue[$code] );
00712                 $used = true;
00713             }
00714         }
00715 
00716         return $used;
00717     }
00718 
00725     public function recache( $code ) {
00726         global $wgExtensionMessagesFiles;
00727         wfProfileIn( __METHOD__ );
00728 
00729         if ( !$code ) {
00730             wfProfileOut( __METHOD__ );
00731             throw new MWException( "Invalid language code requested" );
00732         }
00733         $this->recachedLangs[$code] = true;
00734 
00735         # Initial values
00736         $initialData = array_combine(
00737             self::$allKeys,
00738             array_fill( 0, count( self::$allKeys ), null ) );
00739         $coreData = $initialData;
00740         $deps = array();
00741 
00742         # Load the primary localisation from the source file
00743         $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
00744         if ( $data === false ) {
00745             wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
00746             $coreData['fallback'] = 'en';
00747         } else {
00748             wfDebug( __METHOD__ . ": got localisation for $code from source\n" );
00749 
00750             # Merge primary localisation
00751             foreach ( $data as $key => $value ) {
00752                 $this->mergeItem( $key, $coreData[$key], $value );
00753             }
00754 
00755         }
00756 
00757         # Fill in the fallback if it's not there already
00758         if ( is_null( $coreData['fallback'] ) ) {
00759             $coreData['fallback'] = $code === 'en' ? false : 'en';
00760         }
00761         if ( $coreData['fallback'] === false ) {
00762             $coreData['fallbackSequence'] = array();
00763         } else {
00764             $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
00765             $len = count( $coreData['fallbackSequence'] );
00766 
00767             # Ensure that the sequence ends at en
00768             if ( $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
00769                 $coreData['fallbackSequence'][] = 'en';
00770             }
00771 
00772             # Load the fallback localisation item by item and merge it
00773             foreach ( $coreData['fallbackSequence'] as $fbCode ) {
00774                 # Load the secondary localisation from the source file to
00775                 # avoid infinite cycles on cyclic fallbacks
00776                 $fbData = $this->readSourceFilesAndRegisterDeps( $fbCode, $deps );
00777                 if ( $fbData === false ) {
00778                     continue;
00779                 }
00780 
00781                 foreach ( self::$allKeys as $key ) {
00782                     if ( !isset( $fbData[$key] ) ) {
00783                         continue;
00784                     }
00785 
00786                     if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) {
00787                         $this->mergeItem( $key, $coreData[$key], $fbData[$key] );
00788                     }
00789                 }
00790             }
00791         }
00792 
00793         $codeSequence = array_merge( array( $code ), $coreData['fallbackSequence'] );
00794 
00795         # Load the extension localisations
00796         # This is done after the core because we know the fallback sequence now.
00797         # But it has a higher precedence for merging so that we can support things
00798         # like site-specific message overrides.
00799         wfProfileIn( __METHOD__ . '-extensions' );
00800         $allData = $initialData;
00801         foreach ( $wgExtensionMessagesFiles as $fileName ) {
00802             $data = $this->readPHPFile( $fileName, 'extension' );
00803             $used = false;
00804 
00805             foreach ( $data as $key => $item ) {
00806                 if ( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
00807                     $used = true;
00808                 }
00809             }
00810 
00811             if ( $used ) {
00812                 $deps[] = new FileDependency( $fileName );
00813             }
00814         }
00815 
00816         # Merge core data into extension data
00817         foreach ( $coreData as $key => $item ) {
00818             $this->mergeItem( $key, $allData[$key], $item );
00819         }
00820         wfProfileOut( __METHOD__ . '-extensions' );
00821 
00822         # Add cache dependencies for any referenced globals
00823         $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
00824         $deps['version'] = new ConstantDependency( 'MW_LC_VERSION' );
00825 
00826         # Add dependencies to the cache entry
00827         $allData['deps'] = $deps;
00828 
00829         # Replace spaces with underscores in namespace names
00830         $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
00831 
00832         # And do the same for special page aliases. $page is an array.
00833         foreach ( $allData['specialPageAliases'] as &$page ) {
00834             $page = str_replace( ' ', '_', $page );
00835         }
00836         # Decouple the reference to prevent accidental damage
00837         unset( $page );
00838 
00839         # If there were no plural rules, return an empty array
00840         if ( $allData['pluralRules'] === null ) {
00841             $allData['pluralRules'] = array();
00842         }
00843         if ( $allData['compiledPluralRules'] === null ) {
00844             $allData['compiledPluralRules'] = array();
00845         }
00846         # If there were no plural rule types, return an empty array
00847         if ( $allData['pluralRuleTypes'] === null ) {
00848             $allData['pluralRuleTypes'] = array();
00849         }
00850 
00851         # Set the list keys
00852         $allData['list'] = array();
00853         foreach ( self::$splitKeys as $key ) {
00854             $allData['list'][$key] = array_keys( $allData[$key] );
00855         }
00856         # Run hooks
00857         wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) );
00858 
00859         if ( is_null( $allData['namespaceNames'] ) ) {
00860             wfProfileOut( __METHOD__ );
00861             throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
00862                 'Check that your languages/messages/MessagesEn.php file is intact.' );
00863         }
00864 
00865         # Set the preload key
00866         $allData['preload'] = $this->buildPreload( $allData );
00867 
00868         # Save to the process cache and register the items loaded
00869         $this->data[$code] = $allData;
00870         foreach ( $allData as $key => $item ) {
00871             $this->loadedItems[$code][$key] = true;
00872         }
00873 
00874         # Save to the persistent cache
00875         wfProfileIn( __METHOD__ . '-write' );
00876         $this->store->startWrite( $code );
00877         foreach ( $allData as $key => $value ) {
00878             if ( in_array( $key, self::$splitKeys ) ) {
00879                 foreach ( $value as $subkey => $subvalue ) {
00880                     $this->store->set( "$key:$subkey", $subvalue );
00881                 }
00882             } else {
00883                 $this->store->set( $key, $value );
00884             }
00885         }
00886         $this->store->finishWrite();
00887         wfProfileOut( __METHOD__ . '-write' );
00888 
00889         # Clear out the MessageBlobStore
00890         # HACK: If using a null (i.e. disabled) storage backend, we
00891         # can't write to the MessageBlobStore either
00892         if ( !$this->store instanceof LCStore_Null ) {
00893             MessageBlobStore::clear();
00894         }
00895 
00896         wfProfileOut( __METHOD__ );
00897     }
00898 
00907     protected function buildPreload( $data ) {
00908         $preload = array( 'messages' => array() );
00909         foreach ( self::$preloadedKeys as $key ) {
00910             $preload[$key] = $data[$key];
00911         }
00912 
00913         foreach ( $data['preloadedMessages'] as $subkey ) {
00914             if ( isset( $data['messages'][$subkey] ) ) {
00915                 $subitem = $data['messages'][$subkey];
00916             } else {
00917                 $subitem = null;
00918             }
00919             $preload['messages'][$subkey] = $subitem;
00920         }
00921 
00922         return $preload;
00923     }
00924 
00930     public function unload( $code ) {
00931         unset( $this->data[$code] );
00932         unset( $this->loadedItems[$code] );
00933         unset( $this->loadedSubitems[$code] );
00934         unset( $this->initialisedLangs[$code] );
00935         unset( $this->shallowFallbacks[$code] );
00936 
00937         foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
00938             if ( $fbCode === $code ) {
00939                 $this->unload( $shallowCode );
00940             }
00941         }
00942     }
00943 
00947     public function unloadAll() {
00948         foreach ( $this->initialisedLangs as $lang => $unused ) {
00949             $this->unload( $lang );
00950         }
00951     }
00952 
00956     public function disableBackend() {
00957         $this->store = new LCStore_Null;
00958         $this->manualRecache = false;
00959     }
00960 
00966     public function getInitialisedLanguages() {
00967         return $this->initialisedLangs;
00968     }
00969 
00975     public function setInitialisedLanguages( $languages = array() ) {
00976         $this->initialisedLangs = $languages;
00977     }
00978 
00979 }
00980 
00998 interface LCStore {
01004     function get( $code, $key );
01005 
01010     function startWrite( $code );
01011 
01015     function finishWrite();
01016 
01023     function set( $key, $value );
01024 }
01025 
01031 class LCStore_Accel implements LCStore {
01032     var $currentLang;
01033     var $keys;
01034 
01035     public function __construct() {
01036         $this->cache = wfGetCache( CACHE_ACCEL );
01037     }
01038 
01039     public function get( $code, $key ) {
01040         $k = wfMemcKey( 'l10n', $code, 'k', $key );
01041         $r = $this->cache->get( $k );
01042         return $r === false ? null : $r;
01043     }
01044 
01045     public function startWrite( $code ) {
01046         $k = wfMemcKey( 'l10n', $code, 'l' );
01047         $keys = $this->cache->get( $k );
01048         if ( $keys ) {
01049             foreach ( $keys as $k ) {
01050                 $this->cache->delete( $k );
01051             }
01052         }
01053         $this->currentLang = $code;
01054         $this->keys = array();
01055     }
01056 
01057     public function finishWrite() {
01058         if ( $this->currentLang ) {
01059             $k = wfMemcKey( 'l10n', $this->currentLang, 'l' );
01060             $this->cache->set( $k, array_keys( $this->keys ) );
01061         }
01062         $this->currentLang = null;
01063         $this->keys = array();
01064     }
01065 
01066     public function set( $key, $value ) {
01067         if ( $this->currentLang ) {
01068             $k = wfMemcKey( 'l10n', $this->currentLang, 'k', $key );
01069             $this->keys[$k] = true;
01070             $this->cache->set( $k, $value );
01071         }
01072     }
01073 }
01074 
01079 class LCStore_DB implements LCStore {
01080     var $currentLang;
01081     var $writesDone = false;
01082 
01086     var $dbw;
01087     var $batch;
01088     var $readOnly = false;
01089 
01090     public function get( $code, $key ) {
01091         if ( $this->writesDone ) {
01092             $db = wfGetDB( DB_MASTER );
01093         } else {
01094             $db = wfGetDB( DB_SLAVE );
01095         }
01096         $row = $db->selectRow( 'l10n_cache', array( 'lc_value' ),
01097             array( 'lc_lang' => $code, 'lc_key' => $key ), __METHOD__ );
01098         if ( $row ) {
01099             return unserialize( $row->lc_value );
01100         } else {
01101             return null;
01102         }
01103     }
01104 
01105     public function startWrite( $code ) {
01106         if ( $this->readOnly ) {
01107             return;
01108         }
01109 
01110         if ( !$code ) {
01111             throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
01112         }
01113 
01114         $this->dbw = wfGetDB( DB_MASTER );
01115         try {
01116             $this->dbw->begin( __METHOD__ );
01117             $this->dbw->delete( 'l10n_cache', array( 'lc_lang' => $code ), __METHOD__ );
01118         } catch ( DBQueryError $e ) {
01119             if ( $this->dbw->wasReadOnlyError() ) {
01120                 $this->readOnly = true;
01121                 $this->dbw->rollback( __METHOD__ );
01122                 return;
01123             } else {
01124                 throw $e;
01125             }
01126         }
01127 
01128         $this->currentLang = $code;
01129         $this->batch = array();
01130     }
01131 
01132     public function finishWrite() {
01133         if ( $this->readOnly ) {
01134             return;
01135         }
01136 
01137         if ( $this->batch ) {
01138             $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
01139         }
01140 
01141         $this->dbw->commit( __METHOD__ );
01142         $this->currentLang = null;
01143         $this->dbw = null;
01144         $this->batch = array();
01145         $this->writesDone = true;
01146     }
01147 
01148     public function set( $key, $value ) {
01149         if ( $this->readOnly ) {
01150             return;
01151         }
01152 
01153         if ( is_null( $this->currentLang ) ) {
01154             throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
01155         }
01156 
01157         $this->batch[] = array(
01158             'lc_lang' => $this->currentLang,
01159             'lc_key' => $key,
01160             'lc_value' => serialize( $value ) );
01161 
01162         if ( count( $this->batch ) >= 100 ) {
01163             $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
01164             $this->batch = array();
01165         }
01166     }
01167 }
01168 
01181 class LCStore_CDB implements LCStore {
01182     var $readers, $writer, $currentLang, $directory;
01183 
01184     function __construct( $conf = array() ) {
01185         global $wgCacheDirectory;
01186 
01187         if ( isset( $conf['directory'] ) ) {
01188             $this->directory = $conf['directory'];
01189         } else {
01190             $this->directory = $wgCacheDirectory;
01191         }
01192     }
01193 
01194     public function get( $code, $key ) {
01195         if ( !isset( $this->readers[$code] ) ) {
01196             $fileName = $this->getFileName( $code );
01197 
01198             if ( !file_exists( $fileName ) ) {
01199                 $this->readers[$code] = false;
01200             } else {
01201                 $this->readers[$code] = CdbReader::open( $fileName );
01202             }
01203         }
01204 
01205         if ( !$this->readers[$code] ) {
01206             return null;
01207         } else {
01208             $value = $this->readers[$code]->get( $key );
01209 
01210             if ( $value === false ) {
01211                 return null;
01212             }
01213             return unserialize( $value );
01214         }
01215     }
01216 
01217     public function startWrite( $code ) {
01218         if ( !file_exists( $this->directory ) ) {
01219             if ( !wfMkdirParents( $this->directory, null, __METHOD__ ) ) {
01220                 throw new MWException( "Unable to create the localisation store " .
01221                     "directory \"{$this->directory}\"" );
01222             }
01223         }
01224 
01225         // Close reader to stop permission errors on write
01226         if ( !empty( $this->readers[$code] ) ) {
01227             $this->readers[$code]->close();
01228         }
01229 
01230         $this->writer = CdbWriter::open( $this->getFileName( $code ) );
01231         $this->currentLang = $code;
01232     }
01233 
01234     public function finishWrite() {
01235         // Close the writer
01236         $this->writer->close();
01237         $this->writer = null;
01238         unset( $this->readers[$this->currentLang] );
01239         $this->currentLang = null;
01240     }
01241 
01242     public function set( $key, $value ) {
01243         if ( is_null( $this->writer ) ) {
01244             throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
01245         }
01246         $this->writer->set( $key, serialize( $value ) );
01247     }
01248 
01249     protected function getFileName( $code ) {
01250         if ( strval( $code ) === '' || strpos( $code, '/' ) !== false ) {
01251             throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
01252         }
01253         return "{$this->directory}/l10n_cache-$code.cdb";
01254     }
01255 }
01256 
01260 class LCStore_Null implements LCStore {
01261     public function get( $code, $key ) {
01262         return null;
01263     }
01264 
01265     public function startWrite( $code ) {}
01266     public function finishWrite() {}
01267     public function set( $key, $value ) {}
01268 }
01269 
01274 class LocalisationCache_BulkLoad extends LocalisationCache {
01279     var $fileCache = array();
01280 
01286     var $mruLangs = array();
01287 
01291     var $maxLoadedLangs = 10;
01292 
01298     protected function readPHPFile( $fileName, $fileType ) {
01299         $serialize = $fileType === 'core';
01300         if ( !isset( $this->fileCache[$fileName][$fileType] ) ) {
01301             $data = parent::readPHPFile( $fileName, $fileType );
01302 
01303             if ( $serialize ) {
01304                 $encData = serialize( $data );
01305             } else {
01306                 $encData = $data;
01307             }
01308 
01309             $this->fileCache[$fileName][$fileType] = $encData;
01310 
01311             return $data;
01312         } elseif ( $serialize ) {
01313             return unserialize( $this->fileCache[$fileName][$fileType] );
01314         } else {
01315             return $this->fileCache[$fileName][$fileType];
01316         }
01317     }
01318 
01324     public function getItem( $code, $key ) {
01325         unset( $this->mruLangs[$code] );
01326         $this->mruLangs[$code] = true;
01327         return parent::getItem( $code, $key );
01328     }
01329 
01336     public function getSubitem( $code, $key, $subkey ) {
01337         unset( $this->mruLangs[$code] );
01338         $this->mruLangs[$code] = true;
01339         return parent::getSubitem( $code, $key, $subkey );
01340     }
01341 
01345     public function recache( $code ) {
01346         parent::recache( $code );
01347         unset( $this->mruLangs[$code] );
01348         $this->mruLangs[$code] = true;
01349         $this->trimCache();
01350     }
01351 
01355     public function unload( $code ) {
01356         unset( $this->mruLangs[$code] );
01357         parent::unload( $code );
01358     }
01359 
01363     protected function trimCache() {
01364         while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) {
01365             reset( $this->mruLangs );
01366             $code = key( $this->mruLangs );
01367             wfDebug( __METHOD__ . ": unloading $code\n" );
01368             $this->unload( $code );
01369         }
01370     }
01371 }