[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Localisation messages cache. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup Cache 22 */ 23 24 /** 25 * MediaWiki message cache structure version. 26 * Bump this whenever the message cache format has changed. 27 */ 28 define( 'MSG_CACHE_VERSION', 1 ); 29 30 /** 31 * Memcached timeout when loading a key. 32 * See MessageCache::load() 33 */ 34 define( 'MSG_LOAD_TIMEOUT', 60 ); 35 36 /** 37 * Memcached timeout when locking a key for a writing operation. 38 * See MessageCache::lock() 39 */ 40 define( 'MSG_LOCK_TIMEOUT', 30 ); 41 /** 42 * Number of times we will try to acquire a lock from Memcached. 43 * This comes in addition to MSG_LOCK_TIMEOUT. 44 */ 45 define( 'MSG_WAIT_TIMEOUT', 30 ); 46 47 /** 48 * Message cache 49 * Performs various MediaWiki namespace-related functions 50 * @ingroup Cache 51 */ 52 class MessageCache { 53 /** 54 * Process local cache of loaded messages that are defined in 55 * MediaWiki namespace. First array level is a language code, 56 * second level is message key and the values are either message 57 * content prefixed with space, or !NONEXISTENT for negative 58 * caching. 59 */ 60 protected $mCache; 61 62 /** 63 * Should mean that database cannot be used, but check 64 * @var bool $mDisable 65 */ 66 protected $mDisable; 67 68 /** 69 * Lifetime for cache, used by object caching. 70 * Set on construction, see __construct(). 71 */ 72 protected $mExpiry; 73 74 /** 75 * Message cache has it's own parser which it uses to transform 76 * messages. 77 */ 78 protected $mParserOptions, $mParser; 79 80 /** 81 * Variable for tracking which variables are already loaded 82 * @var array $mLoadedLanguages 83 */ 84 protected $mLoadedLanguages = array(); 85 86 /** 87 * Singleton instance 88 * 89 * @var MessageCache $instance 90 */ 91 private static $instance; 92 93 /** 94 * @var bool $mInParser 95 */ 96 protected $mInParser = false; 97 98 /** 99 * Get the signleton instance of this class 100 * 101 * @since 1.18 102 * @return MessageCache 103 */ 104 public static function singleton() { 105 if ( is_null( self::$instance ) ) { 106 global $wgUseDatabaseMessages, $wgMsgCacheExpiry; 107 self::$instance = new self( 108 wfGetMessageCacheStorage(), 109 $wgUseDatabaseMessages, 110 $wgMsgCacheExpiry 111 ); 112 } 113 114 return self::$instance; 115 } 116 117 /** 118 * Destroy the singleton instance 119 * 120 * @since 1.18 121 */ 122 public static function destroyInstance() { 123 self::$instance = null; 124 } 125 126 /** 127 * @param BagOStuff $memCached A cache instance. If none, fall back to CACHE_NONE. 128 * @param bool $useDB 129 * @param int $expiry Lifetime for cache. @see $mExpiry. 130 */ 131 function __construct( $memCached, $useDB, $expiry ) { 132 if ( !$memCached ) { 133 $memCached = wfGetCache( CACHE_NONE ); 134 } 135 136 $this->mMemc = $memCached; 137 $this->mDisable = !$useDB; 138 $this->mExpiry = $expiry; 139 } 140 141 /** 142 * ParserOptions is lazy initialised. 143 * 144 * @return ParserOptions 145 */ 146 function getParserOptions() { 147 if ( !$this->mParserOptions ) { 148 $this->mParserOptions = new ParserOptions; 149 $this->mParserOptions->setEditSection( false ); 150 } 151 152 return $this->mParserOptions; 153 } 154 155 /** 156 * Try to load the cache from a local file. 157 * 158 * @param string $hash The hash of contents, to check validity. 159 * @param string $code Optional language code, see documenation of load(). 160 * @return array The cache array 161 */ 162 function getLocalCache( $hash, $code ) { 163 global $wgCacheDirectory; 164 165 $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code"; 166 167 # Check file existence 168 wfSuppressWarnings(); 169 $file = fopen( $filename, 'r' ); 170 wfRestoreWarnings(); 171 if ( !$file ) { 172 return false; // No cache file 173 } 174 175 // Check to see if the file has the hash specified 176 $localHash = fread( $file, 32 ); 177 if ( $hash === $localHash ) { 178 // All good, get the rest of it 179 $serialized = ''; 180 while ( !feof( $file ) ) { 181 $serialized .= fread( $file, 100000 ); 182 } 183 fclose( $file ); 184 185 return unserialize( $serialized ); 186 } else { 187 fclose( $file ); 188 189 return false; // Wrong hash 190 } 191 } 192 193 /** 194 * Save the cache to a local file. 195 * @param string $serialized 196 * @param string $hash 197 * @param string $code 198 */ 199 function saveToLocal( $serialized, $hash, $code ) { 200 global $wgCacheDirectory; 201 202 $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code"; 203 wfMkdirParents( $wgCacheDirectory, null, __METHOD__ ); // might fail 204 205 wfSuppressWarnings(); 206 $file = fopen( $filename, 'w' ); 207 wfRestoreWarnings(); 208 209 if ( !$file ) { 210 wfDebug( "Unable to open local cache file for writing\n" ); 211 212 return; 213 } 214 215 fwrite( $file, $hash . $serialized ); 216 fclose( $file ); 217 wfSuppressWarnings(); 218 chmod( $filename, 0666 ); 219 wfRestoreWarnings(); 220 } 221 222 /** 223 * Loads messages from caches or from database in this order: 224 * (1) local message cache (if $wgUseLocalMessageCache is enabled) 225 * (2) memcached 226 * (3) from the database. 227 * 228 * When succesfully loading from (2) or (3), all higher level caches are 229 * updated for the newest version. 230 * 231 * Nothing is loaded if member variable mDisable is true, either manually 232 * set by calling code or if message loading fails (is this possible?). 233 * 234 * Returns true if cache is already populated or it was succesfully populated, 235 * or false if populating empty cache fails. Also returns true if MessageCache 236 * is disabled. 237 * 238 * @param bool|string $code Language to which load messages 239 * @throws MWException 240 * @return bool 241 */ 242 function load( $code = false ) { 243 global $wgUseLocalMessageCache; 244 245 if ( !is_string( $code ) ) { 246 # This isn't really nice, so at least make a note about it and try to 247 # fall back 248 wfDebug( __METHOD__ . " called without providing a language code\n" ); 249 $code = 'en'; 250 } 251 252 # Don't do double loading... 253 if ( isset( $this->mLoadedLanguages[$code] ) ) { 254 return true; 255 } 256 257 # 8 lines of code just to say (once) that message cache is disabled 258 if ( $this->mDisable ) { 259 static $shownDisabled = false; 260 if ( !$shownDisabled ) { 261 wfDebug( __METHOD__ . ": disabled\n" ); 262 $shownDisabled = true; 263 } 264 265 return true; 266 } 267 268 # Loading code starts 269 wfProfileIn( __METHOD__ ); 270 $success = false; # Keep track of success 271 $staleCache = false; # a cache array with expired data, or false if none has been loaded 272 $where = array(); # Debug info, delayed to avoid spamming debug log too much 273 $cacheKey = wfMemcKey( 'messages', $code ); # Key in memc for messages 274 275 # Local cache 276 # Hash of the contents is stored in memcache, to detect if local cache goes 277 # out of date (e.g. due to replace() on some other server) 278 if ( $wgUseLocalMessageCache ) { 279 wfProfileIn( __METHOD__ . '-fromlocal' ); 280 281 $hash = $this->mMemc->get( wfMemcKey( 'messages', $code, 'hash' ) ); 282 if ( $hash ) { 283 $cache = $this->getLocalCache( $hash, $code ); 284 if ( !$cache ) { 285 $where[] = 'local cache is empty or has the wrong hash'; 286 } elseif ( $this->isCacheExpired( $cache ) ) { 287 $where[] = 'local cache is expired'; 288 $staleCache = $cache; 289 } else { 290 $where[] = 'got from local cache'; 291 $success = true; 292 $this->mCache[$code] = $cache; 293 } 294 } 295 wfProfileOut( __METHOD__ . '-fromlocal' ); 296 } 297 298 if ( !$success ) { 299 # Try the global cache. If it is empty, try to acquire a lock. If 300 # the lock can't be acquired, wait for the other thread to finish 301 # and then try the global cache a second time. 302 for ( $failedAttempts = 0; $failedAttempts < 2; $failedAttempts++ ) { 303 wfProfileIn( __METHOD__ . '-fromcache' ); 304 $cache = $this->mMemc->get( $cacheKey ); 305 if ( !$cache ) { 306 $where[] = 'global cache is empty'; 307 } elseif ( $this->isCacheExpired( $cache ) ) { 308 $where[] = 'global cache is expired'; 309 $staleCache = $cache; 310 } else { 311 $where[] = 'got from global cache'; 312 $this->mCache[$code] = $cache; 313 $this->saveToCaches( $cache, 'local-only', $code ); 314 $success = true; 315 } 316 317 wfProfileOut( __METHOD__ . '-fromcache' ); 318 319 if ( $success ) { 320 # Done, no need to retry 321 break; 322 } 323 324 # We need to call loadFromDB. Limit the concurrency to a single 325 # process. This prevents the site from going down when the cache 326 # expires. 327 $statusKey = wfMemcKey( 'messages', $code, 'status' ); 328 $acquired = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT ); 329 if ( $acquired ) { 330 # Unlock the status key if there is an exception 331 $that = $this; 332 $statusUnlocker = new ScopedCallback( function () use ( $that, $statusKey ) { 333 $that->mMemc->delete( $statusKey ); 334 } ); 335 336 # Now let's regenerate 337 $where[] = 'loading from database'; 338 339 # Lock the cache to prevent conflicting writes 340 # If this lock fails, it doesn't really matter, it just means the 341 # write is potentially non-atomic, e.g. the results of a replace() 342 # may be discarded. 343 if ( $this->lock( $cacheKey ) ) { 344 $mainUnlocker = new ScopedCallback( function () use ( $that, $cacheKey ) { 345 $that->unlock( $cacheKey ); 346 } ); 347 } else { 348 $mainUnlocker = null; 349 $where[] = 'could not acquire main lock'; 350 } 351 352 $cache = $this->loadFromDB( $code ); 353 $this->mCache[$code] = $cache; 354 $success = true; 355 $saveSuccess = $this->saveToCaches( $cache, 'all', $code ); 356 357 # Unlock 358 ScopedCallback::consume( $mainUnlocker ); 359 ScopedCallback::consume( $statusUnlocker ); 360 361 if ( !$saveSuccess ) { 362 # Cache save has failed. 363 # There are two main scenarios where this could be a problem: 364 # 365 # - The cache is more than the maximum size (typically 366 # 1MB compressed). 367 # 368 # - Memcached has no space remaining in the relevant slab 369 # class. This is unlikely with recent versions of 370 # memcached. 371 # 372 # Either way, if there is a local cache, nothing bad will 373 # happen. If there is no local cache, disabling the message 374 # cache for all requests avoids incurring a loadFromDB() 375 # overhead on every request, and thus saves the wiki from 376 # complete downtime under moderate traffic conditions. 377 if ( !$wgUseLocalMessageCache ) { 378 $this->mMemc->set( $statusKey, 'error', 60 * 5 ); 379 $where[] = 'could not save cache, disabled globally for 5 minutes'; 380 } else { 381 $where[] = "could not save global cache"; 382 } 383 } 384 385 # Load from DB complete, no need to retry 386 break; 387 } elseif ( $staleCache ) { 388 # Use the stale cache while some other thread constructs the new one 389 $where[] = 'using stale cache'; 390 $this->mCache[$code] = $staleCache; 391 $success = true; 392 break; 393 } elseif ( $failedAttempts > 0 ) { 394 # Already retried once, still failed, so don't do another lock/unlock cycle 395 # This case will typically be hit if memcached is down, or if 396 # loadFromDB() takes longer than MSG_WAIT_TIMEOUT 397 $where[] = "could not acquire status key."; 398 break; 399 } else { 400 $status = $this->mMemc->get( $statusKey ); 401 if ( $status === 'error' ) { 402 # Disable cache 403 break; 404 } else { 405 # Wait for the other thread to finish, then retry 406 $where[] = 'waited for other thread to complete'; 407 $this->lock( $cacheKey ); 408 $this->unlock( $cacheKey ); 409 } 410 } 411 } 412 } 413 414 if ( !$success ) { 415 $where[] = 'loading FAILED - cache is disabled'; 416 $this->mDisable = true; 417 $this->mCache = false; 418 # This used to throw an exception, but that led to nasty side effects like 419 # the whole wiki being instantly down if the memcached server died 420 } else { 421 # All good, just record the success 422 $this->mLoadedLanguages[$code] = true; 423 } 424 $info = implode( ', ', $where ); 425 wfDebug( __METHOD__ . ": Loading $code... $info\n" ); 426 wfProfileOut( __METHOD__ ); 427 428 return $success; 429 } 430 431 /** 432 * Loads cacheable messages from the database. Messages bigger than 433 * $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded 434 * on-demand from the database later. 435 * 436 * @param string $code Language code. 437 * @return array Loaded messages for storing in caches. 438 */ 439 function loadFromDB( $code ) { 440 wfProfileIn( __METHOD__ ); 441 global $wgMaxMsgCacheEntrySize, $wgLanguageCode, $wgAdaptiveMessageCache; 442 $dbr = wfGetDB( DB_SLAVE ); 443 $cache = array(); 444 445 # Common conditions 446 $conds = array( 447 'page_is_redirect' => 0, 448 'page_namespace' => NS_MEDIAWIKI, 449 ); 450 451 $mostused = array(); 452 if ( $wgAdaptiveMessageCache && $code !== $wgLanguageCode ) { 453 if ( !isset( $this->mCache[$wgLanguageCode] ) ) { 454 $this->load( $wgLanguageCode ); 455 } 456 $mostused = array_keys( $this->mCache[$wgLanguageCode] ); 457 foreach ( $mostused as $key => $value ) { 458 $mostused[$key] = "$value/$code"; 459 } 460 } 461 462 if ( count( $mostused ) ) { 463 $conds['page_title'] = $mostused; 464 } elseif ( $code !== $wgLanguageCode ) { 465 $conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), '/', $code ); 466 } else { 467 # Effectively disallows use of '/' character in NS_MEDIAWIKI for uses 468 # other than language code. 469 $conds[] = 'page_title NOT' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() ); 470 } 471 472 # Conditions to fetch oversized pages to ignore them 473 $bigConds = $conds; 474 $bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize ); 475 476 # Load titles for all oversized pages in the MediaWiki namespace 477 $res = $dbr->select( 'page', 'page_title', $bigConds, __METHOD__ . "($code)-big" ); 478 foreach ( $res as $row ) { 479 $cache[$row->page_title] = '!TOO BIG'; 480 } 481 482 # Conditions to load the remaining pages with their contents 483 $smallConds = $conds; 484 $smallConds[] = 'page_latest=rev_id'; 485 $smallConds[] = 'rev_text_id=old_id'; 486 $smallConds[] = 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize ); 487 488 $res = $dbr->select( 489 array( 'page', 'revision', 'text' ), 490 array( 'page_title', 'old_text', 'old_flags' ), 491 $smallConds, 492 __METHOD__ . "($code)-small" 493 ); 494 495 foreach ( $res as $row ) { 496 $text = Revision::getRevisionText( $row ); 497 if ( $text === false ) { 498 // Failed to fetch data; possible ES errors? 499 // Store a marker to fetch on-demand as a workaround... 500 $entry = '!TOO BIG'; 501 wfDebugLog( 502 'MessageCache', 503 __METHOD__ 504 . ": failed to load message page text for {$row->page_title} ($code)" 505 ); 506 } else { 507 $entry = ' ' . $text; 508 } 509 $cache[$row->page_title] = $entry; 510 } 511 512 $cache['VERSION'] = MSG_CACHE_VERSION; 513 $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry ); 514 wfProfileOut( __METHOD__ ); 515 516 return $cache; 517 } 518 519 /** 520 * Updates cache as necessary when message page is changed 521 * 522 * @param string $title Name of the page changed. 523 * @param mixed $text New contents of the page. 524 */ 525 public function replace( $title, $text ) { 526 global $wgMaxMsgCacheEntrySize; 527 wfProfileIn( __METHOD__ ); 528 529 if ( $this->mDisable ) { 530 wfProfileOut( __METHOD__ ); 531 532 return; 533 } 534 535 list( $msg, $code ) = $this->figureMessage( $title ); 536 537 $cacheKey = wfMemcKey( 'messages', $code ); 538 $this->load( $code ); 539 $this->lock( $cacheKey ); 540 541 $titleKey = wfMemcKey( 'messages', 'individual', $title ); 542 543 if ( $text === false ) { 544 # Article was deleted 545 $this->mCache[$code][$title] = '!NONEXISTENT'; 546 $this->mMemc->delete( $titleKey ); 547 } elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) { 548 # Check for size 549 $this->mCache[$code][$title] = '!TOO BIG'; 550 $this->mMemc->set( $titleKey, ' ' . $text, $this->mExpiry ); 551 } else { 552 $this->mCache[$code][$title] = ' ' . $text; 553 $this->mMemc->delete( $titleKey ); 554 } 555 556 # Update caches 557 $this->saveToCaches( $this->mCache[$code], 'all', $code ); 558 $this->unlock( $cacheKey ); 559 560 // Also delete cached sidebar... just in case it is affected 561 $codes = array( $code ); 562 if ( $code === 'en' ) { 563 // Delete all sidebars, like for example on action=purge on the 564 // sidebar messages 565 $codes = array_keys( Language::fetchLanguageNames() ); 566 } 567 568 global $wgMemc; 569 foreach ( $codes as $code ) { 570 $sidebarKey = wfMemcKey( 'sidebar', $code ); 571 $wgMemc->delete( $sidebarKey ); 572 } 573 574 // Update the message in the message blob store 575 global $wgContLang; 576 MessageBlobStore::getInstance()->updateMessage( $wgContLang->lcfirst( $msg ) ); 577 578 wfRunHooks( 'MessageCacheReplace', array( $title, $text ) ); 579 580 wfProfileOut( __METHOD__ ); 581 } 582 583 /** 584 * Is the given cache array expired due to time passing or a version change? 585 * 586 * @param array $cache 587 * @return bool 588 */ 589 protected function isCacheExpired( $cache ) { 590 if ( !isset( $cache['VERSION'] ) || !isset( $cache['EXPIRY'] ) ) { 591 return true; 592 } 593 if ( $cache['VERSION'] != MSG_CACHE_VERSION ) { 594 return true; 595 } 596 if ( wfTimestampNow() >= $cache['EXPIRY'] ) { 597 return true; 598 } 599 600 return false; 601 } 602 603 /** 604 * Shortcut to update caches. 605 * 606 * @param array $cache Cached messages with a version. 607 * @param string $dest Either "local-only" to save to local caches only 608 * or "all" to save to all caches. 609 * @param string|bool $code Language code (default: false) 610 * @return bool 611 */ 612 protected function saveToCaches( $cache, $dest, $code = false ) { 613 wfProfileIn( __METHOD__ ); 614 global $wgUseLocalMessageCache; 615 616 $cacheKey = wfMemcKey( 'messages', $code ); 617 618 if ( $dest === 'all' ) { 619 $success = $this->mMemc->set( $cacheKey, $cache ); 620 } else { 621 $success = true; 622 } 623 624 # Save to local cache 625 if ( $wgUseLocalMessageCache ) { 626 $serialized = serialize( $cache ); 627 $hash = md5( $serialized ); 628 $this->mMemc->set( wfMemcKey( 'messages', $code, 'hash' ), $hash ); 629 $this->saveToLocal( $serialized, $hash, $code ); 630 } 631 632 wfProfileOut( __METHOD__ ); 633 634 return $success; 635 } 636 637 /** 638 * Represents a write lock on the messages key. 639 * 640 * Will retry MessageCache::MSG_WAIT_TIMEOUT times, each operations having 641 * a timeout of MessageCache::MSG_LOCK_TIMEOUT. 642 * 643 * @param string $key 644 * @return bool Success 645 */ 646 function lock( $key ) { 647 $lockKey = $key . ':lock'; 648 $acquired = false; 649 $testDone = false; 650 for ( $i = 0; $i < MSG_WAIT_TIMEOUT && !$acquired; $i++ ) { 651 $acquired = $this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT ); 652 if ( $acquired ) { 653 break; 654 } 655 656 # Fail fast if memcached is totally down 657 if ( !$testDone ) { 658 $testDone = true; 659 if ( !$this->mMemc->set( wfMemcKey( 'test' ), 'test', 1 ) ) { 660 break; 661 } 662 } 663 sleep( 1 ); 664 } 665 666 return $acquired; 667 } 668 669 function unlock( $key ) { 670 $lockKey = $key . ':lock'; 671 $this->mMemc->delete( $lockKey ); 672 } 673 674 /** 675 * Get a message from either the content language or the user language. 676 * 677 * First, assemble a list of languages to attempt getting the message from. This 678 * chain begins with the requested language and its fallbacks and then continues with 679 * the content language and its fallbacks. For each language in the chain, the following 680 * process will occur (in this order): 681 * 1. If a language-specific override, i.e., [[MW:msg/lang]], is available, use that. 682 * Note: for the content language, there is no /lang subpage. 683 * 2. Fetch from the static CDB cache. 684 * 3. If available, check the database for fallback language overrides. 685 * 686 * This process provides a number of guarantees. When changing this code, make sure all 687 * of these guarantees are preserved. 688 * * If the requested language is *not* the content language, then the CDB cache for that 689 * specific language will take precedence over the root database page ([[MW:msg]]). 690 * * Fallbacks will be just that: fallbacks. A fallback language will never be reached if 691 * the message is available *anywhere* in the language for which it is a fallback. 692 * 693 * @param string $key The message key 694 * @param bool $useDB If true, look for the message in the DB, false 695 * to use only the compiled l10n cache. 696 * @param bool|string|object $langcode Code of the language to get the message for. 697 * - If string and a valid code, will create a standard language object 698 * - If string but not a valid code, will create a basic language object 699 * - If boolean and false, create object from the current users language 700 * - If boolean and true, create object from the wikis content language 701 * - If language object, use it as given 702 * @param bool $isFullKey Specifies whether $key is a two part key "msg/lang". 703 * 704 * @throws MWException When given an invalid key 705 * @return string|bool False if the message doesn't exist, otherwise the 706 * message (which can be empty) 707 */ 708 function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) { 709 global $wgContLang; 710 711 $section = new ProfileSection( __METHOD__ ); 712 713 if ( is_int( $key ) ) { 714 // Fix numerical strings that somehow become ints 715 // on their way here 716 $key = (string)$key; 717 } elseif ( !is_string( $key ) ) { 718 throw new MWException( 'Non-string key given' ); 719 } elseif ( $key === '' ) { 720 // Shortcut: the empty key is always missing 721 return false; 722 } 723 724 // For full keys, get the language code from the key 725 $pos = strrpos( $key, '/' ); 726 if ( $isFullKey && $pos !== false ) { 727 $langcode = substr( $key, $pos + 1 ); 728 $key = substr( $key, 0, $pos ); 729 } 730 731 // Normalise title-case input (with some inlining) 732 $lckey = strtr( $key, ' ', '_' ); 733 if ( ord( $lckey ) < 128 ) { 734 $lckey[0] = strtolower( $lckey[0] ); 735 } else { 736 $lckey = $wgContLang->lcfirst( $lckey ); 737 } 738 739 wfRunHooks( 'MessageCache::get', array( &$lckey ) ); 740 741 if ( ord( $lckey ) < 128 ) { 742 $uckey = ucfirst( $lckey ); 743 } else { 744 $uckey = $wgContLang->ucfirst( $lckey ); 745 } 746 747 // Loop through each language in the fallback list until we find something useful 748 $lang = wfGetLangObj( $langcode ); 749 $message = $this->getMessageFromFallbackChain( 750 $lang, 751 $lckey, 752 $uckey, 753 !$this->mDisable && $useDB 754 ); 755 756 // If we still have no message, maybe the key was in fact a full key so try that 757 if ( $message === false ) { 758 $parts = explode( '/', $lckey ); 759 // We may get calls for things that are http-urls from sidebar 760 // Let's not load nonexistent languages for those 761 // They usually have more than one slash. 762 if ( count( $parts ) == 2 && $parts[1] !== '' ) { 763 $message = Language::getMessageFor( $parts[0], $parts[1] ); 764 if ( $message === null ) { 765 $message = false; 766 } 767 } 768 } 769 770 // Post-processing if the message exists 771 if ( $message !== false ) { 772 // Fix whitespace 773 $message = str_replace( 774 array( 775 # Fix for trailing whitespace, removed by textarea 776 ' ', 777 # Fix for NBSP, converted to space by firefox 778 ' ', 779 ' ', 780 ), 781 array( 782 ' ', 783 "\xc2\xa0", 784 "\xc2\xa0" 785 ), 786 $message 787 ); 788 } 789 790 return $message; 791 } 792 793 /** 794 * Given a language, try and fetch a message from that language, then the 795 * fallbacks of that language, then the site language, then the fallbacks for the 796 * site language. 797 * 798 * @param Language $lang Requested language 799 * @param string $lckey Lowercase key for the message 800 * @param string $uckey Uppercase key for the message 801 * @param bool $useDB Whether to use the database 802 * 803 * @see MessageCache::get 804 * @return string|bool The message, or false if not found 805 */ 806 protected function getMessageFromFallbackChain( $lang, $lckey, $uckey, $useDB ) { 807 global $wgLanguageCode, $wgContLang; 808 809 $langcode = $lang->getCode(); 810 $message = false; 811 812 // First try the requested language. 813 if ( $useDB ) { 814 if ( $langcode === $wgLanguageCode ) { 815 // Messages created in the content language will not have the /lang extension 816 $message = $this->getMsgFromNamespace( $uckey, $langcode ); 817 } else { 818 $message = $this->getMsgFromNamespace( "$uckey/$langcode", $langcode ); 819 } 820 } 821 822 if ( $message !== false ) { 823 return $message; 824 } 825 826 // Check the CDB cache 827 $message = $lang->getMessage( $lckey ); 828 if ( $message !== null ) { 829 return $message; 830 } 831 832 list( $fallbackChain, $siteFallbackChain ) = 833 Language::getFallbacksIncludingSiteLanguage( $langcode ); 834 835 // Next try checking the database for all of the fallback languages of the requested language. 836 if ( $useDB ) { 837 foreach ( $fallbackChain as $code ) { 838 if ( $code === $wgLanguageCode ) { 839 // Messages created in the content language will not have the /lang extension 840 $message = $this->getMsgFromNamespace( $uckey, $code ); 841 } else { 842 $message = $this->getMsgFromNamespace( "$uckey/$code", $code ); 843 } 844 845 if ( $message !== false ) { 846 // Found the message. 847 return $message; 848 } 849 } 850 } 851 852 // Now try checking the site language. 853 if ( $useDB ) { 854 $message = $this->getMsgFromNamespace( $uckey, $wgLanguageCode ); 855 if ( $message !== false ) { 856 return $message; 857 } 858 } 859 860 $message = $wgContLang->getMessage( $lckey ); 861 if ( $message !== null ) { 862 return $message; 863 } 864 865 // Finally try the DB for the site language's fallbacks. 866 if ( $useDB ) { 867 foreach ( $siteFallbackChain as $code ) { 868 $message = $this->getMsgFromNamespace( "$uckey/$code", $code ); 869 if ( $message === false && $code === $wgLanguageCode ) { 870 // Messages created in the content language will not have the /lang extension 871 $message = $this->getMsgFromNamespace( $uckey, $code ); 872 } 873 874 if ( $message !== false ) { 875 // Found the message. 876 return $message; 877 } 878 } 879 } 880 881 return false; 882 } 883 884 /** 885 * Get a message from the MediaWiki namespace, with caching. The key must 886 * first be converted to two-part lang/msg form if necessary. 887 * 888 * Unlike self::get(), this function doesn't resolve fallback chains, and 889 * some callers require this behavior. LanguageConverter::parseCachedTable() 890 * and self::get() are some examples in core. 891 * 892 * @param string $title Message cache key with initial uppercase letter. 893 * @param string $code Code denoting the language to try. 894 * @return string|bool The message, or false if it does not exist or on error 895 */ 896 function getMsgFromNamespace( $title, $code ) { 897 $this->load( $code ); 898 if ( isset( $this->mCache[$code][$title] ) ) { 899 $entry = $this->mCache[$code][$title]; 900 if ( substr( $entry, 0, 1 ) === ' ' ) { 901 // The message exists, so make sure a string 902 // is returned. 903 return (string)substr( $entry, 1 ); 904 } elseif ( $entry === '!NONEXISTENT' ) { 905 return false; 906 } elseif ( $entry === '!TOO BIG' ) { 907 // Fall through and try invididual message cache below 908 } 909 } else { 910 // XXX: This is not cached in process cache, should it? 911 $message = false; 912 wfRunHooks( 'MessagesPreLoad', array( $title, &$message ) ); 913 if ( $message !== false ) { 914 return $message; 915 } 916 917 return false; 918 } 919 920 # Try the individual message cache 921 $titleKey = wfMemcKey( 'messages', 'individual', $title ); 922 $entry = $this->mMemc->get( $titleKey ); 923 if ( $entry ) { 924 if ( substr( $entry, 0, 1 ) === ' ' ) { 925 $this->mCache[$code][$title] = $entry; 926 927 // The message exists, so make sure a string 928 // is returned. 929 return (string)substr( $entry, 1 ); 930 } elseif ( $entry === '!NONEXISTENT' ) { 931 $this->mCache[$code][$title] = '!NONEXISTENT'; 932 933 return false; 934 } else { 935 # Corrupt/obsolete entry, delete it 936 $this->mMemc->delete( $titleKey ); 937 } 938 } 939 940 # Try loading it from the database 941 $revision = Revision::newFromTitle( 942 Title::makeTitle( NS_MEDIAWIKI, $title ), false, Revision::READ_LATEST 943 ); 944 if ( $revision ) { 945 $content = $revision->getContent(); 946 if ( !$content ) { 947 // A possibly temporary loading failure. 948 wfDebugLog( 949 'MessageCache', 950 __METHOD__ . ": failed to load message page text for {$title} ($code)" 951 ); 952 $message = null; // no negative caching 953 } else { 954 // XXX: Is this the right way to turn a Content object into a message? 955 // NOTE: $content is typically either WikitextContent, JavaScriptContent or 956 // CssContent. MessageContent is *not* used for storing messages, it's 957 // only used for wrapping them when needed. 958 $message = $content->getWikitextForTransclusion(); 959 960 if ( $message === false || $message === null ) { 961 wfDebugLog( 962 'MessageCache', 963 __METHOD__ . ": message content doesn't provide wikitext " 964 . "(content model: " . $content->getContentHandler() . ")" 965 ); 966 967 $message = false; // negative caching 968 } else { 969 $this->mCache[$code][$title] = ' ' . $message; 970 $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry ); 971 } 972 } 973 } else { 974 $message = false; // negative caching 975 } 976 977 if ( $message === false ) { // negative caching 978 $this->mCache[$code][$title] = '!NONEXISTENT'; 979 $this->mMemc->set( $titleKey, '!NONEXISTENT', $this->mExpiry ); 980 } 981 982 return $message; 983 } 984 985 /** 986 * @param string $message 987 * @param bool $interface 988 * @param string $language Language code 989 * @param Title $title 990 * @return string 991 */ 992 function transform( $message, $interface = false, $language = null, $title = null ) { 993 // Avoid creating parser if nothing to transform 994 if ( strpos( $message, '{{' ) === false ) { 995 return $message; 996 } 997 998 if ( $this->mInParser ) { 999 return $message; 1000 } 1001 1002 $parser = $this->getParser(); 1003 if ( $parser ) { 1004 $popts = $this->getParserOptions(); 1005 $popts->setInterfaceMessage( $interface ); 1006 $popts->setTargetLanguage( $language ); 1007 1008 $userlang = $popts->setUserLang( $language ); 1009 $this->mInParser = true; 1010 $message = $parser->transformMsg( $message, $popts, $title ); 1011 $this->mInParser = false; 1012 $popts->setUserLang( $userlang ); 1013 } 1014 1015 return $message; 1016 } 1017 1018 /** 1019 * @return Parser 1020 */ 1021 function getParser() { 1022 global $wgParser, $wgParserConf; 1023 if ( !$this->mParser && isset( $wgParser ) ) { 1024 # Do some initialisation so that we don't have to do it twice 1025 $wgParser->firstCallInit(); 1026 # Clone it and store it 1027 $class = $wgParserConf['class']; 1028 if ( $class == 'ParserDiffTest' ) { 1029 # Uncloneable 1030 $this->mParser = new $class( $wgParserConf ); 1031 } else { 1032 $this->mParser = clone $wgParser; 1033 } 1034 } 1035 1036 return $this->mParser; 1037 } 1038 1039 /** 1040 * @param string $text 1041 * @param Title $title 1042 * @param bool $linestart Whether or not this is at the start of a line 1043 * @param bool $interface Whether this is an interface message 1044 * @param string $language Language code 1045 * @return ParserOutput|string 1046 */ 1047 public function parse( $text, $title = null, $linestart = true, 1048 $interface = false, $language = null 1049 ) { 1050 if ( $this->mInParser ) { 1051 return htmlspecialchars( $text ); 1052 } 1053 1054 $parser = $this->getParser(); 1055 $popts = $this->getParserOptions(); 1056 $popts->setInterfaceMessage( $interface ); 1057 $popts->setTargetLanguage( $language ); 1058 1059 wfProfileIn( __METHOD__ ); 1060 if ( !$title || !$title instanceof Title ) { 1061 global $wgTitle; 1062 $title = $wgTitle; 1063 } 1064 // Sometimes $wgTitle isn't set either... 1065 if ( !$title ) { 1066 # It's not uncommon having a null $wgTitle in scripts. See r80898 1067 # Create a ghost title in such case 1068 $title = Title::newFromText( 'Dwimmerlaik' ); 1069 } 1070 1071 $this->mInParser = true; 1072 $res = $parser->parse( $text, $title, $popts, $linestart ); 1073 $this->mInParser = false; 1074 1075 wfProfileOut( __METHOD__ ); 1076 1077 return $res; 1078 } 1079 1080 function disable() { 1081 $this->mDisable = true; 1082 } 1083 1084 function enable() { 1085 $this->mDisable = false; 1086 } 1087 1088 /** 1089 * Clear all stored messages. Mainly used after a mass rebuild. 1090 */ 1091 function clear() { 1092 $langs = Language::fetchLanguageNames( null, 'mw' ); 1093 foreach ( array_keys( $langs ) as $code ) { 1094 # Global cache 1095 $this->mMemc->delete( wfMemcKey( 'messages', $code ) ); 1096 # Invalidate all local caches 1097 $this->mMemc->delete( wfMemcKey( 'messages', $code, 'hash' ) ); 1098 } 1099 $this->mLoadedLanguages = array(); 1100 } 1101 1102 /** 1103 * @param string $key 1104 * @return array 1105 */ 1106 public function figureMessage( $key ) { 1107 global $wgLanguageCode; 1108 $pieces = explode( '/', $key ); 1109 if ( count( $pieces ) < 2 ) { 1110 return array( $key, $wgLanguageCode ); 1111 } 1112 1113 $lang = array_pop( $pieces ); 1114 if ( !Language::fetchLanguageName( $lang, null, 'mw' ) ) { 1115 return array( $key, $wgLanguageCode ); 1116 } 1117 1118 $message = implode( '/', $pieces ); 1119 1120 return array( $message, $lang ); 1121 } 1122 1123 /** 1124 * Get all message keys stored in the message cache for a given language. 1125 * If $code is the content language code, this will return all message keys 1126 * for which MediaWiki:msgkey exists. If $code is another language code, this 1127 * will ONLY return message keys for which MediaWiki:msgkey/$code exists. 1128 * @param string $code Language code 1129 * @return array Array of message keys (strings) 1130 */ 1131 public function getAllMessageKeys( $code ) { 1132 global $wgContLang; 1133 $this->load( $code ); 1134 if ( !isset( $this->mCache[$code] ) ) { 1135 // Apparently load() failed 1136 return null; 1137 } 1138 // Remove administrative keys 1139 $cache = $this->mCache[$code]; 1140 unset( $cache['VERSION'] ); 1141 unset( $cache['EXPIRY'] ); 1142 // Remove any !NONEXISTENT keys 1143 $cache = array_diff( $cache, array( '!NONEXISTENT' ) ); 1144 1145 // Keys may appear with a capital first letter. lcfirst them. 1146 return array_map( array( $wgContLang, 'lcfirst' ), array_keys( $cache ) ); 1147 } 1148 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |