Completed
Branch master (86dc85)
by
unknown
23:45
created

MessageCache::getMessageFromFallbackChain()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
rs 9.4285
cc 2
eloc 8
nc 2
nop 3
1
<?php
2
/**
3
 * Localisation messages cache.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Cache
22
 */
23
24
/**
25
 * MediaWiki message cache structure version.
26
 * Bump this whenever the message cache format has changed.
27
 */
28
define( 'MSG_CACHE_VERSION', 2 );
29
30
/**
31
 * Message cache
32
 * Performs various MediaWiki namespace-related functions
33
 * @ingroup Cache
34
 */
35
class MessageCache {
36
	const FOR_UPDATE = 1; // force message reload
37
38
	/** How long to wait for memcached locks */
39
	const WAIT_SEC = 15;
40
	/** How long memcached locks last */
41
	const LOCK_TTL = 30;
42
43
	/**
44
	 * Process local cache of loaded messages that are defined in
45
	 * MediaWiki namespace. First array level is a language code,
46
	 * second level is message key and the values are either message
47
	 * content prefixed with space, or !NONEXISTENT for negative
48
	 * caching.
49
	 * @var array $mCache
50
	 */
51
	protected $mCache;
52
53
	/**
54
	 * Should  mean that database cannot be used, but check
55
	 * @var bool $mDisable
56
	 */
57
	protected $mDisable;
58
59
	/**
60
	 * Lifetime for cache, used by object caching.
61
	 * Set on construction, see __construct().
62
	 */
63
	protected $mExpiry;
64
65
	/**
66
	 * Message cache has its own parser which it uses to transform
67
	 * messages.
68
	 */
69
	protected $mParserOptions, $mParser;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
70
71
	/**
72
	 * Variable for tracking which variables are already loaded
73
	 * @var array $mLoadedLanguages
74
	 */
75
	protected $mLoadedLanguages = [];
76
77
	/**
78
	 * @var bool $mInParser
79
	 */
80
	protected $mInParser = false;
81
82
	/** @var BagOStuff */
83
	protected $mMemc;
84
	/** @var WANObjectCache */
85
	protected $wanCache;
86
87
	/**
88
	 * Singleton instance
89
	 *
90
	 * @var MessageCache $instance
91
	 */
92
	private static $instance;
93
94
	/**
95
	 * Get the signleton instance of this class
96
	 *
97
	 * @since 1.18
98
	 * @return MessageCache
99
	 */
100
	public static function singleton() {
101
		if ( self::$instance === null ) {
102
			global $wgUseDatabaseMessages, $wgMsgCacheExpiry;
103
			self::$instance = new self(
104
				wfGetMessageCacheStorage(),
105
				$wgUseDatabaseMessages,
106
				$wgMsgCacheExpiry
107
			);
108
		}
109
110
		return self::$instance;
111
	}
112
113
	/**
114
	 * Destroy the singleton instance
115
	 *
116
	 * @since 1.18
117
	 */
118
	public static function destroyInstance() {
119
		self::$instance = null;
120
	}
121
122
	/**
123
	 * Normalize message key input
124
	 *
125
	 * @param string $key Input message key to be normalized
126
	 * @return string Normalized message key
127
	 */
128
	public static function normalizeKey( $key ) {
129
		global $wgContLang;
130
		$lckey = strtr( $key, ' ', '_' );
131
		if ( ord( $lckey ) < 128 ) {
132
			$lckey[0] = strtolower( $lckey[0] );
133
		} else {
134
			$lckey = $wgContLang->lcfirst( $lckey );
135
		}
136
137
		return $lckey;
138
	}
139
140
	/**
141
	 * @param BagOStuff $memCached A cache instance. If none, fall back to CACHE_NONE.
142
	 * @param bool $useDB
143
	 * @param int $expiry Lifetime for cache. @see $mExpiry.
144
	 */
145
	function __construct( $memCached, $useDB, $expiry ) {
146
		global $wgUseLocalMessageCache;
147
148
		if ( !$memCached ) {
149
			$memCached = wfGetCache( CACHE_NONE );
150
		}
151
152
		$this->mMemc = $memCached;
153
		$this->mDisable = !$useDB;
154
		$this->mExpiry = $expiry;
155
156
		if ( $wgUseLocalMessageCache ) {
157
			$this->localCache = ObjectCache::getLocalServerInstance( CACHE_NONE );
0 ignored issues
show
Bug introduced by
The property localCache does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
158
		} else {
159
			$this->localCache = wfGetCache( CACHE_NONE );
160
		}
161
162
		$this->wanCache = ObjectCache::getMainWANInstance();
163
	}
164
165
	/**
166
	 * ParserOptions is lazy initialised.
167
	 *
168
	 * @return ParserOptions
169
	 */
170
	function getParserOptions() {
171
		global $wgUser;
172
173
		if ( !$this->mParserOptions ) {
174
			if ( !$wgUser->isSafeToLoad() ) {
175
				// $wgUser isn't unstubbable yet, so don't try to get a
176
				// ParserOptions for it. And don't cache this ParserOptions
177
				// either.
178
				$po = ParserOptions::newFromAnon();
179
				$po->setEditSection( false );
180
				return $po;
181
			}
182
183
			$this->mParserOptions = new ParserOptions;
184
			$this->mParserOptions->setEditSection( false );
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 documenation of load().
194
	 * @return array|bool The cache array, or false if not in cache.
195
	 */
196
	protected function getLocalCache( $code ) {
197
		$cacheKey = wfMemcKey( __CLASS__, $code );
198
199
		return $this->localCache->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 = wfMemcKey( __CLASS__, $code );
210
		$this->localCache->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 succesfully 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 succesfully populated,
226
	 * or false if populating empty cache fails. Also returns true if MessageCache
227
	 * is disabled.
228
	 *
229
	 * @param bool|string $code Language to which load messages
230
	 * @param integer $mode Use MessageCache::FOR_UPDATE to skip process cache
231
	 * @throws MWException
232
	 * @return bool
233
	 */
234
	function load( $code = false, $mode = null ) {
235
		if ( !is_string( $code ) ) {
236
			# This isn't really nice, so at least make a note about it and try to
237
			# fall back
238
			wfDebug( __METHOD__ . " called without providing a language code\n" );
239
			$code = 'en';
240
		}
241
242
		# Don't do double loading...
243
		if ( isset( $this->mLoadedLanguages[$code] ) && $mode != self::FOR_UPDATE ) {
244
			return true;
245
		}
246
247
		# 8 lines of code just to say (once) that message cache is disabled
248
		if ( $this->mDisable ) {
249
			static $shownDisabled = false;
250
			if ( !$shownDisabled ) {
251
				wfDebug( __METHOD__ . ": disabled\n" );
252
				$shownDisabled = true;
253
			}
254
255
			return true;
256
		}
257
258
		# Loading code starts
259
		$success = false; # Keep track of success
260
		$staleCache = false; # a cache array with expired data, or false if none has been loaded
261
		$where = []; # Debug info, delayed to avoid spamming debug log too much
262
263
		# Hash of the contents is stored in memcache, to detect if data-center cache
264
		# or local cache goes out of date (e.g. due to replace() on some other server)
265
		list( $hash, $hashVolatile ) = $this->getValidationHash( $code );
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 ) ) {
0 ignored issues
show
Bug introduced by
It seems like $cache defined by $this->getLocalCache($code) on line 268 can also be of type boolean; however, MessageCache::isCacheExpired() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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
			$success = true;
283
			$this->mCache[$code] = $cache;
284
		}
285
286
		if ( !$success ) {
287
			$cacheKey = wfMemcKey( 'messages', $code ); # Key in memc for messages
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->mMemc->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 slave lag prone until the holdoff TTL passes.
306
						# By then, updates should be reflected in loadFromDBWithLock().
307
						# One thread renerates 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->mCache[$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->mCache[$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 yeild 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->mCache = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type array of property $mCache.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
358
			# This used to throw an exception, but that led to nasty side effects like
359
			# the whole wiki being instantly down if the memcached server died
360
		} else {
361
			# All good, just record the success
362
			$this->mLoadedLanguages[$code] = true;
363
		}
364
365
		$info = implode( ', ', $where );
366
		wfDebugLog( 'MessageCache', __METHOD__ . ": Loading $code... $info\n" );
367
368
		return $success;
369
	}
370
371
	/**
372
	 * @param string $code
373
	 * @param array $where List of wfDebug() comments
374
	 * @param integer $mode Use MessageCache::FOR_UPDATE to use DB_MASTER
375
	 * @return bool|string True on success or one of ("cantacquire", "disabled")
376
	 */
377
	protected function loadFromDBWithLock( $code, array &$where, $mode = null ) {
378
		global $wgUseLocalMessageCache;
379
380
		# If cache updates on all levels fail, give up on message overrides.
381
		# This is to avoid easy site outages; see $saveSuccess comments below.
382
		$statusKey = wfMemcKey( 'messages', $code, 'status' );
383
		$status = $this->mMemc->get( $statusKey );
384
		if ( $status === 'error' ) {
385
			$where[] = "could not load; method is still globally disabled";
386
			return 'disabled';
387
		}
388
389
		# Now let's regenerate
390
		$where[] = 'loading from database';
391
392
		# Lock the cache to prevent conflicting writes.
393
		# This lock is non-blocking so stale cache can quickly be used.
394
		# Note that load() will call a blocking getReentrantScopedLock()
395
		# after this if it really need to wait for any current thread.
396
		$cacheKey = wfMemcKey( 'messages', $code );
397
		$scopedLock = $this->getReentrantScopedLock( $cacheKey, 0 );
398
		if ( !$scopedLock ) {
399
			$where[] = 'could not acquire main lock';
400
			return 'cantacquire';
401
		}
402
403
		$cache = $this->loadFromDB( $code, $mode );
404
		$this->mCache[$code] = $cache;
405
		$saveSuccess = $this->saveToCaches( $cache, 'all', $code );
406
407
		if ( !$saveSuccess ) {
408
			/**
409
			 * Cache save has failed.
410
			 *
411
			 * There are two main scenarios where this could be a problem:
412
			 * - The cache is more than the maximum size (typically 1MB compressed).
413
			 * - Memcached has no space remaining in the relevant slab class. This is
414
			 *   unlikely with recent versions of memcached.
415
			 *
416
			 * Either way, if there is a local cache, nothing bad will happen. If there
417
			 * is no local cache, disabling the message cache for all requests avoids
418
			 * incurring a loadFromDB() overhead on every request, and thus saves the
419
			 * wiki from complete downtime under moderate traffic conditions.
420
			 */
421
			if ( !$wgUseLocalMessageCache ) {
422
				$this->mMemc->set( $statusKey, 'error', 60 * 5 );
423
				$where[] = 'could not save cache, disabled globally for 5 minutes';
424
			} else {
425
				$where[] = "could not save global cache";
426
			}
427
		}
428
429
		return true;
430
	}
431
432
	/**
433
	 * Loads cacheable messages from the database. Messages bigger than
434
	 * $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded
435
	 * on-demand from the database later.
436
	 *
437
	 * @param string $code Language code
438
	 * @param integer $mode Use MessageCache::FOR_UPDATE to skip process cache
439
	 * @return array Loaded messages for storing in caches
440
	 */
441
	function loadFromDB( $code, $mode = null ) {
442
		global $wgMaxMsgCacheEntrySize, $wgLanguageCode, $wgAdaptiveMessageCache;
443
444
		$dbr = wfGetDB( ( $mode == self::FOR_UPDATE ) ? DB_MASTER : DB_SLAVE );
445
446
		$cache = [];
447
448
		# Common conditions
449
		$conds = [
450
			'page_is_redirect' => 0,
451
			'page_namespace' => NS_MEDIAWIKI,
452
		];
453
454
		$mostused = [];
455
		if ( $wgAdaptiveMessageCache && $code !== $wgLanguageCode ) {
456
			if ( !isset( $this->mCache[$wgLanguageCode] ) ) {
457
				$this->load( $wgLanguageCode );
458
			}
459
			$mostused = array_keys( $this->mCache[$wgLanguageCode] );
460
			foreach ( $mostused as $key => $value ) {
461
				$mostused[$key] = "$value/$code";
462
			}
463
		}
464
465
		if ( count( $mostused ) ) {
466
			$conds['page_title'] = $mostused;
467
		} elseif ( $code !== $wgLanguageCode ) {
468
			$conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), '/', $code );
469
		} else {
470
			# Effectively disallows use of '/' character in NS_MEDIAWIKI for uses
471
			# other than language code.
472
			$conds[] = 'page_title NOT' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() );
473
		}
474
475
		# Conditions to fetch oversized pages to ignore them
476
		$bigConds = $conds;
477
		$bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize );
478
479
		# Load titles for all oversized pages in the MediaWiki namespace
480
		$res = $dbr->select( 'page', 'page_title', $bigConds, __METHOD__ . "($code)-big" );
481
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
482
			$cache[$row->page_title] = '!TOO BIG';
483
		}
484
485
		# Conditions to load the remaining pages with their contents
486
		$smallConds = $conds;
487
		$smallConds[] = 'page_latest=rev_id';
488
		$smallConds[] = 'rev_text_id=old_id';
489
		$smallConds[] = 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize );
490
491
		$res = $dbr->select(
492
			[ 'page', 'revision', 'text' ],
493
			[ 'page_title', 'old_text', 'old_flags' ],
494
			$smallConds,
495
			__METHOD__ . "($code)-small"
496
		);
497
498
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
499
			$text = Revision::getRevisionText( $row );
500
			if ( $text === false ) {
501
				// Failed to fetch data; possible ES errors?
502
				// Store a marker to fetch on-demand as a workaround...
503
				$entry = '!TOO BIG';
504
				wfDebugLog(
505
					'MessageCache',
506
					__METHOD__
507
						. ": failed to load message page text for {$row->page_title} ($code)"
508
				);
509
			} else {
510
				$entry = ' ' . $text;
511
			}
512
			$cache[$row->page_title] = $entry;
513
		}
514
515
		$cache['VERSION'] = MSG_CACHE_VERSION;
516
		ksort( $cache );
517
		$cache['HASH'] = md5( serialize( $cache ) );
518
		$cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry );
519
520
		return $cache;
521
	}
522
523
	/**
524
	 * Updates cache as necessary when message page is changed
525
	 *
526
	 * @param string|bool $title Name of the page changed (false if deleted)
527
	 * @param mixed $text New contents of the page.
528
	 */
529
	public function replace( $title, $text ) {
530
		global $wgMaxMsgCacheEntrySize, $wgContLang, $wgLanguageCode;
531
532
		if ( $this->mDisable ) {
533
			return;
534
		}
535
536
		list( $msg, $code ) = $this->figureMessage( $title );
0 ignored issues
show
Bug introduced by
It seems like $title can also be of type boolean; however, MessageCache::figureMessage() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
537
		if ( strpos( $title, '/' ) !== false && $code === $wgLanguageCode ) {
538
			// Content language overrides do not use the /<code> suffix
539
			return;
540
		}
541
542
		// Note that if the cache is volatile, load() may trigger a DB fetch.
543
		// In that case we reenter/reuse the existing cache key lock to avoid
544
		// a self-deadlock. This is safe as no reads happen *directly* in this
545
		// method between getReentrantScopedLock() and load() below. There is
546
		// no risk of data "changing under our feet" for replace().
547
		$cacheKey = wfMemcKey( 'messages', $code );
548
		$scopedLock = $this->getReentrantScopedLock( $cacheKey );
549
		$this->load( $code, self::FOR_UPDATE );
550
551
		$titleKey = wfMemcKey( 'messages', 'individual', $title );
552
		if ( $text === false ) {
553
			// Article was deleted
554
			$this->mCache[$code][$title] = '!NONEXISTENT';
555
			$this->wanCache->delete( $titleKey );
556
		} elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
557
			// Check for size
558
			$this->mCache[$code][$title] = '!TOO BIG';
559
			$this->wanCache->set( $titleKey, ' ' . $text, $this->mExpiry );
560
		} else {
561
			$this->mCache[$code][$title] = ' ' . $text;
562
			$this->wanCache->delete( $titleKey );
563
		}
564
565
		// Mark this cache as definitely "latest" (non-volatile) so
566
		// load() calls do try to refresh the cache with slave data
567
		$this->mCache[$code]['LATEST'] = time();
568
569
		// Update caches if the lock was acquired
570
		if ( $scopedLock ) {
571
			$this->saveToCaches( $this->mCache[$code], 'all', $code );
572
		}
573
574
		ScopedCallback::consume( $scopedLock );
575
		// Relay the purge to APC and other DCs
576
		$this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) );
577
578
		// Also delete cached sidebar... just in case it is affected
579
		$codes = [ $code ];
580
		if ( $code === 'en' ) {
581
			// Delete all sidebars, like for example on action=purge on the
582
			// sidebar messages
583
			$codes = array_keys( Language::fetchLanguageNames() );
584
		}
585
586
		foreach ( $codes as $code ) {
587
			$sidebarKey = wfMemcKey( 'sidebar', $code );
588
			$this->wanCache->delete( $sidebarKey );
589
		}
590
591
		// Update the message in the message blob store
592
		$resourceloader = RequestContext::getMain()->getOutput()->getResourceLoader();
593
		$blobStore = $resourceloader->getMessageBlobStore();
594
		$blobStore->updateMessage( $wgContLang->lcfirst( $msg ) );
595
596
		Hooks::run( 'MessageCacheReplace', [ $title, $text ] );
597
	}
598
599
	/**
600
	 * Is the given cache array expired due to time passing or a version change?
601
	 *
602
	 * @param array $cache
603
	 * @return bool
604
	 */
605
	protected function isCacheExpired( $cache ) {
606
		if ( !isset( $cache['VERSION'] ) || !isset( $cache['EXPIRY'] ) ) {
607
			return true;
608
		}
609
		if ( $cache['VERSION'] != MSG_CACHE_VERSION ) {
610
			return true;
611
		}
612
		if ( wfTimestampNow() >= $cache['EXPIRY'] ) {
613
			return true;
614
		}
615
616
		return false;
617
	}
618
619
	/**
620
	 * Shortcut to update caches.
621
	 *
622
	 * @param array $cache Cached messages with a version.
623
	 * @param string $dest Either "local-only" to save to local caches only
624
	 *   or "all" to save to all caches.
625
	 * @param string|bool $code Language code (default: false)
626
	 * @return bool
627
	 */
628
	protected function saveToCaches( array $cache, $dest, $code = false ) {
629
		if ( $dest === 'all' ) {
630
			$cacheKey = wfMemcKey( 'messages', $code );
631
			$success = $this->mMemc->set( $cacheKey, $cache );
632
		} else {
633
			$success = true;
634
		}
635
636
		$this->setValidationHash( $code, $cache );
0 ignored issues
show
Bug introduced by
It seems like $code defined by parameter $code on line 628 can also be of type boolean; however, MessageCache::setValidationHash() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
637
		$this->saveToLocalCache( $code, $cache );
0 ignored issues
show
Bug introduced by
It seems like $code defined by parameter $code on line 628 can also be of type boolean; however, MessageCache::saveToLocalCache() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
638
639
		return $success;
640
	}
641
642
	/**
643
	 * Get the md5 used to validate the local APC cache
644
	 *
645
	 * @param string $code
646
	 * @return array (hash or false, bool expiry/volatility status)
647
	 */
648
	protected function getValidationHash( $code ) {
649
		$curTTL = null;
650
		$value = $this->wanCache->get(
651
			wfMemcKey( 'messages', $code, 'hash', 'v1' ),
652
			$curTTL,
653
			[ wfMemcKey( 'messages', $code ) ]
654
		);
655
656
		if ( !$value ) {
657
			// No hash found at all; cache must regenerate to be safe
658
			$hash = false;
659
			$expired = true;
660
		} else {
661
			$hash = $value['hash'];
662
			if ( ( time() - $value['latest'] ) < WANObjectCache::HOLDOFF_TTL ) {
663
				// Cache was recently updated via replace() and should be up-to-date
664
				$expired = false;
665
			} else {
666
				// See if the "check" key was bumped after the hash was generated
667
				$expired = ( $curTTL < 0 );
668
			}
669
		}
670
671
		return [ $hash, $expired ];
672
	}
673
674
	/**
675
	 * Set the md5 used to validate the local disk cache
676
	 *
677
	 * If $cache has a 'LATEST' UNIX timestamp key, then the hash will not
678
	 * be treated as "volatile" by getValidationHash() for the next few seconds
679
	 *
680
	 * @param string $code
681
	 * @param array $cache Cached messages with a version
682
	 */
683
	protected function setValidationHash( $code, array $cache ) {
684
		$this->wanCache->set(
685
			wfMemcKey( 'messages', $code, 'hash', 'v1' ),
686
			[
687
				'hash' => $cache['HASH'],
688
				'latest' => isset( $cache['LATEST'] ) ? $cache['LATEST'] : 0
689
			],
690
			WANObjectCache::TTL_INDEFINITE
691
		);
692
	}
693
694
	/**
695
	 * @param string $key A language message cache key that stores blobs
696
	 * @param integer $timeout Wait timeout in seconds
697
	 * @return null|ScopedCallback
698
	 */
699
	protected function getReentrantScopedLock( $key, $timeout = self::WAIT_SEC ) {
700
		return $this->mMemc->getScopedLock( $key, $timeout, self::LOCK_TTL, __METHOD__ );
701
	}
702
703
	/**
704
	 * Get a message from either the content language or the user language.
705
	 *
706
	 * First, assemble a list of languages to attempt getting the message from. This
707
	 * chain begins with the requested language and its fallbacks and then continues with
708
	 * the content language and its fallbacks. For each language in the chain, the following
709
	 * process will occur (in this order):
710
	 *  1. If a language-specific override, i.e., [[MW:msg/lang]], is available, use that.
711
	 *     Note: for the content language, there is no /lang subpage.
712
	 *  2. Fetch from the static CDB cache.
713
	 *  3. If available, check the database for fallback language overrides.
714
	 *
715
	 * This process provides a number of guarantees. When changing this code, make sure all
716
	 * of these guarantees are preserved.
717
	 *  * If the requested language is *not* the content language, then the CDB cache for that
718
	 *    specific language will take precedence over the root database page ([[MW:msg]]).
719
	 *  * Fallbacks will be just that: fallbacks. A fallback language will never be reached if
720
	 *    the message is available *anywhere* in the language for which it is a fallback.
721
	 *
722
	 * @param string $key The message key
723
	 * @param bool $useDB If true, look for the message in the DB, false
724
	 *   to use only the compiled l10n cache.
725
	 * @param bool|string|object $langcode Code of the language to get the message for.
726
	 *   - If string and a valid code, will create a standard language object
727
	 *   - If string but not a valid code, will create a basic language object
728
	 *   - If boolean and false, create object from the current users language
729
	 *   - If boolean and true, create object from the wikis content language
730
	 *   - If language object, use it as given
731
	 * @param bool $isFullKey Specifies whether $key is a two part key "msg/lang".
732
	 *
733
	 * @throws MWException When given an invalid key
734
	 * @return string|bool False if the message doesn't exist, otherwise the
735
	 *   message (which can be empty)
736
	 */
737
	function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) {
738
		if ( is_int( $key ) ) {
739
			// Fix numerical strings that somehow become ints
740
			// on their way here
741
			$key = (string)$key;
742
		} elseif ( !is_string( $key ) ) {
743
			throw new MWException( 'Non-string key given' );
744
		} elseif ( $key === '' ) {
745
			// Shortcut: the empty key is always missing
746
			return false;
747
		}
748
749
		// For full keys, get the language code from the key
750
		$pos = strrpos( $key, '/' );
751
		if ( $isFullKey && $pos !== false ) {
752
			$langcode = substr( $key, $pos + 1 );
753
			$key = substr( $key, 0, $pos );
754
		}
755
756
		// Normalise title-case input (with some inlining)
757
		$lckey = MessageCache::normalizeKey( $key );
758
759
		Hooks::run( 'MessageCache::get', [ &$lckey ] );
760
761
		// Loop through each language in the fallback list until we find something useful
762
		$lang = wfGetLangObj( $langcode );
0 ignored issues
show
Bug introduced by
It seems like $langcode defined by parameter $langcode on line 737 can also be of type object; however, wfGetLangObj() does only seem to accept object<Language>|string|boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
763
		$message = $this->getMessageFromFallbackChain(
764
			$lang,
765
			$lckey,
766
			!$this->mDisable && $useDB
767
		);
768
769
		// If we still have no message, maybe the key was in fact a full key so try that
770
		if ( $message === false ) {
771
			$parts = explode( '/', $lckey );
772
			// We may get calls for things that are http-urls from sidebar
773
			// Let's not load nonexistent languages for those
774
			// They usually have more than one slash.
775
			if ( count( $parts ) == 2 && $parts[1] !== '' ) {
776
				$message = Language::getMessageFor( $parts[0], $parts[1] );
777
				if ( $message === null ) {
778
					$message = false;
779
				}
780
			}
781
		}
782
783
		// Post-processing if the message exists
784
		if ( $message !== false ) {
785
			// Fix whitespace
786
			$message = str_replace(
787
				[
788
					# Fix for trailing whitespace, removed by textarea
789
					'&#32;',
790
					# Fix for NBSP, converted to space by firefox
791
					'&nbsp;',
792
					'&#160;',
793
					'&shy;'
794
				],
795
				[
796
					' ',
797
					"\xc2\xa0",
798
					"\xc2\xa0",
799
					"\xc2\xad"
800
				],
801
				$message
802
			);
803
		}
804
805
		return $message;
806
	}
807
808
	/**
809
	 * Given a language, try and fetch messages from that language.
810
	 *
811
	 * Will also consider fallbacks of that language, the site language, and fallbacks for
812
	 * the site language.
813
	 *
814
	 * @see MessageCache::get
815
	 * @param Language|StubObject $lang Preferred language
816
	 * @param string $lckey Lowercase key for the message (as for localisation cache)
817
	 * @param bool $useDB Whether to include messages from the wiki database
818
	 * @return string|bool The message, or false if not found
819
	 */
820
	protected function getMessageFromFallbackChain( $lang, $lckey, $useDB ) {
821
		global $wgContLang;
822
823
		$alreadyTried = [];
824
825
		 // First try the requested language.
826
		$message = $this->getMessageForLang( $lang, $lckey, $useDB, $alreadyTried );
827
		if ( $message !== false ) {
828
			return $message;
829
		}
830
831
		// Now try checking the site language.
832
		$message = $this->getMessageForLang( $wgContLang, $lckey, $useDB, $alreadyTried );
833
		return $message;
834
	}
835
836
	/**
837
	 * Given a language, try and fetch messages from that language and its fallbacks.
838
	 *
839
	 * @see MessageCache::get
840
	 * @param Language|StubObject $lang Preferred language
841
	 * @param string $lckey Lowercase key for the message (as for localisation cache)
842
	 * @param bool $useDB Whether to include messages from the wiki database
843
	 * @param bool[] $alreadyTried Contains true for each language that has been tried already
844
	 * @return string|bool The message, or false if not found
845
	 */
846
	private function getMessageForLang( $lang, $lckey, $useDB, &$alreadyTried ) {
847
		global $wgContLang;
848
		$langcode = $lang->getCode();
0 ignored issues
show
Bug introduced by
The method getCode does only exist in Language, but not in StubObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
849
850
		// Try checking the database for the requested language
851
		if ( $useDB ) {
852
			$uckey = $wgContLang->ucfirst( $lckey );
853
854
			if ( !isset( $alreadyTried[ $langcode ] ) ) {
855
				$message = $this->getMsgFromNamespace(
856
					$this->getMessagePageName( $langcode, $uckey ),
857
					$langcode
858
				);
859
860
				if ( $message !== false ) {
861
					return $message;
862
				}
863
				$alreadyTried[ $langcode ] = true;
864
			}
865
		}
866
867
		// Check the CDB cache
868
		$message = $lang->getMessage( $lckey );
0 ignored issues
show
Bug introduced by
The method getMessage does only exist in Language, but not in StubObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
869
		if ( $message !== null ) {
870
			return $message;
871
		}
872
873
		// Try checking the database for all of the fallback languages
874
		if ( $useDB ) {
875
			$fallbackChain = Language::getFallbacksFor( $langcode );
876
877
			foreach ( $fallbackChain as $code ) {
878
				if ( isset( $alreadyTried[ $code ] ) ) {
879
					continue;
880
				}
881
882
				$message = $this->getMsgFromNamespace( $this->getMessagePageName( $code, $uckey ), $code );
0 ignored issues
show
Bug introduced by
The variable $uckey does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
883
884
				if ( $message !== false ) {
885
					return $message;
886
				}
887
				$alreadyTried[ $code ] = true;
888
			}
889
		}
890
891
		return false;
892
	}
893
894
	/**
895
	 * Get the message page name for a given language
896
	 *
897
	 * @param string $langcode
898
	 * @param string $uckey Uppercase key for the message
899
	 * @return string The page name
900
	 */
901
	private function getMessagePageName( $langcode, $uckey ) {
902
		global $wgLanguageCode;
903
		if ( $langcode === $wgLanguageCode ) {
904
			// Messages created in the content language will not have the /lang extension
905
			return $uckey;
906
		} else {
907
			return "$uckey/$langcode";
908
		}
909
	}
910
911
	/**
912
	 * Get a message from the MediaWiki namespace, with caching. The key must
913
	 * first be converted to two-part lang/msg form if necessary.
914
	 *
915
	 * Unlike self::get(), this function doesn't resolve fallback chains, and
916
	 * some callers require this behavior. LanguageConverter::parseCachedTable()
917
	 * and self::get() are some examples in core.
918
	 *
919
	 * @param string $title Message cache key with initial uppercase letter.
920
	 * @param string $code Code denoting the language to try.
921
	 * @return string|bool The message, or false if it does not exist or on error
922
	 */
923
	public function getMsgFromNamespace( $title, $code ) {
924
		$this->load( $code );
925
		if ( isset( $this->mCache[$code][$title] ) ) {
926
			$entry = $this->mCache[$code][$title];
927
			if ( substr( $entry, 0, 1 ) === ' ' ) {
928
				// The message exists, so make sure a string
929
				// is returned.
930
				return (string)substr( $entry, 1 );
931
			} elseif ( $entry === '!NONEXISTENT' ) {
932
				return false;
933
			} elseif ( $entry === '!TOO BIG' ) {
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
934
				// Fall through and try invididual message cache below
935
			}
936
		} else {
937
			// XXX: This is not cached in process cache, should it?
938
			$message = false;
939
			Hooks::run( 'MessagesPreLoad', [ $title, &$message ] );
940
			if ( $message !== false ) {
941
				return $message;
942
			}
943
944
			return false;
945
		}
946
947
		# Try the individual message cache
948
		$titleKey = wfMemcKey( 'messages', 'individual', $title );
949
		$entry = $this->wanCache->get( $titleKey );
950
		if ( $entry ) {
951
			if ( substr( $entry, 0, 1 ) === ' ' ) {
952
				$this->mCache[$code][$title] = $entry;
953
954
				// The message exists, so make sure a string
955
				// is returned.
956
				return (string)substr( $entry, 1 );
957
			} elseif ( $entry === '!NONEXISTENT' ) {
958
				$this->mCache[$code][$title] = '!NONEXISTENT';
959
960
				return false;
961
			} else {
962
				# Corrupt/obsolete entry, delete it
963
				$this->wanCache->delete( $titleKey );
964
			}
965
		}
966
967
		# Try loading it from the database
968
		$revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) );
969
		if ( $revision ) {
970
			$content = $revision->getContent();
971
			if ( !$content ) {
972
				// A possibly temporary loading failure.
973
				wfDebugLog(
974
					'MessageCache',
975
					__METHOD__ . ": failed to load message page text for {$title} ($code)"
976
				);
977
				$message = null; // no negative caching
978
			} else {
979
				// XXX: Is this the right way to turn a Content object into a message?
980
				// NOTE: $content is typically either WikitextContent, JavaScriptContent or
981
				//       CssContent. MessageContent is *not* used for storing messages, it's
982
				//       only used for wrapping them when needed.
983
				$message = $content->getWikitextForTransclusion();
984
985
				if ( $message === false || $message === null ) {
986
					wfDebugLog(
987
						'MessageCache',
988
						__METHOD__ . ": message content doesn't provide wikitext "
989
							. "(content model: " . $content->getModel() . ")"
990
					);
991
992
					$message = false; // negative caching
993
				} else {
994
					$this->mCache[$code][$title] = ' ' . $message;
995
					$this->wanCache->set( $titleKey, ' ' . $message, $this->mExpiry );
996
				}
997
			}
998
		} else {
999
			$message = false; // negative caching
1000
		}
1001
1002
		if ( $message === false ) { // negative caching
1003
			$this->mCache[$code][$title] = '!NONEXISTENT';
1004
			$this->wanCache->set( $titleKey, '!NONEXISTENT', $this->mExpiry );
1005
		}
1006
1007
		return $message;
1008
	}
1009
1010
	/**
1011
	 * @param string $message
1012
	 * @param bool $interface
1013
	 * @param string $language Language code
1014
	 * @param Title $title
1015
	 * @return string
1016
	 */
1017
	function transform( $message, $interface = false, $language = null, $title = null ) {
1018
		// Avoid creating parser if nothing to transform
1019
		if ( strpos( $message, '{{' ) === false ) {
1020
			return $message;
1021
		}
1022
1023
		if ( $this->mInParser ) {
1024
			return $message;
1025
		}
1026
1027
		$parser = $this->getParser();
1028
		if ( $parser ) {
1029
			$popts = $this->getParserOptions();
1030
			$popts->setInterfaceMessage( $interface );
1031
			$popts->setTargetLanguage( $language );
1032
1033
			$userlang = $popts->setUserLang( $language );
1034
			$this->mInParser = true;
1035
			$message = $parser->transformMsg( $message, $popts, $title );
1036
			$this->mInParser = false;
1037
			$popts->setUserLang( $userlang );
1038
		}
1039
1040
		return $message;
1041
	}
1042
1043
	/**
1044
	 * @return Parser
1045
	 */
1046
	function getParser() {
1047
		global $wgParser, $wgParserConf;
1048
		if ( !$this->mParser && isset( $wgParser ) ) {
1049
			# Do some initialisation so that we don't have to do it twice
1050
			$wgParser->firstCallInit();
1051
			# Clone it and store it
1052
			$class = $wgParserConf['class'];
1053
			if ( $class == 'ParserDiffTest' ) {
1054
				# Uncloneable
1055
				$this->mParser = new $class( $wgParserConf );
1056
			} else {
1057
				$this->mParser = clone $wgParser;
1058
			}
1059
		}
1060
1061
		return $this->mParser;
1062
	}
1063
1064
	/**
1065
	 * @param string $text
1066
	 * @param Title $title
1067
	 * @param bool $linestart Whether or not this is at the start of a line
1068
	 * @param bool $interface Whether this is an interface message
1069
	 * @param Language|string $language Language code
1070
	 * @return ParserOutput|string
1071
	 */
1072
	public function parse( $text, $title = null, $linestart = true,
1073
		$interface = false, $language = null
1074
	) {
1075
		if ( $this->mInParser ) {
1076
			return htmlspecialchars( $text );
1077
		}
1078
1079
		$parser = $this->getParser();
1080
		$popts = $this->getParserOptions();
1081
		$popts->setInterfaceMessage( $interface );
1082
1083
		if ( is_string( $language ) ) {
1084
			$language = Language::factory( $language );
1085
		}
1086
		$popts->setTargetLanguage( $language );
1087
1088
		if ( !$title || !$title instanceof Title ) {
1089
			global $wgTitle;
1090
			wfDebugLog( 'GlobalTitleFail', __METHOD__ . ' called by ' .
1091
				wfGetAllCallers( 5 ) . ' with no title set.' );
1092
			$title = $wgTitle;
1093
		}
1094
		// Sometimes $wgTitle isn't set either...
1095
		if ( !$title ) {
1096
			# It's not uncommon having a null $wgTitle in scripts. See r80898
1097
			# Create a ghost title in such case
1098
			$title = Title::makeTitle( NS_SPECIAL, 'Badtitle/title not set in ' . __METHOD__ );
1099
		}
1100
1101
		$this->mInParser = true;
1102
		$res = $parser->parse( $text, $title, $popts, $linestart );
1103
		$this->mInParser = false;
1104
1105
		return $res;
1106
	}
1107
1108
	function disable() {
1109
		$this->mDisable = true;
1110
	}
1111
1112
	function enable() {
1113
		$this->mDisable = false;
1114
	}
1115
1116
	/**
1117
	 * Clear all stored messages. Mainly used after a mass rebuild.
1118
	 */
1119
	function clear() {
1120
		$langs = Language::fetchLanguageNames( null, 'mw' );
1121
		foreach ( array_keys( $langs ) as $code ) {
1122
			# Global and local caches
1123
			$this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) );
1124
		}
1125
1126
		$this->mLoadedLanguages = [];
1127
	}
1128
1129
	/**
1130
	 * @param string $key
1131
	 * @return array
1132
	 */
1133
	public function figureMessage( $key ) {
1134
		global $wgLanguageCode;
1135
1136
		$pieces = explode( '/', $key );
1137
		if ( count( $pieces ) < 2 ) {
1138
			return [ $key, $wgLanguageCode ];
1139
		}
1140
1141
		$lang = array_pop( $pieces );
1142
		if ( !Language::fetchLanguageName( $lang, null, 'mw' ) ) {
1143
			return [ $key, $wgLanguageCode ];
1144
		}
1145
1146
		$message = implode( '/', $pieces );
1147
1148
		return [ $message, $lang ];
1149
	}
1150
1151
	/**
1152
	 * Get all message keys stored in the message cache for a given language.
1153
	 * If $code is the content language code, this will return all message keys
1154
	 * for which MediaWiki:msgkey exists. If $code is another language code, this
1155
	 * will ONLY return message keys for which MediaWiki:msgkey/$code exists.
1156
	 * @param string $code Language code
1157
	 * @return array Array of message keys (strings)
1158
	 */
1159
	public function getAllMessageKeys( $code ) {
1160
		global $wgContLang;
1161
		$this->load( $code );
1162
		if ( !isset( $this->mCache[$code] ) ) {
1163
			// Apparently load() failed
1164
			return null;
1165
		}
1166
		// Remove administrative keys
1167
		$cache = $this->mCache[$code];
1168
		unset( $cache['VERSION'] );
1169
		unset( $cache['EXPIRY'] );
1170
		// Remove any !NONEXISTENT keys
1171
		$cache = array_diff( $cache, [ '!NONEXISTENT' ] );
1172
1173
		// Keys may appear with a capital first letter. lcfirst them.
1174
		return array_map( [ $wgContLang, 'lcfirst' ], array_keys( $cache ) );
1175
	}
1176
}
1177