[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/cache/ -> MessageCache.php (source)

   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                      '&#32;',
 777                      # Fix for NBSP, converted to space by firefox
 778                      '&nbsp;',
 779                      '&#160;',
 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  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1