Completed
Branch master (3592b6)
by
unknown
26:28
created

ApiQueryInfo::getDeleteToken()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 13
Ratio 100 %
Metric Value
dl 13
loc 13
rs 9.4285
cc 3
eloc 7
nc 3
nop 2
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
27
/**
28
 * A query module to show basic page information.
29
 *
30
 * @ingroup API
31
 */
32
class ApiQueryInfo extends ApiQueryBase {
33
34
	private $fld_protection = false, $fld_talkid = false,
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
35
		$fld_subjectid = false, $fld_url = false,
36
		$fld_readable = false, $fld_watched = false,
37
		$fld_watchers = false, $fld_visitingwatchers = false,
38
		$fld_notificationtimestamp = false,
39
		$fld_preload = false, $fld_displaytitle = false;
40
41
	private $params;
42
43
	/** @var Title[] */
44
	private $titles;
45
	/** @var Title[] */
46
	private $missing;
47
	/** @var Title[] */
48
	private $everything;
49
50
	private $pageRestrictions, $pageIsRedir, $pageIsNew, $pageTouched,
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
51
		$pageLatest, $pageLength;
52
53
	private $protections, $restrictionTypes, $watched, $watchers, $visitingwatchers,
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
54
		$notificationtimestamps, $talkids, $subjectids, $displaytitles;
55
	private $showZeroWatchers = false;
56
57
	private $tokenFunctions;
58
59
	private $countTestedActions = 0;
60
61
	public function __construct( ApiQuery $query, $moduleName ) {
62
		parent::__construct( $query, $moduleName, 'in' );
63
	}
64
65
	/**
66
	 * @param ApiPageSet $pageSet
67
	 * @return void
68
	 */
69
	public function requestExtraData( $pageSet ) {
70
		$pageSet->requestField( 'page_restrictions' );
71
		// If the pageset is resolving redirects we won't get page_is_redirect.
72
		// But we can't know for sure until the pageset is executed (revids may
73
		// turn it off), so request it unconditionally.
74
		$pageSet->requestField( 'page_is_redirect' );
75
		$pageSet->requestField( 'page_is_new' );
76
		$config = $this->getConfig();
77
		$pageSet->requestField( 'page_touched' );
78
		$pageSet->requestField( 'page_latest' );
79
		$pageSet->requestField( 'page_len' );
80
		if ( $config->get( 'ContentHandlerUseDB' ) ) {
81
			$pageSet->requestField( 'page_content_model' );
82
		}
83
		if ( $config->get( 'PageLanguageUseDB' ) ) {
84
			$pageSet->requestField( 'page_lang' );
85
		}
86
	}
87
88
	/**
89
	 * Get an array mapping token names to their handler functions.
90
	 * The prototype for a token function is func($pageid, $title)
91
	 * it should return a token or false (permission denied)
92
	 * @deprecated since 1.24
93
	 * @return array Array(tokenname => function)
94
	 */
95
	protected function getTokenFunctions() {
96
		// Don't call the hooks twice
97
		if ( isset( $this->tokenFunctions ) ) {
98
			return $this->tokenFunctions;
99
		}
100
101
		// If we're in a mode that breaks the same-origin policy, no tokens can
102
		// be obtained
103
		if ( $this->lacksSameOriginSecurity() ) {
104
			return [];
105
		}
106
107
		$this->tokenFunctions = [
108
			'edit' => [ 'ApiQueryInfo', 'getEditToken' ],
109
			'delete' => [ 'ApiQueryInfo', 'getDeleteToken' ],
110
			'protect' => [ 'ApiQueryInfo', 'getProtectToken' ],
111
			'move' => [ 'ApiQueryInfo', 'getMoveToken' ],
112
			'block' => [ 'ApiQueryInfo', 'getBlockToken' ],
113
			'unblock' => [ 'ApiQueryInfo', 'getUnblockToken' ],
114
			'email' => [ 'ApiQueryInfo', 'getEmailToken' ],
115
			'import' => [ 'ApiQueryInfo', 'getImportToken' ],
116
			'watch' => [ 'ApiQueryInfo', 'getWatchToken' ],
117
		];
118
		Hooks::run( 'APIQueryInfoTokens', [ &$this->tokenFunctions ] );
119
120
		return $this->tokenFunctions;
121
	}
122
123
	static protected $cachedTokens = [];
124
125
	/**
126
	 * @deprecated since 1.24
127
	 */
128
	public static function resetTokenCache() {
129
		ApiQueryInfo::$cachedTokens = [];
130
	}
131
132
	/**
133
	 * @deprecated since 1.24
134
	 */
135 View Code Duplication
	public static function getEditToken( $pageid, $title ) {
136
		// We could check for $title->userCan('edit') here,
137
		// but that's too expensive for this purpose
138
		// and would break caching
139
		global $wgUser;
140
		if ( !$wgUser->isAllowed( 'edit' ) ) {
141
			return false;
142
		}
143
144
		// The token is always the same, let's exploit that
145
		if ( !isset( ApiQueryInfo::$cachedTokens['edit'] ) ) {
146
			ApiQueryInfo::$cachedTokens['edit'] = $wgUser->getEditToken();
147
		}
148
149
		return ApiQueryInfo::$cachedTokens['edit'];
150
	}
151
152
	/**
153
	 * @deprecated since 1.24
154
	 */
155 View Code Duplication
	public static function getDeleteToken( $pageid, $title ) {
156
		global $wgUser;
157
		if ( !$wgUser->isAllowed( 'delete' ) ) {
158
			return false;
159
		}
160
161
		// The token is always the same, let's exploit that
162
		if ( !isset( ApiQueryInfo::$cachedTokens['delete'] ) ) {
163
			ApiQueryInfo::$cachedTokens['delete'] = $wgUser->getEditToken();
164
		}
165
166
		return ApiQueryInfo::$cachedTokens['delete'];
167
	}
168
169
	/**
170
	 * @deprecated since 1.24
171
	 */
172 View Code Duplication
	public static function getProtectToken( $pageid, $title ) {
173
		global $wgUser;
174
		if ( !$wgUser->isAllowed( 'protect' ) ) {
175
			return false;
176
		}
177
178
		// The token is always the same, let's exploit that
179
		if ( !isset( ApiQueryInfo::$cachedTokens['protect'] ) ) {
180
			ApiQueryInfo::$cachedTokens['protect'] = $wgUser->getEditToken();
181
		}
182
183
		return ApiQueryInfo::$cachedTokens['protect'];
184
	}
185
186
	/**
187
	 * @deprecated since 1.24
188
	 */
189 View Code Duplication
	public static function getMoveToken( $pageid, $title ) {
190
		global $wgUser;
191
		if ( !$wgUser->isAllowed( 'move' ) ) {
192
			return false;
193
		}
194
195
		// The token is always the same, let's exploit that
196
		if ( !isset( ApiQueryInfo::$cachedTokens['move'] ) ) {
197
			ApiQueryInfo::$cachedTokens['move'] = $wgUser->getEditToken();
198
		}
199
200
		return ApiQueryInfo::$cachedTokens['move'];
201
	}
202
203
	/**
204
	 * @deprecated since 1.24
205
	 */
206 View Code Duplication
	public static function getBlockToken( $pageid, $title ) {
207
		global $wgUser;
208
		if ( !$wgUser->isAllowed( 'block' ) ) {
209
			return false;
210
		}
211
212
		// The token is always the same, let's exploit that
213
		if ( !isset( ApiQueryInfo::$cachedTokens['block'] ) ) {
214
			ApiQueryInfo::$cachedTokens['block'] = $wgUser->getEditToken();
215
		}
216
217
		return ApiQueryInfo::$cachedTokens['block'];
218
	}
219
220
	/**
221
	 * @deprecated since 1.24
222
	 */
223
	public static function getUnblockToken( $pageid, $title ) {
224
		// Currently, this is exactly the same as the block token
225
		return self::getBlockToken( $pageid, $title );
0 ignored issues
show
Deprecated Code introduced by
The method ApiQueryInfo::getBlockToken() has been deprecated with message: since 1.24

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
226
	}
227
228
	/**
229
	 * @deprecated since 1.24
230
	 */
231 View Code Duplication
	public static function getEmailToken( $pageid, $title ) {
232
		global $wgUser;
233
		if ( !$wgUser->canSendEmail() || $wgUser->isBlockedFromEmailuser() ) {
234
			return false;
235
		}
236
237
		// The token is always the same, let's exploit that
238
		if ( !isset( ApiQueryInfo::$cachedTokens['email'] ) ) {
239
			ApiQueryInfo::$cachedTokens['email'] = $wgUser->getEditToken();
240
		}
241
242
		return ApiQueryInfo::$cachedTokens['email'];
243
	}
244
245
	/**
246
	 * @deprecated since 1.24
247
	 */
248 View Code Duplication
	public static function getImportToken( $pageid, $title ) {
249
		global $wgUser;
250
		if ( !$wgUser->isAllowedAny( 'import', 'importupload' ) ) {
251
			return false;
252
		}
253
254
		// The token is always the same, let's exploit that
255
		if ( !isset( ApiQueryInfo::$cachedTokens['import'] ) ) {
256
			ApiQueryInfo::$cachedTokens['import'] = $wgUser->getEditToken();
257
		}
258
259
		return ApiQueryInfo::$cachedTokens['import'];
260
	}
261
262
	/**
263
	 * @deprecated since 1.24
264
	 */
265 View Code Duplication
	public static function getWatchToken( $pageid, $title ) {
266
		global $wgUser;
267
		if ( !$wgUser->isLoggedIn() ) {
268
			return false;
269
		}
270
271
		// The token is always the same, let's exploit that
272
		if ( !isset( ApiQueryInfo::$cachedTokens['watch'] ) ) {
273
			ApiQueryInfo::$cachedTokens['watch'] = $wgUser->getEditToken( 'watch' );
274
		}
275
276
		return ApiQueryInfo::$cachedTokens['watch'];
277
	}
278
279
	/**
280
	 * @deprecated since 1.24
281
	 */
282 View Code Duplication
	public static function getOptionsToken( $pageid, $title ) {
283
		global $wgUser;
284
		if ( !$wgUser->isLoggedIn() ) {
285
			return false;
286
		}
287
288
		// The token is always the same, let's exploit that
289
		if ( !isset( ApiQueryInfo::$cachedTokens['options'] ) ) {
290
			ApiQueryInfo::$cachedTokens['options'] = $wgUser->getEditToken();
291
		}
292
293
		return ApiQueryInfo::$cachedTokens['options'];
294
	}
295
296
	public function execute() {
297
		$this->params = $this->extractRequestParams();
298
		if ( !is_null( $this->params['prop'] ) ) {
299
			$prop = array_flip( $this->params['prop'] );
300
			$this->fld_protection = isset( $prop['protection'] );
301
			$this->fld_watched = isset( $prop['watched'] );
302
			$this->fld_watchers = isset( $prop['watchers'] );
303
			$this->fld_visitingwatchers = isset( $prop['visitingwatchers'] );
304
			$this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
305
			$this->fld_talkid = isset( $prop['talkid'] );
306
			$this->fld_subjectid = isset( $prop['subjectid'] );
307
			$this->fld_url = isset( $prop['url'] );
308
			$this->fld_readable = isset( $prop['readable'] );
309
			$this->fld_preload = isset( $prop['preload'] );
310
			$this->fld_displaytitle = isset( $prop['displaytitle'] );
311
		}
312
313
		$pageSet = $this->getPageSet();
314
		$this->titles = $pageSet->getGoodTitles();
315
		$this->missing = $pageSet->getMissingTitles();
316
		$this->everything = $this->titles + $this->missing;
317
		$result = $this->getResult();
318
319
		uasort( $this->everything, [ 'Title', 'compare' ] );
320
		if ( !is_null( $this->params['continue'] ) ) {
321
			// Throw away any titles we're gonna skip so they don't
322
			// clutter queries
323
			$cont = explode( '|', $this->params['continue'] );
324
			$this->dieContinueUsageIf( count( $cont ) != 2 );
325
			$conttitle = Title::makeTitleSafe( $cont[0], $cont[1] );
326
			foreach ( $this->everything as $pageid => $title ) {
327
				if ( Title::compare( $title, $conttitle ) >= 0 ) {
0 ignored issues
show
Bug introduced by
It seems like $conttitle defined by \Title::makeTitleSafe($cont[0], $cont[1]) on line 325 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...
328
					break;
329
				}
330
				unset( $this->titles[$pageid] );
331
				unset( $this->missing[$pageid] );
332
				unset( $this->everything[$pageid] );
333
			}
334
		}
335
336
		$this->pageRestrictions = $pageSet->getCustomField( 'page_restrictions' );
337
		// when resolving redirects, no page will have this field
338
		$this->pageIsRedir = !$pageSet->isResolvingRedirects()
339
			? $pageSet->getCustomField( 'page_is_redirect' )
340
			: [];
341
		$this->pageIsNew = $pageSet->getCustomField( 'page_is_new' );
342
343
		$this->pageTouched = $pageSet->getCustomField( 'page_touched' );
344
		$this->pageLatest = $pageSet->getCustomField( 'page_latest' );
345
		$this->pageLength = $pageSet->getCustomField( 'page_len' );
346
347
		// Get protection info if requested
348
		if ( $this->fld_protection ) {
349
			$this->getProtectionInfo();
350
		}
351
352
		if ( $this->fld_watched || $this->fld_notificationtimestamp ) {
353
			$this->getWatchedInfo();
354
		}
355
356
		if ( $this->fld_watchers ) {
357
			$this->getWatcherInfo();
358
		}
359
360
		if ( $this->fld_visitingwatchers ) {
361
			$this->getVisitingWatcherInfo();
362
		}
363
364
		// Run the talkid/subjectid query if requested
365
		if ( $this->fld_talkid || $this->fld_subjectid ) {
366
			$this->getTSIDs();
367
		}
368
369
		if ( $this->fld_displaytitle ) {
370
			$this->getDisplayTitle();
371
		}
372
373
		/** @var $title Title */
374
		foreach ( $this->everything as $pageid => $title ) {
375
			$pageInfo = $this->extractPageInfo( $pageid, $title );
376
			$fit = $pageInfo !== null && $result->addValue( [
377
				'query',
378
				'pages'
379
			], $pageid, $pageInfo );
380
			if ( !$fit ) {
381
				$this->setContinueEnumParameter( 'continue',
382
					$title->getNamespace() . '|' .
383
					$title->getText() );
384
				break;
385
			}
386
		}
387
	}
388
389
	/**
390
	 * Get a result array with information about a title
391
	 * @param int $pageid Page ID (negative for missing titles)
392
	 * @param Title $title
393
	 * @return array|null
394
	 */
395
	private function extractPageInfo( $pageid, $title ) {
396
		$pageInfo = [];
397
		// $title->exists() needs pageid, which is not set for all title objects
398
		$titleExists = $pageid > 0;
399
		$ns = $title->getNamespace();
400
		$dbkey = $title->getDBkey();
401
402
		$pageInfo['contentmodel'] = $title->getContentModel();
403
404
		$pageLanguage = $title->getPageLanguage();
405
		$pageInfo['pagelanguage'] = $pageLanguage->getCode();
406
		$pageInfo['pagelanguagehtmlcode'] = $pageLanguage->getHtmlCode();
407
		$pageInfo['pagelanguagedir'] = $pageLanguage->getDir();
408
409
		if ( $titleExists ) {
410
			$pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] );
411
			$pageInfo['lastrevid'] = intval( $this->pageLatest[$pageid] );
412
			$pageInfo['length'] = intval( $this->pageLength[$pageid] );
413
414
			if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) {
415
				$pageInfo['redirect'] = true;
416
			}
417
			if ( $this->pageIsNew[$pageid] ) {
418
				$pageInfo['new'] = true;
419
			}
420
		}
421
422
		if ( !is_null( $this->params['token'] ) ) {
423
			$tokenFunctions = $this->getTokenFunctions();
0 ignored issues
show
Deprecated Code introduced by
The method ApiQueryInfo::getTokenFunctions() has been deprecated with message: since 1.24

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
424
			$pageInfo['starttimestamp'] = wfTimestamp( TS_ISO_8601, time() );
425
			foreach ( $this->params['token'] as $t ) {
426
				$val = call_user_func( $tokenFunctions[$t], $pageid, $title );
427
				if ( $val === false ) {
428
					$this->setWarning( "Action '$t' is not allowed for the current user" );
429
				} else {
430
					$pageInfo[$t . 'token'] = $val;
431
				}
432
			}
433
		}
434
435
		if ( $this->fld_protection ) {
436
			$pageInfo['protection'] = [];
437
			if ( isset( $this->protections[$ns][$dbkey] ) ) {
438
				$pageInfo['protection'] =
439
					$this->protections[$ns][$dbkey];
440
			}
441
			ApiResult::setIndexedTagName( $pageInfo['protection'], 'pr' );
442
443
			$pageInfo['restrictiontypes'] = [];
444
			if ( isset( $this->restrictionTypes[$ns][$dbkey] ) ) {
445
				$pageInfo['restrictiontypes'] =
446
					$this->restrictionTypes[$ns][$dbkey];
447
			}
448
			ApiResult::setIndexedTagName( $pageInfo['restrictiontypes'], 'rt' );
449
		}
450
451
		if ( $this->fld_watched ) {
452
			$pageInfo['watched'] = isset( $this->watched[$ns][$dbkey] );
453
		}
454
455 View Code Duplication
		if ( $this->fld_watchers ) {
456
			if ( $this->watchers !== null && $this->watchers[$ns][$dbkey] !== 0 ) {
457
				$pageInfo['watchers'] = $this->watchers[$ns][$dbkey];
458
			} elseif ( $this->showZeroWatchers ) {
459
				$pageInfo['watchers'] = 0;
460
			}
461
		}
462
463 View Code Duplication
		if ( $this->fld_visitingwatchers ) {
464
			if ( $this->visitingwatchers !== null && $this->visitingwatchers[$ns][$dbkey] !== 0 ) {
465
				$pageInfo['visitingwatchers'] = $this->visitingwatchers[$ns][$dbkey];
466
			} elseif ( $this->showZeroWatchers ) {
467
				$pageInfo['visitingwatchers'] = 0;
468
			}
469
		}
470
471
		if ( $this->fld_notificationtimestamp ) {
472
			$pageInfo['notificationtimestamp'] = '';
473
			if ( isset( $this->notificationtimestamps[$ns][$dbkey] ) ) {
474
				$pageInfo['notificationtimestamp'] =
475
					wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] );
476
			}
477
		}
478
479 View Code Duplication
		if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) {
480
			$pageInfo['talkid'] = $this->talkids[$ns][$dbkey];
481
		}
482
483 View Code Duplication
		if ( $this->fld_subjectid && isset( $this->subjectids[$ns][$dbkey] ) ) {
484
			$pageInfo['subjectid'] = $this->subjectids[$ns][$dbkey];
485
		}
486
487
		if ( $this->fld_url ) {
488
			$pageInfo['fullurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
489
			$pageInfo['editurl'] = wfExpandUrl( $title->getFullURL( 'action=edit' ), PROTO_CURRENT );
490
			$pageInfo['canonicalurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CANONICAL );
491
		}
492
		if ( $this->fld_readable ) {
493
			$pageInfo['readable'] = $title->userCan( 'read', $this->getUser() );
494
		}
495
496
		if ( $this->fld_preload ) {
497
			if ( $titleExists ) {
498
				$pageInfo['preload'] = '';
499
			} else {
500
				$text = null;
501
				Hooks::run( 'EditFormPreloadText', [ &$text, &$title ] );
502
503
				$pageInfo['preload'] = $text;
504
			}
505
		}
506
507
		if ( $this->fld_displaytitle ) {
508
			if ( isset( $this->displaytitles[$pageid] ) ) {
509
				$pageInfo['displaytitle'] = $this->displaytitles[$pageid];
510
			} else {
511
				$pageInfo['displaytitle'] = $title->getPrefixedText();
512
			}
513
		}
514
515
		if ( $this->params['testactions'] ) {
516
			$limit = $this->getMain()->canApiHighLimits() ? self::LIMIT_SML1 : self::LIMIT_SML2;
517
			if ( $this->countTestedActions >= $limit ) {
518
				return null; // force a continuation
519
			}
520
521
			$user = $this->getUser();
522
			$pageInfo['actions'] = [];
523
			foreach ( $this->params['testactions'] as $action ) {
524
				$this->countTestedActions++;
525
				$pageInfo['actions'][$action] = $title->userCan( $action, $user );
526
			}
527
		}
528
529
		return $pageInfo;
530
	}
531
532
	/**
533
	 * Get information about protections and put it in $protections
534
	 */
535
	private function getProtectionInfo() {
536
		global $wgContLang;
537
		$this->protections = [];
538
		$db = $this->getDB();
539
540
		// Get normal protections for existing titles
541
		if ( count( $this->titles ) ) {
542
			$this->resetQueryParams();
543
			$this->addTables( 'page_restrictions' );
544
			$this->addFields( [ 'pr_page', 'pr_type', 'pr_level',
545
				'pr_expiry', 'pr_cascade' ] );
546
			$this->addWhereFld( 'pr_page', array_keys( $this->titles ) );
547
548
			$res = $this->select( __METHOD__ );
549
			foreach ( $res as $row ) {
550
				/** @var $title Title */
551
				$title = $this->titles[$row->pr_page];
552
				$a = [
553
					'type' => $row->pr_type,
554
					'level' => $row->pr_level,
555
					'expiry' => $wgContLang->formatExpiry( $row->pr_expiry, TS_ISO_8601 )
556
				];
557
				if ( $row->pr_cascade ) {
558
					$a['cascade'] = true;
559
				}
560
				$this->protections[$title->getNamespace()][$title->getDBkey()][] = $a;
561
			}
562
			// Also check old restrictions
563
			foreach ( $this->titles as $pageId => $title ) {
564
				if ( $this->pageRestrictions[$pageId] ) {
565
					$namespace = $title->getNamespace();
566
					$dbKey = $title->getDBkey();
567
					$restrictions = explode( ':', trim( $this->pageRestrictions[$pageId] ) );
568
					foreach ( $restrictions as $restrict ) {
569
						$temp = explode( '=', trim( $restrict ) );
570
						if ( count( $temp ) == 1 ) {
571
							// old old format should be treated as edit/move restriction
572
							$restriction = trim( $temp[0] );
573
574
							if ( $restriction == '' ) {
575
								continue;
576
							}
577
							$this->protections[$namespace][$dbKey][] = [
578
								'type' => 'edit',
579
								'level' => $restriction,
580
								'expiry' => 'infinity',
581
							];
582
							$this->protections[$namespace][$dbKey][] = [
583
								'type' => 'move',
584
								'level' => $restriction,
585
								'expiry' => 'infinity',
586
							];
587
						} else {
588
							$restriction = trim( $temp[1] );
589
							if ( $restriction == '' ) {
590
								continue;
591
							}
592
							$this->protections[$namespace][$dbKey][] = [
593
								'type' => $temp[0],
594
								'level' => $restriction,
595
								'expiry' => 'infinity',
596
							];
597
						}
598
					}
599
				}
600
			}
601
		}
602
603
		// Get protections for missing titles
604
		if ( count( $this->missing ) ) {
605
			$this->resetQueryParams();
606
			$lb = new LinkBatch( $this->missing );
607
			$this->addTables( 'protected_titles' );
608
			$this->addFields( [ 'pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry' ] );
609
			$this->addWhere( $lb->constructSet( 'pt', $db ) );
0 ignored issues
show
Bug introduced by
It seems like $lb->constructSet('pt', $db) targeting LinkBatch::constructSet() can also be of type boolean; however, ApiQueryBase::addWhere() does only seem to accept string|array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
610
			$res = $this->select( __METHOD__ );
611
			foreach ( $res as $row ) {
612
				$this->protections[$row->pt_namespace][$row->pt_title][] = [
613
					'type' => 'create',
614
					'level' => $row->pt_create_perm,
615
					'expiry' => $wgContLang->formatExpiry( $row->pt_expiry, TS_ISO_8601 )
616
				];
617
			}
618
		}
619
620
		// Separate good and missing titles into files and other pages
621
		// and populate $this->restrictionTypes
622
		$images = $others = [];
623
		foreach ( $this->everything as $title ) {
624
			if ( $title->getNamespace() == NS_FILE ) {
625
				$images[] = $title->getDBkey();
626
			} else {
627
				$others[] = $title;
628
			}
629
			// Applicable protection types
630
			$this->restrictionTypes[$title->getNamespace()][$title->getDBkey()] =
631
				array_values( $title->getRestrictionTypes() );
632
		}
633
634
		if ( count( $others ) ) {
635
			// Non-images: check templatelinks
636
			$lb = new LinkBatch( $others );
637
			$this->resetQueryParams();
638
			$this->addTables( [ 'page_restrictions', 'page', 'templatelinks' ] );
639
			$this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
640
				'page_title', 'page_namespace',
641
				'tl_title', 'tl_namespace' ] );
642
			$this->addWhere( $lb->constructSet( 'tl', $db ) );
0 ignored issues
show
Bug introduced by
It seems like $lb->constructSet('tl', $db) targeting LinkBatch::constructSet() can also be of type boolean; however, ApiQueryBase::addWhere() does only seem to accept string|array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
643
			$this->addWhere( 'pr_page = page_id' );
644
			$this->addWhere( 'pr_page = tl_from' );
645
			$this->addWhereFld( 'pr_cascade', 1 );
646
647
			$res = $this->select( __METHOD__ );
648 View Code Duplication
			foreach ( $res as $row ) {
649
				$source = Title::makeTitle( $row->page_namespace, $row->page_title );
650
				$this->protections[$row->tl_namespace][$row->tl_title][] = [
651
					'type' => $row->pr_type,
652
					'level' => $row->pr_level,
653
					'expiry' => $wgContLang->formatExpiry( $row->pr_expiry, TS_ISO_8601 ),
654
					'source' => $source->getPrefixedText()
655
				];
656
			}
657
		}
658
659
		if ( count( $images ) ) {
660
			// Images: check imagelinks
661
			$this->resetQueryParams();
662
			$this->addTables( [ 'page_restrictions', 'page', 'imagelinks' ] );
663
			$this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
664
				'page_title', 'page_namespace', 'il_to' ] );
665
			$this->addWhere( 'pr_page = page_id' );
666
			$this->addWhere( 'pr_page = il_from' );
667
			$this->addWhereFld( 'pr_cascade', 1 );
668
			$this->addWhereFld( 'il_to', $images );
669
670
			$res = $this->select( __METHOD__ );
671 View Code Duplication
			foreach ( $res as $row ) {
672
				$source = Title::makeTitle( $row->page_namespace, $row->page_title );
673
				$this->protections[NS_FILE][$row->il_to][] = [
674
					'type' => $row->pr_type,
675
					'level' => $row->pr_level,
676
					'expiry' => $wgContLang->formatExpiry( $row->pr_expiry, TS_ISO_8601 ),
677
					'source' => $source->getPrefixedText()
678
				];
679
			}
680
		}
681
	}
682
683
	/**
684
	 * Get talk page IDs (if requested) and subject page IDs (if requested)
685
	 * and put them in $talkids and $subjectids
686
	 */
687
	private function getTSIDs() {
688
		$getTitles = $this->talkids = $this->subjectids = [];
689
690
		/** @var $t Title */
691
		foreach ( $this->everything as $t ) {
692
			if ( MWNamespace::isTalk( $t->getNamespace() ) ) {
693
				if ( $this->fld_subjectid ) {
694
					$getTitles[] = $t->getSubjectPage();
695
				}
696
			} elseif ( $this->fld_talkid ) {
697
				$getTitles[] = $t->getTalkPage();
698
			}
699
		}
700
		if ( !count( $getTitles ) ) {
701
			return;
702
		}
703
704
		$db = $this->getDB();
705
706
		// Construct a custom WHERE clause that matches
707
		// all titles in $getTitles
708
		$lb = new LinkBatch( $getTitles );
709
		$this->resetQueryParams();
710
		$this->addTables( 'page' );
711
		$this->addFields( [ 'page_title', 'page_namespace', 'page_id' ] );
712
		$this->addWhere( $lb->constructSet( 'page', $db ) );
0 ignored issues
show
Bug introduced by
It seems like $lb->constructSet('page', $db) targeting LinkBatch::constructSet() can also be of type boolean; however, ApiQueryBase::addWhere() does only seem to accept string|array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
713
		$res = $this->select( __METHOD__ );
714
		foreach ( $res as $row ) {
715
			if ( MWNamespace::isTalk( $row->page_namespace ) ) {
716
				$this->talkids[MWNamespace::getSubject( $row->page_namespace )][$row->page_title] =
717
					intval( $row->page_id );
718
			} else {
719
				$this->subjectids[MWNamespace::getTalk( $row->page_namespace )][$row->page_title] =
720
					intval( $row->page_id );
721
			}
722
		}
723
	}
724
725
	private function getDisplayTitle() {
726
		$this->displaytitles = [];
727
728
		$pageIds = array_keys( $this->titles );
729
730
		if ( !count( $pageIds ) ) {
731
			return;
732
		}
733
734
		$this->resetQueryParams();
735
		$this->addTables( 'page_props' );
736
		$this->addFields( [ 'pp_page', 'pp_value' ] );
737
		$this->addWhereFld( 'pp_page', $pageIds );
738
		$this->addWhereFld( 'pp_propname', 'displaytitle' );
739
		$res = $this->select( __METHOD__ );
740
741
		foreach ( $res as $row ) {
742
			$this->displaytitles[$row->pp_page] = $row->pp_value;
743
		}
744
	}
745
746
	/**
747
	 * Get information about watched status and put it in $this->watched
748
	 * and $this->notificationtimestamps
749
	 */
750
	private function getWatchedInfo() {
751
		$user = $this->getUser();
752
753
		if ( $user->isAnon() || count( $this->everything ) == 0
754
			|| !$user->isAllowed( 'viewmywatchlist' )
755
		) {
756
			return;
757
		}
758
759
		$this->watched = [];
760
		$this->notificationtimestamps = [];
761
		$db = $this->getDB();
762
763
		$lb = new LinkBatch( $this->everything );
764
765
		$this->resetQueryParams();
766
		$this->addTables( [ 'watchlist' ] );
767
		$this->addFields( [ 'wl_title', 'wl_namespace' ] );
768
		$this->addFieldsIf( 'wl_notificationtimestamp', $this->fld_notificationtimestamp );
769
		$this->addWhere( [
770
			$lb->constructSet( 'wl', $db ),
771
			'wl_user' => $user->getId()
772
		] );
773
774
		$res = $this->select( __METHOD__ );
775
776
		foreach ( $res as $row ) {
777
			if ( $this->fld_watched ) {
778
				$this->watched[$row->wl_namespace][$row->wl_title] = true;
779
			}
780
			if ( $this->fld_notificationtimestamp ) {
781
				$this->notificationtimestamps[$row->wl_namespace][$row->wl_title] =
782
					$row->wl_notificationtimestamp;
783
			}
784
		}
785
	}
786
787
	/**
788
	 * Get the count of watchers and put it in $this->watchers
789
	 */
790
	private function getWatcherInfo() {
791
		if ( count( $this->everything ) == 0 ) {
792
			return;
793
		}
794
795
		$user = $this->getUser();
796
		$canUnwatchedpages = $user->isAllowed( 'unwatchedpages' );
797
		$unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' );
798
		if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
799
			return;
800
		}
801
802
		$this->showZeroWatchers = $canUnwatchedpages;
803
804
		$countOptions = [];
805
		if ( !$canUnwatchedpages ) {
806
			$countOptions['minimumWatchers'] = $unwatchedPageThreshold;
807
		}
808
809
		$this->watchers = WatchedItemStore::getDefaultInstance()->countWatchersMultiple(
810
			$this->everything,
811
			$countOptions
812
		);
813
	}
814
815
	/**
816
	 * Get the count of watchers who have visited recent edits and put it in
817
	 * $this->visitingwatchers
818
	 *
819
	 * Based on InfoAction::pageCounts
820
	 */
821
	private function getVisitingWatcherInfo() {
822
		$config = $this->getConfig();
823
		$user = $this->getUser();
824
		$db = $this->getDB();
825
826
		$canUnwatchedpages = $user->isAllowed( 'unwatchedpages' );
827
		$unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' );
828
		if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
829
			return;
830
		}
831
832
		$this->showZeroWatchers = $canUnwatchedpages;
833
834
		$titlesWithThresholds = [];
835
		if ( $this->titles ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->titles of type Title[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
836
			$lb = new LinkBatch( $this->titles );
837
838
			// Fetch last edit timestamps for pages
839
			$this->resetQueryParams();
840
			$this->addTables( [ 'page', 'revision' ] );
841
			$this->addFields( [ 'page_namespace', 'page_title', 'rev_timestamp' ] );
842
			$this->addWhere( [
843
				'page_latest = rev_id',
844
				$lb->constructSet( 'page', $db ),
845
			] );
846
			$this->addOption( 'GROUP BY', [ 'page_namespace', 'page_title' ] );
847
			$timestampRes = $this->select( __METHOD__ );
848
849
			$age = $config->get( 'WatchersMaxAge' );
850
			$timestamps = [];
851
			foreach ( $timestampRes as $row ) {
852
				$revTimestamp = wfTimestamp( TS_UNIX, (int)$row->rev_timestamp );
853
				$timestamps[$row->page_namespace][$row->page_title] = $revTimestamp - $age;
854
			}
855
			$titlesWithThresholds = array_map(
856
				function( LinkTarget $target ) use ( $timestamps ) {
857
					return [
858
						$target, $timestamps[$target->getNamespace()][$target->getDBkey()]
859
					];
860
				},
861
				$this->titles
862
			);
863
		}
864
865
		if ( $this->missing ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->missing of type Title[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
866
			$titlesWithThresholds = array_merge(
867
				$titlesWithThresholds,
868
				array_map(
869
					function( LinkTarget $target ) {
870
						return [ $target, null ];
871
					},
872
					$this->missing
873
				)
874
			);
875
		}
876
877
		$this->visitingwatchers = WatchedItemStore::getDefaultInstance()->countVisitingWatchersMultiple(
878
			$titlesWithThresholds,
879
			!$canUnwatchedpages ? $unwatchedPageThreshold : null
880
		);
881
	}
882
883
	public function getCacheMode( $params ) {
884
		// Other props depend on something about the current user
885
		$publicProps = [
886
			'protection',
887
			'talkid',
888
			'subjectid',
889
			'url',
890
			'preload',
891
			'displaytitle',
892
		];
893
		if ( array_diff( (array)$params['prop'], $publicProps ) ) {
894
			return 'private';
895
		}
896
897
		// testactions also depends on the current user
898
		if ( $params['testactions'] ) {
899
			return 'private';
900
		}
901
902
		if ( !is_null( $params['token'] ) ) {
903
			return 'private';
904
		}
905
906
		return 'public';
907
	}
908
909
	public function getAllowedParams() {
910
		return [
911
			'prop' => [
912
				ApiBase::PARAM_ISMULTI => true,
913
				ApiBase::PARAM_TYPE => [
914
					'protection',
915
					'talkid',
916
					'watched', # private
917
					'watchers', # private
918
					'visitingwatchers', # private
919
					'notificationtimestamp', # private
920
					'subjectid',
921
					'url',
922
					'readable', # private
923
					'preload',
924
					'displaytitle',
925
					// If you add more properties here, please consider whether they
926
					// need to be added to getCacheMode()
927
				],
928
				ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
929
			],
930
			'testactions' => [
931
				ApiBase::PARAM_TYPE => 'string',
932
				ApiBase::PARAM_ISMULTI => true,
933
			],
934
			'token' => [
935
				ApiBase::PARAM_DEPRECATED => true,
936
				ApiBase::PARAM_ISMULTI => true,
937
				ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() )
0 ignored issues
show
Deprecated Code introduced by
The method ApiQueryInfo::getTokenFunctions() has been deprecated with message: since 1.24

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
938
			],
939
			'continue' => [
940
				ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
941
			],
942
		];
943
	}
944
945
	protected function getExamplesMessages() {
946
		return [
947
			'action=query&prop=info&titles=Main%20Page'
948
				=> 'apihelp-query+info-example-simple',
949
			'action=query&prop=info&inprop=protection&titles=Main%20Page'
950
				=> 'apihelp-query+info-example-protection',
951
		];
952
	}
953
954
	public function getHelpUrls() {
955
		return 'https://www.mediawiki.org/wiki/API:Info';
956
	}
957
}
958