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