Merge "tiny optimization Title::isValidRedirectTarget()"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 21 Jul 2015 11:11:26 +0000 (11:11 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 21 Jul 2015 11:11:26 +0000 (11:11 +0000)
1  2 
includes/Title.php

diff --combined includes/Title.php
@@@ -29,6 -29,8 +29,6 @@@
   *       however, it does so inefficiently.
   * @note Consider using a TitleValue object instead. TitleValue is more lightweight
   *       and does not rely on global state or the database.
 - *
 - * @internal documentation reviewed 15 Mar 2010
   */
  class Title {
        /** @var MapCacheLRU */
@@@ -72,9 -74,6 +72,9 @@@
        /** @var string Interwiki prefix */
        public $mInterwiki = '';
  
 +      /** @var bool Was this Title created from a string with a local interwiki prefix? */
 +      private $mLocalInterwiki = false;
 +
        /** @var string Title fragment (i.e. the bit after the #) */
        public $mFragment = '';
  
@@@ -96,7 -95,7 +96,7 @@@
        /** @var array Array of groups allowed to edit this article */
        public $mRestrictions = array();
  
 -      /** @var bool  */
 +      /** @var string|bool */
        protected $mOldRestrictions = false;
  
        /** @var bool Cascade restrictions on this page to included templates and images? */
  
        /** @var TitleValue A corresponding TitleValue object */
        private $mTitleValue = null;
 +
 +      /** @var bool Would deleting this page be a big deletion? */
 +      private $mIsBigDeletion = null;
        // @}
  
        /**
        public static function newFromDBkey( $key ) {
                $t = new Title();
                $t->mDbkeyform = $key;
 -              if ( $t->secureAndSplit() ) {
 +
 +              try {
 +                      $t->secureAndSplit();
                        return $t;
 -              } else {
 +              } catch ( MalformedTitleException $ex ) {
                        return null;
                }
        }
         *   by a prefix.  If you want to force a specific namespace even if
         *   $text might begin with a namespace prefix, use makeTitle() or
         *   makeTitleSafe().
 -       * @throws MWException
 +       * @throws InvalidArgumentException
         * @return Title|null Title or null on an error.
         */
        public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
                if ( is_object( $text ) ) {
 -                      throw new MWException( 'Title::newFromText given an object' );
 +                      throw new InvalidArgumentException( '$text must be a string.' );
 +              } elseif ( !is_string( $text ) ) {
 +                      wfDebugLog( 'T76305', wfGetAllCallers( 5 ) );
 +                      wfWarn( __METHOD__ . ': $text must be a string. This will throw an InvalidArgumentException in future.', 2 );
 +              }
 +
 +              try {
 +                      return Title::newFromTextThrow( $text, $defaultNamespace );
 +              } catch ( MalformedTitleException $ex ) {
 +                      return null;
 +              }
 +      }
 +
 +      /**
 +       * Like Title::newFromText(), but throws MalformedTitleException when the title is invalid,
 +       * rather than returning null.
 +       *
 +       * The exception subclasses encode detailed information about why the title is invalid.
 +       *
 +       * @see Title::newFromText
 +       *
 +       * @since 1.25
 +       * @param string $text Title text to check
 +       * @param int $defaultNamespace
 +       * @throws MalformedTitleException If the title is invalid
 +       * @return Title
 +       */
 +      public static function newFromTextThrow( $text, $defaultNamespace = NS_MAIN ) {
 +              if ( is_object( $text ) ) {
 +                      throw new MWException( 'Title::newFromTextThrow given an object' );
                }
  
                $cache = self::getTitleCache();
                $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
  
                $t = new Title();
 -              $t->mDbkeyform = str_replace( ' ', '_', $filteredText );
 +              $t->mDbkeyform = strtr( $filteredText, ' ', '_' );
                $t->mDefaultNamespace = intval( $defaultNamespace );
  
 -              if ( $t->secureAndSplit() ) {
 -                      if ( $defaultNamespace == NS_MAIN ) {
 -                              $cache->set( $text, $t );
 -                      }
 -                      return $t;
 -              } else {
 -                      return null;
 +              $t->secureAndSplit();
 +              if ( $defaultNamespace == NS_MAIN ) {
 +                      $cache->set( $text, $t );
                }
 +              return $t;
        }
  
        /**
                # but some URLs used it as a space replacement and they still come
                # from some external search tools.
                if ( strpos( self::legalChars(), '+' ) === false ) {
 -                      $url = str_replace( '+', ' ', $url );
 +                      $url = strtr( $url, '+', ' ' );
                }
  
 -              $t->mDbkeyform = str_replace( ' ', '_', $url );
 -              if ( $t->secureAndSplit() ) {
 +              $t->mDbkeyform = strtr( $url, ' ', '_' );
 +
 +              try {
 +                      $t->secureAndSplit();
                        return $t;
 -              } else {
 +              } catch ( MalformedTitleException $ex ) {
                        return null;
                }
        }
                        if ( isset( $row->page_lang ) ) {
                                $this->mDbPageLanguage = (string)$row->page_lang;
                        }
 +                      if ( isset( $row->page_restrictions ) ) {
 +                              $this->mOldRestrictions = $row->page_restrictions;
 +                      }
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                $t->mInterwiki = $interwiki;
                $t->mFragment = $fragment;
                $t->mNamespace = $ns = intval( $ns );
 -              $t->mDbkeyform = str_replace( ' ', '_', $title );
 +              $t->mDbkeyform = strtr( $title, ' ', '_' );
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
 -              $t->mTextform = str_replace( '_', ' ', $title );
 +              $t->mTextform = strtr( $title, '_', ' ' );
                $t->mContentModel = false; # initialized lazily in getContentModel()
                return $t;
        }
                }
  
                $t = new Title();
 -              $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki );
 -              if ( $t->secureAndSplit() ) {
 +              $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki, true );
 +
 +              try {
 +                      $t->secureAndSplit();
                        return $t;
 -              } else {
 +              } catch ( MalformedTitleException $ex ) {
                        return null;
                }
        }
         * 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.
         *
 -       * @todo: move this into MediaWikiTitleCodec
 +       * @deprecated since 1.25, use MediaWikiTitleCodec::getTitleInvalidRegex() instead
         *
         * @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;
 +              wfDeprecated( __METHOD__, '1.25' );
 +              return MediaWikiTitleCodec::getTitleInvalidRegex();
        }
  
        /**
         * @param string $title The DB key form the title
         * @param string $fragment The link fragment (after the "#")
         * @param string $interwiki The interwiki prefix
 +       * @param bool $canoncialNamespace If true, use the canonical name for
 +       *   $ns instead of the localized version.
         * @return string The prefixed form of the title
         */
 -      public static function makeName( $ns, $title, $fragment = '', $interwiki = '' ) {
 +      public static function makeName( $ns, $title, $fragment = '', $interwiki = '',
 +              $canoncialNamespace = false
 +      ) {
                global $wgContLang;
  
 -              $namespace = $wgContLang->getNsText( $ns );
 +              if ( $canoncialNamespace ) {
 +                      $namespace = MWNamespace::getCanonicalName( $ns );
 +              } else {
 +                      $namespace = $wgContLang->getNsText( $ns );
 +              }
                $name = $namespace == '' ? $title : "$namespace:$title";
                if ( strval( $interwiki ) != '' ) {
                        $name = "$interwiki:$name";
  
        /**
         * Determine whether the object refers to a page within
 -       * this project.
 +       * this project (either this wiki or a wiki with a local
 +       * interwiki, see https://www.mediawiki.org/wiki/Manual:Interwiki_table#iw_local )
         *
         * @return bool True if this is an in-project interwiki link or a wikilink, false otherwise
         */
                return $this->mInterwiki;
        }
  
 +      /**
 +       * Was this a local interwiki link?
 +       *
 +       * @return bool
 +       */
 +      public function wasLocalInterwiki() {
 +              return $this->mLocalInterwiki;
 +      }
 +
        /**
         * Determine whether the object refers to a page within
         * this project and is transcludable.
        /**
         * Get a TitleValue object representing this Title.
         *
 -       * @note: Not all valid Titles have a corresponding valid TitleValue
 +       * @note Not all valid Titles have a corresponding valid TitleValue
         * (e.g. TitleValues cannot represent page-local links that have a
         * fragment but no title text).
         *
        /**
         * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
         *
 -       * @throws MWException
 +       * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
         * @return string Content model id
         */
 -      public function getContentModel() {
 -              if ( !$this->mContentModel ) {
 +      public function getContentModel( $flags = 0 ) {
 +              if ( !$this->mContentModel && $this->getArticleID( $flags ) ) {
                        $linkCache = LinkCache::singleton();
 +                      $linkCache->addLinkObj( $this ); # in case we already had an article ID
                        $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
                }
  
                        $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
                }
  
 -              if ( !$this->mContentModel ) {
 -                      throw new MWException( 'Failed to determine content model!' );
 -              }
 -
                return $this->mContentModel;
        }
  
                }
  
                try {
 -                      $formatter = $this->getTitleFormatter();
 +                      $formatter = self::getTitleFormatter();
                        return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform );
 -              } catch ( InvalidArgumentException $ex )  {
 +              } catch ( InvalidArgumentException $ex ) {
                        wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" );
                        return false;
                }
         * Is this in a namespace that allows actual pages?
         *
         * @return bool
 -       * @internal note -- uses hardcoded namespace index instead of constants
         */
        public function canExist() {
                return $this->mNamespace >= NS_MAIN;
        /**
         * Returns true if the title is inside one of the specified namespaces.
         *
 -       * @param ...$namespaces The namespaces to check for
 +       * @param int $namespaces,... The namespaces to check for
         * @return bool
         * @since 1.19
         */
                }
  
                $result = true;
 -              wfRunHooks( 'TitleIsMovable', array( $this, &$result ) );
 +              Hooks::run( 'TitleIsMovable', array( $this, &$result ) );
                return $result;
        }
  
  
                # @note This hook is also called in ContentHandler::getDefaultModel.
                #   It's called here again to make sure hook functions can force this
 -              #   method to return true even outside the mediawiki namespace.
 +              #   method to return true even outside the MediaWiki namespace.
  
 -              wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) );
 +              Hooks::run( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ), '1.25' );
  
                return $isCssOrJsPage;
        }
                return Title::makeTitle( $subjectNS, $this->getDBkey() );
        }
  
 +      /**
 +       * Get the other title for this page, if this is a subject page
 +       * get the talk page, if it is a subject page get the talk page
 +       *
 +       * @since 1.25
 +       * @throws MWException
 +       * @return Title
 +       */
 +      public function getOtherPage() {
 +              if ( $this->isSpecialPage() ) {
 +                      throw new MWException( 'Special pages cannot have other pages' );
 +              }
 +              if ( $this->isTalkPage() ) {
 +                      return $this->getSubjectPage();
 +              } else {
 +                      return $this->getTalkPage();
 +              }
 +      }
 +
        /**
         * Get the default namespace index, for when there is no namespace
         *
         * @param string $fragment Text
         */
        public function setFragment( $fragment ) {
 -              $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) );
 +              $this->mFragment = strtr( substr( $fragment, 1 ), '_', ' ' );
        }
  
        /**
         */
        public function getPrefixedDBkey() {
                $s = $this->prefix( $this->mDbkeyform );
 -              $s = str_replace( ' ', '_', $s );
 +              $s = strtr( $s, ' ', '_' );
                return $s;
        }
  
        public function getPrefixedText() {
                if ( $this->mPrefixedText === null ) {
                        $s = $this->prefix( $this->mTextform );
 -                      $s = str_replace( '_', ' ', $s );
 +                      $s = strtr( $s, '_', ' ' );
                        $this->mPrefixedText = $s;
                }
                return $this->mPrefixedText;
         */
        public function getSubpageUrlForm() {
                $text = $this->getSubpageText();
 -              $text = wfUrlencode( str_replace( ' ', '_', $text ) );
 +              $text = wfUrlencode( strtr( $text, ' ', '_' ) );
                return $text;
        }
  
         */
        public function getPrefixedURL() {
                $s = $this->prefix( $this->mDbkeyform );
 -              $s = wfUrlencode( str_replace( ' ', '_', $s ) );
 +              $s = wfUrlencode( strtr( $s, ' ', '_' ) );
                return $s;
        }
  
                # Finally, add the fragment.
                $url .= $this->getFragmentForURL();
  
 -              wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) );
 +              Hooks::run( 'GetFullURL', array( &$this, &$url, $query ) );
                return $url;
        }
  
         * $wgServer is prepended to make an absolute URL.
         *
         * @see self::getFullURL to always get an absolute URL.
 +       * @see self::getLinkURL to always get a URL that's the simplest URL that will be
 +       *  valid to link, locally, to the current Title.
         * @see self::newFromText to produce a Title object.
         *
 -       * @param string|array $query an optional query string,
 +       * @param string|array $query 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.
                        $dbkey = wfUrlencode( $this->getPrefixedDBkey() );
                        if ( $query == '' ) {
                                $url = str_replace( '$1', $dbkey, $wgArticlePath );
 -                              wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) );
 +                              Hooks::run( 'GetLocalURL::Article', array( &$this, &$url ) );
                        } else {
                                global $wgVariantArticlePath, $wgActionPaths, $wgContLang;
                                $url = false;
                                }
                        }
  
 -                      wfRunHooks( 'GetLocalURL::Internal', array( &$this, &$url, $query ) );
 +                      Hooks::run( '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.
                                $url = $wgServer . $url;
                        }
                }
 -              wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) );
 +              Hooks::run( 'GetLocalURL', array( &$this, &$url, $query ) );
                return $url;
        }
  
         * @return string The URL
         */
        public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
 -              wfProfileIn( __METHOD__ );
                if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) {
                        $ret = $this->getFullURL( $query, $query2, $proto );
                } elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) {
                } else {
                        $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
                }
 -              wfProfileOut( __METHOD__ );
                return $ret;
        }
  
                $query = self::fixUrlQueryArgs( $query, $query2 );
                $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
                $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
 -              wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) );
 +              Hooks::run( 'GetInternalURL', array( &$this, &$url, $query ) );
                return $url;
        }
  
        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 ) );
 +              Hooks::run( 'GetCanonicalURL', array( &$this, &$url, $query ) );
                return $url;
        }
  
                return $this->mWatched;
        }
  
 -      /**
 -       * Can $wgUser read this page?
 -       *
 -       * @deprecated since 1.19; use userCan(), quickUserCan() or getUserPermissionsErrors() instead
 -       * @return bool
 -       */
 -      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
         * @param string $action Action that permission needs to be checked for
         * @param User $user User to check (since 1.19); $wgUser will be used if not
         *   provided.
 -       * @param bool $doExpensiveQueries Set this to false to avoid doing
 -       *   unnecessary queries.
 +       * @param string $rigor Same format as Title::getUserPermissionsErrors()
         * @return bool
         */
 -      public function userCan( $action, $user = null, $doExpensiveQueries = true ) {
 +      public function userCan( $action, $user = null, $rigor = 'secure' ) {
                if ( !$user instanceof User ) {
                        global $wgUser;
                        $user = $wgUser;
                }
  
 -              return !count( $this->getUserPermissionsErrorsInternal(
 -                      $action, $user, $doExpensiveQueries, true ) );
 +              return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) );
        }
  
        /**
         *
         * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
         *
 -       * @param string $action action that permission needs to be checked for
 +       * @param string $action Action that permission needs to be checked for
         * @param User $user User to check
 -       * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary
 -       *   queries by skipping checks for cascading protections and user blocks.
 -       * @param array $ignoreErrors of Strings Set this to a list of message keys
 +       * @param string $rigor One of (quick,full,secure)
 +       *   - quick  : does cheap permission checks from slaves (usable for GUI creation)
 +       *   - full   : does cheap and expensive checks possibly from a slave
 +       *   - secure : does cheap and expensive checks, using the master as needed
 +       * @param bool $short Set this to true to stop after the first permission error.
 +       * @param array $ignoreErrors Array of Strings Set this to a list of message keys
         *   whose corresponding errors may be ignored.
         * @return array Array of arguments to wfMessage to explain permissions problems.
         */
 -      public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true,
 -              $ignoreErrors = array()
 +      public function getUserPermissionsErrors(
 +              $action, $user, $rigor = 'secure', $ignoreErrors = array()
        ) {
 -              $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries );
 +              $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor );
  
                // Remove the errors being ignored.
                foreach ( $errors as $index => $error ) {
        /**
         * Permissions checks that fail most often, and which are easiest to test.
         *
 -       * @param string $action the action to check
 +       * @param string $action The action to check
         * @param User $user User to check
         * @param array $errors List of current errors
 -       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
 +       * @param string $rigor Same format as Title::getUserPermissionsErrors()
         * @param bool $short Short circuit on first error
         *
         * @return array List of errors
         */
 -      private function checkQuickPermissions( $action, $user, $errors,
 -              $doExpensiveQueries, $short
 -      ) {
 -              if ( !wfRunHooks( 'TitleQuickPermissions',
 -                      array( $this, $user, $action, &$errors, $doExpensiveQueries, $short ) )
 +      private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) {
 +              if ( !Hooks::run( 'TitleQuickPermissions',
 +                      array( $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ) )
                ) {
                        return $errors;
                }
         * @param string $action The action to check
         * @param User $user User to check
         * @param array $errors List of current errors
 -       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
 +       * @param string $rigor Same format as Title::getUserPermissionsErrors()
         * @param bool $short Short circuit on first error
         *
         * @return array List of errors
         */
 -      private function checkPermissionHooks( $action, $user, $errors, $doExpensiveQueries, $short ) {
 +      private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) {
                // Use getUserPermissionsErrors instead
                $result = '';
 -              if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) {
 +              if ( !Hooks::run( 'userCan', array( &$this, &$user, $action, &$result ) ) ) {
                        return $result ? array() : array( array( 'badaccess-group0' ) );
                }
                // Check getUserPermissionsErrors hook
 -              if ( !wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) {
 +              if ( !Hooks::run( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) {
                        $errors = $this->resultToError( $errors, $result );
                }
                // Check getUserPermissionsErrorsExpensive hook
                if (
 -                      $doExpensiveQueries
 +                      $rigor !== 'quick'
                        && !( $short && count( $errors ) > 0 )
 -                      && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) )
 +                      && !Hooks::run( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) )
                ) {
                        $errors = $this->resultToError( $errors, $result );
                }
         * @param string $action The action to check
         * @param User $user User to check
         * @param array $errors List of current errors
 -       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
 +       * @param string $rigor Same format as Title::getUserPermissionsErrors()
         * @param bool $short Short circuit on first error
         *
         * @return array List of errors
         */
 -      private function checkSpecialsAndNSPermissions( $action, $user, $errors,
 -              $doExpensiveQueries, $short
 -      ) {
 +      private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) {
                # Only 'createaccount' can be performed on special pages,
                # which don't actually exist in the DB.
                if ( NS_SPECIAL == $this->mNamespace && $action !== 'createaccount' ) {
                        $ns = $this->mNamespace == NS_MAIN ?
                                wfMessage( 'nstab-main' )->text() : $this->getNsText();
                        $errors[] = $this->mNamespace == NS_MEDIAWIKI ?
 -                              array( 'protectedinterface' ) : array( 'namespaceprotected', $ns );
 +                              array( 'protectedinterface', $action ) : array( 'namespaceprotected', $ns, $action );
                }
  
                return $errors;
         * @param string $action The action to check
         * @param User $user User to check
         * @param array $errors List of current errors
 -       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
 +       * @param string $rigor Same format as Title::getUserPermissionsErrors()
         * @param bool $short Short circuit on first error
         *
         * @return array List of errors
         */
 -      private function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
 +      private function checkCSSandJSPermissions( $action, $user, $errors, $rigor, $short ) {
                # Protect css/js subpages of user pages
                # XXX: this might be better using restrictions
                # XXX: right 'editusercssjs' is deprecated, for backward compatibility only
                if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) {
                        if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
                                if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) {
 -                                      $errors[] = array( 'mycustomcssprotected' );
 +                                      $errors[] = array( 'mycustomcssprotected', $action );
                                } elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) {
 -                                      $errors[] = array( 'mycustomjsprotected' );
 +                                      $errors[] = array( 'mycustomjsprotected', $action );
                                }
                        } else {
                                if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) {
 -                                      $errors[] = array( 'customcssprotected' );
 +                                      $errors[] = array( 'customcssprotected', $action );
                                } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
 -                                      $errors[] = array( 'customjsprotected' );
 +                                      $errors[] = array( 'customjsprotected', $action );
                                }
                        }
                }
         * @param string $action The action to check
         * @param User $user User to check
         * @param array $errors List of current errors
 -       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
 +       * @param string $rigor Same format as Title::getUserPermissionsErrors()
         * @param bool $short Short circuit on first error
         *
         * @return array List of errors
         */
 -      private function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) {
 +      private function checkPageRestrictions( $action, $user, $errors, $rigor, $short ) {
                foreach ( $this->getRestrictions( $action ) as $right ) {
                        // Backwards compatibility, rewrite sysop -> editprotected
                        if ( $right == 'sysop' ) {
                                continue;
                        }
                        if ( !$user->isAllowed( $right ) ) {
 -                              $errors[] = array( 'protectedpagetext', $right );
 +                              $errors[] = array( 'protectedpagetext', $right, $action );
                        } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
 -                              $errors[] = array( 'protectedpagetext', 'protect' );
 +                              $errors[] = array( 'protectedpagetext', 'protect', $action );
                        }
                }
  
         * @param string $action The action to check
         * @param User $user User to check
         * @param array $errors List of current errors
 -       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
 +       * @param string $rigor Same format as Title::getUserPermissionsErrors()
         * @param bool $short Short circuit on first error
         *
         * @return array List of errors
         */
 -      private function checkCascadingSourcesRestrictions( $action, $user, $errors,
 -              $doExpensiveQueries, $short
 -      ) {
 -              if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) {
 +      private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
 +              if ( $rigor !== 'quick' && !$this->isCssJsSubpage() ) {
                        # We /could/ use the protection level on the source page, but it's
                        # fairly ugly as we have to establish a precedence hierarchy for pages
                        # included by multiple cascade-protected pages. So just restrict
                                                foreach ( $cascadingSources as $page ) {
                                                        $pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
                                                }
 -                                              $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages );
 +                                              $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages, $action );
                                        }
                                }
                        }
         * @param string $action The action to check
         * @param User $user User to check
         * @param array $errors List of current errors
 -       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
 +       * @param string $rigor Same format as Title::getUserPermissionsErrors()
         * @param bool $short Short circuit on first error
         *
         * @return array List of errors
         */
 -      private function checkActionPermissions( $action, $user, $errors,
 -              $doExpensiveQueries, $short
 -      ) {
 +      private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) {
                global $wgDeleteRevisionsLimit, $wgLang;
  
                if ( $action == 'protect' ) {
 -                      if ( count( $this->getUserPermissionsErrorsInternal( 'edit',
 -                              $user, $doExpensiveQueries, true ) )
 -                      ) {
 +                      if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
                                // If they can't edit, they shouldn't protect.
                                $errors[] = array( 'protect-cantedit' );
                        }
                } elseif ( $action == 'create' ) {
                        $title_protection = $this->getTitleProtection();
                        if ( $title_protection ) {
 -                              if ( $title_protection['pt_create_perm'] == 'sysop' ) {
 -                                      $title_protection['pt_create_perm'] = 'editprotected'; // B/C
 -                              }
 -                              if ( $title_protection['pt_create_perm'] == 'autoconfirmed' ) {
 -                                      $title_protection['pt_create_perm'] = 'editsemiprotected'; // B/C
 -                              }
 -                              if ( $title_protection['pt_create_perm'] == ''
 -                                      || !$user->isAllowed( $title_protection['pt_create_perm'] )
 +                              if ( $title_protection['permission'] == ''
 +                                      || !$user->isAllowed( $title_protection['permission'] )
                                ) {
                                        $errors[] = array(
                                                'titleprotected',
 -                                              User::whoIs( $title_protection['pt_user'] ),
 -                                              $title_protection['pt_reason']
 +                                              User::whoIs( $title_protection['user'] ),
 +                                              $title_protection['reason']
                                        );
                                }
                        }
                                $errors[] = array( 'immobile-target-page' );
                        }
                } elseif ( $action == 'delete' ) {
 -                      if ( $doExpensiveQueries && $wgDeleteRevisionsLimit
 +                      $tempErrors = $this->checkPageRestrictions( 'edit', $user, array(), $rigor, true );
 +                      if ( !$tempErrors ) {
 +                              $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
 +                                      $user, $tempErrors, $rigor, true );
 +                      }
 +                      if ( $tempErrors ) {
 +                              // If protection keeps them from editing, they shouldn't be able to delete.
 +                              $errors[] = array( 'deleteprotected' );
 +                      }
 +                      if ( $rigor !== 'quick' && $wgDeleteRevisionsLimit
                                && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion()
                        ) {
                                $errors[] = array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) );
         * @param string $action The action to check
         * @param User $user User to check
         * @param array $errors List of current errors
 -       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
 +       * @param string $rigor Same format as Title::getUserPermissionsErrors()
         * @param bool $short Short circuit on first error
         *
         * @return array List of errors
         */
 -      private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) {
 +      private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
                // Account creation blocks handled at userlogin.
                // Unblocking handled in SpecialUnblock
 -              if ( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) {
 +              if ( $rigor === 'quick' || in_array( $action, array( 'createaccount', 'unblock' ) ) ) {
                        return $errors;
                }
  
                        $errors[] = array( 'confirmedittext' );
                }
  
 -              if ( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ) {
 +              $useSlave = ( $rigor !== 'secure' );
 +              if ( ( $action == 'edit' || $action == 'create' )
 +                      && !$user->isBlockedFrom( $this, $useSlave )
 +              ) {
                        // 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 ) {
 +              } elseif ( $user->isBlocked() && $user->getBlock()->prevents( $action ) !== false ) {
                        // @todo FIXME: Pass the relevant context into this function.
                        $errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() );
                }
         * @param string $action The action to check
         * @param User $user User to check
         * @param array $errors List of current errors
 -       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
 +       * @param string $rigor Same format as Title::getUserPermissionsErrors()
         * @param bool $short Short circuit on first error
         *
         * @return array List of errors
         */
 -      private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
 +      private function checkReadPermissions( $action, $user, $errors, $rigor, $short ) {
                global $wgWhitelistRead, $wgWhitelistReadRegexp;
  
                $whitelisted = false;
  
                if ( !$whitelisted ) {
                        # If the title is not whitelisted, give extensions a chance to do so...
 -                      wfRunHooks( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) );
 +                      Hooks::run( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) );
                        if ( !$whitelisted ) {
                                $errors[] = $this->missingPermissionError( $action, $short );
                        }
         *
         * @param string $action Action that permission needs to be checked for
         * @param User $user User to check
 -       * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries.
 +       * @param string $rigor One of (quick,full,secure)
 +       *   - quick  : does cheap permission checks from slaves (usable for GUI creation)
 +       *   - full   : does cheap and expensive checks possibly from a slave
 +       *   - secure : does cheap and expensive checks, using the master as needed
         * @param bool $short Set this to true to stop after the first permission error.
         * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
         */
 -      protected function getUserPermissionsErrorsInternal( $action, $user,
 -              $doExpensiveQueries = true, $short = false
 +      protected function getUserPermissionsErrorsInternal(
 +              $action, $user, $rigor = 'secure', $short = false
        ) {
 -              wfProfileIn( __METHOD__ );
 +              if ( $rigor === true ) {
 +                      $rigor = 'secure'; // b/c
 +              } elseif ( $rigor === false ) {
 +                      $rigor = 'quick'; // b/c
 +              } elseif ( !in_array( $rigor, array( 'quick', 'full', 'secure' ) ) ) {
 +                      throw new Exception( "Invalid rigor parameter '$rigor'." );
 +              }
  
                # Read has special handling
                if ( $action == 'read' ) {
                                'checkPermissionHooks',
                                'checkReadPermissions',
                        );
 +              # Don't call checkSpecialsAndNSPermissions or checkCSSandJSPermissions
 +              # here as it will lead to duplicate error messages. This is okay to do
 +              # since anywhere that checks for create will also check for edit, and
 +              # those checks are called for edit.
 +              } elseif ( $action == 'create' ) {
 +                      $checks = array(
 +                              'checkQuickPermissions',
 +                              'checkPermissionHooks',
 +                              'checkPageRestrictions',
 +                              'checkCascadingSourcesRestrictions',
 +                              'checkActionPermissions',
 +                              'checkUserBlock'
 +                      );
                } else {
                        $checks = array(
                                'checkQuickPermissions',
                while ( count( $checks ) > 0 &&
                                !( $short && count( $errors ) > 0 ) ) {
                        $method = array_shift( $checks );
 -                      $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short );
 +                      $errors = $this->$method( $action, $user, $errors, $rigor, $short );
                }
  
 -              wfProfileOut( __METHOD__ );
                return $errors;
        }
  
                        $types = array_diff( $types, array( 'upload' ) );
                }
  
 -              wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) );
 +              Hooks::run( 'TitleGetRestrictionTypes', array( $this, &$types ) );
  
                wfDebug( __METHOD__ . ': applicable restrictions to [[' .
                        $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
         * @return array|bool An associative array representing any existent title
         *   protection, or false if there's none.
         */
 -      private function getTitleProtection() {
 +      public function getTitleProtection() {
                // Can't protect pages in special namespaces
                if ( $this->getNamespace() < 0 ) {
                        return false;
                        $dbr = wfGetDB( DB_SLAVE );
                        $res = $dbr->select(
                                'protected_titles',
 -                              array( 'pt_user', 'pt_reason', 'pt_expiry', 'pt_create_perm' ),
 +                              array(
 +                                      'user' => 'pt_user',
 +                                      'reason' => 'pt_reason',
 +                                      'expiry' => 'pt_expiry',
 +                                      'permission' => 'pt_create_perm'
 +                              ),
                                array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
                                __METHOD__
                        );
  
                        // fetchRow returns false if there are no rows.
 -                      $this->mTitleProtection = $dbr->fetchRow( $res );
 +                      $row = $dbr->fetchRow( $res );
 +                      if ( $row ) {
 +                              if ( $row['permission'] == 'sysop' ) {
 +                                      $row['permission'] = 'editprotected'; // B/C
 +                              }
 +                              if ( $row['permission'] == 'autoconfirmed' ) {
 +                                      $row['permission'] = 'editsemiprotected'; // B/C
 +                              }
 +                              $row['expiry'] = $dbr->decodeExpiry( $row['expiry'] );
 +                      }
 +                      $this->mTitleProtection = $row;
                }
                return $this->mTitleProtection;
        }
        /**
         * Does the title correspond to a protected article?
         *
 -       * @param string $action the action the page is protected from,
 +       * @param string $action The action the page is protected from,
         * by default checks all actions.
         * @return bool
         */
         *        false.
         */
        public function getCascadeProtectionSources( $getPages = true ) {
 -              global $wgContLang;
                $pagerestrictions = array();
  
                if ( $this->mCascadeSources !== null && $getPages ) {
                        return array( $this->mHasCascadingRestrictions, $pagerestrictions );
                }
  
 -              wfProfileIn( __METHOD__ );
 -
                $dbr = wfGetDB( DB_SLAVE );
  
                if ( $this->getNamespace() == NS_FILE ) {
  
                $sources = $getPages ? array() : false;
                $now = wfTimestampNow();
 -              $purgeExpired = false;
  
                foreach ( $res as $row ) {
 -                      $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW );
 +                      $expiry = $dbr->decodeExpiry( $row->pr_expiry );
                        if ( $expiry > $now ) {
                                if ( $getPages ) {
                                        $page_id = $row->pr_page;
                                } else {
                                        $sources = true;
                                }
 -                      } else {
 -                              // Trigger lazy purge of expired restrictions from the db
 -                              $purgeExpired = true;
                        }
                }
 -              if ( $purgeExpired ) {
 -                      Title::purgeExpiredRestrictions();
 -              }
  
                if ( $getPages ) {
                        $this->mCascadeSources = $sources;
                        $this->mHasCascadingRestrictions = $sources;
                }
  
 -              wfProfileOut( __METHOD__ );
                return array( $sources, $pagerestrictions );
        }
  
         * Accessor/initialisation for mRestrictions
         *
         * @param string $action Action that permission needs to be checked for
 -       * @return array Restriction levels needed to take the action. All levels
 -       *     are required.
 +       * @return array Restriction levels needed to take the action. All levels are
 +       *     required. Note that restriction levels are normally user rights, but 'sysop'
 +       *     and 'autoconfirmed' are also allowed for backwards compatibility. These should
 +       *     be mapped to 'editprotected' and 'editsemiprotected' respectively.
         */
        public function getRestrictions( $action ) {
                if ( !$this->mRestrictionsLoaded ) {
         *   restrictions from page table (pre 1.10)
         */
        public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
 -              global $wgContLang;
                $dbr = wfGetDB( DB_SLAVE );
  
                $restrictionTypes = $this->getRestrictionTypes();
  
                foreach ( $restrictionTypes as $type ) {
                        $this->mRestrictions[$type] = array();
 -                      $this->mRestrictionsExpiry[$type] = $wgContLang->formatExpiry( '', TS_MW );
 +                      $this->mRestrictionsExpiry[$type] = 'infinity';
                }
  
                $this->mCascadeRestriction = false;
  
                # Backwards-compatibility: also load the restrictions from the page record (old format).
 +              if ( $oldFashionedRestrictions !== null ) {
 +                      $this->mOldRestrictions = $oldFashionedRestrictions;
 +              }
  
 -              if ( $oldFashionedRestrictions === null ) {
 -                      $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions',
 +              if ( $this->mOldRestrictions === false ) {
 +                      $this->mOldRestrictions = $dbr->selectField( 'page', 'page_restrictions',
                                array( 'page_id' => $this->getArticleID() ), __METHOD__ );
                }
  
 -              if ( $oldFashionedRestrictions != '' ) {
 -
 -                      foreach ( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) {
 +              if ( $this->mOldRestrictions != '' ) {
 +                      foreach ( explode( ':', trim( $this->mOldRestrictions ) ) as $restrict ) {
                                $temp = explode( '=', trim( $restrict ) );
                                if ( count( $temp ) == 1 ) {
                                        // old old format should be treated as edit/move restriction
                                        }
                                }
                        }
 -
 -                      $this->mOldRestrictions = true;
 -
                }
  
                if ( count( $rows ) ) {
                        # Current system - load second to make them override.
                        $now = wfTimestampNow();
 -                      $purgeExpired = false;
  
                        # Cycle through all the restrictions.
                        foreach ( $rows as $row ) {
  
                                // This code should be refactored, now that it's being used more generally,
                                // But I don't really see any harm in leaving it in Block for now -werdna
 -                              $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW );
 +                              $expiry = $dbr->decodeExpiry( $row->pr_expiry );
  
                                // Only apply the restrictions if they haven't expired!
                                if ( !$expiry || $expiry > $now ) {
                                        $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
  
                                        $this->mCascadeRestriction |= $row->pr_cascade;
 -                              } else {
 -                                      // Trigger a lazy purge of expired restrictions
 -                                      $purgeExpired = true;
                                }
                        }
 -
 -                      if ( $purgeExpired ) {
 -                              Title::purgeExpiredRestrictions();
 -                      }
                }
  
                $this->mRestrictionsLoaded = true;
         *   restrictions from page table (pre 1.10)
         */
        public function loadRestrictions( $oldFashionedRestrictions = null ) {
 -              global $wgContLang;
                if ( !$this->mRestrictionsLoaded ) {
 +                      $dbr = wfGetDB( DB_SLAVE );
                        if ( $this->exists() ) {
 -                              $dbr = wfGetDB( DB_SLAVE );
 -
                                $res = $dbr->select(
                                        'page_restrictions',
                                        array( 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ),
  
                                if ( $title_protection ) {
                                        $now = wfTimestampNow();
 -                                      $expiry = $wgContLang->formatExpiry( $title_protection['pt_expiry'], TS_MW );
 +                                      $expiry = $dbr->decodeExpiry( $title_protection['expiry'] );
  
                                        if ( !$expiry || $expiry > $now ) {
                                                // Apply the restrictions
                                                $this->mRestrictionsExpiry['create'] = $expiry;
 -                                              $this->mRestrictions['create'] = explode( ',', trim( $title_protection['pt_create_perm'] ) );
 +                                              $this->mRestrictions['create'] = explode( ',', trim( $title_protection['permission'] ) );
                                        } else { // Get rid of the old restrictions
 -                                              Title::purgeExpiredRestrictions();
                                                $this->mTitleProtection = false;
                                        }
                                } else {
 -                                      $this->mRestrictionsExpiry['create'] = $wgContLang->formatExpiry( '', TS_MW );
 +                                      $this->mRestrictionsExpiry['create'] = 'infinity';
                                }
                                $this->mRestrictionsLoaded = true;
                        }
  
                $method = __METHOD__;
                $dbw = wfGetDB( DB_MASTER );
 -              $dbw->onTransactionIdle( function() use ( $dbw, $method ) {
 +              $dbw->onTransactionIdle( function () use ( $dbw, $method ) {
                        $dbw->delete(
                                'page_restrictions',
                                array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
                if ( !is_null( $this->mRedirect ) ) {
                        return $this->mRedirect;
                }
 -              # Calling getArticleID() loads the field from cache as needed
                if ( !$this->getArticleID( $flags ) ) {
                        $this->mRedirect = false;
                        return $this->mRedirect;
                }
  
                $linkCache = LinkCache::singleton();
 +              $linkCache->addLinkObj( $this ); # in case we already had an article ID
                $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
                if ( $cached === null ) {
                        # Trust LinkCache's state over our own
                if ( $this->mLength != -1 ) {
                        return $this->mLength;
                }
 -              # Calling getArticleID() loads the field from cache as needed
                if ( !$this->getArticleID( $flags ) ) {
                        $this->mLength = 0;
                        return $this->mLength;
                }
                $linkCache = LinkCache::singleton();
 +              $linkCache->addLinkObj( $this ); # in case we already had an article ID
                $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
                if ( $cached === null ) {
                        # Trust LinkCache's state over our own, as for isRedirect()
                if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
                        return intval( $this->mLatestID );
                }
 -              # Calling getArticleID() loads the field from cache as needed
                if ( !$this->getArticleID( $flags ) ) {
                        $this->mLatestID = 0;
                        return $this->mLatestID;
                }
                $linkCache = LinkCache::singleton();
 -              $linkCache->addLinkObj( $this );
 +              $linkCache->addLinkObj( $this ); # in case we already had an article ID
                $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
                if ( $cached === null ) {
                        # Trust LinkCache's state over our own, as for isRedirect()
                }
                $this->mRestrictionsLoaded = false;
                $this->mRestrictions = array();
 +              $this->mOldRestrictions = false;
                $this->mRedirect = null;
                $this->mLength = -1;
                $this->mLatestID = false;
                $this->mEstimateRevisions = null;
                $this->mPageLanguage = false;
                $this->mDbPageLanguage = null;
 +              $this->mIsBigDeletion = null;
 +      }
 +
 +      public static function clearCaches() {
 +              $linkCache = LinkCache::singleton();
 +              $linkCache->clear();
 +
 +              $titleCache = self::getTitleCache();
 +              $titleCache->clear();
        }
  
        /**
         * namespace prefixes, sets the other forms, and canonicalizes
         * everything.
         *
 +       * @throws MalformedTitleException On invalid titles
         * @return bool True on success
         */
        private function secureAndSplit() {
  
                $dbkey = $this->mDbkeyform;
  
 -              try {
 -                      // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
 -                      //        the parsing code with Title, while avoiding massive refactoring.
 -                      // @todo: get rid of secureAndSplit, refactor parsing code.
 -                      $parser = $this->getTitleParser();
 -                      $parts = $parser->splitTitleString( $dbkey, $this->getDefaultNamespace() );
 -              } catch ( MalformedTitleException $ex ) {
 -                      return false;
 -              }
 +              // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
 +              //        the parsing code with Title, while avoiding massive refactoring.
 +              // @todo: get rid of secureAndSplit, refactor parsing code.
 +              $titleParser = self::getTitleParser();
 +              // MalformedTitleException can be thrown here
 +              $parts = $titleParser->splitTitleString( $dbkey, $this->getDefaultNamespace() );
  
                # Fill fields
                $this->setFragment( '#' . $parts['fragment'] );
                $this->mInterwiki = $parts['interwiki'];
 +              $this->mLocalInterwiki = $parts['local_interwiki'];
                $this->mNamespace = $parts['namespace'];
                $this->mUserCaseDBKey = $parts['user_case_dbkey'];
  
                $this->mDbkeyform = $parts['dbkey'];
                $this->mUrlform = wfUrlencode( $this->mDbkeyform );
 -              $this->mTextform = str_replace( '_', ' ', $this->mDbkeyform );
 +              $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
  
                # We already know that some pages won't be in the database!
                if ( $this->isExternal() || $this->mNamespace == NS_SPECIAL ) {
         * @return array Array of Title objects linking here
         */
        public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
 -              global $wgContentHandlerUseDB;
 -
                $id = $this->getArticleID();
  
                # If the page doesn't exist; there can't be any link from this page
                        $db = wfGetDB( DB_SLAVE );
                }
  
 -              $namespaceFiled = "{$prefix}_namespace";
 -              $titleField = "{$prefix}_title";
 -
 -              $fields = array(
 -                      $namespaceFiled,
 -                      $titleField,
 -                      'page_id',
 -                      'page_len',
 -                      'page_is_redirect',
 -                      'page_latest'
 -              );
 -
 -              if ( $wgContentHandlerUseDB ) {
 -                      $fields[] = 'page_content_model';
 -              }
 +              $blNamespace = "{$prefix}_namespace";
 +              $blTitle = "{$prefix}_title";
  
                $res = $db->select(
                        array( $table, 'page' ),
 -                      $fields,
 +                      array_merge(
 +                              array( $blNamespace, $blTitle ),
 +                              WikiPage::selectFields()
 +                      ),
                        array( "{$prefix}_from" => $id ),
                        __METHOD__,
                        $options,
                        array( 'page' => array(
                                'LEFT JOIN',
 -                              array( "page_namespace=$namespaceFiled", "page_title=$titleField" )
 +                              array( "page_namespace=$blNamespace", "page_title=$blTitle" )
                        ) )
                );
  
                $retVal = array();
 -              if ( $res->numRows() ) {
 -                      $linkCache = LinkCache::singleton();
 -                      foreach ( $res as $row ) {
 -                              $titleObj = Title::makeTitle( $row->$namespaceFiled, $row->$titleField );
 -                              if ( $titleObj ) {
 -                                      if ( $row->page_id ) {
 -                                              $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
 -                                      } else {
 -                                              $linkCache->addBadLinkObj( $titleObj );
 -                                      }
 -                                      $retVal[] = $titleObj;
 -                              }
 +              $linkCache = LinkCache::singleton();
 +              foreach ( $res as $row ) {
 +                      if ( $row->page_id ) {
 +                              $titleObj = Title::newFromRow( $row );
 +                      } else {
 +                              $titleObj = Title::makeTitle( $row->$blNamespace, $row->$blTitle );
 +                              $linkCache->addBadLinkObj( $titleObj );
                        }
 +                      $retVal[] = $titleObj;
                }
 +
                return $retVal;
        }
  
                        $urls[] = $this->getInternalUrl( 'action=raw&ctype=text/css' );
                }
  
 -              wfRunHooks( 'TitleSquidURLs', array( $this, &$urls ) );
 +              Hooks::run( 'TitleSquidURLs', array( $this, &$urls ) );
                return $urls;
        }
  
        /**
         * Move this page without authentication
         *
 +       * @deprecated since 1.25 use MovePage class instead
         * @param Title $nt The new page Title
         * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
         */
        public function moveNoAuth( &$nt ) {
 +              wfDeprecated( __METHOD__, '1.25' );
                return $this->moveTo( $nt, false );
        }
  
         * Check whether a given move operation would be valid.
         * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
         *
 +       * @deprecated since 1.25, use MovePage's methods instead
         * @param Title $nt The new title
 -       * @param bool $auth Indicates whether $wgUser's permissions
 -       *  should be checked
 +       * @param bool $auth Whether to check user permissions (uses $wgUser)
         * @param string $reason Is the log summary of the move, used for spam checking
         * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
         */
        public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
 -              global $wgUser, $wgContentHandlerUseDB;
 +              global $wgUser;
  
 -              $errors = array();
 -              if ( !$nt ) {
 +              if ( !( $nt instanceof Title ) ) {
                        // Normally we'd add this to $errors, but we'll get
                        // lots of syntax errors if $nt is not an object
                        return array( array( 'badtitletext' ) );
                }
 -              if ( $this->equals( $nt ) ) {
 -                      $errors[] = array( 'selfmove' );
 -              }
 -              if ( !$this->isMovable() ) {
 -                      $errors[] = array( 'immobile-source-namespace', $this->getNsText() );
 -              }
 -              if ( $nt->isExternal() ) {
 -                      $errors[] = array( 'immobile-target-namespace-iw' );
 -              }
 -              if ( !$nt->isMovable() ) {
 -                      $errors[] = array( 'immobile-target-namespace', $nt->getNsText() );
 -              }
 -
 -              $oldid = $this->getArticleID();
 -              $newid = $nt->getArticleID();
 -
 -              if ( strlen( $nt->getDBkey() ) < 1 ) {
 -                      $errors[] = array( 'articleexists' );
 -              }
 -              if (
 -                      ( $this->getDBkey() == '' ) ||
 -                      ( !$oldid ) ||
 -                      ( $nt->getDBkey() == '' )
 -              ) {
 -                      $errors[] = array( 'badarticleerror' );
 -              }
 -
 -              // Content model checks
 -              if ( !$wgContentHandlerUseDB &&
 -                              $this->getContentModel() !== $nt->getContentModel() ) {
 -                      // can't move a page if that would change the page's content model
 -                      $errors[] = array(
 -                              'bad-target-model',
 -                              ContentHandler::getLocalizedName( $this->getContentModel() ),
 -                              ContentHandler::getLocalizedName( $nt->getContentModel() )
 -                      );
 -              }
 -
 -              // Image-specific checks
 -              if ( $this->getNamespace() == NS_FILE ) {
 -                      $errors = array_merge( $errors, $this->validateFileMoveOperation( $nt ) );
 -              }
 -
 -              if ( $nt->getNamespace() == NS_FILE && $this->getNamespace() != NS_FILE ) {
 -                      $errors[] = array( 'nonfile-cannot-move-to-file' );
 -              }
  
 +              $mp = new MovePage( $this, $nt );
 +              $errors = $mp->isValidMove()->getErrorsArray();
                if ( $auth ) {
 -                      $errors = wfMergeErrorArrays( $errors,
 -                              $this->getUserPermissionsErrors( 'move', $wgUser ),
 -                              $this->getUserPermissionsErrors( 'edit', $wgUser ),
 -                              $nt->getUserPermissionsErrors( 'move-target', $wgUser ),
 -                              $nt->getUserPermissionsErrors( 'edit', $wgUser ) );
 -              }
 -
 -              $match = EditPage::matchSummarySpamRegex( $reason );
 -              if ( $match !== false ) {
 -                      // This is kind of lame, won't display nice
 -                      $errors[] = array( 'spamprotectiontext' );
 -              }
 -
 -              $err = null;
 -              if ( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) {
 -                      $errors[] = array( 'hookaborted', $err );
 +                      $errors = wfMergeErrorArrays(
 +                              $errors,
 +                              $mp->checkPermissions( $wgUser, $reason )->getErrorsArray()
 +                      );
                }
  
 -              # The move is allowed only if (1) the target doesn't exist, or
 -              # (2) the target is a redirect to the source, and has no history
 -              # (so we can undo bad moves right after they're done).
 -
 -              if ( 0 != $newid ) { # Target exists; check for validity
 -                      if ( !$this->isValidMoveTarget( $nt ) ) {
 -                              $errors[] = array( 'articleexists' );
 -                      }
 -              } else {
 -                      $tp = $nt->getTitleProtection();
 -                      $right = $tp['pt_create_perm'];
 -                      if ( $right == 'sysop' ) {
 -                              $right = 'editprotected'; // B/C
 -                      }
 -                      if ( $right == 'autoconfirmed' ) {
 -                              $right = 'editsemiprotected'; // B/C
 -                      }
 -                      if ( $tp and !$wgUser->isAllowed( $right ) ) {
 -                              $errors[] = array( 'cantmove-titleprotected' );
 -                      }
 -              }
 -              if ( empty( $errors ) ) {
 -                      return true;
 -              }
 -              return $errors;
 +              return $errors ?: true;
        }
  
        /**
         * Check if the requested move target is a valid file move target
 +       * @todo move this to MovePage
         * @param Title $nt Target title
         * @return array List of errors
         */
  
                $errors = array();
  
 -              // wfFindFile( $nt ) / wfLocalFile( $nt ) is not allowed until below
 -
 -              $file = wfLocalFile( $this );
 -              if ( $file->exists() ) {
 -                      if ( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) {
 -                              $errors[] = array( 'imageinvalidfilename' );
 -                      }
 -                      if ( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) {
 -                              $errors[] = array( 'imagetypemismatch' );
 -                      }
 -              }
 -
 -              if ( $nt->getNamespace() != NS_FILE ) {
 -                      $errors[] = array( 'imagenocrossnamespace' );
 -                      // From here we want to do checks on a file object, so if we can't
 -                      // create one, we must return.
 -                      return $errors;
 -              }
 -
 -              // wfFindFile( $nt ) / wfLocalFile( $nt ) is allowed below here
 -
                $destFile = wfLocalFile( $nt );
 -              if ( !$wgUser->isAllowed( 'reupload-shared' ) && !$destFile->exists() && wfFindFile( $nt ) ) {
 +              $destFile->load( File::READ_LATEST );
 +              if ( !$wgUser->isAllowed( 'reupload-shared' )
 +                      && !$destFile->exists() && wfFindFile( $nt )
 +              ) {
                        $errors[] = array( 'file-exists-sharedrepo' );
                }
  
        /**
         * Move a title to a new location
         *
 +       * @deprecated since 1.25, use the MovePage class instead
         * @param Title $nt The new title
         * @param bool $auth Indicates whether $wgUser's permissions
         *  should be checked
                        $createRedirect = true;
                }
  
 -              wfRunHooks( 'TitleMove', array( $this, $nt, $wgUser ) );
 -
 -              // If it is a file, move it first.
 -              // It is done before all other moving stuff is done because it's hard to revert.
 -              $dbw = wfGetDB( DB_MASTER );
 -              if ( $this->getNamespace() == NS_FILE ) {
 -                      $file = wfLocalFile( $this );
 -                      if ( $file->exists() ) {
 -                              $status = $file->move( $nt );
 -                              if ( !$status->isOk() ) {
 -                                      return $status->getErrorsArray();
 -                              }
 -                      }
 -                      // Clear RepoGroup process cache
 -                      RepoGroup::singleton()->clearCache( $this );
 -                      RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache
 -              }
 -
 -              $dbw->begin( __METHOD__ ); # If $file was a LocalFile, its transaction would have closed our own.
 -              $pageid = $this->getArticleID( self::GAID_FOR_UPDATE );
 -              $protected = $this->isProtected();
 -
 -              // Do the actual move
 -              $this->moveToInternal( $nt, $reason, $createRedirect );
 -
 -              // 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(
 -                      'categorylinks',
 -                      array( 'cl_sortkey_prefix', 'cl_to' ),
 -                      array( 'cl_from' => $pageid ),
 -                      __METHOD__
 -              );
 -              foreach ( $prefixes as $prefixRow ) {
 -                      $prefix = $prefixRow->cl_sortkey_prefix;
 -                      $catTo = $prefixRow->cl_to;
 -                      $dbw->update( 'categorylinks',
 -                              array(
 -                                      'cl_sortkey' => Collation::singleton()->getSortKey(
 -                                              $nt->getCategorySortkey( $prefix ) ),
 -                                      'cl_timestamp=cl_timestamp' ),
 -                              array(
 -                                      'cl_from' => $pageid,
 -                                      'cl_to' => $catTo ),
 -                              __METHOD__
 -                      );
 -              }
 -
 -              $redirid = $this->getArticleID();
 -
 -              if ( $protected ) {
 -                      # Protect the redirect title as the title used to be...
 -                      $dbw->insertSelect( 'page_restrictions', 'page_restrictions',
 -                              array(
 -                                      'pr_page' => $redirid,
 -                                      'pr_type' => 'pr_type',
 -                                      'pr_level' => 'pr_level',
 -                                      'pr_cascade' => 'pr_cascade',
 -                                      'pr_user' => 'pr_user',
 -                                      'pr_expiry' => 'pr_expiry'
 -                              ),
 -                              array( 'pr_page' => $pageid ),
 -                              __METHOD__,
 -                              array( 'IGNORE' )
 -                      );
 -                      # Update the protection log
 -                      $log = new LogPage( 'protect' );
 -                      $comment = wfMessage(
 -                              'prot_1movedto2',
 -                              $this->getPrefixedText(),
 -                              $nt->getPrefixedText()
 -                      )->inContentLanguage()->text();
 -                      if ( $reason ) {
 -                              $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
 -                      }
 -                      // @todo FIXME: $params?
 -                      $logId = $log->addEntry(
 -                              'move_prot',
 -                              $nt,
 -                              $comment,
 -                              array( $this->getPrefixedText() ),
 -                              $wgUser
 -                      );
 -
 -                      // reread inserted pr_ids for log relation
 -                      $insertedPrIds = $dbw->select(
 -                              'page_restrictions',
 -                              'pr_id',
 -                              array( 'pr_page' => $redirid ),
 -                              __METHOD__
 -                      );
 -                      $logRelationsValues = array();
 -                      foreach ( $insertedPrIds as $prid ) {
 -                              $logRelationsValues[] = $prid->pr_id;
 -                      }
 -                      $log->addRelations( 'pr_id', $logRelationsValues, $logId );
 -              }
 -
 -              # Update watchlists
 -              $oldnamespace = MWNamespace::getSubject( $this->getNamespace() );
 -              $newnamespace = MWNamespace::getSubject( $nt->getNamespace() );
 -              $oldtitle = $this->getDBkey();
 -              $newtitle = $nt->getDBkey();
 -
 -              if ( $oldnamespace != $newnamespace || $oldtitle != $newtitle ) {
 -                      WatchedItem::duplicateEntries( $this, $nt );
 -              }
 -
 -              $dbw->commit( __METHOD__ );
 -
 -              wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid, $reason ) );
 -              return true;
 -      }
 -
 -      /**
 -       * Move page to a title which is either a redirect to the
 -       * source page or nonexistent
 -       *
 -       * @param Title $nt The page to move to, which should be a redirect or nonexistent
 -       * @param string $reason The reason for the move
 -       * @param bool $createRedirect Whether to leave a redirect at the old title. Does not check
 -       *   if the user has the suppressredirect right
 -       * @throws MWException
 -       */
 -      private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) {
 -              global $wgUser, $wgContLang;
 -
 -              if ( $nt->exists() ) {
 -                      $moveOverRedirect = true;
 -                      $logType = 'move_redir';
 -              } else {
 -                      $moveOverRedirect = false;
 -                      $logType = 'move';
 -              }
 -
 -              if ( $createRedirect ) {
 -                      if ( $this->getNamespace() == NS_CATEGORY
 -                              && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
 -                      ) {
 -                              $redirectContent = new WikitextContent(
 -                                      wfMessage( 'category-move-redirect-override' )
 -                                              ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
 -                      } else {
 -                              $contentHandler = ContentHandler::getForTitle( $this );
 -                              $redirectContent = $contentHandler->makeRedirectContent( $nt,
 -                                      wfMessage( 'move-redirect-text' )->inContentLanguage()->plain() );
 -                      }
 -
 -                      // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
 +              $mp = new MovePage( $this, $nt );
 +              $status = $mp->move( $wgUser, $reason, $createRedirect );
 +              if ( $status->isOK() ) {
 +                      return true;
                } else {
 -                      $redirectContent = null;
 -              }
 -
 -              $logEntry = new ManualLogEntry( 'move', $logType );
 -              $logEntry->setPerformer( $wgUser );
 -              $logEntry->setTarget( $this );
 -              $logEntry->setComment( $reason );
 -              $logEntry->setParameters( array(
 -                      '4::target' => $nt->getPrefixedText(),
 -                      '5::noredir' => $redirectContent ? '0': '1',
 -              ) );
 -
 -              $formatter = LogFormatter::newFromEntry( $logEntry );
 -              $formatter->setContext( RequestContext::newExtraneousContext( $this ) );
 -              $comment = $formatter->getPlainActionText();
 -              if ( $reason ) {
 -                      $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
 -              }
 -              # Truncate for whole multibyte characters.
 -              $comment = $wgContLang->truncate( $comment, 255 );
 -
 -              $oldid = $this->getArticleID();
 -
 -              $dbw = wfGetDB( DB_MASTER );
 -
 -              $newpage = WikiPage::factory( $nt );
 -
 -              if ( $moveOverRedirect ) {
 -                      $newid = $nt->getArticleID();
 -                      $newcontent = $newpage->getContent();
 -
 -                      # 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__ );
 -
 -                      $newpage->doDeleteUpdates( $newid, $newcontent );
 -              }
 -
 -              # Save a null revision in the page's history notifying of the move
 -              $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true, $wgUser );
 -              if ( !is_object( $nullRevision ) ) {
 -                      throw new MWException( 'No valid null revision produced in ' . __METHOD__ );
 -              }
 -
 -              $nullRevision->insertOn( $dbw );
 -
 -              # Change the name of the target page:
 -              $dbw->update( 'page',
 -                      /* SET */ array(
 -                              'page_namespace' => $nt->getNamespace(),
 -                              'page_title' => $nt->getDBkey(),
 -                      ),
 -                      /* WHERE */ array( 'page_id' => $oldid ),
 -                      __METHOD__
 -              );
 -
 -              // clean up the old title before reset article id - bug 45348
 -              if ( !$redirectContent ) {
 -                      WikiPage::onArticleDelete( $this );
 -              }
 -
 -              $this->resetArticleID( 0 ); // 0 == non existing
 -              $nt->resetArticleID( $oldid );
 -              $newpage->loadPageData( WikiPage::READ_LOCKING ); // bug 46397
 -
 -              $newpage->updateRevisionOn( $dbw, $nullRevision );
 -
 -              wfRunHooks( 'NewRevisionFromEditComplete',
 -                      array( $newpage, $nullRevision, $nullRevision->getParentId(), $wgUser ) );
 -
 -              $newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) );
 -
 -              if ( !$moveOverRedirect ) {
 -                      WikiPage::onArticleCreate( $nt );
 -              }
 -
 -              # Recreate the redirect, this time in the other direction.
 -              if ( $redirectContent ) {
 -                      $redirectArticle = WikiPage::factory( $this );
 -                      $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // bug 46397
 -                      $newid = $redirectArticle->insertOn( $dbw );
 -                      if ( $newid ) { // sanity
 -                              $this->resetArticleID( $newid );
 -                              $redirectRevision = new Revision( array(
 -                                      'title' => $this, // for determining the default content model
 -                                      'page' => $newid,
 -                                      'user_text' => $wgUser->getName(),
 -                                      'user' => $wgUser->getId(),
 -                                      'comment' => $comment,
 -                                      'content' => $redirectContent ) );
 -                              $redirectRevision->insertOn( $dbw );
 -                              $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
 -
 -                              wfRunHooks( 'NewRevisionFromEditComplete',
 -                                      array( $redirectArticle, $redirectRevision, false, $wgUser ) );
 -
 -                              $redirectArticle->doEditUpdates( $redirectRevision, $wgUser, array( 'created' => true ) );
 -                      }
 +                      return $status->getErrorsArray();
                }
 -
 -              # Log the move
 -              $logid = $logEntry->insert();
 -              $logEntry->publish( $logid );
        }
  
        /**
         * Checks if $this can be moved to a given Title
         * - Selects for update, so don't call it unless you mean business
         *
 +       * @deprecated since 1.25, use MovePage's methods instead
         * @param Title $nt The new title to check
         * @return bool
         */
                # Is it an existing file?
                if ( $nt->getNamespace() == NS_FILE ) {
                        $file = wfLocalFile( $nt );
 +                      $file->load( File::READ_LATEST );
                        if ( $file->exists() ) {
                                wfDebug( __METHOD__ . ": file exists\n" );
                                return false;
                        return false;
                }
  
 -              $revCount = $this->estimateRevisionCount();
 -              return $revCount > $wgDeleteRevisionsLimit;
 +              if ( $this->mIsBigDeletion === null ) {
 +                      $dbr = wfGetDB( DB_SLAVE );
 +
 +                      $revCount = $dbr->selectRowCount(
 +                              'revision',
 +                              '1',
 +                              array( 'rev_page' => $this->getArticleID() ),
 +                              __METHOD__,
 +                              array( 'LIMIT' => $wgDeleteRevisionsLimit + 1 )
 +                      );
 +
 +                      $this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit;
 +              }
 +
 +              return $this->mIsBigDeletion;
        }
  
        /**
 -       * Get the  approximate revision count of this page.
 +       * Get the approximate revision count of this page.
         *
         * @return int
         */
                        'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
                );
                if ( $max !== null ) {
 -                      $res = $dbr->select( 'revision', '1',
 +                      return $dbr->selectRowCount( 'revision', '1',
                                $conds,
                                __METHOD__,
                                array( 'LIMIT' => $max + 1 ) // extra to detect truncation
                        );
 -                      return $res->numRows();
                } else {
                        return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
                }
                }
                // No DB query needed if $old and $new are the same or successive revisions:
                if ( $old->getId() === $new->getId() ) {
 -                      return ( $old_cmp === '>' && $new_cmp === '<' ) ? array() : array( $old->getRawUserText() );
 +                      return ( $old_cmp === '>' && $new_cmp === '<' ) ?
 +                              array() :
 +                              array( $old->getUserText( Revision::RAW ) );
                } elseif ( $old->getId() === $new->getParentId() ) {
                        if ( $old_cmp === '>=' && $new_cmp === '<=' ) {
 -                              $authors[] = $old->getRawUserText();
 -                              if ( $old->getRawUserText() != $new->getRawUserText() ) {
 -                                      $authors[] = $new->getRawUserText();
 +                              $authors[] = $old->getUserText( Revision::RAW );
 +                              if ( $old->getUserText( Revision::RAW ) != $new->getUserText( Revision::RAW ) ) {
 +                                      $authors[] = $new->getUserText( Revision::RAW );
                                }
                        } elseif ( $old_cmp === '>=' ) {
 -                              $authors[] = $old->getRawUserText();
 +                              $authors[] = $old->getUserText( Revision::RAW );
                        } elseif ( $new_cmp === '<=' ) {
 -                              $authors[] = $new->getRawUserText();
 +                              $authors[] = $new->getUserText( Revision::RAW );
                        }
                        return $authors;
                }
         * If you want to know if a title can be meaningfully viewed, you should
         * probably call the isKnown() method instead.
         *
 +       * @param int $flags An optional bit field; may be Title::GAID_FOR_UPDATE to check
 +       *   from master/for update
         * @return bool
         */
 -      public function exists() {
 -              return $this->getArticleID() != 0;
 +      public function exists( $flags = 0 ) {
 +              $exists = $this->getArticleID( $flags ) != 0;
 +              Hooks::run( 'TitleExists', array( $this, &$exists ) );
 +              return $exists;
        }
  
        /**
                 * @param Title $title
                 * @param bool|null $isKnown
                 */
 -              wfRunHooks( 'TitleIsAlwaysKnown', array( $this, &$isKnown ) );
 +              Hooks::run( 'TitleIsAlwaysKnown', array( $this, &$isKnown ) );
  
                if ( !is_null( $isKnown ) ) {
                        return $isKnown;
        /**
         * Updates page_touched for this page; called from LinksUpdate.php
         *
 +       * @param integer $purgeTime TS_MW timestamp [optional]
         * @return bool True if the update succeeded
         */
 -      public function invalidateCache() {
 +      public function invalidateCache( $purgeTime = null ) {
                if ( wfReadOnly() ) {
                        return false;
                }
                $method = __METHOD__;
                $dbw = wfGetDB( DB_MASTER );
                $conds = $this->pageCond();
 -              $dbw->onTransactionIdle( function() use ( $dbw, $conds, $method ) {
 +              $dbw->onTransactionIdle( function () use ( $dbw, $conds, $method, $purgeTime ) {
 +                      $dbTimestamp = $dbw->timestamp( $purgeTime ?: time() );
 +
                        $dbw->update(
                                'page',
 -                              array( 'page_touched' => $dbw->timestamp() ),
 -                              $conds,
 +                              array( 'page_touched' => $dbTimestamp ),
 +                              $conds + array( 'page_touched < ' . $dbw->addQuotes( $dbTimestamp ) ),
                                $method
                        );
                } );
         * @return string|null
         */
        public function getNotificationTimestamp( $user = null ) {
 -              global $wgUser, $wgShowUpdatedMarker;
 +              global $wgUser;
 +
                // Assume current user if none given
                if ( !$user ) {
                        $user = $wgUser;
                }
                // Check cache first
                $uid = $user->getId();
 +              if ( !$uid ) {
 +                      return false;
 +              }
                // avoid isset here, as it'll return false for null entries
                if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
                        return $this->mNotificationTimestamp[$uid];
                }
 -              if ( !$uid || !$wgShowUpdatedMarker || !$user->isAllowed( 'viewmywatchlist' ) ) {
 -                      $this->mNotificationTimestamp[$uid] = false;
 -                      return $this->mNotificationTimestamp[$uid];
 -              }
                // Don't cache too much!
                if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
                        $this->mNotificationTimestamp = array();
                }
 -              $dbr = wfGetDB( DB_SLAVE );
 -              $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist',
 -                      'wl_notificationtimestamp',
 -                      array(
 -                              'wl_user' => $user->getId(),
 -                              'wl_namespace' => $this->getNamespace(),
 -                              'wl_title' => $this->getDBkey(),
 -                      ),
 -                      __METHOD__
 -              );
 +
 +              $watchedItem = WatchedItem::fromUserTitle( $user, $this );
 +              $this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp();
 +
                return $this->mNotificationTimestamp[$uid];
        }
  
        public function isValidRedirectTarget() {
                global $wgInvalidRedirectTargets;
  
-               // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
-               if ( $this->isSpecial( 'Userlogout' ) ) {
-                       return false;
-               }
-               foreach ( $wgInvalidRedirectTargets as $target ) {
-                       if ( $this->isSpecial( $target ) ) {
+               if ( $this->isSpecialPage() ) {
+                       // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
+                       if ( $this->isSpecial( 'Userlogout' ) ) {
                                return false;
                        }
+                       foreach ( $wgInvalidRedirectTargets as $target ) {
+                               if ( $this->isSpecial( $target ) ) {
+                                       return false;
+                               }
+                       }
                }
  
                return true;
                // on the Title object passed in, and should probably
                // tell the users to run updateCollations.php --force
                // in order to re-sort existing category relations.
 -              wfRunHooks( 'GetDefaultSortkey', array( $this, &$unprefixed ) );
 +              Hooks::run( 'GetDefaultSortkey', array( $this, &$unprefixed ) );
                if ( $prefix !== '' ) {
                        # Separate with a line feed, so the unprefixed part is only used as
                        # a tiebreaker when two pages have the exact same prefix.
         */
        public function getPageLanguage() {
                global $wgLang, $wgLanguageCode;
 -              wfProfileIn( __METHOD__ );
                if ( $this->isSpecialPage() ) {
                        // special pages are in the user language
 -                      wfProfileOut( __METHOD__ );
                        return $wgLang;
                }
  
                // Checking if DB language is set
                if ( $this->mDbPageLanguage ) {
 -                      wfProfileOut( __METHOD__ );
                        return wfGetLangObj( $this->mDbPageLanguage );
                }
  
                        $langObj = wfGetLangObj( $this->mPageLanguage[0] );
                }
  
 -              wfProfileOut( __METHOD__ );
                return $langObj;
        }
  
        public function getEditNotices( $oldid = 0 ) {
                $notices = array();
  
 -              # Optional notices on a per-namespace and per-page basis
 +              // Optional notice for the entire namespace
                $editnotice_ns = 'editnotice-' . $this->getNamespace();
 -              $editnotice_ns_message = wfMessage( $editnotice_ns );
 -              if ( $editnotice_ns_message->exists() ) {
 -                      $notices[$editnotice_ns] = $editnotice_ns_message->parseAsBlock();
 +              $msg = wfMessage( $editnotice_ns );
 +              if ( $msg->exists() ) {
 +                      $html = $msg->parseAsBlock();
 +                      // Edit notices may have complex logic, but output nothing (T91715)
 +                      if ( trim( $html ) !== '' ) {
 +                              $notices[$editnotice_ns] = Html::rawElement(
 +                                      'div',
 +                                      array( 'class' => array(
 +                                              'mw-editnotice',
 +                                              'mw-editnotice-namespace',
 +                                              Sanitizer::escapeClass( "mw-$editnotice_ns" )
 +                                      ) ),
 +                                      $html
 +                              );
 +                      }
                }
 +
                if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) {
 +                      // Optional notice for page itself and any parent page
                        $parts = explode( '/', $this->getDBkey() );
                        $editnotice_base = $editnotice_ns;
                        while ( count( $parts ) > 0 ) {
                                $editnotice_base .= '-' . array_shift( $parts );
 -                              $editnotice_base_msg = wfMessage( $editnotice_base );
 -                              if ( $editnotice_base_msg->exists() ) {
 -                                      $notices[$editnotice_base] = $editnotice_base_msg->parseAsBlock();
 +                              $msg = wfMessage( $editnotice_base );
 +                              if ( $msg->exists() ) {
 +                                      $html = $msg->parseAsBlock();
 +                                      if ( trim( $html ) !== '' ) {
 +                                              $notices[$editnotice_base] = Html::rawElement(
 +                                                      'div',
 +                                                      array( 'class' => array(
 +                                                              'mw-editnotice',
 +                                                              'mw-editnotice-base',
 +                                                              Sanitizer::escapeClass( "mw-$editnotice_base" )
 +                                                      ) ),
 +                                                      $html
 +                                              );
 +                                      }
                                }
                        }
                } else {
 -                      # Even if there are no subpages in namespace, we still don't want / in MW ns.
 -                      $editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->getDBkey() );
 -                      $editnoticeMsg = wfMessage( $editnoticeText );
 -                      if ( $editnoticeMsg->exists() ) {
 -                              $notices[$editnoticeText] = $editnoticeMsg->parseAsBlock();
 +                      // Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys
 +                      $editnoticeText = $editnotice_ns . '-' . strtr( $this->getDBkey(), '/', '-' );
 +                      $msg = wfMessage( $editnoticeText );
 +                      if ( $msg->exists() ) {
 +                              $html = $msg->parseAsBlock();
 +                              if ( trim( $html ) !== '' ) {
 +                                      $notices[$editnoticeText] = Html::rawElement(
 +                                              'div',
 +                                              array( 'class' => array(
 +                                                      'mw-editnotice',
 +                                                      'mw-editnotice-page',
 +                                                      Sanitizer::escapeClass( "mw-$editnoticeText" )
 +                                              ) ),
 +                                              $html
 +                                      );
 +                              }
                        }
                }
  
 -              wfRunHooks( 'TitleGetEditNotices', array( $this, $oldid, &$notices ) );
 +              Hooks::run( 'TitleGetEditNotices', array( $this, $oldid, &$notices ) );
                return $notices;
        }
 +
 +      /**
 +       * @return array
 +       */
 +      public function __sleep() {
 +              return array(
 +                      'mNamespace',
 +                      'mDbkeyform',
 +                      'mFragment',
 +                      'mInterwiki',
 +                      'mLocalInterwiki',
 +                      'mUserCaseDBKey',
 +                      'mDefaultNamespace',
 +              );
 +      }
 +
 +      public function __wakeup() {
 +              $this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0;
 +              $this->mUrlform = wfUrlencode( $this->mDbkeyform );
 +              $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
 +      }
 +
  }