MediaWiki  master
MessageCache.php
Go to the documentation of this file.
1 <?php
38 use Psr\Log\LoggerAwareInterface;
39 use Psr\Log\LoggerInterface;
42 use Wikimedia\RequestTimeout\TimeoutException;
43 use Wikimedia\ScopedCallback;
44 
49 define( 'MSG_CACHE_VERSION', 2 );
50 
56 class MessageCache implements LoggerAwareInterface {
60  public const CONSTRUCTOR_OPTIONS = [
61  MainConfigNames::UseDatabaseMessages,
62  MainConfigNames::MaxMsgCacheEntrySize,
63  MainConfigNames::AdaptiveMessageCache,
64  ];
65 
70  public const MAX_REQUEST_LANGUAGES = 10;
71 
72  private const FOR_UPDATE = 1; // force message reload
73 
75  private const WAIT_SEC = 15;
77  private const LOCK_TTL = 30;
78 
83  private const WAN_TTL = IExpiringStore::TTL_DAY;
84 
86  private $logger;
87 
93  private $cache;
94 
100  private $systemMessageNames;
101 
105  private $cacheVolatile = [];
106 
111  private $disable;
112 
114  private $maxEntrySize;
115 
117  private $adaptive;
118 
123  private $parserOptions;
125  private $parser;
126 
130  private $inParser = false;
131 
133  private $wanCache;
135  private $clusterCache;
137  private $srvCache;
139  private $contLang;
141  private $contLangCode;
143  private $contLangConverter;
145  private $langFactory;
147  private $localisationCache;
149  private $languageNameUtils;
151  private $languageFallback;
153  private $hookRunner;
154 
156  private $messageKeyOverrides;
157 
164  public static function normalizeKey( $key ) {
165  $lckey = strtr( $key, ' ', '_' );
166  if ( $lckey === '' ) {
167  // T300792
168  return $lckey;
169  }
170 
171  if ( ord( $lckey ) < 128 ) {
172  $lckey[0] = strtolower( $lckey[0] );
173  } else {
174  $lckey = MediaWikiServices::getInstance()->getContentLanguage()->lcfirst( $lckey );
175  }
176 
177  return $lckey;
178  }
179 
195  public function __construct(
196  WANObjectCache $wanCache,
197  BagOStuff $clusterCache,
198  BagOStuff $serverCache,
199  Language $contLang,
200  LanguageConverterFactory $langConverterFactory,
201  LoggerInterface $logger,
202  ServiceOptions $options,
203  LanguageFactory $langFactory,
204  LocalisationCache $localisationCache,
205  LanguageNameUtils $languageNameUtils,
206  LanguageFallback $languageFallback,
207  HookContainer $hookContainer
208  ) {
209  $this->wanCache = $wanCache;
210  $this->clusterCache = $clusterCache;
211  $this->srvCache = $serverCache;
212  $this->contLang = $contLang;
213  $this->contLangConverter = $langConverterFactory->getLanguageConverter( $contLang );
214  $this->contLangCode = $contLang->getCode();
215  $this->logger = $logger;
216  $this->langFactory = $langFactory;
217  $this->localisationCache = $localisationCache;
218  $this->languageNameUtils = $languageNameUtils;
219  $this->languageFallback = $languageFallback;
220  $this->hookRunner = new HookRunner( $hookContainer );
221 
222  // limit size
223  $this->cache = new MapCacheLRU( self::MAX_REQUEST_LANGUAGES );
224 
225  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
226  $this->disable = !$options->get( MainConfigNames::UseDatabaseMessages );
227  $this->maxEntrySize = $options->get( MainConfigNames::MaxMsgCacheEntrySize );
228  $this->adaptive = $options->get( MainConfigNames::AdaptiveMessageCache );
229  }
230 
231  public function setLogger( LoggerInterface $logger ) {
232  $this->logger = $logger;
233  }
234 
240  private function getParserOptions() {
241  if ( !$this->parserOptions ) {
242  $context = RequestContext::getMain();
243  $user = $context->getUser();
244  if ( !$user->isSafeToLoad() ) {
245  // It isn't safe to use the context user yet, so don't try to get a
246  // ParserOptions for it. And don't cache this ParserOptions
247  // either.
249  $po->setAllowUnsafeRawHtml( false );
250  return $po;
251  }
252 
253  $this->parserOptions = ParserOptions::newFromContext( $context );
254  // Messages may take parameters that could come
255  // from malicious sources. As a precaution, disable
256  // the <html> parser tag when parsing messages.
257  $this->parserOptions->setAllowUnsafeRawHtml( false );
258  }
259 
260  return $this->parserOptions;
261  }
262 
269  private function getLocalCache( $code ) {
270  $cacheKey = $this->srvCache->makeKey( __CLASS__, $code );
271 
272  return $this->srvCache->get( $cacheKey );
273  }
274 
281  private function saveToLocalCache( $code, $cache ) {
282  $cacheKey = $this->srvCache->makeKey( __CLASS__, $code );
283  $this->srvCache->set( $cacheKey, $cache );
284  }
285 
306  private function load( string $code, $mode = null ) {
307  // Don't do double loading...
308  if ( $this->isLanguageLoaded( $code ) && $mode !== self::FOR_UPDATE ) {
309  return true;
310  }
311 
312  // Show a log message (once) if loading is disabled
313  if ( $this->disable ) {
314  static $shownDisabled = false;
315  if ( !$shownDisabled ) {
316  $this->logger->debug( __METHOD__ . ': disabled' );
317  $shownDisabled = true;
318  }
319 
320  return true;
321  }
322 
323  try {
324  return $this->loadUnguarded( $code, $mode );
325  } catch ( Throwable $e ) {
326  // Don't try to load again during the exception handler
327  $this->disable = true;
328  throw $e;
329  }
330  }
331 
339  private function loadUnguarded( $code, $mode ) {
340  $success = false; // Keep track of success
341  $staleCache = false; // a cache array with expired data, or false if none has been loaded
342  $where = []; // Debug info, delayed to avoid spamming debug log too much
343 
344  // A hash of the expected content is stored in a WAN cache key, providing a way
345  // to invalid the local cache on every server whenever a message page changes.
346  [ $hash, $hashVolatile ] = $this->getValidationHash( $code );
347  $this->cacheVolatile[$code] = $hashVolatile;
348  $volatilityOnlyStaleness = false;
349 
350  // Try the local cache and check against the cluster hash key...
351  $cache = $this->getLocalCache( $code );
352  if ( !$cache ) {
353  $where[] = 'local cache is empty';
354  } elseif ( !isset( $cache['HASH'] ) || $cache['HASH'] !== $hash ) {
355  $where[] = 'local cache has the wrong hash';
356  $staleCache = $cache;
357  } elseif ( $this->isCacheExpired( $cache ) ) {
358  $where[] = 'local cache is expired';
359  $staleCache = $cache;
360  } elseif ( $hashVolatile ) {
361  // Some recent message page changes might not show due to DB lag
362  $where[] = 'local cache validation key is expired/volatile';
363  $staleCache = $cache;
364  $volatilityOnlyStaleness = true;
365  } else {
366  $where[] = 'got from local cache';
367  $this->cache->set( $code, $cache );
368  $success = true;
369  }
370 
371  if ( !$success ) {
372  // Try the cluster cache, using a lock for regeneration...
373  $cacheKey = $this->clusterCache->makeKey( 'messages', $code );
374  for ( $failedAttempts = 0; $failedAttempts <= 1; $failedAttempts++ ) {
375  if ( $volatilityOnlyStaleness && $staleCache ) {
376  // While the cluster cache *might* be more up-to-date, we do not want
377  // the I/O strain of every application server fetching the key here during
378  // the volatility period. Either this thread wins the lock and regenerates
379  // the cache or the stale local cache value gets reused.
380  $where[] = 'global cache is presumed expired';
381  } else {
382  $cache = $this->clusterCache->get( $cacheKey );
383  if ( !$cache ) {
384  $where[] = 'global cache is empty';
385  } elseif ( $this->isCacheExpired( $cache ) ) {
386  $where[] = 'global cache is expired';
387  $staleCache = $cache;
388  } elseif ( $hashVolatile ) {
389  // Some recent message page changes might not show due to DB lag
390  $where[] = 'global cache is expired/volatile';
391  $staleCache = $cache;
392  } else {
393  $where[] = 'got from global cache';
394  $this->cache->set( $code, $cache );
395  $this->saveToCaches( $cache, 'local-only', $code );
396  $success = true;
397  break;
398  }
399  }
400 
401  // We need to call loadFromDB(). Limit the concurrency to one thread.
402  // This prevents the site from going down when the cache expires.
403  // Note that the DB slam protection lock here is non-blocking.
404  $loadStatus = $this->loadFromDBWithMainLock( $code, $where, $mode );
405  if ( $loadStatus === true ) {
406  $success = true;
407  break;
408  } elseif ( $staleCache ) {
409  // Use the stale cache while some other thread constructs the new one
410  $where[] = 'using stale cache';
411  $this->cache->set( $code, $staleCache );
412  $success = true;
413  break;
414  } elseif ( $failedAttempts > 0 ) {
415  $where[] = 'failed to find cache after waiting';
416  // Already blocked once, so avoid another lock/unlock cycle.
417  // This case will typically be hit if memcached is down, or if
418  // loadFromDB() takes longer than LOCK_WAIT.
419  break;
420  } elseif ( $loadStatus === 'cantacquire' ) {
421  // Wait for the other thread to finish, then retry. Normally,
422  // the memcached get() will then yield the other thread's result.
423  $where[] = 'waiting for other thread to complete';
424  [ , $ioError ] = $this->getReentrantScopedLock( $code );
425  if ( $ioError ) {
426  $where[] = 'failed waiting';
427  // Call loadFromDB() with concurrency limited to one thread per server.
428  // It should be rare for all servers to lack even a stale local cache.
429  $success = $this->loadFromDBWithLocalLock( $code, $where, $mode );
430  break;
431  }
432  } else {
433  // Disable cache; $loadStatus is 'disabled'
434  break;
435  }
436  }
437  }
438 
439  if ( !$success ) {
440  $where[] = 'loading FAILED - cache is disabled';
441  $this->disable = true;
442  $this->cache->set( $code, [] );
443  $this->logger->error( __METHOD__ . ": Failed to load $code" );
444  // This used to throw an exception, but that led to nasty side effects like
445  // the whole wiki being instantly down if the memcached server died
446  }
447 
448  if ( !$this->isLanguageLoaded( $code ) ) {
449  throw new LogicException( "Process cache for '$code' should be set by now." );
450  }
451 
452  $info = implode( ', ', $where );
453  $this->logger->debug( __METHOD__ . ": Loading $code... $info" );
454 
455  return $success;
456  }
457 
464  private function loadFromDBWithMainLock( $code, array &$where, $mode = null ) {
465  // If cache updates on all levels fail, give up on message overrides.
466  // This is to avoid easy site outages; see $saveSuccess comments below.
467  $statusKey = $this->clusterCache->makeKey( 'messages', $code, 'status' );
468  $status = $this->clusterCache->get( $statusKey );
469  if ( $status === 'error' ) {
470  $where[] = "could not load; method is still globally disabled";
471  return 'disabled';
472  }
473 
474  // Now let's regenerate
475  $where[] = 'loading from DB';
476 
477  // Lock the cache to prevent conflicting writes.
478  // This lock is non-blocking so stale cache can quickly be used.
479  // Note that load() will call a blocking getReentrantScopedLock()
480  // after this if it really need to wait for any current thread.
481  [ $scopedLock ] = $this->getReentrantScopedLock( $code, 0 );
482  if ( !$scopedLock ) {
483  $where[] = 'could not acquire main lock';
484  return 'cantacquire';
485  }
486 
487  $cache = $this->loadFromDB( $code, $mode );
488  $this->cache->set( $code, $cache );
489  $saveSuccess = $this->saveToCaches( $cache, 'all', $code );
490 
491  if ( !$saveSuccess ) {
505  if ( $this->srvCache instanceof EmptyBagOStuff ) {
506  $this->clusterCache->set( $statusKey, 'error', 60 * 5 );
507  $where[] = 'could not save cache, disabled globally for 5 minutes';
508  } else {
509  $where[] = "could not save global cache";
510  }
511  }
512 
513  return true;
514  }
515 
522  private function loadFromDBWithLocalLock( $code, array &$where, $mode = null ) {
523  $success = false;
524  $where[] = 'loading from DB using local lock';
525 
526  $scopedLock = $this->srvCache->getScopedLock(
527  $this->srvCache->makeKey( 'messages', $code ),
528  self::WAIT_SEC,
529  self::LOCK_TTL,
530  __METHOD__
531  );
532  if ( $scopedLock ) {
533  $cache = $this->loadFromDB( $code, $mode );
534  $this->cache->set( $code, $cache );
535  $this->saveToCaches( $cache, 'local-only', $code );
536  $success = true;
537  }
538 
539  return $success;
540  }
541 
551  private function loadFromDB( $code, $mode = null ) {
552  $dbr = wfGetDB( ( $mode === self::FOR_UPDATE ) ? DB_PRIMARY : DB_REPLICA );
553 
554  $cache = [];
555 
556  $mostused = []; // list of "<cased message key>/<code>"
557  if ( $this->adaptive && $code !== $this->contLangCode ) {
558  if ( !$this->cache->has( $this->contLangCode ) ) {
559  $this->load( $this->contLangCode );
560  }
561  $mostused = array_keys( $this->cache->get( $this->contLangCode ) );
562  foreach ( $mostused as $key => $value ) {
563  $mostused[$key] = "$value/$code";
564  }
565  }
566 
567  // Common conditions
568  $conds = [
569  'page_is_redirect' => 0,
570  'page_namespace' => NS_MEDIAWIKI,
571  ];
572  if ( count( $mostused ) ) {
573  $conds['page_title'] = $mostused;
574  } elseif ( $code !== $this->contLangCode ) {
575  $conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), '/', $code );
576  } else {
577  // Effectively disallows use of '/' character in NS_MEDIAWIKI for uses
578  // other than language code.
579  $conds[] = 'page_title NOT' .
580  $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() );
581  }
582 
583  // Set the stubs for oversized software-defined messages in the main cache map
584  $res = $dbr->select(
585  'page',
586  [ 'page_title', 'page_latest' ],
587  array_merge( $conds, [ 'page_len > ' . intval( $this->maxEntrySize ) ] ),
588  __METHOD__ . "($code)-big"
589  );
590  foreach ( $res as $row ) {
591  // Include entries/stubs for all keys in $mostused in adaptive mode
592  if ( $this->adaptive || $this->isMainCacheable( $row->page_title ) ) {
593  $cache[$row->page_title] = '!TOO BIG';
594  }
595  // At least include revision ID so page changes are reflected in the hash
596  $cache['EXCESSIVE'][$row->page_title] = $row->page_latest;
597  }
598 
599  // Can not inject the RevisionStore as it would break the installer since
600  // it instantiates MessageCache before the DB.
601  $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
602  // Set the text for small software-defined messages in the main cache map
603  $revQuery = $revisionStore->getQueryInfo( [ 'page' ] );
604 
605  // T231196: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor` then
606  // `revision` then `page` is somehow better than starting with `page`. Tell it not to reorder the
607  // query (and also reorder it ourselves because as generated by RevisionStore it'll have
608  // `revision` first rather than `page`).
609  $revQuery['joins']['revision'] = $revQuery['joins']['page'];
610  unset( $revQuery['joins']['page'] );
611  // It isn't actually necessary to reorder $revQuery['tables'] as Database does the right thing
612  // when join conditions are given for all joins, but GergÅ‘ is wary of relying on that so pull
613  // `page` to the start.
614  $revQuery['tables'] = array_merge(
615  [ 'page' ],
616  array_diff( $revQuery['tables'], [ 'page' ] )
617  );
618 
619  $res = $dbr->select(
620  $revQuery['tables'],
621  $revQuery['fields'],
622  array_merge( $conds, [
623  'page_len <= ' . intval( $this->maxEntrySize ),
624  'page_latest = rev_id' // get the latest revision only
625  ] ),
626  __METHOD__ . "($code)-small",
627  [ 'STRAIGHT_JOIN' ],
628  $revQuery['joins']
629  );
630 
631  // Don't load content from uncacheable rows (T313004)
632  [ $cacheableRows, $uncacheableRows ] = $this->separateCacheableRows( $res );
633  $result = $revisionStore->newRevisionsFromBatch( $cacheableRows, [
634  'slots' => [ SlotRecord::MAIN ],
635  'content' => true
636  ] );
637  $revisions = $result->isOK() ? $result->getValue() : [];
638 
639  foreach ( $cacheableRows as $row ) {
640  try {
641  $rev = $revisions[$row->rev_id] ?? null;
642  $content = $rev ? $rev->getContent( SlotRecord::MAIN ) : null;
643  $text = $this->getMessageTextFromContent( $content );
644  } catch ( TimeoutException $e ) {
645  throw $e;
646  } catch ( Exception $ex ) {
647  $text = false;
648  }
649 
650  if ( !is_string( $text ) ) {
651  $entry = '!ERROR';
652  $this->logger->error(
653  __METHOD__
654  . ": failed to load message page text for {$row->page_title} ($code)"
655  );
656  } else {
657  $entry = ' ' . $text;
658  }
659  $cache[$row->page_title] = $entry;
660  }
661 
662  foreach ( $uncacheableRows as $row ) {
663  // T193271: cache object gets too big and slow to generate.
664  // At least include revision ID so page changes are reflected in the hash.
665  $cache['EXCESSIVE'][$row->page_title] = $row->page_latest;
666  }
667 
668  $cache['VERSION'] = MSG_CACHE_VERSION;
669  ksort( $cache );
670 
671  // Hash for validating local cache (APC). No need to take into account
672  // messages larger than $wgMaxMsgCacheEntrySize, since those are only
673  // stored and fetched from memcache.
674  $cache['HASH'] = md5( serialize( $cache ) );
675  $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + self::WAN_TTL );
676  unset( $cache['EXCESSIVE'] ); // only needed for hash
677 
678  return $cache;
679  }
680 
687  private function isLanguageLoaded( $lang ) {
688  // It is important that this only returns true if the cache was fully
689  // populated by load(), so that callers can assume all cache keys exist.
690  // It is possible for $this->cache to be only partially populated through
691  // methods like MessageCache::replace(), which must not make this method
692  // return true (T208897). And this method must cease to return true
693  // if the language was evicted by MapCacheLRU (T230690).
694  return $this->cache->hasField( $lang, 'VERSION' );
695  }
696 
708  private function isMainCacheable( $name, $code = null ) {
709  // Convert first letter to lowercase, and strip /code suffix
710  $name = $this->contLang->lcfirst( $name );
711  // Include common conversion table pages. This also avoids problems with
712  // Installer::parse() bailing out due to disallowed DB queries (T207979).
713  if ( strpos( $name, 'conversiontable/' ) === 0 ) {
714  return true;
715  }
716  $msg = preg_replace( '/\/[a-z0-9-]{2,}$/', '', $name );
717 
718  if ( $code === null ) {
719  // Bulk load
720  if ( $this->systemMessageNames === null ) {
721  $this->systemMessageNames = array_fill_keys(
722  $this->localisationCache->getSubitemList( $this->contLangCode, 'messages' ),
723  true );
724  }
725  return isset( $this->systemMessageNames[$msg] );
726  } else {
727  // Use individual subitem
728  return $this->localisationCache->getSubitem( $code, 'messages', $msg ) !== null;
729  }
730  }
731 
739  private function separateCacheableRows( $res ) {
740  if ( $this->adaptive ) {
741  // Include entries/stubs for all keys in $mostused in adaptive mode
742  return [ $res, [] ];
743  }
744  $cacheableRows = [];
745  $uncacheableRows = [];
746  foreach ( $res as $row ) {
747  if ( $this->isMainCacheable( $row->page_title ) ) {
748  $cacheableRows[] = $row;
749  } else {
750  $uncacheableRows[] = $row;
751  }
752  }
753  return [ $cacheableRows, $uncacheableRows ];
754  }
755 
762  public function replace( $title, $text ) {
763  if ( $this->disable ) {
764  return;
765  }
766 
767  [ $msg, $code ] = $this->figureMessage( $title );
768  if ( strpos( $title, '/' ) !== false && $code === $this->contLangCode ) {
769  // Content language overrides do not use the /<code> suffix
770  return;
771  }
772 
773  // (a) Update the process cache with the new message text
774  if ( $text === false ) {
775  // Page deleted
776  $this->cache->setField( $code, $title, '!NONEXISTENT' );
777  } else {
778  // Ignore $wgMaxMsgCacheEntrySize so the process cache is up to date
779  $this->cache->setField( $code, $title, ' ' . $text );
780  }
781 
782  // (b) Update the shared caches in a deferred update with a fresh DB snapshot
784  new MessageCacheUpdate( $code, $title, $msg ),
785  DeferredUpdates::PRESEND
786  );
787  }
788 
793  public function refreshAndReplaceInternal( string $code, array $replacements ) {
794  // Allow one caller at a time to avoid race conditions
795  [ $scopedLock ] = $this->getReentrantScopedLock( $code );
796  if ( !$scopedLock ) {
797  foreach ( $replacements as [ $title ] ) {
798  $this->logger->error(
799  __METHOD__ . ': could not acquire lock to update {title} ({code})',
800  [ 'title' => $title, 'code' => $code ] );
801  }
802 
803  return;
804  }
805 
806  // Load the existing cache to update it in the local DC cache.
807  // The other DCs will see a hash mismatch.
808  if ( $this->load( $code, self::FOR_UPDATE ) ) {
809  $cache = $this->cache->get( $code );
810  } else {
811  // Err? Fall back to loading from the database.
812  $cache = $this->loadFromDB( $code, self::FOR_UPDATE );
813  }
814  // Check if individual cache keys should exist and update cache accordingly
815  $newTextByTitle = []; // map of (title => content)
816  $newBigTitles = []; // map of (title => latest revision ID), like EXCESSIVE in loadFromDB()
817  // Can not inject the WikiPageFactory as it would break the installer since
818  // it instantiates MessageCache before the DB.
819  $wikiPageFactory = MediaWikiServices::getInstance()->getWikiPageFactory();
820  foreach ( $replacements as [ $title ] ) {
821  $page = $wikiPageFactory->newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) );
822  $page->loadPageData( $page::READ_LATEST );
823  $text = $this->getMessageTextFromContent( $page->getContent() );
824  // Remember the text for the blob store update later on
825  $newTextByTitle[$title] = $text ?? '';
826  // Note that if $text is false, then $cache should have a !NONEXISTANT entry
827  if ( !is_string( $text ) ) {
828  $cache[$title] = '!NONEXISTENT';
829  } elseif ( strlen( $text ) > $this->maxEntrySize ) {
830  $cache[$title] = '!TOO BIG';
831  $newBigTitles[$title] = $page->getLatest();
832  } else {
833  $cache[$title] = ' ' . $text;
834  }
835  }
836  // Update HASH for the new key. Incorporates various administrative keys,
837  // including the old HASH (and thereby the EXCESSIVE value from loadFromDB()
838  // and previous replace() calls), but that doesn't really matter since we
839  // only ever compare it for equality with a copy saved by saveToCaches().
840  $cache['HASH'] = md5( serialize( $cache + [ 'EXCESSIVE' => $newBigTitles ] ) );
841  // Update the too-big WAN cache entries now that we have the new HASH
842  foreach ( $newBigTitles as $title => $id ) {
843  // Match logic of loadCachedMessagePageEntry()
844  $this->wanCache->set(
845  $this->bigMessageCacheKey( $cache['HASH'], $title ),
846  ' ' . $newTextByTitle[$title],
847  self::WAN_TTL
848  );
849  }
850  // Mark this cache as definitely being "latest" (non-volatile) so
851  // load() calls do not try to refresh the cache with replica DB data
852  $cache['LATEST'] = time();
853  // Update the process cache
854  $this->cache->set( $code, $cache );
855  // Pre-emptively update the local datacenter cache so things like edit filter and
856  // prevented changes are reflected immediately; these often use MediaWiki: pages.
857  // The datacenter handling replace() calls should be the same one handling edits
858  // as they require HTTP POST.
859  $this->saveToCaches( $cache, 'all', $code );
860  // Release the lock now that the cache is saved
861  ScopedCallback::consume( $scopedLock );
862 
863  // Relay the purge. Touching this check key expires cache contents
864  // and local cache (APC) validation hash across all datacenters.
865  $this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
866 
867  // Purge the messages in the message blob store and fire any hook handlers
868  $blobStore = MediaWikiServices::getInstance()->getResourceLoader()->getMessageBlobStore();
869  foreach ( $replacements as [ $title, $msg ] ) {
870  $blobStore->updateMessage( $this->contLang->lcfirst( $msg ) );
871  $this->hookRunner->onMessageCacheReplace( $title, $newTextByTitle[$title] );
872  }
873  }
874 
881  private function isCacheExpired( $cache ) {
882  if ( !isset( $cache['VERSION'] ) || !isset( $cache['EXPIRY'] ) ) {
883  return true;
884  }
885  if ( $cache['VERSION'] !== MSG_CACHE_VERSION ) {
886  return true;
887  }
888  if ( wfTimestampNow() >= $cache['EXPIRY'] ) {
889  return true;
890  }
891 
892  return false;
893  }
894 
904  private function saveToCaches( array $cache, $dest, $code = false ) {
905  if ( $dest === 'all' ) {
906  $cacheKey = $this->clusterCache->makeKey( 'messages', $code );
907  $success = $this->clusterCache->set( $cacheKey, $cache );
908  $this->setValidationHash( $code, $cache );
909  } else {
910  $success = true;
911  }
912 
913  $this->saveToLocalCache( $code, $cache );
914 
915  return $success;
916  }
917 
924  private function getValidationHash( $code ) {
925  $curTTL = null;
926  $value = $this->wanCache->get(
927  $this->wanCache->makeKey( 'messages', $code, 'hash', 'v1' ),
928  $curTTL,
929  [ $this->getCheckKey( $code ) ]
930  );
931 
932  if ( $value ) {
933  $hash = $value['hash'];
934  if ( ( time() - $value['latest'] ) < WANObjectCache::TTL_MINUTE ) {
935  // Cache was recently updated via replace() and should be up-to-date.
936  // That method is only called in the primary datacenter and uses FOR_UPDATE.
937  $expired = false;
938  } else {
939  // See if the "check" key was bumped after the hash was generated
940  $expired = ( $curTTL < 0 );
941  }
942  } else {
943  // No hash found at all; cache must regenerate to be safe
944  $hash = false;
945  $expired = true;
946  }
947 
948  return [ $hash, $expired ];
949  }
950 
961  private function setValidationHash( $code, array $cache ) {
962  $this->wanCache->set(
963  $this->wanCache->makeKey( 'messages', $code, 'hash', 'v1' ),
964  [
965  'hash' => $cache['HASH'],
966  'latest' => $cache['LATEST'] ?? 0
967  ],
968  WANObjectCache::TTL_INDEFINITE
969  );
970  }
971 
978  private function getReentrantScopedLock( $code, $timeout = self::WAIT_SEC ) {
979  $key = $this->clusterCache->makeKey( 'messages', $code );
980 
981  $watchPoint = $this->clusterCache->watchErrors();
982  $scopedLock = $this->clusterCache->getScopedLock(
983  $key,
984  $timeout,
985  self::LOCK_TTL,
986  __METHOD__
987  );
988  $error = ( !$scopedLock && $this->clusterCache->getLastError( $watchPoint ) );
989 
990  return [ $scopedLock, $error ];
991  }
992 
1025  public function get( $key, $useDB = true, $langcode = true ) {
1026  if ( is_int( $key ) ) {
1027  // Fix numerical strings that somehow become ints on their way here
1028  $key = (string)$key;
1029  } elseif ( !is_string( $key ) ) {
1030  throw new TypeError( 'Message key must be a string' );
1031  } elseif ( $key === '' ) {
1032  // Shortcut: the empty key is always missing
1033  return false;
1034  }
1035 
1036  $language = $this->getLanguageObject( $langcode );
1037 
1038  // Normalise title-case input (with some inlining)
1039  $lckey = self::normalizeKey( $key );
1040 
1041  // Initialize the overrides here to prevent calling the hook too early.
1042  if ( $this->messageKeyOverrides === null ) {
1043  $this->messageKeyOverrides = [];
1044  $this->hookRunner->onMessageCacheFetchOverrides( $this->messageKeyOverrides );
1045  }
1046 
1047  if ( isset( $this->messageKeyOverrides[$lckey] ) ) {
1048  $override = $this->messageKeyOverrides[$lckey];
1049 
1050  // Strings are deliberately interpreted as message keys,
1051  // to prevent ambiguity between message keys and functions.
1052  if ( is_string( $override ) ) {
1053  $lckey = $override;
1054  } else {
1055  $lckey = $override( $lckey, $this, $language, $useDB );
1056  }
1057  }
1058 
1059  $this->hookRunner->onMessageCache__get( $lckey );
1060 
1061  // Loop through each language in the fallback list until we find something useful
1062  $message = $this->getMessageFromFallbackChain(
1063  $language,
1064  $lckey,
1065  !$this->disable && $useDB
1066  );
1067 
1068  // If we still have no message, maybe the key was in fact a full key so try that
1069  if ( $message === false ) {
1070  $parts = explode( '/', $lckey );
1071  // We may get calls for things that are http-urls from sidebar
1072  // Let's not load nonexistent languages for those
1073  // They usually have more than one slash.
1074  if ( count( $parts ) === 2 && $parts[1] !== '' ) {
1075  $message = $this->localisationCache->getSubitem( $parts[1], 'messages', $parts[0] ) ?? false;
1076  }
1077  }
1078 
1079  // Post-processing if the message exists
1080  if ( $message !== false ) {
1081  // Fix whitespace
1082  $message = str_replace(
1083  [
1084  // Fix for trailing whitespace, removed by textarea
1085  '&#32;',
1086  // Fix for NBSP, converted to space by firefox
1087  '&nbsp;',
1088  '&#160;',
1089  '&shy;'
1090  ],
1091  [
1092  ' ',
1093  "\u{00A0}",
1094  "\u{00A0}",
1095  "\u{00AD}"
1096  ],
1097  $message
1098  );
1099  }
1100 
1101  return $message;
1102  }
1103 
1119  private function getLanguageObject( $langcode ) {
1120  # Identify which language to get or create a language object for.
1121  # Using is_object here due to Stub objects.
1122  if ( is_object( $langcode ) ) {
1123  # Great, we already have the object (hopefully)!
1124  return $langcode;
1125  }
1126 
1127  if ( $langcode === true || $langcode === $this->contLangCode ) {
1128  # $langcode is the language code of the wikis content language object.
1129  # or it is a boolean and value is true
1130  return $this->contLang;
1131  }
1132 
1133  global $wgLang;
1134  if ( $langcode === false || $langcode === $wgLang->getCode() ) {
1135  # $langcode is the language code of user language object.
1136  # or it was a boolean and value is false
1137  return $wgLang;
1138  }
1139 
1140  $validCodes = array_keys( $this->languageNameUtils->getLanguageNames() );
1141  if ( in_array( $langcode, $validCodes ) ) {
1142  # $langcode corresponds to a valid language.
1143  return $this->langFactory->getLanguage( $langcode );
1144  }
1145 
1146  # $langcode is a string, but not a valid language code; use content language.
1147  $this->logger->debug( 'Invalid language code passed to' . __METHOD__ . ', falling back to content language.' );
1148  return $this->contLang;
1149  }
1150 
1163  private function getMessageFromFallbackChain( $lang, $lckey, $useDB ) {
1164  $alreadyTried = [];
1165 
1166  // First try the requested language.
1167  $message = $this->getMessageForLang( $lang, $lckey, $useDB, $alreadyTried );
1168  if ( $message !== false ) {
1169  return $message;
1170  }
1171 
1172  // Now try checking the site language.
1173  $message = $this->getMessageForLang( $this->contLang, $lckey, $useDB, $alreadyTried );
1174  return $message;
1175  }
1176 
1187  private function getMessageForLang( $lang, $lckey, $useDB, &$alreadyTried ) {
1188  $langcode = $lang->getCode();
1189 
1190  // Try checking the database for the requested language
1191  if ( $useDB ) {
1192  $uckey = $this->contLang->ucfirst( $lckey );
1193 
1194  if ( !isset( $alreadyTried[$langcode] ) ) {
1195  $message = $this->getMsgFromNamespace(
1196  $this->getMessagePageName( $langcode, $uckey ),
1197  $langcode
1198  );
1199  if ( $message !== false ) {
1200  return $message;
1201  }
1202  $alreadyTried[$langcode] = true;
1203  }
1204  } else {
1205  $uckey = null;
1206  }
1207 
1208  // Check the CDB cache
1209  $message = $lang->getMessage( $lckey );
1210  if ( $message !== null ) {
1211  return $message;
1212  }
1213 
1214  // Try checking the database for all of the fallback languages
1215  if ( $useDB ) {
1216  $fallbackChain = $this->languageFallback->getAll( $langcode );
1217 
1218  foreach ( $fallbackChain as $code ) {
1219  if ( isset( $alreadyTried[$code] ) ) {
1220  continue;
1221  }
1222 
1223  $message = $this->getMsgFromNamespace(
1224  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable uckey is set when used
1225  $this->getMessagePageName( $code, $uckey ), $code );
1226 
1227  if ( $message !== false ) {
1228  return $message;
1229  }
1230  $alreadyTried[$code] = true;
1231  }
1232  }
1233 
1234  return false;
1235  }
1236 
1244  private function getMessagePageName( $langcode, $uckey ) {
1245  if ( $langcode === $this->contLangCode ) {
1246  // Messages created in the content language will not have the /lang extension
1247  return $uckey;
1248  } else {
1249  return "$uckey/$langcode";
1250  }
1251  }
1252 
1265  public function getMsgFromNamespace( $title, $code ) {
1266  // Load all MediaWiki page definitions into cache. Note that individual keys
1267  // already loaded into cache during this request remain in the cache, which
1268  // includes the value of hook-defined messages.
1269  $this->load( $code );
1270 
1271  $entry = $this->cache->getField( $code, $title );
1272 
1273  if ( $entry !== null ) {
1274  // Message page exists as an override of a software messages
1275  if ( substr( $entry, 0, 1 ) === ' ' ) {
1276  // The message exists and is not '!TOO BIG' or '!ERROR'
1277  return (string)substr( $entry, 1 );
1278  } elseif ( $entry === '!NONEXISTENT' ) {
1279  // The text might be '-' or missing due to some data loss
1280  return false;
1281  }
1282  // Load the message page, utilizing the individual message cache.
1283  // If the page does not exist, there will be no hook handler fallbacks.
1284  $entry = $this->loadCachedMessagePageEntry(
1285  $title,
1286  $code,
1287  $this->cache->getField( $code, 'HASH' )
1288  );
1289  } else {
1290  // Message page either does not exist or does not override a software message
1291  if ( !$this->isMainCacheable( $title, $code ) ) {
1292  // Message page does not override any software-defined message. A custom
1293  // message might be defined to have content or settings specific to the wiki.
1294  // Load the message page, utilizing the individual message cache as needed.
1295  $entry = $this->loadCachedMessagePageEntry(
1296  $title,
1297  $code,
1298  $this->cache->getField( $code, 'HASH' )
1299  );
1300  }
1301  if ( $entry === null || substr( $entry, 0, 1 ) !== ' ' ) {
1302  // Message does not have a MediaWiki page definition; try hook handlers
1303  $message = false;
1304  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
1305  $this->hookRunner->onMessagesPreLoad( $title, $message, $code );
1306  if ( $message !== false ) {
1307  $this->cache->setField( $code, $title, ' ' . $message );
1308  } else {
1309  $this->cache->setField( $code, $title, '!NONEXISTENT' );
1310  }
1311 
1312  return $message;
1313  }
1314  }
1315 
1316  if ( $entry !== false && substr( $entry, 0, 1 ) === ' ' ) {
1317  if ( $this->cacheVolatile[$code] ) {
1318  // Make sure that individual keys respect the WAN cache holdoff period too
1319  $this->logger->debug(
1320  __METHOD__ . ': loading volatile key \'{titleKey}\'',
1321  [ 'titleKey' => $title, 'code' => $code ] );
1322  } else {
1323  $this->cache->setField( $code, $title, $entry );
1324  }
1325  // The message exists, so make sure a string is returned
1326  return (string)substr( $entry, 1 );
1327  }
1328 
1329  $this->cache->setField( $code, $title, '!NONEXISTENT' );
1330 
1331  return false;
1332  }
1333 
1340  private function loadCachedMessagePageEntry( $dbKey, $code, $hash ) {
1341  $fname = __METHOD__;
1342  return $this->srvCache->getWithSetCallback(
1343  $this->srvCache->makeKey( 'messages-big', $hash, $dbKey ),
1344  BagOStuff::TTL_HOUR,
1345  function () use ( $code, $dbKey, $hash, $fname ) {
1346  return $this->wanCache->getWithSetCallback(
1347  $this->bigMessageCacheKey( $hash, $dbKey ),
1348  self::WAN_TTL,
1349  function ( $oldValue, &$ttl, &$setOpts ) use ( $dbKey, $code, $fname ) {
1350  // Try loading the message from the database
1351  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1352  // Use newKnownCurrent() to avoid querying revision/user tables
1353  $title = Title::makeTitle( NS_MEDIAWIKI, $dbKey );
1354  // Injecting RevisionStore breaks installer since it
1355  // instantiates MessageCache before DB.
1356  $revision = MediaWikiServices::getInstance()
1357  ->getRevisionLookup()
1358  ->getKnownCurrentRevision( $title );
1359  if ( !$revision ) {
1360  // The wiki doesn't have a local override page. Cache absence with normal TTL.
1361  // When overrides are created, self::replace() takes care of the cache.
1362  return '!NONEXISTENT';
1363  }
1364  $content = $revision->getContent( SlotRecord::MAIN );
1365  if ( $content ) {
1366  $message = $this->getMessageTextFromContent( $content );
1367  } else {
1368  $this->logger->warning(
1369  $fname . ': failed to load page text for \'{titleKey}\'',
1370  [ 'titleKey' => $dbKey, 'code' => $code ]
1371  );
1372  $message = null;
1373  }
1374 
1375  if ( !is_string( $message ) ) {
1376  // Revision failed to load Content, or Content is incompatible with wikitext.
1377  // Possibly a temporary loading failure.
1378  $ttl = 5;
1379 
1380  return '!NONEXISTENT';
1381  }
1382 
1383  return ' ' . $message;
1384  }
1385  );
1386  }
1387  );
1388  }
1389 
1397  public function transform( $message, $interface = false, $language = null, PageReference $page = null ) {
1398  // Avoid creating parser if nothing to transform
1399  if ( strpos( $message, '{{' ) === false ) {
1400  return $message;
1401  }
1402 
1403  if ( $this->inParser ) {
1404  return $message;
1405  }
1406 
1407  $parser = $this->getParser();
1408  if ( $parser ) {
1409  $popts = $this->getParserOptions();
1410  $popts->setInterfaceMessage( $interface );
1411  $popts->setTargetLanguage( $language );
1412 
1413  $userlang = $popts->setUserLang( $language );
1414  $this->inParser = true;
1415  $message = $parser->transformMsg( $message, $popts, $page );
1416  $this->inParser = false;
1417  $popts->setUserLang( $userlang );
1418  }
1419 
1420  return $message;
1421  }
1422 
1426  public function getParser() {
1427  if ( !$this->parser ) {
1428  $parser = MediaWikiServices::getInstance()->getParser();
1429  // Clone it and store it
1430  $this->parser = clone $parser;
1431  }
1432 
1433  return $this->parser;
1434  }
1435 
1444  public function parse( $text, PageReference $page = null, $linestart = true,
1445  $interface = false, $language = null
1446  ) {
1447  global $wgTitle;
1448 
1449  if ( $this->inParser ) {
1450  return htmlspecialchars( $text );
1451  }
1452 
1453  $parser = $this->getParser();
1454  $popts = $this->getParserOptions();
1455  $popts->setInterfaceMessage( $interface );
1456 
1457  if ( is_string( $language ) ) {
1458  $language = $this->langFactory->getLanguage( $language );
1459  }
1460  $popts->setTargetLanguage( $language );
1461 
1462  if ( !$page ) {
1463  $logger = LoggerFactory::getInstance( 'GlobalTitleFail' );
1464  $logger->info(
1465  __METHOD__ . ' called with no title set.',
1466  [ 'exception' => new Exception ]
1467  );
1468  $page = $wgTitle;
1469  }
1470  // Sometimes $wgTitle isn't set either...
1471  if ( !$page ) {
1472  // It's not uncommon having a null $wgTitle in scripts. See r80898
1473  // Create a ghost title in such case
1474  $page = PageReferenceValue::localReference(
1475  NS_SPECIAL,
1476  'Badtitle/title not set in ' . __METHOD__
1477  );
1478  }
1479 
1480  $this->inParser = true;
1481  $res = $parser->parse( $text, $page, $popts, $linestart );
1482  $this->inParser = false;
1483 
1484  return $res;
1485  }
1486 
1487  public function disable() {
1488  $this->disable = true;
1489  }
1490 
1491  public function enable() {
1492  $this->disable = false;
1493  }
1494 
1507  public function isDisabled() {
1508  return $this->disable;
1509  }
1510 
1516  public function clear() {
1517  $langs = $this->languageNameUtils->getLanguageNames();
1518  foreach ( array_keys( $langs ) as $code ) {
1519  $this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
1520  }
1521  $this->cache->clear();
1522  }
1523 
1528  public function figureMessage( $key ) {
1529  $pieces = explode( '/', $key );
1530  if ( count( $pieces ) < 2 ) {
1531  return [ $key, $this->contLangCode ];
1532  }
1533 
1534  $lang = array_pop( $pieces );
1535  if ( !$this->languageNameUtils->getLanguageName(
1536  $lang,
1537  LanguageNameUtils::AUTONYMS,
1538  LanguageNameUtils::DEFINED
1539  ) ) {
1540  return [ $key, $this->contLangCode ];
1541  }
1542 
1543  $message = implode( '/', $pieces );
1544 
1545  return [ $message, $lang ];
1546  }
1547 
1556  public function getAllMessageKeys( $code ) {
1557  $this->load( $code );
1558  if ( !$this->cache->has( $code ) ) {
1559  // Apparently load() failed
1560  return null;
1561  }
1562  // Remove administrative keys
1563  $cache = $this->cache->get( $code );
1564  unset( $cache['VERSION'] );
1565  unset( $cache['EXPIRY'] );
1566  unset( $cache['EXCESSIVE'] );
1567  // Remove any !NONEXISTENT keys
1568  $cache = array_diff( $cache, [ '!NONEXISTENT' ] );
1569 
1570  // Keys may appear with a capital first letter. lcfirst them.
1571  return array_map( [ $this->contLang, 'lcfirst' ], array_keys( $cache ) );
1572  }
1573 
1581  public function updateMessageOverride( LinkTarget $linkTarget, Content $content = null ) {
1582  // treat null as not existing
1583  $msgText = $this->getMessageTextFromContent( $content ) ?? false;
1584 
1585  $this->replace( $linkTarget->getDBkey(), $msgText );
1586 
1587  if ( $this->contLangConverter->hasVariants() ) {
1588  $this->contLangConverter->updateConversionTable( $linkTarget );
1589  }
1590  }
1591 
1596  public function getCheckKey( $code ) {
1597  return $this->wanCache->makeKey( 'messages', $code );
1598  }
1599 
1604  private function getMessageTextFromContent( Content $content = null ) {
1605  // @TODO: could skip pseudo-messages like js/css here, based on content model
1606  if ( $content ) {
1607  // Message page exists...
1608  // XXX: Is this the right way to turn a Content object into a message?
1609  // NOTE: $content is typically either WikitextContent, JavaScriptContent or
1610  // CssContent. MessageContent is *not* used for storing messages, it's
1611  // only used for wrapping them when needed.
1612  $msgText = $content->getWikitextForTransclusion();
1613  if ( $msgText === false || $msgText === null ) {
1614  // This might be due to some kind of misconfiguration...
1615  $msgText = null;
1616  $this->logger->warning(
1617  __METHOD__ . ": message content doesn't provide wikitext "
1618  . "(content model: " . $content->getModel() . ")" );
1619  }
1620  } else {
1621  // Message page does not exist...
1622  $msgText = false;
1623  }
1624 
1625  return $msgText;
1626  }
1627 
1633  private function bigMessageCacheKey( $hash, $title ) {
1634  return $this->wanCache->makeKey( 'messages-big', $hash, $title );
1635  }
1636 }
const NS_MEDIAWIKI
Definition: Defines.php:72
const NS_SPECIAL
Definition: Defines.php:53
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
const MSG_CACHE_VERSION
MediaWiki message cache structure version.
$success
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgLang
Definition: Setup.php:529
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgTitle
Definition: Setup.php:529
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:85
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the pending update queue for execution at the appropriate time.
A BagOStuff object with no objects in it.
Base class for language-specific code.
Definition: Language.php:57
getCode()
Get the internal language code for this language object.
Definition: Language.php:4338
Caching for the contents of localisation files.
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:36
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:566
An interface for creating language converters.
getLanguageConverter( $language=null)
Provide a LanguageConverter for given language.
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
A service that provides utilities to do with language names and codes.
PSR-3 logger instance factory.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Immutable value object representing a page reference.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
Class to implement stub globals, which are globals that delay loading the their associated module cod...
Definition: StubObject.php:55
Stub object for the user language.
Represents a title within MediaWiki.
Definition: Title.php:82
Message cache purging and in-place update handler for specific message page changes.
Cache messages that are defined by MediaWiki-namespace pages or by hooks.
refreshAndReplaceInternal(string $code, array $replacements)
const MAX_REQUEST_LANGUAGES
The size of the MapCacheLRU which stores message data.
getCheckKey( $code)
__construct(WANObjectCache $wanCache, BagOStuff $clusterCache, BagOStuff $serverCache, Language $contLang, LanguageConverterFactory $langConverterFactory, LoggerInterface $logger, ServiceOptions $options, LanguageFactory $langFactory, LocalisationCache $localisationCache, LanguageNameUtils $languageNameUtils, LanguageFallback $languageFallback, HookContainer $hookContainer)
getMsgFromNamespace( $title, $code)
Get a message from the MediaWiki namespace, with caching.
parse( $text, PageReference $page=null, $linestart=true, $interface=false, $language=null)
transform( $message, $interface=false, $language=null, PageReference $page=null)
updateMessageOverride(LinkTarget $linkTarget, Content $content=null)
Purge message caches when a MediaWiki: page is created, updated, or deleted.
const CONSTRUCTOR_OPTIONS
Options to be included in the ServiceOptions.
isDisabled()
Whether DB/cache usage is disabled for determining messages.
setLogger(LoggerInterface $logger)
clear()
Clear all stored messages in global and local cache.
getAllMessageKeys( $code)
Get all message keys stored in the message cache for a given language.
static normalizeKey( $key)
Normalize message key input.
figureMessage( $key)
replace( $title, $text)
Updates cache as necessary when message page is changed.
static newFromContext(IContextSource $context)
Get a ParserOptions object from a IContextSource object.
static newFromAnon()
Get a ParserOptions object for an anonymous user.
parse( $text, PageReference $page, ParserOptions $options, $linestart=true, $clearState=true, $revid=null)
Convert wikitext to HTML Do not call this function recursively.
Definition: Parser.php:668
transformMsg( $text, ParserOptions $options, ?PageReference $page=null)
Wrapper for preprocess()
Definition: Parser.php:4864
static getMain()
Get the RequestContext object associated with the main request.
Multi-datacenter aware caching interface.
Base interface for representing page content.
Definition: Content.php:37
getDBkey()
Get the main part of the link target, in canonical database form.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
Result wrapper for grabbing data queried from an IDatabase object.
const DB_REPLICA
Definition: defines.php:26
const DB_PRIMARY
Definition: defines.php:28
$content
Definition: router.php:76
return true
Definition: router.php:90
if(!isset( $args[0])) $lang
$revQuery