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