]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/Title.php
MediaWiki 1.17.0
[autoinstalls/mediawiki.git] / includes / Title.php
1 <?php
2 /**
3  * See title.txt
4  * @file
5  */
6
7 /**
8  * @todo:  determine if it is really necessary to load this.  Appears to be left over from pre-autoloader versions, and
9  *   is only really needed to provide access to constant UTF8_REPLACEMENT, which actually resides in UtfNormalDefines.php
10  *   and is loaded by UtfNormalUtil.php, which is loaded by UtfNormal.php.
11  */
12 if ( !class_exists( 'UtfNormal' ) ) {
13         require_once( dirname( __FILE__ ) . '/normal/UtfNormal.php' );
14 }
15
16 /**
17  * @deprecated This used to be a define, but was moved to
18  * Title::GAID_FOR_UPDATE in 1.17. This will probably be removed in 1.18
19  */
20 define( 'GAID_FOR_UPDATE', Title::GAID_FOR_UPDATE );
21
22 /**
23  * Represents a title within MediaWiki.
24  * Optionally may contain an interwiki designation or namespace.
25  * @note This class can fetch various kinds of data from the database;
26  *       however, it does so inefficiently.
27  *
28  * @internal documentation reviewed 15 Mar 2010
29  */
30 class Title {
31         /** @name Static cache variables */
32         // @{
33         static private $titleCache = array();
34         // @}
35
36         /**
37          * Title::newFromText maintains a cache to avoid expensive re-normalization of
38          * commonly used titles. On a batch operation this can become a memory leak
39          * if not bounded. After hitting this many titles reset the cache.
40          */
41         const CACHE_MAX = 1000;
42
43         /**
44          * Used to be GAID_FOR_UPDATE define. Used with getArticleId() and friends
45          * to use the master DB
46          */
47         const GAID_FOR_UPDATE = 1;
48
49
50         /**
51          * @name Private member variables
52          * Please use the accessor functions instead.
53          * @private
54          */
55         // @{
56
57         var $mTextform = '';              // /< Text form (spaces not underscores) of the main part
58         var $mUrlform = '';               // /< URL-encoded form of the main part
59         var $mDbkeyform = '';             // /< Main part with underscores
60         var $mUserCaseDBKey;              // /< DB key with the initial letter in the case specified by the user
61         var $mNamespace = NS_MAIN;        // /< Namespace index, i.e. one of the NS_xxxx constants
62         var $mInterwiki = '';             // /< Interwiki prefix (or null string)
63         var $mFragment;                   // /< Title fragment (i.e. the bit after the #)
64         var $mArticleID = -1;             // /< Article ID, fetched from the link cache on demand
65         var $mLatestID = false;           // /< ID of most recent revision
66         var $mRestrictions = array();     // /< Array of groups allowed to edit this article
67         var $mOldRestrictions = false;
68         var $mCascadeRestriction;         ///< Cascade restrictions on this page to included templates and images?
69         var $mCascadingRestrictions;      // Caching the results of getCascadeProtectionSources
70         var $mRestrictionsExpiry = array(); ///< When do the restrictions on this page expire?
71         var $mHasCascadingRestrictions;   ///< Are cascading restrictions in effect on this page?
72         var $mCascadeSources;             ///< Where are the cascading restrictions coming from on this page?
73         var $mRestrictionsLoaded = false; ///< Boolean for initialisation on demand
74         var $mPrefixedText;               ///< Text form including namespace/interwiki, initialised on demand
75         var $mTitleProtection;            ///< Cached value for getTitleProtection (create protection)
76         # Don't change the following default, NS_MAIN is hardcoded in several
77         # places.  See bug 696.
78         var $mDefaultNamespace = NS_MAIN; // /< Namespace index when there is no namespace
79                                           # Zero except in {{transclusion}} tags
80         var $mWatched = null;             // /< Is $wgUser watching this page? null if unfilled, accessed through userIsWatching()
81         var $mLength = -1;                // /< The page length, 0 for special pages
82         var $mRedirect = null;            // /< Is the article at this title a redirect?
83         var $mNotificationTimestamp = array(); // /< Associative array of user ID -> timestamp/false
84         var $mBacklinkCache = null;       // /< Cache of links to this title
85         // @}
86
87
88         /**
89          * Constructor
90          * @private
91          */
92         /* private */ function __construct() { }
93
94         /**
95          * Create a new Title from a prefixed DB key
96          *
97          * @param $key \type{\string} The database key, which has underscores
98          *      instead of spaces, possibly including namespace and
99          *      interwiki prefixes
100          * @return \type{Title} the new object, or NULL on an error
101          */
102         public static function newFromDBkey( $key ) {
103                 $t = new Title();
104                 $t->mDbkeyform = $key;
105                 if ( $t->secureAndSplit() ) {
106                         return $t;
107                 } else {
108                         return null;
109                 }
110         }
111
112         /**
113          * Create a new Title from text, such as what one would find in a link. De-
114          * codes any HTML entities in the text.
115          *
116          * @param $text string  The link text; spaces, prefixes, and an
117          *   initial ':' indicating the main namespace are accepted.
118          * @param $defaultNamespace int The namespace to use if none is speci-
119          *   fied by a prefix.  If you want to force a specific namespace even if
120          *   $text might begin with a namespace prefix, use makeTitle() or
121          *   makeTitleSafe().
122          * @return Title  The new object, or null on an error.
123          */
124         public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
125                 if ( is_object( $text ) ) {
126                         throw new MWException( 'Title::newFromText given an object' );
127                 }
128
129                 /**
130                  * Wiki pages often contain multiple links to the same page.
131                  * Title normalization and parsing can become expensive on
132                  * pages with many links, so we can save a little time by
133                  * caching them.
134                  *
135                  * In theory these are value objects and won't get changed...
136                  */
137                 if ( $defaultNamespace == NS_MAIN && isset( Title::$titleCache[$text] ) ) {
138                         return Title::$titleCache[$text];
139                 }
140
141                 /**
142                  * Convert things like &eacute; &#257; or &#x3017; into normalized (bug 14952) text
143                  */
144                 $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
145
146                 $t = new Title();
147                 $t->mDbkeyform = str_replace( ' ', '_', $filteredText );
148                 $t->mDefaultNamespace = $defaultNamespace;
149
150                 static $cachedcount = 0 ;
151                 if ( $t->secureAndSplit() ) {
152                         if ( $defaultNamespace == NS_MAIN ) {
153                                 if ( $cachedcount >= self::CACHE_MAX ) {
154                                         # Avoid memory leaks on mass operations...
155                                         Title::$titleCache = array();
156                                         $cachedcount = 0;
157                                 }
158                                 $cachedcount++;
159                                 Title::$titleCache[$text] =& $t;
160                         }
161                         return $t;
162                 } else {
163                         $ret = null;
164                         return $ret;
165                 }
166         }
167
168         /**
169          * THIS IS NOT THE FUNCTION YOU WANT. Use Title::newFromText().
170          *
171          * Example of wrong and broken code:
172          * $title = Title::newFromURL( $wgRequest->getVal( 'title' ) );
173          *
174          * Example of right code:
175          * $title = Title::newFromText( $wgRequest->getVal( 'title' ) );
176          *
177          * Create a new Title from URL-encoded text. Ensures that
178          * the given title's length does not exceed the maximum.
179          *
180          * @param $url \type{\string} the title, as might be taken from a URL
181          * @return \type{Title} the new object, or NULL on an error
182          */
183         public static function newFromURL( $url ) {
184                 global $wgLegalTitleChars;
185                 $t = new Title();
186
187                 # For compatibility with old buggy URLs. "+" is usually not valid in titles,
188                 # but some URLs used it as a space replacement and they still come
189                 # from some external search tools.
190                 if ( strpos( $wgLegalTitleChars, '+' ) === false ) {
191                         $url = str_replace( '+', ' ', $url );
192                 }
193
194                 $t->mDbkeyform = str_replace( ' ', '_', $url );
195                 if ( $t->secureAndSplit() ) {
196                         return $t;
197                 } else {
198                         return null;
199                 }
200         }
201
202         /**
203          * Create a new Title from an article ID
204          *
205          * @param $id \type{\int} the page_id corresponding to the Title to create
206          * @param $flags \type{\int} use Title::GAID_FOR_UPDATE to use master
207          * @return \type{Title} the new object, or NULL on an error
208          */
209         public static function newFromID( $id, $flags = 0 ) {
210                 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
211                 $row = $db->selectRow( 'page', '*', array( 'page_id' => $id ), __METHOD__ );
212                 if ( $row !== false ) {
213                         $title = Title::newFromRow( $row );
214                 } else {
215                         $title = null;
216                 }
217                 return $title;
218         }
219
220         /**
221          * Make an array of titles from an array of IDs
222          *
223          * @param $ids \type{\arrayof{\int}} Array of IDs
224          * @return \type{\arrayof{Title}} Array of Titles
225          */
226         public static function newFromIDs( $ids ) {
227                 if ( !count( $ids ) ) {
228                         return array();
229                 }
230                 $dbr = wfGetDB( DB_SLAVE );
231                 
232                 $res = $dbr->select(
233                         'page',
234                         array(
235                                 'page_namespace', 'page_title', 'page_id',
236                                 'page_len', 'page_is_redirect', 'page_latest',
237                         ),
238                         array( 'page_id' => $ids ),
239                         __METHOD__
240                 );
241
242                 $titles = array();
243                 foreach ( $res as $row ) {
244                         $titles[] = Title::newFromRow( $row );
245                 }
246                 return $titles;
247         }
248
249         /**
250          * Make a Title object from a DB row
251          *
252          * @param $row \type{Row} (needs at least page_title,page_namespace)
253          * @return \type{Title} corresponding Title
254          */
255         public static function newFromRow( $row ) {
256                 $t = self::makeTitle( $row->page_namespace, $row->page_title );
257
258                 $t->mArticleID = isset( $row->page_id ) ? intval( $row->page_id ) : -1;
259                 $t->mLength = isset( $row->page_len ) ? intval( $row->page_len ) : -1;
260                 $t->mRedirect = isset( $row->page_is_redirect ) ? (bool)$row->page_is_redirect : null;
261                 $t->mLatestID = isset( $row->page_latest ) ? intval( $row->page_latest ) : false;
262
263                 return $t;
264         }
265
266         /**
267          * Create a new Title from a namespace index and a DB key.
268          * It's assumed that $ns and $title are *valid*, for instance when
269          * they came directly from the database or a special page name.
270          * For convenience, spaces are converted to underscores so that
271          * eg user_text fields can be used directly.
272          *
273          * @param $ns \type{\int} the namespace of the article
274          * @param $title \type{\string} the unprefixed database key form
275          * @param $fragment \type{\string} The link fragment (after the "#")
276          * @param $interwiki \type{\string} The interwiki prefix
277          * @return \type{Title} the new object
278          */
279         public static function &makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) {
280                 $t = new Title();
281                 $t->mInterwiki = $interwiki;
282                 $t->mFragment = $fragment;
283                 $t->mNamespace = $ns = intval( $ns );
284                 $t->mDbkeyform = str_replace( ' ', '_', $title );
285                 $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
286                 $t->mUrlform = wfUrlencode( $t->mDbkeyform );
287                 $t->mTextform = str_replace( '_', ' ', $title );
288                 return $t;
289         }
290
291         /**
292          * Create a new Title from a namespace index and a DB key.
293          * The parameters will be checked for validity, which is a bit slower
294          * than makeTitle() but safer for user-provided data.
295          *
296          * @param $ns \type{\int} the namespace of the article
297          * @param $title \type{\string} the database key form
298          * @param $fragment \type{\string} The link fragment (after the "#")
299          * @param $interwiki \type{\string} The interwiki prefix
300          * @return \type{Title} the new object, or NULL on an error
301          */
302         public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
303                 $t = new Title();
304                 $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki );
305                 if ( $t->secureAndSplit() ) {
306                         return $t;
307                 } else {
308                         return null;
309                 }
310         }
311
312         /**
313          * Create a new Title for the Main Page
314          *
315          * @return \type{Title} the new object
316          */
317         public static function newMainPage() {
318                 $title = Title::newFromText( wfMsgForContent( 'mainpage' ) );
319                 // Don't give fatal errors if the message is broken
320                 if ( !$title ) {
321                         $title = Title::newFromText( 'Main Page' );
322                 }
323                 return $title;
324         }
325
326         /**
327          * Extract a redirect destination from a string and return the
328          * Title, or null if the text doesn't contain a valid redirect
329          * This will only return the very next target, useful for
330          * the redirect table and other checks that don't need full recursion
331          *
332          * @param $text String: Text with possible redirect
333          * @return Title: The corresponding Title
334          */
335         public static function newFromRedirect( $text ) {
336                 return self::newFromRedirectInternal( $text );
337         }
338
339         /**
340          * Extract a redirect destination from a string and return the
341          * Title, or null if the text doesn't contain a valid redirect
342          * This will recurse down $wgMaxRedirects times or until a non-redirect target is hit
343          * in order to provide (hopefully) the Title of the final destination instead of another redirect
344          *
345          * @param $text \type{\string} Text with possible redirect
346          * @return \type{Title} The corresponding Title
347          */
348         public static function newFromRedirectRecurse( $text ) {
349                 $titles = self::newFromRedirectArray( $text );
350                 return $titles ? array_pop( $titles ) : null;
351         }
352
353         /**
354          * Extract a redirect destination from a string and return an
355          * array of Titles, or null if the text doesn't contain a valid redirect
356          * The last element in the array is the final destination after all redirects
357          * have been resolved (up to $wgMaxRedirects times)
358          *
359          * @param $text \type{\string} Text with possible redirect
360          * @return \type{\array} Array of Titles, with the destination last
361          */
362         public static function newFromRedirectArray( $text ) {
363                 global $wgMaxRedirects;
364                 // are redirects disabled?
365                 if ( $wgMaxRedirects < 1 ) {
366                         return null;
367                 }
368                 $title = self::newFromRedirectInternal( $text );
369                 if ( is_null( $title ) ) {
370                         return null;
371                 }
372                 // recursive check to follow double redirects
373                 $recurse = $wgMaxRedirects;
374                 $titles = array( $title );
375                 while ( --$recurse > 0 ) {
376                         if ( $title->isRedirect() ) {
377                                 $article = new Article( $title, 0 );
378                                 $newtitle = $article->getRedirectTarget();
379                         } else {
380                                 break;
381                         }
382                         // Redirects to some special pages are not permitted
383                         if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
384                                 // the new title passes the checks, so make that our current title so that further recursion can be checked
385                                 $title = $newtitle;
386                                 $titles[] = $newtitle;
387                         } else {
388                                 break;
389                         }
390                 }
391                 return $titles;
392         }
393
394         /**
395          * Really extract the redirect destination
396          * Do not call this function directly, use one of the newFromRedirect* functions above
397          *
398          * @param $text \type{\string} Text with possible redirect
399          * @return \type{Title} The corresponding Title
400          */
401         protected static function newFromRedirectInternal( $text ) {
402                 $redir = MagicWord::get( 'redirect' );
403                 $text = trim( $text );
404                 if ( $redir->matchStartAndRemove( $text ) ) {
405                         // Extract the first link and see if it's usable
406                         // Ensure that it really does come directly after #REDIRECT
407                         // Some older redirects included a colon, so don't freak about that!
408                         $m = array();
409                         if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
410                                 // Strip preceding colon used to "escape" categories, etc.
411                                 // and URL-decode links
412                                 if ( strpos( $m[1], '%' ) !== false ) {
413                                         // Match behavior of inline link parsing here;
414                                         // don't interpret + as " " most of the time!
415                                         // It might be safe to just use rawurldecode instead, though.
416                                         $m[1] = urldecode( ltrim( $m[1], ':' ) );
417                                 }
418                                 $title = Title::newFromText( $m[1] );
419                                 // If the title is a redirect to bad special pages or is invalid, return null
420                                 if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
421                                         return null;
422                                 }
423                                 return $title;
424                         }
425                 }
426                 return null;
427         }
428
429 # ----------------------------------------------------------------------------
430 #       Static functions
431 # ----------------------------------------------------------------------------
432
433         /**
434          * Get the prefixed DB key associated with an ID
435          *
436          * @param $id \type{\int} the page_id of the article
437          * @return \type{Title} an object representing the article, or NULL
438          *  if no such article was found
439          */
440         public static function nameOf( $id ) {
441                 $dbr = wfGetDB( DB_SLAVE );
442
443                 $s = $dbr->selectRow(
444                         'page',
445                         array( 'page_namespace', 'page_title' ),
446                         array( 'page_id' => $id ),
447                         __METHOD__
448                 );
449                 if ( $s === false ) {
450                         return null;
451                 }
452
453                 $n = self::makeName( $s->page_namespace, $s->page_title );
454                 return $n;
455         }
456
457         /**
458          * Get a regex character class describing the legal characters in a link
459          *
460          * @return \type{\string} the list of characters, not delimited
461          */
462         public static function legalChars() {
463                 global $wgLegalTitleChars;
464                 return $wgLegalTitleChars;
465         }
466
467         /**
468          * Get a string representation of a title suitable for
469          * including in a search index
470          *
471          * @param $ns \type{\int} a namespace index
472          * @param $title \type{\string} text-form main part
473          * @return \type{\string} a stripped-down title string ready for the
474          *  search index
475          */
476         public static function indexTitle( $ns, $title ) {
477                 global $wgContLang;
478
479                 $lc = SearchEngine::legalSearchChars() . '&#;';
480                 $t = $wgContLang->normalizeForSearch( $title );
481                 $t = preg_replace( "/[^{$lc}]+/", ' ', $t );
482                 $t = $wgContLang->lc( $t );
483
484                 # Handle 's, s'
485                 $t = preg_replace( "/([{$lc}]+)'s( |$)/", "\\1 \\1's ", $t );
486                 $t = preg_replace( "/([{$lc}]+)s'( |$)/", "\\1s ", $t );
487
488                 $t = preg_replace( "/\\s+/", ' ', $t );
489
490                 if ( $ns == NS_FILE ) {
491                         $t = preg_replace( "/ (png|gif|jpg|jpeg|ogg)$/", "", $t );
492                 }
493                 return trim( $t );
494         }
495
496         /**
497          * Make a prefixed DB key from a DB key and a namespace index
498          *
499          * @param $ns \type{\int} numerical representation of the namespace
500          * @param $title \type{\string} the DB key form the title
501          * @param $fragment \type{\string} The link fragment (after the "#")
502          * @param $interwiki \type{\string} The interwiki prefix
503          * @return \type{\string} the prefixed form of the title
504          */
505         public static function makeName( $ns, $title, $fragment = '', $interwiki = '' ) {
506                 global $wgContLang;
507
508                 $namespace = $wgContLang->getNsText( $ns );
509                 $name = $namespace == '' ? $title : "$namespace:$title";
510                 if ( strval( $interwiki ) != '' ) {
511                         $name = "$interwiki:$name";
512                 }
513                 if ( strval( $fragment ) != '' ) {
514                         $name .= '#' . $fragment;
515                 }
516                 return $name;
517         }
518
519         /**
520          * Determine whether the object refers to a page within
521          * this project.
522          *
523          * @return \type{\bool} TRUE if this is an in-project interwiki link
524          *      or a wikilink, FALSE otherwise
525          */
526         public function isLocal() {
527                 if ( $this->mInterwiki != '' ) {
528                         return Interwiki::fetch( $this->mInterwiki )->isLocal();
529                 } else {
530                         return true;
531                 }
532         }
533
534         /**
535          * Determine whether the object refers to a page within
536          * this project and is transcludable.
537          *
538          * @return \type{\bool} TRUE if this is transcludable
539          */
540         public function isTrans() {
541                 if ( $this->mInterwiki == '' ) {
542                         return false;
543                 }
544
545                 return Interwiki::fetch( $this->mInterwiki )->isTranscludable();
546         }
547
548         /**
549          * Returns the DB name of the distant wiki 
550          * which owns the object.
551          *
552          * @return \type{\string} the DB name
553          */
554         public function getTransWikiID() {
555                 if ( $this->mInterwiki == '' ) {
556                         return false;
557                 }
558
559                 return Interwiki::fetch( $this->mInterwiki )->getWikiID();
560         }
561
562         /**
563          * Escape a text fragment, say from a link, for a URL
564          *
565          * @param $fragment string containing a URL or link fragment (after the "#")
566          * @return String: escaped string
567          */
568         static function escapeFragmentForURL( $fragment ) {
569                 # Note that we don't urlencode the fragment.  urlencoded Unicode
570                 # fragments appear not to work in IE (at least up to 7) or in at least
571                 # one version of Opera 9.x.  The W3C validator, for one, doesn't seem
572                 # to care if they aren't encoded.
573                 return Sanitizer::escapeId( $fragment, 'noninitial' );
574         }
575
576 # ----------------------------------------------------------------------------
577 #       Other stuff
578 # ----------------------------------------------------------------------------
579
580         /** Simple accessors */
581         /**
582          * Get the text form (spaces not underscores) of the main part
583          *
584          * @return \type{\string} Main part of the title
585          */
586         public function getText() { return $this->mTextform; }
587
588         /**
589          * Get the URL-encoded form of the main part
590          *
591          * @return \type{\string} Main part of the title, URL-encoded
592          */
593         public function getPartialURL() { return $this->mUrlform; }
594
595         /**
596          * Get the main part with underscores
597          *
598          * @return String: Main part of the title, with underscores
599          */
600         public function getDBkey() { return $this->mDbkeyform; }
601
602         /**
603          * Get the namespace index, i.e.\ one of the NS_xxxx constants.
604          *
605          * @return Integer: Namespace index
606          */
607         public function getNamespace() { return $this->mNamespace; }
608
609         /**
610          * Get the namespace text
611          *
612          * @return String: Namespace text
613          */
614         public function getNsText() {
615                 global $wgContLang;
616
617                 if ( $this->mInterwiki != '' ) {
618                         // This probably shouldn't even happen. ohh man, oh yuck.
619                         // But for interwiki transclusion it sometimes does.
620                         // Shit. Shit shit shit.
621                         //
622                         // Use the canonical namespaces if possible to try to
623                         // resolve a foreign namespace.
624                         if ( MWNamespace::exists( $this->mNamespace ) ) {
625                                 return MWNamespace::getCanonicalName( $this->mNamespace );
626                         }
627                 }
628                 return $wgContLang->getNsText( $this->mNamespace );
629         }
630
631         /**
632          * Get the DB key with the initial letter case as specified by the user
633          *
634          * @return \type{\string} DB key
635          */
636         function getUserCaseDBKey() {
637                 return $this->mUserCaseDBKey;
638         }
639
640         /**
641          * Get the namespace text of the subject (rather than talk) page
642          *
643          * @return \type{\string} Namespace text
644          */
645         public function getSubjectNsText() {
646                 global $wgContLang;
647                 return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) );
648         }
649
650         /**
651          * Get the namespace text of the talk page
652          *
653          * @return \type{\string} Namespace text
654          */
655         public function getTalkNsText() {
656                 global $wgContLang;
657                 return( $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ) );
658         }
659
660         /**
661          * Could this title have a corresponding talk page?
662          *
663          * @return \type{\bool} TRUE or FALSE
664          */
665         public function canTalk() {
666                 return( MWNamespace::canTalk( $this->mNamespace ) );
667         }
668
669         /**
670          * Get the interwiki prefix (or null string)
671          *
672          * @return \type{\string} Interwiki prefix
673          */
674         public function getInterwiki() { return $this->mInterwiki; }
675
676         /**
677          * Get the Title fragment (i.e.\ the bit after the #) in text form
678          *
679          * @return \type{\string} Title fragment
680          */
681         public function getFragment() { return $this->mFragment; }
682
683         /**
684          * Get the fragment in URL form, including the "#" character if there is one
685          * @return \type{\string} Fragment in URL form
686          */
687         public function getFragmentForURL() {
688                 if ( $this->mFragment == '' ) {
689                         return '';
690                 } else {
691                         return '#' . Title::escapeFragmentForURL( $this->mFragment );
692                 }
693         }
694
695         /**
696          * Get the default namespace index, for when there is no namespace
697          *
698          * @return \type{\int} Default namespace index
699          */
700         public function getDefaultNamespace() { return $this->mDefaultNamespace; }
701
702         /**
703          * Get title for search index
704          *
705          * @return \type{\string} a stripped-down title string ready for the
706          *  search index
707          */
708         public function getIndexTitle() {
709                 return Title::indexTitle( $this->mNamespace, $this->mTextform );
710         }
711
712         /**
713          * Get the prefixed database key form
714          *
715          * @return \type{\string} the prefixed title, with underscores and
716          *  any interwiki and namespace prefixes
717          */
718         public function getPrefixedDBkey() {
719                 $s = $this->prefix( $this->mDbkeyform );
720                 $s = str_replace( ' ', '_', $s );
721                 return $s;
722         }
723
724         /**
725          * Get the prefixed title with spaces.
726          * This is the form usually used for display
727          *
728          * @return \type{\string} the prefixed title, with spaces
729          */
730         public function getPrefixedText() {
731                 if ( empty( $this->mPrefixedText ) ) { // FIXME: bad usage of empty() ?
732                         $s = $this->prefix( $this->mTextform );
733                         $s = str_replace( '_', ' ', $s );
734                         $this->mPrefixedText = $s;
735                 }
736                 return $this->mPrefixedText;
737         }
738
739         /**
740          * Get the prefixed title with spaces, plus any fragment
741          * (part beginning with '#')
742          *
743          * @return \type{\string} the prefixed title, with spaces and
744          *  the fragment, including '#'
745          */
746         public function getFullText() {
747                 $text = $this->getPrefixedText();
748                 if ( $this->mFragment != '' ) {
749                         $text .= '#' . $this->mFragment;
750                 }
751                 return $text;
752         }
753
754         /**
755          * Get the base name, i.e. the leftmost parts before the /
756          *
757          * @return \type{\string} Base name
758          */
759         public function getBaseText() {
760                 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
761                         return $this->getText();
762                 }
763
764                 $parts = explode( '/', $this->getText() );
765                 # Don't discard the real title if there's no subpage involved
766                 if ( count( $parts ) > 1 ) {
767                         unset( $parts[count( $parts ) - 1] );
768                 }
769                 return implode( '/', $parts );
770         }
771
772         /**
773          * Get the lowest-level subpage name, i.e. the rightmost part after /
774          *
775          * @return \type{\string} Subpage name
776          */
777         public function getSubpageText() {
778                 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
779                         return( $this->mTextform );
780                 }
781                 $parts = explode( '/', $this->mTextform );
782                 return( $parts[count( $parts ) - 1] );
783         }
784
785         /**
786          * Get a URL-encoded form of the subpage text
787          *
788          * @return \type{\string} URL-encoded subpage name
789          */
790         public function getSubpageUrlForm() {
791                 $text = $this->getSubpageText();
792                 $text = wfUrlencode( str_replace( ' ', '_', $text ) );
793                 return( $text );
794         }
795
796         /**
797          * Get a URL-encoded title (not an actual URL) including interwiki
798          *
799          * @return \type{\string} the URL-encoded form
800          */
801         public function getPrefixedURL() {
802                 $s = $this->prefix( $this->mDbkeyform );
803                 $s = wfUrlencode( str_replace( ' ', '_', $s ) );
804                 return $s;
805         }
806
807         /**
808          * Get a real URL referring to this title, with interwiki link and
809          * fragment
810          *
811          * @param $query \twotypes{\string,\array} an optional query string, not used for interwiki
812          *   links. Can be specified as an associative array as well, e.g.,
813          *   array( 'action' => 'edit' ) (keys and values will be URL-escaped).
814          * @param $variant \type{\string} language variant of url (for sr, zh..)
815          * @return \type{\string} the URL
816          */
817         public function getFullURL( $query = '', $variant = false ) {
818                 global $wgServer, $wgRequest;
819
820                 if ( is_array( $query ) ) {
821                         $query = wfArrayToCGI( $query );
822                 }
823
824                 $interwiki = Interwiki::fetch( $this->mInterwiki );
825                 if ( !$interwiki ) {
826                         $url = $this->getLocalURL( $query, $variant );
827
828                         // Ugly quick hack to avoid duplicate prefixes (bug 4571 etc)
829                         // Correct fix would be to move the prepending elsewhere.
830                         if ( $wgRequest->getVal( 'action' ) != 'render' ) {
831                                 $url = $wgServer . $url;
832                         }
833                 } else {
834                         $baseUrl = $interwiki->getURL();
835
836                         $namespace = wfUrlencode( $this->getNsText() );
837                         if ( $namespace != '' ) {
838                                 # Can this actually happen? Interwikis shouldn't be parsed.
839                                 # Yes! It can in interwiki transclusion. But... it probably shouldn't.
840                                 $namespace .= ':';
841                         }
842                         $url = str_replace( '$1', $namespace . $this->mUrlform, $baseUrl );
843                         $url = wfAppendQuery( $url, $query );
844                 }
845
846                 # Finally, add the fragment.
847                 $url .= $this->getFragmentForURL();
848
849                 wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) );
850                 return $url;
851         }
852
853         /**
854          * Get a URL with no fragment or server name.  If this page is generated
855          * with action=render, $wgServer is prepended.
856          *
857          * @param $query Mixed: an optional query string; if not specified,
858          *   $wgArticlePath will be used.  Can be specified as an associative array
859          *   as well, e.g., array( 'action' => 'edit' ) (keys and values will be
860          *   URL-escaped).
861          * @param $variant \type{\string} language variant of url (for sr, zh..)
862          * @return \type{\string} the URL
863          */
864         public function getLocalURL( $query = '', $variant = false ) {
865                 global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
866                 global $wgVariantArticlePath, $wgContLang;
867
868                 if ( is_array( $query ) ) {
869                         $query = wfArrayToCGI( $query );
870                 }
871
872                 if ( $this->isExternal() ) {
873                         $url = $this->getFullURL();
874                         if ( $query ) {
875                                 // This is currently only used for edit section links in the
876                                 // context of interwiki transclusion. In theory we should
877                                 // append the query to the end of any existing query string,
878                                 // but interwiki transclusion is already broken in that case.
879                                 $url .= "?$query";
880                         }
881                 } else {
882                         $dbkey = wfUrlencode( $this->getPrefixedDBkey() );
883                         if ( $query == '' ) {
884                                 if ( $variant != false && $wgContLang->hasVariants() ) {
885                                         if ( !$wgVariantArticlePath ) {
886                                                 $variantArticlePath =  "$wgScript?title=$1&variant=$2"; // default
887                                         } else {
888                                                 $variantArticlePath = $wgVariantArticlePath;
889                                         }
890                                         $url = str_replace( '$2', urlencode( $variant ), $variantArticlePath );
891                                         $url = str_replace( '$1', $dbkey, $url  );
892                                 } else {
893                                         $url = str_replace( '$1', $dbkey, $wgArticlePath );
894                                 }
895                         } else {
896                                 global $wgActionPaths;
897                                 $url = false;
898                                 $matches = array();
899                                 if ( !empty( $wgActionPaths ) &&
900                                         preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) )
901                                 {
902                                         $action = urldecode( $matches[2] );
903                                         if ( isset( $wgActionPaths[$action] ) ) {
904                                                 $query = $matches[1];
905                                                 if ( isset( $matches[4] ) ) {
906                                                         $query .= $matches[4];
907                                                 }
908                                                 $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
909                                                 if ( $query != '' ) {
910                                                         $url = wfAppendQuery( $url, $query );
911                                                 }
912                                         }
913                                 }
914                                 if ( $url === false ) {
915                                         if ( $query == '-' ) {
916                                                 $query = '';
917                                         }
918                                         $url = "{$wgScript}?title={$dbkey}&{$query}";
919                                 }
920                         }
921
922                         // FIXME: this causes breakage in various places when we
923                         // actually expected a local URL and end up with dupe prefixes.
924                         if ( $wgRequest->getVal( 'action' ) == 'render' ) {
925                                 $url = $wgServer . $url;
926                         }
927                 }
928                 wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) );
929                 return $url;
930         }
931
932         /**
933          * Get a URL that's the simplest URL that will be valid to link, locally,
934          * to the current Title.  It includes the fragment, but does not include
935          * the server unless action=render is used (or the link is external).  If
936          * there's a fragment but the prefixed text is empty, we just return a link
937          * to the fragment.
938          *
939          * The result obviously should not be URL-escaped, but does need to be
940          * HTML-escaped if it's being output in HTML.
941          *
942          * @param $query \type{\arrayof{\string}} An associative array of key => value pairs for the
943          *   query string.  Keys and values will be escaped.
944          * @param $variant \type{\string} Language variant of URL (for sr, zh..).  Ignored
945          *   for external links.  Default is "false" (same variant as current page,
946          *   for anonymous users).
947          * @return \type{\string} the URL
948          */
949         public function getLinkUrl( $query = array(), $variant = false ) {
950                 wfProfileIn( __METHOD__ );
951                 if ( $this->isExternal() ) {
952                         $ret = $this->getFullURL( $query );
953                 } elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) {
954                         $ret = $this->getFragmentForURL();
955                 } else {
956                         $ret = $this->getLocalURL( $query, $variant ) . $this->getFragmentForURL();
957                 }
958                 wfProfileOut( __METHOD__ );
959                 return $ret;
960         }
961
962         /**
963          * Get an HTML-escaped version of the URL form, suitable for
964          * using in a link, without a server name or fragment
965          *
966          * @param $query \type{\string} an optional query string
967          * @return \type{\string} the URL
968          */
969         public function escapeLocalURL( $query = '' ) {
970                 return htmlspecialchars( $this->getLocalURL( $query ) );
971         }
972
973         /**
974          * Get an HTML-escaped version of the URL form, suitable for
975          * using in a link, including the server name and fragment
976          *
977          * @param $query \type{\string} an optional query string
978          * @return \type{\string} the URL
979          */
980         public function escapeFullURL( $query = '' ) {
981                 return htmlspecialchars( $this->getFullURL( $query ) );
982         }
983
984         /**
985          * Get the URL form for an internal link.
986          * - Used in various Squid-related code, in case we have a different
987          * internal hostname for the server from the exposed one.
988          *
989          * @param $query \type{\string} an optional query string
990          * @param $variant \type{\string} language variant of url (for sr, zh..)
991          * @return \type{\string} the URL
992          */
993         public function getInternalURL( $query = '', $variant = false ) {
994                 global $wgInternalServer;
995                 $url = $wgInternalServer . $this->getLocalURL( $query, $variant );
996                 wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) );
997                 return $url;
998         }
999
1000         /**
1001          * Get the edit URL for this Title
1002          *
1003          * @return \type{\string} the URL, or a null string if this is an
1004          *  interwiki link
1005          */
1006         public function getEditURL() {
1007                 if ( $this->mInterwiki != '' ) {
1008                         return '';
1009                 }
1010                 $s = $this->getLocalURL( 'action=edit' );
1011
1012                 return $s;
1013         }
1014
1015         /**
1016          * Get the HTML-escaped displayable text form.
1017          * Used for the title field in <a> tags.
1018          *
1019          * @return \type{\string} the text, including any prefixes
1020          */
1021         public function getEscapedText() {
1022                 return htmlspecialchars( $this->getPrefixedText() );
1023         }
1024
1025         /**
1026          * Is this Title interwiki?
1027          *
1028          * @return \type{\bool}
1029          */
1030         public function isExternal() {
1031                 return ( $this->mInterwiki != '' );
1032         }
1033
1034         /**
1035          * Is this page "semi-protected" - the *only* protection is autoconfirm?
1036          *
1037          * @param $action \type{\string} Action to check (default: edit)
1038          * @return \type{\bool}
1039          */
1040         public function isSemiProtected( $action = 'edit' ) {
1041                 if ( $this->exists() ) {
1042                         $restrictions = $this->getRestrictions( $action );
1043                         if ( count( $restrictions ) > 0 ) {
1044                                 foreach ( $restrictions as $restriction ) {
1045                                         if ( strtolower( $restriction ) != 'autoconfirmed' ) {
1046                                                 return false;
1047                                         }
1048                                 }
1049                         } else {
1050                                 # Not protected
1051                                 return false;
1052                         }
1053                         return true;
1054                 } else {
1055                         # If it doesn't exist, it can't be protected
1056                         return false;
1057                 }
1058         }
1059
1060         /**
1061          * Does the title correspond to a protected article?
1062          *
1063          * @param $action \type{\string} the action the page is protected from,
1064          * by default checks all actions.
1065          * @return \type{\bool}
1066          */
1067         public function isProtected( $action = '' ) {
1068                 global $wgRestrictionLevels;
1069
1070                 $restrictionTypes = $this->getRestrictionTypes();
1071
1072                 # Special pages have inherent protection
1073                 if( $this->getNamespace() == NS_SPECIAL ) {
1074                         return true;
1075                 }
1076
1077                 # Check regular protection levels
1078                 foreach ( $restrictionTypes as $type ) {
1079                         if ( $action == $type || $action == '' ) {
1080                                 $r = $this->getRestrictions( $type );
1081                                 foreach ( $wgRestrictionLevels as $level ) {
1082                                         if ( in_array( $level, $r ) && $level != '' ) {
1083                                                 return true;
1084                                         }
1085                                 }
1086                         }
1087                 }
1088
1089                 return false;
1090         }
1091
1092         /**
1093          * Is this a conversion table for the LanguageConverter?
1094          *
1095          * @return \type{\bool}
1096          */
1097         public function isConversionTable() {
1098                 if(
1099                         $this->getNamespace() == NS_MEDIAWIKI &&
1100                         strpos( $this->getText(), 'Conversiontable' ) !== false
1101                 )
1102                 {
1103                         return true;
1104                 }
1105
1106                 return false;
1107         }
1108
1109         /**
1110          * Is $wgUser watching this page?
1111          *
1112          * @return \type{\bool}
1113          */
1114         public function userIsWatching() {
1115                 global $wgUser;
1116
1117                 if ( is_null( $this->mWatched ) ) {
1118                         if ( NS_SPECIAL == $this->mNamespace || !$wgUser->isLoggedIn() ) {
1119                                 $this->mWatched = false;
1120                         } else {
1121                                 $this->mWatched = $wgUser->isWatched( $this );
1122                         }
1123                 }
1124                 return $this->mWatched;
1125         }
1126
1127         /**
1128          * Can $wgUser perform $action on this page?
1129          * This skips potentially expensive cascading permission checks
1130          * as well as avoids expensive error formatting
1131          *
1132          * Suitable for use for nonessential UI controls in common cases, but
1133          * _not_ for functional access control.
1134          *
1135          * May provide false positives, but should never provide a false negative.
1136          *
1137          * @param $action \type{\string} action that permission needs to be checked for
1138          * @return \type{\bool}
1139          */
1140         public function quickUserCan( $action ) {
1141                 return $this->userCan( $action, false );
1142         }
1143
1144         /**
1145          * Determines if $wgUser is unable to edit this page because it has been protected
1146          * by $wgNamespaceProtection.
1147          *
1148          * @return \type{\bool}
1149          */
1150         public function isNamespaceProtected() {
1151                 global $wgNamespaceProtection, $wgUser;
1152                 if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
1153                         foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
1154                                 if ( $right != '' && !$wgUser->isAllowed( $right ) ) {
1155                                         return true;
1156                                 }
1157                         }
1158                 }
1159                 return false;
1160         }
1161
1162         /**
1163          * Can $wgUser perform $action on this page?
1164          *
1165          * @param $action \type{\string} action that permission needs to be checked for
1166          * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
1167          * @return \type{\bool}
1168          */
1169         public function userCan( $action, $doExpensiveQueries = true ) {
1170                 global $wgUser;
1171                 return ( $this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries, true ) === array() );
1172         }
1173
1174         /**
1175          * Can $user perform $action on this page?
1176          *
1177          * FIXME: This *does not* check throttles (User::pingLimiter()).
1178          *
1179          * @param $action \type{\string}action that permission needs to be checked for
1180          * @param $user \type{User} user to check
1181          * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
1182          * @param $ignoreErrors \type{\arrayof{\string}} Set this to a list of message keys whose corresponding errors may be ignored.
1183          * @return \type{\array} Array of arrays of the arguments to wfMsg to explain permissions problems.
1184          */
1185         public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) {
1186                 $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries );
1187
1188                 // Remove the errors being ignored.
1189                 foreach ( $errors as $index => $error ) {
1190                         $error_key = is_array( $error ) ? $error[0] : $error;
1191
1192                         if ( in_array( $error_key, $ignoreErrors ) ) {
1193                                 unset( $errors[$index] );
1194                         }
1195                 }
1196
1197                 return $errors;
1198         }
1199
1200         /**
1201          * Permissions checks that fail most often, and which are easiest to test.
1202          *
1203          * @param $action String the action to check
1204          * @param $user User user to check
1205          * @param $errors Array list of current errors
1206          * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
1207          * @param $short Boolean short circuit on first error
1208          *
1209          * @return Array list of errors
1210          */
1211         private function checkQuickPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
1212                 if ( $action == 'create' ) {
1213                         if ( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
1214                                  ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) {
1215                                 $errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' );
1216                         }
1217                 } elseif ( $action == 'move' ) {
1218                         if ( !$user->isAllowed( 'move-rootuserpages' )
1219                                         && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1220                                 // Show user page-specific message only if the user can move other pages
1221                                 $errors[] = array( 'cant-move-user-page' );
1222                         }
1223
1224                         // Check if user is allowed to move files if it's a file
1225                         if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
1226                                 $errors[] = array( 'movenotallowedfile' );
1227                         }
1228
1229                         if ( !$user->isAllowed( 'move' ) ) {
1230                                 // User can't move anything
1231                                 global $wgGroupPermissions;
1232                                 $userCanMove = false;
1233                                 if ( isset( $wgGroupPermissions['user']['move'] ) ) {
1234                                         $userCanMove = $wgGroupPermissions['user']['move'];
1235                                 }
1236                                 $autoconfirmedCanMove = false;
1237                                 if ( isset( $wgGroupPermissions['autoconfirmed']['move'] ) ) {
1238                                         $autoconfirmedCanMove = $wgGroupPermissions['autoconfirmed']['move'];
1239                                 }
1240                                 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
1241                                         // custom message if logged-in users without any special rights can move
1242                                         $errors[] = array( 'movenologintext' );
1243                                 } else {
1244                                         $errors[] = array( 'movenotallowed' );
1245                                 }
1246                         }
1247                 } elseif ( $action == 'move-target' ) {
1248                         if ( !$user->isAllowed( 'move' ) ) {
1249                                 // User can't move anything
1250                                 $errors[] = array( 'movenotallowed' );
1251                         } elseif ( !$user->isAllowed( 'move-rootuserpages' )
1252                                         && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1253                                 // Show user page-specific message only if the user can move other pages
1254                                 $errors[] = array( 'cant-move-to-user-page' );
1255                         }
1256                 } elseif ( !$user->isAllowed( $action ) ) {
1257                         // We avoid expensive display logic for quickUserCan's and such
1258                         $groups = false;
1259                         if ( !$short ) {
1260                                 $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
1261                                         User::getGroupsWithPermission( $action ) );
1262                         }
1263
1264                         if ( $groups ) {
1265                                 global $wgLang;
1266                                 $return = array(
1267                                         'badaccess-groups',
1268                                         $wgLang->commaList( $groups ),
1269                                         count( $groups )
1270                                 );
1271                         } else {
1272                                 $return = array( 'badaccess-group0' );
1273                         }
1274                         $errors[] = $return;
1275                 }
1276
1277                 return $errors;
1278         }
1279
1280         /**
1281          * Add the resulting error code to the errors array
1282          *
1283          * @param $errors Array list of current errors
1284          * @param $result Mixed result of errors
1285          *
1286          * @return Array list of errors
1287          */
1288         private function resultToError( $errors, $result ) {
1289                 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
1290                         // A single array representing an error
1291                         $errors[] = $result;
1292                 } else if ( is_array( $result ) && is_array( $result[0] ) ) {
1293                         // A nested array representing multiple errors
1294                         $errors = array_merge( $errors, $result );
1295                 } else if ( $result !== '' && is_string( $result ) ) {
1296                         // A string representing a message-id
1297                         $errors[] = array( $result );
1298                 } else if ( $result === false ) {
1299                         // a generic "We don't want them to do that"
1300                         $errors[] = array( 'badaccess-group0' );
1301                 }
1302                 return $errors;
1303         }
1304
1305         /**
1306          * Check various permission hooks
1307          *
1308          * @param $action String the action to check
1309          * @param $user User user to check
1310          * @param $errors Array list of current errors
1311          * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
1312          * @param $short Boolean short circuit on first error
1313          *
1314          * @return Array list of errors
1315          */
1316         private function checkPermissionHooks( $action, $user, $errors, $doExpensiveQueries, $short ) {
1317                 // Use getUserPermissionsErrors instead
1318                 $result = '';
1319                 if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) {
1320                         return $result ? array() : array( array( 'badaccess-group0' ) );
1321                 }
1322                 // Check getUserPermissionsErrors hook
1323                 if ( !wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) {
1324                         $errors = $this->resultToError( $errors, $result );
1325                 }
1326                 // Check getUserPermissionsErrorsExpensive hook
1327                 if ( $doExpensiveQueries && !( $short && count( $errors ) > 0 ) &&
1328                          !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) ) {
1329                         $errors = $this->resultToError( $errors, $result );
1330                 }
1331
1332                 return $errors;
1333         }
1334
1335         /**
1336          * Check permissions on special pages & namespaces
1337          *
1338          * @param $action String the action to check
1339          * @param $user User user to check
1340          * @param $errors Array list of current errors
1341          * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
1342          * @param $short Boolean short circuit on first error
1343          *
1344          * @return Array list of errors
1345          */
1346         private function checkSpecialsAndNSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
1347                 # Only 'createaccount' and 'execute' can be performed on
1348                 # special pages, which don't actually exist in the DB.
1349                 $specialOKActions = array( 'createaccount', 'execute' );
1350                 if ( NS_SPECIAL == $this->mNamespace && !in_array( $action, $specialOKActions ) ) {
1351                         $errors[] = array( 'ns-specialprotected' );
1352                 }
1353
1354                 # Check $wgNamespaceProtection for restricted namespaces
1355                 if ( $this->isNamespaceProtected() ) {
1356                         $ns = $this->mNamespace == NS_MAIN ?
1357                                 wfMsg( 'nstab-main' ) : $this->getNsText();
1358                         $errors[] = $this->mNamespace == NS_MEDIAWIKI ?
1359                                 array( 'protectedinterface' ) : array( 'namespaceprotected',  $ns );
1360                 }
1361
1362                 return $errors;
1363         }
1364
1365         /**
1366          * Check CSS/JS sub-page permissions
1367          *
1368          * @param $action String the action to check
1369          * @param $user User user to check
1370          * @param $errors Array list of current errors
1371          * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
1372          * @param $short Boolean short circuit on first error
1373          *
1374          * @return Array list of errors
1375          */
1376         private function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
1377                 # Protect css/js subpages of user pages
1378                 # XXX: this might be better using restrictions
1379                 # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssSubpage()
1380                 #      and $this->userCanEditJsSubpage() from working
1381                 # XXX: right 'editusercssjs' is deprecated, for backward compatibility only
1382                 if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' )
1383                                 && !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
1384                         if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) {
1385                                 $errors[] = array( 'customcssjsprotected' );
1386                         } else if ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
1387                                 $errors[] = array( 'customcssjsprotected' );
1388                         }
1389                 }
1390
1391                 return $errors;
1392         }
1393
1394         /**
1395          * Check against page_restrictions table requirements on this
1396          * page. The user must possess all required rights for this
1397          * action.
1398          *
1399          * @param $action String the action to check
1400          * @param $user User user to check
1401          * @param $errors Array list of current errors
1402          * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
1403          * @param $short Boolean short circuit on first error
1404          *
1405          * @return Array list of errors
1406          */
1407         private function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) {
1408                 foreach ( $this->getRestrictions( $action ) as $right ) {
1409                         // Backwards compatibility, rewrite sysop -> protect
1410                         if ( $right == 'sysop' ) {
1411                                 $right = 'protect';
1412                         }
1413                         if ( $right != '' && !$user->isAllowed( $right ) ) {
1414                                 // Users with 'editprotected' permission can edit protected pages
1415                                 if ( $action == 'edit' && $user->isAllowed( 'editprotected' ) ) {
1416                                         // Users with 'editprotected' permission cannot edit protected pages
1417                                         // with cascading option turned on.
1418                                         if ( $this->mCascadeRestriction ) {
1419                                                 $errors[] = array( 'protectedpagetext', $right );
1420                                         }
1421                                 } else {
1422                                         $errors[] = array( 'protectedpagetext', $right );
1423                                 }
1424                         }
1425                 }
1426
1427                 return $errors;
1428         }
1429
1430         /**
1431          * Check restrictions on cascading pages.
1432          * 
1433          * @param $action String the action to check
1434          * @param $user User user to check
1435          * @param $errors Array list of current errors
1436          * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
1437          * @param $short Boolean short circuit on first error
1438          *
1439          * @return Array list of errors
1440          */
1441         private function checkCascadingSourcesRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) {
1442                 if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) {
1443                         # We /could/ use the protection level on the source page, but it's
1444                         # fairly ugly as we have to establish a precedence hierarchy for pages
1445                         # included by multiple cascade-protected pages. So just restrict
1446                         # it to people with 'protect' permission, as they could remove the
1447                         # protection anyway.
1448                         list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
1449                         # Cascading protection depends on more than this page...
1450                         # Several cascading protected pages may include this page...
1451                         # Check each cascading level
1452                         # This is only for protection restrictions, not for all actions
1453                         if ( isset( $restrictions[$action] ) ) {
1454                                 foreach ( $restrictions[$action] as $right ) {
1455                                         $right = ( $right == 'sysop' ) ? 'protect' : $right;
1456                                         if ( $right != '' && !$user->isAllowed( $right ) ) {
1457                                                 $pages = '';
1458                                                 foreach ( $cascadingSources as $page )
1459                                                         $pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
1460                                                 $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages );
1461                                         }
1462                                 }
1463                         }
1464                 }
1465
1466                 return $errors;
1467         }
1468
1469         /**
1470          * Check action permissions not already checked in checkQuickPermissions
1471          *
1472          * @param $action String the action to check
1473          * @param $user User user to check
1474          * @param $errors Array list of current errors
1475          * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
1476          * @param $short Boolean short circuit on first error
1477          *
1478          * @return Array list of errors
1479          */
1480         private function checkActionPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
1481                 if ( $action == 'protect' ) {
1482                         if ( $this->getUserPermissionsErrors( 'edit', $user ) != array() ) {
1483                                 // If they can't edit, they shouldn't protect.
1484                                 $errors[] = array( 'protect-cantedit' );
1485                         }
1486                 } elseif ( $action == 'create' ) {
1487                         $title_protection = $this->getTitleProtection();
1488                         if( $title_protection ) {
1489                                 if( $title_protection['pt_create_perm'] == 'sysop' ) {
1490                                         $title_protection['pt_create_perm'] = 'protect'; // B/C
1491                                 }
1492                                 if( $title_protection['pt_create_perm'] == '' || !$user->isAllowed( $title_protection['pt_create_perm'] ) ) {
1493                                         $errors[] = array( 'titleprotected', User::whoIs( $title_protection['pt_user'] ), $title_protection['pt_reason'] );
1494                                 }
1495                         }
1496                 } elseif ( $action == 'move' ) {
1497                         // Check for immobile pages
1498                         if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
1499                                 // Specific message for this case
1500                                 $errors[] = array( 'immobile-source-namespace', $this->getNsText() );
1501                         } elseif ( !$this->isMovable() ) {
1502                                 // Less specific message for rarer cases
1503                                 $errors[] = array( 'immobile-page' );
1504                         }
1505                 } elseif ( $action == 'move-target' ) {
1506                         if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
1507                                 $errors[] = array( 'immobile-target-namespace', $this->getNsText() );
1508                         } elseif ( !$this->isMovable() ) {
1509                                 $errors[] = array( 'immobile-target-page' );
1510                         }
1511                 }
1512                 return $errors;
1513         }
1514
1515         /**
1516          * Check that the user isn't blocked from editting.
1517          *
1518          * @param $action String the action to check
1519          * @param $user User user to check
1520          * @param $errors Array list of current errors
1521          * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
1522          * @param $short Boolean short circuit on first error
1523          *
1524          * @return Array list of errors
1525          */
1526         private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) {
1527                 if( $short && count( $errors ) > 0 ) {
1528                         return $errors;
1529                 }
1530
1531                 global $wgContLang, $wgLang, $wgEmailConfirmToEdit;
1532
1533                 if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount' ) {
1534                         $errors[] = array( 'confirmedittext' );
1535                 }
1536
1537                 // Edit blocks should not affect reading. Account creation blocks handled at userlogin.
1538                 if ( $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this ) ) {
1539                         $block = $user->mBlock;
1540
1541                         // This is from OutputPage::blockedPage
1542                         // Copied at r23888 by werdna
1543
1544                         $id = $user->blockedBy();
1545                         $reason = $user->blockedFor();
1546                         if ( $reason == '' ) {
1547                                 $reason = wfMsg( 'blockednoreason' );
1548                         }
1549                         $ip = wfGetIP();
1550
1551                         if ( is_numeric( $id ) ) {
1552                                 $name = User::whoIs( $id );
1553                         } else {
1554                                 $name = $id;
1555                         }
1556
1557                         $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]";
1558                         $blockid = $block->mId;
1559                         $blockExpiry = $user->mBlock->mExpiry;
1560                         $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $user->mBlock->mTimestamp ), true );
1561                         if ( $blockExpiry == 'infinity' ) {
1562                                 // Entry in database (table ipblocks) is 'infinity' but 'ipboptions' uses 'infinite' or 'indefinite'
1563                                 $scBlockExpiryOptions = wfMsg( 'ipboptions' );
1564
1565                                 foreach ( explode( ',', $scBlockExpiryOptions ) as $option ) {
1566                                         if ( !strpos( $option, ':' ) )
1567                                                 continue;
1568
1569                                         list( $show, $value ) = explode( ':', $option );
1570
1571                                         if ( $value == 'infinite' || $value == 'indefinite' ) {
1572                                                 $blockExpiry = $show;
1573                                                 break;
1574                                         }
1575                                 }
1576                         } else {
1577                                 $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true );
1578                         }
1579
1580                         $intended = $user->mBlock->mAddress;
1581
1582                         $errors[] = array( ( $block->mAuto ? 'autoblockedtext' : 'blockedtext' ), $link, $reason, $ip, $name,
1583                                 $blockid, $blockExpiry, $intended, $blockTimestamp );
1584                 }
1585
1586                 return $errors;
1587         }
1588
1589         /**
1590          * Can $user perform $action on this page? This is an internal function,
1591          * which checks ONLY that previously checked by userCan (i.e. it leaves out
1592          * checks on wfReadOnly() and blocks)
1593          *
1594          * @param $action \type{\string} action that permission needs to be checked for
1595          * @param $user \type{User} user to check
1596          * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
1597          * @param $short \type{\bool} Set this to true to stop after the first permission error.
1598          * @return \type{\array} Array of arrays of the arguments to wfMsg to explain permissions problems.
1599          */
1600         protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) {
1601                 wfProfileIn( __METHOD__ );
1602
1603                 $errors = array();
1604                 $checks = array(
1605                         'checkQuickPermissions',
1606                         'checkPermissionHooks',
1607                         'checkSpecialsAndNSPermissions',
1608                         'checkCSSandJSPermissions',
1609                         'checkPageRestrictions',
1610                         'checkCascadingSourcesRestrictions',
1611                         'checkActionPermissions',
1612                         'checkUserBlock'
1613                 );
1614
1615                 while( count( $checks ) > 0 &&
1616                            !( $short && count( $errors ) > 0 ) ) {
1617                         $method = array_shift( $checks );
1618                         $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short );
1619                 }
1620
1621                 wfProfileOut( __METHOD__ );
1622                 return $errors;
1623         }
1624
1625         /**
1626          * Is this title subject to title protection?
1627          * Title protection is the one applied against creation of such title.
1628          *
1629          * @return \type{\mixed} An associative array representing any existent title
1630          *   protection, or false if there's none.
1631          */
1632         private function getTitleProtection() {
1633                 // Can't protect pages in special namespaces
1634                 if ( $this->getNamespace() < 0 ) {
1635                         return false;
1636                 }
1637
1638                 // Can't protect pages that exist.
1639                 if ( $this->exists() ) {
1640                         return false;
1641                 }
1642
1643                 if ( !isset( $this->mTitleProtection ) ) {
1644                         $dbr = wfGetDB( DB_SLAVE );
1645                         $res = $dbr->select( 'protected_titles', '*',
1646                                 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
1647                                 __METHOD__ );
1648
1649                         // fetchRow returns false if there are no rows.
1650                         $this->mTitleProtection = $dbr->fetchRow( $res );
1651                 }
1652                 return $this->mTitleProtection;
1653         }
1654
1655         /**
1656          * Update the title protection status
1657          *
1658          * @param $create_perm \type{\string} Permission required for creation
1659          * @param $reason \type{\string} Reason for protection
1660          * @param $expiry \type{\string} Expiry timestamp
1661          * @return boolean true
1662          */
1663         public function updateTitleProtection( $create_perm, $reason, $expiry ) {
1664                 global $wgUser, $wgContLang;
1665
1666                 if ( $create_perm == implode( ',', $this->getRestrictions( 'create' ) )
1667                         && $expiry == $this->mRestrictionsExpiry['create'] ) {
1668                         // No change
1669                         return true;
1670                 }
1671
1672                 list ( $namespace, $title ) = array( $this->getNamespace(), $this->getDBkey() );
1673
1674                 $dbw = wfGetDB( DB_MASTER );
1675
1676                 $encodedExpiry = Block::encodeExpiry( $expiry, $dbw );
1677
1678                 $expiry_description = '';
1679                 if ( $encodedExpiry != 'infinity' ) {
1680                         $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ),
1681                                 $wgContLang->date( $expiry ) , $wgContLang->time( $expiry ) ) . ')';
1682                 } else {
1683                         $expiry_description .= ' (' . wfMsgForContent( 'protect-expiry-indefinite' ) . ')';
1684                 }
1685
1686                 # Update protection table
1687                 if ( $create_perm != '' ) {
1688                         $this->mTitleProtection = array(
1689                                         'pt_namespace' => $namespace,
1690                                         'pt_title' => $title,
1691                                         'pt_create_perm' => $create_perm,
1692                                         'pt_timestamp' => Block::encodeExpiry( wfTimestampNow(), $dbw ),
1693                                         'pt_expiry' => $encodedExpiry,
1694                                         'pt_user' => $wgUser->getId(),
1695                                         'pt_reason' => $reason,
1696                                 );
1697                         $dbw->replace( 'protected_titles', array( array( 'pt_namespace', 'pt_title' ) ),
1698                                 $this->mTitleProtection, __METHOD__     );
1699                 } else {
1700                         $dbw->delete( 'protected_titles', array( 'pt_namespace' => $namespace,
1701                                 'pt_title' => $title ), __METHOD__ );
1702                         $this->mTitleProtection = false;
1703                 }
1704
1705                 # Update the protection log
1706                 if ( $dbw->affectedRows() ) {
1707                         $log = new LogPage( 'protect' );
1708
1709                         if ( $create_perm ) {
1710                                 $params = array( "[create=$create_perm] $expiry_description", '' );
1711                                 $log->addEntry( ( isset( $this->mRestrictions['create'] ) && $this->mRestrictions['create'] ) ? 'modify' : 'protect', $this, trim( $reason ), $params );
1712                         } else {
1713                                 $log->addEntry( 'unprotect', $this, $reason );
1714                         }
1715                 }
1716
1717                 return true;
1718         }
1719
1720         /**
1721          * Remove any title protection due to page existing
1722          */
1723         public function deleteTitleProtection() {
1724                 $dbw = wfGetDB( DB_MASTER );
1725
1726                 $dbw->delete(
1727                         'protected_titles',
1728                         array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
1729                         __METHOD__
1730                 );
1731                 $this->mTitleProtection = false;
1732         }
1733
1734         /**
1735          * Would anybody with sufficient privileges be able to move this page?
1736          * Some pages just aren't movable.
1737          *
1738          * @return \type{\bool} TRUE or FALSE
1739          */
1740         public function isMovable() {
1741                 return MWNamespace::isMovable( $this->getNamespace() ) && $this->getInterwiki() == '';
1742         }
1743
1744         /**
1745          * Can $wgUser read this page?
1746          *
1747          * @return \type{\bool}
1748          * @todo fold these checks into userCan()
1749          */
1750         public function userCanRead() {
1751                 global $wgUser, $wgGroupPermissions;
1752
1753                 static $useShortcut = null;
1754
1755                 # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below
1756                 if ( is_null( $useShortcut ) ) {
1757                         global $wgRevokePermissions;
1758                         $useShortcut = true;
1759                         if ( empty( $wgGroupPermissions['*']['read'] ) ) {
1760                                 # Not a public wiki, so no shortcut
1761                                 $useShortcut = false;
1762                         } elseif ( !empty( $wgRevokePermissions ) ) {
1763                                 /*
1764                                  * Iterate through each group with permissions being revoked (key not included since we don't care
1765                                  * what the group name is), then check if the read permission is being revoked. If it is, then
1766                                  * we don't use the shortcut below since the user might not be able to read, even though anon
1767                                  * reading is allowed.
1768                                  */
1769                                 foreach ( $wgRevokePermissions as $perms ) {
1770                                         if ( !empty( $perms['read'] ) ) {
1771                                                 # We might be removing the read right from the user, so no shortcut
1772                                                 $useShortcut = false;
1773                                                 break;
1774                                         }
1775                                 }
1776                         }
1777                 }
1778
1779                 $result = null;
1780                 wfRunHooks( 'userCan', array( &$this, &$wgUser, 'read', &$result ) );
1781                 if ( $result !== null ) {
1782                         return $result;
1783                 }
1784
1785                 # Shortcut for public wikis, allows skipping quite a bit of code
1786                 if ( $useShortcut ) {
1787                         return true;
1788                 }
1789
1790                 if ( $wgUser->isAllowed( 'read' ) ) {
1791                         return true;
1792                 } else {
1793                         global $wgWhitelistRead;
1794
1795                         /**
1796                          * Always grant access to the login page.
1797                          * Even anons need to be able to log in.
1798                         */
1799                         if ( $this->isSpecial( 'Userlogin' ) || $this->isSpecial( 'Resetpass' ) ) {
1800                                 return true;
1801                         }
1802
1803                         /**
1804                          * Bail out if there isn't whitelist
1805                          */
1806                         if ( !is_array( $wgWhitelistRead ) ) {
1807                                 return false;
1808                         }
1809
1810                         /**
1811                          * Check for explicit whitelisting
1812                          */
1813                         $name = $this->getPrefixedText();
1814                         $dbName = $this->getPrefixedDBKey();
1815                         // Check with and without underscores
1816                         if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) )
1817                                 return true;
1818
1819                         /**
1820                          * Old settings might have the title prefixed with
1821                          * a colon for main-namespace pages
1822                          */
1823                         if ( $this->getNamespace() == NS_MAIN ) {
1824                                 if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
1825                                         return true;
1826                                 }
1827                         }
1828
1829                         /**
1830                          * If it's a special page, ditch the subpage bit
1831                          * and check again
1832                          */
1833                         if ( $this->getNamespace() == NS_SPECIAL ) {
1834                                 $name = $this->getDBkey();
1835                                 list( $name, /* $subpage */ ) = SpecialPage::resolveAliasWithSubpage( $name );
1836                                 if ( $name === false ) {
1837                                         # Invalid special page, but we show standard login required message
1838                                         return false;
1839                                 }
1840
1841                                 $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
1842                                 if ( in_array( $pure, $wgWhitelistRead, true ) ) {
1843                                         return true;
1844                                 }
1845                         }
1846
1847                 }
1848                 return false;
1849         }
1850
1851         /**
1852          * Is this a talk page of some sort?
1853          *
1854          * @return \type{\bool}
1855          */
1856         public function isTalkPage() {
1857                 return MWNamespace::isTalk( $this->getNamespace() );
1858         }
1859
1860         /**
1861          * Is this a subpage?
1862          *
1863          * @return \type{\bool}
1864          */
1865         public function isSubpage() {
1866                 return MWNamespace::hasSubpages( $this->mNamespace )
1867                         ? strpos( $this->getText(), '/' ) !== false
1868                         : false;
1869         }
1870
1871         /**
1872          * Does this have subpages?  (Warning, usually requires an extra DB query.)
1873          *
1874          * @return \type{\bool}
1875          */
1876         public function hasSubpages() {
1877                 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1878                         # Duh
1879                         return false;
1880                 }
1881
1882                 # We dynamically add a member variable for the purpose of this method
1883                 # alone to cache the result.  There's no point in having it hanging
1884                 # around uninitialized in every Title object; therefore we only add it
1885                 # if needed and don't declare it statically.
1886                 if ( isset( $this->mHasSubpages ) ) {
1887                         return $this->mHasSubpages;
1888                 }
1889
1890                 $subpages = $this->getSubpages( 1 );
1891                 if ( $subpages instanceof TitleArray ) {
1892                         return $this->mHasSubpages = (bool)$subpages->count();
1893                 }
1894                 return $this->mHasSubpages = false;
1895         }
1896
1897         /**
1898          * Get all subpages of this page.
1899          *
1900          * @param $limit Maximum number of subpages to fetch; -1 for no limit
1901          * @return mixed TitleArray, or empty array if this page's namespace
1902          *  doesn't allow subpages
1903          */
1904         public function getSubpages( $limit = -1 ) {
1905                 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
1906                         return array();
1907                 }
1908
1909                 $dbr = wfGetDB( DB_SLAVE );
1910                 $conds['page_namespace'] = $this->getNamespace();
1911                 $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
1912                 $options = array();
1913                 if ( $limit > -1 ) {
1914                         $options['LIMIT'] = $limit;
1915                 }
1916                 return $this->mSubpages = TitleArray::newFromResult(
1917                         $dbr->select( 'page',
1918                                 array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ),
1919                                 $conds,
1920                                 __METHOD__,
1921                                 $options
1922                         )
1923                 );
1924         }
1925
1926         /**
1927          * Could this page contain custom CSS or JavaScript, based
1928          * on the title?
1929          *
1930          * @return \type{\bool}
1931          */
1932         public function isCssOrJsPage() {
1933                 return $this->mNamespace == NS_MEDIAWIKI
1934                         && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
1935         }
1936
1937         /**
1938          * Is this a .css or .js subpage of a user page?
1939          * @return \type{\bool}
1940          */
1941         public function isCssJsSubpage() {
1942                 return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
1943         }
1944
1945         /**
1946          * Is this a *valid* .css or .js subpage of a user page?
1947          *
1948          * @return \type{\bool}
1949          * @deprecated
1950          */
1951         public function isValidCssJsSubpage() {
1952                 return $this->isCssJsSubpage();
1953         }
1954
1955         /**
1956          * Trim down a .css or .js subpage title to get the corresponding skin name
1957          *
1958          * @return string containing skin name from .css or .js subpage title
1959          */
1960         public function getSkinFromCssJsSubpage() {
1961                 $subpage = explode( '/', $this->mTextform );
1962                 $subpage = $subpage[ count( $subpage ) - 1 ];
1963                 return( str_replace( array( '.css', '.js' ), array( '', '' ), $subpage ) );
1964         }
1965
1966         /**
1967          * Is this a .css subpage of a user page?
1968          *
1969          * @return \type{\bool}
1970          */
1971         public function isCssSubpage() {
1972                 return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
1973         }
1974
1975         /**
1976          * Is this a .js subpage of a user page?
1977          *
1978          * @return \type{\bool}
1979          */
1980         public function isJsSubpage() {
1981                 return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
1982         }
1983
1984         /**
1985          * Protect css subpages of user pages: can $wgUser edit
1986          * this page?
1987          *
1988          * @return \type{\bool}
1989          * @todo XXX: this might be better using restrictions
1990          */
1991         public function userCanEditCssSubpage() {
1992                 global $wgUser;
1993                 return ( ( $wgUser->isAllowed( 'editusercssjs' ) && $wgUser->isAllowed( 'editusercss' ) )
1994                         || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
1995         }
1996
1997         /**
1998          * Protect js subpages of user pages: can $wgUser edit
1999          * this page?
2000          *
2001          * @return \type{\bool}
2002          * @todo XXX: this might be better using restrictions
2003          */
2004         public function userCanEditJsSubpage() {
2005                 global $wgUser;
2006                 return ( ( $wgUser->isAllowed( 'editusercssjs' ) && $wgUser->isAllowed( 'edituserjs' ) )
2007                        || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
2008         }
2009
2010         /**
2011          * Cascading protection: Return true if cascading restrictions apply to this page, false if not.
2012          *
2013          * @return \type{\bool} If the page is subject to cascading restrictions.
2014          */
2015         public function isCascadeProtected() {
2016                 list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
2017                 return ( $sources > 0 );
2018         }
2019
2020         /**
2021          * Cascading protection: Get the source of any cascading restrictions on this page.
2022          *
2023          * @param $getPages \type{\bool} Whether or not to retrieve the actual pages
2024          *        that the restrictions have come from.
2025          * @return \type{\arrayof{mixed title array, restriction array}} Array of the Title
2026          *         objects of the pages from which cascading restrictions have come,
2027          *         false for none, or true if such restrictions exist, but $getPages was not set.
2028          *         The restriction array is an array of each type, each of which contains a
2029          *         array of unique groups.
2030          */
2031         public function getCascadeProtectionSources( $getPages = true ) {
2032                 $pagerestrictions = array();
2033
2034                 if ( isset( $this->mCascadeSources ) && $getPages ) {
2035                         return array( $this->mCascadeSources, $this->mCascadingRestrictions );
2036                 } else if ( isset( $this->mHasCascadingRestrictions ) && !$getPages ) {
2037                         return array( $this->mHasCascadingRestrictions, $pagerestrictions );
2038                 }
2039
2040                 wfProfileIn( __METHOD__ );
2041
2042                 $dbr = wfGetDB( DB_SLAVE );
2043
2044                 if ( $this->getNamespace() == NS_FILE ) {
2045                         $tables = array( 'imagelinks', 'page_restrictions' );
2046                         $where_clauses = array(
2047                                 'il_to' => $this->getDBkey(),
2048                                 'il_from=pr_page',
2049                                 'pr_cascade' => 1
2050                         );
2051                 } else {
2052                         $tables = array( 'templatelinks', 'page_restrictions' );
2053                         $where_clauses = array(
2054                                 'tl_namespace' => $this->getNamespace(),
2055                                 'tl_title' => $this->getDBkey(),
2056                                 'tl_from=pr_page',
2057                                 'pr_cascade' => 1
2058                         );
2059                 }
2060
2061                 if ( $getPages ) {
2062                         $cols = array( 'pr_page', 'page_namespace', 'page_title',
2063                                                    'pr_expiry', 'pr_type', 'pr_level' );
2064                         $where_clauses[] = 'page_id=pr_page';
2065                         $tables[] = 'page';
2066                 } else {
2067                         $cols = array( 'pr_expiry' );
2068                 }
2069
2070                 $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
2071
2072                 $sources = $getPages ? array() : false;
2073                 $now = wfTimestampNow();
2074                 $purgeExpired = false;
2075
2076                 foreach ( $res as $row ) {
2077                         $expiry = Block::decodeExpiry( $row->pr_expiry );
2078                         if ( $expiry > $now ) {
2079                                 if ( $getPages ) {
2080                                         $page_id = $row->pr_page;
2081                                         $page_ns = $row->page_namespace;
2082                                         $page_title = $row->page_title;
2083                                         $sources[$page_id] = Title::makeTitle( $page_ns, $page_title );
2084                                         # Add groups needed for each restriction type if its not already there
2085                                         # Make sure this restriction type still exists
2086
2087                                         if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
2088                                                 $pagerestrictions[$row->pr_type] = array();
2089                                         }
2090
2091                                         if ( isset( $pagerestrictions[$row->pr_type] ) &&
2092                                                  !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] ) ) {
2093                                                 $pagerestrictions[$row->pr_type][] = $row->pr_level;
2094                                         }
2095                                 } else {
2096                                         $sources = true;
2097                                 }
2098                         } else {
2099                                 // Trigger lazy purge of expired restrictions from the db
2100                                 $purgeExpired = true;
2101                         }
2102                 }
2103                 if ( $purgeExpired ) {
2104                         Title::purgeExpiredRestrictions();
2105                 }
2106
2107                 if ( $getPages ) {
2108                         $this->mCascadeSources = $sources;
2109                         $this->mCascadingRestrictions = $pagerestrictions;
2110                 } else {
2111                         $this->mHasCascadingRestrictions = $sources;
2112                 }
2113
2114                 wfProfileOut( __METHOD__ );
2115                 return array( $sources, $pagerestrictions );
2116         }
2117
2118         /**
2119          * Returns cascading restrictions for the current article
2120          *
2121          * @return Boolean
2122          */
2123         function areRestrictionsCascading() {
2124                 if ( !$this->mRestrictionsLoaded ) {
2125                         $this->loadRestrictions();
2126                 }
2127
2128                 return $this->mCascadeRestriction;
2129         }
2130
2131         /**
2132          * Loads a string into mRestrictions array
2133          *
2134          * @param $res \type{Resource} restrictions as an SQL result.
2135          * @param $oldFashionedRestrictions string comma-separated list of page
2136          *        restrictions from page table (pre 1.10)
2137          */
2138         private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) {
2139                 $rows = array();
2140
2141                 foreach ( $res as $row ) {
2142                         $rows[] = $row;
2143                 }
2144
2145                 $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
2146         }
2147
2148         /**
2149          * Compiles list of active page restrictions from both page table (pre 1.10)
2150          * and page_restrictions table for this existing page.
2151          * Public for usage by LiquidThreads.
2152          *
2153          * @param $rows array of db result objects
2154          * @param $oldFashionedRestrictions string comma-separated list of page
2155          *        restrictions from page table (pre 1.10)
2156          */
2157         public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
2158                 $dbr = wfGetDB( DB_SLAVE );
2159
2160                 $restrictionTypes = $this->getRestrictionTypes();
2161
2162                 foreach ( $restrictionTypes as $type ) {
2163                         $this->mRestrictions[$type] = array();
2164                         $this->mRestrictionsExpiry[$type] = Block::decodeExpiry( '' );
2165                 }
2166
2167                 $this->mCascadeRestriction = false;
2168
2169                 # Backwards-compatibility: also load the restrictions from the page record (old format).
2170
2171                 if ( $oldFashionedRestrictions === null ) {
2172                         $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions',
2173                                 array( 'page_id' => $this->getArticleId() ), __METHOD__ );
2174                 }
2175
2176                 if ( $oldFashionedRestrictions != '' ) {
2177
2178                         foreach ( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) {
2179                                 $temp = explode( '=', trim( $restrict ) );
2180                                 if ( count( $temp ) == 1 ) {
2181                                         // old old format should be treated as edit/move restriction
2182                                         $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
2183                                         $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
2184                                 } else {
2185                                         $this->mRestrictions[$temp[0]] = explode( ',', trim( $temp[1] ) );
2186                                 }
2187                         }
2188
2189                         $this->mOldRestrictions = true;
2190
2191                 }
2192
2193                 if ( count( $rows ) ) {
2194                         # Current system - load second to make them override.
2195                         $now = wfTimestampNow();
2196                         $purgeExpired = false;
2197
2198                         foreach ( $rows as $row ) {
2199                                 # Cycle through all the restrictions.
2200
2201                                 // Don't take care of restrictions types that aren't allowed
2202
2203                                 if ( !in_array( $row->pr_type, $restrictionTypes ) )
2204                                         continue;
2205
2206                                 // This code should be refactored, now that it's being used more generally,
2207                                 // But I don't really see any harm in leaving it in Block for now -werdna
2208                                 $expiry = Block::decodeExpiry( $row->pr_expiry );
2209
2210                                 // Only apply the restrictions if they haven't expired!
2211                                 if ( !$expiry || $expiry > $now ) {
2212                                         $this->mRestrictionsExpiry[$row->pr_type] = $expiry;
2213                                         $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
2214
2215                                         $this->mCascadeRestriction |= $row->pr_cascade;
2216                                 } else {
2217                                         // Trigger a lazy purge of expired restrictions
2218                                         $purgeExpired = true;
2219                                 }
2220                         }
2221
2222                         if ( $purgeExpired ) {
2223                                 Title::purgeExpiredRestrictions();
2224                         }
2225                 }
2226
2227                 $this->mRestrictionsLoaded = true;
2228         }
2229
2230         /**
2231          * Load restrictions from the page_restrictions table
2232          *
2233          * @param $oldFashionedRestrictions string comma-separated list of page
2234          *        restrictions from page table (pre 1.10)
2235          */
2236         public function loadRestrictions( $oldFashionedRestrictions = null ) {
2237                 if ( !$this->mRestrictionsLoaded ) {
2238                         if ( $this->exists() ) {
2239                                 $dbr = wfGetDB( DB_SLAVE );
2240
2241                                 $res = $dbr->select( 'page_restrictions', '*',
2242                                         array( 'pr_page' => $this->getArticleId() ), __METHOD__ );
2243
2244                                 $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions );
2245                         } else {
2246                                 $title_protection = $this->getTitleProtection();
2247
2248                                 if ( $title_protection ) {
2249                                         $now = wfTimestampNow();
2250                                         $expiry = Block::decodeExpiry( $title_protection['pt_expiry'] );
2251
2252                                         if ( !$expiry || $expiry > $now ) {
2253                                                 // Apply the restrictions
2254                                                 $this->mRestrictionsExpiry['create'] = $expiry;
2255                                                 $this->mRestrictions['create'] = explode( ',', trim( $title_protection['pt_create_perm'] ) );
2256                                         } else { // Get rid of the old restrictions
2257                                                 Title::purgeExpiredRestrictions();
2258                                                 $this->mTitleProtection = false;
2259                                         }
2260                                 } else {
2261                                         $this->mRestrictionsExpiry['create'] = Block::decodeExpiry( '' );
2262                                 }
2263                                 $this->mRestrictionsLoaded = true;
2264                         }
2265                 }
2266         }
2267
2268         /**
2269          * Purge expired restrictions from the page_restrictions table
2270          */
2271         static function purgeExpiredRestrictions() {
2272                 $dbw = wfGetDB( DB_MASTER );
2273                 $dbw->delete(
2274                         'page_restrictions',
2275                         array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
2276                         __METHOD__
2277                 );
2278
2279                 $dbw->delete(
2280                         'protected_titles',
2281                         array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
2282                         __METHOD__
2283                 );
2284         }
2285
2286         /**
2287          * Accessor/initialisation for mRestrictions
2288          *
2289          * @param $action \type{\string} action that permission needs to be checked for
2290          * @return \type{\arrayof{\string}} the array of groups allowed to edit this article
2291          */
2292         public function getRestrictions( $action ) {
2293                 if ( !$this->mRestrictionsLoaded ) {
2294                         $this->loadRestrictions();
2295                 }
2296                 return isset( $this->mRestrictions[$action] )
2297                                 ? $this->mRestrictions[$action]
2298                                 : array();
2299         }
2300
2301         /**
2302          * Get the expiry time for the restriction against a given action
2303          *
2304          * @return 14-char timestamp, or 'infinity' if the page is protected forever
2305          *      or not protected at all, or false if the action is not recognised.
2306          */
2307         public function getRestrictionExpiry( $action ) {
2308                 if ( !$this->mRestrictionsLoaded ) {
2309                         $this->loadRestrictions();
2310                 }
2311                 return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
2312         }
2313
2314         /**
2315          * Is there a version of this page in the deletion archive?
2316          *
2317          * @return \type{\int} the number of archived revisions
2318          */
2319         public function isDeleted() {
2320                 if ( $this->getNamespace() < 0 ) {
2321                         $n = 0;
2322                 } else {
2323                         $dbr = wfGetDB( DB_SLAVE );
2324                         $n = $dbr->selectField( 'archive', 'COUNT(*)',
2325                                 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ),
2326                                 __METHOD__
2327                         );
2328                         if ( $this->getNamespace() == NS_FILE ) {
2329                                 $n += $dbr->selectField( 'filearchive', 'COUNT(*)',
2330                                         array( 'fa_name' => $this->getDBkey() ),
2331                                         __METHOD__
2332                                 );
2333                         }
2334                 }
2335                 return (int)$n;
2336         }
2337
2338         /**
2339          * Is there a version of this page in the deletion archive?
2340          *
2341          * @return Boolean
2342          */
2343         public function isDeletedQuick() {
2344                 if ( $this->getNamespace() < 0 ) {
2345                         return false;
2346                 }
2347                 $dbr = wfGetDB( DB_SLAVE );
2348                 $deleted = (bool)$dbr->selectField( 'archive', '1',
2349                         array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ),
2350                         __METHOD__
2351                 );
2352                 if ( !$deleted && $this->getNamespace() == NS_FILE ) {
2353                         $deleted = (bool)$dbr->selectField( 'filearchive', '1',
2354                                 array( 'fa_name' => $this->getDBkey() ),
2355                                 __METHOD__
2356                         );
2357                 }
2358                 return $deleted;
2359         }
2360
2361         /**
2362          * Get the article ID for this Title from the link cache,
2363          * adding it if necessary
2364          *
2365          * @param $flags \type{\int} a bit field; may be Title::GAID_FOR_UPDATE to select
2366          *  for update
2367          * @return \type{\int} the ID
2368          */
2369         public function getArticleID( $flags = 0 ) {
2370                 if ( $this->getNamespace() < 0 ) {
2371                         return $this->mArticleID = 0;
2372                 }
2373                 $linkCache = LinkCache::singleton();
2374                 if ( $flags & self::GAID_FOR_UPDATE ) {
2375                         $oldUpdate = $linkCache->forUpdate( true );
2376                         $linkCache->clearLink( $this );
2377                         $this->mArticleID = $linkCache->addLinkObj( $this );
2378                         $linkCache->forUpdate( $oldUpdate );
2379                 } else {
2380                         if ( -1 == $this->mArticleID ) {
2381                                 $this->mArticleID = $linkCache->addLinkObj( $this );
2382                         }
2383                 }
2384                 return $this->mArticleID;
2385         }
2386
2387         /**
2388          * Is this an article that is a redirect page?
2389          * Uses link cache, adding it if necessary
2390          *
2391          * @param $flags \type{\int} a bit field; may be Title::GAID_FOR_UPDATE to select for update
2392          * @return \type{\bool}
2393          */
2394         public function isRedirect( $flags = 0 ) {
2395                 if ( !is_null( $this->mRedirect ) ) {
2396                         return $this->mRedirect;
2397                 }
2398                 # Calling getArticleID() loads the field from cache as needed
2399                 if ( !$this->getArticleID( $flags ) ) {
2400                         return $this->mRedirect = false;
2401                 }
2402                 $linkCache = LinkCache::singleton();
2403                 $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
2404
2405                 return $this->mRedirect;
2406         }
2407
2408         /**
2409          * What is the length of this page?
2410          * Uses link cache, adding it if necessary
2411          *
2412          * @param $flags \type{\int} a bit field; may be Title::GAID_FOR_UPDATE to select for update
2413          * @return \type{\bool}
2414          */
2415         public function getLength( $flags = 0 ) {
2416                 if ( $this->mLength != -1 ) {
2417                         return $this->mLength;
2418                 }
2419                 # Calling getArticleID() loads the field from cache as needed
2420                 if ( !$this->getArticleID( $flags ) ) {
2421                         return $this->mLength = 0;
2422                 }
2423                 $linkCache = LinkCache::singleton();
2424                 $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) );
2425
2426                 return $this->mLength;
2427         }
2428
2429         /**
2430          * What is the page_latest field for this page?
2431          *
2432          * @param $flags \type{\int} a bit field; may be Title::GAID_FOR_UPDATE to select for update
2433          * @return \type{\int} or 0 if the page doesn't exist
2434          */
2435         public function getLatestRevID( $flags = 0 ) {
2436                 if ( $this->mLatestID !== false ) {
2437                         return intval( $this->mLatestID );
2438                 }
2439                 # Calling getArticleID() loads the field from cache as needed
2440                 if ( !$this->getArticleID( $flags ) ) {
2441                         return $this->mLatestID = 0;
2442                 }
2443                 $linkCache = LinkCache::singleton();
2444                 $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) );
2445
2446                 return $this->mLatestID;
2447         }
2448
2449         /**
2450          * This clears some fields in this object, and clears any associated
2451          * keys in the "bad links" section of the link cache.
2452          *
2453          * - This is called from Article::insertNewArticle() to allow
2454          * loading of the new page_id. It's also called from
2455          * Article::doDeleteArticle()
2456          *
2457          * @param $newid \type{\int} the new Article ID
2458          */
2459         public function resetArticleID( $newid ) {
2460                 $linkCache = LinkCache::singleton();
2461                 $linkCache->clearBadLink( $this->getPrefixedDBkey() );
2462
2463                 if ( $newid === false ) {
2464                         $this->mArticleID = -1;
2465                 } else {
2466                         $this->mArticleID = intval( $newid );
2467                 }
2468                 $this->mRestrictionsLoaded = false;
2469                 $this->mRestrictions = array();
2470                 $this->mRedirect = null;
2471                 $this->mLength = -1;
2472                 $this->mLatestID = false;
2473         }
2474
2475         /**
2476          * Updates page_touched for this page; called from LinksUpdate.php
2477          *
2478          * @return \type{\bool} true if the update succeded
2479          */
2480         public function invalidateCache() {
2481                 if ( wfReadOnly() ) {
2482                         return;
2483                 }
2484                 $dbw = wfGetDB( DB_MASTER );
2485                 $success = $dbw->update(
2486                         'page',
2487                         array( 'page_touched' => $dbw->timestamp() ),
2488                         $this->pageCond(),
2489                         __METHOD__
2490                 );
2491                 HTMLFileCache::clearFileCache( $this );
2492                 return $success;
2493         }
2494
2495         /**
2496          * Prefix some arbitrary text with the namespace or interwiki prefix
2497          * of this object
2498          *
2499          * @param $name \type{\string} the text
2500          * @return \type{\string} the prefixed text
2501          * @private
2502          */
2503         /* private */ function prefix( $name ) {
2504                 $p = '';
2505                 if ( $this->mInterwiki != '' ) {
2506                         $p = $this->mInterwiki . ':';
2507                 }
2508                 if ( 0 != $this->mNamespace ) {
2509                         $p .= $this->getNsText() . ':';
2510                 }
2511                 return $p . $name;
2512         }
2513
2514         /**
2515          * Returns a simple regex that will match on characters and sequences invalid in titles.
2516          * Note that this doesn't pick up many things that could be wrong with titles, but that
2517          * replacing this regex with something valid will make many titles valid.
2518          *
2519          * @return string regex string
2520          */
2521         static function getTitleInvalidRegex() {
2522                 static $rxTc = false;
2523                 if ( !$rxTc ) {
2524                         # Matching titles will be held as illegal.
2525                         $rxTc = '/' .
2526                                 # Any character not allowed is forbidden...
2527                                 '[^' . Title::legalChars() . ']' .
2528                                 # URL percent encoding sequences interfere with the ability
2529                                 # to round-trip titles -- you can't link to them consistently.
2530                                 '|%[0-9A-Fa-f]{2}' .
2531                                 # XML/HTML character references produce similar issues.
2532                                 '|&[A-Za-z0-9\x80-\xff]+;' .
2533                                 '|&#[0-9]+;' .
2534                                 '|&#x[0-9A-Fa-f]+;' .
2535                                 '/S';
2536                 }
2537
2538                 return $rxTc;
2539         }
2540
2541         /**
2542          * Capitalize a text string for a title if it belongs to a namespace that capitalizes
2543          *
2544          * @param $text string containing title to capitalize
2545          * @param $ns int namespace index, defaults to NS_MAIN
2546          * @return String containing capitalized title
2547          */
2548         public static function capitalize( $text, $ns = NS_MAIN ) {
2549                 global $wgContLang;
2550
2551                 if ( MWNamespace::isCapitalized( $ns ) ) {
2552                         return $wgContLang->ucfirst( $text );
2553                 } else {
2554                         return $text;
2555                 }
2556         }
2557
2558         /**
2559          * Secure and split - main initialisation function for this object
2560          *
2561          * Assumes that mDbkeyform has been set, and is urldecoded
2562          * and uses underscores, but not otherwise munged.  This function
2563          * removes illegal characters, splits off the interwiki and
2564          * namespace prefixes, sets the other forms, and canonicalizes
2565          * everything.
2566          *
2567          * @return \type{\bool} true on success
2568          */
2569         private function secureAndSplit() {
2570                 global $wgContLang, $wgLocalInterwiki;
2571
2572                 # Initialisation
2573                 $rxTc = self::getTitleInvalidRegex();
2574
2575                 $this->mInterwiki = $this->mFragment = '';
2576                 $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
2577
2578                 $dbkey = $this->mDbkeyform;
2579
2580                 # Strip Unicode bidi override characters.
2581                 # Sometimes they slip into cut-n-pasted page titles, where the
2582                 # override chars get included in list displays.
2583                 $dbkey = preg_replace( '/\xE2\x80[\x8E\x8F\xAA-\xAE]/S', '', $dbkey );
2584
2585                 # Clean up whitespace
2586                 # Note: use of the /u option on preg_replace here will cause
2587                 # input with invalid UTF-8 sequences to be nullified out in PHP 5.2.x,
2588                 # conveniently disabling them.
2589                 #
2590                 $dbkey = preg_replace( '/[ _\xA0\x{1680}\x{180E}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}]+/u', '_', $dbkey );
2591                 $dbkey = trim( $dbkey, '_' );
2592
2593                 if ( $dbkey == '' ) {
2594                         return false;
2595                 }
2596
2597                 if ( false !== strpos( $dbkey, UTF8_REPLACEMENT ) ) {
2598                         # Contained illegal UTF-8 sequences or forbidden Unicode chars.
2599                         return false;
2600                 }
2601
2602                 $this->mDbkeyform = $dbkey;
2603
2604                 # Initial colon indicates main namespace rather than specified default
2605                 # but should not create invalid {ns,title} pairs such as {0,Project:Foo}
2606                 if ( ':' == $dbkey { 0 } ) {
2607                         $this->mNamespace = NS_MAIN;
2608                         $dbkey = substr( $dbkey, 1 ); # remove the colon but continue processing
2609                         $dbkey = trim( $dbkey, '_' ); # remove any subsequent whitespace
2610                 }
2611
2612                 # Namespace or interwiki prefix
2613                 $firstPass = true;
2614                 $prefixRegexp = "/^(.+?)_*:_*(.*)$/S";
2615                 do {
2616                         $m = array();
2617                         if ( preg_match( $prefixRegexp, $dbkey, $m ) ) {
2618                                 $p = $m[1];
2619                                 if ( ( $ns = $wgContLang->getNsIndex( $p ) ) !== false ) {
2620                                         # Ordinary namespace
2621                                         $dbkey = $m[2];
2622                                         $this->mNamespace = $ns;
2623                                         # For Talk:X pages, check if X has a "namespace" prefix
2624                                         if ( $ns == NS_TALK && preg_match( $prefixRegexp, $dbkey, $x ) ) {
2625                                                 if ( $wgContLang->getNsIndex( $x[1] ) ) {
2626                                                         return false; # Disallow Talk:File:x type titles...
2627                                                 } else if ( Interwiki::isValidInterwiki( $x[1] ) ) {
2628                                                         return false; # Disallow Talk:Interwiki:x type titles...
2629                                                 }
2630                                         }
2631                                 } elseif ( Interwiki::isValidInterwiki( $p ) ) {
2632                                         if ( !$firstPass ) {
2633                                                 # Can't make a local interwiki link to an interwiki link.
2634                                                 # That's just crazy!
2635                                                 return false;
2636                                         }
2637
2638                                         # Interwiki link
2639                                         $dbkey = $m[2];
2640                                         $this->mInterwiki = $wgContLang->lc( $p );
2641
2642                                         # Redundant interwiki prefix to the local wiki
2643                                         if ( $wgLocalInterwiki !== false
2644                                                 && 0 == strcasecmp( $this->mInterwiki, $wgLocalInterwiki ) ) 
2645                                         {
2646                                                 if ( $dbkey == '' ) {
2647                                                         # Can't have an empty self-link
2648                                                         return false;
2649                                                 }
2650                                                 $this->mInterwiki = '';
2651                                                 $firstPass = false;
2652                                                 # Do another namespace split...
2653                                                 continue;
2654                                         }
2655
2656                                         # If there's an initial colon after the interwiki, that also
2657                                         # resets the default namespace
2658                                         if ( $dbkey !== '' && $dbkey[0] == ':' ) {
2659                                                 $this->mNamespace = NS_MAIN;
2660                                                 $dbkey = substr( $dbkey, 1 );
2661                                         }
2662                                 }
2663                                 # If there's no recognized interwiki or namespace,
2664                                 # then let the colon expression be part of the title.
2665                         }
2666                         break;
2667                 } while ( true );
2668
2669                 # We already know that some pages won't be in the database!
2670                 #
2671                 if ( $this->mInterwiki != '' || NS_SPECIAL == $this->mNamespace ) {
2672                         $this->mArticleID = 0;
2673                 }
2674                 $fragment = strstr( $dbkey, '#' );
2675                 if ( false !== $fragment ) {
2676                         $this->setFragment( preg_replace( '/^#_*/', '#', $fragment ) );
2677                         $dbkey = substr( $dbkey, 0, strlen( $dbkey ) - strlen( $fragment ) );
2678                         # remove whitespace again: prevents "Foo_bar_#"
2679                         # becoming "Foo_bar_"
2680                         $dbkey = preg_replace( '/_*$/', '', $dbkey );
2681                 }
2682
2683                 # Reject illegal characters.
2684                 #
2685                 if ( preg_match( $rxTc, $dbkey ) ) {
2686                         return false;
2687                 }
2688
2689                 /**
2690                  * Pages with "/./" or "/../" appearing in the URLs will often be un-
2691                  * reachable due to the way web browsers deal with 'relative' URLs.
2692                  * Also, they conflict with subpage syntax.  Forbid them explicitly.
2693                  */
2694                 if ( strpos( $dbkey, '.' ) !== false &&
2695                      ( $dbkey === '.' || $dbkey === '..' ||
2696                        strpos( $dbkey, './' ) === 0  ||
2697                        strpos( $dbkey, '../' ) === 0 ||
2698                        strpos( $dbkey, '/./' ) !== false ||
2699                        strpos( $dbkey, '/../' ) !== false  ||
2700                        substr( $dbkey, -2 ) == '/.' ||
2701                        substr( $dbkey, -3 ) == '/..' ) )
2702                 {
2703                         return false;
2704                 }
2705
2706                 /**
2707                  * Magic tilde sequences? Nu-uh!
2708                  */
2709                 if ( strpos( $dbkey, '~~~' ) !== false ) {
2710                         return false;
2711                 }
2712
2713                 /**
2714                  * Limit the size of titles to 255 bytes.
2715                  * This is typically the size of the underlying database field.
2716                  * We make an exception for special pages, which don't need to be stored
2717                  * in the database, and may edge over 255 bytes due to subpage syntax
2718                  * for long titles, e.g. [[Special:Block/Long name]]
2719                  */
2720                 if ( ( $this->mNamespace != NS_SPECIAL && strlen( $dbkey ) > 255 ) ||
2721                   strlen( $dbkey ) > 512 )
2722                 {
2723                         return false;
2724                 }
2725
2726                 /**
2727                  * Normally, all wiki links are forced to have
2728                  * an initial capital letter so [[foo]] and [[Foo]]
2729                  * point to the same place.
2730                  *
2731                  * Don't force it for interwikis, since the other
2732                  * site might be case-sensitive.
2733                  */
2734                 $this->mUserCaseDBKey = $dbkey;
2735                 if ( $this->mInterwiki == '' ) {
2736                         $dbkey = self::capitalize( $dbkey, $this->mNamespace );
2737                 }
2738
2739                 /**
2740                  * Can't make a link to a namespace alone...
2741                  * "empty" local links can only be self-links
2742                  * with a fragment identifier.
2743                  */
2744                 if ( $dbkey == '' &&
2745                         $this->mInterwiki == '' &&
2746                         $this->mNamespace != NS_MAIN ) {
2747                         return false;
2748                 }
2749                 // Allow IPv6 usernames to start with '::' by canonicalizing IPv6 titles.
2750                 // IP names are not allowed for accounts, and can only be referring to
2751                 // edits from the IP. Given '::' abbreviations and caps/lowercaps,
2752                 // there are numerous ways to present the same IP. Having sp:contribs scan
2753                 // them all is silly and having some show the edits and others not is
2754                 // inconsistent. Same for talk/userpages. Keep them normalized instead.
2755                 $dbkey = ( $this->mNamespace == NS_USER || $this->mNamespace == NS_USER_TALK ) ?
2756                         IP::sanitizeIP( $dbkey ) : $dbkey;
2757                 // Any remaining initial :s are illegal.
2758                 if ( $dbkey !== '' && ':' == $dbkey { 0 } ) {
2759                         return false;
2760                 }
2761
2762                 # Fill fields
2763                 $this->mDbkeyform = $dbkey;
2764                 $this->mUrlform = wfUrlencode( $dbkey );
2765
2766                 $this->mTextform = str_replace( '_', ' ', $dbkey );
2767
2768                 return true;
2769         }
2770
2771         /**
2772          * Set the fragment for this title. Removes the first character from the
2773          * specified fragment before setting, so it assumes you're passing it with
2774          * an initial "#".
2775          *
2776          * Deprecated for public use, use Title::makeTitle() with fragment parameter.
2777          * Still in active use privately.
2778          *
2779          * @param $fragment \type{\string} text
2780          */
2781         public function setFragment( $fragment ) {
2782                 $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) );
2783         }
2784
2785         /**
2786          * Get a Title object associated with the talk page of this article
2787          *
2788          * @return Title the object for the talk page
2789          */
2790         public function getTalkPage() {
2791                 return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
2792         }
2793
2794         /**
2795          * Get a title object associated with the subject page of this
2796          * talk page
2797          *
2798          * @return Title the object for the subject page
2799          */
2800         public function getSubjectPage() {
2801                 // Is this the same title?
2802                 $subjectNS = MWNamespace::getSubject( $this->getNamespace() );
2803                 if ( $this->getNamespace() == $subjectNS ) {
2804                         return $this;
2805                 }
2806                 return Title::makeTitle( $subjectNS, $this->getDBkey() );
2807         }
2808
2809         /**
2810          * Get an array of Title objects linking to this Title
2811          * Also stores the IDs in the link cache.
2812          *
2813          * WARNING: do not use this function on arbitrary user-supplied titles!
2814          * On heavily-used templates it will max out the memory.
2815          *
2816          * @param $options Array: may be FOR UPDATE
2817          * @param $table String: table name
2818          * @param $prefix String: fields prefix
2819          * @return \type{\arrayof{Title}} the Title objects linking here
2820          */
2821         public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
2822                 $linkCache = LinkCache::singleton();
2823
2824                 if ( count( $options ) > 0 ) {
2825                         $db = wfGetDB( DB_MASTER );
2826                 } else {
2827                         $db = wfGetDB( DB_SLAVE );
2828                 }
2829
2830                 $res = $db->select(
2831                         array( 'page', $table ),
2832                         array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
2833                         array(
2834                                 "{$prefix}_from=page_id",
2835                                 "{$prefix}_namespace" => $this->getNamespace(),
2836                                 "{$prefix}_title"     => $this->getDBkey() ),
2837                         __METHOD__,
2838                         $options
2839                 );
2840
2841                 $retVal = array();
2842                 if ( $db->numRows( $res ) ) {
2843                         foreach ( $res as $row ) {
2844                                 $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title );
2845                                 if ( $titleObj ) {
2846                                         $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect, $row->page_latest );
2847                                         $retVal[] = $titleObj;
2848                                 }
2849                         }
2850                 }
2851                 return $retVal;
2852         }
2853
2854         /**
2855          * Get an array of Title objects using this Title as a template
2856          * Also stores the IDs in the link cache.
2857          *
2858          * WARNING: do not use this function on arbitrary user-supplied titles!
2859          * On heavily-used templates it will max out the memory.
2860          *
2861          * @param $options Array: may be FOR UPDATE
2862          * @return \type{\arrayof{Title}} the Title objects linking here
2863          */
2864         public function getTemplateLinksTo( $options = array() ) {
2865                 return $this->getLinksTo( $options, 'templatelinks', 'tl' );
2866         }
2867
2868         /**
2869          * Get an array of Title objects referring to non-existent articles linked from this page
2870          *
2871          * @todo check if needed (used only in SpecialBrokenRedirects.php, and should use redirect table in this case)
2872          * @return \type{\arrayof{Title}} the Title objects
2873          */
2874         public function getBrokenLinksFrom() {
2875                 if ( $this->getArticleId() == 0 ) {
2876                         # All links from article ID 0 are false positives
2877                         return array();
2878                 }
2879
2880                 $dbr = wfGetDB( DB_SLAVE );
2881                 $res = $dbr->select(
2882                         array( 'page', 'pagelinks' ),
2883                         array( 'pl_namespace', 'pl_title' ),
2884                         array(
2885                                 'pl_from' => $this->getArticleId(),
2886                                 'page_namespace IS NULL'
2887                         ),
2888                         __METHOD__, array(),
2889                         array(
2890                                 'page' => array(
2891                                         'LEFT JOIN',
2892                                         array( 'pl_namespace=page_namespace', 'pl_title=page_title' )
2893                                 )
2894                         )
2895                 );
2896
2897                 $retVal = array();
2898                 foreach ( $res as $row ) {
2899                         $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
2900                 }
2901                 return $retVal;
2902         }
2903
2904
2905         /**
2906          * Get a list of URLs to purge from the Squid cache when this
2907          * page changes
2908          *
2909          * @return \type{\arrayof{\string}} the URLs
2910          */
2911         public function getSquidURLs() {
2912                 global $wgContLang;
2913
2914                 $urls = array(
2915                         $this->getInternalURL(),
2916                         $this->getInternalURL( 'action=history' )
2917                 );
2918
2919                 // purge variant urls as well
2920                 if ( $wgContLang->hasVariants() ) {
2921                         $variants = $wgContLang->getVariants();
2922                         foreach ( $variants as $vCode ) {
2923                                 $urls[] = $this->getInternalURL( '', $vCode );
2924                         }
2925                 }
2926
2927                 return $urls;
2928         }
2929
2930         /**
2931          * Purge all applicable Squid URLs
2932          */
2933         public function purgeSquid() {
2934                 global $wgUseSquid;
2935                 if ( $wgUseSquid ) {
2936                         $urls = $this->getSquidURLs();
2937                         $u = new SquidUpdate( $urls );
2938                         $u->doUpdate();
2939                 }
2940         }
2941
2942         /**
2943          * Move this page without authentication
2944          *
2945          * @param $nt \type{Title} the new page Title
2946          * @return \type{\mixed} true on success, getUserPermissionsErrors()-like array on failure
2947          */
2948         public function moveNoAuth( &$nt ) {
2949                 return $this->moveTo( $nt, false );
2950         }
2951
2952         /**
2953          * Check whether a given move operation would be valid.
2954          * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
2955          *
2956          * @param $nt \type{Title} the new title
2957          * @param $auth \type{\bool} indicates whether $wgUser's permissions
2958          *  should be checked
2959          * @param $reason \type{\string} is the log summary of the move, used for spam checking
2960          * @return \type{\mixed} True on success, getUserPermissionsErrors()-like array on failure
2961          */
2962         public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
2963                 global $wgUser;
2964
2965                 $errors = array();
2966                 if ( !$nt ) {
2967                         // Normally we'd add this to $errors, but we'll get
2968                         // lots of syntax errors if $nt is not an object
2969                         return array( array( 'badtitletext' ) );
2970                 }
2971                 if ( $this->equals( $nt ) ) {
2972                         $errors[] = array( 'selfmove' );
2973                 }
2974                 if ( !$this->isMovable() ) {
2975                         $errors[] = array( 'immobile-source-namespace', $this->getNsText() );
2976                 }
2977                 if ( $nt->getInterwiki() != '' ) {
2978                         $errors[] = array( 'immobile-target-namespace-iw' );
2979                 }
2980                 if ( !$nt->isMovable() ) {
2981                         $errors[] = array( 'immobile-target-namespace', $nt->getNsText() );
2982                 }
2983
2984                 $oldid = $this->getArticleID();
2985                 $newid = $nt->getArticleID();
2986
2987                 if ( strlen( $nt->getDBkey() ) < 1 ) {
2988                         $errors[] = array( 'articleexists' );
2989                 }
2990                 if ( ( $this->getDBkey() == '' ) ||
2991                          ( !$oldid ) ||
2992                      ( $nt->getDBkey() == '' ) ) {
2993                         $errors[] = array( 'badarticleerror' );
2994                 }
2995
2996                 // Image-specific checks
2997                 if ( $this->getNamespace() == NS_FILE ) {
2998                         if ( $nt->getNamespace() != NS_FILE ) {
2999                                 $errors[] = array( 'imagenocrossnamespace' );
3000                         }
3001                         $file = wfLocalFile( $this );
3002                         if ( $file->exists() ) {
3003                                 if ( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) {
3004                                         $errors[] = array( 'imageinvalidfilename' );
3005                                 }
3006                                 if ( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) {
3007                                         $errors[] = array( 'imagetypemismatch' );
3008                                 }
3009                         }
3010                         $destfile = wfLocalFile( $nt );
3011                         if ( !$wgUser->isAllowed( 'reupload-shared' ) && !$destfile->exists() && wfFindFile( $nt ) ) {
3012                                 $errors[] = array( 'file-exists-sharedrepo' );
3013                         }
3014                 }
3015
3016                 if ( $nt->getNamespace() == NS_FILE && $this->getNamespace() != NS_FILE ) {
3017                         $errors[] = array( 'nonfile-cannot-move-to-file' );
3018                 }
3019
3020                 if ( $auth ) {
3021                         $errors = wfMergeErrorArrays( $errors,
3022                                 $this->getUserPermissionsErrors( 'move', $wgUser ),
3023                                 $this->getUserPermissionsErrors( 'edit', $wgUser ),
3024                                 $nt->getUserPermissionsErrors( 'move-target', $wgUser ),
3025                                 $nt->getUserPermissionsErrors( 'edit', $wgUser ) );
3026                 }
3027
3028                 $match = EditPage::matchSummarySpamRegex( $reason );
3029                 if ( $match !== false ) {
3030                         // This is kind of lame, won't display nice
3031                         $errors[] = array( 'spamprotectiontext' );
3032                 }
3033
3034                 $err = null;
3035                 if ( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) {
3036                         $errors[] = array( 'hookaborted', $err );
3037                 }
3038
3039                 # The move is allowed only if (1) the target doesn't exist, or
3040                 # (2) the target is a redirect to the source, and has no history
3041                 # (so we can undo bad moves right after they're done).
3042
3043                 if ( 0 != $newid ) { # Target exists; check for validity
3044                         if ( !$this->isValidMoveTarget( $nt ) ) {
3045                                 $errors[] = array( 'articleexists' );
3046                         }
3047                 } else {
3048                         $tp = $nt->getTitleProtection();
3049                         $right = ( $tp['pt_create_perm'] == 'sysop' ) ? 'protect' : $tp['pt_create_perm'];
3050                         if ( $tp and !$wgUser->isAllowed( $right ) ) {
3051                                 $errors[] = array( 'cantmove-titleprotected' );
3052                         }
3053                 }
3054                 if ( empty( $errors ) ) {
3055                         return true;
3056                 }
3057                 return $errors;
3058         }
3059
3060         /**
3061          * Move a title to a new location
3062          *
3063          * @param $nt \type{Title} the new title
3064          * @param $auth \type{\bool} indicates whether $wgUser's permissions
3065          *  should be checked
3066          * @param $reason \type{\string} The reason for the move
3067          * @param $createRedirect \type{\bool} Whether to create a redirect from the old title to the new title.
3068          *  Ignored if the user doesn't have the suppressredirect right.
3069          * @return \type{\mixed} true on success, getUserPermissionsErrors()-like array on failure
3070          */
3071         public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
3072                 $err = $this->isValidMoveOperation( $nt, $auth, $reason );
3073                 if ( is_array( $err ) ) {
3074                         return $err;
3075                 }
3076
3077                 // If it is a file, move it first. It is done before all other moving stuff is done because it's hard to revert
3078                 $dbw = wfGetDB( DB_MASTER );
3079                 if ( $this->getNamespace() == NS_FILE ) {
3080                         $file = wfLocalFile( $this );
3081                         if ( $file->exists() ) {
3082                                 $status = $file->move( $nt );
3083                                 if ( !$status->isOk() ) {
3084                                         return $status->getErrorsArray();
3085                                 }
3086                         }
3087                 }
3088
3089                 $dbw->begin(); # If $file was a LocalFile, its transaction would have closed our own.
3090                 $pageid = $this->getArticleID( GAID_FOR_UPDATE );
3091                 $protected = $this->isProtected();
3092                 if ( $nt->exists() ) {
3093                         $err = $this->moveOverExistingRedirect( $nt, $reason, $createRedirect );
3094                         $pageCountChange = ( $createRedirect ? 0 : -1 );
3095                 } else { # Target didn't exist, do normal move.
3096                         $err = $this->moveToNewTitle( $nt, $reason, $createRedirect );
3097                         $pageCountChange = ( $createRedirect ? 1 : 0 );
3098                 }
3099
3100                 if ( is_array( $err ) ) {
3101                         # FIXME: What about the File we have already moved?
3102                         $dbw->rollback();
3103                         return $err;
3104                 }
3105                 $redirid = $this->getArticleID();
3106
3107                 // Refresh the sortkey for this row.  Be careful to avoid resetting
3108                 // cl_timestamp, which may disturb time-based lists on some sites.
3109                 $prefix = $dbw->selectField(
3110                         'categorylinks',
3111                         'cl_sortkey_prefix',
3112                         array( 'cl_from' => $pageid ),
3113                         __METHOD__
3114                 );
3115                 $dbw->update( 'categorylinks',
3116                         array(
3117                                 'cl_sortkey' => Collation::singleton()->getSortKey( 
3118                                         $nt->getCategorySortkey( $prefix ) ),
3119                                 'cl_timestamp=cl_timestamp' ),
3120                         array( 'cl_from' => $pageid ),
3121                         __METHOD__ );
3122
3123                 if ( $protected ) {
3124                         # Protect the redirect title as the title used to be...
3125                         $dbw->insertSelect( 'page_restrictions', 'page_restrictions',
3126                                 array(
3127                                         'pr_page'    => $redirid,
3128                                         'pr_type'    => 'pr_type',
3129                                         'pr_level'   => 'pr_level',
3130                                         'pr_cascade' => 'pr_cascade',
3131                                         'pr_user'    => 'pr_user',
3132                                         'pr_expiry'  => 'pr_expiry'
3133                                 ),
3134                                 array( 'pr_page' => $pageid ),
3135                                 __METHOD__,
3136                                 array( 'IGNORE' )
3137                         );
3138                         # Update the protection log
3139                         $log = new LogPage( 'protect' );
3140                         $comment = wfMsgForContent( 'prot_1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() );
3141                         if ( $reason ) {
3142                                 $comment .= wfMsgForContent( 'colon-separator' ) . $reason;
3143                         }
3144                         $log->addEntry( 'move_prot', $nt, $comment, array( $this->getPrefixedText() ) ); // FIXME: $params?
3145                 }
3146
3147                 # Update watchlists
3148                 $oldnamespace = $this->getNamespace() & ~1;
3149                 $newnamespace = $nt->getNamespace() & ~1;
3150                 $oldtitle = $this->getDBkey();
3151                 $newtitle = $nt->getDBkey();
3152
3153                 if ( $oldnamespace != $newnamespace || $oldtitle != $newtitle ) {
3154                         WatchedItem::duplicateEntries( $this, $nt );
3155                 }
3156
3157                 # Update search engine
3158                 $u = new SearchUpdate( $pageid, $nt->getPrefixedDBkey() );
3159                 $u->doUpdate();
3160                 $u = new SearchUpdate( $redirid, $this->getPrefixedDBkey(), '' );
3161                 $u->doUpdate();
3162
3163                 $dbw->commit();
3164                 
3165                 # Update site_stats
3166                 if ( $this->isContentPage() && !$nt->isContentPage() ) {
3167                         # No longer a content page
3168                         # Not viewed, edited, removing
3169                         $u = new SiteStatsUpdate( 0, 1, -1, $pageCountChange );
3170                 } elseif ( !$this->isContentPage() && $nt->isContentPage() ) {
3171                         # Now a content page
3172                         # Not viewed, edited, adding
3173                         $u = new SiteStatsUpdate( 0, 1, + 1, $pageCountChange );
3174                 } elseif ( $pageCountChange ) {
3175                         # Redirect added
3176                         $u = new SiteStatsUpdate( 0, 0, 0, 1 );
3177                 } else {
3178                         # Nothing special
3179                         $u = false;
3180                 }
3181                 if ( $u ) {
3182                         $u->doUpdate();
3183                 }
3184                 # Update message cache for interface messages
3185                 global $wgMessageCache;
3186                 if ( $this->getNamespace() == NS_MEDIAWIKI ) {
3187                         # @bug 17860: old article can be deleted, if this the case,
3188                         # delete it from message cache
3189                         if ( $this->getArticleID() === 0 ) {
3190                                 $wgMessageCache->replace( $this->getDBkey(), false );
3191                         } else {
3192                                 $oldarticle = new Article( $this );
3193                                 $wgMessageCache->replace( $this->getDBkey(), $oldarticle->getContent() );
3194                         }
3195                 }
3196                 if ( $nt->getNamespace() == NS_MEDIAWIKI ) {
3197                         $newarticle = new Article( $nt );
3198                         $wgMessageCache->replace( $nt->getDBkey(), $newarticle->getContent() );
3199                 }
3200
3201                 global $wgUser;
3202                 wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) );
3203                 return true;
3204         }
3205
3206         /**
3207          * Move page to a title which is at present a redirect to the
3208          * source page
3209          *
3210          * @param $nt \type{Title} the page to move to, which should currently
3211          *  be a redirect
3212          * @param $reason \type{\string} The reason for the move
3213          * @param $createRedirect \type{\bool} Whether to leave a redirect at the old title.
3214          *  Ignored if the user doesn't have the suppressredirect right
3215          */
3216         private function moveOverExistingRedirect( &$nt, $reason = '', $createRedirect = true ) {
3217                 global $wgUseSquid, $wgUser, $wgContLang;
3218
3219                 $comment = wfMsgForContent( '1movedto2_redir', $this->getPrefixedText(), $nt->getPrefixedText() );
3220
3221                 if ( $reason ) {
3222                         $comment .= wfMsgForContent( 'colon-separator' ) . $reason;
3223                 }
3224                 # Truncate for whole multibyte characters. +5 bytes for ellipsis
3225                 $comment = $wgContLang->truncate( $comment, 250 );
3226
3227                 $now = wfTimestampNow();
3228                 $newid = $nt->getArticleID();
3229                 $oldid = $this->getArticleID();
3230                 $latest = $this->getLatestRevID();
3231
3232                 $dbw = wfGetDB( DB_MASTER );
3233
3234                 $rcts = $dbw->timestamp( $nt->getEarliestRevTime() );
3235                 $newns = $nt->getNamespace();
3236                 $newdbk = $nt->getDBkey();
3237
3238                 # Delete the old redirect. We don't save it to history since
3239                 # by definition if we've got here it's rather uninteresting.
3240                 # We have to remove it so that the next step doesn't trigger
3241                 # a conflict on the unique namespace+title index...
3242                 $dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__ );
3243                 if ( !$dbw->cascadingDeletes() ) {
3244                         $dbw->delete( 'revision', array( 'rev_page' => $newid ), __METHOD__ );
3245                         global $wgUseTrackbacks;
3246                         if ( $wgUseTrackbacks ) {
3247                                 $dbw->delete( 'trackbacks', array( 'tb_page' => $newid ), __METHOD__ );
3248                         }
3249                         $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ );
3250                         $dbw->delete( 'imagelinks', array( 'il_from' => $newid ), __METHOD__ );
3251                         $dbw->delete( 'categorylinks', array( 'cl_from' => $newid ), __METHOD__ );
3252                         $dbw->delete( 'templatelinks', array( 'tl_from' => $newid ), __METHOD__ );
3253                         $dbw->delete( 'externallinks', array( 'el_from' => $newid ), __METHOD__ );
3254                         $dbw->delete( 'langlinks', array( 'll_from' => $newid ), __METHOD__ );
3255                         $dbw->delete( 'redirect', array( 'rd_from' => $newid ), __METHOD__ );
3256                 }
3257                 // If the redirect was recently created, it may have an entry in recentchanges still
3258                 $dbw->delete( 'recentchanges',
3259                         array( 'rc_timestamp' => $rcts, 'rc_namespace' => $newns, 'rc_title' => $newdbk, 'rc_new' => 1 ),
3260                         __METHOD__
3261                 );
3262
3263                 # Save a null revision in the page's history notifying of the move
3264                 $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
3265                 $nullRevId = $nullRevision->insertOn( $dbw );
3266
3267                 $article = new Article( $this );
3268                 wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $wgUser ) );
3269
3270                 # Change the name of the target page:
3271                 $dbw->update( 'page',
3272                         /* SET */ array(
3273                                 'page_touched'   => $dbw->timestamp( $now ),
3274                                 'page_namespace' => $nt->getNamespace(),
3275                                 'page_title'     => $nt->getDBkey(),
3276                                 'page_latest'    => $nullRevId,
3277                         ),
3278                         /* WHERE */ array( 'page_id' => $oldid ),
3279                         __METHOD__
3280                 );
3281                 $nt->resetArticleID( $oldid );
3282
3283                 # Recreate the redirect, this time in the other direction.
3284                 if ( $createRedirect || !$wgUser->isAllowed( 'suppressredirect' ) ) {
3285                         $mwRedir = MagicWord::get( 'redirect' );
3286                         $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
3287                         $redirectArticle = new Article( $this );
3288                         $newid = $redirectArticle->insertOn( $dbw );
3289                         $redirectRevision = new Revision( array(
3290                                 'page'    => $newid,
3291                                 'comment' => $comment,
3292                                 'text'    => $redirectText ) );
3293                         $redirectRevision->insertOn( $dbw );
3294                         $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
3295
3296                         wfRunHooks( 'NewRevisionFromEditComplete', array( $redirectArticle, $redirectRevision, false, $wgUser ) );
3297
3298                         # Now, we record the link from the redirect to the new title.
3299                         # It should have no other outgoing links...
3300                         $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ );
3301                         $dbw->insert( 'pagelinks',
3302                                 array(
3303                                         'pl_from'      => $newid,
3304                                         'pl_namespace' => $nt->getNamespace(),
3305                                         'pl_title'     => $nt->getDBkey() ),
3306                                 __METHOD__ );
3307                         $redirectSuppressed = false;
3308                 } else {
3309                         $this->resetArticleID( 0 );
3310                         $redirectSuppressed = true;
3311                 }
3312
3313                 # Log the move
3314                 $log = new LogPage( 'move' );
3315                 $log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) );
3316
3317                 # Purge squid
3318                 if ( $wgUseSquid ) {
3319                         $urls = array_merge( $nt->getSquidURLs(), $this->getSquidURLs() );
3320                         $u = new SquidUpdate( $urls );
3321                         $u->doUpdate();
3322                 }
3323
3324         }
3325
3326         /**
3327          * Move page to non-existing title.
3328          *
3329          * @param $nt \type{Title} the new Title
3330          * @param $reason \type{\string} The reason for the move
3331          * @param $createRedirect \type{\bool} Whether to create a redirect from the old title to the new title
3332          *  Ignored if the user doesn't have the suppressredirect right
3333          */
3334         private function moveToNewTitle( &$nt, $reason = '', $createRedirect = true ) {
3335                 global $wgUser, $wgContLang;
3336
3337                 $comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() );
3338                 if ( $reason ) {
3339                         $comment .= wfMsgExt( 'colon-separator',
3340                                 array( 'escapenoentities', 'content' ) );
3341                         $comment .= $reason;
3342                 }
3343                 # Truncate for whole multibyte characters. +5 bytes for ellipsis
3344                 $comment = $wgContLang->truncate( $comment, 250 );
3345
3346                 $oldid = $this->getArticleID();
3347                 $latest = $this->getLatestRevId();
3348
3349                 $dbw = wfGetDB( DB_MASTER );
3350                 $now = $dbw->timestamp();
3351
3352                 # Save a null revision in the page's history notifying of the move
3353                 $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
3354                 if ( !is_object( $nullRevision ) ) {
3355                         throw new MWException( 'No valid null revision produced in ' . __METHOD__ );
3356                 }
3357                 $nullRevId = $nullRevision->insertOn( $dbw );
3358
3359                 $article = new Article( $this );
3360                 wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $wgUser ) );
3361
3362                 # Rename page entry
3363                 $dbw->update( 'page',
3364                         /* SET */ array(
3365                                 'page_touched'   => $now,
3366                                 'page_namespace' => $nt->getNamespace(),
3367                                 'page_title'     => $nt->getDBkey(),
3368                                 'page_latest'    => $nullRevId,
3369                         ),
3370                         /* WHERE */ array( 'page_id' => $oldid ),
3371                         __METHOD__
3372                 );
3373                 $nt->resetArticleID( $oldid );
3374
3375                 if ( $createRedirect || !$wgUser->isAllowed( 'suppressredirect' ) ) {
3376                         # Insert redirect
3377                         $mwRedir = MagicWord::get( 'redirect' );
3378                         $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
3379                         $redirectArticle = new Article( $this );
3380                         $newid = $redirectArticle->insertOn( $dbw );
3381                         $redirectRevision = new Revision( array(
3382                                 'page'    => $newid,
3383                                 'comment' => $comment,
3384                                 'text'    => $redirectText ) );
3385                         $redirectRevision->insertOn( $dbw );
3386                         $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
3387
3388                         wfRunHooks( 'NewRevisionFromEditComplete', array( $redirectArticle, $redirectRevision, false, $wgUser ) );
3389
3390                         # Record the just-created redirect's linking to the page
3391                         $dbw->insert( 'pagelinks',
3392                                 array(
3393                                         'pl_from'      => $newid,
3394                                         'pl_namespace' => $nt->getNamespace(),
3395                                         'pl_title'     => $nt->getDBkey() ),
3396                                 __METHOD__ );
3397                         $redirectSuppressed = false;
3398                 } else {
3399                         $this->resetArticleID( 0 );
3400                         $redirectSuppressed = true;
3401                 }
3402
3403                 # Log the move
3404                 $log = new LogPage( 'move' );
3405                 $log->addEntry( 'move', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) );
3406
3407                 # Purge caches as per article creation
3408                 Article::onArticleCreate( $nt );
3409
3410                 # Purge old title from squid
3411                 # The new title, and links to the new title, are purged in Article::onArticleCreate()
3412                 $this->purgeSquid();
3413         }
3414
3415         /**
3416          * Move this page's subpages to be subpages of $nt
3417          *
3418          * @param $nt Title Move target
3419          * @param $auth bool Whether $wgUser's permissions should be checked
3420          * @param $reason string The reason for the move
3421          * @param $createRedirect bool Whether to create redirects from the old subpages to the new ones
3422          *  Ignored if the user doesn't have the 'suppressredirect' right
3423          * @return mixed array with old page titles as keys, and strings (new page titles) or
3424          *  arrays (errors) as values, or an error array with numeric indices if no pages were moved
3425          */
3426         public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) {
3427                 global $wgMaximumMovedPages;
3428                 // Check permissions
3429                 if ( !$this->userCan( 'move-subpages' ) ) {
3430                         return array( 'cant-move-subpages' );
3431                 }
3432                 // Do the source and target namespaces support subpages?
3433                 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3434                         return array( 'namespace-nosubpages',
3435                                 MWNamespace::getCanonicalName( $this->getNamespace() ) );
3436                 }
3437                 if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
3438                         return array( 'namespace-nosubpages',
3439                                 MWNamespace::getCanonicalName( $nt->getNamespace() ) );
3440                 }
3441
3442                 $subpages = $this->getSubpages( $wgMaximumMovedPages + 1 );
3443                 $retval = array();
3444                 $count = 0;
3445                 foreach ( $subpages as $oldSubpage ) {
3446                         $count++;
3447                         if ( $count > $wgMaximumMovedPages ) {
3448                                 $retval[$oldSubpage->getPrefixedTitle()] =
3449                                                 array( 'movepage-max-pages',
3450                                                         $wgMaximumMovedPages );
3451                                 break;
3452                         }
3453
3454                         // We don't know whether this function was called before
3455                         // or after moving the root page, so check both
3456                         // $this and $nt
3457                         if ( $oldSubpage->getArticleId() == $this->getArticleId() ||
3458                                         $oldSubpage->getArticleID() == $nt->getArticleId() )
3459                         {
3460                                 // When moving a page to a subpage of itself,
3461                                 // don't move it twice
3462                                 continue;
3463                         }
3464                         $newPageName = preg_replace(
3465                                         '#^' . preg_quote( $this->getDBkey(), '#' ) . '#',
3466                                         StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
3467                                         $oldSubpage->getDBkey() );
3468                         if ( $oldSubpage->isTalkPage() ) {
3469                                 $newNs = $nt->getTalkPage()->getNamespace();
3470                         } else {
3471                                 $newNs = $nt->getSubjectPage()->getNamespace();
3472                         }
3473                         # Bug 14385: we need makeTitleSafe because the new page names may
3474                         # be longer than 255 characters.
3475                         $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
3476
3477                         $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect );
3478                         if ( $success === true ) {
3479                                 $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
3480                         } else {
3481                                 $retval[$oldSubpage->getPrefixedText()] = $success;
3482                         }
3483                 }
3484                 return $retval;
3485         }
3486
3487         /**
3488          * Checks if this page is just a one-rev redirect.
3489          * Adds lock, so don't use just for light purposes.
3490          *
3491          * @return \type{\bool}
3492          */
3493         public function isSingleRevRedirect() {
3494                 $dbw = wfGetDB( DB_MASTER );
3495                 # Is it a redirect?
3496                 $row = $dbw->selectRow( 'page',
3497                         array( 'page_is_redirect', 'page_latest', 'page_id' ),
3498                         $this->pageCond(),
3499                         __METHOD__,
3500                         array( 'FOR UPDATE' )
3501                 );
3502                 # Cache some fields we may want
3503                 $this->mArticleID = $row ? intval( $row->page_id ) : 0;
3504                 $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
3505                 $this->mLatestID = $row ? intval( $row->page_latest ) : false;
3506                 if ( !$this->mRedirect ) {
3507                         return false;
3508                 }
3509                 # Does the article have a history?
3510                 $row = $dbw->selectField( array( 'page', 'revision' ),
3511                         'rev_id',
3512                         array( 'page_namespace' => $this->getNamespace(),
3513                                 'page_title' => $this->getDBkey(),
3514                                 'page_id=rev_page',
3515                                 'page_latest != rev_id'
3516                         ),
3517                         __METHOD__,
3518                         array( 'FOR UPDATE' )
3519                 );
3520                 # Return true if there was no history
3521                 return ( $row === false );
3522         }
3523
3524         /**
3525          * Checks if $this can be moved to a given Title
3526          * - Selects for update, so don't call it unless you mean business
3527          *
3528          * @param $nt \type{Title} the new title to check
3529          * @return \type{\bool} TRUE or FALSE
3530          */
3531         public function isValidMoveTarget( $nt ) {
3532                 # Is it an existing file?
3533                 if ( $nt->getNamespace() == NS_FILE ) {
3534                         $file = wfLocalFile( $nt );
3535                         if ( $file->exists() ) {
3536                                 wfDebug( __METHOD__ . ": file exists\n" );
3537                                 return false;
3538                         }
3539                 }
3540                 # Is it a redirect with no history?
3541                 if ( !$nt->isSingleRevRedirect() ) {
3542                         wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
3543                         return false;
3544                 }
3545                 # Get the article text
3546                 $rev = Revision::newFromTitle( $nt );
3547                 $text = $rev->getText();
3548                 # Does the redirect point to the source?
3549                 # Or is it a broken self-redirect, usually caused by namespace collisions?
3550                 $m = array();
3551                 if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) {
3552                         $redirTitle = Title::newFromText( $m[1] );
3553                         if ( !is_object( $redirTitle ) ||
3554                                 ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
3555                                 $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) {
3556                                 wfDebug( __METHOD__ . ": redirect points to other page\n" );
3557                                 return false;
3558                         }
3559                 } else {
3560                         # Fail safe
3561                         wfDebug( __METHOD__ . ": failsafe\n" );
3562                         return false;
3563                 }
3564                 return true;
3565         }
3566
3567         /**
3568          * Can this title be added to a user's watchlist?
3569          *
3570          * @return \type{\bool} TRUE or FALSE
3571          */
3572         public function isWatchable() {
3573                 return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
3574         }
3575
3576         /**
3577          * Get categories to which this Title belongs and return an array of
3578          * categories' names.
3579          *
3580          * @return \type{\array} array an array of parents in the form:
3581          *      $parent => $currentarticle
3582          */
3583         public function getParentCategories() {
3584                 global $wgContLang;
3585
3586                 $titlekey = $this->getArticleId();
3587                 $dbr = wfGetDB( DB_SLAVE );
3588                 $categorylinks = $dbr->tableName( 'categorylinks' );
3589
3590                 # NEW SQL
3591                 $sql = "SELECT * FROM $categorylinks"
3592                      . " WHERE cl_from='$titlekey'"
3593                          . " AND cl_from <> '0'"
3594                          . " ORDER BY cl_sortkey";
3595
3596                 $res = $dbr->query( $sql );
3597                 $data = array();
3598
3599                 if ( $dbr->numRows( $res ) > 0 ) {
3600                         foreach ( $res as $row ) {
3601                                 // $data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$row->cl_to);
3602                                 $data[$wgContLang->getNSText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText();
3603                         }
3604                 }
3605                 return $data;
3606         }
3607
3608         /**
3609          * Get a tree of parent categories
3610          *
3611          * @param $children \type{\array} an array with the children in the keys, to check for circular refs
3612          * @return \type{\array} Tree of parent categories
3613          */
3614         public function getParentCategoryTree( $children = array() ) {
3615                 $stack = array();
3616                 $parents = $this->getParentCategories();
3617
3618                 if ( $parents ) {
3619                         foreach ( $parents as $parent => $current ) {
3620                                 if ( array_key_exists( $parent, $children ) ) {
3621                                         # Circular reference
3622                                         $stack[$parent] = array();
3623                                 } else {
3624                                         $nt = Title::newFromText( $parent );
3625                                         if ( $nt ) {
3626                                                 $stack[$parent] = $nt->getParentCategoryTree( $children + array( $parent => 1 ) );
3627                                         }
3628                                 }
3629                         }
3630                 }
3631
3632                 return $stack;
3633         }
3634
3635
3636         /**
3637          * Get an associative array for selecting this title from
3638          * the "page" table
3639          *
3640          * @return \type{\array} Selection array
3641          */
3642         public function pageCond() {
3643                 if ( $this->mArticleID > 0 ) {
3644                         // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
3645                         return array( 'page_id' => $this->mArticleID );
3646                 } else {
3647                         return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform );
3648                 }
3649         }
3650
3651         /**
3652          * Get the revision ID of the previous revision
3653          *
3654          * @param $revId \type{\int} Revision ID. Get the revision that was before this one.
3655          * @param $flags \type{\int} Title::GAID_FOR_UPDATE
3656          * @return \twotypes{\int,\bool} Old revision ID, or FALSE if none exists
3657          */
3658         public function getPreviousRevisionID( $revId, $flags = 0 ) {
3659                 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3660                 return $db->selectField( 'revision', 'rev_id',
3661                         array(
3662                                 'rev_page' => $this->getArticleId( $flags ),
3663                                 'rev_id < ' . intval( $revId )
3664                         ),
3665                         __METHOD__,
3666                         array( 'ORDER BY' => 'rev_id DESC' )
3667                 );
3668         }
3669
3670         /**
3671          * Get the revision ID of the next revision
3672          *
3673          * @param $revId \type{\int} Revision ID. Get the revision that was after this one.
3674          * @param $flags \type{\int} Title::GAID_FOR_UPDATE
3675          * @return \twotypes{\int,\bool} Next revision ID, or FALSE if none exists
3676          */
3677         public function getNextRevisionID( $revId, $flags = 0 ) {
3678                 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3679                 return $db->selectField( 'revision', 'rev_id',
3680                         array(
3681                                 'rev_page' => $this->getArticleId( $flags ),
3682                                 'rev_id > ' . intval( $revId )
3683                         ),
3684                         __METHOD__,
3685                         array( 'ORDER BY' => 'rev_id' )
3686                 );
3687         }
3688
3689         /**
3690          * Get the first revision of the page
3691          *
3692          * @param $flags \type{\int} Title::GAID_FOR_UPDATE
3693          * @return Revision (or NULL if page doesn't exist)
3694          */
3695         public function getFirstRevision( $flags = 0 ) {
3696                 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3697                 $pageId = $this->getArticleId( $flags );
3698                 if ( !$pageId ) {
3699                         return null;
3700                 }
3701                 $row = $db->selectRow( 'revision', '*',
3702                         array( 'rev_page' => $pageId ),
3703                         __METHOD__,
3704                         array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 )
3705                 );
3706                 if ( !$row ) {
3707                         return null;
3708                 } else {
3709                         return new Revision( $row );
3710                 }
3711         }
3712
3713         /**
3714          * Check if this is a new page
3715          *
3716          * @return bool
3717          */
3718         public function isNewPage() {
3719                 $dbr = wfGetDB( DB_SLAVE );
3720                 return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
3721         }
3722
3723         /**
3724          * Get the oldest revision timestamp of this page
3725          *
3726          * @return String: MW timestamp
3727          */
3728         public function getEarliestRevTime() {
3729                 $dbr = wfGetDB( DB_SLAVE );
3730                 if ( $this->exists() ) {
3731                         $min = $dbr->selectField( 'revision',
3732                                 'MIN(rev_timestamp)',
3733                                 array( 'rev_page' => $this->getArticleId() ),
3734                                 __METHOD__ );
3735                         return wfTimestampOrNull( TS_MW, $min );
3736                 }
3737                 return null;
3738         }
3739
3740         /**
3741          * Get the number of revisions between the given revision IDs.
3742          * Used for diffs and other things that really need it.
3743          *
3744          * @param $old \type{\int} Revision ID.
3745          * @param $new \type{\int} Revision ID.
3746          * @return \type{\int} Number of revisions between these IDs.
3747          */
3748         public function countRevisionsBetween( $old, $new ) {
3749                 $dbr = wfGetDB( DB_SLAVE );
3750                 return (int)$dbr->selectField( 'revision', 'count(*)',
3751                         'rev_page = ' . intval( $this->getArticleId() ) .
3752                         ' AND rev_id > ' . intval( $old ) .
3753                         ' AND rev_id < ' . intval( $new ),
3754                         __METHOD__
3755                 );
3756         }
3757
3758         /**
3759          * Get the number of authors between the given revision IDs.
3760          * Used for diffs and other things that really need it.
3761          *
3762          * @param $fromRevId \type{\int} Revision ID (first before range)
3763          * @param $toRevId \type{\int} Revision ID (first after range)
3764          * @param $limit \type{\int} Maximum number of authors
3765          * @param $flags \type{\int} Title::GAID_FOR_UPDATE
3766          * @return \type{\int}
3767          */
3768         public function countAuthorsBetween( $fromRevId, $toRevId, $limit, $flags = 0 ) {
3769                 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3770                 $res = $db->select( 'revision', 'DISTINCT rev_user_text',
3771                         array(
3772                                 'rev_page = ' . $this->getArticleID(),
3773                                 'rev_id > ' . (int)$fromRevId,
3774                                 'rev_id < ' . (int)$toRevId
3775                         ), __METHOD__,
3776                         array( 'LIMIT' => $limit )
3777                 );
3778                 return (int)$db->numRows( $res );
3779         }
3780
3781         /**
3782          * Compare with another title.
3783          *
3784          * @param $title \type{Title}
3785          * @return \type{\bool} TRUE or FALSE
3786          */
3787         public function equals( Title $title ) {
3788                 // Note: === is necessary for proper matching of number-like titles.
3789                 return $this->getInterwiki() === $title->getInterwiki()
3790                         && $this->getNamespace() == $title->getNamespace()
3791                         && $this->getDBkey() === $title->getDBkey();
3792         }
3793
3794         /**
3795          * Callback for usort() to do title sorts by (namespace, title)
3796          * 
3797          * @return Integer: result of string comparison, or namespace comparison
3798          */
3799         public static function compare( $a, $b ) {
3800                 if ( $a->getNamespace() == $b->getNamespace() ) {
3801                         return strcmp( $a->getText(), $b->getText() );
3802                 } else {
3803                         return $a->getNamespace() - $b->getNamespace();
3804                 }
3805         }
3806
3807         /**
3808          * Return a string representation of this title
3809          *
3810          * @return \type{\string} String representation of this title
3811          */
3812         public function __toString() {
3813                 return $this->getPrefixedText();
3814         }
3815
3816         /**
3817          * Check if page exists.  For historical reasons, this function simply
3818          * checks for the existence of the title in the page table, and will
3819          * thus return false for interwiki links, special pages and the like.
3820          * If you want to know if a title can be meaningfully viewed, you should
3821          * probably call the isKnown() method instead.
3822          *
3823          * @return \type{\bool}
3824          */
3825         public function exists() {
3826                 return $this->getArticleId() != 0;
3827         }
3828
3829         /**
3830          * Should links to this title be shown as potentially viewable (i.e. as
3831          * "bluelinks"), even if there's no record by this title in the page
3832          * table?
3833          *
3834          * This function is semi-deprecated for public use, as well as somewhat
3835          * misleadingly named.  You probably just want to call isKnown(), which
3836          * calls this function internally.
3837          *
3838          * (ISSUE: Most of these checks are cheap, but the file existence check
3839          * can potentially be quite expensive.  Including it here fixes a lot of
3840          * existing code, but we might want to add an optional parameter to skip
3841          * it and any other expensive checks.)
3842          *
3843          * @return \type{\bool}
3844          */
3845         public function isAlwaysKnown() {
3846                 if ( $this->mInterwiki != '' ) {
3847                         return true;  // any interwiki link might be viewable, for all we know
3848                 }
3849                 switch( $this->mNamespace ) {
3850                         case NS_MEDIA:
3851                         case NS_FILE:
3852                                 return (bool)wfFindFile( $this );  // file exists, possibly in a foreign repo
3853                         case NS_SPECIAL:
3854                                 return SpecialPage::exists( $this->getDBkey() );  // valid special page
3855                         case NS_MAIN:
3856                                 return $this->mDbkeyform == '';  // selflink, possibly with fragment
3857                         case NS_MEDIAWIKI:
3858                                 // If the page is form Mediawiki:message/lang, calling wfMsgWeirdKey causes
3859                                 // the full l10n of that language to be loaded. That takes much memory and
3860                                 // isn't needed. So we strip the language part away.
3861                                 list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 );
3862                                 return (bool)wfMsgWeirdKey( $basename );  // known system message
3863                         default:
3864                                 return false;
3865                 }
3866         }
3867
3868         /**
3869          * Does this title refer to a page that can (or might) be meaningfully
3870          * viewed?  In particular, this function may be used to determine if
3871          * links to the title should be rendered as "bluelinks" (as opposed to
3872          * "redlinks" to non-existent pages).
3873          *
3874          * @return \type{\bool}
3875          */
3876         public function isKnown() {
3877                 return $this->isAlwaysKnown() || $this->exists();
3878         }
3879
3880         /**
3881          * Does this page have source text?
3882          *
3883          * @return Boolean
3884          */
3885         public function hasSourceText() {
3886                 if ( $this->exists() ) {
3887                         return true;
3888                 }
3889
3890                 if ( $this->mNamespace == NS_MEDIAWIKI ) {
3891                         // If the page doesn't exist but is a known system message, default
3892                         // message content will be displayed, same for language subpages
3893                         // Also, if the page is form Mediawiki:message/lang, calling wfMsgWeirdKey
3894                         // causes the full l10n of that language to be loaded. That takes much
3895                         // memory and isn't needed. So we strip the language part away.
3896                         list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 );
3897                         return (bool)wfMsgWeirdKey( $basename );
3898                 }
3899
3900                 return false;
3901         }
3902
3903         /**
3904          * Is this in a namespace that allows actual pages?
3905          *
3906          * @return \type{\bool}
3907          * @internal note -- uses hardcoded namespace index instead of constants
3908          */
3909         public function canExist() {
3910                 return $this->mNamespace >= 0 && $this->mNamespace != NS_MEDIA;
3911         }
3912
3913         /**
3914          * Update page_touched timestamps and send squid purge messages for
3915          * pages linking to this title. May be sent to the job queue depending
3916          * on the number of links. Typically called on create and delete.
3917          */
3918         public function touchLinks() {
3919                 $u = new HTMLCacheUpdate( $this, 'pagelinks' );
3920                 $u->doUpdate();
3921
3922                 if ( $this->getNamespace() == NS_CATEGORY ) {
3923                         $u = new HTMLCacheUpdate( $this, 'categorylinks' );
3924                         $u->doUpdate();
3925                 }
3926         }
3927
3928         /**
3929          * Get the last touched timestamp
3930          *
3931          * @param $db DatabaseBase: optional db
3932          * @return \type{\string} Last touched timestamp
3933          */
3934         public function getTouched( $db = null ) {
3935                 $db = isset( $db ) ? $db : wfGetDB( DB_SLAVE );
3936                 $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
3937                 return $touched;
3938         }
3939
3940         /**
3941          * Get the timestamp when this page was updated since the user last saw it.
3942          *
3943          * @param $user User
3944          * @return Mixed: string/null
3945          */
3946         public function getNotificationTimestamp( $user = null ) {
3947                 global $wgUser, $wgShowUpdatedMarker;
3948                 // Assume current user if none given
3949                 if ( !$user ) {
3950                         $user = $wgUser;
3951                 }
3952                 // Check cache first
3953                 $uid = $user->getId();
3954                 // avoid isset here, as it'll return false for null entries
3955                 if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
3956                         return $this->mNotificationTimestamp[$uid];
3957                 }
3958                 if ( !$uid || !$wgShowUpdatedMarker ) {
3959                         return $this->mNotificationTimestamp[$uid] = false;
3960                 }
3961                 // Don't cache too much!
3962                 if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
3963                         $this->mNotificationTimestamp = array();
3964                 }
3965                 $dbr = wfGetDB( DB_SLAVE );
3966                 $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist',
3967                         'wl_notificationtimestamp',
3968                         array( 'wl_namespace' => $this->getNamespace(),
3969                                 'wl_title' => $this->getDBkey(),
3970                                 'wl_user' => $user->getId()
3971                         ),
3972                         __METHOD__
3973                 );
3974                 return $this->mNotificationTimestamp[$uid];
3975         }
3976
3977         /**
3978          * Get the trackback URL for this page
3979          *
3980          * @return \type{\string} Trackback URL
3981          */
3982         public function trackbackURL() {
3983                 global $wgScriptPath, $wgServer, $wgScriptExtension;
3984
3985                 return "$wgServer$wgScriptPath/trackback$wgScriptExtension?article="
3986                         . htmlspecialchars( urlencode( $this->getPrefixedDBkey() ) );
3987         }
3988
3989         /**
3990          * Get the trackback RDF for this page
3991          *
3992          * @return \type{\string} Trackback RDF
3993          */
3994         public function trackbackRDF() {
3995                 $url = htmlspecialchars( $this->getFullURL() );
3996                 $title = htmlspecialchars( $this->getText() );
3997                 $tburl = $this->trackbackURL();
3998
3999                 // Autodiscovery RDF is placed in comments so HTML validator
4000                 // won't barf. This is a rather icky workaround, but seems
4001                 // frequently used by this kind of RDF thingy.
4002                 //
4003                 // Spec: http://www.sixapart.com/pronet/docs/trackback_spec
4004                 return "<!--
4005 <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"
4006          xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
4007          xmlns:trackback=\"http://madskills.com/public/xml/rss/module/trackback/\">
4008 <rdf:Description
4009    rdf:about=\"$url\"
4010    dc:identifier=\"$url\"
4011    dc:title=\"$title\"
4012    trackback:ping=\"$tburl\" />
4013 </rdf:RDF>
4014 -->";
4015         }
4016
4017         /**
4018          * Generate strings used for xml 'id' names in monobook tabs
4019          *
4020          * @param $prepend string defaults to 'nstab-'
4021          * @return \type{\string} XML 'id' name
4022          */
4023         public function getNamespaceKey( $prepend = 'nstab-' ) {
4024                 global $wgContLang;
4025                 // Gets the subject namespace if this title
4026                 $namespace = MWNamespace::getSubject( $this->getNamespace() );
4027                 // Checks if cononical namespace name exists for namespace
4028                 if ( MWNamespace::exists( $this->getNamespace() ) ) {
4029                         // Uses canonical namespace name
4030                         $namespaceKey = MWNamespace::getCanonicalName( $namespace );
4031                 } else {
4032                         // Uses text of namespace
4033                         $namespaceKey = $this->getSubjectNsText();
4034                 }
4035                 // Makes namespace key lowercase
4036                 $namespaceKey = $wgContLang->lc( $namespaceKey );
4037                 // Uses main
4038                 if ( $namespaceKey == '' ) {
4039                         $namespaceKey = 'main';
4040                 }
4041                 // Changes file to image for backwards compatibility
4042                 if ( $namespaceKey == 'file' ) {
4043                         $namespaceKey = 'image';
4044                 }
4045                 return $prepend . $namespaceKey;
4046         }
4047
4048         /**
4049          * Returns true if this is a special page.
4050          *
4051          * @return boolean
4052          */
4053         public function isSpecialPage() {
4054                 return $this->getNamespace() == NS_SPECIAL;
4055         }
4056
4057         /**
4058          * Returns true if this title resolves to the named special page
4059          *
4060          * @param $name \type{\string} The special page name
4061          * @return boolean
4062          */
4063         public function isSpecial( $name ) {
4064                 if ( $this->getNamespace() == NS_SPECIAL ) {
4065                         list( $thisName, /* $subpage */ ) = SpecialPage::resolveAliasWithSubpage( $this->getDBkey() );
4066                         if ( $name == $thisName ) {
4067                                 return true;
4068                         }
4069                 }
4070                 return false;
4071         }
4072
4073         /**
4074          * If the Title refers to a special page alias which is not the local default,
4075          *
4076          * @return \type{Title} A new Title which points to the local default.
4077          *         Otherwise, returns $this.
4078          */
4079         public function fixSpecialName() {
4080                 if ( $this->getNamespace() == NS_SPECIAL ) {
4081                         $canonicalName = SpecialPage::resolveAlias( $this->mDbkeyform );
4082                         if ( $canonicalName ) {
4083                                 $localName = SpecialPage::getLocalNameFor( $canonicalName );
4084                                 if ( $localName != $this->mDbkeyform ) {
4085                                         return Title::makeTitle( NS_SPECIAL, $localName );
4086                                 }
4087                         }
4088                 }
4089                 return $this;
4090         }
4091
4092         /**
4093          * Is this Title in a namespace which contains content?
4094          * In other words, is this a content page, for the purposes of calculating
4095          * statistics, etc?
4096          *
4097          * @return Boolean
4098          */
4099         public function isContentPage() {
4100                 return MWNamespace::isContent( $this->getNamespace() );
4101         }
4102
4103         /**
4104          * Get all extant redirects to this Title
4105          *
4106          * @param $ns \twotypes{\int,\null} Single namespace to consider;
4107          *            NULL to consider all namespaces
4108          * @return \type{\arrayof{Title}} Redirects to this title
4109          */
4110         public function getRedirectsHere( $ns = null ) {
4111                 $redirs = array();
4112
4113                 $dbr = wfGetDB( DB_SLAVE );
4114                 $where = array(
4115                         'rd_namespace' => $this->getNamespace(),
4116                         'rd_title' => $this->getDBkey(),
4117                         'rd_from = page_id'
4118                 );
4119                 if ( !is_null( $ns ) ) {
4120                         $where['page_namespace'] = $ns;
4121                 }
4122
4123                 $res = $dbr->select(
4124                         array( 'redirect', 'page' ),
4125                         array( 'page_namespace', 'page_title' ),
4126                         $where,
4127                         __METHOD__
4128                 );
4129
4130                 foreach ( $res as $row ) {
4131                         $redirs[] = self::newFromRow( $row );
4132                 }
4133                 return $redirs;
4134         }
4135
4136         /**
4137          * Check if this Title is a valid redirect target
4138          *
4139          * @return \type{\bool}
4140          */
4141         public function isValidRedirectTarget() {
4142                 global $wgInvalidRedirectTargets;
4143
4144                 // invalid redirect targets are stored in a global array, but explicity disallow Userlogout here
4145                 if ( $this->isSpecial( 'Userlogout' ) ) {
4146                         return false;
4147                 }
4148
4149                 foreach ( $wgInvalidRedirectTargets as $target ) {
4150                         if ( $this->isSpecial( $target ) ) {
4151                                 return false;
4152                         }
4153                 }
4154
4155                 return true;
4156         }
4157
4158         /**
4159          * Get a backlink cache object
4160          *
4161          * @return object BacklinkCache
4162          */
4163         function getBacklinkCache() {
4164                 if ( is_null( $this->mBacklinkCache ) ) {
4165                         $this->mBacklinkCache = new BacklinkCache( $this );
4166                 }
4167                 return $this->mBacklinkCache;
4168         }
4169
4170         /**
4171          * Whether the magic words __INDEX__ and __NOINDEX__ function for
4172          * this page.
4173          *
4174          * @return Boolean
4175          */
4176         public function canUseNoindex() {
4177                 global $wgContentNamespaces, $wgExemptFromUserRobotsControl;
4178
4179                 $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
4180                         ? $wgContentNamespaces
4181                         : $wgExemptFromUserRobotsControl;
4182
4183                 return !in_array( $this->mNamespace, $bannedNamespaces );
4184
4185         }
4186
4187         /**
4188          * Returns restriction types for the current Title
4189          *
4190          * @return array applicable restriction types
4191          */
4192         public function getRestrictionTypes() {
4193                 $types = self::getFilteredRestrictionTypes( $this->exists() );
4194                 
4195                 if ( $this->getNamespace() != NS_FILE ) {
4196                         # Remove the upload restriction for non-file titles
4197                         $types = array_diff( $types, array( 'upload' ) );
4198                 }
4199                 
4200                 wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) );
4201                 
4202                 wfDebug( __METHOD__ . ': applicable restriction types for ' . 
4203                         $this->getPrefixedText() . ' are ' . implode( ',', $types ) );
4204
4205                 return $types;
4206         }
4207         /**
4208          * Get a filtered list of all restriction types supported by this wiki. 
4209          * @param bool $exists True to get all restriction types that apply to 
4210          * titles that do exist, False for all restriction types that apply to
4211          * titles that do not exist
4212          * @return array
4213          */
4214         public static function getFilteredRestrictionTypes( $exists = true ) {
4215                 global $wgRestrictionTypes;
4216                 $types = $wgRestrictionTypes;
4217                 if ( $exists ) {
4218                         # Remove the create restriction for existing titles
4219                         $types = array_diff( $types, array( 'create' ) );                       
4220                 } else {
4221                         # Only the create and upload restrictions apply to non-existing titles
4222                         $types = array_intersect( $types, array( 'create', 'upload' ) );
4223                 }
4224                 return $types;
4225         }
4226
4227         /**
4228          * Returns the raw sort key to be used for categories, with the specified
4229          * prefix.  This will be fed to Collation::getSortKey() to get a
4230          * binary sortkey that can be used for actual sorting.
4231          *
4232          * @param $prefix string The prefix to be used, specified using
4233          *   {{defaultsort:}} or like [[Category:Foo|prefix]].  Empty for no
4234          *   prefix.
4235          * @return string
4236          */
4237         public function getCategorySortkey( $prefix = '' ) {
4238                 $unprefixed = $this->getText();
4239                 if ( $prefix !== '' ) {
4240                         # Separate with a line feed, so the unprefixed part is only used as
4241                         # a tiebreaker when two pages have the exact same prefix.
4242                         # In UCA, tab is the only character that can sort above LF
4243                         # so we strip both of them from the original prefix.
4244                         $prefix = strtr( $prefix, "\n\t", '  ' );
4245                         return "$prefix\n$unprefixed";
4246                 }
4247                 return $unprefixed;
4248         }
4249 }