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