Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.02% covered (warning)
83.02%
88 / 106
60.00% covered (warning)
60.00%
9 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
LinkBatch
83.81% covered (warning)
83.81%
88 / 105
60.00% covered (warning)
60.00%
9 / 15
38.91
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 setCaller
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addUser
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addObj
64.29% covered (warning)
64.29%
9 / 14
0.00% covered (danger)
0.00%
0 / 1
4.73
 add
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 setArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPageIdentities
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 executeInto
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 addResultToCache
87.18% covered (warning)
87.18%
34 / 39
0.00% covered (danger)
0.00%
0 / 1
7.10
 doQuery
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
 doGenderQuery
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 constructSet
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
2.31
1<?php
2/**
3 * Batch query to determine page existence.
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
24namespace MediaWiki\Cache;
25
26use InvalidArgumentException;
27use MediaWiki\Language\Language;
28use MediaWiki\Linker\LinksMigration;
29use MediaWiki\Linker\LinkTarget;
30use MediaWiki\Page\PageIdentityValue;
31use MediaWiki\Page\PageReference;
32use MediaWiki\Page\ProperPageIdentity;
33use MediaWiki\Title\TitleFormatter;
34use MediaWiki\Title\TitleValue;
35use MediaWiki\User\TempUser\TempUserDetailsLookup;
36use MediaWiki\User\UserIdentity;
37use Psr\Log\LoggerInterface;
38use RuntimeException;
39use Wikimedia\Assert\Assert;
40use Wikimedia\Rdbms\IConnectionProvider;
41use Wikimedia\Rdbms\IResultWrapper;
42use Wikimedia\Rdbms\Platform\ISQLPlatform;
43
44/**
45 * Class representing a list of titles
46 * The execute() method checks them all for existence and adds them to a LinkCache object
47 *
48 * @ingroup Cache
49 */
50class LinkBatch {
51    /**
52     * @var array<int,array<string,mixed>> 2-d array, first index namespace, second index dbkey, value arbitrary
53     */
54    public $data = [];
55
56    /**
57     * @var UserIdentity[] Users to preload temporary account expiration status for
58     */
59    private array $users = [];
60
61    /**
62     * @var ProperPageIdentity[]|null page identity objects corresponding to the links in the batch
63     */
64    private $pageIdentities = null;
65
66    /**
67     * @var string|null For debugging which method is using this class.
68     */
69    protected $caller;
70
71    /**
72     * @var LinkCache
73     */
74    private $linkCache;
75
76    /**
77     * @var TitleFormatter
78     */
79    private $titleFormatter;
80
81    /**
82     * @var Language
83     */
84    private $contentLanguage;
85
86    /**
87     * @var GenderCache
88     */
89    private $genderCache;
90
91    /**
92     * @var IConnectionProvider
93     */
94    private $dbProvider;
95
96    /** @var LinksMigration */
97    private $linksMigration;
98
99    private TempUserDetailsLookup $tempUserDetailsLookup;
100
101    /** @var LoggerInterface */
102    private $logger;
103
104    /**
105     * @see \MediaWiki\Cache\LinkBatchFactory
106     *
107     * @internal
108     * @param iterable<LinkTarget>|iterable<PageReference> $arr Initial items to be added to the batch
109     * @param LinkCache $linkCache
110     * @param TitleFormatter $titleFormatter
111     * @param Language $contentLanguage
112     * @param GenderCache $genderCache
113     * @param IConnectionProvider $dbProvider
114     * @param LinksMigration $linksMigration
115     * @param TempUserDetailsLookup $tempUserDetailsLookup
116     * @param LoggerInterface $logger
117     */
118    public function __construct(
119        iterable $arr,
120        LinkCache $linkCache,
121        TitleFormatter $titleFormatter,
122        Language $contentLanguage,
123        GenderCache $genderCache,
124        IConnectionProvider $dbProvider,
125        LinksMigration $linksMigration,
126        TempUserDetailsLookup $tempUserDetailsLookup,
127        LoggerInterface $logger
128    ) {
129        $this->linkCache = $linkCache;
130        $this->titleFormatter = $titleFormatter;
131        $this->contentLanguage = $contentLanguage;
132        $this->genderCache = $genderCache;
133        $this->dbProvider = $dbProvider;
134        $this->linksMigration = $linksMigration;
135        $this->tempUserDetailsLookup = $tempUserDetailsLookup;
136        $this->logger = $logger;
137
138        foreach ( $arr as $item ) {
139            $this->addObj( $item );
140        }
141    }
142
143    /**
144     * Use ->setCaller( __METHOD__ ) to indicate which code is using this
145     * class. Only used in debugging output.
146     * @since 1.17
147     *
148     * @param string $caller
149     * @return self (since 1.32)
150     */
151    public function setCaller( $caller ) {
152        $this->caller = $caller;
153
154        return $this;
155    }
156
157    /**
158     * Convenience function to add user and user talk pages for a given user to this batch.
159     * Calling {@link execute} will also prefetch the expiration status of temporary accounts
160     * added this way, which is needed for the efficient rendering of user links via UserLinkRenderer.
161     *
162     * @since 1.44
163     *
164     * @param UserIdentity $user
165     * @return void
166     */
167    public function addUser( UserIdentity $user ): void {
168        $this->users[$user->getName()] = $user;
169
170        $this->add( NS_USER, $user->getName() );
171        $this->add( NS_USER_TALK, $user->getName() );
172    }
173
174    /**
175     * @param LinkTarget|PageReference $link
176     */
177    public function addObj( $link ) {
178        if ( !$link ) {
179            // Don't die if we got null, just skip. There is nothing to do anyway.
180            // For now, let's avoid things like T282180. We should be more strict in the future.
181            $this->logger->warning(
182                'Skipping null link, probably due to a bad title.',
183                [ 'exception' => new RuntimeException() ]
184            );
185            return;
186        }
187        if ( $link instanceof LinkTarget && $link->isExternal() ) {
188            $this->logger->warning(
189                'Skipping interwiki link',
190                [ 'exception' => new RuntimeException() ]
191            );
192            return;
193        }
194
195        Assert::parameterType( [ LinkTarget::class, PageReference::class ], $link, '$link' );
196        $this->add( $link->getNamespace(), $link->getDBkey() );
197    }
198
199    /**
200     * @param int $ns
201     * @param string $dbkey
202     */
203    public function add( $ns, $dbkey ) {
204        if ( $ns < 0 || $dbkey === '' ) {
205            // T137083
206            return;
207        }
208        $this->data[$ns][strtr( $dbkey, ' ', '_' )] = 1;
209    }
210
211    /**
212     * Set the link list to a given 2-d array
213     * First key is the namespace, second is the DB key, value arbitrary
214     *
215     * @param array<int,array<string,mixed>> $array
216     */
217    public function setArray( $array ) {
218        $this->data = $array;
219    }
220
221    /**
222     * Returns true if no pages have been added, false otherwise.
223     *
224     * @return bool
225     */
226    public function isEmpty() {
227        return $this->getSize() == 0;
228    }
229
230    /**
231     * Returns the size of the batch.
232     *
233     * @return int
234     */
235    public function getSize() {
236        return count( $this->data );
237    }
238
239    /**
240     * Do the query and add the results to the LinkCache object
241     *
242     * @return int[] Mapping PDBK to ID
243     */
244    public function execute() {
245        return $this->executeInto( $this->linkCache );
246    }
247
248    /**
249     * Do the query, add the results to the LinkCache object,
250     * and return ProperPageIdentity instances corresponding to the pages in the batch.
251     *
252     * @since 1.37
253     * @return ProperPageIdentity[] A list of ProperPageIdentities
254     */
255    public function getPageIdentities(): array {
256        if ( $this->pageIdentities === null ) {
257            $this->execute();
258        }
259
260        return $this->pageIdentities;
261    }
262
263    /**
264     * Do the query and add the results to a given LinkCache object
265     * Return an array mapping PDBK to ID
266     *
267     * @param LinkCache $cache
268     * @return int[] Remaining IDs
269     */
270    protected function executeInto( $cache ) {
271        $res = $this->doQuery();
272        $this->doGenderQuery();
273
274        // Prefetch expiration status for temporary accounts added to this batch via addUser()
275        // for efficient user link rendering (T358469).
276        if ( count( $this->users ) > 0 ) {
277            $this->tempUserDetailsLookup->preloadExpirationStatus( $this->users );
278        }
279
280        return $this->addResultToCache( $cache, $res );
281    }
282
283    /**
284     * Add a result wrapper containing IDs and titles to a LinkCache object.
285     * As normal, titles will go into the static Title cache field.
286     * This function *also* stores extra fields of the title used for link
287     * parsing to avoid extra DB queries.
288     *
289     * @param LinkCache $cache
290     * @param IResultWrapper $res
291     * @return int[] Array of remaining titles
292     */
293    public function addResultToCache( $cache, $res ) {
294        if ( !$res ) {
295            return [];
296        }
297
298        // For each returned entry, add it to the list of good links, and remove it from $remaining
299
300        $this->pageIdentities ??= [];
301
302        $ids = [];
303        $remaining = $this->data;
304        foreach ( $res as $row ) {
305            try {
306                $title = new TitleValue( (int)$row->page_namespace, $row->page_title );
307
308                $cache->addGoodLinkObjFromRow( $title, $row );
309                $pdbk = $this->titleFormatter->getPrefixedDBkey( $title );
310                $ids[$pdbk] = $row->page_id;
311
312                $pageIdentity = new PageIdentityValue(
313                    (int)$row->page_id,
314                    (int)$row->page_namespace,
315                    $row->page_title,
316                    ProperPageIdentity::LOCAL
317                );
318
319                $key = CacheKeyHelper::getKeyForPage( $pageIdentity );
320                $this->pageIdentities[$key] = $pageIdentity;
321            } catch ( InvalidArgumentException $ex ) {
322                $this->logger->warning(
323                    'Encountered invalid title',
324                    [ 'title_namespace' => $row->page_namespace, 'title_dbkey' => $row->page_title ]
325                );
326            }
327
328            unset( $remaining[$row->page_namespace][$row->page_title] );
329        }
330
331        // The remaining links in $data are bad links, register them as such
332        foreach ( $remaining as $ns => $dbkeys ) {
333            foreach ( $dbkeys as $dbkey => $unused ) {
334                try {
335                    $title = new TitleValue( (int)$ns, (string)$dbkey );
336
337                    $cache->addBadLinkObj( $title );
338                    $pdbk = $this->titleFormatter->getPrefixedDBkey( $title );
339                    $ids[$pdbk] = 0;
340
341                    $pageIdentity = new PageIdentityValue( 0, (int)$ns, $dbkey, ProperPageIdentity::LOCAL );
342                    $key = CacheKeyHelper::getKeyForPage( $pageIdentity );
343                    $this->pageIdentities[$key] = $pageIdentity;
344                } catch ( InvalidArgumentException $ex ) {
345                    $this->logger->warning(
346                        'Encountered invalid title',
347                        [ 'title_namespace' => $ns, 'title_dbkey' => $dbkey ]
348                    );
349                }
350            }
351        }
352
353        return $ids;
354    }
355
356    /**
357     * Perform the existence test query, return a result wrapper with page_id fields
358     * @return IResultWrapper|false
359     */
360    public function doQuery() {
361        if ( $this->isEmpty() ) {
362            return false;
363        }
364
365        // This is similar to LinkHolderArray::replaceInternal
366        $dbr = $this->dbProvider->getReplicaDatabase();
367        $queryBuilder = $dbr->newSelectQueryBuilder()
368            ->select( LinkCache::getSelectFields() )
369            ->from( 'page' )
370            ->where( $this->constructSet( 'page', $dbr ) );
371
372        $caller = __METHOD__;
373        if ( strval( $this->caller ) !== '' ) {
374            $caller .= " (for {$this->caller})";
375        }
376
377        return $queryBuilder->caller( $caller )->fetchResultSet();
378    }
379
380    /**
381     * Do (and cache) {{GENDER:...}} information for userpages in this LinkBatch
382     *
383     * @return bool Whether the query was successful
384     */
385    public function doGenderQuery() {
386        if ( $this->isEmpty() || !$this->contentLanguage->needsGenderDistinction() ) {
387            return false;
388        }
389
390        $this->genderCache->doLinkBatch( $this->data, $this->caller );
391
392        return true;
393    }
394
395    /**
396     * Construct a WHERE clause which will match all the given titles.
397     *
398     * It is the caller's responsibility to only call this if the LinkBatch is
399     * not empty, because there is no safe way to represent a SQL conditional
400     * for the empty set.
401     *
402     * @param string $prefix The appropriate table's field name prefix ('page', 'pl', etc)
403     * @param ISQLPlatform $db DB object to use
404     * @return string String with SQL where clause fragment
405     */
406    public function constructSet( $prefix, $db ) {
407        if ( isset( $this->linksMigration::$prefixToTableMapping[$prefix] ) ) {
408            [ $blNamespace, $blTitle ] = $this->linksMigration->getTitleFields(
409                $this->linksMigration::$prefixToTableMapping[$prefix]
410            );
411        } else {
412            $blNamespace = "{$prefix}_namespace";
413            $blTitle = "{$prefix}_title";
414        }
415        return $db->makeWhereFrom2d( $this->data, $blNamespace, $blTitle );
416    }
417}
418
419/** @deprecated class alias since 1.42 */
420class_alias( LinkBatch::class, 'LinkBatch' );