MediaWiki
REL1_23
|
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 ' ', 00775 # Fix for NBSP, converted to space by firefox 00776 ' ', 00777 ' ', 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 }