MediaWiki  REL1_21
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', '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 
00163         var $mergeableKeys = null;
00164 
00173         function __construct( $conf ) {
00174                 global $wgCacheDirectory;
00175 
00176                 $this->conf = $conf;
00177                 $storeConf = array();
00178                 if ( !empty( $conf['storeClass'] ) ) {
00179                         $storeClass = $conf['storeClass'];
00180                 } else {
00181                         switch ( $conf['store'] ) {
00182                                 case 'files':
00183                                 case 'file':
00184                                         $storeClass = 'LCStore_CDB';
00185                                         break;
00186                                 case 'db':
00187                                         $storeClass = 'LCStore_DB';
00188                                         break;
00189                                 case 'accel':
00190                                         $storeClass = 'LCStore_Accel';
00191                                         break;
00192                                 case 'detect':
00193                                         $storeClass = $wgCacheDirectory ? 'LCStore_CDB' : 'LCStore_DB';
00194                                         break;
00195                                 default:
00196                                         throw new MWException(
00197                                                 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' );
00198                         }
00199                 }
00200 
00201                 wfDebug( get_class( $this ) . ": using store $storeClass\n" );
00202                 if ( !empty( $conf['storeDirectory'] ) ) {
00203                         $storeConf['directory'] = $conf['storeDirectory'];
00204                 }
00205 
00206                 $this->store = new $storeClass( $storeConf );
00207                 foreach ( array( 'manualRecache', 'forceRecache' ) as $var ) {
00208                         if ( isset( $conf[$var] ) ) {
00209                                 $this->$var = $conf[$var];
00210                         }
00211                 }
00212         }
00213 
00220         public function isMergeableKey( $key ) {
00221                 if ( $this->mergeableKeys === null ) {
00222                         $this->mergeableKeys = array_flip( array_merge(
00223                                 self::$mergeableMapKeys,
00224                                 self::$mergeableListKeys,
00225                                 self::$mergeableAliasListKeys,
00226                                 self::$optionalMergeKeys,
00227                                 self::$magicWordKeys
00228                         ) );
00229                 }
00230                 return isset( $this->mergeableKeys[$key] );
00231         }
00232 
00242         public function getItem( $code, $key ) {
00243                 if ( !isset( $this->loadedItems[$code][$key] ) ) {
00244                         wfProfileIn( __METHOD__ . '-load' );
00245                         $this->loadItem( $code, $key );
00246                         wfProfileOut( __METHOD__ . '-load' );
00247                 }
00248 
00249                 if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
00250                         return $this->shallowFallbacks[$code];
00251                 }
00252 
00253                 return $this->data[$code][$key];
00254         }
00255 
00263         public function getSubitem( $code, $key, $subkey ) {
00264                 if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
00265                          !isset( $this->loadedItems[$code][$key] ) ) {
00266                         wfProfileIn( __METHOD__ . '-load' );
00267                         $this->loadSubitem( $code, $key, $subkey );
00268                         wfProfileOut( __METHOD__ . '-load' );
00269                 }
00270 
00271                 if ( isset( $this->data[$code][$key][$subkey] ) ) {
00272                         return $this->data[$code][$key][$subkey];
00273                 } else {
00274                         return null;
00275                 }
00276         }
00277 
00290         public function getSubitemList( $code, $key ) {
00291                 if ( in_array( $key, self::$splitKeys ) ) {
00292                         return $this->getSubitem( $code, 'list', $key );
00293                 } else {
00294                         $item = $this->getItem( $code, $key );
00295                         if ( is_array( $item ) ) {
00296                                 return array_keys( $item );
00297                         } else {
00298                                 return false;
00299                         }
00300                 }
00301         }
00302 
00308         protected function loadItem( $code, $key ) {
00309                 if ( !isset( $this->initialisedLangs[$code] ) ) {
00310                         $this->initLanguage( $code );
00311                 }
00312 
00313                 // Check to see if initLanguage() loaded it for us
00314                 if ( isset( $this->loadedItems[$code][$key] ) ) {
00315                         return;
00316                 }
00317 
00318                 if ( isset( $this->shallowFallbacks[$code] ) ) {
00319                         $this->loadItem( $this->shallowFallbacks[$code], $key );
00320                         return;
00321                 }
00322 
00323                 if ( in_array( $key, self::$splitKeys ) ) {
00324                         $subkeyList = $this->getSubitem( $code, 'list', $key );
00325                         foreach ( $subkeyList as $subkey ) {
00326                                 if ( isset( $this->data[$code][$key][$subkey] ) ) {
00327                                         continue;
00328                                 }
00329                                 $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey );
00330                         }
00331                 } else {
00332                         $this->data[$code][$key] = $this->store->get( $code, $key );
00333                 }
00334 
00335                 $this->loadedItems[$code][$key] = true;
00336         }
00337 
00345         protected function loadSubitem( $code, $key, $subkey ) {
00346                 if ( !in_array( $key, self::$splitKeys ) ) {
00347                         $this->loadItem( $code, $key );
00348                         return;
00349                 }
00350 
00351                 if ( !isset( $this->initialisedLangs[$code] ) ) {
00352                         $this->initLanguage( $code );
00353                 }
00354 
00355                 // Check to see if initLanguage() loaded it for us
00356                 if ( isset( $this->loadedItems[$code][$key] ) ||
00357                          isset( $this->loadedSubitems[$code][$key][$subkey] ) ) {
00358                         return;
00359                 }
00360 
00361                 if ( isset( $this->shallowFallbacks[$code] ) ) {
00362                         $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
00363                         return;
00364                 }
00365 
00366                 $value = $this->store->get( $code, "$key:$subkey" );
00367                 $this->data[$code][$key][$subkey] = $value;
00368                 $this->loadedSubitems[$code][$key][$subkey] = true;
00369         }
00370 
00375         public function isExpired( $code ) {
00376                 if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
00377                         wfDebug( __METHOD__ . "($code): forced reload\n" );
00378                         return true;
00379                 }
00380 
00381                 $deps = $this->store->get( $code, 'deps' );
00382                 $keys = $this->store->get( $code, 'list' );
00383                 $preload = $this->store->get( $code, 'preload' );
00384                 // Different keys may expire separately, at least in LCStore_Accel
00385                 if ( $deps === null || $keys === null || $preload === null ) {
00386                         wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" );
00387                         return true;
00388                 }
00389 
00390                 foreach ( $deps as $dep ) {
00391                         // Because we're unserializing stuff from cache, we
00392                         // could receive objects of classes that don't exist
00393                         // anymore (e.g. uninstalled extensions)
00394                         // When this happens, always expire the cache
00395                         if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
00396                                 wfDebug( __METHOD__ . "($code): cache for $code expired due to " .
00397                                         get_class( $dep ) . "\n" );
00398                                 return true;
00399                         }
00400                 }
00401 
00402                 return false;
00403         }
00404 
00410         protected function initLanguage( $code ) {
00411                 if ( isset( $this->initialisedLangs[$code] ) ) {
00412                         return;
00413                 }
00414 
00415                 $this->initialisedLangs[$code] = true;
00416 
00417                 # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
00418                 if ( !Language::isValidBuiltInCode( $code ) ) {
00419                         $this->initShallowFallback( $code, 'en' );
00420                         return;
00421                 }
00422 
00423                 # Recache the data if necessary
00424                 if ( !$this->manualRecache && $this->isExpired( $code ) ) {
00425                         if ( file_exists( Language::getMessagesFileName( $code ) ) ) {
00426                                 $this->recache( $code );
00427                         } elseif ( $code === 'en' ) {
00428                                 throw new MWException( 'MessagesEn.php is missing.' );
00429                         } else {
00430                                 $this->initShallowFallback( $code, 'en' );
00431                         }
00432                         return;
00433                 }
00434 
00435                 # Preload some stuff
00436                 $preload = $this->getItem( $code, 'preload' );
00437                 if ( $preload === null ) {
00438                         if ( $this->manualRecache ) {
00439                                 // No Messages*.php file. Do shallow fallback to en.
00440                                 if ( $code === 'en' ) {
00441                                         throw new MWException( 'No localisation cache found for English. ' .
00442                                                 'Please run maintenance/rebuildLocalisationCache.php.' );
00443                                 }
00444                                 $this->initShallowFallback( $code, 'en' );
00445                                 return;
00446                         } else {
00447                                 throw new MWException( 'Invalid or missing localisation cache.' );
00448                         }
00449                 }
00450                 $this->data[$code] = $preload;
00451                 foreach ( $preload as $key => $item ) {
00452                         if ( in_array( $key, self::$splitKeys ) ) {
00453                                 foreach ( $item as $subkey => $subitem ) {
00454                                         $this->loadedSubitems[$code][$key][$subkey] = true;
00455                                 }
00456                         } else {
00457                                 $this->loadedItems[$code][$key] = true;
00458                         }
00459                 }
00460         }
00461 
00468         public function initShallowFallback( $primaryCode, $fallbackCode ) {
00469                 $this->data[$primaryCode] =& $this->data[$fallbackCode];
00470                 $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
00471                 $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
00472                 $this->shallowFallbacks[$primaryCode] = $fallbackCode;
00473         }
00474 
00482         protected function readPHPFile( $_fileName, $_fileType ) {
00483                 // Disable APC caching
00484                 $_apcEnabled = ini_set( 'apc.cache_by_default', '0' );
00485                 include( $_fileName );
00486                 ini_set( 'apc.cache_by_default', $_apcEnabled );
00487 
00488                 if ( $_fileType == 'core' || $_fileType == 'extension' ) {
00489                         $data = compact( self::$allKeys );
00490                 } elseif ( $_fileType == 'aliases' ) {
00491                         $data = compact( 'aliases' );
00492                 } else {
00493                         throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
00494                 }
00495                 return $data;
00496         }
00497 
00502         public function getCompiledPluralRules( $code ) {
00503                 $rules = $this->getPluralRules( $code );
00504                 if ( $rules === null ) {
00505                         return null;
00506                 }
00507                 try {
00508                         $compiledRules = CLDRPluralRuleEvaluator::compile( $rules );
00509                 } catch( CLDRPluralRuleError $e ) {
00510                         wfDebugLog( 'l10n', $e->getMessage() . "\n" );
00511                         return array();
00512                 }
00513                 return $compiledRules;
00514         }
00515 
00521         public function getPluralRules( $code ) {
00522                 global $IP;
00523 
00524                 if ( $this->pluralRules === null ) {
00525                         $cldrPlural = "$IP/languages/data/plurals.xml";
00526                         $mwPlural = "$IP/languages/data/plurals-mediawiki.xml";
00527                         // Load CLDR plural rules
00528                         $this->loadPluralFile( $cldrPlural );
00529                         if ( file_exists( $mwPlural ) ) {
00530                                 // Override or extend
00531                                 $this->loadPluralFile( $mwPlural );
00532                         }
00533                 }
00534                 if ( !isset( $this->pluralRules[$code] ) ) {
00535                         return null;
00536                 } else {
00537                         return $this->pluralRules[$code];
00538                 }
00539         }
00540 
00545         protected function loadPluralFile( $fileName ) {
00546                 $doc = new DOMDocument;
00547                 $doc->load( $fileName );
00548                 $rulesets = $doc->getElementsByTagName( "pluralRules" );
00549                 foreach ( $rulesets as $ruleset ) {
00550                         $codes = $ruleset->getAttribute( 'locales' );
00551                         $rules = array();
00552                         $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
00553                         foreach ( $ruleElements as $elt ) {
00554                                 $rules[] = $elt->nodeValue;
00555                         }
00556                         foreach ( explode( ' ', $codes ) as $code ) {
00557                                 $this->pluralRules[$code] = $rules;
00558                         }
00559                 }
00560         }
00561 
00567         protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
00568                 global $IP;
00569 
00570                 $fileName = Language::getMessagesFileName( $code );
00571                 if ( !file_exists( $fileName ) ) {
00572                         return false;
00573                 }
00574 
00575                 $deps[] = new FileDependency( $fileName );
00576                 $data = $this->readPHPFile( $fileName, 'core' );
00577 
00578                 # Load CLDR plural rules for JavaScript
00579                 $data['pluralRules'] = $this->getPluralRules( $code );
00580                 # And for PHP
00581                 $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
00582 
00583                 $deps['plurals'] = new FileDependency( "$IP/languages/data/plurals.xml" );
00584                 $deps['plurals-mw'] = new FileDependency( "$IP/languages/data/plurals-mediawiki.xml" );
00585 
00586                 return $data;
00587         }
00588 
00596         protected function mergeItem( $key, &$value, $fallbackValue ) {
00597                 if ( !is_null( $value ) ) {
00598                         if ( !is_null( $fallbackValue ) ) {
00599                                 if ( in_array( $key, self::$mergeableMapKeys ) ) {
00600                                         $value = $value + $fallbackValue;
00601                                 } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
00602                                         $value = array_unique( array_merge( $fallbackValue, $value ) );
00603                                 } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
00604                                         $value = array_merge_recursive( $value, $fallbackValue );
00605                                 } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
00606                                         if ( !empty( $value['inherit'] ) ) {
00607                                                 $value = array_merge( $fallbackValue, $value );
00608                                         }
00609 
00610                                         if ( isset( $value['inherit'] ) ) {
00611                                                 unset( $value['inherit'] );
00612                                         }
00613                                 } elseif ( in_array( $key, self::$magicWordKeys ) ) {
00614                                         $this->mergeMagicWords( $value, $fallbackValue );
00615                                 }
00616                         }
00617                 } else {
00618                         $value = $fallbackValue;
00619                 }
00620         }
00621 
00626         protected function mergeMagicWords( &$value, $fallbackValue ) {
00627                 foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
00628                         if ( !isset( $value[$magicName] ) ) {
00629                                 $value[$magicName] = $fallbackInfo;
00630                         } else {
00631                                 $oldSynonyms = array_slice( $fallbackInfo, 1 );
00632                                 $newSynonyms = array_slice( $value[$magicName], 1 );
00633                                 $synonyms = array_values( array_unique( array_merge(
00634                                         $newSynonyms, $oldSynonyms ) ) );
00635                                 $value[$magicName] = array_merge( array( $fallbackInfo[0] ), $synonyms );
00636                         }
00637                 }
00638         }
00639 
00653         protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
00654                 $used = false;
00655                 foreach ( $codeSequence as $code ) {
00656                         if ( isset( $fallbackValue[$code] ) ) {
00657                                 $this->mergeItem( $key, $value, $fallbackValue[$code] );
00658                                 $used = true;
00659                         }
00660                 }
00661 
00662                 return $used;
00663         }
00664 
00671         public function recache( $code ) {
00672                 global $wgExtensionMessagesFiles;
00673                 wfProfileIn( __METHOD__ );
00674 
00675                 if ( !$code ) {
00676                         throw new MWException( "Invalid language code requested" );
00677                 }
00678                 $this->recachedLangs[$code] = true;
00679 
00680                 # Initial values
00681                 $initialData = array_combine(
00682                         self::$allKeys,
00683                         array_fill( 0, count( self::$allKeys ), null ) );
00684                 $coreData = $initialData;
00685                 $deps = array();
00686 
00687                 # Load the primary localisation from the source file
00688                 $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
00689                 if ( $data === false ) {
00690                         wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
00691                         $coreData['fallback'] = 'en';
00692                 } else {
00693                         wfDebug( __METHOD__ . ": got localisation for $code from source\n" );
00694 
00695                         # Merge primary localisation
00696                         foreach ( $data as $key => $value ) {
00697                                 $this->mergeItem( $key, $coreData[$key], $value );
00698                         }
00699 
00700                 }
00701 
00702                 # Fill in the fallback if it's not there already
00703                 if ( is_null( $coreData['fallback'] ) ) {
00704                         $coreData['fallback'] = $code === 'en' ? false : 'en';
00705                 }
00706                 if ( $coreData['fallback'] === false ) {
00707                         $coreData['fallbackSequence'] = array();
00708                 } else {
00709                         $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
00710                         $len = count( $coreData['fallbackSequence'] );
00711 
00712                         # Ensure that the sequence ends at en
00713                         if ( $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
00714                                 $coreData['fallbackSequence'][] = 'en';
00715                         }
00716 
00717                         # Load the fallback localisation item by item and merge it
00718                         foreach ( $coreData['fallbackSequence'] as $fbCode ) {
00719                                 # Load the secondary localisation from the source file to
00720                                 # avoid infinite cycles on cyclic fallbacks
00721                                 $fbData = $this->readSourceFilesAndRegisterDeps( $fbCode, $deps );
00722                                 if ( $fbData === false ) {
00723                                         continue;
00724                                 }
00725 
00726                                 foreach ( self::$allKeys as $key ) {
00727                                         if ( !isset( $fbData[$key] ) ) {
00728                                                 continue;
00729                                         }
00730 
00731                                         if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) {
00732                                                 $this->mergeItem( $key, $coreData[$key], $fbData[$key] );
00733                                         }
00734                                 }
00735                         }
00736                 }
00737 
00738                 $codeSequence = array_merge( array( $code ), $coreData['fallbackSequence'] );
00739 
00740                 # Load the extension localisations
00741                 # This is done after the core because we know the fallback sequence now.
00742                 # But it has a higher precedence for merging so that we can support things
00743                 # like site-specific message overrides.
00744                 $allData = $initialData;
00745                 foreach ( $wgExtensionMessagesFiles as $fileName ) {
00746                         $data = $this->readPHPFile( $fileName, 'extension' );
00747                         $used = false;
00748 
00749                         foreach ( $data as $key => $item ) {
00750                                 if ( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
00751                                         $used = true;
00752                                 }
00753                         }
00754 
00755                         if ( $used ) {
00756                                 $deps[] = new FileDependency( $fileName );
00757                         }
00758                 }
00759 
00760                 # Merge core data into extension data
00761                 foreach ( $coreData as $key => $item ) {
00762                         $this->mergeItem( $key, $allData[$key], $item );
00763                 }
00764 
00765                 # Add cache dependencies for any referenced globals
00766                 $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
00767                 $deps['version'] = new ConstantDependency( 'MW_LC_VERSION' );
00768 
00769                 # Add dependencies to the cache entry
00770                 $allData['deps'] = $deps;
00771 
00772                 # Replace spaces with underscores in namespace names
00773                 $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
00774 
00775                 # And do the same for special page aliases. $page is an array.
00776                 foreach ( $allData['specialPageAliases'] as &$page ) {
00777                         $page = str_replace( ' ', '_', $page );
00778                 }
00779                 # Decouple the reference to prevent accidental damage
00780                 unset( $page );
00781 
00782                 # If there were no plural rules, return an empty array
00783                 if ( $allData['pluralRules'] === null ) {
00784                         $allData['pluralRules'] = array();
00785                 }
00786                 if ( $allData['compiledPluralRules'] === null ) {
00787                         $allData['compiledPluralRules'] = array();
00788                 }
00789 
00790                 # Set the list keys
00791                 $allData['list'] = array();
00792                 foreach ( self::$splitKeys as $key ) {
00793                         $allData['list'][$key] = array_keys( $allData[$key] );
00794                 }
00795                 # Run hooks
00796                 wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) );
00797 
00798                 if ( is_null( $allData['namespaceNames'] ) ) {
00799                         throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
00800                                 'Check that your languages/messages/MessagesEn.php file is intact.' );
00801                 }
00802 
00803                 # Set the preload key
00804                 $allData['preload'] = $this->buildPreload( $allData );
00805 
00806                 # Save to the process cache and register the items loaded
00807                 $this->data[$code] = $allData;
00808                 foreach ( $allData as $key => $item ) {
00809                         $this->loadedItems[$code][$key] = true;
00810                 }
00811 
00812                 # Save to the persistent cache
00813                 $this->store->startWrite( $code );
00814                 foreach ( $allData as $key => $value ) {
00815                         if ( in_array( $key, self::$splitKeys ) ) {
00816                                 foreach ( $value as $subkey => $subvalue ) {
00817                                         $this->store->set( "$key:$subkey", $subvalue );
00818                                 }
00819                         } else {
00820                                 $this->store->set( $key, $value );
00821                         }
00822                 }
00823                 $this->store->finishWrite();
00824 
00825                 # Clear out the MessageBlobStore
00826                 # HACK: If using a null (i.e. disabled) storage backend, we
00827                 # can't write to the MessageBlobStore either
00828                 if ( !$this->store instanceof LCStore_Null ) {
00829                         MessageBlobStore::clear();
00830                 }
00831 
00832                 wfProfileOut( __METHOD__ );
00833         }
00834 
00843         protected function buildPreload( $data ) {
00844                 $preload = array( 'messages' => array() );
00845                 foreach ( self::$preloadedKeys as $key ) {
00846                         $preload[$key] = $data[$key];
00847                 }
00848 
00849                 foreach ( $data['preloadedMessages'] as $subkey ) {
00850                         if ( isset( $data['messages'][$subkey] ) ) {
00851                                 $subitem = $data['messages'][$subkey];
00852                         } else {
00853                                 $subitem = null;
00854                         }
00855                         $preload['messages'][$subkey] = $subitem;
00856                 }
00857 
00858                 return $preload;
00859         }
00860 
00866         public function unload( $code ) {
00867                 unset( $this->data[$code] );
00868                 unset( $this->loadedItems[$code] );
00869                 unset( $this->loadedSubitems[$code] );
00870                 unset( $this->initialisedLangs[$code] );
00871 
00872                 foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
00873                         if ( $fbCode === $code ) {
00874                                 $this->unload( $shallowCode );
00875                         }
00876                 }
00877         }
00878 
00882         public function unloadAll() {
00883                 foreach ( $this->initialisedLangs as $lang => $unused ) {
00884                         $this->unload( $lang );
00885                 }
00886         }
00887 
00891         public function disableBackend() {
00892                 $this->store = new LCStore_Null;
00893                 $this->manualRecache = false;
00894         }
00895 }
00896 
00914 interface LCStore {
00920         function get( $code, $key );
00921 
00926         function startWrite( $code );
00927 
00931         function finishWrite();
00932 
00939         function set( $key, $value );
00940 }
00941 
00947 class LCStore_Accel implements LCStore {
00948         var $currentLang;
00949         var $keys;
00950 
00951         public function __construct() {
00952                 $this->cache = wfGetCache( CACHE_ACCEL );
00953         }
00954 
00955         public function get( $code, $key ) {
00956                 $k = wfMemcKey( 'l10n', $code, 'k', $key );
00957                 $r = $this->cache->get( $k );
00958                 return $r === false ? null : $r;
00959         }
00960 
00961         public function startWrite( $code ) {
00962                 $k = wfMemcKey( 'l10n', $code, 'l' );
00963                 $keys = $this->cache->get( $k );
00964                 if ( $keys ) {
00965                         foreach ( $keys as $k ) {
00966                                 $this->cache->delete( $k );
00967                         }
00968                 }
00969                 $this->currentLang = $code;
00970                 $this->keys = array();
00971         }
00972 
00973         public function finishWrite() {
00974                 if ( $this->currentLang ) {
00975                         $k = wfMemcKey( 'l10n', $this->currentLang, 'l' );
00976                         $this->cache->set( $k, array_keys( $this->keys ) );
00977                 }
00978                 $this->currentLang = null;
00979                 $this->keys = array();
00980         }
00981 
00982         public function set( $key, $value ) {
00983                 if ( $this->currentLang ) {
00984                         $k = wfMemcKey( 'l10n', $this->currentLang, 'k', $key );
00985                         $this->keys[$k] = true;
00986                         $this->cache->set( $k, $value );
00987                 }
00988         }
00989 }
00990 
00995 class LCStore_DB implements LCStore {
00996         var $currentLang;
00997         var $writesDone = false;
00998 
01002         var $dbw;
01003         var $batch;
01004         var $readOnly = false;
01005 
01006         public function get( $code, $key ) {
01007                 if ( $this->writesDone ) {
01008                         $db = wfGetDB( DB_MASTER );
01009                 } else {
01010                         $db = wfGetDB( DB_SLAVE );
01011                 }
01012                 $row = $db->selectRow( 'l10n_cache', array( 'lc_value' ),
01013                         array( 'lc_lang' => $code, 'lc_key' => $key ), __METHOD__ );
01014                 if ( $row ) {
01015                         return unserialize( $row->lc_value );
01016                 } else {
01017                         return null;
01018                 }
01019         }
01020 
01021         public function startWrite( $code ) {
01022                 if ( $this->readOnly ) {
01023                         return;
01024                 }
01025 
01026                 if ( !$code ) {
01027                         throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
01028                 }
01029 
01030                 $this->dbw = wfGetDB( DB_MASTER );
01031                 try {
01032                         $this->dbw->begin( __METHOD__ );
01033                         $this->dbw->delete( 'l10n_cache', array( 'lc_lang' => $code ), __METHOD__ );
01034                 } catch ( DBQueryError $e ) {
01035                         if ( $this->dbw->wasReadOnlyError() ) {
01036                                 $this->readOnly = true;
01037                                 $this->dbw->rollback( __METHOD__ );
01038                                 return;
01039                         } else {
01040                                 throw $e;
01041                         }
01042                 }
01043 
01044                 $this->currentLang = $code;
01045                 $this->batch = array();
01046         }
01047 
01048         public function finishWrite() {
01049                 if ( $this->readOnly ) {
01050                         return;
01051                 }
01052 
01053                 if ( $this->batch ) {
01054                         $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
01055                 }
01056 
01057                 $this->dbw->commit( __METHOD__ );
01058                 $this->currentLang = null;
01059                 $this->dbw = null;
01060                 $this->batch = array();
01061                 $this->writesDone = true;
01062         }
01063 
01064         public function set( $key, $value ) {
01065                 if ( $this->readOnly ) {
01066                         return;
01067                 }
01068 
01069                 if ( is_null( $this->currentLang ) ) {
01070                         throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
01071                 }
01072 
01073                 $this->batch[] = array(
01074                         'lc_lang' => $this->currentLang,
01075                         'lc_key' => $key,
01076                         'lc_value' => serialize( $value ) );
01077 
01078                 if ( count( $this->batch ) >= 100 ) {
01079                         $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
01080                         $this->batch = array();
01081                 }
01082         }
01083 }
01084 
01097 class LCStore_CDB implements LCStore {
01098         var $readers, $writer, $currentLang, $directory;
01099 
01100         function __construct( $conf = array() ) {
01101                 global $wgCacheDirectory;
01102 
01103                 if ( isset( $conf['directory'] ) ) {
01104                         $this->directory = $conf['directory'];
01105                 } else {
01106                         $this->directory = $wgCacheDirectory;
01107                 }
01108         }
01109 
01110         public function get( $code, $key ) {
01111                 if ( !isset( $this->readers[$code] ) ) {
01112                         $fileName = $this->getFileName( $code );
01113 
01114                         if ( !file_exists( $fileName ) ) {
01115                                 $this->readers[$code] = false;
01116                         } else {
01117                                 $this->readers[$code] = CdbReader::open( $fileName );
01118                         }
01119                 }
01120 
01121                 if ( !$this->readers[$code] ) {
01122                         return null;
01123                 } else {
01124                         $value = $this->readers[$code]->get( $key );
01125 
01126                         if ( $value === false ) {
01127                                 return null;
01128                         }
01129                         return unserialize( $value );
01130                 }
01131         }
01132 
01133         public function startWrite( $code ) {
01134                 if ( !file_exists( $this->directory ) ) {
01135                         if ( !wfMkdirParents( $this->directory, null, __METHOD__ ) ) {
01136                                 throw new MWException( "Unable to create the localisation store " .
01137                                         "directory \"{$this->directory}\"" );
01138                         }
01139                 }
01140 
01141                 // Close reader to stop permission errors on write
01142                 if ( !empty( $this->readers[$code] ) ) {
01143                         $this->readers[$code]->close();
01144                 }
01145 
01146                 $this->writer = CdbWriter::open( $this->getFileName( $code ) );
01147                 $this->currentLang = $code;
01148         }
01149 
01150         public function finishWrite() {
01151                 // Close the writer
01152                 $this->writer->close();
01153                 $this->writer = null;
01154                 unset( $this->readers[$this->currentLang] );
01155                 $this->currentLang = null;
01156         }
01157 
01158         public function set( $key, $value ) {
01159                 if ( is_null( $this->writer ) ) {
01160                         throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
01161                 }
01162                 $this->writer->set( $key, serialize( $value ) );
01163         }
01164 
01165         protected function getFileName( $code ) {
01166                 if ( strval( $code ) === '' || strpos( $code, '/' ) !== false ) {
01167                         throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
01168                 }
01169                 return "{$this->directory}/l10n_cache-$code.cdb";
01170         }
01171 }
01172 
01176 class LCStore_Null implements LCStore {
01177         public function get( $code, $key ) {
01178                 return null;
01179         }
01180 
01181         public function startWrite( $code ) {}
01182         public function finishWrite() {}
01183         public function set( $key, $value ) {}
01184 }
01185 
01190 class LocalisationCache_BulkLoad extends LocalisationCache {
01195         var $fileCache = array();
01196 
01202         var $mruLangs = array();
01203 
01207         var $maxLoadedLangs = 10;
01208 
01214         protected function readPHPFile( $fileName, $fileType ) {
01215                 $serialize = $fileType === 'core';
01216                 if ( !isset( $this->fileCache[$fileName][$fileType] ) ) {
01217                         $data = parent::readPHPFile( $fileName, $fileType );
01218 
01219                         if ( $serialize ) {
01220                                 $encData = serialize( $data );
01221                         } else {
01222                                 $encData = $data;
01223                         }
01224 
01225                         $this->fileCache[$fileName][$fileType] = $encData;
01226 
01227                         return $data;
01228                 } elseif ( $serialize ) {
01229                         return unserialize( $this->fileCache[$fileName][$fileType] );
01230                 } else {
01231                         return $this->fileCache[$fileName][$fileType];
01232                 }
01233         }
01234 
01240         public function getItem( $code, $key ) {
01241                 unset( $this->mruLangs[$code] );
01242                 $this->mruLangs[$code] = true;
01243                 return parent::getItem( $code, $key );
01244         }
01245 
01252         public function getSubitem( $code, $key, $subkey ) {
01253                 unset( $this->mruLangs[$code] );
01254                 $this->mruLangs[$code] = true;
01255                 return parent::getSubitem( $code, $key, $subkey );
01256         }
01257 
01261         public function recache( $code ) {
01262                 parent::recache( $code );
01263                 unset( $this->mruLangs[$code] );
01264                 $this->mruLangs[$code] = true;
01265                 $this->trimCache();
01266         }
01267 
01271         public function unload( $code ) {
01272                 unset( $this->mruLangs[$code] );
01273                 parent::unload( $code );
01274         }
01275 
01279         protected function trimCache() {
01280                 while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) {
01281                         reset( $this->mruLangs );
01282                         $code = key( $this->mruLangs );
01283                         wfDebug( __METHOD__ . ": unloading $code\n" );
01284                         $this->unload( $code );
01285                 }
01286         }
01287 
01288 }