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