MessageCache: Remove $wgMsgCacheExpiry configuration var
[lhc/web/wiklou.git] / includes / cache / MessageCache.php
1 <?php
2 /**
3 * Localisation messages cache.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Cache
22 */
23 use MediaWiki\MediaWikiServices;
24 use Wikimedia\ScopedCallback;
25 use MediaWiki\Logger\LoggerFactory;
26 use Wikimedia\Rdbms\Database;
27
28 /**
29 * MediaWiki message cache structure version.
30 * Bump this whenever the message cache format has changed.
31 */
32 define( 'MSG_CACHE_VERSION', 2 );
33
34 /**
35 * Cache of messages that are defined by MediaWiki namespace pages or by hooks
36 *
37 * Performs various MediaWiki namespace-related functions
38 * @ingroup Cache
39 */
40 class MessageCache {
41 const FOR_UPDATE = 1; // force message reload
42
43 /** How long to wait for memcached locks */
44 const WAIT_SEC = 15;
45 /** How long memcached locks last */
46 const LOCK_TTL = 30;
47
48 /**
49 * Lifetime for cache, for keys stored in $wanCache, in seconds.
50 * @var int
51 */
52 const WAN_TTL = IExpiringStore::TTL_DAY;
53
54 /**
55 * Process cache of loaded messages that are defined in MediaWiki namespace
56 *
57 * @var MapCacheLRU Map of (language code => key => " <MESSAGE>" or "!TOO BIG" or "!ERROR")
58 */
59 protected $cache;
60
61 /**
62 * Map of (lowercase message key => index) for all software defined messages
63 *
64 * @var array
65 */
66 protected $overridable;
67
68 /**
69 * @var bool[] Map of (language code => boolean)
70 */
71 protected $cacheVolatile = [];
72
73 /**
74 * Should mean that database cannot be used, but check
75 * @var bool $mDisable
76 */
77 protected $mDisable;
78
79 /**
80 * Message cache has its own parser which it uses to transform messages
81 * @var ParserOptions
82 */
83 protected $mParserOptions;
84 /** @var Parser */
85 protected $mParser;
86
87 /**
88 * @var bool $mInParser
89 */
90 protected $mInParser = false;
91
92 /** @var WANObjectCache */
93 protected $wanCache;
94 /** @var BagOStuff */
95 protected $clusterCache;
96 /** @var BagOStuff */
97 protected $srvCache;
98 /** @var Language */
99 protected $contLang;
100
101 /**
102 * Track which languages have been loaded by load().
103 * @var array
104 */
105 private $loadedLanguages = [];
106
107 /**
108 * Get the singleton instance of this class
109 *
110 * @deprecated in 1.34 inject an instance of this class instead of using global state
111 * @since 1.18
112 * @return MessageCache
113 */
114 public static function singleton() {
115 return MediaWikiServices::getInstance()->getMessageCache();
116 }
117
118 /**
119 * Normalize message key input
120 *
121 * @param string $key Input message key to be normalized
122 * @return string Normalized message key
123 */
124 public static function normalizeKey( $key ) {
125 $lckey = strtr( $key, ' ', '_' );
126 if ( ord( $lckey ) < 128 ) {
127 $lckey[0] = strtolower( $lckey[0] );
128 } else {
129 $lckey = MediaWikiServices::getInstance()->getContentLanguage()->lcfirst( $lckey );
130 }
131
132 return $lckey;
133 }
134
135 /**
136 * @param WANObjectCache $wanCache
137 * @param BagOStuff $clusterCache
138 * @param BagOStuff $serverCache
139 * @param bool $useDB Whether to look for message overrides (e.g. MediaWiki: pages)
140 * @param Language|null $contLang Content language of site
141 */
142 public function __construct(
143 WANObjectCache $wanCache,
144 BagOStuff $clusterCache,
145 BagOStuff $serverCache,
146 $useDB,
147 Language $contLang = null
148 ) {
149 $this->wanCache = $wanCache;
150 $this->clusterCache = $clusterCache;
151 $this->srvCache = $serverCache;
152
153 $this->cache = new MapCacheLRU( 5 ); // limit size for sanity
154
155 $this->mDisable = !$useDB;
156 $this->contLang = $contLang ?? MediaWikiServices::getInstance()->getContentLanguage();
157 }
158
159 /**
160 * ParserOptions is lazy initialised.
161 *
162 * @return ParserOptions
163 */
164 function getParserOptions() {
165 global $wgUser;
166
167 if ( !$this->mParserOptions ) {
168 if ( !$wgUser->isSafeToLoad() ) {
169 // $wgUser isn't unstubbable yet, so don't try to get a
170 // ParserOptions for it. And don't cache this ParserOptions
171 // either.
172 $po = ParserOptions::newFromAnon();
173 $po->setAllowUnsafeRawHtml( false );
174 $po->setTidy( true );
175 return $po;
176 }
177
178 $this->mParserOptions = new ParserOptions;
179 // Messages may take parameters that could come
180 // from malicious sources. As a precaution, disable
181 // the <html> parser tag when parsing messages.
182 $this->mParserOptions->setAllowUnsafeRawHtml( false );
183 // For the same reason, tidy the output!
184 $this->mParserOptions->setTidy( true );
185 }
186
187 return $this->mParserOptions;
188 }
189
190 /**
191 * Try to load the cache from APC.
192 *
193 * @param string $code Optional language code, see documentation of load().
194 * @return array|bool The cache array, or false if not in cache.
195 */
196 protected function getLocalCache( $code ) {
197 $cacheKey = $this->srvCache->makeKey( __CLASS__, $code );
198
199 return $this->srvCache->get( $cacheKey );
200 }
201
202 /**
203 * Save the cache to APC.
204 *
205 * @param string $code
206 * @param array $cache The cache array
207 */
208 protected function saveToLocalCache( $code, $cache ) {
209 $cacheKey = $this->srvCache->makeKey( __CLASS__, $code );
210 $this->srvCache->set( $cacheKey, $cache );
211 }
212
213 /**
214 * Loads messages from caches or from database in this order:
215 * (1) local message cache (if $wgUseLocalMessageCache is enabled)
216 * (2) memcached
217 * (3) from the database.
218 *
219 * When successfully loading from (2) or (3), all higher level caches are
220 * updated for the newest version.
221 *
222 * Nothing is loaded if member variable mDisable is true, either manually
223 * set by calling code or if message loading fails (is this possible?).
224 *
225 * Returns true if cache is already populated or it was successfully populated,
226 * or false if populating empty cache fails. Also returns true if MessageCache
227 * is disabled.
228 *
229 * @param string $code Language to which load messages
230 * @param int|null $mode Use MessageCache::FOR_UPDATE to skip process cache [optional]
231 * @throws InvalidArgumentException
232 * @return bool
233 */
234 protected function load( $code, $mode = null ) {
235 if ( !is_string( $code ) ) {
236 throw new InvalidArgumentException( "Missing language code" );
237 }
238
239 # Don't do double loading...
240 if ( isset( $this->loadedLanguages[$code] ) && $mode != self::FOR_UPDATE ) {
241 return true;
242 }
243
244 $this->overridable = array_flip( Language::getMessageKeysFor( $code ) );
245
246 # 8 lines of code just to say (once) that message cache is disabled
247 if ( $this->mDisable ) {
248 static $shownDisabled = false;
249 if ( !$shownDisabled ) {
250 wfDebug( __METHOD__ . ": disabled\n" );
251 $shownDisabled = true;
252 }
253
254 return true;
255 }
256
257 # Loading code starts
258 $success = false; # Keep track of success
259 $staleCache = false; # a cache array with expired data, or false if none has been loaded
260 $where = []; # Debug info, delayed to avoid spamming debug log too much
261
262 # Hash of the contents is stored in memcache, to detect if data-center cache
263 # or local cache goes out of date (e.g. due to replace() on some other server)
264 list( $hash, $hashVolatile ) = $this->getValidationHash( $code );
265 $this->cacheVolatile[$code] = $hashVolatile;
266
267 # Try the local cache and check against the cluster hash key...
268 $cache = $this->getLocalCache( $code );
269 if ( !$cache ) {
270 $where[] = 'local cache is empty';
271 } elseif ( !isset( $cache['HASH'] ) || $cache['HASH'] !== $hash ) {
272 $where[] = 'local cache has the wrong hash';
273 $staleCache = $cache;
274 } elseif ( $this->isCacheExpired( $cache ) ) {
275 $where[] = 'local cache is expired';
276 $staleCache = $cache;
277 } elseif ( $hashVolatile ) {
278 $where[] = 'local cache validation key is expired/volatile';
279 $staleCache = $cache;
280 } else {
281 $where[] = 'got from local cache';
282 $this->cache->set( $code, $cache );
283 $success = true;
284 }
285
286 if ( !$success ) {
287 $cacheKey = $this->clusterCache->makeKey( 'messages', $code );
288 # Try the global cache. If it is empty, try to acquire a lock. If
289 # the lock can't be acquired, wait for the other thread to finish
290 # and then try the global cache a second time.
291 for ( $failedAttempts = 0; $failedAttempts <= 1; $failedAttempts++ ) {
292 if ( $hashVolatile && $staleCache ) {
293 # Do not bother fetching the whole cache blob to avoid I/O.
294 # Instead, just try to get the non-blocking $statusKey lock
295 # below, and use the local stale value if it was not acquired.
296 $where[] = 'global cache is presumed expired';
297 } else {
298 $cache = $this->clusterCache->get( $cacheKey );
299 if ( !$cache ) {
300 $where[] = 'global cache is empty';
301 } elseif ( $this->isCacheExpired( $cache ) ) {
302 $where[] = 'global cache is expired';
303 $staleCache = $cache;
304 } elseif ( $hashVolatile ) {
305 # DB results are replica DB lag prone until the holdoff TTL passes.
306 # By then, updates should be reflected in loadFromDBWithLock().
307 # One thread regenerates the cache while others use old values.
308 $where[] = 'global cache is expired/volatile';
309 $staleCache = $cache;
310 } else {
311 $where[] = 'got from global cache';
312 $this->cache->set( $code, $cache );
313 $this->saveToCaches( $cache, 'local-only', $code );
314 $success = true;
315 }
316 }
317
318 if ( $success ) {
319 # Done, no need to retry
320 break;
321 }
322
323 # We need to call loadFromDB. Limit the concurrency to one process.
324 # This prevents the site from going down when the cache expires.
325 # Note that the DB slam protection lock here is non-blocking.
326 $loadStatus = $this->loadFromDBWithLock( $code, $where, $mode );
327 if ( $loadStatus === true ) {
328 $success = true;
329 break;
330 } elseif ( $staleCache ) {
331 # Use the stale cache while some other thread constructs the new one
332 $where[] = 'using stale cache';
333 $this->cache->set( $code, $staleCache );
334 $success = true;
335 break;
336 } elseif ( $failedAttempts > 0 ) {
337 # Already blocked once, so avoid another lock/unlock cycle.
338 # This case will typically be hit if memcached is down, or if
339 # loadFromDB() takes longer than LOCK_WAIT.
340 $where[] = "could not acquire status key.";
341 break;
342 } elseif ( $loadStatus === 'cantacquire' ) {
343 # Wait for the other thread to finish, then retry. Normally,
344 # the memcached get() will then yield the other thread's result.
345 $where[] = 'waited for other thread to complete';
346 $this->getReentrantScopedLock( $cacheKey );
347 } else {
348 # Disable cache; $loadStatus is 'disabled'
349 break;
350 }
351 }
352 }
353
354 if ( !$success ) {
355 $where[] = 'loading FAILED - cache is disabled';
356 $this->mDisable = true;
357 $this->cache->set( $code, [] );
358 wfDebugLog( 'MessageCacheError', __METHOD__ . ": Failed to load $code\n" );
359 # This used to throw an exception, but that led to nasty side effects like
360 # the whole wiki being instantly down if the memcached server died
361 } else {
362 # All good, just record the success
363 $this->loadedLanguages[$code] = true;
364 }
365
366 if ( !$this->cache->has( $code ) ) { // sanity
367 throw new LogicException( "Process cache for '$code' should be set by now." );
368 }
369
370 $info = implode( ', ', $where );
371 wfDebugLog( 'MessageCache', __METHOD__ . ": Loading $code... $info\n" );
372
373 return $success;
374 }
375
376 /**
377 * @param string $code
378 * @param array &$where List of wfDebug() comments
379 * @param int|null $mode Use MessageCache::FOR_UPDATE to use DB_MASTER
380 * @return bool|string True on success or one of ("cantacquire", "disabled")
381 */
382 protected function loadFromDBWithLock( $code, array &$where, $mode = null ) {
383 # If cache updates on all levels fail, give up on message overrides.
384 # This is to avoid easy site outages; see $saveSuccess comments below.
385 $statusKey = $this->clusterCache->makeKey( 'messages', $code, 'status' );
386 $status = $this->clusterCache->get( $statusKey );
387 if ( $status === 'error' ) {
388 $where[] = "could not load; method is still globally disabled";
389 return 'disabled';
390 }
391
392 # Now let's regenerate
393 $where[] = 'loading from database';
394
395 # Lock the cache to prevent conflicting writes.
396 # This lock is non-blocking so stale cache can quickly be used.
397 # Note that load() will call a blocking getReentrantScopedLock()
398 # after this if it really need to wait for any current thread.
399 $cacheKey = $this->clusterCache->makeKey( 'messages', $code );
400 $scopedLock = $this->getReentrantScopedLock( $cacheKey, 0 );
401 if ( !$scopedLock ) {
402 $where[] = 'could not acquire main lock';
403 return 'cantacquire';
404 }
405
406 $cache = $this->loadFromDB( $code, $mode );
407 $this->cache->set( $code, $cache );
408 $saveSuccess = $this->saveToCaches( $cache, 'all', $code );
409
410 if ( !$saveSuccess ) {
411 /**
412 * Cache save has failed.
413 *
414 * There are two main scenarios where this could be a problem:
415 * - The cache is more than the maximum size (typically 1MB compressed).
416 * - Memcached has no space remaining in the relevant slab class. This is
417 * unlikely with recent versions of memcached.
418 *
419 * Either way, if there is a local cache, nothing bad will happen. If there
420 * is no local cache, disabling the message cache for all requests avoids
421 * incurring a loadFromDB() overhead on every request, and thus saves the
422 * wiki from complete downtime under moderate traffic conditions.
423 */
424 if ( $this->srvCache instanceof EmptyBagOStuff ) {
425 $this->clusterCache->set( $statusKey, 'error', 60 * 5 );
426 $where[] = 'could not save cache, disabled globally for 5 minutes';
427 } else {
428 $where[] = "could not save global cache";
429 }
430 }
431
432 return true;
433 }
434
435 /**
436 * Loads cacheable messages from the database. Messages bigger than
437 * $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded
438 * on-demand from the database later.
439 *
440 * @param string $code Language code
441 * @param int|null $mode Use MessageCache::FOR_UPDATE to skip process cache
442 * @return array Loaded messages for storing in caches
443 */
444 protected function loadFromDB( $code, $mode = null ) {
445 global $wgMaxMsgCacheEntrySize, $wgLanguageCode, $wgAdaptiveMessageCache;
446
447 // (T164666) The query here performs really poorly on WMF's
448 // contributions replicas. We don't have a way to say "any group except
449 // contributions", so for the moment let's specify 'api'.
450 // @todo: Get rid of this hack.
451 $dbr = wfGetDB( ( $mode == self::FOR_UPDATE ) ? DB_MASTER : DB_REPLICA, 'api' );
452
453 $cache = [];
454
455 $mostused = []; // list of "<cased message key>/<code>"
456 if ( $wgAdaptiveMessageCache && $code !== $wgLanguageCode ) {
457 if ( !$this->cache->has( $wgLanguageCode ) ) {
458 $this->load( $wgLanguageCode );
459 }
460 $mostused = array_keys( $this->cache->get( $wgLanguageCode ) );
461 foreach ( $mostused as $key => $value ) {
462 $mostused[$key] = "$value/$code";
463 }
464 }
465
466 // Get the list of software-defined messages in core/extensions
467 $overridable = array_flip( Language::getMessageKeysFor( $wgLanguageCode ) );
468
469 // Common conditions
470 $conds = [
471 'page_is_redirect' => 0,
472 'page_namespace' => NS_MEDIAWIKI,
473 ];
474 if ( count( $mostused ) ) {
475 $conds['page_title'] = $mostused;
476 } elseif ( $code !== $wgLanguageCode ) {
477 $conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), '/', $code );
478 } else {
479 # Effectively disallows use of '/' character in NS_MEDIAWIKI for uses
480 # other than language code.
481 $conds[] = 'page_title NOT' .
482 $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() );
483 }
484
485 // Set the stubs for oversized software-defined messages in the main cache map
486 $res = $dbr->select(
487 'page',
488 [ 'page_title', 'page_latest' ],
489 array_merge( $conds, [ 'page_len > ' . intval( $wgMaxMsgCacheEntrySize ) ] ),
490 __METHOD__ . "($code)-big"
491 );
492 foreach ( $res as $row ) {
493 // Include entries/stubs for all keys in $mostused in adaptive mode
494 if ( $wgAdaptiveMessageCache || $this->isMainCacheable( $row->page_title, $overridable ) ) {
495 $cache[$row->page_title] = '!TOO BIG';
496 }
497 // At least include revision ID so page changes are reflected in the hash
498 $cache['EXCESSIVE'][$row->page_title] = $row->page_latest;
499 }
500
501 // Set the text for small software-defined messages in the main cache map
502 $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
503 $revQuery = $revisionStore->getQueryInfo( [ 'page', 'user' ] );
504 $res = $dbr->select(
505 $revQuery['tables'],
506 $revQuery['fields'],
507 array_merge( $conds, [
508 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize ),
509 'page_latest = rev_id' // get the latest revision only
510 ] ),
511 __METHOD__ . "($code)-small",
512 [],
513 $revQuery['joins']
514 );
515 foreach ( $res as $row ) {
516 // Include entries/stubs for all keys in $mostused in adaptive mode
517 if ( $wgAdaptiveMessageCache || $this->isMainCacheable( $row->page_title, $overridable ) ) {
518 try {
519 $rev = $revisionStore->newRevisionFromRow( $row );
520 $content = $rev->getContent( MediaWiki\Revision\SlotRecord::MAIN );
521 $text = $this->getMessageTextFromContent( $content );
522 } catch ( Exception $ex ) {
523 $text = false;
524 }
525
526 if ( !is_string( $text ) ) {
527 $entry = '!ERROR';
528 wfDebugLog(
529 'MessageCache',
530 __METHOD__
531 . ": failed to load message page text for {$row->page_title} ($code)"
532 );
533 } else {
534 $entry = ' ' . $text;
535 }
536 $cache[$row->page_title] = $entry;
537 } else {
538 // T193271: cache object gets too big and slow to generate.
539 // At least include revision ID so page changes are reflected in the hash.
540 $cache['EXCESSIVE'][$row->page_title] = $row->page_latest;
541 }
542 }
543
544 $cache['VERSION'] = MSG_CACHE_VERSION;
545 ksort( $cache );
546
547 # Hash for validating local cache (APC). No need to take into account
548 # messages larger than $wgMaxMsgCacheEntrySize, since those are only
549 # stored and fetched from memcache.
550 $cache['HASH'] = md5( serialize( $cache ) );
551 $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + self::WAN_TTL );
552 unset( $cache['EXCESSIVE'] ); // only needed for hash
553
554 return $cache;
555 }
556
557 /**
558 * @param string $name Message name (possibly with /code suffix)
559 * @param array $overridable Map of (key => unused) for software-defined messages
560 * @return bool
561 */
562 private function isMainCacheable( $name, array $overridable ) {
563 // Convert first letter to lowercase, and strip /code suffix
564 $name = $this->contLang->lcfirst( $name );
565 $msg = preg_replace( '/\/[a-z0-9-]{2,}$/', '', $name );
566 // Include common conversion table pages. This also avoids problems with
567 // Installer::parse() bailing out due to disallowed DB queries (T207979).
568 return ( isset( $overridable[$msg] ) || strpos( $name, 'conversiontable/' ) === 0 );
569 }
570
571 /**
572 * Updates cache as necessary when message page is changed
573 *
574 * @param string $title Message cache key with initial uppercase letter
575 * @param string|bool $text New contents of the page (false if deleted)
576 */
577 public function replace( $title, $text ) {
578 global $wgLanguageCode;
579
580 if ( $this->mDisable ) {
581 return;
582 }
583
584 list( $msg, $code ) = $this->figureMessage( $title );
585 if ( strpos( $title, '/' ) !== false && $code === $wgLanguageCode ) {
586 // Content language overrides do not use the /<code> suffix
587 return;
588 }
589
590 // (a) Update the process cache with the new message text
591 if ( $text === false ) {
592 // Page deleted
593 $this->cache->setField( $code, $title, '!NONEXISTENT' );
594 } else {
595 // Ignore $wgMaxMsgCacheEntrySize so the process cache is up to date
596 $this->cache->setField( $code, $title, ' ' . $text );
597 }
598
599 // (b) Update the shared caches in a deferred update with a fresh DB snapshot
600 DeferredUpdates::addUpdate(
601 new MessageCacheUpdate( $code, $title, $msg ),
602 DeferredUpdates::PRESEND
603 );
604 }
605
606 /**
607 * @param string $code
608 * @param array[] $replacements List of (title, message key) pairs
609 * @throws MWException
610 */
611 public function refreshAndReplaceInternal( $code, array $replacements ) {
612 global $wgMaxMsgCacheEntrySize;
613
614 // Allow one caller at a time to avoid race conditions
615 $scopedLock = $this->getReentrantScopedLock(
616 $this->clusterCache->makeKey( 'messages', $code )
617 );
618 if ( !$scopedLock ) {
619 foreach ( $replacements as list( $title ) ) {
620 LoggerFactory::getInstance( 'MessageCache' )->error(
621 __METHOD__ . ': could not acquire lock to update {title} ({code})',
622 [ 'title' => $title, 'code' => $code ] );
623 }
624
625 return;
626 }
627
628 // Load the existing cache to update it in the local DC cache.
629 // The other DCs will see a hash mismatch.
630 if ( $this->load( $code, self::FOR_UPDATE ) ) {
631 $cache = $this->cache->get( $code );
632 } else {
633 // Err? Fall back to loading from the database.
634 $cache = $this->loadFromDB( $code, self::FOR_UPDATE );
635 }
636 // Check if individual cache keys should exist and update cache accordingly
637 $newTextByTitle = []; // map of (title => content)
638 $newBigTitles = []; // map of (title => latest revision ID), like EXCESSIVE in loadFromDB()
639 foreach ( $replacements as list( $title ) ) {
640 $page = WikiPage::factory( Title::makeTitle( NS_MEDIAWIKI, $title ) );
641 $page->loadPageData( $page::READ_LATEST );
642 $text = $this->getMessageTextFromContent( $page->getContent() );
643 // Remember the text for the blob store update later on
644 $newTextByTitle[$title] = $text;
645 // Note that if $text is false, then $cache should have a !NONEXISTANT entry
646 if ( !is_string( $text ) ) {
647 $cache[$title] = '!NONEXISTENT';
648 } elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
649 $cache[$title] = '!TOO BIG';
650 $newBigTitles[$title] = $page->getLatest();
651 } else {
652 $cache[$title] = ' ' . $text;
653 }
654 }
655 // Update HASH for the new key. Incorporates various administrative keys,
656 // including the old HASH (and thereby the EXCESSIVE value from loadFromDB()
657 // and previous replace() calls), but that doesn't really matter since we
658 // only ever compare it for equality with a copy saved by saveToCaches().
659 $cache['HASH'] = md5( serialize( $cache + [ 'EXCESSIVE' => $newBigTitles ] ) );
660 // Update the too-big WAN cache entries now that we have the new HASH
661 foreach ( $newBigTitles as $title => $id ) {
662 // Match logic of loadCachedMessagePageEntry()
663 $this->wanCache->set(
664 $this->bigMessageCacheKey( $cache['HASH'], $title ),
665 ' ' . $newTextByTitle[$title],
666 self::WAN_TTL
667 );
668 }
669 // Mark this cache as definitely being "latest" (non-volatile) so
670 // load() calls do not try to refresh the cache with replica DB data
671 $cache['LATEST'] = time();
672 // Update the process cache
673 $this->cache->set( $code, $cache );
674 // Pre-emptively update the local datacenter cache so things like edit filter and
675 // blacklist changes are reflected immediately; these often use MediaWiki: pages.
676 // The datacenter handling replace() calls should be the same one handling edits
677 // as they require HTTP POST.
678 $this->saveToCaches( $cache, 'all', $code );
679 // Release the lock now that the cache is saved
680 ScopedCallback::consume( $scopedLock );
681
682 // Relay the purge. Touching this check key expires cache contents
683 // and local cache (APC) validation hash across all datacenters.
684 $this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
685
686 // Purge the messages in the message blob store and fire any hook handlers
687 $blobStore = MediaWikiServices::getInstance()->getResourceLoader()->getMessageBlobStore();
688 foreach ( $replacements as list( $title, $msg ) ) {
689 $blobStore->updateMessage( $this->contLang->lcfirst( $msg ) );
690 Hooks::run( 'MessageCacheReplace', [ $title, $newTextByTitle[$title] ] );
691 }
692 }
693
694 /**
695 * Is the given cache array expired due to time passing or a version change?
696 *
697 * @param array $cache
698 * @return bool
699 */
700 protected function isCacheExpired( $cache ) {
701 if ( !isset( $cache['VERSION'] ) || !isset( $cache['EXPIRY'] ) ) {
702 return true;
703 }
704 if ( $cache['VERSION'] != MSG_CACHE_VERSION ) {
705 return true;
706 }
707 if ( wfTimestampNow() >= $cache['EXPIRY'] ) {
708 return true;
709 }
710
711 return false;
712 }
713
714 /**
715 * Shortcut to update caches.
716 *
717 * @param array $cache Cached messages with a version.
718 * @param string $dest Either "local-only" to save to local caches only
719 * or "all" to save to all caches.
720 * @param string|bool $code Language code (default: false)
721 * @return bool
722 */
723 protected function saveToCaches( array $cache, $dest, $code = false ) {
724 if ( $dest === 'all' ) {
725 $cacheKey = $this->clusterCache->makeKey( 'messages', $code );
726 $success = $this->clusterCache->set( $cacheKey, $cache );
727 $this->setValidationHash( $code, $cache );
728 } else {
729 $success = true;
730 }
731
732 $this->saveToLocalCache( $code, $cache );
733
734 return $success;
735 }
736
737 /**
738 * Get the md5 used to validate the local APC cache
739 *
740 * @param string $code
741 * @return array (hash or false, bool expiry/volatility status)
742 */
743 protected function getValidationHash( $code ) {
744 $curTTL = null;
745 $value = $this->wanCache->get(
746 $this->wanCache->makeKey( 'messages', $code, 'hash', 'v1' ),
747 $curTTL,
748 [ $this->getCheckKey( $code ) ]
749 );
750
751 if ( $value ) {
752 $hash = $value['hash'];
753 if ( ( time() - $value['latest'] ) < WANObjectCache::TTL_MINUTE ) {
754 // Cache was recently updated via replace() and should be up-to-date.
755 // That method is only called in the primary datacenter and uses FOR_UPDATE.
756 // Also, it is unlikely that the current datacenter is *now* secondary one.
757 $expired = false;
758 } else {
759 // See if the "check" key was bumped after the hash was generated
760 $expired = ( $curTTL < 0 );
761 }
762 } else {
763 // No hash found at all; cache must regenerate to be safe
764 $hash = false;
765 $expired = true;
766 }
767
768 return [ $hash, $expired ];
769 }
770
771 /**
772 * Set the md5 used to validate the local disk cache
773 *
774 * If $cache has a 'LATEST' UNIX timestamp key, then the hash will not
775 * be treated as "volatile" by getValidationHash() for the next few seconds.
776 * This is triggered when $cache is generated using FOR_UPDATE mode.
777 *
778 * @param string $code
779 * @param array $cache Cached messages with a version
780 */
781 protected function setValidationHash( $code, array $cache ) {
782 $this->wanCache->set(
783 $this->wanCache->makeKey( 'messages', $code, 'hash', 'v1' ),
784 [
785 'hash' => $cache['HASH'],
786 'latest' => $cache['LATEST'] ?? 0
787 ],
788 WANObjectCache::TTL_INDEFINITE
789 );
790 }
791
792 /**
793 * @param string $key A language message cache key that stores blobs
794 * @param int $timeout Wait timeout in seconds
795 * @return null|ScopedCallback
796 */
797 protected function getReentrantScopedLock( $key, $timeout = self::WAIT_SEC ) {
798 return $this->clusterCache->getScopedLock( $key, $timeout, self::LOCK_TTL, __METHOD__ );
799 }
800
801 /**
802 * Get a message from either the content language or the user language.
803 *
804 * First, assemble a list of languages to attempt getting the message from. This
805 * chain begins with the requested language and its fallbacks and then continues with
806 * the content language and its fallbacks. For each language in the chain, the following
807 * process will occur (in this order):
808 * 1. If a language-specific override, i.e., [[MW:msg/lang]], is available, use that.
809 * Note: for the content language, there is no /lang subpage.
810 * 2. Fetch from the static CDB cache.
811 * 3. If available, check the database for fallback language overrides.
812 *
813 * This process provides a number of guarantees. When changing this code, make sure all
814 * of these guarantees are preserved.
815 * * If the requested language is *not* the content language, then the CDB cache for that
816 * specific language will take precedence over the root database page ([[MW:msg]]).
817 * * Fallbacks will be just that: fallbacks. A fallback language will never be reached if
818 * the message is available *anywhere* in the language for which it is a fallback.
819 *
820 * @param string $key The message key
821 * @param bool $useDB If true, look for the message in the DB, false
822 * to use only the compiled l10n cache.
823 * @param bool|string|object $langcode Code of the language to get the message for.
824 * - If string and a valid code, will create a standard language object
825 * - If string but not a valid code, will create a basic language object
826 * - If boolean and false, create object from the current users language
827 * - If boolean and true, create object from the wikis content language
828 * - If language object, use it as given
829 *
830 * @throws MWException When given an invalid key
831 * @return string|bool False if the message doesn't exist, otherwise the
832 * message (which can be empty)
833 */
834 function get( $key, $useDB = true, $langcode = true ) {
835 if ( is_int( $key ) ) {
836 // Fix numerical strings that somehow become ints
837 // on their way here
838 $key = (string)$key;
839 } elseif ( !is_string( $key ) ) {
840 throw new MWException( 'Non-string key given' );
841 } elseif ( $key === '' ) {
842 // Shortcut: the empty key is always missing
843 return false;
844 }
845
846 // Normalise title-case input (with some inlining)
847 $lckey = self::normalizeKey( $key );
848
849 Hooks::run( 'MessageCache::get', [ &$lckey ] );
850
851 // Loop through each language in the fallback list until we find something useful
852 $message = $this->getMessageFromFallbackChain(
853 wfGetLangObj( $langcode ),
854 $lckey,
855 !$this->mDisable && $useDB
856 );
857
858 // If we still have no message, maybe the key was in fact a full key so try that
859 if ( $message === false ) {
860 $parts = explode( '/', $lckey );
861 // We may get calls for things that are http-urls from sidebar
862 // Let's not load nonexistent languages for those
863 // They usually have more than one slash.
864 if ( count( $parts ) == 2 && $parts[1] !== '' ) {
865 $message = Language::getMessageFor( $parts[0], $parts[1] );
866 if ( $message === null ) {
867 $message = false;
868 }
869 }
870 }
871
872 // Post-processing if the message exists
873 if ( $message !== false ) {
874 // Fix whitespace
875 $message = str_replace(
876 [
877 # Fix for trailing whitespace, removed by textarea
878 '&#32;',
879 # Fix for NBSP, converted to space by firefox
880 '&nbsp;',
881 '&#160;',
882 '&shy;'
883 ],
884 [
885 ' ',
886 "\u{00A0}",
887 "\u{00A0}",
888 "\u{00AD}"
889 ],
890 $message
891 );
892 }
893
894 return $message;
895 }
896
897 /**
898 * Given a language, try and fetch messages from that language.
899 *
900 * Will also consider fallbacks of that language, the site language, and fallbacks for
901 * the site language.
902 *
903 * @see MessageCache::get
904 * @param Language|StubObject $lang Preferred language
905 * @param string $lckey Lowercase key for the message (as for localisation cache)
906 * @param bool $useDB Whether to include messages from the wiki database
907 * @return string|bool The message, or false if not found
908 */
909 protected function getMessageFromFallbackChain( $lang, $lckey, $useDB ) {
910 $alreadyTried = [];
911
912 // First try the requested language.
913 $message = $this->getMessageForLang( $lang, $lckey, $useDB, $alreadyTried );
914 if ( $message !== false ) {
915 return $message;
916 }
917
918 // Now try checking the site language.
919 $message = $this->getMessageForLang( $this->contLang, $lckey, $useDB, $alreadyTried );
920 return $message;
921 }
922
923 /**
924 * Given a language, try and fetch messages from that language and its fallbacks.
925 *
926 * @see MessageCache::get
927 * @param Language|StubObject $lang Preferred language
928 * @param string $lckey Lowercase key for the message (as for localisation cache)
929 * @param bool $useDB Whether to include messages from the wiki database
930 * @param bool[] $alreadyTried Contains true for each language that has been tried already
931 * @return string|bool The message, or false if not found
932 */
933 private function getMessageForLang( $lang, $lckey, $useDB, &$alreadyTried ) {
934 $langcode = $lang->getCode();
935
936 // Try checking the database for the requested language
937 if ( $useDB ) {
938 $uckey = $this->contLang->ucfirst( $lckey );
939
940 if ( !isset( $alreadyTried[$langcode] ) ) {
941 $message = $this->getMsgFromNamespace(
942 $this->getMessagePageName( $langcode, $uckey ),
943 $langcode
944 );
945 if ( $message !== false ) {
946 return $message;
947 }
948 $alreadyTried[$langcode] = true;
949 }
950 } else {
951 $uckey = null;
952 }
953
954 // Check the CDB cache
955 $message = $lang->getMessage( $lckey );
956 if ( $message !== null ) {
957 return $message;
958 }
959
960 // Try checking the database for all of the fallback languages
961 if ( $useDB ) {
962 $fallbackChain = Language::getFallbacksFor( $langcode );
963
964 foreach ( $fallbackChain as $code ) {
965 if ( isset( $alreadyTried[$code] ) ) {
966 continue;
967 }
968
969 $message = $this->getMsgFromNamespace(
970 $this->getMessagePageName( $code, $uckey ), $code );
971
972 if ( $message !== false ) {
973 return $message;
974 }
975 $alreadyTried[$code] = true;
976 }
977 }
978
979 return false;
980 }
981
982 /**
983 * Get the message page name for a given language
984 *
985 * @param string $langcode
986 * @param string $uckey Uppercase key for the message
987 * @return string The page name
988 */
989 private function getMessagePageName( $langcode, $uckey ) {
990 global $wgLanguageCode;
991
992 if ( $langcode === $wgLanguageCode ) {
993 // Messages created in the content language will not have the /lang extension
994 return $uckey;
995 } else {
996 return "$uckey/$langcode";
997 }
998 }
999
1000 /**
1001 * Get a message from the MediaWiki namespace, with caching. The key must
1002 * first be converted to two-part lang/msg form if necessary.
1003 *
1004 * Unlike self::get(), this function doesn't resolve fallback chains, and
1005 * some callers require this behavior. LanguageConverter::parseCachedTable()
1006 * and self::get() are some examples in core.
1007 *
1008 * @param string $title Message cache key with initial uppercase letter
1009 * @param string $code Code denoting the language to try
1010 * @return string|bool The message, or false if it does not exist or on error
1011 */
1012 public function getMsgFromNamespace( $title, $code ) {
1013 // Load all MediaWiki page definitions into cache. Note that individual keys
1014 // already loaded into cache during this request remain in the cache, which
1015 // includes the value of hook-defined messages.
1016 $this->load( $code );
1017
1018 $entry = $this->cache->getField( $code, $title );
1019
1020 if ( $entry !== null ) {
1021 // Message page exists as an override of a software messages
1022 if ( substr( $entry, 0, 1 ) === ' ' ) {
1023 // The message exists and is not '!TOO BIG' or '!ERROR'
1024 return (string)substr( $entry, 1 );
1025 } elseif ( $entry === '!NONEXISTENT' ) {
1026 // The text might be '-' or missing due to some data loss
1027 return false;
1028 }
1029 // Load the message page, utilizing the individual message cache.
1030 // If the page does not exist, there will be no hook handler fallbacks.
1031 $entry = $this->loadCachedMessagePageEntry(
1032 $title,
1033 $code,
1034 $this->cache->getField( $code, 'HASH' )
1035 );
1036 } else {
1037 // Message page either does not exist or does not override a software message
1038 if ( !$this->isMainCacheable( $title, $this->overridable ) ) {
1039 // Message page does not override any software-defined message. A custom
1040 // message might be defined to have content or settings specific to the wiki.
1041 // Load the message page, utilizing the individual message cache as needed.
1042 $entry = $this->loadCachedMessagePageEntry(
1043 $title,
1044 $code,
1045 $this->cache->getField( $code, 'HASH' )
1046 );
1047 }
1048 if ( $entry === null || substr( $entry, 0, 1 ) !== ' ' ) {
1049 // Message does not have a MediaWiki page definition; try hook handlers
1050 $message = false;
1051 Hooks::run( 'MessagesPreLoad', [ $title, &$message, $code ] );
1052 if ( $message !== false ) {
1053 $this->cache->setField( $code, $title, ' ' . $message );
1054 } else {
1055 $this->cache->setField( $code, $title, '!NONEXISTENT' );
1056 }
1057
1058 return $message;
1059 }
1060 }
1061
1062 if ( $entry !== false && substr( $entry, 0, 1 ) === ' ' ) {
1063 if ( $this->cacheVolatile[$code] ) {
1064 // Make sure that individual keys respect the WAN cache holdoff period too
1065 LoggerFactory::getInstance( 'MessageCache' )->debug(
1066 __METHOD__ . ': loading volatile key \'{titleKey}\'',
1067 [ 'titleKey' => $title, 'code' => $code ] );
1068 } else {
1069 $this->cache->setField( $code, $title, $entry );
1070 }
1071 // The message exists, so make sure a string is returned
1072 return (string)substr( $entry, 1 );
1073 }
1074
1075 $this->cache->setField( $code, $title, '!NONEXISTENT' );
1076
1077 return false;
1078 }
1079
1080 /**
1081 * @param string $dbKey
1082 * @param string $code
1083 * @param string $hash
1084 * @return string Either " <MESSAGE>" or "!NONEXISTANT"
1085 */
1086 private function loadCachedMessagePageEntry( $dbKey, $code, $hash ) {
1087 $fname = __METHOD__;
1088 return $this->srvCache->getWithSetCallback(
1089 $this->srvCache->makeKey( 'messages-big', $hash, $dbKey ),
1090 BagOStuff::TTL_MINUTE,
1091 function () use ( $code, $dbKey, $hash, $fname ) {
1092 return $this->wanCache->getWithSetCallback(
1093 $this->bigMessageCacheKey( $hash, $dbKey ),
1094 self::WAN_TTL,
1095 function ( $oldValue, &$ttl, &$setOpts ) use ( $dbKey, $code, $fname ) {
1096 // Try loading the message from the database
1097 $dbr = wfGetDB( DB_REPLICA );
1098 $setOpts += Database::getCacheSetOptions( $dbr );
1099 // Use newKnownCurrent() to avoid querying revision/user tables
1100 $title = Title::makeTitle( NS_MEDIAWIKI, $dbKey );
1101 $revision = Revision::newKnownCurrent( $dbr, $title );
1102 if ( !$revision ) {
1103 // The wiki doesn't have a local override page. Cache absence with normal TTL.
1104 // When overrides are created, self::replace() takes care of the cache.
1105 return '!NONEXISTENT';
1106 }
1107 $content = $revision->getContent();
1108 if ( $content ) {
1109 $message = $this->getMessageTextFromContent( $content );
1110 } else {
1111 LoggerFactory::getInstance( 'MessageCache' )->warning(
1112 $fname . ': failed to load page text for \'{titleKey}\'',
1113 [ 'titleKey' => $dbKey, 'code' => $code ]
1114 );
1115 $message = null;
1116 }
1117
1118 if ( !is_string( $message ) ) {
1119 // Revision failed to load Content, or Content is incompatible with wikitext.
1120 // Possibly a temporary loading failure.
1121 $ttl = 5;
1122
1123 return '!NONEXISTENT';
1124 }
1125
1126 return ' ' . $message;
1127 }
1128 );
1129 }
1130 );
1131 }
1132
1133 /**
1134 * @param string $message
1135 * @param bool $interface
1136 * @param Language|null $language
1137 * @param Title|null $title
1138 * @return string
1139 */
1140 public function transform( $message, $interface = false, $language = null, $title = null ) {
1141 // Avoid creating parser if nothing to transform
1142 if ( strpos( $message, '{{' ) === false ) {
1143 return $message;
1144 }
1145
1146 if ( $this->mInParser ) {
1147 return $message;
1148 }
1149
1150 $parser = $this->getParser();
1151 if ( $parser ) {
1152 $popts = $this->getParserOptions();
1153 $popts->setInterfaceMessage( $interface );
1154 $popts->setTargetLanguage( $language );
1155
1156 $userlang = $popts->setUserLang( $language );
1157 $this->mInParser = true;
1158 $message = $parser->transformMsg( $message, $popts, $title );
1159 $this->mInParser = false;
1160 $popts->setUserLang( $userlang );
1161 }
1162
1163 return $message;
1164 }
1165
1166 /**
1167 * @return Parser
1168 */
1169 public function getParser() {
1170 global $wgParserConf;
1171 if ( !$this->mParser ) {
1172 $parser = MediaWikiServices::getInstance()->getParser();
1173 # Do some initialisation so that we don't have to do it twice
1174 $parser->firstCallInit();
1175 # Clone it and store it
1176 $class = $wgParserConf['class'];
1177 if ( $class == ParserDiffTest::class ) {
1178 # Uncloneable
1179 $this->mParser = new $class( $wgParserConf );
1180 } else {
1181 $this->mParser = clone $parser;
1182 }
1183 }
1184
1185 return $this->mParser;
1186 }
1187
1188 /**
1189 * @param string $text
1190 * @param Title|null $title
1191 * @param bool $linestart Whether or not this is at the start of a line
1192 * @param bool $interface Whether this is an interface message
1193 * @param Language|string|null $language Language code
1194 * @return ParserOutput|string
1195 */
1196 public function parse( $text, $title = null, $linestart = true,
1197 $interface = false, $language = null
1198 ) {
1199 global $wgTitle;
1200
1201 if ( $this->mInParser ) {
1202 return htmlspecialchars( $text );
1203 }
1204
1205 $parser = $this->getParser();
1206 $popts = $this->getParserOptions();
1207 $popts->setInterfaceMessage( $interface );
1208
1209 if ( is_string( $language ) ) {
1210 $language = Language::factory( $language );
1211 }
1212 $popts->setTargetLanguage( $language );
1213
1214 if ( !$title || !$title instanceof Title ) {
1215 wfDebugLog( 'GlobalTitleFail', __METHOD__ . ' called by ' .
1216 wfGetAllCallers( 6 ) . ' with no title set.' );
1217 $title = $wgTitle;
1218 }
1219 // Sometimes $wgTitle isn't set either...
1220 if ( !$title ) {
1221 # It's not uncommon having a null $wgTitle in scripts. See r80898
1222 # Create a ghost title in such case
1223 $title = Title::makeTitle( NS_SPECIAL, 'Badtitle/title not set in ' . __METHOD__ );
1224 }
1225
1226 $this->mInParser = true;
1227 $res = $parser->parse( $text, $title, $popts, $linestart );
1228 $this->mInParser = false;
1229
1230 return $res;
1231 }
1232
1233 public function disable() {
1234 $this->mDisable = true;
1235 }
1236
1237 public function enable() {
1238 $this->mDisable = false;
1239 }
1240
1241 /**
1242 * Whether DB/cache usage is disabled for determining messages
1243 *
1244 * If so, this typically indicates either:
1245 * - a) load() failed to find a cached copy nor query the DB
1246 * - b) we are in a special context or error mode that cannot use the DB
1247 * If the DB is ignored, any derived HTML output or cached objects may be wrong.
1248 * To avoid long-term cache pollution, TTLs can be adjusted accordingly.
1249 *
1250 * @return bool
1251 * @since 1.27
1252 */
1253 public function isDisabled() {
1254 return $this->mDisable;
1255 }
1256
1257 /**
1258 * Clear all stored messages in global and local cache
1259 *
1260 * Mainly used after a mass rebuild
1261 */
1262 public function clear() {
1263 $langs = Language::fetchLanguageNames( null, 'mw' );
1264 foreach ( array_keys( $langs ) as $code ) {
1265 $this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
1266 }
1267 $this->cache->clear();
1268 $this->loadedLanguages = [];
1269 }
1270
1271 /**
1272 * @param string $key
1273 * @return array
1274 */
1275 public function figureMessage( $key ) {
1276 global $wgLanguageCode;
1277
1278 $pieces = explode( '/', $key );
1279 if ( count( $pieces ) < 2 ) {
1280 return [ $key, $wgLanguageCode ];
1281 }
1282
1283 $lang = array_pop( $pieces );
1284 if ( !Language::fetchLanguageName( $lang, null, 'mw' ) ) {
1285 return [ $key, $wgLanguageCode ];
1286 }
1287
1288 $message = implode( '/', $pieces );
1289
1290 return [ $message, $lang ];
1291 }
1292
1293 /**
1294 * Get all message keys stored in the message cache for a given language.
1295 * If $code is the content language code, this will return all message keys
1296 * for which MediaWiki:msgkey exists. If $code is another language code, this
1297 * will ONLY return message keys for which MediaWiki:msgkey/$code exists.
1298 * @param string $code Language code
1299 * @return array Array of message keys (strings)
1300 */
1301 public function getAllMessageKeys( $code ) {
1302 $this->load( $code );
1303 if ( !$this->cache->has( $code ) ) {
1304 // Apparently load() failed
1305 return null;
1306 }
1307 // Remove administrative keys
1308 $cache = $this->cache->get( $code );
1309 unset( $cache['VERSION'] );
1310 unset( $cache['EXPIRY'] );
1311 unset( $cache['EXCESSIVE'] );
1312 // Remove any !NONEXISTENT keys
1313 $cache = array_diff( $cache, [ '!NONEXISTENT' ] );
1314
1315 // Keys may appear with a capital first letter. lcfirst them.
1316 return array_map( [ $this->contLang, 'lcfirst' ], array_keys( $cache ) );
1317 }
1318
1319 /**
1320 * Purge message caches when a MediaWiki: page is created, updated, or deleted
1321 *
1322 * @param Title $title Message page title
1323 * @param Content|null $content New content for edit/create, null on deletion
1324 * @since 1.29
1325 */
1326 public function updateMessageOverride( Title $title, Content $content = null ) {
1327 $msgText = $this->getMessageTextFromContent( $content );
1328 if ( $msgText === null ) {
1329 $msgText = false; // treat as not existing
1330 }
1331
1332 $this->replace( $title->getDBkey(), $msgText );
1333
1334 if ( $this->contLang->hasVariants() ) {
1335 $this->contLang->updateConversionTable( $title );
1336 }
1337 }
1338
1339 /**
1340 * @param string $code Language code
1341 * @return string WAN cache key usable as a "check key" against language page edits
1342 */
1343 public function getCheckKey( $code ) {
1344 return $this->wanCache->makeKey( 'messages', $code );
1345 }
1346
1347 /**
1348 * @param Content|null $content Content or null if the message page does not exist
1349 * @return string|bool|null Returns false if $content is null and null on error
1350 */
1351 private function getMessageTextFromContent( Content $content = null ) {
1352 // @TODO: could skip pseudo-messages like js/css here, based on content model
1353 if ( $content ) {
1354 // Message page exists...
1355 // XXX: Is this the right way to turn a Content object into a message?
1356 // NOTE: $content is typically either WikitextContent, JavaScriptContent or
1357 // CssContent. MessageContent is *not* used for storing messages, it's
1358 // only used for wrapping them when needed.
1359 $msgText = $content->getWikitextForTransclusion();
1360 if ( $msgText === false || $msgText === null ) {
1361 // This might be due to some kind of misconfiguration...
1362 $msgText = null;
1363 LoggerFactory::getInstance( 'MessageCache' )->warning(
1364 __METHOD__ . ": message content doesn't provide wikitext "
1365 . "(content model: " . $content->getModel() . ")" );
1366 }
1367 } else {
1368 // Message page does not exist...
1369 $msgText = false;
1370 }
1371
1372 return $msgText;
1373 }
1374
1375 /**
1376 * @param string $hash Hash for this version of the entire key/value overrides map
1377 * @param string $title Message cache key with initial uppercase letter
1378 * @return string
1379 */
1380 private function bigMessageCacheKey( $hash, $title ) {
1381 return $this->wanCache->makeKey( 'messages-big', $hash, $title );
1382 }
1383 }