MediaWiki REL1_37
ApiQueryBacklinks.php
Go to the documentation of this file.
1<?php
25
35
39 private $rootTitle;
40
41 private $params;
43 private $cont;
44 private $redirect;
46
48 private $helpUrl;
49
55 private $pageMap = [];
56 private $resultArr;
57
58 private $redirTitles = [];
59 private $continueStr = null;
60
63 'backlinks' => [
64 'code' => 'bl',
65 'prefix' => 'pl',
66 'linktbl' => 'pagelinks',
67 'helpurl' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Backlinks',
68 ],
69 'embeddedin' => [
70 'code' => 'ei',
71 'prefix' => 'tl',
72 'linktbl' => 'templatelinks',
73 'helpurl' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Embeddedin',
74 ],
75 'imageusage' => [
76 'code' => 'iu',
77 'prefix' => 'il',
78 'linktbl' => 'imagelinks',
79 'helpurl' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Imageusage',
80 ]
81 ];
82
87 public function __construct( ApiQuery $query, $moduleName ) {
88 $settings = $this->backlinksSettings[$moduleName];
89 $prefix = $settings['prefix'];
90 $code = $settings['code'];
91 $this->resultArr = [];
92
93 parent::__construct( $query, $moduleName, $code );
94 $this->bl_ns = $prefix . '_namespace';
95 $this->bl_from = $prefix . '_from';
96 $this->bl_from_ns = $prefix . '_from_namespace';
97 $this->bl_table = $settings['linktbl'];
98 $this->bl_code = $code;
99 $this->helpUrl = $settings['helpurl'];
100
101 $this->hasNS = $moduleName !== 'imageusage';
102 if ( $this->hasNS ) {
103 $this->bl_title = $prefix . '_title';
104 $this->bl_fields = [
107 ];
108 } else {
109 $this->bl_title = $prefix . '_to';
110 $this->bl_fields = [
112 ];
113 }
114 }
115
116 public function execute() {
117 $this->run();
118 }
119
120 public function getCacheMode( $params ) {
121 return 'public';
122 }
123
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->addWhereFld( $this->bl_title, $this->rootTitle->getDBkey() );
143 if ( $this->hasNS ) {
144 $this->addWhereFld( $this->bl_ns, $this->rootTitle->getNamespace() );
145 }
146 $this->addWhereFld( $this->bl_from_ns, $this->params['namespace'] );
147
148 if ( count( $this->cont ) >= 2 ) {
149 $op = $this->params['dir'] == 'descending' ? '<' : '>';
150 if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
151 $this->addWhere(
152 "{$this->bl_from_ns} $op {$this->cont[0]} OR " .
153 "({$this->bl_from_ns} = {$this->cont[0]} AND " .
154 "{$this->bl_from} $op= {$this->cont[1]})"
155 );
156 } else {
157 $this->addWhere( "{$this->bl_from} $op= {$this->cont[1]}" );
158 }
159 }
160
161 if ( $this->params['filterredir'] == 'redirects' ) {
162 $this->addWhereFld( 'page_is_redirect', 1 );
163 } elseif ( $this->params['filterredir'] == 'nonredirects' && !$this->redirect ) {
164 // T24245 - Check for !redirect, as filtering nonredirects, when
165 // getting what links to them is contradictory
166 $this->addWhereFld( 'page_is_redirect', 0 );
167 }
168
169 $this->addOption( 'LIMIT', $this->params['limit'] + 1 );
170 $sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' );
171 $orderBy = [];
172 if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
173 $orderBy[] = $this->bl_from_ns . $sort;
174 }
175 $orderBy[] = $this->bl_from . $sort;
176 $this->addOption( 'ORDER BY', $orderBy );
177 $this->addOption( 'STRAIGHT_JOIN' );
178
179 $res = $this->select( __METHOD__ );
180
181 if ( $resultPageSet === null ) {
182 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__ );
183 }
184
185 $count = 0;
186 foreach ( $res as $row ) {
187 if ( ++$count > $this->params['limit'] ) {
188 // We've reached the one extra which shows that there are
189 // additional pages to be had. Stop here...
190 // Continue string may be overridden at a later step
191 $this->continueStr = "{$row->from_ns}|{$row->page_id}";
192 break;
193 }
194
195 // Fill in continuation fields for later steps
196 if ( count( $this->cont ) < 2 ) {
197 $this->cont[] = $row->from_ns;
198 $this->cont[] = $row->page_id;
199 }
200
201 $this->pageMap[$row->page_namespace][$row->page_title] = $row->page_id;
202 $t = Title::makeTitle( $row->page_namespace, $row->page_title );
203 if ( $row->page_is_redirect ) {
204 $this->redirTitles[] = $t;
205 }
206
207 if ( $resultPageSet === null ) {
208 $a = [ 'pageid' => (int)$row->page_id ];
210 if ( $row->page_is_redirect ) {
211 $a['redirect'] = true;
212 }
213 // Put all the results in an array first
214 $this->resultArr[$a['pageid']] = $a;
215 } else {
216 $resultPageSet->processDbRow( $row );
217 }
218 }
219 }
220
225 private function runSecondQuery( $resultPageSet = null ) {
226 $db = $this->getDB();
227 $this->addTables( [ $this->bl_table, 'page' ] );
228 $this->addWhere( "{$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 $titleWhere[] = "{$this->bl_title} = " . $db->addQuotes( $redirDBkey ) .
250 ( $this->hasNS ? " AND {$this->bl_ns} = {$redirNs}" : '' );
251 $allRedirNs[$redirNs] = true;
252 $allRedirDBkey[$redirDBkey] = true;
253 }
254 $this->addWhere( $db->makeList( $titleWhere, LIST_OR ) );
255 $this->addWhereFld( 'page_namespace', $this->params['namespace'] );
256
257 if ( count( $this->cont ) >= 6 ) {
258 $op = $this->params['dir'] == 'descending' ? '<' : '>';
259
260 $where = "{$this->bl_from} $op= {$this->cont[5]}";
261 // Don't bother with namespace, title, or from_namespace if it's
262 // otherwise constant in the where clause.
263 if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
264 $where = "{$this->bl_from_ns} $op {$this->cont[4]} OR " .
265 "({$this->bl_from_ns} = {$this->cont[4]} AND ($where))";
266 }
267 if ( count( $allRedirDBkey ) > 1 ) {
268 $title = $db->addQuotes( $this->cont[3] );
269 $where = "{$this->bl_title} $op $title OR " .
270 "({$this->bl_title} = $title AND ($where))";
271 }
272 if ( $this->hasNS && count( $allRedirNs ) > 1 ) {
273 $where = "{$this->bl_ns} $op {$this->cont[2]} OR " .
274 "({$this->bl_ns} = {$this->cont[2]} AND ($where))";
275 }
276
277 $this->addWhere( $where );
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 $this->cont = [];
387 if ( $this->params['continue'] !== null ) {
388 $cont = explode( '|', $this->params['continue'] );
389
390 switch ( count( $cont ) ) {
391 case 8:
392 // redirect page ID for result adding
393 $this->cont[7] = (int)$cont[7];
394 $this->dieContinueUsageIf( $cont[7] !== (string)$this->cont[7] );
395
396 /* Fall through */
397
398 case 7:
399 // top-level page ID for result adding
400 $this->cont[6] = (int)$cont[6];
401 $this->dieContinueUsageIf( $cont[6] !== (string)$this->cont[6] );
402
403 /* Fall through */
404
405 case 6:
406 // ns for 2nd query (even for imageusage)
407 $this->cont[2] = (int)$cont[2];
408 $this->dieContinueUsageIf( $cont[2] !== (string)$this->cont[2] );
409
410 // title for 2nd query
411 $this->cont[3] = $cont[3];
412
413 // from_ns for 2nd query
414 $this->cont[4] = (int)$cont[4];
415 $this->dieContinueUsageIf( $cont[4] !== (string)$this->cont[4] );
416
417 // from_id for 1st query
418 $this->cont[5] = (int)$cont[5];
419 $this->dieContinueUsageIf( $cont[5] !== (string)$this->cont[5] );
420
421 /* Fall through */
422
423 case 2:
424 // from_ns for 1st query
425 $this->cont[0] = (int)$cont[0];
426 $this->dieContinueUsageIf( $cont[0] !== (string)$this->cont[0] );
427
428 // from_id for 1st query
429 $this->cont[1] = (int)$cont[1];
430 $this->dieContinueUsageIf( $cont[1] !== (string)$this->cont[1] );
431
432 break;
433
434 default:
435 // @phan-suppress-next-line PhanImpossibleCondition
436 $this->dieContinueUsageIf( true );
437 }
438
439 ksort( $this->cont );
440 }
441
442 $this->runFirstQuery( $resultPageSet );
443 if ( $this->redirect && count( $this->redirTitles ) ) {
444 $this->resetQueryParams();
445 $this->runSecondQuery( $resultPageSet );
446 }
447
448 // Fill in any missing fields in case it's needed below
449 $this->cont += [ 0, 0, 0, '', 0, 0, 0 ];
450
451 if ( $resultPageSet === null ) {
452 // Try to add the result data in one go and pray that it fits
453 $code = $this->bl_code;
454 $data = array_map( static function ( $arr ) use ( $code ) {
455 if ( isset( $arr['redirlinks'] ) ) {
456 $arr['redirlinks'] = array_values( $arr['redirlinks'] );
457 ApiResult::setIndexedTagName( $arr['redirlinks'], $code );
458 }
459 return $arr;
460 }, array_values( $this->resultArr ) );
461 $fit = $result->addValue( 'query', $this->getModuleName(), $data );
462 if ( !$fit ) {
463 // It didn't fit. Add elements one by one until the
464 // result is full.
465 ksort( $this->resultArr );
466 // @phan-suppress-next-line PhanSuspiciousValueComparison
467 if ( count( $this->cont ) >= 7 ) {
468 $startAt = $this->cont[6];
469 } else {
470 reset( $this->resultArr );
471 $startAt = key( $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 PhanSuspiciousValueComparisonInLoop
493 if ( count( $this->cont ) >= 8 && $pageID == $startAt ) {
494 $redirStartAt = $this->cont[7];
495 } else {
496 reset( $redirLinks );
497 $redirStartAt = key( $redirLinks );
498 }
499 foreach ( $redirLinks as $key => $redir ) {
500 if ( $key < $redirStartAt ) {
501 continue;
502 }
503
504 $fit = $result->addValue(
505 [ 'query', $this->getModuleName(), $idx, 'redirlinks' ],
506 null, $redir );
507 if ( !$fit ) {
508 $this->continueStr = implode( '|', array_slice( $this->cont, 0, 6 ) ) .
509 "|$pageID|$key";
510 break;
511 }
512 $hasRedirs = true;
513 }
514 if ( $hasRedirs ) {
515 $result->addIndexedTagName(
516 [ 'query', $this->getModuleName(), $idx, 'redirlinks' ],
517 $this->bl_code );
518 }
519 if ( !$fit ) {
520 break;
521 }
522
523 $idx++;
524 }
525 }
526
527 $result->addIndexedTagName(
528 [ 'query', $this->getModuleName() ],
529 $this->bl_code
530 );
531 }
532 if ( $this->continueStr !== null ) {
533 $this->setContinueEnumParameter( 'continue', $this->continueStr );
534 }
535 }
536
537 public function getAllowedParams() {
538 $retval = [
539 'title' => [
540 ApiBase::PARAM_TYPE => 'string',
541 ],
542 'pageid' => [
543 ApiBase::PARAM_TYPE => 'integer',
544 ],
545 'continue' => [
546 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
547 ],
548 'namespace' => [
550 ApiBase::PARAM_TYPE => 'namespace'
551 ],
552 'dir' => [
553 ApiBase::PARAM_DFLT => 'ascending',
555 'ascending',
556 'descending'
557 ]
558 ],
559 'filterredir' => [
560 ApiBase::PARAM_DFLT => 'all',
562 'all',
563 'redirects',
564 'nonredirects'
565 ]
566 ],
567 'limit' => [
569 ApiBase::PARAM_TYPE => 'limit',
573 ]
574 ];
575 if ( $this->getModuleName() == 'embeddedin' ) {
576 return $retval;
577 }
578 $retval['redirect'] = false;
579
580 return $retval;
581 }
582
583 protected function getExamplesMessages() {
584 static $examples = [
585 'backlinks' => [
586 'action=query&list=backlinks&bltitle=Main%20Page'
587 => 'apihelp-query+backlinks-example-simple',
588 'action=query&generator=backlinks&gbltitle=Main%20Page&prop=info'
589 => 'apihelp-query+backlinks-example-generator',
590 ],
591 'embeddedin' => [
592 'action=query&list=embeddedin&eititle=Template:Stub'
593 => 'apihelp-query+embeddedin-example-simple',
594 'action=query&generator=embeddedin&geititle=Template:Stub&prop=info'
595 => 'apihelp-query+embeddedin-example-generator',
596 ],
597 'imageusage' => [
598 'action=query&list=imageusage&iutitle=File:Albert%20Einstein%20Head.jpg'
599 => 'apihelp-query+imageusage-example-simple',
600 'action=query&generator=imageusage&giutitle=File:Albert%20Einstein%20Head.jpg&prop=info'
601 => 'apihelp-query+imageusage-example-generator',
602 ]
603 ];
604
605 return $examples[$this->getModuleName()];
606 }
607
608 public function getHelpUrls() {
609 return $this->helpUrl;
610 }
611}
const NS_FILE
Definition Defines.php:70
const LIST_OR
Definition Defines.php:46
const PARAM_MAX2
Definition ApiBase.php:89
const PARAM_MAX
Definition ApiBase.php:85
const PARAM_TYPE
Definition ApiBase.php:81
const PARAM_DFLT
Definition ApiBase.php:73
const PARAM_MIN
Definition ApiBase.php:93
const LIMIT_BIG1
Fast query, standard limit.
Definition ApiBase.php:220
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:162
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:222
const PARAM_ISMULTI
Definition ApiBase.php:77
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:37
Represents a title within MediaWiki.
Definition Title.php:48
Service for formatting and validating API parameters.
Type definition for integer types.