Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/Title.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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