MediaWiki master
MessageCache.php
Go to the documentation of this file.
1<?php
48use Psr\Log\LoggerAwareInterface;
49use Psr\Log\LoggerInterface;
58use Wikimedia\RequestTimeout\TimeoutException;
59use Wikimedia\ScopedCallback;
60
65define( 'MSG_CACHE_VERSION', 2 );
66
72class MessageCache implements LoggerAwareInterface {
76 public const CONSTRUCTOR_OPTIONS = [
77 MainConfigNames::UseDatabaseMessages,
78 MainConfigNames::MaxMsgCacheEntrySize,
79 MainConfigNames::AdaptiveMessageCache,
80 MainConfigNames::UseXssLanguage,
81 MainConfigNames::RawHtmlMessages,
82 ];
83
88 public const MAX_REQUEST_LANGUAGES = 10;
89
90 private const FOR_UPDATE = 1; // force message reload
91
93 private const WAIT_SEC = 15;
95 private const LOCK_TTL = 30;
96
100 private const WAN_TTL = BagOStuff::TTL_DAY;
101
103 private $logger;
104
110 private $cache;
111
117 private $systemMessageNames;
118
122 private $cacheVolatile = [];
123
128 private $disable;
129
131 private $maxEntrySize;
132
134 private $adaptive;
135
137 private $useXssLanguage;
138
140 private $rawHtmlMessages;
141
146 private $parserOptions;
147
149 private array $parsers = [];
150 private int $curParser = -1;
151
157 private const MAX_PARSER_DEPTH = 5;
158
160 private $wanCache;
162 private $clusterCache;
164 private $srvCache;
166 private $contLang;
168 private $contLangCode;
170 private $contLangConverter;
172 private $langFactory;
174 private $localisationCache;
176 private $languageNameUtils;
178 private $languageFallback;
180 private $hookRunner;
182 private $parserFactory;
183
185 private $messageKeyOverrides;
186
193 public static function normalizeKey( $key ) {
194 $lckey = strtr( $key, ' ', '_' );
195 if ( $lckey === '' ) {
196 // T300792
197 return $lckey;
198 }
199
200 if ( ord( $lckey ) < 128 ) {
201 $lckey[0] = strtolower( $lckey[0] );
202 } else {
203 $lckey = MediaWikiServices::getInstance()->getContentLanguage()->lcfirst( $lckey );
204 }
205
206 return $lckey;
207 }
208
225 public function __construct(
226 WANObjectCache $wanCache,
227 BagOStuff $clusterCache,
228 BagOStuff $serverCache,
229 Language $contLang,
230 LanguageConverterFactory $langConverterFactory,
231 LoggerInterface $logger,
232 ServiceOptions $options,
233 LanguageFactory $langFactory,
234 LocalisationCache $localisationCache,
235 LanguageNameUtils $languageNameUtils,
236 LanguageFallback $languageFallback,
237 HookContainer $hookContainer,
238 ParserFactory $parserFactory
239 ) {
240 $this->wanCache = $wanCache;
241 $this->clusterCache = $clusterCache;
242 $this->srvCache = $serverCache;
243 $this->contLang = $contLang;
244 $this->contLangConverter = $langConverterFactory->getLanguageConverter( $contLang );
245 $this->contLangCode = $contLang->getCode();
246 $this->logger = $logger;
247 $this->langFactory = $langFactory;
248 $this->localisationCache = $localisationCache;
249 $this->languageNameUtils = $languageNameUtils;
250 $this->languageFallback = $languageFallback;
251 $this->hookRunner = new HookRunner( $hookContainer );
252 $this->parserFactory = $parserFactory;
253
254 // limit size
255 $this->cache = new MapCacheLRU( self::MAX_REQUEST_LANGUAGES );
256
257 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
258 $this->disable = !$options->get( MainConfigNames::UseDatabaseMessages );
259 $this->maxEntrySize = $options->get( MainConfigNames::MaxMsgCacheEntrySize );
260 $this->adaptive = $options->get( MainConfigNames::AdaptiveMessageCache );
261 $this->useXssLanguage = $options->get( MainConfigNames::UseXssLanguage );
262 $this->rawHtmlMessages = $options->get( MainConfigNames::RawHtmlMessages );
263 }
264
265 public function setLogger( LoggerInterface $logger ) {
266 $this->logger = $logger;
267 }
268
274 private function getParserOptions() {
275 if ( !$this->parserOptions ) {
276 $context = RequestContext::getMain();
277 $user = $context->getUser();
278 if ( !$user->isSafeToLoad() ) {
279 // It isn't safe to use the context user yet, so don't try to get a
280 // ParserOptions for it. And don't cache this ParserOptions
281 // either.
282 $po = ParserOptions::newFromAnon();
283 $po->setAllowUnsafeRawHtml( false );
284 return $po;
285 }
286
287 $this->parserOptions = ParserOptions::newFromContext( $context );
288 // Messages may take parameters that could come
289 // from malicious sources. As a precaution, disable
290 // the <html> parser tag when parsing messages.
291 $this->parserOptions->setAllowUnsafeRawHtml( false );
292 }
293
294 return $this->parserOptions;
295 }
296
303 private function getLocalCache( $code ) {
304 $cacheKey = $this->srvCache->makeKey( __CLASS__, $code );
305
306 return $this->srvCache->get( $cacheKey );
307 }
308
315 private function saveToLocalCache( $code, $cache ) {
316 $cacheKey = $this->srvCache->makeKey( __CLASS__, $code );
317 $this->srvCache->set( $cacheKey, $cache );
318 }
319
340 private function load( string $code, $mode = null ) {
341 // Don't do double loading...
342 if ( $this->isLanguageLoaded( $code ) && $mode !== self::FOR_UPDATE ) {
343 return true;
344 }
345
346 // Show a log message (once) if loading is disabled
347 if ( $this->disable ) {
348 static $shownDisabled = false;
349 if ( !$shownDisabled ) {
350 $this->logger->debug( __METHOD__ . ': disabled' );
351 $shownDisabled = true;
352 }
353
354 return true;
355 }
356
357 try {
358 return $this->loadUnguarded( $code, $mode );
359 } catch ( Throwable $e ) {
360 // Don't try to load again during the exception handler
361 $this->disable = true;
362 throw $e;
363 }
364 }
365
373 private function loadUnguarded( $code, $mode ) {
374 $success = false; // Keep track of success
375 $staleCache = false; // a cache array with expired data, or false if none has been loaded
376 $where = []; // Debug info, delayed to avoid spamming debug log too much
377
378 // A hash of the expected content is stored in a WAN cache key, providing a way
379 // to invalid the local cache on every server whenever a message page changes.
380 [ $hash, $hashVolatile ] = $this->getValidationHash( $code );
381 $this->cacheVolatile[$code] = $hashVolatile;
382 $volatilityOnlyStaleness = false;
383
384 // Try the local cache and check against the cluster hash key...
385 $cache = $this->getLocalCache( $code );
386 if ( !$cache ) {
387 $where[] = 'local cache is empty';
388 } elseif ( !isset( $cache['HASH'] ) || $cache['HASH'] !== $hash ) {
389 $where[] = 'local cache has the wrong hash';
390 $staleCache = $cache;
391 } elseif ( $this->isCacheExpired( $cache ) ) {
392 $where[] = 'local cache is expired';
393 $staleCache = $cache;
394 } elseif ( $hashVolatile ) {
395 // Some recent message page changes might not show due to DB lag
396 $where[] = 'local cache validation key is expired/volatile';
397 $staleCache = $cache;
398 $volatilityOnlyStaleness = true;
399 } else {
400 $where[] = 'got from local cache';
401 $this->cache->set( $code, $cache );
402 $success = true;
403 }
404
405 if ( !$success ) {
406 // Try the cluster cache, using a lock for regeneration...
407 $cacheKey = $this->clusterCache->makeKey( 'messages', $code );
408 for ( $failedAttempts = 0; $failedAttempts <= 1; $failedAttempts++ ) {
409 if ( $volatilityOnlyStaleness && $staleCache ) {
410 // While the cluster cache *might* be more up-to-date, we do not want
411 // the I/O strain of every application server fetching the key here during
412 // the volatility period. Either this thread wins the lock and regenerates
413 // the cache or the stale local cache value gets reused.
414 $where[] = 'global cache is presumed expired';
415 } else {
416 $cache = $this->clusterCache->get( $cacheKey );
417 if ( !$cache ) {
418 $where[] = 'global cache is empty';
419 } elseif ( $this->isCacheExpired( $cache ) ) {
420 $where[] = 'global cache is expired';
421 $staleCache = $cache;
422 } elseif ( $hashVolatile ) {
423 // Some recent message page changes might not show due to DB lag
424 $where[] = 'global cache is expired/volatile';
425 $staleCache = $cache;
426 } else {
427 $where[] = 'got from global cache';
428 $this->cache->set( $code, $cache );
429 $this->saveToCaches( $cache, 'local-only', $code );
430 $success = true;
431 break;
432 }
433 }
434
435 // We need to call loadFromDB(). Limit the concurrency to one thread.
436 // This prevents the site from going down when the cache expires.
437 // Note that the DB slam protection lock here is non-blocking.
438 $loadStatus = $this->loadFromDBWithMainLock( $code, $where, $mode );
439 if ( $loadStatus === true ) {
440 $success = true;
441 break;
442 } elseif ( $staleCache ) {
443 // Use the stale cache while some other thread constructs the new one
444 $where[] = 'using stale cache';
445 $this->cache->set( $code, $staleCache );
446 $success = true;
447 break;
448 } elseif ( $failedAttempts > 0 ) {
449 $where[] = 'failed to find cache after waiting';
450 // Already blocked once, so avoid another lock/unlock cycle.
451 // This case will typically be hit if memcached is down, or if
452 // loadFromDB() takes longer than LOCK_WAIT.
453 break;
454 } elseif ( $loadStatus === 'cantacquire' ) {
455 // Wait for the other thread to finish, then retry. Normally,
456 // the memcached get() will then yield the other thread's result.
457 $where[] = 'waiting for other thread to complete';
458 [ , $ioError ] = $this->getReentrantScopedLock( $code );
459 if ( $ioError ) {
460 $where[] = 'failed waiting';
461 // Call loadFromDB() with concurrency limited to one thread per server.
462 // It should be rare for all servers to lack even a stale local cache.
463 $success = $this->loadFromDBWithLocalLock( $code, $where, $mode );
464 break;
465 }
466 } else {
467 // Disable cache; $loadStatus is 'disabled'
468 break;
469 }
470 }
471 }
472
473 if ( !$success ) {
474 $where[] = 'loading FAILED - cache is disabled';
475 $this->disable = true;
476 $this->cache->set( $code, [] );
477 $this->logger->error( __METHOD__ . ": Failed to load $code" );
478 // This used to throw an exception, but that led to nasty side effects like
479 // the whole wiki being instantly down if the memcached server died
480 }
481
482 if ( !$this->isLanguageLoaded( $code ) ) {
483 throw new LogicException( "Process cache for '$code' should be set by now." );
484 }
485
486 $info = implode( ', ', $where );
487 $this->logger->debug( __METHOD__ . ": Loading $code... $info" );
488
489 return $success;
490 }
491
498 private function loadFromDBWithMainLock( $code, array &$where, $mode = null ) {
499 // If cache updates on all levels fail, give up on message overrides.
500 // This is to avoid easy site outages; see $saveSuccess comments below.
501 $statusKey = $this->clusterCache->makeKey( 'messages', $code, 'status' );
502 $status = $this->clusterCache->get( $statusKey );
503 if ( $status === 'error' ) {
504 $where[] = "could not load; method is still globally disabled";
505 return 'disabled';
506 }
507
508 // Now let's regenerate
509 $where[] = 'loading from DB';
510
511 // Lock the cache to prevent conflicting writes.
512 // This lock is non-blocking so stale cache can quickly be used.
513 // Note that load() will call a blocking getReentrantScopedLock()
514 // after this if it really needs to wait for any current thread.
515 [ $scopedLock ] = $this->getReentrantScopedLock( $code, 0 );
516 if ( !$scopedLock ) {
517 $where[] = 'could not acquire main lock';
518 return 'cantacquire';
519 }
520
521 $cache = $this->loadFromDB( $code, $mode );
522 $this->cache->set( $code, $cache );
523 $saveSuccess = $this->saveToCaches( $cache, 'all', $code );
524
525 if ( !$saveSuccess ) {
539 if ( $this->srvCache instanceof EmptyBagOStuff ) {
540 $this->clusterCache->set( $statusKey, 'error', 60 * 5 );
541 $where[] = 'could not save cache, disabled globally for 5 minutes';
542 } else {
543 $where[] = "could not save global cache";
544 }
545 }
546
547 return true;
548 }
549
556 private function loadFromDBWithLocalLock( $code, array &$where, $mode = null ) {
557 $success = false;
558 $where[] = 'loading from DB using local lock';
559
560 $scopedLock = $this->srvCache->getScopedLock(
561 $this->srvCache->makeKey( 'messages', $code ),
562 self::WAIT_SEC,
563 self::LOCK_TTL,
564 __METHOD__
565 );
566 if ( $scopedLock ) {
567 $cache = $this->loadFromDB( $code, $mode );
568 $this->cache->set( $code, $cache );
569 $this->saveToCaches( $cache, 'local-only', $code );
570 $success = true;
571 }
572
573 return $success;
574 }
575
585 private function loadFromDB( $code, $mode = null ) {
586 $icp = MediaWikiServices::getInstance()->getConnectionProvider();
587
588 $dbr = ( $mode === self::FOR_UPDATE ) ? $icp->getPrimaryDatabase() : $icp->getReplicaDatabase();
589
590 $cache = [];
591
592 $mostused = []; // list of "<cased message key>/<code>"
593 if ( $this->adaptive && $code !== $this->contLangCode ) {
594 if ( !$this->cache->has( $this->contLangCode ) ) {
595 $this->load( $this->contLangCode );
596 }
597 $mostused = array_keys( $this->cache->get( $this->contLangCode ) );
598 foreach ( $mostused as $key => $value ) {
599 $mostused[$key] = "$value/$code";
600 }
601 }
602
603 // Common conditions
604 $conds = [
605 // Treat redirects as not existing (T376398)
606 'page_is_redirect' => 0,
607 'page_namespace' => NS_MEDIAWIKI,
608 ];
609 if ( count( $mostused ) ) {
610 $conds['page_title'] = $mostused;
611 } elseif ( $code !== $this->contLangCode ) {
612 $conds[] = $dbr->expr(
613 'page_title',
614 IExpression::LIKE,
615 new LikeValue( $dbr->anyString(), '/', $code )
616 );
617 } else {
618 // Effectively disallows use of '/' character in NS_MEDIAWIKI for uses
619 // other than language code.
620 $conds[] = $dbr->expr(
621 'page_title',
622 IExpression::NOT_LIKE,
623 new LikeValue( $dbr->anyString(), '/', $dbr->anyString() )
624 );
625 }
626
627 // Set the stubs for oversized software-defined messages in the main cache map
628 $res = $dbr->newSelectQueryBuilder()
629 ->select( [ 'page_title', 'page_latest' ] )
630 ->from( 'page' )
631 ->where( $conds )
632 ->andWhere( $dbr->expr( 'page_len', '>', intval( $this->maxEntrySize ) ) )
633 ->caller( __METHOD__ . "($code)-big" )->fetchResultSet();
634 foreach ( $res as $row ) {
635 // Include entries/stubs for all keys in $mostused in adaptive mode
636 if ( $this->adaptive || $this->isMainCacheable( $row->page_title ) ) {
637 $cache[$row->page_title] = '!TOO BIG';
638 }
639 // At least include revision ID so page changes are reflected in the hash
640 $cache['EXCESSIVE'][$row->page_title] = $row->page_latest;
641 }
642
643 // RevisionStore cannot be injected as it would break the installer since
644 // it instantiates MessageCache before the DB.
645 $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
646 // Set the text for small software-defined messages in the main cache map
647 $revQuery = $revisionStore->getQueryInfo( [ 'page' ] );
648
649 // T231196: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor` then
650 // `revision` then `page` is somehow better than starting with `page`. Tell it not to reorder the
651 // query (and also reorder it ourselves because as generated by RevisionStore it'll have
652 // `revision` first rather than `page`).
653 $revQuery['joins']['revision'] = $revQuery['joins']['page'];
654 unset( $revQuery['joins']['page'] );
655 // It isn't actually necessary to reorder $revQuery['tables'] as Database does the right thing
656 // when join conditions are given for all joins, but Gergő is wary of relying on that so pull
657 // `page` to the start.
658 $revQuery['tables'] = array_merge(
659 [ 'page' ],
660 array_diff( $revQuery['tables'], [ 'page' ] )
661 );
662
663 $res = $dbr->newSelectQueryBuilder()
664 ->queryInfo( $revQuery )
665 ->where( $conds )
666 ->andWhere( [
667 $dbr->expr( 'page_len', '<=', intval( $this->maxEntrySize ) ),
668 'page_latest = rev_id' // get the latest revision only
669 ] )
670 ->caller( __METHOD__ . "($code)-small" )
671 ->straightJoinOption()
672 ->fetchResultSet();
673
674 // Don't load content from uncacheable rows (T313004)
675 [ $cacheableRows, $uncacheableRows ] = $this->separateCacheableRows( $res );
676 $result = $revisionStore->newRevisionsFromBatch( $cacheableRows, [
677 'slots' => [ SlotRecord::MAIN ],
678 'content' => true
679 ] );
680 $revisions = $result->isOK() ? $result->getValue() : [];
681
682 foreach ( $cacheableRows as $row ) {
683 try {
684 $rev = $revisions[$row->rev_id] ?? null;
685 $content = $rev ? $rev->getContent( SlotRecord::MAIN ) : null;
686 $text = $this->getMessageTextFromContent( $content );
687 } catch ( TimeoutException $e ) {
688 throw $e;
689 } catch ( Exception $ex ) {
690 $text = false;
691 }
692
693 if ( !is_string( $text ) ) {
694 $entry = '!ERROR';
695 $this->logger->error(
696 __METHOD__
697 . ": failed to load message page text for {$row->page_title} ($code)"
698 );
699 } else {
700 $entry = ' ' . $text;
701 }
702 $cache[$row->page_title] = $entry;
703 }
704
705 foreach ( $uncacheableRows as $row ) {
706 // T193271: The cache object gets too big and slow to generate.
707 // At least include revision ID, so that page changes are reflected in the hash.
708 $cache['EXCESSIVE'][$row->page_title] = $row->page_latest;
709 }
710
711 $cache['VERSION'] = MSG_CACHE_VERSION;
712 ksort( $cache );
713
714 // Hash for validating local cache (APC). No need to take into account
715 // messages larger than $wgMaxMsgCacheEntrySize, since those are only
716 // stored and fetched from memcache.
717 $cache['HASH'] = md5( serialize( $cache ) );
718 $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + self::WAN_TTL );
719 unset( $cache['EXCESSIVE'] ); // only needed for hash
720
721 return $cache;
722 }
723
730 private function isLanguageLoaded( $lang ) {
731 // It is important that this only returns true if the cache was fully
732 // populated by load(), so that callers can assume all cache keys exist.
733 // It is possible for $this->cache to be only partially populated through
734 // methods like MessageCache::replace(), which must not make this method
735 // return true (T208897). And this method must cease to return true
736 // if the language was evicted by MapCacheLRU (T230690).
737 return $this->cache->hasField( $lang, 'VERSION' );
738 }
739
751 private function isMainCacheable( $name, $code = null ) {
752 // Convert the first letter to lowercase, and strip /code suffix
753 $name = $this->contLang->lcfirst( $name );
754 // Include common conversion table pages. This also avoids problems with
755 // Installer::parse() bailing out due to disallowed DB queries (T207979).
756 if ( strpos( $name, 'conversiontable/' ) === 0 ) {
757 return true;
758 }
759 $msg = preg_replace( '/\/[a-z0-9-]{2,}$/', '', $name );
760
761 if ( $code === null ) {
762 // Bulk load
763 if ( $this->systemMessageNames === null ) {
764 $this->systemMessageNames = array_fill_keys(
765 $this->localisationCache->getSubitemList( $this->contLangCode, 'messages' ),
766 true );
767 }
768 return isset( $this->systemMessageNames[$msg] );
769 } else {
770 // Use individual subitem
771 return $this->localisationCache->getSubitem( $code, 'messages', $msg ) !== null;
772 }
773 }
774
782 private function separateCacheableRows( $res ) {
783 if ( $this->adaptive ) {
784 // Include entries/stubs for all keys in $mostused in adaptive mode
785 return [ $res, [] ];
786 }
787 $cacheableRows = [];
788 $uncacheableRows = [];
789 foreach ( $res as $row ) {
790 if ( $this->isMainCacheable( $row->page_title ) ) {
791 $cacheableRows[] = $row;
792 } else {
793 $uncacheableRows[] = $row;
794 }
795 }
796 return [ $cacheableRows, $uncacheableRows ];
797 }
798
805 public function replace( $title, $text ) {
806 if ( $this->disable ) {
807 return;
808 }
809
810 [ $msg, $code ] = $this->figureMessage( $title );
811 if ( strpos( $title, '/' ) !== false && $code === $this->contLangCode ) {
812 // Content language overrides do not use the /<code> suffix
813 return;
814 }
815
816 // (a) Update the process cache with the new message text
817 if ( $text === false ) {
818 // Page deleted
819 $this->cache->setField( $code, $title, '!NONEXISTENT' );
820 } else {
821 // Ignore $wgMaxMsgCacheEntrySize so the process cache is up-to-date
822 $this->cache->setField( $code, $title, ' ' . $text );
823 }
824
825 // (b) Update the shared caches in a deferred update with a fresh DB snapshot
826 DeferredUpdates::addUpdate(
827 new MessageCacheUpdate( $code, $title, $msg ),
828 DeferredUpdates::PRESEND
829 );
830 }
831
836 public function refreshAndReplaceInternal( string $code, array $replacements ) {
837 // Allow one caller at a time to avoid race conditions
838 [ $scopedLock ] = $this->getReentrantScopedLock( $code );
839 if ( !$scopedLock ) {
840 foreach ( $replacements as [ $title ] ) {
841 $this->logger->error(
842 __METHOD__ . ': could not acquire lock to update {title} ({code})',
843 [ 'title' => $title, 'code' => $code ] );
844 }
845
846 return;
847 }
848
849 // Load the existing cache to update it in the local DC cache.
850 // The other DCs will see a hash mismatch.
851 if ( $this->load( $code, self::FOR_UPDATE ) ) {
852 $cache = $this->cache->get( $code );
853 } else {
854 // Err? Fall back to loading from the database.
855 $cache = $this->loadFromDB( $code, self::FOR_UPDATE );
856 }
857 // Check if individual cache keys should exist and update cache accordingly
858 $newTextByTitle = []; // map of (title => content)
859 $newBigTitles = []; // map of (title => latest revision ID), like EXCESSIVE in loadFromDB()
860 // Can not inject the WikiPageFactory as it would break the installer since
861 // it instantiates MessageCache before the DB.
862 $wikiPageFactory = MediaWikiServices::getInstance()->getWikiPageFactory();
863 foreach ( $replacements as [ $title ] ) {
864 $page = $wikiPageFactory->newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) );
865 $page->loadPageData( IDBAccessObject::READ_LATEST );
866 $text = $this->getMessageTextFromContent( $page->getContent() );
867 // Remember the text for the blob store update later on
868 $newTextByTitle[$title] = $text ?? '';
869 // Note that if $text is false, then $cache should have a !NONEXISTANT entry
870 if ( !is_string( $text ) ) {
871 $cache[$title] = '!NONEXISTENT';
872 } elseif ( strlen( $text ) > $this->maxEntrySize ) {
873 $cache[$title] = '!TOO BIG';
874 $newBigTitles[$title] = $page->getLatest();
875 } else {
876 $cache[$title] = ' ' . $text;
877 }
878 }
879 // Update HASH for the new key. Incorporates various administrative keys,
880 // including the old HASH (and thereby the EXCESSIVE value from loadFromDB()
881 // and previous replace() calls), but that doesn't really matter since we
882 // only ever compare it for equality with a copy saved by saveToCaches().
883 $cache['HASH'] = md5( serialize( $cache + [ 'EXCESSIVE' => $newBigTitles ] ) );
884 // Update the too-big WAN cache entries now that we have the new HASH
885 foreach ( $newBigTitles as $title => $id ) {
886 // Match logic of loadCachedMessagePageEntry()
887 $this->wanCache->set(
888 $this->bigMessageCacheKey( $cache['HASH'], $title ),
889 ' ' . $newTextByTitle[$title],
890 self::WAN_TTL
891 );
892 }
893 // Mark this cache as definitely being "latest" (non-volatile) so
894 // load() calls do not try to refresh the cache with replica DB data
895 $cache['LATEST'] = time();
896 // Update the process cache
897 $this->cache->set( $code, $cache );
898 // Pre-emptively update the local datacenter cache so things like edit filter and
899 // prevented changes are reflected immediately; these often use MediaWiki: pages.
900 // The datacenter handling replace() calls should be the same one handling edits
901 // as they require HTTP POST.
902 $this->saveToCaches( $cache, 'all', $code );
903 // Release the lock now that the cache is saved
904 ScopedCallback::consume( $scopedLock );
905
906 // Relay the purge. Touching this check key expires cache contents
907 // and local cache (APC) validation hash across all datacenters.
908 $this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
909
910 // Purge the messages in the message blob store and fire any hook handlers
911 $blobStore = MediaWikiServices::getInstance()->getResourceLoader()->getMessageBlobStore();
912 foreach ( $replacements as [ $title, $msg ] ) {
913 $blobStore->updateMessage( $this->contLang->lcfirst( $msg ) );
914 $this->hookRunner->onMessageCacheReplace( $title, $newTextByTitle[$title] );
915 }
916 }
917
924 private function isCacheExpired( $cache ) {
925 return !isset( $cache['VERSION'] ) ||
926 !isset( $cache['EXPIRY'] ) ||
927 $cache['VERSION'] !== MSG_CACHE_VERSION ||
928 $cache['EXPIRY'] <= wfTimestampNow();
929 }
930
940 private function saveToCaches( array $cache, $dest, $code = false ) {
941 if ( $dest === 'all' ) {
942 $cacheKey = $this->clusterCache->makeKey( 'messages', $code );
943 $success = $this->clusterCache->set( $cacheKey, $cache );
944 $this->setValidationHash( $code, $cache );
945 } else {
946 $success = true;
947 }
948
949 $this->saveToLocalCache( $code, $cache );
950
951 return $success;
952 }
953
960 private function getValidationHash( $code ) {
961 $curTTL = null;
962 $value = $this->wanCache->get(
963 $this->wanCache->makeKey( 'messages', $code, 'hash', 'v1' ),
964 $curTTL,
965 [ $this->getCheckKey( $code ) ]
966 );
967
968 if ( $value ) {
969 $hash = $value['hash'];
970 if ( ( time() - $value['latest'] ) < WANObjectCache::TTL_MINUTE ) {
971 // Cache was recently updated via replace() and should be up-to-date.
972 // That method is only called in the primary datacenter and uses FOR_UPDATE.
973 $expired = false;
974 } else {
975 // See if the "check" key was bumped after the hash was generated
976 $expired = ( $curTTL < 0 );
977 }
978 } else {
979 // No hash found at all; cache must regenerate to be safe
980 $hash = false;
981 $expired = true;
982 }
983
984 return [ $hash, $expired ];
985 }
986
997 private function setValidationHash( $code, array $cache ) {
998 $this->wanCache->set(
999 $this->wanCache->makeKey( 'messages', $code, 'hash', 'v1' ),
1000 [
1001 'hash' => $cache['HASH'],
1002 'latest' => $cache['LATEST'] ?? 0
1003 ],
1004 WANObjectCache::TTL_INDEFINITE
1005 );
1006 }
1007
1014 private function getReentrantScopedLock( $code, $timeout = self::WAIT_SEC ) {
1015 $key = $this->clusterCache->makeKey( 'messages', $code );
1016
1017 $watchPoint = $this->clusterCache->watchErrors();
1018 $scopedLock = $this->clusterCache->getScopedLock(
1019 $key,
1020 $timeout,
1021 self::LOCK_TTL,
1022 __METHOD__
1023 );
1024 $error = ( !$scopedLock && $this->clusterCache->getLastError( $watchPoint ) );
1025
1026 return [ $scopedLock, $error ];
1027 }
1028
1065 public function get( $key, $useDB = true, $language = null, &$usedKey = '' ) {
1066 if ( is_int( $key ) ) {
1067 // Fix numerical strings that somehow become ints on their way here
1068 $key = (string)$key;
1069 } elseif ( !is_string( $key ) ) {
1070 throw new TypeError( 'Message key must be a string' );
1071 } elseif ( $key === '' ) {
1072 // Shortcut: the empty key is always missing
1073 return false;
1074 }
1075
1076 $language ??= $this->contLang;
1077 $language = $this->getLanguageObject( $language );
1078
1079 // Normalise title-case input (with some inlining)
1080 $lckey = self::normalizeKey( $key );
1081
1082 // Initialize the overrides here to prevent calling the hook too early.
1083 if ( $this->messageKeyOverrides === null ) {
1084 $this->messageKeyOverrides = [];
1085 $this->hookRunner->onMessageCacheFetchOverrides( $this->messageKeyOverrides );
1086 }
1087
1088 if ( isset( $this->messageKeyOverrides[$lckey] ) ) {
1089 $override = $this->messageKeyOverrides[$lckey];
1090
1091 // Strings are deliberately interpreted as message keys,
1092 // to prevent ambiguity between message keys and functions.
1093 if ( is_string( $override ) ) {
1094 $lckey = $override;
1095 } else {
1096 $lckey = $override( $lckey, $this, $language, $useDB );
1097 }
1098 }
1099
1100 $this->hookRunner->onMessageCache__get( $lckey );
1101
1102 $usedKey = $lckey;
1103
1104 // Loop through each language in the fallback list until we find something useful
1105 $message = $this->getMessageFromFallbackChain(
1106 $language,
1107 $lckey,
1108 !$this->disable && $useDB
1109 );
1110
1111 // If we still have no message, maybe the key was in fact a full key so try that
1112 if ( $message === false ) {
1113 $parts = explode( '/', $lckey );
1114 // We may get calls for things that are http-urls from sidebar
1115 // Let's not load nonexistent languages for those
1116 // They usually have more than one slash.
1117 if ( count( $parts ) === 2 && $parts[1] !== '' ) {
1118 $message = $this->localisationCache->getSubitem( $parts[1], 'messages', $parts[0] ) ?? false;
1119 }
1120 }
1121
1122 // Post-processing if the message exists
1123 if ( $message !== false ) {
1124 // Fix whitespace
1125 $message = str_replace(
1126 [
1127 // Fix for trailing whitespace, removed by textarea
1128 '&#32;',
1129 // Fix for NBSP, converted to space by firefox
1130 '&nbsp;',
1131 '&#160;',
1132 '&shy;'
1133 ],
1134 [
1135 ' ',
1136 "\u{00A0}",
1137 "\u{00A0}",
1138 "\u{00AD}"
1139 ],
1140 $message
1141 );
1142 }
1143
1144 return $message;
1145 }
1146
1162 private function getLanguageObject( $langcode ) {
1163 # Identify which language to get or create a language object for.
1164 # Using is_object here due to Stub objects.
1165 if ( is_object( $langcode ) ) {
1166 # Great, we already have the object (hopefully)!
1167 return $langcode;
1168 }
1169
1170 wfDeprecated( __METHOD__ . ' with not a Language object in $langcode', '1.43' );
1171 if ( $langcode === true || $langcode === $this->contLangCode ) {
1172 # $langcode is the language code of the wikis content language object.
1173 # or it is a boolean and value is true
1174 return $this->contLang;
1175 }
1176
1177 global $wgLang;
1178 if ( $langcode === false || $langcode === $wgLang->getCode() ) {
1179 # $langcode is the language code of user language object.
1180 # or it was a boolean and value is false
1181 return $wgLang;
1182 }
1183
1184 $validCodes = array_keys( $this->languageNameUtils->getLanguageNames() );
1185 if ( in_array( $langcode, $validCodes ) ) {
1186 # $langcode corresponds to a valid language.
1187 return $this->langFactory->getLanguage( $langcode );
1188 }
1189
1190 # $langcode is a string, but not a valid language code; use content language.
1191 $this->logger->debug( 'Invalid language code passed to' . __METHOD__ . ', falling back to content language.' );
1192 return $this->contLang;
1193 }
1194
1207 private function getMessageFromFallbackChain( $lang, $lckey, $useDB ) {
1208 $alreadyTried = [];
1209
1210 // First try the requested language.
1211 $message = $this->getMessageForLang( $lang, $lckey, $useDB, $alreadyTried );
1212 if ( $message !== false ) {
1213 return $message;
1214 }
1215
1216 // Now try checking the site language.
1217 $message = $this->getMessageForLang( $this->contLang, $lckey, $useDB, $alreadyTried );
1218 return $message;
1219 }
1220
1231 private function getMessageForLang( $lang, $lckey, $useDB, &$alreadyTried ) {
1232 $langcode = $lang->getCode();
1233
1234 // Try checking the database for the requested language
1235 if ( $useDB ) {
1236 $uckey = $this->contLang->ucfirst( $lckey );
1237
1238 if ( !isset( $alreadyTried[$langcode] ) ) {
1239 $message = $this->getMsgFromNamespace(
1240 $this->getMessagePageName( $langcode, $uckey ),
1241 $langcode
1242 );
1243 if ( $message !== false ) {
1244 return $message;
1245 }
1246 $alreadyTried[$langcode] = true;
1247 }
1248 } else {
1249 $uckey = null;
1250 }
1251
1252 // Return a special value handled in Message::format() to display the message key
1253 // (and fallback keys) and the parameters passed to the message.
1254 // TODO: Move to a better place.
1255 if ( $langcode === 'qqx' ) {
1256 return '($*)';
1257 } elseif (
1258 $langcode === 'x-xss' &&
1259 $this->useXssLanguage &&
1260 !in_array( $lckey, $this->rawHtmlMessages, true )
1261 ) {
1262 $xssViaInnerHtml = "<script>alert('$lckey')</script>";
1263 $xssViaAttribute = '">' . $xssViaInnerHtml . '<x y="';
1264 return $xssViaInnerHtml . $xssViaAttribute . '($*)';
1265 }
1266
1267 // Check the localisation cache
1268 [ $defaultMessage, $messageSource ] =
1269 $this->localisationCache->getSubitemWithSource( $langcode, 'messages', $lckey );
1270 if ( $messageSource === $langcode ) {
1271 return $defaultMessage;
1272 }
1273
1274 // Try checking the database for all of the fallback languages
1275 if ( $useDB ) {
1276 $fallbackChain = $this->languageFallback->getAll( $langcode );
1277
1278 foreach ( $fallbackChain as $code ) {
1279 if ( isset( $alreadyTried[$code] ) ) {
1280 continue;
1281 }
1282
1283 $message = $this->getMsgFromNamespace(
1284 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable uckey is set when used
1285 $this->getMessagePageName( $code, $uckey ), $code );
1286
1287 if ( $message !== false ) {
1288 return $message;
1289 }
1290 $alreadyTried[$code] = true;
1291
1292 // Reached the source language of the default message. Don't look for DB overrides
1293 // further back in the fallback chain. (T229992)
1294 if ( $code === $messageSource ) {
1295 return $defaultMessage;
1296 }
1297 }
1298 }
1299
1300 return $defaultMessage ?? false;
1301 }
1302
1310 private function getMessagePageName( $langcode, $uckey ) {
1311 if ( $langcode === $this->contLangCode ) {
1312 // Messages created in the content language will not have the /lang extension
1313 return $uckey;
1314 } else {
1315 return "$uckey/$langcode";
1316 }
1317 }
1318
1331 public function getMsgFromNamespace( $title, $code ) {
1332 // Load all MediaWiki page definitions into cache. Note that individual keys
1333 // already loaded into the cache during this request remain in the cache, which
1334 // includes the value of hook-defined messages.
1335 $this->load( $code );
1336
1337 $entry = $this->cache->getField( $code, $title );
1338
1339 if ( $entry !== null ) {
1340 // Message page exists as an override of a software messages
1341 if ( substr( $entry, 0, 1 ) === ' ' ) {
1342 // The message exists and is not '!TOO BIG' or '!ERROR'
1343 return (string)substr( $entry, 1 );
1344 } elseif ( $entry === '!NONEXISTENT' ) {
1345 // The text might be '-' or missing due to some data loss
1346 return false;
1347 }
1348 // Load the message page, utilizing the individual message cache.
1349 // If the page does not exist, there will be no hook handler fallbacks.
1350 $entry = $this->loadCachedMessagePageEntry(
1351 $title,
1352 $code,
1353 $this->cache->getField( $code, 'HASH' )
1354 );
1355 } else {
1356 // Message page either does not exist or does not override a software message
1357 if ( !$this->isMainCacheable( $title, $code ) ) {
1358 // Message page does not override any software-defined message. A custom
1359 // message might be defined to have content or settings specific to the wiki.
1360 // Load the message page, utilizing the individual message cache as needed.
1361 $entry = $this->loadCachedMessagePageEntry(
1362 $title,
1363 $code,
1364 $this->cache->getField( $code, 'HASH' )
1365 );
1366 }
1367 if ( $entry === null || substr( $entry, 0, 1 ) !== ' ' ) {
1368 // Message does not have a MediaWiki page definition; try hook handlers
1369 $message = false;
1370 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
1371 $this->hookRunner->onMessagesPreLoad( $title, $message, $code );
1372 if ( $message !== false ) {
1373 $this->cache->setField( $code, $title, ' ' . $message );
1374 } else {
1375 $this->cache->setField( $code, $title, '!NONEXISTENT' );
1376 }
1377
1378 return $message;
1379 }
1380 }
1381
1382 if ( $entry !== false && substr( $entry, 0, 1 ) === ' ' ) {
1383 if ( $this->cacheVolatile[$code] ) {
1384 // Make sure that individual keys respect the WAN cache holdoff period too
1385 $this->logger->debug(
1386 __METHOD__ . ': loading volatile key \'{titleKey}\'',
1387 [ 'titleKey' => $title, 'code' => $code ] );
1388 } else {
1389 $this->cache->setField( $code, $title, $entry );
1390 }
1391 // The message exists, so make sure a string is returned
1392 return (string)substr( $entry, 1 );
1393 }
1394
1395 $this->cache->setField( $code, $title, '!NONEXISTENT' );
1396
1397 return false;
1398 }
1399
1406 private function loadCachedMessagePageEntry( $dbKey, $code, $hash ) {
1407 $fname = __METHOD__;
1408 return $this->srvCache->getWithSetCallback(
1409 $this->srvCache->makeKey( 'messages-big', $hash, $dbKey ),
1410 BagOStuff::TTL_HOUR,
1411 function () use ( $code, $dbKey, $hash, $fname ) {
1412 return $this->wanCache->getWithSetCallback(
1413 $this->bigMessageCacheKey( $hash, $dbKey ),
1414 self::WAN_TTL,
1415 function ( $oldValue, &$ttl, &$setOpts ) use ( $dbKey, $code, $fname ) {
1416 // Try loading the message from the database
1417 $setOpts += Database::getCacheSetOptions(
1418 MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase()
1419 );
1420 // Use newKnownCurrent() to avoid querying revision/user tables
1421 $title = Title::makeTitle( NS_MEDIAWIKI, $dbKey );
1422 // Injecting RevisionStore breaks installer since it
1423 // instantiates MessageCache before DB.
1424 $revision = MediaWikiServices::getInstance()
1425 ->getRevisionLookup()
1426 ->getKnownCurrentRevision( $title );
1427 if ( !$revision ) {
1428 // The wiki doesn't have a local override page. Cache absence with normal TTL.
1429 // When overrides are created, self::replace() takes care of the cache.
1430 return '!NONEXISTENT';
1431 }
1432 $content = $revision->getContent( SlotRecord::MAIN );
1433 if ( $content ) {
1434 $message = $this->getMessageTextFromContent( $content );
1435 } else {
1436 $this->logger->warning(
1437 $fname . ': failed to load page text for \'{titleKey}\'',
1438 [ 'titleKey' => $dbKey, 'code' => $code ]
1439 );
1440 $message = null;
1441 }
1442
1443 if ( !is_string( $message ) ) {
1444 // Revision failed to load Content, or Content is incompatible with wikitext.
1445 // Possibly a temporary loading failure.
1446 $ttl = 5;
1447
1448 return '!NONEXISTENT';
1449 }
1450
1451 return ' ' . $message;
1452 }
1453 );
1454 }
1455 );
1456 }
1457
1465 public function transform( $message, $interface = false, $language = null, ?PageReference $page = null ) {
1466 // Avoid creating parser if nothing to transform
1467 if ( !str_contains( $message, '{{' ) ) {
1468 return $message;
1469 }
1470
1471 $popts = $this->getParserOptions();
1472 $popts->setInterfaceMessage( $interface );
1473 $popts->setTargetLanguage( $language );
1474
1475 $userlang = $popts->setUserLang( $language );
1476 try {
1477 $this->curParser++;
1478 $parser = $this->getParser();
1479 if ( !$parser ) {
1480 return '<span class="error">Message transform depth limit exceeded</span>';
1481 }
1482 $message = $parser->transformMsg( $message, $popts, $page );
1483 } finally {
1484 $this->curParser--;
1485 }
1486 $popts->setUserLang( $userlang );
1487
1488 return $message;
1489 }
1490
1495 private function getParser(): ?Parser {
1496 if ( $this->curParser >= self::MAX_PARSER_DEPTH ) {
1497 $this->logger->debug( __METHOD__ . ": Refusing to create a new parser with index {$this->curParser}" );
1498 return null;
1499 }
1500 if ( !isset( $this->parsers[ $this->curParser ] ) ) {
1501 $this->logger->debug( __METHOD__ . ": Creating a new parser with index {$this->curParser}" );
1502 $this->parsers[ $this->curParser ] = $this->parserFactory->create();
1503 }
1504 return $this->parsers[ $this->curParser ];
1505 }
1506
1519 string $text, PageReference $contextPage,
1520 bool $linestart = true,
1521 bool $interface = false,
1522 $language = null
1523 ): ParserOutput {
1524 $options = [
1525 'allowTOC' => false,
1526 'enableSectionEditLinks' => false,
1527 // Wrapping messages in an extra <div> is probably not expected. If
1528 // they're outside the content area they probably shouldn't be
1529 // targeted by CSS that's targeting the parser output, and if
1530 // they're inside they already are from the outer div.
1531 'unwrap' => true,
1532 'userLang' => $language,
1533 ];
1534 // Parse $text to yield a ParserOutput
1535 $po = $this->parse( $text, $contextPage, $linestart, $interface, $language );
1536 if ( is_string( $po ) ) {
1537 $po = new ParserOutput( $po );
1538 }
1539 // Run the post-processing pipeline
1540 return MediaWikiServices::getInstance()->getDefaultOutputPipeline()
1541 ->run( $po, $this->getParserOptions(), $options );
1542 }
1543
1552 public function parse( $text, ?PageReference $page = null, $linestart = true,
1553 $interface = false, $language = null
1554 ) {
1555 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
1556 global $wgTitle;
1557
1558 $popts = $this->getParserOptions();
1559 $popts->setInterfaceMessage( $interface );
1560
1561 if ( is_string( $language ) ) {
1562 $language = $this->langFactory->getLanguage( $language );
1563 }
1564 $popts->setTargetLanguage( $language );
1565
1566 if ( !$page ) {
1567 $logger = LoggerFactory::getInstance( 'GlobalTitleFail' );
1568 $logger->info(
1569 __METHOD__ . ' called with no title set.',
1570 [ 'exception' => new RuntimeException ]
1571 );
1572 $page = $wgTitle;
1573 }
1574 // Sometimes $wgTitle isn't set either...
1575 if ( !$page ) {
1576 // It's not uncommon having a null $wgTitle in scripts. See r80898
1577 // Create a ghost title in such case
1578 $page = PageReferenceValue::localReference(
1579 NS_SPECIAL,
1580 'Badtitle/title not set in ' . __METHOD__
1581 );
1582 }
1583
1584 try {
1585 $this->curParser++;
1586 $parser = $this->getParser();
1587 if ( !$parser ) {
1588 return '<span class="error">Message parse depth limit exceeded</span>';
1589 }
1590 return $parser->parse( $text, $page, $popts, $linestart );
1591 } finally {
1592 $this->curParser--;
1593 }
1594 }
1595
1596 public function disable() {
1597 $this->disable = true;
1598 }
1599
1600 public function enable() {
1601 $this->disable = false;
1602 }
1603
1616 public function isDisabled() {
1617 return $this->disable;
1618 }
1619
1625 public function clear() {
1626 $langs = $this->languageNameUtils->getLanguageNames();
1627 foreach ( $langs as $code => $_ ) {
1628 $this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
1629 }
1630 $this->cache->clear();
1631 }
1632
1637 public function figureMessage( $key ) {
1638 $pieces = explode( '/', $key );
1639 if ( count( $pieces ) < 2 ) {
1640 return [ $key, $this->contLangCode ];
1641 }
1642
1643 $lang = array_pop( $pieces );
1644 if ( !$this->languageNameUtils->getLanguageName(
1645 $lang,
1646 LanguageNameUtils::AUTONYMS,
1647 LanguageNameUtils::DEFINED
1648 ) ) {
1649 return [ $key, $this->contLangCode ];
1650 }
1651
1652 $message = implode( '/', $pieces );
1653
1654 return [ $message, $lang ];
1655 }
1656
1666 public function getAllMessageKeys( $code ) {
1667 $this->load( $code );
1668 if ( !$this->cache->has( $code ) ) {
1669 // Apparently load() failed
1670 return null;
1671 }
1672 // Remove administrative keys
1673 $cache = $this->cache->get( $code );
1674 unset( $cache['VERSION'] );
1675 unset( $cache['EXPIRY'] );
1676 unset( $cache['EXCESSIVE'] );
1677 // Remove any !NONEXISTENT keys
1678 $cache = array_diff( $cache, [ '!NONEXISTENT' ] );
1679
1680 // Keys may appear with a capital first letter. lcfirst them.
1681 return array_map( [ $this->contLang, 'lcfirst' ], array_keys( $cache ) );
1682 }
1683
1692 public function updateMessageOverride( $page, ?Content $content = null ) {
1693 // treat null as not existing
1694 $msgText = $this->getMessageTextFromContent( $content ) ?? false;
1695
1696 $this->replace( $page->getDBkey(), $msgText );
1697
1698 if ( $this->contLangConverter->hasVariants() ) {
1699 $this->contLangConverter->updateConversionTable( $page );
1700 }
1701 }
1702
1707 public function getCheckKey( $code ) {
1708 return $this->wanCache->makeKey( 'messages', $code );
1709 }
1710
1715 private function getMessageTextFromContent( ?Content $content = null ) {
1716 // @TODO: could skip pseudo-messages like js/css here, based on content model
1717 if ( $content && $content->isRedirect() ) {
1718 // Treat redirects as not existing (T376398)
1719 $msgText = false;
1720 } elseif ( $content ) {
1721 // Message page exists...
1722 // XXX: Is this the right way to turn a Content object into a message?
1723 // NOTE: $content is typically either WikitextContent, JavaScriptContent or
1724 // CssContent.
1725 $msgText = $content->getWikitextForTransclusion();
1726 if ( $msgText === false || $msgText === null ) {
1727 // This might be due to some kind of misconfiguration...
1728 $msgText = null;
1729 $this->logger->warning(
1730 __METHOD__ . ": message content doesn't provide wikitext "
1731 . "(content model: " . $content->getModel() . ")" );
1732 }
1733 } else {
1734 // Message page does not exist...
1735 $msgText = false;
1736 }
1737
1738 return $msgText;
1739 }
1740
1746 private function bigMessageCacheKey( $hash, $title ) {
1747 return $this->wanCache->makeKey( 'messages-big', $hash, $title );
1748 }
1749}
const NS_MEDIAWIKI
Definition Defines.php:73
const NS_SPECIAL
Definition Defines.php:54
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
const MSG_CACHE_VERSION
MediaWiki message cache structure version.
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgLang
Definition Setup.php:570
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgTitle
Definition Setup.php:570
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Caching for the contents of localisation files.
Store key-value entries in a size-limited in-memory LRU cache.
set( $key, $value, $rank=self::RANK_TOP)
Set a key/value pair.
get( $key, $maxAge=INF, $default=null)
Get the value for a key.
hasField( $key, $field, $maxAge=INF)
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Group all the pieces relevant to the context of a request into one instance.
Defer callable updates to run later in the PHP process.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Base class for language-specific code.
Definition Language.php:82
getCode()
Get the internal language code for this language object.
Message cache purging and in-place update handler for specific message page changes.
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.
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Immutable value object representing a page reference.
Set options of the Parser.
ParserOutput is a rendering of a Content object or a message.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:148
Value object representing a content slot associated with a page revision.
Class to implement stub globals, which are globals that delay loading the their associated module cod...
Stub object for the user language.
Represents a title within MediaWiki.
Definition Title.php:78
Cache messages that are defined by MediaWiki-namespace pages or by hooks.
parseWithPostprocessing(string $text, PageReference $contextPage, bool $linestart=true, bool $interface=false, $language=null)
updateMessageOverride( $page, ?Content $content=null)
Purge message caches when a MediaWiki: page is created, updated, or deleted.
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, ParserFactory $parserFactory)
getMsgFromNamespace( $title, $code)
Get a message from the MediaWiki namespace, with caching.
parse( $text, ?PageReference $page=null, $linestart=true, $interface=false, $language=null)
const CONSTRUCTOR_OPTIONS
Options to be included in the ServiceOptions.
transform( $message, $interface=false, $language=null, ?PageReference $page=null)
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.
replace( $title, $text)
Updates cache as necessary when message page is changed.
Abstract class for any ephemeral data store.
Definition BagOStuff.php:88
No-op implementation that stores nothing.
Multi-datacenter aware caching interface.
Content of like value.
Definition LikeValue.php:14
Base interface for representing page content.
Definition Content.php:39
The shared interface for all language converters.
Interface for objects (potentially) representing an editable wiki page.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
Interface for database access objects.
Result wrapper for grabbing data queried from an IDatabase object.