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