Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like MessageCache often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use MessageCache, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 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() { |
||
| 114 | |||
| 115 | /** |
||
| 116 | * Destroy the singleton instance |
||
| 117 | * |
||
| 118 | * @since 1.18 |
||
| 119 | */ |
||
| 120 | public static function destroyInstance() { |
||
| 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 ) { |
||
| 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 ) { |
||
| 166 | |||
| 167 | /** |
||
| 168 | * ParserOptions is lazy initialised. |
||
| 169 | * |
||
| 170 | * @return ParserOptions |
||
| 171 | */ |
||
| 172 | function getParserOptions() { |
||
| 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 ) { |
||
| 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 ) { |
||
| 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 ) { |
||
| 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 ) { |
||
| 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 ) { |
||
| 1064 | |||
| 1065 | /** |
||
| 1066 | * @return Parser |
||
| 1067 | */ |
||
| 1068 | function getParser() { |
||
| 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, |
||
| 1129 | |||
| 1130 | function disable() { |
||
| 1133 | |||
| 1134 | function enable() { |
||
| 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() { |
||
| 1153 | |||
| 1154 | /** |
||
| 1155 | * Clear all stored messages. Mainly used after a mass rebuild. |
||
| 1156 | */ |
||
| 1157 | function clear() { |
||
| 1166 | |||
| 1167 | /** |
||
| 1168 | * @param string $key |
||
| 1169 | * @return array |
||
| 1170 | */ |
||
| 1171 | public function figureMessage( $key ) { |
||
| 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 ) { |
||
| 1214 | } |
||
| 1215 |
Let’s assume that you have a directory layout like this:
. |-- OtherDir | |-- Bar.php | `-- Foo.php `-- SomeDir `-- Foo.phpand let’s assume the following content of
Bar.php:If both files
OtherDir/Foo.phpandSomeDir/Foo.phpare loaded in the same runtime, you will see a PHP error such as the following:PHP Fatal error: Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.phpHowever, as
OtherDir/Foo.phpdoes not necessarily have to be loaded and the error is only triggered if it is loaded beforeOtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias: