* (bug 674) Allow users to be blocked from editing a specific article
[lhc/web/wiklou.git] / includes / Title.php
index 89e20b0..0bef7bc 100644 (file)
 <?php
 /**
  * See title.txt
- *
+ * @file
  */
 
-/** */
 if ( !class_exists( 'UtfNormal' ) ) {
        require_once( dirname(__FILE__) . '/normal/UtfNormal.php' );
 }
 
 define ( 'GAID_FOR_UPDATE', 1 );
 
-# Title::newFromTitle maintains a cache to avoid
-# expensive re-normalization of commonly used titles.
-# On a batch operation this can become a memory leak
-# if not bounded. After hitting this many titles,
-# reset the cache.
-define( 'MW_TITLECACHE_MAX', 1000 );
 
-# Constants for pr_cascade bitfield
+/**
+ * Constants for pr_cascade bitfield
+ */
 define( 'CASCADE', 1 );
 
 /**
- * Title class
- * - Represents a title, which may contain an interwiki designation or namespace
- * - Can fetch various kinds of data from the database, albeit inefficiently.
- *
+ * Represents a title within MediaWiki.
+ * Optionally may contain an interwiki designation or namespace.
+ * @note This class can fetch various kinds of data from the database;
+ *       however, it does so inefficiently.
  */
 class Title {
-       /**
-        * Static cache variables
-        */
+       /** @name Static cache variables */
+       //@{
        static private $titleCache=array();
        static private $interwikiCache=array();
-
+       //@}
 
        /**
-        * All member variables should be considered private
-        * Please use the accessor functions
+        * Title::newFromText maintains a cache to avoid expensive re-normalization of
+        * commonly used titles. On a batch operation this can become a memory leak
+        * if not bounded. After hitting this many titles reset the cache.
         */
+       const CACHE_MAX = 1000;
 
-       /**#@+
+
+       /**
+        * @name Private member variables
+        * Please use the accessor functions instead.
         * @private
         */
-
-       var $mTextform;                 # Text form (spaces not underscores) of the main part
-       var $mUrlform;                  # URL-encoded form of the main part
-       var $mDbkeyform;                # Main part with underscores
-       var $mUserCaseDBKey;        # DB key with the initial letter in the case specified by the user
-       var $mNamespace;                # Namespace index, i.e. one of the NS_xxxx constants
-       var $mInterwiki;                # Interwiki prefix (or null string)
-       var $mFragment;                 # Title fragment (i.e. the bit after the #)
-       var $mArticleID;                # Article ID, fetched from the link cache on demand
-       var $mLatestID;                 # ID of most recent revision
-       var $mRestrictions;             # Array of groups allowed to edit this article
-       var $mCascadeRestriction;       # Cascade restrictions on this page to included templates and images?
-       var $mRestrictionsExpiry;       # When do the restrictions on this page expire?
-       var $mHasCascadingRestrictions; # Are cascading restrictions in effect on this page?
-       var $mCascadeRestrictionSources;# Where are the cascading restrictions coming from on this page?
-       var $mRestrictionsLoaded;       # Boolean for initialisation on demand
-       var $mPrefixedText;             # Text form including namespace/interwiki, initialised on demand
-       var $mDefaultNamespace;         # Namespace index when there is no namespace
-                                       # Zero except in {{transclusion}} tags
-       var $mWatched;                  # Is $wgUser watching this page? NULL if unfilled, accessed through userIsWatching()
-       var $mLength;              # The page length, 0 for special pages
-       var $mRedirect;            # Is the article at this title a redirect?
-       /**#@-*/
+       //@{
+
+       var $mTextform = '';              ///< Text form (spaces not underscores) of the main part
+       var $mUrlform = '';               ///< URL-encoded form of the main part
+       var $mDbkeyform = '';             ///< Main part with underscores
+       var $mUserCaseDBKey;              ///< DB key with the initial letter in the case specified by the user
+       var $mNamespace = NS_MAIN;        ///< Namespace index, i.e. one of the NS_xxxx constants
+       var $mInterwiki = '';             ///< Interwiki prefix (or null string)
+       var $mFragment;                   ///< Title fragment (i.e. the bit after the #)
+       var $mArticleID = -1;             ///< Article ID, fetched from the link cache on demand
+       var $mLatestID = false;           ///< ID of most recent revision
+       var $mRestrictions = array();     ///< Array of groups allowed to edit this article
+       var $mOldRestrictions = false;
+       var $mCascadeRestriction;         ///< Cascade restrictions on this page to included templates and images?
+       var $mRestrictionsExpiry = array();       ///< When do the restrictions on this page expire?
+       var $mHasCascadingRestrictions;   ///< Are cascading restrictions in effect on this page?
+       var $mCascadeSources;  ///< Where are the cascading restrictions coming from on this page?
+       var $mRestrictionsLoaded = false; ///< Boolean for initialisation on demand
+       var $mPrefixedText;               ///< Text form including namespace/interwiki, initialised on demand
+       # Don't change the following default, NS_MAIN is hardcoded in several
+       # places.  See bug 696.
+       var $mDefaultNamespace = NS_MAIN; ///< Namespace index when there is no namespace
+                                         # Zero except in {{transclusion}} tags
+       var $mWatched = null;             ///< Is $wgUser watching this page? null if unfilled, accessed through userIsWatching()
+       var $mLength = -1;                ///< The page length, 0 for special pages
+       var $mRedirect = null;            ///< Is the article at this title a redirect?
+       //@}
 
 
        /**
         * Constructor
         * @private
         */
-       /* private */ function __construct() {
-               $this->mInterwiki = $this->mUrlform =
-               $this->mTextform = $this->mDbkeyform = '';
-               $this->mArticleID = -1;
-               $this->mNamespace = NS_MAIN;
-               $this->mRestrictionsLoaded = false;
-               $this->mRestrictions = array();
-               # Dont change the following, NS_MAIN is hardcoded in several place
-               # See bug #696
-               $this->mDefaultNamespace = NS_MAIN;
-               $this->mWatched = NULL;
-               $this->mLatestID = false;
-               $this->mOldRestrictions = false;
-               $this->mLength = -1;
-               $this->mRedirect = NULL;
-       }
+       /* private */ function __construct() {}
 
        /**
         * Create a new Title from a prefixed DB key
-        * @param string $key The database key, which has underscores
+        * @param $key \type{\string} The database key, which has underscores
         *      instead of spaces, possibly including namespace and
         *      interwiki prefixes
-        * @return Title the new object, or NULL on an error
+        * @return \type{Title} the new object, or NULL on an error
         */
        public static function newFromDBkey( $key ) {
                $t = new Title();
@@ -109,12 +97,12 @@ class Title {
         * Create a new Title from text, such as what one would
         * find in a link. Decodes any HTML entities in the text.
         *
-        * @param string $text the link text; spaces, prefixes,
+        * @param $text \type{\string} the link text; spaces, prefixes,
         *      and an initial ':' indicating the main namespace
         *      are accepted
-        * @param int $defaultNamespace the namespace to use if
+        * @param $defaultNamespace \type{\int} the namespace to use if
         *      none is specified by a prefix
-        * @return Title the new object, or NULL on an error
+        * @return \type{Title} the new object, or NULL on an error
         */
        public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
                if( is_object( $text ) ) {
@@ -145,7 +133,7 @@ class Title {
                static $cachedcount = 0 ;
                if( $t->secureAndSplit() ) {
                        if( $defaultNamespace == NS_MAIN ) {
-                               if( $cachedcount >= MW_TITLECACHE_MAX ) {
+                               if( $cachedcount >= self::CACHE_MAX ) {
                                        # Avoid memory leaks on mass operations...
                                        Title::$titleCache = array();
                                        $cachedcount=0;
@@ -163,8 +151,8 @@ class Title {
        /**
         * Create a new Title from URL-encoded text. Ensures that
         * the given title's length does not exceed the maximum.
-        * @param string $url the title, as might be taken from a URL
-        * @return Title the new object, or NULL on an error
+        * @param $url \type{\string} the title, as might be taken from a URL
+        * @return \type{Title} the new object, or NULL on an error
         */
        public static function newFromURL( $url ) {
                global $wgLegalTitleChars;
@@ -191,9 +179,9 @@ class Title {
         * @todo This is inefficiently implemented, the page row is requested
         *       but not used for anything else
         *
-        * @param int $id the page_id corresponding to the Title to create
-        * @param int $flags, use GAID_FOR_UPDATE to use master
-        * @return Title the new object, or NULL on an error
+        * @param $id \type{\int} the page_id corresponding to the Title to create
+        * @param $flags \type{\int} use GAID_FOR_UPDATE to use master
+        * @return \type{Title} the new object, or NULL on an error
         */
        public static function newFromID( $id, $flags = 0 ) {
                $fname = 'Title::newFromID';
@@ -210,6 +198,8 @@ class Title {
 
        /**
         * Make an array of titles from an array of IDs
+        * @param $ids \type{\arrayof{\int}} Array of IDs
+        * @return \type{\arrayof{Title}} Array of Titles
         */
        public static function newFromIDs( $ids ) {
                if ( !count( $ids ) ) {
@@ -220,7 +210,7 @@ class Title {
                        'page_id IN (' . $dbr->makeList( $ids ) . ')', __METHOD__ );
 
                $titles = array();
-               while ( $row = $dbr->fetchObject( $res ) ) {
+               foreach( $res as $row ) {
                        $titles[] = Title::makeTitle( $row->page_namespace, $row->page_title );
                }
                return $titles;
@@ -228,7 +218,8 @@ class Title {
 
        /**
         * Make a Title object from a DB row
-        * @param Row $row (needs at least page_title,page_namespace)
+        * @param $row \type{Row} (needs at least page_title,page_namespace)
+        * @return \type{Title} corresponding Title
         */
        public static function newFromRow( $row ) {
                $t = self::makeTitle( $row->page_namespace, $row->page_title );
@@ -248,14 +239,15 @@ class Title {
         * For convenience, spaces are converted to underscores so that
         * eg user_text fields can be used directly.
         *
-        * @param int $ns the namespace of the article
-        * @param string $title the unprefixed database key form
-        * @return Title the new object
+        * @param $ns \type{\int} the namespace of the article
+        * @param $title \type{\string} the unprefixed database key form
+        * @param $fragment \type{\string} The link fragment (after the "#")
+        * @return \type{Title} the new object
         */
-       public static function &makeTitle( $ns, $title ) {
+       public static function &makeTitle( $ns, $title, $fragment = '' ) {
                $t = new Title();
                $t->mInterwiki = '';
-               $t->mFragment = '';
+               $t->mFragment = $fragment;
                $t->mNamespace = $ns = intval( $ns );
                $t->mDbkeyform = str_replace( ' ', '_', $title );
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
@@ -269,13 +261,14 @@ class Title {
         * The parameters will be checked for validity, which is a bit slower
         * than makeTitle() but safer for user-provided data.
         *
-        * @param int $ns the namespace of the article
-        * @param string $title the database key form
-        * @return Title the new object, or NULL on an error
+        * @param $ns \type{\int} the namespace of the article
+        * @param $title \type{\string} the database key form
+        * @param $fragment \type{\string} The link fragment (after the "#")
+        * @return \type{Title} the new object, or NULL on an error
         */
-       public static function makeTitleSafe( $ns, $title ) {
+       public static function makeTitleSafe( $ns, $title, $fragment = '' ) {
                $t = new Title();
-               $t->mDbkeyform = Title::makeName( $ns, $title );
+               $t->mDbkeyform = Title::makeName( $ns, $title, $fragment );
                if( $t->secureAndSplit() ) {
                        return $t;
                } else {
@@ -285,7 +278,7 @@ class Title {
 
        /**
         * Create a new Title for the Main Page
-        * @return Title the new object
+        * @return \type{Title} the new object
         */
        public static function newMainPage() {
                $title = Title::newFromText( wfMsgForContent( 'mainpage' ) );
@@ -300,15 +293,18 @@ class Title {
         * Extract a redirect destination from a string and return the
         * Title, or null if the text doesn't contain a valid redirect
         *
-        * @param string $text Text with possible redirect
-        * @return Title
+        * @param $text \type{String} Text with possible redirect
+        * @return \type{Title} The corresponding Title
         */
        public static function newFromRedirect( $text ) {
                $redir = MagicWord::get( 'redirect' );
-               if( $redir->matchStart( trim($text) ) ) {
+               $text = trim($text);
+               if( $redir->matchStartAndRemove( $text ) ) {
                        // Extract the first link and see if it's usable
+                       // Ensure that it really does come directly after #REDIRECT
+                       // Some older redirects included a colon, so don't freak about that!
                        $m = array();
-                       if( preg_match( '!\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
+                       if( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
                                // Strip preceding colon used to "escape" categories, etc.
                                // and URL-decode links
                                if( strpos( $m[1], '%' ) !== false ) {
@@ -332,26 +328,23 @@ class Title {
 
        /**
         * Get the prefixed DB key associated with an ID
-        * @param int $id the page_id of the article
-        * @return Title an object representing the article, or NULL
+        * @param $id \type{\int} the page_id of the article
+        * @return \type{Title} an object representing the article, or NULL
         *      if no such article was found
-        * @static
-        * @access public
         */
-       function nameOf( $id ) {
-               $fname = 'Title::nameOf';
+       public static function nameOf( $id ) {
                $dbr = wfGetDB( DB_SLAVE );
 
-               $s = $dbr->selectRow( 'page', array( 'page_namespace','page_title' ),  array( 'page_id' => $id ), $fname );
+               $s = $dbr->selectRow( 'page', array( 'page_namespace','page_title' ),  array( 'page_id' => $id ), __METHOD__ );
                if ( $s === false ) { return NULL; }
 
-               $n = Title::makeName( $s->page_namespace, $s->page_title );
+               $n = self::makeName( $s->page_namespace, $s->page_title );
                return $n;
        }
 
        /**
         * Get a regex character class describing the legal characters in a link
-        * @return string the list of characters, not delimited
+        * @return \type{\string} the list of characters, not delimited
         */
        public static function legalChars() {
                global $wgLegalTitleChars;
@@ -362,9 +355,9 @@ class Title {
         * Get a string representation of a title suitable for
         * including in a search index
         *
-        * @param int $ns a namespace index
-        * @param string $title text-form main part
-        * @return string a stripped-down title string ready for the
+        * @param $ns \type{\int} a namespace index
+        * @param $title \type{\string} text-form main part
+        * @return \type{\string} a stripped-down title string ready for the
         *      search index
         */
        public static function indexTitle( $ns, $title ) {
@@ -389,22 +382,27 @@ class Title {
 
        /*
         * Make a prefixed DB key from a DB key and a namespace index
-        * @param int $ns numerical representation of the namespace
-        * @param string $title the DB key form the title
-        * @return string the prefixed form of the title
+        * @param $ns \type{\int} numerical representation of the namespace
+        * @param $title \type{\string} the DB key form the title
+        * @param $fragment \type{\string} The link fragment (after the "#")
+        * @return \type{\string} the prefixed form of the title
         */
-       public static function makeName( $ns, $title ) {
+       public static function makeName( $ns, $title, $fragment = '' ) {
                global $wgContLang;
 
-               $n = $wgContLang->getNsText( $ns );
-               return $n == '' ? $title : "$n:$title";
+               $namespace = $wgContLang->getNsText( $ns );
+               $name = $namespace == '' ? $title : "$namespace:$title";
+               if ( strval( $fragment ) != '' ) {
+                       $name .= '#' . $fragment;
+               }
+               return $name;
        }
 
        /**
         * Returns the URL associated with an interwiki prefix
-        * @param string $key the interwiki prefix (e.g. "MeatBall")
-        * @return the associated URL, containing "$1", which should be
-        *      replaced by an article title
+        * @param $key \type{\string} the interwiki prefix (e.g. "MeatBall")
+        * @return \type{\string} the associated URL, containing "$1", 
+        *      which should be replaced by an article title
         * @static (arguably)
         */
        public function getInterwikiLink( $key )  {
@@ -412,6 +410,12 @@ class Title {
                global $wgInterwikiCache, $wgContLang;
                $fname = 'Title::getInterwikiLink';
 
+               if ( count( Title::$interwikiCache ) >= self::CACHE_MAX ) {
+                       // Don't use infinite memory
+                       reset( Title::$interwikiCache );
+                       unset( Title::$interwikiCache[ key( Title::$interwikiCache ) ] );
+               }
+
                $key = $wgContLang->lc( $key );
 
                $k = wfMemcKey( 'interwiki', $key );
@@ -453,11 +457,12 @@ class Title {
        }
 
        /**
-        * Fetch interwiki prefix data from local cache in constant database
+        * Fetch interwiki prefix data from local cache in constant database.
         *
-        * More logic is explained in DefaultSettings
+        * @note More logic is explained in DefaultSettings.
         *
-        * @return string URL of interwiki site
+        * @param $key \type{\string} Database key
+        * @return \type{\string} URL of interwiki site
         */
        public static function getInterwikiCached( $key ) {
                global $wgInterwikiCache, $wgInterwikiScopes, $wgInterwikiFallbackSite;
@@ -494,11 +499,12 @@ class Title {
                Title::$interwikiCache[wfMemcKey( 'interwiki', $key )] = $s;
                return $s->iw_url;
        }
+
        /**
         * Determine whether the object refers to a page within
         * this project.
         *
-        * @return bool TRUE if this is an in-project interwiki link
+        * @return \type{\bool} TRUE if this is an in-project interwiki link
         *      or a wikilink, FALSE otherwise
         */
        public function isLocal() {
@@ -516,7 +522,7 @@ class Title {
         * Determine whether the object refers to a page within
         * this project and is transcludable.
         *
-        * @return bool TRUE if this is transcludable
+        * @return \type{\bool} TRUE if this is transcludable
         */
        public function isTrans() {
                if ($this->mInterwiki == '')
@@ -547,27 +553,27 @@ class Title {
        /** Simple accessors */
        /**
         * Get the text form (spaces not underscores) of the main part
-        * @return string
+        * @return \type{\string} Main part of the title
         */
        public function getText() { return $this->mTextform; }
        /**
         * Get the URL-encoded form of the main part
-        * @return string
+        * @return \type{\string} Main part of the title, URL-encoded
         */
        public function getPartialURL() { return $this->mUrlform; }
        /**
         * Get the main part with underscores
-        * @return string
+        * @return \type{\string} Main part of the title, with underscores
         */
        public function getDBkey() { return $this->mDbkeyform; }
        /**
-        * Get the namespace index, i.e. one of the NS_xxxx constants
-        * @return int
+        * Get the namespace index, i.e.\ one of the NS_xxxx constants.
+        * @return \type{\int} Namespace index
         */
        public function getNamespace() { return $this->mNamespace; }
        /**
         * Get the namespace text
-        * @return string
+        * @return \type{\string} Namespace text
         */
        public function getNsText() {
                global $wgContLang, $wgCanonicalNamespaceNames;
@@ -587,49 +593,47 @@ class Title {
        }
        /**
         * Get the DB key with the initial letter case as specified by the user
+        * @return \type{\string} DB key
         */
        function getUserCaseDBKey() {
                return $this->mUserCaseDBKey;
        }
        /**
         * Get the namespace text of the subject (rather than talk) page
-        * @return string
+        * @return \type{\string} Namespace text
         */
        public function getSubjectNsText() {
                global $wgContLang;
                return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) );
        }
-
        /**
         * Get the namespace text of the talk page
-        * @return string
+        * @return \type{\string} Namespace text
         */
        public function getTalkNsText() {
                global $wgContLang;
                return( $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ) );
        }
-
        /**
         * Could this title have a corresponding talk page?
-        * @return bool
+        * @return \type{\bool} TRUE or FALSE
         */
        public function canTalk() {
                return( MWNamespace::canTalk( $this->mNamespace ) );
        }
-
        /**
         * Get the interwiki prefix (or null string)
-        * @return string
+        * @return \type{\string} Interwiki prefix
         */
        public function getInterwiki() { return $this->mInterwiki; }
        /**
-        * Get the Title fragment (i.e. the bit after the #) in text form
-        * @return string
+        * Get the Title fragment (i.e.\ the bit after the #) in text form
+        * @return \type{\string} Title fragment
         */
        public function getFragment() { return $this->mFragment; }
        /**
         * Get the fragment in URL form, including the "#" character if there is one
-        * @return string
+        * @return \type{\string} Fragment in URL form
         */
        public function getFragmentForURL() {
                if ( $this->mFragment == '' ) {
@@ -640,13 +644,13 @@ class Title {
        }
        /**
         * Get the default namespace index, for when there is no namespace
-        * @return int
+        * @return \type{\int} Default namespace index
         */
        public function getDefaultNamespace() { return $this->mDefaultNamespace; }
 
        /**
         * Get title for search index
-        * @return string a stripped-down title string ready for the
+        * @return \type{\string} a stripped-down title string ready for the
         *      search index
         */
        public function getIndexTitle() {
@@ -655,7 +659,7 @@ class Title {
 
        /**
         * Get the prefixed database key form
-        * @return string the prefixed title, with underscores and
+        * @return \type{\string} the prefixed title, with underscores and
         *      any interwiki and namespace prefixes
         */
        public function getPrefixedDBkey() {
@@ -667,7 +671,7 @@ class Title {
        /**
         * Get the prefixed title with spaces.
         * This is the form usually used for display
-        * @return string the prefixed title, with spaces
+        * @return \type{\string} the prefixed title, with spaces
         */
        public function getPrefixedText() {
                if ( empty( $this->mPrefixedText ) ) { // FIXME: bad usage of empty() ?
@@ -681,7 +685,7 @@ class Title {
        /**
         * Get the prefixed title with spaces, plus any fragment
         * (part beginning with '#')
-        * @return string the prefixed title, with spaces and
+        * @return \type{\string} the prefixed title, with spaces and
         *      the fragment, including '#'
         */
        public function getFullText() {
@@ -694,38 +698,35 @@ class Title {
 
        /**
         * Get the base name, i.e. the leftmost parts before the /
-        * @return string Base name
+        * @return \type{\string} Base name
         */
        public function getBaseText() {
-               global $wgNamespacesWithSubpages;
-               if( !empty( $wgNamespacesWithSubpages[$this->mNamespace] ) ) {
-                       $parts = explode( '/', $this->getText() );
-                       # Don't discard the real title if there's no subpage involved
-                       if( count( $parts ) > 1 )
-                               unset( $parts[ count( $parts ) - 1 ] );
-                       return implode( '/', $parts );
-               } else {
+               if( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
                        return $this->getText();
                }
+
+               $parts = explode( '/', $this->getText() );
+               # Don't discard the real title if there's no subpage involved
+               if( count( $parts ) > 1 )
+                       unset( $parts[ count( $parts ) - 1 ] );
+               return implode( '/', $parts );
        }
 
        /**
         * Get the lowest-level subpage name, i.e. the rightmost part after /
-        * @return string Subpage name
+        * @return \type{\string} Subpage name
         */
        public function getSubpageText() {
-               global $wgNamespacesWithSubpages;
-               if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) && $wgNamespacesWithSubpages[ $this->mNamespace ] ) {
-                       $parts = explode( '/', $this->mTextform );
-                       return( $parts[ count( $parts ) - 1 ] );
-               } else {
+               if( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
                        return( $this->mTextform );
                }
+               $parts = explode( '/', $this->mTextform );
+               return( $parts[ count( $parts ) - 1 ] );
        }
 
        /**
         * Get a URL-encoded form of the subpage text
-        * @return string URL-encoded subpage name
+        * @return \type{\string} URL-encoded subpage name
         */
        public function getSubpageUrlForm() {
                $text = $this->getSubpageText();
@@ -736,7 +737,7 @@ class Title {
 
        /**
         * Get a URL-encoded title (not an actual URL) including interwiki
-        * @return string the URL-encoded form
+        * @return \type{\string} the URL-encoded form
         */
        public function getPrefixedURL() {
                $s = $this->prefix( $this->mDbkeyform );
@@ -755,14 +756,19 @@ class Title {
         * Get a real URL referring to this title, with interwiki link and
         * fragment
         *
-        * @param string $query an optional query string, not used
-        *      for interwiki links
-        * @param string $variant language variant of url (for sr, zh..)
-        * @return string the URL
+        * @param $query \twotypes{\string,\array} an optional query string, not used for interwiki
+        *   links. Can be specified as an associative array as well, e.g.,
+        *   array( 'action' => 'edit' ) (keys and values will be URL-escaped).
+        * @param $variant \type{\string} language variant of url (for sr, zh..)
+        * @return \type{\string} the URL
         */
        public function getFullURL( $query = '', $variant = false ) {
                global $wgContLang, $wgServer, $wgRequest;
 
+               if( is_array( $query ) ) {
+                       $query = wfArrayToCGI( $query );
+               }
+
                if ( '' == $this->mInterwiki ) {
                        $url = $this->getLocalUrl( $query, $variant );
 
@@ -794,14 +800,20 @@ class Title {
        /**
         * Get a URL with no fragment or server name.  If this page is generated
         * with action=render, $wgServer is prepended.
-        * @param string $query an optional query string; if not specified,
-        *      $wgArticlePath will be used.
-        * @param string $variant language variant of url (for sr, zh..)
-        * @return string the URL
+        * @param mixed $query an optional query string; if not specified,
+        *       $wgArticlePath will be used.  Can be specified as an associative array
+        *   as well, e.g., array( 'action' => 'edit' ) (keys and values will be
+        *   URL-escaped).
+        * @param $variant \type{\string} language variant of url (for sr, zh..)
+        * @return \type{\string} the URL
         */
        public function getLocalURL( $query = '', $variant = false ) {
                global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
-               global $wgVariantArticlePath, $wgContLang, $wgUser;
+               global $wgVariantArticlePath, $wgContLang, $wgUser, $wgArticlePathForCurid;
+
+               if( is_array( $query ) ) {
+                       $query = wfArrayToCGI( $query );
+               }
 
                // internal links should point to same variant as current page (only anonymous users)
                if($variant == false && $wgContLang->hasVariants() && !$wgUser->isLoggedIn()){
@@ -821,7 +833,7 @@ class Title {
                        }
                } else {
                        $dbkey = wfUrlencode( $this->getPrefixedDBkey() );
-                       if ( $query == '' ) {
+                       if ( $query == '' || ($wgArticlePathForCurid && substr_count( $query, '&' ) == 0 && strpos( $query, 'curid=' ) === 0 ) ) {
                                if( $variant != false && $wgContLang->hasVariants() ) {
                                        if( $wgVariantArticlePath == false ) {
                                                $variantArticlePath =  "$wgScript?title=$1&variant=$2"; // default
@@ -833,6 +845,7 @@ class Title {
                                } else {
                                        $url = str_replace( '$1', $dbkey, $wgArticlePath );
                                }
+                               $url = wfAppendQuery( $url, $query );
                        } else {
                                global $wgActionPaths;
                                $url = false;
@@ -866,11 +879,41 @@ class Title {
                return $url;
        }
 
+       /**
+        * Get a URL that's the simplest URL that will be valid to link, locally,
+        * to the current Title.  It includes the fragment, but does not include
+        * the server unless action=render is used (or the link is external).  If
+        * there's a fragment but the prefixed text is empty, we just return a link
+        * to the fragment.
+        *
+        * @param $query \type{\arrayof{\string}} An associative array of key => value pairs for the
+        *   query string.  Keys and values will be escaped.
+        * @param $variant \type{\string} Language variant of URL (for sr, zh..).  Ignored
+        *   for external links.  Default is "false" (same variant as current page,
+        *   for anonymous users).
+        * @return \type{\string} the URL
+        */
+       public function getLinkUrl( $query = array(), $variant = false ) {
+               if( !is_array( $query ) ) {
+                       throw new MWException( 'Title::getLinkUrl passed a non-array for '.
+                       '$query' );
+               }
+               if( $this->isExternal() ) {
+                       return $this->getFullURL( $query );
+               } elseif( $this->getPrefixedText() === ''
+               and $this->getFragment() !== '' ) {
+                       return $this->getFragmentForURL();
+               } else {
+                       return $this->getLocalURL( $query, $variant )
+                               . $this->getFragmentForURL();
+               }
+       }
+
        /**
         * Get an HTML-escaped version of the URL form, suitable for
         * using in a link, without a server name or fragment
-        * @param string $query an optional query string
-        * @return string the URL
+        * @param $query \type{\string} an optional query string
+        * @return \type{\string} the URL
         */
        public function escapeLocalURL( $query = '' ) {
                return htmlspecialchars( $this->getLocalURL( $query ) );
@@ -880,8 +923,8 @@ class Title {
         * Get an HTML-escaped version of the URL form, suitable for
         * using in a link, including the server name and fragment
         *
-        * @return string the URL
-        * @param string $query an optional query string
+        * @param $query \type{\string} an optional query string
+        * @return \type{\string} the URL
         */
        public function escapeFullURL( $query = '' ) {
                return htmlspecialchars( $this->getFullURL( $query ) );
@@ -892,9 +935,9 @@ class Title {
         * - Used in various Squid-related code, in case we have a different
         * internal hostname for the server from the exposed one.
         *
-        * @param string $query an optional query string
-        * @param string $variant language variant of url (for sr, zh..)
-        * @return string the URL
+        * @param $query \type{\string} an optional query string
+        * @param $variant \type{\string} language variant of url (for sr, zh..)
+        * @return \type{\string} the URL
         */
        public function getInternalURL( $query = '', $variant = false ) {
                global $wgInternalServer;
@@ -905,7 +948,7 @@ class Title {
 
        /**
         * Get the edit URL for this Title
-        * @return string the URL, or a null string if this is an
+        * @return \type{\string} the URL, or a null string if this is an
         *      interwiki link
         */
        public function getEditURL() {
@@ -918,7 +961,7 @@ class Title {
        /**
         * Get the HTML-escaped displayable text form.
         * Used for the title field in <a> tags.
-        * @return string the text, including any prefixes
+        * @return \type{\string} the text, including any prefixes
         */
        public function getEscapedText() {
                return htmlspecialchars( $this->getPrefixedText() );
@@ -926,15 +969,15 @@ class Title {
 
        /**
         * Is this Title interwiki?
-        * @return boolean
+        * @return \type{\bool}
         */
        public function isExternal() { return ( '' != $this->mInterwiki ); }
 
        /**
         * Is this page "semi-protected" - the *only* protection is autoconfirm?
         *
-        * @param string Action to check (default: edit)
-        * @return bool
+        * @param @action \type{\string} Action to check (default: edit)
+        * @return \type{\bool}
         */
        public function isSemiProtected( $action = 'edit' ) {
                if( $this->exists() ) {
@@ -957,9 +1000,9 @@ class Title {
 
        /**
         * Does the title correspond to a protected article?
-        * @param string $what the action the page is protected from,
+        * @param $what \type{\string} the action the page is protected from,
         * by default checks move and edit
-        * @return boolean
+        * @return \type{\bool}
         */
        public function isProtected( $action = '' ) {
                global $wgRestrictionLevels, $wgRestrictionTypes;
@@ -985,7 +1028,7 @@ class Title {
 
        /**
         * Is $wgUser watching this page?
-        * @return boolean
+        * @return \type{\bool}
         */
        public function userIsWatching() {
                global $wgUser;
@@ -1009,8 +1052,8 @@ class Title {
         *
         * May provide false positives, but should never provide a false negative.
         *
-        * @param string $action action that permission needs to be checked for
-        * @return boolean
+        * @param $action \type{\string} action that permission needs to be checked for
+        * @return \type{\bool}
         */
        public function quickUserCan( $action ) {
                return $this->userCan( $action, false );
@@ -1020,7 +1063,7 @@ class Title {
         * Determines if $wgUser is unable to edit this page because it has been protected
         * by $wgNamespaceProtection.
         *
-        * @return boolean
+        * @return \type{\bool}
         */
        public function isNamespaceProtected() {
                global $wgNamespaceProtection, $wgUser;
@@ -1035,9 +1078,9 @@ class Title {
 
        /**
         * Can $wgUser perform $action on this page?
-        * @param string $action action that permission needs to be checked for
-        * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries.
-        * @return boolean
+        * @param $action \type{\string} action that permission needs to be checked for
+        * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
+        * @return \type{\bool}
         */
        public function userCan( $action, $doExpensiveQueries = true ) {
                global $wgUser;
@@ -1049,23 +1092,29 @@ class Title {
         *
         * FIXME: This *does not* check throttles (User::pingLimiter()).
         *
-        * @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.
-        * @return array Array of arrays of the arguments to wfMsg to explain permissions problems.
-        */
-       public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true ) {
+        * @param $action \type{\string}action that permission needs to be checked for
+        * @param $user \type{User} user to check
+        * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
+        * @param $ignoreErrors \type{\arrayof{\string}} Set this to a list of message keys whose corresponding errors may be ignored.
+        * @return \type{\array} Array of arrays of the arguments to wfMsg to explain permissions problems.
+        */
+       public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) {
+               if( !StubObject::isRealObject( $user ) ) {
+                       //Since StubObject is always used on globals, we can unstub $wgUser here and set $user = $wgUser
+                       global $wgUser;
+                       $wgUser->_unstub( '', 5 );
+                       $user = $wgUser;
+               }
                $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries );
 
-               global $wgContLang;
-               global $wgLang;
-               global $wgEmailConfirmToEdit;
+               global $wgContLang, $wgLang, $wgEmailConfirmToEdit;
 
-               if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
+               if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount' ) {
                        $errors[] = array( 'confirmedittext' );
                }
 
-               if ( $user->isBlockedFrom( $this ) ) {
+               // Edit blocks should not affect reading. Account creation blocks handled at userlogin.
+               if ( $user->isBlockedFrom( $this ) && $action != 'read' && $action != 'createaccount' ) {
                        $block = $user->mBlock;
 
                        // This is from OutputPage::blockedPage
@@ -1090,20 +1139,7 @@ class Title {
                        $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $user->mBlock->mTimestamp ), true );
 
                        if ( $blockExpiry == 'infinity' ) {
-                               // Entry in database (table ipblocks) is 'infinity' but 'ipboptions' uses 'infinite' or 'indefinite'
-                               $scBlockExpiryOptions = wfMsg( 'ipboptions' );
-
-                               foreach ( explode( ',', $scBlockExpiryOptions ) as $option ) {
-                                       if ( strpos( $option, ':' ) == false )
-                                               continue;
-
-                                       list ($show, $value) = explode( ":", $option );
-
-                                       if ( $value == 'infinite' || $value == 'indefinite' ) {
-                                               $blockExpiry = $show;
-                                               break;
-                                       }
-                               }
+                               $blockExpiry = wfMsg( 'ipbinfinite' );
                        } else {
                                $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true );
                        }
@@ -1114,6 +1150,16 @@ class Title {
                                $blockid, $blockExpiry, $intended, $blockTimestamp );
                }
 
+               // Remove the errors being ignored.
+
+               foreach( $errors as $index => $error ) {
+                       $error_key = is_array($error) ? $error[0] : $error;
+                       
+                       if (in_array( $error_key, $ignoreErrors )) {
+                               unset($errors[$index]);
+                       }
+               }
+
                return $errors;
        }
 
@@ -1122,12 +1168,14 @@ class Title {
         * which checks ONLY that previously checked by userCan (i.e. it leaves out
         * checks on wfReadOnly() and blocks)
         *
-        * @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.
-        * @return array Array of arrays of the arguments to wfMsg to explain permissions problems.
+        * @param $action \type{\string} action that permission needs to be checked for
+        * @param $user \type{User} user to check
+        * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
+        * @return \type{\array} Array of arrays of the arguments to wfMsg to explain permissions problems.
         */
        private function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true ) {
+               global $wgLang;
+
                wfProfileIn( __METHOD__ );
 
                $errors = array();
@@ -1158,8 +1206,9 @@ class Title {
                        else if ($result === false )
                                $errors[] = array('badaccess-group0'); # a generic "We don't want them to do that"
                }
-
-               if( NS_SPECIAL == $this->mNamespace ) {
+               
+               $specialOKActions = array( 'createaccount', 'execute' );
+               if( NS_SPECIAL == $this->mNamespace && !in_array( $action, $specialOKActions) ) {
                        $errors[] = array('ns-specialprotected');
                }
 
@@ -1258,40 +1307,47 @@ class Title {
                        $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed');
                } elseif ( !$user->isAllowed( $action ) ) {
                        $return = null;
-                       $groups = array();
-                       global $wgGroupPermissions;
-                       foreach( $wgGroupPermissions as $key => $value ) {
-                               if( isset( $value[$action] ) && $value[$action] == true ) {
-                                       $groupName = User::getGroupName( $key );
-                                       $groupPage = User::getGroupPage( $key );
-                                       if( $groupPage ) {
-                                               $groups[] = '[['.$groupPage->getPrefixedText().'|'.$groupName.']]';
-                                       } else {
-                                               $groups[] = $groupName;
-                                       }
-                               }
+                       $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
+                               User::getGroupsWithPermission( $action ) );
+                       if ( $groups ) {
+                               $return = array( 'badaccess-groups',
+                                       array(
+                                               implode( ', ', $groups ),
+                                               count( $groups ) ) );
                        }
-                       $n = count( $groups );
-                       $groups = implode( ', ', $groups );
-                       switch( $n ) {
-                               case 0:
-                               case 1:
-                               case 2:
-                                       $return = array( "badaccess-group$n", $groups );
-                                       break;
-                               default:
-                                       $return = array( 'badaccess-groups', $groups );
+                       else {
+                               $return = array( "badaccess-group0" );
                        }
                        $errors[] = $return;
                }
 
+               // Check per-user restrictions
+               if( $action != 'read' ) {
+                       $r = $user->getRestrictionForPage( $this );
+                       if( !$r )
+                               $r = $user->getRestrictionForNamespace( $this->getNamespace() );
+                       if( $r ) {
+                               $start = $wgLang->timeanddate( $r->getTimestamp() );
+                               $end = $r->getExpiry() == 'infinity' ?
+                                       wfMsg( 'ipbinfinite' ) :
+                                       $wgLang->timeanddate( $r->getExpiry() );
+                               if( $r->isPage() )
+                                       $errors[] = array( 'userrestricted-page', $this->getFullText(),
+                                               $r->getBlockerText(), $r->getReason(), $start, $end );
+                               elseif( $r->isNamespace() ) {
+                                       $errors[] = array( 'userrestricted-namespace', $wgLang->getDisplayNsText( $this->getNamespace() ),
+                                               $r->getBlockerText(), $r->getReason(), $start, $end );
+                               }
+                       }
+               }
+
                wfProfileOut( __METHOD__ );
                return $errors;
        }
 
        /**
         * Is this title subject to title protection?
-        * @return mixed An associative array representing any existent title
+        * @return \type{\mixed} An associative array representing any existent title
         *   protection, or false if there's none.
         */
        private function getTitleProtection() {
@@ -1311,11 +1367,17 @@ class Title {
                }
        }
 
+       /**
+        * Update the title protection status
+        * @param $create_perm \type{\string} Permission required for creation
+        * @param $reason \type{\string} Reason for protection
+        * @param $expiry \type{\string} Expiry timestamp
+        */
        public function updateTitleProtection( $create_perm, $reason, $expiry ) {
-               global $wgGroupPermissions,$wgUser,$wgContLang;
+               global $wgUser,$wgContLang;
 
                if ($create_perm == implode(',',$this->getRestrictions('create'))
-                       && $expiry == $this->mRestrictionsExpiry) {
+                       && $expiry == $this->mRestrictionsExpiry['create']) {
                        // No change
                        return true;
                }
@@ -1330,7 +1392,10 @@ class Title {
                if ( $encodedExpiry != 'infinity' ) {
                        $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) ).')';
                }
-
+               else {
+                       $expiry_description .= ' (' . wfMsgForContent( 'protect-expiry-indefinite' ).')';
+               }
+       
                # Update protection table
                if ($create_perm != '' ) {
                        $dbw->replace( 'protected_titles', array(array('pt_namespace', 'pt_title')),
@@ -1347,7 +1412,8 @@ class Title {
                $log = new LogPage( 'protect' );
 
                if( $create_perm ) {
-                       $log->addEntry( $this->mRestrictions['create'] ? 'modify' : 'protect', $this, trim( $reason . " [create=$create_perm] $expiry_description" ) );
+                       $params = array("[create=$create_perm] $expiry_description",'');
+                       $log->addEntry( $this->mRestrictions['create'] ? 'modify' : 'protect', $this, trim( $reason ), $params );
                } else {
                        $log->addEntry( 'unprotect', $this, $reason );
                }
@@ -1356,7 +1422,7 @@ class Title {
        }
 
        /**
-        * Remove any title protection (due to page existing
+        * Remove any title protection due to page existing
         */
        public function deleteTitleProtection() {
                $dbw = wfGetDB( DB_MASTER );
@@ -1367,7 +1433,7 @@ class Title {
 
        /**
         * Can $wgUser edit this page?
-        * @return boolean
+        * @return \type{\bool} TRUE or FALSE
         * @deprecated use userCan('edit')
         */
        public function userCanEdit( $doExpensiveQueries = true ) {
@@ -1376,7 +1442,7 @@ class Title {
 
        /**
         * Can $wgUser create this page?
-        * @return boolean
+        * @return \type{\bool} TRUE or FALSE
         * @deprecated use userCan('create')
         */
        public function userCanCreate( $doExpensiveQueries = true ) {
@@ -1385,7 +1451,7 @@ class Title {
 
        /**
         * Can $wgUser move this page?
-        * @return boolean
+        * @return \type{\bool} TRUE or FALSE
         * @deprecated use userCan('move')
         */
        public function userCanMove( $doExpensiveQueries = true ) {
@@ -1396,7 +1462,7 @@ class Title {
         * Would anybody with sufficient privileges be able to move this page?
         * Some pages just aren't movable.
         *
-        * @return boolean
+        * @return \type{\bool} TRUE or FALSE
         */
        public function isMovable() {
                return MWNamespace::isMovable( $this->getNamespace() )
@@ -1405,7 +1471,7 @@ class Title {
 
        /**
         * Can $wgUser read this page?
-        * @return boolean
+        * @return \type{\bool} TRUE or FALSE
         * @todo fold these checks into userCan()
         */
        public function userCanRead() {
@@ -1482,7 +1548,7 @@ class Title {
 
        /**
         * Is this a talk page of some sort?
-        * @return bool
+        * @return \type{\bool} TRUE or FALSE
         */
        public function isTalkPage() {
                return MWNamespace::isTalk( $this->getNamespace() );
@@ -1490,23 +1556,45 @@ class Title {
 
        /**
         * Is this a subpage?
-        * @return bool
+        * @return \type{\bool} TRUE or FALSE
         */
        public function isSubpage() {
-               global $wgNamespacesWithSubpages;
+               return MWNamespace::hasSubpages( $this->mNamespace )
+                       ? strpos( $this->getText(), '/' ) !== false
+                       : false;
+       }
 
-               if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) ) {
-                       return ( strpos( $this->getText(), '/' ) !== false && $wgNamespacesWithSubpages[ $this->mNamespace ] == true );
-               } else {
+       /**
+        * Does this have subpages?  (Warning, usually requires an extra DB query.)
+        * @return \type{\bool} TRUE or FALSE
+        */
+       public function hasSubpages() {
+               if( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+                       # Duh
                        return false;
                }
+
+               # We dynamically add a member variable for the purpose of this method
+               # alone to cache the result.  There's no point in having it hanging
+               # around uninitialized in every Title object; therefore we only add it
+               # if needed and don't declare it statically.
+               if( isset( $this->mHasSubpages ) ) {
+                       return $this->mHasSubpages;
+               }
+
+               $db = wfGetDB( DB_SLAVE );
+               return $this->mHasSubpages = (bool)$db->selectField( 'page', '1',
+                       "page_namespace = {$this->mNamespace} AND page_title LIKE '"
+                       . $db->escapeLike( $this->mDbkeyform ) . "/%'",
+                       __METHOD__
+               );
        }
 
        /**
         * Could this page contain custom CSS or JavaScript, based
         * on the title?
         *
-        * @return bool
+        * @return \type{\bool} TRUE or FALSE
         */
        public function isCssOrJsPage() {
                return $this->mNamespace == NS_MEDIAWIKI
@@ -1515,7 +1603,7 @@ class Title {
 
        /**
         * Is this a .css or .js subpage of a user page?
-        * @return bool
+        * @return \type{\bool} TRUE or FALSE
         */
        public function isCssJsSubpage() {
                return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
@@ -1523,6 +1611,7 @@ class Title {
        /**
         * Is this a *valid* .css or .js subpage of a user page?
         * Check that the corresponding skin exists
+        * @return \type{\bool} TRUE or FALSE
         */
        public function isValidCssJsSubpage() {
                if ( $this->isCssJsSubpage() ) {
@@ -1542,14 +1631,14 @@ class Title {
        }
        /**
         * Is this a .css subpage of a user page?
-        * @return bool
+        * @return \type{\bool} TRUE or FALSE
         */
        public function isCssSubpage() {
                return ( NS_USER == $this->mNamespace && preg_match("/\\/.*\\.css$/", $this->mTextform ) );
        }
        /**
         * Is this a .js subpage of a user page?
-        * @return bool
+        * @return \type{\bool} TRUE or FALSE
         */
        public function isJsSubpage() {
                return ( NS_USER == $this->mNamespace && preg_match("/\\/.*\\.js$/", $this->mTextform ) );
@@ -1558,7 +1647,7 @@ class Title {
         * Protect css/js subpages of user pages: can $wgUser edit
         * this page?
         *
-        * @return boolean
+        * @return \type{\bool} TRUE or FALSE
         * @todo XXX: this might be better using restrictions
         */
        public function userCanEditCssJsSubpage() {
@@ -1569,7 +1658,7 @@ class Title {
        /**
         * Cascading protection: Return true if cascading restrictions apply to this page, false if not.
         *
-        * @return bool If the page is subject to cascading restrictions.
+        * @return \type{\bool} If the page is subject to cascading restrictions.
         */
        public function isCascadeProtected() {
                list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
@@ -1579,22 +1668,19 @@ class Title {
        /**
         * Cascading protection: Get the source of any cascading restrictions on this page.
         *
-        * @param $get_pages bool Whether or not to retrieve the actual pages that the restrictions have come from.
-        * @return array( mixed title array, restriction array)
-        * Array of the Title objects of the pages from which cascading restrictions have come, false for none, or true if such restrictions exist, but $get_pages was not set.
-        * The restriction array is an array of each type, each of which contains an array of unique groups
+        * @param $get_pages \type{\bool} Whether or not to retrieve the actual pages that the restrictions have come from.
+        * @return \type{\arrayof{mixed title array, restriction array}} Array of the Title objects of the pages from 
+        *         which cascading restrictions have come, false for none, or true if such restrictions exist, but $get_pages was not set.
+        *         The restriction array is an array of each type, each of which contains an array of unique groups.
         */
        public function getCascadeProtectionSources( $get_pages = true ) {
-               global $wgEnableCascadingProtection, $wgRestrictionTypes;
+               global $wgRestrictionTypes;
 
                # Define our dimension of restrictions types
                $pagerestrictions = array();
                foreach( $wgRestrictionTypes as $action )
                        $pagerestrictions[$action] = array();
 
-               if (!$wgEnableCascadingProtection)
-                       return array( false, $pagerestrictions );
-
                if ( isset( $this->mCascadeSources ) && $get_pages ) {
                        return array( $this->mCascadeSources, $this->mCascadingRestrictions );
                } else if ( isset( $this->mHasCascadingRestrictions ) && !$get_pages ) {
@@ -1634,7 +1720,7 @@ class Title {
                $now = wfTimestampNow();
                $purgeExpired = false;
 
-               while( $row = $dbr->fetchObject( $res ) ) {
+               foreach( $res as $row ) {
                        $expiry = Block::decodeExpiry( $row->pr_expiry );
                        if( $expiry > $now ) {
                                if ($get_pages) {
@@ -1667,7 +1753,6 @@ class Title {
                } else {
                        $this->mHasCascadingRestrictions = $sources;
                }
-
                return array( $sources, $pagerestrictions );
        }
 
@@ -1681,7 +1766,7 @@ class Title {
 
        /**
         * Loads a string into mRestrictions array
-        * @param resource $res restrictions as an SQL result.
+        * @param $res \type{Resource} restrictions as an SQL result.
         */
        private function loadRestrictionsFromRow( $res, $oldFashionedRestrictions = NULL ) {
                global $wgRestrictionTypes;
@@ -1689,15 +1774,16 @@ class Title {
 
                foreach( $wgRestrictionTypes as $type ){
                        $this->mRestrictions[$type] = array();
+                       $this->mRestrictionsExpiry[$type] = Block::decodeExpiry('');
                }
 
                $this->mCascadeRestriction = false;
-               $this->mRestrictionsExpiry = Block::decodeExpiry('');
 
                # Backwards-compatibility: also load the restrictions from the page record (old format).
 
                if ( $oldFashionedRestrictions === NULL ) {
-                       $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', array( 'page_id' => $this->getArticleId() ), __METHOD__ );
+                       $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', 
+                               array( 'page_id' => $this->getArticleId() ), __METHOD__ );
                }
 
                if ($oldFashionedRestrictions != '') {
@@ -1722,7 +1808,7 @@ class Title {
                        $now = wfTimestampNow();
                        $purgeExpired = false;
 
-                       while ($row = $dbr->fetchObject( $res ) ) {
+                       foreach( $res as $row ) {
                                # Cycle through all the restrictions.
 
                                // Don't take care of restrictions types that aren't in $wgRestrictionTypes
@@ -1735,7 +1821,7 @@ class Title {
 
                                // Only apply the restrictions if they haven't expired!
                                if ( !$expiry || $expiry > $now ) {
-                                       $this->mRestrictionsExpiry = $expiry;
+                                       $this->mRestrictionsExpiry[$row->pr_type] = $expiry;
                                        $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
 
                                        $this->mCascadeRestriction |= $row->pr_cascade;
@@ -1753,6 +1839,9 @@ class Title {
                $this->mRestrictionsLoaded = true;
        }
 
+       /**
+        * Load restrictions from the page_restrictions table
+        */
        public function loadRestrictions( $oldFashionedRestrictions = NULL ) {
                if( !$this->mRestrictionsLoaded ) {
                        if ($this->exists()) {
@@ -1773,11 +1862,13 @@ class Title {
 
                                        if (!$expiry || $expiry > $now) {
                                                // Apply the restrictions
-                                               $this->mRestrictionsExpiry = $expiry;
+                                               $this->mRestrictionsExpiry['create'] = $expiry;
                                                $this->mRestrictions['create'] = explode(',', trim($pt_create_perm) );
                                        } else { // Get rid of the old restrictions
                                                Title::purgeExpiredRestrictions();
                                        }
+                               } else {
+                                       $this->mRestrictionsExpiry['create'] = Block::decodeExpiry('');
                                }
                                $this->mRestrictionsLoaded = true;
                        }
@@ -1801,8 +1892,8 @@ class Title {
        /**
         * Accessor/initialisation for mRestrictions
         *
-        * @param string $action action that permission needs to be checked for
-        * @return array the array of groups allowed to edit this article
+        * @param $action \type{\string} action that permission needs to be checked for
+        * @return \type{\arrayof{\string}} the array of groups allowed to edit this article
         */
        public function getRestrictions( $action ) {
                if( !$this->mRestrictionsLoaded ) {
@@ -1813,9 +1904,21 @@ class Title {
                                : array();
        }
 
+       /**
+        * Get the expiry time for the restriction against a given action
+        * @return 14-char timestamp, or 'infinity' if the page is protected forever 
+        * or not protected at all, or false if the action is not recognised.
+        */
+       public function getRestrictionExpiry( $action ) {
+               if( !$this->mRestrictionsLoaded ) {
+                       $this->loadRestrictions();
+               }
+               return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
+       }
+
        /**
         * Is there a version of this page in the deletion archive?
-        * @return int the number of archived revisions
+        * @return \type{\int} the number of archived revisions
         */
        public function isDeleted() {
                $fname = 'Title::isDeleted';
@@ -1836,18 +1939,19 @@ class Title {
        /**
         * Get the article ID for this Title from the link cache,
         * adding it if necessary
-        * @param int $flags a bit field; may be GAID_FOR_UPDATE to select
+        * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select
         *      for update
-        * @return int the ID
+        * @return \type{\int} the ID
         */
        public function getArticleID( $flags = 0 ) {
                $linkCache = LinkCache::singleton();
-               if ( $flags & GAID_FOR_UPDATE ) {
+               if( $flags & GAID_FOR_UPDATE ) {
                        $oldUpdate = $linkCache->forUpdate( true );
+                       $linkCache->clearLink( $this );
                        $this->mArticleID = $linkCache->addLinkObj( $this );
                        $linkCache->forUpdate( $oldUpdate );
                } else {
-                       if ( -1 == $this->mArticleID ) {
+                       if( -1 == $this->mArticleID ) {
                                $this->mArticleID = $linkCache->addLinkObj( $this );
                        }
                }
@@ -1857,8 +1961,8 @@ class Title {
        /**
         * Is this an article that is a redirect page?
         * Uses link cache, adding it if necessary
-        * @param int $flags a bit field; may be GAID_FOR_UPDATE to select for update
-        * @return bool
+        * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update
+        * @return \type{\bool}
         */
        public function isRedirect( $flags = 0 ) {
                if( !is_null($this->mRedirect) )
@@ -1877,8 +1981,8 @@ class Title {
        /**
         * What is the length of this page?
         * Uses link cache, adding it if necessary
-        * @param int $flags a bit field; may be GAID_FOR_UPDATE to select for update
-        * @return bool
+        * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update
+        * @return \type{\bool}
         */
        public function getLength( $flags = 0 ) {
                if( $this->mLength != -1 )
@@ -1896,18 +2000,18 @@ class Title {
 
        /**
         * What is the page_latest field for this page?
-        * @param int $flags a bit field; may be GAID_FOR_UPDATE to select for update
-        * @return int
+        * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update
+        * @return \type{\int}
         */
        public function getLatestRevID( $flags = 0 ) {
-               if ($this->mLatestID !== false)
+               if( $this->mLatestID !== false )
                        return $this->mLatestID;
 
                $db = ($flags & GAID_FOR_UPDATE) ? wfGetDB(DB_MASTER) : wfGetDB(DB_SLAVE);
-               return $this->mLatestID = $db->selectField( 'revision',
-                       "max(rev_id)",
-                       array('rev_page' => $this->getArticleID()),
-                       'Title::getLatestRevID' );
+               $this->mLatestID = $db->selectField( 'page', 'page_latest',
+                       array( 'page_namespace' => $this->getNamespace(), 'page_title' => $this->getDBKey() ),
+                       __METHOD__ );
+               return $this->mLatestID;
        }
 
        /**
@@ -1918,7 +2022,7 @@ class Title {
         * loading of the new page_id. It's also called from
         * Article::doDeleteArticle()
         *
-        * @param int $newid the new Article ID
+        * @param $newid \type{\int} the new Article ID
         */
        public function resetArticleID( $newid ) {
                $linkCache = LinkCache::singleton();
@@ -1932,7 +2036,7 @@ class Title {
 
        /**
         * Updates page_touched for this page; called from LinksUpdate.php
-        * @return bool true if the update succeded
+        * @return \type{\bool} true if the update succeded
         */
        public function invalidateCache() {
                global $wgUseFileCache;
@@ -1963,8 +2067,8 @@ class Title {
         * Prefix some arbitrary text with the namespace or interwiki prefix
         * of this object
         *
-        * @param string $name the text
-        * @return string the prefixed text
+        * @param $name \type{\string} the text
+        * @return \type{\string} the prefixed text
         * @private
         */
        /* private */ function prefix( $name ) {
@@ -1986,7 +2090,7 @@ class Title {
         * removes illegal characters, splits off the interwiki and
         * namespace prefixes, sets the other forms, and canonicalizes
         * everything.
-        * @return bool true on success
+        * @return \type{\bool} true on success
         */
        private function secureAndSplit() {
                global $wgContLang, $wgLocalInterwiki, $wgCapitalLinks;
@@ -2110,9 +2214,9 @@ class Title {
                }
 
                /**
-                * Pages with "/./" or "/../" appearing in the URLs will
-                * often be unreachable due to the way web browsers deal
-                * with 'relative' URLs. Forbid them explicitly.
+                * Pages with "/./" or "/../" appearing in the URLs will often be un-
+                * reachable due to the way web browsers deal with 'relative' URLs.
+                * Also, they conflict with subpage syntax.  Forbid them explicitly.
                 */
                if ( strpos( $dbkey, '.' ) !== false &&
                     ( $dbkey === '.' || $dbkey === '..' ||
@@ -2192,13 +2296,14 @@ class Title {
        }
 
        /**
-        * Set the fragment for this title
-        * This is kind of bad, since except for this rarely-used function, Title objects
-        * are immutable. The reason this is here is because it's better than setting the
-        * members directly, which is what Linker::formatComment was doing previously.
+        * Set the fragment for this title. Removes the first character from the
+        * specified fragment before setting, so it assumes you're passing it with 
+        * an initial "#".
+        *
+        * Deprecated for public use, use Title::makeTitle() with fragment parameter.
+        * Still in active use privately.
         *
-        * @param string $fragment text
-        * @todo clarify whether access is supposed to be public (was marked as "kind of public")
+        * @param $fragment \type{\string} text
         */
        public function setFragment( $fragment ) {
                $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) );
@@ -2206,7 +2311,7 @@ class Title {
 
        /**
         * Get a Title object associated with the talk page of this article
-        * @return Title the object for the talk page
+        * @return \type{Title} the object for the talk page
         */
        public function getTalkPage() {
                return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
@@ -2216,7 +2321,7 @@ class Title {
         * Get a title object associated with the subject page of this
         * talk page
         *
-        * @return Title the object for the subject page
+        * @return \type{Title} the object for the subject page
         */
        public function getSubjectPage() {
                return Title::makeTitle( MWNamespace::getSubject( $this->getNamespace() ), $this->getDBkey() );
@@ -2229,8 +2334,8 @@ class Title {
         * WARNING: do not use this function on arbitrary user-supplied titles!
         * On heavily-used templates it will max out the memory.
         *
-        * @param string $options may be FOR UPDATE
-        * @return array the Title objects linking here
+        * @param $options \type{\string} may be FOR UPDATE
+        * @return \type{\arrayof{Title}} the Title objects linking here
         */
        public function getLinksTo( $options = '', $table = 'pagelinks', $prefix = 'pl' ) {
                $linkCache = LinkCache::singleton();
@@ -2247,12 +2352,12 @@ class Title {
                                "{$prefix}_from=page_id",
                                "{$prefix}_namespace" => $this->getNamespace(),
                                "{$prefix}_title"     => $this->getDBkey() ),
-                       'Title::getLinksTo',
+                       __METHOD__,
                        $options );
 
                $retVal = array();
                if ( $db->numRows( $res ) ) {
-                       while ( $row = $db->fetchObject( $res ) ) {
+                       foreach( $res as $row ) {
                                if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) {
                                        $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect );
                                        $retVal[] = $titleObj;
@@ -2270,8 +2375,8 @@ class Title {
         * WARNING: do not use this function on arbitrary user-supplied titles!
         * On heavily-used templates it will max out the memory.
         *
-        * @param string $options may be FOR UPDATE
-        * @return array the Title objects linking here
+        * @param $options \type{\string} may be FOR UPDATE
+        * @return \type{\arrayof{Title}} the Title objects linking here
         */
        public function getTemplateLinksTo( $options = '' ) {
                return $this->getLinksTo( $options, 'templatelinks', 'tl' );
@@ -2281,8 +2386,8 @@ class Title {
         * Get an array of Title objects referring to non-existent articles linked from this page
         *
         * @todo check if needed (used only in SpecialBrokenRedirects.php, and should use redirect table in this case)
-        * @param string $options may be FOR UPDATE
-        * @return array the Title objects
+        * @param $options \type{\string} may be FOR UPDATE
+        * @return \type{\arrayof{Title}} the Title objects
         */
        public function getBrokenLinksFrom( $options = '' ) {
                if ( $this->getArticleId() == 0 ) {
@@ -2312,7 +2417,7 @@ class Title {
 
                $retVal = array();
                if ( $db->numRows( $res ) ) {
-                       while ( $row = $db->fetchObject( $res ) ) {
+                       foreach( $res as $row ) {
                                $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
                        }
                }
@@ -2325,7 +2430,7 @@ class Title {
         * Get a list of URLs to purge from the Squid cache when this
         * page changes
         *
-        * @return array the URLs
+        * @return \type{\arrayof{\string}} the URLs
         */
        public function getSquidURLs() {
                global $wgContLang;
@@ -2347,6 +2452,9 @@ class Title {
                return $urls;
        }
 
+       /**
+        * Purge all applicable Squid URLs
+        */
        public function purgeSquid() {
                global $wgUseSquid;
                if ( $wgUseSquid ) {
@@ -2358,7 +2466,7 @@ class Title {
 
        /**
         * Move this page without authentication
-        * @param Title &$nt the new page Title
+        * @param &$nt \type{Title} the new page Title
         */
        public function moveNoAuth( &$nt ) {
                return $this->moveTo( $nt, false );
@@ -2366,34 +2474,37 @@ class Title {
 
        /**
         * Check whether a given move operation would be valid.
-        * Returns true if ok, or a message key string for an error message
-        * if invalid. (Scarrrrry ugly interface this.)
-        * @param Title &$nt the new title
-        * @param bool $auth indicates whether $wgUser's permissions
+        * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
+        * @param &$nt \type{Title} the new title
+        * @param $auth \type{\bool} indicates whether $wgUser's permissions
         *      should be checked
-        * @return mixed true on success, message name on failure
+        * @param $reason \type{\string} is the log summary of the move, used for spam checking
+        * @return \type{\mixed} True on success, getUserPermissionsErrors()-like array on failure
         */
-       public function isValidMoveOperation( &$nt, $auth = true ) {
-               if( !$this or !$nt ) {
-                       return 'badtitletext';
+       public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
+               $errors = array();      
+               if( !$nt ) {
+                       // 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 ) ) {
-                       return 'selfmove';
+                       $errors[] = array('selfmove');
                }
                if( !$this->isMovable() || !$nt->isMovable() ) {
-                       return 'immobile_namespace';
+                       $errors[] = array('immobile_namespace');
                }
 
                $oldid = $this->getArticleID();
                $newid = $nt->getArticleID();
 
                if ( strlen( $nt->getDBkey() ) < 1 ) {
-                       return 'articleexists';
+                       $errors[] = array('articleexists');
                }
                if ( ( '' == $this->getDBkey() ) ||
                         ( !$oldid ) ||
                     ( '' == $nt->getDBkey() ) ) {
-                       return 'badarticleerror';
+                       $errors[] = array('badarticleerror');
                }
 
                // Image-specific checks
@@ -2401,28 +2512,36 @@ class Title {
                        $file = wfLocalFile( $this );
                        if( $file->exists() ) {
                                if( $nt->getNamespace() != NS_IMAGE ) {
-                                       return 'imagenocrossnamespace';
+                                       $errors[] = array('imagenocrossnamespace');
+                               }
+                               if( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) {
+                                       $errors[] = array('imageinvalidfilename');
                                }
                                if( !File::checkExtensionCompatibility( $file, $nt->getDbKey() ) ) {
-                                       return 'imagetypemismatch';
+                                       $errors[] = array('imagetypemismatch');
                                }
                        }
                }
 
                if ( $auth ) {
                        global $wgUser;
-                       $errors = array_merge($this->getUserPermissionsErrors('move', $wgUser),
+                       $errors = wfArrayMerge($errors, 
+                                       $this->getUserPermissionsErrors('move', $wgUser),
                                        $this->getUserPermissionsErrors('edit', $wgUser),
                                        $nt->getUserPermissionsErrors('move', $wgUser),
                                        $nt->getUserPermissionsErrors('edit', $wgUser));
-                       if($errors !== array())
-                               return $errors[0][0];
                }
 
+               $match = EditPage::matchSpamRegex( $reason );
+               if( $match !== false ) {
+                       // This is kind of lame, won't display nice
+                       $errors[] = array('spamprotectiontext');
+               }
+               
                global $wgUser;
                $err = null;
-               if( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err ) ) ) {
-                       return 'hookaborted';
+               if( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) {
+                       $errors[] = array('hookaborted', $err);
                }
 
                # The move is allowed only if (1) the target doesn't exist, or
@@ -2431,35 +2550,38 @@ class Title {
 
                if ( 0 != $newid ) { # Target exists; check for validity
                        if ( ! $this->isValidMoveTarget( $nt ) ) {
-                               return 'articleexists';
+                               $errors[] = array('articleexists');
                        }
                } else {
                        $tp = $nt->getTitleProtection();
                        $right = ( $tp['pt_create_perm'] == 'sysop' ) ? 'protect' : $tp['pt_create_perm'];
                        if ( $tp and !$wgUser->isAllowed( $right ) ) {
-                               return 'cantmove-titleprotected';
+                               $errors[] = array('cantmove-titleprotected');
                        }
                }
-               return true;
+               if(empty($errors))
+                       return true;
+               return $errors;
        }
 
        /**
         * Move a title to a new location
-        * @param Title &$nt the new title
-        * @param bool $auth indicates whether $wgUser's permissions
+        * @param &$nt \type{Title} the new title
+        * @param $auth \type{\bool} indicates whether $wgUser's permissions
         *      should be checked
-        * @param string $reason The reason for the move
-        * @param bool $createRedirect Whether to create a redirect from the old title to the new title.
+        * @param $reason \type{\string} The reason for the move
+        * @param $createRedirect \type{\bool} Whether to create a redirect from the old title to the new title.
         *  Ignored if the user doesn't have the suppressredirect right.
-        * @return mixed true on success, message name on failure
+        * @return \type{\mixed} true on success, getUserPermissionsErrors()-like array on failure
         */
        public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
-               $err = $this->isValidMoveOperation( $nt, $auth );
-               if( is_string( $err ) ) {
+               $err = $this->isValidMoveOperation( $nt, $auth, $reason );
+               if( is_array( $err ) ) {
                        return $err;
                }
 
                $pageid = $this->getArticleID();
+               $protected = $this->isProtected();
                if( $nt->exists() ) {
                        $err = $this->moveOverExistingRedirect( $nt, $reason, $createRedirect );
                        $pageCountChange = ($createRedirect ? 0 : -1);
@@ -2467,7 +2589,8 @@ class Title {
                        $err = $this->moveToNewTitle( $nt, $reason, $createRedirect );
                        $pageCountChange = ($createRedirect ? 1 : 0);
                }
-               if( is_string( $err ) ) {
+
+               if( is_array( $err ) ) {
                        return $err;
                }
                $redirid = $this->getArticleID();
@@ -2493,8 +2616,28 @@ class Title {
                                'cl_sortkey' => $this->getPrefixedText() ),
                        __METHOD__ );
 
-               # Update watchlists
+               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 = wfMsgForContent('1movedto2',$this->getPrefixedText(), $nt->getPrefixedText() );
+                       $log->addEntry( 'protect', $nt, $comment, array() ); // FIXME: $params?
+               }
 
+               # Update watchlists
                $oldnamespace = $this->getNamespace() & ~1;
                $newnamespace = $nt->getNamespace() & ~1;
                $oldtitle = $this->getDBkey();
@@ -2546,10 +2689,10 @@ class Title {
         * Move page to a title which is at present a redirect to the
         * source page
         *
-        * @param Title &$nt the page to move to, which should currently
+        * @param &$nt \type{Title} the page to move to, which should currently
         *      be a redirect
-        * @param string $reason The reason for the move
-        * @param bool $createRedirect Whether to leave a redirect at the old title.
+        * @param $reason \type{\string} The reason for the move
+        * @param $createRedirect \type{\bool} Whether to leave a redirect at the old title.
         *  Ignored if the user doesn't have the suppressredirect right
         */
        private function moveOverExistingRedirect( &$nt, $reason = '', $createRedirect = true ) {
@@ -2564,18 +2707,9 @@ class Title {
                $now = wfTimestampNow();
                $newid = $nt->getArticleID();
                $oldid = $this->getArticleID();
-               $dbw = wfGetDB( DB_MASTER );
+               $latest = $this->getLatestRevID();
 
-               # Move an image if it is
-               if( $this->getNamespace() == NS_IMAGE ) {
-                       $file = wfLocalFile( $this );
-                       if( $file->exists() ) {
-                               $status = $file->move( $nt );
-                               if( !$status->isOk() ) {
-                                       return $status->getWikiText();
-                               }
-                       }
-               }
+               $dbw = wfGetDB( DB_MASTER );
 
                # Delete the old redirect. We don't save it to history since
                # by definition if we've got here it's rather uninteresting.
@@ -2598,7 +2732,10 @@ class Title {
 
                # Save a null revision in the page's history notifying of the move
                $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
-               $nullRevId = $nullRevision->insertOn( $dbw, true );
+               $nullRevId = $nullRevision->insertOn( $dbw );
+               
+               $article = new Article( $this );
+               wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest) );
 
                # Change the name of the target page:
                $dbw->update( 'page',
@@ -2614,8 +2751,7 @@ class Title {
                $nt->resetArticleID( $oldid );
 
                # Recreate the redirect, this time in the other direction.
-               if($createRedirect || !$wgUser->isAllowed('suppressredirect'))
-               {
+               if( $createRedirect || !$wgUser->isAllowed('suppressredirect') ) {
                        $mwRedir = MagicWord::get( 'redirect' );
                        $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
                        $redirectArticle = new Article( $this );
@@ -2624,8 +2760,10 @@ class Title {
                                'page'    => $newid,
                                'comment' => $comment,
                                'text'    => $redirectText ) );
-                       $redirectRevision->insertOn( $dbw, true );
+                       $redirectRevision->insertOn( $dbw );
                        $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
+                       
+                       wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false) );
 
                        # Now, we record the link from the redirect to the new title.
                        # It should have no other outgoing links...
@@ -2639,6 +2777,18 @@ class Title {
                } else {
                        $this->resetArticleID( 0 );
                }
+               
+               # Move an image if this is a file
+               if( $this->getNamespace() == NS_IMAGE ) {
+                       $file = wfLocalFile( $this );
+                       if( $file->exists() ) {
+                               $status = $file->move( $nt );
+                               if( !$status->isOk() ) {
+                                       $dbw->rollback();
+                                       return $status->getErrorsArray();
+                               }
+                       }
+               }
 
                # Log the move
                $log = new LogPage( 'move' );
@@ -2650,13 +2800,14 @@ class Title {
                        $u = new SquidUpdate( $urls );
                        $u->doUpdate();
                }
+               
        }
 
        /**
         * Move page to non-existing title.
-        * @param Title &$nt the new Title
-        * @param string $reason The reason for the move
-        * @param bool $createRedirect Whether to create a redirect from the old title to the new title
+        * @param &$nt \type{Title} the new Title
+        * @param $reason \type{\string} The reason for the move
+        * @param $createRedirect \type{\bool} Whether to create a redirect from the old title to the new title
         *  Ignored if the user doesn't have the suppressredirect right
         */
        private function moveToNewTitle( &$nt, $reason = '', $createRedirect = true ) {
@@ -2664,28 +2815,24 @@ class Title {
                $fname = 'MovePageForm::moveToNewTitle';
                $comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() );
                if ( $reason ) {
-                       $comment .= ": $reason";
+                       $comment .= wfMsgExt( 'colon-separator',
+                               array( 'escapenoentities', 'content' ) );
+                       $comment .= $reason;
                }
 
                $newid = $nt->getArticleID();
                $oldid = $this->getArticleID();
+               $latest = $this->getLatestRevId();
+               
                $dbw = wfGetDB( DB_MASTER );
                $now = $dbw->timestamp();
 
-               # Move an image if it is
-               if( $this->getNamespace() == NS_IMAGE ) {
-                       $file = wfLocalFile( $this );
-                       if( $file->exists() ) {
-                               $status = $file->move( $nt );
-                               if( !$status->isOk() ) {
-                                       return $status->getWikiText();
-                               }
-                       }
-               }
-
                # Save a null revision in the page's history notifying of the move
                $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
-               $nullRevId = $nullRevision->insertOn( $dbw, true );
+               $nullRevId = $nullRevision->insertOn( $dbw );
+               
+               $article = new Article( $this );
+               wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest) );
 
                # Rename page entry
                $dbw->update( 'page',
@@ -2700,8 +2847,7 @@ class Title {
                );
                $nt->resetArticleID( $oldid );
 
-               if($createRedirect || !$wgUser->isAllowed('suppressredirect'))
-               {
+               if( $createRedirect || !$wgUser->isAllowed('suppressredirect') ) {
                        # Insert redirect
                        $mwRedir = MagicWord::get( 'redirect' );
                        $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
@@ -2711,8 +2857,10 @@ class Title {
                                'page'    => $newid,
                                'comment' => $comment,
                                'text'    => $redirectText ) );
-                       $redirectRevision->insertOn( $dbw, true );
+                       $redirectRevision->insertOn( $dbw );
                        $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
+                       
+                       wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false) );
 
                        # Record the just-created redirect's linking to the page
                        $dbw->insert( 'pagelinks',
@@ -2724,6 +2872,18 @@ class Title {
                } else {
                        $this->resetArticleID( 0 );
                }
+               
+               # Move an image if this is a file
+               if( $this->getNamespace() == NS_IMAGE ) {
+                       $file = wfLocalFile( $this );
+                       if( $file->exists() ) {
+                               $status = $file->move( $nt );
+                               if( !$status->isOk() ) {
+                                       $dbw->rollback();
+                                       return $status->getErrorsArray();
+                               }
+                       }
+               }
 
                # Log the move
                $log = new LogPage( 'move' );
@@ -2735,13 +2895,15 @@ class Title {
                # Purge old title from squid
                # The new title, and links to the new title, are purged in Article::onArticleCreate()
                $this->purgeSquid();
+               
        }
 
        /**
         * Checks if $this can be moved to a given Title
         * - Selects for update, so don't call it unless you mean business
         *
-        * @param Title &$nt the new title to check
+        * @param &$nt \type{Title} the new title to check
+        * @return \type{\bool} TRUE or FALSE
         */
        public function isValidMoveTarget( $nt ) {
 
@@ -2804,7 +2966,7 @@ class Title {
        /**
         * Can this title be added to a user's watchlist?
         *
-        * @return bool
+        * @return \type{\bool} TRUE or FALSE
         */
        public function isWatchable() {
                return !$this->isExternal()
@@ -2815,7 +2977,7 @@ class Title {
         * Get categories to which this Title belongs and return an array of
         * categories' names.
         *
-        * @return array an array of parents in the form:
+        * @return \type{\array} array an array of parents in the form:
         *      $parent => $currentarticle
         */
        public function getParentCategories() {
@@ -2831,13 +2993,13 @@ class Title {
                         ." AND cl_from <> '0'"
                         ." ORDER BY cl_sortkey";
 
-               $res = $dbr->query ( $sql ) ;
+               $res = $dbr->query( $sql );
 
-               if($dbr->numRows($res) > 0) {
-                       while ( $x = $dbr->fetchObject ( $res ) )
-                               //$data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$x->cl_to);
-                               $data[$wgContLang->getNSText ( NS_CATEGORY ).':'.$x->cl_to] = $this->getFullText();
-                       $dbr->freeResult ( $res ) ;
+               if( $dbr->numRows( $res ) > 0 ) {
+                       foreach( $res as $row )
+                               //$data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$row->cl_to);
+                               $data[$wgContLang->getNSText( NS_CATEGORY ).':'.$row->cl_to] = $this->getFullText();
+                       $dbr->freeResult( $res );
                } else {
                        $data = array();
                }
@@ -2846,15 +3008,15 @@ class Title {
 
        /**
         * Get a tree of parent categories
-        * @param array $children an array with the children in the keys, to check for circular refs
-        * @return array
+        * @param $children \type{\array} an array with the children in the keys, to check for circular refs
+        * @return \type{\array} Tree of parent categories
         */
        public function getParentCategoryTree( $children = array() ) {
                $stack = array();
                $parents = $this->getParentCategories();
 
-               if($parents != '') {
-                       foreach($parents as $parent => $current) {
+               if( $parents ) {
+                       foreach( $parents as $parent => $current ) {
                                if ( array_key_exists( $parent, $children ) ) {
                                        # Circular reference
                                        $stack[$parent] = array();
@@ -2876,7 +3038,7 @@ class Title {
         * Get an associative array for selecting this title from
         * the "page" table
         *
-        * @return array
+        * @return \type{\array} Selection array
         */
        public function pageCond() {
                return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform );
@@ -2885,38 +3047,48 @@ class Title {
        /**
         * Get the revision ID of the previous revision
         *
-        * @param integer $revision  Revision ID. Get the revision that was before this one.
-        * @param integer $flags, GAID_FOR_UPDATE
-        * @return integer $oldrevision|false
+        * @param $revId \type{\int} Revision ID. Get the revision that was before this one.
+        * @param $flags \type{\int} GAID_FOR_UPDATE
+        * @return \twotypes{\int,\bool} Old revision ID, or FALSE if none exists
         */
-       public function getPreviousRevisionID( $revision, $flags=0 ) {
+       public function getPreviousRevisionID( $revId, $flags=0 ) {
                $db = ($flags & GAID_FOR_UPDATE) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                return $db->selectField( 'revision', 'rev_id',
-                       'rev_page=' . intval( $this->getArticleId() ) .
-                       ' AND rev_id<' . intval( $revision ) . ' ORDER BY rev_id DESC' );
+                       array(
+                               'rev_page' => $this->getArticleId($flags),
+                               'rev_id < ' . intval( $revId )
+                       ),
+                       __METHOD__,
+                       array( 'ORDER BY' => 'rev_id DESC' )
+               );
        }
 
        /**
         * Get the revision ID of the next revision
         *
-        * @param integer $revision  Revision ID. Get the revision that was after this one.
-        * @param integer $flags, GAID_FOR_UPDATE
-        * @return integer $oldrevision|false
+        * @param $revId \type{\int} Revision ID. Get the revision that was after this one.
+        * @param $flags \type{\int} GAID_FOR_UPDATE
+        * @return \twotypes{\int,\bool} Next revision ID, or FALSE if none exists
         */
-       public function getNextRevisionID( $revision, $flags=0 ) {
+       public function getNextRevisionID( $revId, $flags=0 ) {
                $db = ($flags & GAID_FOR_UPDATE) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                return $db->selectField( 'revision', 'rev_id',
-                       'rev_page=' . intval( $this->getArticleId() ) .
-                       ' AND rev_id>' . intval( $revision ) . ' ORDER BY rev_id' );
+                       array(
+                               'rev_page' => $this->getArticleId($flags),
+                               'rev_id > ' . intval( $revId )
+                       ),
+                       __METHOD__,
+                       array( 'ORDER BY' => 'rev_id' )
+               );
        }
 
        /**
         * Get the number of revisions between the given revision IDs.
         * Used for diffs and other things that really need it.
         *
-        * @param integer $old  Revision ID.
-        * @param integer $new  Revision ID.
-        * @return integer  Number of revisions between these IDs.
+        * @param $old \type{\int} Revision ID.
+        * @param $new \type{\int} Revision ID.
+        * @return \type{\int} Number of revisions between these IDs.
         */
        public function countRevisionsBetween( $old, $new ) {
                $dbr = wfGetDB( DB_SLAVE );
@@ -2931,10 +3103,10 @@ class Title {
        /**
         * Compare with another title.
         *
-        * @param Title $title
-        * @return bool
+        * @param \type{Title} $title
+        * @return \type{\bool} TRUE or FALSE
         */
-       public function equals( $title ) {
+       public function equals( Title $title ) {
                // Note: === is necessary for proper matching of number-like titles.
                return $this->getInterwiki() === $title->getInterwiki()
                        && $this->getNamespace() == $title->getNamespace()
@@ -2955,7 +3127,7 @@ class Title {
        /**
         * Return a string representation of this title
         *
-        * @return string
+        * @return \type{\string} String representation of this title
         */
        public function __toString() {
                return $this->getPrefixedText();
@@ -2963,7 +3135,7 @@ class Title {
 
        /**
         * Check if page exists
-        * @return bool
+        * @return \type{\bool} TRUE or FALSE
         */
        public function exists() {
                return $this->getArticleId() != 0;
@@ -2973,7 +3145,7 @@ class Title {
         * Do we know that this title definitely exists, or should we otherwise
         * consider that it exists?
         *
-        * @return bool
+        * @return \type{\bool} TRUE or FALSE
         */
        public function isAlwaysKnown() {
                // If the page is form Mediawiki:message/lang, calling wfMsgWeirdKey causes
@@ -3004,10 +3176,12 @@ class Title {
 
        /**
         * Get the last touched timestamp
+        * @param Database $db, optional db
+        * @return \type{\string} Last touched timestamp
         */
-       public function getTouched() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $touched = $dbr->selectField( 'page', 'page_touched',
+       public function getTouched( $db = NULL ) {
+               $db = isset($db) ? $db : wfGetDB( DB_SLAVE );
+               $touched = $db->selectField( 'page', 'page_touched',
                        array(
                                'page_namespace' => $this->getNamespace(),
                                'page_title' => $this->getDBkey()
@@ -3016,13 +3190,21 @@ class Title {
                return $touched;
        }
 
+       /**
+        * Get the trackback URL for this page
+        * @return \type{\string} Trackback URL
+        */
        public function trackbackURL() {
-               global $wgTitle, $wgScriptPath, $wgServer;
+               global $wgScriptPath, $wgServer;
 
                return "$wgServer$wgScriptPath/trackback.php?article="
-                       . htmlspecialchars(urlencode($wgTitle->getPrefixedDBkey()));
+                       . htmlspecialchars(urlencode($this->getPrefixedDBkey()));
        }
 
+       /**
+        * Get the trackback RDF for this page
+        * @return \type{\string} Trackback RDF
+        */
        public function trackbackRDF() {
                $url = htmlspecialchars($this->getFullURL());
                $title = htmlspecialchars($this->getText());
@@ -3048,7 +3230,7 @@ class Title {
 
        /**
         * Generate strings used for xml 'id' names in monobook tabs
-        * @return string
+        * @return \type{\string} XML 'id' name
         */
        public function getNamespaceKey() {
                global $wgContLang;
@@ -3088,7 +3270,7 @@ class Title {
 
        /**
         * Returns true if this title resolves to the named special page
-        * @param string $name The special page name
+        * @param $name \type{\string} The special page name
         */
        public function isSpecial( $name ) {
                if ( $this->getNamespace() == NS_SPECIAL ) {
@@ -3102,7 +3284,7 @@ class Title {
 
        /**
         * If the Title refers to a special page alias which is not the local default,
-        * returns a new Title which points to the local default. Otherwise, returns $this.
+        * @return \type{Title} A new Title which points to the local default. Otherwise, returns $this.
         */
        public function fixSpecialName() {
                if ( $this->getNamespace() == NS_SPECIAL ) {
@@ -3122,12 +3304,19 @@ class Title {
         * In other words, is this a content page, for the purposes of calculating
         * statistics, etc?
         *
-        * @return bool
+        * @return \type{\bool} TRUE or FALSE
         */
        public function isContentPage() {
                return MWNamespace::isContent( $this->getNamespace() );
        }
 
+       /**
+        * Get all extant redirects to this Title
+        *
+        * @param $ns \twotypes{\int,\null} Single namespace to consider; 
+        *            NULL to consider all namespaces
+        * @return \type{\arrayof{Title}} Redirects to this title
+        */
        public function getRedirectsHere( $ns = null ) {
                $redirs = array();
                
@@ -3139,7 +3328,7 @@ class Title {
                );
                if ( !is_null($ns) ) $where['page_namespace'] = $ns;
                
-               $result = $dbr->select(
+               $res = $dbr->select(
                        array( 'redirect', 'page' ),
                        array( 'page_namespace', 'page_title' ),
                        $where,
@@ -3147,7 +3336,7 @@ class Title {
                );
 
 
-               while( $row = $dbr->fetchObject( $result ) ) {
+               foreach( $res as $row ) {
                        $redirs[] = self::newFromRow( $row );
                }
                return $redirs;