Follow-up to r111091. Dont paste md5 in the code.
[lhc/web/wiklou.git] / includes / LocalisationCache.php
index 7933007..f62adc7 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 
-define( 'MW_LC_VERSION', 1 );
+define( 'MW_LC_VERSION', 2 );
 
 /**
  * Class for caching the contents of localisation files, Messages*.php
@@ -8,8 +8,8 @@ define( 'MW_LC_VERSION', 1 );
  *
  * An instance of this class is available using Language::getLocalisationCache().
  *
- * The values retrieved from here are merged, containing items from extension 
- * files, core messages files and the language fallback sequence (e.g. zh-cn -> 
+ * The values retrieved from here are merged, containing items from extension
+ * files, core messages files and the language fallback sequence (e.g. zh-cn ->
  * zh-hans -> en ). Some common errors are corrected, for example namespace
  * names with spaces instead of underscores, but heavyweight processing, such
  * as grammatical transformation, is done by the caller.
@@ -40,6 +40,8 @@ class LocalisationCache {
 
        /**
         * The persistent store object. An instance of LCStore.
+        *
+        * @var LCStore
         */
        var $store;
 
@@ -81,22 +83,22 @@ class LocalisationCache {
         * All item keys
         */
        static public $allKeys = array(
-               'fallback', 'namespaceNames', 'mathNames', 'bookstoreList',
+               'fallback', 'namespaceNames', 'bookstoreList',
                'magicWords', 'messages', 'rtl', 'capitalizeAllNouns', 'digitTransformTable',
                'separatorTransformTable', 'fallback8bitEncoding', 'linkPrefixExtension',
-               'defaultUserOptionOverrides', 'linkTrail', 'namespaceAliases',
+               'linkTrail', 'namespaceAliases',
                'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
                'defaultDateFormat', 'extraUserToggles', 'specialPageAliases',
-               'imageFiles', 'preloadedMessages',
+               'imageFiles', 'preloadedMessages', 'namespaceGenderAliases',
+               'digitGroupingPattern'
        );
 
        /**
         * Keys for items which consist of associative arrays, which may be merged
         * by a fallback sequence.
         */
-       static public $mergeableMapKeys = array( 'messages', 'namespaceNames', 'mathNames',
-               'dateFormats', 'defaultUserOptionOverrides', 'magicWords', 'imageFiles',
-               'preloadedMessages',
+       static public $mergeableMapKeys = array( 'messages', 'namespaceNames',
+               'dateFormats', 'imageFiles', 'preloadedMessages',
        );
 
        /**
@@ -117,6 +119,11 @@ class LocalisationCache {
         */
        static public $optionalMergeKeys = array( 'bookstoreList' );
 
+       /**
+        * Keys for items that are formatted like $magicWords
+        */
+       static public $magicWordKeys = array( 'magicWords' );
+
        /**
         * Keys for items where the subitems are stored in the backend separately.
         */
@@ -125,22 +132,22 @@ class LocalisationCache {
        /**
         * Keys which are loaded automatically by initLanguage()
         */
-       static public $preloadedKeys = array( 'dateFormats', 'namespaceNames',
-               'defaultUserOptionOverrides' );
+       static public $preloadedKeys = array( 'dateFormats', 'namespaceNames' );
+
+       var $mergeableKeys = null;
 
        /**
         * Constructor.
-        * For constructor parameters, see the documentation in DefaultSettings.php 
+        * For constructor parameters, see the documentation in DefaultSettings.php
         * for $wgLocalisationCacheConf.
+        *
+        * @param $conf Array
         */
        function __construct( $conf ) {
                global $wgCacheDirectory;
 
                $this->conf = $conf;
-               $this->data = array();
-               $this->loadedItems = array();
-               $this->loadedSubitems = array();
-               $this->initialisedLangs = array();
+               $storeConf = array();
                if ( !empty( $conf['storeClass'] ) ) {
                        $storeClass = $conf['storeClass'];
                } else {
@@ -152,17 +159,24 @@ class LocalisationCache {
                                case 'db':
                                        $storeClass = 'LCStore_DB';
                                        break;
+                               case 'accel':
+                                       $storeClass = 'LCStore_Accel';
+                                       break;
                                case 'detect':
                                        $storeClass = $wgCacheDirectory ? 'LCStore_CDB' : 'LCStore_DB';
                                        break;
                                default:
-                                       throw new MWException( 
-                                               'Please set $wgLocalisationConf[\'store\'] to something sensible.' );
+                                       throw new MWException(
+                                               'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' );
                        }
                }
 
                wfDebug( get_class( $this ) . ": using store $storeClass\n" );
-               $this->store = new $storeClass;
+               if ( !empty( $conf['storeDirectory'] ) ) {
+                       $storeConf['directory'] = $conf['storeDirectory'];
+               }
+
+               $this->store = new $storeClass( $storeConf );
                foreach ( array( 'manualRecache', 'forceRecache' ) as $var ) {
                        if ( isset( $conf[$var] ) ) {
                                $this->$var = $conf[$var];
@@ -173,14 +187,17 @@ class LocalisationCache {
        /**
         * Returns true if the given key is mergeable, that is, if it is an associative
         * array which can be merged through a fallback sequence.
+        * @param $key
+        * @return bool
         */
        public function isMergeableKey( $key ) {
-               if ( !isset( $this->mergeableKeys ) ) {
+               if ( $this->mergeableKeys === null ) {
                        $this->mergeableKeys = array_flip( array_merge(
                                self::$mergeableMapKeys,
                                self::$mergeableListKeys,
                                self::$mergeableAliasListKeys,
-                               self::$optionalMergeKeys
+                               self::$optionalMergeKeys,
+                               self::$magicWordKeys
                        ) );
                }
                return isset( $this->mergeableKeys[$key] );
@@ -191,6 +208,9 @@ class LocalisationCache {
         *
         * Warning: this may be slow for split items (messages), since it will
         * need to fetch all of the subitems from the cache individually.
+        * @param $code
+        * @param $key
+        * @return mixed
         */
        public function getItem( $code, $key ) {
                if ( !isset( $this->loadedItems[$code][$key] ) ) {
@@ -198,47 +218,81 @@ class LocalisationCache {
                        $this->loadItem( $code, $key );
                        wfProfileOut( __METHOD__.'-load' );
                }
+
                if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
                        return $this->shallowFallbacks[$code];
                }
+
                return $this->data[$code][$key];
        }
 
        /**
         * Get a subitem, for instance a single message for a given language.
+        * @param $code
+        * @param $key
+        * @param $subkey
+        * @return null
         */
        public function getSubitem( $code, $key, $subkey ) {
-               if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) ) {
-                       if ( isset( $this->loadedItems[$code][$key] ) ) {
-                               if ( isset( $this->data[$code][$key][$subkey] ) ) {
-                                       return $this->data[$code][$key][$subkey];
-                               } else {
-                                       return null;
-                               }
+               if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
+                        !isset( $this->loadedItems[$code][$key] ) ) {
+                       wfProfileIn( __METHOD__.'-load' );
+                       $this->loadSubitem( $code, $key, $subkey );
+                       wfProfileOut( __METHOD__.'-load' );
+               }
+
+               if ( isset( $this->data[$code][$key][$subkey] ) ) {
+                       return $this->data[$code][$key][$subkey];
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Get the list of subitem keys for a given item.
+        *
+        * This is faster than array_keys($lc->getItem(...)) for the items listed in
+        * self::$splitKeys.
+        *
+        * Will return null if the item is not found, or false if the item is not an
+        * array.
+        * @param $code
+        * @param $key
+        * @return bool|null|string
+        */
+       public function getSubitemList( $code, $key ) {
+               if ( in_array( $key, self::$splitKeys ) ) {
+                       return $this->getSubitem( $code, 'list', $key );
+               } else {
+                       $item = $this->getItem( $code, $key );
+                       if ( is_array( $item ) ) {
+                               return array_keys( $item );
                        } else {
-                               wfProfileIn( __METHOD__.'-load' );
-                               $this->loadSubitem( $code, $key, $subkey );
-                               wfProfileOut( __METHOD__.'-load' );
+                               return false;
                        }
                }
-               return $this->data[$code][$key][$subkey];
        }
 
        /**
         * Load an item into the cache.
+        * @param $code
+        * @param $key
         */
        protected function loadItem( $code, $key ) {
                if ( !isset( $this->initialisedLangs[$code] ) ) {
                        $this->initLanguage( $code );
                }
+
                // Check to see if initLanguage() loaded it for us
                if ( isset( $this->loadedItems[$code][$key] ) ) {
                        return;
                }
+
                if ( isset( $this->shallowFallbacks[$code] ) ) {
                        $this->loadItem( $this->shallowFallbacks[$code], $key );
                        return;
                }
+
                if ( in_array( $key, self::$splitKeys ) ) {
                        $subkeyList = $this->getSubitem( $code, 'list', $key );
                        foreach ( $subkeyList as $subkey ) {
@@ -250,28 +304,38 @@ class LocalisationCache {
                } else {
                        $this->data[$code][$key] = $this->store->get( $code, $key );
                }
+
                $this->loadedItems[$code][$key] = true;
        }
 
        /**
         * Load a subitem into the cache
+        * @param $code
+        * @param $key
+        * @param $subkey
+        * @return
         */
        protected function loadSubitem( $code, $key, $subkey ) {
                if ( !in_array( $key, self::$splitKeys ) ) {
                        $this->loadItem( $code, $key );
                        return;
                }
+
                if ( !isset( $this->initialisedLangs[$code] ) ) {
                        $this->initLanguage( $code );
                }
+
                // Check to see if initLanguage() loaded it for us
-               if ( isset( $this->loadedSubitems[$code][$key][$subkey] ) ) {
+               if ( isset( $this->loadedItems[$code][$key] ) ||
+                        isset( $this->loadedSubitems[$code][$key][$subkey] ) ) {
                        return;
                }
+
                if ( isset( $this->shallowFallbacks[$code] ) ) {
                        $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
                        return;
                }
+
                $value = $this->store->get( $code, "$key:$subkey" );
                $this->data[$code][$key][$subkey] = $value;
                $this->loadedSubitems[$code][$key][$subkey] = true;
@@ -279,6 +343,7 @@ class LocalisationCache {
 
        /**
         * Returns true if the cache identified by $code is missing or expired.
+        * @return bool
         */
        public function isExpired( $code ) {
                if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
@@ -287,29 +352,46 @@ class LocalisationCache {
                }
 
                $deps = $this->store->get( $code, 'deps' );
-               if ( $deps === null ) {
+               $keys = $this->store->get( $code, 'list', 'messages' );
+               $preload = $this->store->get( $code, 'preload' );
+               // Different keys may expire separately, at least in LCStore_Accel
+               if ( $deps === null || $keys === null || $preload === null ) {
                        wfDebug( __METHOD__."($code): cache missing, need to make one\n" );
                        return true;
                }
+
                foreach ( $deps as $dep ) {
-                       if ( $dep->isExpired() ) {
-                               wfDebug( __METHOD__."($code): cache for $code expired due to " . 
+                       // Because we're unserializing stuff from cache, we
+                       // could receive objects of classes that don't exist
+                       // anymore (e.g. uninstalled extensions)
+                       // When this happens, always expire the cache
+                       if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
+                               wfDebug( __METHOD__."($code): cache for $code expired due to " .
                                        get_class( $dep ) . "\n" );
                                return true;
                        }
                }
+
                return false;
        }
 
        /**
         * Initialise a language in this object. Rebuild the cache if necessary.
+        * @param $code
         */
        protected function initLanguage( $code ) {
                if ( isset( $this->initialisedLangs[$code] ) ) {
                        return;
                }
+
                $this->initialisedLangs[$code] = true;
 
+               # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
+               if ( !Language::isValidBuiltInCode( $code ) ) {
+                       $this->initShallowFallback( $code, 'en' );
+                       return;
+               }
+
                # Recache the data if necessary
                if ( !$this->manualRecache && $this->isExpired( $code ) ) {
                        if ( file_exists( Language::getMessagesFileName( $code ) ) ) {
@@ -328,7 +410,7 @@ class LocalisationCache {
                        if ( $this->manualRecache ) {
                                // No Messages*.php file. Do shallow fallback to en.
                                if ( $code === 'en' ) {
-                                       throw new MWException( 'No localisation cache found for English. ' . 
+                                       throw new MWException( 'No localisation cache found for English. ' .
                                                'Please run maintenance/rebuildLocalisationCache.php.' );
                                }
                                $this->initShallowFallback( $code, 'en' );
@@ -350,8 +432,10 @@ class LocalisationCache {
        }
 
        /**
-        * Create a fallback from one language to another, without creating a 
+        * Create a fallback from one language to another, without creating a
         * complete persistent cache.
+        * @param $primaryCode
+        * @param $fallbackCode
         */
        public function initShallowFallback( $primaryCode, $fallbackCode ) {
                $this->data[$primaryCode] =& $this->data[$fallbackCode];
@@ -362,12 +446,15 @@ class LocalisationCache {
 
        /**
         * Read a PHP file containing localisation data.
+        * @param $_fileName
+        * @param $_fileType
+        * @return array
         */
        protected function readPHPFile( $_fileName, $_fileType ) {
                // Disable APC caching
-               $_apcEnabled = ini_set( 'apc.enabled', '0' );
+               $_apcEnabled = ini_set( 'apc.cache_by_default', '0' );
                include( $_fileName );
-               ini_set( 'apc.enabled', $_apcEnabled );
+               ini_set( 'apc.cache_by_default', $_apcEnabled );
 
                if ( $_fileType == 'core' || $_fileType == 'extension' ) {
                        $data = compact( self::$allKeys );
@@ -376,12 +463,16 @@ class LocalisationCache {
                } else {
                        throw new MWException( __METHOD__.": Invalid file type: $_fileType" );
                }
+
                return $data;
        }
 
        /**
-        * Merge two localisation values, a primary and a fallback, overwriting the 
+        * Merge two localisation values, a primary and a fallback, overwriting the
         * primary value in place.
+        * @param $key
+        * @param $value
+        * @param $fallbackValue
         */
        protected function mergeItem( $key, &$value, $fallbackValue ) {
                if ( !is_null( $value ) ) {
@@ -396,9 +487,12 @@ class LocalisationCache {
                                        if ( !empty( $value['inherit'] ) )  {
                                                $value = array_merge( $fallbackValue, $value );
                                        }
+
                                        if ( isset( $value['inherit'] ) ) {
                                                unset( $value['inherit'] );
                                        }
+                               } elseif ( in_array( $key, self::$magicWordKeys ) ) {
+                                       $this->mergeMagicWords( $value, $fallbackValue );
                                }
                        }
                } else {
@@ -406,13 +500,36 @@ class LocalisationCache {
                }
        }
 
+       /**
+        * @param $value
+        * @param $fallbackValue
+        */
+       protected function mergeMagicWords( &$value, $fallbackValue ) {
+               foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
+                       if ( !isset( $value[$magicName] ) ) {
+                               $value[$magicName] = $fallbackInfo;
+                       } else {
+                               $oldSynonyms = array_slice( $fallbackInfo, 1 );
+                               $newSynonyms = array_slice( $value[$magicName], 1 );
+                               $synonyms = array_values( array_unique( array_merge(
+                                       $newSynonyms, $oldSynonyms ) ) );
+                               $value[$magicName] = array_merge( array( $fallbackInfo[0] ), $synonyms );
+                       }
+               }
+       }
+
        /**
         * Given an array mapping language code to localisation value, such as is
         * found in extension *.i18n.php files, iterate through a fallback sequence
         * to merge the given data with an existing primary value.
         *
-        * Returns true if any data from the extension array was used, false 
+        * Returns true if any data from the extension array was used, false
         * otherwise.
+        * @param $codeSequence
+        * @param $key
+        * @param $value
+        * @param $fallbackValue
+        * @return bool
         */
        protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
                $used = false;
@@ -422,16 +539,17 @@ class LocalisationCache {
                                $used = true;
                        }
                }
+
                return $used;
        }
 
        /**
         * Load localisation data for a given language for both core and extensions
         * and save it to the persistent cache store and the process cache
+        * @param $code
         */
        public function recache( $code ) {
-               static $recursionGuard = array();
-               global $wgExtensionMessagesFiles, $wgExtensionAliasesFiles;
+               global $wgExtensionMessagesFiles;
                wfProfileIn( __METHOD__ );
 
                if ( !$code ) {
@@ -441,7 +559,7 @@ class LocalisationCache {
 
                # Initial values
                $initialData = array_combine(
-                       self::$allKeys, 
+                       self::$allKeys,
                        array_fill( 0, count( self::$allKeys ), null ) );
                $coreData = $initialData;
                $deps = array();
@@ -460,6 +578,7 @@ class LocalisationCache {
                        foreach ( $data as $key => $value ) {
                                $this->mergeItem( $key, $coreData[$key], $value );
                        }
+
                }
 
                # Fill in the fallback if it's not there already
@@ -467,55 +586,59 @@ class LocalisationCache {
                        $coreData['fallback'] = $code === 'en' ? false : 'en';
                }
 
-               if ( $coreData['fallback'] !== false ) {
-                       # Guard against circular references
-                       if ( isset( $recursionGuard[$code] ) ) {
-                               throw new MWException( "Error: Circular fallback reference in language code $code" );
+               if ( $coreData['fallback'] === false ) {
+                       $coreData['fallbackSequence'] = array();
+               } else {
+                       $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
+                       $len = count( $coreData['fallbackSequence'] );
+
+                       # Ensure that the sequence ends at en
+                       if ( $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
+                               $coreData['fallbackSequence'][] = 'en';
                        }
-                       $recursionGuard[$code] = true;
 
                        # Load the fallback localisation item by item and merge it
-                       $deps = array_merge( $deps, $this->getItem( $coreData['fallback'], 'deps' ) );
-                       foreach ( self::$allKeys as $key ) {
-                               if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) {
-                                       $fallbackValue = $this->getItem( $coreData['fallback'], $key );
-                                       $this->mergeItem( $key, $coreData[$key], $fallbackValue );
+                       foreach ( $coreData['fallbackSequence'] as $fbCode ) {
+                               # Load the secondary localisation from the source file to
+                               # avoid infinite cycles on cyclic fallbacks
+                               $fbFilename = Language::getMessagesFileName( $fbCode );
+
+                               if ( !file_exists( $fbFilename ) ) {
+                                       continue;
+                               }
+
+                               $deps[] = new FileDependency( $fbFilename );
+                               $fbData = $this->readPHPFile( $fbFilename, 'core' );
+
+                               foreach ( self::$allKeys as $key ) {
+                                       if ( !isset( $fbData[$key] ) ) {
+                                               continue;
+                                       }
+
+                                       if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) {
+                                               $this->mergeItem( $key, $coreData[$key], $fbData[$key] );
+                                       }
                                }
                        }
-                       $fallbackSequence = $this->getItem( $coreData['fallback'], 'fallbackSequence' );
-                       array_unshift( $fallbackSequence, $coreData['fallback'] );
-                       $coreData['fallbackSequence'] = $fallbackSequence;
-                       unset( $recursionGuard[$code] );
-               } else {
-                       $coreData['fallbackSequence'] = array();
                }
+
                $codeSequence = array_merge( array( $code ), $coreData['fallbackSequence'] );
 
                # Load the extension localisations
                # This is done after the core because we know the fallback sequence now.
-               # But it has a higher precedence for merging so that we can support things 
+               # But it has a higher precedence for merging so that we can support things
                # like site-specific message overrides.
                $allData = $initialData;
                foreach ( $wgExtensionMessagesFiles as $fileName ) {
                        $data = $this->readPHPFile( $fileName, 'extension' );
                        $used = false;
+
                        foreach ( $data as $key => $item ) {
-                               $used = $used || 
-                                       $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item );
-                       }
-                       if ( $used ) {
-                               $deps[] = new FileDependency( $fileName );
+                               if( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
+                                       $used = true;
+                               }
                        }
-               }
 
-               # Load deprecated $wgExtensionAliasesFiles
-               foreach ( $wgExtensionAliasesFiles as $fileName ) {
-                       $data = $this->readPHPFile( $fileName, 'aliases' );
-                       if ( !isset( $data['aliases'] ) ) {
-                               continue;
-                       }
-                       $used = $this->mergeExtensionItem( $codeSequence, 'specialPageAliases',
-                               $allData['specialPageAliases'], $data['aliases'] );
                        if ( $used ) {
                                $deps[] = new FileDependency( $fileName );
                        }
@@ -528,7 +651,6 @@ class LocalisationCache {
 
                # Add cache dependencies for any referenced globals
                $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
-               $deps['wgExtensionAliasesFiles'] = new GlobalDependency( 'wgExtensionAliasesFiles' );
                $deps['version'] = new ConstantDependency( 'MW_LC_VERSION' );
 
                # Add dependencies to the cache entry
@@ -543,14 +665,6 @@ class LocalisationCache {
                }
                # Decouple the reference to prevent accidental damage
                unset($page);
-       
-               # Fix broken defaultUserOptionOverrides
-               if ( !is_array( $allData['defaultUserOptionOverrides'] ) ) {
-                       $allData['defaultUserOptionOverrides'] = array();
-               }
-
-               # Set the preload key
-               $allData['preload'] = $this->buildPreload( $allData );
 
                # Set the list keys
                $allData['list'] = array();
@@ -561,11 +675,14 @@ class LocalisationCache {
                # Run hooks
                wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) );
 
-               if ( is_null( $allData['defaultUserOptionOverrides'] ) ) {
-                       throw new MWException( __METHOD__.': Localisation data failed sanity check! ' . 
+               if ( is_null( $allData['namespaceNames'] ) ) {
+                       throw new MWException( __METHOD__.': Localisation data failed sanity check! ' .
                                'Check that your languages/messages/MessagesEn.php file is intact.' );
                }
 
+               # Set the preload key
+               $allData['preload'] = $this->buildPreload( $allData );
+
                # Save to the process cache and register the items loaded
                $this->data[$code] = $allData;
                foreach ( $allData as $key => $item ) {
@@ -585,6 +702,13 @@ class LocalisationCache {
                }
                $this->store->finishWrite();
 
+               # Clear out the MessageBlobStore
+               # HACK: If using a null (i.e. disabled) storage backend, we
+               # can't write to the MessageBlobStore either
+               if ( !$this->store instanceof LCStore_Null ) {
+                       MessageBlobStore::clear();
+               }
+
                wfProfileOut( __METHOD__ );
        }
 
@@ -593,12 +717,15 @@ class LocalisationCache {
         *
         * The preload item will be loaded automatically, improving performance
         * for the commonly-requested items it contains.
+        * @param $data
+        * @return array
         */
        protected function buildPreload( $data ) {
                $preload = array( 'messages' => array() );
                foreach ( self::$preloadedKeys as $key ) {
                        $preload[$key] = $data[$key];
                }
+
                foreach ( $data['preloadedMessages'] as $subkey ) {
                        if ( isset( $data['messages'][$subkey] ) ) {
                                $subitem = $data['messages'][$subkey];
@@ -607,24 +734,44 @@ class LocalisationCache {
                        }
                        $preload['messages'][$subkey] = $subitem;
                }
+
                return $preload;
        }
 
        /**
-        * Unload the data for a given language from the object cache. 
+        * Unload the data for a given language from the object cache.
         * Reduces memory usage.
+        * @param $code
         */
        public function unload( $code ) {
                unset( $this->data[$code] );
                unset( $this->loadedItems[$code] );
                unset( $this->loadedSubitems[$code] );
                unset( $this->initialisedLangs[$code] );
+
                foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
                        if ( $fbCode === $code ) {
                                $this->unload( $shallowCode );
                        }
                }
        }
+
+       /**
+        * Unload all data
+        */
+       public function unloadAll() {
+               foreach ( $this->initialisedLangs as $lang => $unused ) {
+                       $this->unload( $lang );
+               }
+       }
+
+       /**
+        * Disable the storage backend
+        */
+       public function disableBackend() {
+               $this->store = new LCStore_Null;
+               $this->manualRecache = false;
+       }
 }
 
 /**
@@ -633,52 +780,108 @@ class LocalisationCache {
  * The persistence layer is two-level hierarchical cache. The first level
  * is the language, the second level is the item or subitem.
  *
- * Since the data for a whole language is rebuilt in one operation, it needs 
- * to have a fast and atomic method for deleting or replacing all of the 
+ * Since the data for a whole language is rebuilt in one operation, it needs
+ * to have a fast and atomic method for deleting or replacing all of the
  * current data for a given language. The interface reflects this bulk update
- * operation. Callers writing to the cache must first call startWrite(), then 
- * will call set() a couple of thousand times, then will call finishWrite() 
- * to commit the operation. When finishWrite() is called, the cache is 
+ * operation. Callers writing to the cache must first call startWrite(), then
+ * will call set() a couple of thousand times, then will call finishWrite()
+ * to commit the operation. When finishWrite() is called, the cache is
  * expected to delete all data previously stored for that language.
  *
- * The values stored are PHP variables suitable for serialize(). Implementations 
+ * The values stored are PHP variables suitable for serialize(). Implementations
  * of LCStore are responsible for serializing and unserializing.
  */
 interface LCStore {
        /**
         * Get a value.
-        * @param $code Language code
-        * @param $key Cache key
+        * @param $code string Language code
+        * @param $key string Cache key
         */
-       public function get( $code, $key );
+       function get( $code, $key );
 
        /**
         * Start a write transaction.
         * @param $code Language code
         */
-       public function startWrite( $code );
+       function startWrite( $code );
 
        /**
         * Finish a write transaction.
         */
-       public function finishWrite();
+       function finishWrite();
 
        /**
         * Set a key to a given value. startWrite() must be called before this
         * is called, and finishWrite() must be called afterwards.
+        * @param $key
+        * @param $value
         */
-       public function set( $key, $value );
+       function set( $key, $value );
+}
+
+/**
+ * LCStore implementation which uses PHP accelerator to store data.
+ * This will work if one of XCache, WinCache or APC cacher is configured.
+ * (See ObjectCache.php)
+ */
+class LCStore_Accel implements LCStore {
+       var $currentLang;
+       var $keys;
 
+       public function __construct() {
+               $this->cache = wfGetCache( CACHE_ACCEL );
+       }
+
+       public function get( $code, $key ) {
+               $k = wfMemcKey( 'l10n', $code, 'k', $key );
+               $r = $this->cache->get( $k );
+               return $r === false ? null : $r;
+       }
+
+       public function startWrite( $code ) {
+               $k = wfMemcKey( 'l10n', $code, 'l' );
+               $keys = $this->cache->get( $k );
+               if ( $keys ) {
+                       foreach ( $keys as $k ) {
+                               $this->cache->delete( $k );
+                       }
+               }
+               $this->currentLang = $code;
+               $this->keys = array();
+       }
+
+       public function finishWrite() {
+               if ( $this->currentLang ) {
+                       $k = wfMemcKey( 'l10n', $this->currentLang, 'l' );
+                       $this->cache->set( $k, array_keys( $this->keys ) );
+               }
+               $this->currentLang = null;
+               $this->keys = array();
+       }
+
+       public function set( $key, $value ) {
+               if ( $this->currentLang ) {
+                       $k = wfMemcKey( 'l10n', $this->currentLang, 'k', $key );
+                       $this->keys[$k] = true;
+                       $this->cache->set( $k, $value );
+               }
+       }
 }
 
 /**
- * LCStore implementation which uses the standard DB functions to store data. 
+ * LCStore implementation which uses the standard DB functions to store data.
  * This will work on any MediaWiki installation.
  */
 class LCStore_DB implements LCStore {
        var $currentLang;
        var $writesDone = false;
-       var $dbw, $batch;
+
+       /**
+        * @var DatabaseBase
+        */
+       var $dbw;
+       var $batch;
+       var $readOnly = false;
 
        public function get( $code, $key ) {
                if ( $this->writesDone ) {
@@ -696,20 +899,42 @@ class LCStore_DB implements LCStore {
        }
 
        public function startWrite( $code ) {
+               if ( $this->readOnly ) {
+                       return;
+               }
+
                if ( !$code ) {
                        throw new MWException( __METHOD__.": Invalid language \"$code\"" );
                }
+
                $this->dbw = wfGetDB( DB_MASTER );
-               $this->dbw->begin();
-               $this->dbw->delete( 'l10n_cache', array( 'lc_lang' => $code ), __METHOD__ );
+               try {
+                       $this->dbw->begin();
+                       $this->dbw->delete( 'l10n_cache', array( 'lc_lang' => $code ), __METHOD__ );
+               } catch ( DBQueryError $e ) {
+                       if ( $this->dbw->wasReadOnlyError() ) {
+                               $this->readOnly = true;
+                               $this->dbw->rollback();
+                               $this->dbw->ignoreErrors( false );
+                               return;
+                       } else {
+                               throw $e;
+                       }
+               }
+
                $this->currentLang = $code;
                $this->batch = array();
        }
 
        public function finishWrite() {
+               if ( $this->readOnly ) {
+                       return;
+               }
+
                if ( $this->batch ) {
                        $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
                }
+
                $this->dbw->commit();
                $this->currentLang = null;
                $this->dbw = null;
@@ -718,13 +943,19 @@ class LCStore_DB implements LCStore {
        }
 
        public function set( $key, $value ) {
+               if ( $this->readOnly ) {
+                       return;
+               }
+
                if ( is_null( $this->currentLang ) ) {
                        throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
                }
+
                $this->batch[] = array(
                        'lc_lang' => $this->currentLang,
                        'lc_key' => $key,
                        'lc_value' => serialize( $value ) );
+
                if ( count( $this->batch ) >= 100 ) {
                        $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
                        $this->batch = array();
@@ -737,29 +968,42 @@ class LCStore_DB implements LCStore {
  * directory given by $wgCacheDirectory. If $wgCacheDirectory is not set, this
  * will throw an exception.
  *
- * Profiling indicates that on Linux, this implementation outperforms MySQL if 
- * the directory is on a local filesystem and there is ample kernel cache 
- * space. The performance advantage is greater when the DBA extension is 
+ * Profiling indicates that on Linux, this implementation outperforms MySQL if
+ * the directory is on a local filesystem and there is ample kernel cache
+ * space. The performance advantage is greater when the DBA extension is
  * available than it is with the PHP port.
  *
  * See Cdb.php and http://cr.yp.to/cdb.html
  */
 class LCStore_CDB implements LCStore {
-       var $readers, $writer, $currentLang;
-       
+       var $readers, $writer, $currentLang, $directory;
+
+       function __construct( $conf = array() ) {
+               global $wgCacheDirectory;
+
+               if ( isset( $conf['directory'] ) ) {
+                       $this->directory = $conf['directory'];
+               } else {
+                       $this->directory = $wgCacheDirectory;
+               }
+       }
+
        public function get( $code, $key ) {
                if ( !isset( $this->readers[$code] ) ) {
                        $fileName = $this->getFileName( $code );
+
                        if ( !file_exists( $fileName ) ) {
                                $this->readers[$code] = false;
                        } else {
                                $this->readers[$code] = CdbReader::open( $fileName );
                        }
                }
+
                if ( !$this->readers[$code] ) {
                        return null;
                } else {
                        $value = $this->readers[$code]->get( $key );
+
                        if ( $value === false ) {
                                return null;
                        }
@@ -768,6 +1012,18 @@ class LCStore_CDB implements LCStore {
        }
 
        public function startWrite( $code ) {
+               if ( !file_exists( $this->directory ) ) {
+                       if ( !wfMkdirParents( $this->directory, null, __METHOD__ ) ) {
+                               throw new MWException( "Unable to create the localisation store " .
+                                       "directory \"{$this->directory}\"" );
+                       }
+               }
+
+               // Close reader to stop permission errors on write
+               if( !empty($this->readers[$code]) ) {
+                       $this->readers[$code]->close();
+               }
+
                $this->writer = CdbWriter::open( $this->getFileName( $code ) );
                $this->currentLang = $code;
        }
@@ -776,11 +1032,6 @@ class LCStore_CDB implements LCStore {
                // Close the writer
                $this->writer->close();
                $this->writer = null;
-
-               // Reopen the reader
-               if ( !empty( $this->readers[$this->currentLang] ) ) {
-                       $this->readers[$this->currentLang]->close();
-               }
                unset( $this->readers[$this->currentLang] );
                $this->currentLang = null;
        }
@@ -793,16 +1044,28 @@ class LCStore_CDB implements LCStore {
        }
 
        protected function getFileName( $code ) {
-               global $wgCacheDirectory;
                if ( !$code || strpos( $code, '/' ) !== false ) {
                        throw new MWException( __METHOD__.": Invalid language \"$code\"" );
                }
-               return "$wgCacheDirectory/l10n_cache-$code.cdb";
+               return "{$this->directory}/l10n_cache-$code.cdb";
        }
 }
 
 /**
- * A localisation cache optimised for loading large amounts of data for many 
+ * Null store backend, used to avoid DB errors during install
+ */
+class LCStore_Null implements LCStore {
+       public function get( $code, $key ) {
+               return null;
+       }
+
+       public function startWrite( $code ) {}
+       public function finishWrite() {}
+       public function set( $key, $value ) {}
+}
+
+/**
+ * A localisation cache optimised for loading large amounts of data for many
  * languages. Used by rebuildLocalisationCache.php.
  */
 class LocalisationCache_BulkLoad extends LocalisationCache {
@@ -814,7 +1077,7 @@ class LocalisationCache_BulkLoad extends LocalisationCache {
 
        /**
         * Most recently used languages. Uses the linked-list aspect of PHP hashtables
-        * to keep the most recently used language codes at the end of the array, and 
+        * to keep the most recently used language codes at the end of the array, and
         * the language codes that are ready to be deleted at the beginning.
         */
        var $mruLangs = array();
@@ -824,16 +1087,24 @@ class LocalisationCache_BulkLoad extends LocalisationCache {
         */
        var $maxLoadedLangs = 10;
 
+       /**
+        * @param $fileName
+        * @param $fileType
+        * @return array|mixed
+        */
        protected function readPHPFile( $fileName, $fileType ) {
                $serialize = $fileType === 'core';
                if ( !isset( $this->fileCache[$fileName][$fileType] ) ) {
                        $data = parent::readPHPFile( $fileName, $fileType );
+
                        if ( $serialize ) {
                                $encData = serialize( $data );
                        } else {
                                $encData = $data;
                        }
+
                        $this->fileCache[$fileName][$fileType] = $encData;
+
                        return $data;
                } elseif ( $serialize ) {
                        return unserialize( $this->fileCache[$fileName][$fileType] );
@@ -842,18 +1113,32 @@ class LocalisationCache_BulkLoad extends LocalisationCache {
                }
        }
 
+       /**
+        * @param $code
+        * @param $key
+        * @return mixed
+        */
        public function getItem( $code, $key ) {
                unset( $this->mruLangs[$code] );
                $this->mruLangs[$code] = true;
                return parent::getItem( $code, $key );
        }
 
+       /**
+        * @param $code
+        * @param $key
+        * @param $subkey
+        * @return
+        */
        public function getSubitem( $code, $key, $subkey ) {
                unset( $this->mruLangs[$code] );
                $this->mruLangs[$code] = true;
                return parent::getSubitem( $code, $key, $subkey );
        }
 
+       /**
+        * @param $code
+        */
        public function recache( $code ) {
                parent::recache( $code );
                unset( $this->mruLangs[$code] );
@@ -861,6 +1146,9 @@ class LocalisationCache_BulkLoad extends LocalisationCache {
                $this->trimCache();
        }
 
+       /**
+        * @param $code
+        */
        public function unload( $code ) {
                unset( $this->mruLangs[$code] );
                parent::unload( $code );