Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.37% covered (warning)
72.37%
165 / 228
55.00% covered (warning)
55.00%
11 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
LinkCache
73.01% covered (warning)
73.01%
165 / 226
55.00% covered (warning)
55.00%
11 / 20
263.07
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 setLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCacheKey
54.84% covered (warning)
54.84%
17 / 31
0.00% covered (danger)
0.00%
0 / 1
25.26
 getGoodLinkID
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getGoodLinkFieldObj
92.31% covered (success)
92.31%
24 / 26
0.00% covered (danger)
0.00%
0 / 1
13.08
 isBadLink
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 addGoodLinkObjFromRow
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 addBadLinkObj
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 clearBadLink
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 clearLink
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getSelectFields
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
2.00
 addLinkObj
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getGoodLinkRowInternal
94.44% covered (success)
94.44%
34 / 36
0.00% covered (danger)
0.00%
0 / 1
13.03
 getGoodLinkRow
61.90% covered (warning)
61.90%
13 / 21
0.00% covered (danger)
0.00%
0 / 1
7.99
 getPersistentCacheKey
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 usePersistentCache
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
8.09
 fetchPageRow
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 executeBatch
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
90
 invalidateTitle
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 clear
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Page;
8
9use InvalidArgumentException;
10use MediaWiki\MainConfigNames;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Title\NamespaceInfo;
13use MediaWiki\Title\Title;
14use MediaWiki\Title\TitleFormatter;
15use MediaWiki\Title\TitleValue;
16use Psr\Log\LoggerAwareInterface;
17use Psr\Log\LoggerInterface;
18use Psr\Log\NullLogger;
19use stdClass;
20use Wikimedia\MapCacheLRU\MapCacheLRU;
21use Wikimedia\ObjectCache\WANObjectCache;
22use Wikimedia\Parsoid\Core\LinkTarget;
23use Wikimedia\Rdbms\Database;
24use Wikimedia\Rdbms\IDBAccessObject;
25use Wikimedia\Rdbms\ILoadBalancer;
26use Wikimedia\Rdbms\IReadableDatabase;
27use 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 */
54class 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 */
647class_alias( LinkCache::class, 'LinkCache' );
648
649/** @deprecated class alias since 1.45 */
650class_alias( LinkCache::class, 'MediaWiki\Cache\LinkCache' );