Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
83.02% |
88 / 106 |
|
60.00% |
9 / 15 |
CRAP | |
0.00% |
0 / 1 |
LinkBatch | |
83.81% |
88 / 105 |
|
60.00% |
9 / 15 |
38.91 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
setCaller | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
addUser | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
addObj | |
64.29% |
9 / 14 |
|
0.00% |
0 / 1 |
4.73 | |||
add | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
setArray | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isEmpty | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSize | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPageIdentities | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
executeInto | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
addResultToCache | |
87.18% |
34 / 39 |
|
0.00% |
0 / 1 |
7.10 | |||
doQuery | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
3.01 | |||
doGenderQuery | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
constructSet | |
57.14% |
4 / 7 |
|
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 | |
24 | namespace MediaWiki\Cache; |
25 | |
26 | use InvalidArgumentException; |
27 | use MediaWiki\Language\Language; |
28 | use MediaWiki\Linker\LinksMigration; |
29 | use MediaWiki\Linker\LinkTarget; |
30 | use MediaWiki\Page\PageIdentityValue; |
31 | use MediaWiki\Page\PageReference; |
32 | use MediaWiki\Page\ProperPageIdentity; |
33 | use MediaWiki\Title\TitleFormatter; |
34 | use MediaWiki\Title\TitleValue; |
35 | use MediaWiki\User\TempUser\TempUserDetailsLookup; |
36 | use MediaWiki\User\UserIdentity; |
37 | use Psr\Log\LoggerInterface; |
38 | use RuntimeException; |
39 | use Wikimedia\Assert\Assert; |
40 | use Wikimedia\Rdbms\IConnectionProvider; |
41 | use Wikimedia\Rdbms\IResultWrapper; |
42 | use 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 | */ |
50 | class 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 */ |
420 | class_alias( LinkBatch::class, 'LinkBatch' ); |