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

includes/api/ApiQueryInfo.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 Sep 25, 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
use MediaWiki\MediaWikiServices;
27
use MediaWiki\Linker\LinkTarget;
28
29
/**
30
 * A query module to show basic page information.
31
 *
32
 * @ingroup API
33
 */
34
class ApiQueryInfo extends ApiQueryBase {
35
36
	private $fld_protection = false, $fld_talkid = false,
37
		$fld_subjectid = false, $fld_url = false,
38
		$fld_readable = false, $fld_watched = false,
39
		$fld_watchers = false, $fld_visitingwatchers = false,
40
		$fld_notificationtimestamp = false,
41
		$fld_preload = false, $fld_displaytitle = false;
42
43
	private $params;
44
45
	/** @var Title[] */
46
	private $titles;
47
	/** @var Title[] */
48
	private $missing;
49
	/** @var Title[] */
50
	private $everything;
51
52
	private $pageRestrictions, $pageIsRedir, $pageIsNew, $pageTouched,
53
		$pageLatest, $pageLength;
54
55
	private $protections, $restrictionTypes, $watched, $watchers, $visitingwatchers,
56
		$notificationtimestamps, $talkids, $subjectids, $displaytitles;
57
	private $showZeroWatchers = false;
58
59
	private $tokenFunctions;
60
61
	private $countTestedActions = 0;
62
63
	public function __construct( ApiQuery $query, $moduleName ) {
64
		parent::__construct( $query, $moduleName, 'in' );
65
	}
66
67
	/**
68
	 * @param ApiPageSet $pageSet
69
	 * @return void
70
	 */
71
	public function requestExtraData( $pageSet ) {
72
		$pageSet->requestField( 'page_restrictions' );
73
		// If the pageset is resolving redirects we won't get page_is_redirect.
74
		// But we can't know for sure until the pageset is executed (revids may
75
		// turn it off), so request it unconditionally.
76
		$pageSet->requestField( 'page_is_redirect' );
77
		$pageSet->requestField( 'page_is_new' );
78
		$config = $this->getConfig();
79
		$pageSet->requestField( 'page_touched' );
80
		$pageSet->requestField( 'page_latest' );
81
		$pageSet->requestField( 'page_len' );
82
		if ( $config->get( 'ContentHandlerUseDB' ) ) {
83
			$pageSet->requestField( 'page_content_model' );
84
		}
85
		if ( $config->get( 'PageLanguageUseDB' ) ) {
86
			$pageSet->requestField( 'page_lang' );
87
		}
88
	}
89
90
	/**
91
	 * Get an array mapping token names to their handler functions.
92
	 * The prototype for a token function is func($pageid, $title)
93
	 * it should return a token or false (permission denied)
94
	 * @deprecated since 1.24
95
	 * @return array [ tokenname => function ]
96
	 */
97
	protected function getTokenFunctions() {
98
		// Don't call the hooks twice
99
		if ( isset( $this->tokenFunctions ) ) {
100
			return $this->tokenFunctions;
101
		}
102
103
		// If we're in a mode that breaks the same-origin policy, no tokens can
104
		// be obtained
105
		if ( $this->lacksSameOriginSecurity() ) {
106
			return [];
107
		}
108
109
		$this->tokenFunctions = [
110
			'edit' => [ 'ApiQueryInfo', 'getEditToken' ],
111
			'delete' => [ 'ApiQueryInfo', 'getDeleteToken' ],
112
			'protect' => [ 'ApiQueryInfo', 'getProtectToken' ],
113
			'move' => [ 'ApiQueryInfo', 'getMoveToken' ],
114
			'block' => [ 'ApiQueryInfo', 'getBlockToken' ],
115
			'unblock' => [ 'ApiQueryInfo', 'getUnblockToken' ],
116
			'email' => [ 'ApiQueryInfo', 'getEmailToken' ],
117
			'import' => [ 'ApiQueryInfo', 'getImportToken' ],
118
			'watch' => [ 'ApiQueryInfo', 'getWatchToken' ],
119
		];
120
		Hooks::run( 'APIQueryInfoTokens', [ &$this->tokenFunctions ] );
121
122
		return $this->tokenFunctions;
123
	}
124
125
	static protected $cachedTokens = [];
126
127
	/**
128
	 * @deprecated since 1.24
129
	 */
130
	public static function resetTokenCache() {
131
		ApiQueryInfo::$cachedTokens = [];
132
	}
133
134
	/**
135
	 * @deprecated since 1.24
136
	 */
137 View Code Duplication
	public static function getEditToken( $pageid, $title ) {
138
		// We could check for $title->userCan('edit') here,
139
		// but that's too expensive for this purpose
140
		// and would break caching
141
		global $wgUser;
142
		if ( !$wgUser->isAllowed( 'edit' ) ) {
143
			return false;
144
		}
145
146
		// The token is always the same, let's exploit that
147
		if ( !isset( ApiQueryInfo::$cachedTokens['edit'] ) ) {
148
			ApiQueryInfo::$cachedTokens['edit'] = $wgUser->getEditToken();
149
		}
150
151
		return ApiQueryInfo::$cachedTokens['edit'];
152
	}
153
154
	/**
155
	 * @deprecated since 1.24
156
	 */
157 View Code Duplication
	public static function getDeleteToken( $pageid, $title ) {
158
		global $wgUser;
159
		if ( !$wgUser->isAllowed( 'delete' ) ) {
160
			return false;
161
		}
162
163
		// The token is always the same, let's exploit that
164
		if ( !isset( ApiQueryInfo::$cachedTokens['delete'] ) ) {
165
			ApiQueryInfo::$cachedTokens['delete'] = $wgUser->getEditToken();
166
		}
167
168
		return ApiQueryInfo::$cachedTokens['delete'];
169
	}
170
171
	/**
172
	 * @deprecated since 1.24
173
	 */
174 View Code Duplication
	public static function getProtectToken( $pageid, $title ) {
175
		global $wgUser;
176
		if ( !$wgUser->isAllowed( 'protect' ) ) {
177
			return false;
178
		}
179
180
		// The token is always the same, let's exploit that
181
		if ( !isset( ApiQueryInfo::$cachedTokens['protect'] ) ) {
182
			ApiQueryInfo::$cachedTokens['protect'] = $wgUser->getEditToken();
183
		}
184
185
		return ApiQueryInfo::$cachedTokens['protect'];
186
	}
187
188
	/**
189
	 * @deprecated since 1.24
190
	 */
191 View Code Duplication
	public static function getMoveToken( $pageid, $title ) {
192
		global $wgUser;
193
		if ( !$wgUser->isAllowed( 'move' ) ) {
194
			return false;
195
		}
196
197
		// The token is always the same, let's exploit that
198
		if ( !isset( ApiQueryInfo::$cachedTokens['move'] ) ) {
199
			ApiQueryInfo::$cachedTokens['move'] = $wgUser->getEditToken();
200
		}
201
202
		return ApiQueryInfo::$cachedTokens['move'];
203
	}
204
205
	/**
206
	 * @deprecated since 1.24
207
	 */
208 View Code Duplication
	public static function getBlockToken( $pageid, $title ) {
209
		global $wgUser;
210
		if ( !$wgUser->isAllowed( 'block' ) ) {
211
			return false;
212
		}
213
214
		// The token is always the same, let's exploit that
215
		if ( !isset( ApiQueryInfo::$cachedTokens['block'] ) ) {
216
			ApiQueryInfo::$cachedTokens['block'] = $wgUser->getEditToken();
217
		}
218
219
		return ApiQueryInfo::$cachedTokens['block'];
220
	}
221
222
	/**
223
	 * @deprecated since 1.24
224
	 */
225
	public static function getUnblockToken( $pageid, $title ) {
226
		// Currently, this is exactly the same as the block token
227
		return self::getBlockToken( $pageid, $title );
228
	}
229
230
	/**
231
	 * @deprecated since 1.24
232
	 */
233 View Code Duplication
	public static function getEmailToken( $pageid, $title ) {
234
		global $wgUser;
235
		if ( !$wgUser->canSendEmail() || $wgUser->isBlockedFromEmailuser() ) {
236
			return false;
237
		}
238
239
		// The token is always the same, let's exploit that
240
		if ( !isset( ApiQueryInfo::$cachedTokens['email'] ) ) {
241
			ApiQueryInfo::$cachedTokens['email'] = $wgUser->getEditToken();
242
		}
243
244
		return ApiQueryInfo::$cachedTokens['email'];
245
	}
246
247
	/**
248
	 * @deprecated since 1.24
249
	 */
250 View Code Duplication
	public static function getImportToken( $pageid, $title ) {
251
		global $wgUser;
252
		if ( !$wgUser->isAllowedAny( 'import', 'importupload' ) ) {
253
			return false;
254
		}
255
256
		// The token is always the same, let's exploit that
257
		if ( !isset( ApiQueryInfo::$cachedTokens['import'] ) ) {
258
			ApiQueryInfo::$cachedTokens['import'] = $wgUser->getEditToken();
259
		}
260
261
		return ApiQueryInfo::$cachedTokens['import'];
262
	}
263
264
	/**
265
	 * @deprecated since 1.24
266
	 */
267 View Code Duplication
	public static function getWatchToken( $pageid, $title ) {
268
		global $wgUser;
269
		if ( !$wgUser->isLoggedIn() ) {
270
			return false;
271
		}
272
273
		// The token is always the same, let's exploit that
274
		if ( !isset( ApiQueryInfo::$cachedTokens['watch'] ) ) {
275
			ApiQueryInfo::$cachedTokens['watch'] = $wgUser->getEditToken( 'watch' );
276
		}
277
278
		return ApiQueryInfo::$cachedTokens['watch'];
279
	}
280
281
	/**
282
	 * @deprecated since 1.24
283
	 */
284 View Code Duplication
	public static function getOptionsToken( $pageid, $title ) {
285
		global $wgUser;
286
		if ( !$wgUser->isLoggedIn() ) {
287
			return false;
288
		}
289
290
		// The token is always the same, let's exploit that
291
		if ( !isset( ApiQueryInfo::$cachedTokens['options'] ) ) {
292
			ApiQueryInfo::$cachedTokens['options'] = $wgUser->getEditToken();
293
		}
294
295
		return ApiQueryInfo::$cachedTokens['options'];
296
	}
297
298
	public function execute() {
299
		$this->params = $this->extractRequestParams();
300
		if ( !is_null( $this->params['prop'] ) ) {
301
			$prop = array_flip( $this->params['prop'] );
302
			$this->fld_protection = isset( $prop['protection'] );
303
			$this->fld_watched = isset( $prop['watched'] );
304
			$this->fld_watchers = isset( $prop['watchers'] );
305
			$this->fld_visitingwatchers = isset( $prop['visitingwatchers'] );
306
			$this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
307
			$this->fld_talkid = isset( $prop['talkid'] );
308
			$this->fld_subjectid = isset( $prop['subjectid'] );
309
			$this->fld_url = isset( $prop['url'] );
310
			$this->fld_readable = isset( $prop['readable'] );
311
			$this->fld_preload = isset( $prop['preload'] );
312
			$this->fld_displaytitle = isset( $prop['displaytitle'] );
313
		}
314
315
		$pageSet = $this->getPageSet();
316
		$this->titles = $pageSet->getGoodTitles();
317
		$this->missing = $pageSet->getMissingTitles();
318
		$this->everything = $this->titles + $this->missing;
319
		$result = $this->getResult();
320
321
		uasort( $this->everything, [ 'Title', 'compare' ] );
322
		if ( !is_null( $this->params['continue'] ) ) {
323
			// Throw away any titles we're gonna skip so they don't
324
			// clutter queries
325
			$cont = explode( '|', $this->params['continue'] );
326
			$this->dieContinueUsageIf( count( $cont ) != 2 );
327
			$conttitle = Title::makeTitleSafe( $cont[0], $cont[1] );
328
			foreach ( $this->everything as $pageid => $title ) {
329
				if ( Title::compare( $title, $conttitle ) >= 0 ) {
0 ignored issues
show
It seems like $conttitle defined by \Title::makeTitleSafe($cont[0], $cont[1]) on line 327 can be null; however, Title::compare() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
330
					break;
331
				}
332
				unset( $this->titles[$pageid] );
333
				unset( $this->missing[$pageid] );
334
				unset( $this->everything[$pageid] );
335
			}
336
		}
337
338
		$this->pageRestrictions = $pageSet->getCustomField( 'page_restrictions' );
339
		// when resolving redirects, no page will have this field
340
		$this->pageIsRedir = !$pageSet->isResolvingRedirects()
341
			? $pageSet->getCustomField( 'page_is_redirect' )
342
			: [];
343
		$this->pageIsNew = $pageSet->getCustomField( 'page_is_new' );
344
345
		$this->pageTouched = $pageSet->getCustomField( 'page_touched' );
346
		$this->pageLatest = $pageSet->getCustomField( 'page_latest' );
347
		$this->pageLength = $pageSet->getCustomField( 'page_len' );
348
349
		// Get protection info if requested
350
		if ( $this->fld_protection ) {
351
			$this->getProtectionInfo();
352
		}
353
354
		if ( $this->fld_watched || $this->fld_notificationtimestamp ) {
355
			$this->getWatchedInfo();
356
		}
357
358
		if ( $this->fld_watchers ) {
359
			$this->getWatcherInfo();
360
		}
361
362
		if ( $this->fld_visitingwatchers ) {
363
			$this->getVisitingWatcherInfo();
364
		}
365
366
		// Run the talkid/subjectid query if requested
367
		if ( $this->fld_talkid || $this->fld_subjectid ) {
368
			$this->getTSIDs();
369
		}
370
371
		if ( $this->fld_displaytitle ) {
372
			$this->getDisplayTitle();
373
		}
374
375
		/** @var $title Title */
376
		foreach ( $this->everything as $pageid => $title ) {
377
			$pageInfo = $this->extractPageInfo( $pageid, $title );
378
			$fit = $pageInfo !== null && $result->addValue( [
379
				'query',
380
				'pages'
381
			], $pageid, $pageInfo );
382
			if ( !$fit ) {
383
				$this->setContinueEnumParameter( 'continue',
384
					$title->getNamespace() . '|' .
385
					$title->getText() );
386
				break;
387
			}
388
		}
389
	}
390
391
	/**
392
	 * Get a result array with information about a title
393
	 * @param int $pageid Page ID (negative for missing titles)
394
	 * @param Title $title
395
	 * @return array|null
396
	 */
397
	private function extractPageInfo( $pageid, $title ) {
398
		$pageInfo = [];
399
		// $title->exists() needs pageid, which is not set for all title objects
400
		$titleExists = $pageid > 0;
401
		$ns = $title->getNamespace();
402
		$dbkey = $title->getDBkey();
403
404
		$pageInfo['contentmodel'] = $title->getContentModel();
405
406
		$pageLanguage = $title->getPageLanguage();
407
		$pageInfo['pagelanguage'] = $pageLanguage->getCode();
408
		$pageInfo['pagelanguagehtmlcode'] = $pageLanguage->getHtmlCode();
409
		$pageInfo['pagelanguagedir'] = $pageLanguage->getDir();
410
411
		if ( $titleExists ) {
412
			$pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] );
413
			$pageInfo['lastrevid'] = intval( $this->pageLatest[$pageid] );
414
			$pageInfo['length'] = intval( $this->pageLength[$pageid] );
415
416
			if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) {
417
				$pageInfo['redirect'] = true;
418
			}
419
			if ( $this->pageIsNew[$pageid] ) {
420
				$pageInfo['new'] = true;
421
			}
422
		}
423
424
		if ( !is_null( $this->params['token'] ) ) {
425
			$tokenFunctions = $this->getTokenFunctions();
426
			$pageInfo['starttimestamp'] = wfTimestamp( TS_ISO_8601, time() );
427
			foreach ( $this->params['token'] as $t ) {
428
				$val = call_user_func( $tokenFunctions[$t], $pageid, $title );
429
				if ( $val === false ) {
430
					$this->setWarning( "Action '$t' is not allowed for the current user" );
431
				} else {
432
					$pageInfo[$t . 'token'] = $val;
433
				}
434
			}
435
		}
436
437
		if ( $this->fld_protection ) {
438
			$pageInfo['protection'] = [];
439
			if ( isset( $this->protections[$ns][$dbkey] ) ) {
440
				$pageInfo['protection'] =
441
					$this->protections[$ns][$dbkey];
442
			}
443
			ApiResult::setIndexedTagName( $pageInfo['protection'], 'pr' );
444
445
			$pageInfo['restrictiontypes'] = [];
446
			if ( isset( $this->restrictionTypes[$ns][$dbkey] ) ) {
447
				$pageInfo['restrictiontypes'] =
448
					$this->restrictionTypes[$ns][$dbkey];
449
			}
450
			ApiResult::setIndexedTagName( $pageInfo['restrictiontypes'], 'rt' );
451
		}
452
453
		if ( $this->fld_watched && $this->watched !== null ) {
454
			$pageInfo['watched'] = $this->watched[$ns][$dbkey];
455
		}
456
457 View Code Duplication
		if ( $this->fld_watchers ) {
458
			if ( $this->watchers !== null && $this->watchers[$ns][$dbkey] !== 0 ) {
459
				$pageInfo['watchers'] = $this->watchers[$ns][$dbkey];
460
			} elseif ( $this->showZeroWatchers ) {
461
				$pageInfo['watchers'] = 0;
462
			}
463
		}
464
465 View Code Duplication
		if ( $this->fld_visitingwatchers ) {
466
			if ( $this->visitingwatchers !== null && $this->visitingwatchers[$ns][$dbkey] !== 0 ) {
467
				$pageInfo['visitingwatchers'] = $this->visitingwatchers[$ns][$dbkey];
468
			} elseif ( $this->showZeroWatchers ) {
469
				$pageInfo['visitingwatchers'] = 0;
470
			}
471
		}
472
473
		if ( $this->fld_notificationtimestamp ) {
474
			$pageInfo['notificationtimestamp'] = '';
475
			if ( $this->notificationtimestamps[$ns][$dbkey] ) {
476
				$pageInfo['notificationtimestamp'] =
477
					wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] );
478
			}
479
		}
480
481 View Code Duplication
		if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) {
482
			$pageInfo['talkid'] = $this->talkids[$ns][$dbkey];
483
		}
484
485 View Code Duplication
		if ( $this->fld_subjectid && isset( $this->subjectids[$ns][$dbkey] ) ) {
486
			$pageInfo['subjectid'] = $this->subjectids[$ns][$dbkey];
487
		}
488
489
		if ( $this->fld_url ) {
490
			$pageInfo['fullurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
491
			$pageInfo['editurl'] = wfExpandUrl( $title->getFullURL( 'action=edit' ), PROTO_CURRENT );
492
			$pageInfo['canonicalurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CANONICAL );
493
		}
494
		if ( $this->fld_readable ) {
495
			$pageInfo['readable'] = $title->userCan( 'read', $this->getUser() );
496
		}
497
498
		if ( $this->fld_preload ) {
499
			if ( $titleExists ) {
500
				$pageInfo['preload'] = '';
501
			} else {
502
				$text = null;
503
				Hooks::run( 'EditFormPreloadText', [ &$text, &$title ] );
504
505
				$pageInfo['preload'] = $text;
506
			}
507
		}
508
509
		if ( $this->fld_displaytitle ) {
510
			if ( isset( $this->displaytitles[$pageid] ) ) {
511
				$pageInfo['displaytitle'] = $this->displaytitles[$pageid];
512
			} else {
513
				$pageInfo['displaytitle'] = $title->getPrefixedText();
514
			}
515
		}
516
517
		if ( $this->params['testactions'] ) {
518
			$limit = $this->getMain()->canApiHighLimits() ? self::LIMIT_SML1 : self::LIMIT_SML2;
519
			if ( $this->countTestedActions >= $limit ) {
520
				return null; // force a continuation
521
			}
522
523
			$user = $this->getUser();
524
			$pageInfo['actions'] = [];
525
			foreach ( $this->params['testactions'] as $action ) {
526
				$this->countTestedActions++;
527
				$pageInfo['actions'][$action] = $title->userCan( $action, $user );
528
			}
529
		}
530
531
		return $pageInfo;
532
	}
533
534
	/**
535
	 * Get information about protections and put it in $protections
536
	 */
537
	private function getProtectionInfo() {
538
		global $wgContLang;
539
		$this->protections = [];
540
		$db = $this->getDB();
541
542
		// Get normal protections for existing titles
543
		if ( count( $this->titles ) ) {
544
			$this->resetQueryParams();
545
			$this->addTables( 'page_restrictions' );
546
			$this->addFields( [ 'pr_page', 'pr_type', 'pr_level',
547
				'pr_expiry', 'pr_cascade' ] );
548
			$this->addWhereFld( 'pr_page', array_keys( $this->titles ) );
549
550
			$res = $this->select( __METHOD__ );
551
			foreach ( $res as $row ) {
552
				/** @var $title Title */
553
				$title = $this->titles[$row->pr_page];
554
				$a = [
555
					'type' => $row->pr_type,
556
					'level' => $row->pr_level,
557
					'expiry' => $wgContLang->formatExpiry( $row->pr_expiry, TS_ISO_8601 )
558
				];
559
				if ( $row->pr_cascade ) {
560
					$a['cascade'] = true;
561
				}
562
				$this->protections[$title->getNamespace()][$title->getDBkey()][] = $a;
563
			}
564
			// Also check old restrictions
565
			foreach ( $this->titles as $pageId => $title ) {
566
				if ( $this->pageRestrictions[$pageId] ) {
567
					$namespace = $title->getNamespace();
568
					$dbKey = $title->getDBkey();
569
					$restrictions = explode( ':', trim( $this->pageRestrictions[$pageId] ) );
570
					foreach ( $restrictions as $restrict ) {
571
						$temp = explode( '=', trim( $restrict ) );
572
						if ( count( $temp ) == 1 ) {
573
							// old old format should be treated as edit/move restriction
574
							$restriction = trim( $temp[0] );
575
576
							if ( $restriction == '' ) {
577
								continue;
578
							}
579
							$this->protections[$namespace][$dbKey][] = [
580
								'type' => 'edit',
581
								'level' => $restriction,
582
								'expiry' => 'infinity',
583
							];
584
							$this->protections[$namespace][$dbKey][] = [
585
								'type' => 'move',
586
								'level' => $restriction,
587
								'expiry' => 'infinity',
588
							];
589
						} else {
590
							$restriction = trim( $temp[1] );
591
							if ( $restriction == '' ) {
592
								continue;
593
							}
594
							$this->protections[$namespace][$dbKey][] = [
595
								'type' => $temp[0],
596
								'level' => $restriction,
597
								'expiry' => 'infinity',
598
							];
599
						}
600
					}
601
				}
602
			}
603
		}
604
605
		// Get protections for missing titles
606
		if ( count( $this->missing ) ) {
607
			$this->resetQueryParams();
608
			$lb = new LinkBatch( $this->missing );
609
			$this->addTables( 'protected_titles' );
610
			$this->addFields( [ 'pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry' ] );
611
			$this->addWhere( $lb->constructSet( 'pt', $db ) );
612
			$res = $this->select( __METHOD__ );
613
			foreach ( $res as $row ) {
614
				$this->protections[$row->pt_namespace][$row->pt_title][] = [
615
					'type' => 'create',
616
					'level' => $row->pt_create_perm,
617
					'expiry' => $wgContLang->formatExpiry( $row->pt_expiry, TS_ISO_8601 )
618
				];
619
			}
620
		}
621
622
		// Separate good and missing titles into files and other pages
623
		// and populate $this->restrictionTypes
624
		$images = $others = [];
625
		foreach ( $this->everything as $title ) {
626
			if ( $title->getNamespace() == NS_FILE ) {
627
				$images[] = $title->getDBkey();
628
			} else {
629
				$others[] = $title;
630
			}
631
			// Applicable protection types
632
			$this->restrictionTypes[$title->getNamespace()][$title->getDBkey()] =
633
				array_values( $title->getRestrictionTypes() );
634
		}
635
636
		if ( count( $others ) ) {
637
			// Non-images: check templatelinks
638
			$lb = new LinkBatch( $others );
639
			$this->resetQueryParams();
640
			$this->addTables( [ 'page_restrictions', 'page', 'templatelinks' ] );
641
			$this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
642
				'page_title', 'page_namespace',
643
				'tl_title', 'tl_namespace' ] );
644
			$this->addWhere( $lb->constructSet( 'tl', $db ) );
645
			$this->addWhere( 'pr_page = page_id' );
646
			$this->addWhere( 'pr_page = tl_from' );
647
			$this->addWhereFld( 'pr_cascade', 1 );
648
649
			$res = $this->select( __METHOD__ );
650 View Code Duplication
			foreach ( $res as $row ) {
651
				$source = Title::makeTitle( $row->page_namespace, $row->page_title );
652
				$this->protections[$row->tl_namespace][$row->tl_title][] = [
653
					'type' => $row->pr_type,
654
					'level' => $row->pr_level,
655
					'expiry' => $wgContLang->formatExpiry( $row->pr_expiry, TS_ISO_8601 ),
656
					'source' => $source->getPrefixedText()
657
				];
658
			}
659
		}
660
661
		if ( count( $images ) ) {
662
			// Images: check imagelinks
663
			$this->resetQueryParams();
664
			$this->addTables( [ 'page_restrictions', 'page', 'imagelinks' ] );
665
			$this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
666
				'page_title', 'page_namespace', 'il_to' ] );
667
			$this->addWhere( 'pr_page = page_id' );
668
			$this->addWhere( 'pr_page = il_from' );
669
			$this->addWhereFld( 'pr_cascade', 1 );
670
			$this->addWhereFld( 'il_to', $images );
671
672
			$res = $this->select( __METHOD__ );
673 View Code Duplication
			foreach ( $res as $row ) {
674
				$source = Title::makeTitle( $row->page_namespace, $row->page_title );
675
				$this->protections[NS_FILE][$row->il_to][] = [
676
					'type' => $row->pr_type,
677
					'level' => $row->pr_level,
678
					'expiry' => $wgContLang->formatExpiry( $row->pr_expiry, TS_ISO_8601 ),
679
					'source' => $source->getPrefixedText()
680
				];
681
			}
682
		}
683
	}
684
685
	/**
686
	 * Get talk page IDs (if requested) and subject page IDs (if requested)
687
	 * and put them in $talkids and $subjectids
688
	 */
689
	private function getTSIDs() {
690
		$getTitles = $this->talkids = $this->subjectids = [];
691
692
		/** @var $t Title */
693
		foreach ( $this->everything as $t ) {
694
			if ( MWNamespace::isTalk( $t->getNamespace() ) ) {
695
				if ( $this->fld_subjectid ) {
696
					$getTitles[] = $t->getSubjectPage();
697
				}
698
			} elseif ( $this->fld_talkid ) {
699
				$getTitles[] = $t->getTalkPage();
700
			}
701
		}
702
		if ( !count( $getTitles ) ) {
703
			return;
704
		}
705
706
		$db = $this->getDB();
707
708
		// Construct a custom WHERE clause that matches
709
		// all titles in $getTitles
710
		$lb = new LinkBatch( $getTitles );
711
		$this->resetQueryParams();
712
		$this->addTables( 'page' );
713
		$this->addFields( [ 'page_title', 'page_namespace', 'page_id' ] );
714
		$this->addWhere( $lb->constructSet( 'page', $db ) );
715
		$res = $this->select( __METHOD__ );
716
		foreach ( $res as $row ) {
717
			if ( MWNamespace::isTalk( $row->page_namespace ) ) {
718
				$this->talkids[MWNamespace::getSubject( $row->page_namespace )][$row->page_title] =
719
					intval( $row->page_id );
720
			} else {
721
				$this->subjectids[MWNamespace::getTalk( $row->page_namespace )][$row->page_title] =
722
					intval( $row->page_id );
723
			}
724
		}
725
	}
726
727
	private function getDisplayTitle() {
728
		$this->displaytitles = [];
729
730
		$pageIds = array_keys( $this->titles );
731
732
		if ( !count( $pageIds ) ) {
733
			return;
734
		}
735
736
		$this->resetQueryParams();
737
		$this->addTables( 'page_props' );
738
		$this->addFields( [ 'pp_page', 'pp_value' ] );
739
		$this->addWhereFld( 'pp_page', $pageIds );
740
		$this->addWhereFld( 'pp_propname', 'displaytitle' );
741
		$res = $this->select( __METHOD__ );
742
743
		foreach ( $res as $row ) {
744
			$this->displaytitles[$row->pp_page] = $row->pp_value;
745
		}
746
	}
747
748
	/**
749
	 * Get information about watched status and put it in $this->watched
750
	 * and $this->notificationtimestamps
751
	 */
752
	private function getWatchedInfo() {
753
		$user = $this->getUser();
754
755
		if ( $user->isAnon() || count( $this->everything ) == 0
756
			|| !$user->isAllowed( 'viewmywatchlist' )
757
		) {
758
			return;
759
		}
760
761
		$this->watched = [];
762
		$this->notificationtimestamps = [];
763
764
		$store = MediaWikiServices::getInstance()->getWatchedItemStore();
765
		$timestamps = $store->getNotificationTimestampsBatch( $user, $this->everything );
766
767
		if ( $this->fld_watched ) {
768
			foreach ( $timestamps as $namespaceId => $dbKeys ) {
769
				$this->watched[$namespaceId] = array_map(
770
					function( $x ) {
771
						return $x !== false;
772
					},
773
					$dbKeys
774
				);
775
			}
776
		}
777
		if ( $this->fld_notificationtimestamp ) {
778
			$this->notificationtimestamps = $timestamps;
779
		}
780
	}
781
782
	/**
783
	 * Get the count of watchers and put it in $this->watchers
784
	 */
785
	private function getWatcherInfo() {
786
		if ( count( $this->everything ) == 0 ) {
787
			return;
788
		}
789
790
		$user = $this->getUser();
791
		$canUnwatchedpages = $user->isAllowed( 'unwatchedpages' );
792
		$unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' );
793
		if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
794
			return;
795
		}
796
797
		$this->showZeroWatchers = $canUnwatchedpages;
798
799
		$countOptions = [];
800
		if ( !$canUnwatchedpages ) {
801
			$countOptions['minimumWatchers'] = $unwatchedPageThreshold;
802
		}
803
804
		$this->watchers = MediaWikiServices::getInstance()->getWatchedItemStore()->countWatchersMultiple(
805
			$this->everything,
806
			$countOptions
807
		);
808
	}
809
810
	/**
811
	 * Get the count of watchers who have visited recent edits and put it in
812
	 * $this->visitingwatchers
813
	 *
814
	 * Based on InfoAction::pageCounts
815
	 */
816
	private function getVisitingWatcherInfo() {
817
		$config = $this->getConfig();
818
		$user = $this->getUser();
819
		$db = $this->getDB();
820
821
		$canUnwatchedpages = $user->isAllowed( 'unwatchedpages' );
822
		$unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' );
823
		if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
824
			return;
825
		}
826
827
		$this->showZeroWatchers = $canUnwatchedpages;
828
829
		$titlesWithThresholds = [];
830
		if ( $this->titles ) {
831
			$lb = new LinkBatch( $this->titles );
832
833
			// Fetch last edit timestamps for pages
834
			$this->resetQueryParams();
835
			$this->addTables( [ 'page', 'revision' ] );
836
			$this->addFields( [ 'page_namespace', 'page_title', 'rev_timestamp' ] );
837
			$this->addWhere( [
838
				'page_latest = rev_id',
839
				$lb->constructSet( 'page', $db ),
840
			] );
841
			$this->addOption( 'GROUP BY', [ 'page_namespace', 'page_title' ] );
842
			$timestampRes = $this->select( __METHOD__ );
843
844
			$age = $config->get( 'WatchersMaxAge' );
845
			$timestamps = [];
846
			foreach ( $timestampRes as $row ) {
847
				$revTimestamp = wfTimestamp( TS_UNIX, (int)$row->rev_timestamp );
848
				$timestamps[$row->page_namespace][$row->page_title] = $revTimestamp - $age;
849
			}
850
			$titlesWithThresholds = array_map(
851
				function( LinkTarget $target ) use ( $timestamps ) {
852
					return [
853
						$target, $timestamps[$target->getNamespace()][$target->getDBkey()]
854
					];
855
				},
856
				$this->titles
857
			);
858
		}
859
860
		if ( $this->missing ) {
861
			$titlesWithThresholds = array_merge(
862
				$titlesWithThresholds,
863
				array_map(
864
					function( LinkTarget $target ) {
865
						return [ $target, null ];
866
					},
867
					$this->missing
868
				)
869
			);
870
		}
871
		$store = MediaWikiServices::getInstance()->getWatchedItemStore();
872
		$this->visitingwatchers = $store->countVisitingWatchersMultiple(
873
			$titlesWithThresholds,
874
			!$canUnwatchedpages ? $unwatchedPageThreshold : null
875
		);
876
	}
877
878
	public function getCacheMode( $params ) {
879
		// Other props depend on something about the current user
880
		$publicProps = [
881
			'protection',
882
			'talkid',
883
			'subjectid',
884
			'url',
885
			'preload',
886
			'displaytitle',
887
		];
888
		if ( array_diff( (array)$params['prop'], $publicProps ) ) {
889
			return 'private';
890
		}
891
892
		// testactions also depends on the current user
893
		if ( $params['testactions'] ) {
894
			return 'private';
895
		}
896
897
		if ( !is_null( $params['token'] ) ) {
898
			return 'private';
899
		}
900
901
		return 'public';
902
	}
903
904
	public function getAllowedParams() {
905
		return [
906
			'prop' => [
907
				ApiBase::PARAM_ISMULTI => true,
908
				ApiBase::PARAM_TYPE => [
909
					'protection',
910
					'talkid',
911
					'watched', # private
912
					'watchers', # private
913
					'visitingwatchers', # private
914
					'notificationtimestamp', # private
915
					'subjectid',
916
					'url',
917
					'readable', # private
918
					'preload',
919
					'displaytitle',
920
					// If you add more properties here, please consider whether they
921
					// need to be added to getCacheMode()
922
				],
923
				ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
924
			],
925
			'testactions' => [
926
				ApiBase::PARAM_TYPE => 'string',
927
				ApiBase::PARAM_ISMULTI => true,
928
			],
929
			'token' => [
930
				ApiBase::PARAM_DEPRECATED => true,
931
				ApiBase::PARAM_ISMULTI => true,
932
				ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() )
933
			],
934
			'continue' => [
935
				ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
936
			],
937
		];
938
	}
939
940
	protected function getExamplesMessages() {
941
		return [
942
			'action=query&prop=info&titles=Main%20Page'
943
				=> 'apihelp-query+info-example-simple',
944
			'action=query&prop=info&inprop=protection&titles=Main%20Page'
945
				=> 'apihelp-query+info-example-protection',
946
		];
947
	}
948
949
	public function getHelpUrls() {
950
		return 'https://www.mediawiki.org/wiki/API:Info';
951
	}
952
}
953