Added post-commit callback support to DB classes.
[lhc/web/wiklou.git] / includes / LocalisationCache.php
index ad707dc..d8e5d3a 100644 (file)
@@ -1,4 +1,24 @@
 <?php
+/**
+ * Cache of the contents of localisation files.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
 
 define( 'MW_LC_VERSION', 2 );
 
@@ -90,7 +110,7 @@ class LocalisationCache {
                'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
                'defaultDateFormat', 'extraUserToggles', 'specialPageAliases',
                'imageFiles', 'preloadedMessages', 'namespaceGenderAliases',
-               'digitGroupingPattern'
+               'digitGroupingPattern', 'pluralRules', 'compiledPluralRules',
        );
 
        /**
@@ -98,7 +118,7 @@ class LocalisationCache {
         * by a fallback sequence.
         */
        static public $mergeableMapKeys = array( 'messages', 'namespaceNames',
-               'dateFormats', 'imageFiles', 'preloadedMessages',
+               'dateFormats', 'imageFiles', 'preloadedMessages'
        );
 
        /**
@@ -134,6 +154,12 @@ class LocalisationCache {
         */
        static public $preloadedKeys = array( 'dateFormats', 'namespaceNames' );
 
+       /**
+        * Associative array of cached plural rules. The key is the language code,
+        * the value is an array of plural rules for that language.
+        */
+       var $pluralRules = null;
+
        var $mergeableKeys = null;
 
        /**
@@ -163,12 +189,7 @@ class LocalisationCache {
                                        $storeClass = 'LCStore_Accel';
                                        break;
                                case 'detect':
-                                       try {
-                                               $c = wfGetCache( CACHE_ACCEL );
-                                               $storeClass = 'LCStore_Accel';
-                                       } catch( Exception $c ) {
-                                               $storeClass = $wgCacheDirectory ? 'LCStore_CDB' : 'LCStore_DB';
-                                       }
+                                       $storeClass = $wgCacheDirectory ? 'LCStore_CDB' : 'LCStore_DB';
                                        break;
                                default:
                                        throw new MWException(
@@ -219,9 +240,9 @@ class LocalisationCache {
         */
        public function getItem( $code, $key ) {
                if ( !isset( $this->loadedItems[$code][$key] ) ) {
-                       wfProfileIn( __METHOD__.'-load' );
+                       wfProfileIn( __METHOD__ . '-load' );
                        $this->loadItem( $code, $key );
-                       wfProfileOut( __METHOD__.'-load' );
+                       wfProfileOut( __METHOD__ . '-load' );
                }
 
                if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
@@ -241,9 +262,9 @@ class LocalisationCache {
        public function getSubitem( $code, $key, $subkey ) {
                if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
                         !isset( $this->loadedItems[$code][$key] ) ) {
-                       wfProfileIn( __METHOD__.'-load' );
+                       wfProfileIn( __METHOD__ . '-load' );
                        $this->loadSubitem( $code, $key, $subkey );
-                       wfProfileOut( __METHOD__.'-load' );
+                       wfProfileOut( __METHOD__ . '-load' );
                }
 
                if ( isset( $this->data[$code][$key][$subkey] ) ) {
@@ -348,10 +369,11 @@ 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] ) ) {
-                       wfDebug( __METHOD__."($code): forced reload\n" );
+                       wfDebug( __METHOD__ . "($code): forced reload\n" );
                        return true;
                }
 
@@ -360,7 +382,7 @@ class LocalisationCache {
                $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" );
+                       wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" );
                        return true;
                }
 
@@ -370,7 +392,7 @@ class LocalisationCache {
                        // 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 " .
+                               wfDebug( __METHOD__ . "($code): cache for $code expired due to " .
                                        get_class( $dep ) . "\n" );
                                return true;
                        }
@@ -465,9 +487,95 @@ class LocalisationCache {
                } elseif ( $_fileType == 'aliases' ) {
                        $data = compact( 'aliases' );
                } else {
-                       throw new MWException( __METHOD__.": Invalid file type: $_fileType" );
+                       throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
+               }
+               return $data;
+       }
+
+       /**
+        * Get the compiled plural rules for a given language from the XML files.
+        * @since 1.20
+        */
+       public function getCompiledPluralRules( $code ) {
+               $rules = $this->getPluralRules( $code );
+               if ( $rules === null ) {
+                       return null;
+               }
+               try {
+                       $compiledRules = CLDRPluralRuleEvaluator::compile( $rules );
+               } catch( CLDRPluralRuleError $e ) {
+                       wfDebugLog( 'l10n', $e->getMessage() . "\n" );
+                       return array();
+               }
+               return $compiledRules;
+       }
+
+       /**
+        * Get the plural rules for a given language from the XML files.
+        * Cached.
+        * @since 1.20
+        */
+       public function getPluralRules( $code ) {
+               if ( $this->pluralRules === null ) {
+                       $cldrPlural = __DIR__ . "/../languages/data/plurals.xml";
+                       $mwPlural = __DIR__ . "/../languages/data/plurals-mediawiki.xml";
+                       // Load CLDR plural rules
+                       $this->loadPluralFile( $cldrPlural );
+                       if ( file_exists( $mwPlural ) ) {
+                               // Override or extend
+                               $this->loadPluralFile( $mwPlural );
+                       }
+               }
+               if ( !isset( $this->pluralRules[$code] ) ) {
+                       return null;
+               } else {
+                       return $this->pluralRules[$code];
                }
+       }
 
+
+       /**
+        * Load a plural XML file with the given filename, compile the relevant
+        * rules, and save the compiled rules in a process-local cache.
+        */
+       protected function loadPluralFile( $fileName ) {
+               $doc = new DOMDocument;
+               $doc->load( $fileName );
+               $rulesets = $doc->getElementsByTagName( "pluralRules" );
+               foreach ( $rulesets as $ruleset ) {
+                       $codes = $ruleset->getAttribute( 'locales' );
+                       $rules = array();
+                       $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
+                       foreach ( $ruleElements as $elt ) {
+                               $rules[] = $elt->nodeValue;
+                       }
+                       foreach ( explode( ' ', $codes ) as $code ) {
+                               $this->pluralRules[$code] = $rules;
+                       }
+               }
+       }
+
+       /**
+        * Read the data from the source files for a given language, and register
+        * the relevant dependencies in the $deps array. If the localisation
+        * exists, the data array is returned, otherwise false is returned.
+        */
+       protected function readSourceFilesAndRegisterDeps( $code, &$deps ) {
+               $fileName = Language::getMessagesFileName( $code );
+               if ( !file_exists( $fileName ) ) {
+                       return false;
+               }
+
+               $deps[] = new FileDependency( $fileName );
+               $data = $this->readPHPFile( $fileName, 'core' );
+
+               # Load CLDR plural rules for JavaScript
+               $data['pluralRules'] = $this->getPluralRules( $code );
+               # And for PHP
+               $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
+
+               $deps['plurals'] = new FileDependency( __DIR__ . "/../languages/data/plurals.xml" );
+               $deps['plurals-mw'] = new FileDependency( __DIR__ . "/../languages/data/plurals-mediawiki.xml" );
                return $data;
        }
 
@@ -553,7 +661,7 @@ class LocalisationCache {
         * @param $code
         */
        public function recache( $code ) {
-               global $wgExtensionMessagesFiles, $wgExtensionAliasesFiles;
+               global $wgExtensionMessagesFiles;
                wfProfileIn( __METHOD__ );
 
                if ( !$code ) {
@@ -569,14 +677,12 @@ class LocalisationCache {
                $deps = array();
 
                # Load the primary localisation from the source file
-               $fileName = Language::getMessagesFileName( $code );
-               if ( !file_exists( $fileName ) ) {
-                       wfDebug( __METHOD__.": no localisation file for $code, using fallback to en\n" );
+               $data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
+               if ( $data === false ) {
+                       wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
                        $coreData['fallback'] = 'en';
                } else {
-                       $deps[] = new FileDependency( $fileName );
-                       $data = $this->readPHPFile( $fileName, 'core' );
-                       wfDebug( __METHOD__.": got localisation for $code from source\n" );
+                       wfDebug( __METHOD__ . ": got localisation for $code from source\n" );
 
                        # Merge primary localisation
                        foreach ( $data as $key => $value ) {
@@ -589,7 +695,6 @@ class LocalisationCache {
                if ( is_null( $coreData['fallback'] ) ) {
                        $coreData['fallback'] = $code === 'en' ? false : 'en';
                }
-
                if ( $coreData['fallback'] === false ) {
                        $coreData['fallbackSequence'] = array();
                } else {
@@ -605,15 +710,11 @@ class LocalisationCache {
                        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 ) ) {
+                               $fbData = $this->readSourceFilesAndRegisterDeps( $fbCode, $deps );
+                               if ( $fbData === false ) {
                                        continue;
                                }
 
-                               $deps[] = new FileDependency( $fbFilename );
-                               $fbData = $this->readPHPFile( $fbFilename, 'core' );
-
                                foreach ( self::$allKeys as $key ) {
                                        if ( !isset( $fbData[$key] ) ) {
                                                continue;
@@ -638,7 +739,7 @@ class LocalisationCache {
                        $used = false;
 
                        foreach ( $data as $key => $item ) {
-                               if( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
+                               if ( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
                                        $used = true;
                                }
                        }
@@ -648,22 +749,6 @@ class LocalisationCache {
                        }
                }
 
-               # 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 );
-                       }
-               }
-
                # Merge core data into extension data
                foreach ( $coreData as $key => $item ) {
                        $this->mergeItem( $key, $allData[$key], $item );
@@ -671,7 +756,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
@@ -685,19 +769,26 @@ class LocalisationCache {
                        $page = str_replace( ' ', '_', $page );
                }
                # Decouple the reference to prevent accidental damage
-               unset($page);
+               unset( $page );
+
+               # If there were no plural rules, return an empty array
+               if ( $allData['pluralRules'] === null ) {
+                       $allData['pluralRules'] = array();
+               }
+               if ( $allData['compiledPluralRules'] === null ) {
+                       $allData['compiledPluralRules'] = array();
+               }
 
                # Set the list keys
                $allData['list'] = array();
                foreach ( self::$splitKeys as $key ) {
                        $allData['list'][$key] = array_keys( $allData[$key] );
                }
-
                # Run hooks
                wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) );
 
                if ( is_null( $allData['namespaceNames'] ) ) {
-                       throw new MWException( __METHOD__.': Localisation data failed sanity check! ' .
+                       throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
                                'Check that your languages/messages/MessagesEn.php file is intact.' );
                }
 
@@ -815,8 +906,8 @@ class LocalisationCache {
 interface LCStore {
        /**
         * Get a value.
-        * @param $code Language code
-        * @param $key Cache key
+        * @param $code string Language code
+        * @param $key string Cache key
         */
        function get( $code, $key );
 
@@ -842,7 +933,7 @@ interface LCStore {
 
 /**
  * LCStore implementation which uses PHP accelerator to store data.
- * This will work if one of XCache, eAccelerator, or APC cacher is configured.
+ * This will work if one of XCache, WinCache or APC cacher is configured.
  * (See ObjectCache.php)
  */
 class LCStore_Accel implements LCStore {
@@ -925,17 +1016,17 @@ class LCStore_DB implements LCStore {
                }
 
                if ( !$code ) {
-                       throw new MWException( __METHOD__.": Invalid language \"$code\"" );
+                       throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
                }
 
                $this->dbw = wfGetDB( DB_MASTER );
                try {
-                       $this->dbw->begin();
+                       $this->dbw->begin( __METHOD__ );
                        $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->rollback( __METHOD__ );
                                $this->dbw->ignoreErrors( false );
                                return;
                        } else {
@@ -956,7 +1047,7 @@ class LCStore_DB implements LCStore {
                        $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
                }
 
-               $this->dbw->commit();
+               $this->dbw->commit( __METHOD__ );
                $this->currentLang = null;
                $this->dbw = null;
                $this->batch = array();
@@ -969,7 +1060,7 @@ class LCStore_DB implements LCStore {
                }
 
                if ( is_null( $this->currentLang ) ) {
-                       throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
+                       throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
                }
 
                $this->batch[] = array(
@@ -1041,7 +1132,7 @@ class LCStore_CDB implements LCStore {
                }
 
                // Close reader to stop permission errors on write
-               if( !empty($this->readers[$code]) ) {
+               if ( !empty( $this->readers[$code] ) ) {
                        $this->readers[$code]->close();
                }
 
@@ -1059,14 +1150,14 @@ class LCStore_CDB implements LCStore {
 
        public function set( $key, $value ) {
                if ( is_null( $this->writer ) ) {
-                       throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
+                       throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
                }
                $this->writer->set( $key, serialize( $value ) );
        }
 
        protected function getFileName( $code ) {
                if ( !$code || strpos( $code, '/' ) !== false ) {
-                       throw new MWException( __METHOD__.": Invalid language \"$code\"" );
+                       throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
                }
                return "{$this->directory}/l10n_cache-$code.cdb";
        }
@@ -1182,8 +1273,9 @@ class LocalisationCache_BulkLoad extends LocalisationCache {
                while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) {
                        reset( $this->mruLangs );
                        $code = key( $this->mruLangs );
-                       wfDebug( __METHOD__.": unloading $code\n" );
+                       wfDebug( __METHOD__ . ": unloading $code\n" );
                        $this->unload( $code );
                }
        }
-}
\ No newline at end of file
+
+}