Follow-up to r111091. Dont paste md5 in the code.
[lhc/web/wiklou.git] / includes / Title.php
index b31b87e..0ef4cda 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
@@ -387,8 +387,8 @@ class Title {
                $titles = array( $title );
                while ( --$recurse > 0 ) {
                        if ( $title->isRedirect() ) {
-                               $article = new Article( $title, 0 );
-                               $newtitle = $article->getRedirectTarget();
+                               $page = WikiPage::factory( $title );
+                               $newtitle = $page->getRedirectTarget();
                        } else {
                                break;
                        }
@@ -442,10 +442,6 @@ class Title {
                return null;
        }
 
-# ----------------------------------------------------------------------------
-#      Static functions
-# ----------------------------------------------------------------------------
-
        /**
         * Get the prefixed DB key associated with an ID
         *
@@ -479,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
@@ -530,6 +553,36 @@ class Title {
                return $name;
        }
 
+       /**
+        * Escape a text fragment, say from a link, for a URL
+        *
+        * @param $fragment string containing a URL or link fragment (after the "#")
+        * @return String: escaped string
+        */
+       static function escapeFragmentForURL( $fragment ) {
+               # Note that we don't urlencode the fragment.  urlencoded Unicode
+               # fragments appear not to work in IE (at least up to 7) or in at least
+               # one version of Opera 9.x.  The W3C validator, for one, doesn't seem
+               # to care if they aren't encoded.
+               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.
@@ -544,6 +597,24 @@ class Title {
                }
        }
 
+       /**
+        * Is this Title interwiki?
+        *
+        * @return Bool
+        */
+       public function isExternal() {
+               return ( $this->mInterwiki != '' );
+       }
+
+       /**
+        * Get the interwiki prefix (or null string)
+        *
+        * @return String Interwiki prefix
+        */
+       public function getInterwiki() {
+               return $this->mInterwiki;
+       }
+
        /**
         * Determine whether the object refers to a page within
         * this project and is transcludable.
@@ -571,52 +642,50 @@ class Title {
                return Interwiki::fetch( $this->mInterwiki )->getWikiID();
        }
 
-       /**
-        * Escape a text fragment, say from a link, for a URL
-        *
-        * @param $fragment string containing a URL or link fragment (after the "#")
-        * @return String: escaped string
-        */
-       static function escapeFragmentForURL( $fragment ) {
-               # Note that we don't urlencode the fragment.  urlencoded Unicode
-               # fragments appear not to work in IE (at least up to 7) or in at least
-               # one version of Opera 9.x.  The W3C validator, for one, doesn't seem
-               # to care if they aren't encoded.
-               return Sanitizer::escapeId( $fragment, 'noninitial' );
-       }
-
-# ----------------------------------------------------------------------------
-#      Other stuff
-# ----------------------------------------------------------------------------
-
-       /** Simple accessors */
        /**
         * Get the text form (spaces not underscores) of the main part
         *
         * @return String Main part of the title
         */
-       public function getText() { return $this->mTextform; }
+       public function getText() {
+               return $this->mTextform;
+       }
 
        /**
         * Get the URL-encoded form of the main part
         *
         * @return String Main part of the title, URL-encoded
         */
-       public function getPartialURL() { return $this->mUrlform; }
+       public function getPartialURL() {
+               return $this->mUrlform;
+       }
 
        /**
         * Get the main part with underscores
         *
         * @return String: Main part of the title, with underscores
         */
-       public function getDBkey() { return $this->mDbkeyform; }
+       public function getDBkey() {
+               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.
         *
         * @return Integer: Namespace index
         */
-       public function getNamespace() { return $this->mNamespace; }
+       public function getNamespace() {
+               return $this->mNamespace;
+       }
 
        /**
         * Get the namespace text
@@ -655,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
         *
@@ -694,460 +754,751 @@ class Title {
        }
 
        /**
-        * Get the interwiki prefix (or null string)
-        *
-        * @return String Interwiki prefix
-        */
-       public function getInterwiki() { return $this->mInterwiki; }
-
-       /**
-        * 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 a URL-encoded form of the subpage text
+        * 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 URL-encoded subpage name
+        * @return Boolean
         */
-       public function getSubpageUrlForm() {
-               $text = $this->getSubpageText();
-               $text = wfUrlencode( str_replace( ' ', '_', $text ) );
-               return( $text );
+       public function isContentPage() {
+               return MWNamespace::isContent( $this->getNamespace() );
        }
 
        /**
-        * Get a URL-encoded title (not an actual URL) including interwiki
+        * Would anybody with sufficient privileges be able to move this page?
+        * Some pages just aren't movable.
         *
-        * @return String the URL-encoded form
+        * @return Bool TRUE or FALSE
         */
-       public function getPrefixedURL() {
-               $s = $this->prefix( $this->mDbkeyform );
-               $s = wfUrlencode( str_replace( ' ', '_', $s ) );
-               return $s;
+       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 real URL referring to this title, with interwiki link and
-        * fragment
+        * 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();
         *
-        * @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
+        * @since 1.18
+        * @return Bool
         */
-       public function getFullURL( $query = '', $variant = false ) {
-               global $wgServer, $wgRequest;
-
-               # Hand off all the decisions on urls to getLocalURL
-               $url = $this->getLocalURL( $query, $variant );
-
-               # 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 ) );
-               return $url;
+       public function isMainPage() {
+               return $this->equals( Title::newMainPage() );
        }
 
        /**
-        * Get a URL with no fragment or server name.  If this page is generated
-        * with action=render, $wgServer is prepended.
+        * Is this a subpage?
         *
-        * @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..)
-        * @return String the URL
+        * @return Bool
         */
-       public function getLocalURL( $query = '', $variant = false ) {
-               global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
-               global $wgVariantArticlePath;
-
-               if ( is_array( $query ) ) {
-                       $query = wfArrayToCGI( $query );
-               }
-
-               $interwiki = Interwiki::fetch( $this->mInterwiki );
-               if ( $interwiki ) {
-                       $namespace = $this->getNsText();
-                       if ( $namespace != '' ) {
-                               # Can this actually happen? Interwikis shouldn't be parsed.
-                               # Yes! It can in interwiki transclusion. But... it probably shouldn't.
-                               $namespace .= ':';
-                       }
-                       $url = $interwiki->getURL( $namespace . $this->getDBkey() );
-                       $url = wfAppendQuery( $url, $query );
-               } 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 ) );
-                               }
-                       } else {
-                               global $wgActionPaths;
-                               $url = false;
-                               $matches = array();
-                               if ( !empty( $wgActionPaths ) &&
-                                       preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) )
-                               {
-                                       $action = urldecode( $matches[2] );
-                                       if ( isset( $wgActionPaths[$action] ) ) {
-                                               $query = $matches[1];
-                                               if ( isset( $matches[4] ) ) {
-                                                       $query .= $matches[4];
-                                               }
-                                               $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
-                                               if ( $query != '' ) {
-                                                       $url = wfAppendQuery( $url, $query );
-                                               }
-                                       }
-                               }
-
-                               if ( $url === false ) {
-                                       if ( $query == '-' ) {
-                                               $query = '';
-                                       }
-                                       $url = "{$wgScript}?title={$dbkey}&{$query}";
-                               }
-                       }
-
-                       wfRunHooks( 'GetLocalURL::Internal', array( &$this, &$url, $query, $variant ) );
-
-                       // @todo FIXME: This causes breakage in various places when we
-                       // actually expected a local URL and end up with dupe prefixes.
-                       if ( $wgRequest->getVal( 'action' ) == 'render' ) {
-                               $url = $wgServer . $url;
-                       }
-               }
-               wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query, $variant ) );
-               return $url;
+       public function isSubpage() {
+               return MWNamespace::hasSubpages( $this->mNamespace )
+                       ? strpos( $this->getText(), '/' ) !== false
+                       : false;
        }
 
        /**
-        * Get a URL that's the simplest URL that will be valid to link, locally,
-        * to the current Title.  It includes the fragment, but does not include
-        * the server unless action=render is used (or the link is external).  If
-        * there's a fragment but the prefixed text is empty, we just return a link
-        * to the fragment.
-        *
-        * The result obviously should not be URL-escaped, but does need to be
-        * HTML-escaped if it's being output in HTML.
+        * Is this a conversion table for the LanguageConverter?
         *
-        * @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).
-        * @return String the URL
+        * @return Bool
         */
-       public function getLinkUrl( $query = array(), $variant = false ) {
-               wfProfileIn( __METHOD__ );
-               if ( $this->isExternal() ) {
-                       $ret = $this->getFullURL( $query );
-               } elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) {
-                       $ret = $this->getFragmentForURL();
-               } else {
-                       $ret = $this->getLocalURL( $query, $variant ) . $this->getFragmentForURL();
-               }
-               wfProfileOut( __METHOD__ );
-               return $ret;
+       public function isConversionTable() {
+               return $this->getNamespace() == NS_MEDIAWIKI &&
+                       strpos( $this->getText(), 'Conversiontable' ) !== false;
        }
 
        /**
-        * Get an HTML-escaped version of the URL form, suitable for
-        * using in a link, without a server name or fragment
+        * Does that page contain wikitext, or it is JS, CSS or whatever?
         *
-        * @param $query String an optional query string
-        * @return String the URL
+        * @return Bool
         */
-       public function escapeLocalURL( $query = '' ) {
-               return htmlspecialchars( $this->getLocalURL( $query ) );
+       public function isWikitextPage() {
+               $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
+               wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
+               return $retval;
        }
 
        /**
-        * Get an HTML-escaped version of the URL form, suitable for
-        * using in a link, including the server name and fragment
+        * Could this page contain custom CSS or JavaScript, based
+        * on the title?
         *
-        * @param $query String an optional query string
-        * @return String the URL
+        * @return Bool
         */
-       public function escapeFullURL( $query = '' ) {
-               return htmlspecialchars( $this->getFullURL( $query ) );
+       public function isCssOrJsPage() {
+               $retval = $this->mNamespace == NS_MEDIAWIKI
+                       && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
+               wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
+               return $retval;
        }
 
        /**
-        * HTML-escaped version of getCanonicalURL()
+        * Is this a .css or .js subpage of a user page?
+        * @return Bool
         */
-       public function escapeCanonicalURL( $query = '', $variant = false ) {
-               return htmlspecialchars( $this->getCanonicalURL( $query, $variant ) );
+       public function isCssJsSubpage() {
+               return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
        }
 
        /**
-        * Get the URL form for an internal link.
-        * - Used in various Squid-related code, in case we have a different
-        * internal hostname for the server from the exposed one.
-        *
-        * This uses $wgInternalServer to qualify the path, or $wgServer
-        * if $wgInternalServer is not set. If the server variable used is
-        * protocol-relative, the URL will be expanded to http://
+        * Trim down a .css or .js subpage title to get the corresponding skin name
         *
-        * @param $query String an optional query string
-        * @param $variant String language variant of url (for sr, zh..)
-        * @return String the URL
+        * @return string containing skin name from .css or .js subpage title
         */
-       public function getInternalURL( $query = '', $variant = false ) {
-               global $wgInternalServer, $wgServer;
-               $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
-               $url = wfExpandUrl( $server . $this->getLocalURL( $query, $variant ), PROTO_HTTP );
-               wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query, $variant ) );
-               return $url;
+       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 );
        }
 
        /**
-        * Get the URL for a canonical link, for use in things like IRC and
-        * e-mail notifications. Uses $wgCanonicalServer and the
-        * GetCanonicalURL hook.
-        *
-        * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment
+        * Is this a .css subpage of a user page?
         *
-        * @param $query string An optional query string
-        * @param $variant string Language variant of URL (for sr, zh, ...)
-        * @return string The URL
+        * @return Bool
         */
-       public function getCanonicalURL( $query = '', $variant = false ) {
-               $url = wfExpandUrl( $this->getLocalURL( $query, $variant ) . $this->getFragmentForURL(), PROTO_CANONICAL );
-               wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query, $variant ) );
-               return $url;
+       public function isCssSubpage() {
+               return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
        }
 
        /**
-        * Get the edit URL for this Title
+        * Is this a .js subpage of a user page?
         *
-        * @return String the URL, or a null string if this is an
-        *  interwiki link
+        * @return Bool
         */
-       public function getEditURL() {
-               if ( $this->mInterwiki != '' ) {
-                       return '';
-               }
-               $s = $this->getLocalURL( 'action=edit' );
-
-               return $s;
+       public function isJsSubpage() {
+               return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
        }
 
        /**
-        * Get the HTML-escaped displayable text form.
-        * Used for the title field in <a> tags.
+        * Is this a talk page of some sort?
         *
-        * @return String the text, including any prefixes
+        * @return Bool
         */
-       public function getEscapedText() {
-               return htmlspecialchars( $this->getPrefixedText() );
+       public function isTalkPage() {
+               return MWNamespace::isTalk( $this->getNamespace() );
        }
 
        /**
-        * Is this Title interwiki?
+        * Get a Title object associated with the talk page of this article
         *
-        * @return Bool
+        * @return Title the object for the talk page
         */
-       public function isExternal() {
-               return ( $this->mInterwiki != '' );
+       public function getTalkPage() {
+               return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
        }
 
        /**
-        * Is this page "semi-protected" - the *only* protection is autoconfirm?
+        * Get a title object associated with the subject page of this
+        * talk page
         *
-        * @param $action String Action to check (default: edit)
-        * @return Bool
+        * @return Title the object for the subject page
         */
-       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;
+       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() );
        }
 
        /**
-        * Does the title correspond to a protected article?
+        * Get the default namespace index, for when there is no namespace
         *
-        * @param $action String the action the page is protected from,
-        * by default checks all actions.
-        * @return Bool
+        * @return Int Default namespace index
         */
-       public function isProtected( $action = '' ) {
-               global $wgRestrictionLevels;
-
-               $restrictionTypes = $this->getRestrictionTypes();
-
-               # Special pages have inherent protection
-               if( $this->getNamespace() == NS_SPECIAL ) {
-                       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;
+       public function getDefaultNamespace() {
+               return $this->mDefaultNamespace;
        }
 
        /**
-        * Is this a conversion table for the LanguageConverter?
+        * Get title for search index
         *
-        * @return Bool
+        * @return String a stripped-down title string ready for the
+        *  search index
         */
-       public function isConversionTable() {
-               if(
-                       $this->getNamespace() == NS_MEDIAWIKI &&
-                       strpos( $this->getText(), 'Conversiontable' ) !== false
-               )
-               {
-                       return true;
+       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 ) );
+               return $url;
+       }
+
+       /**
+        * Get a URL with no fragment or server name.  If this page is generated
+        * with action=render, $wgServer is prepended.
+        *
+
+        * @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 = '', $query2 = false ) {
+               global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
+
+               $query = self::fixUrlQueryArgs( $query, $query2 );
+
+               $interwiki = Interwiki::fetch( $this->mInterwiki );
+               if ( $interwiki ) {
+                       $namespace = $this->getNsText();
+                       if ( $namespace != '' ) {
+                               # Can this actually happen? Interwikis shouldn't be parsed.
+                               # Yes! It can in interwiki transclusion. But... it probably shouldn't.
+                               $namespace .= ':';
+                       }
+                       $url = $interwiki->getURL( $namespace . $this->getDBkey() );
+                       $url = wfAppendQuery( $url, $query );
+               } else {
+                       $dbkey = wfUrlencode( $this->getPrefixedDBkey() );
+                       if ( $query == '' ) {
+                               $url = str_replace( '$1', $dbkey, $wgArticlePath );
+                               wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) );
+                       } else {
+                               global $wgVariantArticlePath, $wgActionPaths;
+                               $url = false;
+                               $matches = array();
+
+                               if ( !empty( $wgActionPaths ) &&
+                                       preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) )
+                               {
+                                       $action = urldecode( $matches[2] );
+                                       if ( isset( $wgActionPaths[$action] ) ) {
+                                               $query = $matches[1];
+                                               if ( isset( $matches[4] ) ) {
+                                                       $query .= $matches[4];
+                                               }
+                                               $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
+                                               if ( $query != '' ) {
+                                                       $url = wfAppendQuery( $url, $query );
+                                               }
+                                       }
+                               }
+
+                               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 = '';
+                                       }
+                                       $url = "{$wgScript}?title={$dbkey}&{$query}";
+                               }
+                       }
+
+                       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.
+                       if ( $wgRequest->getVal( 'action' ) == 'render' ) {
+                               $url = $wgServer . $url;
+                       }
+               }
+               wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) );
+               return $url;
+       }
+
+       /**
+        * Get a URL that's the simplest URL that will be valid to link, locally,
+        * to the current Title.  It includes the fragment, but does not include
+        * the server unless action=render is used (or the link is external).  If
+        * there's a fragment but the prefixed text is empty, we just return a link
+        * to the fragment.
+        *
+        * The result obviously should not be URL-escaped, but does need to be
+        * HTML-escaped if it's being output in HTML.
+        *
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
+        * @return String the URL
+        */
+       public function getLinkURL( $query = '', $query2 = false ) {
+               wfProfileIn( __METHOD__ );
+               if ( $this->isExternal() ) {
+                       $ret = $this->getFullURL( $query, $query2 );
+               } elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) {
+                       $ret = $this->getFragmentForURL();
+               } else {
+                       $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
+               }
+               wfProfileOut( __METHOD__ );
+               return $ret;
+       }
+
+       /**
+        * Get an HTML-escaped version of the URL form, suitable for
+        * using in a link, without a server name or fragment
+        *
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
+        * @return String the URL
+        */
+       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
+        *
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
+        * @return String the URL
+        */
+       public function escapeFullURL( $query = '', $query2 = false ) {
+               wfDeprecated( __METHOD__, '1.19' );
+               return htmlspecialchars( $this->getFullURL( $query, $query2 ) );
+       }
+
+       /**
+        * Get the URL form for an internal link.
+        * - Used in various Squid-related code, in case we have a different
+        * internal hostname for the server from the exposed one.
+        *
+        * This uses $wgInternalServer to qualify the path, or $wgServer
+        * if $wgInternalServer is not set. If the server variable used is
+        * protocol-relative, the URL will be expanded to http://
+        *
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
+        * @return String the URL
+        */
+       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 ), PROTO_HTTP );
+               wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) );
+               return $url;
+       }
+
+       /**
+        * Get the URL for a canonical link, for use in things like IRC and
+        * e-mail notifications. Uses $wgCanonicalServer and the
+        * GetCanonicalURL hook.
+        *
+        * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment
+        *
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
+        * @return string The URL
+        * @since 1.18
+        */
+       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 = '', $query2 = false ) {
+               wfDeprecated( __METHOD__, '1.19' );
+               return htmlspecialchars( $this->getCanonicalURL( $query, $query2 ) );
+       }
+
+       /**
+        * Get the edit URL for this Title
+        *
+        * @return String the URL, or a null string if this is an
+        *  interwiki link
+        */
+       public function getEditURL() {
+               if ( $this->mInterwiki != '' ) {
+                       return '';
                }
+               $s = $this->getLocalURL( 'action=edit' );
 
-               return false;
+               return $s;
        }
 
        /**
@@ -1169,7 +1520,19 @@ class Title {
        }
 
        /**
-        * Can $wgUser perform $action on this page?
+        * Can $wgUser read this page?
+        *
+        * @deprecated in 1.19; use userCan(), quickUserCan() or getUserPermissionsErrors() instead
+        * @return Bool
+        * @todo fold these checks into userCan()
+        */
+       public function userCanRead() {
+               wfDeprecated( __METHOD__, '1.19' );
+               return $this->userCan( 'read' );
+       }
+
+       /**
+        * Can $user perform $action on this page?
         * This skips potentially expensive cascading permission checks
         * as well as avoids expensive error formatting
         *
@@ -1179,42 +1542,30 @@ 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); $wgUser will be used if not
+        *              provided.
         * @return Bool
         */
-       public function quickUserCan( $action ) {
-               return $this->userCan( $action, 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;
+       public function quickUserCan( $action, $user = null ) {
+               return $this->userCan( $action, $user, false );
        }
 
        /**
-        * 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 $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, $doExpensiveQueries = true ) {
-               global $wgUser;
-               return ( $this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries, true ) === array() );
+       public function userCan( $action, $user = null, $doExpensiveQueries = true ) {
+               if ( !$user instanceof User ) {
+                       global $wgUser;
+                       $user = $wgUser;
+               }
+               return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries, true ) );
        }
 
        /**
@@ -1224,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() ) {
@@ -1256,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' );
@@ -1291,33 +1644,16 @@ 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 ) ) {
-                       // We avoid expensive display logic for quickUserCan's and such
-                       $groups = false;
-                       if ( !$short ) {
-                               $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
-                                       User::getGroupsWithPermission( $action, $ns ) );
-                       }
-
-                       if ( $groups ) {
-                               global $wgLang;
-                               $return = array(
-                                       'badaccess-groups',
-                                       $wgLang->commaList( $groups ),
-                                       count( $groups )
-                               );
-                       } else {
-                               $return = array( 'badaccess-group0' );
-                       }
-                       $errors[] = $return;
+               } elseif ( !$user->isAllowed( $action ) ) {
+                       $errors[] = $this->missingPermissionError( $action, $short );
                }
 
                return $errors;
@@ -1392,7 +1728,7 @@ class Title {
        private function checkSpecialsAndNSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
                # Only 'createaccount' and 'execute' can be performed on
                # special pages, which don't actually exist in the DB.
-               $specialOKActions = array( 'createaccount', 'execute' );
+               $specialOKActions = array( 'createaccount', 'execute', 'read' );
                if ( NS_SPECIAL == $this->mNamespace && !in_array( $action, $specialOKActions ) ) {
                        $errors[] = array( 'ns-specialprotected' );
                }
@@ -1454,15 +1790,12 @@ 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
-                               if ( $action == 'edit' && $user->isAllowed( 'editprotected', $this->mNamespace ) ) {
-                                       // Users with 'editprotected' permission cannot edit protected pages
-                                       // with cascading option turned on.
-                                       if ( $this->mCascadeRestriction ) {
-                                               $errors[] = array( 'protectedpagetext', $right );
-                                       }
-                               } else {
+                               // without cascading option turned on.
+                               if ( $action != 'edit' || !$user->isAllowed( 'editprotected' )
+                                       || $this->mCascadeRestriction )
+                               {
                                        $errors[] = array( 'protectedpagetext', $right );
                                }
                        }
@@ -1497,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";
@@ -1522,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' );
                        }
@@ -1534,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'] );
                                }
                        }
@@ -1554,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;
        }
@@ -1570,21 +1911,19 @@ class Title {
         * @return Array list of errors
         */
        private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) {
-               if( !$doExpensiveQueries ) {
+               // Account creation blocks handled at userlogin.
+               // Unblocking handled in SpecialUnblock
+               if( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) {
                        return $errors;
                }
 
                global $wgContLang, $wgLang, $wgEmailConfirmToEdit;
 
-               if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount' ) {
+               if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
                        $errors[] = array( 'confirmedittext' );
                }
 
-               if ( in_array( $action, array( 'read', 'createaccount', 'unblock' ) ) ){
-                       // Edit blocks should not affect reading.
-                       // Account creation blocks handled at userlogin.
-                       // Unblocking handled in SpecialUnblock
-               } elseif( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ){
+               if ( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ) {
                        // 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 ) {
@@ -1626,181 +1965,22 @@ class Title {
        }
 
        /**
-        * Can $user perform $action on this page? This is an internal function,
-        * which checks ONLY that previously checked by userCan (i.e. it leaves out
-        * checks on wfReadOnly() and blocks)
+        * Check that the user is allowed to read this page.
         *
-        * @param $action String action that permission needs to be checked for
+        * @param $action String the action to check
         * @param $user User to check
-        * @param $doExpensiveQueries Bool Set this to false to avoid doing unnecessary queries.
-        * @param $short Bool Set this to true to stop after the first permission error.
-        * @return Array of arrays of the arguments to wfMsg to explain permissions problems.
-        */
-       protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) {
-               wfProfileIn( __METHOD__ );
-
-               $errors = array();
-               $checks = array(
-                       'checkQuickPermissions',
-                       'checkPermissionHooks',
-                       'checkSpecialsAndNSPermissions',
-                       'checkCSSandJSPermissions',
-                       'checkPageRestrictions',
-                       'checkCascadingSourcesRestrictions',
-                       'checkActionPermissions',
-                       'checkUserBlock'
-               );
-
-               while( count( $checks ) > 0 &&
-                          !( $short && count( $errors ) > 0 ) ) {
-                       $method = array_shift( $checks );
-                       $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short );
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $errors;
-       }
-
-       /**
-        * Is this title subject to title protection?
-        * Title protection is the one applied against creation of such title.
-        *
-        * @return Mixed An associative array representing any existent title
-        *   protection, or false if there's none.
-        */
-       private function getTitleProtection() {
-               // Can't protect pages in special namespaces
-               if ( $this->getNamespace() < 0 ) {
-                       return false;
-               }
-
-               // Can't protect pages that exist.
-               if ( $this->exists() ) {
-                       return false;
-               }
-
-               if ( !isset( $this->mTitleProtection ) ) {
-                       $dbr = wfGetDB( DB_SLAVE );
-                       $res = $dbr->select( 'protected_titles', '*',
-                               array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
-                               __METHOD__ );
-
-                       // fetchRow returns false if there are no rows.
-                       $this->mTitleProtection = $dbr->fetchRow( $res );
-               }
-               return $this->mTitleProtection;
-       }
-
-       /**
-        * Update the title protection status
-        *
-        * @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() );
-
-               $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;
-               }
-
-               # Update the protection log
-               if ( $dbw->affectedRows() ) {
-                       $log = new LogPage( 'protect' );
-
-                       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 );
-                       }
-               }
-
-               return true;
-       }
-
-       /**
-        * Remove any title protection due to page existing
-        */
-       public function deleteTitleProtection() {
-               $dbw = wfGetDB( DB_MASTER );
-
-               $dbw->delete(
-                       'protected_titles',
-                       array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
-                       __METHOD__
-               );
-               $this->mTitleProtection = false;
-       }
-
-       /**
-        * 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;
-       }
-
-       /**
-        * Can $wgUser read this page?
+        * @param $errors Array list of current errors
+        * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
+        * @param $short Boolean short circuit on first error
         *
-        * @return Bool
-        * @todo fold these checks into userCan()
+        * @return Array list of errors
         */
-       public function userCanRead() {
-               global $wgUser, $wgGroupPermissions;
-
+       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 $wgRevokePermissions;
                        $useShortcut = true;
                        if ( empty( $wgGroupPermissions['*']['read'] ) ) {
                                # Not a public wiki, so no shortcut
@@ -1822,253 +2002,348 @@ class Title {
                        }
                }
 
-               $result = null;
-               wfRunHooks( 'userCan', array( &$this, &$wgUser, 'read', &$result ) );
-               if ( $result !== null ) {
-                       return $result;
-               }
-
-               # Shortcut for public wikis, allows skipping quite a bit of code
+               $whitelisted = false;
                if ( $useShortcut ) {
-                       return true;
-               }
-
-               if ( $wgUser->isAllowed( 'read' ) ) {
-                       return true;
-               } else {
-                       global $wgWhitelistRead;
-
+                       # 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' )
+               ) {
                        # Always grant access to the login page.
                        # Even anons need to be able to log in.
-                       if ( $this->isSpecial( 'Userlogin' ) || $this->isSpecial( 'ChangePassword' ) ) {
-                               return true;
-                       }
-
-                       # Bail out if there isn't whitelist
-                       if ( !is_array( $wgWhitelistRead ) ) {
-                               return false;
-                       }
-
-                       # Check for explicit whitelisting
+                       $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
-                       if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) )
-                               return true;
 
-                       # Old settings might have the title prefixed with
-                       # a colon for main-namespace pages
-                       if ( $this->getNamespace() == NS_MAIN ) {
+                       // Check for explicit whitelisting with and without underscores
+                       if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
+                               $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 true;
+                                       $whitelisted = true;
                                }
-                       }
-
-                       # If it's a special page, ditch the subpage bit and check again
-                       if ( $this->getNamespace() == NS_SPECIAL ) {
+                       } 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 ) {
-                                       # Invalid special page, but we show standard login required message
-                                       return false;
-                               }
-
-                               $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
-                               if ( in_array( $pure, $wgWhitelistRead, true ) ) {
-                                       return true;
+                               if ( $name !== false ) {
+                                       $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
+                                       if ( in_array( $pure, $wgWhitelistRead, true ) ) {
+                                               $whitelisted = true;
+                                       }
                                }
                        }
+               }
 
+               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 false;
+
+               return $errors;
        }
 
        /**
-        * 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();
+        * Get a description array when the user doesn't have the right to perform
+        * $action (i.e. when User::isAllowed() returns false)
         *
-        * @since 1.18
-        * @return Bool
+        * @param $action String the action to check
+        * @param $short Boolean short circuit on first error
+        * @return Array list of errors
         */
-       public function isMainPage() {
-               return $this->equals( Title::newMainPage() );
+       private function missingPermissionError( $action, $short ) {
+               // We avoid expensive display logic for quickUserCan's and such
+               if ( $short ) {
+                       return array( 'badaccess-group0' );
+               }
+
+               $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
+                       User::getGroupsWithPermission( $action ) );
+
+               if ( count( $groups ) ) {
+                       global $wgLang;
+                       return array(
+                               'badaccess-groups',
+                               $wgLang->commaList( $groups ),
+                               count( $groups )
+                       );
+               } else {
+                       return array( 'badaccess-group0' );
+               }
        }
 
        /**
-        * Is this a talk page of some sort?
+        * Can $user perform $action on this page? This is an internal function,
+        * which checks ONLY that previously checked by userCan (i.e. it leaves out
+        * checks on wfReadOnly() and blocks)
         *
-        * @return Bool
+        * @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.
+        * @param $short Bool Set this to true to stop after the first permission error.
+        * @return Array of arrays of the arguments to wfMsg to explain permissions problems.
         */
-       public function isTalkPage() {
-               return MWNamespace::isTalk( $this->getNamespace() );
+       protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) {
+               wfProfileIn( __METHOD__ );
+
+               # Read has special handling
+               if ( $action == 'read' ) {
+                       $checks = array(
+                               'checkPermissionHooks',
+                               'checkReadPermissions',
+                       );
+               } else {
+                       $checks = array(
+                               'checkQuickPermissions',
+                               'checkPermissionHooks',
+                               'checkSpecialsAndNSPermissions',
+                               'checkCSSandJSPermissions',
+                               'checkPageRestrictions',
+                               'checkCascadingSourcesRestrictions',
+                               'checkActionPermissions',
+                               'checkUserBlock'
+                       );
+               }
+
+               $errors = array();
+               while( count( $checks ) > 0 &&
+                               !( $short && count( $errors ) > 0 ) ) {
+                       $method = array_shift( $checks );
+                       $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short );
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $errors;
        }
 
        /**
-        * Is this a subpage?
+        * 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 isSubpage() {
-               return MWNamespace::hasSubpages( $this->mNamespace )
-                       ? strpos( $this->getText(), '/' ) !== false
-                       : false;
+       public function userCanEditCssSubpage() {
+               global $wgUser;
+               wfDeprecated( __METHOD__, '1.19' );
+               return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) )
+                       || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
        }
 
        /**
-        * Does this have subpages?  (Warning, usually requires an extra DB query.)
+        * 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 hasSubpages() {
-               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
-                       # Duh
-                       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;
-               }
+       public function userCanEditJsSubpage() {
+               global $wgUser;
+               wfDeprecated( __METHOD__, '1.19' );
+               return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) )
+                          || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
+       }
 
-               $subpages = $this->getSubpages( 1 );
-               if ( $subpages instanceof TitleArray ) {
-                       return $this->mHasSubpages = (bool)$subpages->count();
+       /**
+        * 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 $this->mHasSubpages = false;
+               return $types;
        }
 
        /**
-        * Get all subpages of this page.
+        * Returns restriction types for the current Title
         *
-        * @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
+        * @return array applicable restriction types
         */
-       public function getSubpages( $limit = -1 ) {
-               if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
+       public function getRestrictionTypes() {
+               if ( $this->isSpecialPage() ) {
                        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;
+               $types = self::getFilteredRestrictionTypes( $this->exists() );
+
+               if ( $this->getNamespace() != NS_FILE ) {
+                       # Remove the upload restriction for non-file titles
+                       $types = array_diff( $types, array( 'upload' ) );
                }
-               return $this->mSubpages = TitleArray::newFromResult(
-                       $dbr->select( 'page',
-                               array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ),
-                               $conds,
-                               __METHOD__,
-                               $options
-                       )
-               );
+
+               wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) );
+
+               wfDebug( __METHOD__ . ': applicable restrictions to [[' .
+                       $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
+
+               return $types;
        }
 
        /**
-        * Does that page contain wikitext, or it is JS, CSS or whatever?
+        * Is this title subject to title protection?
+        * Title protection is the one applied against creation of such title.
         *
-        * @return Bool
+        * @return Mixed An associative array representing any existent title
+        *   protection, or false if there's none.
         */
-       public function isWikitextPage() {
-               $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
-               wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
-               return $retval;
+       private function getTitleProtection() {
+               // Can't protect pages in special namespaces
+               if ( $this->getNamespace() < 0 ) {
+                       return false;
+               }
+
+               // Can't protect pages that exist.
+               if ( $this->exists() ) {
+                       return false;
+               }
+
+               if ( !isset( $this->mTitleProtection ) ) {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $res = $dbr->select( 'protected_titles', '*',
+                               array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
+                               __METHOD__ );
+
+                       // fetchRow returns false if there are no rows.
+                       $this->mTitleProtection = $dbr->fetchRow( $res );
+               }
+               return $this->mTitleProtection;
        }
 
        /**
-        * Could this page contain custom CSS or JavaScript, based
-        * on the title?
+        * Update the title protection status
         *
-        * @return Bool
+        * @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 isCssOrJsPage() {
-               $retval = $this->mNamespace == NS_MEDIAWIKI
-                       && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
-               wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
-               return $retval;
-       }
+       public function updateTitleProtection( $create_perm, $reason, $expiry ) {
+               wfDeprecated( __METHOD__, '1.19' );
 
-       /**
-        * 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 ) );
+               global $wgUser;
+
+               $limit = array( 'create' => $create_perm );
+               $expiry = array( 'create' => $expiry );
+
+               $page = WikiPage::factory( $this );
+               $status = $page->doUpdateRestrictions( $limit, $expiry, false, $reason, $wgUser );
+
+               return $status->isOK();
        }
 
        /**
-        * Is this a *valid* .css or .js subpage of a user page?
-        *
-        * @return Bool
-        * @deprecated since 1.17
+        * Remove any title protection due to page existing
         */
-       public function isValidCssJsSubpage() {
-               return $this->isCssJsSubpage();
+       public function deleteTitleProtection() {
+               $dbw = wfGetDB( DB_MASTER );
+
+               $dbw->delete(
+                       'protected_titles',
+                       array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
+                       __METHOD__
+               );
+               $this->mTitleProtection = false;
        }
 
        /**
-        * Trim down a .css or .js subpage title to get the corresponding skin name
+        * Is this page "semi-protected" - the *only* protection is autoconfirm?
         *
-        * @return string containing skin name from .css or .js subpage title
+        * @param $action String Action to check (default: edit)
+        * @return Bool
         */
-       public function getSkinFromCssJsSubpage() {
-               $subpage = explode( '/', $this->mTextform );
-               $subpage = $subpage[ count( $subpage ) - 1 ];
-               return( str_replace( array( '.css', '.js' ), array( '', '' ), $subpage ) );
+       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;
+               }
        }
 
        /**
-        * Is this a .css subpage of a user page?
+        * 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 isCssSubpage() {
-               return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
-       }
+       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;
+                                       }
+                               }
+                       }
+               }
 
-       /**
-        * 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;
        }
 
        /**
@@ -2179,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
         *
@@ -2334,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
         */
@@ -2353,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
+                       )
+               );
        }
 
        /**
@@ -2520,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
         */
@@ -2540,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;
-               }
-               $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;
        }
 
        /**
@@ -2824,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!
@@ -2873,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 );
@@ -2882,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;
                                }
                        }
@@ -2907,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' );
        }
 
        /**
@@ -3148,8 +3452,11 @@ class Title {
         * @return Mixed true on success, getUserPermissionsErrors()-like array on failure
         */
        public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
+               global $wgUser;
                $err = $this->isValidMoveOperation( $nt, $auth, $reason );
                if ( is_array( $err ) ) {
+                       // Auto-block user's IP if the account was "hard" blocked
+                       $wgUser->spreadAnyEditBlock();
                        return $err;
                }
 
@@ -3164,15 +3471,14 @@ 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.
                $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 );
@@ -3182,8 +3488,6 @@ class Title {
                        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(
@@ -3207,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',
@@ -3242,51 +3548,8 @@ 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 {
-                               $oldarticle = new Article( $this );
-                               MessageCache::singleton()->replace( $this->getDBkey(), $oldarticle->getContent() );
-                       }
-               }
-               if ( $nt->getNamespace() == NS_MEDIAWIKI ) {
-                       $newarticle = new Article( $nt );
-                       MessageCache::singleton()->replace( $nt->getDBkey(), $newarticle->getContent() );
-               }
-
-               global $wgUser;
                wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) );
                return true;
        }
@@ -3336,38 +3599,18 @@ class Title {
 
                $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__ );
-                               global $wgUseTrackbacks;
-                               if ( $wgUseTrackbacks ) {
-                                       $dbw->delete( 'trackbacks', array( 'tb_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( '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
@@ -3377,30 +3620,33 @@ 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 = new Article( $nt );
+               $newpage->updateRevisionOn( $dbw, $nullRevision );
+
                wfRunHooks( 'NewRevisionFromEditComplete',
-                       array( $article, $nullRevision, $latest, $wgUser ) );
-               $article->setCachedLastEditTime( $now );
+                       array( $newpage, $nullRevision, $latest, $wgUser ) );
+
+               $newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) );
 
                # 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 = new Article( $this );
+                       $redirectArticle = WikiPage::factory( $this );
                        $newid = $redirectArticle->insertOn( $dbw );
                        if ( $newid ) { // sanity
                                $redirectRevision = new Revision( array(
@@ -3413,33 +3659,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();
        }
 
        /**
@@ -3575,6 +3801,9 @@ class Title {
                }
                # Get the article text
                $rev = Revision::newFromTitle( $nt );
+               if( !is_object( $rev ) ){
+                       return false;
+               }
                $text = $rev->getText();
                # Does the redirect point to the source?
                # Or is it a broken self-redirect, usually caused by namespace collisions?
@@ -3595,15 +3824,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.
@@ -3764,6 +3984,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.
@@ -3849,31 +4104,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
@@ -3935,7 +4165,21 @@ class Title {
         * @return Bool
         */
        public function isKnown() {
-               return $this->isAlwaysKnown() || $this->exists();
+               $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( 'TitleIsKnown', array( $this, &$isKnown ) );
+               
+               return is_null( $isKnown ) ? ( $this->isAlwaysKnown() || $this->exists() ) : $isKnown;
        }
 
        /**
@@ -3985,13 +4229,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;
        }
 
        /**
@@ -4058,46 +4312,6 @@ class Title {
                return $this->mNotificationTimestamp[$uid];
        }
 
-       /**
-        * Get the trackback URL for this page
-        *
-        * @return String Trackback URL
-        */
-       public function trackbackURL() {
-               global $wgScriptPath, $wgServer, $wgScriptExtension;
-
-               return "$wgServer$wgScriptPath/trackback$wgScriptExtension?article="
-                       . htmlspecialchars( urlencode( $this->getPrefixedDBkey() ) );
-       }
-
-       /**
-        * Get the trackback RDF for this page
-        *
-        * @return String Trackback RDF
-        */
-       public function trackbackRDF() {
-               $url = htmlspecialchars( $this->getFullURL() );
-               $title = htmlspecialchars( $this->getText() );
-               $tburl = $this->trackbackURL();
-
-               // Autodiscovery RDF is placed in comments so HTML validator
-               // won't barf. This is a rather icky workaround, but seems
-               // frequently used by this kind of RDF thingy.
-               //
-               // Spec: http://www.sixapart.com/pronet/docs/trackback_spec
-               return "<!--
-<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"
-                xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
-                xmlns:trackback=\"http://madskills.com/public/xml/rss/module/trackback/\">
-<rdf:Description
-   rdf:about=\"$url\"
-   dc:identifier=\"$url\"
-   dc:title=\"$title\"
-   trackback:ping=\"$tburl\" />
-</rdf:RDF>
--->";
-       }
-
        /**
         * Generate strings used for xml 'id' names in monobook tabs
         *
@@ -4129,61 +4343,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->getNamespace() == NS_SPECIAL ) {
-                       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->getNamespace() == NS_SPECIAL ) {
-                       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
         *
@@ -4241,7 +4400,7 @@ class Title {
        /**
         * Get a backlink cache object
         *
-        * @return object BacklinkCache
+        * @return BacklinkCache
         */
        function getBacklinkCache() {
                if ( is_null( $this->mBacklinkCache ) ) {
@@ -4266,50 +4425,6 @@ class Title {
 
        }
 
-       /**
-        * Returns restriction types for the current Title
-        *
-        * @return array applicable restriction types
-        */
-       public function getRestrictionTypes() {
-               if ( $this->getNamespace() == NS_SPECIAL ) {
-                       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 restriction types for ' .
-                       $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
@@ -4349,7 +4464,7 @@ class Title {
         */
        public function getPageLanguage() {
                global $wgLang;
-               if ( $this->getNamespace() == NS_SPECIAL ) {
+               if ( $this->isSpecialPage() ) {
                        // special pages are in the user language
                        return $wgLang;
                } elseif ( $this->isCssOrJsPage() ) {
@@ -4367,4 +4482,4 @@ class Title {
                wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) );
                return wfGetLangObj( $pageLang );
        }
-}
\ No newline at end of file
+}