MediaWiki  master
ApiQueryBacklinksprop.php
Go to the documentation of this file.
1 <?php
28 
37 
39  private static $settings = [
40  'redirects' => [
41  'code' => 'rd',
42  'prefix' => 'rd',
43  'linktable' => 'redirect',
44  'props' => [
45  'fragment',
46  ],
47  'showredirects' => false,
48  'show' => [
49  'fragment',
50  '!fragment',
51  ],
52  ],
53  'linkshere' => [
54  'code' => 'lh',
55  'prefix' => 'pl',
56  'linktable' => 'pagelinks',
57  'indexes' => [ 'pl_namespace', 'pl_backlinks_namespace' ],
58  'from_namespace' => true,
59  'showredirects' => true,
60  ],
61  'transcludedin' => [
62  'code' => 'ti',
63  'prefix' => 'tl',
64  'linktable' => 'templatelinks',
65  'indexes' => [ 'tl_namespace', 'tl_backlinks_namespace' ],
66  'from_namespace' => true,
67  'showredirects' => true,
68  ],
69  'fileusage' => [
70  'code' => 'fu',
71  'prefix' => 'il',
72  'linktable' => 'imagelinks',
73  'indexes' => [ 'il_to', 'il_backlinks_namespace' ],
74  'from_namespace' => true,
75  'to_namespace' => NS_FILE,
76  'exampletitle' => 'File:Example.jpg',
77  'showredirects' => true,
78  ],
79  ];
80 
82  private $linksMigration;
83 
89  public function __construct(
90  ApiQuery $query,
91  $moduleName,
93  ) {
94  parent::__construct( $query, $moduleName, self::$settings[$moduleName]['code'] );
95  $this->linksMigration = $linksMigration;
96  }
97 
98  public function execute() {
99  $this->run();
100  }
101 
102  public function executeGenerator( $resultPageSet ) {
103  $this->run( $resultPageSet );
104  }
105 
109  private function run( ApiPageSet $resultPageSet = null ) {
110  $settings = self::$settings[$this->getModuleName()];
111 
112  $db = $this->getDB();
113  $params = $this->extractRequestParams();
114  $prop = array_fill_keys( $params['prop'], true );
115  $emptyString = $db->addQuotes( '' );
116 
117  $pageSet = $this->getPageSet();
118  $titles = $pageSet->getGoodAndMissingPages();
119  $map = $pageSet->getGoodAndMissingTitlesByNamespace();
120 
121  // Add in special pages, they can theoretically have backlinks too.
122  // (although currently they only do for prop=redirects)
123  foreach ( $pageSet->getSpecialPages() as $id => $title ) {
124  $titles[] = $title;
125  $map[$title->getNamespace()][$title->getDBkey()] = $id;
126  }
127 
128  // Determine our fields to query on
129  $p = $settings['prefix'];
130  $hasNS = !isset( $settings['to_namespace'] );
131  if ( $hasNS ) {
132  if ( isset( $this->linksMigration::$mapping[$settings['linktable']] ) ) {
133  list( $bl_namespace, $bl_title ) = $this->linksMigration->getTitleFields( $settings['linktable'] );
134  } else {
135  $bl_namespace = "{$p}_namespace";
136  $bl_title = "{$p}_title";
137  }
138  } else {
139  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
140  $bl_namespace = $settings['to_namespace'];
141  $bl_title = "{$p}_to";
142 
143  $titles = array_filter( $titles, static function ( $t ) use ( $bl_namespace ) {
144  return $t->getNamespace() === $bl_namespace;
145  } );
146  $map = array_intersect_key( $map, [ $bl_namespace => true ] );
147  }
148  $bl_from = "{$p}_from";
149 
150  if ( !$titles ) {
151  return; // nothing to do
152  }
153  if ( $params['namespace'] !== null && count( $params['namespace'] ) === 0 ) {
154  return; // nothing to do
155  }
156 
157  // Figure out what we're sorting by, and add associated WHERE clauses.
158  // MySQL's query planner screws up if we include a field in ORDER BY
159  // when it's constant in WHERE, so we have to test that for each field.
160  $sortby = [];
161  if ( $hasNS && count( $map ) > 1 ) {
162  $sortby[$bl_namespace] = 'ns';
163  }
164  $theTitle = null;
165  foreach ( $map as $nsTitles ) {
166  reset( $nsTitles );
167  $key = key( $nsTitles );
168  if ( $theTitle === null ) {
169  $theTitle = $key;
170  }
171  if ( count( $nsTitles ) > 1 || $key !== $theTitle ) {
172  $sortby[$bl_title] = 'title';
173  break;
174  }
175  }
176  $miser_ns = null;
177  if ( $params['namespace'] !== null ) {
178  if ( empty( $settings['from_namespace'] ) ) {
179  if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
180  $miser_ns = $params['namespace'];
181  } else {
182  $this->addWhereFld( 'page_namespace', $params['namespace'] );
183  }
184  } else {
185  $this->addWhereFld( "{$p}_from_namespace", $params['namespace'] );
186  if ( !empty( $settings['from_namespace'] )
187  && $params['namespace'] !== null && count( $params['namespace'] ) > 1
188  ) {
189  $sortby["{$p}_from_namespace"] = 'int';
190  }
191  }
192  }
193  $sortby[$bl_from] = 'int';
194 
195  // Now use the $sortby to figure out the continuation
196  if ( $params['continue'] !== null ) {
197  $cont = explode( '|', $params['continue'] );
198  $this->dieContinueUsageIf( count( $cont ) != count( $sortby ) );
199  $where = '';
200  $i = count( $sortby ) - 1;
201  foreach ( array_reverse( $sortby, true ) as $field => $type ) {
202  $v = $cont[$i];
203  switch ( $type ) {
204  case 'ns':
205  case 'int':
206  $v = (int)$v;
207  $this->dieContinueUsageIf( $v != $cont[$i] );
208  break;
209  default:
210  $v = $db->addQuotes( $v );
211  break;
212  }
213 
214  if ( $where === '' ) {
215  $where = "$field >= $v";
216  } else {
217  $where = "$field > $v OR ($field = $v AND ($where))";
218  }
219 
220  $i--;
221  }
222  $this->addWhere( $where );
223  }
224 
225  // Populate the rest of the query
226  list( $idxNoFromNS, $idxWithFromNS ) = $settings['indexes'] ?? [ '', '' ];
227  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
228  if ( isset( $this->linksMigration::$mapping[$settings['linktable']] ) ) {
229  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
230  $queryInfo = $this->linksMigration->getQueryInfo( $settings['linktable'] );
231  $this->addTables( array_merge( [ 'page' ], $queryInfo['tables'] ) );
232  $this->addJoinConds( $queryInfo['joins'] );
233  // TODO: Move to links migration
234  if ( in_array( 'linktarget', $queryInfo['tables'] ) ) {
235  $idxWithFromNS .= '_target_id';
236  }
237  } else {
238  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
239  $this->addTables( [ $settings['linktable'], 'page' ] );
240  }
241  $this->addWhere( "$bl_from = page_id" );
242 
243  if ( $this->getModuleName() === 'redirects' ) {
244  $this->addWhere( "rd_interwiki = $emptyString OR rd_interwiki IS NULL" );
245  }
246 
247  $this->addFields( array_keys( $sortby ) );
248  $this->addFields( [ 'bl_namespace' => $bl_namespace, 'bl_title' => $bl_title ] );
249  if ( $resultPageSet === null ) {
250  $fld_pageid = isset( $prop['pageid'] );
251  $fld_title = isset( $prop['title'] );
252  $fld_redirect = isset( $prop['redirect'] );
253 
254  $this->addFieldsIf( 'page_id', $fld_pageid );
255  $this->addFieldsIf( [ 'page_title', 'page_namespace' ], $fld_title );
256  $this->addFieldsIf( 'page_is_redirect', $fld_redirect );
257 
258  // prop=redirects
259  $fld_fragment = isset( $prop['fragment'] );
260  $this->addFieldsIf( 'rd_fragment', $fld_fragment );
261  } else {
262  $this->addFields( $resultPageSet->getPageTableFields() );
263  }
264 
265  $this->addFieldsIf( 'page_namespace', $miser_ns !== null );
266 
267  if ( $hasNS ) {
268  // Can't use LinkBatch because it throws away Special titles.
269  // And we already have the needed data structure anyway.
270  $this->addWhere( $db->makeWhereFrom2d( $map, $bl_namespace, $bl_title ) );
271  } else {
272  $where = [];
273  foreach ( $titles as $t ) {
274  if ( $t->getNamespace() == $bl_namespace ) {
275  $where[] = "$bl_title = " . $db->addQuotes( $t->getDBkey() );
276  }
277  }
278  $this->addWhere( $db->makeList( $where, LIST_OR ) );
279  }
280 
281  if ( $params['show'] !== null ) {
282  // prop=redirects only
283  $show = array_fill_keys( $params['show'], true );
284  if ( isset( $show['fragment'] ) && isset( $show['!fragment'] ) ||
285  isset( $show['redirect'] ) && isset( $show['!redirect'] )
286  ) {
287  $this->dieWithError( 'apierror-show' );
288  }
289  $this->addWhereIf( "rd_fragment != $emptyString", isset( $show['fragment'] ) );
290  $this->addWhereIf(
291  "rd_fragment = $emptyString OR rd_fragment IS NULL",
292  isset( $show['!fragment'] )
293  );
294  $this->addWhereIf( [ 'page_is_redirect' => 1 ], isset( $show['redirect'] ) );
295  $this->addWhereIf( [ 'page_is_redirect' => 0 ], isset( $show['!redirect'] ) );
296  }
297 
298  // Override any ORDER BY from above with what we calculated earlier.
299  $this->addOption( 'ORDER BY', array_keys( $sortby ) );
300 
301  // MySQL's optimizer chokes if we have too many values in "$bl_title IN
302  // (...)" and chooses the wrong index, so specify the correct index to
303  // use for the query. See T139056 for details.
304  if ( !empty( $settings['indexes'] ) ) {
305  if ( $params['namespace'] !== null && !empty( $settings['from_namespace'] ) ) {
306  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
307  $this->addOption( 'USE INDEX', [ $settings['linktable'] => $idxWithFromNS ] );
308  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
309  } elseif ( !isset( $this->linksMigration::$mapping[$settings['linktable']] ) ) {
310  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
311  $this->addOption( 'USE INDEX', [ $settings['linktable'] => $idxNoFromNS ] );
312  }
313  }
314 
315  $this->addOption( 'LIMIT', $params['limit'] + 1 );
316 
317  $res = $this->select( __METHOD__ );
318 
319  if ( $resultPageSet === null ) {
320  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable set when used
321  if ( $fld_title ) {
322  $this->executeGenderCacheFromResultWrapper( $res, __METHOD__ );
323  }
324 
325  $count = 0;
326  foreach ( $res as $row ) {
327  if ( ++$count > $params['limit'] ) {
328  // We've reached the one extra which shows that
329  // there are additional pages to be had. Stop here...
330  $this->setContinue( $row, $sortby );
331  break;
332  }
333 
334  if ( $miser_ns !== null && !in_array( $row->page_namespace, $miser_ns ) ) {
335  // Miser mode namespace check
336  continue;
337  }
338 
339  // Get the ID of the current page
340  $id = $map[$row->bl_namespace][$row->bl_title];
341 
342  $vals = [];
343  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable set when used
344  if ( $fld_pageid ) {
345  $vals['pageid'] = (int)$row->page_id;
346  }
347  if ( $fld_title ) {
349  Title::makeTitle( $row->page_namespace, $row->page_title )
350  );
351  }
352  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable set when used
353  if ( $fld_fragment && $row->rd_fragment !== null && $row->rd_fragment !== '' ) {
354  $vals['fragment'] = $row->rd_fragment;
355  }
356  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable set when used
357  if ( $fld_redirect ) {
358  $vals['redirect'] = (bool)$row->page_is_redirect;
359  }
360  $fit = $this->addPageSubItem( $id, $vals );
361  if ( !$fit ) {
362  $this->setContinue( $row, $sortby );
363  break;
364  }
365  }
366  } else {
367  $titles = [];
368  $count = 0;
369  foreach ( $res as $row ) {
370  if ( ++$count > $params['limit'] ) {
371  // We've reached the one extra which shows that
372  // there are additional pages to be had. Stop here...
373  $this->setContinue( $row, $sortby );
374  break;
375  }
376 
377  if ( $miser_ns !== null && !in_array( $row->page_namespace, $miser_ns ) ) {
378  // Miser mode namespace check
379  continue;
380  }
381 
382  $titles[] = Title::makeTitle( $row->page_namespace, $row->page_title );
383  }
384  $resultPageSet->populateFromTitles( $titles );
385  }
386  }
387 
388  private function setContinue( $row, $sortby ) {
389  $cont = [];
390  foreach ( $sortby as $field => $v ) {
391  $cont[] = $row->$field;
392  }
393  $this->setContinueEnumParameter( 'continue', implode( '|', $cont ) );
394  }
395 
396  public function getCacheMode( $params ) {
397  return 'public';
398  }
399 
400  public function getAllowedParams() {
401  $settings = self::$settings[$this->getModuleName()];
402 
403  $ret = [
404  'prop' => [
406  'pageid',
407  'title',
408  ],
409  ApiBase::PARAM_ISMULTI => true,
410  ApiBase::PARAM_DFLT => 'pageid|title',
412  ],
413  'namespace' => [
414  ApiBase::PARAM_ISMULTI => true,
415  ApiBase::PARAM_TYPE => 'namespace',
416  ],
417  'show' => null, // Will be filled/removed below
418  'limit' => [
419  ApiBase::PARAM_DFLT => 10,
420  ApiBase::PARAM_TYPE => 'limit',
421  ApiBase::PARAM_MIN => 1,
424  ],
425  'continue' => [
426  ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
427  ],
428  ];
429 
430  if ( empty( $settings['from_namespace'] ) &&
431  $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
432  $ret['namespace'][ApiBase::PARAM_HELP_MSG_APPEND] = [
433  'api-help-param-limited-in-miser-mode',
434  ];
435  }
436 
437  if ( !empty( $settings['showredirects'] ) ) {
438  $ret['prop'][ApiBase::PARAM_TYPE][] = 'redirect';
439  $ret['prop'][ApiBase::PARAM_DFLT] .= '|redirect';
440  }
441  if ( isset( $settings['props'] ) ) {
442  $ret['prop'][ApiBase::PARAM_TYPE] = array_merge(
443  $ret['prop'][ApiBase::PARAM_TYPE], $settings['props']
444  );
445  }
446 
447  $show = [];
448  if ( !empty( $settings['showredirects'] ) ) {
449  $show[] = 'redirect';
450  $show[] = '!redirect';
451  }
452  if ( isset( $settings['show'] ) ) {
453  $show = array_merge( $show, $settings['show'] );
454  }
455  if ( $show ) {
456  $ret['show'] = [
457  ApiBase::PARAM_TYPE => $show,
458  ApiBase::PARAM_ISMULTI => true,
459  ];
460  } else {
461  unset( $ret['show'] );
462  }
463 
464  return $ret;
465  }
466 
467  protected function getExamplesMessages() {
468  $settings = self::$settings[$this->getModuleName()];
469  $name = $this->getModuleName();
470  $path = $this->getModulePath();
471  $title = $settings['exampletitle'] ?? 'Main Page';
472  $etitle = rawurlencode( $title );
473 
474  return [
475  "action=query&prop={$name}&titles={$etitle}"
476  => "apihelp-$path-example-simple",
477  "action=query&generator={$name}&titles={$etitle}&prop=info"
478  => "apihelp-$path-example-generator",
479  ];
480  }
481 
482  public function getHelpUrls() {
483  $name = ucfirst( $this->getModuleName() );
484  return "https://www.mediawiki.org/wiki/Special:MyLanguage/API:{$name}";
485  }
486 }
const NS_FILE
Definition: Defines.php:70
const LIST_OR
Definition: Defines.php:46
const PARAM_MAX2
Definition: ApiBase.php:90
const PARAM_MAX
Definition: ApiBase.php:86
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition: ApiBase.php:1446
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition: ApiBase.php:1638
const PARAM_TYPE
Definition: ApiBase.php:82
const PARAM_DFLT
Definition: ApiBase.php:74
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
Definition: ApiBase.php:170
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, this is an array mapping those values to $msg...
Definition: ApiBase.php:196
const PARAM_MIN
Definition: ApiBase.php:94
const LIMIT_BIG1
Fast query, standard limit.
Definition: ApiBase.php:221
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:765
getModulePath()
Get the path to this module.
Definition: ApiBase.php:573
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
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:498
const PARAM_ISMULTI
Definition: ApiBase.php:78
This class contains a list of pages that the client has requested.
Definition: ApiPageSet.php:49
This implements prop=redirects, prop=linkshere, prop=catmembers, prop=transcludedin,...
executeGenerator( $resultPageSet)
Execute this module as a generator.
static array $settings
Data for the various modules implemented by this class.
getHelpUrls()
Return links to more detailed help pages about the module.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
run(ApiPageSet $resultPageSet=null)
getExamplesMessages()
Returns usage examples for this module.
__construct(ApiQuery $query, $moduleName, LinksMigration $linksMigration)
getCacheMode( $params)
Get the cache mode for the data generated by this module.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
addWhereIf( $value, $condition)
Same as addWhere(), but add the WHERE clauses only if a condition is met.
addFields( $value)
Add a set of fields to select to the internal array.
addPageSubItem( $pageId, $item, $elemname=null)
Same as addPageSubItems(), but one element of $data at a time.
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.
addFieldsIf( $value, $condition)
Same as addFields(), but add the fields only if a condition is met.
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.
setContinueEnumParameter( $paramName, $paramValue)
Overridden to set the generator param if in generator mode.
getPageSet()
Get the PageSet object to work on.
This is the main query class.
Definition: ApiQuery.php:40
Service for compat reading of links tables.
A class containing constants representing the names of configuration variables.
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:637