Completed
Branch master (939199)
by
unknown
39:35
created

includes/api/ApiQueryBacklinks.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 *
4
 *
5
 * Created on Oct 16, 2006
6
 *
7
 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
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
 */
26
27
/**
28
 * This is a three-in-one module to query:
29
 *   * backlinks  - links pointing to the given page,
30
 *   * embeddedin - what pages transclude the given page within themselves,
31
 *   * imageusage - what pages use the given image
32
 *
33
 * @ingroup API
34
 */
35
class ApiQueryBacklinks extends ApiQueryGeneratorBase {
36
37
	/**
38
	 * @var Title
39
	 */
40
	private $rootTitle;
41
42
	private $params, $cont, $redirect;
43
	private $bl_ns, $bl_from, $bl_from_ns, $bl_table, $bl_code, $bl_title, $bl_fields, $hasNS;
44
45
	/**
46
	 * Maps ns and title to pageid
47
	 *
48
	 * @var array
49
	 */
50
	private $pageMap = [];
51
	private $resultArr;
52
53
	private $redirTitles = [];
54
	private $continueStr = null;
55
56
	// output element name, database column field prefix, database table
57
	private $backlinksSettings = [
58
		'backlinks' => [
59
			'code' => 'bl',
60
			'prefix' => 'pl',
61
			'linktbl' => 'pagelinks',
62
			'helpurl' => 'https://www.mediawiki.org/wiki/API:Backlinks',
63
		],
64
		'embeddedin' => [
65
			'code' => 'ei',
66
			'prefix' => 'tl',
67
			'linktbl' => 'templatelinks',
68
			'helpurl' => 'https://www.mediawiki.org/wiki/API:Embeddedin',
69
		],
70
		'imageusage' => [
71
			'code' => 'iu',
72
			'prefix' => 'il',
73
			'linktbl' => 'imagelinks',
74
			'helpurl' => 'https://www.mediawiki.org/wiki/API:Imageusage',
75
		]
76
	];
77
78
	public function __construct( ApiQuery $query, $moduleName ) {
79
		$settings = $this->backlinksSettings[$moduleName];
80
		$prefix = $settings['prefix'];
81
		$code = $settings['code'];
82
		$this->resultArr = [];
83
84
		parent::__construct( $query, $moduleName, $code );
85
		$this->bl_ns = $prefix . '_namespace';
86
		$this->bl_from = $prefix . '_from';
87
		$this->bl_from_ns = $prefix . '_from_namespace';
88
		$this->bl_table = $settings['linktbl'];
89
		$this->bl_code = $code;
90
		$this->helpUrl = $settings['helpurl'];
0 ignored issues
show
The property helpUrl does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
91
92
		$this->hasNS = $moduleName !== 'imageusage';
93
		if ( $this->hasNS ) {
94
			$this->bl_title = $prefix . '_title';
95
			$this->bl_fields = [
96
				$this->bl_ns,
97
				$this->bl_title
98
			];
99
		} else {
100
			$this->bl_title = $prefix . '_to';
101
			$this->bl_fields = [
102
				$this->bl_title
103
			];
104
		}
105
	}
106
107
	public function execute() {
108
		$this->run();
109
	}
110
111
	public function getCacheMode( $params ) {
112
		return 'public';
113
	}
114
115
	public function executeGenerator( $resultPageSet ) {
116
		$this->run( $resultPageSet );
117
	}
118
119
	/**
120
	 * @param ApiPageSet $resultPageSet
121
	 * @return void
122
	 */
123
	private function runFirstQuery( $resultPageSet = null ) {
124
		$this->addTables( [ $this->bl_table, 'page' ] );
125
		$this->addWhere( "{$this->bl_from}=page_id" );
126
		if ( is_null( $resultPageSet ) ) {
127
			$this->addFields( [ 'page_id', 'page_title', 'page_namespace' ] );
128
		} else {
129
			$this->addFields( $resultPageSet->getPageTableFields() );
130
		}
131
		$this->addFields( [ 'page_is_redirect', 'from_ns' => 'page_namespace' ] );
132
133
		$this->addWhereFld( $this->bl_title, $this->rootTitle->getDBkey() );
134
		if ( $this->hasNS ) {
135
			$this->addWhereFld( $this->bl_ns, $this->rootTitle->getNamespace() );
136
		}
137
		$this->addWhereFld( $this->bl_from_ns, $this->params['namespace'] );
138
139
		if ( count( $this->cont ) >= 2 ) {
140
			$op = $this->params['dir'] == 'descending' ? '<' : '>';
141
			if ( count( $this->params['namespace'] ) > 1 ) {
142
				$this->addWhere(
143
					"{$this->bl_from_ns} $op {$this->cont[0]} OR " .
144
					"({$this->bl_from_ns} = {$this->cont[0]} AND " .
145
					"{$this->bl_from} $op= {$this->cont[1]})"
146
				);
147
			} else {
148
				$this->addWhere( "{$this->bl_from} $op= {$this->cont[1]}" );
149
			}
150
		}
151
152 View Code Duplication
		if ( $this->params['filterredir'] == 'redirects' ) {
153
			$this->addWhereFld( 'page_is_redirect', 1 );
154
		} elseif ( $this->params['filterredir'] == 'nonredirects' && !$this->redirect ) {
155
			// bug 22245 - Check for !redirect, as filtering nonredirects, when
156
			// getting what links to them is contradictory
157
			$this->addWhereFld( 'page_is_redirect', 0 );
158
		}
159
160
		$this->addOption( 'LIMIT', $this->params['limit'] + 1 );
161
		$sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' );
162
		$orderBy = [];
163 View Code Duplication
		if ( count( $this->params['namespace'] ) > 1 ) {
164
			$orderBy[] = $this->bl_from_ns . $sort;
165
		}
166
		$orderBy[] = $this->bl_from . $sort;
167
		$this->addOption( 'ORDER BY', $orderBy );
168
		$this->addOption( 'STRAIGHT_JOIN' );
169
170
		$res = $this->select( __METHOD__ );
171
		$count = 0;
172
		foreach ( $res as $row ) {
173
			if ( ++$count > $this->params['limit'] ) {
174
				// We've reached the one extra which shows that there are
175
				// additional pages to be had. Stop here...
176
				// Continue string may be overridden at a later step
177
				$this->continueStr = "{$row->from_ns}|{$row->page_id}";
178
				break;
179
			}
180
181
			// Fill in continuation fields for later steps
182
			if ( count( $this->cont ) < 2 ) {
183
				$this->cont[] = $row->from_ns;
184
				$this->cont[] = $row->page_id;
185
			}
186
187
			$this->pageMap[$row->page_namespace][$row->page_title] = $row->page_id;
188
			$t = Title::makeTitle( $row->page_namespace, $row->page_title );
189
			if ( $row->page_is_redirect ) {
190
				$this->redirTitles[] = $t;
191
			}
192
193
			if ( is_null( $resultPageSet ) ) {
194
				$a = [ 'pageid' => intval( $row->page_id ) ];
195
				ApiQueryBase::addTitleInfo( $a, $t );
196
				if ( $row->page_is_redirect ) {
197
					$a['redirect'] = true;
198
				}
199
				// Put all the results in an array first
200
				$this->resultArr[$a['pageid']] = $a;
201
			} else {
202
				$resultPageSet->processDbRow( $row );
203
			}
204
		}
205
	}
206
207
	/**
208
	 * @param ApiPageSet $resultPageSet
209
	 * @return void
210
	 */
211
	private function runSecondQuery( $resultPageSet = null ) {
212
		$db = $this->getDB();
213
		$this->addTables( [ 'page', $this->bl_table ] );
214
		$this->addWhere( "{$this->bl_from}=page_id" );
215
216
		if ( is_null( $resultPageSet ) ) {
217
			$this->addFields( [ 'page_id', 'page_title', 'page_namespace', 'page_is_redirect' ] );
218
		} else {
219
			$this->addFields( $resultPageSet->getPageTableFields() );
220
		}
221
222
		$this->addFields( [ $this->bl_title, 'from_ns' => 'page_namespace' ] );
223
		if ( $this->hasNS ) {
224
			$this->addFields( $this->bl_ns );
225
		}
226
227
		// We can't use LinkBatch here because $this->hasNS may be false
228
		$titleWhere = [];
229
		$allRedirNs = [];
230
		$allRedirDBkey = [];
231
		/** @var $t Title */
232
		foreach ( $this->redirTitles as $t ) {
233
			$redirNs = $t->getNamespace();
234
			$redirDBkey = $t->getDBkey();
235
			$titleWhere[] = "{$this->bl_title} = " . $db->addQuotes( $redirDBkey ) .
236
				( $this->hasNS ? " AND {$this->bl_ns} = {$redirNs}" : '' );
237
			$allRedirNs[$redirNs] = true;
238
			$allRedirDBkey[$redirDBkey] = true;
239
		}
240
		$this->addWhere( $db->makeList( $titleWhere, LIST_OR ) );
241
		$this->addWhereFld( 'page_namespace', $this->params['namespace'] );
242
243
		if ( count( $this->cont ) >= 6 ) {
244
			$op = $this->params['dir'] == 'descending' ? '<' : '>';
245
246
			$where = "{$this->bl_from} $op= {$this->cont[5]}";
247
			// Don't bother with namespace, title, or from_namespace if it's
248
			// otherwise constant in the where clause.
249
			if ( count( $this->params['namespace'] ) > 1 ) {
250
				$where = "{$this->bl_from_ns} $op {$this->cont[4]} OR " .
251
					"({$this->bl_from_ns} = {$this->cont[4]} AND ($where))";
252
			}
253
			if ( count( $allRedirDBkey ) > 1 ) {
254
				$title = $db->addQuotes( $this->cont[3] );
255
				$where = "{$this->bl_title} $op $title OR " .
256
					"({$this->bl_title} = $title AND ($where))";
257
			}
258
			if ( $this->hasNS && count( $allRedirNs ) > 1 ) {
259
				$where = "{$this->bl_ns} $op {$this->cont[2]} OR " .
260
					"({$this->bl_ns} = {$this->cont[2]} AND ($where))";
261
			}
262
263
			$this->addWhere( $where );
264
		}
265 View Code Duplication
		if ( $this->params['filterredir'] == 'redirects' ) {
266
			$this->addWhereFld( 'page_is_redirect', 1 );
267
		} elseif ( $this->params['filterredir'] == 'nonredirects' ) {
268
			$this->addWhereFld( 'page_is_redirect', 0 );
269
		}
270
271
		$this->addOption( 'LIMIT', $this->params['limit'] + 1 );
272
		$orderBy = [];
273
		$sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' );
274
		// Don't order by namespace/title/from_namespace if it's constant in the WHERE clause
275
		if ( $this->hasNS && count( $allRedirNs ) > 1 ) {
276
			$orderBy[] = $this->bl_ns . $sort;
277
		}
278
		if ( count( $allRedirDBkey ) > 1 ) {
279
			$orderBy[] = $this->bl_title . $sort;
280
		}
281 View Code Duplication
		if ( count( $this->params['namespace'] ) > 1 ) {
282
			$orderBy[] = $this->bl_from_ns . $sort;
283
		}
284
		$orderBy[] = $this->bl_from . $sort;
285
		$this->addOption( 'ORDER BY', $orderBy );
286
		$this->addOption( 'USE INDEX', [ 'page' => 'PRIMARY' ] );
287
288
		$res = $this->select( __METHOD__ );
289
		$count = 0;
290
		foreach ( $res as $row ) {
291
			$ns = $this->hasNS ? $row->{$this->bl_ns} : NS_FILE;
292
293
			if ( ++$count > $this->params['limit'] ) {
294
				// We've reached the one extra which shows that there are
295
				// additional pages to be had. Stop here...
296
				// Note we must keep the parameters for the first query constant
297
				// This may be overridden at a later step
298
				$title = $row->{$this->bl_title};
299
				$this->continueStr = implode( '|', array_slice( $this->cont, 0, 2 ) ) .
300
					"|$ns|$title|{$row->from_ns}|{$row->page_id}";
301
				break;
302
			}
303
304
			// Fill in continuation fields for later steps
305
			if ( count( $this->cont ) < 6 ) {
306
				$this->cont[] = $ns;
307
				$this->cont[] = $row->{$this->bl_title};
308
				$this->cont[] = $row->from_ns;
309
				$this->cont[] = $row->page_id;
310
			}
311
312
			if ( is_null( $resultPageSet ) ) {
313
				$a['pageid'] = intval( $row->page_id );
314
				ApiQueryBase::addTitleInfo( $a, Title::makeTitle( $row->page_namespace, $row->page_title ) );
315
				if ( $row->page_is_redirect ) {
316
					$a['redirect'] = true;
317
				}
318
				$parentID = $this->pageMap[$ns][$row->{$this->bl_title}];
319
				// Put all the results in an array first
320
				$this->resultArr[$parentID]['redirlinks'][$row->page_id] = $a;
321
			} else {
322
				$resultPageSet->processDbRow( $row );
323
			}
324
		}
325
	}
326
327
	/**
328
	 * @param ApiPageSet $resultPageSet
329
	 * @return void
330
	 */
331
	private function run( $resultPageSet = null ) {
332
		$this->params = $this->extractRequestParams( false );
333
		$this->redirect = isset( $this->params['redirect'] ) && $this->params['redirect'];
334
		$userMax = ( $this->redirect ? ApiBase::LIMIT_BIG1 / 2 : ApiBase::LIMIT_BIG1 );
335
		$botMax = ( $this->redirect ? ApiBase::LIMIT_BIG2 / 2 : ApiBase::LIMIT_BIG2 );
336
337
		$result = $this->getResult();
338
339
		if ( $this->params['limit'] == 'max' ) {
340
			$this->params['limit'] = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
341
			$result->addParsedLimit( $this->getModuleName(), $this->params['limit'] );
342
		} else {
343
			$this->params['limit'] = intval( $this->params['limit'] );
344
			$this->validateLimit( 'limit', $this->params['limit'], 1, $userMax, $botMax );
345
		}
346
347
		$this->rootTitle = $this->getTitleOrPageId( $this->params )->getTitle();
348
349
		// only image titles are allowed for the root in imageinfo mode
350
		if ( !$this->hasNS && $this->rootTitle->getNamespace() !== NS_FILE ) {
351
			$this->dieUsage(
352
				"The title for {$this->getModuleName()} query must be a file",
353
				'bad_image_title'
354
			);
355
		}
356
357
		// Parse and validate continuation parameter
358
		$this->cont = [];
359
		if ( $this->params['continue'] !== null ) {
360
			$cont = explode( '|', $this->params['continue'] );
361
362
			switch ( count( $cont ) ) {
363 View Code Duplication
				case 8:
364
					// redirect page ID for result adding
365
					$this->cont[7] = (int)$cont[7];
366
					$this->dieContinueUsageIf( $cont[7] !== (string)$this->cont[7] );
367
368
					/* Fall through */
369
370 View Code Duplication
				case 7:
371
					// top-level page ID for result adding
372
					$this->cont[6] = (int)$cont[6];
373
					$this->dieContinueUsageIf( $cont[6] !== (string)$this->cont[6] );
374
375
					/* Fall through */
376
377
				case 6:
378
					// ns for 2nd query (even for imageusage)
379
					$this->cont[2] = (int)$cont[2];
380
					$this->dieContinueUsageIf( $cont[2] !== (string)$this->cont[2] );
381
382
					// title for 2nd query
383
					$this->cont[3] = $cont[3];
384
385
					// from_ns for 2nd query
386
					$this->cont[4] = (int)$cont[4];
387
					$this->dieContinueUsageIf( $cont[4] !== (string)$this->cont[4] );
388
389
					// from_id for 1st query
390
					$this->cont[5] = (int)$cont[5];
391
					$this->dieContinueUsageIf( $cont[5] !== (string)$this->cont[5] );
392
393
					/* Fall through */
394
395
				case 2:
396
					// from_ns for 1st query
397
					$this->cont[0] = (int)$cont[0];
398
					$this->dieContinueUsageIf( $cont[0] !== (string)$this->cont[0] );
399
400
					// from_id for 1st query
401
					$this->cont[1] = (int)$cont[1];
402
					$this->dieContinueUsageIf( $cont[1] !== (string)$this->cont[1] );
403
404
					break;
405
406
				default:
407
					$this->dieContinueUsageIf( true );
408
			}
409
410
			ksort( $this->cont );
411
		}
412
413
		$this->runFirstQuery( $resultPageSet );
414
		if ( $this->redirect && count( $this->redirTitles ) ) {
415
			$this->resetQueryParams();
416
			$this->runSecondQuery( $resultPageSet );
417
		}
418
419
		// Fill in any missing fields in case it's needed below
420
		$this->cont += [ 0, 0, 0, '', 0, 0, 0 ];
421
422
		if ( is_null( $resultPageSet ) ) {
423
			// Try to add the result data in one go and pray that it fits
424
			$code = $this->bl_code;
425
			$data = array_map( function ( $arr ) use ( $result, $code ) {
426
				if ( isset( $arr['redirlinks'] ) ) {
427
					$arr['redirlinks'] = array_values( $arr['redirlinks'] );
428
					ApiResult::setIndexedTagName( $arr['redirlinks'], $code );
429
				}
430
				return $arr;
431
			}, array_values( $this->resultArr ) );
432
			$fit = $result->addValue( 'query', $this->getModuleName(), $data );
433
			if ( !$fit ) {
434
				// It didn't fit. Add elements one by one until the
435
				// result is full.
436
				ksort( $this->resultArr );
437
				if ( count( $this->cont ) >= 7 ) {
438
					$startAt = $this->cont[6];
439
				} else {
440
					reset( $this->resultArr );
441
					$startAt = key( $this->resultArr );
442
				}
443
				$idx = 0;
444
				foreach ( $this->resultArr as $pageID => $arr ) {
445
					if ( $pageID < $startAt ) {
446
						continue;
447
					}
448
449
					// Add the basic entry without redirlinks first
450
					$fit = $result->addValue(
451
						[ 'query', $this->getModuleName() ],
452
						$idx, array_diff_key( $arr, [ 'redirlinks' => '' ] ) );
453 View Code Duplication
					if ( !$fit ) {
454
						$this->continueStr = implode( '|', array_slice( $this->cont, 0, 6 ) ) .
455
							"|$pageID";
456
						break;
457
					}
458
459
					$hasRedirs = false;
460
					$redirLinks = isset( $arr['redirlinks'] ) ? (array)$arr['redirlinks'] : [];
461
					ksort( $redirLinks );
462
					if ( count( $this->cont ) >= 8 && $pageID == $startAt ) {
463
						$redirStartAt = $this->cont[7];
464
					} else {
465
						reset( $redirLinks );
466
						$redirStartAt = key( $redirLinks );
467
					}
468
					foreach ( $redirLinks as $key => $redir ) {
469
						if ( $key < $redirStartAt ) {
470
							continue;
471
						}
472
473
						$fit = $result->addValue(
474
							[ 'query', $this->getModuleName(), $idx, 'redirlinks' ],
475
							null, $redir );
476 View Code Duplication
						if ( !$fit ) {
477
							$this->continueStr = implode( '|', array_slice( $this->cont, 0, 6 ) ) .
478
								"|$pageID|$key";
479
							break;
480
						}
481
						$hasRedirs = true;
482
					}
483
					if ( $hasRedirs ) {
484
						$result->addIndexedTagName(
485
							[ 'query', $this->getModuleName(), $idx, 'redirlinks' ],
486
							$this->bl_code );
487
					}
488
					if ( !$fit ) {
489
						break;
490
					}
491
492
					$idx++;
493
				}
494
			}
495
496
			$result->addIndexedTagName(
497
				[ 'query', $this->getModuleName() ],
498
				$this->bl_code
499
			);
500
		}
501
		if ( !is_null( $this->continueStr ) ) {
502
			$this->setContinueEnumParameter( 'continue', $this->continueStr );
503
		}
504
	}
505
506
	public function getAllowedParams() {
507
		$retval = [
508
			'title' => [
509
				ApiBase::PARAM_TYPE => 'string',
510
			],
511
			'pageid' => [
512
				ApiBase::PARAM_TYPE => 'integer',
513
			],
514
			'continue' => [
515
				ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
516
			],
517
			'namespace' => [
518
				ApiBase::PARAM_ISMULTI => true,
519
				ApiBase::PARAM_TYPE => 'namespace'
520
			],
521
			'dir' => [
522
				ApiBase::PARAM_DFLT => 'ascending',
523
				ApiBase::PARAM_TYPE => [
524
					'ascending',
525
					'descending'
526
				]
527
			],
528
			'filterredir' => [
529
				ApiBase::PARAM_DFLT => 'all',
530
				ApiBase::PARAM_TYPE => [
531
					'all',
532
					'redirects',
533
					'nonredirects'
534
				]
535
			],
536
			'limit' => [
537
				ApiBase::PARAM_DFLT => 10,
538
				ApiBase::PARAM_TYPE => 'limit',
539
				ApiBase::PARAM_MIN => 1,
540
				ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
541
				ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
542
			]
543
		];
544
		if ( $this->getModuleName() == 'embeddedin' ) {
545
			return $retval;
546
		}
547
		$retval['redirect'] = false;
548
549
		return $retval;
550
	}
551
552
	protected function getExamplesMessages() {
553
		static $examples = [
554
			'backlinks' => [
555
				'action=query&list=backlinks&bltitle=Main%20Page'
556
					=> 'apihelp-query+backlinks-example-simple',
557
				'action=query&generator=backlinks&gbltitle=Main%20Page&prop=info'
558
					=> 'apihelp-query+backlinks-example-generator',
559
			],
560
			'embeddedin' => [
561
				'action=query&list=embeddedin&eititle=Template:Stub'
562
					=> 'apihelp-query+embeddedin-example-simple',
563
				'action=query&generator=embeddedin&geititle=Template:Stub&prop=info'
564
					=> 'apihelp-query+embeddedin-example-generator',
565
			],
566
			'imageusage' => [
567
				'action=query&list=imageusage&iutitle=File:Albert%20Einstein%20Head.jpg'
568
					=> 'apihelp-query+imageusage-example-simple',
569
				'action=query&generator=imageusage&giutitle=File:Albert%20Einstein%20Head.jpg&prop=info'
570
					=> 'apihelp-query+imageusage-example-generator',
571
			]
572
		];
573
574
		return $examples[$this->getModuleName()];
575
	}
576
577
	public function getHelpUrls() {
578
		return $this->helpUrl;
579
	}
580
}
581