Completed
Branch master (54277f)
by
unknown
24:54
created

Title::newFromRedirectArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 6
rs 9.4285
cc 1
eloc 4
nc 1
nop 1

1 Method

Rating   Name   Duplication   Size   Complexity  
A Title::legalChars() 0 4 1
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
25
/**
26
 * Represents a title within MediaWiki.
27
 * Optionally may contain an interwiki designation or namespace.
28
 * @note This class can fetch various kinds of data from the database;
29
 *       however, it does so inefficiently.
30
 * @note Consider using a TitleValue object instead. TitleValue is more lightweight
31
 *       and does not rely on global state or the database.
32
 */
33
class Title implements LinkTarget {
34
	/** @var HashBagOStuff */
35
	static private $titleCache = null;
36
37
	/**
38
	 * Title::newFromText maintains a cache to avoid expensive re-normalization of
39
	 * commonly used titles. On a batch operation this can become a memory leak
40
	 * if not bounded. After hitting this many titles reset the cache.
41
	 */
42
	const CACHE_MAX = 1000;
43
44
	/**
45
	 * Used to be GAID_FOR_UPDATE define. Used with getArticleID() and friends
46
	 * to use the master DB
47
	 */
48
	const GAID_FOR_UPDATE = 1;
49
50
	/**
51
	 * @name Private member variables
52
	 * Please use the accessor functions instead.
53
	 * @private
54
	 */
55
	// @{
56
57
	/** @var string Text form (spaces not underscores) of the main part */
58
	public $mTextform = '';
59
60
	/** @var string URL-encoded form of the main part */
61
	public $mUrlform = '';
62
63
	/** @var string Main part with underscores */
64
	public $mDbkeyform = '';
65
66
	/** @var string Database key with the initial letter in the case specified by the user */
67
	protected $mUserCaseDBKey;
68
69
	/** @var int Namespace index, i.e. one of the NS_xxxx constants */
70
	public $mNamespace = NS_MAIN;
71
72
	/** @var string Interwiki prefix */
73
	public $mInterwiki = '';
74
75
	/** @var bool Was this Title created from a string with a local interwiki prefix? */
76
	private $mLocalInterwiki = false;
77
78
	/** @var string Title fragment (i.e. the bit after the #) */
79
	public $mFragment = '';
80
81
	/** @var int Article ID, fetched from the link cache on demand */
82
	public $mArticleID = -1;
83
84
	/** @var bool|int ID of most recent revision */
85
	protected $mLatestID = false;
86
87
	/**
88
	 * @var bool|string ID of the page's content model, i.e. one of the
89
	 *   CONTENT_MODEL_XXX constants
90
	 */
91
	public $mContentModel = false;
92
93
	/** @var int Estimated number of revisions; null of not loaded */
94
	private $mEstimateRevisions;
95
96
	/** @var array Array of groups allowed to edit this article */
97
	public $mRestrictions = [];
98
99
	/** @var string|bool */
100
	protected $mOldRestrictions = false;
101
102
	/** @var bool Cascade restrictions on this page to included templates and images? */
103
	public $mCascadeRestriction;
104
105
	/** Caching the results of getCascadeProtectionSources */
106
	public $mCascadingRestrictions;
107
108
	/** @var array When do the restrictions on this page expire? */
109
	protected $mRestrictionsExpiry = [];
110
111
	/** @var bool Are cascading restrictions in effect on this page? */
112
	protected $mHasCascadingRestrictions;
113
114
	/** @var array Where are the cascading restrictions coming from on this page? */
115
	public $mCascadeSources;
116
117
	/** @var bool Boolean for initialisation on demand */
118
	public $mRestrictionsLoaded = false;
119
120
	/** @var string Text form including namespace/interwiki, initialised on demand */
121
	protected $mPrefixedText = null;
122
123
	/** @var mixed Cached value for getTitleProtection (create protection) */
124
	public $mTitleProtection;
125
126
	/**
127
	 * @var int Namespace index when there is no namespace. Don't change the
128
	 *   following default, NS_MAIN is hardcoded in several places. See bug 696.
129
	 *   Zero except in {{transclusion}} tags.
130
	 */
131
	public $mDefaultNamespace = NS_MAIN;
132
133
	/** @var int The page length, 0 for special pages */
134
	protected $mLength = -1;
135
136
	/** @var null Is the article at this title a redirect? */
137
	public $mRedirect = null;
138
139
	/** @var array Associative array of user ID -> timestamp/false */
140
	private $mNotificationTimestamp = [];
141
142
	/** @var bool Whether a page has any subpages */
143
	private $mHasSubpages;
144
145
	/** @var bool The (string) language code of the page's language and content code. */
146
	private $mPageLanguage = false;
147
148
	/** @var string|bool|null The page language code from the database, null if not saved in
149
	 * the database or false if not loaded, yet. */
150
	private $mDbPageLanguage = false;
151
152
	/** @var TitleValue A corresponding TitleValue object */
153
	private $mTitleValue = null;
154
155
	/** @var bool Would deleting this page be a big deletion? */
156
	private $mIsBigDeletion = null;
157
	// @}
158
159
	/**
160
	 * B/C kludge: provide a TitleParser for use by Title.
161
	 * Ideally, Title would have no methods that need this.
162
	 * Avoid usage of this singleton by using TitleValue
163
	 * and the associated services when possible.
164
	 *
165
	 * @return MediaWikiTitleCodec
166
	 */
167
	private static function getMediaWikiTitleCodec() {
168
		global $wgContLang, $wgLocalInterwikis;
169
170
		static $titleCodec = null;
171
		static $titleCodecFingerprint = null;
172
173
		// $wgContLang and $wgLocalInterwikis may change (especially while testing),
174
		// make sure we are using the right one. To detect changes over the course
175
		// of a request, we remember a fingerprint of the config used to create the
176
		// codec singleton, and re-create it if the fingerprint doesn't match.
177
		$fingerprint = spl_object_hash( $wgContLang ) . '|' . implode( '+', $wgLocalInterwikis );
178
179
		if ( $fingerprint !== $titleCodecFingerprint ) {
180
			$titleCodec = null;
181
		}
182
183
		if ( !$titleCodec ) {
184
			$titleCodec = new MediaWikiTitleCodec(
185
				$wgContLang,
186
				GenderCache::singleton(),
0 ignored issues
show
Bug introduced by
It seems like \GenderCache::singleton() can be null; however, __construct() 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...
187
				$wgLocalInterwikis
188
			);
189
			$titleCodecFingerprint = $fingerprint;
190
		}
191
192
		return $titleCodec;
193
	}
194
195
	/**
196
	 * B/C kludge: provide a TitleParser for use by Title.
197
	 * Ideally, Title would have no methods that need this.
198
	 * Avoid usage of this singleton by using TitleValue
199
	 * and the associated services when possible.
200
	 *
201
	 * @return TitleFormatter
202
	 */
203
	private static function getTitleFormatter() {
204
		// NOTE: we know that getMediaWikiTitleCodec() returns a MediaWikiTitleCodec,
205
		//      which implements TitleFormatter.
206
		return self::getMediaWikiTitleCodec();
207
	}
208
209
	function __construct() {
210
	}
211
212
	/**
213
	 * Create a new Title from a prefixed DB key
214
	 *
215
	 * @param string $key The database key, which has underscores
216
	 *	instead of spaces, possibly including namespace and
217
	 *	interwiki prefixes
218
	 * @return Title|null Title, or null on an error
219
	 */
220
	public static function newFromDBkey( $key ) {
221
		$t = new Title();
222
		$t->mDbkeyform = $key;
223
224
		try {
225
			$t->secureAndSplit();
226
			return $t;
227
		} catch ( MalformedTitleException $ex ) {
228
			return null;
229
		}
230
	}
231
232
	/**
233
	 * Create a new Title from a TitleValue
234
	 *
235
	 * @param TitleValue $titleValue Assumed to be safe.
236
	 *
237
	 * @return Title
238
	 */
239
	public static function newFromTitleValue( TitleValue $titleValue ) {
240
		return self::newFromLinkTarget( $titleValue );
241
	}
242
243
	/**
244
	 * Create a new Title from a LinkTarget
245
	 *
246
	 * @param LinkTarget $linkTarget Assumed to be safe.
247
	 *
248
	 * @return Title
249
	 */
250
	public static function newFromLinkTarget( LinkTarget $linkTarget ) {
251
		return self::makeTitle(
252
			$linkTarget->getNamespace(),
253
			$linkTarget->getText(),
254
			$linkTarget->getFragment() );
255
	}
256
257
	/**
258
	 * Create a new Title from text, such as what one would find in a link. De-
259
	 * codes any HTML entities in the text.
260
	 *
261
	 * @param string|int|null $text The link text; spaces, prefixes, and an
262
	 *   initial ':' indicating the main namespace are accepted.
263
	 * @param int $defaultNamespace The namespace to use if none is specified
264
	 *   by a prefix.  If you want to force a specific namespace even if
265
	 *   $text might begin with a namespace prefix, use makeTitle() or
266
	 *   makeTitleSafe().
267
	 * @throws InvalidArgumentException
268
	 * @return Title|null Title or null on an error.
269
	 */
270
	public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
271
		if ( is_object( $text ) ) {
272
			throw new InvalidArgumentException( '$text must be a string.' );
273
		}
274
		// DWIM: Integers can be passed in here when page titles are used as array keys.
275
		if ( $text !== null && !is_string( $text ) && !is_int( $text ) ) {
276
			wfDebugLog( 'T76305', wfGetAllCallers( 5 ) );
277
			return null;
278
		}
279
		if ( $text === null ) {
280
			return null;
281
		}
282
283
		try {
284
			return Title::newFromTextThrow( strval( $text ), $defaultNamespace );
285
		} catch ( MalformedTitleException $ex ) {
286
			return null;
287
		}
288
	}
289
290
	/**
291
	 * Like Title::newFromText(), but throws MalformedTitleException when the title is invalid,
292
	 * rather than returning null.
293
	 *
294
	 * The exception subclasses encode detailed information about why the title is invalid.
295
	 *
296
	 * @see Title::newFromText
297
	 *
298
	 * @since 1.25
299
	 * @param string $text Title text to check
300
	 * @param int $defaultNamespace
301
	 * @throws MalformedTitleException If the title is invalid
302
	 * @return Title
303
	 */
304
	public static function newFromTextThrow( $text, $defaultNamespace = NS_MAIN ) {
305
		if ( is_object( $text ) ) {
306
			throw new MWException( '$text must be a string, given an object' );
307
		}
308
309
		$titleCache = self::getTitleCache();
310
311
		// Wiki pages often contain multiple links to the same page.
312
		// Title normalization and parsing can become expensive on pages with many
313
		// links, so we can save a little time by caching them.
314
		// In theory these are value objects and won't get changed...
315
		if ( $defaultNamespace == NS_MAIN ) {
316
			$t = $titleCache->get( $text );
317
			if ( $t ) {
318
				return $t;
319
			}
320
		}
321
322
		// Convert things like &eacute; &#257; or &#x3017; into normalized (bug 14952) text
323
		$filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
324
325
		$t = new Title();
326
		$t->mDbkeyform = strtr( $filteredText, ' ', '_' );
327
		$t->mDefaultNamespace = intval( $defaultNamespace );
328
329
		$t->secureAndSplit();
330
		if ( $defaultNamespace == NS_MAIN ) {
331
			$titleCache->set( $text, $t );
332
		}
333
		return $t;
334
	}
335
336
	/**
337
	 * THIS IS NOT THE FUNCTION YOU WANT. Use Title::newFromText().
338
	 *
339
	 * Example of wrong and broken code:
340
	 * $title = Title::newFromURL( $wgRequest->getVal( 'title' ) );
341
	 *
342
	 * Example of right code:
343
	 * $title = Title::newFromText( $wgRequest->getVal( 'title' ) );
344
	 *
345
	 * Create a new Title from URL-encoded text. Ensures that
346
	 * the given title's length does not exceed the maximum.
347
	 *
348
	 * @param string $url The title, as might be taken from a URL
349
	 * @return Title|null The new object, or null on an error
350
	 */
351
	public static function newFromURL( $url ) {
352
		$t = new Title();
353
354
		# For compatibility with old buggy URLs. "+" is usually not valid in titles,
355
		# but some URLs used it as a space replacement and they still come
356
		# from some external search tools.
357
		if ( strpos( self::legalChars(), '+' ) === false ) {
358
			$url = strtr( $url, '+', ' ' );
359
		}
360
361
		$t->mDbkeyform = strtr( $url, ' ', '_' );
362
363
		try {
364
			$t->secureAndSplit();
365
			return $t;
366
		} catch ( MalformedTitleException $ex ) {
367
			return null;
368
		}
369
	}
370
371
	/**
372
	 * @return HashBagOStuff
373
	 */
374
	private static function getTitleCache() {
375
		if ( self::$titleCache == null ) {
376
			self::$titleCache = new HashBagOStuff( [ 'maxKeys' => self::CACHE_MAX ] );
377
		}
378
		return self::$titleCache;
379
	}
380
381
	/**
382
	 * Returns a list of fields that are to be selected for initializing Title
383
	 * objects or LinkCache entries. Uses $wgContentHandlerUseDB to determine
384
	 * whether to include page_content_model.
385
	 *
386
	 * @return array
387
	 */
388
	protected static function getSelectFields() {
389
		global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
390
391
		$fields = [
392
			'page_namespace', 'page_title', 'page_id',
393
			'page_len', 'page_is_redirect', 'page_latest',
394
		];
395
396
		if ( $wgContentHandlerUseDB ) {
397
			$fields[] = 'page_content_model';
398
		}
399
400
		if ( $wgPageLanguageUseDB ) {
401
			$fields[] = 'page_lang';
402
		}
403
404
		return $fields;
405
	}
406
407
	/**
408
	 * Create a new Title from an article ID
409
	 *
410
	 * @param int $id The page_id corresponding to the Title to create
411
	 * @param int $flags Use Title::GAID_FOR_UPDATE to use master
412
	 * @return Title|null The new object, or null on an error
413
	 */
414
	public static function newFromID( $id, $flags = 0 ) {
415
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
416
		$row = $db->selectRow(
417
			'page',
418
			self::getSelectFields(),
419
			[ 'page_id' => $id ],
420
			__METHOD__
421
		);
422
		if ( $row !== false ) {
423
			$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 416 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...
424
		} else {
425
			$title = null;
426
		}
427
		return $title;
428
	}
429
430
	/**
431
	 * Make an array of titles from an array of IDs
432
	 *
433
	 * @param int[] $ids Array of IDs
434
	 * @return Title[] Array of Titles
435
	 */
436
	public static function newFromIDs( $ids ) {
437
		if ( !count( $ids ) ) {
438
			return [];
439
		}
440
		$dbr = wfGetDB( DB_SLAVE );
441
442
		$res = $dbr->select(
443
			'page',
444
			self::getSelectFields(),
445
			[ 'page_id' => $ids ],
446
			__METHOD__
447
		);
448
449
		$titles = [];
450
		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...
451
			$titles[] = Title::newFromRow( $row );
452
		}
453
		return $titles;
454
	}
455
456
	/**
457
	 * Make a Title object from a DB row
458
	 *
459
	 * @param stdClass $row Object database row (needs at least page_title,page_namespace)
460
	 * @return Title Corresponding Title
461
	 */
462
	public static function newFromRow( $row ) {
463
		$t = self::makeTitle( $row->page_namespace, $row->page_title );
464
		$t->loadFromRow( $row );
465
		return $t;
466
	}
467
468
	/**
469
	 * Load Title object fields from a DB row.
470
	 * If false is given, the title will be treated as non-existing.
471
	 *
472
	 * @param stdClass|bool $row Database row
473
	 */
474
	public function loadFromRow( $row ) {
475
		if ( $row ) { // page found
476
			if ( isset( $row->page_id ) ) {
477
				$this->mArticleID = (int)$row->page_id;
478
			}
479
			if ( isset( $row->page_len ) ) {
480
				$this->mLength = (int)$row->page_len;
481
			}
482
			if ( isset( $row->page_is_redirect ) ) {
483
				$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...
484
			}
485
			if ( isset( $row->page_latest ) ) {
486
				$this->mLatestID = (int)$row->page_latest;
487
			}
488
			if ( isset( $row->page_content_model ) ) {
489
				$this->mContentModel = strval( $row->page_content_model );
490
			} else {
491
				$this->mContentModel = false; # initialized lazily in getContentModel()
492
			}
493
			if ( isset( $row->page_lang ) ) {
494
				$this->mDbPageLanguage = (string)$row->page_lang;
495
			}
496
			if ( isset( $row->page_restrictions ) ) {
497
				$this->mOldRestrictions = $row->page_restrictions;
498
			}
499
		} else { // page not found
500
			$this->mArticleID = 0;
501
			$this->mLength = 0;
502
			$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...
503
			$this->mLatestID = 0;
504
			$this->mContentModel = false; # initialized lazily in getContentModel()
505
		}
506
	}
507
508
	/**
509
	 * Create a new Title from a namespace index and a DB key.
510
	 * It's assumed that $ns and $title are *valid*, for instance when
511
	 * they came directly from the database or a special page name.
512
	 * For convenience, spaces are converted to underscores so that
513
	 * eg user_text fields can be used directly.
514
	 *
515
	 * @param int $ns The namespace of the article
516
	 * @param string $title The unprefixed database key form
517
	 * @param string $fragment The link fragment (after the "#")
518
	 * @param string $interwiki The interwiki prefix
519
	 * @return Title The new object
520
	 */
521
	public static function &makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) {
522
		$t = new Title();
523
		$t->mInterwiki = $interwiki;
524
		$t->mFragment = $fragment;
525
		$t->mNamespace = $ns = intval( $ns );
526
		$t->mDbkeyform = strtr( $title, ' ', '_' );
527
		$t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
528
		$t->mUrlform = wfUrlencode( $t->mDbkeyform );
529
		$t->mTextform = strtr( $title, '_', ' ' );
530
		$t->mContentModel = false; # initialized lazily in getContentModel()
531
		return $t;
532
	}
533
534
	/**
535
	 * Create a new Title from a namespace index and a DB key.
536
	 * The parameters will be checked for validity, which is a bit slower
537
	 * than makeTitle() but safer for user-provided data.
538
	 *
539
	 * @param int $ns The namespace of the article
540
	 * @param string $title Database key form
541
	 * @param string $fragment The link fragment (after the "#")
542
	 * @param string $interwiki Interwiki prefix
543
	 * @return Title|null The new object, or null on an error
544
	 */
545
	public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
546
		if ( !MWNamespace::exists( $ns ) ) {
547
			return null;
548
		}
549
550
		$t = new Title();
551
		$t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki, true );
552
553
		try {
554
			$t->secureAndSplit();
555
			return $t;
556
		} catch ( MalformedTitleException $ex ) {
557
			return null;
558
		}
559
	}
560
561
	/**
562
	 * Create a new Title for the Main Page
563
	 *
564
	 * @return Title The new object
565
	 */
566
	public static function newMainPage() {
567
		$title = Title::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() );
568
		// Don't give fatal errors if the message is broken
569
		if ( !$title ) {
570
			$title = Title::newFromText( 'Main Page' );
571
		}
572
		return $title;
573
	}
574
575
	/**
576
	 * Extract a redirect destination from a string and return the
577
	 * Title, or null if the text doesn't contain a valid redirect
578
	 * This will recurse down $wgMaxRedirects times or until a non-redirect target is hit
579
	 * in order to provide (hopefully) the Title of the final destination instead of another redirect
580
	 *
581
	 * @param string $text Text with possible redirect
582
	 * @return Title
583
	 * @deprecated since 1.21, use Content::getUltimateRedirectTarget instead.
584
	 */
585
	public static function newFromRedirectRecurse( $text ) {
586
		ContentHandler::deprecated( __METHOD__, '1.21' );
587
588
		$content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
589
		return $content->getUltimateRedirectTarget();
590
	}
591
592
	/**
593
	 * Get the prefixed DB key associated with an ID
594
	 *
595
	 * @param int $id The page_id of the article
596
	 * @return Title|null An object representing the article, or null if no such article was found
597
	 */
598
	public static function nameOf( $id ) {
599
		$dbr = wfGetDB( DB_SLAVE );
600
601
		$s = $dbr->selectRow(
602
			'page',
603
			[ 'page_namespace', 'page_title' ],
604
			[ 'page_id' => $id ],
605
			__METHOD__
606
		);
607
		if ( $s === false ) {
608
			return null;
609
		}
610
611
		$n = self::makeName( $s->page_namespace, $s->page_title );
612
		return $n;
613
	}
614
615
	/**
616
	 * Get a regex character class describing the legal characters in a link
617
	 *
618
	 * @return string The list of characters, not delimited
619
	 */
620
	public static function legalChars() {
621
		global $wgLegalTitleChars;
622
		return $wgLegalTitleChars;
623
	}
624
625
	/**
626
	 * Returns a simple regex that will match on characters and sequences invalid in titles.
627
	 * Note that this doesn't pick up many things that could be wrong with titles, but that
628
	 * replacing this regex with something valid will make many titles valid.
629
	 *
630
	 * @deprecated since 1.25, use MediaWikiTitleCodec::getTitleInvalidRegex() instead
631
	 *
632
	 * @return string Regex string
633
	 */
634
	static function getTitleInvalidRegex() {
635
		wfDeprecated( __METHOD__, '1.25' );
636
		return MediaWikiTitleCodec::getTitleInvalidRegex();
637
	}
638
639
	/**
640
	 * Utility method for converting a character sequence from bytes to Unicode.
641
	 *
642
	 * Primary usecase being converting $wgLegalTitleChars to a sequence usable in
643
	 * javascript, as PHP uses UTF-8 bytes where javascript uses Unicode code units.
644
	 *
645
	 * @param string $byteClass
646
	 * @return string
647
	 */
648
	public static function convertByteClassToUnicodeClass( $byteClass ) {
649
		$length = strlen( $byteClass );
650
		// Input token queue
651
		$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...
652
		// Decoded queue
653
		$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...
654
		// Decoded integer codepoints
655
		$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...
656
		// Re-encoded queue
657
		$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...
658
		// Output
659
		$out = '';
660
		// Flags
661
		$allowUnicode = false;
662
		for ( $pos = 0; $pos < $length; $pos++ ) {
663
			// Shift the queues down
664
			$x2 = $x1;
665
			$x1 = $x0;
666
			$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...
667
			$d1 = $d0;
668
			$ord2 = $ord1;
669
			$ord1 = $ord0;
670
			$r2 = $r1;
671
			$r1 = $r0;
672
			// Load the current input token and decoded values
673
			$inChar = $byteClass[$pos];
674
			if ( $inChar == '\\' ) {
675
				if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) {
676
					$x0 = $inChar . $m[0];
677
					$d0 = chr( hexdec( $m[1] ) );
678
					$pos += strlen( $m[0] );
679
				} elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) {
680
					$x0 = $inChar . $m[0];
681
					$d0 = chr( octdec( $m[0] ) );
682
					$pos += strlen( $m[0] );
683
				} elseif ( $pos + 1 >= $length ) {
684
					$x0 = $d0 = '\\';
685
				} else {
686
					$d0 = $byteClass[$pos + 1];
687
					$x0 = $inChar . $d0;
688
					$pos += 1;
689
				}
690
			} else {
691
				$x0 = $d0 = $inChar;
692
			}
693
			$ord0 = ord( $d0 );
694
			// Load the current re-encoded value
695
			if ( $ord0 < 32 || $ord0 == 0x7f ) {
696
				$r0 = sprintf( '\x%02x', $ord0 );
697
			} elseif ( $ord0 >= 0x80 ) {
698
				// Allow unicode if a single high-bit character appears
699
				$r0 = sprintf( '\x%02x', $ord0 );
700
				$allowUnicode = true;
701
			} elseif ( strpos( '-\\[]^', $d0 ) !== false ) {
702
				$r0 = '\\' . $d0;
703
			} else {
704
				$r0 = $d0;
705
			}
706
			// Do the output
707
			if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) {
708
				// Range
709
				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...
710
					// Empty range
711
				} elseif ( $ord0 >= 0x80 ) {
712
					// Unicode range
713
					$allowUnicode = true;
714
					if ( $ord2 < 0x80 ) {
715
						// Keep the non-unicode section of the range
716
						$out .= "$r2-\\x7F";
717
					}
718
				} else {
719
					// Normal range
720
					$out .= "$r2-$r0";
721
				}
722
				// Reset state to the initial value
723
				$x0 = $x1 = $d0 = $d1 = $r0 = $r1 = '';
724
			} elseif ( $ord2 < 0x80 ) {
725
				// ASCII character
726
				$out .= $r2;
727
			}
728
		}
729
		if ( $ord1 < 0x80 ) {
730
			$out .= $r1;
731
		}
732
		if ( $ord0 < 0x80 ) {
733
			$out .= $r0;
734
		}
735
		if ( $allowUnicode ) {
736
			$out .= '\u0080-\uFFFF';
737
		}
738
		return $out;
739
	}
740
741
	/**
742
	 * Make a prefixed DB key from a DB key and a namespace index
743
	 *
744
	 * @param int $ns Numerical representation of the namespace
745
	 * @param string $title The DB key form the title
746
	 * @param string $fragment The link fragment (after the "#")
747
	 * @param string $interwiki The interwiki prefix
748
	 * @param bool $canonicalNamespace If true, use the canonical name for
749
	 *   $ns instead of the localized version.
750
	 * @return string The prefixed form of the title
751
	 */
752
	public static function makeName( $ns, $title, $fragment = '', $interwiki = '',
753
		$canonicalNamespace = false
754
	) {
755
		global $wgContLang;
756
757
		if ( $canonicalNamespace ) {
758
			$namespace = MWNamespace::getCanonicalName( $ns );
759
		} else {
760
			$namespace = $wgContLang->getNsText( $ns );
761
		}
762
		$name = $namespace == '' ? $title : "$namespace:$title";
763
		if ( strval( $interwiki ) != '' ) {
764
			$name = "$interwiki:$name";
765
		}
766
		if ( strval( $fragment ) != '' ) {
767
			$name .= '#' . $fragment;
768
		}
769
		return $name;
770
	}
771
772
	/**
773
	 * Escape a text fragment, say from a link, for a URL
774
	 *
775
	 * @param string $fragment Containing a URL or link fragment (after the "#")
776
	 * @return string Escaped string
777
	 */
778
	static function escapeFragmentForURL( $fragment ) {
779
		# Note that we don't urlencode the fragment.  urlencoded Unicode
780
		# fragments appear not to work in IE (at least up to 7) or in at least
781
		# one version of Opera 9.x.  The W3C validator, for one, doesn't seem
782
		# to care if they aren't encoded.
783
		return Sanitizer::escapeId( $fragment, 'noninitial' );
784
	}
785
786
	/**
787
	 * Callback for usort() to do title sorts by (namespace, title)
788
	 *
789
	 * @param Title $a
790
	 * @param Title $b
791
	 *
792
	 * @return int Result of string comparison, or namespace comparison
793
	 */
794
	public static function compare( $a, $b ) {
795
		if ( $a->getNamespace() == $b->getNamespace() ) {
796
			return strcmp( $a->getText(), $b->getText() );
797
		} else {
798
			return $a->getNamespace() - $b->getNamespace();
799
		}
800
	}
801
802
	/**
803
	 * Determine whether the object refers to a page within
804
	 * this project (either this wiki or a wiki with a local
805
	 * interwiki, see https://www.mediawiki.org/wiki/Manual:Interwiki_table#iw_local )
806
	 *
807
	 * @return bool True if this is an in-project interwiki link or a wikilink, false otherwise
808
	 */
809
	public function isLocal() {
810
		if ( $this->isExternal() ) {
811
			$iw = Interwiki::fetch( $this->mInterwiki );
812
			if ( $iw ) {
813
				return $iw->isLocal();
814
			}
815
		}
816
		return true;
817
	}
818
819
	/**
820
	 * Is this Title interwiki?
821
	 *
822
	 * @return bool
823
	 */
824
	public function isExternal() {
825
		return $this->mInterwiki !== '';
826
	}
827
828
	/**
829
	 * Get the interwiki prefix
830
	 *
831
	 * Use Title::isExternal to check if a interwiki is set
832
	 *
833
	 * @return string Interwiki prefix
834
	 */
835
	public function getInterwiki() {
836
		return $this->mInterwiki;
837
	}
838
839
	/**
840
	 * Was this a local interwiki link?
841
	 *
842
	 * @return bool
843
	 */
844
	public function wasLocalInterwiki() {
845
		return $this->mLocalInterwiki;
846
	}
847
848
	/**
849
	 * Determine whether the object refers to a page within
850
	 * this project and is transcludable.
851
	 *
852
	 * @return bool True if this is transcludable
853
	 */
854
	public function isTrans() {
855
		if ( !$this->isExternal() ) {
856
			return false;
857
		}
858
859
		return Interwiki::fetch( $this->mInterwiki )->isTranscludable();
860
	}
861
862
	/**
863
	 * Returns the DB name of the distant wiki which owns the object.
864
	 *
865
	 * @return string The DB name
866
	 */
867
	public function getTransWikiID() {
868
		if ( !$this->isExternal() ) {
869
			return false;
870
		}
871
872
		return Interwiki::fetch( $this->mInterwiki )->getWikiID();
873
	}
874
875
	/**
876
	 * Get a TitleValue object representing this Title.
877
	 *
878
	 * @note Not all valid Titles have a corresponding valid TitleValue
879
	 * (e.g. TitleValues cannot represent page-local links that have a
880
	 * fragment but no title text).
881
	 *
882
	 * @return TitleValue|null
883
	 */
884
	public function getTitleValue() {
885
		if ( $this->mTitleValue === null ) {
886
			try {
887
				$this->mTitleValue = new TitleValue(
888
					$this->getNamespace(),
889
					$this->getDBkey(),
890
					$this->getFragment() );
891
			} catch ( InvalidArgumentException $ex ) {
892
				wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' .
893
					$this->getPrefixedText() . ']]: ' . $ex->getMessage() . "\n" );
894
			}
895
		}
896
897
		return $this->mTitleValue;
898
	}
899
900
	/**
901
	 * Get the text form (spaces not underscores) of the main part
902
	 *
903
	 * @return string Main part of the title
904
	 */
905
	public function getText() {
906
		return $this->mTextform;
907
	}
908
909
	/**
910
	 * Get the URL-encoded form of the main part
911
	 *
912
	 * @return string Main part of the title, URL-encoded
913
	 */
914
	public function getPartialURL() {
915
		return $this->mUrlform;
916
	}
917
918
	/**
919
	 * Get the main part with underscores
920
	 *
921
	 * @return string Main part of the title, with underscores
922
	 */
923
	public function getDBkey() {
924
		return $this->mDbkeyform;
925
	}
926
927
	/**
928
	 * Get the DB key with the initial letter case as specified by the user
929
	 *
930
	 * @return string DB key
931
	 */
932
	function getUserCaseDBKey() {
933
		if ( !is_null( $this->mUserCaseDBKey ) ) {
934
			return $this->mUserCaseDBKey;
935
		} else {
936
			// If created via makeTitle(), $this->mUserCaseDBKey is not set.
937
			return $this->mDbkeyform;
938
		}
939
	}
940
941
	/**
942
	 * Get the namespace index, i.e. one of the NS_xxxx constants.
943
	 *
944
	 * @return int Namespace index
945
	 */
946
	public function getNamespace() {
947
		return $this->mNamespace;
948
	}
949
950
	/**
951
	 * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
952
	 *
953
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
954
	 * @return string Content model id
955
	 */
956
	public function getContentModel( $flags = 0 ) {
957 View Code Duplication
		if ( !$this->mContentModel && $this->getArticleID( $flags ) ) {
958
			$linkCache = LinkCache::singleton();
959
			$linkCache->addLinkObj( $this ); # in case we already had an article ID
960
			$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...
961
		}
962
963
		if ( !$this->mContentModel ) {
964
			$this->mContentModel = ContentHandler::getDefaultModelFor( $this );
965
		}
966
967
		return $this->mContentModel;
968
	}
969
970
	/**
971
	 * Convenience method for checking a title's content model name
972
	 *
973
	 * @param string $id The content model ID (use the CONTENT_MODEL_XXX constants).
974
	 * @return bool True if $this->getContentModel() == $id
975
	 */
976
	public function hasContentModel( $id ) {
977
		return $this->getContentModel() == $id;
978
	}
979
980
	/**
981
	 * Get the namespace text
982
	 *
983
	 * @return string Namespace text
984
	 */
985
	public function getNsText() {
986
		if ( $this->isExternal() ) {
987
			// This probably shouldn't even happen,
988
			// but for interwiki transclusion it sometimes does.
989
			// Use the canonical namespaces if possible to try to
990
			// resolve a foreign namespace.
991
			if ( MWNamespace::exists( $this->mNamespace ) ) {
992
				return MWNamespace::getCanonicalName( $this->mNamespace );
993
			}
994
		}
995
996
		try {
997
			$formatter = self::getTitleFormatter();
998
			return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform );
999
		} catch ( InvalidArgumentException $ex ) {
1000
			wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" );
1001
			return false;
1002
		}
1003
	}
1004
1005
	/**
1006
	 * Get the namespace text of the subject (rather than talk) page
1007
	 *
1008
	 * @return string Namespace text
1009
	 */
1010
	public function getSubjectNsText() {
1011
		global $wgContLang;
1012
		return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) );
1013
	}
1014
1015
	/**
1016
	 * Get the namespace text of the talk page
1017
	 *
1018
	 * @return string Namespace text
1019
	 */
1020
	public function getTalkNsText() {
1021
		global $wgContLang;
1022
		return $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) );
1023
	}
1024
1025
	/**
1026
	 * Could this title have a corresponding talk page?
1027
	 *
1028
	 * @return bool
1029
	 */
1030
	public function canTalk() {
1031
		return MWNamespace::canTalk( $this->mNamespace );
1032
	}
1033
1034
	/**
1035
	 * Is this in a namespace that allows actual pages?
1036
	 *
1037
	 * @return bool
1038
	 */
1039
	public function canExist() {
1040
		return $this->mNamespace >= NS_MAIN;
1041
	}
1042
1043
	/**
1044
	 * Can this title be added to a user's watchlist?
1045
	 *
1046
	 * @return bool
1047
	 */
1048
	public function isWatchable() {
1049
		return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
1050
	}
1051
1052
	/**
1053
	 * Returns true if this is a special page.
1054
	 *
1055
	 * @return bool
1056
	 */
1057
	public function isSpecialPage() {
1058
		return $this->getNamespace() == NS_SPECIAL;
1059
	}
1060
1061
	/**
1062
	 * Returns true if this title resolves to the named special page
1063
	 *
1064
	 * @param string $name The special page name
1065
	 * @return bool
1066
	 */
1067
	public function isSpecial( $name ) {
1068
		if ( $this->isSpecialPage() ) {
1069
			list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() );
1070
			if ( $name == $thisName ) {
1071
				return true;
1072
			}
1073
		}
1074
		return false;
1075
	}
1076
1077
	/**
1078
	 * If the Title refers to a special page alias which is not the local default, resolve
1079
	 * the alias, and localise the name as necessary.  Otherwise, return $this
1080
	 *
1081
	 * @return Title
1082
	 */
1083
	public function fixSpecialName() {
1084
		if ( $this->isSpecialPage() ) {
1085
			list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform );
1086
			if ( $canonicalName ) {
1087
				$localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par );
1088
				if ( $localName != $this->mDbkeyform ) {
1089
					return Title::makeTitle( NS_SPECIAL, $localName );
1090
				}
1091
			}
1092
		}
1093
		return $this;
1094
	}
1095
1096
	/**
1097
	 * Returns true if the title is inside the specified namespace.
1098
	 *
1099
	 * Please make use of this instead of comparing to getNamespace()
1100
	 * This function is much more resistant to changes we may make
1101
	 * to namespaces than code that makes direct comparisons.
1102
	 * @param int $ns The namespace
1103
	 * @return bool
1104
	 * @since 1.19
1105
	 */
1106
	public function inNamespace( $ns ) {
1107
		return MWNamespace::equals( $this->getNamespace(), $ns );
1108
	}
1109
1110
	/**
1111
	 * Returns true if the title is inside one of the specified namespaces.
1112
	 *
1113
	 * @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...
1114
	 * @return bool
1115
	 * @since 1.19
1116
	 */
1117
	public function inNamespaces( /* ... */ ) {
1118
		$namespaces = func_get_args();
1119
		if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
1120
			$namespaces = $namespaces[0];
1121
		}
1122
1123
		foreach ( $namespaces as $ns ) {
1124
			if ( $this->inNamespace( $ns ) ) {
1125
				return true;
1126
			}
1127
		}
1128
1129
		return false;
1130
	}
1131
1132
	/**
1133
	 * Returns true if the title has the same subject namespace as the
1134
	 * namespace specified.
1135
	 * For example this method will take NS_USER and return true if namespace
1136
	 * is either NS_USER or NS_USER_TALK since both of them have NS_USER
1137
	 * as their subject namespace.
1138
	 *
1139
	 * This is MUCH simpler than individually testing for equivalence
1140
	 * against both NS_USER and NS_USER_TALK, and is also forward compatible.
1141
	 * @since 1.19
1142
	 * @param int $ns
1143
	 * @return bool
1144
	 */
1145
	public function hasSubjectNamespace( $ns ) {
1146
		return MWNamespace::subjectEquals( $this->getNamespace(), $ns );
1147
	}
1148
1149
	/**
1150
	 * Is this Title in a namespace which contains content?
1151
	 * In other words, is this a content page, for the purposes of calculating
1152
	 * statistics, etc?
1153
	 *
1154
	 * @return bool
1155
	 */
1156
	public function isContentPage() {
1157
		return MWNamespace::isContent( $this->getNamespace() );
1158
	}
1159
1160
	/**
1161
	 * Would anybody with sufficient privileges be able to move this page?
1162
	 * Some pages just aren't movable.
1163
	 *
1164
	 * @return bool
1165
	 */
1166
	public function isMovable() {
1167
		if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->isExternal() ) {
1168
			// Interwiki title or immovable namespace. Hooks don't get to override here
1169
			return false;
1170
		}
1171
1172
		$result = true;
1173
		Hooks::run( 'TitleIsMovable', [ $this, &$result ] );
1174
		return $result;
1175
	}
1176
1177
	/**
1178
	 * Is this the mainpage?
1179
	 * @note Title::newFromText seems to be sufficiently optimized by the title
1180
	 * cache that we don't need to over-optimize by doing direct comparisons and
1181
	 * accidentally creating new bugs where $title->equals( Title::newFromText() )
1182
	 * ends up reporting something differently than $title->isMainPage();
1183
	 *
1184
	 * @since 1.18
1185
	 * @return bool
1186
	 */
1187
	public function isMainPage() {
1188
		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...
1189
	}
1190
1191
	/**
1192
	 * Is this a subpage?
1193
	 *
1194
	 * @return bool
1195
	 */
1196
	public function isSubpage() {
1197
		return MWNamespace::hasSubpages( $this->mNamespace )
1198
			? strpos( $this->getText(), '/' ) !== false
1199
			: false;
1200
	}
1201
1202
	/**
1203
	 * Is this a conversion table for the LanguageConverter?
1204
	 *
1205
	 * @return bool
1206
	 */
1207
	public function isConversionTable() {
1208
		// @todo ConversionTable should become a separate content model.
1209
1210
		return $this->getNamespace() == NS_MEDIAWIKI &&
1211
			strpos( $this->getText(), 'Conversiontable/' ) === 0;
1212
	}
1213
1214
	/**
1215
	 * Does that page contain wikitext, or it is JS, CSS or whatever?
1216
	 *
1217
	 * @return bool
1218
	 */
1219
	public function isWikitextPage() {
1220
		return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
1221
	}
1222
1223
	/**
1224
	 * Could this page contain custom CSS or JavaScript for the global UI.
1225
	 * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
1226
	 * or CONTENT_MODEL_JAVASCRIPT.
1227
	 *
1228
	 * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage()
1229
	 * for that!
1230
	 *
1231
	 * Note that this method should not return true for pages that contain and
1232
	 * show "inactive" CSS or JS.
1233
	 *
1234
	 * @return bool
1235
	 * @todo FIXME: Rename to isSiteConfigPage() and remove deprecated hook
1236
	 */
1237
	public function isCssOrJsPage() {
1238
		$isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
1239
			&& ( $this->hasContentModel( CONTENT_MODEL_CSS )
1240
				|| $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
1241
1242
		# @note This hook is also called in ContentHandler::getDefaultModel.
1243
		#   It's called here again to make sure hook functions can force this
1244
		#   method to return true even outside the MediaWiki namespace.
1245
1246
		Hooks::run( 'TitleIsCssOrJsPage', [ $this, &$isCssOrJsPage ], '1.25' );
1247
1248
		return $isCssOrJsPage;
1249
	}
1250
1251
	/**
1252
	 * Is this a .css or .js subpage of a user page?
1253
	 * @return bool
1254
	 * @todo FIXME: Rename to isUserConfigPage()
1255
	 */
1256
	public function isCssJsSubpage() {
1257
		return ( NS_USER == $this->mNamespace && $this->isSubpage()
1258
				&& ( $this->hasContentModel( CONTENT_MODEL_CSS )
1259
					|| $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
1260
	}
1261
1262
	/**
1263
	 * Trim down a .css or .js subpage title to get the corresponding skin name
1264
	 *
1265
	 * @return string Containing skin name from .css or .js subpage title
1266
	 */
1267
	public function getSkinFromCssJsSubpage() {
1268
		$subpage = explode( '/', $this->mTextform );
1269
		$subpage = $subpage[count( $subpage ) - 1];
1270
		$lastdot = strrpos( $subpage, '.' );
1271
		if ( $lastdot === false ) {
1272
			return $subpage; # Never happens: only called for names ending in '.css' or '.js'
1273
		}
1274
		return substr( $subpage, 0, $lastdot );
1275
	}
1276
1277
	/**
1278
	 * Is this a .css subpage of a user page?
1279
	 *
1280
	 * @return bool
1281
	 */
1282
	public function isCssSubpage() {
1283
		return ( NS_USER == $this->mNamespace && $this->isSubpage()
1284
			&& $this->hasContentModel( CONTENT_MODEL_CSS ) );
1285
	}
1286
1287
	/**
1288
	 * Is this a .js subpage of a user page?
1289
	 *
1290
	 * @return bool
1291
	 */
1292
	public function isJsSubpage() {
1293
		return ( NS_USER == $this->mNamespace && $this->isSubpage()
1294
			&& $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
1295
	}
1296
1297
	/**
1298
	 * Is this a talk page of some sort?
1299
	 *
1300
	 * @return bool
1301
	 */
1302
	public function isTalkPage() {
1303
		return MWNamespace::isTalk( $this->getNamespace() );
1304
	}
1305
1306
	/**
1307
	 * Get a Title object associated with the talk page of this article
1308
	 *
1309
	 * @return Title The object for the talk page
1310
	 */
1311
	public function getTalkPage() {
1312
		return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
1313
	}
1314
1315
	/**
1316
	 * Get a title object associated with the subject page of this
1317
	 * talk page
1318
	 *
1319
	 * @return Title The object for the subject page
1320
	 */
1321
	public function getSubjectPage() {
1322
		// Is this the same title?
1323
		$subjectNS = MWNamespace::getSubject( $this->getNamespace() );
1324
		if ( $this->getNamespace() == $subjectNS ) {
1325
			return $this;
1326
		}
1327
		return Title::makeTitle( $subjectNS, $this->getDBkey() );
1328
	}
1329
1330
	/**
1331
	 * Get the other title for this page, if this is a subject page
1332
	 * get the talk page, if it is a subject page get the talk page
1333
	 *
1334
	 * @since 1.25
1335
	 * @throws MWException
1336
	 * @return Title
1337
	 */
1338
	public function getOtherPage() {
1339
		if ( $this->isSpecialPage() ) {
1340
			throw new MWException( 'Special pages cannot have other pages' );
1341
		}
1342
		if ( $this->isTalkPage() ) {
1343
			return $this->getSubjectPage();
1344
		} else {
1345
			return $this->getTalkPage();
1346
		}
1347
	}
1348
1349
	/**
1350
	 * Get the default namespace index, for when there is no namespace
1351
	 *
1352
	 * @return int Default namespace index
1353
	 */
1354
	public function getDefaultNamespace() {
1355
		return $this->mDefaultNamespace;
1356
	}
1357
1358
	/**
1359
	 * Get the Title fragment (i.e.\ the bit after the #) in text form
1360
	 *
1361
	 * Use Title::hasFragment to check for a fragment
1362
	 *
1363
	 * @return string Title fragment
1364
	 */
1365
	public function getFragment() {
1366
		return $this->mFragment;
1367
	}
1368
1369
	/**
1370
	 * Check if a Title fragment is set
1371
	 *
1372
	 * @return bool
1373
	 * @since 1.23
1374
	 */
1375
	public function hasFragment() {
1376
		return $this->mFragment !== '';
1377
	}
1378
1379
	/**
1380
	 * Get the fragment in URL form, including the "#" character if there is one
1381
	 * @return string Fragment in URL form
1382
	 */
1383
	public function getFragmentForURL() {
1384
		if ( !$this->hasFragment() ) {
1385
			return '';
1386
		} else {
1387
			return '#' . Title::escapeFragmentForURL( $this->getFragment() );
1388
		}
1389
	}
1390
1391
	/**
1392
	 * Set the fragment for this title. Removes the first character from the
1393
	 * specified fragment before setting, so it assumes you're passing it with
1394
	 * an initial "#".
1395
	 *
1396
	 * Deprecated for public use, use Title::makeTitle() with fragment parameter.
1397
	 * Still in active use privately.
1398
	 *
1399
	 * @private
1400
	 * @param string $fragment Text
1401
	 */
1402
	public function setFragment( $fragment ) {
1403
		$this->mFragment = strtr( substr( $fragment, 1 ), '_', ' ' );
1404
	}
1405
1406
	/**
1407
	 * Prefix some arbitrary text with the namespace or interwiki prefix
1408
	 * of this object
1409
	 *
1410
	 * @param string $name The text
1411
	 * @return string The prefixed text
1412
	 */
1413
	private function prefix( $name ) {
1414
		$p = '';
1415
		if ( $this->isExternal() ) {
1416
			$p = $this->mInterwiki . ':';
1417
		}
1418
1419
		if ( 0 != $this->mNamespace ) {
1420
			$p .= $this->getNsText() . ':';
1421
		}
1422
		return $p . $name;
1423
	}
1424
1425
	/**
1426
	 * Get the prefixed database key form
1427
	 *
1428
	 * @return string The prefixed title, with underscores and
1429
	 *  any interwiki and namespace prefixes
1430
	 */
1431
	public function getPrefixedDBkey() {
1432
		$s = $this->prefix( $this->mDbkeyform );
1433
		$s = strtr( $s, ' ', '_' );
1434
		return $s;
1435
	}
1436
1437
	/**
1438
	 * Get the prefixed title with spaces.
1439
	 * This is the form usually used for display
1440
	 *
1441
	 * @return string The prefixed title, with spaces
1442
	 */
1443
	public function getPrefixedText() {
1444
		if ( $this->mPrefixedText === null ) {
1445
			$s = $this->prefix( $this->mTextform );
1446
			$s = strtr( $s, '_', ' ' );
1447
			$this->mPrefixedText = $s;
1448
		}
1449
		return $this->mPrefixedText;
1450
	}
1451
1452
	/**
1453
	 * Return a string representation of this title
1454
	 *
1455
	 * @return string Representation of this title
1456
	 */
1457
	public function __toString() {
1458
		return $this->getPrefixedText();
1459
	}
1460
1461
	/**
1462
	 * Get the prefixed title with spaces, plus any fragment
1463
	 * (part beginning with '#')
1464
	 *
1465
	 * @return string The prefixed title, with spaces and the fragment, including '#'
1466
	 */
1467
	public function getFullText() {
1468
		$text = $this->getPrefixedText();
1469
		if ( $this->hasFragment() ) {
1470
			$text .= '#' . $this->getFragment();
1471
		}
1472
		return $text;
1473
	}
1474
1475
	/**
1476
	 * Get the root page name text without a namespace, i.e. the leftmost part before any slashes
1477
	 *
1478
	 * @par Example:
1479
	 * @code
1480
	 * Title::newFromText('User:Foo/Bar/Baz')->getRootText();
1481
	 * # returns: 'Foo'
1482
	 * @endcode
1483
	 *
1484
	 * @return string Root name
1485
	 * @since 1.20
1486
	 */
1487
	public function getRootText() {
1488
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1489
			return $this->getText();
1490
		}
1491
1492
		return strtok( $this->getText(), '/' );
1493
	}
1494
1495
	/**
1496
	 * Get the root page name title, i.e. the leftmost part before any slashes
1497
	 *
1498
	 * @par Example:
1499
	 * @code
1500
	 * Title::newFromText('User:Foo/Bar/Baz')->getRootTitle();
1501
	 * # returns: Title{User:Foo}
1502
	 * @endcode
1503
	 *
1504
	 * @return Title Root title
1505
	 * @since 1.20
1506
	 */
1507
	public function getRootTitle() {
1508
		return Title::makeTitle( $this->getNamespace(), $this->getRootText() );
1509
	}
1510
1511
	/**
1512
	 * Get the base page name without a namespace, i.e. the part before the subpage name
1513
	 *
1514
	 * @par Example:
1515
	 * @code
1516
	 * Title::newFromText('User:Foo/Bar/Baz')->getBaseText();
1517
	 * # returns: 'Foo/Bar'
1518
	 * @endcode
1519
	 *
1520
	 * @return string Base name
1521
	 */
1522
	public function getBaseText() {
1523
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1524
			return $this->getText();
1525
		}
1526
1527
		$parts = explode( '/', $this->getText() );
1528
		# Don't discard the real title if there's no subpage involved
1529
		if ( count( $parts ) > 1 ) {
1530
			unset( $parts[count( $parts ) - 1] );
1531
		}
1532
		return implode( '/', $parts );
1533
	}
1534
1535
	/**
1536
	 * Get the base page name title, i.e. the part before the subpage name
1537
	 *
1538
	 * @par Example:
1539
	 * @code
1540
	 * Title::newFromText('User:Foo/Bar/Baz')->getBaseTitle();
1541
	 * # returns: Title{User:Foo/Bar}
1542
	 * @endcode
1543
	 *
1544
	 * @return Title Base title
1545
	 * @since 1.20
1546
	 */
1547
	public function getBaseTitle() {
1548
		return Title::makeTitle( $this->getNamespace(), $this->getBaseText() );
1549
	}
1550
1551
	/**
1552
	 * Get the lowest-level subpage name, i.e. the rightmost part after any slashes
1553
	 *
1554
	 * @par Example:
1555
	 * @code
1556
	 * Title::newFromText('User:Foo/Bar/Baz')->getSubpageText();
1557
	 * # returns: "Baz"
1558
	 * @endcode
1559
	 *
1560
	 * @return string Subpage name
1561
	 */
1562
	public function getSubpageText() {
1563
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1564
			return $this->mTextform;
1565
		}
1566
		$parts = explode( '/', $this->mTextform );
1567
		return $parts[count( $parts ) - 1];
1568
	}
1569
1570
	/**
1571
	 * Get the title for a subpage of the current page
1572
	 *
1573
	 * @par Example:
1574
	 * @code
1575
	 * Title::newFromText('User:Foo/Bar/Baz')->getSubpage("Asdf");
1576
	 * # returns: Title{User:Foo/Bar/Baz/Asdf}
1577
	 * @endcode
1578
	 *
1579
	 * @param string $text The subpage name to add to the title
1580
	 * @return Title Subpage title
1581
	 * @since 1.20
1582
	 */
1583
	public function getSubpage( $text ) {
1584
		return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text );
1585
	}
1586
1587
	/**
1588
	 * Get a URL-encoded form of the subpage text
1589
	 *
1590
	 * @return string URL-encoded subpage name
1591
	 */
1592
	public function getSubpageUrlForm() {
1593
		$text = $this->getSubpageText();
1594
		$text = wfUrlencode( strtr( $text, ' ', '_' ) );
1595
		return $text;
1596
	}
1597
1598
	/**
1599
	 * Get a URL-encoded title (not an actual URL) including interwiki
1600
	 *
1601
	 * @return string The URL-encoded form
1602
	 */
1603
	public function getPrefixedURL() {
1604
		$s = $this->prefix( $this->mDbkeyform );
1605
		$s = wfUrlencode( strtr( $s, ' ', '_' ) );
1606
		return $s;
1607
	}
1608
1609
	/**
1610
	 * Helper to fix up the get{Canonical,Full,Link,Local,Internal}URL args
1611
	 * get{Canonical,Full,Link,Local,Internal}URL methods accepted an optional
1612
	 * second argument named variant. This was deprecated in favor
1613
	 * of passing an array of option with a "variant" key
1614
	 * Once $query2 is removed for good, this helper can be dropped
1615
	 * and the wfArrayToCgi moved to getLocalURL();
1616
	 *
1617
	 * @since 1.19 (r105919)
1618
	 * @param array|string $query
1619
	 * @param bool $query2
1620
	 * @return string
1621
	 */
1622
	private static function fixUrlQueryArgs( $query, $query2 = false ) {
1623
		if ( $query2 !== false ) {
1624
			wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " .
1625
				"method called with a second parameter is deprecated. Add your " .
1626
				"parameter to an array passed as the first parameter.", "1.19" );
1627
		}
1628
		if ( is_array( $query ) ) {
1629
			$query = wfArrayToCgi( $query );
1630
		}
1631
		if ( $query2 ) {
1632
			if ( is_string( $query2 ) ) {
1633
				// $query2 is a string, we will consider this to be
1634
				// a deprecated $variant argument and add it to the query
1635
				$query2 = wfArrayToCgi( [ 'variant' => $query2 ] );
1636
			} else {
1637
				$query2 = wfArrayToCgi( $query2 );
1638
			}
1639
			// If we have $query content add a & to it first
1640
			if ( $query ) {
1641
				$query .= '&';
1642
			}
1643
			// Now append the queries together
1644
			$query .= $query2;
1645
		}
1646
		return $query;
1647
	}
1648
1649
	/**
1650
	 * Get a real URL referring to this title, with interwiki link and
1651
	 * fragment
1652
	 *
1653
	 * @see self::getLocalURL for the arguments.
1654
	 * @see wfExpandUrl
1655
	 * @param array|string $query
1656
	 * @param bool $query2
1657
	 * @param string $proto Protocol type to use in URL
1658
	 * @return string The URL
1659
	 */
1660
	public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
1661
		$query = self::fixUrlQueryArgs( $query, $query2 );
1662
1663
		# Hand off all the decisions on urls to getLocalURL
1664
		$url = $this->getLocalURL( $query );
1665
1666
		# Expand the url to make it a full url. Note that getLocalURL has the
1667
		# potential to output full urls for a variety of reasons, so we use
1668
		# wfExpandUrl instead of simply prepending $wgServer
1669
		$url = wfExpandUrl( $url, $proto );
1670
1671
		# Finally, add the fragment.
1672
		$url .= $this->getFragmentForURL();
1673
1674
		Hooks::run( 'GetFullURL', [ &$this, &$url, $query ] );
1675
		return $url;
1676
	}
1677
1678
	/**
1679
	 * Get a URL with no fragment or server name (relative URL) from a Title object.
1680
	 * If this page is generated with action=render, however,
1681
	 * $wgServer is prepended to make an absolute URL.
1682
	 *
1683
	 * @see self::getFullURL to always get an absolute URL.
1684
	 * @see self::getLinkURL to always get a URL that's the simplest URL that will be
1685
	 *  valid to link, locally, to the current Title.
1686
	 * @see self::newFromText to produce a Title object.
1687
	 *
1688
	 * @param string|array $query An optional query string,
1689
	 *   not used for interwiki links. Can be specified as an associative array as well,
1690
	 *   e.g., array( 'action' => 'edit' ) (keys and values will be URL-escaped).
1691
	 *   Some query patterns will trigger various shorturl path replacements.
1692
	 * @param array $query2 An optional secondary query array. This one MUST
1693
	 *   be an array. If a string is passed it will be interpreted as a deprecated
1694
	 *   variant argument and urlencoded into a variant= argument.
1695
	 *   This second query argument will be added to the $query
1696
	 *   The second parameter is deprecated since 1.19. Pass it as a key,value
1697
	 *   pair in the first parameter array instead.
1698
	 *
1699
	 * @return string String of the URL.
1700
	 */
1701
	public function getLocalURL( $query = '', $query2 = false ) {
1702
		global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
1703
1704
		$query = self::fixUrlQueryArgs( $query, $query2 );
0 ignored issues
show
Bug introduced by
It seems like $query2 defined by parameter $query2 on line 1701 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...
1705
1706
		$interwiki = Interwiki::fetch( $this->mInterwiki );
1707
		if ( $interwiki ) {
1708
			$namespace = $this->getNsText();
1709
			if ( $namespace != '' ) {
1710
				# Can this actually happen? Interwikis shouldn't be parsed.
1711
				# Yes! It can in interwiki transclusion. But... it probably shouldn't.
1712
				$namespace .= ':';
1713
			}
1714
			$url = $interwiki->getURL( $namespace . $this->getDBkey() );
1715
			$url = wfAppendQuery( $url, $query );
1716
		} else {
1717
			$dbkey = wfUrlencode( $this->getPrefixedDBkey() );
1718
			if ( $query == '' ) {
1719
				$url = str_replace( '$1', $dbkey, $wgArticlePath );
1720
				Hooks::run( 'GetLocalURL::Article', [ &$this, &$url ] );
1721
			} else {
1722
				global $wgVariantArticlePath, $wgActionPaths, $wgContLang;
1723
				$url = false;
1724
				$matches = [];
1725
1726
				if ( !empty( $wgActionPaths )
1727
					&& preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches )
1728
				) {
1729
					$action = urldecode( $matches[2] );
1730
					if ( isset( $wgActionPaths[$action] ) ) {
1731
						$query = $matches[1];
1732
						if ( isset( $matches[4] ) ) {
1733
							$query .= $matches[4];
1734
						}
1735
						$url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
1736
						if ( $query != '' ) {
1737
							$url = wfAppendQuery( $url, $query );
1738
						}
1739
					}
1740
				}
1741
1742
				if ( $url === false
1743
					&& $wgVariantArticlePath
1744
					&& $wgContLang->getCode() === $this->getPageLanguage()->getCode()
1745
					&& $this->getPageLanguage()->hasVariants()
1746
					&& preg_match( '/^variant=([^&]*)$/', $query, $matches )
1747
				) {
1748
					$variant = urldecode( $matches[1] );
1749
					if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
1750
						// Only do the variant replacement if the given variant is a valid
1751
						// variant for the page's language.
1752
						$url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath );
1753
						$url = str_replace( '$1', $dbkey, $url );
1754
					}
1755
				}
1756
1757
				if ( $url === false ) {
1758
					if ( $query == '-' ) {
1759
						$query = '';
1760
					}
1761
					$url = "{$wgScript}?title={$dbkey}&{$query}";
1762
				}
1763
			}
1764
1765
			Hooks::run( 'GetLocalURL::Internal', [ &$this, &$url, $query ] );
1766
1767
			// @todo FIXME: This causes breakage in various places when we
1768
			// actually expected a local URL and end up with dupe prefixes.
1769
			if ( $wgRequest->getVal( 'action' ) == 'render' ) {
1770
				$url = $wgServer . $url;
1771
			}
1772
		}
1773
		Hooks::run( 'GetLocalURL', [ &$this, &$url, $query ] );
1774
		return $url;
1775
	}
1776
1777
	/**
1778
	 * Get a URL that's the simplest URL that will be valid to link, locally,
1779
	 * to the current Title.  It includes the fragment, but does not include
1780
	 * the server unless action=render is used (or the link is external).  If
1781
	 * there's a fragment but the prefixed text is empty, we just return a link
1782
	 * to the fragment.
1783
	 *
1784
	 * The result obviously should not be URL-escaped, but does need to be
1785
	 * HTML-escaped if it's being output in HTML.
1786
	 *
1787
	 * @param array $query
1788
	 * @param bool $query2
1789
	 * @param string $proto Protocol to use; setting this will cause a full URL to be used
1790
	 * @see self::getLocalURL for the arguments.
1791
	 * @return string The URL
1792
	 */
1793
	public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
1794
		if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) {
1795
			$ret = $this->getFullURL( $query, $query2, $proto );
1796
		} elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) {
1797
			$ret = $this->getFragmentForURL();
1798
		} else {
1799
			$ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
1800
		}
1801
		return $ret;
1802
	}
1803
1804
	/**
1805
	 * Get the URL form for an internal link.
1806
	 * - Used in various CDN-related code, in case we have a different
1807
	 * internal hostname for the server from the exposed one.
1808
	 *
1809
	 * This uses $wgInternalServer to qualify the path, or $wgServer
1810
	 * if $wgInternalServer is not set. If the server variable used is
1811
	 * protocol-relative, the URL will be expanded to http://
1812
	 *
1813
	 * @see self::getLocalURL for the arguments.
1814
	 * @return string The URL
1815
	 */
1816
	public function getInternalURL( $query = '', $query2 = false ) {
1817
		global $wgInternalServer, $wgServer;
1818
		$query = self::fixUrlQueryArgs( $query, $query2 );
1819
		$server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
1820
		$url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
1821
		Hooks::run( 'GetInternalURL', [ &$this, &$url, $query ] );
1822
		return $url;
1823
	}
1824
1825
	/**
1826
	 * Get the URL for a canonical link, for use in things like IRC and
1827
	 * e-mail notifications. Uses $wgCanonicalServer and the
1828
	 * GetCanonicalURL hook.
1829
	 *
1830
	 * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment
1831
	 *
1832
	 * @see self::getLocalURL for the arguments.
1833
	 * @return string The URL
1834
	 * @since 1.18
1835
	 */
1836
	public function getCanonicalURL( $query = '', $query2 = false ) {
1837
		$query = self::fixUrlQueryArgs( $query, $query2 );
1838
		$url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
1839
		Hooks::run( 'GetCanonicalURL', [ &$this, &$url, $query ] );
1840
		return $url;
1841
	}
1842
1843
	/**
1844
	 * Get the edit URL for this Title
1845
	 *
1846
	 * @return string The URL, or a null string if this is an interwiki link
1847
	 */
1848
	public function getEditURL() {
1849
		if ( $this->isExternal() ) {
1850
			return '';
1851
		}
1852
		$s = $this->getLocalURL( 'action=edit' );
1853
1854
		return $s;
1855
	}
1856
1857
	/**
1858
	 * Can $user perform $action on this page?
1859
	 * This skips potentially expensive cascading permission checks
1860
	 * as well as avoids expensive error formatting
1861
	 *
1862
	 * Suitable for use for nonessential UI controls in common cases, but
1863
	 * _not_ for functional access control.
1864
	 *
1865
	 * May provide false positives, but should never provide a false negative.
1866
	 *
1867
	 * @param string $action Action that permission needs to be checked for
1868
	 * @param User $user User to check (since 1.19); $wgUser will be used if not provided.
1869
	 * @return bool
1870
	 */
1871
	public function quickUserCan( $action, $user = null ) {
1872
		return $this->userCan( $action, $user, false );
1873
	}
1874
1875
	/**
1876
	 * Can $user perform $action on this page?
1877
	 *
1878
	 * @param string $action Action that permission needs to be checked for
1879
	 * @param User $user User to check (since 1.19); $wgUser will be used if not
1880
	 *   provided.
1881
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
1882
	 * @return bool
1883
	 */
1884
	public function userCan( $action, $user = null, $rigor = 'secure' ) {
1885
		if ( !$user instanceof User ) {
1886
			global $wgUser;
1887
			$user = $wgUser;
1888
		}
1889
1890
		return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) );
1891
	}
1892
1893
	/**
1894
	 * Can $user perform $action on this page?
1895
	 *
1896
	 * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
1897
	 *
1898
	 * @param string $action Action that permission needs to be checked for
1899
	 * @param User $user User to check
1900
	 * @param string $rigor One of (quick,full,secure)
1901
	 *   - quick  : does cheap permission checks from slaves (usable for GUI creation)
1902
	 *   - full   : does cheap and expensive checks possibly from a slave
1903
	 *   - secure : does cheap and expensive checks, using the master as needed
1904
	 * @param array $ignoreErrors Array of Strings Set this to a list of message keys
1905
	 *   whose corresponding errors may be ignored.
1906
	 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
1907
	 */
1908
	public function getUserPermissionsErrors(
1909
		$action, $user, $rigor = 'secure', $ignoreErrors = []
1910
	) {
1911
		$errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor );
1912
1913
		// Remove the errors being ignored.
1914
		foreach ( $errors as $index => $error ) {
1915
			$errKey = is_array( $error ) ? $error[0] : $error;
1916
1917
			if ( in_array( $errKey, $ignoreErrors ) ) {
1918
				unset( $errors[$index] );
1919
			}
1920
			if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
1921
				unset( $errors[$index] );
1922
			}
1923
		}
1924
1925
		return $errors;
1926
	}
1927
1928
	/**
1929
	 * Permissions checks that fail most often, and which are easiest to test.
1930
	 *
1931
	 * @param string $action The action to check
1932
	 * @param User $user User to check
1933
	 * @param array $errors List of current errors
1934
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
1935
	 * @param bool $short Short circuit on first error
1936
	 *
1937
	 * @return array List of errors
1938
	 */
1939
	private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) {
1940
		if ( !Hooks::run( 'TitleQuickPermissions',
1941
			[ $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ] )
1942
		) {
1943
			return $errors;
1944
		}
1945
1946
		if ( $action == 'create' ) {
1947
			if (
1948
				( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
1949
				( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
1950
			) {
1951
				$errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
1952
			}
1953
		} elseif ( $action == 'move' ) {
1954 View Code Duplication
			if ( !$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-user-page' ];
1958
			}
1959
1960
			// Check if user is allowed to move files if it's a file
1961
			if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
1962
				$errors[] = [ 'movenotallowedfile' ];
1963
			}
1964
1965
			// Check if user is allowed to move category pages if it's a category page
1966
			if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
1967
				$errors[] = [ 'cant-move-category-page' ];
1968
			}
1969
1970
			if ( !$user->isAllowed( 'move' ) ) {
1971
				// User can't move anything
1972
				$userCanMove = User::groupHasPermission( 'user', 'move' );
1973
				$autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
1974
				if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
1975
					// custom message if logged-in users without any special rights can move
1976
					$errors[] = [ 'movenologintext' ];
1977
				} else {
1978
					$errors[] = [ 'movenotallowed' ];
1979
				}
1980
			}
1981
		} elseif ( $action == 'move-target' ) {
1982
			if ( !$user->isAllowed( 'move' ) ) {
1983
				// User can't move anything
1984
				$errors[] = [ 'movenotallowed' ];
1985 View Code Duplication
			} elseif ( !$user->isAllowed( 'move-rootuserpages' )
1986
					&& $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1987
				// Show user page-specific message only if the user can move other pages
1988
				$errors[] = [ 'cant-move-to-user-page' ];
1989
			} elseif ( !$user->isAllowed( 'move-categorypages' )
1990
					&& $this->mNamespace == NS_CATEGORY ) {
1991
				// Show category page-specific message only if the user can move other pages
1992
				$errors[] = [ 'cant-move-to-category-page' ];
1993
			}
1994
		} elseif ( !$user->isAllowed( $action ) ) {
1995
			$errors[] = $this->missingPermissionError( $action, $short );
1996
		}
1997
1998
		return $errors;
1999
	}
2000
2001
	/**
2002
	 * Add the resulting error code to the errors array
2003
	 *
2004
	 * @param array $errors List of current errors
2005
	 * @param array $result Result of errors
2006
	 *
2007
	 * @return array List of errors
2008
	 */
2009
	private function resultToError( $errors, $result ) {
2010
		if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
2011
			// A single array representing an error
2012
			$errors[] = $result;
2013
		} elseif ( is_array( $result ) && is_array( $result[0] ) ) {
2014
			// A nested array representing multiple errors
2015
			$errors = array_merge( $errors, $result );
2016
		} elseif ( $result !== '' && is_string( $result ) ) {
2017
			// A string representing a message-id
2018
			$errors[] = [ $result ];
2019
		} elseif ( $result instanceof MessageSpecifier ) {
2020
			// A message specifier representing an error
2021
			$errors[] = [ $result ];
2022
		} elseif ( $result === false ) {
2023
			// a generic "We don't want them to do that"
2024
			$errors[] = [ 'badaccess-group0' ];
2025
		}
2026
		return $errors;
2027
	}
2028
2029
	/**
2030
	 * Check various permission hooks
2031
	 *
2032
	 * @param string $action The action to check
2033
	 * @param User $user User to check
2034
	 * @param array $errors List of current errors
2035
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2036
	 * @param bool $short Short circuit on first error
2037
	 *
2038
	 * @return array List of errors
2039
	 */
2040
	private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) {
2041
		// Use getUserPermissionsErrors instead
2042
		$result = '';
2043
		if ( !Hooks::run( 'userCan', [ &$this, &$user, $action, &$result ] ) ) {
2044
			return $result ? [] : [ [ 'badaccess-group0' ] ];
2045
		}
2046
		// Check getUserPermissionsErrors hook
2047
		if ( !Hooks::run( 'getUserPermissionsErrors', [ &$this, &$user, $action, &$result ] ) ) {
2048
			$errors = $this->resultToError( $errors, $result );
2049
		}
2050
		// Check getUserPermissionsErrorsExpensive hook
2051
		if (
2052
			$rigor !== 'quick'
2053
			&& !( $short && count( $errors ) > 0 )
2054
			&& !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$this, &$user, $action, &$result ] )
2055
		) {
2056
			$errors = $this->resultToError( $errors, $result );
2057
		}
2058
2059
		return $errors;
2060
	}
2061
2062
	/**
2063
	 * Check permissions on special pages & namespaces
2064
	 *
2065
	 * @param string $action The action to check
2066
	 * @param User $user User to check
2067
	 * @param array $errors List of current errors
2068
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2069
	 * @param bool $short Short circuit on first error
2070
	 *
2071
	 * @return array List of errors
2072
	 */
2073
	private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) {
2074
		# Only 'createaccount' can be performed on special pages,
2075
		# which don't actually exist in the DB.
2076
		if ( NS_SPECIAL == $this->mNamespace && $action !== 'createaccount' ) {
2077
			$errors[] = [ 'ns-specialprotected' ];
2078
		}
2079
2080
		# Check $wgNamespaceProtection for restricted namespaces
2081
		if ( $this->isNamespaceProtected( $user ) ) {
2082
			$ns = $this->mNamespace == NS_MAIN ?
2083
				wfMessage( 'nstab-main' )->text() : $this->getNsText();
2084
			$errors[] = $this->mNamespace == NS_MEDIAWIKI ?
2085
				[ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
2086
		}
2087
2088
		return $errors;
2089
	}
2090
2091
	/**
2092
	 * Check CSS/JS sub-page permissions
2093
	 *
2094
	 * @param string $action The action to check
2095
	 * @param User $user User to check
2096
	 * @param array $errors List of current errors
2097
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2098
	 * @param bool $short Short circuit on first error
2099
	 *
2100
	 * @return array List of errors
2101
	 */
2102
	private function checkCSSandJSPermissions( $action, $user, $errors, $rigor, $short ) {
2103
		# Protect css/js subpages of user pages
2104
		# XXX: this might be better using restrictions
2105
		# XXX: right 'editusercssjs' is deprecated, for backward compatibility only
2106
		if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) {
2107
			if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
2108 View Code Duplication
				if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) {
2109
					$errors[] = [ 'mycustomcssprotected', $action ];
2110
				} elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) {
2111
					$errors[] = [ 'mycustomjsprotected', $action ];
2112
				}
2113 View Code Duplication
			} else {
2114
				if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) {
2115
					$errors[] = [ 'customcssprotected', $action ];
2116
				} elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
2117
					$errors[] = [ 'customjsprotected', $action ];
2118
				}
2119
			}
2120
		}
2121
2122
		return $errors;
2123
	}
2124
2125
	/**
2126
	 * Check against page_restrictions table requirements on this
2127
	 * page. The user must possess all required rights for this
2128
	 * action.
2129
	 *
2130
	 * @param string $action The action to check
2131
	 * @param User $user User to check
2132
	 * @param array $errors List of current errors
2133
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2134
	 * @param bool $short Short circuit on first error
2135
	 *
2136
	 * @return array List of errors
2137
	 */
2138
	private function checkPageRestrictions( $action, $user, $errors, $rigor, $short ) {
2139
		foreach ( $this->getRestrictions( $action ) as $right ) {
2140
			// Backwards compatibility, rewrite sysop -> editprotected
2141
			if ( $right == 'sysop' ) {
2142
				$right = 'editprotected';
2143
			}
2144
			// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2145
			if ( $right == 'autoconfirmed' ) {
2146
				$right = 'editsemiprotected';
2147
			}
2148
			if ( $right == '' ) {
2149
				continue;
2150
			}
2151
			if ( !$user->isAllowed( $right ) ) {
2152
				$errors[] = [ 'protectedpagetext', $right, $action ];
2153
			} elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
2154
				$errors[] = [ 'protectedpagetext', 'protect', $action ];
2155
			}
2156
		}
2157
2158
		return $errors;
2159
	}
2160
2161
	/**
2162
	 * Check restrictions on cascading pages.
2163
	 *
2164
	 * @param string $action The action to check
2165
	 * @param User $user User to check
2166
	 * @param array $errors List of current errors
2167
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2168
	 * @param bool $short Short circuit on first error
2169
	 *
2170
	 * @return array List of errors
2171
	 */
2172
	private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
2173
		if ( $rigor !== 'quick' && !$this->isCssJsSubpage() ) {
2174
			# We /could/ use the protection level on the source page, but it's
2175
			# fairly ugly as we have to establish a precedence hierarchy for pages
2176
			# included by multiple cascade-protected pages. So just restrict
2177
			# it to people with 'protect' permission, as they could remove the
2178
			# protection anyway.
2179
			list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
2180
			# Cascading protection depends on more than this page...
2181
			# Several cascading protected pages may include this page...
2182
			# Check each cascading level
2183
			# This is only for protection restrictions, not for all actions
2184
			if ( isset( $restrictions[$action] ) ) {
2185
				foreach ( $restrictions[$action] as $right ) {
2186
					// Backwards compatibility, rewrite sysop -> editprotected
2187
					if ( $right == 'sysop' ) {
2188
						$right = 'editprotected';
2189
					}
2190
					// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2191
					if ( $right == 'autoconfirmed' ) {
2192
						$right = 'editsemiprotected';
2193
					}
2194
					if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
2195
						$pages = '';
2196
						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...
2197
							$pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
2198
						}
2199
						$errors[] = [ 'cascadeprotected', count( $cascadingSources ), $pages, $action ];
2200
					}
2201
				}
2202
			}
2203
		}
2204
2205
		return $errors;
2206
	}
2207
2208
	/**
2209
	 * Check action permissions not already checked in checkQuickPermissions
2210
	 *
2211
	 * @param string $action The action to check
2212
	 * @param User $user User to check
2213
	 * @param array $errors List of current errors
2214
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2215
	 * @param bool $short Short circuit on first error
2216
	 *
2217
	 * @return array List of errors
2218
	 */
2219
	private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) {
2220
		global $wgDeleteRevisionsLimit, $wgLang;
2221
2222
		if ( $action == 'protect' ) {
2223
			if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
2224
				// If they can't edit, they shouldn't protect.
2225
				$errors[] = [ 'protect-cantedit' ];
2226
			}
2227
		} elseif ( $action == 'create' ) {
2228
			$title_protection = $this->getTitleProtection();
2229
			if ( $title_protection ) {
2230
				if ( $title_protection['permission'] == ''
2231
					|| !$user->isAllowed( $title_protection['permission'] )
2232
				) {
2233
					$errors[] = [
2234
						'titleprotected',
2235
						User::whoIs( $title_protection['user'] ),
2236
						$title_protection['reason']
2237
					];
2238
				}
2239
			}
2240
		} elseif ( $action == 'move' ) {
2241
			// Check for immobile pages
2242
			if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2243
				// Specific message for this case
2244
				$errors[] = [ 'immobile-source-namespace', $this->getNsText() ];
2245
			} elseif ( !$this->isMovable() ) {
2246
				// Less specific message for rarer cases
2247
				$errors[] = [ 'immobile-source-page' ];
2248
			}
2249
		} elseif ( $action == 'move-target' ) {
2250
			if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2251
				$errors[] = [ 'immobile-target-namespace', $this->getNsText() ];
2252
			} elseif ( !$this->isMovable() ) {
2253
				$errors[] = [ 'immobile-target-page' ];
2254
			}
2255
		} elseif ( $action == 'delete' ) {
2256
			$tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true );
2257
			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...
2258
				$tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
2259
					$user, $tempErrors, $rigor, true );
2260
			}
2261
			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...
2262
				// If protection keeps them from editing, they shouldn't be able to delete.
2263
				$errors[] = [ 'deleteprotected' ];
2264
			}
2265
			if ( $rigor !== 'quick' && $wgDeleteRevisionsLimit
2266
				&& !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion()
2267
			) {
2268
				$errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
2269
			}
2270
		}
2271
		return $errors;
2272
	}
2273
2274
	/**
2275
	 * Check that the user isn't blocked from editing.
2276
	 *
2277
	 * @param string $action The action to check
2278
	 * @param User $user User to check
2279
	 * @param array $errors List of current errors
2280
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2281
	 * @param bool $short Short circuit on first error
2282
	 *
2283
	 * @return array List of errors
2284
	 */
2285
	private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
2286
		// Account creation blocks handled at userlogin.
2287
		// Unblocking handled in SpecialUnblock
2288
		if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
2289
			return $errors;
2290
		}
2291
2292
		global $wgEmailConfirmToEdit;
2293
2294
		if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
2295
			$errors[] = [ 'confirmedittext' ];
2296
		}
2297
2298
		$useSlave = ( $rigor !== 'secure' );
2299
		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...
2300
			&& !$user->isBlockedFrom( $this, $useSlave )
2301
		) {
2302
			// Don't block the user from editing their own talk page unless they've been
2303
			// explicitly blocked from that too.
2304
		} elseif ( $user->isBlocked() && $user->getBlock()->prevents( $action ) !== false ) {
2305
			// @todo FIXME: Pass the relevant context into this function.
2306
			$errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() );
2307
		}
2308
2309
		return $errors;
2310
	}
2311
2312
	/**
2313
	 * Check that the user is allowed to read this page.
2314
	 *
2315
	 * @param string $action The action to check
2316
	 * @param User $user User to check
2317
	 * @param array $errors List of current errors
2318
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2319
	 * @param bool $short Short circuit on first error
2320
	 *
2321
	 * @return array List of errors
2322
	 */
2323
	private function checkReadPermissions( $action, $user, $errors, $rigor, $short ) {
2324
		global $wgWhitelistRead, $wgWhitelistReadRegexp;
2325
2326
		$whitelisted = false;
2327
		if ( User::isEveryoneAllowed( 'read' ) ) {
2328
			# Shortcut for public wikis, allows skipping quite a bit of code
2329
			$whitelisted = true;
2330
		} elseif ( $user->isAllowed( 'read' ) ) {
2331
			# If the user is allowed to read pages, he is allowed to read all pages
2332
			$whitelisted = true;
2333
		} elseif ( $this->isSpecial( 'Userlogin' )
2334
			|| $this->isSpecial( 'ChangePassword' )
2335
			|| $this->isSpecial( 'PasswordReset' )
2336
		) {
2337
			# Always grant access to the login page.
2338
			# Even anons need to be able to log in.
2339
			$whitelisted = true;
2340
		} elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
2341
			# Time to check the whitelist
2342
			# Only do these checks is there's something to check against
2343
			$name = $this->getPrefixedText();
2344
			$dbName = $this->getPrefixedDBkey();
2345
2346
			// Check for explicit whitelisting with and without underscores
2347
			if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
2348
				$whitelisted = true;
2349
			} elseif ( $this->getNamespace() == NS_MAIN ) {
2350
				# Old settings might have the title prefixed with
2351
				# a colon for main-namespace pages
2352
				if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
2353
					$whitelisted = true;
2354
				}
2355
			} elseif ( $this->isSpecialPage() ) {
2356
				# If it's a special page, ditch the subpage bit and check again
2357
				$name = $this->getDBkey();
2358
				list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name );
2359
				if ( $name ) {
2360
					$pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
2361
					if ( in_array( $pure, $wgWhitelistRead, true ) ) {
2362
						$whitelisted = true;
2363
					}
2364
				}
2365
			}
2366
		}
2367
2368
		if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) {
2369
			$name = $this->getPrefixedText();
2370
			// Check for regex whitelisting
2371
			foreach ( $wgWhitelistReadRegexp as $listItem ) {
2372
				if ( preg_match( $listItem, $name ) ) {
2373
					$whitelisted = true;
2374
					break;
2375
				}
2376
			}
2377
		}
2378
2379
		if ( !$whitelisted ) {
2380
			# If the title is not whitelisted, give extensions a chance to do so...
2381
			Hooks::run( 'TitleReadWhitelist', [ $this, $user, &$whitelisted ] );
2382
			if ( !$whitelisted ) {
2383
				$errors[] = $this->missingPermissionError( $action, $short );
2384
			}
2385
		}
2386
2387
		return $errors;
2388
	}
2389
2390
	/**
2391
	 * Get a description array when the user doesn't have the right to perform
2392
	 * $action (i.e. when User::isAllowed() returns false)
2393
	 *
2394
	 * @param string $action The action to check
2395
	 * @param bool $short Short circuit on first error
2396
	 * @return array List of errors
2397
	 */
2398
	private function missingPermissionError( $action, $short ) {
2399
		// We avoid expensive display logic for quickUserCan's and such
2400
		if ( $short ) {
2401
			return [ 'badaccess-group0' ];
2402
		}
2403
2404
		$groups = array_map( [ 'User', 'makeGroupLinkWiki' ],
2405
			User::getGroupsWithPermission( $action ) );
2406
2407
		if ( count( $groups ) ) {
2408
			global $wgLang;
2409
			return [
2410
				'badaccess-groups',
2411
				$wgLang->commaList( $groups ),
2412
				count( $groups )
2413
			];
2414
		} else {
2415
			return [ 'badaccess-group0' ];
2416
		}
2417
	}
2418
2419
	/**
2420
	 * Can $user perform $action on this page? This is an internal function,
2421
	 * with multiple levels of checks depending on performance needs; see $rigor below.
2422
	 * It does not check wfReadOnly().
2423
	 *
2424
	 * @param string $action Action that permission needs to be checked for
2425
	 * @param User $user User to check
2426
	 * @param string $rigor One of (quick,full,secure)
2427
	 *   - quick  : does cheap permission checks from slaves (usable for GUI creation)
2428
	 *   - full   : does cheap and expensive checks possibly from a slave
2429
	 *   - secure : does cheap and expensive checks, using the master as needed
2430
	 * @param bool $short Set this to true to stop after the first permission error.
2431
	 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
2432
	 */
2433
	protected function getUserPermissionsErrorsInternal(
2434
		$action, $user, $rigor = 'secure', $short = false
2435
	) {
2436
		if ( $rigor === true ) {
2437
			$rigor = 'secure'; // b/c
2438
		} elseif ( $rigor === false ) {
2439
			$rigor = 'quick'; // b/c
2440
		} elseif ( !in_array( $rigor, [ 'quick', 'full', 'secure' ] ) ) {
2441
			throw new Exception( "Invalid rigor parameter '$rigor'." );
2442
		}
2443
2444
		# Read has special handling
2445
		if ( $action == 'read' ) {
2446
			$checks = [
2447
				'checkPermissionHooks',
2448
				'checkReadPermissions',
2449
			];
2450
		# Don't call checkSpecialsAndNSPermissions or checkCSSandJSPermissions
2451
		# here as it will lead to duplicate error messages. This is okay to do
2452
		# since anywhere that checks for create will also check for edit, and
2453
		# those checks are called for edit.
2454
		} elseif ( $action == 'create' ) {
2455
			$checks = [
2456
				'checkQuickPermissions',
2457
				'checkPermissionHooks',
2458
				'checkPageRestrictions',
2459
				'checkCascadingSourcesRestrictions',
2460
				'checkActionPermissions',
2461
				'checkUserBlock'
2462
			];
2463
		} else {
2464
			$checks = [
2465
				'checkQuickPermissions',
2466
				'checkPermissionHooks',
2467
				'checkSpecialsAndNSPermissions',
2468
				'checkCSSandJSPermissions',
2469
				'checkPageRestrictions',
2470
				'checkCascadingSourcesRestrictions',
2471
				'checkActionPermissions',
2472
				'checkUserBlock'
2473
			];
2474
		}
2475
2476
		$errors = [];
2477
		while ( count( $checks ) > 0 &&
2478
				!( $short && count( $errors ) > 0 ) ) {
2479
			$method = array_shift( $checks );
2480
			$errors = $this->$method( $action, $user, $errors, $rigor, $short );
2481
		}
2482
2483
		return $errors;
2484
	}
2485
2486
	/**
2487
	 * Get a filtered list of all restriction types supported by this wiki.
2488
	 * @param bool $exists True to get all restriction types that apply to
2489
	 * titles that do exist, False for all restriction types that apply to
2490
	 * titles that do not exist
2491
	 * @return array
2492
	 */
2493
	public static function getFilteredRestrictionTypes( $exists = true ) {
2494
		global $wgRestrictionTypes;
2495
		$types = $wgRestrictionTypes;
2496
		if ( $exists ) {
2497
			# Remove the create restriction for existing titles
2498
			$types = array_diff( $types, [ 'create' ] );
2499
		} else {
2500
			# Only the create and upload restrictions apply to non-existing titles
2501
			$types = array_intersect( $types, [ 'create', 'upload' ] );
2502
		}
2503
		return $types;
2504
	}
2505
2506
	/**
2507
	 * Returns restriction types for the current Title
2508
	 *
2509
	 * @return array Applicable restriction types
2510
	 */
2511
	public function getRestrictionTypes() {
2512
		if ( $this->isSpecialPage() ) {
2513
			return [];
2514
		}
2515
2516
		$types = self::getFilteredRestrictionTypes( $this->exists() );
2517
2518
		if ( $this->getNamespace() != NS_FILE ) {
2519
			# Remove the upload restriction for non-file titles
2520
			$types = array_diff( $types, [ 'upload' ] );
2521
		}
2522
2523
		Hooks::run( 'TitleGetRestrictionTypes', [ $this, &$types ] );
2524
2525
		wfDebug( __METHOD__ . ': applicable restrictions to [[' .
2526
			$this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
2527
2528
		return $types;
2529
	}
2530
2531
	/**
2532
	 * Is this title subject to title protection?
2533
	 * Title protection is the one applied against creation of such title.
2534
	 *
2535
	 * @return array|bool An associative array representing any existent title
2536
	 *   protection, or false if there's none.
2537
	 */
2538
	public function getTitleProtection() {
2539
		// Can't protect pages in special namespaces
2540
		if ( $this->getNamespace() < 0 ) {
2541
			return false;
2542
		}
2543
2544
		// Can't protect pages that exist.
2545
		if ( $this->exists() ) {
2546
			return false;
2547
		}
2548
2549
		if ( $this->mTitleProtection === null ) {
2550
			$dbr = wfGetDB( DB_SLAVE );
2551
			$res = $dbr->select(
2552
				'protected_titles',
2553
				[
2554
					'user' => 'pt_user',
2555
					'reason' => 'pt_reason',
2556
					'expiry' => 'pt_expiry',
2557
					'permission' => 'pt_create_perm'
2558
				],
2559
				[ 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ],
2560
				__METHOD__
2561
			);
2562
2563
			// fetchRow returns false if there are no rows.
2564
			$row = $dbr->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $dbr->select('protected_...etDBkey()), __METHOD__) on line 2551 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...
2565
			if ( $row ) {
2566
				if ( $row['permission'] == 'sysop' ) {
2567
					$row['permission'] = 'editprotected'; // B/C
2568
				}
2569
				if ( $row['permission'] == 'autoconfirmed' ) {
2570
					$row['permission'] = 'editsemiprotected'; // B/C
2571
				}
2572
				$row['expiry'] = $dbr->decodeExpiry( $row['expiry'] );
2573
			}
2574
			$this->mTitleProtection = $row;
2575
		}
2576
		return $this->mTitleProtection;
2577
	}
2578
2579
	/**
2580
	 * Remove any title protection due to page existing
2581
	 */
2582
	public function deleteTitleProtection() {
2583
		$dbw = wfGetDB( DB_MASTER );
2584
2585
		$dbw->delete(
2586
			'protected_titles',
2587
			[ 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ],
2588
			__METHOD__
2589
		);
2590
		$this->mTitleProtection = false;
2591
	}
2592
2593
	/**
2594
	 * Is this page "semi-protected" - the *only* protection levels are listed
2595
	 * in $wgSemiprotectedRestrictionLevels?
2596
	 *
2597
	 * @param string $action Action to check (default: edit)
2598
	 * @return bool
2599
	 */
2600
	public function isSemiProtected( $action = 'edit' ) {
2601
		global $wgSemiprotectedRestrictionLevels;
2602
2603
		$restrictions = $this->getRestrictions( $action );
2604
		$semi = $wgSemiprotectedRestrictionLevels;
2605
		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...
2606
			// Not protected, or all protection is full protection
2607
			return false;
2608
		}
2609
2610
		// Remap autoconfirmed to editsemiprotected for BC
2611
		foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) {
2612
			$semi[$key] = 'editsemiprotected';
2613
		}
2614
		foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) {
2615
			$restrictions[$key] = 'editsemiprotected';
2616
		}
2617
2618
		return !array_diff( $restrictions, $semi );
2619
	}
2620
2621
	/**
2622
	 * Does the title correspond to a protected article?
2623
	 *
2624
	 * @param string $action The action the page is protected from,
2625
	 * by default checks all actions.
2626
	 * @return bool
2627
	 */
2628
	public function isProtected( $action = '' ) {
2629
		global $wgRestrictionLevels;
2630
2631
		$restrictionTypes = $this->getRestrictionTypes();
2632
2633
		# Special pages have inherent protection
2634
		if ( $this->isSpecialPage() ) {
2635
			return true;
2636
		}
2637
2638
		# Check regular protection levels
2639
		foreach ( $restrictionTypes as $type ) {
2640
			if ( $action == $type || $action == '' ) {
2641
				$r = $this->getRestrictions( $type );
2642
				foreach ( $wgRestrictionLevels as $level ) {
2643
					if ( in_array( $level, $r ) && $level != '' ) {
2644
						return true;
2645
					}
2646
				}
2647
			}
2648
		}
2649
2650
		return false;
2651
	}
2652
2653
	/**
2654
	 * Determines if $user is unable to edit this page because it has been protected
2655
	 * by $wgNamespaceProtection.
2656
	 *
2657
	 * @param User $user User object to check permissions
2658
	 * @return bool
2659
	 */
2660
	public function isNamespaceProtected( User $user ) {
2661
		global $wgNamespaceProtection;
2662
2663
		if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
2664
			foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
2665
				if ( $right != '' && !$user->isAllowed( $right ) ) {
2666
					return true;
2667
				}
2668
			}
2669
		}
2670
		return false;
2671
	}
2672
2673
	/**
2674
	 * Cascading protection: Return true if cascading restrictions apply to this page, false if not.
2675
	 *
2676
	 * @return bool If the page is subject to cascading restrictions.
2677
	 */
2678
	public function isCascadeProtected() {
2679
		list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
2680
		return ( $sources > 0 );
2681
	}
2682
2683
	/**
2684
	 * Determines whether cascading protection sources have already been loaded from
2685
	 * the database.
2686
	 *
2687
	 * @param bool $getPages True to check if the pages are loaded, or false to check
2688
	 * if the status is loaded.
2689
	 * @return bool Whether or not the specified information has been loaded
2690
	 * @since 1.23
2691
	 */
2692
	public function areCascadeProtectionSourcesLoaded( $getPages = true ) {
2693
		return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null;
2694
	}
2695
2696
	/**
2697
	 * Cascading protection: Get the source of any cascading restrictions on this page.
2698
	 *
2699
	 * @param bool $getPages Whether or not to retrieve the actual pages
2700
	 *        that the restrictions have come from and the actual restrictions
2701
	 *        themselves.
2702
	 * @return array Two elements: First is an array of Title objects of the
2703
	 *        pages from which cascading restrictions have come, false for
2704
	 *        none, or true if such restrictions exist but $getPages was not
2705
	 *        set. Second is an array like that returned by
2706
	 *        Title::getAllRestrictions(), or an empty array if $getPages is
2707
	 *        false.
2708
	 */
2709
	public function getCascadeProtectionSources( $getPages = true ) {
2710
		$pagerestrictions = [];
2711
2712
		if ( $this->mCascadeSources !== null && $getPages ) {
2713
			return [ $this->mCascadeSources, $this->mCascadingRestrictions ];
2714
		} elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) {
2715
			return [ $this->mHasCascadingRestrictions, $pagerestrictions ];
2716
		}
2717
2718
		$dbr = wfGetDB( DB_SLAVE );
2719
2720
		if ( $this->getNamespace() == NS_FILE ) {
2721
			$tables = [ 'imagelinks', 'page_restrictions' ];
2722
			$where_clauses = [
2723
				'il_to' => $this->getDBkey(),
2724
				'il_from=pr_page',
2725
				'pr_cascade' => 1
2726
			];
2727
		} else {
2728
			$tables = [ 'templatelinks', 'page_restrictions' ];
2729
			$where_clauses = [
2730
				'tl_namespace' => $this->getNamespace(),
2731
				'tl_title' => $this->getDBkey(),
2732
				'tl_from=pr_page',
2733
				'pr_cascade' => 1
2734
			];
2735
		}
2736
2737
		if ( $getPages ) {
2738
			$cols = [ 'pr_page', 'page_namespace', 'page_title',
2739
				'pr_expiry', 'pr_type', 'pr_level' ];
2740
			$where_clauses[] = 'page_id=pr_page';
2741
			$tables[] = 'page';
2742
		} else {
2743
			$cols = [ 'pr_expiry' ];
2744
		}
2745
2746
		$res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
2747
2748
		$sources = $getPages ? [] : false;
2749
		$now = wfTimestampNow();
2750
2751
		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...
2752
			$expiry = $dbr->decodeExpiry( $row->pr_expiry );
2753
			if ( $expiry > $now ) {
2754
				if ( $getPages ) {
2755
					$page_id = $row->pr_page;
2756
					$page_ns = $row->page_namespace;
2757
					$page_title = $row->page_title;
2758
					$sources[$page_id] = Title::makeTitle( $page_ns, $page_title );
2759
					# Add groups needed for each restriction type if its not already there
2760
					# Make sure this restriction type still exists
2761
2762
					if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
2763
						$pagerestrictions[$row->pr_type] = [];
2764
					}
2765
2766
					if (
2767
						isset( $pagerestrictions[$row->pr_type] )
2768
						&& !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] )
2769
					) {
2770
						$pagerestrictions[$row->pr_type][] = $row->pr_level;
2771
					}
2772
				} else {
2773
					$sources = true;
2774
				}
2775
			}
2776
		}
2777
2778
		if ( $getPages ) {
2779
			$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...
2780
			$this->mCascadingRestrictions = $pagerestrictions;
2781
		} else {
2782
			$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...
2783
		}
2784
2785
		return [ $sources, $pagerestrictions ];
2786
	}
2787
2788
	/**
2789
	 * Accessor for mRestrictionsLoaded
2790
	 *
2791
	 * @return bool Whether or not the page's restrictions have already been
2792
	 * loaded from the database
2793
	 * @since 1.23
2794
	 */
2795
	public function areRestrictionsLoaded() {
2796
		return $this->mRestrictionsLoaded;
2797
	}
2798
2799
	/**
2800
	 * Accessor/initialisation for mRestrictions
2801
	 *
2802
	 * @param string $action Action that permission needs to be checked for
2803
	 * @return array Restriction levels needed to take the action. All levels are
2804
	 *     required. Note that restriction levels are normally user rights, but 'sysop'
2805
	 *     and 'autoconfirmed' are also allowed for backwards compatibility. These should
2806
	 *     be mapped to 'editprotected' and 'editsemiprotected' respectively.
2807
	 */
2808
	public function getRestrictions( $action ) {
2809
		if ( !$this->mRestrictionsLoaded ) {
2810
			$this->loadRestrictions();
2811
		}
2812
		return isset( $this->mRestrictions[$action] )
2813
				? $this->mRestrictions[$action]
2814
				: [];
2815
	}
2816
2817
	/**
2818
	 * Accessor/initialisation for mRestrictions
2819
	 *
2820
	 * @return array Keys are actions, values are arrays as returned by
2821
	 *     Title::getRestrictions()
2822
	 * @since 1.23
2823
	 */
2824
	public function getAllRestrictions() {
2825
		if ( !$this->mRestrictionsLoaded ) {
2826
			$this->loadRestrictions();
2827
		}
2828
		return $this->mRestrictions;
2829
	}
2830
2831
	/**
2832
	 * Get the expiry time for the restriction against a given action
2833
	 *
2834
	 * @param string $action
2835
	 * @return string|bool 14-char timestamp, or 'infinity' if the page is protected forever
2836
	 *     or not protected at all, or false if the action is not recognised.
2837
	 */
2838
	public function getRestrictionExpiry( $action ) {
2839
		if ( !$this->mRestrictionsLoaded ) {
2840
			$this->loadRestrictions();
2841
		}
2842
		return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
2843
	}
2844
2845
	/**
2846
	 * Returns cascading restrictions for the current article
2847
	 *
2848
	 * @return bool
2849
	 */
2850
	function areRestrictionsCascading() {
2851
		if ( !$this->mRestrictionsLoaded ) {
2852
			$this->loadRestrictions();
2853
		}
2854
2855
		return $this->mCascadeRestriction;
2856
	}
2857
2858
	/**
2859
	 * Loads a string into mRestrictions array
2860
	 *
2861
	 * @param ResultWrapper $res Resource restrictions as an SQL result.
2862
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2863
	 *        restrictions from page table (pre 1.10)
2864
	 */
2865
	private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) {
2866
		$rows = [];
2867
2868
		foreach ( $res as $row ) {
2869
			$rows[] = $row;
2870
		}
2871
2872
		$this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
2873
	}
2874
2875
	/**
2876
	 * Compiles list of active page restrictions from both page table (pre 1.10)
2877
	 * and page_restrictions table for this existing page.
2878
	 * Public for usage by LiquidThreads.
2879
	 *
2880
	 * @param array $rows Array of db result objects
2881
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2882
	 *   restrictions from page table (pre 1.10)
2883
	 */
2884
	public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
2885
		$dbr = wfGetDB( DB_SLAVE );
2886
2887
		$restrictionTypes = $this->getRestrictionTypes();
2888
2889
		foreach ( $restrictionTypes as $type ) {
2890
			$this->mRestrictions[$type] = [];
2891
			$this->mRestrictionsExpiry[$type] = 'infinity';
2892
		}
2893
2894
		$this->mCascadeRestriction = false;
2895
2896
		# Backwards-compatibility: also load the restrictions from the page record (old format).
2897
		if ( $oldFashionedRestrictions !== null ) {
2898
			$this->mOldRestrictions = $oldFashionedRestrictions;
2899
		}
2900
2901
		if ( $this->mOldRestrictions === false ) {
2902
			$this->mOldRestrictions = $dbr->selectField( 'page', 'page_restrictions',
2903
				[ 'page_id' => $this->getArticleID() ], __METHOD__ );
2904
		}
2905
2906
		if ( $this->mOldRestrictions != '' ) {
2907
			foreach ( explode( ':', trim( $this->mOldRestrictions ) ) as $restrict ) {
2908
				$temp = explode( '=', trim( $restrict ) );
2909
				if ( count( $temp ) == 1 ) {
2910
					// old old format should be treated as edit/move restriction
2911
					$this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
2912
					$this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
2913
				} else {
2914
					$restriction = trim( $temp[1] );
2915
					if ( $restriction != '' ) { // some old entries are empty
2916
						$this->mRestrictions[$temp[0]] = explode( ',', $restriction );
2917
					}
2918
				}
2919
			}
2920
		}
2921
2922
		if ( count( $rows ) ) {
2923
			# Current system - load second to make them override.
2924
			$now = wfTimestampNow();
2925
2926
			# Cycle through all the restrictions.
2927
			foreach ( $rows as $row ) {
2928
2929
				// Don't take care of restrictions types that aren't allowed
2930
				if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
2931
					continue;
2932
				}
2933
2934
				// This code should be refactored, now that it's being used more generally,
2935
				// But I don't really see any harm in leaving it in Block for now -werdna
2936
				$expiry = $dbr->decodeExpiry( $row->pr_expiry );
2937
2938
				// Only apply the restrictions if they haven't expired!
2939
				if ( !$expiry || $expiry > $now ) {
2940
					$this->mRestrictionsExpiry[$row->pr_type] = $expiry;
2941
					$this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
2942
2943
					$this->mCascadeRestriction |= $row->pr_cascade;
2944
				}
2945
			}
2946
		}
2947
2948
		$this->mRestrictionsLoaded = true;
2949
	}
2950
2951
	/**
2952
	 * Load restrictions from the page_restrictions table
2953
	 *
2954
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2955
	 *   restrictions from page table (pre 1.10)
2956
	 */
2957
	public function loadRestrictions( $oldFashionedRestrictions = null ) {
2958
		if ( !$this->mRestrictionsLoaded ) {
2959
			$dbr = wfGetDB( DB_SLAVE );
2960
			if ( $this->exists() ) {
2961
				$res = $dbr->select(
2962
					'page_restrictions',
2963
					[ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ],
2964
					[ 'pr_page' => $this->getArticleID() ],
2965
					__METHOD__
2966
				);
2967
2968
				$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 2961 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...
2969
			} else {
2970
				$title_protection = $this->getTitleProtection();
2971
2972
				if ( $title_protection ) {
2973
					$now = wfTimestampNow();
2974
					$expiry = $dbr->decodeExpiry( $title_protection['expiry'] );
2975
2976
					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...
2977
						// Apply the restrictions
2978
						$this->mRestrictionsExpiry['create'] = $expiry;
2979
						$this->mRestrictions['create'] = explode( ',', trim( $title_protection['permission'] ) );
2980
					} else { // Get rid of the old restrictions
2981
						$this->mTitleProtection = false;
2982
					}
2983
				} else {
2984
					$this->mRestrictionsExpiry['create'] = 'infinity';
2985
				}
2986
				$this->mRestrictionsLoaded = true;
2987
			}
2988
		}
2989
	}
2990
2991
	/**
2992
	 * Flush the protection cache in this object and force reload from the database.
2993
	 * This is used when updating protection from WikiPage::doUpdateRestrictions().
2994
	 */
2995
	public function flushRestrictions() {
2996
		$this->mRestrictionsLoaded = false;
2997
		$this->mTitleProtection = null;
2998
	}
2999
3000
	/**
3001
	 * Purge expired restrictions from the page_restrictions table
3002
	 */
3003
	static function purgeExpiredRestrictions() {
3004
		if ( wfReadOnly() ) {
3005
			return;
3006
		}
3007
3008
		DeferredUpdates::addUpdate( new AtomicSectionUpdate(
3009
			wfGetDB( DB_MASTER ),
3010
			__METHOD__,
3011
			function ( IDatabase $dbw, $fname ) {
3012
				$dbw->delete(
3013
					'page_restrictions',
3014
					[ 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3015
					$fname
3016
				);
3017
				$dbw->delete(
3018
					'protected_titles',
3019
					[ 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3020
					$fname
3021
				);
3022
			}
3023
		) );
3024
	}
3025
3026
	/**
3027
	 * Does this have subpages?  (Warning, usually requires an extra DB query.)
3028
	 *
3029
	 * @return bool
3030
	 */
3031
	public function hasSubpages() {
3032
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
3033
			# Duh
3034
			return false;
3035
		}
3036
3037
		# We dynamically add a member variable for the purpose of this method
3038
		# alone to cache the result.  There's no point in having it hanging
3039
		# around uninitialized in every Title object; therefore we only add it
3040
		# if needed and don't declare it statically.
3041
		if ( $this->mHasSubpages === null ) {
3042
			$this->mHasSubpages = false;
3043
			$subpages = $this->getSubpages( 1 );
3044
			if ( $subpages instanceof TitleArray ) {
3045
				$this->mHasSubpages = (bool)$subpages->count();
3046
			}
3047
		}
3048
3049
		return $this->mHasSubpages;
3050
	}
3051
3052
	/**
3053
	 * Get all subpages of this page.
3054
	 *
3055
	 * @param int $limit Maximum number of subpages to fetch; -1 for no limit
3056
	 * @return TitleArray|array TitleArray, or empty array if this page's namespace
3057
	 *  doesn't allow subpages
3058
	 */
3059
	public function getSubpages( $limit = -1 ) {
3060
		if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3061
			return [];
3062
		}
3063
3064
		$dbr = wfGetDB( DB_SLAVE );
3065
		$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...
3066
		$conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
3067
		$options = [];
3068
		if ( $limit > -1 ) {
3069
			$options['LIMIT'] = $limit;
3070
		}
3071
		$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...
3072
			$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...
3073
				[ 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ],
3074
				$conds,
3075
				__METHOD__,
3076
				$options
3077
			)
3078
		);
3079
		return $this->mSubpages;
3080
	}
3081
3082
	/**
3083
	 * Is there a version of this page in the deletion archive?
3084
	 *
3085
	 * @return int The number of archived revisions
3086
	 */
3087
	public function isDeleted() {
3088
		if ( $this->getNamespace() < 0 ) {
3089
			$n = 0;
3090
		} else {
3091
			$dbr = wfGetDB( DB_SLAVE );
3092
3093
			$n = $dbr->selectField( 'archive', 'COUNT(*)',
3094
				[ 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ],
3095
				__METHOD__
3096
			);
3097 View Code Duplication
			if ( $this->getNamespace() == NS_FILE ) {
3098
				$n += $dbr->selectField( 'filearchive', 'COUNT(*)',
3099
					[ 'fa_name' => $this->getDBkey() ],
3100
					__METHOD__
3101
				);
3102
			}
3103
		}
3104
		return (int)$n;
3105
	}
3106
3107
	/**
3108
	 * Is there a version of this page in the deletion archive?
3109
	 *
3110
	 * @return bool
3111
	 */
3112
	public function isDeletedQuick() {
3113
		if ( $this->getNamespace() < 0 ) {
3114
			return false;
3115
		}
3116
		$dbr = wfGetDB( DB_SLAVE );
3117
		$deleted = (bool)$dbr->selectField( 'archive', '1',
3118
			[ 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ],
3119
			__METHOD__
3120
		);
3121 View Code Duplication
		if ( !$deleted && $this->getNamespace() == NS_FILE ) {
3122
			$deleted = (bool)$dbr->selectField( 'filearchive', '1',
3123
				[ 'fa_name' => $this->getDBkey() ],
3124
				__METHOD__
3125
			);
3126
		}
3127
		return $deleted;
3128
	}
3129
3130
	/**
3131
	 * Get the article ID for this Title from the link cache,
3132
	 * adding it if necessary
3133
	 *
3134
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select
3135
	 *  for update
3136
	 * @return int The ID
3137
	 */
3138
	public function getArticleID( $flags = 0 ) {
3139
		if ( $this->getNamespace() < 0 ) {
3140
			$this->mArticleID = 0;
3141
			return $this->mArticleID;
3142
		}
3143
		$linkCache = LinkCache::singleton();
3144
		if ( $flags & self::GAID_FOR_UPDATE ) {
3145
			$oldUpdate = $linkCache->forUpdate( true );
3146
			$linkCache->clearLink( $this );
3147
			$this->mArticleID = $linkCache->addLinkObj( $this );
3148
			$linkCache->forUpdate( $oldUpdate );
3149
		} else {
3150
			if ( -1 == $this->mArticleID ) {
3151
				$this->mArticleID = $linkCache->addLinkObj( $this );
3152
			}
3153
		}
3154
		return $this->mArticleID;
3155
	}
3156
3157
	/**
3158
	 * Is this an article that is a redirect page?
3159
	 * Uses link cache, adding it if necessary
3160
	 *
3161
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3162
	 * @return bool
3163
	 */
3164
	public function isRedirect( $flags = 0 ) {
3165
		if ( !is_null( $this->mRedirect ) ) {
3166
			return $this->mRedirect;
3167
		}
3168
		if ( !$this->getArticleID( $flags ) ) {
3169
			$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...
3170
			return $this->mRedirect;
3171
		}
3172
3173
		$linkCache = LinkCache::singleton();
3174
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3175
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
3176
		if ( $cached === null ) {
3177
			# Trust LinkCache's state over our own
3178
			# LinkCache is telling us that the page doesn't exist, despite there being cached
3179
			# data relating to an existing page in $this->mArticleID. Updaters should clear
3180
			# LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is
3181
			# set, then LinkCache will definitely be up to date here, since getArticleID() forces
3182
			# LinkCache to refresh its data from the master.
3183
			$this->mRedirect = false;
3184
			return $this->mRedirect;
3185
		}
3186
3187
		$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...
3188
3189
		return $this->mRedirect;
3190
	}
3191
3192
	/**
3193
	 * What is the length of this page?
3194
	 * Uses link cache, adding it if necessary
3195
	 *
3196
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3197
	 * @return int
3198
	 */
3199
	public function getLength( $flags = 0 ) {
3200
		if ( $this->mLength != -1 ) {
3201
			return $this->mLength;
3202
		}
3203
		if ( !$this->getArticleID( $flags ) ) {
3204
			$this->mLength = 0;
3205
			return $this->mLength;
3206
		}
3207
		$linkCache = LinkCache::singleton();
3208
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3209
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
3210
		if ( $cached === null ) {
3211
			# Trust LinkCache's state over our own, as for isRedirect()
3212
			$this->mLength = 0;
3213
			return $this->mLength;
3214
		}
3215
3216
		$this->mLength = intval( $cached );
3217
3218
		return $this->mLength;
3219
	}
3220
3221
	/**
3222
	 * What is the page_latest field for this page?
3223
	 *
3224
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3225
	 * @return int Int or 0 if the page doesn't exist
3226
	 */
3227
	public function getLatestRevID( $flags = 0 ) {
3228
		if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
3229
			return intval( $this->mLatestID );
3230
		}
3231
		if ( !$this->getArticleID( $flags ) ) {
3232
			$this->mLatestID = 0;
3233
			return $this->mLatestID;
3234
		}
3235
		$linkCache = LinkCache::singleton();
3236
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3237
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
3238
		if ( $cached === null ) {
3239
			# Trust LinkCache's state over our own, as for isRedirect()
3240
			$this->mLatestID = 0;
3241
			return $this->mLatestID;
3242
		}
3243
3244
		$this->mLatestID = intval( $cached );
3245
3246
		return $this->mLatestID;
3247
	}
3248
3249
	/**
3250
	 * This clears some fields in this object, and clears any associated
3251
	 * keys in the "bad links" section of the link cache.
3252
	 *
3253
	 * - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow
3254
	 * loading of the new page_id. It's also called from
3255
	 * WikiPage::doDeleteArticleReal()
3256
	 *
3257
	 * @param int $newid The new Article ID
3258
	 */
3259
	public function resetArticleID( $newid ) {
3260
		$linkCache = LinkCache::singleton();
3261
		$linkCache->clearLink( $this );
3262
3263
		if ( $newid === false ) {
3264
			$this->mArticleID = -1;
3265
		} else {
3266
			$this->mArticleID = intval( $newid );
3267
		}
3268
		$this->mRestrictionsLoaded = false;
3269
		$this->mRestrictions = [];
3270
		$this->mOldRestrictions = false;
3271
		$this->mRedirect = null;
3272
		$this->mLength = -1;
3273
		$this->mLatestID = false;
3274
		$this->mContentModel = false;
3275
		$this->mEstimateRevisions = null;
3276
		$this->mPageLanguage = false;
3277
		$this->mDbPageLanguage = false;
3278
		$this->mIsBigDeletion = null;
3279
	}
3280
3281
	public static function clearCaches() {
3282
		$linkCache = LinkCache::singleton();
3283
		$linkCache->clear();
3284
3285
		$titleCache = self::getTitleCache();
3286
		$titleCache->clear();
3287
	}
3288
3289
	/**
3290
	 * Capitalize a text string for a title if it belongs to a namespace that capitalizes
3291
	 *
3292
	 * @param string $text Containing title to capitalize
3293
	 * @param int $ns Namespace index, defaults to NS_MAIN
3294
	 * @return string Containing capitalized title
3295
	 */
3296
	public static function capitalize( $text, $ns = NS_MAIN ) {
3297
		global $wgContLang;
3298
3299
		if ( MWNamespace::isCapitalized( $ns ) ) {
3300
			return $wgContLang->ucfirst( $text );
3301
		} else {
3302
			return $text;
3303
		}
3304
	}
3305
3306
	/**
3307
	 * Secure and split - main initialisation function for this object
3308
	 *
3309
	 * Assumes that mDbkeyform has been set, and is urldecoded
3310
	 * and uses underscores, but not otherwise munged.  This function
3311
	 * removes illegal characters, splits off the interwiki and
3312
	 * namespace prefixes, sets the other forms, and canonicalizes
3313
	 * everything.
3314
	 *
3315
	 * @throws MalformedTitleException On invalid titles
3316
	 * @return bool True on success
3317
	 */
3318
	private function secureAndSplit() {
3319
		# Initialisation
3320
		$this->mInterwiki = '';
3321
		$this->mFragment = '';
3322
		$this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
3323
3324
		$dbkey = $this->mDbkeyform;
3325
3326
		// @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
3327
		//        the parsing code with Title, while avoiding massive refactoring.
3328
		// @todo: get rid of secureAndSplit, refactor parsing code.
3329
		$titleParser = self::getMediaWikiTitleCodec();
3330
		// MalformedTitleException can be thrown here
3331
		$parts = $titleParser->splitTitleString( $dbkey, $this->getDefaultNamespace() );
3332
3333
		# Fill fields
3334
		$this->setFragment( '#' . $parts['fragment'] );
3335
		$this->mInterwiki = $parts['interwiki'];
3336
		$this->mLocalInterwiki = $parts['local_interwiki'];
3337
		$this->mNamespace = $parts['namespace'];
3338
		$this->mUserCaseDBKey = $parts['user_case_dbkey'];
3339
3340
		$this->mDbkeyform = $parts['dbkey'];
3341
		$this->mUrlform = wfUrlencode( $this->mDbkeyform );
3342
		$this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
3343
3344
		# We already know that some pages won't be in the database!
3345
		if ( $this->isExternal() || $this->mNamespace == NS_SPECIAL ) {
3346
			$this->mArticleID = 0;
3347
		}
3348
3349
		return true;
3350
	}
3351
3352
	/**
3353
	 * Get an array of Title objects linking to this Title
3354
	 * Also stores the IDs in the link cache.
3355
	 *
3356
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3357
	 * On heavily-used templates it will max out the memory.
3358
	 *
3359
	 * @param array $options May be FOR UPDATE
3360
	 * @param string $table Table name
3361
	 * @param string $prefix Fields prefix
3362
	 * @return Title[] Array of Title objects linking here
3363
	 */
3364
	public function getLinksTo( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3365
		if ( count( $options ) > 0 ) {
3366
			$db = wfGetDB( DB_MASTER );
3367
		} else {
3368
			$db = wfGetDB( DB_SLAVE );
3369
		}
3370
3371
		$res = $db->select(
3372
			[ 'page', $table ],
3373
			self::getSelectFields(),
3374
			[
3375
				"{$prefix}_from=page_id",
3376
				"{$prefix}_namespace" => $this->getNamespace(),
3377
				"{$prefix}_title" => $this->getDBkey() ],
3378
			__METHOD__,
3379
			$options
3380
		);
3381
3382
		$retVal = [];
3383
		if ( $res->numRows() ) {
3384
			$linkCache = LinkCache::singleton();
3385 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...
3386
				$titleObj = Title::makeTitle( $row->page_namespace, $row->page_title );
3387
				if ( $titleObj ) {
3388
					$linkCache->addGoodLinkObjFromRow( $titleObj, $row );
3389
					$retVal[] = $titleObj;
3390
				}
3391
			}
3392
		}
3393
		return $retVal;
3394
	}
3395
3396
	/**
3397
	 * Get an array of Title objects using this Title as a template
3398
	 * Also stores the IDs in the link cache.
3399
	 *
3400
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3401
	 * On heavily-used templates it will max out the memory.
3402
	 *
3403
	 * @param array $options Query option to Database::select()
3404
	 * @return Title[] Array of Title the Title objects linking here
3405
	 */
3406
	public function getTemplateLinksTo( $options = [] ) {
3407
		return $this->getLinksTo( $options, 'templatelinks', 'tl' );
3408
	}
3409
3410
	/**
3411
	 * Get an array of Title objects linked from this Title
3412
	 * Also stores the IDs in the link cache.
3413
	 *
3414
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3415
	 * On heavily-used templates it will max out the memory.
3416
	 *
3417
	 * @param array $options Query option to Database::select()
3418
	 * @param string $table Table name
3419
	 * @param string $prefix Fields prefix
3420
	 * @return array Array of Title objects linking here
3421
	 */
3422
	public function getLinksFrom( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3423
		$id = $this->getArticleID();
3424
3425
		# If the page doesn't exist; there can't be any link from this page
3426
		if ( !$id ) {
3427
			return [];
3428
		}
3429
3430
		$db = wfGetDB( DB_SLAVE );
3431
3432
		$blNamespace = "{$prefix}_namespace";
3433
		$blTitle = "{$prefix}_title";
3434
3435
		$res = $db->select(
3436
			[ $table, 'page' ],
3437
			array_merge(
3438
				[ $blNamespace, $blTitle ],
3439
				WikiPage::selectFields()
3440
			),
3441
			[ "{$prefix}_from" => $id ],
3442
			__METHOD__,
3443
			$options,
3444
			[ 'page' => [
3445
				'LEFT JOIN',
3446
				[ "page_namespace=$blNamespace", "page_title=$blTitle" ]
3447
			] ]
3448
		);
3449
3450
		$retVal = [];
3451
		$linkCache = LinkCache::singleton();
3452
		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...
3453
			if ( $row->page_id ) {
3454
				$titleObj = Title::newFromRow( $row );
3455
			} else {
3456
				$titleObj = Title::makeTitle( $row->$blNamespace, $row->$blTitle );
3457
				$linkCache->addBadLinkObj( $titleObj );
3458
			}
3459
			$retVal[] = $titleObj;
3460
		}
3461
3462
		return $retVal;
3463
	}
3464
3465
	/**
3466
	 * Get an array of Title objects used on this Title as a template
3467
	 * Also stores the IDs in the link cache.
3468
	 *
3469
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3470
	 * On heavily-used templates it will max out the memory.
3471
	 *
3472
	 * @param array $options May be FOR UPDATE
3473
	 * @return Title[] Array of Title the Title objects used here
3474
	 */
3475
	public function getTemplateLinksFrom( $options = [] ) {
3476
		return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
3477
	}
3478
3479
	/**
3480
	 * Get an array of Title objects referring to non-existent articles linked
3481
	 * from this page.
3482
	 *
3483
	 * @todo check if needed (used only in SpecialBrokenRedirects.php, and
3484
	 *   should use redirect table in this case).
3485
	 * @return Title[] Array of Title the Title objects
3486
	 */
3487
	public function getBrokenLinksFrom() {
3488
		if ( $this->getArticleID() == 0 ) {
3489
			# All links from article ID 0 are false positives
3490
			return [];
3491
		}
3492
3493
		$dbr = wfGetDB( DB_SLAVE );
3494
		$res = $dbr->select(
3495
			[ 'page', 'pagelinks' ],
3496
			[ 'pl_namespace', 'pl_title' ],
3497
			[
3498
				'pl_from' => $this->getArticleID(),
3499
				'page_namespace IS NULL'
3500
			],
3501
			__METHOD__, [],
3502
			[
3503
				'page' => [
3504
					'LEFT JOIN',
3505
					[ 'pl_namespace=page_namespace', 'pl_title=page_title' ]
3506
				]
3507
			]
3508
		);
3509
3510
		$retVal = [];
3511
		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...
3512
			$retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
3513
		}
3514
		return $retVal;
3515
	}
3516
3517
	/**
3518
	 * Get a list of URLs to purge from the CDN cache when this
3519
	 * page changes
3520
	 *
3521
	 * @return string[] Array of String the URLs
3522
	 */
3523
	public function getCdnUrls() {
3524
		$urls = [
3525
			$this->getInternalURL(),
3526
			$this->getInternalURL( 'action=history' )
3527
		];
3528
3529
		$pageLang = $this->getPageLanguage();
3530
		if ( $pageLang->hasVariants() ) {
3531
			$variants = $pageLang->getVariants();
3532
			foreach ( $variants as $vCode ) {
3533
				$urls[] = $this->getInternalURL( $vCode );
3534
			}
3535
		}
3536
3537
		// If we are looking at a css/js user subpage, purge the action=raw.
3538
		if ( $this->isJsSubpage() ) {
3539
			$urls[] = $this->getInternalURL( 'action=raw&ctype=text/javascript' );
3540
		} elseif ( $this->isCssSubpage() ) {
3541
			$urls[] = $this->getInternalURL( 'action=raw&ctype=text/css' );
3542
		}
3543
3544
		Hooks::run( 'TitleSquidURLs', [ $this, &$urls ] );
3545
		return $urls;
3546
	}
3547
3548
	/**
3549
	 * @deprecated since 1.27 use getCdnUrls()
3550
	 */
3551
	public function getSquidURLs() {
3552
		return $this->getCdnUrls();
3553
	}
3554
3555
	/**
3556
	 * Purge all applicable CDN URLs
3557
	 */
3558
	public function purgeSquid() {
3559
		DeferredUpdates::addUpdate(
3560
			new CdnCacheUpdate( $this->getCdnUrls() ),
3561
			DeferredUpdates::PRESEND
3562
		);
3563
	}
3564
3565
	/**
3566
	 * Move this page without authentication
3567
	 *
3568
	 * @deprecated since 1.25 use MovePage class instead
3569
	 * @param Title $nt The new page Title
3570
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3571
	 */
3572
	public function moveNoAuth( &$nt ) {
3573
		wfDeprecated( __METHOD__, '1.25' );
3574
		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...
3575
	}
3576
3577
	/**
3578
	 * Check whether a given move operation would be valid.
3579
	 * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
3580
	 *
3581
	 * @deprecated since 1.25, use MovePage's methods instead
3582
	 * @param Title $nt The new title
3583
	 * @param bool $auth Whether to check user permissions (uses $wgUser)
3584
	 * @param string $reason Is the log summary of the move, used for spam checking
3585
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3586
	 */
3587
	public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
3588
		global $wgUser;
3589
3590
		if ( !( $nt instanceof Title ) ) {
3591
			// Normally we'd add this to $errors, but we'll get
3592
			// lots of syntax errors if $nt is not an object
3593
			return [ [ 'badtitletext' ] ];
3594
		}
3595
3596
		$mp = new MovePage( $this, $nt );
3597
		$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...
3598
		if ( $auth ) {
3599
			$errors = wfMergeErrorArrays(
3600
				$errors,
3601
				$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...
3602
			);
3603
		}
3604
3605
		return $errors ?: true;
3606
	}
3607
3608
	/**
3609
	 * Check if the requested move target is a valid file move target
3610
	 * @todo move this to MovePage
3611
	 * @param Title $nt Target title
3612
	 * @return array List of errors
3613
	 */
3614
	protected function validateFileMoveOperation( $nt ) {
3615
		global $wgUser;
3616
3617
		$errors = [];
3618
3619
		$destFile = wfLocalFile( $nt );
3620
		$destFile->load( File::READ_LATEST );
3621
		if ( !$wgUser->isAllowed( 'reupload-shared' )
3622
			&& !$destFile->exists() && wfFindFile( $nt )
3623
		) {
3624
			$errors[] = [ 'file-exists-sharedrepo' ];
3625
		}
3626
3627
		return $errors;
3628
	}
3629
3630
	/**
3631
	 * Move a title to a new location
3632
	 *
3633
	 * @deprecated since 1.25, use the MovePage class instead
3634
	 * @param Title $nt The new title
3635
	 * @param bool $auth Indicates whether $wgUser's permissions
3636
	 *  should be checked
3637
	 * @param string $reason The reason for the move
3638
	 * @param bool $createRedirect Whether to create a redirect from the old title to the new title.
3639
	 *  Ignored if the user doesn't have the suppressredirect right.
3640
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3641
	 */
3642
	public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
3643
		global $wgUser;
3644
		$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...
3645
		if ( is_array( $err ) ) {
3646
			// Auto-block user's IP if the account was "hard" blocked
3647
			$wgUser->spreadAnyEditBlock();
3648
			return $err;
3649
		}
3650
		// Check suppressredirect permission
3651
		if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
3652
			$createRedirect = true;
3653
		}
3654
3655
		$mp = new MovePage( $this, $nt );
3656
		$status = $mp->move( $wgUser, $reason, $createRedirect );
3657
		if ( $status->isOK() ) {
3658
			return true;
3659
		} else {
3660
			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...
3661
		}
3662
	}
3663
3664
	/**
3665
	 * Move this page's subpages to be subpages of $nt
3666
	 *
3667
	 * @param Title $nt Move target
3668
	 * @param bool $auth Whether $wgUser's permissions should be checked
3669
	 * @param string $reason The reason for the move
3670
	 * @param bool $createRedirect Whether to create redirects from the old subpages to
3671
	 *     the new ones Ignored if the user doesn't have the 'suppressredirect' right
3672
	 * @return array Array with old page titles as keys, and strings (new page titles) or
3673
	 *     arrays (errors) as values, or an error array with numeric indices if no pages
3674
	 *     were moved
3675
	 */
3676
	public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) {
3677
		global $wgMaximumMovedPages;
3678
		// Check permissions
3679
		if ( !$this->userCan( 'move-subpages' ) ) {
3680
			return [ 'cant-move-subpages' ];
3681
		}
3682
		// Do the source and target namespaces support subpages?
3683
		if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3684
			return [ 'namespace-nosubpages',
3685
				MWNamespace::getCanonicalName( $this->getNamespace() ) ];
3686
		}
3687
		if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
3688
			return [ 'namespace-nosubpages',
3689
				MWNamespace::getCanonicalName( $nt->getNamespace() ) ];
3690
		}
3691
3692
		$subpages = $this->getSubpages( $wgMaximumMovedPages + 1 );
3693
		$retval = [];
3694
		$count = 0;
3695
		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...
3696
			$count++;
3697
			if ( $count > $wgMaximumMovedPages ) {
3698
				$retval[$oldSubpage->getPrefixedText()] =
3699
						[ 'movepage-max-pages',
3700
							$wgMaximumMovedPages ];
3701
				break;
3702
			}
3703
3704
			// We don't know whether this function was called before
3705
			// or after moving the root page, so check both
3706
			// $this and $nt
3707
			if ( $oldSubpage->getArticleID() == $this->getArticleID()
3708
				|| $oldSubpage->getArticleID() == $nt->getArticleID()
3709
			) {
3710
				// When moving a page to a subpage of itself,
3711
				// don't move it twice
3712
				continue;
3713
			}
3714
			$newPageName = preg_replace(
3715
					'#^' . preg_quote( $this->getDBkey(), '#' ) . '#',
3716
					StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
3717
					$oldSubpage->getDBkey() );
3718
			if ( $oldSubpage->isTalkPage() ) {
3719
				$newNs = $nt->getTalkPage()->getNamespace();
3720
			} else {
3721
				$newNs = $nt->getSubjectPage()->getNamespace();
3722
			}
3723
			# Bug 14385: we need makeTitleSafe because the new page names may
3724
			# be longer than 255 characters.
3725
			$newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
3726
3727
			$success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect );
3728
			if ( $success === true ) {
3729
				$retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
3730
			} else {
3731
				$retval[$oldSubpage->getPrefixedText()] = $success;
3732
			}
3733
		}
3734
		return $retval;
3735
	}
3736
3737
	/**
3738
	 * Checks if this page is just a one-rev redirect.
3739
	 * Adds lock, so don't use just for light purposes.
3740
	 *
3741
	 * @return bool
3742
	 */
3743
	public function isSingleRevRedirect() {
3744
		global $wgContentHandlerUseDB;
3745
3746
		$dbw = wfGetDB( DB_MASTER );
3747
3748
		# Is it a redirect?
3749
		$fields = [ 'page_is_redirect', 'page_latest', 'page_id' ];
3750
		if ( $wgContentHandlerUseDB ) {
3751
			$fields[] = 'page_content_model';
3752
		}
3753
3754
		$row = $dbw->selectRow( 'page',
3755
			$fields,
3756
			$this->pageCond(),
3757
			__METHOD__,
3758
			[ 'FOR UPDATE' ]
3759
		);
3760
		# Cache some fields we may want
3761
		$this->mArticleID = $row ? intval( $row->page_id ) : 0;
3762
		$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...
3763
		$this->mLatestID = $row ? intval( $row->page_latest ) : false;
3764
		$this->mContentModel = $row && isset( $row->page_content_model )
3765
			? strval( $row->page_content_model )
3766
			: false;
3767
3768
		if ( !$this->mRedirect ) {
3769
			return false;
3770
		}
3771
		# Does the article have a history?
3772
		$row = $dbw->selectField( [ 'page', 'revision' ],
3773
			'rev_id',
3774
			[ 'page_namespace' => $this->getNamespace(),
3775
				'page_title' => $this->getDBkey(),
3776
				'page_id=rev_page',
3777
				'page_latest != rev_id'
3778
			],
3779
			__METHOD__,
3780
			[ 'FOR UPDATE' ]
3781
		);
3782
		# Return true if there was no history
3783
		return ( $row === false );
3784
	}
3785
3786
	/**
3787
	 * Checks if $this can be moved to a given Title
3788
	 * - Selects for update, so don't call it unless you mean business
3789
	 *
3790
	 * @deprecated since 1.25, use MovePage's methods instead
3791
	 * @param Title $nt The new title to check
3792
	 * @return bool
3793
	 */
3794
	public function isValidMoveTarget( $nt ) {
3795
		# Is it an existing file?
3796
		if ( $nt->getNamespace() == NS_FILE ) {
3797
			$file = wfLocalFile( $nt );
3798
			$file->load( File::READ_LATEST );
3799
			if ( $file->exists() ) {
3800
				wfDebug( __METHOD__ . ": file exists\n" );
3801
				return false;
3802
			}
3803
		}
3804
		# Is it a redirect with no history?
3805
		if ( !$nt->isSingleRevRedirect() ) {
3806
			wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
3807
			return false;
3808
		}
3809
		# Get the article text
3810
		$rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST );
3811
		if ( !is_object( $rev ) ) {
3812
			return false;
3813
		}
3814
		$content = $rev->getContent();
3815
		# Does the redirect point to the source?
3816
		# Or is it a broken self-redirect, usually caused by namespace collisions?
3817
		$redirTitle = $content ? $content->getRedirectTarget() : null;
3818
3819
		if ( $redirTitle ) {
3820
			if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
3821
				$redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
3822
				wfDebug( __METHOD__ . ": redirect points to other page\n" );
3823
				return false;
3824
			} else {
3825
				return true;
3826
			}
3827
		} else {
3828
			# Fail safe (not a redirect after all. strange.)
3829
			wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
3830
						" is a redirect, but it doesn't contain a valid redirect.\n" );
3831
			return false;
3832
		}
3833
	}
3834
3835
	/**
3836
	 * Get categories to which this Title belongs and return an array of
3837
	 * categories' names.
3838
	 *
3839
	 * @return array Array of parents in the form:
3840
	 *	  $parent => $currentarticle
3841
	 */
3842
	public function getParentCategories() {
3843
		global $wgContLang;
3844
3845
		$data = [];
3846
3847
		$titleKey = $this->getArticleID();
3848
3849
		if ( $titleKey === 0 ) {
3850
			return $data;
3851
		}
3852
3853
		$dbr = wfGetDB( DB_SLAVE );
3854
3855
		$res = $dbr->select(
3856
			'categorylinks',
3857
			'cl_to',
3858
			[ 'cl_from' => $titleKey ],
3859
			__METHOD__
3860
		);
3861
3862
		if ( $res->numRows() > 0 ) {
3863
			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...
3864
				// $data[] = Title::newFromText($wgContLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to);
3865
				$data[$wgContLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText();
3866
			}
3867
		}
3868
		return $data;
3869
	}
3870
3871
	/**
3872
	 * Get a tree of parent categories
3873
	 *
3874
	 * @param array $children Array with the children in the keys, to check for circular refs
3875
	 * @return array Tree of parent categories
3876
	 */
3877
	public function getParentCategoryTree( $children = [] ) {
3878
		$stack = [];
3879
		$parents = $this->getParentCategories();
3880
3881
		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...
3882
			foreach ( $parents as $parent => $current ) {
3883
				if ( array_key_exists( $parent, $children ) ) {
3884
					# Circular reference
3885
					$stack[$parent] = [];
3886
				} else {
3887
					$nt = Title::newFromText( $parent );
3888
					if ( $nt ) {
3889
						$stack[$parent] = $nt->getParentCategoryTree( $children + [ $parent => 1 ] );
3890
					}
3891
				}
3892
			}
3893
		}
3894
3895
		return $stack;
3896
	}
3897
3898
	/**
3899
	 * Get an associative array for selecting this title from
3900
	 * the "page" table
3901
	 *
3902
	 * @return array Array suitable for the $where parameter of DB::select()
3903
	 */
3904
	public function pageCond() {
3905
		if ( $this->mArticleID > 0 ) {
3906
			// PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
3907
			return [ 'page_id' => $this->mArticleID ];
3908
		} else {
3909
			return [ 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ];
3910
		}
3911
	}
3912
3913
	/**
3914
	 * Get the revision ID of the previous revision
3915
	 *
3916
	 * @param int $revId Revision ID. Get the revision that was before this one.
3917
	 * @param int $flags Title::GAID_FOR_UPDATE
3918
	 * @return int|bool Old revision ID, or false if none exists
3919
	 */
3920 View Code Duplication
	public function getPreviousRevisionID( $revId, $flags = 0 ) {
3921
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3922
		$revId = $db->selectField( 'revision', 'rev_id',
3923
			[
3924
				'rev_page' => $this->getArticleID( $flags ),
3925
				'rev_id < ' . intval( $revId )
3926
			],
3927
			__METHOD__,
3928
			[ 'ORDER BY' => 'rev_id DESC' ]
3929
		);
3930
3931
		if ( $revId === false ) {
3932
			return false;
3933
		} else {
3934
			return intval( $revId );
3935
		}
3936
	}
3937
3938
	/**
3939
	 * Get the revision ID of the next revision
3940
	 *
3941
	 * @param int $revId Revision ID. Get the revision that was after this one.
3942
	 * @param int $flags Title::GAID_FOR_UPDATE
3943
	 * @return int|bool Next revision ID, or false if none exists
3944
	 */
3945 View Code Duplication
	public function getNextRevisionID( $revId, $flags = 0 ) {
3946
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3947
		$revId = $db->selectField( 'revision', 'rev_id',
3948
			[
3949
				'rev_page' => $this->getArticleID( $flags ),
3950
				'rev_id > ' . intval( $revId )
3951
			],
3952
			__METHOD__,
3953
			[ 'ORDER BY' => 'rev_id' ]
3954
		);
3955
3956
		if ( $revId === false ) {
3957
			return false;
3958
		} else {
3959
			return intval( $revId );
3960
		}
3961
	}
3962
3963
	/**
3964
	 * Get the first revision of the page
3965
	 *
3966
	 * @param int $flags Title::GAID_FOR_UPDATE
3967
	 * @return Revision|null If page doesn't exist
3968
	 */
3969
	public function getFirstRevision( $flags = 0 ) {
3970
		$pageId = $this->getArticleID( $flags );
3971
		if ( $pageId ) {
3972
			$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3973
			$row = $db->selectRow( 'revision', Revision::selectFields(),
3974
				[ 'rev_page' => $pageId ],
3975
				__METHOD__,
3976
				[ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ]
3977
			);
3978
			if ( $row ) {
3979
				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 3973 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...
3980
			}
3981
		}
3982
		return null;
3983
	}
3984
3985
	/**
3986
	 * Get the oldest revision timestamp of this page
3987
	 *
3988
	 * @param int $flags Title::GAID_FOR_UPDATE
3989
	 * @return string MW timestamp
3990
	 */
3991
	public function getEarliestRevTime( $flags = 0 ) {
3992
		$rev = $this->getFirstRevision( $flags );
3993
		return $rev ? $rev->getTimestamp() : null;
3994
	}
3995
3996
	/**
3997
	 * Check if this is a new page
3998
	 *
3999
	 * @return bool
4000
	 */
4001
	public function isNewPage() {
4002
		$dbr = wfGetDB( DB_SLAVE );
4003
		return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
4004
	}
4005
4006
	/**
4007
	 * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit
4008
	 *
4009
	 * @return bool
4010
	 */
4011
	public function isBigDeletion() {
4012
		global $wgDeleteRevisionsLimit;
4013
4014
		if ( !$wgDeleteRevisionsLimit ) {
4015
			return false;
4016
		}
4017
4018
		if ( $this->mIsBigDeletion === null ) {
4019
			$dbr = wfGetDB( DB_SLAVE );
4020
4021
			$revCount = $dbr->selectRowCount(
4022
				'revision',
4023
				'1',
4024
				[ 'rev_page' => $this->getArticleID() ],
4025
				__METHOD__,
4026
				[ 'LIMIT' => $wgDeleteRevisionsLimit + 1 ]
4027
			);
4028
4029
			$this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit;
4030
		}
4031
4032
		return $this->mIsBigDeletion;
4033
	}
4034
4035
	/**
4036
	 * Get the approximate revision count of this page.
4037
	 *
4038
	 * @return int
4039
	 */
4040
	public function estimateRevisionCount() {
4041
		if ( !$this->exists() ) {
4042
			return 0;
4043
		}
4044
4045
		if ( $this->mEstimateRevisions === null ) {
4046
			$dbr = wfGetDB( DB_SLAVE );
4047
			$this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
4048
				[ 'rev_page' => $this->getArticleID() ], __METHOD__ );
4049
		}
4050
4051
		return $this->mEstimateRevisions;
4052
	}
4053
4054
	/**
4055
	 * Get the number of revisions between the given revision.
4056
	 * Used for diffs and other things that really need it.
4057
	 *
4058
	 * @param int|Revision $old Old revision or rev ID (first before range)
4059
	 * @param int|Revision $new New revision or rev ID (first after range)
4060
	 * @param int|null $max Limit of Revisions to count, will be incremented to detect truncations
4061
	 * @return int Number of revisions between these revisions.
4062
	 */
4063
	public function countRevisionsBetween( $old, $new, $max = null ) {
4064
		if ( !( $old instanceof Revision ) ) {
4065
			$old = Revision::newFromTitle( $this, (int)$old );
4066
		}
4067
		if ( !( $new instanceof Revision ) ) {
4068
			$new = Revision::newFromTitle( $this, (int)$new );
4069
		}
4070
		if ( !$old || !$new ) {
4071
			return 0; // nothing to compare
4072
		}
4073
		$dbr = wfGetDB( DB_SLAVE );
4074
		$conds = [
4075
			'rev_page' => $this->getArticleID(),
4076
			'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...
4077
			'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...
4078
		];
4079
		if ( $max !== null ) {
4080
			return $dbr->selectRowCount( 'revision', '1',
4081
				$conds,
4082
				__METHOD__,
4083
				[ 'LIMIT' => $max + 1 ] // extra to detect truncation
4084
			);
4085
		} else {
4086
			return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
4087
		}
4088
	}
4089
4090
	/**
4091
	 * Get the authors between the given revisions or revision IDs.
4092
	 * Used for diffs and other things that really need it.
4093
	 *
4094
	 * @since 1.23
4095
	 *
4096
	 * @param int|Revision $old Old revision or rev ID (first before range by default)
4097
	 * @param int|Revision $new New revision or rev ID (first after range by default)
4098
	 * @param int $limit Maximum number of authors
4099
	 * @param string|array $options (Optional): Single option, or an array of options:
4100
	 *     'include_old' Include $old in the range; $new is excluded.
4101
	 *     'include_new' Include $new in the range; $old is excluded.
4102
	 *     'include_both' Include both $old and $new in the range.
4103
	 *     Unknown option values are ignored.
4104
	 * @return array|null Names of revision authors in the range; null if not both revisions exist
4105
	 */
4106
	public function getAuthorsBetween( $old, $new, $limit, $options = [] ) {
4107
		if ( !( $old instanceof Revision ) ) {
4108
			$old = Revision::newFromTitle( $this, (int)$old );
4109
		}
4110
		if ( !( $new instanceof Revision ) ) {
4111
			$new = Revision::newFromTitle( $this, (int)$new );
4112
		}
4113
		// XXX: what if Revision objects are passed in, but they don't refer to this title?
4114
		// Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID()
4115
		// in the sanity check below?
4116
		if ( !$old || !$new ) {
4117
			return null; // nothing to compare
4118
		}
4119
		$authors = [];
4120
		$old_cmp = '>';
4121
		$new_cmp = '<';
4122
		$options = (array)$options;
4123
		if ( in_array( 'include_old', $options ) ) {
4124
			$old_cmp = '>=';
4125
		}
4126
		if ( in_array( 'include_new', $options ) ) {
4127
			$new_cmp = '<=';
4128
		}
4129
		if ( in_array( 'include_both', $options ) ) {
4130
			$old_cmp = '>=';
4131
			$new_cmp = '<=';
4132
		}
4133
		// No DB query needed if $old and $new are the same or successive revisions:
4134
		if ( $old->getId() === $new->getId() ) {
4135
			return ( $old_cmp === '>' && $new_cmp === '<' ) ?
4136
				[] :
4137
				[ $old->getUserText( Revision::RAW ) ];
4138
		} elseif ( $old->getId() === $new->getParentId() ) {
4139
			if ( $old_cmp === '>=' && $new_cmp === '<=' ) {
4140
				$authors[] = $old->getUserText( Revision::RAW );
4141
				if ( $old->getUserText( Revision::RAW ) != $new->getUserText( Revision::RAW ) ) {
4142
					$authors[] = $new->getUserText( Revision::RAW );
4143
				}
4144
			} elseif ( $old_cmp === '>=' ) {
4145
				$authors[] = $old->getUserText( Revision::RAW );
4146
			} elseif ( $new_cmp === '<=' ) {
4147
				$authors[] = $new->getUserText( Revision::RAW );
4148
			}
4149
			return $authors;
4150
		}
4151
		$dbr = wfGetDB( DB_SLAVE );
4152
		$res = $dbr->select( 'revision', 'DISTINCT rev_user_text',
4153
			[
4154
				'rev_page' => $this->getArticleID(),
4155
				"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...
4156
				"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...
4157
			], __METHOD__,
4158
			[ 'LIMIT' => $limit + 1 ] // add one so caller knows it was truncated
4159
		);
4160
		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...
4161
			$authors[] = $row->rev_user_text;
4162
		}
4163
		return $authors;
4164
	}
4165
4166
	/**
4167
	 * Get the number of authors between the given revisions or revision IDs.
4168
	 * Used for diffs and other things that really need it.
4169
	 *
4170
	 * @param int|Revision $old Old revision or rev ID (first before range by default)
4171
	 * @param int|Revision $new New revision or rev ID (first after range by default)
4172
	 * @param int $limit Maximum number of authors
4173
	 * @param string|array $options (Optional): Single option, or an array of options:
4174
	 *     'include_old' Include $old in the range; $new is excluded.
4175
	 *     'include_new' Include $new in the range; $old is excluded.
4176
	 *     'include_both' Include both $old and $new in the range.
4177
	 *     Unknown option values are ignored.
4178
	 * @return int Number of revision authors in the range; zero if not both revisions exist
4179
	 */
4180
	public function countAuthorsBetween( $old, $new, $limit, $options = [] ) {
4181
		$authors = $this->getAuthorsBetween( $old, $new, $limit, $options );
4182
		return $authors ? count( $authors ) : 0;
4183
	}
4184
4185
	/**
4186
	 * Compare with another title.
4187
	 *
4188
	 * @param Title $title
4189
	 * @return bool
4190
	 */
4191
	public function equals( Title $title ) {
4192
		// Note: === is necessary for proper matching of number-like titles.
4193
		return $this->getInterwiki() === $title->getInterwiki()
4194
			&& $this->getNamespace() == $title->getNamespace()
4195
			&& $this->getDBkey() === $title->getDBkey();
4196
	}
4197
4198
	/**
4199
	 * Check if this title is a subpage of another title
4200
	 *
4201
	 * @param Title $title
4202
	 * @return bool
4203
	 */
4204
	public function isSubpageOf( Title $title ) {
4205
		return $this->getInterwiki() === $title->getInterwiki()
4206
			&& $this->getNamespace() == $title->getNamespace()
4207
			&& strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0;
4208
	}
4209
4210
	/**
4211
	 * Check if page exists.  For historical reasons, this function simply
4212
	 * checks for the existence of the title in the page table, and will
4213
	 * thus return false for interwiki links, special pages and the like.
4214
	 * If you want to know if a title can be meaningfully viewed, you should
4215
	 * probably call the isKnown() method instead.
4216
	 *
4217
	 * @param int $flags An optional bit field; may be Title::GAID_FOR_UPDATE to check
4218
	 *   from master/for update
4219
	 * @return bool
4220
	 */
4221
	public function exists( $flags = 0 ) {
4222
		$exists = $this->getArticleID( $flags ) != 0;
4223
		Hooks::run( 'TitleExists', [ $this, &$exists ] );
4224
		return $exists;
4225
	}
4226
4227
	/**
4228
	 * Should links to this title be shown as potentially viewable (i.e. as
4229
	 * "bluelinks"), even if there's no record by this title in the page
4230
	 * table?
4231
	 *
4232
	 * This function is semi-deprecated for public use, as well as somewhat
4233
	 * misleadingly named.  You probably just want to call isKnown(), which
4234
	 * calls this function internally.
4235
	 *
4236
	 * (ISSUE: Most of these checks are cheap, but the file existence check
4237
	 * can potentially be quite expensive.  Including it here fixes a lot of
4238
	 * existing code, but we might want to add an optional parameter to skip
4239
	 * it and any other expensive checks.)
4240
	 *
4241
	 * @return bool
4242
	 */
4243
	public function isAlwaysKnown() {
4244
		$isKnown = null;
4245
4246
		/**
4247
		 * Allows overriding default behavior for determining if a page exists.
4248
		 * If $isKnown is kept as null, regular checks happen. If it's
4249
		 * a boolean, this value is returned by the isKnown method.
4250
		 *
4251
		 * @since 1.20
4252
		 *
4253
		 * @param Title $title
4254
		 * @param bool|null $isKnown
4255
		 */
4256
		Hooks::run( 'TitleIsAlwaysKnown', [ $this, &$isKnown ] );
4257
4258
		if ( !is_null( $isKnown ) ) {
4259
			return $isKnown;
4260
		}
4261
4262
		if ( $this->isExternal() ) {
4263
			return true;  // any interwiki link might be viewable, for all we know
4264
		}
4265
4266
		switch ( $this->mNamespace ) {
4267
			case NS_MEDIA:
4268
			case NS_FILE:
4269
				// file exists, possibly in a foreign repo
4270
				return (bool)wfFindFile( $this );
4271
			case NS_SPECIAL:
4272
				// valid special page
4273
				return SpecialPageFactory::exists( $this->getDBkey() );
4274
			case NS_MAIN:
4275
				// selflink, possibly with fragment
4276
				return $this->mDbkeyform == '';
4277
			case NS_MEDIAWIKI:
4278
				// known system message
4279
				return $this->hasSourceText() !== false;
4280
			default:
4281
				return false;
4282
		}
4283
	}
4284
4285
	/**
4286
	 * Does this title refer to a page that can (or might) be meaningfully
4287
	 * viewed?  In particular, this function may be used to determine if
4288
	 * links to the title should be rendered as "bluelinks" (as opposed to
4289
	 * "redlinks" to non-existent pages).
4290
	 * Adding something else to this function will cause inconsistency
4291
	 * since LinkHolderArray calls isAlwaysKnown() and does its own
4292
	 * page existence check.
4293
	 *
4294
	 * @return bool
4295
	 */
4296
	public function isKnown() {
4297
		return $this->isAlwaysKnown() || $this->exists();
4298
	}
4299
4300
	/**
4301
	 * Does this page have source text?
4302
	 *
4303
	 * @return bool
4304
	 */
4305
	public function hasSourceText() {
4306
		if ( $this->exists() ) {
4307
			return true;
4308
		}
4309
4310
		if ( $this->mNamespace == NS_MEDIAWIKI ) {
4311
			// If the page doesn't exist but is a known system message, default
4312
			// message content will be displayed, same for language subpages-
4313
			// Use always content language to avoid loading hundreds of languages
4314
			// to get the link color.
4315
			global $wgContLang;
4316
			list( $name, ) = MessageCache::singleton()->figureMessage(
4317
				$wgContLang->lcfirst( $this->getText() )
4318
			);
4319
			$message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false );
4320
			return $message->exists();
4321
		}
4322
4323
		return false;
4324
	}
4325
4326
	/**
4327
	 * Get the default message text or false if the message doesn't exist
4328
	 *
4329
	 * @return string|bool
4330
	 */
4331
	public function getDefaultMessageText() {
4332
		global $wgContLang;
4333
4334
		if ( $this->getNamespace() != NS_MEDIAWIKI ) { // Just in case
4335
			return false;
4336
		}
4337
4338
		list( $name, $lang ) = MessageCache::singleton()->figureMessage(
4339
			$wgContLang->lcfirst( $this->getText() )
4340
		);
4341
		$message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false );
4342
4343
		if ( $message->exists() ) {
4344
			return $message->plain();
4345
		} else {
4346
			return false;
4347
		}
4348
	}
4349
4350
	/**
4351
	 * Updates page_touched for this page; called from LinksUpdate.php
4352
	 *
4353
	 * @param string $purgeTime [optional] TS_MW timestamp
4354
	 * @return bool True if the update succeeded
4355
	 */
4356
	public function invalidateCache( $purgeTime = null ) {
4357
		if ( wfReadOnly() ) {
4358
			return false;
4359
		}
4360
4361
		if ( $this->mArticleID === 0 ) {
4362
			return true; // avoid gap locking if we know it's not there
4363
		}
4364
4365
		$method = __METHOD__;
4366
		$dbw = wfGetDB( DB_MASTER );
4367
		$conds = $this->pageCond();
4368
		$dbw->onTransactionIdle( function () use ( $dbw, $conds, $method, $purgeTime ) {
4369
			$dbTimestamp = $dbw->timestamp( $purgeTime ?: time() );
4370
4371
			$dbw->update(
4372
				'page',
4373
				[ 'page_touched' => $dbTimestamp ],
4374
				$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 4369 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...
4375
				$method
4376
			);
4377
		} );
4378
4379
		return true;
4380
	}
4381
4382
	/**
4383
	 * Update page_touched timestamps and send CDN purge messages for
4384
	 * pages linking to this title. May be sent to the job queue depending
4385
	 * on the number of links. Typically called on create and delete.
4386
	 */
4387
	public function touchLinks() {
4388
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks' ) );
4389
		if ( $this->getNamespace() == NS_CATEGORY ) {
4390
			DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'categorylinks' ) );
4391
		}
4392
	}
4393
4394
	/**
4395
	 * Get the last touched timestamp
4396
	 *
4397
	 * @param IDatabase $db Optional db
4398
	 * @return string Last-touched timestamp
4399
	 */
4400
	public function getTouched( $db = null ) {
4401
		if ( $db === null ) {
4402
			$db = wfGetDB( DB_SLAVE );
4403
		}
4404
		$touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
4405
		return $touched;
4406
	}
4407
4408
	/**
4409
	 * Get the timestamp when this page was updated since the user last saw it.
4410
	 *
4411
	 * @param User $user
4412
	 * @return string|null
4413
	 */
4414
	public function getNotificationTimestamp( $user = null ) {
4415
		global $wgUser;
4416
4417
		// Assume current user if none given
4418
		if ( !$user ) {
4419
			$user = $wgUser;
4420
		}
4421
		// Check cache first
4422
		$uid = $user->getId();
4423
		if ( !$uid ) {
4424
			return false;
4425
		}
4426
		// avoid isset here, as it'll return false for null entries
4427
		if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
4428
			return $this->mNotificationTimestamp[$uid];
4429
		}
4430
		// Don't cache too much!
4431
		if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
4432
			$this->mNotificationTimestamp = [];
4433
		}
4434
4435
		$watchedItem = WatchedItemStore::getDefaultInstance()->getWatchedItem( $user, $this );
4436
		if ( $watchedItem ) {
4437
			$this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp();
4438
		} else {
4439
			$this->mNotificationTimestamp[$uid] = false;
4440
		}
4441
4442
		return $this->mNotificationTimestamp[$uid];
4443
	}
4444
4445
	/**
4446
	 * Generate strings used for xml 'id' names in monobook tabs
4447
	 *
4448
	 * @param string $prepend Defaults to 'nstab-'
4449
	 * @return string XML 'id' name
4450
	 */
4451
	public function getNamespaceKey( $prepend = 'nstab-' ) {
4452
		global $wgContLang;
4453
		// Gets the subject namespace if this title
4454
		$namespace = MWNamespace::getSubject( $this->getNamespace() );
4455
		// Checks if canonical namespace name exists for namespace
4456
		if ( MWNamespace::exists( $this->getNamespace() ) ) {
4457
			// Uses canonical namespace name
4458
			$namespaceKey = MWNamespace::getCanonicalName( $namespace );
4459
		} else {
4460
			// Uses text of namespace
4461
			$namespaceKey = $this->getSubjectNsText();
4462
		}
4463
		// Makes namespace key lowercase
4464
		$namespaceKey = $wgContLang->lc( $namespaceKey );
4465
		// Uses main
4466
		if ( $namespaceKey == '' ) {
4467
			$namespaceKey = 'main';
4468
		}
4469
		// Changes file to image for backwards compatibility
4470
		if ( $namespaceKey == 'file' ) {
4471
			$namespaceKey = 'image';
4472
		}
4473
		return $prepend . $namespaceKey;
4474
	}
4475
4476
	/**
4477
	 * Get all extant redirects to this Title
4478
	 *
4479
	 * @param int|null $ns Single namespace to consider; null to consider all namespaces
4480
	 * @return Title[] Array of Title redirects to this title
4481
	 */
4482
	public function getRedirectsHere( $ns = null ) {
4483
		$redirs = [];
4484
4485
		$dbr = wfGetDB( DB_SLAVE );
4486
		$where = [
4487
			'rd_namespace' => $this->getNamespace(),
4488
			'rd_title' => $this->getDBkey(),
4489
			'rd_from = page_id'
4490
		];
4491
		if ( $this->isExternal() ) {
4492
			$where['rd_interwiki'] = $this->getInterwiki();
4493
		} else {
4494
			$where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL';
4495
		}
4496
		if ( !is_null( $ns ) ) {
4497
			$where['page_namespace'] = $ns;
4498
		}
4499
4500
		$res = $dbr->select(
4501
			[ 'redirect', 'page' ],
4502
			[ 'page_namespace', 'page_title' ],
4503
			$where,
4504
			__METHOD__
4505
		);
4506
4507
		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...
4508
			$redirs[] = self::newFromRow( $row );
4509
		}
4510
		return $redirs;
4511
	}
4512
4513
	/**
4514
	 * Check if this Title is a valid redirect target
4515
	 *
4516
	 * @return bool
4517
	 */
4518
	public function isValidRedirectTarget() {
4519
		global $wgInvalidRedirectTargets;
4520
4521
		if ( $this->isSpecialPage() ) {
4522
			// invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
4523
			if ( $this->isSpecial( 'Userlogout' ) ) {
4524
				return false;
4525
			}
4526
4527
			foreach ( $wgInvalidRedirectTargets as $target ) {
4528
				if ( $this->isSpecial( $target ) ) {
4529
					return false;
4530
				}
4531
			}
4532
		}
4533
4534
		return true;
4535
	}
4536
4537
	/**
4538
	 * Get a backlink cache object
4539
	 *
4540
	 * @return BacklinkCache
4541
	 */
4542
	public function getBacklinkCache() {
4543
		return BacklinkCache::get( $this );
4544
	}
4545
4546
	/**
4547
	 * Whether the magic words __INDEX__ and __NOINDEX__ function for  this page.
4548
	 *
4549
	 * @return bool
4550
	 */
4551
	public function canUseNoindex() {
4552
		global $wgContentNamespaces, $wgExemptFromUserRobotsControl;
4553
4554
		$bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
4555
			? $wgContentNamespaces
4556
			: $wgExemptFromUserRobotsControl;
4557
4558
		return !in_array( $this->mNamespace, $bannedNamespaces );
4559
4560
	}
4561
4562
	/**
4563
	 * Returns the raw sort key to be used for categories, with the specified
4564
	 * prefix.  This will be fed to Collation::getSortKey() to get a
4565
	 * binary sortkey that can be used for actual sorting.
4566
	 *
4567
	 * @param string $prefix The prefix to be used, specified using
4568
	 *   {{defaultsort:}} or like [[Category:Foo|prefix]].  Empty for no
4569
	 *   prefix.
4570
	 * @return string
4571
	 */
4572
	public function getCategorySortkey( $prefix = '' ) {
4573
		$unprefixed = $this->getText();
4574
4575
		// Anything that uses this hook should only depend
4576
		// on the Title object passed in, and should probably
4577
		// tell the users to run updateCollations.php --force
4578
		// in order to re-sort existing category relations.
4579
		Hooks::run( 'GetDefaultSortkey', [ $this, &$unprefixed ] );
4580
		if ( $prefix !== '' ) {
4581
			# Separate with a line feed, so the unprefixed part is only used as
4582
			# a tiebreaker when two pages have the exact same prefix.
4583
			# In UCA, tab is the only character that can sort above LF
4584
			# so we strip both of them from the original prefix.
4585
			$prefix = strtr( $prefix, "\n\t", '  ' );
4586
			return "$prefix\n$unprefixed";
4587
		}
4588
		return $unprefixed;
4589
	}
4590
4591
	/**
4592
	 * Returns the page language code saved in the database, if $wgPageLanguageUseDB is set
4593
	 * to true in LocalSettings.php, otherwise returns false. If there is no language saved in
4594
	 * the db, it will return NULL.
4595
	 *
4596
	 * @return string|null|bool
4597
	 */
4598
	private function getDbPageLanguageCode() {
4599
		global $wgPageLanguageUseDB;
4600
4601
		// check, if the page language could be saved in the database, and if so and
4602
		// the value is not requested already, lookup the page language using LinkCache
4603 View Code Duplication
		if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) {
4604
			$linkCache = LinkCache::singleton();
4605
			$linkCache->addLinkObj( $this );
4606
			$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...
4607
		}
4608
4609
		return $this->mDbPageLanguage;
4610
	}
4611
4612
	/**
4613
	 * Get the language in which the content of this page is written in
4614
	 * wikitext. Defaults to $wgContLang, but in certain cases it can be
4615
	 * e.g. $wgLang (such as special pages, which are in the user language).
4616
	 *
4617
	 * @since 1.18
4618
	 * @return Language
4619
	 */
4620
	public function getPageLanguage() {
4621
		global $wgLang, $wgLanguageCode;
4622
		if ( $this->isSpecialPage() ) {
4623
			// special pages are in the user language
4624
			return $wgLang;
4625
		}
4626
4627
		// Checking if DB language is set
4628
		$dbPageLanguage = $this->getDbPageLanguageCode();
4629
		if ( $dbPageLanguage ) {
4630
			return wfGetLangObj( $dbPageLanguage );
4631
		}
4632
4633
		if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) {
4634
			// Note that this may depend on user settings, so the cache should
4635
			// be only per-request.
4636
			// NOTE: ContentHandler::getPageLanguage() may need to load the
4637
			// content to determine the page language!
4638
			// Checking $wgLanguageCode hasn't changed for the benefit of unit
4639
			// tests.
4640
			$contentHandler = ContentHandler::getForTitle( $this );
4641
			$langObj = $contentHandler->getPageLanguage( $this );
4642
			$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...
4643
		} else {
4644
			$langObj = wfGetLangObj( $this->mPageLanguage[0] );
4645
		}
4646
4647
		return $langObj;
4648
	}
4649
4650
	/**
4651
	 * Get the language in which the content of this page is written when
4652
	 * viewed by user. Defaults to $wgContLang, but in certain cases it can be
4653
	 * e.g. $wgLang (such as special pages, which are in the user language).
4654
	 *
4655
	 * @since 1.20
4656
	 * @return Language
4657
	 */
4658
	public function getPageViewLanguage() {
4659
		global $wgLang;
4660
4661
		if ( $this->isSpecialPage() ) {
4662
			// If the user chooses a variant, the content is actually
4663
			// in a language whose code is the variant code.
4664
			$variant = $wgLang->getPreferredVariant();
4665
			if ( $wgLang->getCode() !== $variant ) {
4666
				return Language::factory( $variant );
4667
			}
4668
4669
			return $wgLang;
4670
		}
4671
4672
		// Checking if DB language is set
4673
		$dbPageLanguage = $this->getDbPageLanguageCode();
4674
		if ( $dbPageLanguage ) {
4675
			$pageLang = wfGetLangObj( $dbPageLanguage );
4676
			$variant = $pageLang->getPreferredVariant();
4677
			if ( $pageLang->getCode() !== $variant ) {
4678
				$pageLang = Language::factory( $variant );
4679
			}
4680
4681
			return $pageLang;
4682
		}
4683
4684
		// @note Can't be cached persistently, depends on user settings.
4685
		// @note ContentHandler::getPageViewLanguage() may need to load the
4686
		//   content to determine the page language!
4687
		$contentHandler = ContentHandler::getForTitle( $this );
4688
		$pageLang = $contentHandler->getPageViewLanguage( $this );
4689
		return $pageLang;
4690
	}
4691
4692
	/**
4693
	 * Get a list of rendered edit notices for this page.
4694
	 *
4695
	 * Array is keyed by the original message key, and values are rendered using parseAsBlock, so
4696
	 * they will already be wrapped in paragraphs.
4697
	 *
4698
	 * @since 1.21
4699
	 * @param int $oldid Revision ID that's being edited
4700
	 * @return array
4701
	 */
4702
	public function getEditNotices( $oldid = 0 ) {
4703
		$notices = [];
4704
4705
		// Optional notice for the entire namespace
4706
		$editnotice_ns = 'editnotice-' . $this->getNamespace();
4707
		$msg = wfMessage( $editnotice_ns );
4708 View Code Duplication
		if ( $msg->exists() ) {
4709
			$html = $msg->parseAsBlock();
4710
			// Edit notices may have complex logic, but output nothing (T91715)
4711
			if ( trim( $html ) !== '' ) {
4712
				$notices[$editnotice_ns] = Html::rawElement(
4713
					'div',
4714
					[ 'class' => [
4715
						'mw-editnotice',
4716
						'mw-editnotice-namespace',
4717
						Sanitizer::escapeClass( "mw-$editnotice_ns" )
4718
					] ],
4719
					$html
4720
				);
4721
			}
4722
		}
4723
4724
		if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) {
4725
			// Optional notice for page itself and any parent page
4726
			$parts = explode( '/', $this->getDBkey() );
4727
			$editnotice_base = $editnotice_ns;
4728
			while ( count( $parts ) > 0 ) {
4729
				$editnotice_base .= '-' . array_shift( $parts );
4730
				$msg = wfMessage( $editnotice_base );
4731 View Code Duplication
				if ( $msg->exists() ) {
4732
					$html = $msg->parseAsBlock();
4733
					if ( trim( $html ) !== '' ) {
4734
						$notices[$editnotice_base] = Html::rawElement(
4735
							'div',
4736
							[ 'class' => [
4737
								'mw-editnotice',
4738
								'mw-editnotice-base',
4739
								Sanitizer::escapeClass( "mw-$editnotice_base" )
4740
							] ],
4741
							$html
4742
						);
4743
					}
4744
				}
4745
			}
4746
		} else {
4747
			// Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys
4748
			$editnoticeText = $editnotice_ns . '-' . strtr( $this->getDBkey(), '/', '-' );
4749
			$msg = wfMessage( $editnoticeText );
4750 View Code Duplication
			if ( $msg->exists() ) {
4751
				$html = $msg->parseAsBlock();
4752
				if ( trim( $html ) !== '' ) {
4753
					$notices[$editnoticeText] = Html::rawElement(
4754
						'div',
4755
						[ 'class' => [
4756
							'mw-editnotice',
4757
							'mw-editnotice-page',
4758
							Sanitizer::escapeClass( "mw-$editnoticeText" )
4759
						] ],
4760
						$html
4761
					);
4762
				}
4763
			}
4764
		}
4765
4766
		Hooks::run( 'TitleGetEditNotices', [ $this, $oldid, &$notices ] );
4767
		return $notices;
4768
	}
4769
4770
	/**
4771
	 * @return array
4772
	 */
4773
	public function __sleep() {
4774
		return [
4775
			'mNamespace',
4776
			'mDbkeyform',
4777
			'mFragment',
4778
			'mInterwiki',
4779
			'mLocalInterwiki',
4780
			'mUserCaseDBKey',
4781
			'mDefaultNamespace',
4782
		];
4783
	}
4784
4785
	public function __wakeup() {
4786
		$this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0;
4787
		$this->mUrlform = wfUrlencode( $this->mDbkeyform );
4788
		$this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
4789
	}
4790
4791
}
4792