Completed
Branch master (5cbada)
by
unknown
28:59
created

Title::getPrefixedURL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

    return array();
}

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

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

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

There are different options of fixing this problem.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

could be turned into

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

This is much more concise to read.

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

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

Loading history...
1083
	 * @return bool
1084
	 * @since 1.19
1085
	 */
1086
	public function inNamespaces( /* ... */ ) {
1087
		$namespaces = func_get_args();
1088
		if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
1089
			$namespaces = $namespaces[0];
1090
		}
1091
1092
		foreach ( $namespaces as $ns ) {
1093
			if ( $this->inNamespace( $ns ) ) {
1094
				return true;
1095
			}
1096
		}
1097
1098
		return false;
1099
	}
1100
1101
	/**
1102
	 * Returns true if the title has the same subject namespace as the
1103
	 * namespace specified.
1104
	 * For example this method will take NS_USER and return true if namespace
1105
	 * is either NS_USER or NS_USER_TALK since both of them have NS_USER
1106
	 * as their subject namespace.
1107
	 *
1108
	 * This is MUCH simpler than individually testing for equivalence
1109
	 * against both NS_USER and NS_USER_TALK, and is also forward compatible.
1110
	 * @since 1.19
1111
	 * @param int $ns
1112
	 * @return bool
1113
	 */
1114
	public function hasSubjectNamespace( $ns ) {
1115
		return MWNamespace::subjectEquals( $this->getNamespace(), $ns );
1116
	}
1117
1118
	/**
1119
	 * Is this Title in a namespace which contains content?
1120
	 * In other words, is this a content page, for the purposes of calculating
1121
	 * statistics, etc?
1122
	 *
1123
	 * @return bool
1124
	 */
1125
	public function isContentPage() {
1126
		return MWNamespace::isContent( $this->getNamespace() );
1127
	}
1128
1129
	/**
1130
	 * Would anybody with sufficient privileges be able to move this page?
1131
	 * Some pages just aren't movable.
1132
	 *
1133
	 * @return bool
1134
	 */
1135
	public function isMovable() {
1136
		if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->isExternal() ) {
1137
			// Interwiki title or immovable namespace. Hooks don't get to override here
1138
			return false;
1139
		}
1140
1141
		$result = true;
1142
		Hooks::run( 'TitleIsMovable', [ $this, &$result ] );
1143
		return $result;
1144
	}
1145
1146
	/**
1147
	 * Is this the mainpage?
1148
	 * @note Title::newFromText seems to be sufficiently optimized by the title
1149
	 * cache that we don't need to over-optimize by doing direct comparisons and
1150
	 * accidentally creating new bugs where $title->equals( Title::newFromText() )
1151
	 * ends up reporting something differently than $title->isMainPage();
1152
	 *
1153
	 * @since 1.18
1154
	 * @return bool
1155
	 */
1156
	public function isMainPage() {
1157
		return $this->equals( Title::newMainPage() );
0 ignored issues
show
Bug introduced by
It seems like \Title::newMainPage() can be null; however, equals() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

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

An additional type check may prevent trouble.

Loading history...
1692
1693
		$interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki );
1694
		if ( $interwiki ) {
1695
			$namespace = $this->getNsText();
1696
			if ( $namespace != '' ) {
1697
				# Can this actually happen? Interwikis shouldn't be parsed.
1698
				# Yes! It can in interwiki transclusion. But... it probably shouldn't.
1699
				$namespace .= ':';
1700
			}
1701
			$url = $interwiki->getURL( $namespace . $this->getDBkey() );
1702
			$url = wfAppendQuery( $url, $query );
1703
		} else {
1704
			$dbkey = wfUrlencode( $this->getPrefixedDBkey() );
1705
			if ( $query == '' ) {
1706
				$url = str_replace( '$1', $dbkey, $wgArticlePath );
1707
				Hooks::run( 'GetLocalURL::Article', [ &$this, &$url ] );
1708
			} else {
1709
				global $wgVariantArticlePath, $wgActionPaths, $wgContLang;
1710
				$url = false;
1711
				$matches = [];
1712
1713
				if ( !empty( $wgActionPaths )
1714
					&& preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches )
1715
				) {
1716
					$action = urldecode( $matches[2] );
1717
					if ( isset( $wgActionPaths[$action] ) ) {
1718
						$query = $matches[1];
1719
						if ( isset( $matches[4] ) ) {
1720
							$query .= $matches[4];
1721
						}
1722
						$url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
1723
						if ( $query != '' ) {
1724
							$url = wfAppendQuery( $url, $query );
1725
						}
1726
					}
1727
				}
1728
1729
				if ( $url === false
1730
					&& $wgVariantArticlePath
1731
					&& preg_match( '/^variant=([^&]*)$/', $query, $matches )
1732
					&& $this->getPageLanguage()->equals( $wgContLang )
1733
					&& $this->getPageLanguage()->hasVariants()
1734
				) {
1735
					$variant = urldecode( $matches[1] );
1736
					if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
1737
						// Only do the variant replacement if the given variant is a valid
1738
						// variant for the page's language.
1739
						$url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath );
1740
						$url = str_replace( '$1', $dbkey, $url );
1741
					}
1742
				}
1743
1744
				if ( $url === false ) {
1745
					if ( $query == '-' ) {
1746
						$query = '';
1747
					}
1748
					$url = "{$wgScript}?title={$dbkey}&{$query}";
1749
				}
1750
			}
1751
1752
			Hooks::run( 'GetLocalURL::Internal', [ &$this, &$url, $query ] );
1753
1754
			// @todo FIXME: This causes breakage in various places when we
1755
			// actually expected a local URL and end up with dupe prefixes.
1756
			if ( $wgRequest->getVal( 'action' ) == 'render' ) {
1757
				$url = $wgServer . $url;
1758
			}
1759
		}
1760
		Hooks::run( 'GetLocalURL', [ &$this, &$url, $query ] );
1761
		return $url;
1762
	}
1763
1764
	/**
1765
	 * Get a URL that's the simplest URL that will be valid to link, locally,
1766
	 * to the current Title.  It includes the fragment, but does not include
1767
	 * the server unless action=render is used (or the link is external).  If
1768
	 * there's a fragment but the prefixed text is empty, we just return a link
1769
	 * to the fragment.
1770
	 *
1771
	 * The result obviously should not be URL-escaped, but does need to be
1772
	 * HTML-escaped if it's being output in HTML.
1773
	 *
1774
	 * @param array $query
1775
	 * @param bool $query2
1776
	 * @param string $proto Protocol to use; setting this will cause a full URL to be used
1777
	 * @see self::getLocalURL for the arguments.
1778
	 * @return string The URL
1779
	 */
1780
	public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
1781
		if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) {
1782
			$ret = $this->getFullURL( $query, $query2, $proto );
1783
		} elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) {
1784
			$ret = $this->getFragmentForURL();
1785
		} else {
1786
			$ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
1787
		}
1788
		return $ret;
1789
	}
1790
1791
	/**
1792
	 * Get the URL form for an internal link.
1793
	 * - Used in various CDN-related code, in case we have a different
1794
	 * internal hostname for the server from the exposed one.
1795
	 *
1796
	 * This uses $wgInternalServer to qualify the path, or $wgServer
1797
	 * if $wgInternalServer is not set. If the server variable used is
1798
	 * protocol-relative, the URL will be expanded to http://
1799
	 *
1800
	 * @see self::getLocalURL for the arguments.
1801
	 * @return string The URL
1802
	 */
1803
	public function getInternalURL( $query = '', $query2 = false ) {
1804
		global $wgInternalServer, $wgServer;
1805
		$query = self::fixUrlQueryArgs( $query, $query2 );
1806
		$server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
1807
		$url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
1808
		Hooks::run( 'GetInternalURL', [ &$this, &$url, $query ] );
1809
		return $url;
1810
	}
1811
1812
	/**
1813
	 * Get the URL for a canonical link, for use in things like IRC and
1814
	 * e-mail notifications. Uses $wgCanonicalServer and the
1815
	 * GetCanonicalURL hook.
1816
	 *
1817
	 * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment
1818
	 *
1819
	 * @see self::getLocalURL for the arguments.
1820
	 * @return string The URL
1821
	 * @since 1.18
1822
	 */
1823
	public function getCanonicalURL( $query = '', $query2 = false ) {
1824
		$query = self::fixUrlQueryArgs( $query, $query2 );
1825
		$url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
1826
		Hooks::run( 'GetCanonicalURL', [ &$this, &$url, $query ] );
1827
		return $url;
1828
	}
1829
1830
	/**
1831
	 * Get the edit URL for this Title
1832
	 *
1833
	 * @return string The URL, or a null string if this is an interwiki link
1834
	 */
1835
	public function getEditURL() {
1836
		if ( $this->isExternal() ) {
1837
			return '';
1838
		}
1839
		$s = $this->getLocalURL( 'action=edit' );
1840
1841
		return $s;
1842
	}
1843
1844
	/**
1845
	 * Can $user perform $action on this page?
1846
	 * This skips potentially expensive cascading permission checks
1847
	 * as well as avoids expensive error formatting
1848
	 *
1849
	 * Suitable for use for nonessential UI controls in common cases, but
1850
	 * _not_ for functional access control.
1851
	 *
1852
	 * May provide false positives, but should never provide a false negative.
1853
	 *
1854
	 * @param string $action Action that permission needs to be checked for
1855
	 * @param User $user User to check (since 1.19); $wgUser will be used if not provided.
1856
	 * @return bool
1857
	 */
1858
	public function quickUserCan( $action, $user = null ) {
1859
		return $this->userCan( $action, $user, false );
1860
	}
1861
1862
	/**
1863
	 * Can $user perform $action on this page?
1864
	 *
1865
	 * @param string $action Action that permission needs to be checked for
1866
	 * @param User $user User to check (since 1.19); $wgUser will be used if not
1867
	 *   provided.
1868
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
1869
	 * @return bool
1870
	 */
1871
	public function userCan( $action, $user = null, $rigor = 'secure' ) {
1872
		if ( !$user instanceof User ) {
1873
			global $wgUser;
1874
			$user = $wgUser;
1875
		}
1876
1877
		return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) );
1878
	}
1879
1880
	/**
1881
	 * Can $user perform $action on this page?
1882
	 *
1883
	 * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
1884
	 *
1885
	 * @param string $action Action that permission needs to be checked for
1886
	 * @param User $user User to check
1887
	 * @param string $rigor One of (quick,full,secure)
1888
	 *   - quick  : does cheap permission checks from slaves (usable for GUI creation)
1889
	 *   - full   : does cheap and expensive checks possibly from a slave
1890
	 *   - secure : does cheap and expensive checks, using the master as needed
1891
	 * @param array $ignoreErrors Array of Strings Set this to a list of message keys
1892
	 *   whose corresponding errors may be ignored.
1893
	 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
1894
	 */
1895
	public function getUserPermissionsErrors(
1896
		$action, $user, $rigor = 'secure', $ignoreErrors = []
1897
	) {
1898
		$errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor );
1899
1900
		// Remove the errors being ignored.
1901
		foreach ( $errors as $index => $error ) {
1902
			$errKey = is_array( $error ) ? $error[0] : $error;
1903
1904
			if ( in_array( $errKey, $ignoreErrors ) ) {
1905
				unset( $errors[$index] );
1906
			}
1907
			if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
1908
				unset( $errors[$index] );
1909
			}
1910
		}
1911
1912
		return $errors;
1913
	}
1914
1915
	/**
1916
	 * Permissions checks that fail most often, and which are easiest to test.
1917
	 *
1918
	 * @param string $action The action to check
1919
	 * @param User $user User to check
1920
	 * @param array $errors List of current errors
1921
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
1922
	 * @param bool $short Short circuit on first error
1923
	 *
1924
	 * @return array List of errors
1925
	 */
1926
	private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) {
1927
		if ( !Hooks::run( 'TitleQuickPermissions',
1928
			[ $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ] )
1929
		) {
1930
			return $errors;
1931
		}
1932
1933
		if ( $action == 'create' ) {
1934
			if (
1935
				( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
1936
				( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
1937
			) {
1938
				$errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
1939
			}
1940
		} elseif ( $action == 'move' ) {
1941 View Code Duplication
			if ( !$user->isAllowed( 'move-rootuserpages' )
1942
					&& $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1943
				// Show user page-specific message only if the user can move other pages
1944
				$errors[] = [ 'cant-move-user-page' ];
1945
			}
1946
1947
			// Check if user is allowed to move files if it's a file
1948
			if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
1949
				$errors[] = [ 'movenotallowedfile' ];
1950
			}
1951
1952
			// Check if user is allowed to move category pages if it's a category page
1953
			if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
1954
				$errors[] = [ 'cant-move-category-page' ];
1955
			}
1956
1957
			if ( !$user->isAllowed( 'move' ) ) {
1958
				// User can't move anything
1959
				$userCanMove = User::groupHasPermission( 'user', 'move' );
1960
				$autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
1961
				if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
1962
					// custom message if logged-in users without any special rights can move
1963
					$errors[] = [ 'movenologintext' ];
1964
				} else {
1965
					$errors[] = [ 'movenotallowed' ];
1966
				}
1967
			}
1968
		} elseif ( $action == 'move-target' ) {
1969
			if ( !$user->isAllowed( 'move' ) ) {
1970
				// User can't move anything
1971
				$errors[] = [ 'movenotallowed' ];
1972 View Code Duplication
			} elseif ( !$user->isAllowed( 'move-rootuserpages' )
1973
					&& $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1974
				// Show user page-specific message only if the user can move other pages
1975
				$errors[] = [ 'cant-move-to-user-page' ];
1976
			} elseif ( !$user->isAllowed( 'move-categorypages' )
1977
					&& $this->mNamespace == NS_CATEGORY ) {
1978
				// Show category page-specific message only if the user can move other pages
1979
				$errors[] = [ 'cant-move-to-category-page' ];
1980
			}
1981
		} elseif ( !$user->isAllowed( $action ) ) {
1982
			$errors[] = $this->missingPermissionError( $action, $short );
1983
		}
1984
1985
		return $errors;
1986
	}
1987
1988
	/**
1989
	 * Add the resulting error code to the errors array
1990
	 *
1991
	 * @param array $errors List of current errors
1992
	 * @param array $result Result of errors
1993
	 *
1994
	 * @return array List of errors
1995
	 */
1996
	private function resultToError( $errors, $result ) {
1997
		if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
1998
			// A single array representing an error
1999
			$errors[] = $result;
2000
		} elseif ( is_array( $result ) && is_array( $result[0] ) ) {
2001
			// A nested array representing multiple errors
2002
			$errors = array_merge( $errors, $result );
2003
		} elseif ( $result !== '' && is_string( $result ) ) {
2004
			// A string representing a message-id
2005
			$errors[] = [ $result ];
2006
		} elseif ( $result instanceof MessageSpecifier ) {
2007
			// A message specifier representing an error
2008
			$errors[] = [ $result ];
2009
		} elseif ( $result === false ) {
2010
			// a generic "We don't want them to do that"
2011
			$errors[] = [ 'badaccess-group0' ];
2012
		}
2013
		return $errors;
2014
	}
2015
2016
	/**
2017
	 * Check various permission hooks
2018
	 *
2019
	 * @param string $action The action to check
2020
	 * @param User $user User to check
2021
	 * @param array $errors List of current errors
2022
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2023
	 * @param bool $short Short circuit on first error
2024
	 *
2025
	 * @return array List of errors
2026
	 */
2027
	private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) {
2028
		// Use getUserPermissionsErrors instead
2029
		$result = '';
2030
		if ( !Hooks::run( 'userCan', [ &$this, &$user, $action, &$result ] ) ) {
2031
			return $result ? [] : [ [ 'badaccess-group0' ] ];
2032
		}
2033
		// Check getUserPermissionsErrors hook
2034
		if ( !Hooks::run( 'getUserPermissionsErrors', [ &$this, &$user, $action, &$result ] ) ) {
2035
			$errors = $this->resultToError( $errors, $result );
2036
		}
2037
		// Check getUserPermissionsErrorsExpensive hook
2038
		if (
2039
			$rigor !== 'quick'
2040
			&& !( $short && count( $errors ) > 0 )
2041
			&& !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$this, &$user, $action, &$result ] )
2042
		) {
2043
			$errors = $this->resultToError( $errors, $result );
2044
		}
2045
2046
		return $errors;
2047
	}
2048
2049
	/**
2050
	 * Check permissions on special pages & namespaces
2051
	 *
2052
	 * @param string $action The action to check
2053
	 * @param User $user User to check
2054
	 * @param array $errors List of current errors
2055
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2056
	 * @param bool $short Short circuit on first error
2057
	 *
2058
	 * @return array List of errors
2059
	 */
2060
	private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) {
2061
		# Only 'createaccount' can be performed on special pages,
2062
		# which don't actually exist in the DB.
2063
		if ( NS_SPECIAL == $this->mNamespace && $action !== 'createaccount' ) {
2064
			$errors[] = [ 'ns-specialprotected' ];
2065
		}
2066
2067
		# Check $wgNamespaceProtection for restricted namespaces
2068
		if ( $this->isNamespaceProtected( $user ) ) {
2069
			$ns = $this->mNamespace == NS_MAIN ?
2070
				wfMessage( 'nstab-main' )->text() : $this->getNsText();
2071
			$errors[] = $this->mNamespace == NS_MEDIAWIKI ?
2072
				[ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
2073
		}
2074
2075
		return $errors;
2076
	}
2077
2078
	/**
2079
	 * Check CSS/JS sub-page permissions
2080
	 *
2081
	 * @param string $action The action to check
2082
	 * @param User $user User to check
2083
	 * @param array $errors List of current errors
2084
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2085
	 * @param bool $short Short circuit on first error
2086
	 *
2087
	 * @return array List of errors
2088
	 */
2089
	private function checkCSSandJSPermissions( $action, $user, $errors, $rigor, $short ) {
2090
		# Protect css/js subpages of user pages
2091
		# XXX: this might be better using restrictions
2092
		# XXX: right 'editusercssjs' is deprecated, for backward compatibility only
2093
		if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) {
2094
			if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
2095 View Code Duplication
				if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) {
2096
					$errors[] = [ 'mycustomcssprotected', $action ];
2097
				} elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) {
2098
					$errors[] = [ 'mycustomjsprotected', $action ];
2099
				}
2100 View Code Duplication
			} else {
2101
				if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) {
2102
					$errors[] = [ 'customcssprotected', $action ];
2103
				} elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
2104
					$errors[] = [ 'customjsprotected', $action ];
2105
				}
2106
			}
2107
		}
2108
2109
		return $errors;
2110
	}
2111
2112
	/**
2113
	 * Check against page_restrictions table requirements on this
2114
	 * page. The user must possess all required rights for this
2115
	 * action.
2116
	 *
2117
	 * @param string $action The action to check
2118
	 * @param User $user User to check
2119
	 * @param array $errors List of current errors
2120
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2121
	 * @param bool $short Short circuit on first error
2122
	 *
2123
	 * @return array List of errors
2124
	 */
2125
	private function checkPageRestrictions( $action, $user, $errors, $rigor, $short ) {
2126
		foreach ( $this->getRestrictions( $action ) as $right ) {
2127
			// Backwards compatibility, rewrite sysop -> editprotected
2128
			if ( $right == 'sysop' ) {
2129
				$right = 'editprotected';
2130
			}
2131
			// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2132
			if ( $right == 'autoconfirmed' ) {
2133
				$right = 'editsemiprotected';
2134
			}
2135
			if ( $right == '' ) {
2136
				continue;
2137
			}
2138
			if ( !$user->isAllowed( $right ) ) {
2139
				$errors[] = [ 'protectedpagetext', $right, $action ];
2140
			} elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
2141
				$errors[] = [ 'protectedpagetext', 'protect', $action ];
2142
			}
2143
		}
2144
2145
		return $errors;
2146
	}
2147
2148
	/**
2149
	 * Check restrictions on cascading pages.
2150
	 *
2151
	 * @param string $action The action to check
2152
	 * @param User $user User to check
2153
	 * @param array $errors List of current errors
2154
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2155
	 * @param bool $short Short circuit on first error
2156
	 *
2157
	 * @return array List of errors
2158
	 */
2159
	private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
2160
		if ( $rigor !== 'quick' && !$this->isCssJsSubpage() ) {
2161
			# We /could/ use the protection level on the source page, but it's
2162
			# fairly ugly as we have to establish a precedence hierarchy for pages
2163
			# included by multiple cascade-protected pages. So just restrict
2164
			# it to people with 'protect' permission, as they could remove the
2165
			# protection anyway.
2166
			list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
2167
			# Cascading protection depends on more than this page...
2168
			# Several cascading protected pages may include this page...
2169
			# Check each cascading level
2170
			# This is only for protection restrictions, not for all actions
2171
			if ( isset( $restrictions[$action] ) ) {
2172
				foreach ( $restrictions[$action] as $right ) {
2173
					// Backwards compatibility, rewrite sysop -> editprotected
2174
					if ( $right == 'sysop' ) {
2175
						$right = 'editprotected';
2176
					}
2177
					// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2178
					if ( $right == 'autoconfirmed' ) {
2179
						$right = 'editsemiprotected';
2180
					}
2181
					if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
2182
						$pages = '';
2183
						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...
2184
							$pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
2185
						}
2186
						$errors[] = [ 'cascadeprotected', count( $cascadingSources ), $pages, $action ];
2187
					}
2188
				}
2189
			}
2190
		}
2191
2192
		return $errors;
2193
	}
2194
2195
	/**
2196
	 * Check action permissions not already checked in checkQuickPermissions
2197
	 *
2198
	 * @param string $action The action to check
2199
	 * @param User $user User to check
2200
	 * @param array $errors List of current errors
2201
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2202
	 * @param bool $short Short circuit on first error
2203
	 *
2204
	 * @return array List of errors
2205
	 */
2206
	private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) {
2207
		global $wgDeleteRevisionsLimit, $wgLang;
2208
2209
		if ( $action == 'protect' ) {
2210
			if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
2211
				// If they can't edit, they shouldn't protect.
2212
				$errors[] = [ 'protect-cantedit' ];
2213
			}
2214
		} elseif ( $action == 'create' ) {
2215
			$title_protection = $this->getTitleProtection();
2216
			if ( $title_protection ) {
2217
				if ( $title_protection['permission'] == ''
2218
					|| !$user->isAllowed( $title_protection['permission'] )
2219
				) {
2220
					$errors[] = [
2221
						'titleprotected',
2222
						User::whoIs( $title_protection['user'] ),
2223
						$title_protection['reason']
2224
					];
2225
				}
2226
			}
2227
		} elseif ( $action == 'move' ) {
2228
			// Check for immobile pages
2229
			if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2230
				// Specific message for this case
2231
				$errors[] = [ 'immobile-source-namespace', $this->getNsText() ];
2232
			} elseif ( !$this->isMovable() ) {
2233
				// Less specific message for rarer cases
2234
				$errors[] = [ 'immobile-source-page' ];
2235
			}
2236
		} elseif ( $action == 'move-target' ) {
2237
			if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2238
				$errors[] = [ 'immobile-target-namespace', $this->getNsText() ];
2239
			} elseif ( !$this->isMovable() ) {
2240
				$errors[] = [ 'immobile-target-page' ];
2241
			}
2242
		} elseif ( $action == 'delete' ) {
2243
			$tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true );
2244
			if ( !$tempErrors ) {
2245
				$tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
2246
					$user, $tempErrors, $rigor, true );
2247
			}
2248
			if ( $tempErrors ) {
2249
				// If protection keeps them from editing, they shouldn't be able to delete.
2250
				$errors[] = [ 'deleteprotected' ];
2251
			}
2252
			if ( $rigor !== 'quick' && $wgDeleteRevisionsLimit
2253
				&& !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion()
2254
			) {
2255
				$errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
2256
			}
2257
		}
2258
		return $errors;
2259
	}
2260
2261
	/**
2262
	 * Check that the user isn't blocked from editing.
2263
	 *
2264
	 * @param string $action The action to check
2265
	 * @param User $user User to check
2266
	 * @param array $errors List of current errors
2267
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2268
	 * @param bool $short Short circuit on first error
2269
	 *
2270
	 * @return array List of errors
2271
	 */
2272
	private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
2273
		// Account creation blocks handled at userlogin.
2274
		// Unblocking handled in SpecialUnblock
2275
		if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
2276
			return $errors;
2277
		}
2278
2279
		global $wgEmailConfirmToEdit;
2280
2281
		if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
2282
			$errors[] = [ 'confirmedittext' ];
2283
		}
2284
2285
		$useSlave = ( $rigor !== 'secure' );
2286
		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...
2287
			&& !$user->isBlockedFrom( $this, $useSlave )
2288
		) {
2289
			// Don't block the user from editing their own talk page unless they've been
2290
			// explicitly blocked from that too.
2291
		} elseif ( $user->isBlocked() && $user->getBlock()->prevents( $action ) !== false ) {
2292
			// @todo FIXME: Pass the relevant context into this function.
2293
			$errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() );
2294
		}
2295
2296
		return $errors;
2297
	}
2298
2299
	/**
2300
	 * Check that the user is allowed to read this page.
2301
	 *
2302
	 * @param string $action The action to check
2303
	 * @param User $user User to check
2304
	 * @param array $errors List of current errors
2305
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2306
	 * @param bool $short Short circuit on first error
2307
	 *
2308
	 * @return array List of errors
2309
	 */
2310
	private function checkReadPermissions( $action, $user, $errors, $rigor, $short ) {
2311
		global $wgWhitelistRead, $wgWhitelistReadRegexp;
2312
2313
		$whitelisted = false;
2314
		if ( User::isEveryoneAllowed( 'read' ) ) {
2315
			# Shortcut for public wikis, allows skipping quite a bit of code
2316
			$whitelisted = true;
2317
		} elseif ( $user->isAllowed( 'read' ) ) {
2318
			# If the user is allowed to read pages, he is allowed to read all pages
2319
			$whitelisted = true;
2320
		} elseif ( $this->isSpecial( 'Userlogin' )
2321
			|| $this->isSpecial( 'ChangePassword' )
2322
			|| $this->isSpecial( 'PasswordReset' )
2323
		) {
2324
			# Always grant access to the login page.
2325
			# Even anons need to be able to log in.
2326
			$whitelisted = true;
2327
		} elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
2328
			# Time to check the whitelist
2329
			# Only do these checks is there's something to check against
2330
			$name = $this->getPrefixedText();
2331
			$dbName = $this->getPrefixedDBkey();
2332
2333
			// Check for explicit whitelisting with and without underscores
2334
			if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
2335
				$whitelisted = true;
2336
			} elseif ( $this->getNamespace() == NS_MAIN ) {
2337
				# Old settings might have the title prefixed with
2338
				# a colon for main-namespace pages
2339
				if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
2340
					$whitelisted = true;
2341
				}
2342
			} elseif ( $this->isSpecialPage() ) {
2343
				# If it's a special page, ditch the subpage bit and check again
2344
				$name = $this->getDBkey();
2345
				list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name );
2346
				if ( $name ) {
2347
					$pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
2348
					if ( in_array( $pure, $wgWhitelistRead, true ) ) {
2349
						$whitelisted = true;
2350
					}
2351
				}
2352
			}
2353
		}
2354
2355
		if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) {
2356
			$name = $this->getPrefixedText();
2357
			// Check for regex whitelisting
2358
			foreach ( $wgWhitelistReadRegexp as $listItem ) {
2359
				if ( preg_match( $listItem, $name ) ) {
2360
					$whitelisted = true;
2361
					break;
2362
				}
2363
			}
2364
		}
2365
2366
		if ( !$whitelisted ) {
2367
			# If the title is not whitelisted, give extensions a chance to do so...
2368
			Hooks::run( 'TitleReadWhitelist', [ $this, $user, &$whitelisted ] );
2369
			if ( !$whitelisted ) {
2370
				$errors[] = $this->missingPermissionError( $action, $short );
2371
			}
2372
		}
2373
2374
		return $errors;
2375
	}
2376
2377
	/**
2378
	 * Get a description array when the user doesn't have the right to perform
2379
	 * $action (i.e. when User::isAllowed() returns false)
2380
	 *
2381
	 * @param string $action The action to check
2382
	 * @param bool $short Short circuit on first error
2383
	 * @return array List of errors
2384
	 */
2385
	private function missingPermissionError( $action, $short ) {
2386
		// We avoid expensive display logic for quickUserCan's and such
2387
		if ( $short ) {
2388
			return [ 'badaccess-group0' ];
2389
		}
2390
2391
		$groups = array_map( [ 'User', 'makeGroupLinkWiki' ],
2392
			User::getGroupsWithPermission( $action ) );
2393
2394
		if ( count( $groups ) ) {
2395
			global $wgLang;
2396
			return [
2397
				'badaccess-groups',
2398
				$wgLang->commaList( $groups ),
2399
				count( $groups )
2400
			];
2401
		} else {
2402
			return [ 'badaccess-group0' ];
2403
		}
2404
	}
2405
2406
	/**
2407
	 * Can $user perform $action on this page? This is an internal function,
2408
	 * with multiple levels of checks depending on performance needs; see $rigor below.
2409
	 * It does not check wfReadOnly().
2410
	 *
2411
	 * @param string $action Action that permission needs to be checked for
2412
	 * @param User $user User to check
2413
	 * @param string $rigor One of (quick,full,secure)
2414
	 *   - quick  : does cheap permission checks from slaves (usable for GUI creation)
2415
	 *   - full   : does cheap and expensive checks possibly from a slave
2416
	 *   - secure : does cheap and expensive checks, using the master as needed
2417
	 * @param bool $short Set this to true to stop after the first permission error.
2418
	 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
2419
	 */
2420
	protected function getUserPermissionsErrorsInternal(
2421
		$action, $user, $rigor = 'secure', $short = false
2422
	) {
2423
		if ( $rigor === true ) {
2424
			$rigor = 'secure'; // b/c
2425
		} elseif ( $rigor === false ) {
2426
			$rigor = 'quick'; // b/c
2427
		} elseif ( !in_array( $rigor, [ 'quick', 'full', 'secure' ] ) ) {
2428
			throw new Exception( "Invalid rigor parameter '$rigor'." );
2429
		}
2430
2431
		# Read has special handling
2432
		if ( $action == 'read' ) {
2433
			$checks = [
2434
				'checkPermissionHooks',
2435
				'checkReadPermissions',
2436
			];
2437
		# Don't call checkSpecialsAndNSPermissions or checkCSSandJSPermissions
2438
		# here as it will lead to duplicate error messages. This is okay to do
2439
		# since anywhere that checks for create will also check for edit, and
2440
		# those checks are called for edit.
2441
		} elseif ( $action == 'create' ) {
2442
			$checks = [
2443
				'checkQuickPermissions',
2444
				'checkPermissionHooks',
2445
				'checkPageRestrictions',
2446
				'checkCascadingSourcesRestrictions',
2447
				'checkActionPermissions',
2448
				'checkUserBlock'
2449
			];
2450
		} else {
2451
			$checks = [
2452
				'checkQuickPermissions',
2453
				'checkPermissionHooks',
2454
				'checkSpecialsAndNSPermissions',
2455
				'checkCSSandJSPermissions',
2456
				'checkPageRestrictions',
2457
				'checkCascadingSourcesRestrictions',
2458
				'checkActionPermissions',
2459
				'checkUserBlock'
2460
			];
2461
		}
2462
2463
		$errors = [];
2464
		while ( count( $checks ) > 0 &&
2465
				!( $short && count( $errors ) > 0 ) ) {
2466
			$method = array_shift( $checks );
2467
			$errors = $this->$method( $action, $user, $errors, $rigor, $short );
2468
		}
2469
2470
		return $errors;
2471
	}
2472
2473
	/**
2474
	 * Get a filtered list of all restriction types supported by this wiki.
2475
	 * @param bool $exists True to get all restriction types that apply to
2476
	 * titles that do exist, False for all restriction types that apply to
2477
	 * titles that do not exist
2478
	 * @return array
2479
	 */
2480
	public static function getFilteredRestrictionTypes( $exists = true ) {
2481
		global $wgRestrictionTypes;
2482
		$types = $wgRestrictionTypes;
2483
		if ( $exists ) {
2484
			# Remove the create restriction for existing titles
2485
			$types = array_diff( $types, [ 'create' ] );
2486
		} else {
2487
			# Only the create and upload restrictions apply to non-existing titles
2488
			$types = array_intersect( $types, [ 'create', 'upload' ] );
2489
		}
2490
		return $types;
2491
	}
2492
2493
	/**
2494
	 * Returns restriction types for the current Title
2495
	 *
2496
	 * @return array Applicable restriction types
2497
	 */
2498
	public function getRestrictionTypes() {
2499
		if ( $this->isSpecialPage() ) {
2500
			return [];
2501
		}
2502
2503
		$types = self::getFilteredRestrictionTypes( $this->exists() );
2504
2505
		if ( $this->getNamespace() != NS_FILE ) {
2506
			# Remove the upload restriction for non-file titles
2507
			$types = array_diff( $types, [ 'upload' ] );
2508
		}
2509
2510
		Hooks::run( 'TitleGetRestrictionTypes', [ $this, &$types ] );
2511
2512
		wfDebug( __METHOD__ . ': applicable restrictions to [[' .
2513
			$this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
2514
2515
		return $types;
2516
	}
2517
2518
	/**
2519
	 * Is this title subject to title protection?
2520
	 * Title protection is the one applied against creation of such title.
2521
	 *
2522
	 * @return array|bool An associative array representing any existent title
2523
	 *   protection, or false if there's none.
2524
	 */
2525
	public function getTitleProtection() {
2526
		// Can't protect pages in special namespaces
2527
		if ( $this->getNamespace() < 0 ) {
2528
			return false;
2529
		}
2530
2531
		// Can't protect pages that exist.
2532
		if ( $this->exists() ) {
2533
			return false;
2534
		}
2535
2536
		if ( $this->mTitleProtection === null ) {
2537
			$dbr = wfGetDB( DB_SLAVE );
2538
			$res = $dbr->select(
2539
				'protected_titles',
2540
				[
2541
					'user' => 'pt_user',
2542
					'reason' => 'pt_reason',
2543
					'expiry' => 'pt_expiry',
2544
					'permission' => 'pt_create_perm'
2545
				],
2546
				[ 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ],
2547
				__METHOD__
2548
			);
2549
2550
			// fetchRow returns false if there are no rows.
2551
			$row = $dbr->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $dbr->select('protected_...etDBkey()), __METHOD__) on line 2538 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...
2552
			if ( $row ) {
2553
				if ( $row['permission'] == 'sysop' ) {
2554
					$row['permission'] = 'editprotected'; // B/C
2555
				}
2556
				if ( $row['permission'] == 'autoconfirmed' ) {
2557
					$row['permission'] = 'editsemiprotected'; // B/C
2558
				}
2559
				$row['expiry'] = $dbr->decodeExpiry( $row['expiry'] );
2560
			}
2561
			$this->mTitleProtection = $row;
2562
		}
2563
		return $this->mTitleProtection;
2564
	}
2565
2566
	/**
2567
	 * Remove any title protection due to page existing
2568
	 */
2569
	public function deleteTitleProtection() {
2570
		$dbw = wfGetDB( DB_MASTER );
2571
2572
		$dbw->delete(
2573
			'protected_titles',
2574
			[ 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ],
2575
			__METHOD__
2576
		);
2577
		$this->mTitleProtection = false;
2578
	}
2579
2580
	/**
2581
	 * Is this page "semi-protected" - the *only* protection levels are listed
2582
	 * in $wgSemiprotectedRestrictionLevels?
2583
	 *
2584
	 * @param string $action Action to check (default: edit)
2585
	 * @return bool
2586
	 */
2587
	public function isSemiProtected( $action = 'edit' ) {
2588
		global $wgSemiprotectedRestrictionLevels;
2589
2590
		$restrictions = $this->getRestrictions( $action );
2591
		$semi = $wgSemiprotectedRestrictionLevels;
2592
		if ( !$restrictions || !$semi ) {
2593
			// Not protected, or all protection is full protection
2594
			return false;
2595
		}
2596
2597
		// Remap autoconfirmed to editsemiprotected for BC
2598
		foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) {
2599
			$semi[$key] = 'editsemiprotected';
2600
		}
2601
		foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) {
2602
			$restrictions[$key] = 'editsemiprotected';
2603
		}
2604
2605
		return !array_diff( $restrictions, $semi );
2606
	}
2607
2608
	/**
2609
	 * Does the title correspond to a protected article?
2610
	 *
2611
	 * @param string $action The action the page is protected from,
2612
	 * by default checks all actions.
2613
	 * @return bool
2614
	 */
2615
	public function isProtected( $action = '' ) {
2616
		global $wgRestrictionLevels;
2617
2618
		$restrictionTypes = $this->getRestrictionTypes();
2619
2620
		# Special pages have inherent protection
2621
		if ( $this->isSpecialPage() ) {
2622
			return true;
2623
		}
2624
2625
		# Check regular protection levels
2626
		foreach ( $restrictionTypes as $type ) {
2627
			if ( $action == $type || $action == '' ) {
2628
				$r = $this->getRestrictions( $type );
2629
				foreach ( $wgRestrictionLevels as $level ) {
2630
					if ( in_array( $level, $r ) && $level != '' ) {
2631
						return true;
2632
					}
2633
				}
2634
			}
2635
		}
2636
2637
		return false;
2638
	}
2639
2640
	/**
2641
	 * Determines if $user is unable to edit this page because it has been protected
2642
	 * by $wgNamespaceProtection.
2643
	 *
2644
	 * @param User $user User object to check permissions
2645
	 * @return bool
2646
	 */
2647
	public function isNamespaceProtected( User $user ) {
2648
		global $wgNamespaceProtection;
2649
2650
		if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
2651
			foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
2652
				if ( $right != '' && !$user->isAllowed( $right ) ) {
2653
					return true;
2654
				}
2655
			}
2656
		}
2657
		return false;
2658
	}
2659
2660
	/**
2661
	 * Cascading protection: Return true if cascading restrictions apply to this page, false if not.
2662
	 *
2663
	 * @return bool If the page is subject to cascading restrictions.
2664
	 */
2665
	public function isCascadeProtected() {
2666
		list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
2667
		return ( $sources > 0 );
2668
	}
2669
2670
	/**
2671
	 * Determines whether cascading protection sources have already been loaded from
2672
	 * the database.
2673
	 *
2674
	 * @param bool $getPages True to check if the pages are loaded, or false to check
2675
	 * if the status is loaded.
2676
	 * @return bool Whether or not the specified information has been loaded
2677
	 * @since 1.23
2678
	 */
2679
	public function areCascadeProtectionSourcesLoaded( $getPages = true ) {
2680
		return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null;
2681
	}
2682
2683
	/**
2684
	 * Cascading protection: Get the source of any cascading restrictions on this page.
2685
	 *
2686
	 * @param bool $getPages Whether or not to retrieve the actual pages
2687
	 *        that the restrictions have come from and the actual restrictions
2688
	 *        themselves.
2689
	 * @return array Two elements: First is an array of Title objects of the
2690
	 *        pages from which cascading restrictions have come, false for
2691
	 *        none, or true if such restrictions exist but $getPages was not
2692
	 *        set. Second is an array like that returned by
2693
	 *        Title::getAllRestrictions(), or an empty array if $getPages is
2694
	 *        false.
2695
	 */
2696
	public function getCascadeProtectionSources( $getPages = true ) {
2697
		$pagerestrictions = [];
2698
2699
		if ( $this->mCascadeSources !== null && $getPages ) {
2700
			return [ $this->mCascadeSources, $this->mCascadingRestrictions ];
2701
		} elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) {
2702
			return [ $this->mHasCascadingRestrictions, $pagerestrictions ];
2703
		}
2704
2705
		$dbr = wfGetDB( DB_SLAVE );
2706
2707
		if ( $this->getNamespace() == NS_FILE ) {
2708
			$tables = [ 'imagelinks', 'page_restrictions' ];
2709
			$where_clauses = [
2710
				'il_to' => $this->getDBkey(),
2711
				'il_from=pr_page',
2712
				'pr_cascade' => 1
2713
			];
2714
		} else {
2715
			$tables = [ 'templatelinks', 'page_restrictions' ];
2716
			$where_clauses = [
2717
				'tl_namespace' => $this->getNamespace(),
2718
				'tl_title' => $this->getDBkey(),
2719
				'tl_from=pr_page',
2720
				'pr_cascade' => 1
2721
			];
2722
		}
2723
2724
		if ( $getPages ) {
2725
			$cols = [ 'pr_page', 'page_namespace', 'page_title',
2726
				'pr_expiry', 'pr_type', 'pr_level' ];
2727
			$where_clauses[] = 'page_id=pr_page';
2728
			$tables[] = 'page';
2729
		} else {
2730
			$cols = [ 'pr_expiry' ];
2731
		}
2732
2733
		$res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
2734
2735
		$sources = $getPages ? [] : false;
2736
		$now = wfTimestampNow();
2737
2738
		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...
2739
			$expiry = $dbr->decodeExpiry( $row->pr_expiry );
2740
			if ( $expiry > $now ) {
2741
				if ( $getPages ) {
2742
					$page_id = $row->pr_page;
2743
					$page_ns = $row->page_namespace;
2744
					$page_title = $row->page_title;
2745
					$sources[$page_id] = Title::makeTitle( $page_ns, $page_title );
2746
					# Add groups needed for each restriction type if its not already there
2747
					# Make sure this restriction type still exists
2748
2749
					if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
2750
						$pagerestrictions[$row->pr_type] = [];
2751
					}
2752
2753
					if (
2754
						isset( $pagerestrictions[$row->pr_type] )
2755
						&& !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] )
2756
					) {
2757
						$pagerestrictions[$row->pr_type][] = $row->pr_level;
2758
					}
2759
				} else {
2760
					$sources = true;
2761
				}
2762
			}
2763
		}
2764
2765
		if ( $getPages ) {
2766
			$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...
2767
			$this->mCascadingRestrictions = $pagerestrictions;
2768
		} else {
2769
			$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...
2770
		}
2771
2772
		return [ $sources, $pagerestrictions ];
2773
	}
2774
2775
	/**
2776
	 * Accessor for mRestrictionsLoaded
2777
	 *
2778
	 * @return bool Whether or not the page's restrictions have already been
2779
	 * loaded from the database
2780
	 * @since 1.23
2781
	 */
2782
	public function areRestrictionsLoaded() {
2783
		return $this->mRestrictionsLoaded;
2784
	}
2785
2786
	/**
2787
	 * Accessor/initialisation for mRestrictions
2788
	 *
2789
	 * @param string $action Action that permission needs to be checked for
2790
	 * @return array Restriction levels needed to take the action. All levels are
2791
	 *     required. Note that restriction levels are normally user rights, but 'sysop'
2792
	 *     and 'autoconfirmed' are also allowed for backwards compatibility. These should
2793
	 *     be mapped to 'editprotected' and 'editsemiprotected' respectively.
2794
	 */
2795
	public function getRestrictions( $action ) {
2796
		if ( !$this->mRestrictionsLoaded ) {
2797
			$this->loadRestrictions();
2798
		}
2799
		return isset( $this->mRestrictions[$action] )
2800
				? $this->mRestrictions[$action]
2801
				: [];
2802
	}
2803
2804
	/**
2805
	 * Accessor/initialisation for mRestrictions
2806
	 *
2807
	 * @return array Keys are actions, values are arrays as returned by
2808
	 *     Title::getRestrictions()
2809
	 * @since 1.23
2810
	 */
2811
	public function getAllRestrictions() {
2812
		if ( !$this->mRestrictionsLoaded ) {
2813
			$this->loadRestrictions();
2814
		}
2815
		return $this->mRestrictions;
2816
	}
2817
2818
	/**
2819
	 * Get the expiry time for the restriction against a given action
2820
	 *
2821
	 * @param string $action
2822
	 * @return string|bool 14-char timestamp, or 'infinity' if the page is protected forever
2823
	 *     or not protected at all, or false if the action is not recognised.
2824
	 */
2825
	public function getRestrictionExpiry( $action ) {
2826
		if ( !$this->mRestrictionsLoaded ) {
2827
			$this->loadRestrictions();
2828
		}
2829
		return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
2830
	}
2831
2832
	/**
2833
	 * Returns cascading restrictions for the current article
2834
	 *
2835
	 * @return bool
2836
	 */
2837
	function areRestrictionsCascading() {
2838
		if ( !$this->mRestrictionsLoaded ) {
2839
			$this->loadRestrictions();
2840
		}
2841
2842
		return $this->mCascadeRestriction;
2843
	}
2844
2845
	/**
2846
	 * Loads a string into mRestrictions array
2847
	 *
2848
	 * @param ResultWrapper $res Resource restrictions as an SQL result.
2849
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2850
	 *        restrictions from page table (pre 1.10)
2851
	 */
2852
	private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) {
2853
		$rows = [];
2854
2855
		foreach ( $res as $row ) {
2856
			$rows[] = $row;
2857
		}
2858
2859
		$this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
2860
	}
2861
2862
	/**
2863
	 * Compiles list of active page restrictions from both page table (pre 1.10)
2864
	 * and page_restrictions table for this existing page.
2865
	 * Public for usage by LiquidThreads.
2866
	 *
2867
	 * @param array $rows Array of db result objects
2868
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2869
	 *   restrictions from page table (pre 1.10)
2870
	 */
2871
	public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
2872
		$dbr = wfGetDB( DB_SLAVE );
2873
2874
		$restrictionTypes = $this->getRestrictionTypes();
2875
2876
		foreach ( $restrictionTypes as $type ) {
2877
			$this->mRestrictions[$type] = [];
2878
			$this->mRestrictionsExpiry[$type] = 'infinity';
2879
		}
2880
2881
		$this->mCascadeRestriction = false;
2882
2883
		# Backwards-compatibility: also load the restrictions from the page record (old format).
2884
		if ( $oldFashionedRestrictions !== null ) {
2885
			$this->mOldRestrictions = $oldFashionedRestrictions;
2886
		}
2887
2888
		if ( $this->mOldRestrictions === false ) {
2889
			$this->mOldRestrictions = $dbr->selectField( 'page', 'page_restrictions',
2890
				[ 'page_id' => $this->getArticleID() ], __METHOD__ );
2891
		}
2892
2893
		if ( $this->mOldRestrictions != '' ) {
2894
			foreach ( explode( ':', trim( $this->mOldRestrictions ) ) as $restrict ) {
2895
				$temp = explode( '=', trim( $restrict ) );
2896
				if ( count( $temp ) == 1 ) {
2897
					// old old format should be treated as edit/move restriction
2898
					$this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
2899
					$this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
2900
				} else {
2901
					$restriction = trim( $temp[1] );
2902
					if ( $restriction != '' ) { // some old entries are empty
2903
						$this->mRestrictions[$temp[0]] = explode( ',', $restriction );
2904
					}
2905
				}
2906
			}
2907
		}
2908
2909
		if ( count( $rows ) ) {
2910
			# Current system - load second to make them override.
2911
			$now = wfTimestampNow();
2912
2913
			# Cycle through all the restrictions.
2914
			foreach ( $rows as $row ) {
2915
2916
				// Don't take care of restrictions types that aren't allowed
2917
				if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
2918
					continue;
2919
				}
2920
2921
				// This code should be refactored, now that it's being used more generally,
2922
				// But I don't really see any harm in leaving it in Block for now -werdna
2923
				$expiry = $dbr->decodeExpiry( $row->pr_expiry );
2924
2925
				// Only apply the restrictions if they haven't expired!
2926
				if ( !$expiry || $expiry > $now ) {
2927
					$this->mRestrictionsExpiry[$row->pr_type] = $expiry;
2928
					$this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
2929
2930
					$this->mCascadeRestriction |= $row->pr_cascade;
2931
				}
2932
			}
2933
		}
2934
2935
		$this->mRestrictionsLoaded = true;
2936
	}
2937
2938
	/**
2939
	 * Load restrictions from the page_restrictions table
2940
	 *
2941
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2942
	 *   restrictions from page table (pre 1.10)
2943
	 */
2944
	public function loadRestrictions( $oldFashionedRestrictions = null ) {
2945
		if ( !$this->mRestrictionsLoaded ) {
2946
			$dbr = wfGetDB( DB_SLAVE );
2947
			if ( $this->exists() ) {
2948
				$res = $dbr->select(
2949
					'page_restrictions',
2950
					[ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ],
2951
					[ 'pr_page' => $this->getArticleID() ],
2952
					__METHOD__
2953
				);
2954
2955
				$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 2948 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...
2956
			} else {
2957
				$title_protection = $this->getTitleProtection();
2958
2959
				if ( $title_protection ) {
2960
					$now = wfTimestampNow();
2961
					$expiry = $dbr->decodeExpiry( $title_protection['expiry'] );
2962
2963
					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...
2964
						// Apply the restrictions
2965
						$this->mRestrictionsExpiry['create'] = $expiry;
2966
						$this->mRestrictions['create'] = explode( ',', trim( $title_protection['permission'] ) );
2967
					} else { // Get rid of the old restrictions
2968
						$this->mTitleProtection = false;
2969
					}
2970
				} else {
2971
					$this->mRestrictionsExpiry['create'] = 'infinity';
2972
				}
2973
				$this->mRestrictionsLoaded = true;
2974
			}
2975
		}
2976
	}
2977
2978
	/**
2979
	 * Flush the protection cache in this object and force reload from the database.
2980
	 * This is used when updating protection from WikiPage::doUpdateRestrictions().
2981
	 */
2982
	public function flushRestrictions() {
2983
		$this->mRestrictionsLoaded = false;
2984
		$this->mTitleProtection = null;
2985
	}
2986
2987
	/**
2988
	 * Purge expired restrictions from the page_restrictions table
2989
	 *
2990
	 * This will purge no more than $wgUpdateRowsPerQuery page_restrictions rows
2991
	 */
2992
	static function purgeExpiredRestrictions() {
2993
		if ( wfReadOnly() ) {
2994
			return;
2995
		}
2996
2997
		DeferredUpdates::addUpdate( new AtomicSectionUpdate(
2998
			wfGetDB( DB_MASTER ),
2999
			__METHOD__,
3000
			function ( IDatabase $dbw, $fname ) {
3001
				$config = MediaWikiServices::getInstance()->getMainConfig();
3002
				$ids = $dbw->selectFieldValues(
3003
					'page_restrictions',
3004
					'pr_id',
3005
					[ 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3006
					$fname,
3007
					[ 'LIMIT' => $config->get( 'UpdateRowsPerQuery' ) ] // T135470
3008
				);
3009
				if ( $ids ) {
3010
					$dbw->delete( 'page_restrictions', [ 'pr_id' => $ids ], $fname );
3011
				}
3012
			}
3013
		) );
3014
3015
		DeferredUpdates::addUpdate( new AtomicSectionUpdate(
3016
			wfGetDB( DB_MASTER ),
3017
			__METHOD__,
3018
			function ( IDatabase $dbw, $fname ) {
3019
				$dbw->delete(
3020
					'protected_titles',
3021
					[ 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3022
					$fname
3023
				);
3024
			}
3025
		) );
3026
	}
3027
3028
	/**
3029
	 * Does this have subpages?  (Warning, usually requires an extra DB query.)
3030
	 *
3031
	 * @return bool
3032
	 */
3033
	public function hasSubpages() {
3034
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
3035
			# Duh
3036
			return false;
3037
		}
3038
3039
		# We dynamically add a member variable for the purpose of this method
3040
		# alone to cache the result.  There's no point in having it hanging
3041
		# around uninitialized in every Title object; therefore we only add it
3042
		# if needed and don't declare it statically.
3043
		if ( $this->mHasSubpages === null ) {
3044
			$this->mHasSubpages = false;
3045
			$subpages = $this->getSubpages( 1 );
3046
			if ( $subpages instanceof TitleArray ) {
3047
				$this->mHasSubpages = (bool)$subpages->count();
3048
			}
3049
		}
3050
3051
		return $this->mHasSubpages;
3052
	}
3053
3054
	/**
3055
	 * Get all subpages of this page.
3056
	 *
3057
	 * @param int $limit Maximum number of subpages to fetch; -1 for no limit
3058
	 * @return TitleArray|array TitleArray, or empty array if this page's namespace
3059
	 *  doesn't allow subpages
3060
	 */
3061
	public function getSubpages( $limit = -1 ) {
3062
		if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3063
			return [];
3064
		}
3065
3066
		$dbr = wfGetDB( DB_SLAVE );
3067
		$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...
3068
		$conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
3069
		$options = [];
3070
		if ( $limit > -1 ) {
3071
			$options['LIMIT'] = $limit;
3072
		}
3073
		$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...
3074
			$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...
3075
				[ 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ],
3076
				$conds,
3077
				__METHOD__,
3078
				$options
3079
			)
3080
		);
3081
		return $this->mSubpages;
3082
	}
3083
3084
	/**
3085
	 * Is there a version of this page in the deletion archive?
3086
	 *
3087
	 * @return int The number of archived revisions
3088
	 */
3089
	public function isDeleted() {
3090
		if ( $this->getNamespace() < 0 ) {
3091
			$n = 0;
3092
		} else {
3093
			$dbr = wfGetDB( DB_SLAVE );
3094
3095
			$n = $dbr->selectField( 'archive', 'COUNT(*)',
3096
				[ 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ],
3097
				__METHOD__
3098
			);
3099 View Code Duplication
			if ( $this->getNamespace() == NS_FILE ) {
3100
				$n += $dbr->selectField( 'filearchive', 'COUNT(*)',
3101
					[ 'fa_name' => $this->getDBkey() ],
3102
					__METHOD__
3103
				);
3104
			}
3105
		}
3106
		return (int)$n;
3107
	}
3108
3109
	/**
3110
	 * Is there a version of this page in the deletion archive?
3111
	 *
3112
	 * @return bool
3113
	 */
3114
	public function isDeletedQuick() {
3115
		if ( $this->getNamespace() < 0 ) {
3116
			return false;
3117
		}
3118
		$dbr = wfGetDB( DB_SLAVE );
3119
		$deleted = (bool)$dbr->selectField( 'archive', '1',
3120
			[ 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ],
3121
			__METHOD__
3122
		);
3123 View Code Duplication
		if ( !$deleted && $this->getNamespace() == NS_FILE ) {
3124
			$deleted = (bool)$dbr->selectField( 'filearchive', '1',
3125
				[ 'fa_name' => $this->getDBkey() ],
3126
				__METHOD__
3127
			);
3128
		}
3129
		return $deleted;
3130
	}
3131
3132
	/**
3133
	 * Get the article ID for this Title from the link cache,
3134
	 * adding it if necessary
3135
	 *
3136
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select
3137
	 *  for update
3138
	 * @return int The ID
3139
	 */
3140
	public function getArticleID( $flags = 0 ) {
3141
		if ( $this->getNamespace() < 0 ) {
3142
			$this->mArticleID = 0;
3143
			return $this->mArticleID;
3144
		}
3145
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices 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...
3146
		if ( $flags & self::GAID_FOR_UPDATE ) {
3147
			$oldUpdate = $linkCache->forUpdate( true );
3148
			$linkCache->clearLink( $this );
3149
			$this->mArticleID = $linkCache->addLinkObj( $this );
3150
			$linkCache->forUpdate( $oldUpdate );
3151
		} else {
3152
			if ( -1 == $this->mArticleID ) {
3153
				$this->mArticleID = $linkCache->addLinkObj( $this );
3154
			}
3155
		}
3156
		return $this->mArticleID;
3157
	}
3158
3159
	/**
3160
	 * Is this an article that is a redirect page?
3161
	 * Uses link cache, adding it if necessary
3162
	 *
3163
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3164
	 * @return bool
3165
	 */
3166
	public function isRedirect( $flags = 0 ) {
3167
		if ( !is_null( $this->mRedirect ) ) {
3168
			return $this->mRedirect;
3169
		}
3170
		if ( !$this->getArticleID( $flags ) ) {
3171
			$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...
3172
			return $this->mRedirect;
3173
		}
3174
3175
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices 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...
3176
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3177
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
3178
		if ( $cached === null ) {
3179
			# Trust LinkCache's state over our own
3180
			# LinkCache is telling us that the page doesn't exist, despite there being cached
3181
			# data relating to an existing page in $this->mArticleID. Updaters should clear
3182
			# LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is
3183
			# set, then LinkCache will definitely be up to date here, since getArticleID() forces
3184
			# LinkCache to refresh its data from the master.
3185
			$this->mRedirect = false;
3186
			return $this->mRedirect;
3187
		}
3188
3189
		$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...
3190
3191
		return $this->mRedirect;
3192
	}
3193
3194
	/**
3195
	 * What is the length of this page?
3196
	 * Uses link cache, adding it if necessary
3197
	 *
3198
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3199
	 * @return int
3200
	 */
3201
	public function getLength( $flags = 0 ) {
3202
		if ( $this->mLength != -1 ) {
3203
			return $this->mLength;
3204
		}
3205
		if ( !$this->getArticleID( $flags ) ) {
3206
			$this->mLength = 0;
3207
			return $this->mLength;
3208
		}
3209
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices 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...
3210
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3211
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
3212
		if ( $cached === null ) {
3213
			# Trust LinkCache's state over our own, as for isRedirect()
3214
			$this->mLength = 0;
3215
			return $this->mLength;
3216
		}
3217
3218
		$this->mLength = intval( $cached );
3219
3220
		return $this->mLength;
3221
	}
3222
3223
	/**
3224
	 * What is the page_latest field for this page?
3225
	 *
3226
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3227
	 * @return int Int or 0 if the page doesn't exist
3228
	 */
3229
	public function getLatestRevID( $flags = 0 ) {
3230
		if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
3231
			return intval( $this->mLatestID );
3232
		}
3233
		if ( !$this->getArticleID( $flags ) ) {
3234
			$this->mLatestID = 0;
3235
			return $this->mLatestID;
3236
		}
3237
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices 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...
3238
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3239
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
3240
		if ( $cached === null ) {
3241
			# Trust LinkCache's state over our own, as for isRedirect()
3242
			$this->mLatestID = 0;
3243
			return $this->mLatestID;
3244
		}
3245
3246
		$this->mLatestID = intval( $cached );
3247
3248
		return $this->mLatestID;
3249
	}
3250
3251
	/**
3252
	 * This clears some fields in this object, and clears any associated
3253
	 * keys in the "bad links" section of the link cache.
3254
	 *
3255
	 * - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow
3256
	 * loading of the new page_id. It's also called from
3257
	 * WikiPage::doDeleteArticleReal()
3258
	 *
3259
	 * @param int $newid The new Article ID
3260
	 */
3261
	public function resetArticleID( $newid ) {
3262
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices 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...
3263
		$linkCache->clearLink( $this );
3264
3265
		if ( $newid === false ) {
3266
			$this->mArticleID = -1;
3267
		} else {
3268
			$this->mArticleID = intval( $newid );
3269
		}
3270
		$this->mRestrictionsLoaded = false;
3271
		$this->mRestrictions = [];
3272
		$this->mOldRestrictions = false;
3273
		$this->mRedirect = null;
3274
		$this->mLength = -1;
3275
		$this->mLatestID = false;
3276
		$this->mContentModel = false;
3277
		$this->mEstimateRevisions = null;
3278
		$this->mPageLanguage = false;
3279
		$this->mDbPageLanguage = false;
3280
		$this->mIsBigDeletion = null;
3281
	}
3282
3283
	public static function clearCaches() {
3284
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices 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...
3285
		$linkCache->clear();
3286
3287
		$titleCache = self::getTitleCache();
3288
		$titleCache->clear();
3289
	}
3290
3291
	/**
3292
	 * Capitalize a text string for a title if it belongs to a namespace that capitalizes
3293
	 *
3294
	 * @param string $text Containing title to capitalize
3295
	 * @param int $ns Namespace index, defaults to NS_MAIN
3296
	 * @return string Containing capitalized title
3297
	 */
3298
	public static function capitalize( $text, $ns = NS_MAIN ) {
3299
		global $wgContLang;
3300
3301
		if ( MWNamespace::isCapitalized( $ns ) ) {
3302
			return $wgContLang->ucfirst( $text );
3303
		} else {
3304
			return $text;
3305
		}
3306
	}
3307
3308
	/**
3309
	 * Secure and split - main initialisation function for this object
3310
	 *
3311
	 * Assumes that mDbkeyform has been set, and is urldecoded
3312
	 * and uses underscores, but not otherwise munged.  This function
3313
	 * removes illegal characters, splits off the interwiki and
3314
	 * namespace prefixes, sets the other forms, and canonicalizes
3315
	 * everything.
3316
	 *
3317
	 * @throws MalformedTitleException On invalid titles
3318
	 * @return bool True on success
3319
	 */
3320
	private function secureAndSplit() {
3321
		# Initialisation
3322
		$this->mInterwiki = '';
3323
		$this->mFragment = '';
3324
		$this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
3325
3326
		$dbkey = $this->mDbkeyform;
3327
3328
		// @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
3329
		//        the parsing code with Title, while avoiding massive refactoring.
3330
		// @todo: get rid of secureAndSplit, refactor parsing code.
3331
		// @note: getTitleParser() returns a TitleParser implementation which does not have a
3332
		//        splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
3333
		$titleCodec = MediaWikiServices::getInstance()->getTitleParser();
3334
		// MalformedTitleException can be thrown here
3335
		$parts = $titleCodec->splitTitleString( $dbkey, $this->getDefaultNamespace() );
3336
3337
		# Fill fields
3338
		$this->setFragment( '#' . $parts['fragment'] );
3339
		$this->mInterwiki = $parts['interwiki'];
3340
		$this->mLocalInterwiki = $parts['local_interwiki'];
3341
		$this->mNamespace = $parts['namespace'];
3342
		$this->mUserCaseDBKey = $parts['user_case_dbkey'];
3343
3344
		$this->mDbkeyform = $parts['dbkey'];
3345
		$this->mUrlform = wfUrlencode( $this->mDbkeyform );
3346
		$this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
3347
3348
		# We already know that some pages won't be in the database!
3349
		if ( $this->isExternal() || $this->mNamespace == NS_SPECIAL ) {
3350
			$this->mArticleID = 0;
3351
		}
3352
3353
		return true;
3354
	}
3355
3356
	/**
3357
	 * Get an array of Title objects linking to this Title
3358
	 * Also stores the IDs in the link cache.
3359
	 *
3360
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3361
	 * On heavily-used templates it will max out the memory.
3362
	 *
3363
	 * @param array $options May be FOR UPDATE
3364
	 * @param string $table Table name
3365
	 * @param string $prefix Fields prefix
3366
	 * @return Title[] Array of Title objects linking here
3367
	 */
3368
	public function getLinksTo( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3369
		if ( count( $options ) > 0 ) {
3370
			$db = wfGetDB( DB_MASTER );
3371
		} else {
3372
			$db = wfGetDB( DB_SLAVE );
3373
		}
3374
3375
		$res = $db->select(
3376
			[ 'page', $table ],
3377
			self::getSelectFields(),
3378
			[
3379
				"{$prefix}_from=page_id",
3380
				"{$prefix}_namespace" => $this->getNamespace(),
3381
				"{$prefix}_title" => $this->getDBkey() ],
3382
			__METHOD__,
3383
			$options
3384
		);
3385
3386
		$retVal = [];
3387
		if ( $res->numRows() ) {
3388
			$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices 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...
3389 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...
3390
				$titleObj = Title::makeTitle( $row->page_namespace, $row->page_title );
3391
				if ( $titleObj ) {
3392
					$linkCache->addGoodLinkObjFromRow( $titleObj, $row );
3393
					$retVal[] = $titleObj;
3394
				}
3395
			}
3396
		}
3397
		return $retVal;
3398
	}
3399
3400
	/**
3401
	 * Get an array of Title objects using this Title as a template
3402
	 * Also stores the IDs in the link cache.
3403
	 *
3404
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3405
	 * On heavily-used templates it will max out the memory.
3406
	 *
3407
	 * @param array $options Query option to Database::select()
3408
	 * @return Title[] Array of Title the Title objects linking here
3409
	 */
3410
	public function getTemplateLinksTo( $options = [] ) {
3411
		return $this->getLinksTo( $options, 'templatelinks', 'tl' );
3412
	}
3413
3414
	/**
3415
	 * Get an array of Title objects linked from this Title
3416
	 * Also stores the IDs in the link cache.
3417
	 *
3418
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3419
	 * On heavily-used templates it will max out the memory.
3420
	 *
3421
	 * @param array $options Query option to Database::select()
3422
	 * @param string $table Table name
3423
	 * @param string $prefix Fields prefix
3424
	 * @return array Array of Title objects linking here
3425
	 */
3426
	public function getLinksFrom( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3427
		$id = $this->getArticleID();
3428
3429
		# If the page doesn't exist; there can't be any link from this page
3430
		if ( !$id ) {
3431
			return [];
3432
		}
3433
3434
		$db = wfGetDB( DB_SLAVE );
3435
3436
		$blNamespace = "{$prefix}_namespace";
3437
		$blTitle = "{$prefix}_title";
3438
3439
		$res = $db->select(
3440
			[ $table, 'page' ],
3441
			array_merge(
3442
				[ $blNamespace, $blTitle ],
3443
				WikiPage::selectFields()
3444
			),
3445
			[ "{$prefix}_from" => $id ],
3446
			__METHOD__,
3447
			$options,
3448
			[ 'page' => [
3449
				'LEFT JOIN',
3450
				[ "page_namespace=$blNamespace", "page_title=$blTitle" ]
3451
			] ]
3452
		);
3453
3454
		$retVal = [];
3455
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices 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...
3456
		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...
3457
			if ( $row->page_id ) {
3458
				$titleObj = Title::newFromRow( $row );
3459
			} else {
3460
				$titleObj = Title::makeTitle( $row->$blNamespace, $row->$blTitle );
3461
				$linkCache->addBadLinkObj( $titleObj );
3462
			}
3463
			$retVal[] = $titleObj;
3464
		}
3465
3466
		return $retVal;
3467
	}
3468
3469
	/**
3470
	 * Get an array of Title objects used on this Title as a template
3471
	 * Also stores the IDs in the link cache.
3472
	 *
3473
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3474
	 * On heavily-used templates it will max out the memory.
3475
	 *
3476
	 * @param array $options May be FOR UPDATE
3477
	 * @return Title[] Array of Title the Title objects used here
3478
	 */
3479
	public function getTemplateLinksFrom( $options = [] ) {
3480
		return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
3481
	}
3482
3483
	/**
3484
	 * Get an array of Title objects referring to non-existent articles linked
3485
	 * from this page.
3486
	 *
3487
	 * @todo check if needed (used only in SpecialBrokenRedirects.php, and
3488
	 *   should use redirect table in this case).
3489
	 * @return Title[] Array of Title the Title objects
3490
	 */
3491
	public function getBrokenLinksFrom() {
3492
		if ( $this->getArticleID() == 0 ) {
3493
			# All links from article ID 0 are false positives
3494
			return [];
3495
		}
3496
3497
		$dbr = wfGetDB( DB_SLAVE );
3498
		$res = $dbr->select(
3499
			[ 'page', 'pagelinks' ],
3500
			[ 'pl_namespace', 'pl_title' ],
3501
			[
3502
				'pl_from' => $this->getArticleID(),
3503
				'page_namespace IS NULL'
3504
			],
3505
			__METHOD__, [],
3506
			[
3507
				'page' => [
3508
					'LEFT JOIN',
3509
					[ 'pl_namespace=page_namespace', 'pl_title=page_title' ]
3510
				]
3511
			]
3512
		);
3513
3514
		$retVal = [];
3515
		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...
3516
			$retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
3517
		}
3518
		return $retVal;
3519
	}
3520
3521
	/**
3522
	 * Get a list of URLs to purge from the CDN cache when this
3523
	 * page changes
3524
	 *
3525
	 * @return string[] Array of String the URLs
3526
	 */
3527
	public function getCdnUrls() {
3528
		$urls = [
3529
			$this->getInternalURL(),
3530
			$this->getInternalURL( 'action=history' )
3531
		];
3532
3533
		$pageLang = $this->getPageLanguage();
3534
		if ( $pageLang->hasVariants() ) {
3535
			$variants = $pageLang->getVariants();
3536
			foreach ( $variants as $vCode ) {
3537
				$urls[] = $this->getInternalURL( $vCode );
3538
			}
3539
		}
3540
3541
		// If we are looking at a css/js user subpage, purge the action=raw.
3542
		if ( $this->isJsSubpage() ) {
3543
			$urls[] = $this->getInternalURL( 'action=raw&ctype=text/javascript' );
3544
		} elseif ( $this->isCssSubpage() ) {
3545
			$urls[] = $this->getInternalURL( 'action=raw&ctype=text/css' );
3546
		}
3547
3548
		Hooks::run( 'TitleSquidURLs', [ $this, &$urls ] );
3549
		return $urls;
3550
	}
3551
3552
	/**
3553
	 * @deprecated since 1.27 use getCdnUrls()
3554
	 */
3555
	public function getSquidURLs() {
3556
		return $this->getCdnUrls();
3557
	}
3558
3559
	/**
3560
	 * Purge all applicable CDN URLs
3561
	 */
3562
	public function purgeSquid() {
3563
		DeferredUpdates::addUpdate(
3564
			new CdnCacheUpdate( $this->getCdnUrls() ),
3565
			DeferredUpdates::PRESEND
3566
		);
3567
	}
3568
3569
	/**
3570
	 * Move this page without authentication
3571
	 *
3572
	 * @deprecated since 1.25 use MovePage class instead
3573
	 * @param Title $nt The new page Title
3574
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3575
	 */
3576
	public function moveNoAuth( &$nt ) {
3577
		wfDeprecated( __METHOD__, '1.25' );
3578
		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...
3579
	}
3580
3581
	/**
3582
	 * Check whether a given move operation would be valid.
3583
	 * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
3584
	 *
3585
	 * @deprecated since 1.25, use MovePage's methods instead
3586
	 * @param Title $nt The new title
3587
	 * @param bool $auth Whether to check user permissions (uses $wgUser)
3588
	 * @param string $reason Is the log summary of the move, used for spam checking
3589
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3590
	 */
3591
	public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
3592
		global $wgUser;
3593
3594
		if ( !( $nt instanceof Title ) ) {
3595
			// Normally we'd add this to $errors, but we'll get
3596
			// lots of syntax errors if $nt is not an object
3597
			return [ [ 'badtitletext' ] ];
3598
		}
3599
3600
		$mp = new MovePage( $this, $nt );
3601
		$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...
3602
		if ( $auth ) {
3603
			$errors = wfMergeErrorArrays(
3604
				$errors,
3605
				$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...
3606
			);
3607
		}
3608
3609
		return $errors ?: true;
3610
	}
3611
3612
	/**
3613
	 * Check if the requested move target is a valid file move target
3614
	 * @todo move this to MovePage
3615
	 * @param Title $nt Target title
3616
	 * @return array List of errors
3617
	 */
3618
	protected function validateFileMoveOperation( $nt ) {
3619
		global $wgUser;
3620
3621
		$errors = [];
3622
3623
		$destFile = wfLocalFile( $nt );
3624
		$destFile->load( File::READ_LATEST );
3625
		if ( !$wgUser->isAllowed( 'reupload-shared' )
3626
			&& !$destFile->exists() && wfFindFile( $nt )
3627
		) {
3628
			$errors[] = [ 'file-exists-sharedrepo' ];
3629
		}
3630
3631
		return $errors;
3632
	}
3633
3634
	/**
3635
	 * Move a title to a new location
3636
	 *
3637
	 * @deprecated since 1.25, use the MovePage class instead
3638
	 * @param Title $nt The new title
3639
	 * @param bool $auth Indicates whether $wgUser's permissions
3640
	 *  should be checked
3641
	 * @param string $reason The reason for the move
3642
	 * @param bool $createRedirect Whether to create a redirect from the old title to the new title.
3643
	 *  Ignored if the user doesn't have the suppressredirect right.
3644
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3645
	 */
3646
	public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
3647
		global $wgUser;
3648
		$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...
3649
		if ( is_array( $err ) ) {
3650
			// Auto-block user's IP if the account was "hard" blocked
3651
			$wgUser->spreadAnyEditBlock();
3652
			return $err;
3653
		}
3654
		// Check suppressredirect permission
3655
		if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
3656
			$createRedirect = true;
3657
		}
3658
3659
		$mp = new MovePage( $this, $nt );
3660
		$status = $mp->move( $wgUser, $reason, $createRedirect );
3661
		if ( $status->isOK() ) {
3662
			return true;
3663
		} else {
3664
			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...
3665
		}
3666
	}
3667
3668
	/**
3669
	 * Move this page's subpages to be subpages of $nt
3670
	 *
3671
	 * @param Title $nt Move target
3672
	 * @param bool $auth Whether $wgUser's permissions should be checked
3673
	 * @param string $reason The reason for the move
3674
	 * @param bool $createRedirect Whether to create redirects from the old subpages to
3675
	 *     the new ones Ignored if the user doesn't have the 'suppressredirect' right
3676
	 * @return array Array with old page titles as keys, and strings (new page titles) or
3677
	 *     arrays (errors) as values, or an error array with numeric indices if no pages
3678
	 *     were moved
3679
	 */
3680
	public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) {
3681
		global $wgMaximumMovedPages;
3682
		// Check permissions
3683
		if ( !$this->userCan( 'move-subpages' ) ) {
3684
			return [ 'cant-move-subpages' ];
3685
		}
3686
		// Do the source and target namespaces support subpages?
3687
		if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3688
			return [ 'namespace-nosubpages',
3689
				MWNamespace::getCanonicalName( $this->getNamespace() ) ];
3690
		}
3691
		if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
3692
			return [ 'namespace-nosubpages',
3693
				MWNamespace::getCanonicalName( $nt->getNamespace() ) ];
3694
		}
3695
3696
		$subpages = $this->getSubpages( $wgMaximumMovedPages + 1 );
3697
		$retval = [];
3698
		$count = 0;
3699
		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...
3700
			$count++;
3701
			if ( $count > $wgMaximumMovedPages ) {
3702
				$retval[$oldSubpage->getPrefixedText()] =
3703
						[ 'movepage-max-pages',
3704
							$wgMaximumMovedPages ];
3705
				break;
3706
			}
3707
3708
			// We don't know whether this function was called before
3709
			// or after moving the root page, so check both
3710
			// $this and $nt
3711
			if ( $oldSubpage->getArticleID() == $this->getArticleID()
3712
				|| $oldSubpage->getArticleID() == $nt->getArticleID()
3713
			) {
3714
				// When moving a page to a subpage of itself,
3715
				// don't move it twice
3716
				continue;
3717
			}
3718
			$newPageName = preg_replace(
3719
					'#^' . preg_quote( $this->getDBkey(), '#' ) . '#',
3720
					StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
3721
					$oldSubpage->getDBkey() );
3722
			if ( $oldSubpage->isTalkPage() ) {
3723
				$newNs = $nt->getTalkPage()->getNamespace();
3724
			} else {
3725
				$newNs = $nt->getSubjectPage()->getNamespace();
3726
			}
3727
			# Bug 14385: we need makeTitleSafe because the new page names may
3728
			# be longer than 255 characters.
3729
			$newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
3730
3731
			$success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect );
3732
			if ( $success === true ) {
3733
				$retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
3734
			} else {
3735
				$retval[$oldSubpage->getPrefixedText()] = $success;
3736
			}
3737
		}
3738
		return $retval;
3739
	}
3740
3741
	/**
3742
	 * Checks if this page is just a one-rev redirect.
3743
	 * Adds lock, so don't use just for light purposes.
3744
	 *
3745
	 * @return bool
3746
	 */
3747
	public function isSingleRevRedirect() {
3748
		global $wgContentHandlerUseDB;
3749
3750
		$dbw = wfGetDB( DB_MASTER );
3751
3752
		# Is it a redirect?
3753
		$fields = [ 'page_is_redirect', 'page_latest', 'page_id' ];
3754
		if ( $wgContentHandlerUseDB ) {
3755
			$fields[] = 'page_content_model';
3756
		}
3757
3758
		$row = $dbw->selectRow( 'page',
3759
			$fields,
3760
			$this->pageCond(),
3761
			__METHOD__,
3762
			[ 'FOR UPDATE' ]
3763
		);
3764
		# Cache some fields we may want
3765
		$this->mArticleID = $row ? intval( $row->page_id ) : 0;
3766
		$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...
3767
		$this->mLatestID = $row ? intval( $row->page_latest ) : false;
3768
		$this->mContentModel = $row && isset( $row->page_content_model )
3769
			? strval( $row->page_content_model )
3770
			: false;
3771
3772
		if ( !$this->mRedirect ) {
3773
			return false;
3774
		}
3775
		# Does the article have a history?
3776
		$row = $dbw->selectField( [ 'page', 'revision' ],
3777
			'rev_id',
3778
			[ 'page_namespace' => $this->getNamespace(),
3779
				'page_title' => $this->getDBkey(),
3780
				'page_id=rev_page',
3781
				'page_latest != rev_id'
3782
			],
3783
			__METHOD__,
3784
			[ 'FOR UPDATE' ]
3785
		);
3786
		# Return true if there was no history
3787
		return ( $row === false );
3788
	}
3789
3790
	/**
3791
	 * Checks if $this can be moved to a given Title
3792
	 * - Selects for update, so don't call it unless you mean business
3793
	 *
3794
	 * @deprecated since 1.25, use MovePage's methods instead
3795
	 * @param Title $nt The new title to check
3796
	 * @return bool
3797
	 */
3798
	public function isValidMoveTarget( $nt ) {
3799
		# Is it an existing file?
3800
		if ( $nt->getNamespace() == NS_FILE ) {
3801
			$file = wfLocalFile( $nt );
3802
			$file->load( File::READ_LATEST );
3803
			if ( $file->exists() ) {
3804
				wfDebug( __METHOD__ . ": file exists\n" );
3805
				return false;
3806
			}
3807
		}
3808
		# Is it a redirect with no history?
3809
		if ( !$nt->isSingleRevRedirect() ) {
3810
			wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
3811
			return false;
3812
		}
3813
		# Get the article text
3814
		$rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST );
3815
		if ( !is_object( $rev ) ) {
3816
			return false;
3817
		}
3818
		$content = $rev->getContent();
3819
		# Does the redirect point to the source?
3820
		# Or is it a broken self-redirect, usually caused by namespace collisions?
3821
		$redirTitle = $content ? $content->getRedirectTarget() : null;
3822
3823
		if ( $redirTitle ) {
3824
			if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
3825
				$redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
3826
				wfDebug( __METHOD__ . ": redirect points to other page\n" );
3827
				return false;
3828
			} else {
3829
				return true;
3830
			}
3831
		} else {
3832
			# Fail safe (not a redirect after all. strange.)
3833
			wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
3834
						" is a redirect, but it doesn't contain a valid redirect.\n" );
3835
			return false;
3836
		}
3837
	}
3838
3839
	/**
3840
	 * Get categories to which this Title belongs and return an array of
3841
	 * categories' names.
3842
	 *
3843
	 * @return array Array of parents in the form:
3844
	 *	  $parent => $currentarticle
3845
	 */
3846
	public function getParentCategories() {
3847
		global $wgContLang;
3848
3849
		$data = [];
3850
3851
		$titleKey = $this->getArticleID();
3852
3853
		if ( $titleKey === 0 ) {
3854
			return $data;
3855
		}
3856
3857
		$dbr = wfGetDB( DB_SLAVE );
3858
3859
		$res = $dbr->select(
3860
			'categorylinks',
3861
			'cl_to',
3862
			[ 'cl_from' => $titleKey ],
3863
			__METHOD__
3864
		);
3865
3866
		if ( $res->numRows() > 0 ) {
3867
			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...
3868
				// $data[] = Title::newFromText($wgContLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to);
3869
				$data[$wgContLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText();
3870
			}
3871
		}
3872
		return $data;
3873
	}
3874
3875
	/**
3876
	 * Get a tree of parent categories
3877
	 *
3878
	 * @param array $children Array with the children in the keys, to check for circular refs
3879
	 * @return array Tree of parent categories
3880
	 */
3881
	public function getParentCategoryTree( $children = [] ) {
3882
		$stack = [];
3883
		$parents = $this->getParentCategories();
3884
3885
		if ( $parents ) {
3886
			foreach ( $parents as $parent => $current ) {
3887
				if ( array_key_exists( $parent, $children ) ) {
3888
					# Circular reference
3889
					$stack[$parent] = [];
3890
				} else {
3891
					$nt = Title::newFromText( $parent );
3892
					if ( $nt ) {
3893
						$stack[$parent] = $nt->getParentCategoryTree( $children + [ $parent => 1 ] );
3894
					}
3895
				}
3896
			}
3897
		}
3898
3899
		return $stack;
3900
	}
3901
3902
	/**
3903
	 * Get an associative array for selecting this title from
3904
	 * the "page" table
3905
	 *
3906
	 * @return array Array suitable for the $where parameter of DB::select()
3907
	 */
3908
	public function pageCond() {
3909
		if ( $this->mArticleID > 0 ) {
3910
			// PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
3911
			return [ 'page_id' => $this->mArticleID ];
3912
		} else {
3913
			return [ 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ];
3914
		}
3915
	}
3916
3917
	/**
3918
	 * Get the revision ID of the previous revision
3919
	 *
3920
	 * @param int $revId Revision ID. Get the revision that was before this one.
3921
	 * @param int $flags Title::GAID_FOR_UPDATE
3922
	 * @return int|bool Old revision ID, or false if none exists
3923
	 */
3924 View Code Duplication
	public function getPreviousRevisionID( $revId, $flags = 0 ) {
3925
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3926
		$revId = $db->selectField( 'revision', 'rev_id',
3927
			[
3928
				'rev_page' => $this->getArticleID( $flags ),
3929
				'rev_id < ' . intval( $revId )
3930
			],
3931
			__METHOD__,
3932
			[ 'ORDER BY' => 'rev_id DESC' ]
3933
		);
3934
3935
		if ( $revId === false ) {
3936
			return false;
3937
		} else {
3938
			return intval( $revId );
3939
		}
3940
	}
3941
3942
	/**
3943
	 * Get the revision ID of the next revision
3944
	 *
3945
	 * @param int $revId Revision ID. Get the revision that was after this one.
3946
	 * @param int $flags Title::GAID_FOR_UPDATE
3947
	 * @return int|bool Next revision ID, or false if none exists
3948
	 */
3949 View Code Duplication
	public function getNextRevisionID( $revId, $flags = 0 ) {
3950
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3951
		$revId = $db->selectField( 'revision', 'rev_id',
3952
			[
3953
				'rev_page' => $this->getArticleID( $flags ),
3954
				'rev_id > ' . intval( $revId )
3955
			],
3956
			__METHOD__,
3957
			[ 'ORDER BY' => 'rev_id' ]
3958
		);
3959
3960
		if ( $revId === false ) {
3961
			return false;
3962
		} else {
3963
			return intval( $revId );
3964
		}
3965
	}
3966
3967
	/**
3968
	 * Get the first revision of the page
3969
	 *
3970
	 * @param int $flags Title::GAID_FOR_UPDATE
3971
	 * @return Revision|null If page doesn't exist
3972
	 */
3973
	public function getFirstRevision( $flags = 0 ) {
3974
		$pageId = $this->getArticleID( $flags );
3975
		if ( $pageId ) {
3976
			$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3977
			$row = $db->selectRow( 'revision', Revision::selectFields(),
3978
				[ 'rev_page' => $pageId ],
3979
				__METHOD__,
3980
				[ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ]
3981
			);
3982
			if ( $row ) {
3983
				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 3977 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...
3984
			}
3985
		}
3986
		return null;
3987
	}
3988
3989
	/**
3990
	 * Get the oldest revision timestamp of this page
3991
	 *
3992
	 * @param int $flags Title::GAID_FOR_UPDATE
3993
	 * @return string MW timestamp
3994
	 */
3995
	public function getEarliestRevTime( $flags = 0 ) {
3996
		$rev = $this->getFirstRevision( $flags );
3997
		return $rev ? $rev->getTimestamp() : null;
3998
	}
3999
4000
	/**
4001
	 * Check if this is a new page
4002
	 *
4003
	 * @return bool
4004
	 */
4005
	public function isNewPage() {
4006
		$dbr = wfGetDB( DB_SLAVE );
4007
		return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
4008
	}
4009
4010
	/**
4011
	 * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit
4012
	 *
4013
	 * @return bool
4014
	 */
4015
	public function isBigDeletion() {
4016
		global $wgDeleteRevisionsLimit;
4017
4018
		if ( !$wgDeleteRevisionsLimit ) {
4019
			return false;
4020
		}
4021
4022
		if ( $this->mIsBigDeletion === null ) {
4023
			$dbr = wfGetDB( DB_SLAVE );
4024
4025
			$revCount = $dbr->selectRowCount(
4026
				'revision',
4027
				'1',
4028
				[ 'rev_page' => $this->getArticleID() ],
4029
				__METHOD__,
4030
				[ 'LIMIT' => $wgDeleteRevisionsLimit + 1 ]
4031
			);
4032
4033
			$this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit;
4034
		}
4035
4036
		return $this->mIsBigDeletion;
4037
	}
4038
4039
	/**
4040
	 * Get the approximate revision count of this page.
4041
	 *
4042
	 * @return int
4043
	 */
4044
	public function estimateRevisionCount() {
4045
		if ( !$this->exists() ) {
4046
			return 0;
4047
		}
4048
4049
		if ( $this->mEstimateRevisions === null ) {
4050
			$dbr = wfGetDB( DB_SLAVE );
4051
			$this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
4052
				[ 'rev_page' => $this->getArticleID() ], __METHOD__ );
4053
		}
4054
4055
		return $this->mEstimateRevisions;
4056
	}
4057
4058
	/**
4059
	 * Get the number of revisions between the given revision.
4060
	 * Used for diffs and other things that really need it.
4061
	 *
4062
	 * @param int|Revision $old Old revision or rev ID (first before range)
4063
	 * @param int|Revision $new New revision or rev ID (first after range)
4064
	 * @param int|null $max Limit of Revisions to count, will be incremented to detect truncations
4065
	 * @return int Number of revisions between these revisions.
4066
	 */
4067
	public function countRevisionsBetween( $old, $new, $max = null ) {
4068
		if ( !( $old instanceof Revision ) ) {
4069
			$old = Revision::newFromTitle( $this, (int)$old );
4070
		}
4071
		if ( !( $new instanceof Revision ) ) {
4072
			$new = Revision::newFromTitle( $this, (int)$new );
4073
		}
4074
		if ( !$old || !$new ) {
4075
			return 0; // nothing to compare
4076
		}
4077
		$dbr = wfGetDB( DB_SLAVE );
4078
		$conds = [
4079
			'rev_page' => $this->getArticleID(),
4080
			'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...
4081
			'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...
4082
		];
4083
		if ( $max !== null ) {
4084
			return $dbr->selectRowCount( 'revision', '1',
4085
				$conds,
4086
				__METHOD__,
4087
				[ 'LIMIT' => $max + 1 ] // extra to detect truncation
4088
			);
4089
		} else {
4090
			return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
4091
		}
4092
	}
4093
4094
	/**
4095
	 * Get the authors between the given revisions or revision IDs.
4096
	 * Used for diffs and other things that really need it.
4097
	 *
4098
	 * @since 1.23
4099
	 *
4100
	 * @param int|Revision $old Old revision or rev ID (first before range by default)
4101
	 * @param int|Revision $new New revision or rev ID (first after range by default)
4102
	 * @param int $limit Maximum number of authors
4103
	 * @param string|array $options (Optional): Single option, or an array of options:
4104
	 *     'include_old' Include $old in the range; $new is excluded.
4105
	 *     'include_new' Include $new in the range; $old is excluded.
4106
	 *     'include_both' Include both $old and $new in the range.
4107
	 *     Unknown option values are ignored.
4108
	 * @return array|null Names of revision authors in the range; null if not both revisions exist
4109
	 */
4110
	public function getAuthorsBetween( $old, $new, $limit, $options = [] ) {
4111
		if ( !( $old instanceof Revision ) ) {
4112
			$old = Revision::newFromTitle( $this, (int)$old );
4113
		}
4114
		if ( !( $new instanceof Revision ) ) {
4115
			$new = Revision::newFromTitle( $this, (int)$new );
4116
		}
4117
		// XXX: what if Revision objects are passed in, but they don't refer to this title?
4118
		// Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID()
4119
		// in the sanity check below?
4120
		if ( !$old || !$new ) {
4121
			return null; // nothing to compare
4122
		}
4123
		$authors = [];
4124
		$old_cmp = '>';
4125
		$new_cmp = '<';
4126
		$options = (array)$options;
4127
		if ( in_array( 'include_old', $options ) ) {
4128
			$old_cmp = '>=';
4129
		}
4130
		if ( in_array( 'include_new', $options ) ) {
4131
			$new_cmp = '<=';
4132
		}
4133
		if ( in_array( 'include_both', $options ) ) {
4134
			$old_cmp = '>=';
4135
			$new_cmp = '<=';
4136
		}
4137
		// No DB query needed if $old and $new are the same or successive revisions:
4138
		if ( $old->getId() === $new->getId() ) {
4139
			return ( $old_cmp === '>' && $new_cmp === '<' ) ?
4140
				[] :
4141
				[ $old->getUserText( Revision::RAW ) ];
4142
		} elseif ( $old->getId() === $new->getParentId() ) {
4143
			if ( $old_cmp === '>=' && $new_cmp === '<=' ) {
4144
				$authors[] = $old->getUserText( Revision::RAW );
4145
				if ( $old->getUserText( Revision::RAW ) != $new->getUserText( Revision::RAW ) ) {
4146
					$authors[] = $new->getUserText( Revision::RAW );
4147
				}
4148
			} elseif ( $old_cmp === '>=' ) {
4149
				$authors[] = $old->getUserText( Revision::RAW );
4150
			} elseif ( $new_cmp === '<=' ) {
4151
				$authors[] = $new->getUserText( Revision::RAW );
4152
			}
4153
			return $authors;
4154
		}
4155
		$dbr = wfGetDB( DB_SLAVE );
4156
		$res = $dbr->select( 'revision', 'DISTINCT rev_user_text',
4157
			[
4158
				'rev_page' => $this->getArticleID(),
4159
				"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...
4160
				"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...
4161
			], __METHOD__,
4162
			[ 'LIMIT' => $limit + 1 ] // add one so caller knows it was truncated
4163
		);
4164
		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...
4165
			$authors[] = $row->rev_user_text;
4166
		}
4167
		return $authors;
4168
	}
4169
4170
	/**
4171
	 * Get the number of authors between the given revisions or revision IDs.
4172
	 * Used for diffs and other things that really need it.
4173
	 *
4174
	 * @param int|Revision $old Old revision or rev ID (first before range by default)
4175
	 * @param int|Revision $new New revision or rev ID (first after range by default)
4176
	 * @param int $limit Maximum number of authors
4177
	 * @param string|array $options (Optional): Single option, or an array of options:
4178
	 *     'include_old' Include $old in the range; $new is excluded.
4179
	 *     'include_new' Include $new in the range; $old is excluded.
4180
	 *     'include_both' Include both $old and $new in the range.
4181
	 *     Unknown option values are ignored.
4182
	 * @return int Number of revision authors in the range; zero if not both revisions exist
4183
	 */
4184
	public function countAuthorsBetween( $old, $new, $limit, $options = [] ) {
4185
		$authors = $this->getAuthorsBetween( $old, $new, $limit, $options );
4186
		return $authors ? count( $authors ) : 0;
4187
	}
4188
4189
	/**
4190
	 * Compare with another title.
4191
	 *
4192
	 * @param Title $title
4193
	 * @return bool
4194
	 */
4195
	public function equals( Title $title ) {
4196
		// Note: === is necessary for proper matching of number-like titles.
4197
		return $this->getInterwiki() === $title->getInterwiki()
4198
			&& $this->getNamespace() == $title->getNamespace()
4199
			&& $this->getDBkey() === $title->getDBkey();
4200
	}
4201
4202
	/**
4203
	 * Check if this title is a subpage of another title
4204
	 *
4205
	 * @param Title $title
4206
	 * @return bool
4207
	 */
4208
	public function isSubpageOf( Title $title ) {
4209
		return $this->getInterwiki() === $title->getInterwiki()
4210
			&& $this->getNamespace() == $title->getNamespace()
4211
			&& strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0;
4212
	}
4213
4214
	/**
4215
	 * Check if page exists.  For historical reasons, this function simply
4216
	 * checks for the existence of the title in the page table, and will
4217
	 * thus return false for interwiki links, special pages and the like.
4218
	 * If you want to know if a title can be meaningfully viewed, you should
4219
	 * probably call the isKnown() method instead.
4220
	 *
4221
	 * @param int $flags An optional bit field; may be Title::GAID_FOR_UPDATE to check
4222
	 *   from master/for update
4223
	 * @return bool
4224
	 */
4225
	public function exists( $flags = 0 ) {
4226
		$exists = $this->getArticleID( $flags ) != 0;
4227
		Hooks::run( 'TitleExists', [ $this, &$exists ] );
4228
		return $exists;
4229
	}
4230
4231
	/**
4232
	 * Should links to this title be shown as potentially viewable (i.e. as
4233
	 * "bluelinks"), even if there's no record by this title in the page
4234
	 * table?
4235
	 *
4236
	 * This function is semi-deprecated for public use, as well as somewhat
4237
	 * misleadingly named.  You probably just want to call isKnown(), which
4238
	 * calls this function internally.
4239
	 *
4240
	 * (ISSUE: Most of these checks are cheap, but the file existence check
4241
	 * can potentially be quite expensive.  Including it here fixes a lot of
4242
	 * existing code, but we might want to add an optional parameter to skip
4243
	 * it and any other expensive checks.)
4244
	 *
4245
	 * @return bool
4246
	 */
4247
	public function isAlwaysKnown() {
4248
		$isKnown = null;
4249
4250
		/**
4251
		 * Allows overriding default behavior for determining if a page exists.
4252
		 * If $isKnown is kept as null, regular checks happen. If it's
4253
		 * a boolean, this value is returned by the isKnown method.
4254
		 *
4255
		 * @since 1.20
4256
		 *
4257
		 * @param Title $title
4258
		 * @param bool|null $isKnown
4259
		 */
4260
		Hooks::run( 'TitleIsAlwaysKnown', [ $this, &$isKnown ] );
4261
4262
		if ( !is_null( $isKnown ) ) {
4263
			return $isKnown;
4264
		}
4265
4266
		if ( $this->isExternal() ) {
4267
			return true;  // any interwiki link might be viewable, for all we know
4268
		}
4269
4270
		switch ( $this->mNamespace ) {
4271
			case NS_MEDIA:
4272
			case NS_FILE:
4273
				// file exists, possibly in a foreign repo
4274
				return (bool)wfFindFile( $this );
4275
			case NS_SPECIAL:
4276
				// valid special page
4277
				return SpecialPageFactory::exists( $this->getDBkey() );
4278
			case NS_MAIN:
4279
				// selflink, possibly with fragment
4280
				return $this->mDbkeyform == '';
4281
			case NS_MEDIAWIKI:
4282
				// known system message
4283
				return $this->hasSourceText() !== false;
4284
			default:
4285
				return false;
4286
		}
4287
	}
4288
4289
	/**
4290
	 * Does this title refer to a page that can (or might) be meaningfully
4291
	 * viewed?  In particular, this function may be used to determine if
4292
	 * links to the title should be rendered as "bluelinks" (as opposed to
4293
	 * "redlinks" to non-existent pages).
4294
	 * Adding something else to this function will cause inconsistency
4295
	 * since LinkHolderArray calls isAlwaysKnown() and does its own
4296
	 * page existence check.
4297
	 *
4298
	 * @return bool
4299
	 */
4300
	public function isKnown() {
4301
		return $this->isAlwaysKnown() || $this->exists();
4302
	}
4303
4304
	/**
4305
	 * Does this page have source text?
4306
	 *
4307
	 * @return bool
4308
	 */
4309
	public function hasSourceText() {
4310
		if ( $this->exists() ) {
4311
			return true;
4312
		}
4313
4314
		if ( $this->mNamespace == NS_MEDIAWIKI ) {
4315
			// If the page doesn't exist but is a known system message, default
4316
			// message content will be displayed, same for language subpages-
4317
			// Use always content language to avoid loading hundreds of languages
4318
			// to get the link color.
4319
			global $wgContLang;
4320
			list( $name, ) = MessageCache::singleton()->figureMessage(
4321
				$wgContLang->lcfirst( $this->getText() )
4322
			);
4323
			$message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false );
4324
			return $message->exists();
4325
		}
4326
4327
		return false;
4328
	}
4329
4330
	/**
4331
	 * Get the default message text or false if the message doesn't exist
4332
	 *
4333
	 * @return string|bool
4334
	 */
4335
	public function getDefaultMessageText() {
4336
		global $wgContLang;
4337
4338
		if ( $this->getNamespace() != NS_MEDIAWIKI ) { // Just in case
4339
			return false;
4340
		}
4341
4342
		list( $name, $lang ) = MessageCache::singleton()->figureMessage(
4343
			$wgContLang->lcfirst( $this->getText() )
4344
		);
4345
		$message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false );
4346
4347
		if ( $message->exists() ) {
4348
			return $message->plain();
4349
		} else {
4350
			return false;
4351
		}
4352
	}
4353
4354
	/**
4355
	 * Updates page_touched for this page; called from LinksUpdate.php
4356
	 *
4357
	 * @param string $purgeTime [optional] TS_MW timestamp
4358
	 * @return bool True if the update succeeded
4359
	 */
4360
	public function invalidateCache( $purgeTime = null ) {
4361
		if ( wfReadOnly() ) {
4362
			return false;
4363
		}
4364
4365
		if ( $this->mArticleID === 0 ) {
4366
			return true; // avoid gap locking if we know it's not there
4367
		}
4368
4369
		$method = __METHOD__;
4370
		$dbw = wfGetDB( DB_MASTER );
4371
		$conds = $this->pageCond();
4372
		$dbw->onTransactionIdle( function () use ( $dbw, $conds, $method, $purgeTime ) {
4373
			$dbTimestamp = $dbw->timestamp( $purgeTime ?: time() );
4374
4375
			$dbw->update(
4376
				'page',
4377
				[ 'page_touched' => $dbTimestamp ],
4378
				$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 4373 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...
4379
				$method
4380
			);
4381
		} );
4382
4383
		return true;
4384
	}
4385
4386
	/**
4387
	 * Update page_touched timestamps and send CDN purge messages for
4388
	 * pages linking to this title. May be sent to the job queue depending
4389
	 * on the number of links. Typically called on create and delete.
4390
	 */
4391
	public function touchLinks() {
4392
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks' ) );
4393
		if ( $this->getNamespace() == NS_CATEGORY ) {
4394
			DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'categorylinks' ) );
4395
		}
4396
	}
4397
4398
	/**
4399
	 * Get the last touched timestamp
4400
	 *
4401
	 * @param IDatabase $db Optional db
4402
	 * @return string Last-touched timestamp
4403
	 */
4404
	public function getTouched( $db = null ) {
4405
		if ( $db === null ) {
4406
			$db = wfGetDB( DB_SLAVE );
4407
		}
4408
		$touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
4409
		return $touched;
4410
	}
4411
4412
	/**
4413
	 * Get the timestamp when this page was updated since the user last saw it.
4414
	 *
4415
	 * @param User $user
4416
	 * @return string|null
4417
	 */
4418
	public function getNotificationTimestamp( $user = null ) {
4419
		global $wgUser;
4420
4421
		// Assume current user if none given
4422
		if ( !$user ) {
4423
			$user = $wgUser;
4424
		}
4425
		// Check cache first
4426
		$uid = $user->getId();
4427
		if ( !$uid ) {
4428
			return false;
4429
		}
4430
		// avoid isset here, as it'll return false for null entries
4431
		if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
4432
			return $this->mNotificationTimestamp[$uid];
4433
		}
4434
		// Don't cache too much!
4435
		if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
4436
			$this->mNotificationTimestamp = [];
4437
		}
4438
4439
		$store = MediaWikiServices::getInstance()->getWatchedItemStore();
4440
		$watchedItem = $store->getWatchedItem( $user, $this );
4441
		if ( $watchedItem ) {
4442
			$this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp();
4443
		} else {
4444
			$this->mNotificationTimestamp[$uid] = false;
4445
		}
4446
4447
		return $this->mNotificationTimestamp[$uid];
4448
	}
4449
4450
	/**
4451
	 * Generate strings used for xml 'id' names in monobook tabs
4452
	 *
4453
	 * @param string $prepend Defaults to 'nstab-'
4454
	 * @return string XML 'id' name
4455
	 */
4456
	public function getNamespaceKey( $prepend = 'nstab-' ) {
4457
		global $wgContLang;
4458
		// Gets the subject namespace if this title
4459
		$namespace = MWNamespace::getSubject( $this->getNamespace() );
4460
		// Checks if canonical namespace name exists for namespace
4461
		if ( MWNamespace::exists( $this->getNamespace() ) ) {
4462
			// Uses canonical namespace name
4463
			$namespaceKey = MWNamespace::getCanonicalName( $namespace );
4464
		} else {
4465
			// Uses text of namespace
4466
			$namespaceKey = $this->getSubjectNsText();
4467
		}
4468
		// Makes namespace key lowercase
4469
		$namespaceKey = $wgContLang->lc( $namespaceKey );
4470
		// Uses main
4471
		if ( $namespaceKey == '' ) {
4472
			$namespaceKey = 'main';
4473
		}
4474
		// Changes file to image for backwards compatibility
4475
		if ( $namespaceKey == 'file' ) {
4476
			$namespaceKey = 'image';
4477
		}
4478
		return $prepend . $namespaceKey;
4479
	}
4480
4481
	/**
4482
	 * Get all extant redirects to this Title
4483
	 *
4484
	 * @param int|null $ns Single namespace to consider; null to consider all namespaces
4485
	 * @return Title[] Array of Title redirects to this title
4486
	 */
4487
	public function getRedirectsHere( $ns = null ) {
4488
		$redirs = [];
4489
4490
		$dbr = wfGetDB( DB_SLAVE );
4491
		$where = [
4492
			'rd_namespace' => $this->getNamespace(),
4493
			'rd_title' => $this->getDBkey(),
4494
			'rd_from = page_id'
4495
		];
4496
		if ( $this->isExternal() ) {
4497
			$where['rd_interwiki'] = $this->getInterwiki();
4498
		} else {
4499
			$where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL';
4500
		}
4501
		if ( !is_null( $ns ) ) {
4502
			$where['page_namespace'] = $ns;
4503
		}
4504
4505
		$res = $dbr->select(
4506
			[ 'redirect', 'page' ],
4507
			[ 'page_namespace', 'page_title' ],
4508
			$where,
4509
			__METHOD__
4510
		);
4511
4512
		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...
4513
			$redirs[] = self::newFromRow( $row );
4514
		}
4515
		return $redirs;
4516
	}
4517
4518
	/**
4519
	 * Check if this Title is a valid redirect target
4520
	 *
4521
	 * @return bool
4522
	 */
4523
	public function isValidRedirectTarget() {
4524
		global $wgInvalidRedirectTargets;
4525
4526
		if ( $this->isSpecialPage() ) {
4527
			// invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
4528
			if ( $this->isSpecial( 'Userlogout' ) ) {
4529
				return false;
4530
			}
4531
4532
			foreach ( $wgInvalidRedirectTargets as $target ) {
4533
				if ( $this->isSpecial( $target ) ) {
4534
					return false;
4535
				}
4536
			}
4537
		}
4538
4539
		return true;
4540
	}
4541
4542
	/**
4543
	 * Get a backlink cache object
4544
	 *
4545
	 * @return BacklinkCache
4546
	 */
4547
	public function getBacklinkCache() {
4548
		return BacklinkCache::get( $this );
4549
	}
4550
4551
	/**
4552
	 * Whether the magic words __INDEX__ and __NOINDEX__ function for  this page.
4553
	 *
4554
	 * @return bool
4555
	 */
4556
	public function canUseNoindex() {
4557
		global $wgContentNamespaces, $wgExemptFromUserRobotsControl;
4558
4559
		$bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
4560
			? $wgContentNamespaces
4561
			: $wgExemptFromUserRobotsControl;
4562
4563
		return !in_array( $this->mNamespace, $bannedNamespaces );
4564
4565
	}
4566
4567
	/**
4568
	 * Returns the raw sort key to be used for categories, with the specified
4569
	 * prefix.  This will be fed to Collation::getSortKey() to get a
4570
	 * binary sortkey that can be used for actual sorting.
4571
	 *
4572
	 * @param string $prefix The prefix to be used, specified using
4573
	 *   {{defaultsort:}} or like [[Category:Foo|prefix]].  Empty for no
4574
	 *   prefix.
4575
	 * @return string
4576
	 */
4577
	public function getCategorySortkey( $prefix = '' ) {
4578
		$unprefixed = $this->getText();
4579
4580
		// Anything that uses this hook should only depend
4581
		// on the Title object passed in, and should probably
4582
		// tell the users to run updateCollations.php --force
4583
		// in order to re-sort existing category relations.
4584
		Hooks::run( 'GetDefaultSortkey', [ $this, &$unprefixed ] );
4585
		if ( $prefix !== '' ) {
4586
			# Separate with a line feed, so the unprefixed part is only used as
4587
			# a tiebreaker when two pages have the exact same prefix.
4588
			# In UCA, tab is the only character that can sort above LF
4589
			# so we strip both of them from the original prefix.
4590
			$prefix = strtr( $prefix, "\n\t", '  ' );
4591
			return "$prefix\n$unprefixed";
4592
		}
4593
		return $unprefixed;
4594
	}
4595
4596
	/**
4597
	 * Returns the page language code saved in the database, if $wgPageLanguageUseDB is set
4598
	 * to true in LocalSettings.php, otherwise returns false. If there is no language saved in
4599
	 * the db, it will return NULL.
4600
	 *
4601
	 * @return string|null|bool
4602
	 */
4603
	private function getDbPageLanguageCode() {
4604
		global $wgPageLanguageUseDB;
4605
4606
		// check, if the page language could be saved in the database, and if so and
4607
		// the value is not requested already, lookup the page language using LinkCache
4608 View Code Duplication
		if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) {
4609
			$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices 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...
4610
			$linkCache->addLinkObj( $this );
4611
			$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...
4612
		}
4613
4614
		return $this->mDbPageLanguage;
4615
	}
4616
4617
	/**
4618
	 * Get the language in which the content of this page is written in
4619
	 * wikitext. Defaults to $wgContLang, but in certain cases it can be
4620
	 * e.g. $wgLang (such as special pages, which are in the user language).
4621
	 *
4622
	 * @since 1.18
4623
	 * @return Language
4624
	 */
4625
	public function getPageLanguage() {
4626
		global $wgLang, $wgLanguageCode;
4627
		if ( $this->isSpecialPage() ) {
4628
			// special pages are in the user language
4629
			return $wgLang;
4630
		}
4631
4632
		// Checking if DB language is set
4633
		$dbPageLanguage = $this->getDbPageLanguageCode();
4634
		if ( $dbPageLanguage ) {
4635
			return wfGetLangObj( $dbPageLanguage );
4636
		}
4637
4638
		if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) {
4639
			// Note that this may depend on user settings, so the cache should
4640
			// be only per-request.
4641
			// NOTE: ContentHandler::getPageLanguage() may need to load the
4642
			// content to determine the page language!
4643
			// Checking $wgLanguageCode hasn't changed for the benefit of unit
4644
			// tests.
4645
			$contentHandler = ContentHandler::getForTitle( $this );
4646
			$langObj = $contentHandler->getPageLanguage( $this );
4647
			$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...
4648
		} else {
4649
			$langObj = wfGetLangObj( $this->mPageLanguage[0] );
4650
		}
4651
4652
		return $langObj;
4653
	}
4654
4655
	/**
4656
	 * Get the language in which the content of this page is written when
4657
	 * viewed by user. Defaults to $wgContLang, but in certain cases it can be
4658
	 * e.g. $wgLang (such as special pages, which are in the user language).
4659
	 *
4660
	 * @since 1.20
4661
	 * @return Language
4662
	 */
4663
	public function getPageViewLanguage() {
4664
		global $wgLang;
4665
4666
		if ( $this->isSpecialPage() ) {
4667
			// If the user chooses a variant, the content is actually
4668
			// in a language whose code is the variant code.
4669
			$variant = $wgLang->getPreferredVariant();
4670
			if ( $wgLang->getCode() !== $variant ) {
4671
				return Language::factory( $variant );
4672
			}
4673
4674
			return $wgLang;
4675
		}
4676
4677
		// Checking if DB language is set
4678
		$dbPageLanguage = $this->getDbPageLanguageCode();
4679
		if ( $dbPageLanguage ) {
4680
			$pageLang = wfGetLangObj( $dbPageLanguage );
4681
			$variant = $pageLang->getPreferredVariant();
4682
			if ( $pageLang->getCode() !== $variant ) {
4683
				$pageLang = Language::factory( $variant );
4684
			}
4685
4686
			return $pageLang;
4687
		}
4688
4689
		// @note Can't be cached persistently, depends on user settings.
4690
		// @note ContentHandler::getPageViewLanguage() may need to load the
4691
		//   content to determine the page language!
4692
		$contentHandler = ContentHandler::getForTitle( $this );
4693
		$pageLang = $contentHandler->getPageViewLanguage( $this );
4694
		return $pageLang;
4695
	}
4696
4697
	/**
4698
	 * Get a list of rendered edit notices for this page.
4699
	 *
4700
	 * Array is keyed by the original message key, and values are rendered using parseAsBlock, so
4701
	 * they will already be wrapped in paragraphs.
4702
	 *
4703
	 * @since 1.21
4704
	 * @param int $oldid Revision ID that's being edited
4705
	 * @return array
4706
	 */
4707
	public function getEditNotices( $oldid = 0 ) {
4708
		$notices = [];
4709
4710
		// Optional notice for the entire namespace
4711
		$editnotice_ns = 'editnotice-' . $this->getNamespace();
4712
		$msg = wfMessage( $editnotice_ns );
4713 View Code Duplication
		if ( $msg->exists() ) {
4714
			$html = $msg->parseAsBlock();
4715
			// Edit notices may have complex logic, but output nothing (T91715)
4716
			if ( trim( $html ) !== '' ) {
4717
				$notices[$editnotice_ns] = Html::rawElement(
4718
					'div',
4719
					[ 'class' => [
4720
						'mw-editnotice',
4721
						'mw-editnotice-namespace',
4722
						Sanitizer::escapeClass( "mw-$editnotice_ns" )
4723
					] ],
4724
					$html
4725
				);
4726
			}
4727
		}
4728
4729
		if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) {
4730
			// Optional notice for page itself and any parent page
4731
			$parts = explode( '/', $this->getDBkey() );
4732
			$editnotice_base = $editnotice_ns;
4733
			while ( count( $parts ) > 0 ) {
4734
				$editnotice_base .= '-' . array_shift( $parts );
4735
				$msg = wfMessage( $editnotice_base );
4736 View Code Duplication
				if ( $msg->exists() ) {
4737
					$html = $msg->parseAsBlock();
4738
					if ( trim( $html ) !== '' ) {
4739
						$notices[$editnotice_base] = Html::rawElement(
4740
							'div',
4741
							[ 'class' => [
4742
								'mw-editnotice',
4743
								'mw-editnotice-base',
4744
								Sanitizer::escapeClass( "mw-$editnotice_base" )
4745
							] ],
4746
							$html
4747
						);
4748
					}
4749
				}
4750
			}
4751
		} else {
4752
			// Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys
4753
			$editnoticeText = $editnotice_ns . '-' . strtr( $this->getDBkey(), '/', '-' );
4754
			$msg = wfMessage( $editnoticeText );
4755 View Code Duplication
			if ( $msg->exists() ) {
4756
				$html = $msg->parseAsBlock();
4757
				if ( trim( $html ) !== '' ) {
4758
					$notices[$editnoticeText] = Html::rawElement(
4759
						'div',
4760
						[ 'class' => [
4761
							'mw-editnotice',
4762
							'mw-editnotice-page',
4763
							Sanitizer::escapeClass( "mw-$editnoticeText" )
4764
						] ],
4765
						$html
4766
					);
4767
				}
4768
			}
4769
		}
4770
4771
		Hooks::run( 'TitleGetEditNotices', [ $this, $oldid, &$notices ] );
4772
		return $notices;
4773
	}
4774
4775
	/**
4776
	 * @return array
4777
	 */
4778
	public function __sleep() {
4779
		return [
4780
			'mNamespace',
4781
			'mDbkeyform',
4782
			'mFragment',
4783
			'mInterwiki',
4784
			'mLocalInterwiki',
4785
			'mUserCaseDBKey',
4786
			'mDefaultNamespace',
4787
		];
4788
	}
4789
4790
	public function __wakeup() {
4791
		$this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0;
4792
		$this->mUrlform = wfUrlencode( $this->mDbkeyform );
4793
		$this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
4794
	}
4795
4796
}
4797