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