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