MediaWiki master
ApiQueryBacklinks.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Api;
10
18
28
32 private $rootTitle;
33
35 private $params;
37 private $cont;
39 private $redirect;
40
41 private string $bl_ns;
42 private string $bl_from;
43 private string $bl_from_ns;
44 private string $bl_table;
45 private string $bl_code;
46 private string $bl_title;
48 private $virtual_domain;
49 private bool $hasNS;
50
52 private $helpUrl;
53
59 private $pageMap = [];
61 private $resultArr;
62
64 private $redirTitles = [];
66 private $continueStr = null;
67
69 private $backlinksSettings = [
70 'backlinks' => [
71 'code' => 'bl',
72 'prefix' => 'pl',
73 'linktbl' => 'pagelinks',
74 'helpurl' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Backlinks',
75 'virtualdomain' => PageLinksTable::VIRTUAL_DOMAIN,
76 ],
77 'embeddedin' => [
78 'code' => 'ei',
79 'prefix' => 'tl',
80 'linktbl' => 'templatelinks',
81 'helpurl' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Embeddedin',
82 'virtualdomain' => TemplateLinksTable::VIRTUAL_DOMAIN,
83 ],
84 'imageusage' => [
85 'code' => 'iu',
86 'prefix' => 'il',
87 'linktbl' => 'imagelinks',
88 'helpurl' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Imageusage',
89 'virtualdomain' => ImageLinksTable::VIRTUAL_DOMAIN,
90 ]
91 ];
92
93 public function __construct(
94 ApiQuery $query,
95 string $moduleName,
96 private readonly LinksMigration $linksMigration,
97 ) {
98 $settings = $this->backlinksSettings[$moduleName];
99 $prefix = $settings['prefix'];
100 $code = $settings['code'];
101 $this->resultArr = [];
102
103 parent::__construct( $query, $moduleName, $code );
104 $this->bl_table = $settings['linktbl'];
105 $this->hasNS = $moduleName !== 'imageusage';
106 [ $this->bl_ns, $this->bl_title ] = $this->linksMigration->getTitleFields( $this->bl_table );
107 $this->bl_from = $prefix . '_from';
108 $this->bl_from_ns = $prefix . '_from_namespace';
109 $this->bl_code = $code;
110 $this->helpUrl = $settings['helpurl'];
111 $this->virtual_domain = $settings['virtualdomain'];
112 }
113
114 public function execute() {
115 $this->run();
116 }
117
119 public function getCacheMode( $params ) {
120 return 'public';
121 }
122
124 public function executeGenerator( $resultPageSet ) {
125 $this->run( $resultPageSet );
126 }
127
132 private function runFirstQuery( $resultPageSet = null ) {
133 $this->addTables( [ $this->bl_table, 'page' ] );
134 $this->addWhere( "{$this->bl_from}=page_id" );
135 if ( $resultPageSet === null ) {
136 $this->addFields( [ 'page_id', 'page_title', 'page_namespace' ] );
137 } else {
138 $this->addFields( $resultPageSet->getPageTableFields() );
139 }
140 $this->addFields( [ 'page_is_redirect', 'from_ns' => 'page_namespace' ] );
141
142 $this->addWhere( $this->linksMigration->getLinksConditions( $this->bl_table, $this->rootTitle ) );
143 $this->addWhereFld( $this->bl_from_ns, $this->params['namespace'] );
144
145 if ( count( $this->cont ) >= 2 ) {
146 $db = $this->getDB();
147 $op = $this->params['dir'] == 'descending' ? '<=' : '>=';
148 if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
149 $this->addWhere( $db->buildComparison( $op, [
150 $this->bl_from_ns => $this->cont[0],
151 $this->bl_from => $this->cont[1],
152 ] ) );
153 } else {
154 $this->addWhere( $db->buildComparison( $op, [ $this->bl_from => $this->cont[1] ] ) );
155 }
156 }
157
158 if ( $this->params['filterredir'] == 'redirects' ) {
159 $this->addWhereFld( 'page_is_redirect', 1 );
160 } elseif ( $this->params['filterredir'] == 'nonredirects' && !$this->redirect ) {
161 // T24245 - Check for !redirect, as filtering nonredirects, when
162 // getting what links to them is contradictory
163 $this->addWhereFld( 'page_is_redirect', 0 );
164 }
165
166 $this->addOption( 'LIMIT', $this->params['limit'] + 1 );
167 $sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' );
168 $orderBy = [];
169 if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
170 $orderBy[] = $this->bl_from_ns . $sort;
171 }
172 $orderBy[] = $this->bl_from . $sort;
173 $this->addOption( 'ORDER BY', $orderBy );
174 $this->addOption( 'STRAIGHT_JOIN' );
175
176 $this->setVirtualDomain( $this->virtual_domain );
177 $res = $this->select( __METHOD__ );
178 $this->resetVirtualDomain();
179
180 if ( $resultPageSet === null ) {
181 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__ );
182 }
183
184 $count = 0;
185 foreach ( $res as $row ) {
186 if ( ++$count > $this->params['limit'] ) {
187 // We've reached the one extra which shows that there are
188 // additional pages to be had. Stop here...
189 // Continue string may be overridden at a later step
190 $this->continueStr = "{$row->from_ns}|{$row->page_id}";
191 break;
192 }
193
194 // Fill in continuation fields for later steps
195 if ( count( $this->cont ) < 2 ) {
196 $this->cont[] = $row->from_ns;
197 $this->cont[] = $row->page_id;
198 }
199
200 $this->pageMap[$row->page_namespace][$row->page_title] = $row->page_id;
201 $t = Title::makeTitle( $row->page_namespace, $row->page_title );
202 if ( $row->page_is_redirect ) {
203 $this->redirTitles[] = $t;
204 }
205
206 if ( $resultPageSet === null ) {
207 $a = [ 'pageid' => (int)$row->page_id ];
209 if ( $row->page_is_redirect ) {
210 $a['redirect'] = true;
211 }
212 // Put all the results in an array first
213 $this->resultArr[$a['pageid']] = $a;
214 } else {
215 $resultPageSet->processDbRow( $row );
216 }
217 }
218 }
219
224 private function runSecondQuery( $resultPageSet = null ) {
225 $db = $this->getDB();
226 $queryInfo = $this->linksMigration->getQueryInfo( $this->bl_table, $this->bl_table );
227 $this->addTables( [ ...$queryInfo['tables'], 'page' ] );
228 $this->addJoinConds( [ ...$queryInfo['joins'], 'page' => [ 'JOIN', "{$this->bl_from}=page_id" ] ] );
229
230 if ( $resultPageSet === null ) {
231 $this->addFields( [ 'page_id', 'page_title', 'page_namespace', 'page_is_redirect' ] );
232 } else {
233 $this->addFields( $resultPageSet->getPageTableFields() );
234 }
235
236 $this->addFields( [ $this->bl_title, 'from_ns' => 'page_namespace' ] );
237 if ( $this->hasNS ) {
238 $this->addFields( $this->bl_ns );
239 }
240
241 // We can't use LinkBatch here because $this->hasNS may be false
242 $titleWhere = [];
243 $allRedirNs = [];
244 $allRedirDBkey = [];
246 foreach ( $this->redirTitles as $t ) {
247 $redirNs = $t->getNamespace();
248 $redirDBkey = $t->getDBkey();
249 $expr = $db->expr( $this->bl_title, '=', $redirDBkey );
250 if ( $this->hasNS ) {
251 $expr = $expr->and( $this->bl_ns, '=', $redirNs );
252 }
253 $titleWhere[] = $expr;
254 $allRedirNs[$redirNs] = true;
255 $allRedirDBkey[$redirDBkey] = true;
256 }
257 $this->addWhere( $db->orExpr( $titleWhere ) );
258 $this->addWhereFld( 'page_namespace', $this->params['namespace'] );
259
260 if ( count( $this->cont ) >= 6 ) {
261 $op = $this->params['dir'] == 'descending' ? '<=' : '>=';
262
263 $conds = [];
264 if ( $this->hasNS && count( $allRedirNs ) > 1 ) {
265 $conds[ $this->bl_ns ] = $this->cont[2];
266 }
267 if ( count( $allRedirDBkey ) > 1 ) {
268 $conds[ $this->bl_title ] = $this->cont[3];
269 }
270 // Don't bother with namespace, title, or from_namespace if it's
271 // otherwise constant in the where clause.
272 if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
273 $conds[ $this->bl_from_ns ] = $this->cont[4];
274 }
275 $conds[ $this->bl_from ] = $this->cont[5];
276
277 $this->addWhere( $db->buildComparison( $op, $conds ) );
278 }
279 if ( $this->params['filterredir'] == 'redirects' ) {
280 $this->addWhereFld( 'page_is_redirect', 1 );
281 } elseif ( $this->params['filterredir'] == 'nonredirects' ) {
282 $this->addWhereFld( 'page_is_redirect', 0 );
283 }
284
285 $this->addOption( 'LIMIT', $this->params['limit'] + 1 );
286 $orderBy = [];
287 $sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' );
288 // Don't order by namespace/title/from_namespace if it's constant in the WHERE clause
289 if ( $this->hasNS && count( $allRedirNs ) > 1 ) {
290 $orderBy[] = $this->bl_ns . $sort;
291 }
292 if ( count( $allRedirDBkey ) > 1 ) {
293 $orderBy[] = $this->bl_title . $sort;
294 }
295 if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
296 $orderBy[] = $this->bl_from_ns . $sort;
297 }
298 $orderBy[] = $this->bl_from . $sort;
299 $this->addOption( 'ORDER BY', $orderBy );
300 $this->addOption( 'USE INDEX', [ 'page' => 'PRIMARY' ] );
301 // T290379: Avoid MariaDB deciding to scan all of `page`.
302 $this->addOption( 'STRAIGHT_JOIN' );
303
304 $res = $this->select( __METHOD__ );
305
306 if ( $resultPageSet === null ) {
307 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__ );
308 }
309
310 $count = 0;
311 foreach ( $res as $row ) {
312 $ns = $this->hasNS ? $row->{$this->bl_ns} : NS_FILE;
313
314 if ( ++$count > $this->params['limit'] ) {
315 // We've reached the one extra which shows that there are
316 // additional pages to be had. Stop here...
317 // Note we must keep the parameters for the first query constant
318 // This may be overridden at a later step
319 $title = $row->{$this->bl_title};
320 $this->continueStr = implode( '|', array_slice( $this->cont, 0, 2 ) ) .
321 "|$ns|$title|{$row->from_ns}|{$row->page_id}";
322 break;
323 }
324
325 // Fill in continuation fields for later steps
326 if ( count( $this->cont ) < 6 ) {
327 $this->cont[] = $ns;
328 $this->cont[] = $row->{$this->bl_title};
329 $this->cont[] = $row->from_ns;
330 $this->cont[] = $row->page_id;
331 }
332
333 if ( $resultPageSet === null ) {
334 $a = [ 'pageid' => (int)$row->page_id ];
335 ApiQueryBase::addTitleInfo( $a, Title::makeTitle( $row->page_namespace, $row->page_title ) );
336 if ( $row->page_is_redirect ) {
337 $a['redirect'] = true;
338 }
339 $parentID = $this->pageMap[$ns][$row->{$this->bl_title}];
340 // Put all the results in an array first
341 $this->resultArr[$parentID]['redirlinks'][$row->page_id] = $a;
342 } else {
343 $resultPageSet->processDbRow( $row );
344 }
345 }
346 }
347
352 private function run( $resultPageSet = null ) {
353 $this->params = $this->extractRequestParams( false );
354 $this->redirect = isset( $this->params['redirect'] ) && $this->params['redirect'];
355 $userMax = ( $this->redirect ? ApiBase::LIMIT_BIG1 / 2 : ApiBase::LIMIT_BIG1 );
356 $botMax = ( $this->redirect ? ApiBase::LIMIT_BIG2 / 2 : ApiBase::LIMIT_BIG2 );
357
358 $result = $this->getResult();
359
360 if ( $this->params['limit'] == 'max' ) {
361 $this->params['limit'] = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
362 $result->addParsedLimit( $this->getModuleName(), $this->params['limit'] );
363 } else {
364 $this->params['limit'] = $this->getMain()->getParamValidator()->validateValue(
365 $this, 'limit', (int)$this->params['limit'], [
366 ParamValidator::PARAM_TYPE => 'limit',
367 IntegerDef::PARAM_MIN => 1,
368 IntegerDef::PARAM_MAX => $userMax,
369 IntegerDef::PARAM_MAX2 => $botMax,
370 IntegerDef::PARAM_IGNORE_RANGE => true,
371 ]
372 );
373 }
374
375 $this->rootTitle = $this->getTitleFromTitleOrPageId( $this->params );
376
377 // only image titles are allowed for the root in imageinfo mode
378 if ( !$this->hasNS && $this->rootTitle->getNamespace() !== NS_FILE ) {
379 $this->dieWithError(
380 [ 'apierror-imageusage-badtitle', $this->getModuleName() ],
381 'bad_image_title'
382 );
383 }
384
385 // Parse and validate continuation parameter
386 // (Can't use parseContinueParamOrDie(), because the length is variable)
387 $this->cont = [];
388 if ( $this->params['continue'] !== null ) {
389 $cont = explode( '|', $this->params['continue'] );
390
391 switch ( count( $cont ) ) {
392 case 8:
393 // redirect page ID for result adding
394 $this->cont[7] = (int)$cont[7];
395 $this->dieContinueUsageIf( $cont[7] !== (string)$this->cont[7] );
396
397 /* Fall through */
398
399 case 7:
400 // top-level page ID for result adding
401 $this->cont[6] = (int)$cont[6];
402 $this->dieContinueUsageIf( $cont[6] !== (string)$this->cont[6] );
403
404 /* Fall through */
405
406 case 6:
407 // ns for 2nd query (even for imageusage)
408 $this->cont[2] = (int)$cont[2];
409 $this->dieContinueUsageIf( $cont[2] !== (string)$this->cont[2] );
410
411 // title for 2nd query
412 $this->cont[3] = $cont[3];
413
414 // from_ns for 2nd query
415 $this->cont[4] = (int)$cont[4];
416 $this->dieContinueUsageIf( $cont[4] !== (string)$this->cont[4] );
417
418 // from_id for 1st query
419 $this->cont[5] = (int)$cont[5];
420 $this->dieContinueUsageIf( $cont[5] !== (string)$this->cont[5] );
421
422 /* Fall through */
423
424 case 2:
425 // from_ns for 1st query
426 $this->cont[0] = (int)$cont[0];
427 $this->dieContinueUsageIf( $cont[0] !== (string)$this->cont[0] );
428
429 // from_id for 1st query
430 $this->cont[1] = (int)$cont[1];
431 $this->dieContinueUsageIf( $cont[1] !== (string)$this->cont[1] );
432
433 break;
434
435 default:
436 // @phan-suppress-next-line PhanImpossibleCondition
437 $this->dieContinueUsageIf( true );
438 }
439
440 ksort( $this->cont );
441 }
442
443 $this->runFirstQuery( $resultPageSet );
444 if ( $this->redirect && count( $this->redirTitles ) ) {
445 $this->resetQueryParams();
446 $this->runSecondQuery( $resultPageSet );
447 }
448
449 // Fill in any missing fields in case it's needed below
450 $this->cont += [ 0, 0, 0, '', 0, 0, 0 ];
451
452 if ( $resultPageSet === null ) {
453 // Try to add the result data in one go and pray that it fits
454 $code = $this->bl_code;
455 $data = array_map( static function ( $arr ) use ( $code ) {
456 if ( isset( $arr['redirlinks'] ) ) {
457 $arr['redirlinks'] = array_values( $arr['redirlinks'] );
458 ApiResult::setIndexedTagName( $arr['redirlinks'], $code );
459 }
460 return $arr;
461 }, array_values( $this->resultArr ) );
462 $fit = $result->addValue( 'query', $this->getModuleName(), $data );
463 if ( !$fit ) {
464 // It didn't fit. Add elements one by one until the
465 // result is full.
466 ksort( $this->resultArr );
467 // @phan-suppress-next-line PhanRedundantValueComparison
468 if ( count( $this->cont ) >= 7 ) {
469 $startAt = $this->cont[6];
470 } else {
471 $startAt = array_key_first( $this->resultArr );
472 }
473 $idx = 0;
474 foreach ( $this->resultArr as $pageID => $arr ) {
475 if ( $pageID < $startAt ) {
476 continue;
477 }
478
479 // Add the basic entry without redirlinks first
480 $fit = $result->addValue(
481 [ 'query', $this->getModuleName() ],
482 $idx, array_diff_key( $arr, [ 'redirlinks' => '' ] ) );
483 if ( !$fit ) {
484 $this->continueStr = implode( '|', array_slice( $this->cont, 0, 6 ) ) .
485 "|$pageID";
486 break;
487 }
488
489 $hasRedirs = false;
490 $redirLinks = isset( $arr['redirlinks'] ) ? (array)$arr['redirlinks'] : [];
491 ksort( $redirLinks );
492 // @phan-suppress-next-line PhanRedundantValueComparisonInLoop
493 if ( count( $this->cont ) >= 8 && $pageID == $startAt ) {
494 $redirStartAt = $this->cont[7];
495 } else {
496 $redirStartAt = array_key_first( $redirLinks );
497 }
498 foreach ( $redirLinks as $key => $redir ) {
499 if ( $key < $redirStartAt ) {
500 continue;
501 }
502
503 $fit = $result->addValue(
504 [ 'query', $this->getModuleName(), $idx, 'redirlinks' ],
505 null, $redir );
506 if ( !$fit ) {
507 $this->continueStr = implode( '|', array_slice( $this->cont, 0, 6 ) ) .
508 "|$pageID|$key";
509 break;
510 }
511 $hasRedirs = true;
512 }
513 if ( $hasRedirs ) {
514 $result->addIndexedTagName(
515 [ 'query', $this->getModuleName(), $idx, 'redirlinks' ],
516 $this->bl_code );
517 }
518 if ( !$fit ) {
519 break;
520 }
521
522 $idx++;
523 }
524 }
525
526 $result->addIndexedTagName(
527 [ 'query', $this->getModuleName() ],
528 $this->bl_code
529 );
530 }
531 if ( $this->continueStr !== null ) {
532 $this->setContinueEnumParameter( 'continue', $this->continueStr );
533 }
534 }
535
537 public function getAllowedParams() {
538 $retval = [
539 'title' => [
540 ParamValidator::PARAM_TYPE => 'string',
541 ],
542 'pageid' => [
543 ParamValidator::PARAM_TYPE => 'integer',
544 ],
545 'continue' => [
546 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
547 ],
548 'namespace' => [
549 ParamValidator::PARAM_ISMULTI => true,
550 ParamValidator::PARAM_TYPE => 'namespace'
551 ],
552 'dir' => [
553 ParamValidator::PARAM_DEFAULT => 'ascending',
554 ParamValidator::PARAM_TYPE => [
555 'ascending',
556 'descending'
557 ]
558 ],
559 'filterredir' => [
560 ParamValidator::PARAM_DEFAULT => 'all',
561 ParamValidator::PARAM_TYPE => [
562 'all',
563 'redirects',
564 'nonredirects'
565 ]
566 ],
567 'limit' => [
568 ParamValidator::PARAM_DEFAULT => 10,
569 ParamValidator::PARAM_TYPE => 'limit',
570 IntegerDef::PARAM_MIN => 1,
571 IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
572 IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
573 ]
574 ];
575 if ( $this->getModuleName() !== 'embeddedin' ) {
576 $retval['redirect'] = false;
577 }
578
579 return $retval;
580 }
581
583 protected function getExamplesMessages() {
584 $title = Title::newMainPage()->getPrefixedText();
585 $mp = rawurlencode( $title );
586 $examples = [
587 'backlinks' => [
588 "action=query&list=backlinks&bltitle={$mp}"
589 => 'apihelp-query+backlinks-example-simple',
590 "action=query&generator=backlinks&gbltitle={$mp}&prop=info"
591 => 'apihelp-query+backlinks-example-generator',
592 ],
593 'embeddedin' => [
594 'action=query&list=embeddedin&eititle=Template:Stub'
595 => 'apihelp-query+embeddedin-example-simple',
596 'action=query&generator=embeddedin&geititle=Template:Stub&prop=info'
597 => 'apihelp-query+embeddedin-example-generator',
598 ],
599 'imageusage' => [
600 'action=query&list=imageusage&iutitle=File:Albert%20Einstein%20Head.jpg'
601 => 'apihelp-query+imageusage-example-simple',
602 'action=query&generator=imageusage&giutitle=File:Albert%20Einstein%20Head.jpg&prop=info'
603 => 'apihelp-query+imageusage-example-generator',
604 ]
605 ];
606
607 return $examples[$this->getModuleName()];
608 }
609
611 public function getHelpUrls() {
612 return $this->helpUrl;
613 }
614}
615
617class_alias( ApiQueryBacklinks::class, 'ApiQueryBacklinks' );
const NS_FILE
Definition Defines.php:57
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
addTables( $tables, $alias=null)
Add a set of tables to the internal array.
addJoinConds( $join_conds)
Add a set of JOIN conditions to the internal array.
resetVirtualDomain()
Reset the virtual domain to the main database.
setVirtualDomain(string|false $virtualDomain)
Set the Query database connection (read-only)
getDB()
Get the Query database connection (read-only).
select( $method, $extraQuery=[], ?array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
addWhere( $value)
Add a set of WHERE clauses to the internal array.
executeGenderCacheFromResultWrapper(IResultWrapper $res, $fname=__METHOD__, $fieldPrefix='page')
Preprocess the result set to fill the GenderCache with the necessary information before using self::a...
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
addFields( $value)
Add a set of fields to select to the internal array.
This is the main query class.
Definition ApiQuery.php:36
makeTitle( $linkId)
Convert a link ID to a Title.to override Title
Service for compat reading of links tables.
Represents a title within MediaWiki.
Definition Title.php:69
Service for formatting and validating API parameters.
Type definition for integer types.