Completed
Branch master (e2eefa)
by
unknown
25:58
created

Title::getMediaWikiTitleCodec()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 27
rs 8.8571
cc 3
eloc 14
nc 4
nop 0

2 Methods

Rating   Name   Duplication   Size   Complexity  
A Title::__construct() 0 2 1
A Title::newFromDBkey() 0 11 2
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\MediaWikiServices;
26
27
/**
28
 * Represents a title within MediaWiki.
29
 * Optionally may contain an interwiki designation or namespace.
30
 * @note This class can fetch various kinds of data from the database;
31
 *       however, it does so inefficiently.
32
 * @note Consider using a TitleValue object instead. TitleValue is more lightweight
33
 *       and does not rely on global state or the database.
34
 */
35
class Title implements LinkTarget {
36
	/** @var HashBagOStuff */
37
	static private $titleCache = null;
38
39
	/**
40
	 * Title::newFromText maintains a cache to avoid expensive re-normalization of
41
	 * commonly used titles. On a batch operation this can become a memory leak
42
	 * if not bounded. After hitting this many titles reset the cache.
43
	 */
44
	const CACHE_MAX = 1000;
45
46
	/**
47
	 * Used to be GAID_FOR_UPDATE define. Used with getArticleID() and friends
48
	 * to use the master DB
49
	 */
50
	const GAID_FOR_UPDATE = 1;
51
52
	/**
53
	 * @name Private member variables
54
	 * Please use the accessor functions instead.
55
	 * @private
56
	 */
57
	// @{
58
59
	/** @var string Text form (spaces not underscores) of the main part */
60
	public $mTextform = '';
61
62
	/** @var string URL-encoded form of the main part */
63
	public $mUrlform = '';
64
65
	/** @var string Main part with underscores */
66
	public $mDbkeyform = '';
67
68
	/** @var string Database key with the initial letter in the case specified by the user */
69
	protected $mUserCaseDBKey;
70
71
	/** @var int Namespace index, i.e. one of the NS_xxxx constants */
72
	public $mNamespace = NS_MAIN;
73
74
	/** @var string Interwiki prefix */
75
	public $mInterwiki = '';
76
77
	/** @var bool Was this Title created from a string with a local interwiki prefix? */
78
	private $mLocalInterwiki = false;
79
80
	/** @var string Title fragment (i.e. the bit after the #) */
81
	public $mFragment = '';
82
83
	/** @var int Article ID, fetched from the link cache on demand */
84
	public $mArticleID = -1;
85
86
	/** @var bool|int ID of most recent revision */
87
	protected $mLatestID = false;
88
89
	/**
90
	 * @var bool|string ID of the page's content model, i.e. one of the
91
	 *   CONTENT_MODEL_XXX constants
92
	 */
93
	public $mContentModel = false;
94
95
	/** @var int Estimated number of revisions; null of not loaded */
96
	private $mEstimateRevisions;
97
98
	/** @var array Array of groups allowed to edit this article */
99
	public $mRestrictions = [];
100
101
	/** @var string|bool */
102
	protected $mOldRestrictions = false;
103
104
	/** @var bool Cascade restrictions on this page to included templates and images? */
105
	public $mCascadeRestriction;
106
107
	/** Caching the results of getCascadeProtectionSources */
108
	public $mCascadingRestrictions;
109
110
	/** @var array When do the restrictions on this page expire? */
111
	protected $mRestrictionsExpiry = [];
112
113
	/** @var bool Are cascading restrictions in effect on this page? */
114
	protected $mHasCascadingRestrictions;
115
116
	/** @var array Where are the cascading restrictions coming from on this page? */
117
	public $mCascadeSources;
118
119
	/** @var bool Boolean for initialisation on demand */
120
	public $mRestrictionsLoaded = false;
121
122
	/** @var string Text form including namespace/interwiki, initialised on demand */
123
	protected $mPrefixedText = null;
124
125
	/** @var mixed Cached value for getTitleProtection (create protection) */
126
	public $mTitleProtection;
127
128
	/**
129
	 * @var int Namespace index when there is no namespace. Don't change the
130
	 *   following default, NS_MAIN is hardcoded in several places. See bug 696.
131
	 *   Zero except in {{transclusion}} tags.
132
	 */
133
	public $mDefaultNamespace = NS_MAIN;
134
135
	/** @var int The page length, 0 for special pages */
136
	protected $mLength = -1;
137
138
	/** @var null Is the article at this title a redirect? */
139
	public $mRedirect = null;
140
141
	/** @var array Associative array of user ID -> timestamp/false */
142
	private $mNotificationTimestamp = [];
143
144
	/** @var bool Whether a page has any subpages */
145
	private $mHasSubpages;
146
147
	/** @var bool The (string) language code of the page's language and content code. */
148
	private $mPageLanguage = false;
149
150
	/** @var string|bool|null The page language code from the database, null if not saved in
151
	 * the database or false if not loaded, yet. */
152
	private $mDbPageLanguage = false;
153
154
	/** @var TitleValue A corresponding TitleValue object */
155
	private $mTitleValue = null;
156
157
	/** @var bool Would deleting this page be a big deletion? */
158
	private $mIsBigDeletion = null;
159
	// @}
160
161
	/**
162
	 * B/C kludge: provide a TitleParser for use by Title.
163
	 * Ideally, Title would have no methods that need this.
164
	 * Avoid usage of this singleton by using TitleValue
165
	 * and the associated services when possible.
166
	 *
167
	 * @return TitleFormatter
168
	 */
169
	private static function getTitleFormatter() {
170
		return MediaWikiServices::getInstance()->getTitleFormatter();
171
	}
172
173
	function __construct() {
174
	}
175
176
	/**
177
	 * Create a new Title from a prefixed DB key
178
	 *
179
	 * @param string $key The database key, which has underscores
180
	 *	instead of spaces, possibly including namespace and
181
	 *	interwiki prefixes
182
	 * @return Title|null Title, or null on an error
183
	 */
184
	public static function newFromDBkey( $key ) {
185
		$t = new Title();
186
		$t->mDbkeyform = $key;
187
188
		try {
189
			$t->secureAndSplit();
190
			return $t;
191
		} catch ( MalformedTitleException $ex ) {
192
			return null;
193
		}
194
	}
195
196
	/**
197
	 * Create a new Title from a TitleValue
198
	 *
199
	 * @param TitleValue $titleValue Assumed to be safe.
200
	 *
201
	 * @return Title
202
	 */
203
	public static function newFromTitleValue( TitleValue $titleValue ) {
204
		return self::newFromLinkTarget( $titleValue );
205
	}
206
207
	/**
208
	 * Create a new Title from a LinkTarget
209
	 *
210
	 * @param LinkTarget $linkTarget Assumed to be safe.
211
	 *
212
	 * @return Title
213
	 */
214
	public static function newFromLinkTarget( LinkTarget $linkTarget ) {
215
		if ( $linkTarget instanceof Title ) {
216
			// Special case if it's already a Title object
217
			return $linkTarget;
218
		}
219
		return self::makeTitle(
220
			$linkTarget->getNamespace(),
221
			$linkTarget->getText(),
222
			$linkTarget->getFragment(),
223
			$linkTarget->getInterwiki()
224
		);
225
	}
226
227
	/**
228
	 * Create a new Title from text, such as what one would find in a link. De-
229
	 * codes any HTML entities in the text.
230
	 *
231
	 * @param string|int|null $text The link text; spaces, prefixes, and an
232
	 *   initial ':' indicating the main namespace are accepted.
233
	 * @param int $defaultNamespace The namespace to use if none is specified
234
	 *   by a prefix.  If you want to force a specific namespace even if
235
	 *   $text might begin with a namespace prefix, use makeTitle() or
236
	 *   makeTitleSafe().
237
	 * @throws InvalidArgumentException
238
	 * @return Title|null Title or null on an error.
239
	 */
240
	public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
241
		// DWIM: Integers can be passed in here when page titles are used as array keys.
242
		if ( $text !== null && !is_string( $text ) && !is_int( $text ) ) {
243
			throw new InvalidArgumentException( '$text must be a string.' );
244
		}
245
		if ( $text === null ) {
246
			return null;
247
		}
248
249
		try {
250
			return Title::newFromTextThrow( strval( $text ), $defaultNamespace );
251
		} catch ( MalformedTitleException $ex ) {
252
			return null;
253
		}
254
	}
255
256
	/**
257
	 * Like Title::newFromText(), but throws MalformedTitleException when the title is invalid,
258
	 * rather than returning null.
259
	 *
260
	 * The exception subclasses encode detailed information about why the title is invalid.
261
	 *
262
	 * @see Title::newFromText
263
	 *
264
	 * @since 1.25
265
	 * @param string $text Title text to check
266
	 * @param int $defaultNamespace
267
	 * @throws MalformedTitleException If the title is invalid
268
	 * @return Title
269
	 */
270
	public static function newFromTextThrow( $text, $defaultNamespace = NS_MAIN ) {
271
		if ( is_object( $text ) ) {
272
			throw new MWException( '$text must be a string, given an object' );
273
		}
274
275
		$titleCache = self::getTitleCache();
276
277
		// Wiki pages often contain multiple links to the same page.
278
		// Title normalization and parsing can become expensive on pages with many
279
		// links, so we can save a little time by caching them.
280
		// In theory these are value objects and won't get changed...
281
		if ( $defaultNamespace == NS_MAIN ) {
282
			$t = $titleCache->get( $text );
283
			if ( $t ) {
284
				return $t;
285
			}
286
		}
287
288
		// Convert things like &eacute; &#257; or &#x3017; into normalized (bug 14952) text
289
		$filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
290
291
		$t = new Title();
292
		$t->mDbkeyform = strtr( $filteredText, ' ', '_' );
293
		$t->mDefaultNamespace = intval( $defaultNamespace );
294
295
		$t->secureAndSplit();
296
		if ( $defaultNamespace == NS_MAIN ) {
297
			$titleCache->set( $text, $t );
298
		}
299
		return $t;
300
	}
301
302
	/**
303
	 * THIS IS NOT THE FUNCTION YOU WANT. Use Title::newFromText().
304
	 *
305
	 * Example of wrong and broken code:
306
	 * $title = Title::newFromURL( $wgRequest->getVal( 'title' ) );
307
	 *
308
	 * Example of right code:
309
	 * $title = Title::newFromText( $wgRequest->getVal( 'title' ) );
310
	 *
311
	 * Create a new Title from URL-encoded text. Ensures that
312
	 * the given title's length does not exceed the maximum.
313
	 *
314
	 * @param string $url The title, as might be taken from a URL
315
	 * @return Title|null The new object, or null on an error
316
	 */
317
	public static function newFromURL( $url ) {
318
		$t = new Title();
319
320
		# For compatibility with old buggy URLs. "+" is usually not valid in titles,
321
		# but some URLs used it as a space replacement and they still come
322
		# from some external search tools.
323
		if ( strpos( self::legalChars(), '+' ) === false ) {
324
			$url = strtr( $url, '+', ' ' );
325
		}
326
327
		$t->mDbkeyform = strtr( $url, ' ', '_' );
328
329
		try {
330
			$t->secureAndSplit();
331
			return $t;
332
		} catch ( MalformedTitleException $ex ) {
333
			return null;
334
		}
335
	}
336
337
	/**
338
	 * @return HashBagOStuff
339
	 */
340
	private static function getTitleCache() {
341
		if ( self::$titleCache == null ) {
342
			self::$titleCache = new HashBagOStuff( [ 'maxKeys' => self::CACHE_MAX ] );
343
		}
344
		return self::$titleCache;
345
	}
346
347
	/**
348
	 * Returns a list of fields that are to be selected for initializing Title
349
	 * objects or LinkCache entries. Uses $wgContentHandlerUseDB to determine
350
	 * whether to include page_content_model.
351
	 *
352
	 * @return array
353
	 */
354
	protected static function getSelectFields() {
355
		global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
356
357
		$fields = [
358
			'page_namespace', 'page_title', 'page_id',
359
			'page_len', 'page_is_redirect', 'page_latest',
360
		];
361
362
		if ( $wgContentHandlerUseDB ) {
363
			$fields[] = 'page_content_model';
364
		}
365
366
		if ( $wgPageLanguageUseDB ) {
367
			$fields[] = 'page_lang';
368
		}
369
370
		return $fields;
371
	}
372
373
	/**
374
	 * Create a new Title from an article ID
375
	 *
376
	 * @param int $id The page_id corresponding to the Title to create
377
	 * @param int $flags Use Title::GAID_FOR_UPDATE to use master
378
	 * @return Title|null The new object, or null on an error
379
	 */
380
	public static function newFromID( $id, $flags = 0 ) {
381
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
382
		$row = $db->selectRow(
383
			'page',
384
			self::getSelectFields(),
385
			[ 'page_id' => $id ],
386
			__METHOD__
387
		);
388
		if ( $row !== false ) {
389
			$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 382 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...
390
		} else {
391
			$title = null;
392
		}
393
		return $title;
394
	}
395
396
	/**
397
	 * Make an array of titles from an array of IDs
398
	 *
399
	 * @param int[] $ids Array of IDs
400
	 * @return Title[] Array of Titles
401
	 */
402
	public static function newFromIDs( $ids ) {
403
		if ( !count( $ids ) ) {
404
			return [];
405
		}
406
		$dbr = wfGetDB( DB_SLAVE );
407
408
		$res = $dbr->select(
409
			'page',
410
			self::getSelectFields(),
411
			[ 'page_id' => $ids ],
412
			__METHOD__
413
		);
414
415
		$titles = [];
416
		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...
417
			$titles[] = Title::newFromRow( $row );
418
		}
419
		return $titles;
420
	}
421
422
	/**
423
	 * Make a Title object from a DB row
424
	 *
425
	 * @param stdClass $row Object database row (needs at least page_title,page_namespace)
426
	 * @return Title Corresponding Title
427
	 */
428
	public static function newFromRow( $row ) {
429
		$t = self::makeTitle( $row->page_namespace, $row->page_title );
430
		$t->loadFromRow( $row );
431
		return $t;
432
	}
433
434
	/**
435
	 * Load Title object fields from a DB row.
436
	 * If false is given, the title will be treated as non-existing.
437
	 *
438
	 * @param stdClass|bool $row Database row
439
	 */
440
	public function loadFromRow( $row ) {
441
		if ( $row ) { // page found
442
			if ( isset( $row->page_id ) ) {
443
				$this->mArticleID = (int)$row->page_id;
444
			}
445
			if ( isset( $row->page_len ) ) {
446
				$this->mLength = (int)$row->page_len;
447
			}
448
			if ( isset( $row->page_is_redirect ) ) {
449
				$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...
450
			}
451
			if ( isset( $row->page_latest ) ) {
452
				$this->mLatestID = (int)$row->page_latest;
453
			}
454
			if ( isset( $row->page_content_model ) ) {
455
				$this->mContentModel = strval( $row->page_content_model );
456
			} else {
457
				$this->mContentModel = false; # initialized lazily in getContentModel()
458
			}
459
			if ( isset( $row->page_lang ) ) {
460
				$this->mDbPageLanguage = (string)$row->page_lang;
461
			}
462
			if ( isset( $row->page_restrictions ) ) {
463
				$this->mOldRestrictions = $row->page_restrictions;
464
			}
465
		} else { // page not found
466
			$this->mArticleID = 0;
467
			$this->mLength = 0;
468
			$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...
469
			$this->mLatestID = 0;
470
			$this->mContentModel = false; # initialized lazily in getContentModel()
471
		}
472
	}
473
474
	/**
475
	 * Create a new Title from a namespace index and a DB key.
476
	 * It's assumed that $ns and $title are *valid*, for instance when
477
	 * they came directly from the database or a special page name.
478
	 * For convenience, spaces are converted to underscores so that
479
	 * eg user_text fields can be used directly.
480
	 *
481
	 * @param int $ns The namespace of the article
482
	 * @param string $title The unprefixed database key form
483
	 * @param string $fragment The link fragment (after the "#")
484
	 * @param string $interwiki The interwiki prefix
485
	 * @return Title The new object
486
	 */
487
	public static function &makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) {
488
		$t = new Title();
489
		$t->mInterwiki = $interwiki;
490
		$t->mFragment = $fragment;
491
		$t->mNamespace = $ns = intval( $ns );
492
		$t->mDbkeyform = strtr( $title, ' ', '_' );
493
		$t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
494
		$t->mUrlform = wfUrlencode( $t->mDbkeyform );
495
		$t->mTextform = strtr( $title, '_', ' ' );
496
		$t->mContentModel = false; # initialized lazily in getContentModel()
497
		return $t;
498
	}
499
500
	/**
501
	 * Create a new Title from a namespace index and a DB key.
502
	 * The parameters will be checked for validity, which is a bit slower
503
	 * than makeTitle() but safer for user-provided data.
504
	 *
505
	 * @param int $ns The namespace of the article
506
	 * @param string $title Database key form
507
	 * @param string $fragment The link fragment (after the "#")
508
	 * @param string $interwiki Interwiki prefix
509
	 * @return Title|null The new object, or null on an error
510
	 */
511
	public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
512
		if ( !MWNamespace::exists( $ns ) ) {
513
			return null;
514
		}
515
516
		$t = new Title();
517
		$t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki, true );
518
519
		try {
520
			$t->secureAndSplit();
521
			return $t;
522
		} catch ( MalformedTitleException $ex ) {
523
			return null;
524
		}
525
	}
526
527
	/**
528
	 * Create a new Title for the Main Page
529
	 *
530
	 * @return Title The new object
531
	 */
532
	public static function newMainPage() {
533
		$title = Title::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() );
534
		// Don't give fatal errors if the message is broken
535
		if ( !$title ) {
536
			$title = Title::newFromText( 'Main Page' );
537
		}
538
		return $title;
539
	}
540
541
	/**
542
	 * Get the prefixed DB key associated with an ID
543
	 *
544
	 * @param int $id The page_id of the article
545
	 * @return Title|null An object representing the article, or null if no such article was found
546
	 */
547
	public static function nameOf( $id ) {
548
		$dbr = wfGetDB( DB_SLAVE );
549
550
		$s = $dbr->selectRow(
551
			'page',
552
			[ 'page_namespace', 'page_title' ],
553
			[ 'page_id' => $id ],
554
			__METHOD__
555
		);
556
		if ( $s === false ) {
557
			return null;
558
		}
559
560
		$n = self::makeName( $s->page_namespace, $s->page_title );
561
		return $n;
562
	}
563
564
	/**
565
	 * Get a regex character class describing the legal characters in a link
566
	 *
567
	 * @return string The list of characters, not delimited
568
	 */
569
	public static function legalChars() {
570
		global $wgLegalTitleChars;
571
		return $wgLegalTitleChars;
572
	}
573
574
	/**
575
	 * Returns a simple regex that will match on characters and sequences invalid in titles.
576
	 * Note that this doesn't pick up many things that could be wrong with titles, but that
577
	 * replacing this regex with something valid will make many titles valid.
578
	 *
579
	 * @deprecated since 1.25, use MediaWikiTitleCodec::getTitleInvalidRegex() instead
580
	 *
581
	 * @return string Regex string
582
	 */
583
	static function getTitleInvalidRegex() {
584
		wfDeprecated( __METHOD__, '1.25' );
585
		return MediaWikiTitleCodec::getTitleInvalidRegex();
586
	}
587
588
	/**
589
	 * Utility method for converting a character sequence from bytes to Unicode.
590
	 *
591
	 * Primary usecase being converting $wgLegalTitleChars to a sequence usable in
592
	 * javascript, as PHP uses UTF-8 bytes where javascript uses Unicode code units.
593
	 *
594
	 * @param string $byteClass
595
	 * @return string
596
	 */
597
	public static function convertByteClassToUnicodeClass( $byteClass ) {
598
		$length = strlen( $byteClass );
599
		// Input token queue
600
		$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...
601
		// Decoded queue
602
		$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...
603
		// Decoded integer codepoints
604
		$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...
605
		// Re-encoded queue
606
		$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...
607
		// Output
608
		$out = '';
609
		// Flags
610
		$allowUnicode = false;
611
		for ( $pos = 0; $pos < $length; $pos++ ) {
612
			// Shift the queues down
613
			$x2 = $x1;
614
			$x1 = $x0;
615
			$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...
616
			$d1 = $d0;
617
			$ord2 = $ord1;
618
			$ord1 = $ord0;
619
			$r2 = $r1;
620
			$r1 = $r0;
621
			// Load the current input token and decoded values
622
			$inChar = $byteClass[$pos];
623
			if ( $inChar == '\\' ) {
624
				if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) {
625
					$x0 = $inChar . $m[0];
626
					$d0 = chr( hexdec( $m[1] ) );
627
					$pos += strlen( $m[0] );
628
				} elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) {
629
					$x0 = $inChar . $m[0];
630
					$d0 = chr( octdec( $m[0] ) );
631
					$pos += strlen( $m[0] );
632
				} elseif ( $pos + 1 >= $length ) {
633
					$x0 = $d0 = '\\';
634
				} else {
635
					$d0 = $byteClass[$pos + 1];
636
					$x0 = $inChar . $d0;
637
					$pos += 1;
638
				}
639
			} else {
640
				$x0 = $d0 = $inChar;
641
			}
642
			$ord0 = ord( $d0 );
643
			// Load the current re-encoded value
644
			if ( $ord0 < 32 || $ord0 == 0x7f ) {
645
				$r0 = sprintf( '\x%02x', $ord0 );
646
			} elseif ( $ord0 >= 0x80 ) {
647
				// Allow unicode if a single high-bit character appears
648
				$r0 = sprintf( '\x%02x', $ord0 );
649
				$allowUnicode = true;
650
			} elseif ( strpos( '-\\[]^', $d0 ) !== false ) {
651
				$r0 = '\\' . $d0;
652
			} else {
653
				$r0 = $d0;
654
			}
655
			// Do the output
656
			if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) {
657
				// Range
658
				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...
659
					// Empty range
660
				} elseif ( $ord0 >= 0x80 ) {
661
					// Unicode range
662
					$allowUnicode = true;
663
					if ( $ord2 < 0x80 ) {
664
						// Keep the non-unicode section of the range
665
						$out .= "$r2-\\x7F";
666
					}
667
				} else {
668
					// Normal range
669
					$out .= "$r2-$r0";
670
				}
671
				// Reset state to the initial value
672
				$x0 = $x1 = $d0 = $d1 = $r0 = $r1 = '';
673
			} elseif ( $ord2 < 0x80 ) {
674
				// ASCII character
675
				$out .= $r2;
676
			}
677
		}
678
		if ( $ord1 < 0x80 ) {
679
			$out .= $r1;
680
		}
681
		if ( $ord0 < 0x80 ) {
682
			$out .= $r0;
683
		}
684
		if ( $allowUnicode ) {
685
			$out .= '\u0080-\uFFFF';
686
		}
687
		return $out;
688
	}
689
690
	/**
691
	 * Make a prefixed DB key from a DB key and a namespace index
692
	 *
693
	 * @param int $ns Numerical representation of the namespace
694
	 * @param string $title The DB key form the title
695
	 * @param string $fragment The link fragment (after the "#")
696
	 * @param string $interwiki The interwiki prefix
697
	 * @param bool $canonicalNamespace If true, use the canonical name for
698
	 *   $ns instead of the localized version.
699
	 * @return string The prefixed form of the title
700
	 */
701
	public static function makeName( $ns, $title, $fragment = '', $interwiki = '',
702
		$canonicalNamespace = false
703
	) {
704
		global $wgContLang;
705
706
		if ( $canonicalNamespace ) {
707
			$namespace = MWNamespace::getCanonicalName( $ns );
708
		} else {
709
			$namespace = $wgContLang->getNsText( $ns );
710
		}
711
		$name = $namespace == '' ? $title : "$namespace:$title";
712
		if ( strval( $interwiki ) != '' ) {
713
			$name = "$interwiki:$name";
714
		}
715
		if ( strval( $fragment ) != '' ) {
716
			$name .= '#' . $fragment;
717
		}
718
		return $name;
719
	}
720
721
	/**
722
	 * Escape a text fragment, say from a link, for a URL
723
	 *
724
	 * @param string $fragment Containing a URL or link fragment (after the "#")
725
	 * @return string Escaped string
726
	 */
727
	static function escapeFragmentForURL( $fragment ) {
728
		# Note that we don't urlencode the fragment.  urlencoded Unicode
729
		# fragments appear not to work in IE (at least up to 7) or in at least
730
		# one version of Opera 9.x.  The W3C validator, for one, doesn't seem
731
		# to care if they aren't encoded.
732
		return Sanitizer::escapeId( $fragment, 'noninitial' );
733
	}
734
735
	/**
736
	 * Callback for usort() to do title sorts by (namespace, title)
737
	 *
738
	 * @param Title $a
739
	 * @param Title $b
740
	 *
741
	 * @return int Result of string comparison, or namespace comparison
742
	 */
743
	public static function compare( $a, $b ) {
744
		if ( $a->getNamespace() == $b->getNamespace() ) {
745
			return strcmp( $a->getText(), $b->getText() );
746
		} else {
747
			return $a->getNamespace() - $b->getNamespace();
748
		}
749
	}
750
751
	/**
752
	 * Determine whether the object refers to a page within
753
	 * this project (either this wiki or a wiki with a local
754
	 * interwiki, see https://www.mediawiki.org/wiki/Manual:Interwiki_table#iw_local )
755
	 *
756
	 * @return bool True if this is an in-project interwiki link or a wikilink, false otherwise
757
	 */
758
	public function isLocal() {
759
		if ( $this->isExternal() ) {
760
			$iw = Interwiki::fetch( $this->mInterwiki );
761
			if ( $iw ) {
762
				return $iw->isLocal();
763
			}
764
		}
765
		return true;
766
	}
767
768
	/**
769
	 * Is this Title interwiki?
770
	 *
771
	 * @return bool
772
	 */
773
	public function isExternal() {
774
		return $this->mInterwiki !== '';
775
	}
776
777
	/**
778
	 * Get the interwiki prefix
779
	 *
780
	 * Use Title::isExternal to check if a interwiki is set
781
	 *
782
	 * @return string Interwiki prefix
783
	 */
784
	public function getInterwiki() {
785
		return $this->mInterwiki;
786
	}
787
788
	/**
789
	 * Was this a local interwiki link?
790
	 *
791
	 * @return bool
792
	 */
793
	public function wasLocalInterwiki() {
794
		return $this->mLocalInterwiki;
795
	}
796
797
	/**
798
	 * Determine whether the object refers to a page within
799
	 * this project and is transcludable.
800
	 *
801
	 * @return bool True if this is transcludable
802
	 */
803
	public function isTrans() {
804
		if ( !$this->isExternal() ) {
805
			return false;
806
		}
807
808
		return Interwiki::fetch( $this->mInterwiki )->isTranscludable();
809
	}
810
811
	/**
812
	 * Returns the DB name of the distant wiki which owns the object.
813
	 *
814
	 * @return string The DB name
815
	 */
816
	public function getTransWikiID() {
817
		if ( !$this->isExternal() ) {
818
			return false;
819
		}
820
821
		return Interwiki::fetch( $this->mInterwiki )->getWikiID();
822
	}
823
824
	/**
825
	 * Get a TitleValue object representing this Title.
826
	 *
827
	 * @note Not all valid Titles have a corresponding valid TitleValue
828
	 * (e.g. TitleValues cannot represent page-local links that have a
829
	 * fragment but no title text).
830
	 *
831
	 * @return TitleValue|null
832
	 */
833
	public function getTitleValue() {
834
		if ( $this->mTitleValue === null ) {
835
			try {
836
				$this->mTitleValue = new TitleValue(
837
					$this->getNamespace(),
838
					$this->getDBkey(),
839
					$this->getFragment(),
840
					$this->getInterwiki()
841
				);
842
			} catch ( InvalidArgumentException $ex ) {
843
				wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' .
844
					$this->getPrefixedText() . ']]: ' . $ex->getMessage() . "\n" );
845
			}
846
		}
847
848
		return $this->mTitleValue;
849
	}
850
851
	/**
852
	 * Get the text form (spaces not underscores) of the main part
853
	 *
854
	 * @return string Main part of the title
855
	 */
856
	public function getText() {
857
		return $this->mTextform;
858
	}
859
860
	/**
861
	 * Get the URL-encoded form of the main part
862
	 *
863
	 * @return string Main part of the title, URL-encoded
864
	 */
865
	public function getPartialURL() {
866
		return $this->mUrlform;
867
	}
868
869
	/**
870
	 * Get the main part with underscores
871
	 *
872
	 * @return string Main part of the title, with underscores
873
	 */
874
	public function getDBkey() {
875
		return $this->mDbkeyform;
876
	}
877
878
	/**
879
	 * Get the DB key with the initial letter case as specified by the user
880
	 *
881
	 * @return string DB key
882
	 */
883
	function getUserCaseDBKey() {
884
		if ( !is_null( $this->mUserCaseDBKey ) ) {
885
			return $this->mUserCaseDBKey;
886
		} else {
887
			// If created via makeTitle(), $this->mUserCaseDBKey is not set.
888
			return $this->mDbkeyform;
889
		}
890
	}
891
892
	/**
893
	 * Get the namespace index, i.e. one of the NS_xxxx constants.
894
	 *
895
	 * @return int Namespace index
896
	 */
897
	public function getNamespace() {
898
		return $this->mNamespace;
899
	}
900
901
	/**
902
	 * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
903
	 *
904
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
905
	 * @return string Content model id
906
	 */
907
	public function getContentModel( $flags = 0 ) {
908 View Code Duplication
		if ( !$this->mContentModel && $this->getArticleID( $flags ) ) {
909
			$linkCache = LinkCache::singleton();
910
			$linkCache->addLinkObj( $this ); # in case we already had an article ID
911
			$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...
912
		}
913
914
		if ( !$this->mContentModel ) {
915
			$this->mContentModel = ContentHandler::getDefaultModelFor( $this );
916
		}
917
918
		return $this->mContentModel;
919
	}
920
921
	/**
922
	 * Convenience method for checking a title's content model name
923
	 *
924
	 * @param string $id The content model ID (use the CONTENT_MODEL_XXX constants).
925
	 * @return bool True if $this->getContentModel() == $id
926
	 */
927
	public function hasContentModel( $id ) {
928
		return $this->getContentModel() == $id;
929
	}
930
931
	/**
932
	 * Get the namespace text
933
	 *
934
	 * @return string Namespace text
935
	 */
936
	public function getNsText() {
937
		if ( $this->isExternal() ) {
938
			// This probably shouldn't even happen,
939
			// but for interwiki transclusion it sometimes does.
940
			// Use the canonical namespaces if possible to try to
941
			// resolve a foreign namespace.
942
			if ( MWNamespace::exists( $this->mNamespace ) ) {
943
				return MWNamespace::getCanonicalName( $this->mNamespace );
944
			}
945
		}
946
947
		try {
948
			$formatter = self::getTitleFormatter();
949
			return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform );
950
		} catch ( InvalidArgumentException $ex ) {
951
			wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" );
952
			return false;
953
		}
954
	}
955
956
	/**
957
	 * Get the namespace text of the subject (rather than talk) page
958
	 *
959
	 * @return string Namespace text
960
	 */
961
	public function getSubjectNsText() {
962
		global $wgContLang;
963
		return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) );
964
	}
965
966
	/**
967
	 * Get the namespace text of the talk page
968
	 *
969
	 * @return string Namespace text
970
	 */
971
	public function getTalkNsText() {
972
		global $wgContLang;
973
		return $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) );
974
	}
975
976
	/**
977
	 * Could this title have a corresponding talk page?
978
	 *
979
	 * @return bool
980
	 */
981
	public function canTalk() {
982
		return MWNamespace::canTalk( $this->mNamespace );
983
	}
984
985
	/**
986
	 * Is this in a namespace that allows actual pages?
987
	 *
988
	 * @return bool
989
	 */
990
	public function canExist() {
991
		return $this->mNamespace >= NS_MAIN;
992
	}
993
994
	/**
995
	 * Can this title be added to a user's watchlist?
996
	 *
997
	 * @return bool
998
	 */
999
	public function isWatchable() {
1000
		return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
1001
	}
1002
1003
	/**
1004
	 * Returns true if this is a special page.
1005
	 *
1006
	 * @return bool
1007
	 */
1008
	public function isSpecialPage() {
1009
		return $this->getNamespace() == NS_SPECIAL;
1010
	}
1011
1012
	/**
1013
	 * Returns true if this title resolves to the named special page
1014
	 *
1015
	 * @param string $name The special page name
1016
	 * @return bool
1017
	 */
1018
	public function isSpecial( $name ) {
1019
		if ( $this->isSpecialPage() ) {
1020
			list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() );
1021
			if ( $name == $thisName ) {
1022
				return true;
1023
			}
1024
		}
1025
		return false;
1026
	}
1027
1028
	/**
1029
	 * If the Title refers to a special page alias which is not the local default, resolve
1030
	 * the alias, and localise the name as necessary.  Otherwise, return $this
1031
	 *
1032
	 * @return Title
1033
	 */
1034
	public function fixSpecialName() {
1035
		if ( $this->isSpecialPage() ) {
1036
			list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform );
1037
			if ( $canonicalName ) {
1038
				$localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par );
1039
				if ( $localName != $this->mDbkeyform ) {
1040
					return Title::makeTitle( NS_SPECIAL, $localName );
1041
				}
1042
			}
1043
		}
1044
		return $this;
1045
	}
1046
1047
	/**
1048
	 * Returns true if the title is inside the specified namespace.
1049
	 *
1050
	 * Please make use of this instead of comparing to getNamespace()
1051
	 * This function is much more resistant to changes we may make
1052
	 * to namespaces than code that makes direct comparisons.
1053
	 * @param int $ns The namespace
1054
	 * @return bool
1055
	 * @since 1.19
1056
	 */
1057
	public function inNamespace( $ns ) {
1058
		return MWNamespace::equals( $this->getNamespace(), $ns );
1059
	}
1060
1061
	/**
1062
	 * Returns true if the title is inside one of the specified namespaces.
1063
	 *
1064
	 * @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...
1065
	 * @return bool
1066
	 * @since 1.19
1067
	 */
1068
	public function inNamespaces( /* ... */ ) {
1069
		$namespaces = func_get_args();
1070
		if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
1071
			$namespaces = $namespaces[0];
1072
		}
1073
1074
		foreach ( $namespaces as $ns ) {
1075
			if ( $this->inNamespace( $ns ) ) {
1076
				return true;
1077
			}
1078
		}
1079
1080
		return false;
1081
	}
1082
1083
	/**
1084
	 * Returns true if the title has the same subject namespace as the
1085
	 * namespace specified.
1086
	 * For example this method will take NS_USER and return true if namespace
1087
	 * is either NS_USER or NS_USER_TALK since both of them have NS_USER
1088
	 * as their subject namespace.
1089
	 *
1090
	 * This is MUCH simpler than individually testing for equivalence
1091
	 * against both NS_USER and NS_USER_TALK, and is also forward compatible.
1092
	 * @since 1.19
1093
	 * @param int $ns
1094
	 * @return bool
1095
	 */
1096
	public function hasSubjectNamespace( $ns ) {
1097
		return MWNamespace::subjectEquals( $this->getNamespace(), $ns );
1098
	}
1099
1100
	/**
1101
	 * Is this Title in a namespace which contains content?
1102
	 * In other words, is this a content page, for the purposes of calculating
1103
	 * statistics, etc?
1104
	 *
1105
	 * @return bool
1106
	 */
1107
	public function isContentPage() {
1108
		return MWNamespace::isContent( $this->getNamespace() );
1109
	}
1110
1111
	/**
1112
	 * Would anybody with sufficient privileges be able to move this page?
1113
	 * Some pages just aren't movable.
1114
	 *
1115
	 * @return bool
1116
	 */
1117
	public function isMovable() {
1118
		if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->isExternal() ) {
1119
			// Interwiki title or immovable namespace. Hooks don't get to override here
1120
			return false;
1121
		}
1122
1123
		$result = true;
1124
		Hooks::run( 'TitleIsMovable', [ $this, &$result ] );
1125
		return $result;
1126
	}
1127
1128
	/**
1129
	 * Is this the mainpage?
1130
	 * @note Title::newFromText seems to be sufficiently optimized by the title
1131
	 * cache that we don't need to over-optimize by doing direct comparisons and
1132
	 * accidentally creating new bugs where $title->equals( Title::newFromText() )
1133
	 * ends up reporting something differently than $title->isMainPage();
1134
	 *
1135
	 * @since 1.18
1136
	 * @return bool
1137
	 */
1138
	public function isMainPage() {
1139
		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...
1140
	}
1141
1142
	/**
1143
	 * Is this a subpage?
1144
	 *
1145
	 * @return bool
1146
	 */
1147
	public function isSubpage() {
1148
		return MWNamespace::hasSubpages( $this->mNamespace )
1149
			? strpos( $this->getText(), '/' ) !== false
1150
			: false;
1151
	}
1152
1153
	/**
1154
	 * Is this a conversion table for the LanguageConverter?
1155
	 *
1156
	 * @return bool
1157
	 */
1158
	public function isConversionTable() {
1159
		// @todo ConversionTable should become a separate content model.
1160
1161
		return $this->getNamespace() == NS_MEDIAWIKI &&
1162
			strpos( $this->getText(), 'Conversiontable/' ) === 0;
1163
	}
1164
1165
	/**
1166
	 * Does that page contain wikitext, or it is JS, CSS or whatever?
1167
	 *
1168
	 * @return bool
1169
	 */
1170
	public function isWikitextPage() {
1171
		return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
1172
	}
1173
1174
	/**
1175
	 * Could this page contain custom CSS or JavaScript for the global UI.
1176
	 * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
1177
	 * or CONTENT_MODEL_JAVASCRIPT.
1178
	 *
1179
	 * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage()
1180
	 * for that!
1181
	 *
1182
	 * Note that this method should not return true for pages that contain and
1183
	 * show "inactive" CSS or JS.
1184
	 *
1185
	 * @return bool
1186
	 * @todo FIXME: Rename to isSiteConfigPage() and remove deprecated hook
1187
	 */
1188
	public function isCssOrJsPage() {
1189
		$isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
1190
			&& ( $this->hasContentModel( CONTENT_MODEL_CSS )
1191
				|| $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
1192
1193
		# @note This hook is also called in ContentHandler::getDefaultModel.
1194
		#   It's called here again to make sure hook functions can force this
1195
		#   method to return true even outside the MediaWiki namespace.
1196
1197
		Hooks::run( 'TitleIsCssOrJsPage', [ $this, &$isCssOrJsPage ], '1.25' );
1198
1199
		return $isCssOrJsPage;
1200
	}
1201
1202
	/**
1203
	 * Is this a .css or .js subpage of a user page?
1204
	 * @return bool
1205
	 * @todo FIXME: Rename to isUserConfigPage()
1206
	 */
1207
	public function isCssJsSubpage() {
1208
		return ( NS_USER == $this->mNamespace && $this->isSubpage()
1209
				&& ( $this->hasContentModel( CONTENT_MODEL_CSS )
1210
					|| $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
1211
	}
1212
1213
	/**
1214
	 * Trim down a .css or .js subpage title to get the corresponding skin name
1215
	 *
1216
	 * @return string Containing skin name from .css or .js subpage title
1217
	 */
1218
	public function getSkinFromCssJsSubpage() {
1219
		$subpage = explode( '/', $this->mTextform );
1220
		$subpage = $subpage[count( $subpage ) - 1];
1221
		$lastdot = strrpos( $subpage, '.' );
1222
		if ( $lastdot === false ) {
1223
			return $subpage; # Never happens: only called for names ending in '.css' or '.js'
1224
		}
1225
		return substr( $subpage, 0, $lastdot );
1226
	}
1227
1228
	/**
1229
	 * Is this a .css subpage of a user page?
1230
	 *
1231
	 * @return bool
1232
	 */
1233
	public function isCssSubpage() {
1234
		return ( NS_USER == $this->mNamespace && $this->isSubpage()
1235
			&& $this->hasContentModel( CONTENT_MODEL_CSS ) );
1236
	}
1237
1238
	/**
1239
	 * Is this a .js subpage of a user page?
1240
	 *
1241
	 * @return bool
1242
	 */
1243
	public function isJsSubpage() {
1244
		return ( NS_USER == $this->mNamespace && $this->isSubpage()
1245
			&& $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
1246
	}
1247
1248
	/**
1249
	 * Is this a talk page of some sort?
1250
	 *
1251
	 * @return bool
1252
	 */
1253
	public function isTalkPage() {
1254
		return MWNamespace::isTalk( $this->getNamespace() );
1255
	}
1256
1257
	/**
1258
	 * Get a Title object associated with the talk page of this article
1259
	 *
1260
	 * @return Title The object for the talk page
1261
	 */
1262
	public function getTalkPage() {
1263
		return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
1264
	}
1265
1266
	/**
1267
	 * Get a title object associated with the subject page of this
1268
	 * talk page
1269
	 *
1270
	 * @return Title The object for the subject page
1271
	 */
1272
	public function getSubjectPage() {
1273
		// Is this the same title?
1274
		$subjectNS = MWNamespace::getSubject( $this->getNamespace() );
1275
		if ( $this->getNamespace() == $subjectNS ) {
1276
			return $this;
1277
		}
1278
		return Title::makeTitle( $subjectNS, $this->getDBkey() );
1279
	}
1280
1281
	/**
1282
	 * Get the other title for this page, if this is a subject page
1283
	 * get the talk page, if it is a subject page get the talk page
1284
	 *
1285
	 * @since 1.25
1286
	 * @throws MWException
1287
	 * @return Title
1288
	 */
1289
	public function getOtherPage() {
1290
		if ( $this->isSpecialPage() ) {
1291
			throw new MWException( 'Special pages cannot have other pages' );
1292
		}
1293
		if ( $this->isTalkPage() ) {
1294
			return $this->getSubjectPage();
1295
		} else {
1296
			return $this->getTalkPage();
1297
		}
1298
	}
1299
1300
	/**
1301
	 * Get the default namespace index, for when there is no namespace
1302
	 *
1303
	 * @return int Default namespace index
1304
	 */
1305
	public function getDefaultNamespace() {
1306
		return $this->mDefaultNamespace;
1307
	}
1308
1309
	/**
1310
	 * Get the Title fragment (i.e.\ the bit after the #) in text form
1311
	 *
1312
	 * Use Title::hasFragment to check for a fragment
1313
	 *
1314
	 * @return string Title fragment
1315
	 */
1316
	public function getFragment() {
1317
		return $this->mFragment;
1318
	}
1319
1320
	/**
1321
	 * Check if a Title fragment is set
1322
	 *
1323
	 * @return bool
1324
	 * @since 1.23
1325
	 */
1326
	public function hasFragment() {
1327
		return $this->mFragment !== '';
1328
	}
1329
1330
	/**
1331
	 * Get the fragment in URL form, including the "#" character if there is one
1332
	 * @return string Fragment in URL form
1333
	 */
1334
	public function getFragmentForURL() {
1335
		if ( !$this->hasFragment() ) {
1336
			return '';
1337
		} else {
1338
			return '#' . Title::escapeFragmentForURL( $this->getFragment() );
1339
		}
1340
	}
1341
1342
	/**
1343
	 * Set the fragment for this title. Removes the first character from the
1344
	 * specified fragment before setting, so it assumes you're passing it with
1345
	 * an initial "#".
1346
	 *
1347
	 * Deprecated for public use, use Title::makeTitle() with fragment parameter,
1348
	 * or Title::createFragmentTarget().
1349
	 * Still in active use privately.
1350
	 *
1351
	 * @private
1352
	 * @param string $fragment Text
1353
	 */
1354
	public function setFragment( $fragment ) {
1355
		$this->mFragment = strtr( substr( $fragment, 1 ), '_', ' ' );
1356
	}
1357
1358
	/**
1359
	 * Creates a new Title for a different fragment of the same page.
1360
	 *
1361
	 * @since 1.27
1362
	 * @param string $fragment
1363
	 * @return Title
1364
	 */
1365
	public function createFragmentTarget( $fragment ) {
1366
		return self::makeTitle(
1367
			$this->getNamespace(),
1368
			$this->getText(),
1369
			$fragment,
1370
			$this->getInterwiki()
1371
		);
1372
1373
	}
1374
1375
	/**
1376
	 * Prefix some arbitrary text with the namespace or interwiki prefix
1377
	 * of this object
1378
	 *
1379
	 * @param string $name The text
1380
	 * @return string The prefixed text
1381
	 */
1382
	private function prefix( $name ) {
1383
		$p = '';
1384
		if ( $this->isExternal() ) {
1385
			$p = $this->mInterwiki . ':';
1386
		}
1387
1388
		if ( 0 != $this->mNamespace ) {
1389
			$p .= $this->getNsText() . ':';
1390
		}
1391
		return $p . $name;
1392
	}
1393
1394
	/**
1395
	 * Get the prefixed database key form
1396
	 *
1397
	 * @return string The prefixed title, with underscores and
1398
	 *  any interwiki and namespace prefixes
1399
	 */
1400
	public function getPrefixedDBkey() {
1401
		$s = $this->prefix( $this->mDbkeyform );
1402
		$s = strtr( $s, ' ', '_' );
1403
		return $s;
1404
	}
1405
1406
	/**
1407
	 * Get the prefixed title with spaces.
1408
	 * This is the form usually used for display
1409
	 *
1410
	 * @return string The prefixed title, with spaces
1411
	 */
1412
	public function getPrefixedText() {
1413
		if ( $this->mPrefixedText === null ) {
1414
			$s = $this->prefix( $this->mTextform );
1415
			$s = strtr( $s, '_', ' ' );
1416
			$this->mPrefixedText = $s;
1417
		}
1418
		return $this->mPrefixedText;
1419
	}
1420
1421
	/**
1422
	 * Return a string representation of this title
1423
	 *
1424
	 * @return string Representation of this title
1425
	 */
1426
	public function __toString() {
1427
		return $this->getPrefixedText();
1428
	}
1429
1430
	/**
1431
	 * Get the prefixed title with spaces, plus any fragment
1432
	 * (part beginning with '#')
1433
	 *
1434
	 * @return string The prefixed title, with spaces and the fragment, including '#'
1435
	 */
1436
	public function getFullText() {
1437
		$text = $this->getPrefixedText();
1438
		if ( $this->hasFragment() ) {
1439
			$text .= '#' . $this->getFragment();
1440
		}
1441
		return $text;
1442
	}
1443
1444
	/**
1445
	 * Get the root page name text without a namespace, i.e. the leftmost part before any slashes
1446
	 *
1447
	 * @par Example:
1448
	 * @code
1449
	 * Title::newFromText('User:Foo/Bar/Baz')->getRootText();
1450
	 * # returns: 'Foo'
1451
	 * @endcode
1452
	 *
1453
	 * @return string Root name
1454
	 * @since 1.20
1455
	 */
1456
	public function getRootText() {
1457
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1458
			return $this->getText();
1459
		}
1460
1461
		return strtok( $this->getText(), '/' );
1462
	}
1463
1464
	/**
1465
	 * Get the root page name title, i.e. the leftmost part before any slashes
1466
	 *
1467
	 * @par Example:
1468
	 * @code
1469
	 * Title::newFromText('User:Foo/Bar/Baz')->getRootTitle();
1470
	 * # returns: Title{User:Foo}
1471
	 * @endcode
1472
	 *
1473
	 * @return Title Root title
1474
	 * @since 1.20
1475
	 */
1476
	public function getRootTitle() {
1477
		return Title::makeTitle( $this->getNamespace(), $this->getRootText() );
1478
	}
1479
1480
	/**
1481
	 * Get the base page name without a namespace, i.e. the part before the subpage name
1482
	 *
1483
	 * @par Example:
1484
	 * @code
1485
	 * Title::newFromText('User:Foo/Bar/Baz')->getBaseText();
1486
	 * # returns: 'Foo/Bar'
1487
	 * @endcode
1488
	 *
1489
	 * @return string Base name
1490
	 */
1491
	public function getBaseText() {
1492
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1493
			return $this->getText();
1494
		}
1495
1496
		$parts = explode( '/', $this->getText() );
1497
		# Don't discard the real title if there's no subpage involved
1498
		if ( count( $parts ) > 1 ) {
1499
			unset( $parts[count( $parts ) - 1] );
1500
		}
1501
		return implode( '/', $parts );
1502
	}
1503
1504
	/**
1505
	 * Get the base page name title, i.e. the part before the subpage name
1506
	 *
1507
	 * @par Example:
1508
	 * @code
1509
	 * Title::newFromText('User:Foo/Bar/Baz')->getBaseTitle();
1510
	 * # returns: Title{User:Foo/Bar}
1511
	 * @endcode
1512
	 *
1513
	 * @return Title Base title
1514
	 * @since 1.20
1515
	 */
1516
	public function getBaseTitle() {
1517
		return Title::makeTitle( $this->getNamespace(), $this->getBaseText() );
1518
	}
1519
1520
	/**
1521
	 * Get the lowest-level subpage name, i.e. the rightmost part after any slashes
1522
	 *
1523
	 * @par Example:
1524
	 * @code
1525
	 * Title::newFromText('User:Foo/Bar/Baz')->getSubpageText();
1526
	 * # returns: "Baz"
1527
	 * @endcode
1528
	 *
1529
	 * @return string Subpage name
1530
	 */
1531
	public function getSubpageText() {
1532
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1533
			return $this->mTextform;
1534
		}
1535
		$parts = explode( '/', $this->mTextform );
1536
		return $parts[count( $parts ) - 1];
1537
	}
1538
1539
	/**
1540
	 * Get the title for a subpage of the current page
1541
	 *
1542
	 * @par Example:
1543
	 * @code
1544
	 * Title::newFromText('User:Foo/Bar/Baz')->getSubpage("Asdf");
1545
	 * # returns: Title{User:Foo/Bar/Baz/Asdf}
1546
	 * @endcode
1547
	 *
1548
	 * @param string $text The subpage name to add to the title
1549
	 * @return Title Subpage title
1550
	 * @since 1.20
1551
	 */
1552
	public function getSubpage( $text ) {
1553
		return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text );
1554
	}
1555
1556
	/**
1557
	 * Get a URL-encoded form of the subpage text
1558
	 *
1559
	 * @return string URL-encoded subpage name
1560
	 */
1561
	public function getSubpageUrlForm() {
1562
		$text = $this->getSubpageText();
1563
		$text = wfUrlencode( strtr( $text, ' ', '_' ) );
1564
		return $text;
1565
	}
1566
1567
	/**
1568
	 * Get a URL-encoded title (not an actual URL) including interwiki
1569
	 *
1570
	 * @return string The URL-encoded form
1571
	 */
1572
	public function getPrefixedURL() {
1573
		$s = $this->prefix( $this->mDbkeyform );
1574
		$s = wfUrlencode( strtr( $s, ' ', '_' ) );
1575
		return $s;
1576
	}
1577
1578
	/**
1579
	 * Helper to fix up the get{Canonical,Full,Link,Local,Internal}URL args
1580
	 * get{Canonical,Full,Link,Local,Internal}URL methods accepted an optional
1581
	 * second argument named variant. This was deprecated in favor
1582
	 * of passing an array of option with a "variant" key
1583
	 * Once $query2 is removed for good, this helper can be dropped
1584
	 * and the wfArrayToCgi moved to getLocalURL();
1585
	 *
1586
	 * @since 1.19 (r105919)
1587
	 * @param array|string $query
1588
	 * @param bool $query2
1589
	 * @return string
1590
	 */
1591
	private static function fixUrlQueryArgs( $query, $query2 = false ) {
1592
		if ( $query2 !== false ) {
1593
			wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " .
1594
				"method called with a second parameter is deprecated. Add your " .
1595
				"parameter to an array passed as the first parameter.", "1.19" );
1596
		}
1597
		if ( is_array( $query ) ) {
1598
			$query = wfArrayToCgi( $query );
1599
		}
1600
		if ( $query2 ) {
1601
			if ( is_string( $query2 ) ) {
1602
				// $query2 is a string, we will consider this to be
1603
				// a deprecated $variant argument and add it to the query
1604
				$query2 = wfArrayToCgi( [ 'variant' => $query2 ] );
1605
			} else {
1606
				$query2 = wfArrayToCgi( $query2 );
1607
			}
1608
			// If we have $query content add a & to it first
1609
			if ( $query ) {
1610
				$query .= '&';
1611
			}
1612
			// Now append the queries together
1613
			$query .= $query2;
1614
		}
1615
		return $query;
1616
	}
1617
1618
	/**
1619
	 * Get a real URL referring to this title, with interwiki link and
1620
	 * fragment
1621
	 *
1622
	 * @see self::getLocalURL for the arguments.
1623
	 * @see wfExpandUrl
1624
	 * @param array|string $query
1625
	 * @param bool $query2
1626
	 * @param string $proto Protocol type to use in URL
1627
	 * @return string The URL
1628
	 */
1629
	public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
1630
		$query = self::fixUrlQueryArgs( $query, $query2 );
1631
1632
		# Hand off all the decisions on urls to getLocalURL
1633
		$url = $this->getLocalURL( $query );
1634
1635
		# Expand the url to make it a full url. Note that getLocalURL has the
1636
		# potential to output full urls for a variety of reasons, so we use
1637
		# wfExpandUrl instead of simply prepending $wgServer
1638
		$url = wfExpandUrl( $url, $proto );
1639
1640
		# Finally, add the fragment.
1641
		$url .= $this->getFragmentForURL();
1642
1643
		Hooks::run( 'GetFullURL', [ &$this, &$url, $query ] );
1644
		return $url;
1645
	}
1646
1647
	/**
1648
	 * Get a URL with no fragment or server name (relative URL) from a Title object.
1649
	 * If this page is generated with action=render, however,
1650
	 * $wgServer is prepended to make an absolute URL.
1651
	 *
1652
	 * @see self::getFullURL to always get an absolute URL.
1653
	 * @see self::getLinkURL to always get a URL that's the simplest URL that will be
1654
	 *  valid to link, locally, to the current Title.
1655
	 * @see self::newFromText to produce a Title object.
1656
	 *
1657
	 * @param string|array $query An optional query string,
1658
	 *   not used for interwiki links. Can be specified as an associative array as well,
1659
	 *   e.g., array( 'action' => 'edit' ) (keys and values will be URL-escaped).
1660
	 *   Some query patterns will trigger various shorturl path replacements.
1661
	 * @param array $query2 An optional secondary query array. This one MUST
1662
	 *   be an array. If a string is passed it will be interpreted as a deprecated
1663
	 *   variant argument and urlencoded into a variant= argument.
1664
	 *   This second query argument will be added to the $query
1665
	 *   The second parameter is deprecated since 1.19. Pass it as a key,value
1666
	 *   pair in the first parameter array instead.
1667
	 *
1668
	 * @return string String of the URL.
1669
	 */
1670
	public function getLocalURL( $query = '', $query2 = false ) {
1671
		global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
1672
1673
		$query = self::fixUrlQueryArgs( $query, $query2 );
0 ignored issues
show
Bug introduced by
It seems like $query2 defined by parameter $query2 on line 1670 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...
1674
1675
		$interwiki = Interwiki::fetch( $this->mInterwiki );
1676
		if ( $interwiki ) {
1677
			$namespace = $this->getNsText();
1678
			if ( $namespace != '' ) {
1679
				# Can this actually happen? Interwikis shouldn't be parsed.
1680
				# Yes! It can in interwiki transclusion. But... it probably shouldn't.
1681
				$namespace .= ':';
1682
			}
1683
			$url = $interwiki->getURL( $namespace . $this->getDBkey() );
1684
			$url = wfAppendQuery( $url, $query );
1685
		} else {
1686
			$dbkey = wfUrlencode( $this->getPrefixedDBkey() );
1687
			if ( $query == '' ) {
1688
				$url = str_replace( '$1', $dbkey, $wgArticlePath );
1689
				Hooks::run( 'GetLocalURL::Article', [ &$this, &$url ] );
1690
			} else {
1691
				global $wgVariantArticlePath, $wgActionPaths, $wgContLang;
1692
				$url = false;
1693
				$matches = [];
1694
1695
				if ( !empty( $wgActionPaths )
1696
					&& preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches )
1697
				) {
1698
					$action = urldecode( $matches[2] );
1699
					if ( isset( $wgActionPaths[$action] ) ) {
1700
						$query = $matches[1];
1701
						if ( isset( $matches[4] ) ) {
1702
							$query .= $matches[4];
1703
						}
1704
						$url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
1705
						if ( $query != '' ) {
1706
							$url = wfAppendQuery( $url, $query );
1707
						}
1708
					}
1709
				}
1710
1711
				if ( $url === false
1712
					&& $wgVariantArticlePath
1713
					&& $wgContLang->getCode() === $this->getPageLanguage()->getCode()
1714
					&& $this->getPageLanguage()->hasVariants()
1715
					&& preg_match( '/^variant=([^&]*)$/', $query, $matches )
1716
				) {
1717
					$variant = urldecode( $matches[1] );
1718
					if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
1719
						// Only do the variant replacement if the given variant is a valid
1720
						// variant for the page's language.
1721
						$url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath );
1722
						$url = str_replace( '$1', $dbkey, $url );
1723
					}
1724
				}
1725
1726
				if ( $url === false ) {
1727
					if ( $query == '-' ) {
1728
						$query = '';
1729
					}
1730
					$url = "{$wgScript}?title={$dbkey}&{$query}";
1731
				}
1732
			}
1733
1734
			Hooks::run( 'GetLocalURL::Internal', [ &$this, &$url, $query ] );
1735
1736
			// @todo FIXME: This causes breakage in various places when we
1737
			// actually expected a local URL and end up with dupe prefixes.
1738
			if ( $wgRequest->getVal( 'action' ) == 'render' ) {
1739
				$url = $wgServer . $url;
1740
			}
1741
		}
1742
		Hooks::run( 'GetLocalURL', [ &$this, &$url, $query ] );
1743
		return $url;
1744
	}
1745
1746
	/**
1747
	 * Get a URL that's the simplest URL that will be valid to link, locally,
1748
	 * to the current Title.  It includes the fragment, but does not include
1749
	 * the server unless action=render is used (or the link is external).  If
1750
	 * there's a fragment but the prefixed text is empty, we just return a link
1751
	 * to the fragment.
1752
	 *
1753
	 * The result obviously should not be URL-escaped, but does need to be
1754
	 * HTML-escaped if it's being output in HTML.
1755
	 *
1756
	 * @param array $query
1757
	 * @param bool $query2
1758
	 * @param string $proto Protocol to use; setting this will cause a full URL to be used
1759
	 * @see self::getLocalURL for the arguments.
1760
	 * @return string The URL
1761
	 */
1762
	public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
1763
		if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) {
1764
			$ret = $this->getFullURL( $query, $query2, $proto );
1765
		} elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) {
1766
			$ret = $this->getFragmentForURL();
1767
		} else {
1768
			$ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
1769
		}
1770
		return $ret;
1771
	}
1772
1773
	/**
1774
	 * Get the URL form for an internal link.
1775
	 * - Used in various CDN-related code, in case we have a different
1776
	 * internal hostname for the server from the exposed one.
1777
	 *
1778
	 * This uses $wgInternalServer to qualify the path, or $wgServer
1779
	 * if $wgInternalServer is not set. If the server variable used is
1780
	 * protocol-relative, the URL will be expanded to http://
1781
	 *
1782
	 * @see self::getLocalURL for the arguments.
1783
	 * @return string The URL
1784
	 */
1785
	public function getInternalURL( $query = '', $query2 = false ) {
1786
		global $wgInternalServer, $wgServer;
1787
		$query = self::fixUrlQueryArgs( $query, $query2 );
1788
		$server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
1789
		$url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
1790
		Hooks::run( 'GetInternalURL', [ &$this, &$url, $query ] );
1791
		return $url;
1792
	}
1793
1794
	/**
1795
	 * Get the URL for a canonical link, for use in things like IRC and
1796
	 * e-mail notifications. Uses $wgCanonicalServer and the
1797
	 * GetCanonicalURL hook.
1798
	 *
1799
	 * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment
1800
	 *
1801
	 * @see self::getLocalURL for the arguments.
1802
	 * @return string The URL
1803
	 * @since 1.18
1804
	 */
1805
	public function getCanonicalURL( $query = '', $query2 = false ) {
1806
		$query = self::fixUrlQueryArgs( $query, $query2 );
1807
		$url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
1808
		Hooks::run( 'GetCanonicalURL', [ &$this, &$url, $query ] );
1809
		return $url;
1810
	}
1811
1812
	/**
1813
	 * Get the edit URL for this Title
1814
	 *
1815
	 * @return string The URL, or a null string if this is an interwiki link
1816
	 */
1817
	public function getEditURL() {
1818
		if ( $this->isExternal() ) {
1819
			return '';
1820
		}
1821
		$s = $this->getLocalURL( 'action=edit' );
1822
1823
		return $s;
1824
	}
1825
1826
	/**
1827
	 * Can $user perform $action on this page?
1828
	 * This skips potentially expensive cascading permission checks
1829
	 * as well as avoids expensive error formatting
1830
	 *
1831
	 * Suitable for use for nonessential UI controls in common cases, but
1832
	 * _not_ for functional access control.
1833
	 *
1834
	 * May provide false positives, but should never provide a false negative.
1835
	 *
1836
	 * @param string $action Action that permission needs to be checked for
1837
	 * @param User $user User to check (since 1.19); $wgUser will be used if not provided.
1838
	 * @return bool
1839
	 */
1840
	public function quickUserCan( $action, $user = null ) {
1841
		return $this->userCan( $action, $user, false );
1842
	}
1843
1844
	/**
1845
	 * Can $user perform $action on this page?
1846
	 *
1847
	 * @param string $action Action that permission needs to be checked for
1848
	 * @param User $user User to check (since 1.19); $wgUser will be used if not
1849
	 *   provided.
1850
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
1851
	 * @return bool
1852
	 */
1853
	public function userCan( $action, $user = null, $rigor = 'secure' ) {
1854
		if ( !$user instanceof User ) {
1855
			global $wgUser;
1856
			$user = $wgUser;
1857
		}
1858
1859
		return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) );
1860
	}
1861
1862
	/**
1863
	 * Can $user perform $action on this page?
1864
	 *
1865
	 * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
1866
	 *
1867
	 * @param string $action Action that permission needs to be checked for
1868
	 * @param User $user User to check
1869
	 * @param string $rigor One of (quick,full,secure)
1870
	 *   - quick  : does cheap permission checks from slaves (usable for GUI creation)
1871
	 *   - full   : does cheap and expensive checks possibly from a slave
1872
	 *   - secure : does cheap and expensive checks, using the master as needed
1873
	 * @param array $ignoreErrors Array of Strings Set this to a list of message keys
1874
	 *   whose corresponding errors may be ignored.
1875
	 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
1876
	 */
1877
	public function getUserPermissionsErrors(
1878
		$action, $user, $rigor = 'secure', $ignoreErrors = []
1879
	) {
1880
		$errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor );
1881
1882
		// Remove the errors being ignored.
1883
		foreach ( $errors as $index => $error ) {
1884
			$errKey = is_array( $error ) ? $error[0] : $error;
1885
1886
			if ( in_array( $errKey, $ignoreErrors ) ) {
1887
				unset( $errors[$index] );
1888
			}
1889
			if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
1890
				unset( $errors[$index] );
1891
			}
1892
		}
1893
1894
		return $errors;
1895
	}
1896
1897
	/**
1898
	 * Permissions checks that fail most often, and which are easiest to test.
1899
	 *
1900
	 * @param string $action The action to check
1901
	 * @param User $user User to check
1902
	 * @param array $errors List of current errors
1903
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
1904
	 * @param bool $short Short circuit on first error
1905
	 *
1906
	 * @return array List of errors
1907
	 */
1908
	private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) {
1909
		if ( !Hooks::run( 'TitleQuickPermissions',
1910
			[ $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ] )
1911
		) {
1912
			return $errors;
1913
		}
1914
1915
		if ( $action == 'create' ) {
1916
			if (
1917
				( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
1918
				( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
1919
			) {
1920
				$errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
1921
			}
1922
		} elseif ( $action == 'move' ) {
1923 View Code Duplication
			if ( !$user->isAllowed( 'move-rootuserpages' )
1924
					&& $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1925
				// Show user page-specific message only if the user can move other pages
1926
				$errors[] = [ 'cant-move-user-page' ];
1927
			}
1928
1929
			// Check if user is allowed to move files if it's a file
1930
			if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
1931
				$errors[] = [ 'movenotallowedfile' ];
1932
			}
1933
1934
			// Check if user is allowed to move category pages if it's a category page
1935
			if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
1936
				$errors[] = [ 'cant-move-category-page' ];
1937
			}
1938
1939
			if ( !$user->isAllowed( 'move' ) ) {
1940
				// User can't move anything
1941
				$userCanMove = User::groupHasPermission( 'user', 'move' );
1942
				$autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
1943
				if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
1944
					// custom message if logged-in users without any special rights can move
1945
					$errors[] = [ 'movenologintext' ];
1946
				} else {
1947
					$errors[] = [ 'movenotallowed' ];
1948
				}
1949
			}
1950
		} elseif ( $action == 'move-target' ) {
1951
			if ( !$user->isAllowed( 'move' ) ) {
1952
				// User can't move anything
1953
				$errors[] = [ 'movenotallowed' ];
1954 View Code Duplication
			} elseif ( !$user->isAllowed( 'move-rootuserpages' )
1955
					&& $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1956
				// Show user page-specific message only if the user can move other pages
1957
				$errors[] = [ 'cant-move-to-user-page' ];
1958
			} elseif ( !$user->isAllowed( 'move-categorypages' )
1959
					&& $this->mNamespace == NS_CATEGORY ) {
1960
				// Show category page-specific message only if the user can move other pages
1961
				$errors[] = [ 'cant-move-to-category-page' ];
1962
			}
1963
		} elseif ( !$user->isAllowed( $action ) ) {
1964
			$errors[] = $this->missingPermissionError( $action, $short );
1965
		}
1966
1967
		return $errors;
1968
	}
1969
1970
	/**
1971
	 * Add the resulting error code to the errors array
1972
	 *
1973
	 * @param array $errors List of current errors
1974
	 * @param array $result Result of errors
1975
	 *
1976
	 * @return array List of errors
1977
	 */
1978
	private function resultToError( $errors, $result ) {
1979
		if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
1980
			// A single array representing an error
1981
			$errors[] = $result;
1982
		} elseif ( is_array( $result ) && is_array( $result[0] ) ) {
1983
			// A nested array representing multiple errors
1984
			$errors = array_merge( $errors, $result );
1985
		} elseif ( $result !== '' && is_string( $result ) ) {
1986
			// A string representing a message-id
1987
			$errors[] = [ $result ];
1988
		} elseif ( $result instanceof MessageSpecifier ) {
1989
			// A message specifier representing an error
1990
			$errors[] = [ $result ];
1991
		} elseif ( $result === false ) {
1992
			// a generic "We don't want them to do that"
1993
			$errors[] = [ 'badaccess-group0' ];
1994
		}
1995
		return $errors;
1996
	}
1997
1998
	/**
1999
	 * Check various permission hooks
2000
	 *
2001
	 * @param string $action The action to check
2002
	 * @param User $user User to check
2003
	 * @param array $errors List of current errors
2004
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2005
	 * @param bool $short Short circuit on first error
2006
	 *
2007
	 * @return array List of errors
2008
	 */
2009
	private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) {
2010
		// Use getUserPermissionsErrors instead
2011
		$result = '';
2012
		if ( !Hooks::run( 'userCan', [ &$this, &$user, $action, &$result ] ) ) {
2013
			return $result ? [] : [ [ 'badaccess-group0' ] ];
2014
		}
2015
		// Check getUserPermissionsErrors hook
2016
		if ( !Hooks::run( 'getUserPermissionsErrors', [ &$this, &$user, $action, &$result ] ) ) {
2017
			$errors = $this->resultToError( $errors, $result );
2018
		}
2019
		// Check getUserPermissionsErrorsExpensive hook
2020
		if (
2021
			$rigor !== 'quick'
2022
			&& !( $short && count( $errors ) > 0 )
2023
			&& !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$this, &$user, $action, &$result ] )
2024
		) {
2025
			$errors = $this->resultToError( $errors, $result );
2026
		}
2027
2028
		return $errors;
2029
	}
2030
2031
	/**
2032
	 * Check permissions on special pages & namespaces
2033
	 *
2034
	 * @param string $action The action to check
2035
	 * @param User $user User to check
2036
	 * @param array $errors List of current errors
2037
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2038
	 * @param bool $short Short circuit on first error
2039
	 *
2040
	 * @return array List of errors
2041
	 */
2042
	private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) {
2043
		# Only 'createaccount' can be performed on special pages,
2044
		# which don't actually exist in the DB.
2045
		if ( NS_SPECIAL == $this->mNamespace && $action !== 'createaccount' ) {
2046
			$errors[] = [ 'ns-specialprotected' ];
2047
		}
2048
2049
		# Check $wgNamespaceProtection for restricted namespaces
2050
		if ( $this->isNamespaceProtected( $user ) ) {
2051
			$ns = $this->mNamespace == NS_MAIN ?
2052
				wfMessage( 'nstab-main' )->text() : $this->getNsText();
2053
			$errors[] = $this->mNamespace == NS_MEDIAWIKI ?
2054
				[ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
2055
		}
2056
2057
		return $errors;
2058
	}
2059
2060
	/**
2061
	 * Check CSS/JS sub-page permissions
2062
	 *
2063
	 * @param string $action The action to check
2064
	 * @param User $user User to check
2065
	 * @param array $errors List of current errors
2066
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2067
	 * @param bool $short Short circuit on first error
2068
	 *
2069
	 * @return array List of errors
2070
	 */
2071
	private function checkCSSandJSPermissions( $action, $user, $errors, $rigor, $short ) {
2072
		# Protect css/js subpages of user pages
2073
		# XXX: this might be better using restrictions
2074
		# XXX: right 'editusercssjs' is deprecated, for backward compatibility only
2075
		if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) {
2076
			if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
2077 View Code Duplication
				if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) {
2078
					$errors[] = [ 'mycustomcssprotected', $action ];
2079
				} elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) {
2080
					$errors[] = [ 'mycustomjsprotected', $action ];
2081
				}
2082 View Code Duplication
			} else {
2083
				if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) {
2084
					$errors[] = [ 'customcssprotected', $action ];
2085
				} elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
2086
					$errors[] = [ 'customjsprotected', $action ];
2087
				}
2088
			}
2089
		}
2090
2091
		return $errors;
2092
	}
2093
2094
	/**
2095
	 * Check against page_restrictions table requirements on this
2096
	 * page. The user must possess all required rights for this
2097
	 * action.
2098
	 *
2099
	 * @param string $action The action to check
2100
	 * @param User $user User to check
2101
	 * @param array $errors List of current errors
2102
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2103
	 * @param bool $short Short circuit on first error
2104
	 *
2105
	 * @return array List of errors
2106
	 */
2107
	private function checkPageRestrictions( $action, $user, $errors, $rigor, $short ) {
2108
		foreach ( $this->getRestrictions( $action ) as $right ) {
2109
			// Backwards compatibility, rewrite sysop -> editprotected
2110
			if ( $right == 'sysop' ) {
2111
				$right = 'editprotected';
2112
			}
2113
			// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2114
			if ( $right == 'autoconfirmed' ) {
2115
				$right = 'editsemiprotected';
2116
			}
2117
			if ( $right == '' ) {
2118
				continue;
2119
			}
2120
			if ( !$user->isAllowed( $right ) ) {
2121
				$errors[] = [ 'protectedpagetext', $right, $action ];
2122
			} elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
2123
				$errors[] = [ 'protectedpagetext', 'protect', $action ];
2124
			}
2125
		}
2126
2127
		return $errors;
2128
	}
2129
2130
	/**
2131
	 * Check restrictions on cascading pages.
2132
	 *
2133
	 * @param string $action The action to check
2134
	 * @param User $user User to check
2135
	 * @param array $errors List of current errors
2136
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2137
	 * @param bool $short Short circuit on first error
2138
	 *
2139
	 * @return array List of errors
2140
	 */
2141
	private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
2142
		if ( $rigor !== 'quick' && !$this->isCssJsSubpage() ) {
2143
			# We /could/ use the protection level on the source page, but it's
2144
			# fairly ugly as we have to establish a precedence hierarchy for pages
2145
			# included by multiple cascade-protected pages. So just restrict
2146
			# it to people with 'protect' permission, as they could remove the
2147
			# protection anyway.
2148
			list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
2149
			# Cascading protection depends on more than this page...
2150
			# Several cascading protected pages may include this page...
2151
			# Check each cascading level
2152
			# This is only for protection restrictions, not for all actions
2153
			if ( isset( $restrictions[$action] ) ) {
2154
				foreach ( $restrictions[$action] as $right ) {
2155
					// Backwards compatibility, rewrite sysop -> editprotected
2156
					if ( $right == 'sysop' ) {
2157
						$right = 'editprotected';
2158
					}
2159
					// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2160
					if ( $right == 'autoconfirmed' ) {
2161
						$right = 'editsemiprotected';
2162
					}
2163
					if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
2164
						$pages = '';
2165
						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...
2166
							$pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
2167
						}
2168
						$errors[] = [ 'cascadeprotected', count( $cascadingSources ), $pages, $action ];
2169
					}
2170
				}
2171
			}
2172
		}
2173
2174
		return $errors;
2175
	}
2176
2177
	/**
2178
	 * Check action permissions not already checked in checkQuickPermissions
2179
	 *
2180
	 * @param string $action The action to check
2181
	 * @param User $user User to check
2182
	 * @param array $errors List of current errors
2183
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2184
	 * @param bool $short Short circuit on first error
2185
	 *
2186
	 * @return array List of errors
2187
	 */
2188
	private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) {
2189
		global $wgDeleteRevisionsLimit, $wgLang;
2190
2191
		if ( $action == 'protect' ) {
2192
			if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
2193
				// If they can't edit, they shouldn't protect.
2194
				$errors[] = [ 'protect-cantedit' ];
2195
			}
2196
		} elseif ( $action == 'create' ) {
2197
			$title_protection = $this->getTitleProtection();
2198
			if ( $title_protection ) {
2199
				if ( $title_protection['permission'] == ''
2200
					|| !$user->isAllowed( $title_protection['permission'] )
2201
				) {
2202
					$errors[] = [
2203
						'titleprotected',
2204
						User::whoIs( $title_protection['user'] ),
2205
						$title_protection['reason']
2206
					];
2207
				}
2208
			}
2209
		} elseif ( $action == 'move' ) {
2210
			// Check for immobile pages
2211
			if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2212
				// Specific message for this case
2213
				$errors[] = [ 'immobile-source-namespace', $this->getNsText() ];
2214
			} elseif ( !$this->isMovable() ) {
2215
				// Less specific message for rarer cases
2216
				$errors[] = [ 'immobile-source-page' ];
2217
			}
2218
		} elseif ( $action == 'move-target' ) {
2219
			if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2220
				$errors[] = [ 'immobile-target-namespace', $this->getNsText() ];
2221
			} elseif ( !$this->isMovable() ) {
2222
				$errors[] = [ 'immobile-target-page' ];
2223
			}
2224
		} elseif ( $action == 'delete' ) {
2225
			$tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true );
2226
			if ( !$tempErrors ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tempErrors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2227
				$tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
2228
					$user, $tempErrors, $rigor, true );
2229
			}
2230
			if ( $tempErrors ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tempErrors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

Loading history...
2575
			// Not protected, or all protection is full protection
2576
			return false;
2577
		}
2578
2579
		// Remap autoconfirmed to editsemiprotected for BC
2580
		foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) {
2581
			$semi[$key] = 'editsemiprotected';
2582
		}
2583
		foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) {
2584
			$restrictions[$key] = 'editsemiprotected';
2585
		}
2586
2587
		return !array_diff( $restrictions, $semi );
2588
	}
2589
2590
	/**
2591
	 * Does the title correspond to a protected article?
2592
	 *
2593
	 * @param string $action The action the page is protected from,
2594
	 * by default checks all actions.
2595
	 * @return bool
2596
	 */
2597
	public function isProtected( $action = '' ) {
2598
		global $wgRestrictionLevels;
2599
2600
		$restrictionTypes = $this->getRestrictionTypes();
2601
2602
		# Special pages have inherent protection
2603
		if ( $this->isSpecialPage() ) {
2604
			return true;
2605
		}
2606
2607
		# Check regular protection levels
2608
		foreach ( $restrictionTypes as $type ) {
2609
			if ( $action == $type || $action == '' ) {
2610
				$r = $this->getRestrictions( $type );
2611
				foreach ( $wgRestrictionLevels as $level ) {
2612
					if ( in_array( $level, $r ) && $level != '' ) {
2613
						return true;
2614
					}
2615
				}
2616
			}
2617
		}
2618
2619
		return false;
2620
	}
2621
2622
	/**
2623
	 * Determines if $user is unable to edit this page because it has been protected
2624
	 * by $wgNamespaceProtection.
2625
	 *
2626
	 * @param User $user User object to check permissions
2627
	 * @return bool
2628
	 */
2629
	public function isNamespaceProtected( User $user ) {
2630
		global $wgNamespaceProtection;
2631
2632
		if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
2633
			foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
2634
				if ( $right != '' && !$user->isAllowed( $right ) ) {
2635
					return true;
2636
				}
2637
			}
2638
		}
2639
		return false;
2640
	}
2641
2642
	/**
2643
	 * Cascading protection: Return true if cascading restrictions apply to this page, false if not.
2644
	 *
2645
	 * @return bool If the page is subject to cascading restrictions.
2646
	 */
2647
	public function isCascadeProtected() {
2648
		list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
2649
		return ( $sources > 0 );
2650
	}
2651
2652
	/**
2653
	 * Determines whether cascading protection sources have already been loaded from
2654
	 * the database.
2655
	 *
2656
	 * @param bool $getPages True to check if the pages are loaded, or false to check
2657
	 * if the status is loaded.
2658
	 * @return bool Whether or not the specified information has been loaded
2659
	 * @since 1.23
2660
	 */
2661
	public function areCascadeProtectionSourcesLoaded( $getPages = true ) {
2662
		return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null;
2663
	}
2664
2665
	/**
2666
	 * Cascading protection: Get the source of any cascading restrictions on this page.
2667
	 *
2668
	 * @param bool $getPages Whether or not to retrieve the actual pages
2669
	 *        that the restrictions have come from and the actual restrictions
2670
	 *        themselves.
2671
	 * @return array Two elements: First is an array of Title objects of the
2672
	 *        pages from which cascading restrictions have come, false for
2673
	 *        none, or true if such restrictions exist but $getPages was not
2674
	 *        set. Second is an array like that returned by
2675
	 *        Title::getAllRestrictions(), or an empty array if $getPages is
2676
	 *        false.
2677
	 */
2678
	public function getCascadeProtectionSources( $getPages = true ) {
2679
		$pagerestrictions = [];
2680
2681
		if ( $this->mCascadeSources !== null && $getPages ) {
2682
			return [ $this->mCascadeSources, $this->mCascadingRestrictions ];
2683
		} elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) {
2684
			return [ $this->mHasCascadingRestrictions, $pagerestrictions ];
2685
		}
2686
2687
		$dbr = wfGetDB( DB_SLAVE );
2688
2689
		if ( $this->getNamespace() == NS_FILE ) {
2690
			$tables = [ 'imagelinks', 'page_restrictions' ];
2691
			$where_clauses = [
2692
				'il_to' => $this->getDBkey(),
2693
				'il_from=pr_page',
2694
				'pr_cascade' => 1
2695
			];
2696
		} else {
2697
			$tables = [ 'templatelinks', 'page_restrictions' ];
2698
			$where_clauses = [
2699
				'tl_namespace' => $this->getNamespace(),
2700
				'tl_title' => $this->getDBkey(),
2701
				'tl_from=pr_page',
2702
				'pr_cascade' => 1
2703
			];
2704
		}
2705
2706
		if ( $getPages ) {
2707
			$cols = [ 'pr_page', 'page_namespace', 'page_title',
2708
				'pr_expiry', 'pr_type', 'pr_level' ];
2709
			$where_clauses[] = 'page_id=pr_page';
2710
			$tables[] = 'page';
2711
		} else {
2712
			$cols = [ 'pr_expiry' ];
2713
		}
2714
2715
		$res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
2716
2717
		$sources = $getPages ? [] : false;
2718
		$now = wfTimestampNow();
2719
2720
		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...
2721
			$expiry = $dbr->decodeExpiry( $row->pr_expiry );
2722
			if ( $expiry > $now ) {
2723
				if ( $getPages ) {
2724
					$page_id = $row->pr_page;
2725
					$page_ns = $row->page_namespace;
2726
					$page_title = $row->page_title;
2727
					$sources[$page_id] = Title::makeTitle( $page_ns, $page_title );
2728
					# Add groups needed for each restriction type if its not already there
2729
					# Make sure this restriction type still exists
2730
2731
					if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
2732
						$pagerestrictions[$row->pr_type] = [];
2733
					}
2734
2735
					if (
2736
						isset( $pagerestrictions[$row->pr_type] )
2737
						&& !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] )
2738
					) {
2739
						$pagerestrictions[$row->pr_type][] = $row->pr_level;
2740
					}
2741
				} else {
2742
					$sources = true;
2743
				}
2744
			}
2745
		}
2746
2747
		if ( $getPages ) {
2748
			$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...
2749
			$this->mCascadingRestrictions = $pagerestrictions;
2750
		} else {
2751
			$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...
2752
		}
2753
2754
		return [ $sources, $pagerestrictions ];
2755
	}
2756
2757
	/**
2758
	 * Accessor for mRestrictionsLoaded
2759
	 *
2760
	 * @return bool Whether or not the page's restrictions have already been
2761
	 * loaded from the database
2762
	 * @since 1.23
2763
	 */
2764
	public function areRestrictionsLoaded() {
2765
		return $this->mRestrictionsLoaded;
2766
	}
2767
2768
	/**
2769
	 * Accessor/initialisation for mRestrictions
2770
	 *
2771
	 * @param string $action Action that permission needs to be checked for
2772
	 * @return array Restriction levels needed to take the action. All levels are
2773
	 *     required. Note that restriction levels are normally user rights, but 'sysop'
2774
	 *     and 'autoconfirmed' are also allowed for backwards compatibility. These should
2775
	 *     be mapped to 'editprotected' and 'editsemiprotected' respectively.
2776
	 */
2777
	public function getRestrictions( $action ) {
2778
		if ( !$this->mRestrictionsLoaded ) {
2779
			$this->loadRestrictions();
2780
		}
2781
		return isset( $this->mRestrictions[$action] )
2782
				? $this->mRestrictions[$action]
2783
				: [];
2784
	}
2785
2786
	/**
2787
	 * Accessor/initialisation for mRestrictions
2788
	 *
2789
	 * @return array Keys are actions, values are arrays as returned by
2790
	 *     Title::getRestrictions()
2791
	 * @since 1.23
2792
	 */
2793
	public function getAllRestrictions() {
2794
		if ( !$this->mRestrictionsLoaded ) {
2795
			$this->loadRestrictions();
2796
		}
2797
		return $this->mRestrictions;
2798
	}
2799
2800
	/**
2801
	 * Get the expiry time for the restriction against a given action
2802
	 *
2803
	 * @param string $action
2804
	 * @return string|bool 14-char timestamp, or 'infinity' if the page is protected forever
2805
	 *     or not protected at all, or false if the action is not recognised.
2806
	 */
2807
	public function getRestrictionExpiry( $action ) {
2808
		if ( !$this->mRestrictionsLoaded ) {
2809
			$this->loadRestrictions();
2810
		}
2811
		return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
2812
	}
2813
2814
	/**
2815
	 * Returns cascading restrictions for the current article
2816
	 *
2817
	 * @return bool
2818
	 */
2819
	function areRestrictionsCascading() {
2820
		if ( !$this->mRestrictionsLoaded ) {
2821
			$this->loadRestrictions();
2822
		}
2823
2824
		return $this->mCascadeRestriction;
2825
	}
2826
2827
	/**
2828
	 * Loads a string into mRestrictions array
2829
	 *
2830
	 * @param ResultWrapper $res Resource restrictions as an SQL result.
2831
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2832
	 *        restrictions from page table (pre 1.10)
2833
	 */
2834
	private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) {
2835
		$rows = [];
2836
2837
		foreach ( $res as $row ) {
2838
			$rows[] = $row;
2839
		}
2840
2841
		$this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
2842
	}
2843
2844
	/**
2845
	 * Compiles list of active page restrictions from both page table (pre 1.10)
2846
	 * and page_restrictions table for this existing page.
2847
	 * Public for usage by LiquidThreads.
2848
	 *
2849
	 * @param array $rows Array of db result objects
2850
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2851
	 *   restrictions from page table (pre 1.10)
2852
	 */
2853
	public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
2854
		$dbr = wfGetDB( DB_SLAVE );
2855
2856
		$restrictionTypes = $this->getRestrictionTypes();
2857
2858
		foreach ( $restrictionTypes as $type ) {
2859
			$this->mRestrictions[$type] = [];
2860
			$this->mRestrictionsExpiry[$type] = 'infinity';
2861
		}
2862
2863
		$this->mCascadeRestriction = false;
2864
2865
		# Backwards-compatibility: also load the restrictions from the page record (old format).
2866
		if ( $oldFashionedRestrictions !== null ) {
2867
			$this->mOldRestrictions = $oldFashionedRestrictions;
2868
		}
2869
2870
		if ( $this->mOldRestrictions === false ) {
2871
			$this->mOldRestrictions = $dbr->selectField( 'page', 'page_restrictions',
2872
				[ 'page_id' => $this->getArticleID() ], __METHOD__ );
2873
		}
2874
2875
		if ( $this->mOldRestrictions != '' ) {
2876
			foreach ( explode( ':', trim( $this->mOldRestrictions ) ) as $restrict ) {
2877
				$temp = explode( '=', trim( $restrict ) );
2878
				if ( count( $temp ) == 1 ) {
2879
					// old old format should be treated as edit/move restriction
2880
					$this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
2881
					$this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
2882
				} else {
2883
					$restriction = trim( $temp[1] );
2884
					if ( $restriction != '' ) { // some old entries are empty
2885
						$this->mRestrictions[$temp[0]] = explode( ',', $restriction );
2886
					}
2887
				}
2888
			}
2889
		}
2890
2891
		if ( count( $rows ) ) {
2892
			# Current system - load second to make them override.
2893
			$now = wfTimestampNow();
2894
2895
			# Cycle through all the restrictions.
2896
			foreach ( $rows as $row ) {
2897
2898
				// Don't take care of restrictions types that aren't allowed
2899
				if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
2900
					continue;
2901
				}
2902
2903
				// This code should be refactored, now that it's being used more generally,
2904
				// But I don't really see any harm in leaving it in Block for now -werdna
2905
				$expiry = $dbr->decodeExpiry( $row->pr_expiry );
2906
2907
				// Only apply the restrictions if they haven't expired!
2908
				if ( !$expiry || $expiry > $now ) {
2909
					$this->mRestrictionsExpiry[$row->pr_type] = $expiry;
2910
					$this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
2911
2912
					$this->mCascadeRestriction |= $row->pr_cascade;
2913
				}
2914
			}
2915
		}
2916
2917
		$this->mRestrictionsLoaded = true;
2918
	}
2919
2920
	/**
2921
	 * Load restrictions from the page_restrictions table
2922
	 *
2923
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2924
	 *   restrictions from page table (pre 1.10)
2925
	 */
2926
	public function loadRestrictions( $oldFashionedRestrictions = null ) {
2927
		if ( !$this->mRestrictionsLoaded ) {
2928
			$dbr = wfGetDB( DB_SLAVE );
2929
			if ( $this->exists() ) {
2930
				$res = $dbr->select(
2931
					'page_restrictions',
2932
					[ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ],
2933
					[ 'pr_page' => $this->getArticleID() ],
2934
					__METHOD__
2935
				);
2936
2937
				$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 2930 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...
2938
			} else {
2939
				$title_protection = $this->getTitleProtection();
2940
2941
				if ( $title_protection ) {
2942
					$now = wfTimestampNow();
2943
					$expiry = $dbr->decodeExpiry( $title_protection['expiry'] );
2944
2945
					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...
2946
						// Apply the restrictions
2947
						$this->mRestrictionsExpiry['create'] = $expiry;
2948
						$this->mRestrictions['create'] = explode( ',', trim( $title_protection['permission'] ) );
2949
					} else { // Get rid of the old restrictions
2950
						$this->mTitleProtection = false;
2951
					}
2952
				} else {
2953
					$this->mRestrictionsExpiry['create'] = 'infinity';
2954
				}
2955
				$this->mRestrictionsLoaded = true;
2956
			}
2957
		}
2958
	}
2959
2960
	/**
2961
	 * Flush the protection cache in this object and force reload from the database.
2962
	 * This is used when updating protection from WikiPage::doUpdateRestrictions().
2963
	 */
2964
	public function flushRestrictions() {
2965
		$this->mRestrictionsLoaded = false;
2966
		$this->mTitleProtection = null;
2967
	}
2968
2969
	/**
2970
	 * Purge expired restrictions from the page_restrictions table
2971
	 */
2972
	static function purgeExpiredRestrictions() {
2973
		if ( wfReadOnly() ) {
2974
			return;
2975
		}
2976
2977
		DeferredUpdates::addUpdate( new AtomicSectionUpdate(
2978
			wfGetDB( DB_MASTER ),
2979
			__METHOD__,
2980
			function ( IDatabase $dbw, $fname ) {
2981
				$dbw->delete(
2982
					'page_restrictions',
2983
					[ 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
2984
					$fname
2985
				);
2986
				$dbw->delete(
2987
					'protected_titles',
2988
					[ 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
2989
					$fname
2990
				);
2991
			}
2992
		) );
2993
	}
2994
2995
	/**
2996
	 * Does this have subpages?  (Warning, usually requires an extra DB query.)
2997
	 *
2998
	 * @return bool
2999
	 */
3000
	public function hasSubpages() {
3001
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
3002
			# Duh
3003
			return false;
3004
		}
3005
3006
		# We dynamically add a member variable for the purpose of this method
3007
		# alone to cache the result.  There's no point in having it hanging
3008
		# around uninitialized in every Title object; therefore we only add it
3009
		# if needed and don't declare it statically.
3010
		if ( $this->mHasSubpages === null ) {
3011
			$this->mHasSubpages = false;
3012
			$subpages = $this->getSubpages( 1 );
3013
			if ( $subpages instanceof TitleArray ) {
3014
				$this->mHasSubpages = (bool)$subpages->count();
3015
			}
3016
		}
3017
3018
		return $this->mHasSubpages;
3019
	}
3020
3021
	/**
3022
	 * Get all subpages of this page.
3023
	 *
3024
	 * @param int $limit Maximum number of subpages to fetch; -1 for no limit
3025
	 * @return TitleArray|array TitleArray, or empty array if this page's namespace
3026
	 *  doesn't allow subpages
3027
	 */
3028
	public function getSubpages( $limit = -1 ) {
3029
		if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3030
			return [];
3031
		}
3032
3033
		$dbr = wfGetDB( DB_SLAVE );
3034
		$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...
3035
		$conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
3036
		$options = [];
3037
		if ( $limit > -1 ) {
3038
			$options['LIMIT'] = $limit;
3039
		}
3040
		$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...
3041
			$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...
3042
				[ 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ],
3043
				$conds,
3044
				__METHOD__,
3045
				$options
3046
			)
3047
		);
3048
		return $this->mSubpages;
3049
	}
3050
3051
	/**
3052
	 * Is there a version of this page in the deletion archive?
3053
	 *
3054
	 * @return int The number of archived revisions
3055
	 */
3056
	public function isDeleted() {
3057
		if ( $this->getNamespace() < 0 ) {
3058
			$n = 0;
3059
		} else {
3060
			$dbr = wfGetDB( DB_SLAVE );
3061
3062
			$n = $dbr->selectField( 'archive', 'COUNT(*)',
3063
				[ 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ],
3064
				__METHOD__
3065
			);
3066 View Code Duplication
			if ( $this->getNamespace() == NS_FILE ) {
3067
				$n += $dbr->selectField( 'filearchive', 'COUNT(*)',
3068
					[ 'fa_name' => $this->getDBkey() ],
3069
					__METHOD__
3070
				);
3071
			}
3072
		}
3073
		return (int)$n;
3074
	}
3075
3076
	/**
3077
	 * Is there a version of this page in the deletion archive?
3078
	 *
3079
	 * @return bool
3080
	 */
3081
	public function isDeletedQuick() {
3082
		if ( $this->getNamespace() < 0 ) {
3083
			return false;
3084
		}
3085
		$dbr = wfGetDB( DB_SLAVE );
3086
		$deleted = (bool)$dbr->selectField( 'archive', '1',
3087
			[ 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ],
3088
			__METHOD__
3089
		);
3090 View Code Duplication
		if ( !$deleted && $this->getNamespace() == NS_FILE ) {
3091
			$deleted = (bool)$dbr->selectField( 'filearchive', '1',
3092
				[ 'fa_name' => $this->getDBkey() ],
3093
				__METHOD__
3094
			);
3095
		}
3096
		return $deleted;
3097
	}
3098
3099
	/**
3100
	 * Get the article ID for this Title from the link cache,
3101
	 * adding it if necessary
3102
	 *
3103
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select
3104
	 *  for update
3105
	 * @return int The ID
3106
	 */
3107
	public function getArticleID( $flags = 0 ) {
3108
		if ( $this->getNamespace() < 0 ) {
3109
			$this->mArticleID = 0;
3110
			return $this->mArticleID;
3111
		}
3112
		$linkCache = LinkCache::singleton();
3113
		if ( $flags & self::GAID_FOR_UPDATE ) {
3114
			$oldUpdate = $linkCache->forUpdate( true );
3115
			$linkCache->clearLink( $this );
3116
			$this->mArticleID = $linkCache->addLinkObj( $this );
3117
			$linkCache->forUpdate( $oldUpdate );
3118
		} else {
3119
			if ( -1 == $this->mArticleID ) {
3120
				$this->mArticleID = $linkCache->addLinkObj( $this );
3121
			}
3122
		}
3123
		return $this->mArticleID;
3124
	}
3125
3126
	/**
3127
	 * Is this an article that is a redirect page?
3128
	 * Uses link cache, adding it if necessary
3129
	 *
3130
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3131
	 * @return bool
3132
	 */
3133
	public function isRedirect( $flags = 0 ) {
3134
		if ( !is_null( $this->mRedirect ) ) {
3135
			return $this->mRedirect;
3136
		}
3137
		if ( !$this->getArticleID( $flags ) ) {
3138
			$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...
3139
			return $this->mRedirect;
3140
		}
3141
3142
		$linkCache = LinkCache::singleton();
3143
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3144
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
3145
		if ( $cached === null ) {
3146
			# Trust LinkCache's state over our own
3147
			# LinkCache is telling us that the page doesn't exist, despite there being cached
3148
			# data relating to an existing page in $this->mArticleID. Updaters should clear
3149
			# LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is
3150
			# set, then LinkCache will definitely be up to date here, since getArticleID() forces
3151
			# LinkCache to refresh its data from the master.
3152
			$this->mRedirect = false;
3153
			return $this->mRedirect;
3154
		}
3155
3156
		$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...
3157
3158
		return $this->mRedirect;
3159
	}
3160
3161
	/**
3162
	 * What is the length of this page?
3163
	 * Uses link cache, adding it if necessary
3164
	 *
3165
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3166
	 * @return int
3167
	 */
3168
	public function getLength( $flags = 0 ) {
3169
		if ( $this->mLength != -1 ) {
3170
			return $this->mLength;
3171
		}
3172
		if ( !$this->getArticleID( $flags ) ) {
3173
			$this->mLength = 0;
3174
			return $this->mLength;
3175
		}
3176
		$linkCache = LinkCache::singleton();
3177
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3178
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
3179
		if ( $cached === null ) {
3180
			# Trust LinkCache's state over our own, as for isRedirect()
3181
			$this->mLength = 0;
3182
			return $this->mLength;
3183
		}
3184
3185
		$this->mLength = intval( $cached );
3186
3187
		return $this->mLength;
3188
	}
3189
3190
	/**
3191
	 * What is the page_latest field for this page?
3192
	 *
3193
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3194
	 * @return int Int or 0 if the page doesn't exist
3195
	 */
3196
	public function getLatestRevID( $flags = 0 ) {
3197
		if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
3198
			return intval( $this->mLatestID );
3199
		}
3200
		if ( !$this->getArticleID( $flags ) ) {
3201
			$this->mLatestID = 0;
3202
			return $this->mLatestID;
3203
		}
3204
		$linkCache = LinkCache::singleton();
3205
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3206
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
3207
		if ( $cached === null ) {
3208
			# Trust LinkCache's state over our own, as for isRedirect()
3209
			$this->mLatestID = 0;
3210
			return $this->mLatestID;
3211
		}
3212
3213
		$this->mLatestID = intval( $cached );
3214
3215
		return $this->mLatestID;
3216
	}
3217
3218
	/**
3219
	 * This clears some fields in this object, and clears any associated
3220
	 * keys in the "bad links" section of the link cache.
3221
	 *
3222
	 * - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow
3223
	 * loading of the new page_id. It's also called from
3224
	 * WikiPage::doDeleteArticleReal()
3225
	 *
3226
	 * @param int $newid The new Article ID
3227
	 */
3228
	public function resetArticleID( $newid ) {
3229
		$linkCache = LinkCache::singleton();
3230
		$linkCache->clearLink( $this );
3231
3232
		if ( $newid === false ) {
3233
			$this->mArticleID = -1;
3234
		} else {
3235
			$this->mArticleID = intval( $newid );
3236
		}
3237
		$this->mRestrictionsLoaded = false;
3238
		$this->mRestrictions = [];
3239
		$this->mOldRestrictions = false;
3240
		$this->mRedirect = null;
3241
		$this->mLength = -1;
3242
		$this->mLatestID = false;
3243
		$this->mContentModel = false;
3244
		$this->mEstimateRevisions = null;
3245
		$this->mPageLanguage = false;
3246
		$this->mDbPageLanguage = false;
3247
		$this->mIsBigDeletion = null;
3248
	}
3249
3250
	public static function clearCaches() {
3251
		$linkCache = LinkCache::singleton();
3252
		$linkCache->clear();
3253
3254
		$titleCache = self::getTitleCache();
3255
		$titleCache->clear();
3256
	}
3257
3258
	/**
3259
	 * Capitalize a text string for a title if it belongs to a namespace that capitalizes
3260
	 *
3261
	 * @param string $text Containing title to capitalize
3262
	 * @param int $ns Namespace index, defaults to NS_MAIN
3263
	 * @return string Containing capitalized title
3264
	 */
3265
	public static function capitalize( $text, $ns = NS_MAIN ) {
3266
		global $wgContLang;
3267
3268
		if ( MWNamespace::isCapitalized( $ns ) ) {
3269
			return $wgContLang->ucfirst( $text );
3270
		} else {
3271
			return $text;
3272
		}
3273
	}
3274
3275
	/**
3276
	 * Secure and split - main initialisation function for this object
3277
	 *
3278
	 * Assumes that mDbkeyform has been set, and is urldecoded
3279
	 * and uses underscores, but not otherwise munged.  This function
3280
	 * removes illegal characters, splits off the interwiki and
3281
	 * namespace prefixes, sets the other forms, and canonicalizes
3282
	 * everything.
3283
	 *
3284
	 * @throws MalformedTitleException On invalid titles
3285
	 * @return bool True on success
3286
	 */
3287
	private function secureAndSplit() {
3288
		# Initialisation
3289
		$this->mInterwiki = '';
3290
		$this->mFragment = '';
3291
		$this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
3292
3293
		$dbkey = $this->mDbkeyform;
3294
3295
		// @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
3296
		//        the parsing code with Title, while avoiding massive refactoring.
3297
		// @todo: get rid of secureAndSplit, refactor parsing code.
3298
		// @note: getTitleParser() returns a TitleParser implementation which does not have a
3299
		//        splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
3300
		$titleCodec = MediaWikiServices::getInstance()->getTitleParser();
3301
		// MalformedTitleException can be thrown here
3302
		$parts = $titleCodec->splitTitleString( $dbkey, $this->getDefaultNamespace() );
3303
3304
		# Fill fields
3305
		$this->setFragment( '#' . $parts['fragment'] );
3306
		$this->mInterwiki = $parts['interwiki'];
3307
		$this->mLocalInterwiki = $parts['local_interwiki'];
3308
		$this->mNamespace = $parts['namespace'];
3309
		$this->mUserCaseDBKey = $parts['user_case_dbkey'];
3310
3311
		$this->mDbkeyform = $parts['dbkey'];
3312
		$this->mUrlform = wfUrlencode( $this->mDbkeyform );
3313
		$this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
3314
3315
		# We already know that some pages won't be in the database!
3316
		if ( $this->isExternal() || $this->mNamespace == NS_SPECIAL ) {
3317
			$this->mArticleID = 0;
3318
		}
3319
3320
		return true;
3321
	}
3322
3323
	/**
3324
	 * Get an array of Title objects linking to this Title
3325
	 * Also stores the IDs in the link cache.
3326
	 *
3327
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3328
	 * On heavily-used templates it will max out the memory.
3329
	 *
3330
	 * @param array $options May be FOR UPDATE
3331
	 * @param string $table Table name
3332
	 * @param string $prefix Fields prefix
3333
	 * @return Title[] Array of Title objects linking here
3334
	 */
3335
	public function getLinksTo( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3336
		if ( count( $options ) > 0 ) {
3337
			$db = wfGetDB( DB_MASTER );
3338
		} else {
3339
			$db = wfGetDB( DB_SLAVE );
3340
		}
3341
3342
		$res = $db->select(
3343
			[ 'page', $table ],
3344
			self::getSelectFields(),
3345
			[
3346
				"{$prefix}_from=page_id",
3347
				"{$prefix}_namespace" => $this->getNamespace(),
3348
				"{$prefix}_title" => $this->getDBkey() ],
3349
			__METHOD__,
3350
			$options
3351
		);
3352
3353
		$retVal = [];
3354
		if ( $res->numRows() ) {
3355
			$linkCache = LinkCache::singleton();
3356 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...
3357
				$titleObj = Title::makeTitle( $row->page_namespace, $row->page_title );
3358
				if ( $titleObj ) {
3359
					$linkCache->addGoodLinkObjFromRow( $titleObj, $row );
3360
					$retVal[] = $titleObj;
3361
				}
3362
			}
3363
		}
3364
		return $retVal;
3365
	}
3366
3367
	/**
3368
	 * Get an array of Title objects using this Title as a template
3369
	 * Also stores the IDs in the link cache.
3370
	 *
3371
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3372
	 * On heavily-used templates it will max out the memory.
3373
	 *
3374
	 * @param array $options Query option to Database::select()
3375
	 * @return Title[] Array of Title the Title objects linking here
3376
	 */
3377
	public function getTemplateLinksTo( $options = [] ) {
3378
		return $this->getLinksTo( $options, 'templatelinks', 'tl' );
3379
	}
3380
3381
	/**
3382
	 * Get an array of Title objects linked from this Title
3383
	 * Also stores the IDs in the link cache.
3384
	 *
3385
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3386
	 * On heavily-used templates it will max out the memory.
3387
	 *
3388
	 * @param array $options Query option to Database::select()
3389
	 * @param string $table Table name
3390
	 * @param string $prefix Fields prefix
3391
	 * @return array Array of Title objects linking here
3392
	 */
3393
	public function getLinksFrom( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3394
		$id = $this->getArticleID();
3395
3396
		# If the page doesn't exist; there can't be any link from this page
3397
		if ( !$id ) {
3398
			return [];
3399
		}
3400
3401
		$db = wfGetDB( DB_SLAVE );
3402
3403
		$blNamespace = "{$prefix}_namespace";
3404
		$blTitle = "{$prefix}_title";
3405
3406
		$res = $db->select(
3407
			[ $table, 'page' ],
3408
			array_merge(
3409
				[ $blNamespace, $blTitle ],
3410
				WikiPage::selectFields()
3411
			),
3412
			[ "{$prefix}_from" => $id ],
3413
			__METHOD__,
3414
			$options,
3415
			[ 'page' => [
3416
				'LEFT JOIN',
3417
				[ "page_namespace=$blNamespace", "page_title=$blTitle" ]
3418
			] ]
3419
		);
3420
3421
		$retVal = [];
3422
		$linkCache = LinkCache::singleton();
3423
		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...
3424
			if ( $row->page_id ) {
3425
				$titleObj = Title::newFromRow( $row );
3426
			} else {
3427
				$titleObj = Title::makeTitle( $row->$blNamespace, $row->$blTitle );
3428
				$linkCache->addBadLinkObj( $titleObj );
3429
			}
3430
			$retVal[] = $titleObj;
3431
		}
3432
3433
		return $retVal;
3434
	}
3435
3436
	/**
3437
	 * Get an array of Title objects used on this Title as a template
3438
	 * Also stores the IDs in the link cache.
3439
	 *
3440
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3441
	 * On heavily-used templates it will max out the memory.
3442
	 *
3443
	 * @param array $options May be FOR UPDATE
3444
	 * @return Title[] Array of Title the Title objects used here
3445
	 */
3446
	public function getTemplateLinksFrom( $options = [] ) {
3447
		return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
3448
	}
3449
3450
	/**
3451
	 * Get an array of Title objects referring to non-existent articles linked
3452
	 * from this page.
3453
	 *
3454
	 * @todo check if needed (used only in SpecialBrokenRedirects.php, and
3455
	 *   should use redirect table in this case).
3456
	 * @return Title[] Array of Title the Title objects
3457
	 */
3458
	public function getBrokenLinksFrom() {
3459
		if ( $this->getArticleID() == 0 ) {
3460
			# All links from article ID 0 are false positives
3461
			return [];
3462
		}
3463
3464
		$dbr = wfGetDB( DB_SLAVE );
3465
		$res = $dbr->select(
3466
			[ 'page', 'pagelinks' ],
3467
			[ 'pl_namespace', 'pl_title' ],
3468
			[
3469
				'pl_from' => $this->getArticleID(),
3470
				'page_namespace IS NULL'
3471
			],
3472
			__METHOD__, [],
3473
			[
3474
				'page' => [
3475
					'LEFT JOIN',
3476
					[ 'pl_namespace=page_namespace', 'pl_title=page_title' ]
3477
				]
3478
			]
3479
		);
3480
3481
		$retVal = [];
3482
		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...
3483
			$retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
3484
		}
3485
		return $retVal;
3486
	}
3487
3488
	/**
3489
	 * Get a list of URLs to purge from the CDN cache when this
3490
	 * page changes
3491
	 *
3492
	 * @return string[] Array of String the URLs
3493
	 */
3494
	public function getCdnUrls() {
3495
		$urls = [
3496
			$this->getInternalURL(),
3497
			$this->getInternalURL( 'action=history' )
3498
		];
3499
3500
		$pageLang = $this->getPageLanguage();
3501
		if ( $pageLang->hasVariants() ) {
3502
			$variants = $pageLang->getVariants();
3503
			foreach ( $variants as $vCode ) {
3504
				$urls[] = $this->getInternalURL( $vCode );
3505
			}
3506
		}
3507
3508
		// If we are looking at a css/js user subpage, purge the action=raw.
3509
		if ( $this->isJsSubpage() ) {
3510
			$urls[] = $this->getInternalURL( 'action=raw&ctype=text/javascript' );
3511
		} elseif ( $this->isCssSubpage() ) {
3512
			$urls[] = $this->getInternalURL( 'action=raw&ctype=text/css' );
3513
		}
3514
3515
		Hooks::run( 'TitleSquidURLs', [ $this, &$urls ] );
3516
		return $urls;
3517
	}
3518
3519
	/**
3520
	 * @deprecated since 1.27 use getCdnUrls()
3521
	 */
3522
	public function getSquidURLs() {
3523
		return $this->getCdnUrls();
3524
	}
3525
3526
	/**
3527
	 * Purge all applicable CDN URLs
3528
	 */
3529
	public function purgeSquid() {
3530
		DeferredUpdates::addUpdate(
3531
			new CdnCacheUpdate( $this->getCdnUrls() ),
3532
			DeferredUpdates::PRESEND
3533
		);
3534
	}
3535
3536
	/**
3537
	 * Move this page without authentication
3538
	 *
3539
	 * @deprecated since 1.25 use MovePage class instead
3540
	 * @param Title $nt The new page Title
3541
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3542
	 */
3543
	public function moveNoAuth( &$nt ) {
3544
		wfDeprecated( __METHOD__, '1.25' );
3545
		return $this->moveTo( $nt, false );
0 ignored issues
show
Deprecated Code introduced by
The method Title::moveTo() has been deprecated with message: since 1.25, use the MovePage class instead

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

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

Loading history...
3546
	}
3547
3548
	/**
3549
	 * Check whether a given move operation would be valid.
3550
	 * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
3551
	 *
3552
	 * @deprecated since 1.25, use MovePage's methods instead
3553
	 * @param Title $nt The new title
3554
	 * @param bool $auth Whether to check user permissions (uses $wgUser)
3555
	 * @param string $reason Is the log summary of the move, used for spam checking
3556
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3557
	 */
3558
	public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
3559
		global $wgUser;
3560
3561
		if ( !( $nt instanceof Title ) ) {
3562
			// Normally we'd add this to $errors, but we'll get
3563
			// lots of syntax errors if $nt is not an object
3564
			return [ [ 'badtitletext' ] ];
3565
		}
3566
3567
		$mp = new MovePage( $this, $nt );
3568
		$errors = $mp->isValidMove()->getErrorsArray();
0 ignored issues
show
Deprecated Code introduced by
The method Status::getErrorsArray() has been deprecated with message: 1.25

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

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

Loading history...
3569
		if ( $auth ) {
3570
			$errors = wfMergeErrorArrays(
3571
				$errors,
3572
				$mp->checkPermissions( $wgUser, $reason )->getErrorsArray()
0 ignored issues
show
Deprecated Code introduced by
The method Status::getErrorsArray() has been deprecated with message: 1.25

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

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

Loading history...
3573
			);
3574
		}
3575
3576
		return $errors ?: true;
3577
	}
3578
3579
	/**
3580
	 * Check if the requested move target is a valid file move target
3581
	 * @todo move this to MovePage
3582
	 * @param Title $nt Target title
3583
	 * @return array List of errors
3584
	 */
3585
	protected function validateFileMoveOperation( $nt ) {
3586
		global $wgUser;
3587
3588
		$errors = [];
3589
3590
		$destFile = wfLocalFile( $nt );
3591
		$destFile->load( File::READ_LATEST );
3592
		if ( !$wgUser->isAllowed( 'reupload-shared' )
3593
			&& !$destFile->exists() && wfFindFile( $nt )
3594
		) {
3595
			$errors[] = [ 'file-exists-sharedrepo' ];
3596
		}
3597
3598
		return $errors;
3599
	}
3600
3601
	/**
3602
	 * Move a title to a new location
3603
	 *
3604
	 * @deprecated since 1.25, use the MovePage class instead
3605
	 * @param Title $nt The new title
3606
	 * @param bool $auth Indicates whether $wgUser's permissions
3607
	 *  should be checked
3608
	 * @param string $reason The reason for the move
3609
	 * @param bool $createRedirect Whether to create a redirect from the old title to the new title.
3610
	 *  Ignored if the user doesn't have the suppressredirect right.
3611
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3612
	 */
3613
	public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
3614
		global $wgUser;
3615
		$err = $this->isValidMoveOperation( $nt, $auth, $reason );
0 ignored issues
show
Deprecated Code introduced by
The method Title::isValidMoveOperation() has been deprecated with message: since 1.25, use MovePage's methods instead

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

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

Loading history...
3616
		if ( is_array( $err ) ) {
3617
			// Auto-block user's IP if the account was "hard" blocked
3618
			$wgUser->spreadAnyEditBlock();
3619
			return $err;
3620
		}
3621
		// Check suppressredirect permission
3622
		if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
3623
			$createRedirect = true;
3624
		}
3625
3626
		$mp = new MovePage( $this, $nt );
3627
		$status = $mp->move( $wgUser, $reason, $createRedirect );
3628
		if ( $status->isOK() ) {
3629
			return true;
3630
		} else {
3631
			return $status->getErrorsArray();
0 ignored issues
show
Deprecated Code introduced by
The method Status::getErrorsArray() has been deprecated with message: 1.25

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

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

Loading history...
3632
		}
3633
	}
3634
3635
	/**
3636
	 * Move this page's subpages to be subpages of $nt
3637
	 *
3638
	 * @param Title $nt Move target
3639
	 * @param bool $auth Whether $wgUser's permissions should be checked
3640
	 * @param string $reason The reason for the move
3641
	 * @param bool $createRedirect Whether to create redirects from the old subpages to
3642
	 *     the new ones Ignored if the user doesn't have the 'suppressredirect' right
3643
	 * @return array Array with old page titles as keys, and strings (new page titles) or
3644
	 *     arrays (errors) as values, or an error array with numeric indices if no pages
3645
	 *     were moved
3646
	 */
3647
	public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) {
3648
		global $wgMaximumMovedPages;
3649
		// Check permissions
3650
		if ( !$this->userCan( 'move-subpages' ) ) {
3651
			return [ 'cant-move-subpages' ];
3652
		}
3653
		// Do the source and target namespaces support subpages?
3654
		if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3655
			return [ 'namespace-nosubpages',
3656
				MWNamespace::getCanonicalName( $this->getNamespace() ) ];
3657
		}
3658
		if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
3659
			return [ 'namespace-nosubpages',
3660
				MWNamespace::getCanonicalName( $nt->getNamespace() ) ];
3661
		}
3662
3663
		$subpages = $this->getSubpages( $wgMaximumMovedPages + 1 );
3664
		$retval = [];
3665
		$count = 0;
3666
		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...
3667
			$count++;
3668
			if ( $count > $wgMaximumMovedPages ) {
3669
				$retval[$oldSubpage->getPrefixedText()] =
3670
						[ 'movepage-max-pages',
3671
							$wgMaximumMovedPages ];
3672
				break;
3673
			}
3674
3675
			// We don't know whether this function was called before
3676
			// or after moving the root page, so check both
3677
			// $this and $nt
3678
			if ( $oldSubpage->getArticleID() == $this->getArticleID()
3679
				|| $oldSubpage->getArticleID() == $nt->getArticleID()
3680
			) {
3681
				// When moving a page to a subpage of itself,
3682
				// don't move it twice
3683
				continue;
3684
			}
3685
			$newPageName = preg_replace(
3686
					'#^' . preg_quote( $this->getDBkey(), '#' ) . '#',
3687
					StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
3688
					$oldSubpage->getDBkey() );
3689
			if ( $oldSubpage->isTalkPage() ) {
3690
				$newNs = $nt->getTalkPage()->getNamespace();
3691
			} else {
3692
				$newNs = $nt->getSubjectPage()->getNamespace();
3693
			}
3694
			# Bug 14385: we need makeTitleSafe because the new page names may
3695
			# be longer than 255 characters.
3696
			$newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
3697
3698
			$success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect );
3699
			if ( $success === true ) {
3700
				$retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
3701
			} else {
3702
				$retval[$oldSubpage->getPrefixedText()] = $success;
3703
			}
3704
		}
3705
		return $retval;
3706
	}
3707
3708
	/**
3709
	 * Checks if this page is just a one-rev redirect.
3710
	 * Adds lock, so don't use just for light purposes.
3711
	 *
3712
	 * @return bool
3713
	 */
3714
	public function isSingleRevRedirect() {
3715
		global $wgContentHandlerUseDB;
3716
3717
		$dbw = wfGetDB( DB_MASTER );
3718
3719
		# Is it a redirect?
3720
		$fields = [ 'page_is_redirect', 'page_latest', 'page_id' ];
3721
		if ( $wgContentHandlerUseDB ) {
3722
			$fields[] = 'page_content_model';
3723
		}
3724
3725
		$row = $dbw->selectRow( 'page',
3726
			$fields,
3727
			$this->pageCond(),
3728
			__METHOD__,
3729
			[ 'FOR UPDATE' ]
3730
		);
3731
		# Cache some fields we may want
3732
		$this->mArticleID = $row ? intval( $row->page_id ) : 0;
3733
		$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...
3734
		$this->mLatestID = $row ? intval( $row->page_latest ) : false;
3735
		$this->mContentModel = $row && isset( $row->page_content_model )
3736
			? strval( $row->page_content_model )
3737
			: false;
3738
3739
		if ( !$this->mRedirect ) {
3740
			return false;
3741
		}
3742
		# Does the article have a history?
3743
		$row = $dbw->selectField( [ 'page', 'revision' ],
3744
			'rev_id',
3745
			[ 'page_namespace' => $this->getNamespace(),
3746
				'page_title' => $this->getDBkey(),
3747
				'page_id=rev_page',
3748
				'page_latest != rev_id'
3749
			],
3750
			__METHOD__,
3751
			[ 'FOR UPDATE' ]
3752
		);
3753
		# Return true if there was no history
3754
		return ( $row === false );
3755
	}
3756
3757
	/**
3758
	 * Checks if $this can be moved to a given Title
3759
	 * - Selects for update, so don't call it unless you mean business
3760
	 *
3761
	 * @deprecated since 1.25, use MovePage's methods instead
3762
	 * @param Title $nt The new title to check
3763
	 * @return bool
3764
	 */
3765
	public function isValidMoveTarget( $nt ) {
3766
		# Is it an existing file?
3767
		if ( $nt->getNamespace() == NS_FILE ) {
3768
			$file = wfLocalFile( $nt );
3769
			$file->load( File::READ_LATEST );
3770
			if ( $file->exists() ) {
3771
				wfDebug( __METHOD__ . ": file exists\n" );
3772
				return false;
3773
			}
3774
		}
3775
		# Is it a redirect with no history?
3776
		if ( !$nt->isSingleRevRedirect() ) {
3777
			wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
3778
			return false;
3779
		}
3780
		# Get the article text
3781
		$rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST );
3782
		if ( !is_object( $rev ) ) {
3783
			return false;
3784
		}
3785
		$content = $rev->getContent();
3786
		# Does the redirect point to the source?
3787
		# Or is it a broken self-redirect, usually caused by namespace collisions?
3788
		$redirTitle = $content ? $content->getRedirectTarget() : null;
3789
3790
		if ( $redirTitle ) {
3791
			if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
3792
				$redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
3793
				wfDebug( __METHOD__ . ": redirect points to other page\n" );
3794
				return false;
3795
			} else {
3796
				return true;
3797
			}
3798
		} else {
3799
			# Fail safe (not a redirect after all. strange.)
3800
			wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
3801
						" is a redirect, but it doesn't contain a valid redirect.\n" );
3802
			return false;
3803
		}
3804
	}
3805
3806
	/**
3807
	 * Get categories to which this Title belongs and return an array of
3808
	 * categories' names.
3809
	 *
3810
	 * @return array Array of parents in the form:
3811
	 *	  $parent => $currentarticle
3812
	 */
3813
	public function getParentCategories() {
3814
		global $wgContLang;
3815
3816
		$data = [];
3817
3818
		$titleKey = $this->getArticleID();
3819
3820
		if ( $titleKey === 0 ) {
3821
			return $data;
3822
		}
3823
3824
		$dbr = wfGetDB( DB_SLAVE );
3825
3826
		$res = $dbr->select(
3827
			'categorylinks',
3828
			'cl_to',
3829
			[ 'cl_from' => $titleKey ],
3830
			__METHOD__
3831
		);
3832
3833
		if ( $res->numRows() > 0 ) {
3834
			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...
3835
				// $data[] = Title::newFromText($wgContLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to);
3836
				$data[$wgContLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText();
3837
			}
3838
		}
3839
		return $data;
3840
	}
3841
3842
	/**
3843
	 * Get a tree of parent categories
3844
	 *
3845
	 * @param array $children Array with the children in the keys, to check for circular refs
3846
	 * @return array Tree of parent categories
3847
	 */
3848
	public function getParentCategoryTree( $children = [] ) {
3849
		$stack = [];
3850
		$parents = $this->getParentCategories();
3851
3852
		if ( $parents ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parents of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
3853
			foreach ( $parents as $parent => $current ) {
3854
				if ( array_key_exists( $parent, $children ) ) {
3855
					# Circular reference
3856
					$stack[$parent] = [];
3857
				} else {
3858
					$nt = Title::newFromText( $parent );
3859
					if ( $nt ) {
3860
						$stack[$parent] = $nt->getParentCategoryTree( $children + [ $parent => 1 ] );
3861
					}
3862
				}
3863
			}
3864
		}
3865
3866
		return $stack;
3867
	}
3868
3869
	/**
3870
	 * Get an associative array for selecting this title from
3871
	 * the "page" table
3872
	 *
3873
	 * @return array Array suitable for the $where parameter of DB::select()
3874
	 */
3875
	public function pageCond() {
3876
		if ( $this->mArticleID > 0 ) {
3877
			// PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
3878
			return [ 'page_id' => $this->mArticleID ];
3879
		} else {
3880
			return [ 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ];
3881
		}
3882
	}
3883
3884
	/**
3885
	 * Get the revision ID of the previous revision
3886
	 *
3887
	 * @param int $revId Revision ID. Get the revision that was before this one.
3888
	 * @param int $flags Title::GAID_FOR_UPDATE
3889
	 * @return int|bool Old revision ID, or false if none exists
3890
	 */
3891 View Code Duplication
	public function getPreviousRevisionID( $revId, $flags = 0 ) {
3892
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3893
		$revId = $db->selectField( 'revision', 'rev_id',
3894
			[
3895
				'rev_page' => $this->getArticleID( $flags ),
3896
				'rev_id < ' . intval( $revId )
3897
			],
3898
			__METHOD__,
3899
			[ 'ORDER BY' => 'rev_id DESC' ]
3900
		);
3901
3902
		if ( $revId === false ) {
3903
			return false;
3904
		} else {
3905
			return intval( $revId );
3906
		}
3907
	}
3908
3909
	/**
3910
	 * Get the revision ID of the next revision
3911
	 *
3912
	 * @param int $revId Revision ID. Get the revision that was after this one.
3913
	 * @param int $flags Title::GAID_FOR_UPDATE
3914
	 * @return int|bool Next revision ID, or false if none exists
3915
	 */
3916 View Code Duplication
	public function getNextRevisionID( $revId, $flags = 0 ) {
3917
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3918
		$revId = $db->selectField( 'revision', 'rev_id',
3919
			[
3920
				'rev_page' => $this->getArticleID( $flags ),
3921
				'rev_id > ' . intval( $revId )
3922
			],
3923
			__METHOD__,
3924
			[ 'ORDER BY' => 'rev_id' ]
3925
		);
3926
3927
		if ( $revId === false ) {
3928
			return false;
3929
		} else {
3930
			return intval( $revId );
3931
		}
3932
	}
3933
3934
	/**
3935
	 * Get the first revision of the page
3936
	 *
3937
	 * @param int $flags Title::GAID_FOR_UPDATE
3938
	 * @return Revision|null If page doesn't exist
3939
	 */
3940
	public function getFirstRevision( $flags = 0 ) {
3941
		$pageId = $this->getArticleID( $flags );
3942
		if ( $pageId ) {
3943
			$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3944
			$row = $db->selectRow( 'revision', Revision::selectFields(),
3945
				[ 'rev_page' => $pageId ],
3946
				__METHOD__,
3947
				[ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ]
3948
			);
3949
			if ( $row ) {
3950
				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 3944 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...
3951
			}
3952
		}
3953
		return null;
3954
	}
3955
3956
	/**
3957
	 * Get the oldest revision timestamp of this page
3958
	 *
3959
	 * @param int $flags Title::GAID_FOR_UPDATE
3960
	 * @return string MW timestamp
3961
	 */
3962
	public function getEarliestRevTime( $flags = 0 ) {
3963
		$rev = $this->getFirstRevision( $flags );
3964
		return $rev ? $rev->getTimestamp() : null;
3965
	}
3966
3967
	/**
3968
	 * Check if this is a new page
3969
	 *
3970
	 * @return bool
3971
	 */
3972
	public function isNewPage() {
3973
		$dbr = wfGetDB( DB_SLAVE );
3974
		return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
3975
	}
3976
3977
	/**
3978
	 * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit
3979
	 *
3980
	 * @return bool
3981
	 */
3982
	public function isBigDeletion() {
3983
		global $wgDeleteRevisionsLimit;
3984
3985
		if ( !$wgDeleteRevisionsLimit ) {
3986
			return false;
3987
		}
3988
3989
		if ( $this->mIsBigDeletion === null ) {
3990
			$dbr = wfGetDB( DB_SLAVE );
3991
3992
			$revCount = $dbr->selectRowCount(
3993
				'revision',
3994
				'1',
3995
				[ 'rev_page' => $this->getArticleID() ],
3996
				__METHOD__,
3997
				[ 'LIMIT' => $wgDeleteRevisionsLimit + 1 ]
3998
			);
3999
4000
			$this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit;
4001
		}
4002
4003
		return $this->mIsBigDeletion;
4004
	}
4005
4006
	/**
4007
	 * Get the approximate revision count of this page.
4008
	 *
4009
	 * @return int
4010
	 */
4011
	public function estimateRevisionCount() {
4012
		if ( !$this->exists() ) {
4013
			return 0;
4014
		}
4015
4016
		if ( $this->mEstimateRevisions === null ) {
4017
			$dbr = wfGetDB( DB_SLAVE );
4018
			$this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
4019
				[ 'rev_page' => $this->getArticleID() ], __METHOD__ );
4020
		}
4021
4022
		return $this->mEstimateRevisions;
4023
	}
4024
4025
	/**
4026
	 * Get the number of revisions between the given revision.
4027
	 * Used for diffs and other things that really need it.
4028
	 *
4029
	 * @param int|Revision $old Old revision or rev ID (first before range)
4030
	 * @param int|Revision $new New revision or rev ID (first after range)
4031
	 * @param int|null $max Limit of Revisions to count, will be incremented to detect truncations
4032
	 * @return int Number of revisions between these revisions.
4033
	 */
4034
	public function countRevisionsBetween( $old, $new, $max = null ) {
4035
		if ( !( $old instanceof Revision ) ) {
4036
			$old = Revision::newFromTitle( $this, (int)$old );
4037
		}
4038
		if ( !( $new instanceof Revision ) ) {
4039
			$new = Revision::newFromTitle( $this, (int)$new );
4040
		}
4041
		if ( !$old || !$new ) {
4042
			return 0; // nothing to compare
4043
		}
4044
		$dbr = wfGetDB( DB_SLAVE );
4045
		$conds = [
4046
			'rev_page' => $this->getArticleID(),
4047
			'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...
4048
			'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...
4049
		];
4050
		if ( $max !== null ) {
4051
			return $dbr->selectRowCount( 'revision', '1',
4052
				$conds,
4053
				__METHOD__,
4054
				[ 'LIMIT' => $max + 1 ] // extra to detect truncation
4055
			);
4056
		} else {
4057
			return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
4058
		}
4059
	}
4060
4061
	/**
4062
	 * Get the authors between the given revisions or revision IDs.
4063
	 * Used for diffs and other things that really need it.
4064
	 *
4065
	 * @since 1.23
4066
	 *
4067
	 * @param int|Revision $old Old revision or rev ID (first before range by default)
4068
	 * @param int|Revision $new New revision or rev ID (first after range by default)
4069
	 * @param int $limit Maximum number of authors
4070
	 * @param string|array $options (Optional): Single option, or an array of options:
4071
	 *     'include_old' Include $old in the range; $new is excluded.
4072
	 *     'include_new' Include $new in the range; $old is excluded.
4073
	 *     'include_both' Include both $old and $new in the range.
4074
	 *     Unknown option values are ignored.
4075
	 * @return array|null Names of revision authors in the range; null if not both revisions exist
4076
	 */
4077
	public function getAuthorsBetween( $old, $new, $limit, $options = [] ) {
4078
		if ( !( $old instanceof Revision ) ) {
4079
			$old = Revision::newFromTitle( $this, (int)$old );
4080
		}
4081
		if ( !( $new instanceof Revision ) ) {
4082
			$new = Revision::newFromTitle( $this, (int)$new );
4083
		}
4084
		// XXX: what if Revision objects are passed in, but they don't refer to this title?
4085
		// Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID()
4086
		// in the sanity check below?
4087
		if ( !$old || !$new ) {
4088
			return null; // nothing to compare
4089
		}
4090
		$authors = [];
4091
		$old_cmp = '>';
4092
		$new_cmp = '<';
4093
		$options = (array)$options;
4094
		if ( in_array( 'include_old', $options ) ) {
4095
			$old_cmp = '>=';
4096
		}
4097
		if ( in_array( 'include_new', $options ) ) {
4098
			$new_cmp = '<=';
4099
		}
4100
		if ( in_array( 'include_both', $options ) ) {
4101
			$old_cmp = '>=';
4102
			$new_cmp = '<=';
4103
		}
4104
		// No DB query needed if $old and $new are the same or successive revisions:
4105
		if ( $old->getId() === $new->getId() ) {
4106
			return ( $old_cmp === '>' && $new_cmp === '<' ) ?
4107
				[] :
4108
				[ $old->getUserText( Revision::RAW ) ];
4109
		} elseif ( $old->getId() === $new->getParentId() ) {
4110
			if ( $old_cmp === '>=' && $new_cmp === '<=' ) {
4111
				$authors[] = $old->getUserText( Revision::RAW );
4112
				if ( $old->getUserText( Revision::RAW ) != $new->getUserText( Revision::RAW ) ) {
4113
					$authors[] = $new->getUserText( Revision::RAW );
4114
				}
4115
			} elseif ( $old_cmp === '>=' ) {
4116
				$authors[] = $old->getUserText( Revision::RAW );
4117
			} elseif ( $new_cmp === '<=' ) {
4118
				$authors[] = $new->getUserText( Revision::RAW );
4119
			}
4120
			return $authors;
4121
		}
4122
		$dbr = wfGetDB( DB_SLAVE );
4123
		$res = $dbr->select( 'revision', 'DISTINCT rev_user_text',
4124
			[
4125
				'rev_page' => $this->getArticleID(),
4126
				"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...
4127
				"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...
4128
			], __METHOD__,
4129
			[ 'LIMIT' => $limit + 1 ] // add one so caller knows it was truncated
4130
		);
4131
		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...
4132
			$authors[] = $row->rev_user_text;
4133
		}
4134
		return $authors;
4135
	}
4136
4137
	/**
4138
	 * Get the number of authors between the given revisions or revision IDs.
4139
	 * Used for diffs and other things that really need it.
4140
	 *
4141
	 * @param int|Revision $old Old revision or rev ID (first before range by default)
4142
	 * @param int|Revision $new New revision or rev ID (first after range by default)
4143
	 * @param int $limit Maximum number of authors
4144
	 * @param string|array $options (Optional): Single option, or an array of options:
4145
	 *     'include_old' Include $old in the range; $new is excluded.
4146
	 *     'include_new' Include $new in the range; $old is excluded.
4147
	 *     'include_both' Include both $old and $new in the range.
4148
	 *     Unknown option values are ignored.
4149
	 * @return int Number of revision authors in the range; zero if not both revisions exist
4150
	 */
4151
	public function countAuthorsBetween( $old, $new, $limit, $options = [] ) {
4152
		$authors = $this->getAuthorsBetween( $old, $new, $limit, $options );
4153
		return $authors ? count( $authors ) : 0;
4154
	}
4155
4156
	/**
4157
	 * Compare with another title.
4158
	 *
4159
	 * @param Title $title
4160
	 * @return bool
4161
	 */
4162
	public function equals( Title $title ) {
4163
		// Note: === is necessary for proper matching of number-like titles.
4164
		return $this->getInterwiki() === $title->getInterwiki()
4165
			&& $this->getNamespace() == $title->getNamespace()
4166
			&& $this->getDBkey() === $title->getDBkey();
4167
	}
4168
4169
	/**
4170
	 * Check if this title is a subpage of another title
4171
	 *
4172
	 * @param Title $title
4173
	 * @return bool
4174
	 */
4175
	public function isSubpageOf( Title $title ) {
4176
		return $this->getInterwiki() === $title->getInterwiki()
4177
			&& $this->getNamespace() == $title->getNamespace()
4178
			&& strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0;
4179
	}
4180
4181
	/**
4182
	 * Check if page exists.  For historical reasons, this function simply
4183
	 * checks for the existence of the title in the page table, and will
4184
	 * thus return false for interwiki links, special pages and the like.
4185
	 * If you want to know if a title can be meaningfully viewed, you should
4186
	 * probably call the isKnown() method instead.
4187
	 *
4188
	 * @param int $flags An optional bit field; may be Title::GAID_FOR_UPDATE to check
4189
	 *   from master/for update
4190
	 * @return bool
4191
	 */
4192
	public function exists( $flags = 0 ) {
4193
		$exists = $this->getArticleID( $flags ) != 0;
4194
		Hooks::run( 'TitleExists', [ $this, &$exists ] );
4195
		return $exists;
4196
	}
4197
4198
	/**
4199
	 * Should links to this title be shown as potentially viewable (i.e. as
4200
	 * "bluelinks"), even if there's no record by this title in the page
4201
	 * table?
4202
	 *
4203
	 * This function is semi-deprecated for public use, as well as somewhat
4204
	 * misleadingly named.  You probably just want to call isKnown(), which
4205
	 * calls this function internally.
4206
	 *
4207
	 * (ISSUE: Most of these checks are cheap, but the file existence check
4208
	 * can potentially be quite expensive.  Including it here fixes a lot of
4209
	 * existing code, but we might want to add an optional parameter to skip
4210
	 * it and any other expensive checks.)
4211
	 *
4212
	 * @return bool
4213
	 */
4214
	public function isAlwaysKnown() {
4215
		$isKnown = null;
4216
4217
		/**
4218
		 * Allows overriding default behavior for determining if a page exists.
4219
		 * If $isKnown is kept as null, regular checks happen. If it's
4220
		 * a boolean, this value is returned by the isKnown method.
4221
		 *
4222
		 * @since 1.20
4223
		 *
4224
		 * @param Title $title
4225
		 * @param bool|null $isKnown
4226
		 */
4227
		Hooks::run( 'TitleIsAlwaysKnown', [ $this, &$isKnown ] );
4228
4229
		if ( !is_null( $isKnown ) ) {
4230
			return $isKnown;
4231
		}
4232
4233
		if ( $this->isExternal() ) {
4234
			return true;  // any interwiki link might be viewable, for all we know
4235
		}
4236
4237
		switch ( $this->mNamespace ) {
4238
			case NS_MEDIA:
4239
			case NS_FILE:
4240
				// file exists, possibly in a foreign repo
4241
				return (bool)wfFindFile( $this );
4242
			case NS_SPECIAL:
4243
				// valid special page
4244
				return SpecialPageFactory::exists( $this->getDBkey() );
4245
			case NS_MAIN:
4246
				// selflink, possibly with fragment
4247
				return $this->mDbkeyform == '';
4248
			case NS_MEDIAWIKI:
4249
				// known system message
4250
				return $this->hasSourceText() !== false;
4251
			default:
4252
				return false;
4253
		}
4254
	}
4255
4256
	/**
4257
	 * Does this title refer to a page that can (or might) be meaningfully
4258
	 * viewed?  In particular, this function may be used to determine if
4259
	 * links to the title should be rendered as "bluelinks" (as opposed to
4260
	 * "redlinks" to non-existent pages).
4261
	 * Adding something else to this function will cause inconsistency
4262
	 * since LinkHolderArray calls isAlwaysKnown() and does its own
4263
	 * page existence check.
4264
	 *
4265
	 * @return bool
4266
	 */
4267
	public function isKnown() {
4268
		return $this->isAlwaysKnown() || $this->exists();
4269
	}
4270
4271
	/**
4272
	 * Does this page have source text?
4273
	 *
4274
	 * @return bool
4275
	 */
4276
	public function hasSourceText() {
4277
		if ( $this->exists() ) {
4278
			return true;
4279
		}
4280
4281
		if ( $this->mNamespace == NS_MEDIAWIKI ) {
4282
			// If the page doesn't exist but is a known system message, default
4283
			// message content will be displayed, same for language subpages-
4284
			// Use always content language to avoid loading hundreds of languages
4285
			// to get the link color.
4286
			global $wgContLang;
4287
			list( $name, ) = MessageCache::singleton()->figureMessage(
4288
				$wgContLang->lcfirst( $this->getText() )
4289
			);
4290
			$message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false );
4291
			return $message->exists();
4292
		}
4293
4294
		return false;
4295
	}
4296
4297
	/**
4298
	 * Get the default message text or false if the message doesn't exist
4299
	 *
4300
	 * @return string|bool
4301
	 */
4302
	public function getDefaultMessageText() {
4303
		global $wgContLang;
4304
4305
		if ( $this->getNamespace() != NS_MEDIAWIKI ) { // Just in case
4306
			return false;
4307
		}
4308
4309
		list( $name, $lang ) = MessageCache::singleton()->figureMessage(
4310
			$wgContLang->lcfirst( $this->getText() )
4311
		);
4312
		$message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false );
4313
4314
		if ( $message->exists() ) {
4315
			return $message->plain();
4316
		} else {
4317
			return false;
4318
		}
4319
	}
4320
4321
	/**
4322
	 * Updates page_touched for this page; called from LinksUpdate.php
4323
	 *
4324
	 * @param string $purgeTime [optional] TS_MW timestamp
4325
	 * @return bool True if the update succeeded
4326
	 */
4327
	public function invalidateCache( $purgeTime = null ) {
4328
		if ( wfReadOnly() ) {
4329
			return false;
4330
		}
4331
4332
		if ( $this->mArticleID === 0 ) {
4333
			return true; // avoid gap locking if we know it's not there
4334
		}
4335
4336
		$method = __METHOD__;
4337
		$dbw = wfGetDB( DB_MASTER );
4338
		$conds = $this->pageCond();
4339
		$dbw->onTransactionIdle( function () use ( $dbw, $conds, $method, $purgeTime ) {
4340
			$dbTimestamp = $dbw->timestamp( $purgeTime ?: time() );
4341
4342
			$dbw->update(
4343
				'page',
4344
				[ 'page_touched' => $dbTimestamp ],
4345
				$conds + [ 'page_touched < ' . $dbw->addQuotes( $dbTimestamp ) ],
0 ignored issues
show
Security Bug introduced by
It seems like $dbTimestamp defined by $dbw->timestamp($purgeTime ?: time()) on line 4340 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?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
4346
				$method
4347
			);
4348
		} );
4349
4350
		return true;
4351
	}
4352
4353
	/**
4354
	 * Update page_touched timestamps and send CDN purge messages for
4355
	 * pages linking to this title. May be sent to the job queue depending
4356
	 * on the number of links. Typically called on create and delete.
4357
	 */
4358
	public function touchLinks() {
4359
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks' ) );
4360
		if ( $this->getNamespace() == NS_CATEGORY ) {
4361
			DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'categorylinks' ) );
4362
		}
4363
	}
4364
4365
	/**
4366
	 * Get the last touched timestamp
4367
	 *
4368
	 * @param IDatabase $db Optional db
4369
	 * @return string Last-touched timestamp
4370
	 */
4371
	public function getTouched( $db = null ) {
4372
		if ( $db === null ) {
4373
			$db = wfGetDB( DB_SLAVE );
4374
		}
4375
		$touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
4376
		return $touched;
4377
	}
4378
4379
	/**
4380
	 * Get the timestamp when this page was updated since the user last saw it.
4381
	 *
4382
	 * @param User $user
4383
	 * @return string|null
4384
	 */
4385
	public function getNotificationTimestamp( $user = null ) {
4386
		global $wgUser;
4387
4388
		// Assume current user if none given
4389
		if ( !$user ) {
4390
			$user = $wgUser;
4391
		}
4392
		// Check cache first
4393
		$uid = $user->getId();
4394
		if ( !$uid ) {
4395
			return false;
4396
		}
4397
		// avoid isset here, as it'll return false for null entries
4398
		if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
4399
			return $this->mNotificationTimestamp[$uid];
4400
		}
4401
		// Don't cache too much!
4402
		if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
4403
			$this->mNotificationTimestamp = [];
4404
		}
4405
4406
		$store = MediaWikiServices::getInstance()->getWatchedItemStore();
4407
		$watchedItem = $store->getWatchedItem( $user, $this );
4408
		if ( $watchedItem ) {
4409
			$this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp();
4410
		} else {
4411
			$this->mNotificationTimestamp[$uid] = false;
4412
		}
4413
4414
		return $this->mNotificationTimestamp[$uid];
4415
	}
4416
4417
	/**
4418
	 * Generate strings used for xml 'id' names in monobook tabs
4419
	 *
4420
	 * @param string $prepend Defaults to 'nstab-'
4421
	 * @return string XML 'id' name
4422
	 */
4423
	public function getNamespaceKey( $prepend = 'nstab-' ) {
4424
		global $wgContLang;
4425
		// Gets the subject namespace if this title
4426
		$namespace = MWNamespace::getSubject( $this->getNamespace() );
4427
		// Checks if canonical namespace name exists for namespace
4428
		if ( MWNamespace::exists( $this->getNamespace() ) ) {
4429
			// Uses canonical namespace name
4430
			$namespaceKey = MWNamespace::getCanonicalName( $namespace );
4431
		} else {
4432
			// Uses text of namespace
4433
			$namespaceKey = $this->getSubjectNsText();
4434
		}
4435
		// Makes namespace key lowercase
4436
		$namespaceKey = $wgContLang->lc( $namespaceKey );
4437
		// Uses main
4438
		if ( $namespaceKey == '' ) {
4439
			$namespaceKey = 'main';
4440
		}
4441
		// Changes file to image for backwards compatibility
4442
		if ( $namespaceKey == 'file' ) {
4443
			$namespaceKey = 'image';
4444
		}
4445
		return $prepend . $namespaceKey;
4446
	}
4447
4448
	/**
4449
	 * Get all extant redirects to this Title
4450
	 *
4451
	 * @param int|null $ns Single namespace to consider; null to consider all namespaces
4452
	 * @return Title[] Array of Title redirects to this title
4453
	 */
4454
	public function getRedirectsHere( $ns = null ) {
4455
		$redirs = [];
4456
4457
		$dbr = wfGetDB( DB_SLAVE );
4458
		$where = [
4459
			'rd_namespace' => $this->getNamespace(),
4460
			'rd_title' => $this->getDBkey(),
4461
			'rd_from = page_id'
4462
		];
4463
		if ( $this->isExternal() ) {
4464
			$where['rd_interwiki'] = $this->getInterwiki();
4465
		} else {
4466
			$where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL';
4467
		}
4468
		if ( !is_null( $ns ) ) {
4469
			$where['page_namespace'] = $ns;
4470
		}
4471
4472
		$res = $dbr->select(
4473
			[ 'redirect', 'page' ],
4474
			[ 'page_namespace', 'page_title' ],
4475
			$where,
4476
			__METHOD__
4477
		);
4478
4479
		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...
4480
			$redirs[] = self::newFromRow( $row );
4481
		}
4482
		return $redirs;
4483
	}
4484
4485
	/**
4486
	 * Check if this Title is a valid redirect target
4487
	 *
4488
	 * @return bool
4489
	 */
4490
	public function isValidRedirectTarget() {
4491
		global $wgInvalidRedirectTargets;
4492
4493
		if ( $this->isSpecialPage() ) {
4494
			// invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
4495
			if ( $this->isSpecial( 'Userlogout' ) ) {
4496
				return false;
4497
			}
4498
4499
			foreach ( $wgInvalidRedirectTargets as $target ) {
4500
				if ( $this->isSpecial( $target ) ) {
4501
					return false;
4502
				}
4503
			}
4504
		}
4505
4506
		return true;
4507
	}
4508
4509
	/**
4510
	 * Get a backlink cache object
4511
	 *
4512
	 * @return BacklinkCache
4513
	 */
4514
	public function getBacklinkCache() {
4515
		return BacklinkCache::get( $this );
4516
	}
4517
4518
	/**
4519
	 * Whether the magic words __INDEX__ and __NOINDEX__ function for  this page.
4520
	 *
4521
	 * @return bool
4522
	 */
4523
	public function canUseNoindex() {
4524
		global $wgContentNamespaces, $wgExemptFromUserRobotsControl;
4525
4526
		$bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
4527
			? $wgContentNamespaces
4528
			: $wgExemptFromUserRobotsControl;
4529
4530
		return !in_array( $this->mNamespace, $bannedNamespaces );
4531
4532
	}
4533
4534
	/**
4535
	 * Returns the raw sort key to be used for categories, with the specified
4536
	 * prefix.  This will be fed to Collation::getSortKey() to get a
4537
	 * binary sortkey that can be used for actual sorting.
4538
	 *
4539
	 * @param string $prefix The prefix to be used, specified using
4540
	 *   {{defaultsort:}} or like [[Category:Foo|prefix]].  Empty for no
4541
	 *   prefix.
4542
	 * @return string
4543
	 */
4544
	public function getCategorySortkey( $prefix = '' ) {
4545
		$unprefixed = $this->getText();
4546
4547
		// Anything that uses this hook should only depend
4548
		// on the Title object passed in, and should probably
4549
		// tell the users to run updateCollations.php --force
4550
		// in order to re-sort existing category relations.
4551
		Hooks::run( 'GetDefaultSortkey', [ $this, &$unprefixed ] );
4552
		if ( $prefix !== '' ) {
4553
			# Separate with a line feed, so the unprefixed part is only used as
4554
			# a tiebreaker when two pages have the exact same prefix.
4555
			# In UCA, tab is the only character that can sort above LF
4556
			# so we strip both of them from the original prefix.
4557
			$prefix = strtr( $prefix, "\n\t", '  ' );
4558
			return "$prefix\n$unprefixed";
4559
		}
4560
		return $unprefixed;
4561
	}
4562
4563
	/**
4564
	 * Returns the page language code saved in the database, if $wgPageLanguageUseDB is set
4565
	 * to true in LocalSettings.php, otherwise returns false. If there is no language saved in
4566
	 * the db, it will return NULL.
4567
	 *
4568
	 * @return string|null|bool
4569
	 */
4570
	private function getDbPageLanguageCode() {
4571
		global $wgPageLanguageUseDB;
4572
4573
		// check, if the page language could be saved in the database, and if so and
4574
		// the value is not requested already, lookup the page language using LinkCache
4575 View Code Duplication
		if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) {
4576
			$linkCache = LinkCache::singleton();
4577
			$linkCache->addLinkObj( $this );
4578
			$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...
4579
		}
4580
4581
		return $this->mDbPageLanguage;
4582
	}
4583
4584
	/**
4585
	 * Get the language in which the content of this page is written in
4586
	 * wikitext. Defaults to $wgContLang, but in certain cases it can be
4587
	 * e.g. $wgLang (such as special pages, which are in the user language).
4588
	 *
4589
	 * @since 1.18
4590
	 * @return Language
4591
	 */
4592
	public function getPageLanguage() {
4593
		global $wgLang, $wgLanguageCode;
4594
		if ( $this->isSpecialPage() ) {
4595
			// special pages are in the user language
4596
			return $wgLang;
4597
		}
4598
4599
		// Checking if DB language is set
4600
		$dbPageLanguage = $this->getDbPageLanguageCode();
4601
		if ( $dbPageLanguage ) {
4602
			return wfGetLangObj( $dbPageLanguage );
4603
		}
4604
4605
		if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) {
4606
			// Note that this may depend on user settings, so the cache should
4607
			// be only per-request.
4608
			// NOTE: ContentHandler::getPageLanguage() may need to load the
4609
			// content to determine the page language!
4610
			// Checking $wgLanguageCode hasn't changed for the benefit of unit
4611
			// tests.
4612
			$contentHandler = ContentHandler::getForTitle( $this );
4613
			$langObj = $contentHandler->getPageLanguage( $this );
4614
			$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...
4615
		} else {
4616
			$langObj = wfGetLangObj( $this->mPageLanguage[0] );
4617
		}
4618
4619
		return $langObj;
4620
	}
4621
4622
	/**
4623
	 * Get the language in which the content of this page is written when
4624
	 * viewed by user. Defaults to $wgContLang, but in certain cases it can be
4625
	 * e.g. $wgLang (such as special pages, which are in the user language).
4626
	 *
4627
	 * @since 1.20
4628
	 * @return Language
4629
	 */
4630
	public function getPageViewLanguage() {
4631
		global $wgLang;
4632
4633
		if ( $this->isSpecialPage() ) {
4634
			// If the user chooses a variant, the content is actually
4635
			// in a language whose code is the variant code.
4636
			$variant = $wgLang->getPreferredVariant();
4637
			if ( $wgLang->getCode() !== $variant ) {
4638
				return Language::factory( $variant );
4639
			}
4640
4641
			return $wgLang;
4642
		}
4643
4644
		// Checking if DB language is set
4645
		$dbPageLanguage = $this->getDbPageLanguageCode();
4646
		if ( $dbPageLanguage ) {
4647
			$pageLang = wfGetLangObj( $dbPageLanguage );
4648
			$variant = $pageLang->getPreferredVariant();
4649
			if ( $pageLang->getCode() !== $variant ) {
4650
				$pageLang = Language::factory( $variant );
4651
			}
4652
4653
			return $pageLang;
4654
		}
4655
4656
		// @note Can't be cached persistently, depends on user settings.
4657
		// @note ContentHandler::getPageViewLanguage() may need to load the
4658
		//   content to determine the page language!
4659
		$contentHandler = ContentHandler::getForTitle( $this );
4660
		$pageLang = $contentHandler->getPageViewLanguage( $this );
4661
		return $pageLang;
4662
	}
4663
4664
	/**
4665
	 * Get a list of rendered edit notices for this page.
4666
	 *
4667
	 * Array is keyed by the original message key, and values are rendered using parseAsBlock, so
4668
	 * they will already be wrapped in paragraphs.
4669
	 *
4670
	 * @since 1.21
4671
	 * @param int $oldid Revision ID that's being edited
4672
	 * @return array
4673
	 */
4674
	public function getEditNotices( $oldid = 0 ) {
4675
		$notices = [];
4676
4677
		// Optional notice for the entire namespace
4678
		$editnotice_ns = 'editnotice-' . $this->getNamespace();
4679
		$msg = wfMessage( $editnotice_ns );
4680 View Code Duplication
		if ( $msg->exists() ) {
4681
			$html = $msg->parseAsBlock();
4682
			// Edit notices may have complex logic, but output nothing (T91715)
4683
			if ( trim( $html ) !== '' ) {
4684
				$notices[$editnotice_ns] = Html::rawElement(
4685
					'div',
4686
					[ 'class' => [
4687
						'mw-editnotice',
4688
						'mw-editnotice-namespace',
4689
						Sanitizer::escapeClass( "mw-$editnotice_ns" )
4690
					] ],
4691
					$html
4692
				);
4693
			}
4694
		}
4695
4696
		if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) {
4697
			// Optional notice for page itself and any parent page
4698
			$parts = explode( '/', $this->getDBkey() );
4699
			$editnotice_base = $editnotice_ns;
4700
			while ( count( $parts ) > 0 ) {
4701
				$editnotice_base .= '-' . array_shift( $parts );
4702
				$msg = wfMessage( $editnotice_base );
4703 View Code Duplication
				if ( $msg->exists() ) {
4704
					$html = $msg->parseAsBlock();
4705
					if ( trim( $html ) !== '' ) {
4706
						$notices[$editnotice_base] = Html::rawElement(
4707
							'div',
4708
							[ 'class' => [
4709
								'mw-editnotice',
4710
								'mw-editnotice-base',
4711
								Sanitizer::escapeClass( "mw-$editnotice_base" )
4712
							] ],
4713
							$html
4714
						);
4715
					}
4716
				}
4717
			}
4718
		} else {
4719
			// Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys
4720
			$editnoticeText = $editnotice_ns . '-' . strtr( $this->getDBkey(), '/', '-' );
4721
			$msg = wfMessage( $editnoticeText );
4722 View Code Duplication
			if ( $msg->exists() ) {
4723
				$html = $msg->parseAsBlock();
4724
				if ( trim( $html ) !== '' ) {
4725
					$notices[$editnoticeText] = Html::rawElement(
4726
						'div',
4727
						[ 'class' => [
4728
							'mw-editnotice',
4729
							'mw-editnotice-page',
4730
							Sanitizer::escapeClass( "mw-$editnoticeText" )
4731
						] ],
4732
						$html
4733
					);
4734
				}
4735
			}
4736
		}
4737
4738
		Hooks::run( 'TitleGetEditNotices', [ $this, $oldid, &$notices ] );
4739
		return $notices;
4740
	}
4741
4742
	/**
4743
	 * @return array
4744
	 */
4745
	public function __sleep() {
4746
		return [
4747
			'mNamespace',
4748
			'mDbkeyform',
4749
			'mFragment',
4750
			'mInterwiki',
4751
			'mLocalInterwiki',
4752
			'mUserCaseDBKey',
4753
			'mDefaultNamespace',
4754
		];
4755
	}
4756
4757
	public function __wakeup() {
4758
		$this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0;
4759
		$this->mUrlform = wfUrlencode( $this->mDbkeyform );
4760
		$this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
4761
	}
4762
4763
}
4764