Completed
Branch master (d7c4e6)
by
unknown
29:20
created

Title::checkUserBlock()   C

Complexity

Conditions 12
Paths 8

Size

Total Lines 30
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 14
c 1
b 0
f 0
nc 8
nop 5
dl 0
loc 30
rs 5.1612

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Representation of a title within %MediaWiki.
4
 *
5
 * See title.txt
6
 *
7
 * This program is free software; you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation; either version 2 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License along
18
 * with this program; if not, write to the Free Software Foundation, Inc.,
19
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
 * http://www.gnu.org/copyleft/gpl.html
21
 *
22
 * @file
23
 */
24
use MediaWiki\Linker\LinkTarget;
25
use MediaWiki\Interwiki\InterwikiLookup;
26
use MediaWiki\MediaWikiServices;
27
28
/**
29
 * Represents a title within MediaWiki.
30
 * Optionally may contain an interwiki designation or namespace.
31
 * @note This class can fetch various kinds of data from the database;
32
 *       however, it does so inefficiently.
33
 * @note Consider using a TitleValue object instead. TitleValue is more lightweight
34
 *       and does not rely on global state or the database.
35
 */
36
class Title implements LinkTarget {
37
	/** @var HashBagOStuff */
38
	static private $titleCache = null;
39
40
	/**
41
	 * Title::newFromText maintains a cache to avoid expensive re-normalization of
42
	 * commonly used titles. On a batch operation this can become a memory leak
43
	 * if not bounded. After hitting this many titles reset the cache.
44
	 */
45
	const CACHE_MAX = 1000;
46
47
	/**
48
	 * Used to be GAID_FOR_UPDATE define. Used with getArticleID() and friends
49
	 * to use the master DB
50
	 */
51
	const GAID_FOR_UPDATE = 1;
52
53
	/**
54
	 * @name Private member variables
55
	 * Please use the accessor functions instead.
56
	 * @private
57
	 */
58
	// @{
59
60
	/** @var string Text form (spaces not underscores) of the main part */
61
	public $mTextform = '';
62
63
	/** @var string URL-encoded form of the main part */
64
	public $mUrlform = '';
65
66
	/** @var string Main part with underscores */
67
	public $mDbkeyform = '';
68
69
	/** @var string Database key with the initial letter in the case specified by the user */
70
	protected $mUserCaseDBKey;
71
72
	/** @var int Namespace index, i.e. one of the NS_xxxx constants */
73
	public $mNamespace = NS_MAIN;
74
75
	/** @var string Interwiki prefix */
76
	public $mInterwiki = '';
77
78
	/** @var bool Was this Title created from a string with a local interwiki prefix? */
79
	private $mLocalInterwiki = false;
80
81
	/** @var string Title fragment (i.e. the bit after the #) */
82
	public $mFragment = '';
83
84
	/** @var int Article ID, fetched from the link cache on demand */
85
	public $mArticleID = -1;
86
87
	/** @var bool|int ID of most recent revision */
88
	protected $mLatestID = false;
89
90
	/**
91
	 * @var bool|string ID of the page's content model, i.e. one of the
92
	 *   CONTENT_MODEL_XXX constants
93
	 */
94
	public $mContentModel = false;
95
96
	/** @var int Estimated number of revisions; null of not loaded */
97
	private $mEstimateRevisions;
98
99
	/** @var array Array of groups allowed to edit this article */
100
	public $mRestrictions = [];
101
102
	/** @var string|bool */
103
	protected $mOldRestrictions = false;
104
105
	/** @var bool Cascade restrictions on this page to included templates and images? */
106
	public $mCascadeRestriction;
107
108
	/** Caching the results of getCascadeProtectionSources */
109
	public $mCascadingRestrictions;
110
111
	/** @var array When do the restrictions on this page expire? */
112
	protected $mRestrictionsExpiry = [];
113
114
	/** @var bool Are cascading restrictions in effect on this page? */
115
	protected $mHasCascadingRestrictions;
116
117
	/** @var array Where are the cascading restrictions coming from on this page? */
118
	public $mCascadeSources;
119
120
	/** @var bool Boolean for initialisation on demand */
121
	public $mRestrictionsLoaded = false;
122
123
	/** @var string Text form including namespace/interwiki, initialised on demand */
124
	protected $mPrefixedText = null;
125
126
	/** @var mixed Cached value for getTitleProtection (create protection) */
127
	public $mTitleProtection;
128
129
	/**
130
	 * @var int Namespace index when there is no namespace. Don't change the
131
	 *   following default, NS_MAIN is hardcoded in several places. See bug 696.
132
	 *   Zero except in {{transclusion}} tags.
133
	 */
134
	public $mDefaultNamespace = NS_MAIN;
135
136
	/** @var int The page length, 0 for special pages */
137
	protected $mLength = -1;
138
139
	/** @var null Is the article at this title a redirect? */
140
	public $mRedirect = null;
141
142
	/** @var array Associative array of user ID -> timestamp/false */
143
	private $mNotificationTimestamp = [];
144
145
	/** @var bool Whether a page has any subpages */
146
	private $mHasSubpages;
147
148
	/** @var bool The (string) language code of the page's language and content code. */
149
	private $mPageLanguage = false;
150
151
	/** @var string|bool|null The page language code from the database, null if not saved in
152
	 * the database or false if not loaded, yet. */
153
	private $mDbPageLanguage = false;
154
155
	/** @var TitleValue A corresponding TitleValue object */
156
	private $mTitleValue = null;
157
158
	/** @var bool Would deleting this page be a big deletion? */
159
	private $mIsBigDeletion = null;
160
	// @}
161
162
	/**
163
	 * B/C kludge: provide a TitleParser for use by Title.
164
	 * Ideally, Title would have no methods that need this.
165
	 * Avoid usage of this singleton by using TitleValue
166
	 * and the associated services when possible.
167
	 *
168
	 * @return TitleFormatter
169
	 */
170
	private static function getTitleFormatter() {
171
		return MediaWikiServices::getInstance()->getTitleFormatter();
172
	}
173
174
	/**
175
	 * B/C kludge: provide an InterwikiLookup for use by Title.
176
	 * Ideally, Title would have no methods that need this.
177
	 * Avoid usage of this singleton by using TitleValue
178
	 * and the associated services when possible.
179
	 *
180
	 * @return InterwikiLookup
181
	 */
182
	private static function getInterwikiLookup() {
183
		return MediaWikiServices::getInstance()->getInterwikiLookup();
184
	}
185
186
	/**
187
	 * @access protected
188
	 */
189
	function __construct() {
190
	}
191
192
	/**
193
	 * Create a new Title from a prefixed DB key
194
	 *
195
	 * @param string $key The database key, which has underscores
196
	 *	instead of spaces, possibly including namespace and
197
	 *	interwiki prefixes
198
	 * @return Title|null Title, or null on an error
199
	 */
200
	public static function newFromDBkey( $key ) {
201
		$t = new Title();
202
		$t->mDbkeyform = $key;
203
204
		try {
205
			$t->secureAndSplit();
206
			return $t;
207
		} catch ( MalformedTitleException $ex ) {
208
			return null;
209
		}
210
	}
211
212
	/**
213
	 * Create a new Title from a TitleValue
214
	 *
215
	 * @param TitleValue $titleValue Assumed to be safe.
216
	 *
217
	 * @return Title
218
	 */
219
	public static function newFromTitleValue( TitleValue $titleValue ) {
220
		return self::newFromLinkTarget( $titleValue );
221
	}
222
223
	/**
224
	 * Create a new Title from a LinkTarget
225
	 *
226
	 * @param LinkTarget $linkTarget Assumed to be safe.
227
	 *
228
	 * @return Title
229
	 */
230
	public static function newFromLinkTarget( LinkTarget $linkTarget ) {
231
		if ( $linkTarget instanceof Title ) {
232
			// Special case if it's already a Title object
233
			return $linkTarget;
234
		}
235
		return self::makeTitle(
236
			$linkTarget->getNamespace(),
237
			$linkTarget->getText(),
238
			$linkTarget->getFragment(),
239
			$linkTarget->getInterwiki()
240
		);
241
	}
242
243
	/**
244
	 * Create a new Title from text, such as what one would find in a link. De-
245
	 * codes any HTML entities in the text.
246
	 *
247
	 * @param string|int|null $text The link text; spaces, prefixes, and an
248
	 *   initial ':' indicating the main namespace are accepted.
249
	 * @param int $defaultNamespace The namespace to use if none is specified
250
	 *   by a prefix.  If you want to force a specific namespace even if
251
	 *   $text might begin with a namespace prefix, use makeTitle() or
252
	 *   makeTitleSafe().
253
	 * @throws InvalidArgumentException
254
	 * @return Title|null Title or null on an error.
255
	 */
256
	public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
257
		// DWIM: Integers can be passed in here when page titles are used as array keys.
258
		if ( $text !== null && !is_string( $text ) && !is_int( $text ) ) {
259
			throw new InvalidArgumentException( '$text must be a string.' );
260
		}
261
		if ( $text === null ) {
262
			return null;
263
		}
264
265
		try {
266
			return Title::newFromTextThrow( strval( $text ), $defaultNamespace );
267
		} catch ( MalformedTitleException $ex ) {
268
			return null;
269
		}
270
	}
271
272
	/**
273
	 * Like Title::newFromText(), but throws MalformedTitleException when the title is invalid,
274
	 * rather than returning null.
275
	 *
276
	 * The exception subclasses encode detailed information about why the title is invalid.
277
	 *
278
	 * @see Title::newFromText
279
	 *
280
	 * @since 1.25
281
	 * @param string $text Title text to check
282
	 * @param int $defaultNamespace
283
	 * @throws MalformedTitleException If the title is invalid
284
	 * @return Title
285
	 */
286
	public static function newFromTextThrow( $text, $defaultNamespace = NS_MAIN ) {
287
		if ( is_object( $text ) ) {
288
			throw new MWException( '$text must be a string, given an object' );
289
		}
290
291
		$titleCache = self::getTitleCache();
292
293
		// Wiki pages often contain multiple links to the same page.
294
		// Title normalization and parsing can become expensive on pages with many
295
		// links, so we can save a little time by caching them.
296
		// In theory these are value objects and won't get changed...
297
		if ( $defaultNamespace == NS_MAIN ) {
298
			$t = $titleCache->get( $text );
299
			if ( $t ) {
300
				return $t;
301
			}
302
		}
303
304
		// Convert things like &eacute; &#257; or &#x3017; into normalized (bug 14952) text
305
		$filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
306
307
		$t = new Title();
308
		$t->mDbkeyform = strtr( $filteredText, ' ', '_' );
309
		$t->mDefaultNamespace = intval( $defaultNamespace );
310
311
		$t->secureAndSplit();
312
		if ( $defaultNamespace == NS_MAIN ) {
313
			$titleCache->set( $text, $t );
314
		}
315
		return $t;
316
	}
317
318
	/**
319
	 * THIS IS NOT THE FUNCTION YOU WANT. Use Title::newFromText().
320
	 *
321
	 * Example of wrong and broken code:
322
	 * $title = Title::newFromURL( $wgRequest->getVal( 'title' ) );
323
	 *
324
	 * Example of right code:
325
	 * $title = Title::newFromText( $wgRequest->getVal( 'title' ) );
326
	 *
327
	 * Create a new Title from URL-encoded text. Ensures that
328
	 * the given title's length does not exceed the maximum.
329
	 *
330
	 * @param string $url The title, as might be taken from a URL
331
	 * @return Title|null The new object, or null on an error
332
	 */
333
	public static function newFromURL( $url ) {
334
		$t = new Title();
335
336
		# For compatibility with old buggy URLs. "+" is usually not valid in titles,
337
		# but some URLs used it as a space replacement and they still come
338
		# from some external search tools.
339
		if ( strpos( self::legalChars(), '+' ) === false ) {
340
			$url = strtr( $url, '+', ' ' );
341
		}
342
343
		$t->mDbkeyform = strtr( $url, ' ', '_' );
344
345
		try {
346
			$t->secureAndSplit();
347
			return $t;
348
		} catch ( MalformedTitleException $ex ) {
349
			return null;
350
		}
351
	}
352
353
	/**
354
	 * @return HashBagOStuff
355
	 */
356
	private static function getTitleCache() {
357
		if ( self::$titleCache == null ) {
358
			self::$titleCache = new HashBagOStuff( [ 'maxKeys' => self::CACHE_MAX ] );
359
		}
360
		return self::$titleCache;
361
	}
362
363
	/**
364
	 * Returns a list of fields that are to be selected for initializing Title
365
	 * objects or LinkCache entries. Uses $wgContentHandlerUseDB to determine
366
	 * whether to include page_content_model.
367
	 *
368
	 * @return array
369
	 */
370 View Code Duplication
	protected static function getSelectFields() {
371
		global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
372
373
		$fields = [
374
			'page_namespace', 'page_title', 'page_id',
375
			'page_len', 'page_is_redirect', 'page_latest',
376
		];
377
378
		if ( $wgContentHandlerUseDB ) {
379
			$fields[] = 'page_content_model';
380
		}
381
382
		if ( $wgPageLanguageUseDB ) {
383
			$fields[] = 'page_lang';
384
		}
385
386
		return $fields;
387
	}
388
389
	/**
390
	 * Create a new Title from an article ID
391
	 *
392
	 * @param int $id The page_id corresponding to the Title to create
393
	 * @param int $flags Use Title::GAID_FOR_UPDATE to use master
394
	 * @return Title|null The new object, or null on an error
395
	 */
396
	public static function newFromID( $id, $flags = 0 ) {
397
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
398
		$row = $db->selectRow(
399
			'page',
400
			self::getSelectFields(),
401
			[ 'page_id' => $id ],
402
			__METHOD__
403
		);
404
		if ( $row !== false ) {
405
			$title = Title::newFromRow( $row );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $db->selectRow('page', s...d' => $id), __METHOD__) on line 398 can also be of type boolean; however, Title::newFromRow() does only seem to accept object<stdClass>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
406
		} else {
407
			$title = null;
408
		}
409
		return $title;
410
	}
411
412
	/**
413
	 * Make an array of titles from an array of IDs
414
	 *
415
	 * @param int[] $ids Array of IDs
416
	 * @return Title[] Array of Titles
417
	 */
418
	public static function newFromIDs( $ids ) {
419
		if ( !count( $ids ) ) {
420
			return [];
421
		}
422
		$dbr = wfGetDB( DB_SLAVE );
423
424
		$res = $dbr->select(
425
			'page',
426
			self::getSelectFields(),
427
			[ 'page_id' => $ids ],
428
			__METHOD__
429
		);
430
431
		$titles = [];
432
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
433
			$titles[] = Title::newFromRow( $row );
434
		}
435
		return $titles;
436
	}
437
438
	/**
439
	 * Make a Title object from a DB row
440
	 *
441
	 * @param stdClass $row Object database row (needs at least page_title,page_namespace)
442
	 * @return Title Corresponding Title
443
	 */
444
	public static function newFromRow( $row ) {
445
		$t = self::makeTitle( $row->page_namespace, $row->page_title );
446
		$t->loadFromRow( $row );
447
		return $t;
448
	}
449
450
	/**
451
	 * Load Title object fields from a DB row.
452
	 * If false is given, the title will be treated as non-existing.
453
	 *
454
	 * @param stdClass|bool $row Database row
455
	 */
456
	public function loadFromRow( $row ) {
457
		if ( $row ) { // page found
458
			if ( isset( $row->page_id ) ) {
459
				$this->mArticleID = (int)$row->page_id;
460
			}
461
			if ( isset( $row->page_len ) ) {
462
				$this->mLength = (int)$row->page_len;
463
			}
464
			if ( isset( $row->page_is_redirect ) ) {
465
				$this->mRedirect = (bool)$row->page_is_redirect;
0 ignored issues
show
Documentation Bug introduced by
It seems like (bool) $row->page_is_redirect of type boolean is incompatible with the declared type null of property $mRedirect.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
466
			}
467
			if ( isset( $row->page_latest ) ) {
468
				$this->mLatestID = (int)$row->page_latest;
469
			}
470
			if ( isset( $row->page_content_model ) ) {
471
				$this->mContentModel = strval( $row->page_content_model );
472
			} else {
473
				$this->mContentModel = false; # initialized lazily in getContentModel()
474
			}
475
			if ( isset( $row->page_lang ) ) {
476
				$this->mDbPageLanguage = (string)$row->page_lang;
477
			}
478
			if ( isset( $row->page_restrictions ) ) {
479
				$this->mOldRestrictions = $row->page_restrictions;
480
			}
481
		} else { // page not found
482
			$this->mArticleID = 0;
483
			$this->mLength = 0;
484
			$this->mRedirect = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type null of property $mRedirect.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
485
			$this->mLatestID = 0;
486
			$this->mContentModel = false; # initialized lazily in getContentModel()
487
		}
488
	}
489
490
	/**
491
	 * Create a new Title from a namespace index and a DB key.
492
	 * It's assumed that $ns and $title are *valid*, for instance when
493
	 * they came directly from the database or a special page name.
494
	 * For convenience, spaces are converted to underscores so that
495
	 * eg user_text fields can be used directly.
496
	 *
497
	 * @param int $ns The namespace of the article
498
	 * @param string $title The unprefixed database key form
499
	 * @param string $fragment The link fragment (after the "#")
500
	 * @param string $interwiki The interwiki prefix
501
	 * @return Title The new object
502
	 */
503
	public static function makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) {
504
		$t = new Title();
505
		$t->mInterwiki = $interwiki;
506
		$t->mFragment = $fragment;
507
		$t->mNamespace = $ns = intval( $ns );
508
		$t->mDbkeyform = strtr( $title, ' ', '_' );
509
		$t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
510
		$t->mUrlform = wfUrlencode( $t->mDbkeyform );
511
		$t->mTextform = strtr( $title, '_', ' ' );
512
		$t->mContentModel = false; # initialized lazily in getContentModel()
513
		return $t;
514
	}
515
516
	/**
517
	 * Create a new Title from a namespace index and a DB key.
518
	 * The parameters will be checked for validity, which is a bit slower
519
	 * than makeTitle() but safer for user-provided data.
520
	 *
521
	 * @param int $ns The namespace of the article
522
	 * @param string $title Database key form
523
	 * @param string $fragment The link fragment (after the "#")
524
	 * @param string $interwiki Interwiki prefix
525
	 * @return Title|null The new object, or null on an error
526
	 */
527
	public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
528
		if ( !MWNamespace::exists( $ns ) ) {
529
			return null;
530
		}
531
532
		$t = new Title();
533
		$t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki, true );
534
535
		try {
536
			$t->secureAndSplit();
537
			return $t;
538
		} catch ( MalformedTitleException $ex ) {
539
			return null;
540
		}
541
	}
542
543
	/**
544
	 * Create a new Title for the Main Page
545
	 *
546
	 * @return Title The new object
547
	 */
548
	public static function newMainPage() {
549
		$title = Title::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() );
550
		// Don't give fatal errors if the message is broken
551
		if ( !$title ) {
552
			$title = Title::newFromText( 'Main Page' );
553
		}
554
		return $title;
555
	}
556
557
	/**
558
	 * Get the prefixed DB key associated with an ID
559
	 *
560
	 * @param int $id The page_id of the article
561
	 * @return Title|null An object representing the article, or null if no such article was found
562
	 */
563
	public static function nameOf( $id ) {
564
		$dbr = wfGetDB( DB_SLAVE );
565
566
		$s = $dbr->selectRow(
567
			'page',
568
			[ 'page_namespace', 'page_title' ],
569
			[ 'page_id' => $id ],
570
			__METHOD__
571
		);
572
		if ( $s === false ) {
573
			return null;
574
		}
575
576
		$n = self::makeName( $s->page_namespace, $s->page_title );
577
		return $n;
578
	}
579
580
	/**
581
	 * Get a regex character class describing the legal characters in a link
582
	 *
583
	 * @return string The list of characters, not delimited
584
	 */
585
	public static function legalChars() {
586
		global $wgLegalTitleChars;
587
		return $wgLegalTitleChars;
588
	}
589
590
	/**
591
	 * Returns a simple regex that will match on characters and sequences invalid in titles.
592
	 * Note that this doesn't pick up many things that could be wrong with titles, but that
593
	 * replacing this regex with something valid will make many titles valid.
594
	 *
595
	 * @deprecated since 1.25, use MediaWikiTitleCodec::getTitleInvalidRegex() instead
596
	 *
597
	 * @return string Regex string
598
	 */
599
	static function getTitleInvalidRegex() {
600
		wfDeprecated( __METHOD__, '1.25' );
601
		return MediaWikiTitleCodec::getTitleInvalidRegex();
602
	}
603
604
	/**
605
	 * Utility method for converting a character sequence from bytes to Unicode.
606
	 *
607
	 * Primary usecase being converting $wgLegalTitleChars to a sequence usable in
608
	 * javascript, as PHP uses UTF-8 bytes where javascript uses Unicode code units.
609
	 *
610
	 * @param string $byteClass
611
	 * @return string
612
	 */
613
	public static function convertByteClassToUnicodeClass( $byteClass ) {
614
		$length = strlen( $byteClass );
615
		// Input token queue
616
		$x0 = $x1 = $x2 = '';
0 ignored issues
show
Unused Code introduced by
$x2 is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
617
		// Decoded queue
618
		$d0 = $d1 = $d2 = '';
0 ignored issues
show
Unused Code introduced by
$d2 is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
619
		// Decoded integer codepoints
620
		$ord0 = $ord1 = $ord2 = 0;
0 ignored issues
show
Unused Code introduced by
$ord2 is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
621
		// Re-encoded queue
622
		$r0 = $r1 = $r2 = '';
0 ignored issues
show
Unused Code introduced by
$r2 is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
623
		// Output
624
		$out = '';
625
		// Flags
626
		$allowUnicode = false;
627
		for ( $pos = 0; $pos < $length; $pos++ ) {
628
			// Shift the queues down
629
			$x2 = $x1;
630
			$x1 = $x0;
631
			$d2 = $d1;
0 ignored issues
show
Unused Code introduced by
$d2 is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
632
			$d1 = $d0;
633
			$ord2 = $ord1;
634
			$ord1 = $ord0;
635
			$r2 = $r1;
636
			$r1 = $r0;
637
			// Load the current input token and decoded values
638
			$inChar = $byteClass[$pos];
639
			if ( $inChar == '\\' ) {
640
				if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) {
641
					$x0 = $inChar . $m[0];
642
					$d0 = chr( hexdec( $m[1] ) );
643
					$pos += strlen( $m[0] );
644
				} elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) {
645
					$x0 = $inChar . $m[0];
646
					$d0 = chr( octdec( $m[0] ) );
647
					$pos += strlen( $m[0] );
648
				} elseif ( $pos + 1 >= $length ) {
649
					$x0 = $d0 = '\\';
650
				} else {
651
					$d0 = $byteClass[$pos + 1];
652
					$x0 = $inChar . $d0;
653
					$pos += 1;
654
				}
655
			} else {
656
				$x0 = $d0 = $inChar;
657
			}
658
			$ord0 = ord( $d0 );
659
			// Load the current re-encoded value
660
			if ( $ord0 < 32 || $ord0 == 0x7f ) {
661
				$r0 = sprintf( '\x%02x', $ord0 );
662
			} elseif ( $ord0 >= 0x80 ) {
663
				// Allow unicode if a single high-bit character appears
664
				$r0 = sprintf( '\x%02x', $ord0 );
665
				$allowUnicode = true;
666
			} elseif ( strpos( '-\\[]^', $d0 ) !== false ) {
667
				$r0 = '\\' . $d0;
668
			} else {
669
				$r0 = $d0;
670
			}
671
			// Do the output
672
			if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) {
673
				// Range
674
				if ( $ord2 > $ord0 ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
675
					// Empty range
676
				} elseif ( $ord0 >= 0x80 ) {
677
					// Unicode range
678
					$allowUnicode = true;
679
					if ( $ord2 < 0x80 ) {
680
						// Keep the non-unicode section of the range
681
						$out .= "$r2-\\x7F";
682
					}
683
				} else {
684
					// Normal range
685
					$out .= "$r2-$r0";
686
				}
687
				// Reset state to the initial value
688
				$x0 = $x1 = $d0 = $d1 = $r0 = $r1 = '';
689
			} elseif ( $ord2 < 0x80 ) {
690
				// ASCII character
691
				$out .= $r2;
692
			}
693
		}
694
		if ( $ord1 < 0x80 ) {
695
			$out .= $r1;
696
		}
697
		if ( $ord0 < 0x80 ) {
698
			$out .= $r0;
699
		}
700
		if ( $allowUnicode ) {
701
			$out .= '\u0080-\uFFFF';
702
		}
703
		return $out;
704
	}
705
706
	/**
707
	 * Make a prefixed DB key from a DB key and a namespace index
708
	 *
709
	 * @param int $ns Numerical representation of the namespace
710
	 * @param string $title The DB key form the title
711
	 * @param string $fragment The link fragment (after the "#")
712
	 * @param string $interwiki The interwiki prefix
713
	 * @param bool $canonicalNamespace If true, use the canonical name for
714
	 *   $ns instead of the localized version.
715
	 * @return string The prefixed form of the title
716
	 */
717
	public static function makeName( $ns, $title, $fragment = '', $interwiki = '',
718
		$canonicalNamespace = false
719
	) {
720
		global $wgContLang;
721
722
		if ( $canonicalNamespace ) {
723
			$namespace = MWNamespace::getCanonicalName( $ns );
724
		} else {
725
			$namespace = $wgContLang->getNsText( $ns );
726
		}
727
		$name = $namespace == '' ? $title : "$namespace:$title";
728
		if ( strval( $interwiki ) != '' ) {
729
			$name = "$interwiki:$name";
730
		}
731
		if ( strval( $fragment ) != '' ) {
732
			$name .= '#' . $fragment;
733
		}
734
		return $name;
735
	}
736
737
	/**
738
	 * Escape a text fragment, say from a link, for a URL
739
	 *
740
	 * @param string $fragment Containing a URL or link fragment (after the "#")
741
	 * @return string Escaped string
742
	 */
743
	static function escapeFragmentForURL( $fragment ) {
744
		# Note that we don't urlencode the fragment.  urlencoded Unicode
745
		# fragments appear not to work in IE (at least up to 7) or in at least
746
		# one version of Opera 9.x.  The W3C validator, for one, doesn't seem
747
		# to care if they aren't encoded.
748
		return Sanitizer::escapeId( $fragment, 'noninitial' );
749
	}
750
751
	/**
752
	 * Callback for usort() to do title sorts by (namespace, title)
753
	 *
754
	 * @param Title $a
755
	 * @param Title $b
756
	 *
757
	 * @return int Result of string comparison, or namespace comparison
758
	 */
759
	public static function compare( $a, $b ) {
760
		if ( $a->getNamespace() == $b->getNamespace() ) {
761
			return strcmp( $a->getText(), $b->getText() );
762
		} else {
763
			return $a->getNamespace() - $b->getNamespace();
764
		}
765
	}
766
767
	/**
768
	 * Determine whether the object refers to a page within
769
	 * this project (either this wiki or a wiki with a local
770
	 * interwiki, see https://www.mediawiki.org/wiki/Manual:Interwiki_table#iw_local )
771
	 *
772
	 * @return bool True if this is an in-project interwiki link or a wikilink, false otherwise
773
	 */
774
	public function isLocal() {
775
		if ( $this->isExternal() ) {
776
			$iw = self::getInterwikiLookup()->fetch( $this->mInterwiki );
777
			if ( $iw ) {
778
				return $iw->isLocal();
779
			}
780
		}
781
		return true;
782
	}
783
784
	/**
785
	 * Is this Title interwiki?
786
	 *
787
	 * @return bool
788
	 */
789
	public function isExternal() {
790
		return $this->mInterwiki !== '';
791
	}
792
793
	/**
794
	 * Get the interwiki prefix
795
	 *
796
	 * Use Title::isExternal to check if a interwiki is set
797
	 *
798
	 * @return string Interwiki prefix
799
	 */
800
	public function getInterwiki() {
801
		return $this->mInterwiki;
802
	}
803
804
	/**
805
	 * Was this a local interwiki link?
806
	 *
807
	 * @return bool
808
	 */
809
	public function wasLocalInterwiki() {
810
		return $this->mLocalInterwiki;
811
	}
812
813
	/**
814
	 * Determine whether the object refers to a page within
815
	 * this project and is transcludable.
816
	 *
817
	 * @return bool True if this is transcludable
818
	 */
819
	public function isTrans() {
820
		if ( !$this->isExternal() ) {
821
			return false;
822
		}
823
824
		return self::getInterwikiLookup()->fetch( $this->mInterwiki )->isTranscludable();
825
	}
826
827
	/**
828
	 * Returns the DB name of the distant wiki which owns the object.
829
	 *
830
	 * @return string The DB name
831
	 */
832
	public function getTransWikiID() {
833
		if ( !$this->isExternal() ) {
834
			return false;
835
		}
836
837
		return self::getInterwikiLookup()->fetch( $this->mInterwiki )->getWikiID();
838
	}
839
840
	/**
841
	 * Get a TitleValue object representing this Title.
842
	 *
843
	 * @note Not all valid Titles have a corresponding valid TitleValue
844
	 * (e.g. TitleValues cannot represent page-local links that have a
845
	 * fragment but no title text).
846
	 *
847
	 * @return TitleValue|null
848
	 */
849
	public function getTitleValue() {
850
		if ( $this->mTitleValue === null ) {
851
			try {
852
				$this->mTitleValue = new TitleValue(
853
					$this->getNamespace(),
854
					$this->getDBkey(),
855
					$this->getFragment(),
856
					$this->getInterwiki()
857
				);
858
			} catch ( InvalidArgumentException $ex ) {
859
				wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' .
860
					$this->getPrefixedText() . ']]: ' . $ex->getMessage() . "\n" );
861
			}
862
		}
863
864
		return $this->mTitleValue;
865
	}
866
867
	/**
868
	 * Get the text form (spaces not underscores) of the main part
869
	 *
870
	 * @return string Main part of the title
871
	 */
872
	public function getText() {
873
		return $this->mTextform;
874
	}
875
876
	/**
877
	 * Get the URL-encoded form of the main part
878
	 *
879
	 * @return string Main part of the title, URL-encoded
880
	 */
881
	public function getPartialURL() {
882
		return $this->mUrlform;
883
	}
884
885
	/**
886
	 * Get the main part with underscores
887
	 *
888
	 * @return string Main part of the title, with underscores
889
	 */
890
	public function getDBkey() {
891
		return $this->mDbkeyform;
892
	}
893
894
	/**
895
	 * Get the DB key with the initial letter case as specified by the user
896
	 *
897
	 * @return string DB key
898
	 */
899
	function getUserCaseDBKey() {
900
		if ( !is_null( $this->mUserCaseDBKey ) ) {
901
			return $this->mUserCaseDBKey;
902
		} else {
903
			// If created via makeTitle(), $this->mUserCaseDBKey is not set.
904
			return $this->mDbkeyform;
905
		}
906
	}
907
908
	/**
909
	 * Get the namespace index, i.e. one of the NS_xxxx constants.
910
	 *
911
	 * @return int Namespace index
912
	 */
913
	public function getNamespace() {
914
		return $this->mNamespace;
915
	}
916
917
	/**
918
	 * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
919
	 *
920
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
921
	 * @return string Content model id
922
	 */
923
	public function getContentModel( $flags = 0 ) {
924 View Code Duplication
		if ( ( !$this->mContentModel || $flags === Title::GAID_FOR_UPDATE ) &&
925
			$this->getArticleID( $flags )
926
		) {
927
			$linkCache = LinkCache::singleton();
928
			$linkCache->addLinkObj( $this ); # in case we already had an article ID
929
			$this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
0 ignored issues
show
Documentation Bug introduced by
It seems like $linkCache->getGoodLinkFieldObj($this, 'model') can also be of type integer. However, the property $mContentModel is declared as type boolean|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
930
		}
931
932
		if ( !$this->mContentModel ) {
933
			$this->mContentModel = ContentHandler::getDefaultModelFor( $this );
934
		}
935
936
		return $this->mContentModel;
937
	}
938
939
	/**
940
	 * Convenience method for checking a title's content model name
941
	 *
942
	 * @param string $id The content model ID (use the CONTENT_MODEL_XXX constants).
943
	 * @return bool True if $this->getContentModel() == $id
944
	 */
945
	public function hasContentModel( $id ) {
946
		return $this->getContentModel() == $id;
947
	}
948
949
	/**
950
	 * Get the namespace text
951
	 *
952
	 * @return string Namespace text
953
	 */
954
	public function getNsText() {
955
		if ( $this->isExternal() ) {
956
			// This probably shouldn't even happen,
957
			// but for interwiki transclusion it sometimes does.
958
			// Use the canonical namespaces if possible to try to
959
			// resolve a foreign namespace.
960
			if ( MWNamespace::exists( $this->mNamespace ) ) {
961
				return MWNamespace::getCanonicalName( $this->mNamespace );
962
			}
963
		}
964
965
		try {
966
			$formatter = self::getTitleFormatter();
967
			return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform );
968
		} catch ( InvalidArgumentException $ex ) {
969
			wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" );
970
			return false;
971
		}
972
	}
973
974
	/**
975
	 * Get the namespace text of the subject (rather than talk) page
976
	 *
977
	 * @return string Namespace text
978
	 */
979
	public function getSubjectNsText() {
980
		global $wgContLang;
981
		return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) );
982
	}
983
984
	/**
985
	 * Get the namespace text of the talk page
986
	 *
987
	 * @return string Namespace text
988
	 */
989
	public function getTalkNsText() {
990
		global $wgContLang;
991
		return $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) );
992
	}
993
994
	/**
995
	 * Could this title have a corresponding talk page?
996
	 *
997
	 * @return bool
998
	 */
999
	public function canTalk() {
1000
		return MWNamespace::canTalk( $this->mNamespace );
1001
	}
1002
1003
	/**
1004
	 * Is this in a namespace that allows actual pages?
1005
	 *
1006
	 * @return bool
1007
	 */
1008
	public function canExist() {
1009
		return $this->mNamespace >= NS_MAIN;
1010
	}
1011
1012
	/**
1013
	 * Can this title be added to a user's watchlist?
1014
	 *
1015
	 * @return bool
1016
	 */
1017
	public function isWatchable() {
1018
		return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
1019
	}
1020
1021
	/**
1022
	 * Returns true if this is a special page.
1023
	 *
1024
	 * @return bool
1025
	 */
1026
	public function isSpecialPage() {
1027
		return $this->getNamespace() == NS_SPECIAL;
1028
	}
1029
1030
	/**
1031
	 * Returns true if this title resolves to the named special page
1032
	 *
1033
	 * @param string $name The special page name
1034
	 * @return bool
1035
	 */
1036
	public function isSpecial( $name ) {
1037
		if ( $this->isSpecialPage() ) {
1038
			list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() );
1039
			if ( $name == $thisName ) {
1040
				return true;
1041
			}
1042
		}
1043
		return false;
1044
	}
1045
1046
	/**
1047
	 * If the Title refers to a special page alias which is not the local default, resolve
1048
	 * the alias, and localise the name as necessary.  Otherwise, return $this
1049
	 *
1050
	 * @return Title
1051
	 */
1052
	public function fixSpecialName() {
1053
		if ( $this->isSpecialPage() ) {
1054
			list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform );
1055
			if ( $canonicalName ) {
1056
				$localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par );
1057
				if ( $localName != $this->mDbkeyform ) {
1058
					return Title::makeTitle( NS_SPECIAL, $localName );
1059
				}
1060
			}
1061
		}
1062
		return $this;
1063
	}
1064
1065
	/**
1066
	 * Returns true if the title is inside the specified namespace.
1067
	 *
1068
	 * Please make use of this instead of comparing to getNamespace()
1069
	 * This function is much more resistant to changes we may make
1070
	 * to namespaces than code that makes direct comparisons.
1071
	 * @param int $ns The namespace
1072
	 * @return bool
1073
	 * @since 1.19
1074
	 */
1075
	public function inNamespace( $ns ) {
1076
		return MWNamespace::equals( $this->getNamespace(), $ns );
1077
	}
1078
1079
	/**
1080
	 * Returns true if the title is inside one of the specified namespaces.
1081
	 *
1082
	 * @param int $namespaces,... The namespaces to check for
0 ignored issues
show
Bug introduced by
There is no parameter named $namespaces,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1083
	 * @return bool
1084
	 * @since 1.19
1085
	 */
1086
	public function inNamespaces( /* ... */ ) {
1087
		$namespaces = func_get_args();
1088
		if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
1089
			$namespaces = $namespaces[0];
1090
		}
1091
1092
		foreach ( $namespaces as $ns ) {
1093
			if ( $this->inNamespace( $ns ) ) {
1094
				return true;
1095
			}
1096
		}
1097
1098
		return false;
1099
	}
1100
1101
	/**
1102
	 * Returns true if the title has the same subject namespace as the
1103
	 * namespace specified.
1104
	 * For example this method will take NS_USER and return true if namespace
1105
	 * is either NS_USER or NS_USER_TALK since both of them have NS_USER
1106
	 * as their subject namespace.
1107
	 *
1108
	 * This is MUCH simpler than individually testing for equivalence
1109
	 * against both NS_USER and NS_USER_TALK, and is also forward compatible.
1110
	 * @since 1.19
1111
	 * @param int $ns
1112
	 * @return bool
1113
	 */
1114
	public function hasSubjectNamespace( $ns ) {
1115
		return MWNamespace::subjectEquals( $this->getNamespace(), $ns );
1116
	}
1117
1118
	/**
1119
	 * Is this Title in a namespace which contains content?
1120
	 * In other words, is this a content page, for the purposes of calculating
1121
	 * statistics, etc?
1122
	 *
1123
	 * @return bool
1124
	 */
1125
	public function isContentPage() {
1126
		return MWNamespace::isContent( $this->getNamespace() );
1127
	}
1128
1129
	/**
1130
	 * Would anybody with sufficient privileges be able to move this page?
1131
	 * Some pages just aren't movable.
1132
	 *
1133
	 * @return bool
1134
	 */
1135
	public function isMovable() {
1136
		if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->isExternal() ) {
1137
			// Interwiki title or immovable namespace. Hooks don't get to override here
1138
			return false;
1139
		}
1140
1141
		$result = true;
1142
		Hooks::run( 'TitleIsMovable', [ $this, &$result ] );
1143
		return $result;
1144
	}
1145
1146
	/**
1147
	 * Is this the mainpage?
1148
	 * @note Title::newFromText seems to be sufficiently optimized by the title
1149
	 * cache that we don't need to over-optimize by doing direct comparisons and
1150
	 * accidentally creating new bugs where $title->equals( Title::newFromText() )
1151
	 * ends up reporting something differently than $title->isMainPage();
1152
	 *
1153
	 * @since 1.18
1154
	 * @return bool
1155
	 */
1156
	public function isMainPage() {
1157
		return $this->equals( Title::newMainPage() );
0 ignored issues
show
Bug introduced by
It seems like \Title::newMainPage() can be null; however, equals() 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...
1158
	}
1159
1160
	/**
1161
	 * Is this a subpage?
1162
	 *
1163
	 * @return bool
1164
	 */
1165
	public function isSubpage() {
1166
		return MWNamespace::hasSubpages( $this->mNamespace )
1167
			? strpos( $this->getText(), '/' ) !== false
1168
			: false;
1169
	}
1170
1171
	/**
1172
	 * Is this a conversion table for the LanguageConverter?
1173
	 *
1174
	 * @return bool
1175
	 */
1176
	public function isConversionTable() {
1177
		// @todo ConversionTable should become a separate content model.
1178
1179
		return $this->getNamespace() == NS_MEDIAWIKI &&
1180
			strpos( $this->getText(), 'Conversiontable/' ) === 0;
1181
	}
1182
1183
	/**
1184
	 * Does that page contain wikitext, or it is JS, CSS or whatever?
1185
	 *
1186
	 * @return bool
1187
	 */
1188
	public function isWikitextPage() {
1189
		return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
1190
	}
1191
1192
	/**
1193
	 * Could this page contain custom CSS or JavaScript for the global UI.
1194
	 * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
1195
	 * or CONTENT_MODEL_JAVASCRIPT.
1196
	 *
1197
	 * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage()
1198
	 * for that!
1199
	 *
1200
	 * Note that this method should not return true for pages that contain and
1201
	 * show "inactive" CSS or JS.
1202
	 *
1203
	 * @return bool
1204
	 * @todo FIXME: Rename to isSiteConfigPage() and remove deprecated hook
1205
	 */
1206
	public function isCssOrJsPage() {
1207
		$isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
1208
			&& ( $this->hasContentModel( CONTENT_MODEL_CSS )
1209
				|| $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
1210
1211
		# @note This hook is also called in ContentHandler::getDefaultModel.
1212
		#   It's called here again to make sure hook functions can force this
1213
		#   method to return true even outside the MediaWiki namespace.
1214
1215
		Hooks::run( 'TitleIsCssOrJsPage', [ $this, &$isCssOrJsPage ], '1.25' );
1216
1217
		return $isCssOrJsPage;
1218
	}
1219
1220
	/**
1221
	 * Is this a .css or .js subpage of a user page?
1222
	 * @return bool
1223
	 * @todo FIXME: Rename to isUserConfigPage()
1224
	 */
1225
	public function isCssJsSubpage() {
1226
		return ( NS_USER == $this->mNamespace && $this->isSubpage()
1227
				&& ( $this->hasContentModel( CONTENT_MODEL_CSS )
1228
					|| $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
1229
	}
1230
1231
	/**
1232
	 * Trim down a .css or .js subpage title to get the corresponding skin name
1233
	 *
1234
	 * @return string Containing skin name from .css or .js subpage title
1235
	 */
1236
	public function getSkinFromCssJsSubpage() {
1237
		$subpage = explode( '/', $this->mTextform );
1238
		$subpage = $subpage[count( $subpage ) - 1];
1239
		$lastdot = strrpos( $subpage, '.' );
1240
		if ( $lastdot === false ) {
1241
			return $subpage; # Never happens: only called for names ending in '.css' or '.js'
1242
		}
1243
		return substr( $subpage, 0, $lastdot );
1244
	}
1245
1246
	/**
1247
	 * Is this a .css subpage of a user page?
1248
	 *
1249
	 * @return bool
1250
	 */
1251
	public function isCssSubpage() {
1252
		return ( NS_USER == $this->mNamespace && $this->isSubpage()
1253
			&& $this->hasContentModel( CONTENT_MODEL_CSS ) );
1254
	}
1255
1256
	/**
1257
	 * Is this a .js subpage of a user page?
1258
	 *
1259
	 * @return bool
1260
	 */
1261
	public function isJsSubpage() {
1262
		return ( NS_USER == $this->mNamespace && $this->isSubpage()
1263
			&& $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
1264
	}
1265
1266
	/**
1267
	 * Is this a talk page of some sort?
1268
	 *
1269
	 * @return bool
1270
	 */
1271
	public function isTalkPage() {
1272
		return MWNamespace::isTalk( $this->getNamespace() );
1273
	}
1274
1275
	/**
1276
	 * Get a Title object associated with the talk page of this article
1277
	 *
1278
	 * @return Title The object for the talk page
1279
	 */
1280
	public function getTalkPage() {
1281
		return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
1282
	}
1283
1284
	/**
1285
	 * Get a title object associated with the subject page of this
1286
	 * talk page
1287
	 *
1288
	 * @return Title The object for the subject page
1289
	 */
1290
	public function getSubjectPage() {
1291
		// Is this the same title?
1292
		$subjectNS = MWNamespace::getSubject( $this->getNamespace() );
1293
		if ( $this->getNamespace() == $subjectNS ) {
1294
			return $this;
1295
		}
1296
		return Title::makeTitle( $subjectNS, $this->getDBkey() );
1297
	}
1298
1299
	/**
1300
	 * Get the other title for this page, if this is a subject page
1301
	 * get the talk page, if it is a subject page get the talk page
1302
	 *
1303
	 * @since 1.25
1304
	 * @throws MWException
1305
	 * @return Title
1306
	 */
1307
	public function getOtherPage() {
1308
		if ( $this->isSpecialPage() ) {
1309
			throw new MWException( 'Special pages cannot have other pages' );
1310
		}
1311
		if ( $this->isTalkPage() ) {
1312
			return $this->getSubjectPage();
1313
		} else {
1314
			return $this->getTalkPage();
1315
		}
1316
	}
1317
1318
	/**
1319
	 * Get the default namespace index, for when there is no namespace
1320
	 *
1321
	 * @return int Default namespace index
1322
	 */
1323
	public function getDefaultNamespace() {
1324
		return $this->mDefaultNamespace;
1325
	}
1326
1327
	/**
1328
	 * Get the Title fragment (i.e.\ the bit after the #) in text form
1329
	 *
1330
	 * Use Title::hasFragment to check for a fragment
1331
	 *
1332
	 * @return string Title fragment
1333
	 */
1334
	public function getFragment() {
1335
		return $this->mFragment;
1336
	}
1337
1338
	/**
1339
	 * Check if a Title fragment is set
1340
	 *
1341
	 * @return bool
1342
	 * @since 1.23
1343
	 */
1344
	public function hasFragment() {
1345
		return $this->mFragment !== '';
1346
	}
1347
1348
	/**
1349
	 * Get the fragment in URL form, including the "#" character if there is one
1350
	 * @return string Fragment in URL form
1351
	 */
1352
	public function getFragmentForURL() {
1353
		if ( !$this->hasFragment() ) {
1354
			return '';
1355
		} else {
1356
			return '#' . Title::escapeFragmentForURL( $this->getFragment() );
1357
		}
1358
	}
1359
1360
	/**
1361
	 * Set the fragment for this title. Removes the first character from the
1362
	 * specified fragment before setting, so it assumes you're passing it with
1363
	 * an initial "#".
1364
	 *
1365
	 * Deprecated for public use, use Title::makeTitle() with fragment parameter,
1366
	 * or Title::createFragmentTarget().
1367
	 * Still in active use privately.
1368
	 *
1369
	 * @private
1370
	 * @param string $fragment Text
1371
	 */
1372
	public function setFragment( $fragment ) {
1373
		$this->mFragment = strtr( substr( $fragment, 1 ), '_', ' ' );
1374
	}
1375
1376
	/**
1377
	 * Creates a new Title for a different fragment of the same page.
1378
	 *
1379
	 * @since 1.27
1380
	 * @param string $fragment
1381
	 * @return Title
1382
	 */
1383
	public function createFragmentTarget( $fragment ) {
1384
		return self::makeTitle(
1385
			$this->getNamespace(),
1386
			$this->getText(),
1387
			$fragment,
1388
			$this->getInterwiki()
1389
		);
1390
1391
	}
1392
1393
	/**
1394
	 * Prefix some arbitrary text with the namespace or interwiki prefix
1395
	 * of this object
1396
	 *
1397
	 * @param string $name The text
1398
	 * @return string The prefixed text
1399
	 */
1400
	private function prefix( $name ) {
1401
		$p = '';
1402
		if ( $this->isExternal() ) {
1403
			$p = $this->mInterwiki . ':';
1404
		}
1405
1406
		if ( 0 != $this->mNamespace ) {
1407
			$p .= $this->getNsText() . ':';
1408
		}
1409
		return $p . $name;
1410
	}
1411
1412
	/**
1413
	 * Get the prefixed database key form
1414
	 *
1415
	 * @return string The prefixed title, with underscores and
1416
	 *  any interwiki and namespace prefixes
1417
	 */
1418
	public function getPrefixedDBkey() {
1419
		$s = $this->prefix( $this->mDbkeyform );
1420
		$s = strtr( $s, ' ', '_' );
1421
		return $s;
1422
	}
1423
1424
	/**
1425
	 * Get the prefixed title with spaces.
1426
	 * This is the form usually used for display
1427
	 *
1428
	 * @return string The prefixed title, with spaces
1429
	 */
1430
	public function getPrefixedText() {
1431
		if ( $this->mPrefixedText === null ) {
1432
			$s = $this->prefix( $this->mTextform );
1433
			$s = strtr( $s, '_', ' ' );
1434
			$this->mPrefixedText = $s;
1435
		}
1436
		return $this->mPrefixedText;
1437
	}
1438
1439
	/**
1440
	 * Return a string representation of this title
1441
	 *
1442
	 * @return string Representation of this title
1443
	 */
1444
	public function __toString() {
1445
		return $this->getPrefixedText();
1446
	}
1447
1448
	/**
1449
	 * Get the prefixed title with spaces, plus any fragment
1450
	 * (part beginning with '#')
1451
	 *
1452
	 * @return string The prefixed title, with spaces and the fragment, including '#'
1453
	 */
1454
	public function getFullText() {
1455
		$text = $this->getPrefixedText();
1456
		if ( $this->hasFragment() ) {
1457
			$text .= '#' . $this->getFragment();
1458
		}
1459
		return $text;
1460
	}
1461
1462
	/**
1463
	 * Get the root page name text without a namespace, i.e. the leftmost part before any slashes
1464
	 *
1465
	 * @par Example:
1466
	 * @code
1467
	 * Title::newFromText('User:Foo/Bar/Baz')->getRootText();
1468
	 * # returns: 'Foo'
1469
	 * @endcode
1470
	 *
1471
	 * @return string Root name
1472
	 * @since 1.20
1473
	 */
1474
	public function getRootText() {
1475
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1476
			return $this->getText();
1477
		}
1478
1479
		return strtok( $this->getText(), '/' );
1480
	}
1481
1482
	/**
1483
	 * Get the root page name title, i.e. the leftmost part before any slashes
1484
	 *
1485
	 * @par Example:
1486
	 * @code
1487
	 * Title::newFromText('User:Foo/Bar/Baz')->getRootTitle();
1488
	 * # returns: Title{User:Foo}
1489
	 * @endcode
1490
	 *
1491
	 * @return Title Root title
1492
	 * @since 1.20
1493
	 */
1494
	public function getRootTitle() {
1495
		return Title::makeTitle( $this->getNamespace(), $this->getRootText() );
1496
	}
1497
1498
	/**
1499
	 * Get the base page name without a namespace, i.e. the part before the subpage name
1500
	 *
1501
	 * @par Example:
1502
	 * @code
1503
	 * Title::newFromText('User:Foo/Bar/Baz')->getBaseText();
1504
	 * # returns: 'Foo/Bar'
1505
	 * @endcode
1506
	 *
1507
	 * @return string Base name
1508
	 */
1509
	public function getBaseText() {
1510
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1511
			return $this->getText();
1512
		}
1513
1514
		$parts = explode( '/', $this->getText() );
1515
		# Don't discard the real title if there's no subpage involved
1516
		if ( count( $parts ) > 1 ) {
1517
			unset( $parts[count( $parts ) - 1] );
1518
		}
1519
		return implode( '/', $parts );
1520
	}
1521
1522
	/**
1523
	 * Get the base page name title, i.e. the part before the subpage name
1524
	 *
1525
	 * @par Example:
1526
	 * @code
1527
	 * Title::newFromText('User:Foo/Bar/Baz')->getBaseTitle();
1528
	 * # returns: Title{User:Foo/Bar}
1529
	 * @endcode
1530
	 *
1531
	 * @return Title Base title
1532
	 * @since 1.20
1533
	 */
1534
	public function getBaseTitle() {
1535
		return Title::makeTitle( $this->getNamespace(), $this->getBaseText() );
1536
	}
1537
1538
	/**
1539
	 * Get the lowest-level subpage name, i.e. the rightmost part after any slashes
1540
	 *
1541
	 * @par Example:
1542
	 * @code
1543
	 * Title::newFromText('User:Foo/Bar/Baz')->getSubpageText();
1544
	 * # returns: "Baz"
1545
	 * @endcode
1546
	 *
1547
	 * @return string Subpage name
1548
	 */
1549
	public function getSubpageText() {
1550
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1551
			return $this->mTextform;
1552
		}
1553
		$parts = explode( '/', $this->mTextform );
1554
		return $parts[count( $parts ) - 1];
1555
	}
1556
1557
	/**
1558
	 * Get the title for a subpage of the current page
1559
	 *
1560
	 * @par Example:
1561
	 * @code
1562
	 * Title::newFromText('User:Foo/Bar/Baz')->getSubpage("Asdf");
1563
	 * # returns: Title{User:Foo/Bar/Baz/Asdf}
1564
	 * @endcode
1565
	 *
1566
	 * @param string $text The subpage name to add to the title
1567
	 * @return Title Subpage title
1568
	 * @since 1.20
1569
	 */
1570
	public function getSubpage( $text ) {
1571
		return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text );
1572
	}
1573
1574
	/**
1575
	 * Get a URL-encoded form of the subpage text
1576
	 *
1577
	 * @return string URL-encoded subpage name
1578
	 */
1579
	public function getSubpageUrlForm() {
1580
		$text = $this->getSubpageText();
1581
		$text = wfUrlencode( strtr( $text, ' ', '_' ) );
1582
		return $text;
1583
	}
1584
1585
	/**
1586
	 * Get a URL-encoded title (not an actual URL) including interwiki
1587
	 *
1588
	 * @return string The URL-encoded form
1589
	 */
1590
	public function getPrefixedURL() {
1591
		$s = $this->prefix( $this->mDbkeyform );
1592
		$s = wfUrlencode( strtr( $s, ' ', '_' ) );
1593
		return $s;
1594
	}
1595
1596
	/**
1597
	 * Helper to fix up the get{Canonical,Full,Link,Local,Internal}URL args
1598
	 * get{Canonical,Full,Link,Local,Internal}URL methods accepted an optional
1599
	 * second argument named variant. This was deprecated in favor
1600
	 * of passing an array of option with a "variant" key
1601
	 * Once $query2 is removed for good, this helper can be dropped
1602
	 * and the wfArrayToCgi moved to getLocalURL();
1603
	 *
1604
	 * @since 1.19 (r105919)
1605
	 * @param array|string $query
1606
	 * @param bool $query2
1607
	 * @return string
1608
	 */
1609
	private static function fixUrlQueryArgs( $query, $query2 = false ) {
1610
		if ( $query2 !== false ) {
1611
			wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " .
1612
				"method called with a second parameter is deprecated. Add your " .
1613
				"parameter to an array passed as the first parameter.", "1.19" );
1614
		}
1615
		if ( is_array( $query ) ) {
1616
			$query = wfArrayToCgi( $query );
1617
		}
1618
		if ( $query2 ) {
1619
			if ( is_string( $query2 ) ) {
1620
				// $query2 is a string, we will consider this to be
1621
				// a deprecated $variant argument and add it to the query
1622
				$query2 = wfArrayToCgi( [ 'variant' => $query2 ] );
1623
			} else {
1624
				$query2 = wfArrayToCgi( $query2 );
1625
			}
1626
			// If we have $query content add a & to it first
1627
			if ( $query ) {
1628
				$query .= '&';
1629
			}
1630
			// Now append the queries together
1631
			$query .= $query2;
1632
		}
1633
		return $query;
1634
	}
1635
1636
	/**
1637
	 * Get a real URL referring to this title, with interwiki link and
1638
	 * fragment
1639
	 *
1640
	 * @see self::getLocalURL for the arguments.
1641
	 * @see wfExpandUrl
1642
	 * @param array|string $query
1643
	 * @param bool $query2
1644
	 * @param string $proto Protocol type to use in URL
1645
	 * @return string The URL
1646
	 */
1647
	public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
1648
		$query = self::fixUrlQueryArgs( $query, $query2 );
1649
1650
		# Hand off all the decisions on urls to getLocalURL
1651
		$url = $this->getLocalURL( $query );
1652
1653
		# Expand the url to make it a full url. Note that getLocalURL has the
1654
		# potential to output full urls for a variety of reasons, so we use
1655
		# wfExpandUrl instead of simply prepending $wgServer
1656
		$url = wfExpandUrl( $url, $proto );
1657
1658
		# Finally, add the fragment.
1659
		$url .= $this->getFragmentForURL();
1660
1661
		Hooks::run( 'GetFullURL', [ &$this, &$url, $query ] );
1662
		return $url;
1663
	}
1664
1665
	/**
1666
	 * Get a URL with no fragment or server name (relative URL) from a Title object.
1667
	 * If this page is generated with action=render, however,
1668
	 * $wgServer is prepended to make an absolute URL.
1669
	 *
1670
	 * @see self::getFullURL to always get an absolute URL.
1671
	 * @see self::getLinkURL to always get a URL that's the simplest URL that will be
1672
	 *  valid to link, locally, to the current Title.
1673
	 * @see self::newFromText to produce a Title object.
1674
	 *
1675
	 * @param string|array $query An optional query string,
1676
	 *   not used for interwiki links. Can be specified as an associative array as well,
1677
	 *   e.g., array( 'action' => 'edit' ) (keys and values will be URL-escaped).
1678
	 *   Some query patterns will trigger various shorturl path replacements.
1679
	 * @param array $query2 An optional secondary query array. This one MUST
1680
	 *   be an array. If a string is passed it will be interpreted as a deprecated
1681
	 *   variant argument and urlencoded into a variant= argument.
1682
	 *   This second query argument will be added to the $query
1683
	 *   The second parameter is deprecated since 1.19. Pass it as a key,value
1684
	 *   pair in the first parameter array instead.
1685
	 *
1686
	 * @return string String of the URL.
1687
	 */
1688
	public function getLocalURL( $query = '', $query2 = false ) {
1689
		global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
1690
1691
		$query = self::fixUrlQueryArgs( $query, $query2 );
0 ignored issues
show
Bug introduced by
It seems like $query2 defined by parameter $query2 on line 1688 can also be of type array; however, Title::fixUrlQueryArgs() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and 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...
1692
1693
		$interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki );
1694
		if ( $interwiki ) {
1695
			$namespace = $this->getNsText();
1696
			if ( $namespace != '' ) {
1697
				# Can this actually happen? Interwikis shouldn't be parsed.
1698
				# Yes! It can in interwiki transclusion. But... it probably shouldn't.
1699
				$namespace .= ':';
1700
			}
1701
			$url = $interwiki->getURL( $namespace . $this->getDBkey() );
1702
			$url = wfAppendQuery( $url, $query );
1703
		} else {
1704
			$dbkey = wfUrlencode( $this->getPrefixedDBkey() );
1705
			if ( $query == '' ) {
1706
				$url = str_replace( '$1', $dbkey, $wgArticlePath );
1707
				Hooks::run( 'GetLocalURL::Article', [ &$this, &$url ] );
1708
			} else {
1709
				global $wgVariantArticlePath, $wgActionPaths, $wgContLang;
1710
				$url = false;
1711
				$matches = [];
1712
1713
				if ( !empty( $wgActionPaths )
1714
					&& preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches )
1715
				) {
1716
					$action = urldecode( $matches[2] );
1717
					if ( isset( $wgActionPaths[$action] ) ) {
1718
						$query = $matches[1];
1719
						if ( isset( $matches[4] ) ) {
1720
							$query .= $matches[4];
1721
						}
1722
						$url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
1723
						if ( $query != '' ) {
1724
							$url = wfAppendQuery( $url, $query );
1725
						}
1726
					}
1727
				}
1728
1729
				if ( $url === false
1730
					&& $wgVariantArticlePath
1731
					&& preg_match( '/^variant=([^&]*)$/', $query, $matches )
1732
					&& $this->getPageLanguage()->equals( $wgContLang )
1733
					&& $this->getPageLanguage()->hasVariants()
1734
				) {
1735
					$variant = urldecode( $matches[1] );
1736
					if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
1737
						// Only do the variant replacement if the given variant is a valid
1738
						// variant for the page's language.
1739
						$url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath );
1740
						$url = str_replace( '$1', $dbkey, $url );
1741
					}
1742
				}
1743
1744
				if ( $url === false ) {
1745
					if ( $query == '-' ) {
1746
						$query = '';
1747
					}
1748
					$url = "{$wgScript}?title={$dbkey}&{$query}";
1749
				}
1750
			}
1751
1752
			Hooks::run( 'GetLocalURL::Internal', [ &$this, &$url, $query ] );
1753
1754
			// @todo FIXME: This causes breakage in various places when we
1755
			// actually expected a local URL and end up with dupe prefixes.
1756
			if ( $wgRequest->getVal( 'action' ) == 'render' ) {
1757
				$url = $wgServer . $url;
1758
			}
1759
		}
1760
		Hooks::run( 'GetLocalURL', [ &$this, &$url, $query ] );
1761
		return $url;
1762
	}
1763
1764
	/**
1765
	 * Get a URL that's the simplest URL that will be valid to link, locally,
1766
	 * to the current Title.  It includes the fragment, but does not include
1767
	 * the server unless action=render is used (or the link is external).  If
1768
	 * there's a fragment but the prefixed text is empty, we just return a link
1769
	 * to the fragment.
1770
	 *
1771
	 * The result obviously should not be URL-escaped, but does need to be
1772
	 * HTML-escaped if it's being output in HTML.
1773
	 *
1774
	 * @param array $query
1775
	 * @param bool $query2
1776
	 * @param string|int|bool $proto A PROTO_* constant on how the URL should be expanded,
1777
	 *                               or false (default) for no expansion
1778
	 * @see self::getLocalURL for the arguments.
1779
	 * @return string The URL
1780
	 */
1781
	public function getLinkURL( $query = '', $query2 = false, $proto = false ) {
1782
		if ( $this->isExternal() || $proto !== false ) {
1783
			$ret = $this->getFullURL( $query, $query2, $proto );
1784
		} elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) {
1785
			$ret = $this->getFragmentForURL();
1786
		} else {
1787
			$ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
1788
		}
1789
		return $ret;
1790
	}
1791
1792
	/**
1793
	 * Get the URL form for an internal link.
1794
	 * - Used in various CDN-related code, in case we have a different
1795
	 * internal hostname for the server from the exposed one.
1796
	 *
1797
	 * This uses $wgInternalServer to qualify the path, or $wgServer
1798
	 * if $wgInternalServer is not set. If the server variable used is
1799
	 * protocol-relative, the URL will be expanded to http://
1800
	 *
1801
	 * @see self::getLocalURL for the arguments.
1802
	 * @return string The URL
1803
	 */
1804
	public function getInternalURL( $query = '', $query2 = false ) {
1805
		global $wgInternalServer, $wgServer;
1806
		$query = self::fixUrlQueryArgs( $query, $query2 );
1807
		$server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
1808
		$url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
1809
		Hooks::run( 'GetInternalURL', [ &$this, &$url, $query ] );
1810
		return $url;
1811
	}
1812
1813
	/**
1814
	 * Get the URL for a canonical link, for use in things like IRC and
1815
	 * e-mail notifications. Uses $wgCanonicalServer and the
1816
	 * GetCanonicalURL hook.
1817
	 *
1818
	 * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment
1819
	 *
1820
	 * @see self::getLocalURL for the arguments.
1821
	 * @return string The URL
1822
	 * @since 1.18
1823
	 */
1824
	public function getCanonicalURL( $query = '', $query2 = false ) {
1825
		$query = self::fixUrlQueryArgs( $query, $query2 );
1826
		$url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
1827
		Hooks::run( 'GetCanonicalURL', [ &$this, &$url, $query ] );
1828
		return $url;
1829
	}
1830
1831
	/**
1832
	 * Get the edit URL for this Title
1833
	 *
1834
	 * @return string The URL, or a null string if this is an interwiki link
1835
	 */
1836
	public function getEditURL() {
1837
		if ( $this->isExternal() ) {
1838
			return '';
1839
		}
1840
		$s = $this->getLocalURL( 'action=edit' );
1841
1842
		return $s;
1843
	}
1844
1845
	/**
1846
	 * Can $user perform $action on this page?
1847
	 * This skips potentially expensive cascading permission checks
1848
	 * as well as avoids expensive error formatting
1849
	 *
1850
	 * Suitable for use for nonessential UI controls in common cases, but
1851
	 * _not_ for functional access control.
1852
	 *
1853
	 * May provide false positives, but should never provide a false negative.
1854
	 *
1855
	 * @param string $action Action that permission needs to be checked for
1856
	 * @param User $user User to check (since 1.19); $wgUser will be used if not provided.
1857
	 * @return bool
1858
	 */
1859
	public function quickUserCan( $action, $user = null ) {
1860
		return $this->userCan( $action, $user, false );
1861
	}
1862
1863
	/**
1864
	 * Can $user perform $action on this page?
1865
	 *
1866
	 * @param string $action Action that permission needs to be checked for
1867
	 * @param User $user User to check (since 1.19); $wgUser will be used if not
1868
	 *   provided.
1869
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
1870
	 * @return bool
1871
	 */
1872
	public function userCan( $action, $user = null, $rigor = 'secure' ) {
1873
		if ( !$user instanceof User ) {
1874
			global $wgUser;
1875
			$user = $wgUser;
1876
		}
1877
1878
		return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) );
1879
	}
1880
1881
	/**
1882
	 * Can $user perform $action on this page?
1883
	 *
1884
	 * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
1885
	 *
1886
	 * @param string $action Action that permission needs to be checked for
1887
	 * @param User $user User to check
1888
	 * @param string $rigor One of (quick,full,secure)
1889
	 *   - quick  : does cheap permission checks from slaves (usable for GUI creation)
1890
	 *   - full   : does cheap and expensive checks possibly from a slave
1891
	 *   - secure : does cheap and expensive checks, using the master as needed
1892
	 * @param array $ignoreErrors Array of Strings Set this to a list of message keys
1893
	 *   whose corresponding errors may be ignored.
1894
	 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
1895
	 */
1896
	public function getUserPermissionsErrors(
1897
		$action, $user, $rigor = 'secure', $ignoreErrors = []
1898
	) {
1899
		$errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor );
1900
1901
		// Remove the errors being ignored.
1902
		foreach ( $errors as $index => $error ) {
1903
			$errKey = is_array( $error ) ? $error[0] : $error;
1904
1905
			if ( in_array( $errKey, $ignoreErrors ) ) {
1906
				unset( $errors[$index] );
1907
			}
1908
			if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
1909
				unset( $errors[$index] );
1910
			}
1911
		}
1912
1913
		return $errors;
1914
	}
1915
1916
	/**
1917
	 * Permissions checks that fail most often, and which are easiest to test.
1918
	 *
1919
	 * @param string $action The action to check
1920
	 * @param User $user User to check
1921
	 * @param array $errors List of current errors
1922
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
1923
	 * @param bool $short Short circuit on first error
1924
	 *
1925
	 * @return array List of errors
1926
	 */
1927
	private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) {
1928
		if ( !Hooks::run( 'TitleQuickPermissions',
1929
			[ $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ] )
1930
		) {
1931
			return $errors;
1932
		}
1933
1934
		if ( $action == 'create' ) {
1935
			if (
1936
				( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
1937
				( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
1938
			) {
1939
				$errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
1940
			}
1941
		} elseif ( $action == 'move' ) {
1942 View Code Duplication
			if ( !$user->isAllowed( 'move-rootuserpages' )
1943
					&& $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1944
				// Show user page-specific message only if the user can move other pages
1945
				$errors[] = [ 'cant-move-user-page' ];
1946
			}
1947
1948
			// Check if user is allowed to move files if it's a file
1949
			if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
1950
				$errors[] = [ 'movenotallowedfile' ];
1951
			}
1952
1953
			// Check if user is allowed to move category pages if it's a category page
1954
			if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
1955
				$errors[] = [ 'cant-move-category-page' ];
1956
			}
1957
1958
			if ( !$user->isAllowed( 'move' ) ) {
1959
				// User can't move anything
1960
				$userCanMove = User::groupHasPermission( 'user', 'move' );
1961
				$autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
1962
				if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
1963
					// custom message if logged-in users without any special rights can move
1964
					$errors[] = [ 'movenologintext' ];
1965
				} else {
1966
					$errors[] = [ 'movenotallowed' ];
1967
				}
1968
			}
1969
		} elseif ( $action == 'move-target' ) {
1970
			if ( !$user->isAllowed( 'move' ) ) {
1971
				// User can't move anything
1972
				$errors[] = [ 'movenotallowed' ];
1973 View Code Duplication
			} elseif ( !$user->isAllowed( 'move-rootuserpages' )
1974
					&& $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1975
				// Show user page-specific message only if the user can move other pages
1976
				$errors[] = [ 'cant-move-to-user-page' ];
1977
			} elseif ( !$user->isAllowed( 'move-categorypages' )
1978
					&& $this->mNamespace == NS_CATEGORY ) {
1979
				// Show category page-specific message only if the user can move other pages
1980
				$errors[] = [ 'cant-move-to-category-page' ];
1981
			}
1982
		} elseif ( !$user->isAllowed( $action ) ) {
1983
			$errors[] = $this->missingPermissionError( $action, $short );
1984
		}
1985
1986
		return $errors;
1987
	}
1988
1989
	/**
1990
	 * Add the resulting error code to the errors array
1991
	 *
1992
	 * @param array $errors List of current errors
1993
	 * @param array $result Result of errors
1994
	 *
1995
	 * @return array List of errors
1996
	 */
1997
	private function resultToError( $errors, $result ) {
1998
		if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
1999
			// A single array representing an error
2000
			$errors[] = $result;
2001
		} elseif ( is_array( $result ) && is_array( $result[0] ) ) {
2002
			// A nested array representing multiple errors
2003
			$errors = array_merge( $errors, $result );
2004
		} elseif ( $result !== '' && is_string( $result ) ) {
2005
			// A string representing a message-id
2006
			$errors[] = [ $result ];
2007
		} elseif ( $result instanceof MessageSpecifier ) {
2008
			// A message specifier representing an error
2009
			$errors[] = [ $result ];
2010
		} elseif ( $result === false ) {
2011
			// a generic "We don't want them to do that"
2012
			$errors[] = [ 'badaccess-group0' ];
2013
		}
2014
		return $errors;
2015
	}
2016
2017
	/**
2018
	 * Check various permission hooks
2019
	 *
2020
	 * @param string $action The action to check
2021
	 * @param User $user User to check
2022
	 * @param array $errors List of current errors
2023
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2024
	 * @param bool $short Short circuit on first error
2025
	 *
2026
	 * @return array List of errors
2027
	 */
2028
	private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) {
2029
		// Use getUserPermissionsErrors instead
2030
		$result = '';
2031
		if ( !Hooks::run( 'userCan', [ &$this, &$user, $action, &$result ] ) ) {
2032
			return $result ? [] : [ [ 'badaccess-group0' ] ];
2033
		}
2034
		// Check getUserPermissionsErrors hook
2035
		if ( !Hooks::run( 'getUserPermissionsErrors', [ &$this, &$user, $action, &$result ] ) ) {
2036
			$errors = $this->resultToError( $errors, $result );
2037
		}
2038
		// Check getUserPermissionsErrorsExpensive hook
2039
		if (
2040
			$rigor !== 'quick'
2041
			&& !( $short && count( $errors ) > 0 )
2042
			&& !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$this, &$user, $action, &$result ] )
2043
		) {
2044
			$errors = $this->resultToError( $errors, $result );
2045
		}
2046
2047
		return $errors;
2048
	}
2049
2050
	/**
2051
	 * Check permissions on special pages & namespaces
2052
	 *
2053
	 * @param string $action The action to check
2054
	 * @param User $user User to check
2055
	 * @param array $errors List of current errors
2056
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2057
	 * @param bool $short Short circuit on first error
2058
	 *
2059
	 * @return array List of errors
2060
	 */
2061
	private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) {
2062
		# Only 'createaccount' can be performed on special pages,
2063
		# which don't actually exist in the DB.
2064
		if ( NS_SPECIAL == $this->mNamespace && $action !== 'createaccount' ) {
2065
			$errors[] = [ 'ns-specialprotected' ];
2066
		}
2067
2068
		# Check $wgNamespaceProtection for restricted namespaces
2069
		if ( $this->isNamespaceProtected( $user ) ) {
2070
			$ns = $this->mNamespace == NS_MAIN ?
2071
				wfMessage( 'nstab-main' )->text() : $this->getNsText();
2072
			$errors[] = $this->mNamespace == NS_MEDIAWIKI ?
2073
				[ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
2074
		}
2075
2076
		return $errors;
2077
	}
2078
2079
	/**
2080
	 * Check CSS/JS sub-page permissions
2081
	 *
2082
	 * @param string $action The action to check
2083
	 * @param User $user User to check
2084
	 * @param array $errors List of current errors
2085
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2086
	 * @param bool $short Short circuit on first error
2087
	 *
2088
	 * @return array List of errors
2089
	 */
2090
	private function checkCSSandJSPermissions( $action, $user, $errors, $rigor, $short ) {
2091
		# Protect css/js subpages of user pages
2092
		# XXX: this might be better using restrictions
2093
		# XXX: right 'editusercssjs' is deprecated, for backward compatibility only
2094
		if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) {
2095
			if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
2096 View Code Duplication
				if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) {
2097
					$errors[] = [ 'mycustomcssprotected', $action ];
2098
				} elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) {
2099
					$errors[] = [ 'mycustomjsprotected', $action ];
2100
				}
2101 View Code Duplication
			} else {
2102
				if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) {
2103
					$errors[] = [ 'customcssprotected', $action ];
2104
				} elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
2105
					$errors[] = [ 'customjsprotected', $action ];
2106
				}
2107
			}
2108
		}
2109
2110
		return $errors;
2111
	}
2112
2113
	/**
2114
	 * Check against page_restrictions table requirements on this
2115
	 * page. The user must possess all required rights for this
2116
	 * action.
2117
	 *
2118
	 * @param string $action The action to check
2119
	 * @param User $user User to check
2120
	 * @param array $errors List of current errors
2121
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2122
	 * @param bool $short Short circuit on first error
2123
	 *
2124
	 * @return array List of errors
2125
	 */
2126
	private function checkPageRestrictions( $action, $user, $errors, $rigor, $short ) {
2127
		foreach ( $this->getRestrictions( $action ) as $right ) {
2128
			// Backwards compatibility, rewrite sysop -> editprotected
2129
			if ( $right == 'sysop' ) {
2130
				$right = 'editprotected';
2131
			}
2132
			// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2133
			if ( $right == 'autoconfirmed' ) {
2134
				$right = 'editsemiprotected';
2135
			}
2136
			if ( $right == '' ) {
2137
				continue;
2138
			}
2139
			if ( !$user->isAllowed( $right ) ) {
2140
				$errors[] = [ 'protectedpagetext', $right, $action ];
2141
			} elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
2142
				$errors[] = [ 'protectedpagetext', 'protect', $action ];
2143
			}
2144
		}
2145
2146
		return $errors;
2147
	}
2148
2149
	/**
2150
	 * Check restrictions on cascading pages.
2151
	 *
2152
	 * @param string $action The action to check
2153
	 * @param User $user User to check
2154
	 * @param array $errors List of current errors
2155
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2156
	 * @param bool $short Short circuit on first error
2157
	 *
2158
	 * @return array List of errors
2159
	 */
2160
	private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
2161
		if ( $rigor !== 'quick' && !$this->isCssJsSubpage() ) {
2162
			# We /could/ use the protection level on the source page, but it's
2163
			# fairly ugly as we have to establish a precedence hierarchy for pages
2164
			# included by multiple cascade-protected pages. So just restrict
2165
			# it to people with 'protect' permission, as they could remove the
2166
			# protection anyway.
2167
			list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
2168
			# Cascading protection depends on more than this page...
2169
			# Several cascading protected pages may include this page...
2170
			# Check each cascading level
2171
			# This is only for protection restrictions, not for all actions
2172
			if ( isset( $restrictions[$action] ) ) {
2173
				foreach ( $restrictions[$action] as $right ) {
2174
					// Backwards compatibility, rewrite sysop -> editprotected
2175
					if ( $right == 'sysop' ) {
2176
						$right = 'editprotected';
2177
					}
2178
					// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2179
					if ( $right == 'autoconfirmed' ) {
2180
						$right = 'editsemiprotected';
2181
					}
2182
					if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
2183
						$pages = '';
2184
						foreach ( $cascadingSources as $page ) {
0 ignored issues
show
Bug introduced by
The expression $cascadingSources of type array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
2185
							$pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
2186
						}
2187
						$errors[] = [ 'cascadeprotected', count( $cascadingSources ), $pages, $action ];
2188
					}
2189
				}
2190
			}
2191
		}
2192
2193
		return $errors;
2194
	}
2195
2196
	/**
2197
	 * Check action permissions not already checked in checkQuickPermissions
2198
	 *
2199
	 * @param string $action The action to check
2200
	 * @param User $user User to check
2201
	 * @param array $errors List of current errors
2202
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2203
	 * @param bool $short Short circuit on first error
2204
	 *
2205
	 * @return array List of errors
2206
	 */
2207
	private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) {
2208
		global $wgDeleteRevisionsLimit, $wgLang;
2209
2210
		if ( $action == 'protect' ) {
2211
			if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
2212
				// If they can't edit, they shouldn't protect.
2213
				$errors[] = [ 'protect-cantedit' ];
2214
			}
2215
		} elseif ( $action == 'create' ) {
2216
			$title_protection = $this->getTitleProtection();
2217
			if ( $title_protection ) {
2218
				if ( $title_protection['permission'] == ''
2219
					|| !$user->isAllowed( $title_protection['permission'] )
2220
				) {
2221
					$errors[] = [
2222
						'titleprotected',
2223
						User::whoIs( $title_protection['user'] ),
2224
						$title_protection['reason']
2225
					];
2226
				}
2227
			}
2228
		} elseif ( $action == 'move' ) {
2229
			// Check for immobile pages
2230
			if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2231
				// Specific message for this case
2232
				$errors[] = [ 'immobile-source-namespace', $this->getNsText() ];
2233
			} elseif ( !$this->isMovable() ) {
2234
				// Less specific message for rarer cases
2235
				$errors[] = [ 'immobile-source-page' ];
2236
			}
2237
		} elseif ( $action == 'move-target' ) {
2238
			if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2239
				$errors[] = [ 'immobile-target-namespace', $this->getNsText() ];
2240
			} elseif ( !$this->isMovable() ) {
2241
				$errors[] = [ 'immobile-target-page' ];
2242
			}
2243
		} elseif ( $action == 'delete' ) {
2244
			$tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true );
2245
			if ( !$tempErrors ) {
2246
				$tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
2247
					$user, $tempErrors, $rigor, true );
2248
			}
2249
			if ( $tempErrors ) {
2250
				// If protection keeps them from editing, they shouldn't be able to delete.
2251
				$errors[] = [ 'deleteprotected' ];
2252
			}
2253
			if ( $rigor !== 'quick' && $wgDeleteRevisionsLimit
2254
				&& !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion()
2255
			) {
2256
				$errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
2257
			}
2258
		}
2259
		return $errors;
2260
	}
2261
2262
	/**
2263
	 * Check that the user isn't blocked from editing.
2264
	 *
2265
	 * @param string $action The action to check
2266
	 * @param User $user User to check
2267
	 * @param array $errors List of current errors
2268
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2269
	 * @param bool $short Short circuit on first error
2270
	 *
2271
	 * @return array List of errors
2272
	 */
2273
	private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
2274
		global $wgEmailConfirmToEdit, $wgBlockDisablesLogin;
2275
		// Account creation blocks handled at userlogin.
2276
		// Unblocking handled in SpecialUnblock
2277
		if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
2278
			return $errors;
2279
		}
2280
2281
		// Optimize for a very common case
2282
		if ( $action === 'read' && !$wgBlockDisablesLogin ) {
2283
			return $errors;
2284
		}
2285
2286
		if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
2287
			$errors[] = [ 'confirmedittext' ];
2288
		}
2289
2290
		$useSlave = ( $rigor !== 'secure' );
2291
		if ( ( $action == 'edit' || $action == 'create' )
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
2292
			&& !$user->isBlockedFrom( $this, $useSlave )
2293
		) {
2294
			// Don't block the user from editing their own talk page unless they've been
2295
			// explicitly blocked from that too.
2296
		} elseif ( $user->isBlocked() && $user->getBlock()->prevents( $action ) !== false ) {
2297
			// @todo FIXME: Pass the relevant context into this function.
2298
			$errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() );
2299
		}
2300
2301
		return $errors;
2302
	}
2303
2304
	/**
2305
	 * Check that the user is allowed to read this page.
2306
	 *
2307
	 * @param string $action The action to check
2308
	 * @param User $user User to check
2309
	 * @param array $errors List of current errors
2310
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2311
	 * @param bool $short Short circuit on first error
2312
	 *
2313
	 * @return array List of errors
2314
	 */
2315
	private function checkReadPermissions( $action, $user, $errors, $rigor, $short ) {
2316
		global $wgWhitelistRead, $wgWhitelistReadRegexp;
2317
2318
		$whitelisted = false;
2319
		if ( User::isEveryoneAllowed( 'read' ) ) {
2320
			# Shortcut for public wikis, allows skipping quite a bit of code
2321
			$whitelisted = true;
2322
		} elseif ( $user->isAllowed( 'read' ) ) {
2323
			# If the user is allowed to read pages, he is allowed to read all pages
2324
			$whitelisted = true;
2325
		} elseif ( $this->isSpecial( 'Userlogin' )
2326
			|| $this->isSpecial( 'PasswordReset' )
2327
			|| $this->isSpecial( 'Userlogout' )
2328
		) {
2329
			# Always grant access to the login page.
2330
			# Even anons need to be able to log in.
2331
			$whitelisted = true;
2332
		} elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
2333
			# Time to check the whitelist
2334
			# Only do these checks is there's something to check against
2335
			$name = $this->getPrefixedText();
2336
			$dbName = $this->getPrefixedDBkey();
2337
2338
			// Check for explicit whitelisting with and without underscores
2339
			if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
2340
				$whitelisted = true;
2341
			} elseif ( $this->getNamespace() == NS_MAIN ) {
2342
				# Old settings might have the title prefixed with
2343
				# a colon for main-namespace pages
2344
				if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
2345
					$whitelisted = true;
2346
				}
2347
			} elseif ( $this->isSpecialPage() ) {
2348
				# If it's a special page, ditch the subpage bit and check again
2349
				$name = $this->getDBkey();
2350
				list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name );
2351
				if ( $name ) {
2352
					$pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
2353
					if ( in_array( $pure, $wgWhitelistRead, true ) ) {
2354
						$whitelisted = true;
2355
					}
2356
				}
2357
			}
2358
		}
2359
2360
		if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) {
2361
			$name = $this->getPrefixedText();
2362
			// Check for regex whitelisting
2363
			foreach ( $wgWhitelistReadRegexp as $listItem ) {
2364
				if ( preg_match( $listItem, $name ) ) {
2365
					$whitelisted = true;
2366
					break;
2367
				}
2368
			}
2369
		}
2370
2371
		if ( !$whitelisted ) {
2372
			# If the title is not whitelisted, give extensions a chance to do so...
2373
			Hooks::run( 'TitleReadWhitelist', [ $this, $user, &$whitelisted ] );
2374
			if ( !$whitelisted ) {
2375
				$errors[] = $this->missingPermissionError( $action, $short );
2376
			}
2377
		}
2378
2379
		return $errors;
2380
	}
2381
2382
	/**
2383
	 * Get a description array when the user doesn't have the right to perform
2384
	 * $action (i.e. when User::isAllowed() returns false)
2385
	 *
2386
	 * @param string $action The action to check
2387
	 * @param bool $short Short circuit on first error
2388
	 * @return array List of errors
2389
	 */
2390
	private function missingPermissionError( $action, $short ) {
2391
		// We avoid expensive display logic for quickUserCan's and such
2392
		if ( $short ) {
2393
			return [ 'badaccess-group0' ];
2394
		}
2395
2396
		$groups = array_map( [ 'User', 'makeGroupLinkWiki' ],
2397
			User::getGroupsWithPermission( $action ) );
2398
2399
		if ( count( $groups ) ) {
2400
			global $wgLang;
2401
			return [
2402
				'badaccess-groups',
2403
				$wgLang->commaList( $groups ),
2404
				count( $groups )
2405
			];
2406
		} else {
2407
			return [ 'badaccess-group0' ];
2408
		}
2409
	}
2410
2411
	/**
2412
	 * Can $user perform $action on this page? This is an internal function,
2413
	 * with multiple levels of checks depending on performance needs; see $rigor below.
2414
	 * It does not check wfReadOnly().
2415
	 *
2416
	 * @param string $action Action that permission needs to be checked for
2417
	 * @param User $user User to check
2418
	 * @param string $rigor One of (quick,full,secure)
2419
	 *   - quick  : does cheap permission checks from slaves (usable for GUI creation)
2420
	 *   - full   : does cheap and expensive checks possibly from a slave
2421
	 *   - secure : does cheap and expensive checks, using the master as needed
2422
	 * @param bool $short Set this to true to stop after the first permission error.
2423
	 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
2424
	 */
2425
	protected function getUserPermissionsErrorsInternal(
2426
		$action, $user, $rigor = 'secure', $short = false
2427
	) {
2428
		if ( $rigor === true ) {
2429
			$rigor = 'secure'; // b/c
2430
		} elseif ( $rigor === false ) {
2431
			$rigor = 'quick'; // b/c
2432
		} elseif ( !in_array( $rigor, [ 'quick', 'full', 'secure' ] ) ) {
2433
			throw new Exception( "Invalid rigor parameter '$rigor'." );
2434
		}
2435
2436
		# Read has special handling
2437
		if ( $action == 'read' ) {
2438
			$checks = [
2439
				'checkPermissionHooks',
2440
				'checkReadPermissions',
2441
				'checkUserBlock', // for wgBlockDisablesLogin
2442
			];
2443
		# Don't call checkSpecialsAndNSPermissions or checkCSSandJSPermissions
2444
		# here as it will lead to duplicate error messages. This is okay to do
2445
		# since anywhere that checks for create will also check for edit, and
2446
		# those checks are called for edit.
2447
		} elseif ( $action == 'create' ) {
2448
			$checks = [
2449
				'checkQuickPermissions',
2450
				'checkPermissionHooks',
2451
				'checkPageRestrictions',
2452
				'checkCascadingSourcesRestrictions',
2453
				'checkActionPermissions',
2454
				'checkUserBlock'
2455
			];
2456
		} else {
2457
			$checks = [
2458
				'checkQuickPermissions',
2459
				'checkPermissionHooks',
2460
				'checkSpecialsAndNSPermissions',
2461
				'checkCSSandJSPermissions',
2462
				'checkPageRestrictions',
2463
				'checkCascadingSourcesRestrictions',
2464
				'checkActionPermissions',
2465
				'checkUserBlock'
2466
			];
2467
		}
2468
2469
		$errors = [];
2470
		while ( count( $checks ) > 0 &&
2471
				!( $short && count( $errors ) > 0 ) ) {
2472
			$method = array_shift( $checks );
2473
			$errors = $this->$method( $action, $user, $errors, $rigor, $short );
2474
		}
2475
2476
		return $errors;
2477
	}
2478
2479
	/**
2480
	 * Get a filtered list of all restriction types supported by this wiki.
2481
	 * @param bool $exists True to get all restriction types that apply to
2482
	 * titles that do exist, False for all restriction types that apply to
2483
	 * titles that do not exist
2484
	 * @return array
2485
	 */
2486
	public static function getFilteredRestrictionTypes( $exists = true ) {
2487
		global $wgRestrictionTypes;
2488
		$types = $wgRestrictionTypes;
2489
		if ( $exists ) {
2490
			# Remove the create restriction for existing titles
2491
			$types = array_diff( $types, [ 'create' ] );
2492
		} else {
2493
			# Only the create and upload restrictions apply to non-existing titles
2494
			$types = array_intersect( $types, [ 'create', 'upload' ] );
2495
		}
2496
		return $types;
2497
	}
2498
2499
	/**
2500
	 * Returns restriction types for the current Title
2501
	 *
2502
	 * @return array Applicable restriction types
2503
	 */
2504
	public function getRestrictionTypes() {
2505
		if ( $this->isSpecialPage() ) {
2506
			return [];
2507
		}
2508
2509
		$types = self::getFilteredRestrictionTypes( $this->exists() );
2510
2511
		if ( $this->getNamespace() != NS_FILE ) {
2512
			# Remove the upload restriction for non-file titles
2513
			$types = array_diff( $types, [ 'upload' ] );
2514
		}
2515
2516
		Hooks::run( 'TitleGetRestrictionTypes', [ $this, &$types ] );
2517
2518
		wfDebug( __METHOD__ . ': applicable restrictions to [[' .
2519
			$this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
2520
2521
		return $types;
2522
	}
2523
2524
	/**
2525
	 * Is this title subject to title protection?
2526
	 * Title protection is the one applied against creation of such title.
2527
	 *
2528
	 * @return array|bool An associative array representing any existent title
2529
	 *   protection, or false if there's none.
2530
	 */
2531
	public function getTitleProtection() {
2532
		// Can't protect pages in special namespaces
2533
		if ( $this->getNamespace() < 0 ) {
2534
			return false;
2535
		}
2536
2537
		// Can't protect pages that exist.
2538
		if ( $this->exists() ) {
2539
			return false;
2540
		}
2541
2542
		if ( $this->mTitleProtection === null ) {
2543
			$dbr = wfGetDB( DB_SLAVE );
2544
			$res = $dbr->select(
2545
				'protected_titles',
2546
				[
2547
					'user' => 'pt_user',
2548
					'reason' => 'pt_reason',
2549
					'expiry' => 'pt_expiry',
2550
					'permission' => 'pt_create_perm'
2551
				],
2552
				[ 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ],
2553
				__METHOD__
2554
			);
2555
2556
			// fetchRow returns false if there are no rows.
2557
			$row = $dbr->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $dbr->select('protected_...etDBkey()), __METHOD__) on line 2544 can also be of type boolean; however, IDatabase::fetchRow() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2558
			if ( $row ) {
2559
				if ( $row['permission'] == 'sysop' ) {
2560
					$row['permission'] = 'editprotected'; // B/C
2561
				}
2562
				if ( $row['permission'] == 'autoconfirmed' ) {
2563
					$row['permission'] = 'editsemiprotected'; // B/C
2564
				}
2565
				$row['expiry'] = $dbr->decodeExpiry( $row['expiry'] );
2566
			}
2567
			$this->mTitleProtection = $row;
2568
		}
2569
		return $this->mTitleProtection;
2570
	}
2571
2572
	/**
2573
	 * Remove any title protection due to page existing
2574
	 */
2575
	public function deleteTitleProtection() {
2576
		$dbw = wfGetDB( DB_MASTER );
2577
2578
		$dbw->delete(
2579
			'protected_titles',
2580
			[ 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ],
2581
			__METHOD__
2582
		);
2583
		$this->mTitleProtection = false;
2584
	}
2585
2586
	/**
2587
	 * Is this page "semi-protected" - the *only* protection levels are listed
2588
	 * in $wgSemiprotectedRestrictionLevels?
2589
	 *
2590
	 * @param string $action Action to check (default: edit)
2591
	 * @return bool
2592
	 */
2593
	public function isSemiProtected( $action = 'edit' ) {
2594
		global $wgSemiprotectedRestrictionLevels;
2595
2596
		$restrictions = $this->getRestrictions( $action );
2597
		$semi = $wgSemiprotectedRestrictionLevels;
2598
		if ( !$restrictions || !$semi ) {
2599
			// Not protected, or all protection is full protection
2600
			return false;
2601
		}
2602
2603
		// Remap autoconfirmed to editsemiprotected for BC
2604
		foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) {
2605
			$semi[$key] = 'editsemiprotected';
2606
		}
2607
		foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) {
2608
			$restrictions[$key] = 'editsemiprotected';
2609
		}
2610
2611
		return !array_diff( $restrictions, $semi );
2612
	}
2613
2614
	/**
2615
	 * Does the title correspond to a protected article?
2616
	 *
2617
	 * @param string $action The action the page is protected from,
2618
	 * by default checks all actions.
2619
	 * @return bool
2620
	 */
2621
	public function isProtected( $action = '' ) {
2622
		global $wgRestrictionLevels;
2623
2624
		$restrictionTypes = $this->getRestrictionTypes();
2625
2626
		# Special pages have inherent protection
2627
		if ( $this->isSpecialPage() ) {
2628
			return true;
2629
		}
2630
2631
		# Check regular protection levels
2632
		foreach ( $restrictionTypes as $type ) {
2633
			if ( $action == $type || $action == '' ) {
2634
				$r = $this->getRestrictions( $type );
2635
				foreach ( $wgRestrictionLevels as $level ) {
2636
					if ( in_array( $level, $r ) && $level != '' ) {
2637
						return true;
2638
					}
2639
				}
2640
			}
2641
		}
2642
2643
		return false;
2644
	}
2645
2646
	/**
2647
	 * Determines if $user is unable to edit this page because it has been protected
2648
	 * by $wgNamespaceProtection.
2649
	 *
2650
	 * @param User $user User object to check permissions
2651
	 * @return bool
2652
	 */
2653
	public function isNamespaceProtected( User $user ) {
2654
		global $wgNamespaceProtection;
2655
2656
		if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
2657
			foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
2658
				if ( $right != '' && !$user->isAllowed( $right ) ) {
2659
					return true;
2660
				}
2661
			}
2662
		}
2663
		return false;
2664
	}
2665
2666
	/**
2667
	 * Cascading protection: Return true if cascading restrictions apply to this page, false if not.
2668
	 *
2669
	 * @return bool If the page is subject to cascading restrictions.
2670
	 */
2671
	public function isCascadeProtected() {
2672
		list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
2673
		return ( $sources > 0 );
2674
	}
2675
2676
	/**
2677
	 * Determines whether cascading protection sources have already been loaded from
2678
	 * the database.
2679
	 *
2680
	 * @param bool $getPages True to check if the pages are loaded, or false to check
2681
	 * if the status is loaded.
2682
	 * @return bool Whether or not the specified information has been loaded
2683
	 * @since 1.23
2684
	 */
2685
	public function areCascadeProtectionSourcesLoaded( $getPages = true ) {
2686
		return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null;
2687
	}
2688
2689
	/**
2690
	 * Cascading protection: Get the source of any cascading restrictions on this page.
2691
	 *
2692
	 * @param bool $getPages Whether or not to retrieve the actual pages
2693
	 *        that the restrictions have come from and the actual restrictions
2694
	 *        themselves.
2695
	 * @return array Two elements: First is an array of Title objects of the
2696
	 *        pages from which cascading restrictions have come, false for
2697
	 *        none, or true if such restrictions exist but $getPages was not
2698
	 *        set. Second is an array like that returned by
2699
	 *        Title::getAllRestrictions(), or an empty array if $getPages is
2700
	 *        false.
2701
	 */
2702
	public function getCascadeProtectionSources( $getPages = true ) {
2703
		$pagerestrictions = [];
2704
2705
		if ( $this->mCascadeSources !== null && $getPages ) {
2706
			return [ $this->mCascadeSources, $this->mCascadingRestrictions ];
2707
		} elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) {
2708
			return [ $this->mHasCascadingRestrictions, $pagerestrictions ];
2709
		}
2710
2711
		$dbr = wfGetDB( DB_SLAVE );
2712
2713
		if ( $this->getNamespace() == NS_FILE ) {
2714
			$tables = [ 'imagelinks', 'page_restrictions' ];
2715
			$where_clauses = [
2716
				'il_to' => $this->getDBkey(),
2717
				'il_from=pr_page',
2718
				'pr_cascade' => 1
2719
			];
2720
		} else {
2721
			$tables = [ 'templatelinks', 'page_restrictions' ];
2722
			$where_clauses = [
2723
				'tl_namespace' => $this->getNamespace(),
2724
				'tl_title' => $this->getDBkey(),
2725
				'tl_from=pr_page',
2726
				'pr_cascade' => 1
2727
			];
2728
		}
2729
2730
		if ( $getPages ) {
2731
			$cols = [ 'pr_page', 'page_namespace', 'page_title',
2732
				'pr_expiry', 'pr_type', 'pr_level' ];
2733
			$where_clauses[] = 'page_id=pr_page';
2734
			$tables[] = 'page';
2735
		} else {
2736
			$cols = [ 'pr_expiry' ];
2737
		}
2738
2739
		$res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
2740
2741
		$sources = $getPages ? [] : false;
2742
		$now = wfTimestampNow();
2743
2744
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
2745
			$expiry = $dbr->decodeExpiry( $row->pr_expiry );
2746
			if ( $expiry > $now ) {
2747
				if ( $getPages ) {
2748
					$page_id = $row->pr_page;
2749
					$page_ns = $row->page_namespace;
2750
					$page_title = $row->page_title;
2751
					$sources[$page_id] = Title::makeTitle( $page_ns, $page_title );
2752
					# Add groups needed for each restriction type if its not already there
2753
					# Make sure this restriction type still exists
2754
2755
					if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
2756
						$pagerestrictions[$row->pr_type] = [];
2757
					}
2758
2759
					if (
2760
						isset( $pagerestrictions[$row->pr_type] )
2761
						&& !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] )
2762
					) {
2763
						$pagerestrictions[$row->pr_type][] = $row->pr_level;
2764
					}
2765
				} else {
2766
					$sources = true;
2767
				}
2768
			}
2769
		}
2770
2771
		if ( $getPages ) {
2772
			$this->mCascadeSources = $sources;
0 ignored issues
show
Documentation Bug introduced by
It seems like $sources can also be of type boolean. However, the property $mCascadeSources is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2773
			$this->mCascadingRestrictions = $pagerestrictions;
2774
		} else {
2775
			$this->mHasCascadingRestrictions = $sources;
0 ignored issues
show
Documentation Bug introduced by
It seems like $sources can also be of type array. However, the property $mHasCascadingRestrictions is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2776
		}
2777
2778
		return [ $sources, $pagerestrictions ];
2779
	}
2780
2781
	/**
2782
	 * Accessor for mRestrictionsLoaded
2783
	 *
2784
	 * @return bool Whether or not the page's restrictions have already been
2785
	 * loaded from the database
2786
	 * @since 1.23
2787
	 */
2788
	public function areRestrictionsLoaded() {
2789
		return $this->mRestrictionsLoaded;
2790
	}
2791
2792
	/**
2793
	 * Accessor/initialisation for mRestrictions
2794
	 *
2795
	 * @param string $action Action that permission needs to be checked for
2796
	 * @return array Restriction levels needed to take the action. All levels are
2797
	 *     required. Note that restriction levels are normally user rights, but 'sysop'
2798
	 *     and 'autoconfirmed' are also allowed for backwards compatibility. These should
2799
	 *     be mapped to 'editprotected' and 'editsemiprotected' respectively.
2800
	 */
2801
	public function getRestrictions( $action ) {
2802
		if ( !$this->mRestrictionsLoaded ) {
2803
			$this->loadRestrictions();
2804
		}
2805
		return isset( $this->mRestrictions[$action] )
2806
				? $this->mRestrictions[$action]
2807
				: [];
2808
	}
2809
2810
	/**
2811
	 * Accessor/initialisation for mRestrictions
2812
	 *
2813
	 * @return array Keys are actions, values are arrays as returned by
2814
	 *     Title::getRestrictions()
2815
	 * @since 1.23
2816
	 */
2817
	public function getAllRestrictions() {
2818
		if ( !$this->mRestrictionsLoaded ) {
2819
			$this->loadRestrictions();
2820
		}
2821
		return $this->mRestrictions;
2822
	}
2823
2824
	/**
2825
	 * Get the expiry time for the restriction against a given action
2826
	 *
2827
	 * @param string $action
2828
	 * @return string|bool 14-char timestamp, or 'infinity' if the page is protected forever
2829
	 *     or not protected at all, or false if the action is not recognised.
2830
	 */
2831
	public function getRestrictionExpiry( $action ) {
2832
		if ( !$this->mRestrictionsLoaded ) {
2833
			$this->loadRestrictions();
2834
		}
2835
		return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
2836
	}
2837
2838
	/**
2839
	 * Returns cascading restrictions for the current article
2840
	 *
2841
	 * @return bool
2842
	 */
2843
	function areRestrictionsCascading() {
2844
		if ( !$this->mRestrictionsLoaded ) {
2845
			$this->loadRestrictions();
2846
		}
2847
2848
		return $this->mCascadeRestriction;
2849
	}
2850
2851
	/**
2852
	 * Loads a string into mRestrictions array
2853
	 *
2854
	 * @param ResultWrapper $res Resource restrictions as an SQL result.
2855
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2856
	 *        restrictions from page table (pre 1.10)
2857
	 */
2858
	private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) {
2859
		$rows = [];
2860
2861
		foreach ( $res as $row ) {
2862
			$rows[] = $row;
2863
		}
2864
2865
		$this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
2866
	}
2867
2868
	/**
2869
	 * Compiles list of active page restrictions from both page table (pre 1.10)
2870
	 * and page_restrictions table for this existing page.
2871
	 * Public for usage by LiquidThreads.
2872
	 *
2873
	 * @param array $rows Array of db result objects
2874
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2875
	 *   restrictions from page table (pre 1.10)
2876
	 */
2877
	public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
2878
		$dbr = wfGetDB( DB_SLAVE );
2879
2880
		$restrictionTypes = $this->getRestrictionTypes();
2881
2882
		foreach ( $restrictionTypes as $type ) {
2883
			$this->mRestrictions[$type] = [];
2884
			$this->mRestrictionsExpiry[$type] = 'infinity';
2885
		}
2886
2887
		$this->mCascadeRestriction = false;
2888
2889
		# Backwards-compatibility: also load the restrictions from the page record (old format).
2890
		if ( $oldFashionedRestrictions !== null ) {
2891
			$this->mOldRestrictions = $oldFashionedRestrictions;
2892
		}
2893
2894
		if ( $this->mOldRestrictions === false ) {
2895
			$this->mOldRestrictions = $dbr->selectField( 'page', 'page_restrictions',
2896
				[ 'page_id' => $this->getArticleID() ], __METHOD__ );
2897
		}
2898
2899
		if ( $this->mOldRestrictions != '' ) {
2900
			foreach ( explode( ':', trim( $this->mOldRestrictions ) ) as $restrict ) {
2901
				$temp = explode( '=', trim( $restrict ) );
2902
				if ( count( $temp ) == 1 ) {
2903
					// old old format should be treated as edit/move restriction
2904
					$this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
2905
					$this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
2906
				} else {
2907
					$restriction = trim( $temp[1] );
2908
					if ( $restriction != '' ) { // some old entries are empty
2909
						$this->mRestrictions[$temp[0]] = explode( ',', $restriction );
2910
					}
2911
				}
2912
			}
2913
		}
2914
2915
		if ( count( $rows ) ) {
2916
			# Current system - load second to make them override.
2917
			$now = wfTimestampNow();
2918
2919
			# Cycle through all the restrictions.
2920
			foreach ( $rows as $row ) {
2921
2922
				// Don't take care of restrictions types that aren't allowed
2923
				if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
2924
					continue;
2925
				}
2926
2927
				// This code should be refactored, now that it's being used more generally,
2928
				// But I don't really see any harm in leaving it in Block for now -werdna
2929
				$expiry = $dbr->decodeExpiry( $row->pr_expiry );
2930
2931
				// Only apply the restrictions if they haven't expired!
2932
				if ( !$expiry || $expiry > $now ) {
2933
					$this->mRestrictionsExpiry[$row->pr_type] = $expiry;
2934
					$this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
2935
2936
					$this->mCascadeRestriction |= $row->pr_cascade;
2937
				}
2938
			}
2939
		}
2940
2941
		$this->mRestrictionsLoaded = true;
2942
	}
2943
2944
	/**
2945
	 * Load restrictions from the page_restrictions table
2946
	 *
2947
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2948
	 *   restrictions from page table (pre 1.10)
2949
	 */
2950
	public function loadRestrictions( $oldFashionedRestrictions = null ) {
2951
		if ( !$this->mRestrictionsLoaded ) {
2952
			$dbr = wfGetDB( DB_SLAVE );
2953
			if ( $this->exists() ) {
2954
				$res = $dbr->select(
2955
					'page_restrictions',
2956
					[ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ],
2957
					[ 'pr_page' => $this->getArticleID() ],
2958
					__METHOD__
2959
				);
2960
2961
				$this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $dbr->select('page_restr...ticleID()), __METHOD__) on line 2954 can also be of type boolean; however, Title::loadRestrictionsFromResultWrapper() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2962
			} else {
2963
				$title_protection = $this->getTitleProtection();
2964
2965
				if ( $title_protection ) {
2966
					$now = wfTimestampNow();
2967
					$expiry = $dbr->decodeExpiry( $title_protection['expiry'] );
2968
2969
					if ( !$expiry || $expiry > $now ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $expiry of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2970
						// Apply the restrictions
2971
						$this->mRestrictionsExpiry['create'] = $expiry;
2972
						$this->mRestrictions['create'] = explode( ',', trim( $title_protection['permission'] ) );
2973
					} else { // Get rid of the old restrictions
2974
						$this->mTitleProtection = false;
2975
					}
2976
				} else {
2977
					$this->mRestrictionsExpiry['create'] = 'infinity';
2978
				}
2979
				$this->mRestrictionsLoaded = true;
2980
			}
2981
		}
2982
	}
2983
2984
	/**
2985
	 * Flush the protection cache in this object and force reload from the database.
2986
	 * This is used when updating protection from WikiPage::doUpdateRestrictions().
2987
	 */
2988
	public function flushRestrictions() {
2989
		$this->mRestrictionsLoaded = false;
2990
		$this->mTitleProtection = null;
2991
	}
2992
2993
	/**
2994
	 * Purge expired restrictions from the page_restrictions table
2995
	 *
2996
	 * This will purge no more than $wgUpdateRowsPerQuery page_restrictions rows
2997
	 */
2998
	static function purgeExpiredRestrictions() {
2999
		if ( wfReadOnly() ) {
3000
			return;
3001
		}
3002
3003
		DeferredUpdates::addUpdate( new AtomicSectionUpdate(
3004
			wfGetDB( DB_MASTER ),
3005
			__METHOD__,
3006
			function ( IDatabase $dbw, $fname ) {
3007
				$config = MediaWikiServices::getInstance()->getMainConfig();
3008
				$ids = $dbw->selectFieldValues(
3009
					'page_restrictions',
3010
					'pr_id',
3011
					[ 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3012
					$fname,
3013
					[ 'LIMIT' => $config->get( 'UpdateRowsPerQuery' ) ] // T135470
3014
				);
3015
				if ( $ids ) {
3016
					$dbw->delete( 'page_restrictions', [ 'pr_id' => $ids ], $fname );
3017
				}
3018
			}
3019
		) );
3020
3021
		DeferredUpdates::addUpdate( new AtomicSectionUpdate(
3022
			wfGetDB( DB_MASTER ),
3023
			__METHOD__,
3024
			function ( IDatabase $dbw, $fname ) {
3025
				$dbw->delete(
3026
					'protected_titles',
3027
					[ 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3028
					$fname
3029
				);
3030
			}
3031
		) );
3032
	}
3033
3034
	/**
3035
	 * Does this have subpages?  (Warning, usually requires an extra DB query.)
3036
	 *
3037
	 * @return bool
3038
	 */
3039
	public function hasSubpages() {
3040
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
3041
			# Duh
3042
			return false;
3043
		}
3044
3045
		# We dynamically add a member variable for the purpose of this method
3046
		# alone to cache the result.  There's no point in having it hanging
3047
		# around uninitialized in every Title object; therefore we only add it
3048
		# if needed and don't declare it statically.
3049
		if ( $this->mHasSubpages === null ) {
3050
			$this->mHasSubpages = false;
3051
			$subpages = $this->getSubpages( 1 );
3052
			if ( $subpages instanceof TitleArray ) {
3053
				$this->mHasSubpages = (bool)$subpages->count();
3054
			}
3055
		}
3056
3057
		return $this->mHasSubpages;
3058
	}
3059
3060
	/**
3061
	 * Get all subpages of this page.
3062
	 *
3063
	 * @param int $limit Maximum number of subpages to fetch; -1 for no limit
3064
	 * @return TitleArray|array TitleArray, or empty array if this page's namespace
3065
	 *  doesn't allow subpages
3066
	 */
3067
	public function getSubpages( $limit = -1 ) {
3068
		if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3069
			return [];
3070
		}
3071
3072
		$dbr = wfGetDB( DB_SLAVE );
3073
		$conds['page_namespace'] = $this->getNamespace();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$conds was never initialized. Although not strictly required by PHP, it is generally a good practice to add $conds = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
3074
		$conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
3075
		$options = [];
3076
		if ( $limit > -1 ) {
3077
			$options['LIMIT'] = $limit;
3078
		}
3079
		$this->mSubpages = TitleArray::newFromResult(
0 ignored issues
show
Bug introduced by
The property mSubpages 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...
3080
			$dbr->select( 'page',
0 ignored issues
show
Bug introduced by
It seems like $dbr->select('page', arr..., __METHOD__, $options) targeting DatabaseBase::select() can also be of type boolean; however, TitleArray::newFromResult() does only seem to accept object<ResultWrapper>, 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...
3081
				[ 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ],
3082
				$conds,
3083
				__METHOD__,
3084
				$options
3085
			)
3086
		);
3087
		return $this->mSubpages;
3088
	}
3089
3090
	/**
3091
	 * Is there a version of this page in the deletion archive?
3092
	 *
3093
	 * @return int The number of archived revisions
3094
	 */
3095
	public function isDeleted() {
3096
		if ( $this->getNamespace() < 0 ) {
3097
			$n = 0;
3098
		} else {
3099
			$dbr = wfGetDB( DB_SLAVE );
3100
3101
			$n = $dbr->selectField( 'archive', 'COUNT(*)',
3102
				[ 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ],
3103
				__METHOD__
3104
			);
3105 View Code Duplication
			if ( $this->getNamespace() == NS_FILE ) {
3106
				$n += $dbr->selectField( 'filearchive', 'COUNT(*)',
3107
					[ 'fa_name' => $this->getDBkey() ],
3108
					__METHOD__
3109
				);
3110
			}
3111
		}
3112
		return (int)$n;
3113
	}
3114
3115
	/**
3116
	 * Is there a version of this page in the deletion archive?
3117
	 *
3118
	 * @return bool
3119
	 */
3120
	public function isDeletedQuick() {
3121
		if ( $this->getNamespace() < 0 ) {
3122
			return false;
3123
		}
3124
		$dbr = wfGetDB( DB_SLAVE );
3125
		$deleted = (bool)$dbr->selectField( 'archive', '1',
3126
			[ 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ],
3127
			__METHOD__
3128
		);
3129 View Code Duplication
		if ( !$deleted && $this->getNamespace() == NS_FILE ) {
3130
			$deleted = (bool)$dbr->selectField( 'filearchive', '1',
3131
				[ 'fa_name' => $this->getDBkey() ],
3132
				__METHOD__
3133
			);
3134
		}
3135
		return $deleted;
3136
	}
3137
3138
	/**
3139
	 * Get the article ID for this Title from the link cache,
3140
	 * adding it if necessary
3141
	 *
3142
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select
3143
	 *  for update
3144
	 * @return int The ID
3145
	 */
3146
	public function getArticleID( $flags = 0 ) {
3147
		if ( $this->getNamespace() < 0 ) {
3148
			$this->mArticleID = 0;
3149
			return $this->mArticleID;
3150
		}
3151
		$linkCache = LinkCache::singleton();
3152
		if ( $flags & self::GAID_FOR_UPDATE ) {
3153
			$oldUpdate = $linkCache->forUpdate( true );
3154
			$linkCache->clearLink( $this );
3155
			$this->mArticleID = $linkCache->addLinkObj( $this );
3156
			$linkCache->forUpdate( $oldUpdate );
3157
		} else {
3158
			if ( -1 == $this->mArticleID ) {
3159
				$this->mArticleID = $linkCache->addLinkObj( $this );
3160
			}
3161
		}
3162
		return $this->mArticleID;
3163
	}
3164
3165
	/**
3166
	 * Is this an article that is a redirect page?
3167
	 * Uses link cache, adding it if necessary
3168
	 *
3169
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3170
	 * @return bool
3171
	 */
3172
	public function isRedirect( $flags = 0 ) {
3173
		if ( !is_null( $this->mRedirect ) ) {
3174
			return $this->mRedirect;
3175
		}
3176
		if ( !$this->getArticleID( $flags ) ) {
3177
			$this->mRedirect = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type null of property $mRedirect.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
3178
			return $this->mRedirect;
3179
		}
3180
3181
		$linkCache = LinkCache::singleton();
3182
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3183
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
3184
		if ( $cached === null ) {
3185
			# Trust LinkCache's state over our own
3186
			# LinkCache is telling us that the page doesn't exist, despite there being cached
3187
			# data relating to an existing page in $this->mArticleID. Updaters should clear
3188
			# LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is
3189
			# set, then LinkCache will definitely be up to date here, since getArticleID() forces
3190
			# LinkCache to refresh its data from the master.
3191
			$this->mRedirect = false;
3192
			return $this->mRedirect;
3193
		}
3194
3195
		$this->mRedirect = (bool)$cached;
0 ignored issues
show
Documentation Bug introduced by
It seems like (bool) $cached of type boolean is incompatible with the declared type null of property $mRedirect.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
3196
3197
		return $this->mRedirect;
3198
	}
3199
3200
	/**
3201
	 * What is the length of this page?
3202
	 * Uses link cache, adding it if necessary
3203
	 *
3204
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3205
	 * @return int
3206
	 */
3207
	public function getLength( $flags = 0 ) {
3208
		if ( $this->mLength != -1 ) {
3209
			return $this->mLength;
3210
		}
3211
		if ( !$this->getArticleID( $flags ) ) {
3212
			$this->mLength = 0;
3213
			return $this->mLength;
3214
		}
3215
		$linkCache = LinkCache::singleton();
3216
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3217
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
3218
		if ( $cached === null ) {
3219
			# Trust LinkCache's state over our own, as for isRedirect()
3220
			$this->mLength = 0;
3221
			return $this->mLength;
3222
		}
3223
3224
		$this->mLength = intval( $cached );
3225
3226
		return $this->mLength;
3227
	}
3228
3229
	/**
3230
	 * What is the page_latest field for this page?
3231
	 *
3232
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3233
	 * @return int Int or 0 if the page doesn't exist
3234
	 */
3235
	public function getLatestRevID( $flags = 0 ) {
3236
		if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
3237
			return intval( $this->mLatestID );
3238
		}
3239
		if ( !$this->getArticleID( $flags ) ) {
3240
			$this->mLatestID = 0;
3241
			return $this->mLatestID;
3242
		}
3243
		$linkCache = LinkCache::singleton();
3244
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3245
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
3246
		if ( $cached === null ) {
3247
			# Trust LinkCache's state over our own, as for isRedirect()
3248
			$this->mLatestID = 0;
3249
			return $this->mLatestID;
3250
		}
3251
3252
		$this->mLatestID = intval( $cached );
3253
3254
		return $this->mLatestID;
3255
	}
3256
3257
	/**
3258
	 * This clears some fields in this object, and clears any associated
3259
	 * keys in the "bad links" section of the link cache.
3260
	 *
3261
	 * - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow
3262
	 * loading of the new page_id. It's also called from
3263
	 * WikiPage::doDeleteArticleReal()
3264
	 *
3265
	 * @param int $newid The new Article ID
3266
	 */
3267
	public function resetArticleID( $newid ) {
3268
		$linkCache = LinkCache::singleton();
3269
		$linkCache->clearLink( $this );
3270
3271
		if ( $newid === false ) {
3272
			$this->mArticleID = -1;
3273
		} else {
3274
			$this->mArticleID = intval( $newid );
3275
		}
3276
		$this->mRestrictionsLoaded = false;
3277
		$this->mRestrictions = [];
3278
		$this->mOldRestrictions = false;
3279
		$this->mRedirect = null;
3280
		$this->mLength = -1;
3281
		$this->mLatestID = false;
3282
		$this->mContentModel = false;
3283
		$this->mEstimateRevisions = null;
3284
		$this->mPageLanguage = false;
3285
		$this->mDbPageLanguage = false;
3286
		$this->mIsBigDeletion = null;
3287
	}
3288
3289
	public static function clearCaches() {
3290
		$linkCache = LinkCache::singleton();
3291
		$linkCache->clear();
3292
3293
		$titleCache = self::getTitleCache();
3294
		$titleCache->clear();
3295
	}
3296
3297
	/**
3298
	 * Capitalize a text string for a title if it belongs to a namespace that capitalizes
3299
	 *
3300
	 * @param string $text Containing title to capitalize
3301
	 * @param int $ns Namespace index, defaults to NS_MAIN
3302
	 * @return string Containing capitalized title
3303
	 */
3304
	public static function capitalize( $text, $ns = NS_MAIN ) {
3305
		global $wgContLang;
3306
3307
		if ( MWNamespace::isCapitalized( $ns ) ) {
3308
			return $wgContLang->ucfirst( $text );
3309
		} else {
3310
			return $text;
3311
		}
3312
	}
3313
3314
	/**
3315
	 * Secure and split - main initialisation function for this object
3316
	 *
3317
	 * Assumes that mDbkeyform has been set, and is urldecoded
3318
	 * and uses underscores, but not otherwise munged.  This function
3319
	 * removes illegal characters, splits off the interwiki and
3320
	 * namespace prefixes, sets the other forms, and canonicalizes
3321
	 * everything.
3322
	 *
3323
	 * @throws MalformedTitleException On invalid titles
3324
	 * @return bool True on success
3325
	 */
3326
	private function secureAndSplit() {
3327
		# Initialisation
3328
		$this->mInterwiki = '';
3329
		$this->mFragment = '';
3330
		$this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
3331
3332
		$dbkey = $this->mDbkeyform;
3333
3334
		// @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
3335
		//        the parsing code with Title, while avoiding massive refactoring.
3336
		// @todo: get rid of secureAndSplit, refactor parsing code.
3337
		// @note: getTitleParser() returns a TitleParser implementation which does not have a
3338
		//        splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
3339
		$titleCodec = MediaWikiServices::getInstance()->getTitleParser();
3340
		// MalformedTitleException can be thrown here
3341
		$parts = $titleCodec->splitTitleString( $dbkey, $this->getDefaultNamespace() );
3342
3343
		# Fill fields
3344
		$this->setFragment( '#' . $parts['fragment'] );
3345
		$this->mInterwiki = $parts['interwiki'];
3346
		$this->mLocalInterwiki = $parts['local_interwiki'];
3347
		$this->mNamespace = $parts['namespace'];
3348
		$this->mUserCaseDBKey = $parts['user_case_dbkey'];
3349
3350
		$this->mDbkeyform = $parts['dbkey'];
3351
		$this->mUrlform = wfUrlencode( $this->mDbkeyform );
3352
		$this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
3353
3354
		# We already know that some pages won't be in the database!
3355
		if ( $this->isExternal() || $this->mNamespace == NS_SPECIAL ) {
3356
			$this->mArticleID = 0;
3357
		}
3358
3359
		return true;
3360
	}
3361
3362
	/**
3363
	 * Get an array of Title objects linking to this Title
3364
	 * Also stores the IDs in the link cache.
3365
	 *
3366
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3367
	 * On heavily-used templates it will max out the memory.
3368
	 *
3369
	 * @param array $options May be FOR UPDATE
3370
	 * @param string $table Table name
3371
	 * @param string $prefix Fields prefix
3372
	 * @return Title[] Array of Title objects linking here
3373
	 */
3374
	public function getLinksTo( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3375
		if ( count( $options ) > 0 ) {
3376
			$db = wfGetDB( DB_MASTER );
3377
		} else {
3378
			$db = wfGetDB( DB_SLAVE );
3379
		}
3380
3381
		$res = $db->select(
3382
			[ 'page', $table ],
3383
			self::getSelectFields(),
3384
			[
3385
				"{$prefix}_from=page_id",
3386
				"{$prefix}_namespace" => $this->getNamespace(),
3387
				"{$prefix}_title" => $this->getDBkey() ],
3388
			__METHOD__,
3389
			$options
3390
		);
3391
3392
		$retVal = [];
3393
		if ( $res->numRows() ) {
3394
			$linkCache = LinkCache::singleton();
3395 View Code Duplication
			foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
3396
				$titleObj = Title::makeTitle( $row->page_namespace, $row->page_title );
3397
				if ( $titleObj ) {
3398
					$linkCache->addGoodLinkObjFromRow( $titleObj, $row );
3399
					$retVal[] = $titleObj;
3400
				}
3401
			}
3402
		}
3403
		return $retVal;
3404
	}
3405
3406
	/**
3407
	 * Get an array of Title objects using this Title as a template
3408
	 * Also stores the IDs in the link cache.
3409
	 *
3410
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3411
	 * On heavily-used templates it will max out the memory.
3412
	 *
3413
	 * @param array $options Query option to Database::select()
3414
	 * @return Title[] Array of Title the Title objects linking here
3415
	 */
3416
	public function getTemplateLinksTo( $options = [] ) {
3417
		return $this->getLinksTo( $options, 'templatelinks', 'tl' );
3418
	}
3419
3420
	/**
3421
	 * Get an array of Title objects linked from this Title
3422
	 * Also stores the IDs in the link cache.
3423
	 *
3424
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3425
	 * On heavily-used templates it will max out the memory.
3426
	 *
3427
	 * @param array $options Query option to Database::select()
3428
	 * @param string $table Table name
3429
	 * @param string $prefix Fields prefix
3430
	 * @return array Array of Title objects linking here
3431
	 */
3432
	public function getLinksFrom( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3433
		$id = $this->getArticleID();
3434
3435
		# If the page doesn't exist; there can't be any link from this page
3436
		if ( !$id ) {
3437
			return [];
3438
		}
3439
3440
		$db = wfGetDB( DB_SLAVE );
3441
3442
		$blNamespace = "{$prefix}_namespace";
3443
		$blTitle = "{$prefix}_title";
3444
3445
		$res = $db->select(
3446
			[ $table, 'page' ],
3447
			array_merge(
3448
				[ $blNamespace, $blTitle ],
3449
				WikiPage::selectFields()
3450
			),
3451
			[ "{$prefix}_from" => $id ],
3452
			__METHOD__,
3453
			$options,
3454
			[ 'page' => [
3455
				'LEFT JOIN',
3456
				[ "page_namespace=$blNamespace", "page_title=$blTitle" ]
3457
			] ]
3458
		);
3459
3460
		$retVal = [];
3461
		$linkCache = LinkCache::singleton();
3462
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
3463
			if ( $row->page_id ) {
3464
				$titleObj = Title::newFromRow( $row );
3465
			} else {
3466
				$titleObj = Title::makeTitle( $row->$blNamespace, $row->$blTitle );
3467
				$linkCache->addBadLinkObj( $titleObj );
3468
			}
3469
			$retVal[] = $titleObj;
3470
		}
3471
3472
		return $retVal;
3473
	}
3474
3475
	/**
3476
	 * Get an array of Title objects used on this Title as a template
3477
	 * Also stores the IDs in the link cache.
3478
	 *
3479
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3480
	 * On heavily-used templates it will max out the memory.
3481
	 *
3482
	 * @param array $options May be FOR UPDATE
3483
	 * @return Title[] Array of Title the Title objects used here
3484
	 */
3485
	public function getTemplateLinksFrom( $options = [] ) {
3486
		return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
3487
	}
3488
3489
	/**
3490
	 * Get an array of Title objects referring to non-existent articles linked
3491
	 * from this page.
3492
	 *
3493
	 * @todo check if needed (used only in SpecialBrokenRedirects.php, and
3494
	 *   should use redirect table in this case).
3495
	 * @return Title[] Array of Title the Title objects
3496
	 */
3497
	public function getBrokenLinksFrom() {
3498
		if ( $this->getArticleID() == 0 ) {
3499
			# All links from article ID 0 are false positives
3500
			return [];
3501
		}
3502
3503
		$dbr = wfGetDB( DB_SLAVE );
3504
		$res = $dbr->select(
3505
			[ 'page', 'pagelinks' ],
3506
			[ 'pl_namespace', 'pl_title' ],
3507
			[
3508
				'pl_from' => $this->getArticleID(),
3509
				'page_namespace IS NULL'
3510
			],
3511
			__METHOD__, [],
3512
			[
3513
				'page' => [
3514
					'LEFT JOIN',
3515
					[ 'pl_namespace=page_namespace', 'pl_title=page_title' ]
3516
				]
3517
			]
3518
		);
3519
3520
		$retVal = [];
3521
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
3522
			$retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
3523
		}
3524
		return $retVal;
3525
	}
3526
3527
	/**
3528
	 * Get a list of URLs to purge from the CDN cache when this
3529
	 * page changes
3530
	 *
3531
	 * @return string[] Array of String the URLs
3532
	 */
3533
	public function getCdnUrls() {
3534
		$urls = [
3535
			$this->getInternalURL(),
3536
			$this->getInternalURL( 'action=history' )
3537
		];
3538
3539
		$pageLang = $this->getPageLanguage();
3540
		if ( $pageLang->hasVariants() ) {
3541
			$variants = $pageLang->getVariants();
3542
			foreach ( $variants as $vCode ) {
3543
				$urls[] = $this->getInternalURL( $vCode );
3544
			}
3545
		}
3546
3547
		// If we are looking at a css/js user subpage, purge the action=raw.
3548
		if ( $this->isJsSubpage() ) {
3549
			$urls[] = $this->getInternalURL( 'action=raw&ctype=text/javascript' );
3550
		} elseif ( $this->isCssSubpage() ) {
3551
			$urls[] = $this->getInternalURL( 'action=raw&ctype=text/css' );
3552
		}
3553
3554
		Hooks::run( 'TitleSquidURLs', [ $this, &$urls ] );
3555
		return $urls;
3556
	}
3557
3558
	/**
3559
	 * @deprecated since 1.27 use getCdnUrls()
3560
	 */
3561
	public function getSquidURLs() {
3562
		return $this->getCdnUrls();
3563
	}
3564
3565
	/**
3566
	 * Purge all applicable CDN URLs
3567
	 */
3568
	public function purgeSquid() {
3569
		DeferredUpdates::addUpdate(
3570
			new CdnCacheUpdate( $this->getCdnUrls() ),
3571
			DeferredUpdates::PRESEND
3572
		);
3573
	}
3574
3575
	/**
3576
	 * Move this page without authentication
3577
	 *
3578
	 * @deprecated since 1.25 use MovePage class instead
3579
	 * @param Title $nt The new page Title
3580
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3581
	 */
3582
	public function moveNoAuth( &$nt ) {
3583
		wfDeprecated( __METHOD__, '1.25' );
3584
		return $this->moveTo( $nt, false );
3585
	}
3586
3587
	/**
3588
	 * Check whether a given move operation would be valid.
3589
	 * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
3590
	 *
3591
	 * @deprecated since 1.25, use MovePage's methods instead
3592
	 * @param Title $nt The new title
3593
	 * @param bool $auth Whether to check user permissions (uses $wgUser)
3594
	 * @param string $reason Is the log summary of the move, used for spam checking
3595
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3596
	 */
3597
	public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
3598
		global $wgUser;
3599
3600
		if ( !( $nt instanceof Title ) ) {
3601
			// Normally we'd add this to $errors, but we'll get
3602
			// lots of syntax errors if $nt is not an object
3603
			return [ [ 'badtitletext' ] ];
3604
		}
3605
3606
		$mp = new MovePage( $this, $nt );
3607
		$errors = $mp->isValidMove()->getErrorsArray();
3608
		if ( $auth ) {
3609
			$errors = wfMergeErrorArrays(
3610
				$errors,
3611
				$mp->checkPermissions( $wgUser, $reason )->getErrorsArray()
3612
			);
3613
		}
3614
3615
		return $errors ?: true;
3616
	}
3617
3618
	/**
3619
	 * Check if the requested move target is a valid file move target
3620
	 * @todo move this to MovePage
3621
	 * @param Title $nt Target title
3622
	 * @return array List of errors
3623
	 */
3624
	protected function validateFileMoveOperation( $nt ) {
3625
		global $wgUser;
3626
3627
		$errors = [];
3628
3629
		$destFile = wfLocalFile( $nt );
3630
		$destFile->load( File::READ_LATEST );
3631
		if ( !$wgUser->isAllowed( 'reupload-shared' )
3632
			&& !$destFile->exists() && wfFindFile( $nt )
3633
		) {
3634
			$errors[] = [ 'file-exists-sharedrepo' ];
3635
		}
3636
3637
		return $errors;
3638
	}
3639
3640
	/**
3641
	 * Move a title to a new location
3642
	 *
3643
	 * @deprecated since 1.25, use the MovePage class instead
3644
	 * @param Title $nt The new title
3645
	 * @param bool $auth Indicates whether $wgUser's permissions
3646
	 *  should be checked
3647
	 * @param string $reason The reason for the move
3648
	 * @param bool $createRedirect Whether to create a redirect from the old title to the new title.
3649
	 *  Ignored if the user doesn't have the suppressredirect right.
3650
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3651
	 */
3652
	public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
3653
		global $wgUser;
3654
		$err = $this->isValidMoveOperation( $nt, $auth, $reason );
3655
		if ( is_array( $err ) ) {
3656
			// Auto-block user's IP if the account was "hard" blocked
3657
			$wgUser->spreadAnyEditBlock();
3658
			return $err;
3659
		}
3660
		// Check suppressredirect permission
3661
		if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
3662
			$createRedirect = true;
3663
		}
3664
3665
		$mp = new MovePage( $this, $nt );
3666
		$status = $mp->move( $wgUser, $reason, $createRedirect );
3667
		if ( $status->isOK() ) {
3668
			return true;
3669
		} else {
3670
			return $status->getErrorsArray();
3671
		}
3672
	}
3673
3674
	/**
3675
	 * Move this page's subpages to be subpages of $nt
3676
	 *
3677
	 * @param Title $nt Move target
3678
	 * @param bool $auth Whether $wgUser's permissions should be checked
3679
	 * @param string $reason The reason for the move
3680
	 * @param bool $createRedirect Whether to create redirects from the old subpages to
3681
	 *     the new ones Ignored if the user doesn't have the 'suppressredirect' right
3682
	 * @return array Array with old page titles as keys, and strings (new page titles) or
3683
	 *     arrays (errors) as values, or an error array with numeric indices if no pages
3684
	 *     were moved
3685
	 */
3686
	public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) {
3687
		global $wgMaximumMovedPages;
3688
		// Check permissions
3689
		if ( !$this->userCan( 'move-subpages' ) ) {
3690
			return [ 'cant-move-subpages' ];
3691
		}
3692
		// Do the source and target namespaces support subpages?
3693
		if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3694
			return [ 'namespace-nosubpages',
3695
				MWNamespace::getCanonicalName( $this->getNamespace() ) ];
3696
		}
3697
		if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
3698
			return [ 'namespace-nosubpages',
3699
				MWNamespace::getCanonicalName( $nt->getNamespace() ) ];
3700
		}
3701
3702
		$subpages = $this->getSubpages( $wgMaximumMovedPages + 1 );
3703
		$retval = [];
3704
		$count = 0;
3705
		foreach ( $subpages as $oldSubpage ) {
0 ignored issues
show
Bug introduced by
The expression $subpages of type array|object<TitleArrayFromResult>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
3706
			$count++;
3707
			if ( $count > $wgMaximumMovedPages ) {
3708
				$retval[$oldSubpage->getPrefixedText()] =
3709
						[ 'movepage-max-pages',
3710
							$wgMaximumMovedPages ];
3711
				break;
3712
			}
3713
3714
			// We don't know whether this function was called before
3715
			// or after moving the root page, so check both
3716
			// $this and $nt
3717
			if ( $oldSubpage->getArticleID() == $this->getArticleID()
3718
				|| $oldSubpage->getArticleID() == $nt->getArticleID()
3719
			) {
3720
				// When moving a page to a subpage of itself,
3721
				// don't move it twice
3722
				continue;
3723
			}
3724
			$newPageName = preg_replace(
3725
					'#^' . preg_quote( $this->getDBkey(), '#' ) . '#',
3726
					StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
3727
					$oldSubpage->getDBkey() );
3728
			if ( $oldSubpage->isTalkPage() ) {
3729
				$newNs = $nt->getTalkPage()->getNamespace();
3730
			} else {
3731
				$newNs = $nt->getSubjectPage()->getNamespace();
3732
			}
3733
			# Bug 14385: we need makeTitleSafe because the new page names may
3734
			# be longer than 255 characters.
3735
			$newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
3736
3737
			$success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect );
3738
			if ( $success === true ) {
3739
				$retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
3740
			} else {
3741
				$retval[$oldSubpage->getPrefixedText()] = $success;
3742
			}
3743
		}
3744
		return $retval;
3745
	}
3746
3747
	/**
3748
	 * Checks if this page is just a one-rev redirect.
3749
	 * Adds lock, so don't use just for light purposes.
3750
	 *
3751
	 * @return bool
3752
	 */
3753
	public function isSingleRevRedirect() {
3754
		global $wgContentHandlerUseDB;
3755
3756
		$dbw = wfGetDB( DB_MASTER );
3757
3758
		# Is it a redirect?
3759
		$fields = [ 'page_is_redirect', 'page_latest', 'page_id' ];
3760
		if ( $wgContentHandlerUseDB ) {
3761
			$fields[] = 'page_content_model';
3762
		}
3763
3764
		$row = $dbw->selectRow( 'page',
3765
			$fields,
3766
			$this->pageCond(),
3767
			__METHOD__,
3768
			[ 'FOR UPDATE' ]
3769
		);
3770
		# Cache some fields we may want
3771
		$this->mArticleID = $row ? intval( $row->page_id ) : 0;
3772
		$this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
0 ignored issues
show
Documentation Bug introduced by
It seems like $row ? (bool) $row->page_is_redirect : false of type boolean is incompatible with the declared type null of property $mRedirect.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
3773
		$this->mLatestID = $row ? intval( $row->page_latest ) : false;
3774
		$this->mContentModel = $row && isset( $row->page_content_model )
3775
			? strval( $row->page_content_model )
3776
			: false;
3777
3778
		if ( !$this->mRedirect ) {
3779
			return false;
3780
		}
3781
		# Does the article have a history?
3782
		$row = $dbw->selectField( [ 'page', 'revision' ],
3783
			'rev_id',
3784
			[ 'page_namespace' => $this->getNamespace(),
3785
				'page_title' => $this->getDBkey(),
3786
				'page_id=rev_page',
3787
				'page_latest != rev_id'
3788
			],
3789
			__METHOD__,
3790
			[ 'FOR UPDATE' ]
3791
		);
3792
		# Return true if there was no history
3793
		return ( $row === false );
3794
	}
3795
3796
	/**
3797
	 * Checks if $this can be moved to a given Title
3798
	 * - Selects for update, so don't call it unless you mean business
3799
	 *
3800
	 * @deprecated since 1.25, use MovePage's methods instead
3801
	 * @param Title $nt The new title to check
3802
	 * @return bool
3803
	 */
3804
	public function isValidMoveTarget( $nt ) {
3805
		# Is it an existing file?
3806
		if ( $nt->getNamespace() == NS_FILE ) {
3807
			$file = wfLocalFile( $nt );
3808
			$file->load( File::READ_LATEST );
3809
			if ( $file->exists() ) {
3810
				wfDebug( __METHOD__ . ": file exists\n" );
3811
				return false;
3812
			}
3813
		}
3814
		# Is it a redirect with no history?
3815
		if ( !$nt->isSingleRevRedirect() ) {
3816
			wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
3817
			return false;
3818
		}
3819
		# Get the article text
3820
		$rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST );
3821
		if ( !is_object( $rev ) ) {
3822
			return false;
3823
		}
3824
		$content = $rev->getContent();
3825
		# Does the redirect point to the source?
3826
		# Or is it a broken self-redirect, usually caused by namespace collisions?
3827
		$redirTitle = $content ? $content->getRedirectTarget() : null;
3828
3829
		if ( $redirTitle ) {
3830
			if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
3831
				$redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
3832
				wfDebug( __METHOD__ . ": redirect points to other page\n" );
3833
				return false;
3834
			} else {
3835
				return true;
3836
			}
3837
		} else {
3838
			# Fail safe (not a redirect after all. strange.)
3839
			wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
3840
						" is a redirect, but it doesn't contain a valid redirect.\n" );
3841
			return false;
3842
		}
3843
	}
3844
3845
	/**
3846
	 * Get categories to which this Title belongs and return an array of
3847
	 * categories' names.
3848
	 *
3849
	 * @return array Array of parents in the form:
3850
	 *	  $parent => $currentarticle
3851
	 */
3852
	public function getParentCategories() {
3853
		global $wgContLang;
3854
3855
		$data = [];
3856
3857
		$titleKey = $this->getArticleID();
3858
3859
		if ( $titleKey === 0 ) {
3860
			return $data;
3861
		}
3862
3863
		$dbr = wfGetDB( DB_SLAVE );
3864
3865
		$res = $dbr->select(
3866
			'categorylinks',
3867
			'cl_to',
3868
			[ 'cl_from' => $titleKey ],
3869
			__METHOD__
3870
		);
3871
3872
		if ( $res->numRows() > 0 ) {
3873
			foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
3874
				// $data[] = Title::newFromText($wgContLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to);
3875
				$data[$wgContLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText();
3876
			}
3877
		}
3878
		return $data;
3879
	}
3880
3881
	/**
3882
	 * Get a tree of parent categories
3883
	 *
3884
	 * @param array $children Array with the children in the keys, to check for circular refs
3885
	 * @return array Tree of parent categories
3886
	 */
3887
	public function getParentCategoryTree( $children = [] ) {
3888
		$stack = [];
3889
		$parents = $this->getParentCategories();
3890
3891
		if ( $parents ) {
3892
			foreach ( $parents as $parent => $current ) {
3893
				if ( array_key_exists( $parent, $children ) ) {
3894
					# Circular reference
3895
					$stack[$parent] = [];
3896
				} else {
3897
					$nt = Title::newFromText( $parent );
3898
					if ( $nt ) {
3899
						$stack[$parent] = $nt->getParentCategoryTree( $children + [ $parent => 1 ] );
3900
					}
3901
				}
3902
			}
3903
		}
3904
3905
		return $stack;
3906
	}
3907
3908
	/**
3909
	 * Get an associative array for selecting this title from
3910
	 * the "page" table
3911
	 *
3912
	 * @return array Array suitable for the $where parameter of DB::select()
3913
	 */
3914
	public function pageCond() {
3915
		if ( $this->mArticleID > 0 ) {
3916
			// PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
3917
			return [ 'page_id' => $this->mArticleID ];
3918
		} else {
3919
			return [ 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ];
3920
		}
3921
	}
3922
3923
	/**
3924
	 * Get the revision ID of the previous revision
3925
	 *
3926
	 * @param int $revId Revision ID. Get the revision that was before this one.
3927
	 * @param int $flags Title::GAID_FOR_UPDATE
3928
	 * @return int|bool Old revision ID, or false if none exists
3929
	 */
3930 View Code Duplication
	public function getPreviousRevisionID( $revId, $flags = 0 ) {
3931
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3932
		$revId = $db->selectField( 'revision', 'rev_id',
3933
			[
3934
				'rev_page' => $this->getArticleID( $flags ),
3935
				'rev_id < ' . intval( $revId )
3936
			],
3937
			__METHOD__,
3938
			[ 'ORDER BY' => 'rev_id DESC' ]
3939
		);
3940
3941
		if ( $revId === false ) {
3942
			return false;
3943
		} else {
3944
			return intval( $revId );
3945
		}
3946
	}
3947
3948
	/**
3949
	 * Get the revision ID of the next revision
3950
	 *
3951
	 * @param int $revId Revision ID. Get the revision that was after this one.
3952
	 * @param int $flags Title::GAID_FOR_UPDATE
3953
	 * @return int|bool Next revision ID, or false if none exists
3954
	 */
3955 View Code Duplication
	public function getNextRevisionID( $revId, $flags = 0 ) {
3956
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3957
		$revId = $db->selectField( 'revision', 'rev_id',
3958
			[
3959
				'rev_page' => $this->getArticleID( $flags ),
3960
				'rev_id > ' . intval( $revId )
3961
			],
3962
			__METHOD__,
3963
			[ 'ORDER BY' => 'rev_id' ]
3964
		);
3965
3966
		if ( $revId === false ) {
3967
			return false;
3968
		} else {
3969
			return intval( $revId );
3970
		}
3971
	}
3972
3973
	/**
3974
	 * Get the first revision of the page
3975
	 *
3976
	 * @param int $flags Title::GAID_FOR_UPDATE
3977
	 * @return Revision|null If page doesn't exist
3978
	 */
3979
	public function getFirstRevision( $flags = 0 ) {
3980
		$pageId = $this->getArticleID( $flags );
3981
		if ( $pageId ) {
3982
			$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3983
			$row = $db->selectRow( 'revision', Revision::selectFields(),
3984
				[ 'rev_page' => $pageId ],
3985
				__METHOD__,
3986
				[ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ]
3987
			);
3988
			if ( $row ) {
3989
				return new Revision( $row );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $db->selectRow('revision...mp ASC', 'LIMIT' => 1)) on line 3983 can also be of type boolean; however, Revision::__construct() does only seem to accept object|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
3990
			}
3991
		}
3992
		return null;
3993
	}
3994
3995
	/**
3996
	 * Get the oldest revision timestamp of this page
3997
	 *
3998
	 * @param int $flags Title::GAID_FOR_UPDATE
3999
	 * @return string MW timestamp
4000
	 */
4001
	public function getEarliestRevTime( $flags = 0 ) {
4002
		$rev = $this->getFirstRevision( $flags );
4003
		return $rev ? $rev->getTimestamp() : null;
4004
	}
4005
4006
	/**
4007
	 * Check if this is a new page
4008
	 *
4009
	 * @return bool
4010
	 */
4011
	public function isNewPage() {
4012
		$dbr = wfGetDB( DB_SLAVE );
4013
		return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
4014
	}
4015
4016
	/**
4017
	 * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit
4018
	 *
4019
	 * @return bool
4020
	 */
4021
	public function isBigDeletion() {
4022
		global $wgDeleteRevisionsLimit;
4023
4024
		if ( !$wgDeleteRevisionsLimit ) {
4025
			return false;
4026
		}
4027
4028
		if ( $this->mIsBigDeletion === null ) {
4029
			$dbr = wfGetDB( DB_SLAVE );
4030
4031
			$revCount = $dbr->selectRowCount(
4032
				'revision',
4033
				'1',
4034
				[ 'rev_page' => $this->getArticleID() ],
4035
				__METHOD__,
4036
				[ 'LIMIT' => $wgDeleteRevisionsLimit + 1 ]
4037
			);
4038
4039
			$this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit;
4040
		}
4041
4042
		return $this->mIsBigDeletion;
4043
	}
4044
4045
	/**
4046
	 * Get the approximate revision count of this page.
4047
	 *
4048
	 * @return int
4049
	 */
4050
	public function estimateRevisionCount() {
4051
		if ( !$this->exists() ) {
4052
			return 0;
4053
		}
4054
4055
		if ( $this->mEstimateRevisions === null ) {
4056
			$dbr = wfGetDB( DB_SLAVE );
4057
			$this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
4058
				[ 'rev_page' => $this->getArticleID() ], __METHOD__ );
4059
		}
4060
4061
		return $this->mEstimateRevisions;
4062
	}
4063
4064
	/**
4065
	 * Get the number of revisions between the given revision.
4066
	 * Used for diffs and other things that really need it.
4067
	 *
4068
	 * @param int|Revision $old Old revision or rev ID (first before range)
4069
	 * @param int|Revision $new New revision or rev ID (first after range)
4070
	 * @param int|null $max Limit of Revisions to count, will be incremented to detect truncations
4071
	 * @return int Number of revisions between these revisions.
4072
	 */
4073
	public function countRevisionsBetween( $old, $new, $max = null ) {
4074
		if ( !( $old instanceof Revision ) ) {
4075
			$old = Revision::newFromTitle( $this, (int)$old );
4076
		}
4077
		if ( !( $new instanceof Revision ) ) {
4078
			$new = Revision::newFromTitle( $this, (int)$new );
4079
		}
4080
		if ( !$old || !$new ) {
4081
			return 0; // nothing to compare
4082
		}
4083
		$dbr = wfGetDB( DB_SLAVE );
4084
		$conds = [
4085
			'rev_page' => $this->getArticleID(),
4086
			'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
0 ignored issues
show
Security Bug introduced by
It seems like $dbr->timestamp($old->getTimestamp()) targeting DatabaseBase::timestamp() can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?
Loading history...
4087
			'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
0 ignored issues
show
Security Bug introduced by
It seems like $dbr->timestamp($new->getTimestamp()) targeting DatabaseBase::timestamp() can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?
Loading history...
4088
		];
4089
		if ( $max !== null ) {
4090
			return $dbr->selectRowCount( 'revision', '1',
4091
				$conds,
4092
				__METHOD__,
4093
				[ 'LIMIT' => $max + 1 ] // extra to detect truncation
4094
			);
4095
		} else {
4096
			return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
4097
		}
4098
	}
4099
4100
	/**
4101
	 * Get the authors between the given revisions or revision IDs.
4102
	 * Used for diffs and other things that really need it.
4103
	 *
4104
	 * @since 1.23
4105
	 *
4106
	 * @param int|Revision $old Old revision or rev ID (first before range by default)
4107
	 * @param int|Revision $new New revision or rev ID (first after range by default)
4108
	 * @param int $limit Maximum number of authors
4109
	 * @param string|array $options (Optional): Single option, or an array of options:
4110
	 *     'include_old' Include $old in the range; $new is excluded.
4111
	 *     'include_new' Include $new in the range; $old is excluded.
4112
	 *     'include_both' Include both $old and $new in the range.
4113
	 *     Unknown option values are ignored.
4114
	 * @return array|null Names of revision authors in the range; null if not both revisions exist
4115
	 */
4116
	public function getAuthorsBetween( $old, $new, $limit, $options = [] ) {
4117
		if ( !( $old instanceof Revision ) ) {
4118
			$old = Revision::newFromTitle( $this, (int)$old );
4119
		}
4120
		if ( !( $new instanceof Revision ) ) {
4121
			$new = Revision::newFromTitle( $this, (int)$new );
4122
		}
4123
		// XXX: what if Revision objects are passed in, but they don't refer to this title?
4124
		// Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID()
4125
		// in the sanity check below?
4126
		if ( !$old || !$new ) {
4127
			return null; // nothing to compare
4128
		}
4129
		$authors = [];
4130
		$old_cmp = '>';
4131
		$new_cmp = '<';
4132
		$options = (array)$options;
4133
		if ( in_array( 'include_old', $options ) ) {
4134
			$old_cmp = '>=';
4135
		}
4136
		if ( in_array( 'include_new', $options ) ) {
4137
			$new_cmp = '<=';
4138
		}
4139
		if ( in_array( 'include_both', $options ) ) {
4140
			$old_cmp = '>=';
4141
			$new_cmp = '<=';
4142
		}
4143
		// No DB query needed if $old and $new are the same or successive revisions:
4144
		if ( $old->getId() === $new->getId() ) {
4145
			return ( $old_cmp === '>' && $new_cmp === '<' ) ?
4146
				[] :
4147
				[ $old->getUserText( Revision::RAW ) ];
4148
		} elseif ( $old->getId() === $new->getParentId() ) {
4149
			if ( $old_cmp === '>=' && $new_cmp === '<=' ) {
4150
				$authors[] = $old->getUserText( Revision::RAW );
4151
				if ( $old->getUserText( Revision::RAW ) != $new->getUserText( Revision::RAW ) ) {
4152
					$authors[] = $new->getUserText( Revision::RAW );
4153
				}
4154
			} elseif ( $old_cmp === '>=' ) {
4155
				$authors[] = $old->getUserText( Revision::RAW );
4156
			} elseif ( $new_cmp === '<=' ) {
4157
				$authors[] = $new->getUserText( Revision::RAW );
4158
			}
4159
			return $authors;
4160
		}
4161
		$dbr = wfGetDB( DB_SLAVE );
4162
		$res = $dbr->select( 'revision', 'DISTINCT rev_user_text',
4163
			[
4164
				'rev_page' => $this->getArticleID(),
4165
				"rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
0 ignored issues
show
Security Bug introduced by
It seems like $dbr->timestamp($old->getTimestamp()) targeting DatabaseBase::timestamp() can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?
Loading history...
4166
				"rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
0 ignored issues
show
Security Bug introduced by
It seems like $dbr->timestamp($new->getTimestamp()) targeting DatabaseBase::timestamp() can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?
Loading history...
4167
			], __METHOD__,
4168
			[ 'LIMIT' => $limit + 1 ] // add one so caller knows it was truncated
4169
		);
4170
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
4171
			$authors[] = $row->rev_user_text;
4172
		}
4173
		return $authors;
4174
	}
4175
4176
	/**
4177
	 * Get the number of authors between the given revisions or revision IDs.
4178
	 * Used for diffs and other things that really need it.
4179
	 *
4180
	 * @param int|Revision $old Old revision or rev ID (first before range by default)
4181
	 * @param int|Revision $new New revision or rev ID (first after range by default)
4182
	 * @param int $limit Maximum number of authors
4183
	 * @param string|array $options (Optional): Single option, or an array of options:
4184
	 *     'include_old' Include $old in the range; $new is excluded.
4185
	 *     'include_new' Include $new in the range; $old is excluded.
4186
	 *     'include_both' Include both $old and $new in the range.
4187
	 *     Unknown option values are ignored.
4188
	 * @return int Number of revision authors in the range; zero if not both revisions exist
4189
	 */
4190
	public function countAuthorsBetween( $old, $new, $limit, $options = [] ) {
4191
		$authors = $this->getAuthorsBetween( $old, $new, $limit, $options );
4192
		return $authors ? count( $authors ) : 0;
4193
	}
4194
4195
	/**
4196
	 * Compare with another title.
4197
	 *
4198
	 * @param Title $title
4199
	 * @return bool
4200
	 */
4201
	public function equals( Title $title ) {
4202
		// Note: === is necessary for proper matching of number-like titles.
4203
		return $this->getInterwiki() === $title->getInterwiki()
4204
			&& $this->getNamespace() == $title->getNamespace()
4205
			&& $this->getDBkey() === $title->getDBkey();
4206
	}
4207
4208
	/**
4209
	 * Check if this title is a subpage of another title
4210
	 *
4211
	 * @param Title $title
4212
	 * @return bool
4213
	 */
4214
	public function isSubpageOf( Title $title ) {
4215
		return $this->getInterwiki() === $title->getInterwiki()
4216
			&& $this->getNamespace() == $title->getNamespace()
4217
			&& strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0;
4218
	}
4219
4220
	/**
4221
	 * Check if page exists.  For historical reasons, this function simply
4222
	 * checks for the existence of the title in the page table, and will
4223
	 * thus return false for interwiki links, special pages and the like.
4224
	 * If you want to know if a title can be meaningfully viewed, you should
4225
	 * probably call the isKnown() method instead.
4226
	 *
4227
	 * @param int $flags An optional bit field; may be Title::GAID_FOR_UPDATE to check
4228
	 *   from master/for update
4229
	 * @return bool
4230
	 */
4231
	public function exists( $flags = 0 ) {
4232
		$exists = $this->getArticleID( $flags ) != 0;
4233
		Hooks::run( 'TitleExists', [ $this, &$exists ] );
4234
		return $exists;
4235
	}
4236
4237
	/**
4238
	 * Should links to this title be shown as potentially viewable (i.e. as
4239
	 * "bluelinks"), even if there's no record by this title in the page
4240
	 * table?
4241
	 *
4242
	 * This function is semi-deprecated for public use, as well as somewhat
4243
	 * misleadingly named.  You probably just want to call isKnown(), which
4244
	 * calls this function internally.
4245
	 *
4246
	 * (ISSUE: Most of these checks are cheap, but the file existence check
4247
	 * can potentially be quite expensive.  Including it here fixes a lot of
4248
	 * existing code, but we might want to add an optional parameter to skip
4249
	 * it and any other expensive checks.)
4250
	 *
4251
	 * @return bool
4252
	 */
4253
	public function isAlwaysKnown() {
4254
		$isKnown = null;
4255
4256
		/**
4257
		 * Allows overriding default behavior for determining if a page exists.
4258
		 * If $isKnown is kept as null, regular checks happen. If it's
4259
		 * a boolean, this value is returned by the isKnown method.
4260
		 *
4261
		 * @since 1.20
4262
		 *
4263
		 * @param Title $title
4264
		 * @param bool|null $isKnown
4265
		 */
4266
		Hooks::run( 'TitleIsAlwaysKnown', [ $this, &$isKnown ] );
4267
4268
		if ( !is_null( $isKnown ) ) {
4269
			return $isKnown;
4270
		}
4271
4272
		if ( $this->isExternal() ) {
4273
			return true;  // any interwiki link might be viewable, for all we know
4274
		}
4275
4276
		switch ( $this->mNamespace ) {
4277
			case NS_MEDIA:
4278
			case NS_FILE:
4279
				// file exists, possibly in a foreign repo
4280
				return (bool)wfFindFile( $this );
4281
			case NS_SPECIAL:
4282
				// valid special page
4283
				return SpecialPageFactory::exists( $this->getDBkey() );
4284
			case NS_MAIN:
4285
				// selflink, possibly with fragment
4286
				return $this->mDbkeyform == '';
4287
			case NS_MEDIAWIKI:
4288
				// known system message
4289
				return $this->hasSourceText() !== false;
4290
			default:
4291
				return false;
4292
		}
4293
	}
4294
4295
	/**
4296
	 * Does this title refer to a page that can (or might) be meaningfully
4297
	 * viewed?  In particular, this function may be used to determine if
4298
	 * links to the title should be rendered as "bluelinks" (as opposed to
4299
	 * "redlinks" to non-existent pages).
4300
	 * Adding something else to this function will cause inconsistency
4301
	 * since LinkHolderArray calls isAlwaysKnown() and does its own
4302
	 * page existence check.
4303
	 *
4304
	 * @return bool
4305
	 */
4306
	public function isKnown() {
4307
		return $this->isAlwaysKnown() || $this->exists();
4308
	}
4309
4310
	/**
4311
	 * Does this page have source text?
4312
	 *
4313
	 * @return bool
4314
	 */
4315
	public function hasSourceText() {
4316
		if ( $this->exists() ) {
4317
			return true;
4318
		}
4319
4320
		if ( $this->mNamespace == NS_MEDIAWIKI ) {
4321
			// If the page doesn't exist but is a known system message, default
4322
			// message content will be displayed, same for language subpages-
4323
			// Use always content language to avoid loading hundreds of languages
4324
			// to get the link color.
4325
			global $wgContLang;
4326
			list( $name, ) = MessageCache::singleton()->figureMessage(
4327
				$wgContLang->lcfirst( $this->getText() )
4328
			);
4329
			$message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false );
4330
			return $message->exists();
4331
		}
4332
4333
		return false;
4334
	}
4335
4336
	/**
4337
	 * Get the default message text or false if the message doesn't exist
4338
	 *
4339
	 * @return string|bool
4340
	 */
4341
	public function getDefaultMessageText() {
4342
		global $wgContLang;
4343
4344
		if ( $this->getNamespace() != NS_MEDIAWIKI ) { // Just in case
4345
			return false;
4346
		}
4347
4348
		list( $name, $lang ) = MessageCache::singleton()->figureMessage(
4349
			$wgContLang->lcfirst( $this->getText() )
4350
		);
4351
		$message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false );
4352
4353
		if ( $message->exists() ) {
4354
			return $message->plain();
4355
		} else {
4356
			return false;
4357
		}
4358
	}
4359
4360
	/**
4361
	 * Updates page_touched for this page; called from LinksUpdate.php
4362
	 *
4363
	 * @param string $purgeTime [optional] TS_MW timestamp
4364
	 * @return bool True if the update succeeded
4365
	 */
4366
	public function invalidateCache( $purgeTime = null ) {
4367
		if ( wfReadOnly() ) {
4368
			return false;
4369
		}
4370
4371
		if ( $this->mArticleID === 0 ) {
4372
			return true; // avoid gap locking if we know it's not there
4373
		}
4374
4375
		$conds = $this->pageCond();
4376
		DeferredUpdates::addUpdate(
4377
			new AutoCommitUpdate(
4378
				wfGetDB( DB_MASTER ),
4379
				__METHOD__,
4380
				function ( IDatabase $dbw, $fname ) use ( $conds, $purgeTime ) {
4381
					$dbTimestamp = $dbw->timestamp( $purgeTime ?: time() );
4382
					$dbw->update(
4383
						'page',
4384
						[ 'page_touched' => $dbTimestamp ],
4385
						$conds + [ 'page_touched < ' . $dbw->addQuotes( $dbTimestamp ) ],
4386
						$fname
4387
					);
4388
				}
4389
			),
4390
			DeferredUpdates::PRESEND
4391
		);
4392
4393
		return true;
4394
	}
4395
4396
	/**
4397
	 * Update page_touched timestamps and send CDN purge messages for
4398
	 * pages linking to this title. May be sent to the job queue depending
4399
	 * on the number of links. Typically called on create and delete.
4400
	 */
4401
	public function touchLinks() {
4402
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks' ) );
4403
		if ( $this->getNamespace() == NS_CATEGORY ) {
4404
			DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'categorylinks' ) );
4405
		}
4406
	}
4407
4408
	/**
4409
	 * Get the last touched timestamp
4410
	 *
4411
	 * @param IDatabase $db Optional db
4412
	 * @return string Last-touched timestamp
4413
	 */
4414
	public function getTouched( $db = null ) {
4415
		if ( $db === null ) {
4416
			$db = wfGetDB( DB_SLAVE );
4417
		}
4418
		$touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
4419
		return $touched;
4420
	}
4421
4422
	/**
4423
	 * Get the timestamp when this page was updated since the user last saw it.
4424
	 *
4425
	 * @param User $user
4426
	 * @return string|null
4427
	 */
4428
	public function getNotificationTimestamp( $user = null ) {
4429
		global $wgUser;
4430
4431
		// Assume current user if none given
4432
		if ( !$user ) {
4433
			$user = $wgUser;
4434
		}
4435
		// Check cache first
4436
		$uid = $user->getId();
4437
		if ( !$uid ) {
4438
			return false;
4439
		}
4440
		// avoid isset here, as it'll return false for null entries
4441
		if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
4442
			return $this->mNotificationTimestamp[$uid];
4443
		}
4444
		// Don't cache too much!
4445
		if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
4446
			$this->mNotificationTimestamp = [];
4447
		}
4448
4449
		$store = MediaWikiServices::getInstance()->getWatchedItemStore();
4450
		$watchedItem = $store->getWatchedItem( $user, $this );
4451
		if ( $watchedItem ) {
4452
			$this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp();
4453
		} else {
4454
			$this->mNotificationTimestamp[$uid] = false;
4455
		}
4456
4457
		return $this->mNotificationTimestamp[$uid];
4458
	}
4459
4460
	/**
4461
	 * Generate strings used for xml 'id' names in monobook tabs
4462
	 *
4463
	 * @param string $prepend Defaults to 'nstab-'
4464
	 * @return string XML 'id' name
4465
	 */
4466
	public function getNamespaceKey( $prepend = 'nstab-' ) {
4467
		global $wgContLang;
4468
		// Gets the subject namespace if this title
4469
		$namespace = MWNamespace::getSubject( $this->getNamespace() );
4470
		// Checks if canonical namespace name exists for namespace
4471
		if ( MWNamespace::exists( $this->getNamespace() ) ) {
4472
			// Uses canonical namespace name
4473
			$namespaceKey = MWNamespace::getCanonicalName( $namespace );
4474
		} else {
4475
			// Uses text of namespace
4476
			$namespaceKey = $this->getSubjectNsText();
4477
		}
4478
		// Makes namespace key lowercase
4479
		$namespaceKey = $wgContLang->lc( $namespaceKey );
4480
		// Uses main
4481
		if ( $namespaceKey == '' ) {
4482
			$namespaceKey = 'main';
4483
		}
4484
		// Changes file to image for backwards compatibility
4485
		if ( $namespaceKey == 'file' ) {
4486
			$namespaceKey = 'image';
4487
		}
4488
		return $prepend . $namespaceKey;
4489
	}
4490
4491
	/**
4492
	 * Get all extant redirects to this Title
4493
	 *
4494
	 * @param int|null $ns Single namespace to consider; null to consider all namespaces
4495
	 * @return Title[] Array of Title redirects to this title
4496
	 */
4497
	public function getRedirectsHere( $ns = null ) {
4498
		$redirs = [];
4499
4500
		$dbr = wfGetDB( DB_SLAVE );
4501
		$where = [
4502
			'rd_namespace' => $this->getNamespace(),
4503
			'rd_title' => $this->getDBkey(),
4504
			'rd_from = page_id'
4505
		];
4506
		if ( $this->isExternal() ) {
4507
			$where['rd_interwiki'] = $this->getInterwiki();
4508
		} else {
4509
			$where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL';
4510
		}
4511
		if ( !is_null( $ns ) ) {
4512
			$where['page_namespace'] = $ns;
4513
		}
4514
4515
		$res = $dbr->select(
4516
			[ 'redirect', 'page' ],
4517
			[ 'page_namespace', 'page_title' ],
4518
			$where,
4519
			__METHOD__
4520
		);
4521
4522
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
4523
			$redirs[] = self::newFromRow( $row );
4524
		}
4525
		return $redirs;
4526
	}
4527
4528
	/**
4529
	 * Check if this Title is a valid redirect target
4530
	 *
4531
	 * @return bool
4532
	 */
4533
	public function isValidRedirectTarget() {
4534
		global $wgInvalidRedirectTargets;
4535
4536
		if ( $this->isSpecialPage() ) {
4537
			// invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
4538
			if ( $this->isSpecial( 'Userlogout' ) ) {
4539
				return false;
4540
			}
4541
4542
			foreach ( $wgInvalidRedirectTargets as $target ) {
4543
				if ( $this->isSpecial( $target ) ) {
4544
					return false;
4545
				}
4546
			}
4547
		}
4548
4549
		return true;
4550
	}
4551
4552
	/**
4553
	 * Get a backlink cache object
4554
	 *
4555
	 * @return BacklinkCache
4556
	 */
4557
	public function getBacklinkCache() {
4558
		return BacklinkCache::get( $this );
4559
	}
4560
4561
	/**
4562
	 * Whether the magic words __INDEX__ and __NOINDEX__ function for  this page.
4563
	 *
4564
	 * @return bool
4565
	 */
4566
	public function canUseNoindex() {
4567
		global $wgExemptFromUserRobotsControl;
4568
4569
		$bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
4570
			? MWNamespace::getContentNamespaces()
4571
			: $wgExemptFromUserRobotsControl;
4572
4573
		return !in_array( $this->mNamespace, $bannedNamespaces );
4574
4575
	}
4576
4577
	/**
4578
	 * Returns the raw sort key to be used for categories, with the specified
4579
	 * prefix.  This will be fed to Collation::getSortKey() to get a
4580
	 * binary sortkey that can be used for actual sorting.
4581
	 *
4582
	 * @param string $prefix The prefix to be used, specified using
4583
	 *   {{defaultsort:}} or like [[Category:Foo|prefix]].  Empty for no
4584
	 *   prefix.
4585
	 * @return string
4586
	 */
4587
	public function getCategorySortkey( $prefix = '' ) {
4588
		$unprefixed = $this->getText();
4589
4590
		// Anything that uses this hook should only depend
4591
		// on the Title object passed in, and should probably
4592
		// tell the users to run updateCollations.php --force
4593
		// in order to re-sort existing category relations.
4594
		Hooks::run( 'GetDefaultSortkey', [ $this, &$unprefixed ] );
4595
		if ( $prefix !== '' ) {
4596
			# Separate with a line feed, so the unprefixed part is only used as
4597
			# a tiebreaker when two pages have the exact same prefix.
4598
			# In UCA, tab is the only character that can sort above LF
4599
			# so we strip both of them from the original prefix.
4600
			$prefix = strtr( $prefix, "\n\t", '  ' );
4601
			return "$prefix\n$unprefixed";
4602
		}
4603
		return $unprefixed;
4604
	}
4605
4606
	/**
4607
	 * Returns the page language code saved in the database, if $wgPageLanguageUseDB is set
4608
	 * to true in LocalSettings.php, otherwise returns false. If there is no language saved in
4609
	 * the db, it will return NULL.
4610
	 *
4611
	 * @return string|null|bool
4612
	 */
4613
	private function getDbPageLanguageCode() {
4614
		global $wgPageLanguageUseDB;
4615
4616
		// check, if the page language could be saved in the database, and if so and
4617
		// the value is not requested already, lookup the page language using LinkCache
4618 View Code Duplication
		if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) {
4619
			$linkCache = LinkCache::singleton();
4620
			$linkCache->addLinkObj( $this );
4621
			$this->mDbPageLanguage = $linkCache->getGoodLinkFieldObj( $this, 'lang' );
0 ignored issues
show
Documentation Bug introduced by
It seems like $linkCache->getGoodLinkFieldObj($this, 'lang') can also be of type integer. However, the property $mDbPageLanguage is declared as type string|boolean|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
4622
		}
4623
4624
		return $this->mDbPageLanguage;
4625
	}
4626
4627
	/**
4628
	 * Get the language in which the content of this page is written in
4629
	 * wikitext. Defaults to $wgContLang, but in certain cases it can be
4630
	 * e.g. $wgLang (such as special pages, which are in the user language).
4631
	 *
4632
	 * @since 1.18
4633
	 * @return Language
4634
	 */
4635
	public function getPageLanguage() {
4636
		global $wgLang, $wgLanguageCode;
4637
		if ( $this->isSpecialPage() ) {
4638
			// special pages are in the user language
4639
			return $wgLang;
4640
		}
4641
4642
		// Checking if DB language is set
4643
		$dbPageLanguage = $this->getDbPageLanguageCode();
4644
		if ( $dbPageLanguage ) {
4645
			return wfGetLangObj( $dbPageLanguage );
4646
		}
4647
4648
		if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) {
4649
			// Note that this may depend on user settings, so the cache should
4650
			// be only per-request.
4651
			// NOTE: ContentHandler::getPageLanguage() may need to load the
4652
			// content to determine the page language!
4653
			// Checking $wgLanguageCode hasn't changed for the benefit of unit
4654
			// tests.
4655
			$contentHandler = ContentHandler::getForTitle( $this );
4656
			$langObj = $contentHandler->getPageLanguage( $this );
4657
			$this->mPageLanguage = [ $langObj->getCode(), $wgLanguageCode ];
0 ignored issues
show
Documentation Bug introduced by
It seems like array($langObj->getCode(), $wgLanguageCode) of type array<integer,string,{"0":"string"}> is incompatible with the declared type boolean of property $mPageLanguage.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
4658
		} else {
4659
			$langObj = wfGetLangObj( $this->mPageLanguage[0] );
4660
		}
4661
4662
		return $langObj;
4663
	}
4664
4665
	/**
4666
	 * Get the language in which the content of this page is written when
4667
	 * viewed by user. Defaults to $wgContLang, but in certain cases it can be
4668
	 * e.g. $wgLang (such as special pages, which are in the user language).
4669
	 *
4670
	 * @since 1.20
4671
	 * @return Language
4672
	 */
4673
	public function getPageViewLanguage() {
4674
		global $wgLang;
4675
4676
		if ( $this->isSpecialPage() ) {
4677
			// If the user chooses a variant, the content is actually
4678
			// in a language whose code is the variant code.
4679
			$variant = $wgLang->getPreferredVariant();
4680
			if ( $wgLang->getCode() !== $variant ) {
4681
				return Language::factory( $variant );
4682
			}
4683
4684
			return $wgLang;
4685
		}
4686
4687
		// Checking if DB language is set
4688
		$dbPageLanguage = $this->getDbPageLanguageCode();
4689
		if ( $dbPageLanguage ) {
4690
			$pageLang = wfGetLangObj( $dbPageLanguage );
4691
			$variant = $pageLang->getPreferredVariant();
4692
			if ( $pageLang->getCode() !== $variant ) {
4693
				$pageLang = Language::factory( $variant );
4694
			}
4695
4696
			return $pageLang;
4697
		}
4698
4699
		// @note Can't be cached persistently, depends on user settings.
4700
		// @note ContentHandler::getPageViewLanguage() may need to load the
4701
		//   content to determine the page language!
4702
		$contentHandler = ContentHandler::getForTitle( $this );
4703
		$pageLang = $contentHandler->getPageViewLanguage( $this );
4704
		return $pageLang;
4705
	}
4706
4707
	/**
4708
	 * Get a list of rendered edit notices for this page.
4709
	 *
4710
	 * Array is keyed by the original message key, and values are rendered using parseAsBlock, so
4711
	 * they will already be wrapped in paragraphs.
4712
	 *
4713
	 * @since 1.21
4714
	 * @param int $oldid Revision ID that's being edited
4715
	 * @return array
4716
	 */
4717
	public function getEditNotices( $oldid = 0 ) {
4718
		$notices = [];
4719
4720
		// Optional notice for the entire namespace
4721
		$editnotice_ns = 'editnotice-' . $this->getNamespace();
4722
		$msg = wfMessage( $editnotice_ns );
4723 View Code Duplication
		if ( $msg->exists() ) {
4724
			$html = $msg->parseAsBlock();
4725
			// Edit notices may have complex logic, but output nothing (T91715)
4726
			if ( trim( $html ) !== '' ) {
4727
				$notices[$editnotice_ns] = Html::rawElement(
4728
					'div',
4729
					[ 'class' => [
4730
						'mw-editnotice',
4731
						'mw-editnotice-namespace',
4732
						Sanitizer::escapeClass( "mw-$editnotice_ns" )
4733
					] ],
4734
					$html
4735
				);
4736
			}
4737
		}
4738
4739
		if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) {
4740
			// Optional notice for page itself and any parent page
4741
			$parts = explode( '/', $this->getDBkey() );
4742
			$editnotice_base = $editnotice_ns;
4743
			while ( count( $parts ) > 0 ) {
4744
				$editnotice_base .= '-' . array_shift( $parts );
4745
				$msg = wfMessage( $editnotice_base );
4746 View Code Duplication
				if ( $msg->exists() ) {
4747
					$html = $msg->parseAsBlock();
4748
					if ( trim( $html ) !== '' ) {
4749
						$notices[$editnotice_base] = Html::rawElement(
4750
							'div',
4751
							[ 'class' => [
4752
								'mw-editnotice',
4753
								'mw-editnotice-base',
4754
								Sanitizer::escapeClass( "mw-$editnotice_base" )
4755
							] ],
4756
							$html
4757
						);
4758
					}
4759
				}
4760
			}
4761
		} else {
4762
			// Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys
4763
			$editnoticeText = $editnotice_ns . '-' . strtr( $this->getDBkey(), '/', '-' );
4764
			$msg = wfMessage( $editnoticeText );
4765 View Code Duplication
			if ( $msg->exists() ) {
4766
				$html = $msg->parseAsBlock();
4767
				if ( trim( $html ) !== '' ) {
4768
					$notices[$editnoticeText] = Html::rawElement(
4769
						'div',
4770
						[ 'class' => [
4771
							'mw-editnotice',
4772
							'mw-editnotice-page',
4773
							Sanitizer::escapeClass( "mw-$editnoticeText" )
4774
						] ],
4775
						$html
4776
					);
4777
				}
4778
			}
4779
		}
4780
4781
		Hooks::run( 'TitleGetEditNotices', [ $this, $oldid, &$notices ] );
4782
		return $notices;
4783
	}
4784
4785
	/**
4786
	 * @return array
4787
	 */
4788
	public function __sleep() {
4789
		return [
4790
			'mNamespace',
4791
			'mDbkeyform',
4792
			'mFragment',
4793
			'mInterwiki',
4794
			'mLocalInterwiki',
4795
			'mUserCaseDBKey',
4796
			'mDefaultNamespace',
4797
		];
4798
	}
4799
4800
	public function __wakeup() {
4801
		$this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0;
4802
		$this->mUrlform = wfUrlencode( $this->mDbkeyform );
4803
		$this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
4804
	}
4805
4806
}
4807