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