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; |
||
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 | ' ', |
||
790 | # Fix for NBSP, converted to space by firefox |
||
791 | ' ', |
||
792 | ' ', |
||
793 | '­' |
||
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, |
||
0 ignored issues
–
show
|
|||
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 |
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: