Be consistent in the interface and use Title::quickUserCan() instead of Title::userCa...
[lhc/web/wiklou.git] / includes / Title.php
index b7360eb..769adb9 100644 (file)
@@ -63,6 +63,7 @@ class Title {
        var $mFragment;                   // /< Title fragment (i.e. the bit after the #)
        var $mArticleID = -1;             // /< Article ID, fetched from the link cache on demand
        var $mLatestID = false;           // /< ID of most recent revision
+       private $mEstimateRevisions;      // /< Estimated number of revisions; null of not loaded
        var $mRestrictions = array();     // /< Array of groups allowed to edit this article
        var $mOldRestrictions = false;
        var $mCascadeRestriction;         ///< Cascade restrictions on this page to included templates and images?
@@ -259,8 +260,7 @@ class Title {
         * Load Title object fields from a DB row.
         * If false is given, the title will be treated as non-existing.
         *
-        * @param $row Object|false database row
-        * @return void
+        * @param $row Object|bool database row
         */
        public function loadFromRow( $row ) {
                if ( $row ) { // page found
@@ -475,6 +475,33 @@ class Title {
                return $wgLegalTitleChars;
        }
 
+       /**
+        * Returns a simple regex that will match on characters and sequences invalid in titles.
+        * Note that this doesn't pick up many things that could be wrong with titles, but that
+        * replacing this regex with something valid will make many titles valid.
+        *
+        * @return String regex string
+        */
+       static function getTitleInvalidRegex() {
+               static $rxTc = false;
+               if ( !$rxTc ) {
+                       # Matching titles will be held as illegal.
+                       $rxTc = '/' .
+                               # Any character not allowed is forbidden...
+                               '[^' . self::legalChars() . ']' .
+                               # URL percent encoding sequences interfere with the ability
+                               # to round-trip titles -- you can't link to them consistently.
+                               '|%[0-9A-Fa-f]{2}' .
+                               # XML/HTML character references produce similar issues.
+                               '|&[A-Za-z0-9\x80-\xff]+;' .
+                               '|&#[0-9]+;' .
+                               '|&#x[0-9A-Fa-f]+;' .
+                               '/S';
+               }
+
+               return $rxTc;
+       }
+
        /**
         * Get a string representation of a title suitable for
         * including in a search index
@@ -540,6 +567,22 @@ class Title {
                return Sanitizer::escapeId( $fragment, 'noninitial' );
        }
 
+       /**
+        * Callback for usort() to do title sorts by (namespace, title)
+        *
+        * @param $a Title
+        * @param $b Title
+        *
+        * @return Integer: result of string comparison, or namespace comparison
+        */
+       public static function compare( $a, $b ) {
+               if ( $a->getNamespace() == $b->getNamespace() ) {
+                       return strcmp( $a->getText(), $b->getText() );
+               } else {
+                       return $a->getNamespace() - $b->getNamespace();
+               }
+       }
+
        /**
         * Determine whether the object refers to a page within
         * this project.
@@ -626,6 +669,15 @@ class Title {
                return $this->mDbkeyform;
        }
 
+       /**
+        * Get the DB key with the initial letter case as specified by the user
+        *
+        * @return String DB key
+        */
+       function getUserCaseDBKey() {
+               return $this->mUserCaseDBKey;
+       }
+
        /**
         * Get the namespace index, i.e. one of the NS_xxxx constants.
         *
@@ -672,15 +724,6 @@ class Title {
                return $wgContLang->getNsText( $this->mNamespace );
        }
 
-       /**
-        * Get the DB key with the initial letter case as specified by the user
-        *
-        * @return String DB key
-        */
-       function getUserCaseDBKey() {
-               return $this->mUserCaseDBKey;
-       }
-
        /**
         * Get the namespace text of the subject (rather than talk) page
         *
@@ -711,172 +754,529 @@ class Title {
        }
 
        /**
-        * Get the Title fragment (i.e.\ the bit after the #) in text form
+        * Is this in a namespace that allows actual pages?
         *
-        * @return String Title fragment
-        */
-       public function getFragment() { return $this->mFragment; }
-
-       /**
-        * Get the fragment in URL form, including the "#" character if there is one
-        * @return String Fragment in URL form
+        * @return Bool
+        * @internal note -- uses hardcoded namespace index instead of constants
         */
-       public function getFragmentForURL() {
-               if ( $this->mFragment == '' ) {
-                       return '';
-               } else {
-                       return '#' . Title::escapeFragmentForURL( $this->mFragment );
-               }
+       public function canExist() {
+               return $this->mNamespace >= NS_MAIN;
        }
 
        /**
-        * Get the default namespace index, for when there is no namespace
+        * Can this title be added to a user's watchlist?
         *
-        * @return Int Default namespace index
+        * @return Bool TRUE or FALSE
         */
-       public function getDefaultNamespace() {
-               return $this->mDefaultNamespace;
+       public function isWatchable() {
+               return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
        }
 
        /**
-        * Get title for search index
+        * Returns true if this is a special page.
         *
-        * @return String a stripped-down title string ready for the
-        *  search index
+        * @return boolean
         */
-       public function getIndexTitle() {
-               return Title::indexTitle( $this->mNamespace, $this->mTextform );
+       public function isSpecialPage() {
+               return $this->getNamespace() == NS_SPECIAL;
        }
 
        /**
-        * Get the prefixed database key form
+        * Returns true if this title resolves to the named special page
         *
-        * @return String the prefixed title, with underscores and
-        *  any interwiki and namespace prefixes
+        * @param $name String The special page name
+        * @return boolean
         */
-       public function getPrefixedDBkey() {
-               $s = $this->prefix( $this->mDbkeyform );
-               $s = str_replace( ' ', '_', $s );
-               return $s;
+       public function isSpecial( $name ) {
+               if ( $this->isSpecialPage() ) {
+                       list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() );
+                       if ( $name == $thisName ) {
+                               return true;
+                       }
+               }
+               return false;
        }
 
        /**
-        * Get the prefixed title with spaces.
-        * This is the form usually used for display
+        * If the Title refers to a special page alias which is not the local default, resolve
+        * the alias, and localise the name as necessary.  Otherwise, return $this
         *
-        * @return String the prefixed title, with spaces
+        * @return Title
         */
-       public function getPrefixedText() {
-               // @todo FIXME: Bad usage of empty() ?
-               if ( empty( $this->mPrefixedText ) ) {
-                       $s = $this->prefix( $this->mTextform );
-                       $s = str_replace( '_', ' ', $s );
-                       $this->mPrefixedText = $s;
+       public function fixSpecialName() {
+               if ( $this->isSpecialPage() ) {
+                       list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform );
+                       if ( $canonicalName ) {
+                               $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par );
+                               if ( $localName != $this->mDbkeyform ) {
+                                       return Title::makeTitle( NS_SPECIAL, $localName );
+                               }
+                       }
                }
-               return $this->mPrefixedText;
+               return $this;
        }
 
        /**
-       /**
-        * Get the prefixed title with spaces, plus any fragment
-        * (part beginning with '#')
-        *
-        * @return String the prefixed title, with spaces and the fragment, including '#'
+        * Returns true if the title is inside the specified namespace.
+        * 
+        * Please make use of this instead of comparing to getNamespace()
+        * This function is much more resistant to changes we may make
+        * to namespaces than code that makes direct comparisons.
+        * @param $ns int The namespace
+        * @return bool
+        * @since 1.19
         */
-       public function getFullText() {
-               $text = $this->getPrefixedText();
-               if ( $this->mFragment != '' ) {
-                       $text .= '#' . $this->mFragment;
-               }
-               return $text;
+       public function inNamespace( $ns ) {
+               return MWNamespace::equals( $this->getNamespace(), $ns );
        }
 
        /**
-        * Get the base page name, i.e. the leftmost part before any slashes
+        * Returns true if the title is inside one of the specified namespaces.
         *
-        * @return String Base name
+        * @param ...$namespaces The namespaces to check for
+        * @return bool
+        * @since 1.19
         */
-       public function getBaseText() {
-               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
-                       return $this->getText();
+       public function inNamespaces( /* ... */ ) {
+               $namespaces = func_get_args();
+               if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
+                       $namespaces = $namespaces[0];
                }
 
-               $parts = explode( '/', $this->getText() );
-               # Don't discard the real title if there's no subpage involved
-               if ( count( $parts ) > 1 ) {
-                       unset( $parts[count( $parts ) - 1] );
+               foreach ( $namespaces as $ns ) {
+                       if ( $this->inNamespace( $ns ) ) {
+                               return true;
+                       }
                }
-               return implode( '/', $parts );
+
+               return false;
        }
 
        /**
-        * Get the lowest-level subpage name, i.e. the rightmost part after any slashes
+        * Returns true if the title has the same subject namespace as the
+        * namespace specified.
+        * For example this method will take NS_USER and return true if namespace
+        * is either NS_USER or NS_USER_TALK since both of them have NS_USER
+        * as their subject namespace.
         *
-        * @return String Subpage name
+        * This is MUCH simpler than individually testing for equivilance
+        * against both NS_USER and NS_USER_TALK, and is also forward compatible.
+        * @since 1.19
+        * @return bool
         */
-       public function getSubpageText() {
-               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
-                       return( $this->mTextform );
-               }
-               $parts = explode( '/', $this->mTextform );
-               return( $parts[count( $parts ) - 1] );
+       public function hasSubjectNamespace( $ns ) {
+               return MWNamespace::subjectEquals( $this->getNamespace(), $ns );
        }
 
        /**
-        * Get the HTML-escaped displayable text form.
-        * Used for the title field in <a> tags.
+        * Is this Title in a namespace which contains content?
+        * In other words, is this a content page, for the purposes of calculating
+        * statistics, etc?
         *
-        * @return String the text, including any prefixes
+        * @return Boolean
         */
-       public function getEscapedText() {
-               return htmlspecialchars( $this->getPrefixedText() );
+       public function isContentPage() {
+               return MWNamespace::isContent( $this->getNamespace() );
        }
 
        /**
-        * Get a URL-encoded form of the subpage text
+        * Would anybody with sufficient privileges be able to move this page?
+        * Some pages just aren't movable.
         *
-        * @return String URL-encoded subpage name
+        * @return Bool TRUE or FALSE
         */
-       public function getSubpageUrlForm() {
-               $text = $this->getSubpageText();
-               $text = wfUrlencode( str_replace( ' ', '_', $text ) );
-               return( $text );
+       public function isMovable() {
+               if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->getInterwiki() != '' ) {
+                       // Interwiki title or immovable namespace. Hooks don't get to override here
+                       return false;
+               }
+
+               $result = true;
+               wfRunHooks( 'TitleIsMovable', array( $this, &$result ) );
+               return $result;
        }
 
        /**
-        * Get a URL-encoded title (not an actual URL) including interwiki
+        * Is this the mainpage?
+        * @note Title::newFromText seams to be sufficiently optimized by the title
+        * cache that we don't need to over-optimize by doing direct comparisons and
+        * acidentally creating new bugs where $title->equals( Title::newFromText() )
+        * ends up reporting something differently than $title->isMainPage();
         *
-        * @return String the URL-encoded form
+        * @since 1.18
+        * @return Bool
         */
-       public function getPrefixedURL() {
-               $s = $this->prefix( $this->mDbkeyform );
-               $s = wfUrlencode( str_replace( ' ', '_', $s ) );
-               return $s;
+       public function isMainPage() {
+               return $this->equals( Title::newMainPage() );
        }
 
        /**
-        * Get a real URL referring to this title, with interwiki link and
-        * fragment
+        * Is this a subpage?
         *
-        * @param $query \twotypes{\string,\array} an optional query string, not used for interwiki
-        *   links. Can be specified as an associative array as well, e.g.,
-        *   array( 'action' => 'edit' ) (keys and values will be URL-escaped).
-        * @param $variant String language variant of url (for sr, zh..)
-        * @return String the URL
+        * @return Bool
         */
-       public function getFullURL( $query = '', $variant = false ) {
-               # Hand off all the decisions on urls to getLocalURL
-               $url = $this->getLocalURL( $query, $variant );
+       public function isSubpage() {
+               return MWNamespace::hasSubpages( $this->mNamespace )
+                       ? strpos( $this->getText(), '/' ) !== false
+                       : false;
+       }
 
-               # Expand the url to make it a full url. Note that getLocalURL has the
-               # potential to output full urls for a variety of reasons, so we use
+       /**
+        * Is this a conversion table for the LanguageConverter?
+        *
+        * @return Bool
+        */
+       public function isConversionTable() {
+               return $this->getNamespace() == NS_MEDIAWIKI &&
+                       strpos( $this->getText(), 'Conversiontable' ) !== false;
+       }
+
+       /**
+        * Does that page contain wikitext, or it is JS, CSS or whatever?
+        *
+        * @return Bool
+        */
+       public function isWikitextPage() {
+               $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
+               wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
+               return $retval;
+       }
+
+       /**
+        * Could this page contain custom CSS or JavaScript, based
+        * on the title?
+        *
+        * @return Bool
+        */
+       public function isCssOrJsPage() {
+               $retval = $this->mNamespace == NS_MEDIAWIKI
+                       && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
+               wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
+               return $retval;
+       }
+
+       /**
+        * Is this a .css or .js subpage of a user page?
+        * @return Bool
+        */
+       public function isCssJsSubpage() {
+               return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
+       }
+
+       /**
+        * Trim down a .css or .js subpage title to get the corresponding skin name
+        *
+        * @return string containing skin name from .css or .js subpage title
+        */
+       public function getSkinFromCssJsSubpage() {
+               $subpage = explode( '/', $this->mTextform );
+               $subpage = $subpage[ count( $subpage ) - 1 ];
+               $lastdot = strrpos( $subpage, '.' );
+               if ( $lastdot === false )
+                       return $subpage; # Never happens: only called for names ending in '.css' or '.js'
+               return substr( $subpage, 0, $lastdot );
+       }
+
+       /**
+        * Is this a .css subpage of a user page?
+        *
+        * @return Bool
+        */
+       public function isCssSubpage() {
+               return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
+       }
+
+       /**
+        * Is this a .js subpage of a user page?
+        *
+        * @return Bool
+        */
+       public function isJsSubpage() {
+               return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
+       }
+
+       /**
+        * Is this a talk page of some sort?
+        *
+        * @return Bool
+        */
+       public function isTalkPage() {
+               return MWNamespace::isTalk( $this->getNamespace() );
+       }
+
+       /**
+        * Get a Title object associated with the talk page of this article
+        *
+        * @return Title the object for the talk page
+        */
+       public function getTalkPage() {
+               return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
+       }
+
+       /**
+        * Get a title object associated with the subject page of this
+        * talk page
+        *
+        * @return Title the object for the subject page
+        */
+       public function getSubjectPage() {
+               // Is this the same title?
+               $subjectNS = MWNamespace::getSubject( $this->getNamespace() );
+               if ( $this->getNamespace() == $subjectNS ) {
+                       return $this;
+               }
+               return Title::makeTitle( $subjectNS, $this->getDBkey() );
+       }
+
+       /**
+        * Get the default namespace index, for when there is no namespace
+        *
+        * @return Int Default namespace index
+        */
+       public function getDefaultNamespace() {
+               return $this->mDefaultNamespace;
+       }
+
+       /**
+        * Get title for search index
+        *
+        * @return String a stripped-down title string ready for the
+        *  search index
+        */
+       public function getIndexTitle() {
+               return Title::indexTitle( $this->mNamespace, $this->mTextform );
+       }
+
+       /**
+        * Get the Title fragment (i.e.\ the bit after the #) in text form
+        *
+        * @return String Title fragment
+        */
+       public function getFragment() {
+               return $this->mFragment;
+       }
+
+       /**
+        * Get the fragment in URL form, including the "#" character if there is one
+        * @return String Fragment in URL form
+        */
+       public function getFragmentForURL() {
+               if ( $this->mFragment == '' ) {
+                       return '';
+               } else {
+                       return '#' . Title::escapeFragmentForURL( $this->mFragment );
+               }
+       }
+
+       /**
+        * Set the fragment for this title. Removes the first character from the
+        * specified fragment before setting, so it assumes you're passing it with
+        * an initial "#".
+        *
+        * Deprecated for public use, use Title::makeTitle() with fragment parameter.
+        * Still in active use privately.
+        *
+        * @param $fragment String text
+        */
+       public function setFragment( $fragment ) {
+               $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) );
+       }
+
+       /**
+        * Prefix some arbitrary text with the namespace or interwiki prefix
+        * of this object
+        *
+        * @param $name String the text
+        * @return String the prefixed text
+        * @private
+        */
+       private function prefix( $name ) {
+               $p = '';
+               if ( $this->mInterwiki != '' ) {
+                       $p = $this->mInterwiki . ':';
+               }
+
+               if ( 0 != $this->mNamespace ) {
+                       $p .= $this->getNsText() . ':';
+               }
+               return $p . $name;
+       }
+
+       /**
+        * Get the prefixed database key form
+        *
+        * @return String the prefixed title, with underscores and
+        *  any interwiki and namespace prefixes
+        */
+       public function getPrefixedDBkey() {
+               $s = $this->prefix( $this->mDbkeyform );
+               $s = str_replace( ' ', '_', $s );
+               return $s;
+       }
+
+       /**
+        * Get the prefixed title with spaces.
+        * This is the form usually used for display
+        *
+        * @return String the prefixed title, with spaces
+        */
+       public function getPrefixedText() {
+               // @todo FIXME: Bad usage of empty() ?
+               if ( empty( $this->mPrefixedText ) ) {
+                       $s = $this->prefix( $this->mTextform );
+                       $s = str_replace( '_', ' ', $s );
+                       $this->mPrefixedText = $s;
+               }
+               return $this->mPrefixedText;
+       }
+
+       /**
+        * Return a string representation of this title
+        *
+        * @return String representation of this title
+        */
+       public function __toString() {
+               return $this->getPrefixedText();
+       }
+
+       /**
+        * Get the prefixed title with spaces, plus any fragment
+        * (part beginning with '#')
+        *
+        * @return String the prefixed title, with spaces and the fragment, including '#'
+        */
+       public function getFullText() {
+               $text = $this->getPrefixedText();
+               if ( $this->mFragment != '' ) {
+                       $text .= '#' . $this->mFragment;
+               }
+               return $text;
+       }
+
+       /**
+        * Get the base page name, i.e. the leftmost part before any slashes
+        *
+        * @return String Base name
+        */
+       public function getBaseText() {
+               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+                       return $this->getText();
+               }
+
+               $parts = explode( '/', $this->getText() );
+               # Don't discard the real title if there's no subpage involved
+               if ( count( $parts ) > 1 ) {
+                       unset( $parts[count( $parts ) - 1] );
+               }
+               return implode( '/', $parts );
+       }
+
+       /**
+        * Get the lowest-level subpage name, i.e. the rightmost part after any slashes
+        *
+        * @return String Subpage name
+        */
+       public function getSubpageText() {
+               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+                       return( $this->mTextform );
+               }
+               $parts = explode( '/', $this->mTextform );
+               return( $parts[count( $parts ) - 1] );
+       }
+
+       /**
+        * Get the HTML-escaped displayable text form.
+        * Used for the title field in <a> tags.
+        *
+        * @return String the text, including any prefixes
+        */
+       public function getEscapedText() {
+               wfDeprecated( __METHOD__, '1.19' );
+               return htmlspecialchars( $this->getPrefixedText() );
+       }
+
+       /**
+        * Get a URL-encoded form of the subpage text
+        *
+        * @return String URL-encoded subpage name
+        */
+       public function getSubpageUrlForm() {
+               $text = $this->getSubpageText();
+               $text = wfUrlencode( str_replace( ' ', '_', $text ) );
+               return( $text );
+       }
+
+       /**
+        * Get a URL-encoded title (not an actual URL) including interwiki
+        *
+        * @return String the URL-encoded form
+        */
+       public function getPrefixedURL() {
+               $s = $this->prefix( $this->mDbkeyform );
+               $s = wfUrlencode( str_replace( ' ', '_', $s ) );
+               return $s;
+       }
+
+       /**
+        * Helper to fix up the get{Local,Full,Link,Canonical}URL args
+        * get{Canonical,Full,Link,Local}URL methods accepted an optional
+        * second argument named variant. This was deprecated in favor
+        * of passing an array of option with a "variant" key
+        * Once $query2 is removed for good, this helper can be dropped
+        * andthe wfArrayToCGI moved to getLocalURL();
+        *
+        * @since 1.19 (r105919)
+        * @return String
+        */
+       private static function fixUrlQueryArgs( $query, $query2 = false ) {
+               if( $query2 !== false ) {
+                       wfDeprecated( "Title::get{Canonical,Full,Link,Local} method called with a second parameter is deprecated. Add your parameter to an array passed as the first parameter.", "1.19" );
+               }
+               if ( is_array( $query ) ) {
+                       $query = wfArrayToCGI( $query );
+               }
+               if ( $query2 ) {
+                       if ( is_string( $query2 ) ) {
+                               // $query2 is a string, we will consider this to be
+                               // a deprecated $variant argument and add it to the query
+                               $query2 = wfArrayToCGI( array( 'variant' => $query2 ) );
+                       } else {
+                               $query2 = wfArrayToCGI( $query2 );
+                       }
+                       // If we have $query content add a & to it first
+                       if ( $query ) {
+                               $query .= '&';
+                       }
+                       // Now append the queries together
+                       $query .= $query2;
+               }
+               return $query;
+       }
+
+       /**
+        * Get a real URL referring to this title, with interwiki link and
+        * fragment
+        *
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
+        * @return String the URL
+        */
+       public function getFullURL( $query = '', $query2 = false ) {
+               $query = self::fixUrlQueryArgs( $query, $query2 );
+
+               # Hand off all the decisions on urls to getLocalURL
+               $url = $this->getLocalURL( $query );
+
+               # Expand the url to make it a full url. Note that getLocalURL has the
+               # potential to output full urls for a variety of reasons, so we use
                # wfExpandUrl instead of simply prepending $wgServer
                $url = wfExpandUrl( $url, PROTO_RELATIVE );
 
                # Finally, add the fragment.
                $url .= $this->getFragmentForURL();
 
-               wfRunHooks( 'GetFullURL', array( &$this, &$url, $query, $variant ) );
+               wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) );
                return $url;
        }
 
@@ -884,20 +1284,24 @@ class Title {
         * Get a URL with no fragment or server name.  If this page is generated
         * with action=render, $wgServer is prepended.
         *
-        * @param $query Mixed: an optional query string; if not specified,
-        *   $wgArticlePath will be used.  Can be specified as an associative array
-        *   as well, e.g., array( 'action' => 'edit' ) (keys and values will be
-        *   URL-escaped).
-        * @param $variant String language variant of url (for sr, zh..)
+
+        * @param $query string|array an optional query string,
+        *   not used for interwiki     links. Can be specified as an associative array as well,
+        *   e.g., array( 'action' => 'edit' ) (keys and values will be URL-escaped).
+        *   Some query patterns will trigger various shorturl path replacements.
+        * @param $query2 Mixed: An optional secondary query array. This one MUST
+        *   be an array. If a string is passed it will be interpreted as a deprecated
+        *   variant argument and urlencoded into a variant= argument.
+        *   This second query argument will be added to the $query
+        *   The second parameter is deprecated since 1.19. Pass it as a key,value
+        *   pair in the first parameter array instead.
+        *
         * @return String the URL
         */
-       public function getLocalURL( $query = '', $variant = false ) {
+       public function getLocalURL( $query = '', $query2 = false ) {
                global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
-               global $wgVariantArticlePath;
 
-               if ( is_array( $query ) ) {
-                       $query = wfArrayToCGI( $query );
-               }
+               $query = self::fixUrlQueryArgs( $query, $query2 );
 
                $interwiki = Interwiki::fetch( $this->mInterwiki );
                if ( $interwiki ) {
@@ -912,22 +1316,13 @@ class Title {
                } else {
                        $dbkey = wfUrlencode( $this->getPrefixedDBkey() );
                        if ( $query == '' ) {
-                               if ( $variant != false && $this->getPageLanguage()->hasVariants() ) {
-                                       if ( !$wgVariantArticlePath ) {
-                                               $variantArticlePath =  "$wgScript?title=$1&variant=$2"; // default
-                                       } else {
-                                               $variantArticlePath = $wgVariantArticlePath;
-                                       }
-                                       $url = str_replace( '$2', urlencode( $variant ), $variantArticlePath );
-                                       $url = str_replace( '$1', $dbkey, $url  );
-                               } else {
-                                       $url = str_replace( '$1', $dbkey, $wgArticlePath );
-                                       wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) );
-                               }
+                               $url = str_replace( '$1', $dbkey, $wgArticlePath );
+                               wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) );
                        } else {
-                               global $wgActionPaths;
+                               global $wgVariantArticlePath, $wgActionPaths;
                                $url = false;
                                $matches = array();
+
                                if ( !empty( $wgActionPaths ) &&
                                        preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) )
                                {
@@ -944,6 +1339,20 @@ class Title {
                                        }
                                }
 
+                               if ( $url === false &&
+                                       $wgVariantArticlePath &&
+                                       $this->getPageLanguage()->hasVariants() &&
+                                       preg_match( '/^variant=([^&]*)$/', $query, $matches ) )
+                               {
+                                       $variant = urldecode( $matches[1] );
+                                       if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
+                                               // Only do the variant replacement if the given variant is a valid
+                                               // variant for the page's language.
+                                               $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath );
+                                               $url = str_replace( '$1', $dbkey, $url );
+                                       }
+                               }
+
                                if ( $url === false ) {
                                        if ( $query == '-' ) {
                                                $query = '';
@@ -952,7 +1361,7 @@ class Title {
                                }
                        }
 
-                       wfRunHooks( 'GetLocalURL::Internal', array( &$this, &$url, $query, $variant ) );
+                       wfRunHooks( 'GetLocalURL::Internal', array( &$this, &$url, $query ) );
 
                        // @todo FIXME: This causes breakage in various places when we
                        // actually expected a local URL and end up with dupe prefixes.
@@ -960,7 +1369,7 @@ class Title {
                                $url = $wgServer . $url;
                        }
                }
-               wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query, $variant ) );
+               wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) );
                return $url;
        }
 
@@ -974,21 +1383,19 @@ class Title {
         * The result obviously should not be URL-escaped, but does need to be
         * HTML-escaped if it's being output in HTML.
         *
-        * @param $query Array of Strings An associative array of key => value pairs for the
-        *   query string.  Keys and values will be escaped.
-        * @param $variant String language variant of URL (for sr, zh..).  Ignored
-        *   for external links.  Default is "false" (same variant as current page,
-        *   for anonymous users).
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
         * @return String the URL
         */
-       public function getLinkURL( $query = array(), $variant = false ) {
+       public function getLinkURL( $query = '', $query2 = false ) {
                wfProfileIn( __METHOD__ );
                if ( $this->isExternal() ) {
-                       $ret = $this->getFullURL( $query );
+                       $ret = $this->getFullURL( $query, $query2 );
                } elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) {
                        $ret = $this->getFragmentForURL();
                } else {
-                       $ret = $this->getLocalURL( $query, $variant ) . $this->getFragmentForURL();
+                       $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
                }
                wfProfileOut( __METHOD__ );
                return $ret;
@@ -998,22 +1405,28 @@ class Title {
         * Get an HTML-escaped version of the URL form, suitable for
         * using in a link, without a server name or fragment
         *
-        * @param $query String an optional query string
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
         * @return String the URL
         */
-       public function escapeLocalURL( $query = '' ) {
-               return htmlspecialchars( $this->getLocalURL( $query ) );
+       public function escapeLocalURL( $query = '', $query2 = false ) {
+               wfDeprecated( __METHOD__, '1.19' );
+               return htmlspecialchars( $this->getLocalURL( $query, $query2 ) );
        }
 
        /**
         * Get an HTML-escaped version of the URL form, suitable for
         * using in a link, including the server name and fragment
         *
-        * @param $query String an optional query string
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
         * @return String the URL
         */
-       public function escapeFullURL( $query = '' ) {
-               return htmlspecialchars( $this->getFullURL( $query ) );
+       public function escapeFullURL( $query = '', $query2 = false ) {
+               wfDeprecated( __METHOD__, '1.19' );
+               return htmlspecialchars( $this->getFullURL( $query, $query2 ) );
        }
 
        /**
@@ -1025,15 +1438,17 @@ class Title {
         * if $wgInternalServer is not set. If the server variable used is
         * protocol-relative, the URL will be expanded to http://
         *
-        * @param $query String an optional query string
-        * @param $variant String language variant of url (for sr, zh..)
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
         * @return String the URL
         */
-       public function getInternalURL( $query = '', $variant = false ) {
+       public function getInternalURL( $query = '', $query2 = false ) {
                global $wgInternalServer, $wgServer;
+               $query = self::fixUrlQueryArgs( $query, $query2 );
                $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
-               $url = wfExpandUrl( $server . $this->getLocalURL( $query, $variant ), PROTO_HTTP );
-               wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query, $variant ) );
+               $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
+               wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) );
                return $url;
        }
 
@@ -1044,23 +1459,31 @@ class Title {
         *
         * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment
         *
-        * @param $query string An optional query string
-        * @param $variant string Language variant of URL (for sr, zh, ...)
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
         * @return string The URL
         * @since 1.18
         */
-       public function getCanonicalURL( $query = '', $variant = false ) {
-               $url = wfExpandUrl( $this->getLocalURL( $query, $variant ) . $this->getFragmentForURL(), PROTO_CANONICAL );
-               wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query, $variant ) );
+       public function getCanonicalURL( $query = '', $query2 = false ) {
+               $query = self::fixUrlQueryArgs( $query, $query2 );
+               $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
+               wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query ) );
                return $url;
        }
 
        /**
         * HTML-escaped version of getCanonicalURL()
+        *
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
         * @since 1.18
+        * @return string
         */
-       public function escapeCanonicalURL( $query = '', $variant = false ) {
-               return htmlspecialchars( $this->getCanonicalURL( $query, $variant ) );
+       public function escapeCanonicalURL( $query = '', $query2 = false ) {
+               wfDeprecated( __METHOD__, '1.19' );
+               return htmlspecialchars( $this->getCanonicalURL( $query, $query2 ) );
        }
 
        /**
@@ -1078,101 +1501,6 @@ class Title {
                return $s;
        }
 
-       /**
-        * Is this page "semi-protected" - the *only* protection is autoconfirm?
-        *
-        * @param $action String Action to check (default: edit)
-        * @return Bool
-        */
-       public function isSemiProtected( $action = 'edit' ) {
-               if ( $this->exists() ) {
-                       $restrictions = $this->getRestrictions( $action );
-                       if ( count( $restrictions ) > 0 ) {
-                               foreach ( $restrictions as $restriction ) {
-                                       if ( strtolower( $restriction ) != 'autoconfirmed' ) {
-                                               return false;
-                                       }
-                               }
-                       } else {
-                               # Not protected
-                               return false;
-                       }
-                       return true;
-               } else {
-                       # If it doesn't exist, it can't be protected
-                       return false;
-               }
-       }
-
-       /**
-        * Does the title correspond to a protected article?
-        *
-        * @param $action String the action the page is protected from,
-        * by default checks all actions.
-        * @return Bool
-        */
-       public function isProtected( $action = '' ) {
-               global $wgRestrictionLevels;
-
-               $restrictionTypes = $this->getRestrictionTypes();
-
-               # Special pages have inherent protection
-               if( $this->isSpecialPage() ) {
-                       return true;
-               }
-
-               # Check regular protection levels
-               foreach ( $restrictionTypes as $type ) {
-                       if ( $action == $type || $action == '' ) {
-                               $r = $this->getRestrictions( $type );
-                               foreach ( $wgRestrictionLevels as $level ) {
-                                       if ( in_array( $level, $r ) && $level != '' ) {
-                                               return true;
-                                       }
-                               }
-                       }
-               }
-
-               return false;
-       }
-
-       /**
-        * Determines if $user is unable to edit this page because it has been protected
-        * by $wgNamespaceProtection.
-        *
-        * @param $user User object to check permissions
-        * @return Bool
-        */
-       public function isNamespaceProtected( User $user ) {
-               global $wgNamespaceProtection;
-
-               if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
-                       foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
-                               if ( $right != '' && !$user->isAllowed( $right ) ) {
-                                       return true;
-                               }
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * Is this a conversion table for the LanguageConverter?
-        *
-        * @return Bool
-        */
-       public function isConversionTable() {
-               if(
-                       $this->getNamespace() == NS_MEDIAWIKI &&
-                       strpos( $this->getText(), 'Conversiontable' ) !== false
-               )
-               {
-                       return true;
-               }
-
-               return false;
-       }
-
        /**
         * Is $wgUser watching this page?
         *
@@ -1199,11 +1527,12 @@ class Title {
         * @todo fold these checks into userCan()
         */
        public function userCanRead() {
+               wfDeprecated( __METHOD__, '1.19' );
                return $this->userCan( 'read' );
        }
 
        /**
-        * Can $wgUser perform $action on this page?
+        * Can $user perform $action on this page?
         * This skips potentially expensive cascading permission checks
         * as well as avoids expensive error formatting
         *
@@ -1213,7 +1542,8 @@ class Title {
         * May provide false positives, but should never provide a false negative.
         *
         * @param $action String action that permission needs to be checked for
-        * @param $user User to check (since 1.19)
+        * @param $user User to check (since 1.19); $wgUser will be used if not
+        *              provided.
         * @return Bool
         */
        public function quickUserCan( $action, $user = null ) {
@@ -1221,11 +1551,13 @@ class Title {
        }
 
        /**
-        * Can $wgUser perform $action on this page?
+        * Can $user perform $action on this page?
         *
         * @param $action String action that permission needs to be checked for
-        * @param $user User to check (since 1.19)
-        * @param $doExpensiveQueries Bool Set this to false to avoid doing unnecessary queries.
+        * @param $user User to check (since 1.19); $wgUser will be used if not
+        *   provided.
+        * @param $doExpensiveQueries Bool Set this to false to avoid doing
+        *   unnecessary queries.
         * @return Bool
         */
        public function userCan( $action, $user = null, $doExpensiveQueries = true ) {
@@ -1243,9 +1575,10 @@ class Title {
         *
         * @param $action String action that permission needs to be checked for
         * @param $user User to check
-        * @param $doExpensiveQueries Bool Set this to false to avoid doing unnecessary queries by
-        *   skipping checks for cascading protections and user blocks.
-        * @param $ignoreErrors Array of Strings Set this to a list of message keys whose corresponding errors may be ignored.
+        * @param $doExpensiveQueries Bool Set this to false to avoid doing unnecessary
+        *   queries by skipping checks for cascading protections and user blocks.
+        * @param $ignoreErrors Array of Strings Set this to a list of message keys
+        *   whose corresponding errors may be ignored.
         * @return Array of arguments to wfMsg to explain permissions problems.
         */
        public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) {
@@ -1275,33 +1608,34 @@ class Title {
         * @return Array list of errors
         */
        private function checkQuickPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
-               $ns = $this->getNamespace();
-
                if ( $action == 'create' ) {
-                       if ( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk', $ns ) ) ||
-                                ( !$this->isTalkPage() && !$user->isAllowed( 'createpage', $ns ) ) ) {
+                       if ( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
+                                ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) {
                                $errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' );
                        }
                } elseif ( $action == 'move' ) {
-                       if ( !$user->isAllowed( 'move-rootuserpages', $ns )
-                                       && $ns == NS_USER && !$this->isSubpage() ) {
+                       if ( !$user->isAllowed( 'move-rootuserpages' )
+                                       && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
                                // Show user page-specific message only if the user can move other pages
                                $errors[] = array( 'cant-move-user-page' );
                        }
 
                        // Check if user is allowed to move files if it's a file
-                       if ( $ns == NS_FILE && !$user->isAllowed( 'movefile', $ns ) ) {
+                       if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
                                $errors[] = array( 'movenotallowedfile' );
                        }
 
-                       if ( !$user->isAllowed( 'move', $ns) ) {
+                       if ( !$user->isAllowed( 'move' ) ) {
                                // User can't move anything
-
-                               $userCanMove = in_array( 'move', User::getGroupPermissions(
-                                       array( 'user' ), $ns ), true );
-                               $autoconfirmedCanMove = in_array( 'move', User::getGroupPermissions(
-                                       array( 'autoconfirmed' ), $ns ), true );
-
+                               global $wgGroupPermissions;
+                               $userCanMove = false;
+                               if ( isset( $wgGroupPermissions['user']['move'] ) ) {
+                                       $userCanMove = $wgGroupPermissions['user']['move'];
+                               }
+                               $autoconfirmedCanMove = false;
+                               if ( isset( $wgGroupPermissions['autoconfirmed']['move'] ) ) {
+                                       $autoconfirmedCanMove = $wgGroupPermissions['autoconfirmed']['move'];
+                               }
                                if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
                                        // custom message if logged-in users without any special rights can move
                                        $errors[] = array( 'movenologintext' );
@@ -1310,15 +1644,15 @@ class Title {
                                }
                        }
                } elseif ( $action == 'move-target' ) {
-                       if ( !$user->isAllowed( 'move', $ns ) ) {
+                       if ( !$user->isAllowed( 'move' ) ) {
                                // User can't move anything
                                $errors[] = array( 'movenotallowed' );
-                       } elseif ( !$user->isAllowed( 'move-rootuserpages', $ns )
-                                       && $ns == NS_USER && !$this->isSubpage() ) {
+                       } elseif ( !$user->isAllowed( 'move-rootuserpages' )
+                                       && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
                                // Show user page-specific message only if the user can move other pages
                                $errors[] = array( 'cant-move-to-user-page' );
                        }
-               } elseif ( !$user->isAllowed( $action, $ns ) ) {
+               } elseif ( !$user->isAllowed( $action ) ) {
                        $errors[] = $this->missingPermissionError( $action, $short );
                }
 
@@ -1456,10 +1790,10 @@ class Title {
                        if ( $right == 'sysop' ) {
                                $right = 'protect';
                        }
-                       if ( $right != '' && !$user->isAllowed( $right, $this->mNamespace ) ) {
+                       if ( $right != '' && !$user->isAllowed( $right ) ) {
                                // Users with 'editprotected' permission can edit protected pages
                                // without cascading option turned on.
-                               if ( $action != 'edit' || !$user->isAllowed( 'editprotected', $this->mNamespace )
+                               if ( $action != 'edit' || !$user->isAllowed( 'editprotected' )
                                        || $this->mCascadeRestriction )
                                {
                                        $errors[] = array( 'protectedpagetext', $right );
@@ -1496,7 +1830,7 @@ class Title {
                        if ( isset( $restrictions[$action] ) ) {
                                foreach ( $restrictions[$action] as $right ) {
                                        $right = ( $right == 'sysop' ) ? 'protect' : $right;
-                                       if ( $right != '' && !$user->isAllowed( $right, $this->mNamespace ) ) {
+                                       if ( $right != '' && !$user->isAllowed( $right ) ) {
                                                $pages = '';
                                                foreach ( $cascadingSources as $page )
                                                        $pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
@@ -1521,8 +1855,10 @@ class Title {
         * @return Array list of errors
         */
        private function checkActionPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
+               global $wgDeleteRevisionsLimit, $wgLang;
+
                if ( $action == 'protect' ) {
-                       if ( $this->getUserPermissionsErrors( 'edit', $user ) != array() ) {
+                       if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $doExpensiveQueries, true ) ) ) {
                                // If they can't edit, they shouldn't protect.
                                $errors[] = array( 'protect-cantedit' );
                        }
@@ -1533,8 +1869,8 @@ class Title {
                                        $title_protection['pt_create_perm'] = 'protect'; // B/C
                                }
                                if( $title_protection['pt_create_perm'] == '' ||
-                                               !$user->isAllowed( $title_protection['pt_create_perm'],
-                                               $this->mNamespace ) ) {
+                                       !$user->isAllowed( $title_protection['pt_create_perm'] ) ) 
+                               {
                                        $errors[] = array( 'titleprotected', User::whoIs( $title_protection['pt_user'] ), $title_protection['pt_reason'] );
                                }
                        }
@@ -1553,6 +1889,12 @@ class Title {
                        } elseif ( !$this->isMovable() ) {
                                $errors[] = array( 'immobile-target-page' );
                        }
+               } elseif ( $action == 'delete' ) {
+                       if ( $doExpensiveQueries && $wgDeleteRevisionsLimit
+                               && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion() )
+                       {
+                               $errors[] = array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) );
+                       }
                }
                return $errors;
        }
@@ -1585,7 +1927,7 @@ class Title {
                        // Don't block the user from editing their own talk page unless they've been
                        // explicitly blocked from that too.
                } elseif( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) {
-                       $block = $user->mBlock;
+                       $block = $user->getBlock();
 
                        // This is from OutputPage::blockedPage
                        // Copied at r23888 by werdna
@@ -1605,15 +1947,15 @@ class Title {
 
                        $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]";
                        $blockid = $block->getId();
-                       $blockExpiry = $user->mBlock->mExpiry;
-                       $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $user->mBlock->mTimestamp ), true );
+                       $blockExpiry = $block->getExpiry();
+                       $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $block->mTimestamp ), true );
                        if ( $blockExpiry == 'infinity' ) {
                                $blockExpiry = wfMessage( 'infiniteblock' )->text();
                        } else {
                                $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true );
                        }
 
-                       $intended = strval( $user->mBlock->getTarget() );
+                       $intended = strval( $block->getTarget() );
 
                        $errors[] = array( ( $block->mAuto ? 'autoblockedtext' : 'blockedtext' ), $link, $reason, $ip, $name,
                                $blockid, $blockExpiry, $intended, $blockTimestamp );
@@ -1634,11 +1976,11 @@ class Title {
         * @return Array list of errors
         */
        private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
+               global $wgWhitelistRead, $wgGroupPermissions, $wgRevokePermissions;
                static $useShortcut = null;
 
                # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below
                if ( is_null( $useShortcut ) ) {
-                       global $wgGroupPermissions, $wgRevokePermissions;
                        $useShortcut = true;
                        if ( empty( $wgGroupPermissions['*']['read'] ) ) {
                                # Not a public wiki, so no shortcut
@@ -1660,61 +2002,56 @@ class Title {
                        }
                }
 
-               # Shortcut for public wikis, allows skipping quite a bit of code
+               $whitelisted = false;
                if ( $useShortcut ) {
-                       return $errors;
-               }
-
-               # If the user is allowed to read pages, he is allowed to read all pages
-               if ( $user->isAllowed( 'read', $this->mNamespace ) ) {
-                       return $errors;
-               }
-
-               # Always grant access to the login page.
-               # Even anons need to be able to log in.
-               if ( $this->isSpecial( 'Userlogin' )
+                       # Shortcut for public wikis, allows skipping quite a bit of code
+                       $whitelisted = true;
+               } elseif ( $user->isAllowed( 'read' ) ) {
+                       # If the user is allowed to read pages, he is allowed to read all pages
+                       $whitelisted = true;
+               } elseif ( $this->isSpecial( 'Userlogin' )
                        || $this->isSpecial( 'ChangePassword' )
                        || $this->isSpecial( 'PasswordReset' )
                ) {
-                       return $errors;
-               }
-
-               # Time to check the whitelist
-               global $wgWhitelistRead;
-
-               # Only to these checks is there's something to check against
-               if ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
-                       # Check for explicit whitelisting
+                       # Always grant access to the login page.
+                       # Even anons need to be able to log in.
+                       $whitelisted = true;
+               } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
+                       # Time to check the whitelist
+                       # Only do these checks is there's something to check against
                        $name = $this->getPrefixedText();
                        $dbName = $this->getPrefixedDBKey();
 
-                       // Check with and without underscores
+                       // Check for explicit whitelisting with and without underscores
                        if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
-                               return $errors;
-                       }
-
-                       # Old settings might have the title prefixed with
-                       # a colon for main-namespace pages
-                       if ( $this->getNamespace() == NS_MAIN ) {
+                               $whitelisted = true;
+                       } elseif ( $this->getNamespace() == NS_MAIN ) {
+                               # Old settings might have the title prefixed with
+                               # a colon for main-namespace pages
                                if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
-                                       return $errors;
+                                       $whitelisted = true;
                                }
-                       }
-
-                       # If it's a special page, ditch the subpage bit and check again
-                       if ( $this->isSpecialPage() ) {
+                       } elseif ( $this->isSpecialPage() ) {
+                               # If it's a special page, ditch the subpage bit and check again
                                $name = $this->getDBkey();
                                list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name );
                                if ( $name !== false ) {
                                        $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
                                        if ( in_array( $pure, $wgWhitelistRead, true ) ) {
-                                               return $errors;
+                                               $whitelisted = true;
                                        }
                                }
                        }
                }
 
-               $errors[] = $this->missingPermissionError( $action, $short );
+               if ( !$whitelisted ) {
+                       # If the title is not whitelisted, give extensions a chance to do so...
+                       wfRunHooks( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) );
+                       if ( !$whitelisted ) {
+                               $errors[] = $this->missingPermissionError( $action, $short );
+                       }
+               }
+
                return $errors;
        }
 
@@ -1733,7 +2070,7 @@ class Title {
                }
 
                $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
-                       User::getGroupsWithPermission( $action, $this->mNamespace ) );
+                       User::getGroupsWithPermission( $action ) );
 
                if ( count( $groups ) ) {
                        global $wgLang;
@@ -1787,8 +2124,81 @@ class Title {
                        $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short );
                }
 
-               wfProfileOut( __METHOD__ );
-               return $errors;
+               wfProfileOut( __METHOD__ );
+               return $errors;
+       }
+
+       /**
+        * Protect css subpages of user pages: can $wgUser edit
+        * this page?
+        *
+        * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead.
+        * @return Bool
+        */
+       public function userCanEditCssSubpage() {
+               global $wgUser;
+               wfDeprecated( __METHOD__, '1.19' );
+               return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) )
+                       || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
+       }
+
+       /**
+        * Protect js subpages of user pages: can $wgUser edit
+        * this page?
+        *
+        * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead.
+        * @return Bool
+        */
+       public function userCanEditJsSubpage() {
+               global $wgUser;
+               wfDeprecated( __METHOD__, '1.19' );
+               return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) )
+                          || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
+       }
+
+       /**
+        * Get a filtered list of all restriction types supported by this wiki.
+        * @param bool $exists True to get all restriction types that apply to
+        * titles that do exist, False for all restriction types that apply to
+        * titles that do not exist
+        * @return array
+        */
+       public static function getFilteredRestrictionTypes( $exists = true ) {
+               global $wgRestrictionTypes;
+               $types = $wgRestrictionTypes;
+               if ( $exists ) {
+                       # Remove the create restriction for existing titles
+                       $types = array_diff( $types, array( 'create' ) );
+               } else {
+                       # Only the create and upload restrictions apply to non-existing titles
+                       $types = array_intersect( $types, array( 'create', 'upload' ) );
+               }
+               return $types;
+       }
+
+       /**
+        * Returns restriction types for the current Title
+        *
+        * @return array applicable restriction types
+        */
+       public function getRestrictionTypes() {
+               if ( $this->isSpecialPage() ) {
+                       return array();
+               }
+
+               $types = self::getFilteredRestrictionTypes( $this->exists() );
+
+               if ( $this->getNamespace() != NS_FILE ) {
+                       # Remove the upload restriction for non-file titles
+                       $types = array_diff( $types, array( 'upload' ) );
+               }
+
+               wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) );
+
+               wfDebug( __METHOD__ . ': applicable restrictions to [[' .
+                       $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
+
+               return $types;
        }
 
        /**
@@ -1824,66 +2234,24 @@ class Title {
        /**
         * Update the title protection status
         *
+        * @deprecated in 1.19; will be removed in 1.20. Use WikiPage::doUpdateRestrictions() instead.
         * @param $create_perm String Permission required for creation
         * @param $reason String Reason for protection
         * @param $expiry String Expiry timestamp
         * @return boolean true
         */
        public function updateTitleProtection( $create_perm, $reason, $expiry ) {
-               global $wgUser, $wgContLang;
-
-               if ( $create_perm == implode( ',', $this->getRestrictions( 'create' ) )
-                       && $expiry == $this->mRestrictionsExpiry['create'] ) {
-                       // No change
-                       return true;
-               }
-
-               list ( $namespace, $title ) = array( $this->getNamespace(), $this->getDBkey() );
+               wfDeprecated( __METHOD__, '1.19' );
 
-               $dbw = wfGetDB( DB_MASTER );
-
-               $encodedExpiry = $dbw->encodeExpiry( $expiry );
-
-               $expiry_description = '';
-               if ( $encodedExpiry != $dbw->getInfinity() ) {
-                       $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ),
-                               $wgContLang->date( $expiry ) , $wgContLang->time( $expiry ) ) . ')';
-               } else {
-                       $expiry_description .= ' (' . wfMsgForContent( 'protect-expiry-indefinite' ) . ')';
-               }
-
-               # Update protection table
-               if ( $create_perm != '' ) {
-                       $this->mTitleProtection = array(
-                                       'pt_namespace' => $namespace,
-                                       'pt_title' => $title,
-                                       'pt_create_perm' => $create_perm,
-                                       'pt_timestamp' => $dbw->encodeExpiry( wfTimestampNow() ),
-                                       'pt_expiry' => $encodedExpiry,
-                                       'pt_user' => $wgUser->getId(),
-                                       'pt_reason' => $reason,
-                               );
-                       $dbw->replace( 'protected_titles', array( array( 'pt_namespace', 'pt_title' ) ),
-                               $this->mTitleProtection, __METHOD__     );
-               } else {
-                       $dbw->delete( 'protected_titles', array( 'pt_namespace' => $namespace,
-                               'pt_title' => $title ), __METHOD__ );
-                       $this->mTitleProtection = false;
-               }
+               global $wgUser;
 
-               # Update the protection log
-               if ( $dbw->affectedRows() ) {
-                       $log = new LogPage( 'protect' );
+               $limit = array( 'create' => $create_perm );
+               $expiry = array( 'create' => $expiry );
 
-                       if ( $create_perm ) {
-                               $params = array( "[create=$create_perm] $expiry_description", '' );
-                               $log->addEntry( ( isset( $this->mRestrictions['create'] ) && $this->mRestrictions['create'] ) ? 'modify' : 'protect', $this, trim( $reason ), $params );
-                       } else {
-                               $log->addEntry( 'unprotect', $this, $reason );
-                       }
-               }
+               $page = WikiPage::factory( $this );
+               $status = $page->doUpdateRestrictions( $limit, $expiry, false, $reason, $wgUser );
 
-               return true;
+               return $status->isOK();
        }
 
        /**
@@ -1901,262 +2269,81 @@ class Title {
        }
 
        /**
-        * Would anybody with sufficient privileges be able to move this page?
-        * Some pages just aren't movable.
-        *
-        * @return Bool TRUE or FALSE
-        */
-       public function isMovable() {
-               if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->getInterwiki() != '' ) {
-                       // Interwiki title or immovable namespace. Hooks don't get to override here
-                       return false;
-               }
-
-               $result = true;
-               wfRunHooks( 'TitleIsMovable', array( $this, &$result ) );
-               return $result;
-       }
-
-       /**
-        * Is this the mainpage?
-        * @note Title::newFromText seams to be sufficiently optimized by the title
-        * cache that we don't need to over-optimize by doing direct comparisons and
-        * acidentally creating new bugs where $title->equals( Title::newFromText() )
-        * ends up reporting something differently than $title->isMainPage();
-        *
-        * @since 1.18
-        * @return Bool
-        */
-       public function isMainPage() {
-               return $this->equals( Title::newMainPage() );
-       }
-
-       /**
-        * Is this a talk page of some sort?
-        *
-        * @return Bool
-        */
-       public function isTalkPage() {
-               return MWNamespace::isTalk( $this->getNamespace() );
-       }
-
-       /**
-        * Is this a subpage?
+        * Is this page "semi-protected" - the *only* protection is autoconfirm?
         *
+        * @param $action String Action to check (default: edit)
         * @return Bool
         */
-       public function isSubpage() {
-               return MWNamespace::hasSubpages( $this->mNamespace )
-                       ? strpos( $this->getText(), '/' ) !== false
-                       : false;
-       }
-
-       /**
-        * Returns true if the title is inside the specified namespace.
-        * 
-        * Please make use of this instead of comparing to getNamespace()
-        * This function is much more resistant to changes we may make
-        * to namespaces than code that makes direct comparisons.
-        * @param $ns int The namespace
-        * @return bool
-        * @since 1.19
-        */
-       public function inNamespace( $ns ) {
-               return MWNamespace::equals( $this->getNamespace(), $ns );
-       }
-
-       /**
-        * Returns true if the title is inside one of the specified namespaces.
-        *
-        * @param ...$namespaces The namespaces to check for
-        * @return bool
-        * @since 1.19
-        */
-       public function inNamespaces( /* ... */ ) {
-               $namespaces = func_get_args();
-               if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
-                       $namespaces = $namespaces[0];
-               }
-
-               foreach ( $namespaces as $ns ) {
-                       if ( $this->inNamespace( $ns ) ) {
-                               return true;
+       public function isSemiProtected( $action = 'edit' ) {
+               if ( $this->exists() ) {
+                       $restrictions = $this->getRestrictions( $action );
+                       if ( count( $restrictions ) > 0 ) {
+                               foreach ( $restrictions as $restriction ) {
+                                       if ( strtolower( $restriction ) != 'autoconfirmed' ) {
+                                               return false;
+                                       }
+                               }
+                       } else {
+                               # Not protected
+                               return false;
                        }
-               }
-
-               return false;
-       }
-
-       /**
-        * Returns true if the title has the same subject namespace as the
-        * namespace specified.
-        * For example this method will take NS_USER and return true if namespace
-        * is either NS_USER or NS_USER_TALK since both of them have NS_USER
-        * as their subject namespace.
-        *
-        * This is MUCH simpler than individually testing for equivilance
-        * against both NS_USER and NS_USER_TALK, and is also forward compatible.
-        * @since 1.19
-        */
-       public function hasSubjectNamespace( $ns ) {
-               return MWNamespace::subjectEquals( $this->getNamespace(), $ns );
-       }
-
-       /**
-        * Does this have subpages?  (Warning, usually requires an extra DB query.)
-        *
-        * @return Bool
-        */
-       public function hasSubpages() {
-               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
-                       # Duh
+                       return true;
+               } else {
+                       # If it doesn't exist, it can't be protected
                        return false;
                }
-
-               # We dynamically add a member variable for the purpose of this method
-               # alone to cache the result.  There's no point in having it hanging
-               # around uninitialized in every Title object; therefore we only add it
-               # if needed and don't declare it statically.
-               if ( isset( $this->mHasSubpages ) ) {
-                       return $this->mHasSubpages;
-               }
-
-               $subpages = $this->getSubpages( 1 );
-               if ( $subpages instanceof TitleArray ) {
-                       return $this->mHasSubpages = (bool)$subpages->count();
-               }
-               return $this->mHasSubpages = false;
-       }
-
-       /**
-        * Get all subpages of this page.
-        *
-        * @param $limit Int maximum number of subpages to fetch; -1 for no limit
-        * @return mixed TitleArray, or empty array if this page's namespace
-        *  doesn't allow subpages
-        */
-       public function getSubpages( $limit = -1 ) {
-               if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
-                       return array();
-               }
-
-               $dbr = wfGetDB( DB_SLAVE );
-               $conds['page_namespace'] = $this->getNamespace();
-               $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
-               $options = array();
-               if ( $limit > -1 ) {
-                       $options['LIMIT'] = $limit;
-               }
-               return $this->mSubpages = TitleArray::newFromResult(
-                       $dbr->select( 'page',
-                               array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ),
-                               $conds,
-                               __METHOD__,
-                               $options
-                       )
-               );
-       }
-
-       /**
-        * Does that page contain wikitext, or it is JS, CSS or whatever?
-        *
-        * @return Bool
-        */
-       public function isWikitextPage() {
-               $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
-               wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
-               return $retval;
        }
 
        /**
-        * Could this page contain custom CSS or JavaScript, based
-        * on the title?
+        * Does the title correspond to a protected article?
         *
+        * @param $action String the action the page is protected from,
+        * by default checks all actions.
         * @return Bool
         */
-       public function isCssOrJsPage() {
-               $retval = $this->mNamespace == NS_MEDIAWIKI
-                       && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
-               wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
-               return $retval;
-       }
-
-       /**
-        * Is this a .css or .js subpage of a user page?
-        * @return Bool
-        */
-       public function isCssJsSubpage() {
-               return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
-       }
+       public function isProtected( $action = '' ) {
+               global $wgRestrictionLevels;
 
-       /**
-        * Is this a *valid* .css or .js subpage of a user page?
-        *
-        * @return Bool
-        * @deprecated since 1.17
-        */
-       public function isValidCssJsSubpage() {
-               return $this->isCssJsSubpage();
-       }
+               $restrictionTypes = $this->getRestrictionTypes();
 
-       /**
-        * Trim down a .css or .js subpage title to get the corresponding skin name
-        *
-        * @return string containing skin name from .css or .js subpage title
-        */
-       public function getSkinFromCssJsSubpage() {
-               $subpage = explode( '/', $this->mTextform );
-               $subpage = $subpage[ count( $subpage ) - 1 ];
-               $lastdot = strrpos( $subpage, '.' );
-               if ( $lastdot === false )
-                       return $subpage; # Never happens: only called for names ending in '.css' or '.js'
-               return substr( $subpage, 0, $lastdot );
-       }
+               # Special pages have inherent protection
+               if( $this->isSpecialPage() ) {
+                       return true;
+               }
 
-       /**
-        * Is this a .css subpage of a user page?
-        *
-        * @return Bool
-        */
-       public function isCssSubpage() {
-               return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
-       }
+               # Check regular protection levels
+               foreach ( $restrictionTypes as $type ) {
+                       if ( $action == $type || $action == '' ) {
+                               $r = $this->getRestrictions( $type );
+                               foreach ( $wgRestrictionLevels as $level ) {
+                                       if ( in_array( $level, $r ) && $level != '' ) {
+                                               return true;
+                                       }
+                               }
+                       }
+               }
 
-       /**
-        * Is this a .js subpage of a user page?
-        *
-        * @return Bool
-        */
-       public function isJsSubpage() {
-               return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
+               return false;
        }
 
        /**
-        * Protect css subpages of user pages: can $wgUser edit
-        * this page?
+        * Determines if $user is unable to edit this page because it has been protected
+        * by $wgNamespaceProtection.
         *
-        * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead.
+        * @param $user User object to check permissions
         * @return Bool
         */
-       public function userCanEditCssSubpage() {
-               global $wgUser;
-               wfDeprecated( __METHOD__ );
-               return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) )
-                       || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
-       }
+       public function isNamespaceProtected( User $user ) {
+               global $wgNamespaceProtection;
 
-       /**
-        * Protect js subpages of user pages: can $wgUser edit
-        * this page?
-        *
-        * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead.
-        * @return Bool
-        */
-       public function userCanEditJsSubpage() {
-               global $wgUser;
-               wfDeprecated( __METHOD__ );
-               return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) )
-                          || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
+               if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
+                       foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
+                               if ( $right != '' && !$user->isAllowed( $right ) ) {
+                                       return true;
+                               }
+                       }
+               }
+               return false;
        }
 
        /**
@@ -2267,6 +2454,34 @@ class Title {
                return array( $sources, $pagerestrictions );
        }
 
+       /**
+        * Accessor/initialisation for mRestrictions
+        *
+        * @param $action String action that permission needs to be checked for
+        * @return Array of Strings the array of groups allowed to edit this article
+        */
+       public function getRestrictions( $action ) {
+               if ( !$this->mRestrictionsLoaded ) {
+                       $this->loadRestrictions();
+               }
+               return isset( $this->mRestrictions[$action] )
+                               ? $this->mRestrictions[$action]
+                               : array();
+       }
+
+       /**
+        * Get the expiry time for the restriction against a given action
+        *
+        * @return String|Bool 14-char timestamp, or 'infinity' if the page is protected forever
+        *      or not protected at all, or false if the action is not recognised.
+        */
+       public function getRestrictionExpiry( $action ) {
+               if ( !$this->mRestrictionsLoaded ) {
+                       $this->loadRestrictions();
+               }
+               return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
+       }
+
        /**
         * Returns cascading restrictions for the current article
         *
@@ -2323,7 +2538,7 @@ class Title {
 
                if ( $oldFashionedRestrictions === null ) {
                        $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions',
-                               array( 'page_id' => $this->getArticleId() ), __METHOD__ );
+                               array( 'page_id' => $this->getArticleID() ), __METHOD__ );
                }
 
                if ( $oldFashionedRestrictions != '' ) {
@@ -2394,7 +2609,7 @@ class Title {
                                $res = $dbr->select(
                                        'page_restrictions',
                                        '*',
-                                       array( 'pr_page' => $this->getArticleId() ),
+                                       array( 'pr_page' => $this->getArticleID() ),
                                        __METHOD__
                                );
 
@@ -2422,6 +2637,15 @@ class Title {
                }
        }
 
+       /**
+        * Flush the protection cache in this object and force reload from the database.
+        * This is used when updating protection from WikiPage::doUpdateRestrictions().
+        */
+       public function flushRestrictions() {
+               $this->mRestrictionsLoaded = false;
+               $this->mTitleProtection = null;
+       }
+
        /**
         * Purge expired restrictions from the page_restrictions table
         */
@@ -2441,31 +2665,58 @@ class Title {
        }
 
        /**
-        * Accessor/initialisation for mRestrictions
+        * Does this have subpages?  (Warning, usually requires an extra DB query.)
         *
-        * @param $action String action that permission needs to be checked for
-        * @return Array of Strings the array of groups allowed to edit this article
+        * @return Bool
         */
-       public function getRestrictions( $action ) {
-               if ( !$this->mRestrictionsLoaded ) {
-                       $this->loadRestrictions();
+       public function hasSubpages() {
+               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+                       # Duh
+                       return false;
                }
-               return isset( $this->mRestrictions[$action] )
-                               ? $this->mRestrictions[$action]
-                               : array();
+
+               # We dynamically add a member variable for the purpose of this method
+               # alone to cache the result.  There's no point in having it hanging
+               # around uninitialized in every Title object; therefore we only add it
+               # if needed and don't declare it statically.
+               if ( isset( $this->mHasSubpages ) ) {
+                       return $this->mHasSubpages;
+               }
+
+               $subpages = $this->getSubpages( 1 );
+               if ( $subpages instanceof TitleArray ) {
+                       return $this->mHasSubpages = (bool)$subpages->count();
+               }
+               return $this->mHasSubpages = false;
        }
 
        /**
-        * Get the expiry time for the restriction against a given action
+        * Get all subpages of this page.
         *
-        * @return String|Bool 14-char timestamp, or 'infinity' if the page is protected forever
-        *      or not protected at all, or false if the action is not recognised.
+        * @param $limit Int maximum number of subpages to fetch; -1 for no limit
+        * @return mixed TitleArray, or empty array if this page's namespace
+        *  doesn't allow subpages
         */
-       public function getRestrictionExpiry( $action ) {
-               if ( !$this->mRestrictionsLoaded ) {
-                       $this->loadRestrictions();
+       public function getSubpages( $limit = -1 ) {
+               if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
+                       return array();
                }
-               return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
+
+               $dbr = wfGetDB( DB_SLAVE );
+               $conds['page_namespace'] = $this->getNamespace();
+               $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
+               $options = array();
+               if ( $limit > -1 ) {
+                       $options['LIMIT'] = $limit;
+               }
+               return $this->mSubpages = TitleArray::newFromResult(
+                       $dbr->select( 'page',
+                               array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ),
+                               $conds,
+                               __METHOD__,
+                               $options
+                       )
+               );
        }
 
        /**
@@ -2608,9 +2859,9 @@ class Title {
         * This clears some fields in this object, and clears any associated
         * keys in the "bad links" section of the link cache.
         *
-        * - This is called from Article::doEdit() and Article::insertOn() to allow
+        * - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow
         * loading of the new page_id. It's also called from
-        * Article::doDeleteArticle()
+        * WikiPage::doDeleteArticle()
         *
         * @param $newid Int the new Article ID
         */
@@ -2628,73 +2879,7 @@ class Title {
                $this->mRedirect = null;
                $this->mLength = -1;
                $this->mLatestID = false;
-       }
-
-       /**
-        * Updates page_touched for this page; called from LinksUpdate.php
-        *
-        * @return Bool true if the update succeded
-        */
-       public function invalidateCache() {
-               if ( wfReadOnly() ) {
-                       return false;
-               }
-               $dbw = wfGetDB( DB_MASTER );
-               $success = $dbw->update(
-                       'page',
-                       array( 'page_touched' => $dbw->timestamp() ),
-                       $this->pageCond(),
-                       __METHOD__
-               );
-               HTMLFileCache::clearFileCache( $this );
-               return $success;
-       }
-
-       /**
-        * Prefix some arbitrary text with the namespace or interwiki prefix
-        * of this object
-        *
-        * @param $name String the text
-        * @return String the prefixed text
-        * @private
-        */
-       private function prefix( $name ) {
-               $p = '';
-               if ( $this->mInterwiki != '' ) {
-                       $p = $this->mInterwiki . ':';
-               }
-
-               if ( 0 != $this->mNamespace ) {
-                       $p .= $this->getNsText() . ':';
-               }
-               return $p . $name;
-       }
-
-       /**
-        * Returns a simple regex that will match on characters and sequences invalid in titles.
-        * Note that this doesn't pick up many things that could be wrong with titles, but that
-        * replacing this regex with something valid will make many titles valid.
-        *
-        * @return String regex string
-        */
-       static function getTitleInvalidRegex() {
-               static $rxTc = false;
-               if ( !$rxTc ) {
-                       # Matching titles will be held as illegal.
-                       $rxTc = '/' .
-                               # Any character not allowed is forbidden...
-                               '[^' . Title::legalChars() . ']' .
-                               # URL percent encoding sequences interfere with the ability
-                               # to round-trip titles -- you can't link to them consistently.
-                               '|%[0-9A-Fa-f]{2}' .
-                               # XML/HTML character references produce similar issues.
-                               '|&[A-Za-z0-9\x80-\xff]+;' .
-                               '|&#[0-9]+;' .
-                               '|&#x[0-9A-Fa-f]+;' .
-                               '/S';
-               }
-
-               return $rxTc;
+               $this->mEstimateRevisions = null;
        }
 
        /**
@@ -2912,45 +3097,65 @@ class Title {
        }
 
        /**
-        * Set the fragment for this title. Removes the first character from the
-        * specified fragment before setting, so it assumes you're passing it with
-        * an initial "#".
+        * Get an array of Title objects linking to this Title
+        * Also stores the IDs in the link cache.
         *
-        * Deprecated for public use, use Title::makeTitle() with fragment parameter.
-        * Still in active use privately.
+        * WARNING: do not use this function on arbitrary user-supplied titles!
+        * On heavily-used templates it will max out the memory.
         *
-        * @param $fragment String text
+        * @param $options Array: may be FOR UPDATE
+        * @param $table String: table name
+        * @param $prefix String: fields prefix
+        * @return Array of Title objects linking here
         */
-       public function setFragment( $fragment ) {
-               $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) );
-       }
+       public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
+               if ( count( $options ) > 0 ) {
+                       $db = wfGetDB( DB_MASTER );
+               } else {
+                       $db = wfGetDB( DB_SLAVE );
+               }
 
-       /**
-        * Get a Title object associated with the talk page of this article
-        *
-        * @return Title the object for the talk page
-        */
-       public function getTalkPage() {
-               return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
+               $res = $db->select(
+                       array( 'page', $table ),
+                       array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+                       array(
+                               "{$prefix}_from=page_id",
+                               "{$prefix}_namespace" => $this->getNamespace(),
+                               "{$prefix}_title"     => $this->getDBkey() ),
+                       __METHOD__,
+                       $options
+               );
+
+               $retVal = array();
+               if ( $res->numRows() ) {
+                       $linkCache = LinkCache::singleton();
+                       foreach ( $res as $row ) {
+                               $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title );
+                               if ( $titleObj ) {
+                                       $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
+                                       $retVal[] = $titleObj;
+                               }
+                       }
+               }
+               return $retVal;
        }
 
-       /**
-        * Get a title object associated with the subject page of this
-        * talk page
+       /**
+        * Get an array of Title objects using this Title as a template
+        * Also stores the IDs in the link cache.
         *
-        * @return Title the object for the subject page
+        * WARNING: do not use this function on arbitrary user-supplied titles!
+        * On heavily-used templates it will max out the memory.
+        *
+        * @param $options Array: may be FOR UPDATE
+        * @return Array of Title the Title objects linking here
         */
-       public function getSubjectPage() {
-               // Is this the same title?
-               $subjectNS = MWNamespace::getSubject( $this->getNamespace() );
-               if ( $this->getNamespace() == $subjectNS ) {
-                       return $this;
-               }
-               return Title::makeTitle( $subjectNS, $this->getDBkey() );
+       public function getTemplateLinksTo( $options = array() ) {
+               return $this->getLinksTo( $options, 'templatelinks', 'tl' );
        }
 
        /**
-        * Get an array of Title objects linking to this Title
+        * Get an array of Title objects linked from this Title
         * Also stores the IDs in the link cache.
         *
         * WARNING: do not use this function on arbitrary user-supplied titles!
@@ -2961,8 +3166,13 @@ class Title {
         * @param $prefix String: fields prefix
         * @return Array of Title objects linking here
         */
-       public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
-               $linkCache = LinkCache::singleton();
+       public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
+               $id = $this->getArticleID();
+
+               # If the page doesn't exist; there can't be any link from this page
+               if ( !$id ) {
+                       return array();
+               }
 
                if ( count( $options ) > 0 ) {
                        $db = wfGetDB( DB_MASTER );
@@ -2970,23 +3180,29 @@ class Title {
                        $db = wfGetDB( DB_SLAVE );
                }
 
+               $namespaceFiled = "{$prefix}_namespace";
+               $titleField = "{$prefix}_title";
+
                $res = $db->select(
-                       array( 'page', $table ),
-                       array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
-                       array(
-                               "{$prefix}_from=page_id",
-                               "{$prefix}_namespace" => $this->getNamespace(),
-                               "{$prefix}_title"     => $this->getDBkey() ),
+                       array( $table, 'page' ),
+                       array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+                       array( "{$prefix}_from" => $id ),
                        __METHOD__,
-                       $options
+                       $options,
+                       array( 'page' => array( 'LEFT JOIN', array( "page_namespace=$namespaceFiled", "page_title=$titleField" ) ) )
                );
 
                $retVal = array();
-               if ( $db->numRows( $res ) ) {
+               if ( $res->numRows() ) {
+                       $linkCache = LinkCache::singleton();
                        foreach ( $res as $row ) {
-                               $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title );
+                               $titleObj = Title::makeTitle( $row->$namespaceFiled, $row->$titleField );
                                if ( $titleObj ) {
-                                       $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
+                                       if ( $row->page_id ) {
+                                               $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
+                                       } else {
+                                               $linkCache->addBadLinkObj( $titleObj );
+                                       }
                                        $retVal[] = $titleObj;
                                }
                        }
@@ -2995,17 +3211,17 @@ class Title {
        }
 
        /**
-        * Get an array of Title objects using this Title as a template
+        * Get an array of Title objects used on this Title as a template
         * Also stores the IDs in the link cache.
         *
         * WARNING: do not use this function on arbitrary user-supplied titles!
         * On heavily-used templates it will max out the memory.
         *
         * @param $options Array: may be FOR UPDATE
-        * @return Array of Title the Title objects linking here
+        * @return Array of Title the Title objects used here
         */
-       public function getTemplateLinksTo( $options = array() ) {
-               return $this->getLinksTo( $options, 'templatelinks', 'tl' );
+       public function getTemplateLinksFrom( $options = array() ) {
+               return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
        }
 
        /**
@@ -3015,7 +3231,7 @@ class Title {
         * @return Array of Title the Title objects
         */
        public function getBrokenLinksFrom() {
-               if ( $this->getArticleId() == 0 ) {
+               if ( $this->getArticleID() == 0 ) {
                        # All links from article ID 0 are false positives
                        return array();
                }
@@ -3025,7 +3241,7 @@ class Title {
                        array( 'page', 'pagelinks' ),
                        array( 'pl_namespace', 'pl_title' ),
                        array(
-                               'pl_from' => $this->getArticleId(),
+                               'pl_from' => $this->getArticleID(),
                                'page_namespace IS NULL'
                        ),
                        __METHOD__, array(),
@@ -3255,26 +3471,23 @@ class Title {
                                        return $status->getErrorsArray();
                                }
                        }
+                       // Clear RepoGroup process cache
+                       RepoGroup::singleton()->clearCache( $this );
+                       RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache
                }
-               // Clear RepoGroup process cache
-               RepoGroup::singleton()->clearCache( $this );
-               RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache
 
-               $dbw->begin(); # If $file was a LocalFile, its transaction would have closed our own.
+               $dbw->begin( __METHOD__ ); # If $file was a LocalFile, its transaction would have closed our own.
                $pageid = $this->getArticleID( self::GAID_FOR_UPDATE );
                $protected = $this->isProtected();
-               $pageCountChange = ( $createRedirect ? 1 : 0 ) - ( $nt->exists() ? 1 : 0 );
 
                // Do the actual move
                $err = $this->moveToInternal( $nt, $reason, $createRedirect );
                if ( is_array( $err ) ) {
                        # @todo FIXME: What about the File we have already moved?
-                       $dbw->rollback();
+                       $dbw->rollback( __METHOD__ );
                        return $err;
                }
 
-               $redirid = $this->getArticleID();
-
                // Refresh the sortkey for this row.  Be careful to avoid resetting
                // cl_timestamp, which may disturb time-based lists on some sites.
                $prefixes = $dbw->select(
@@ -3298,6 +3511,8 @@ class Title {
                        );
                }
 
+               $redirid = $this->getArticleID();
+
                if ( $protected ) {
                        # Protect the redirect title as the title used to be...
                        $dbw->insertSelect( 'page_restrictions', 'page_restrictions',
@@ -3333,49 +3548,7 @@ class Title {
                        WatchedItem::duplicateEntries( $this, $nt );
                }
 
-               # Update search engine
-               $u = new SearchUpdate( $pageid, $nt->getPrefixedDBkey() );
-               $u->doUpdate();
-               $u = new SearchUpdate( $redirid, $this->getPrefixedDBkey(), '' );
-               $u->doUpdate();
-
-               $dbw->commit();
-
-               # Update site_stats
-               if ( $this->isContentPage() && !$nt->isContentPage() ) {
-                       # No longer a content page
-                       # Not viewed, edited, removing
-                       $u = new SiteStatsUpdate( 0, 1, -1, $pageCountChange );
-               } elseif ( !$this->isContentPage() && $nt->isContentPage() ) {
-                       # Now a content page
-                       # Not viewed, edited, adding
-                       $u = new SiteStatsUpdate( 0, 1, + 1, $pageCountChange );
-               } elseif ( $pageCountChange ) {
-                       # Redirect added
-                       $u = new SiteStatsUpdate( 0, 0, 0, 1 );
-               } else {
-                       # Nothing special
-                       $u = false;
-               }
-               if ( $u ) {
-                       $u->doUpdate();
-               }
-
-               # Update message cache for interface messages
-               if ( $this->getNamespace() == NS_MEDIAWIKI ) {
-                       # @bug 17860: old article can be deleted, if this the case,
-                       # delete it from message cache
-                       if ( $this->getArticleID() === 0 ) {
-                               MessageCache::singleton()->replace( $this->getDBkey(), false );
-                       } else {
-                               $rev = Revision::newFromTitle( $this );
-                               MessageCache::singleton()->replace( $this->getDBkey(), $rev->getText() );
-                       }
-               }
-               if ( $nt->getNamespace() == NS_MEDIAWIKI ) {
-                       $rev = Revision::newFromTitle( $nt );
-                       MessageCache::singleton()->replace( $nt->getDBkey(), $rev->getText() );
-               }
+               $dbw->commit( __METHOD__ );
 
                wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) );
                return true;
@@ -3422,40 +3595,21 @@ class Title {
                $comment = $wgContLang->truncate( $comment, 255 );
 
                $oldid = $this->getArticleID();
-               $latest = $this->getLatestRevID();
 
                $dbw = wfGetDB( DB_MASTER );
 
-               if ( $moveOverRedirect ) {
-                       $rcts = $dbw->timestamp( $nt->getEarliestRevTime() );
+               $newpage = WikiPage::factory( $nt );
 
+               if ( $moveOverRedirect ) {
                        $newid = $nt->getArticleID();
-                       $newns = $nt->getNamespace();
-                       $newdbk = $nt->getDBkey();
 
                        # Delete the old redirect. We don't save it to history since
                        # by definition if we've got here it's rather uninteresting.
                        # We have to remove it so that the next step doesn't trigger
                        # a conflict on the unique namespace+title index...
                        $dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__ );
-                       if ( !$dbw->cascadingDeletes() ) {
-                               $dbw->delete( 'revision', array( 'rev_page' => $newid ), __METHOD__ );
-
-                               $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'imagelinks', array( 'il_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'categorylinks', array( 'cl_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'templatelinks', array( 'tl_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'externallinks', array( 'el_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'langlinks', array( 'll_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'iwlinks', array( 'iwl_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'redirect', array( 'rd_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'page_props', array( 'pp_page' => $newid ), __METHOD__ );
-                       }
-                       // If the target page was recently created, it may have an entry in recentchanges still
-                       $dbw->delete( 'recentchanges',
-                               array( 'rc_timestamp' => $rcts, 'rc_namespace' => $newns, 'rc_title' => $newdbk, 'rc_new' => 1 ),
-                               __METHOD__
-                       );
+
+                       $newpage->doDeleteUpdates( $newid );
                }
 
                # Save a null revision in the page's history notifying of the move
@@ -3465,27 +3619,34 @@ class Title {
                }
                $nullRevId = $nullRevision->insertOn( $dbw );
 
-               $now = wfTimestampNow();
                # Change the name of the target page:
                $dbw->update( 'page',
                        /* SET */ array(
-                               'page_touched'   => $dbw->timestamp( $now ),
                                'page_namespace' => $nt->getNamespace(),
                                'page_title'     => $nt->getDBkey(),
-                               'page_latest'    => $nullRevId,
                        ),
                        /* WHERE */ array( 'page_id' => $oldid ),
                        __METHOD__
                );
+
+               $this->resetArticleID( 0 );
                $nt->resetArticleID( $oldid );
 
-               $article = WikiPage::factory( $nt );
+               $newpage->updateRevisionOn( $dbw, $nullRevision );
+
                wfRunHooks( 'NewRevisionFromEditComplete',
-                       array( $article, $nullRevision, $latest, $wgUser ) );
-               $article->setCachedLastEditTime( $now );
+                       array( $newpage, $nullRevision, $nullRevision->getParentId(), $wgUser ) );
+
+               $newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) );
+
+               if ( !$moveOverRedirect ) {
+                       WikiPage::onArticleCreate( $nt );
+               }
 
                # Recreate the redirect, this time in the other direction.
-               if ( $createRedirect || !$wgUser->isAllowed( 'suppressredirect' ) ) {
+               if ( $redirectSuppressed ) {
+                       WikiPage::onArticleDelete( $this );
+               } else {
                        $mwRedir = MagicWord::get( 'redirect' );
                        $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
                        $redirectArticle = WikiPage::factory( $this );
@@ -3501,33 +3662,13 @@ class Title {
                                wfRunHooks( 'NewRevisionFromEditComplete',
                                        array( $redirectArticle, $redirectRevision, false, $wgUser ) );
 
-                               # Now, we record the link from the redirect to the new title.
-                               # It should have no other outgoing links...
-                               $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ );
-                               $dbw->insert( 'pagelinks',
-                                       array(
-                                               'pl_from'      => $newid,
-                                               'pl_namespace' => $nt->getNamespace(),
-                                               'pl_title'     => $nt->getDBkey() ),
-                                       __METHOD__ );
+                               $redirectArticle->doEditUpdates( $redirectRevision, $wgUser, array( 'created' => true ) );
                        }
-               } else {
-                       $this->resetArticleID( 0 );
                }
 
                # Log the move
                $logid = $logEntry->insert();
                $logEntry->publish( $logid );
-
-               # Purge caches for old and new titles
-               if ( $moveOverRedirect ) {
-                       # A simple purge is enough when moving over a redirect
-                       $nt->purgeSquid();
-               } else {
-                       # Purge caches as per article creation, including any pages that link to this title
-                       Article::onArticleCreate( $nt );
-               }
-               $this->purgeSquid();
        }
 
        /**
@@ -3573,8 +3714,8 @@ class Title {
                        // We don't know whether this function was called before
                        // or after moving the root page, so check both
                        // $this and $nt
-                       if ( $oldSubpage->getArticleId() == $this->getArticleId() ||
-                                       $oldSubpage->getArticleID() == $nt->getArticleId() )
+                       if ( $oldSubpage->getArticleID() == $this->getArticleID() ||
+                                       $oldSubpage->getArticleID() == $nt->getArticleID() )
                        {
                                // When moving a page to a subpage of itself,
                                // don't move it twice
@@ -3686,15 +3827,6 @@ class Title {
                return true;
        }
 
-       /**
-        * Can this title be added to a user's watchlist?
-        *
-        * @return Bool TRUE or FALSE
-        */
-       public function isWatchable() {
-               return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
-       }
-
        /**
         * Get categories to which this Title belongs and return an array of
         * categories' names.
@@ -3707,7 +3839,7 @@ class Title {
 
                $data = array();
 
-               $titleKey = $this->getArticleId();
+               $titleKey = $this->getArticleID();
 
                if ( $titleKey === 0 ) {
                        return $data;
@@ -3785,7 +3917,7 @@ class Title {
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                return $db->selectField( 'revision', 'rev_id',
                        array(
-                               'rev_page' => $this->getArticleId( $flags ),
+                               'rev_page' => $this->getArticleID( $flags ),
                                'rev_id < ' . intval( $revId )
                        ),
                        __METHOD__,
@@ -3804,7 +3936,7 @@ class Title {
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                return $db->selectField( 'revision', 'rev_id',
                        array(
-                               'rev_page' => $this->getArticleId( $flags ),
+                               'rev_page' => $this->getArticleID( $flags ),
                                'rev_id > ' . intval( $revId )
                        ),
                        __METHOD__,
@@ -3819,7 +3951,7 @@ class Title {
         * @return Revision|Null if page doesn't exist
         */
        public function getFirstRevision( $flags = 0 ) {
-               $pageId = $this->getArticleId( $flags );
+               $pageId = $this->getArticleID( $flags );
                if ( $pageId ) {
                        $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                        $row = $db->selectRow( 'revision', '*',
@@ -3855,6 +3987,41 @@ class Title {
                return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
        }
 
+       /**
+        * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit
+        *
+        * @return bool
+        */
+       public function isBigDeletion() {
+               global $wgDeleteRevisionsLimit;
+
+               if ( !$wgDeleteRevisionsLimit ) {
+                       return false;
+               }
+
+               $revCount = $this->estimateRevisionCount();
+               return $revCount > $wgDeleteRevisionsLimit;
+       }
+
+       /**
+        * Get the  approximate revision count of this page.
+        *
+        * @return int
+        */
+       public function estimateRevisionCount() {
+               if ( !$this->exists() ) {
+                       return 0;
+               }
+
+               if ( $this->mEstimateRevisions === null ) {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
+                               array( 'rev_page' => $this->getArticleID() ), __METHOD__ );
+               }
+
+               return $this->mEstimateRevisions;
+       }
+
        /**
         * Get the number of revisions between the given revision.
         * Used for diffs and other things that really need it.
@@ -3876,7 +4043,7 @@ class Title {
                $dbr = wfGetDB( DB_SLAVE );
                return (int)$dbr->selectField( 'revision', 'count(*)',
                        array(
-                               'rev_page' => $this->getArticleId(),
+                               'rev_page' => $this->getArticleID(),
                                'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
                                'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
                        ),
@@ -3940,31 +4107,6 @@ class Title {
                        && strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0;
        }
 
-       /**
-        * Callback for usort() to do title sorts by (namespace, title)
-        *
-        * @param $a Title
-        * @param $b Title
-        *
-        * @return Integer: result of string comparison, or namespace comparison
-        */
-       public static function compare( $a, $b ) {
-               if ( $a->getNamespace() == $b->getNamespace() ) {
-                       return strcmp( $a->getText(), $b->getText() );
-               } else {
-                       return $a->getNamespace() - $b->getNamespace();
-               }
-       }
-
-       /**
-        * Return a string representation of this title
-        *
-        * @return String representation of this title
-        */
-       public function __toString() {
-               return $this->getPrefixedText();
-       }
-
        /**
         * Check if page exists.  For historical reasons, this function simply
         * checks for the existence of the title in the page table, and will
@@ -3975,7 +4117,7 @@ class Title {
         * @return Bool
         */
        public function exists() {
-               return $this->getArticleId() != 0;
+               return $this->getArticleID() != 0;
        }
 
        /**
@@ -3995,9 +4137,28 @@ class Title {
         * @return Bool
         */
        public function isAlwaysKnown() {
+               $isKnown = null;
+
+               /**
+                * Allows overriding default behaviour for determining if a page exists.
+                * If $isKnown is kept as null, regular checks happen. If it's
+                * a boolean, this value is returned by the isKnown method.
+                *
+                * @since 1.20
+                *
+                * @param Title $title
+                * @param boolean|null $isKnown
+                */
+               wfRunHooks( 'TitleIsAlwaysKnown', array( $this, &$isKnown ) );
+
+               if ( !is_null( $isKnown ) ) {
+                       return $isKnown;
+               }
+
                if ( $this->mInterwiki != '' ) {
                        return true;  // any interwiki link might be viewable, for all we know
                }
+
                switch( $this->mNamespace ) {
                        case NS_MEDIA:
                        case NS_FILE:
@@ -4022,6 +4183,9 @@ class Title {
         * viewed?  In particular, this function may be used to determine if
         * links to the title should be rendered as "bluelinks" (as opposed to
         * "redlinks" to non-existent pages).
+        * Adding something else to this function will cause inconsistency
+        * since LinkHolderArray calls isAlwaysKnown() and does its own
+        * page existence check.
         *
         * @return Bool
         */
@@ -4076,13 +4240,23 @@ class Title {
        }
 
        /**
-        * Is this in a namespace that allows actual pages?
+        * Updates page_touched for this page; called from LinksUpdate.php
         *
-        * @return Bool
-        * @internal note -- uses hardcoded namespace index instead of constants
+        * @return Bool true if the update succeded
         */
-       public function canExist() {
-               return $this->mNamespace >= 0 && $this->mNamespace != NS_MEDIA;
+       public function invalidateCache() {
+               if ( wfReadOnly() ) {
+                       return false;
+               }
+               $dbw = wfGetDB( DB_MASTER );
+               $success = $dbw->update(
+                       'page',
+                       array( 'page_touched' => $dbw->timestamp() ),
+                       $this->pageCond(),
+                       __METHOD__
+               );
+               HTMLFileCache::clearFileCache( $this );
+               return $success;
        }
 
        /**
@@ -4180,61 +4354,6 @@ class Title {
                return $prepend . $namespaceKey;
        }
 
-       /**
-        * Returns true if this is a special page.
-        *
-        * @return boolean
-        */
-       public function isSpecialPage() {
-               return $this->getNamespace() == NS_SPECIAL;
-       }
-
-       /**
-        * Returns true if this title resolves to the named special page
-        *
-        * @param $name String The special page name
-        * @return boolean
-        */
-       public function isSpecial( $name ) {
-               if ( $this->isSpecialPage() ) {
-                       list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() );
-                       if ( $name == $thisName ) {
-                               return true;
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * If the Title refers to a special page alias which is not the local default, resolve
-        * the alias, and localise the name as necessary.  Otherwise, return $this
-        *
-        * @return Title
-        */
-       public function fixSpecialName() {
-               if ( $this->isSpecialPage() ) {
-                       list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform );
-                       if ( $canonicalName ) {
-                               $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par );
-                               if ( $localName != $this->mDbkeyform ) {
-                                       return Title::makeTitle( NS_SPECIAL, $localName );
-                               }
-                       }
-               }
-               return $this;
-       }
-
-       /**
-        * Is this Title in a namespace which contains content?
-        * In other words, is this a content page, for the purposes of calculating
-        * statistics, etc?
-        *
-        * @return Boolean
-        */
-       public function isContentPage() {
-               return MWNamespace::isContent( $this->getNamespace() );
-       }
-
        /**
         * Get all extant redirects to this Title
         *
@@ -4317,50 +4436,6 @@ class Title {
 
        }
 
-       /**
-        * Returns restriction types for the current Title
-        *
-        * @return array applicable restriction types
-        */
-       public function getRestrictionTypes() {
-               if ( $this->isSpecialPage() ) {
-                       return array();
-               }
-
-               $types = self::getFilteredRestrictionTypes( $this->exists() );
-
-               if ( $this->getNamespace() != NS_FILE ) {
-                       # Remove the upload restriction for non-file titles
-                       $types = array_diff( $types, array( 'upload' ) );
-               }
-
-               wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) );
-
-               wfDebug( __METHOD__ . ': applicable restrictions to [[' .
-                       $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
-
-               return $types;
-       }
-       /**
-        * Get a filtered list of all restriction types supported by this wiki.
-        * @param bool $exists True to get all restriction types that apply to
-        * titles that do exist, False for all restriction types that apply to
-        * titles that do not exist
-        * @return array
-        */
-       public static function getFilteredRestrictionTypes( $exists = true ) {
-               global $wgRestrictionTypes;
-               $types = $wgRestrictionTypes;
-               if ( $exists ) {
-                       # Remove the create restriction for existing titles
-                       $types = array_diff( $types, array( 'create' ) );
-               } else {
-                       # Only the create and upload restrictions apply to non-existing titles
-                       $types = array_intersect( $types, array( 'create', 'upload' ) );
-               }
-               return $types;
-       }
-
        /**
         * Returns the raw sort key to be used for categories, with the specified
         * prefix.  This will be fed to Collation::getSortKey() to get a
@@ -4403,7 +4478,7 @@ class Title {
                if ( $this->isSpecialPage() ) {
                        // special pages are in the user language
                        return $wgLang;
-               } elseif ( $this->isCssOrJsPage() ) {
+               } elseif ( $this->isCssOrJsPage() || $this->isCssJsSubpage() ) {
                        // css/js should always be LTR and is, in fact, English
                        return wfGetLangObj( 'en' );
                } elseif ( $this->getNamespace() == NS_MEDIAWIKI ) {