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