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