MediaWiki  REL1_24
MessageCache.php
Go to the documentation of this file.
00001 <?php
00028 define( 'MSG_CACHE_VERSION', 1 );
00029 
00034 define( 'MSG_LOAD_TIMEOUT', 60 );
00035 
00040 define( 'MSG_LOCK_TIMEOUT', 30 );
00045 define( 'MSG_WAIT_TIMEOUT', 30 );
00046 
00052 class MessageCache {
00060     protected $mCache;
00061 
00066     protected $mDisable;
00067 
00072     protected $mExpiry;
00073 
00078     protected $mParserOptions, $mParser;
00079 
00084     protected $mLoadedLanguages = array();
00085 
00091     private static $instance;
00092 
00096     protected $mInParser = false;
00097 
00104     public static function singleton() {
00105         if ( is_null( self::$instance ) ) {
00106             global $wgUseDatabaseMessages, $wgMsgCacheExpiry;
00107             self::$instance = new self(
00108                 wfGetMessageCacheStorage(),
00109                 $wgUseDatabaseMessages,
00110                 $wgMsgCacheExpiry
00111             );
00112         }
00113 
00114         return self::$instance;
00115     }
00116 
00122     public static function destroyInstance() {
00123         self::$instance = null;
00124     }
00125 
00131     function __construct( $memCached, $useDB, $expiry ) {
00132         if ( !$memCached ) {
00133             $memCached = wfGetCache( CACHE_NONE );
00134         }
00135 
00136         $this->mMemc = $memCached;
00137         $this->mDisable = !$useDB;
00138         $this->mExpiry = $expiry;
00139     }
00140 
00146     function getParserOptions() {
00147         if ( !$this->mParserOptions ) {
00148             $this->mParserOptions = new ParserOptions;
00149             $this->mParserOptions->setEditSection( false );
00150         }
00151 
00152         return $this->mParserOptions;
00153     }
00154 
00162     function getLocalCache( $hash, $code ) {
00163         global $wgCacheDirectory;
00164 
00165         $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code";
00166 
00167         # Check file existence
00168         wfSuppressWarnings();
00169         $file = fopen( $filename, 'r' );
00170         wfRestoreWarnings();
00171         if ( !$file ) {
00172             return false; // No cache file
00173         }
00174 
00175         // Check to see if the file has the hash specified
00176         $localHash = fread( $file, 32 );
00177         if ( $hash === $localHash ) {
00178             // All good, get the rest of it
00179             $serialized = '';
00180             while ( !feof( $file ) ) {
00181                 $serialized .= fread( $file, 100000 );
00182             }
00183             fclose( $file );
00184 
00185             return unserialize( $serialized );
00186         } else {
00187             fclose( $file );
00188 
00189             return false; // Wrong hash
00190         }
00191     }
00192 
00199     function saveToLocal( $serialized, $hash, $code ) {
00200         global $wgCacheDirectory;
00201 
00202         $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code";
00203         wfMkdirParents( $wgCacheDirectory, null, __METHOD__ ); // might fail
00204 
00205         wfSuppressWarnings();
00206         $file = fopen( $filename, 'w' );
00207         wfRestoreWarnings();
00208 
00209         if ( !$file ) {
00210             wfDebug( "Unable to open local cache file for writing\n" );
00211 
00212             return;
00213         }
00214 
00215         fwrite( $file, $hash . $serialized );
00216         fclose( $file );
00217         wfSuppressWarnings();
00218         chmod( $filename, 0666 );
00219         wfRestoreWarnings();
00220     }
00221 
00242     function load( $code = false ) {
00243         global $wgUseLocalMessageCache;
00244 
00245         if ( !is_string( $code ) ) {
00246             # This isn't really nice, so at least make a note about it and try to
00247             # fall back
00248             wfDebug( __METHOD__ . " called without providing a language code\n" );
00249             $code = 'en';
00250         }
00251 
00252         # Don't do double loading...
00253         if ( isset( $this->mLoadedLanguages[$code] ) ) {
00254             return true;
00255         }
00256 
00257         # 8 lines of code just to say (once) that message cache is disabled
00258         if ( $this->mDisable ) {
00259             static $shownDisabled = false;
00260             if ( !$shownDisabled ) {
00261                 wfDebug( __METHOD__ . ": disabled\n" );
00262                 $shownDisabled = true;
00263             }
00264 
00265             return true;
00266         }
00267 
00268         # Loading code starts
00269         wfProfileIn( __METHOD__ );
00270         $success = false; # Keep track of success
00271         $staleCache = false; # a cache array with expired data, or false if none has been loaded
00272         $where = array(); # Debug info, delayed to avoid spamming debug log too much
00273         $cacheKey = wfMemcKey( 'messages', $code ); # Key in memc for messages
00274 
00275         # Local cache
00276         # Hash of the contents is stored in memcache, to detect if local cache goes
00277         # out of date (e.g. due to replace() on some other server)
00278         if ( $wgUseLocalMessageCache ) {
00279             wfProfileIn( __METHOD__ . '-fromlocal' );
00280 
00281             $hash = $this->mMemc->get( wfMemcKey( 'messages', $code, 'hash' ) );
00282             if ( $hash ) {
00283                 $cache = $this->getLocalCache( $hash, $code );
00284                 if ( !$cache ) {
00285                     $where[] = 'local cache is empty or has the wrong hash';
00286                 } elseif ( $this->isCacheExpired( $cache ) ) {
00287                     $where[] = 'local cache is expired';
00288                     $staleCache = $cache;
00289                 } else {
00290                     $where[] = 'got from local cache';
00291                     $success = true;
00292                     $this->mCache[$code] = $cache;
00293                 }
00294             }
00295             wfProfileOut( __METHOD__ . '-fromlocal' );
00296         }
00297 
00298         if ( !$success ) {
00299             # Try the global cache. If it is empty, try to acquire a lock. If
00300             # the lock can't be acquired, wait for the other thread to finish
00301             # and then try the global cache a second time.
00302             for ( $failedAttempts = 0; $failedAttempts < 2; $failedAttempts++ ) {
00303                 wfProfileIn( __METHOD__ . '-fromcache' );
00304                 $cache = $this->mMemc->get( $cacheKey );
00305                 if ( !$cache ) {
00306                     $where[] = 'global cache is empty';
00307                 } elseif ( $this->isCacheExpired( $cache ) ) {
00308                     $where[] = 'global cache is expired';
00309                     $staleCache = $cache;
00310                 } else {
00311                     $where[] = 'got from global cache';
00312                     $this->mCache[$code] = $cache;
00313                     $this->saveToCaches( $cache, 'local-only', $code );
00314                     $success = true;
00315                 }
00316 
00317                 wfProfileOut( __METHOD__ . '-fromcache' );
00318 
00319                 if ( $success ) {
00320                     # Done, no need to retry
00321                     break;
00322                 }
00323 
00324                 # We need to call loadFromDB. Limit the concurrency to a single
00325                 # process. This prevents the site from going down when the cache
00326                 # expires.
00327                 $statusKey = wfMemcKey( 'messages', $code, 'status' );
00328                 $acquired = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT );
00329                 if ( $acquired ) {
00330                     # Unlock the status key if there is an exception
00331                     $that = $this;
00332                     $statusUnlocker = new ScopedCallback( function () use ( $that, $statusKey ) {
00333                         $that->mMemc->delete( $statusKey );
00334                     } );
00335 
00336                     # Now let's regenerate
00337                     $where[] = 'loading from database';
00338 
00339                     # Lock the cache to prevent conflicting writes
00340                     # If this lock fails, it doesn't really matter, it just means the
00341                     # write is potentially non-atomic, e.g. the results of a replace()
00342                     # may be discarded.
00343                     if ( $this->lock( $cacheKey ) ) {
00344                         $mainUnlocker = new ScopedCallback( function () use ( $that, $cacheKey ) {
00345                             $that->unlock( $cacheKey );
00346                         } );
00347                     } else {
00348                         $mainUnlocker = null;
00349                         $where[] = 'could not acquire main lock';
00350                     }
00351 
00352                     $cache = $this->loadFromDB( $code );
00353                     $this->mCache[$code] = $cache;
00354                     $success = true;
00355                     $saveSuccess = $this->saveToCaches( $cache, 'all', $code );
00356 
00357                     # Unlock
00358                     ScopedCallback::consume( $mainUnlocker );
00359                     ScopedCallback::consume( $statusUnlocker );
00360 
00361                     if ( !$saveSuccess ) {
00362                         # Cache save has failed.
00363                         # There are two main scenarios where this could be a problem:
00364                         #
00365                         #   - The cache is more than the maximum size (typically
00366                         #     1MB compressed).
00367                         #
00368                         #   - Memcached has no space remaining in the relevant slab
00369                         #     class. This is unlikely with recent versions of
00370                         #     memcached.
00371                         #
00372                         # Either way, if there is a local cache, nothing bad will
00373                         # happen. If there is no local cache, disabling the message
00374                         # cache for all requests avoids incurring a loadFromDB()
00375                         # overhead on every request, and thus saves the wiki from
00376                         # complete downtime under moderate traffic conditions.
00377                         if ( !$wgUseLocalMessageCache ) {
00378                             $this->mMemc->set( $statusKey, 'error', 60 * 5 );
00379                             $where[] = 'could not save cache, disabled globally for 5 minutes';
00380                         } else {
00381                             $where[] = "could not save global cache";
00382                         }
00383                     }
00384 
00385                     # Load from DB complete, no need to retry
00386                     break;
00387                 } elseif ( $staleCache ) {
00388                     # Use the stale cache while some other thread constructs the new one
00389                     $where[] = 'using stale cache';
00390                     $this->mCache[$code] = $staleCache;
00391                     $success = true;
00392                     break;
00393                 } elseif ( $failedAttempts > 0 ) {
00394                     # Already retried once, still failed, so don't do another lock/unlock cycle
00395                     # This case will typically be hit if memcached is down, or if
00396                     # loadFromDB() takes longer than MSG_WAIT_TIMEOUT
00397                     $where[] = "could not acquire status key.";
00398                     break;
00399                 } else {
00400                     $status = $this->mMemc->get( $statusKey );
00401                     if ( $status === 'error' ) {
00402                         # Disable cache
00403                         break;
00404                     } else {
00405                         # Wait for the other thread to finish, then retry
00406                         $where[] = 'waited for other thread to complete';
00407                         $this->lock( $cacheKey );
00408                         $this->unlock( $cacheKey );
00409                     }
00410                 }
00411             }
00412         }
00413 
00414         if ( !$success ) {
00415             $where[] = 'loading FAILED - cache is disabled';
00416             $this->mDisable = true;
00417             $this->mCache = false;
00418             # This used to throw an exception, but that led to nasty side effects like
00419             # the whole wiki being instantly down if the memcached server died
00420         } else {
00421             # All good, just record the success
00422             $this->mLoadedLanguages[$code] = true;
00423         }
00424         $info = implode( ', ', $where );
00425         wfDebug( __METHOD__ . ": Loading $code... $info\n" );
00426         wfProfileOut( __METHOD__ );
00427 
00428         return $success;
00429     }
00430 
00439     function loadFromDB( $code ) {
00440         wfProfileIn( __METHOD__ );
00441         global $wgMaxMsgCacheEntrySize, $wgLanguageCode, $wgAdaptiveMessageCache;
00442         $dbr = wfGetDB( DB_SLAVE );
00443         $cache = array();
00444 
00445         # Common conditions
00446         $conds = array(
00447             'page_is_redirect' => 0,
00448             'page_namespace' => NS_MEDIAWIKI,
00449         );
00450 
00451         $mostused = array();
00452         if ( $wgAdaptiveMessageCache && $code !== $wgLanguageCode ) {
00453             if ( !isset( $this->mCache[$wgLanguageCode] ) ) {
00454                 $this->load( $wgLanguageCode );
00455             }
00456             $mostused = array_keys( $this->mCache[$wgLanguageCode] );
00457             foreach ( $mostused as $key => $value ) {
00458                 $mostused[$key] = "$value/$code";
00459             }
00460         }
00461 
00462         if ( count( $mostused ) ) {
00463             $conds['page_title'] = $mostused;
00464         } elseif ( $code !== $wgLanguageCode ) {
00465             $conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), '/', $code );
00466         } else {
00467             # Effectively disallows use of '/' character in NS_MEDIAWIKI for uses
00468             # other than language code.
00469             $conds[] = 'page_title NOT' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() );
00470         }
00471 
00472         # Conditions to fetch oversized pages to ignore them
00473         $bigConds = $conds;
00474         $bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize );
00475 
00476         # Load titles for all oversized pages in the MediaWiki namespace
00477         $res = $dbr->select( 'page', 'page_title', $bigConds, __METHOD__ . "($code)-big" );
00478         foreach ( $res as $row ) {
00479             $cache[$row->page_title] = '!TOO BIG';
00480         }
00481 
00482         # Conditions to load the remaining pages with their contents
00483         $smallConds = $conds;
00484         $smallConds[] = 'page_latest=rev_id';
00485         $smallConds[] = 'rev_text_id=old_id';
00486         $smallConds[] = 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize );
00487 
00488         $res = $dbr->select(
00489             array( 'page', 'revision', 'text' ),
00490             array( 'page_title', 'old_text', 'old_flags' ),
00491             $smallConds,
00492             __METHOD__ . "($code)-small"
00493         );
00494 
00495         foreach ( $res as $row ) {
00496             $text = Revision::getRevisionText( $row );
00497             if ( $text === false ) {
00498                 // Failed to fetch data; possible ES errors?
00499                 // Store a marker to fetch on-demand as a workaround...
00500                 $entry = '!TOO BIG';
00501                 wfDebugLog(
00502                     'MessageCache',
00503                     __METHOD__
00504                         . ": failed to load message page text for {$row->page_title} ($code)"
00505                 );
00506             } else {
00507                 $entry = ' ' . $text;
00508             }
00509             $cache[$row->page_title] = $entry;
00510         }
00511 
00512         $cache['VERSION'] = MSG_CACHE_VERSION;
00513         $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry );
00514         wfProfileOut( __METHOD__ );
00515 
00516         return $cache;
00517     }
00518 
00525     public function replace( $title, $text ) {
00526         global $wgMaxMsgCacheEntrySize;
00527         wfProfileIn( __METHOD__ );
00528 
00529         if ( $this->mDisable ) {
00530             wfProfileOut( __METHOD__ );
00531 
00532             return;
00533         }
00534 
00535         list( $msg, $code ) = $this->figureMessage( $title );
00536 
00537         $cacheKey = wfMemcKey( 'messages', $code );
00538         $this->load( $code );
00539         $this->lock( $cacheKey );
00540 
00541         $titleKey = wfMemcKey( 'messages', 'individual', $title );
00542 
00543         if ( $text === false ) {
00544             # Article was deleted
00545             $this->mCache[$code][$title] = '!NONEXISTENT';
00546             $this->mMemc->delete( $titleKey );
00547         } elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
00548             # Check for size
00549             $this->mCache[$code][$title] = '!TOO BIG';
00550             $this->mMemc->set( $titleKey, ' ' . $text, $this->mExpiry );
00551         } else {
00552             $this->mCache[$code][$title] = ' ' . $text;
00553             $this->mMemc->delete( $titleKey );
00554         }
00555 
00556         # Update caches
00557         $this->saveToCaches( $this->mCache[$code], 'all', $code );
00558         $this->unlock( $cacheKey );
00559 
00560         // Also delete cached sidebar... just in case it is affected
00561         $codes = array( $code );
00562         if ( $code === 'en' ) {
00563             // Delete all sidebars, like for example on action=purge on the
00564             // sidebar messages
00565             $codes = array_keys( Language::fetchLanguageNames() );
00566         }
00567 
00568         global $wgMemc;
00569         foreach ( $codes as $code ) {
00570             $sidebarKey = wfMemcKey( 'sidebar', $code );
00571             $wgMemc->delete( $sidebarKey );
00572         }
00573 
00574         // Update the message in the message blob store
00575         global $wgContLang;
00576         MessageBlobStore::getInstance()->updateMessage( $wgContLang->lcfirst( $msg ) );
00577 
00578         wfRunHooks( 'MessageCacheReplace', array( $title, $text ) );
00579 
00580         wfProfileOut( __METHOD__ );
00581     }
00582 
00589     protected function isCacheExpired( $cache ) {
00590         if ( !isset( $cache['VERSION'] ) || !isset( $cache['EXPIRY'] ) ) {
00591             return true;
00592         }
00593         if ( $cache['VERSION'] != MSG_CACHE_VERSION ) {
00594             return true;
00595         }
00596         if ( wfTimestampNow() >= $cache['EXPIRY'] ) {
00597             return true;
00598         }
00599 
00600         return false;
00601     }
00602 
00612     protected function saveToCaches( $cache, $dest, $code = false ) {
00613         wfProfileIn( __METHOD__ );
00614         global $wgUseLocalMessageCache;
00615 
00616         $cacheKey = wfMemcKey( 'messages', $code );
00617 
00618         if ( $dest === 'all' ) {
00619             $success = $this->mMemc->set( $cacheKey, $cache );
00620         } else {
00621             $success = true;
00622         }
00623 
00624         # Save to local cache
00625         if ( $wgUseLocalMessageCache ) {
00626             $serialized = serialize( $cache );
00627             $hash = md5( $serialized );
00628             $this->mMemc->set( wfMemcKey( 'messages', $code, 'hash' ), $hash );
00629             $this->saveToLocal( $serialized, $hash, $code );
00630         }
00631 
00632         wfProfileOut( __METHOD__ );
00633 
00634         return $success;
00635     }
00636 
00646     function lock( $key ) {
00647         $lockKey = $key . ':lock';
00648         $acquired = false;
00649         $testDone = false;
00650         for ( $i = 0; $i < MSG_WAIT_TIMEOUT && !$acquired; $i++ ) {
00651             $acquired = $this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT );
00652             if ( $acquired ) {
00653                 break;
00654             }
00655 
00656             # Fail fast if memcached is totally down
00657             if ( !$testDone ) {
00658                 $testDone = true;
00659                 if ( !$this->mMemc->set( wfMemcKey( 'test' ), 'test', 1 ) ) {
00660                     break;
00661                 }
00662             }
00663             sleep( 1 );
00664         }
00665 
00666         return $acquired;
00667     }
00668 
00669     function unlock( $key ) {
00670         $lockKey = $key . ':lock';
00671         $this->mMemc->delete( $lockKey );
00672     }
00673 
00708     function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) {
00709         global $wgContLang;
00710 
00711         $section = new ProfileSection( __METHOD__ );
00712 
00713         if ( is_int( $key ) ) {
00714             // Fix numerical strings that somehow become ints
00715             // on their way here
00716             $key = (string)$key;
00717         } elseif ( !is_string( $key ) ) {
00718             throw new MWException( 'Non-string key given' );
00719         } elseif ( $key === '' ) {
00720             // Shortcut: the empty key is always missing
00721             return false;
00722         }
00723 
00724         // For full keys, get the language code from the key
00725         $pos = strrpos( $key, '/' );
00726         if ( $isFullKey && $pos !== false ) {
00727             $langcode = substr( $key, $pos + 1 );
00728             $key = substr( $key, 0, $pos );
00729         }
00730 
00731         // Normalise title-case input (with some inlining)
00732         $lckey = strtr( $key, ' ', '_' );
00733         if ( ord( $lckey ) < 128 ) {
00734             $lckey[0] = strtolower( $lckey[0] );
00735         } else {
00736             $lckey = $wgContLang->lcfirst( $lckey );
00737         }
00738 
00739         wfRunHooks( 'MessageCache::get', array( &$lckey ) );
00740 
00741         if ( ord( $lckey ) < 128 ) {
00742             $uckey = ucfirst( $lckey );
00743         } else {
00744             $uckey = $wgContLang->ucfirst( $lckey );
00745         }
00746 
00747         // Loop through each language in the fallback list until we find something useful
00748         $lang = wfGetLangObj( $langcode );
00749         $message = $this->getMessageFromFallbackChain(
00750             $lang,
00751             $lckey,
00752             $uckey,
00753             !$this->mDisable && $useDB
00754         );
00755 
00756         // If we still have no message, maybe the key was in fact a full key so try that
00757         if ( $message === false ) {
00758             $parts = explode( '/', $lckey );
00759             // We may get calls for things that are http-urls from sidebar
00760             // Let's not load nonexistent languages for those
00761             // They usually have more than one slash.
00762             if ( count( $parts ) == 2 && $parts[1] !== '' ) {
00763                 $message = Language::getMessageFor( $parts[0], $parts[1] );
00764                 if ( $message === null ) {
00765                     $message = false;
00766                 }
00767             }
00768         }
00769 
00770         // Post-processing if the message exists
00771         if ( $message !== false ) {
00772             // Fix whitespace
00773             $message = str_replace(
00774                 array(
00775                     # Fix for trailing whitespace, removed by textarea
00776                     '&#32;',
00777                     # Fix for NBSP, converted to space by firefox
00778                     '&nbsp;',
00779                     '&#160;',
00780                 ),
00781                 array(
00782                     ' ',
00783                     "\xc2\xa0",
00784                     "\xc2\xa0"
00785                 ),
00786                 $message
00787             );
00788         }
00789 
00790         return $message;
00791     }
00792 
00806     protected function getMessageFromFallbackChain( $lang, $lckey, $uckey, $useDB ) {
00807         global $wgLanguageCode, $wgContLang;
00808 
00809         $langcode = $lang->getCode();
00810         $message = false;
00811 
00812         // First try the requested language.
00813         if ( $useDB ) {
00814             if ( $langcode === $wgLanguageCode ) {
00815                 // Messages created in the content language will not have the /lang extension
00816                 $message = $this->getMsgFromNamespace( $uckey, $langcode );
00817             } else {
00818                 $message = $this->getMsgFromNamespace( "$uckey/$langcode", $langcode );
00819             }
00820         }
00821 
00822         if ( $message !== false ) {
00823             return $message;
00824         }
00825 
00826         // Check the CDB cache
00827         $message = $lang->getMessage( $lckey );
00828         if ( $message !== null ) {
00829             return $message;
00830         }
00831 
00832         list( $fallbackChain, $siteFallbackChain ) =
00833             Language::getFallbacksIncludingSiteLanguage( $langcode );
00834 
00835         // Next try checking the database for all of the fallback languages of the requested language.
00836         if ( $useDB ) {
00837             foreach ( $fallbackChain as $code ) {
00838                 if ( $code === $wgLanguageCode ) {
00839                     // Messages created in the content language will not have the /lang extension
00840                     $message = $this->getMsgFromNamespace( $uckey, $code );
00841                 } else {
00842                     $message = $this->getMsgFromNamespace( "$uckey/$code", $code );
00843                 }
00844 
00845                 if ( $message !== false ) {
00846                     // Found the message.
00847                     return $message;
00848                 }
00849             }
00850         }
00851 
00852         // Now try checking the site language.
00853         if ( $useDB ) {
00854             $message = $this->getMsgFromNamespace( $uckey, $wgLanguageCode );
00855             if ( $message !== false ) {
00856                 return $message;
00857             }
00858         }
00859 
00860         $message = $wgContLang->getMessage( $lckey );
00861         if ( $message !== null ) {
00862             return $message;
00863         }
00864 
00865         // Finally try the DB for the site language's fallbacks.
00866         if ( $useDB ) {
00867             foreach ( $siteFallbackChain as $code ) {
00868                 $message = $this->getMsgFromNamespace( "$uckey/$code", $code );
00869                 if ( $message === false && $code === $wgLanguageCode ) {
00870                     // Messages created in the content language will not have the /lang extension
00871                     $message = $this->getMsgFromNamespace( $uckey, $code );
00872                 }
00873 
00874                 if ( $message !== false ) {
00875                     // Found the message.
00876                     return $message;
00877                 }
00878             }
00879         }
00880 
00881         return false;
00882     }
00883 
00896     function getMsgFromNamespace( $title, $code ) {
00897         $this->load( $code );
00898         if ( isset( $this->mCache[$code][$title] ) ) {
00899             $entry = $this->mCache[$code][$title];
00900             if ( substr( $entry, 0, 1 ) === ' ' ) {
00901                 // The message exists, so make sure a string
00902                 // is returned.
00903                 return (string)substr( $entry, 1 );
00904             } elseif ( $entry === '!NONEXISTENT' ) {
00905                 return false;
00906             } elseif ( $entry === '!TOO BIG' ) {
00907                 // Fall through and try invididual message cache below
00908             }
00909         } else {
00910             // XXX: This is not cached in process cache, should it?
00911             $message = false;
00912             wfRunHooks( 'MessagesPreLoad', array( $title, &$message ) );
00913             if ( $message !== false ) {
00914                 return $message;
00915             }
00916 
00917             return false;
00918         }
00919 
00920         # Try the individual message cache
00921         $titleKey = wfMemcKey( 'messages', 'individual', $title );
00922         $entry = $this->mMemc->get( $titleKey );
00923         if ( $entry ) {
00924             if ( substr( $entry, 0, 1 ) === ' ' ) {
00925                 $this->mCache[$code][$title] = $entry;
00926 
00927                 // The message exists, so make sure a string
00928                 // is returned.
00929                 return (string)substr( $entry, 1 );
00930             } elseif ( $entry === '!NONEXISTENT' ) {
00931                 $this->mCache[$code][$title] = '!NONEXISTENT';
00932 
00933                 return false;
00934             } else {
00935                 # Corrupt/obsolete entry, delete it
00936                 $this->mMemc->delete( $titleKey );
00937             }
00938         }
00939 
00940         # Try loading it from the database
00941         $revision = Revision::newFromTitle(
00942             Title::makeTitle( NS_MEDIAWIKI, $title ), false, Revision::READ_LATEST
00943         );
00944         if ( $revision ) {
00945             $content = $revision->getContent();
00946             if ( !$content ) {
00947                 // A possibly temporary loading failure.
00948                 wfDebugLog(
00949                     'MessageCache',
00950                     __METHOD__ . ": failed to load message page text for {$title} ($code)"
00951                 );
00952                 $message = null; // no negative caching
00953             } else {
00954                 // XXX: Is this the right way to turn a Content object into a message?
00955                 // NOTE: $content is typically either WikitextContent, JavaScriptContent or
00956                 //       CssContent. MessageContent is *not* used for storing messages, it's
00957                 //       only used for wrapping them when needed.
00958                 $message = $content->getWikitextForTransclusion();
00959 
00960                 if ( $message === false || $message === null ) {
00961                     wfDebugLog(
00962                         'MessageCache',
00963                         __METHOD__ . ": message content doesn't provide wikitext "
00964                             . "(content model: " . $content->getContentHandler() . ")"
00965                     );
00966 
00967                     $message = false; // negative caching
00968                 } else {
00969                     $this->mCache[$code][$title] = ' ' . $message;
00970                     $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry );
00971                 }
00972             }
00973         } else {
00974             $message = false; // negative caching
00975         }
00976 
00977         if ( $message === false ) { // negative caching
00978             $this->mCache[$code][$title] = '!NONEXISTENT';
00979             $this->mMemc->set( $titleKey, '!NONEXISTENT', $this->mExpiry );
00980         }
00981 
00982         return $message;
00983     }
00984 
00992     function transform( $message, $interface = false, $language = null, $title = null ) {
00993         // Avoid creating parser if nothing to transform
00994         if ( strpos( $message, '{{' ) === false ) {
00995             return $message;
00996         }
00997 
00998         if ( $this->mInParser ) {
00999             return $message;
01000         }
01001 
01002         $parser = $this->getParser();
01003         if ( $parser ) {
01004             $popts = $this->getParserOptions();
01005             $popts->setInterfaceMessage( $interface );
01006             $popts->setTargetLanguage( $language );
01007 
01008             $userlang = $popts->setUserLang( $language );
01009             $this->mInParser = true;
01010             $message = $parser->transformMsg( $message, $popts, $title );
01011             $this->mInParser = false;
01012             $popts->setUserLang( $userlang );
01013         }
01014 
01015         return $message;
01016     }
01017 
01021     function getParser() {
01022         global $wgParser, $wgParserConf;
01023         if ( !$this->mParser && isset( $wgParser ) ) {
01024             # Do some initialisation so that we don't have to do it twice
01025             $wgParser->firstCallInit();
01026             # Clone it and store it
01027             $class = $wgParserConf['class'];
01028             if ( $class == 'ParserDiffTest' ) {
01029                 # Uncloneable
01030                 $this->mParser = new $class( $wgParserConf );
01031             } else {
01032                 $this->mParser = clone $wgParser;
01033             }
01034         }
01035 
01036         return $this->mParser;
01037     }
01038 
01047     public function parse( $text, $title = null, $linestart = true,
01048         $interface = false, $language = null
01049     ) {
01050         if ( $this->mInParser ) {
01051             return htmlspecialchars( $text );
01052         }
01053 
01054         $parser = $this->getParser();
01055         $popts = $this->getParserOptions();
01056         $popts->setInterfaceMessage( $interface );
01057         $popts->setTargetLanguage( $language );
01058 
01059         wfProfileIn( __METHOD__ );
01060         if ( !$title || !$title instanceof Title ) {
01061             global $wgTitle;
01062             $title = $wgTitle;
01063         }
01064         // Sometimes $wgTitle isn't set either...
01065         if ( !$title ) {
01066             # It's not uncommon having a null $wgTitle in scripts. See r80898
01067             # Create a ghost title in such case
01068             $title = Title::newFromText( 'Dwimmerlaik' );
01069         }
01070 
01071         $this->mInParser = true;
01072         $res = $parser->parse( $text, $title, $popts, $linestart );
01073         $this->mInParser = false;
01074 
01075         wfProfileOut( __METHOD__ );
01076 
01077         return $res;
01078     }
01079 
01080     function disable() {
01081         $this->mDisable = true;
01082     }
01083 
01084     function enable() {
01085         $this->mDisable = false;
01086     }
01087 
01091     function clear() {
01092         $langs = Language::fetchLanguageNames( null, 'mw' );
01093         foreach ( array_keys( $langs ) as $code ) {
01094             # Global cache
01095             $this->mMemc->delete( wfMemcKey( 'messages', $code ) );
01096             # Invalidate all local caches
01097             $this->mMemc->delete( wfMemcKey( 'messages', $code, 'hash' ) );
01098         }
01099         $this->mLoadedLanguages = array();
01100     }
01101 
01106     public function figureMessage( $key ) {
01107         global $wgLanguageCode;
01108         $pieces = explode( '/', $key );
01109         if ( count( $pieces ) < 2 ) {
01110             return array( $key, $wgLanguageCode );
01111         }
01112 
01113         $lang = array_pop( $pieces );
01114         if ( !Language::fetchLanguageName( $lang, null, 'mw' ) ) {
01115             return array( $key, $wgLanguageCode );
01116         }
01117 
01118         $message = implode( '/', $pieces );
01119 
01120         return array( $message, $lang );
01121     }
01122 
01131     public function getAllMessageKeys( $code ) {
01132         global $wgContLang;
01133         $this->load( $code );
01134         if ( !isset( $this->mCache[$code] ) ) {
01135             // Apparently load() failed
01136             return null;
01137         }
01138         // Remove administrative keys
01139         $cache = $this->mCache[$code];
01140         unset( $cache['VERSION'] );
01141         unset( $cache['EXPIRY'] );
01142         // Remove any !NONEXISTENT keys
01143         $cache = array_diff( $cache, array( '!NONEXISTENT' ) );
01144 
01145         // Keys may appear with a capital first letter. lcfirst them.
01146         return array_map( array( $wgContLang, 'lcfirst' ), array_keys( $cache ) );
01147     }
01148 }