Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/cache/MessageCache.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Localisation messages cache.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Cache
22
 */
23
use MediaWiki\MediaWikiServices;
24
use Wikimedia\ScopedCallback;
25
26
/**
27
 * MediaWiki message cache structure version.
28
 * Bump this whenever the message cache format has changed.
29
 */
30
define( 'MSG_CACHE_VERSION', 2 );
31
32
/**
33
 * Message cache
34
 * Performs various MediaWiki namespace-related functions
35
 * @ingroup Cache
36
 */
37
class MessageCache {
38
	const FOR_UPDATE = 1; // force message reload
39
40
	/** How long to wait for memcached locks */
41
	const WAIT_SEC = 15;
42
	/** How long memcached locks last */
43
	const LOCK_TTL = 30;
44
45
	/**
46
	 * Process local cache of loaded messages that are defined in
47
	 * MediaWiki namespace. First array level is a language code,
48
	 * second level is message key and the values are either message
49
	 * content prefixed with space, or !NONEXISTENT for negative
50
	 * caching.
51
	 * @var array $mCache
52
	 */
53
	protected $mCache;
54
55
	/**
56
	 * Should mean that database cannot be used, but check
57
	 * @var bool $mDisable
58
	 */
59
	protected $mDisable;
60
61
	/**
62
	 * Lifetime for cache, used by object caching.
63
	 * Set on construction, see __construct().
64
	 */
65
	protected $mExpiry;
66
67
	/**
68
	 * Message cache has its own parser which it uses to transform
69
	 * messages.
70
	 */
71
	protected $mParserOptions, $mParser;
72
73
	/**
74
	 * Variable for tracking which variables are already loaded
75
	 * @var array $mLoadedLanguages
76
	 */
77
	protected $mLoadedLanguages = [];
78
79
	/**
80
	 * @var bool $mInParser
81
	 */
82
	protected $mInParser = false;
83
84
	/** @var BagOStuff */
85
	protected $mMemc;
86
	/** @var WANObjectCache */
87
	protected $wanCache;
88
89
	/**
90
	 * Singleton instance
91
	 *
92
	 * @var MessageCache $instance
93
	 */
94
	private static $instance;
95
96
	/**
97
	 * Get the signleton instance of this class
98
	 *
99
	 * @since 1.18
100
	 * @return MessageCache
101
	 */
102
	public static function singleton() {
103
		if ( self::$instance === null ) {
104
			global $wgUseDatabaseMessages, $wgMsgCacheExpiry;
105
			self::$instance = new self(
106
				wfGetMessageCacheStorage(),
107
				$wgUseDatabaseMessages,
108
				$wgMsgCacheExpiry
109
			);
110
		}
111
112
		return self::$instance;
113
	}
114
115
	/**
116
	 * Destroy the singleton instance
117
	 *
118
	 * @since 1.18
119
	 */
120
	public static function destroyInstance() {
121
		self::$instance = null;
122
	}
123
124
	/**
125
	 * Normalize message key input
126
	 *
127
	 * @param string $key Input message key to be normalized
128
	 * @return string Normalized message key
129
	 */
130
	public static function normalizeKey( $key ) {
131
		global $wgContLang;
132
		$lckey = strtr( $key, ' ', '_' );
133
		if ( ord( $lckey ) < 128 ) {
134
			$lckey[0] = strtolower( $lckey[0] );
135
		} else {
136
			$lckey = $wgContLang->lcfirst( $lckey );
137
		}
138
139
		return $lckey;
140
	}
141
142
	/**
143
	 * @param BagOStuff $memCached A cache instance. If none, fall back to CACHE_NONE.
144
	 * @param bool $useDB
145
	 * @param int $expiry Lifetime for cache. @see $mExpiry.
146
	 */
147
	function __construct( $memCached, $useDB, $expiry ) {
148
		global $wgUseLocalMessageCache;
149
150
		if ( !$memCached ) {
151
			$memCached = wfGetCache( CACHE_NONE );
152
		}
153
154
		$this->mMemc = $memCached;
155
		$this->mDisable = !$useDB;
156
		$this->mExpiry = $expiry;
157
158
		if ( $wgUseLocalMessageCache ) {
159
			$this->localCache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
160
		} else {
161
			$this->localCache = new EmptyBagOStuff();
162
		}
163
164
		$this->wanCache = ObjectCache::getMainWANInstance();
165
	}
166
167
	/**
168
	 * ParserOptions is lazy initialised.
169
	 *
170
	 * @return ParserOptions
171
	 */
172
	function getParserOptions() {
173
		global $wgUser;
174
175
		if ( !$this->mParserOptions ) {
176
			if ( !$wgUser->isSafeToLoad() ) {
177
				// $wgUser isn't unstubbable yet, so don't try to get a
178
				// ParserOptions for it. And don't cache this ParserOptions
179
				// either.
180
				$po = ParserOptions::newFromAnon();
181
				$po->setEditSection( false );
182
				return $po;
183
			}
184
185
			$this->mParserOptions = new ParserOptions;
186
			$this->mParserOptions->setEditSection( false );
187
		}
188
189
		return $this->mParserOptions;
190
	}
191
192
	/**
193
	 * Try to load the cache from APC.
194
	 *
195
	 * @param string $code Optional language code, see documenation of load().
196
	 * @return array|bool The cache array, or false if not in cache.
197
	 */
198
	protected function getLocalCache( $code ) {
199
		$cacheKey = wfMemcKey( __CLASS__, $code );
200
201
		return $this->localCache->get( $cacheKey );
202
	}
203
204
	/**
205
	 * Save the cache to APC.
206
	 *
207
	 * @param string $code
208
	 * @param array $cache The cache array
209
	 */
210
	protected function saveToLocalCache( $code, $cache ) {
211
		$cacheKey = wfMemcKey( __CLASS__, $code );
212
		$this->localCache->set( $cacheKey, $cache );
213
	}
214
215
	/**
216
	 * Loads messages from caches or from database in this order:
217
	 * (1) local message cache (if $wgUseLocalMessageCache is enabled)
218
	 * (2) memcached
219
	 * (3) from the database.
220
	 *
221
	 * When succesfully loading from (2) or (3), all higher level caches are
222
	 * updated for the newest version.
223
	 *
224
	 * Nothing is loaded if member variable mDisable is true, either manually
225
	 * set by calling code or if message loading fails (is this possible?).
226
	 *
227
	 * Returns true if cache is already populated or it was succesfully populated,
228
	 * or false if populating empty cache fails. Also returns true if MessageCache
229
	 * is disabled.
230
	 *
231
	 * @param string $code Language to which load messages
232
	 * @param integer $mode Use MessageCache::FOR_UPDATE to skip process cache [optional]
233
	 * @throws MWException
234
	 * @return bool
235
	 */
236
	protected function load( $code, $mode = null ) {
237
		if ( !is_string( $code ) ) {
238
			throw new InvalidArgumentException( "Missing language code" );
239
		}
240
241
		# Don't do double loading...
242
		if ( isset( $this->mLoadedLanguages[$code] ) && $mode != self::FOR_UPDATE ) {
243
			return true;
244
		}
245
246
		# 8 lines of code just to say (once) that message cache is disabled
247
		if ( $this->mDisable ) {
248
			static $shownDisabled = false;
249
			if ( !$shownDisabled ) {
250
				wfDebug( __METHOD__ . ": disabled\n" );
251
				$shownDisabled = true;
252
			}
253
254
			return true;
255
		}
256
257
		# Loading code starts
258
		$success = false; # Keep track of success
259
		$staleCache = false; # a cache array with expired data, or false if none has been loaded
260
		$where = []; # Debug info, delayed to avoid spamming debug log too much
261
262
		# Hash of the contents is stored in memcache, to detect if data-center cache
263
		# or local cache goes out of date (e.g. due to replace() on some other server)
264
		list( $hash, $hashVolatile ) = $this->getValidationHash( $code );
265
266
		# Try the local cache and check against the cluster hash key...
267
		$cache = $this->getLocalCache( $code );
268
		if ( !$cache ) {
269
			$where[] = 'local cache is empty';
270
		} elseif ( !isset( $cache['HASH'] ) || $cache['HASH'] !== $hash ) {
271
			$where[] = 'local cache has the wrong hash';
272
			$staleCache = $cache;
273
		} elseif ( $this->isCacheExpired( $cache ) ) {
274
			$where[] = 'local cache is expired';
275
			$staleCache = $cache;
276
		} elseif ( $hashVolatile ) {
277
			$where[] = 'local cache validation key is expired/volatile';
278
			$staleCache = $cache;
279
		} else {
280
			$where[] = 'got from local cache';
281
			$success = true;
282
			$this->mCache[$code] = $cache;
283
		}
284
285
		if ( !$success ) {
286
			$cacheKey = wfMemcKey( 'messages', $code ); # Key in memc for messages
287
			# Try the global cache. If it is empty, try to acquire a lock. If
288
			# the lock can't be acquired, wait for the other thread to finish
289
			# and then try the global cache a second time.
290
			for ( $failedAttempts = 0; $failedAttempts <= 1; $failedAttempts++ ) {
291
				if ( $hashVolatile && $staleCache ) {
292
					# Do not bother fetching the whole cache blob to avoid I/O.
293
					# Instead, just try to get the non-blocking $statusKey lock
294
					# below, and use the local stale value if it was not acquired.
295
					$where[] = 'global cache is presumed expired';
296
				} else {
297
					$cache = $this->mMemc->get( $cacheKey );
298
					if ( !$cache ) {
299
						$where[] = 'global cache is empty';
300
					} elseif ( $this->isCacheExpired( $cache ) ) {
301
						$where[] = 'global cache is expired';
302
						$staleCache = $cache;
303
					} elseif ( $hashVolatile ) {
304
						# DB results are replica DB lag prone until the holdoff TTL passes.
305
						# By then, updates should be reflected in loadFromDBWithLock().
306
						# One thread renerates the cache while others use old values.
307
						$where[] = 'global cache is expired/volatile';
308
						$staleCache = $cache;
309
					} else {
310
						$where[] = 'got from global cache';
311
						$this->mCache[$code] = $cache;
312
						$this->saveToCaches( $cache, 'local-only', $code );
313
						$success = true;
314
					}
315
				}
316
317
				if ( $success ) {
318
					# Done, no need to retry
319
					break;
320
				}
321
322
				# We need to call loadFromDB. Limit the concurrency to one process.
323
				# This prevents the site from going down when the cache expires.
324
				# Note that the DB slam protection lock here is non-blocking.
325
				$loadStatus = $this->loadFromDBWithLock( $code, $where, $mode );
326
				if ( $loadStatus === true ) {
327
					$success = true;
328
					break;
329
				} elseif ( $staleCache ) {
330
					# Use the stale cache while some other thread constructs the new one
331
					$where[] = 'using stale cache';
332
					$this->mCache[$code] = $staleCache;
333
					$success = true;
334
					break;
335
				} elseif ( $failedAttempts > 0 ) {
336
					# Already blocked once, so avoid another lock/unlock cycle.
337
					# This case will typically be hit if memcached is down, or if
338
					# loadFromDB() takes longer than LOCK_WAIT.
339
					$where[] = "could not acquire status key.";
340
					break;
341
				} elseif ( $loadStatus === 'cantacquire' ) {
342
					# Wait for the other thread to finish, then retry. Normally,
343
					# the memcached get() will then yeild the other thread's result.
344
					$where[] = 'waited for other thread to complete';
345
					$this->getReentrantScopedLock( $cacheKey );
346
				} else {
347
					# Disable cache; $loadStatus is 'disabled'
348
					break;
349
				}
350
			}
351
		}
352
353
		if ( !$success ) {
354
			$where[] = 'loading FAILED - cache is disabled';
355
			$this->mDisable = true;
356
			$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...
357
			wfDebugLog( 'MessageCacheError', __METHOD__ . ": Failed to load $code\n" );
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_REPLICA );
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 ) {
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 ) {
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 );
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 View Code Duplication
		} 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 replica DB 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
			$this->setValidationHash( $code, $cache );
633
		} else {
634
			$success = true;
635
		}
636
637
		$this->saveToLocalCache( $code, $cache );
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 );
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();
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
		} else {
866
			$uckey = null;
867
		}
868
869
		// Check the CDB cache
870
		$message = $lang->getMessage( $lckey );
871
		if ( $message !== null ) {
872
			return $message;
873
		}
874
875
		// Try checking the database for all of the fallback languages
876
		if ( $useDB ) {
877
			$fallbackChain = Language::getFallbacksFor( $langcode );
878
879
			foreach ( $fallbackChain as $code ) {
880
				if ( isset( $alreadyTried[ $code ] ) ) {
881
					continue;
882
				}
883
884
				$message = $this->getMsgFromNamespace(
885
					$this->getMessagePageName( $code, $uckey ), $code );
886
887
				if ( $message !== false ) {
888
					return $message;
889
				}
890
				$alreadyTried[ $code ] = true;
891
			}
892
		}
893
894
		return false;
895
	}
896
897
	/**
898
	 * Get the message page name for a given language
899
	 *
900
	 * @param string $langcode
901
	 * @param string $uckey Uppercase key for the message
902
	 * @return string The page name
903
	 */
904
	private function getMessagePageName( $langcode, $uckey ) {
905
		global $wgLanguageCode;
906
		if ( $langcode === $wgLanguageCode ) {
907
			// Messages created in the content language will not have the /lang extension
908
			return $uckey;
909
		} else {
910
			return "$uckey/$langcode";
911
		}
912
	}
913
914
	/**
915
	 * Get a message from the MediaWiki namespace, with caching. The key must
916
	 * first be converted to two-part lang/msg form if necessary.
917
	 *
918
	 * Unlike self::get(), this function doesn't resolve fallback chains, and
919
	 * some callers require this behavior. LanguageConverter::parseCachedTable()
920
	 * and self::get() are some examples in core.
921
	 *
922
	 * @param string $title Message cache key with initial uppercase letter.
923
	 * @param string $code Code denoting the language to try.
924
	 * @return string|bool The message, or false if it does not exist or on error
925
	 */
926
	public function getMsgFromNamespace( $title, $code ) {
927
		$this->load( $code );
928
		if ( isset( $this->mCache[$code][$title] ) ) {
929
			$entry = $this->mCache[$code][$title];
930
			if ( substr( $entry, 0, 1 ) === ' ' ) {
931
				// The message exists, so make sure a string
932
				// is returned.
933
				return (string)substr( $entry, 1 );
934
			} elseif ( $entry === '!NONEXISTENT' ) {
935
				return false;
936
			} elseif ( $entry === '!TOO BIG' ) {
937
				// Fall through and try invididual message cache below
938
			}
939
		} else {
940
			// XXX: This is not cached in process cache, should it?
941
			$message = false;
942
			Hooks::run( 'MessagesPreLoad', [ $title, &$message ] );
943
			if ( $message !== false ) {
944
				return $message;
945
			}
946
947
			return false;
948
		}
949
950
		// Try the individual message cache
951
		$titleKey = wfMemcKey( 'messages', 'individual', $title );
952
953
		$curTTL = null;
954
		$entry = $this->wanCache->get(
955
			$titleKey,
956
			$curTTL,
957
			[ wfMemcKey( 'messages', $code ) ]
958
		);
959
		$entry = ( $curTTL >= 0 ) ? $entry : false;
960
961
		if ( $entry ) {
962
			if ( substr( $entry, 0, 1 ) === ' ' ) {
963
				$this->mCache[$code][$title] = $entry;
964
				// The message exists, so make sure a string is returned
965
				return (string)substr( $entry, 1 );
966
			} elseif ( $entry === '!NONEXISTENT' ) {
967
				$this->mCache[$code][$title] = '!NONEXISTENT';
968
969
				return false;
970
			} else {
971
				// Corrupt/obsolete entry, delete it
972
				$this->wanCache->delete( $titleKey );
973
			}
974
		}
975
976
		// Try loading it from the database
977
		$dbr = wfGetDB( DB_REPLICA );
978
		$cacheOpts = Database::getCacheSetOptions( $dbr );
979
		// Use newKnownCurrent() to avoid querying revision/user tables
980
		$titleObj = Title::makeTitle( NS_MEDIAWIKI, $title );
981
		if ( $titleObj->getLatestRevID() ) {
982
			$revision = Revision::newKnownCurrent(
983
				$dbr,
984
				$titleObj->getArticleID(),
985
				$titleObj->getLatestRevID()
986
			);
987
		} else {
988
			$revision = false;
989
		}
990
991
		if ( $revision ) {
992
			$content = $revision->getContent();
993
			if ( !$content ) {
994
				// A possibly temporary loading failure.
995
				wfDebugLog(
996
					'MessageCache',
997
					__METHOD__ . ": failed to load message page text for {$title} ($code)"
998
				);
999
				$message = null; // no negative caching
1000
			} else {
1001
				// XXX: Is this the right way to turn a Content object into a message?
1002
				// NOTE: $content is typically either WikitextContent, JavaScriptContent or
1003
				//       CssContent. MessageContent is *not* used for storing messages, it's
1004
				//       only used for wrapping them when needed.
1005
				$message = $content->getWikitextForTransclusion();
1006
1007
				if ( $message === false || $message === null ) {
1008
					wfDebugLog(
1009
						'MessageCache',
1010
						__METHOD__ . ": message content doesn't provide wikitext "
1011
							. "(content model: " . $content->getModel() . ")"
1012
					);
1013
1014
					$message = false; // negative caching
1015
				} else {
1016
					$this->mCache[$code][$title] = ' ' . $message;
1017
					$this->wanCache->set( $titleKey, ' ' . $message, $this->mExpiry, $cacheOpts );
1018
				}
1019
			}
1020
		} else {
1021
			$message = false; // negative caching
1022
		}
1023
1024 View Code Duplication
		if ( $message === false ) { // negative caching
1025
			$this->mCache[$code][$title] = '!NONEXISTENT';
1026
			$this->wanCache->set( $titleKey, '!NONEXISTENT', $this->mExpiry, $cacheOpts );
1027
		}
1028
1029
		return $message;
1030
	}
1031
1032
	/**
1033
	 * @param string $message
1034
	 * @param bool $interface
1035
	 * @param string $language Language code
1036
	 * @param Title $title
1037
	 * @return string
1038
	 */
1039
	function transform( $message, $interface = false, $language = null, $title = null ) {
1040
		// Avoid creating parser if nothing to transform
1041
		if ( strpos( $message, '{{' ) === false ) {
1042
			return $message;
1043
		}
1044
1045
		if ( $this->mInParser ) {
1046
			return $message;
1047
		}
1048
1049
		$parser = $this->getParser();
1050
		if ( $parser ) {
1051
			$popts = $this->getParserOptions();
1052
			$popts->setInterfaceMessage( $interface );
1053
			$popts->setTargetLanguage( $language );
1054
1055
			$userlang = $popts->setUserLang( $language );
1056
			$this->mInParser = true;
1057
			$message = $parser->transformMsg( $message, $popts, $title );
1058
			$this->mInParser = false;
1059
			$popts->setUserLang( $userlang );
1060
		}
1061
1062
		return $message;
1063
	}
1064
1065
	/**
1066
	 * @return Parser
1067
	 */
1068
	function getParser() {
1069
		global $wgParser, $wgParserConf;
1070
		if ( !$this->mParser && isset( $wgParser ) ) {
1071
			# Do some initialisation so that we don't have to do it twice
1072
			$wgParser->firstCallInit();
1073
			# Clone it and store it
1074
			$class = $wgParserConf['class'];
1075
			if ( $class == 'ParserDiffTest' ) {
1076
				# Uncloneable
1077
				$this->mParser = new $class( $wgParserConf );
1078
			} else {
1079
				$this->mParser = clone $wgParser;
1080
			}
1081
		}
1082
1083
		return $this->mParser;
1084
	}
1085
1086
	/**
1087
	 * @param string $text
1088
	 * @param Title $title
1089
	 * @param bool $linestart Whether or not this is at the start of a line
1090
	 * @param bool $interface Whether this is an interface message
1091
	 * @param Language|string $language Language code
1092
	 * @return ParserOutput|string
1093
	 */
1094
	public function parse( $text, $title = null, $linestart = true,
1095
		$interface = false, $language = null
1096
	) {
1097
		if ( $this->mInParser ) {
1098
			return htmlspecialchars( $text );
1099
		}
1100
1101
		$parser = $this->getParser();
1102
		$popts = $this->getParserOptions();
1103
		$popts->setInterfaceMessage( $interface );
1104
1105
		if ( is_string( $language ) ) {
1106
			$language = Language::factory( $language );
1107
		}
1108
		$popts->setTargetLanguage( $language );
1109
1110
		if ( !$title || !$title instanceof Title ) {
1111
			global $wgTitle;
1112
			wfDebugLog( 'GlobalTitleFail', __METHOD__ . ' called by ' .
1113
				wfGetAllCallers( 6 ) . ' with no title set.' );
1114
			$title = $wgTitle;
1115
		}
1116
		// Sometimes $wgTitle isn't set either...
1117
		if ( !$title ) {
1118
			# It's not uncommon having a null $wgTitle in scripts. See r80898
1119
			# Create a ghost title in such case
1120
			$title = Title::makeTitle( NS_SPECIAL, 'Badtitle/title not set in ' . __METHOD__ );
1121
		}
1122
1123
		$this->mInParser = true;
1124
		$res = $parser->parse( $text, $title, $popts, $linestart );
1125
		$this->mInParser = false;
1126
1127
		return $res;
1128
	}
1129
1130
	function disable() {
1131
		$this->mDisable = true;
1132
	}
1133
1134
	function enable() {
1135
		$this->mDisable = false;
1136
	}
1137
1138
	/**
1139
	 * Whether DB/cache usage is disabled for determining messages
1140
	 *
1141
	 * If so, this typically indicates either:
1142
	 *   - a) load() failed to find a cached copy nor query the DB
1143
	 *   - b) we are in a special context or error mode that cannot use the DB
1144
	 * If the DB is ignored, any derived HTML output or cached objects may be wrong.
1145
	 * To avoid long-term cache pollution, TTLs can be adjusted accordingly.
1146
	 *
1147
	 * @return bool
1148
	 * @since 1.27
1149
	 */
1150
	public function isDisabled() {
1151
		return $this->mDisable;
1152
	}
1153
1154
	/**
1155
	 * Clear all stored messages. Mainly used after a mass rebuild.
1156
	 */
1157
	function clear() {
1158
		$langs = Language::fetchLanguageNames( null, 'mw' );
1159
		foreach ( array_keys( $langs ) as $code ) {
1160
			# Global and local caches
1161
			$this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) );
1162
		}
1163
1164
		$this->mLoadedLanguages = [];
1165
	}
1166
1167
	/**
1168
	 * @param string $key
1169
	 * @return array
1170
	 */
1171
	public function figureMessage( $key ) {
1172
		global $wgLanguageCode;
1173
1174
		$pieces = explode( '/', $key );
1175
		if ( count( $pieces ) < 2 ) {
1176
			return [ $key, $wgLanguageCode ];
1177
		}
1178
1179
		$lang = array_pop( $pieces );
1180
		if ( !Language::fetchLanguageName( $lang, null, 'mw' ) ) {
1181
			return [ $key, $wgLanguageCode ];
1182
		}
1183
1184
		$message = implode( '/', $pieces );
1185
1186
		return [ $message, $lang ];
1187
	}
1188
1189
	/**
1190
	 * Get all message keys stored in the message cache for a given language.
1191
	 * If $code is the content language code, this will return all message keys
1192
	 * for which MediaWiki:msgkey exists. If $code is another language code, this
1193
	 * will ONLY return message keys for which MediaWiki:msgkey/$code exists.
1194
	 * @param string $code Language code
1195
	 * @return array Array of message keys (strings)
1196
	 */
1197
	public function getAllMessageKeys( $code ) {
1198
		global $wgContLang;
1199
		$this->load( $code );
1200
		if ( !isset( $this->mCache[$code] ) ) {
1201
			// Apparently load() failed
1202
			return null;
1203
		}
1204
		// Remove administrative keys
1205
		$cache = $this->mCache[$code];
1206
		unset( $cache['VERSION'] );
1207
		unset( $cache['EXPIRY'] );
1208
		// Remove any !NONEXISTENT keys
1209
		$cache = array_diff( $cache, [ '!NONEXISTENT' ] );
1210
1211
		// Keys may appear with a capital first letter. lcfirst them.
1212
		return array_map( [ $wgContLang, 'lcfirst' ], array_keys( $cache ) );
1213
	}
1214
}
1215