MediaWiki
REL1_22
|
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 ' ', 00752 # Fix for NBSP, converted to space by firefox 00753 ' ', 00754 ' ', 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 }