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