]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/api/ApiQueryBacklinksprop.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / api / ApiQueryBacklinksprop.php
1 <?php
2 /**
3  * API module to handle links table back-queries
4  *
5  * Created on Aug 19, 2014
6  *
7  * Copyright © 2014 Wikimedia Foundation and contributors
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License along
20  * with this program; if not, write to the Free Software Foundation, Inc.,
21  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22  * http://www.gnu.org/copyleft/gpl.html
23  *
24  * @file
25  * @since 1.24
26  */
27
28 /**
29  * This implements prop=redirects, prop=linkshere, prop=catmembers,
30  * prop=transcludedin, and prop=fileusage
31  *
32  * @ingroup API
33  * @since 1.24
34  */
35 class ApiQueryBacklinksprop extends ApiQueryGeneratorBase {
36
37         // Data for the various modules implemented by this class
38         private static $settings = [
39                 'redirects' => [
40                         'code' => 'rd',
41                         'prefix' => 'rd',
42                         'linktable' => 'redirect',
43                         'props' => [
44                                 'fragment',
45                         ],
46                         'showredirects' => false,
47                         'show' => [
48                                 'fragment',
49                                 '!fragment',
50                         ],
51                 ],
52                 'linkshere' => [
53                         'code' => 'lh',
54                         'prefix' => 'pl',
55                         'linktable' => 'pagelinks',
56                         'indexes' => [ 'pl_namespace', 'pl_backlinks_namespace' ],
57                         'from_namespace' => true,
58                         'showredirects' => true,
59                 ],
60                 'transcludedin' => [
61                         'code' => 'ti',
62                         'prefix' => 'tl',
63                         'linktable' => 'templatelinks',
64                         'indexes' => [ 'tl_namespace', 'tl_backlinks_namespace' ],
65                         'from_namespace' => true,
66                         'showredirects' => true,
67                 ],
68                 'fileusage' => [
69                         'code' => 'fu',
70                         'prefix' => 'il',
71                         'linktable' => 'imagelinks',
72                         'indexes' => [ 'il_to', 'il_backlinks_namespace' ],
73                         'from_namespace' => true,
74                         'to_namespace' => NS_FILE,
75                         'exampletitle' => 'File:Example.jpg',
76                         'showredirects' => true,
77                 ],
78         ];
79
80         public function __construct( ApiQuery $query, $moduleName ) {
81                 parent::__construct( $query, $moduleName, self::$settings[$moduleName]['code'] );
82         }
83
84         public function execute() {
85                 $this->run();
86         }
87
88         public function executeGenerator( $resultPageSet ) {
89                 $this->run( $resultPageSet );
90         }
91
92         /**
93          * @param ApiPageSet $resultPageSet
94          */
95         private function run( ApiPageSet $resultPageSet = null ) {
96                 $settings = self::$settings[$this->getModuleName()];
97
98                 $db = $this->getDB();
99                 $params = $this->extractRequestParams();
100                 $prop = array_flip( $params['prop'] );
101                 $emptyString = $db->addQuotes( '' );
102
103                 $pageSet = $this->getPageSet();
104                 $titles = $pageSet->getGoodAndMissingTitles();
105                 $map = $pageSet->getGoodAndMissingTitlesByNamespace();
106
107                 // Add in special pages, they can theoretically have backlinks too.
108                 // (although currently they only do for prop=redirects)
109                 foreach ( $pageSet->getSpecialTitles() as $id => $title ) {
110                         $titles[] = $title;
111                         $map[$title->getNamespace()][$title->getDBkey()] = $id;
112                 }
113
114                 // Determine our fields to query on
115                 $p = $settings['prefix'];
116                 $hasNS = !isset( $settings['to_namespace'] );
117                 if ( $hasNS ) {
118                         $bl_namespace = "{$p}_namespace";
119                         $bl_title = "{$p}_title";
120                 } else {
121                         $bl_namespace = $settings['to_namespace'];
122                         $bl_title = "{$p}_to";
123
124                         $titles = array_filter( $titles, function ( $t ) use ( $bl_namespace ) {
125                                 return $t->getNamespace() === $bl_namespace;
126                         } );
127                         $map = array_intersect_key( $map, [ $bl_namespace => true ] );
128                 }
129                 $bl_from = "{$p}_from";
130
131                 if ( !$titles ) {
132                         return; // nothing to do
133                 }
134
135                 // Figure out what we're sorting by, and add associated WHERE clauses.
136                 // MySQL's query planner screws up if we include a field in ORDER BY
137                 // when it's constant in WHERE, so we have to test that for each field.
138                 $sortby = [];
139                 if ( $hasNS && count( $map ) > 1 ) {
140                         $sortby[$bl_namespace] = 'ns';
141                 }
142                 $theTitle = null;
143                 foreach ( $map as $nsTitles ) {
144                         reset( $nsTitles );
145                         $key = key( $nsTitles );
146                         if ( $theTitle === null ) {
147                                 $theTitle = $key;
148                         }
149                         if ( count( $nsTitles ) > 1 || $key !== $theTitle ) {
150                                 $sortby[$bl_title] = 'title';
151                                 break;
152                         }
153                 }
154                 $miser_ns = null;
155                 if ( $params['namespace'] !== null ) {
156                         if ( empty( $settings['from_namespace'] ) ) {
157                                 if ( $this->getConfig()->get( 'MiserMode' ) ) {
158                                         $miser_ns = $params['namespace'];
159                                 } else {
160                                         $this->addWhereFld( 'page_namespace', $params['namespace'] );
161                                 }
162                         } else {
163                                 $this->addWhereFld( "{$p}_from_namespace", $params['namespace'] );
164                                 if ( !empty( $settings['from_namespace'] ) && count( $params['namespace'] ) > 1 ) {
165                                         $sortby["{$p}_from_namespace"] = 'int';
166                                 }
167                         }
168                 }
169                 $sortby[$bl_from] = 'int';
170
171                 // Now use the $sortby to figure out the continuation
172                 if ( !is_null( $params['continue'] ) ) {
173                         $cont = explode( '|', $params['continue'] );
174                         $this->dieContinueUsageIf( count( $cont ) != count( $sortby ) );
175                         $where = '';
176                         $i = count( $sortby ) - 1;
177                         foreach ( array_reverse( $sortby, true ) as $field => $type ) {
178                                 $v = $cont[$i];
179                                 switch ( $type ) {
180                                         case 'ns':
181                                         case 'int':
182                                                 $v = (int)$v;
183                                                 $this->dieContinueUsageIf( $v != $cont[$i] );
184                                                 break;
185                                         default:
186                                                 $v = $db->addQuotes( $v );
187                                                 break;
188                                 }
189
190                                 if ( $where === '' ) {
191                                         $where = "$field >= $v";
192                                 } else {
193                                         $where = "$field > $v OR ($field = $v AND ($where))";
194                                 }
195
196                                 $i--;
197                         }
198                         $this->addWhere( $where );
199                 }
200
201                 // Populate the rest of the query
202                 $this->addTables( [ $settings['linktable'], 'page' ] );
203                 $this->addWhere( "$bl_from = page_id" );
204
205                 if ( $this->getModuleName() === 'redirects' ) {
206                         $this->addWhere( "rd_interwiki = $emptyString OR rd_interwiki IS NULL" );
207                 }
208
209                 $this->addFields( array_keys( $sortby ) );
210                 $this->addFields( [ 'bl_namespace' => $bl_namespace, 'bl_title' => $bl_title ] );
211                 if ( is_null( $resultPageSet ) ) {
212                         $fld_pageid = isset( $prop['pageid'] );
213                         $fld_title = isset( $prop['title'] );
214                         $fld_redirect = isset( $prop['redirect'] );
215
216                         $this->addFieldsIf( 'page_id', $fld_pageid );
217                         $this->addFieldsIf( [ 'page_title', 'page_namespace' ], $fld_title );
218                         $this->addFieldsIf( 'page_is_redirect', $fld_redirect );
219
220                         // prop=redirects
221                         $fld_fragment = isset( $prop['fragment'] );
222                         $this->addFieldsIf( 'rd_fragment', $fld_fragment );
223                 } else {
224                         $this->addFields( $resultPageSet->getPageTableFields() );
225                 }
226
227                 $this->addFieldsIf( 'page_namespace', $miser_ns !== null );
228
229                 if ( $hasNS ) {
230                         // Can't use LinkBatch because it throws away Special titles.
231                         // And we already have the needed data structure anyway.
232                         $this->addWhere( $db->makeWhereFrom2d( $map, $bl_namespace, $bl_title ) );
233                 } else {
234                         $where = [];
235                         foreach ( $titles as $t ) {
236                                 if ( $t->getNamespace() == $bl_namespace ) {
237                                         $where[] = "$bl_title = " . $db->addQuotes( $t->getDBkey() );
238                                 }
239                         }
240                         $this->addWhere( $db->makeList( $where, LIST_OR ) );
241                 }
242
243                 if ( $params['show'] !== null ) {
244                         // prop=redirects only
245                         $show = array_flip( $params['show'] );
246                         if ( isset( $show['fragment'] ) && isset( $show['!fragment'] ) ||
247                                 isset( $show['redirect'] ) && isset( $show['!redirect'] )
248                         ) {
249                                 $this->dieWithError( 'apierror-show' );
250                         }
251                         $this->addWhereIf( "rd_fragment != $emptyString", isset( $show['fragment'] ) );
252                         $this->addWhereIf(
253                                 "rd_fragment = $emptyString OR rd_fragment IS NULL",
254                                 isset( $show['!fragment'] )
255                         );
256                         $this->addWhereIf( [ 'page_is_redirect' => 1 ], isset( $show['redirect'] ) );
257                         $this->addWhereIf( [ 'page_is_redirect' => 0 ], isset( $show['!redirect'] ) );
258                 }
259
260                 // Override any ORDER BY from above with what we calculated earlier.
261                 $this->addOption( 'ORDER BY', array_keys( $sortby ) );
262
263                 // MySQL's optimizer chokes if we have too many values in "$bl_title IN
264                 // (...)" and chooses the wrong index, so specify the correct index to
265                 // use for the query. See T139056 for details.
266                 if ( !empty( $settings['indexes'] ) ) {
267                         list( $idxNoFromNS, $idxWithFromNS ) = $settings['indexes'];
268                         if ( $params['namespace'] !== null && !empty( $settings['from_namespace'] ) ) {
269                                 $this->addOption( 'USE INDEX', [ $settings['linktable'] => $idxWithFromNS ] );
270                         } else {
271                                 $this->addOption( 'USE INDEX', [ $settings['linktable'] => $idxNoFromNS ] );
272                         }
273                 }
274
275                 // MySQL (or at least 5.5.5-10.0.23-MariaDB) chooses a really bad query
276                 // plan if it thinks there will be more matching rows in the linktable
277                 // than are in page. Use STRAIGHT_JOIN here to force it to use the
278                 // intended, fast plan. See T145079 for details.
279                 $this->addOption( 'STRAIGHT_JOIN' );
280
281                 $this->addOption( 'LIMIT', $params['limit'] + 1 );
282
283                 $res = $this->select( __METHOD__ );
284
285                 if ( is_null( $resultPageSet ) ) {
286                         $count = 0;
287                         foreach ( $res as $row ) {
288                                 if ( ++$count > $params['limit'] ) {
289                                         // We've reached the one extra which shows that
290                                         // there are additional pages to be had. Stop here...
291                                         $this->setContinue( $row, $sortby );
292                                         break;
293                                 }
294
295                                 if ( $miser_ns !== null && !in_array( $row->page_namespace, $miser_ns ) ) {
296                                         // Miser mode namespace check
297                                         continue;
298                                 }
299
300                                 // Get the ID of the current page
301                                 $id = $map[$row->bl_namespace][$row->bl_title];
302
303                                 $vals = [];
304                                 if ( $fld_pageid ) {
305                                         $vals['pageid'] = (int)$row->page_id;
306                                 }
307                                 if ( $fld_title ) {
308                                         ApiQueryBase::addTitleInfo( $vals,
309                                                 Title::makeTitle( $row->page_namespace, $row->page_title )
310                                         );
311                                 }
312                                 if ( $fld_fragment && $row->rd_fragment !== null && $row->rd_fragment !== '' ) {
313                                         $vals['fragment'] = $row->rd_fragment;
314                                 }
315                                 if ( $fld_redirect ) {
316                                         $vals['redirect'] = (bool)$row->page_is_redirect;
317                                 }
318                                 $fit = $this->addPageSubItem( $id, $vals );
319                                 if ( !$fit ) {
320                                         $this->setContinue( $row, $sortby );
321                                         break;
322                                 }
323                         }
324                 } else {
325                         $titles = [];
326                         $count = 0;
327                         foreach ( $res as $row ) {
328                                 if ( ++$count > $params['limit'] ) {
329                                         // We've reached the one extra which shows that
330                                         // there are additional pages to be had. Stop here...
331                                         $this->setContinue( $row, $sortby );
332                                         break;
333                                 }
334                                 $titles[] = Title::makeTitle( $row->page_namespace, $row->page_title );
335                         }
336                         $resultPageSet->populateFromTitles( $titles );
337                 }
338         }
339
340         private function setContinue( $row, $sortby ) {
341                 $cont = [];
342                 foreach ( $sortby as $field => $v ) {
343                         $cont[] = $row->$field;
344                 }
345                 $this->setContinueEnumParameter( 'continue', implode( '|', $cont ) );
346         }
347
348         public function getCacheMode( $params ) {
349                 return 'public';
350         }
351
352         public function getAllowedParams() {
353                 $settings = self::$settings[$this->getModuleName()];
354
355                 $ret = [
356                         'prop' => [
357                                 ApiBase::PARAM_TYPE => [
358                                         'pageid',
359                                         'title',
360                                 ],
361                                 ApiBase::PARAM_ISMULTI => true,
362                                 ApiBase::PARAM_DFLT => 'pageid|title',
363                                 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
364                         ],
365                         'namespace' => [
366                                 ApiBase::PARAM_ISMULTI => true,
367                                 ApiBase::PARAM_TYPE => 'namespace',
368                         ],
369                         'show' => null, // Will be filled/removed below
370                         'limit' => [
371                                 ApiBase::PARAM_DFLT => 10,
372                                 ApiBase::PARAM_TYPE => 'limit',
373                                 ApiBase::PARAM_MIN => 1,
374                                 ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
375                                 ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
376                         ],
377                         'continue' => [
378                                 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
379                         ],
380                 ];
381
382                 if ( empty( $settings['from_namespace'] ) && $this->getConfig()->get( 'MiserMode' ) ) {
383                         $ret['namespace'][ApiBase::PARAM_HELP_MSG_APPEND] = [
384                                 'api-help-param-limited-in-miser-mode',
385                         ];
386                 }
387
388                 if ( !empty( $settings['showredirects'] ) ) {
389                         $ret['prop'][ApiBase::PARAM_TYPE][] = 'redirect';
390                         $ret['prop'][ApiBase::PARAM_DFLT] .= '|redirect';
391                 }
392                 if ( isset( $settings['props'] ) ) {
393                         $ret['prop'][ApiBase::PARAM_TYPE] = array_merge(
394                                 $ret['prop'][ApiBase::PARAM_TYPE], $settings['props']
395                         );
396                 }
397
398                 $show = [];
399                 if ( !empty( $settings['showredirects'] ) ) {
400                         $show[] = 'redirect';
401                         $show[] = '!redirect';
402                 }
403                 if ( isset( $settings['show'] ) ) {
404                         $show = array_merge( $show, $settings['show'] );
405                 }
406                 if ( $show ) {
407                         $ret['show'] = [
408                                 ApiBase::PARAM_TYPE => $show,
409                                 ApiBase::PARAM_ISMULTI => true,
410                         ];
411                 } else {
412                         unset( $ret['show'] );
413                 }
414
415                 return $ret;
416         }
417
418         protected function getExamplesMessages() {
419                 $settings = self::$settings[$this->getModuleName()];
420                 $name = $this->getModuleName();
421                 $path = $this->getModulePath();
422                 $title = isset( $settings['exampletitle'] ) ? $settings['exampletitle'] : 'Main Page';
423                 $etitle = rawurlencode( $title );
424
425                 return [
426                         "action=query&prop={$name}&titles={$etitle}"
427                                 => "apihelp-$path-example-simple",
428                         "action=query&generator={$name}&titles={$etitle}&prop=info"
429                                 => "apihelp-$path-example-generator",
430                 ];
431         }
432
433         public function getHelpUrls() {
434                 $name = ucfirst( $this->getModuleName() );
435                 return "https://www.mediawiki.org/wiki/Special:MyLanguage/API:{$name}";
436         }
437 }