Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
72.37% |
165 / 228 |
|
55.00% |
11 / 20 |
CRAP | |
0.00% |
0 / 1 |
| LinkCache | |
73.01% |
165 / 226 |
|
55.00% |
11 / 20 |
263.07 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| setLogger | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getCacheKey | |
54.84% |
17 / 31 |
|
0.00% |
0 / 1 |
25.26 | |||
| getGoodLinkID | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
| getGoodLinkFieldObj | |
92.31% |
24 / 26 |
|
0.00% |
0 / 1 |
13.08 | |||
| isBadLink | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| addGoodLinkObjFromRow | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
| addBadLinkObj | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| clearBadLink | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
4.59 | |||
| clearLink | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| getSelectFields | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
2.00 | |||
| addLinkObj | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| getGoodLinkRowInternal | |
94.44% |
34 / 36 |
|
0.00% |
0 / 1 |
13.03 | |||
| getGoodLinkRow | |
61.90% |
13 / 21 |
|
0.00% |
0 / 1 |
7.99 | |||
| getPersistentCacheKey | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| usePersistentCache | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
8.09 | |||
| fetchPageRow | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| executeBatch | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
90 | |||
| invalidateTitle | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| clear | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | |
| 7 | namespace MediaWiki\Page; |
| 8 | |
| 9 | use InvalidArgumentException; |
| 10 | use MediaWiki\MainConfigNames; |
| 11 | use MediaWiki\MediaWikiServices; |
| 12 | use MediaWiki\Title\NamespaceInfo; |
| 13 | use MediaWiki\Title\Title; |
| 14 | use MediaWiki\Title\TitleFormatter; |
| 15 | use MediaWiki\Title\TitleValue; |
| 16 | use Psr\Log\LoggerAwareInterface; |
| 17 | use Psr\Log\LoggerInterface; |
| 18 | use Psr\Log\NullLogger; |
| 19 | use stdClass; |
| 20 | use Wikimedia\MapCacheLRU\MapCacheLRU; |
| 21 | use Wikimedia\ObjectCache\WANObjectCache; |
| 22 | use Wikimedia\Parsoid\Core\LinkTarget; |
| 23 | use Wikimedia\Rdbms\Database; |
| 24 | use Wikimedia\Rdbms\IDBAccessObject; |
| 25 | use Wikimedia\Rdbms\ILoadBalancer; |
| 26 | use Wikimedia\Rdbms\IReadableDatabase; |
| 27 | use Wikimedia\Timestamp\TimestampFormat as TS; |
| 28 | |
| 29 | /** |
| 30 | * Page existence and metadata cache. |
| 31 | * |
| 32 | * This is exists primarily to reduce heavy database load from the Parser when |
| 33 | * rendering each individual outgoing link and template transclusion when |
| 34 | * parsing wikitext. |
| 35 | * |
| 36 | * See [the architecture doc](@ref linkcache) at docs/LinkCache.md for more information. |
| 37 | * |
| 38 | * To create a batch, you can use the following code: |
| 39 | * |
| 40 | * @code |
| 41 | * $titles = []; |
| 42 | * foreach ( [ 'Main Page', 'Project:Help' ] as $page ) { |
| 43 | * $titles[] = Title::newFromText( $page ); |
| 44 | * } |
| 45 | * $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory(); |
| 46 | * $linkBatchFactory->newLinkBatch( $titles )->setCaller( __METHOD__ )->execute(); |
| 47 | * @endcode |
| 48 | * |
| 49 | * @see MediaWiki\Page\LinkBatchFactory |
| 50 | * @see MediaWiki\Page\LinkBatch |
| 51 | * @since 1.1 |
| 52 | * @ingroup Page |
| 53 | */ |
| 54 | class LinkCache implements LoggerAwareInterface { |
| 55 | /** @var MapCacheLRU */ |
| 56 | private $entries; |
| 57 | /** @var WANObjectCache */ |
| 58 | private $wanCache; |
| 59 | /** @var TitleFormatter */ |
| 60 | private $titleFormatter; |
| 61 | /** @var NamespaceInfo */ |
| 62 | private $nsInfo; |
| 63 | /** @var ILoadBalancer|null */ |
| 64 | private $loadBalancer; |
| 65 | /** @var LoggerInterface */ |
| 66 | private $logger; |
| 67 | |
| 68 | /** How many Titles to store */ |
| 69 | private const MAX_SIZE = 10000; |
| 70 | |
| 71 | /** Key to page row object or null */ |
| 72 | private const ROW = 0; |
| 73 | /** Key to query READ_* flags */ |
| 74 | private const FLAGS = 1; |
| 75 | |
| 76 | /** |
| 77 | * @param TitleFormatter $titleFormatter |
| 78 | * @param WANObjectCache $cache |
| 79 | * @param NamespaceInfo $nsInfo |
| 80 | * @param ILoadBalancer|null $loadBalancer Use null when no database is set up, for example on installation |
| 81 | */ |
| 82 | public function __construct( |
| 83 | TitleFormatter $titleFormatter, |
| 84 | WANObjectCache $cache, |
| 85 | NamespaceInfo $nsInfo, |
| 86 | ?ILoadBalancer $loadBalancer = null |
| 87 | ) { |
| 88 | $this->entries = new MapCacheLRU( self::MAX_SIZE ); |
| 89 | $this->wanCache = $cache; |
| 90 | $this->titleFormatter = $titleFormatter; |
| 91 | $this->nsInfo = $nsInfo; |
| 92 | $this->loadBalancer = $loadBalancer; |
| 93 | $this->logger = new NullLogger(); |
| 94 | } |
| 95 | |
| 96 | public function setLogger( LoggerInterface $logger ): void { |
| 97 | $this->logger = $logger; |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * @param LinkTarget|PageReference|array|string $page |
| 102 | * @param bool $passThrough Return $page if $page is a string |
| 103 | * @return ?string the cache key |
| 104 | */ |
| 105 | private function getCacheKey( $page, $passThrough = false ) { |
| 106 | if ( is_string( $page ) ) { |
| 107 | if ( $passThrough ) { |
| 108 | return $page; |
| 109 | } else { |
| 110 | throw new InvalidArgumentException( 'They key may not be given as a string here' ); |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | if ( is_array( $page ) ) { |
| 115 | $namespace = $page['page_namespace']; |
| 116 | $dbkey = $page['page_title']; |
| 117 | return strtr( $this->titleFormatter->formatTitle( $namespace, $dbkey ), ' ', '_' ); |
| 118 | } |
| 119 | |
| 120 | if ( $page instanceof PageReference && $page->getWikiId() !== PageReference::LOCAL ) { |
| 121 | // No cross-wiki support yet. Perhaps LinkCache can become wiki-aware in the future. |
| 122 | $this->logger->info( |
| 123 | 'cross-wiki page reference', |
| 124 | [ |
| 125 | 'page-wiki' => $page->getWikiId(), |
| 126 | 'page-reference' => $this->titleFormatter->getFullText( $page ) |
| 127 | ] |
| 128 | ); |
| 129 | return null; |
| 130 | } |
| 131 | |
| 132 | if ( $page instanceof PageIdentity && !$page->canExist() ) { |
| 133 | // Non-proper page, perhaps a special page or interwiki link or relative section link. |
| 134 | $this->logger->warning( |
| 135 | 'non-proper page reference: {page-reference}', |
| 136 | [ 'page-reference' => $this->titleFormatter->getFullText( $page ) ] |
| 137 | ); |
| 138 | return null; |
| 139 | } |
| 140 | |
| 141 | if ( $page instanceof LinkTarget |
| 142 | && ( $page->isExternal() || $page->getText() === '' || $page->getNamespace() < 0 ) |
| 143 | ) { |
| 144 | // Interwiki link or relative section link. These do not have a page ID, so they |
| 145 | // can neither be "good" nor "bad" in the sense of this class. |
| 146 | $this->logger->warning( |
| 147 | 'link to non-proper page: {page-link}', |
| 148 | [ 'page-link' => $this->titleFormatter->getFullText( $page ) ] |
| 149 | ); |
| 150 | return null; |
| 151 | } |
| 152 | |
| 153 | return $this->titleFormatter->getPrefixedDBkey( $page ); |
| 154 | } |
| 155 | |
| 156 | /** |
| 157 | * Get the ID of a page known to the process cache |
| 158 | * |
| 159 | * @param LinkTarget|PageReference|array|string $page The page to get the ID for, |
| 160 | * as an object, an array containing the page_namespace and page_title fields, |
| 161 | * or a prefixed DB key. In MediaWiki 1.36 and earlier, only a string was accepted. |
| 162 | * @return int Page ID, or zero if the page was not cached or does not exist or is not a |
| 163 | * proper page (e.g. a special page or an interwiki link). |
| 164 | */ |
| 165 | public function getGoodLinkID( $page ) { |
| 166 | $key = $this->getCacheKey( $page, true ); |
| 167 | if ( $key === null ) { |
| 168 | return 0; |
| 169 | } |
| 170 | |
| 171 | $entry = $this->entries->get( $key ); |
| 172 | if ( !$entry ) { |
| 173 | return 0; |
| 174 | } |
| 175 | |
| 176 | $row = $entry[self::ROW]; |
| 177 | |
| 178 | return $row ? (int)$row->page_id : 0; |
| 179 | } |
| 180 | |
| 181 | /** |
| 182 | * Get the field of a page known to the process cache |
| 183 | * |
| 184 | * If this link is not a cached good title, it will return NULL. |
| 185 | * @param LinkTarget|PageReference|array $page The page to get cached info for. |
| 186 | * Can be given as an object or an associative array containing the |
| 187 | * page_namespace and page_title fields. |
| 188 | * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. |
| 189 | * @param string $field ( 'id', 'length', 'redirect', 'revision', 'model', 'lang' ) |
| 190 | * @return string|int|null The field value, or null if the page was not cached or does not exist |
| 191 | * or is not a proper page (e.g. a special page or interwiki link). |
| 192 | */ |
| 193 | public function getGoodLinkFieldObj( $page, string $field ) { |
| 194 | $key = $this->getCacheKey( $page ); |
| 195 | if ( $key === null ) { |
| 196 | return null; |
| 197 | } |
| 198 | |
| 199 | $entry = $this->entries->get( $key ); |
| 200 | if ( !$entry ) { |
| 201 | return null; |
| 202 | } |
| 203 | |
| 204 | $row = $entry[self::ROW]; |
| 205 | if ( !$row ) { |
| 206 | return null; |
| 207 | } |
| 208 | |
| 209 | switch ( $field ) { |
| 210 | case 'id': |
| 211 | return (int)$row->page_id; |
| 212 | case 'length': |
| 213 | return (int)$row->page_len; |
| 214 | case 'redirect': |
| 215 | return (int)$row->page_is_redirect; |
| 216 | case 'revision': |
| 217 | return (int)$row->page_latest; |
| 218 | case 'model': |
| 219 | return !empty( $row->page_content_model ) |
| 220 | ? (string)$row->page_content_model |
| 221 | : null; |
| 222 | case 'lang': |
| 223 | return !empty( $row->page_lang ) |
| 224 | ? (string)$row->page_lang |
| 225 | : null; |
| 226 | default: |
| 227 | throw new InvalidArgumentException( "Unknown field: $field" ); |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | /** |
| 232 | * Check if a page is known to be missing based on the process cache |
| 233 | * |
| 234 | * @param LinkTarget|PageReference|array|string $page The page to get cached info for, |
| 235 | * as an object, an array containing the page_namespace and page_title fields, |
| 236 | * or a prefixed DB key. In MediaWiki 1.36 and earlier, only a string was accepted. |
| 237 | * In MediaWiki 1.36 and earlier, only a string was accepted. |
| 238 | * @return bool Whether the page is known to be missing based on the process cache |
| 239 | */ |
| 240 | public function isBadLink( $page ) { |
| 241 | $key = $this->getCacheKey( $page, true ); |
| 242 | if ( $key === null ) { |
| 243 | return false; |
| 244 | } |
| 245 | |
| 246 | $entry = $this->entries->get( $key ); |
| 247 | |
| 248 | return ( $entry && !$entry[self::ROW] ); |
| 249 | } |
| 250 | |
| 251 | /** |
| 252 | * Add information about an existing page to the process cache |
| 253 | * |
| 254 | * Callers must set the READ_LATEST flag if the row came from a DB_PRIMARY source. |
| 255 | * However, the use of such data is highly discouraged; most callers rely on seeing |
| 256 | * consistent DB_REPLICA data (e.g. REPEATABLE-READ point-in-time snapshots) and the |
| 257 | * accidental use of DB_PRIMARY data via LinkCache is prone to causing anomalies. |
| 258 | * |
| 259 | * @param LinkTarget|PageReference|array $page The page to set cached info for. |
| 260 | * Can be given as an object or an associative array containing the |
| 261 | * page_namespace and page_title fields. |
| 262 | * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. |
| 263 | * @param stdClass $row Object which has all fields returned by getSelectFields(). |
| 264 | * @param int $queryFlags The query flags used to retrieve the row, IDBAccessObject::READ_* |
| 265 | * @since 1.19 |
| 266 | */ |
| 267 | public function addGoodLinkObjFromRow( |
| 268 | $page, |
| 269 | stdClass $row, |
| 270 | int $queryFlags = IDBAccessObject::READ_NORMAL |
| 271 | ) { |
| 272 | $key = $this->getCacheKey( $page ); |
| 273 | if ( $key === null ) { |
| 274 | return; |
| 275 | } |
| 276 | |
| 277 | foreach ( self::getSelectFields() as $field ) { |
| 278 | if ( !property_exists( $row, $field ) ) { |
| 279 | throw new InvalidArgumentException( "Missing field: $field" ); |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | $this->entries->set( $key, [ self::ROW => $row, self::FLAGS => $queryFlags ] ); |
| 284 | } |
| 285 | |
| 286 | /** |
| 287 | * Add information about a missing page to the process cache |
| 288 | * |
| 289 | * Callers must set the READ_LATEST flag if the row came from a DB_PRIMARY source. |
| 290 | * However, the use of such data is highly discouraged; most callers rely on seeing |
| 291 | * consistent DB_REPLICA data (e.g. REPEATABLE-READ point-in-time snapshots) and the |
| 292 | * accidental use of DB_PRIMARY data via LinkCache is prone to causing anomalies. |
| 293 | * |
| 294 | * @param LinkTarget|PageReference|array $page The page to set cached info for. |
| 295 | * Can be given as an object or an associative array containing the |
| 296 | * page_namespace and page_title fields. |
| 297 | * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. |
| 298 | * @param int $queryFlags The query flags used to retrieve the row, IDBAccessObject::READ_* |
| 299 | */ |
| 300 | public function addBadLinkObj( $page, int $queryFlags = IDBAccessObject::READ_NORMAL ) { |
| 301 | $key = $this->getCacheKey( $page ); |
| 302 | if ( $key === null ) { |
| 303 | return; |
| 304 | } |
| 305 | |
| 306 | $this->entries->set( $key, [ self::ROW => null, self::FLAGS => $queryFlags ] ); |
| 307 | } |
| 308 | |
| 309 | /** |
| 310 | * Clear information about a page being missing from the process cache |
| 311 | * |
| 312 | * @param LinkTarget|PageReference|array|string $page The page to clear cached info for, |
| 313 | * as an object, an array containing the page_namespace and page_title fields, |
| 314 | * or a prefixed DB key. In MediaWiki 1.36 and earlier, only a string was accepted. |
| 315 | * In MediaWiki 1.36 and earlier, only a string was accepted. |
| 316 | */ |
| 317 | public function clearBadLink( $page ) { |
| 318 | $key = $this->getCacheKey( $page, true ); |
| 319 | if ( $key === null ) { |
| 320 | return; |
| 321 | } |
| 322 | |
| 323 | $entry = $this->entries->get( $key ); |
| 324 | if ( $entry && !$entry[self::ROW] ) { |
| 325 | $this->entries->clear( $key ); |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | /** |
| 330 | * Clear information about a page from the process cache |
| 331 | * |
| 332 | * @param LinkTarget|PageReference|array $page The page to clear cached info for. |
| 333 | * Can be given as an object or an associative array containing the |
| 334 | * page_namespace and page_title fields. |
| 335 | * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. |
| 336 | */ |
| 337 | public function clearLink( $page ) { |
| 338 | $key = $this->getCacheKey( $page ); |
| 339 | if ( $key !== null ) { |
| 340 | $this->entries->clear( $key ); |
| 341 | } |
| 342 | } |
| 343 | |
| 344 | /** |
| 345 | * Fields that LinkCache needs to select |
| 346 | * |
| 347 | * @since 1.28 |
| 348 | * @return array |
| 349 | */ |
| 350 | public static function getSelectFields() { |
| 351 | $pageLanguageUseDB = MediaWikiServices::getInstance()->getMainConfig() |
| 352 | ->get( MainConfigNames::PageLanguageUseDB ); |
| 353 | |
| 354 | $fields = array_merge( |
| 355 | PageStoreRecord::REQUIRED_FIELDS, |
| 356 | [ |
| 357 | 'page_len', |
| 358 | 'page_content_model', |
| 359 | ] |
| 360 | ); |
| 361 | |
| 362 | if ( $pageLanguageUseDB ) { |
| 363 | $fields[] = 'page_lang'; |
| 364 | } |
| 365 | |
| 366 | return $fields; |
| 367 | } |
| 368 | |
| 369 | /** |
| 370 | * Add a title to the link cache, return the page_id or zero if non-existent. |
| 371 | * This causes the link to be looked up in the database if it is not yet cached. |
| 372 | * |
| 373 | * @deprecated since 1.37, use PageStore::getPageForLink() instead. |
| 374 | * |
| 375 | * @param LinkTarget|PageReference|array $page The page to load. |
| 376 | * Can be given as an object or an associative array containing the |
| 377 | * page_namespace and page_title fields. |
| 378 | * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. |
| 379 | * @param int $queryFlags IDBAccessObject::READ_XXX |
| 380 | * |
| 381 | * @return int Page ID or zero |
| 382 | */ |
| 383 | public function addLinkObj( $page, int $queryFlags = IDBAccessObject::READ_NORMAL ) { |
| 384 | $row = $this->getGoodLinkRow( |
| 385 | $page->getNamespace(), |
| 386 | $page->getDBkey(), |
| 387 | $this->fetchPageRow( ... ), |
| 388 | $queryFlags |
| 389 | ); |
| 390 | |
| 391 | return $row ? (int)$row->page_id : 0; |
| 392 | } |
| 393 | |
| 394 | /** |
| 395 | * @param TitleValue $link |
| 396 | * @param callable|null $fetchCallback |
| 397 | * @param int $queryFlags |
| 398 | * @return array [ $shouldAddGoodLink, $row ], $shouldAddGoodLink is a bool indicating |
| 399 | * whether addGoodLinkObjFromRow should be called, and $row is the row the caller was looking |
| 400 | * for (or null, when it was not found). |
| 401 | */ |
| 402 | private function getGoodLinkRowInternal( |
| 403 | TitleValue $link, |
| 404 | ?callable $fetchCallback = null, |
| 405 | int $queryFlags = IDBAccessObject::READ_NORMAL |
| 406 | ): array { |
| 407 | $callerShouldAddGoodLink = false; |
| 408 | |
| 409 | $key = $this->getCacheKey( $link ); |
| 410 | if ( $key === null ) { |
| 411 | return [ $callerShouldAddGoodLink, null ]; |
| 412 | } |
| 413 | |
| 414 | $ns = $link->getNamespace(); |
| 415 | $dbkey = $link->getDBkey(); |
| 416 | |
| 417 | $entry = $this->entries->get( $key ); |
| 418 | if ( $entry && $entry[self::FLAGS] >= $queryFlags ) { |
| 419 | return [ $callerShouldAddGoodLink, $entry[self::ROW] ?: null ]; |
| 420 | } |
| 421 | |
| 422 | if ( !$fetchCallback ) { |
| 423 | return [ $callerShouldAddGoodLink, null ]; |
| 424 | } |
| 425 | |
| 426 | $callerShouldAddGoodLink = true; |
| 427 | |
| 428 | $wanCacheKey = $this->getPersistentCacheKey( $link ); |
| 429 | if ( $wanCacheKey !== null && !( $queryFlags & IDBAccessObject::READ_LATEST ) ) { |
| 430 | // Some pages are often transcluded heavily, so use persistent caching |
| 431 | $row = $this->wanCache->getWithSetCallback( |
| 432 | $wanCacheKey, |
| 433 | WANObjectCache::TTL_DAY, |
| 434 | function ( $curValue, &$ttl, array &$setOpts ) use ( $fetchCallback, $ns, $dbkey ) { |
| 435 | $dbr = $this->loadBalancer->getConnection( ILoadBalancer::DB_REPLICA ); |
| 436 | $setOpts += Database::getCacheSetOptions( $dbr ); |
| 437 | |
| 438 | $row = $fetchCallback( $dbr, $ns, $dbkey, [] ); |
| 439 | $mtime = $row ? (int)wfTimestamp( TS::UNIX, $row->page_touched ) : false; |
| 440 | $ttl = $this->wanCache->adaptiveTTL( $mtime, $ttl ); |
| 441 | |
| 442 | return $row; |
| 443 | } |
| 444 | ); |
| 445 | } else { |
| 446 | // No persistent caching needed, but we can still use the callback. |
| 447 | if ( ( $queryFlags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST ) { |
| 448 | $dbr = $this->loadBalancer->getConnection( DB_PRIMARY ); |
| 449 | } else { |
| 450 | $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); |
| 451 | } |
| 452 | $options = []; |
| 453 | if ( ( $queryFlags & IDBAccessObject::READ_EXCLUSIVE ) == IDBAccessObject::READ_EXCLUSIVE ) { |
| 454 | $options[] = 'FOR UPDATE'; |
| 455 | } elseif ( ( $queryFlags & IDBAccessObject::READ_LOCKING ) == IDBAccessObject::READ_LOCKING ) { |
| 456 | $options[] = 'LOCK IN SHARE MODE'; |
| 457 | } |
| 458 | $row = $fetchCallback( $dbr, $ns, $dbkey, $options ); |
| 459 | } |
| 460 | |
| 461 | return [ $callerShouldAddGoodLink, $row ?: null ]; |
| 462 | } |
| 463 | |
| 464 | /** |
| 465 | * Returns the row for the page if the page exists (subject to race conditions). |
| 466 | * The row will be returned from local cache or WAN cache if possible, or it |
| 467 | * will be looked up using the callback provided. |
| 468 | * |
| 469 | * @param int $ns |
| 470 | * @param string $dbkey |
| 471 | * @param callable|null $fetchCallback A callback that will retrieve the link row with the |
| 472 | * signature ( IReadableDatabase $db, int $ns, string $dbkey, array $queryOptions ): ?stdObj. |
| 473 | * @param int $queryFlags IDBAccessObject::READ_XXX |
| 474 | * |
| 475 | * @return stdClass|null |
| 476 | * @internal for use by PageStore. Other code should use a PageLookup instead. |
| 477 | */ |
| 478 | public function getGoodLinkRow( |
| 479 | int $ns, |
| 480 | string $dbkey, |
| 481 | ?callable $fetchCallback = null, |
| 482 | int $queryFlags = IDBAccessObject::READ_NORMAL |
| 483 | ): ?stdClass { |
| 484 | $link = TitleValue::tryNew( $ns, $dbkey ); |
| 485 | if ( $link === null ) { |
| 486 | return null; |
| 487 | } |
| 488 | |
| 489 | [ $shouldAddGoodLink, $row ] = $this->getGoodLinkRowInternal( |
| 490 | $link, |
| 491 | $fetchCallback, |
| 492 | $queryFlags |
| 493 | ); |
| 494 | |
| 495 | if ( $row ) { |
| 496 | if ( $shouldAddGoodLink ) { |
| 497 | try { |
| 498 | $this->addGoodLinkObjFromRow( $link, $row, $queryFlags ); |
| 499 | } catch ( InvalidArgumentException ) { |
| 500 | // a field is missing from $row; maybe we used a cache?; invalidate it and try again |
| 501 | $this->invalidateTitle( $link ); |
| 502 | [ , $row ] = $this->getGoodLinkRowInternal( |
| 503 | $link, |
| 504 | $fetchCallback, |
| 505 | $queryFlags |
| 506 | ); |
| 507 | $this->addGoodLinkObjFromRow( $link, $row, $queryFlags ); |
| 508 | } |
| 509 | } |
| 510 | } else { |
| 511 | $this->addBadLinkObj( $link ); |
| 512 | } |
| 513 | |
| 514 | return $row ?: null; |
| 515 | } |
| 516 | |
| 517 | /** |
| 518 | * @param LinkTarget|PageReference|TitleValue $page |
| 519 | * @return string|null |
| 520 | */ |
| 521 | private function getPersistentCacheKey( $page ) { |
| 522 | // if no key can be derived, the page isn't cacheable |
| 523 | if ( $this->getCacheKey( $page ) === null || !$this->usePersistentCache( $page ) ) { |
| 524 | return null; |
| 525 | } |
| 526 | return $this->wanCache->makeKey( 'page', $page->getNamespace(), sha1( $page->getDBkey() ) ); |
| 527 | } |
| 528 | |
| 529 | /** |
| 530 | * @param LinkTarget|PageReference|int $pageOrNamespace |
| 531 | * @return bool |
| 532 | */ |
| 533 | private function usePersistentCache( $pageOrNamespace ) { |
| 534 | $ns = is_int( $pageOrNamespace ) ? $pageOrNamespace : $pageOrNamespace->getNamespace(); |
| 535 | |
| 536 | if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY, NS_MEDIAWIKI ] ) || |
| 537 | ( !is_int( $pageOrNamespace ) && |
| 538 | ( str_ends_with( $pageOrNamespace->getDBkey(), '.css' ) || |
| 539 | str_ends_with( $pageOrNamespace->getDBkey(), '.js' ) ) ) ) { |
| 540 | return true; |
| 541 | } |
| 542 | // Focus on transcluded pages more than the main content |
| 543 | if ( $this->nsInfo->isContent( $ns ) ) { |
| 544 | return false; |
| 545 | } |
| 546 | // Non-talk extension namespaces (e.g. NS_MODULE) |
| 547 | return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) ); |
| 548 | } |
| 549 | |
| 550 | /** |
| 551 | * @param IReadableDatabase $db |
| 552 | * @param int $ns |
| 553 | * @param string $dbkey |
| 554 | * @param array $options Query options, see IDatabase::select() for details. |
| 555 | * @return stdClass|false |
| 556 | */ |
| 557 | private function fetchPageRow( IReadableDatabase $db, int $ns, string $dbkey, $options = [] ) { |
| 558 | $queryBuilder = $db->newSelectQueryBuilder() |
| 559 | ->select( self::getSelectFields() ) |
| 560 | ->from( 'page' ) |
| 561 | ->where( [ 'page_namespace' => $ns, 'page_title' => $dbkey ] ) |
| 562 | ->options( $options ); |
| 563 | |
| 564 | return $queryBuilder->caller( __METHOD__ )->fetchRow(); |
| 565 | } |
| 566 | |
| 567 | /** |
| 568 | * @param string[] $pages |
| 569 | * @param string $fname |
| 570 | * @return void |
| 571 | */ |
| 572 | public function executeBatch( array $pages, $fname ) { |
| 573 | $pageObject = []; |
| 574 | $result = []; |
| 575 | |
| 576 | foreach ( $pages as $page ) { |
| 577 | $title = Title::newFromText( $page ); |
| 578 | if ( $title ) { |
| 579 | $cacheKey = $this->getPersistentCacheKey( $title ); |
| 580 | $pageObject[$cacheKey] = $title; |
| 581 | } |
| 582 | } |
| 583 | |
| 584 | $rows = $this->wanCache->getMulti( array_keys( $pageObject ) ); |
| 585 | foreach ( $rows as $key => $row ) { |
| 586 | if ( $row ) { |
| 587 | $title = TitleValue::tryNew( (int)$row->page_namespace, $row->page_title ); |
| 588 | $this->addGoodLinkObjFromRow( $title, $row ); |
| 589 | } else { |
| 590 | $this->addBadLinkObj( $pageObject[$key] ); |
| 591 | } |
| 592 | unset( $pageObject[$key] ); |
| 593 | } |
| 594 | |
| 595 | $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory(); |
| 596 | |
| 597 | if ( count( $pageObject ) > 0 ) { |
| 598 | $linkBatch = $linkBatchFactory->newLinkBatch( array_values( $pageObject ) ); |
| 599 | $linkBatch->setCaller( $fname ); |
| 600 | $result = $linkBatch->doQuery(); |
| 601 | $linkBatch->doGenderQuery(); |
| 602 | } |
| 603 | |
| 604 | foreach ( $result as $row ) { |
| 605 | $title = TitleValue::tryNew( (int)$row->page_namespace, $row->page_title ); |
| 606 | $cacheKey = $this->getPersistentCacheKey( $title ); |
| 607 | $this->addGoodLinkObjFromRow( $title, $row ); |
| 608 | $pageObject[$cacheKey] = $row; |
| 609 | } |
| 610 | |
| 611 | foreach ( $pageObject as $key => $row ) { |
| 612 | if ( !$row instanceof Title ) { |
| 613 | $this->wanCache->set( $key, $row, WANObjectCache::TTL_DAY ); |
| 614 | } else { |
| 615 | $this->wanCache->set( $key, null, WANObjectCache::TTL_DAY ); |
| 616 | $this->addBadLinkObj( $row ); |
| 617 | } |
| 618 | } |
| 619 | } |
| 620 | |
| 621 | /** |
| 622 | * Purge the persistent link cache for a title |
| 623 | * |
| 624 | * @param LinkTarget|PageReference $page |
| 625 | * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. |
| 626 | * @since 1.28 |
| 627 | */ |
| 628 | public function invalidateTitle( $page ) { |
| 629 | // for use by ResourceLoader Wikimodule |
| 630 | $wanCacheKey = $this->getPersistentCacheKey( $page ); |
| 631 | if ( $wanCacheKey !== null ) { |
| 632 | $this->wanCache->delete( $wanCacheKey ); |
| 633 | } |
| 634 | |
| 635 | $this->clearLink( $page ); |
| 636 | } |
| 637 | |
| 638 | /** |
| 639 | * Clears cache |
| 640 | */ |
| 641 | public function clear() { |
| 642 | $this->entries->clear(); |
| 643 | } |
| 644 | } |
| 645 | |
| 646 | /** @deprecated class alias since 1.42 */ |
| 647 | class_alias( LinkCache::class, 'LinkCache' ); |
| 648 | |
| 649 | /** @deprecated class alias since 1.45 */ |
| 650 | class_alias( LinkCache::class, 'MediaWiki\Cache\LinkCache' ); |