Completed
Branch master (9259dd)
by
unknown
27:26
created

MessageCache   F

Complexity

Total Complexity 138

Size/Duplication

Total Lines 1159
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 17

Importance

Changes 0
Metric Value
dl 0
loc 1159
rs 0.6314
c 0
b 0
f 0
wmc 138
lcom 3
cbo 17

30 Methods

Rating   Name   Duplication   Size   Complexity  
A singleton() 0 12 2
A destroyInstance() 0 3 1
A normalizeKey() 0 11 2
A __construct() 0 19 3
A getParserOptions() 0 19 3
A getLocalCache() 0 5 1
A saveToLocalCache() 0 4 1
F load() 0 137 24
B loadFromDBWithLock() 0 54 5
C loadFromDB() 0 81 11
C replace() 0 69 9
B isCacheExpired() 0 13 5
A saveToCaches() 0 13 2
B getValidationHash() 0 25 3
A setValidationHash() 0 10 2
A getReentrantScopedLock() 0 3 1
C get() 0 70 12
A getMessageFromFallbackChain() 0 15 2
C getMessageForLang() 0 47 9
A getMessagePageName() 0 9 2
D getMsgFromNamespace() 0 86 14
B transform() 0 25 4
A getParser() 0 17 4
B parse() 0 35 6
A disable() 0 3 1
A enable() 0 3 1
A isDisabled() 0 3 1
A clear() 0 9 2
A figureMessage() 0 17 3
A getAllMessageKeys() 0 17 2

How to fix   Complexity   

Complex Class

Complex classes like MessageCache often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MessageCache, and based on these observations, apply Extract Interface, too.

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
			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->mLoadedLanguages[$code] = true;
364
		}
365
366
		$info = implode( ', ', $where );
367
		wfDebugLog( 'MessageCache', __METHOD__ . ": Loading $code... $info\n" );
368
369
		return $success;
370
	}
371
372
	/**
373
	 * @param string $code
374
	 * @param array $where List of wfDebug() comments
375
	 * @param integer $mode Use MessageCache::FOR_UPDATE to use DB_MASTER
376
	 * @return bool|string True on success or one of ("cantacquire", "disabled")
377
	 */
378
	protected function loadFromDBWithLock( $code, array &$where, $mode = null ) {
379
		global $wgUseLocalMessageCache;
380
381
		# If cache updates on all levels fail, give up on message overrides.
382
		# This is to avoid easy site outages; see $saveSuccess comments below.
383
		$statusKey = wfMemcKey( 'messages', $code, 'status' );
384
		$status = $this->mMemc->get( $statusKey );
385
		if ( $status === 'error' ) {
386
			$where[] = "could not load; method is still globally disabled";
387
			return 'disabled';
388
		}
389
390
		# Now let's regenerate
391
		$where[] = 'loading from database';
392
393
		# Lock the cache to prevent conflicting writes.
394
		# This lock is non-blocking so stale cache can quickly be used.
395
		# Note that load() will call a blocking getReentrantScopedLock()
396
		# after this if it really need to wait for any current thread.
397
		$cacheKey = wfMemcKey( 'messages', $code );
398
		$scopedLock = $this->getReentrantScopedLock( $cacheKey, 0 );
399
		if ( !$scopedLock ) {
400
			$where[] = 'could not acquire main lock';
401
			return 'cantacquire';
402
		}
403
404
		$cache = $this->loadFromDB( $code, $mode );
405
		$this->mCache[$code] = $cache;
406
		$saveSuccess = $this->saveToCaches( $cache, 'all', $code );
407
408
		if ( !$saveSuccess ) {
409
			/**
410
			 * Cache save has failed.
411
			 *
412
			 * There are two main scenarios where this could be a problem:
413
			 * - The cache is more than the maximum size (typically 1MB compressed).
414
			 * - Memcached has no space remaining in the relevant slab class. This is
415
			 *   unlikely with recent versions of memcached.
416
			 *
417
			 * Either way, if there is a local cache, nothing bad will happen. If there
418
			 * is no local cache, disabling the message cache for all requests avoids
419
			 * incurring a loadFromDB() overhead on every request, and thus saves the
420
			 * wiki from complete downtime under moderate traffic conditions.
421
			 */
422
			if ( !$wgUseLocalMessageCache ) {
423
				$this->mMemc->set( $statusKey, 'error', 60 * 5 );
424
				$where[] = 'could not save cache, disabled globally for 5 minutes';
425
			} else {
426
				$where[] = "could not save global cache";
427
			}
428
		}
429
430
		return true;
431
	}
432
433
	/**
434
	 * Loads cacheable messages from the database. Messages bigger than
435
	 * $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded
436
	 * on-demand from the database later.
437
	 *
438
	 * @param string $code Language code
439
	 * @param integer $mode Use MessageCache::FOR_UPDATE to skip process cache
440
	 * @return array Loaded messages for storing in caches
441
	 */
442
	function loadFromDB( $code, $mode = null ) {
443
		global $wgMaxMsgCacheEntrySize, $wgLanguageCode, $wgAdaptiveMessageCache;
444
445
		$dbr = wfGetDB( ( $mode == self::FOR_UPDATE ) ? DB_MASTER : DB_SLAVE );
446
447
		$cache = [];
448
449
		# Common conditions
450
		$conds = [
451
			'page_is_redirect' => 0,
452
			'page_namespace' => NS_MEDIAWIKI,
453
		];
454
455
		$mostused = [];
456
		if ( $wgAdaptiveMessageCache && $code !== $wgLanguageCode ) {
457
			if ( !isset( $this->mCache[$wgLanguageCode] ) ) {
458
				$this->load( $wgLanguageCode );
459
			}
460
			$mostused = array_keys( $this->mCache[$wgLanguageCode] );
461
			foreach ( $mostused as $key => $value ) {
462
				$mostused[$key] = "$value/$code";
463
			}
464
		}
465
466
		if ( count( $mostused ) ) {
467
			$conds['page_title'] = $mostused;
468
		} elseif ( $code !== $wgLanguageCode ) {
469
			$conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), '/', $code );
470
		} else {
471
			# Effectively disallows use of '/' character in NS_MEDIAWIKI for uses
472
			# other than language code.
473
			$conds[] = 'page_title NOT' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() );
474
		}
475
476
		# Conditions to fetch oversized pages to ignore them
477
		$bigConds = $conds;
478
		$bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize );
479
480
		# Load titles for all oversized pages in the MediaWiki namespace
481
		$res = $dbr->select( 'page', 'page_title', $bigConds, __METHOD__ . "($code)-big" );
482
		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...
483
			$cache[$row->page_title] = '!TOO BIG';
484
		}
485
486
		# Conditions to load the remaining pages with their contents
487
		$smallConds = $conds;
488
		$smallConds[] = 'page_latest=rev_id';
489
		$smallConds[] = 'rev_text_id=old_id';
490
		$smallConds[] = 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize );
491
492
		$res = $dbr->select(
493
			[ 'page', 'revision', 'text' ],
494
			[ 'page_title', 'old_text', 'old_flags' ],
495
			$smallConds,
496
			__METHOD__ . "($code)-small"
497
		);
498
499
		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...
500
			$text = Revision::getRevisionText( $row );
501
			if ( $text === false ) {
502
				// Failed to fetch data; possible ES errors?
503
				// Store a marker to fetch on-demand as a workaround...
504
				$entry = '!TOO BIG';
505
				wfDebugLog(
506
					'MessageCache',
507
					__METHOD__
508
						. ": failed to load message page text for {$row->page_title} ($code)"
509
				);
510
			} else {
511
				$entry = ' ' . $text;
512
			}
513
			$cache[$row->page_title] = $entry;
514
		}
515
516
		$cache['VERSION'] = MSG_CACHE_VERSION;
517
		ksort( $cache );
518
		$cache['HASH'] = md5( serialize( $cache ) );
519
		$cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry );
520
521
		return $cache;
522
	}
523
524
	/**
525
	 * Updates cache as necessary when message page is changed
526
	 *
527
	 * @param string|bool $title Name of the page changed (false if deleted)
528
	 * @param mixed $text New contents of the page.
529
	 */
530
	public function replace( $title, $text ) {
531
		global $wgMaxMsgCacheEntrySize, $wgContLang, $wgLanguageCode;
532
533
		if ( $this->mDisable ) {
534
			return;
535
		}
536
537
		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...
538
		if ( strpos( $title, '/' ) !== false && $code === $wgLanguageCode ) {
539
			// Content language overrides do not use the /<code> suffix
540
			return;
541
		}
542
543
		// Note that if the cache is volatile, load() may trigger a DB fetch.
544
		// In that case we reenter/reuse the existing cache key lock to avoid
545
		// a self-deadlock. This is safe as no reads happen *directly* in this
546
		// method between getReentrantScopedLock() and load() below. There is
547
		// no risk of data "changing under our feet" for replace().
548
		$cacheKey = wfMemcKey( 'messages', $code );
549
		$scopedLock = $this->getReentrantScopedLock( $cacheKey );
550
		$this->load( $code, self::FOR_UPDATE );
551
552
		$titleKey = wfMemcKey( 'messages', 'individual', $title );
553
		if ( $text === false ) {
554
			// Article was deleted
555
			$this->mCache[$code][$title] = '!NONEXISTENT';
556
			$this->wanCache->delete( $titleKey );
557
		} elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
558
			// Check for size
559
			$this->mCache[$code][$title] = '!TOO BIG';
560
			$this->wanCache->set( $titleKey, ' ' . $text, $this->mExpiry );
561
		} else {
562
			$this->mCache[$code][$title] = ' ' . $text;
563
			$this->wanCache->delete( $titleKey );
564
		}
565
566
		// Mark this cache as definitely "latest" (non-volatile) so
567
		// load() calls do try to refresh the cache with slave data
568
		$this->mCache[$code]['LATEST'] = time();
569
570
		// Update caches if the lock was acquired
571
		if ( $scopedLock ) {
572
			$this->saveToCaches( $this->mCache[$code], 'all', $code );
573
		}
574
575
		ScopedCallback::consume( $scopedLock );
576
		// Relay the purge to APC and other DCs
577
		$this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) );
578
579
		// Also delete cached sidebar... just in case it is affected
580
		$codes = [ $code ];
581
		if ( $code === 'en' ) {
582
			// Delete all sidebars, like for example on action=purge on the
583
			// sidebar messages
584
			$codes = array_keys( Language::fetchLanguageNames() );
585
		}
586
587
		foreach ( $codes as $code ) {
588
			$sidebarKey = wfMemcKey( 'sidebar', $code );
589
			$this->wanCache->delete( $sidebarKey );
590
		}
591
592
		// Update the message in the message blob store
593
		$resourceloader = RequestContext::getMain()->getOutput()->getResourceLoader();
594
		$blobStore = $resourceloader->getMessageBlobStore();
595
		$blobStore->updateMessage( $wgContLang->lcfirst( $msg ) );
596
597
		Hooks::run( 'MessageCacheReplace', [ $title, $text ] );
598
	}
599
600
	/**
601
	 * Is the given cache array expired due to time passing or a version change?
602
	 *
603
	 * @param array $cache
604
	 * @return bool
605
	 */
606
	protected function isCacheExpired( $cache ) {
607
		if ( !isset( $cache['VERSION'] ) || !isset( $cache['EXPIRY'] ) ) {
608
			return true;
609
		}
610
		if ( $cache['VERSION'] != MSG_CACHE_VERSION ) {
611
			return true;
612
		}
613
		if ( wfTimestampNow() >= $cache['EXPIRY'] ) {
614
			return true;
615
		}
616
617
		return false;
618
	}
619
620
	/**
621
	 * Shortcut to update caches.
622
	 *
623
	 * @param array $cache Cached messages with a version.
624
	 * @param string $dest Either "local-only" to save to local caches only
625
	 *   or "all" to save to all caches.
626
	 * @param string|bool $code Language code (default: false)
627
	 * @return bool
628
	 */
629
	protected function saveToCaches( array $cache, $dest, $code = false ) {
630
		if ( $dest === 'all' ) {
631
			$cacheKey = wfMemcKey( 'messages', $code );
632
			$success = $this->mMemc->set( $cacheKey, $cache );
633
		} else {
634
			$success = true;
635
		}
636
637
		$this->setValidationHash( $code, $cache );
0 ignored issues
show
Bug introduced by
It seems like $code defined by parameter $code on line 629 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...
638
		$this->saveToLocalCache( $code, $cache );
0 ignored issues
show
Bug introduced by
It seems like $code defined by parameter $code on line 629 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...
639
640
		return $success;
641
	}
642
643
	/**
644
	 * Get the md5 used to validate the local APC cache
645
	 *
646
	 * @param string $code
647
	 * @return array (hash or false, bool expiry/volatility status)
648
	 */
649
	protected function getValidationHash( $code ) {
650
		$curTTL = null;
651
		$value = $this->wanCache->get(
652
			wfMemcKey( 'messages', $code, 'hash', 'v1' ),
653
			$curTTL,
654
			[ wfMemcKey( 'messages', $code ) ]
655
		);
656
657
		if ( !$value ) {
658
			// No hash found at all; cache must regenerate to be safe
659
			$hash = false;
660
			$expired = true;
661
		} else {
662
			$hash = $value['hash'];
663
			if ( ( time() - $value['latest'] ) < WANObjectCache::HOLDOFF_TTL ) {
664
				// Cache was recently updated via replace() and should be up-to-date
665
				$expired = false;
666
			} else {
667
				// See if the "check" key was bumped after the hash was generated
668
				$expired = ( $curTTL < 0 );
669
			}
670
		}
671
672
		return [ $hash, $expired ];
673
	}
674
675
	/**
676
	 * Set the md5 used to validate the local disk cache
677
	 *
678
	 * If $cache has a 'LATEST' UNIX timestamp key, then the hash will not
679
	 * be treated as "volatile" by getValidationHash() for the next few seconds
680
	 *
681
	 * @param string $code
682
	 * @param array $cache Cached messages with a version
683
	 */
684
	protected function setValidationHash( $code, array $cache ) {
685
		$this->wanCache->set(
686
			wfMemcKey( 'messages', $code, 'hash', 'v1' ),
687
			[
688
				'hash' => $cache['HASH'],
689
				'latest' => isset( $cache['LATEST'] ) ? $cache['LATEST'] : 0
690
			],
691
			WANObjectCache::TTL_INDEFINITE
692
		);
693
	}
694
695
	/**
696
	 * @param string $key A language message cache key that stores blobs
697
	 * @param integer $timeout Wait timeout in seconds
698
	 * @return null|ScopedCallback
699
	 */
700
	protected function getReentrantScopedLock( $key, $timeout = self::WAIT_SEC ) {
701
		return $this->mMemc->getScopedLock( $key, $timeout, self::LOCK_TTL, __METHOD__ );
702
	}
703
704
	/**
705
	 * Get a message from either the content language or the user language.
706
	 *
707
	 * First, assemble a list of languages to attempt getting the message from. This
708
	 * chain begins with the requested language and its fallbacks and then continues with
709
	 * the content language and its fallbacks. For each language in the chain, the following
710
	 * process will occur (in this order):
711
	 *  1. If a language-specific override, i.e., [[MW:msg/lang]], is available, use that.
712
	 *     Note: for the content language, there is no /lang subpage.
713
	 *  2. Fetch from the static CDB cache.
714
	 *  3. If available, check the database for fallback language overrides.
715
	 *
716
	 * This process provides a number of guarantees. When changing this code, make sure all
717
	 * of these guarantees are preserved.
718
	 *  * If the requested language is *not* the content language, then the CDB cache for that
719
	 *    specific language will take precedence over the root database page ([[MW:msg]]).
720
	 *  * Fallbacks will be just that: fallbacks. A fallback language will never be reached if
721
	 *    the message is available *anywhere* in the language for which it is a fallback.
722
	 *
723
	 * @param string $key The message key
724
	 * @param bool $useDB If true, look for the message in the DB, false
725
	 *   to use only the compiled l10n cache.
726
	 * @param bool|string|object $langcode Code of the language to get the message for.
727
	 *   - If string and a valid code, will create a standard language object
728
	 *   - If string but not a valid code, will create a basic language object
729
	 *   - If boolean and false, create object from the current users language
730
	 *   - If boolean and true, create object from the wikis content language
731
	 *   - If language object, use it as given
732
	 * @param bool $isFullKey Specifies whether $key is a two part key "msg/lang".
733
	 *
734
	 * @throws MWException When given an invalid key
735
	 * @return string|bool False if the message doesn't exist, otherwise the
736
	 *   message (which can be empty)
737
	 */
738
	function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) {
739
		if ( is_int( $key ) ) {
740
			// Fix numerical strings that somehow become ints
741
			// on their way here
742
			$key = (string)$key;
743
		} elseif ( !is_string( $key ) ) {
744
			throw new MWException( 'Non-string key given' );
745
		} elseif ( $key === '' ) {
746
			// Shortcut: the empty key is always missing
747
			return false;
748
		}
749
750
		// For full keys, get the language code from the key
751
		$pos = strrpos( $key, '/' );
752
		if ( $isFullKey && $pos !== false ) {
753
			$langcode = substr( $key, $pos + 1 );
754
			$key = substr( $key, 0, $pos );
755
		}
756
757
		// Normalise title-case input (with some inlining)
758
		$lckey = MessageCache::normalizeKey( $key );
759
760
		Hooks::run( 'MessageCache::get', [ &$lckey ] );
761
762
		// Loop through each language in the fallback list until we find something useful
763
		$lang = wfGetLangObj( $langcode );
0 ignored issues
show
Bug introduced by
It seems like $langcode defined by parameter $langcode on line 738 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...
764
		$message = $this->getMessageFromFallbackChain(
765
			$lang,
766
			$lckey,
767
			!$this->mDisable && $useDB
768
		);
769
770
		// If we still have no message, maybe the key was in fact a full key so try that
771
		if ( $message === false ) {
772
			$parts = explode( '/', $lckey );
773
			// We may get calls for things that are http-urls from sidebar
774
			// Let's not load nonexistent languages for those
775
			// They usually have more than one slash.
776
			if ( count( $parts ) == 2 && $parts[1] !== '' ) {
777
				$message = Language::getMessageFor( $parts[0], $parts[1] );
778
				if ( $message === null ) {
779
					$message = false;
780
				}
781
			}
782
		}
783
784
		// Post-processing if the message exists
785
		if ( $message !== false ) {
786
			// Fix whitespace
787
			$message = str_replace(
788
				[
789
					# Fix for trailing whitespace, removed by textarea
790
					'&#32;',
791
					# Fix for NBSP, converted to space by firefox
792
					'&nbsp;',
793
					'&#160;',
794
					'&shy;'
795
				],
796
				[
797
					' ',
798
					"\xc2\xa0",
799
					"\xc2\xa0",
800
					"\xc2\xad"
801
				],
802
				$message
803
			);
804
		}
805
806
		return $message;
807
	}
808
809
	/**
810
	 * Given a language, try and fetch messages from that language.
811
	 *
812
	 * Will also consider fallbacks of that language, the site language, and fallbacks for
813
	 * the site language.
814
	 *
815
	 * @see MessageCache::get
816
	 * @param Language|StubObject $lang Preferred language
817
	 * @param string $lckey Lowercase key for the message (as for localisation cache)
818
	 * @param bool $useDB Whether to include messages from the wiki database
819
	 * @return string|bool The message, or false if not found
820
	 */
821
	protected function getMessageFromFallbackChain( $lang, $lckey, $useDB ) {
822
		global $wgContLang;
823
824
		$alreadyTried = [];
825
826
		 // First try the requested language.
827
		$message = $this->getMessageForLang( $lang, $lckey, $useDB, $alreadyTried );
828
		if ( $message !== false ) {
829
			return $message;
830
		}
831
832
		// Now try checking the site language.
833
		$message = $this->getMessageForLang( $wgContLang, $lckey, $useDB, $alreadyTried );
834
		return $message;
835
	}
836
837
	/**
838
	 * Given a language, try and fetch messages from that language and its fallbacks.
839
	 *
840
	 * @see MessageCache::get
841
	 * @param Language|StubObject $lang Preferred language
842
	 * @param string $lckey Lowercase key for the message (as for localisation cache)
843
	 * @param bool $useDB Whether to include messages from the wiki database
844
	 * @param bool[] $alreadyTried Contains true for each language that has been tried already
845
	 * @return string|bool The message, or false if not found
846
	 */
847
	private function getMessageForLang( $lang, $lckey, $useDB, &$alreadyTried ) {
848
		global $wgContLang;
849
		$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...
850
851
		// Try checking the database for the requested language
852
		if ( $useDB ) {
853
			$uckey = $wgContLang->ucfirst( $lckey );
854
855
			if ( !isset( $alreadyTried[ $langcode ] ) ) {
856
				$message = $this->getMsgFromNamespace(
857
					$this->getMessagePageName( $langcode, $uckey ),
858
					$langcode
859
				);
860
861
				if ( $message !== false ) {
862
					return $message;
863
				}
864
				$alreadyTried[ $langcode ] = true;
865
			}
866
		}
867
868
		// Check the CDB cache
869
		$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...
870
		if ( $message !== null ) {
871
			return $message;
872
		}
873
874
		// Try checking the database for all of the fallback languages
875
		if ( $useDB ) {
876
			$fallbackChain = Language::getFallbacksFor( $langcode );
877
878
			foreach ( $fallbackChain as $code ) {
879
				if ( isset( $alreadyTried[ $code ] ) ) {
880
					continue;
881
				}
882
883
				$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...
884
885
				if ( $message !== false ) {
886
					return $message;
887
				}
888
				$alreadyTried[ $code ] = true;
889
			}
890
		}
891
892
		return false;
893
	}
894
895
	/**
896
	 * Get the message page name for a given language
897
	 *
898
	 * @param string $langcode
899
	 * @param string $uckey Uppercase key for the message
900
	 * @return string The page name
901
	 */
902
	private function getMessagePageName( $langcode, $uckey ) {
903
		global $wgLanguageCode;
904
		if ( $langcode === $wgLanguageCode ) {
905
			// Messages created in the content language will not have the /lang extension
906
			return $uckey;
907
		} else {
908
			return "$uckey/$langcode";
909
		}
910
	}
911
912
	/**
913
	 * Get a message from the MediaWiki namespace, with caching. The key must
914
	 * first be converted to two-part lang/msg form if necessary.
915
	 *
916
	 * Unlike self::get(), this function doesn't resolve fallback chains, and
917
	 * some callers require this behavior. LanguageConverter::parseCachedTable()
918
	 * and self::get() are some examples in core.
919
	 *
920
	 * @param string $title Message cache key with initial uppercase letter.
921
	 * @param string $code Code denoting the language to try.
922
	 * @return string|bool The message, or false if it does not exist or on error
923
	 */
924
	public function getMsgFromNamespace( $title, $code ) {
925
		$this->load( $code );
926
		if ( isset( $this->mCache[$code][$title] ) ) {
927
			$entry = $this->mCache[$code][$title];
928
			if ( substr( $entry, 0, 1 ) === ' ' ) {
929
				// The message exists, so make sure a string
930
				// is returned.
931
				return (string)substr( $entry, 1 );
932
			} elseif ( $entry === '!NONEXISTENT' ) {
933
				return false;
934
			} 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...
935
				// Fall through and try invididual message cache below
936
			}
937
		} else {
938
			// XXX: This is not cached in process cache, should it?
939
			$message = false;
940
			Hooks::run( 'MessagesPreLoad', [ $title, &$message ] );
941
			if ( $message !== false ) {
942
				return $message;
943
			}
944
945
			return false;
946
		}
947
948
		# Try the individual message cache
949
		$titleKey = wfMemcKey( 'messages', 'individual', $title );
950
		$entry = $this->wanCache->get( $titleKey );
951
		if ( $entry ) {
952
			if ( substr( $entry, 0, 1 ) === ' ' ) {
953
				$this->mCache[$code][$title] = $entry;
954
955
				// The message exists, so make sure a string
956
				// is returned.
957
				return (string)substr( $entry, 1 );
958
			} elseif ( $entry === '!NONEXISTENT' ) {
959
				$this->mCache[$code][$title] = '!NONEXISTENT';
960
961
				return false;
962
			} else {
963
				# Corrupt/obsolete entry, delete it
964
				$this->wanCache->delete( $titleKey );
965
			}
966
		}
967
968
		# Try loading it from the database
969
		$revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) );
970
		if ( $revision ) {
971
			$content = $revision->getContent();
972
			if ( !$content ) {
973
				// A possibly temporary loading failure.
974
				wfDebugLog(
975
					'MessageCache',
976
					__METHOD__ . ": failed to load message page text for {$title} ($code)"
977
				);
978
				$message = null; // no negative caching
979
			} else {
980
				// XXX: Is this the right way to turn a Content object into a message?
981
				// NOTE: $content is typically either WikitextContent, JavaScriptContent or
982
				//       CssContent. MessageContent is *not* used for storing messages, it's
983
				//       only used for wrapping them when needed.
984
				$message = $content->getWikitextForTransclusion();
985
986
				if ( $message === false || $message === null ) {
987
					wfDebugLog(
988
						'MessageCache',
989
						__METHOD__ . ": message content doesn't provide wikitext "
990
							. "(content model: " . $content->getModel() . ")"
991
					);
992
993
					$message = false; // negative caching
994
				} else {
995
					$this->mCache[$code][$title] = ' ' . $message;
996
					$this->wanCache->set( $titleKey, ' ' . $message, $this->mExpiry );
997
				}
998
			}
999
		} else {
1000
			$message = false; // negative caching
1001
		}
1002
1003
		if ( $message === false ) { // negative caching
1004
			$this->mCache[$code][$title] = '!NONEXISTENT';
1005
			$this->wanCache->set( $titleKey, '!NONEXISTENT', $this->mExpiry );
1006
		}
1007
1008
		return $message;
1009
	}
1010
1011
	/**
1012
	 * @param string $message
1013
	 * @param bool $interface
1014
	 * @param string $language Language code
1015
	 * @param Title $title
1016
	 * @return string
1017
	 */
1018
	function transform( $message, $interface = false, $language = null, $title = null ) {
1019
		// Avoid creating parser if nothing to transform
1020
		if ( strpos( $message, '{{' ) === false ) {
1021
			return $message;
1022
		}
1023
1024
		if ( $this->mInParser ) {
1025
			return $message;
1026
		}
1027
1028
		$parser = $this->getParser();
1029
		if ( $parser ) {
1030
			$popts = $this->getParserOptions();
1031
			$popts->setInterfaceMessage( $interface );
1032
			$popts->setTargetLanguage( $language );
1033
1034
			$userlang = $popts->setUserLang( $language );
1035
			$this->mInParser = true;
1036
			$message = $parser->transformMsg( $message, $popts, $title );
1037
			$this->mInParser = false;
1038
			$popts->setUserLang( $userlang );
1039
		}
1040
1041
		return $message;
1042
	}
1043
1044
	/**
1045
	 * @return Parser
1046
	 */
1047
	function getParser() {
1048
		global $wgParser, $wgParserConf;
1049
		if ( !$this->mParser && isset( $wgParser ) ) {
1050
			# Do some initialisation so that we don't have to do it twice
1051
			$wgParser->firstCallInit();
1052
			# Clone it and store it
1053
			$class = $wgParserConf['class'];
1054
			if ( $class == 'ParserDiffTest' ) {
1055
				# Uncloneable
1056
				$this->mParser = new $class( $wgParserConf );
1057
			} else {
1058
				$this->mParser = clone $wgParser;
1059
			}
1060
		}
1061
1062
		return $this->mParser;
1063
	}
1064
1065
	/**
1066
	 * @param string $text
1067
	 * @param Title $title
1068
	 * @param bool $linestart Whether or not this is at the start of a line
1069
	 * @param bool $interface Whether this is an interface message
1070
	 * @param Language|string $language Language code
1071
	 * @return ParserOutput|string
1072
	 */
1073
	public function parse( $text, $title = null, $linestart = true,
1074
		$interface = false, $language = null
1075
	) {
1076
		if ( $this->mInParser ) {
1077
			return htmlspecialchars( $text );
1078
		}
1079
1080
		$parser = $this->getParser();
1081
		$popts = $this->getParserOptions();
1082
		$popts->setInterfaceMessage( $interface );
1083
1084
		if ( is_string( $language ) ) {
1085
			$language = Language::factory( $language );
1086
		}
1087
		$popts->setTargetLanguage( $language );
1088
1089
		if ( !$title || !$title instanceof Title ) {
1090
			global $wgTitle;
1091
			wfDebugLog( 'GlobalTitleFail', __METHOD__ . ' called by ' .
1092
				wfGetAllCallers( 5 ) . ' with no title set.' );
1093
			$title = $wgTitle;
1094
		}
1095
		// Sometimes $wgTitle isn't set either...
1096
		if ( !$title ) {
1097
			# It's not uncommon having a null $wgTitle in scripts. See r80898
1098
			# Create a ghost title in such case
1099
			$title = Title::makeTitle( NS_SPECIAL, 'Badtitle/title not set in ' . __METHOD__ );
1100
		}
1101
1102
		$this->mInParser = true;
1103
		$res = $parser->parse( $text, $title, $popts, $linestart );
1104
		$this->mInParser = false;
1105
1106
		return $res;
1107
	}
1108
1109
	function disable() {
1110
		$this->mDisable = true;
1111
	}
1112
1113
	function enable() {
1114
		$this->mDisable = false;
1115
	}
1116
1117
	/**
1118
	 * Whether DB/cache usage is disabled for determining messages
1119
	 *
1120
	 * If so, this typically indicates either:
1121
	 *   - a) load() failed to find a cached copy nor query the DB
1122
	 *   - b) we are in a special context or error mode that cannot use the DB
1123
	 * If the DB is ignored, any derived HTML output or cached objects may be wrong.
1124
	 * To avoid long-term cache pollution, TTLs can be adjusted accordingly.
1125
	 *
1126
	 * @return bool
1127
	 * @since 1.27
1128
	 */
1129
	public function isDisabled() {
1130
		return $this->mDisable;
1131
	}
1132
1133
	/**
1134
	 * Clear all stored messages. Mainly used after a mass rebuild.
1135
	 */
1136
	function clear() {
1137
		$langs = Language::fetchLanguageNames( null, 'mw' );
1138
		foreach ( array_keys( $langs ) as $code ) {
1139
			# Global and local caches
1140
			$this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) );
1141
		}
1142
1143
		$this->mLoadedLanguages = [];
1144
	}
1145
1146
	/**
1147
	 * @param string $key
1148
	 * @return array
1149
	 */
1150
	public function figureMessage( $key ) {
1151
		global $wgLanguageCode;
1152
1153
		$pieces = explode( '/', $key );
1154
		if ( count( $pieces ) < 2 ) {
1155
			return [ $key, $wgLanguageCode ];
1156
		}
1157
1158
		$lang = array_pop( $pieces );
1159
		if ( !Language::fetchLanguageName( $lang, null, 'mw' ) ) {
1160
			return [ $key, $wgLanguageCode ];
1161
		}
1162
1163
		$message = implode( '/', $pieces );
1164
1165
		return [ $message, $lang ];
1166
	}
1167
1168
	/**
1169
	 * Get all message keys stored in the message cache for a given language.
1170
	 * If $code is the content language code, this will return all message keys
1171
	 * for which MediaWiki:msgkey exists. If $code is another language code, this
1172
	 * will ONLY return message keys for which MediaWiki:msgkey/$code exists.
1173
	 * @param string $code Language code
1174
	 * @return array Array of message keys (strings)
1175
	 */
1176
	public function getAllMessageKeys( $code ) {
1177
		global $wgContLang;
1178
		$this->load( $code );
1179
		if ( !isset( $this->mCache[$code] ) ) {
1180
			// Apparently load() failed
1181
			return null;
1182
		}
1183
		// Remove administrative keys
1184
		$cache = $this->mCache[$code];
1185
		unset( $cache['VERSION'] );
1186
		unset( $cache['EXPIRY'] );
1187
		// Remove any !NONEXISTENT keys
1188
		$cache = array_diff( $cache, [ '!NONEXISTENT' ] );
1189
1190
		// Keys may appear with a capital first letter. lcfirst them.
1191
		return array_map( [ $wgContLang, 'lcfirst' ], array_keys( $cache ) );
1192
	}
1193
}
1194