Add wfUnserialize() wrapper around unserialize to prevent E_NOTICE and use it in...
[lhc/web/wiklou.git] / includes / Title.php
index d69610f..63d690e 100644 (file)
@@ -1,24 +1,25 @@
 <?php
 /**
  * See title.txt
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * @file
  */
 
-/**
- * @todo:  determine if it is really necessary to load this.  Appears to be left over from pre-autoloader versions, and
- *   is only really needed to provide access to constant UTF8_REPLACEMENT, which actually resides in UtfNormalDefines.php
- *   and is loaded by UtfNormalUtil.php, which is loaded by UtfNormal.php.
- */
-if ( !class_exists( 'UtfNormal' ) ) {
-       require_once( dirname( __FILE__ ) . '/normal/UtfNormal.php' );
-}
-
-/**
- * @deprecated This used to be a define, but was moved to
- * Title::GAID_FOR_UPDATE in 1.17. This will probably be removed in 1.18
- */
-define( 'GAID_FOR_UPDATE', Title::GAID_FOR_UPDATE );
-
 /**
  * Represents a title within MediaWiki.
  * Optionally may contain an interwiki designation or namespace.
@@ -87,9 +88,8 @@ class Title {
 
        /**
         * Constructor
-        * @private
         */
-       /* private */ function __construct() { }
+       /*protected*/ function __construct() { }
 
        /**
         * Create a new Title from a prefixed DB key
@@ -728,7 +728,8 @@ class Title {
         * @return String the prefixed title, with spaces
         */
        public function getPrefixedText() {
-               if ( empty( $this->mPrefixedText ) ) { // FIXME: bad usage of empty() ?
+               // @todo FIXME: Bad usage of empty() ?
+               if ( empty( $this->mPrefixedText ) ) {
                        $s = $this->prefix( $this->mTextform );
                        $s = str_replace( '_', ' ', $s );
                        $this->mPrefixedText = $s;
@@ -910,6 +911,7 @@ class Title {
                                                }
                                        }
                                }
+
                                if ( $url === false ) {
                                        if ( $query == '-' ) {
                                                $query = '';
@@ -918,7 +920,7 @@ class Title {
                                }
                        }
 
-                       // FIXME: this causes breakage in various places when we
+                       // @todo FIXME: This causes breakage in various places when we
                        // actually expected a local URL and end up with dupe prefixes.
                        if ( $wgRequest->getVal( 'action' ) == 'render' ) {
                                $url = $wgServer . $url;
@@ -990,8 +992,9 @@ class Title {
         * @return String the URL
         */
        public function getInternalURL( $query = '', $variant = false ) {
-               global $wgInternalServer;
-               $url = $wgInternalServer . $this->getLocalURL( $query, $variant );
+               global $wgInternalServer, $wgServer;
+               $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
+               $url = $server . $this->getLocalURL( $query, $variant );
                wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) );
                return $url;
        }
@@ -1180,11 +1183,12 @@ class Title {
        /**
         * Can $user perform $action on this page?
         *
-        * FIXME: This *does not* check throttles (User::pingLimiter()).
+        * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
         *
         * @param $action String action that permission needs to be checked for
         * @param $user User to check
-        * @param $doExpensiveQueries Bool Set this to false to avoid doing unnecessary queries.
+        * @param $doExpensiveQueries Bool Set this to false to avoid doing unnecessary queries by
+        *   skipping checks for cascading protections and user blocks.
         * @param $ignoreErrors Array of Strings Set this to a list of message keys whose corresponding errors may be ignored.
         * @return Array of arguments to wfMsg to explain permissions problems.
         */
@@ -1295,13 +1299,13 @@ class Title {
                if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
                        // A single array representing an error
                        $errors[] = $result;
-               } else if ( is_array( $result ) && is_array( $result[0] ) ) {
+               } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
                        // A nested array representing multiple errors
                        $errors = array_merge( $errors, $result );
-               } else if ( $result !== '' && is_string( $result ) ) {
+               } elseif ( $result !== '' && is_string( $result ) ) {
                        // A string representing a message-id
                        $errors[] = array( $result );
-               } else if ( $result === false ) {
+               } elseif ( $result === false ) {
                        // a generic "We don't want them to do that"
                        $errors[] = array( 'badaccess-group0' );
                }
@@ -1388,9 +1392,9 @@ class Title {
                if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' )
                                && !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
                        if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) {
-                               $errors[] = array( 'customcssjsprotected' );
-                       } else if ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
-                               $errors[] = array( 'customcssjsprotected' );
+                               $errors[] = array( 'customcssprotected' );
+                       } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
+                               $errors[] = array( 'customjsprotected' );
                        }
                }
 
@@ -1530,7 +1534,7 @@ class Title {
         * @return Array list of errors
         */
        private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) {
-               if( $short && count( $errors ) > 0 ) {
+               if( !$doExpensiveQueries ) {
                        return $errors;
                }
 
@@ -1540,8 +1544,14 @@ class Title {
                        $errors[] = array( 'confirmedittext' );
                }
 
-               // Edit blocks should not affect reading. Account creation blocks handled at userlogin.
-               if ( $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this ) ) {
+               if ( in_array( $action, array( 'read', 'createaccount', 'unblock' ) ) ){
+                       // Edit blocks should not affect reading.
+                       // Account creation blocks handled at userlogin.
+                       // Unblocking handled in SpecialUnblock
+               } elseif( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ){
+                       // Don't block the user from editing their own talk page unless they've been
+                       // explicitly blocked from that too.
+               } elseif( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) {
                        $block = $user->mBlock;
 
                        // This is from OutputPage::blockedPage
@@ -1561,29 +1571,16 @@ class Title {
                        }
 
                        $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]";
-                       $blockid = $block->mId;
+                       $blockid = $block->getId();
                        $blockExpiry = $user->mBlock->mExpiry;
                        $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, ':' ) )
-                                               continue;
-
-                                       list( $show, $value ) = explode( ':', $option );
-
-                                       if ( $value == 'infinite' || $value == 'indefinite' ) {
-                                               $blockExpiry = $show;
-                                               break;
-                                       }
-                               }
+                               $blockExpiry = wfMessage( 'infiniteblock' )->text();
                        } else {
                                $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true );
                        }
 
-                       $intended = $user->mBlock->mAddress;
+                       $intended = strval( $user->mBlock->getTarget() );
 
                        $errors[] = array( ( $block->mAuto ? 'autoblockedtext' : 'blockedtext' ), $link, $reason, $ip, $name,
                                $blockid, $blockExpiry, $intended, $blockTimestamp );
@@ -1679,10 +1676,10 @@ class Title {
 
                $dbw = wfGetDB( DB_MASTER );
 
-               $encodedExpiry = Block::encodeExpiry( $expiry, $dbw );
+               $encodedExpiry = $dbw->encodeExpiry( $expiry );
 
                $expiry_description = '';
-               if ( $encodedExpiry != 'infinity' ) {
+               if ( $encodedExpiry != $dbw->getInfinity() ) {
                        $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ),
                                $wgContLang->date( $expiry ) , $wgContLang->time( $expiry ) ) . ')';
                } else {
@@ -1695,7 +1692,7 @@ class Title {
                                        'pt_namespace' => $namespace,
                                        'pt_title' => $title,
                                        'pt_create_perm' => $create_perm,
-                                       'pt_timestamp' => Block::encodeExpiry( wfTimestampNow(), $dbw ),
+                                       'pt_timestamp' => $dbw->encodeExpiry( wfTimestampNow() ),
                                        'pt_expiry' => $encodedExpiry,
                                        'pt_user' => $wgUser->getId(),
                                        'pt_reason' => $reason,
@@ -1766,7 +1763,7 @@ class Title {
                                # Not a public wiki, so no shortcut
                                $useShortcut = false;
                        } elseif ( !empty( $wgRevokePermissions ) ) {
-                               /*
+                               /**
                                 * Iterate through each group with permissions being revoked (key not included since we don't care
                                 * what the group name is), then check if the read permission is being revoked. If it is, then
                                 * we don't use the shortcut below since the user might not be able to read, even though anon
@@ -1800,7 +1797,7 @@ class Title {
 
                        # Always grant access to the login page.
                        # Even anons need to be able to log in.
-                       if ( $this->isSpecial( 'Userlogin' ) || $this->isSpecial( 'Resetpass' ) ) {
+                       if ( $this->isSpecial( 'Userlogin' ) || $this->isSpecial( 'ChangePassword' ) ) {
                                return true;
                        }
 
@@ -1827,7 +1824,7 @@ class Title {
                        # If it's a special page, ditch the subpage bit and check again
                        if ( $this->getNamespace() == NS_SPECIAL ) {
                                $name = $this->getDBkey();
-                               list( $name, /* $subpage */ ) = SpecialPage::resolveAliasWithSubpage( $name );
+                               list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name );
                                if ( $name === false ) {
                                        # Invalid special page, but we show standard login required message
                                        return false;
@@ -1954,7 +1951,7 @@ class Title {
         * Is this a *valid* .css or .js subpage of a user page?
         *
         * @return Bool
-        * @deprecated @since 1.17
+        * @deprecated since 1.17
         */
        public function isValidCssJsSubpage() {
                return $this->isCssJsSubpage();
@@ -1998,7 +1995,7 @@ class Title {
         */
        public function userCanEditCssSubpage() {
                global $wgUser;
-               return ( ( $wgUser->isAllowed( 'editusercssjs' ) && $wgUser->isAllowed( 'editusercss' ) )
+               return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) )
                        || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
        }
 
@@ -2011,7 +2008,7 @@ class Title {
         */
        public function userCanEditJsSubpage() {
                global $wgUser;
-               return ( ( $wgUser->isAllowed( 'editusercssjs' ) && $wgUser->isAllowed( 'edituserjs' ) )
+               return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) )
                           || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
        }
 
@@ -2036,11 +2033,12 @@ class Title {
         *     contains a array of unique groups.
         */
        public function getCascadeProtectionSources( $getPages = true ) {
+               global $wgContLang;
                $pagerestrictions = array();
 
                if ( isset( $this->mCascadeSources ) && $getPages ) {
                        return array( $this->mCascadeSources, $this->mCascadingRestrictions );
-               } else if ( isset( $this->mHasCascadingRestrictions ) && !$getPages ) {
+               } elseif ( isset( $this->mHasCascadingRestrictions ) && !$getPages ) {
                        return array( $this->mHasCascadingRestrictions, $pagerestrictions );
                }
 
@@ -2081,7 +2079,7 @@ class Title {
                $purgeExpired = false;
 
                foreach ( $res as $row ) {
-                       $expiry = Block::decodeExpiry( $row->pr_expiry );
+                       $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW );
                        if ( $expiry > $now ) {
                                if ( $getPages ) {
                                        $page_id = $row->pr_page;
@@ -2162,13 +2160,14 @@ class Title {
         *        restrictions from page table (pre 1.10)
         */
        public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
+               global $wgContLang;
                $dbr = wfGetDB( DB_SLAVE );
 
                $restrictionTypes = $this->getRestrictionTypes();
 
                foreach ( $restrictionTypes as $type ) {
                        $this->mRestrictions[$type] = array();
-                       $this->mRestrictionsExpiry[$type] = Block::decodeExpiry( '' );
+                       $this->mRestrictionsExpiry[$type] = $wgContLang->formatExpiry( '', TS_MW );
                }
 
                $this->mCascadeRestriction = false;
@@ -2211,7 +2210,7 @@ class Title {
 
                                // This code should be refactored, now that it's being used more generally,
                                // But I don't really see any harm in leaving it in Block for now -werdna
-                               $expiry = Block::decodeExpiry( $row->pr_expiry );
+                               $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW );
 
                                // Only apply the restrictions if they haven't expired!
                                if ( !$expiry || $expiry > $now ) {
@@ -2240,12 +2239,17 @@ class Title {
         *        restrictions from page table (pre 1.10)
         */
        public function loadRestrictions( $oldFashionedRestrictions = null ) {
+               global $wgContLang;
                if ( !$this->mRestrictionsLoaded ) {
                        if ( $this->exists() ) {
                                $dbr = wfGetDB( DB_SLAVE );
 
-                               $res = $dbr->select( 'page_restrictions', '*',
-                                       array( 'pr_page' => $this->getArticleId() ), __METHOD__ );
+                               $res = $dbr->select(
+                                       'page_restrictions',
+                                       '*',
+                                       array( 'pr_page' => $this->getArticleId() ),
+                                       __METHOD__
+                               );
 
                                $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions );
                        } else {
@@ -2253,7 +2257,7 @@ class Title {
 
                                if ( $title_protection ) {
                                        $now = wfTimestampNow();
-                                       $expiry = Block::decodeExpiry( $title_protection['pt_expiry'] );
+                                       $expiry = $wgContLang->formatExpiry( $title_protection['pt_expiry'], TS_MW );
 
                                        if ( !$expiry || $expiry > $now ) {
                                                // Apply the restrictions
@@ -2264,7 +2268,7 @@ class Title {
                                                $this->mTitleProtection = false;
                                        }
                                } else {
-                                       $this->mRestrictionsExpiry['create'] = Block::decodeExpiry( '' );
+                                       $this->mRestrictionsExpiry['create'] = $wgContLang->formatExpiry( '', TS_MW );
                                }
                                $this->mRestrictionsLoaded = true;
                        }
@@ -2506,7 +2510,7 @@ class Title {
         * @return String the prefixed text
         * @private
         */
-       /* private */ function prefix( $name ) {
+       private function prefix( $name ) {
                $p = '';
                if ( $this->mInterwiki != '' ) {
                        $p = $this->mInterwiki . ':';
@@ -2577,8 +2581,6 @@ class Title {
                global $wgContLang, $wgLocalInterwiki;
 
                # Initialisation
-               $rxTc = self::getTitleInvalidRegex();
-
                $this->mInterwiki = $this->mFragment = '';
                $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
 
@@ -2609,7 +2611,7 @@ class Title {
 
                # Initial colon indicates main namespace rather than specified default
                # but should not create invalid {ns,title} pairs such as {0,Project:Foo}
-               if ( ':' == $dbkey { 0 } ) {
+               if ( ':' == $dbkey[0] ) {
                        $this->mNamespace = NS_MAIN;
                        $dbkey = substr( $dbkey, 1 ); # remove the colon but continue processing
                        $dbkey = trim( $dbkey, '_' ); # remove any subsequent whitespace
@@ -2631,7 +2633,7 @@ class Title {
                                                if ( $wgContLang->getNsIndex( $x[1] ) ) {
                                                        # Disallow Talk:File:x type titles...
                                                        return false;
-                                               } else if ( Interwiki::isValidInterwiki( $x[1] ) ) {
+                                               } elseif ( Interwiki::isValidInterwiki( $x[1] ) ) {
                                                        # Disallow Talk:Interwiki:x type titles...
                                                        return false;
                                                }
@@ -2680,7 +2682,7 @@ class Title {
                }
                $fragment = strstr( $dbkey, '#' );
                if ( false !== $fragment ) {
-                       $this->setFragment( preg_replace( '/^#_*/', '#', $fragment ) );
+                       $this->setFragment( $fragment );
                        $dbkey = substr( $dbkey, 0, strlen( $dbkey ) - strlen( $fragment ) );
                        # remove whitespace again: prevents "Foo_bar_#"
                        # becoming "Foo_bar_"
@@ -2688,6 +2690,7 @@ class Title {
                }
 
                # Reject illegal characters.
+               $rxTc = self::getTitleInvalidRegex();
                if ( preg_match( $rxTc, $dbkey ) ) {
                        return false;
                }
@@ -2987,22 +2990,7 @@ class Title {
 
                // Image-specific checks
                if ( $this->getNamespace() == NS_FILE ) {
-                       if ( $nt->getNamespace() != NS_FILE ) {
-                               $errors[] = array( 'imagenocrossnamespace' );
-                       }
-                       $file = wfLocalFile( $this );
-                       if ( $file->exists() ) {
-                               if ( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) {
-                                       $errors[] = array( 'imageinvalidfilename' );
-                               }
-                               if ( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) {
-                                       $errors[] = array( 'imagetypemismatch' );
-                               }
-                       }
-                       $destfile = wfLocalFile( $nt );
-                       if ( !$wgUser->isAllowed( 'reupload-shared' ) && !$destfile->exists() && wfFindFile( $nt ) ) {
-                               $errors[] = array( 'file-exists-sharedrepo' );
-                       }
+                       $errors = array_merge( $errors, $this->validateFileMoveOperation( $nt ) );
                }
 
                if ( $nt->getNamespace() == NS_FILE && $this->getNamespace() != NS_FILE ) {
@@ -3049,6 +3037,38 @@ class Title {
                return $errors;
        }
 
+       /**
+        * Check if the requested move target is a valid file move target
+        * @param Title $nt Target title
+        * @return array List of errors
+        */
+       protected function validateFileMoveOperation( $nt ) {
+               global $wgUser;
+
+               $errors = array();
+
+               if ( $nt->getNamespace() != NS_FILE ) {
+                       $errors[] = array( 'imagenocrossnamespace' );
+               }
+
+               $file = wfLocalFile( $this );
+               if ( $file->exists() ) {
+                       if ( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) {
+                               $errors[] = array( 'imageinvalidfilename' );
+                       }
+                       if ( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) {
+                               $errors[] = array( 'imagetypemismatch' );
+                       }
+               }
+
+               $destFile = wfLocalFile( $nt );
+               if ( !$wgUser->isAllowed( 'reupload-shared' ) && !$destFile->exists() && wfFindFile( $nt ) ) {
+                       $errors[] = array( 'file-exists-sharedrepo' );
+               }
+
+               return $errors;
+       }
+
        /**
         * Move a title to a new location
         *
@@ -3079,13 +3099,16 @@ class Title {
                        }
                }
 
-               $pageid = $this->getArticleID();
+               $dbw->begin(); # If $file was a LocalFile, its transaction would have closed our own.
+               $pageid = $this->getArticleID( self::GAID_FOR_UPDATE );
                $protected = $this->isProtected();
                $pageCountChange = ( $createRedirect ? 1 : 0 ) - ( $nt->exists() ? 1 : 0 );
 
                // Do the actual move
                $err = $this->moveToInternal( $nt, $reason, $createRedirect );
                if ( is_array( $err ) ) {
+                       # @todo FIXME: What about the File we have already moved?
+                       $dbw->rollback();
                        return $err;
                }
 
@@ -3093,19 +3116,26 @@ class Title {
 
                // Refresh the sortkey for this row.  Be careful to avoid resetting
                // cl_timestamp, which may disturb time-based lists on some sites.
-               $prefix = $dbw->selectField(
+               $prefixes = $dbw->select(
                        'categorylinks',
-                       'cl_sortkey_prefix',
+                       array( 'cl_sortkey_prefix', 'cl_to' ),
                        array( 'cl_from' => $pageid ),
                        __METHOD__
                );
-               $dbw->update( 'categorylinks',
-                       array(
-                               'cl_sortkey' => Collation::singleton()->getSortKey(
-                                       $nt->getCategorySortkey( $prefix ) ),
-                               'cl_timestamp=cl_timestamp' ),
-                       array( 'cl_from' => $pageid ),
-                       __METHOD__ );
+               foreach ( $prefixes as $prefixRow ) {
+                       $prefix = $prefixRow->cl_sortkey_prefix;
+                       $catTo = $prefixRow->cl_to;
+                       $dbw->update( 'categorylinks',
+                               array(
+                                       'cl_sortkey' => Collation::singleton()->getSortKey(
+                                               $nt->getCategorySortkey( $prefix ) ),
+                                       'cl_timestamp=cl_timestamp' ),
+                               array(
+                                       'cl_from' => $pageid,
+                                       'cl_to' => $catTo ),
+                               __METHOD__
+                       );
+               }
 
                if ( $protected ) {
                        # Protect the redirect title as the title used to be...
@@ -3128,7 +3158,8 @@ class Title {
                        if ( $reason ) {
                                $comment .= wfMsgForContent( 'colon-separator' ) . $reason;
                        }
-                       $log->addEntry( 'move_prot', $nt, $comment, array( $this->getPrefixedText() ) ); // FIXME: $params?
+                       // @todo FIXME: $params?
+                       $log->addEntry( 'move_prot', $nt, $comment, array( $this->getPrefixedText() ) );
                }
 
                # Update watchlists
@@ -3147,6 +3178,8 @@ class Title {
                $u = new SearchUpdate( $redirid, $this->getPrefixedDBkey(), '' );
                $u->doUpdate();
 
+               $dbw->commit();
+
                # Update site_stats
                if ( $this->isContentPage() && !$nt->isContentPage() ) {
                        # No longer a content page
@@ -3207,12 +3240,15 @@ class Title {
                if ( $reason ) {
                        $comment .= wfMsgForContent( 'colon-separator' ) . $reason;
                }
-               # Truncate for whole multibyte characters. +5 bytes for ellipsis
-               $comment = $wgContLang->truncate( $comment, 250 );
+               # Truncate for whole multibyte characters.
+               $comment = $wgContLang->truncate( $comment, 255 );
 
                $oldid = $this->getArticleID();
                $latest = $this->getLatestRevID();
 
+               $oldns = $this->getNamespace();
+               $olddbk = $this->getDBkey();
+
                $dbw = wfGetDB( DB_MASTER );
 
                if ( $moveOverRedirect ) {
@@ -3255,9 +3291,6 @@ class Title {
                }
                $nullRevId = $nullRevision->insertOn( $dbw );
 
-               $article = new Article( $this );
-               wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $wgUser ) );
-
                # Change the name of the target page:
                $dbw->update( 'page',
                        /* SET */ array(
@@ -3271,6 +3304,9 @@ class Title {
                );
                $nt->resetArticleID( $oldid );
 
+               $article = new Article( $nt );
+               wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $wgUser ) );
+
                # Recreate the redirect, this time in the other direction.
                if ( $createRedirect || !$wgUser->isAllowed( 'suppressredirect' ) ) {
                        $mwRedir = MagicWord::get( 'redirect' );
@@ -3297,6 +3333,17 @@ class Title {
                                __METHOD__ );
                        $redirectSuppressed = false;
                } else {
+                       // Get rid of old new page entries in Special:NewPages and RC.
+                       // Needs to be before $this->resetArticleID( 0 ).
+                       $dbw->delete( 'recentchanges', array(
+                                       'rc_timestamp' => $dbw->timestamp( $this->getEarliestRevTime() ),
+                                       'rc_namespace' => $oldns,
+                                       'rc_title' => $olddbk,
+                                       'rc_new' => 1
+                               ),
+                               __METHOD__
+                       );
+
                        $this->resetArticleID( 0 );
                        $redirectSuppressed = true;
                }
@@ -3603,65 +3650,68 @@ class Title {
         * @return Revision|Null if page doesn't exist
         */
        public function getFirstRevision( $flags = 0 ) {
-               $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                $pageId = $this->getArticleId( $flags );
-               if ( !$pageId ) {
-                       return null;
-               }
-               $row = $db->selectRow( 'revision', '*',
-                       array( 'rev_page' => $pageId ),
-                       __METHOD__,
-                       array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 )
-               );
-               if ( !$row ) {
-                       return null;
-               } else {
-                       return new Revision( $row );
+               if ( $pageId ) {
+                       $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
+                       $row = $db->selectRow( 'revision', '*',
+                               array( 'rev_page' => $pageId ),
+                               __METHOD__,
+                               array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 )
+                       );
+                       if ( $row ) {
+                               return new Revision( $row );
+                       }
                }
+               return null;
        }
 
        /**
-        * Check if this is a new page
+        * Get the oldest revision timestamp of this page
         *
-        * @return bool
+        * @param $flags Int Title::GAID_FOR_UPDATE
+        * @return String: MW timestamp
         */
-       public function isNewPage() {
-               $dbr = wfGetDB( DB_SLAVE );
-               return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
+       public function getEarliestRevTime( $flags = 0 ) {
+               $rev = $this->getFirstRevision( $flags );
+               return $rev ? $rev->getTimestamp() : null;
        }
 
        /**
-        * Get the oldest revision timestamp of this page
+        * Check if this is a new page
         *
-        * @return String: MW timestamp
+        * @return bool
         */
-       public function getEarliestRevTime() {
+       public function isNewPage() {
                $dbr = wfGetDB( DB_SLAVE );
-               if ( $this->exists() ) {
-                       $min = $dbr->selectField( 'revision',
-                               'MIN(rev_timestamp)',
-                               array( 'rev_page' => $this->getArticleId() ),
-                               __METHOD__ );
-                       return wfTimestampOrNull( TS_MW, $min );
-               }
-               return null;
+               return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
        }
 
        /**
-        * Get the number of revisions between the given revision IDs.
+        * Get the number of revisions between the given revision.
         * Used for diffs and other things that really need it.
         *
-        * @param $old Int Revision ID.
-        * @param $new Int Revision ID.
-        * @return Int Number of revisions between these IDs.
+        * @param $old int|Revision Old revision or rev ID (first before range)
+        * @param $new int|Revision New revision or rev ID (first after range)
+        * @return Int Number of revisions between these revisions.
         */
        public function countRevisionsBetween( $old, $new ) {
+               if ( !( $old instanceof Revision ) ) {
+                       $old = Revision::newFromTitle( $this, (int)$old );
+               }
+               if ( !( $new instanceof Revision ) ) {
+                       $new = Revision::newFromTitle( $this, (int)$new );
+               }
+               if ( !$old || !$new ) {
+                       return 0; // nothing to compare
+               }
                $dbr = wfGetDB( DB_SLAVE );
-               return (int)$dbr->selectField( 'revision', 'count(*)', array(
-                               'rev_page' => intval( $this->getArticleId() ),
-                               'rev_id > ' . intval( $old ),
-                               'rev_id < ' . intval( $new )
-                       ), __METHOD__
+               return (int)$dbr->selectField( 'revision', 'count(*)',
+                       array(
+                               'rev_page' => $this->getArticleId(),
+                               'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
+                               'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
+                       ),
+                       __METHOD__
                );
        }
 
@@ -3669,23 +3719,31 @@ class Title {
         * Get the number of authors between the given revision IDs.
         * Used for diffs and other things that really need it.
         *
-        * @param $fromRevId Int Revision ID (first before range)
-        * @param $toRevId Int Revision ID (first after range)
+        * @param $old int|Revision Old revision or rev ID (first before range)
+        * @param $new int|Revision New revision or rev ID (first after range)
         * @param $limit Int Maximum number of authors
-        * @param $flags Int Title::GAID_FOR_UPDATE
-        * @return Int
+        * @return Int Number of revision authors between these revisions.
         */
-       public function countAuthorsBetween( $fromRevId, $toRevId, $limit, $flags = 0 ) {
-               $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
-               $res = $db->select( 'revision', 'DISTINCT rev_user_text',
+       public function countAuthorsBetween( $old, $new, $limit ) {
+               if ( !( $old instanceof Revision ) ) {
+                       $old = Revision::newFromTitle( $this, (int)$old );
+               }
+               if ( !( $new instanceof Revision ) ) {
+                       $new = Revision::newFromTitle( $this, (int)$new );
+               }
+               if ( !$old || !$new ) {
+                       return 0; // nothing to compare
+               }
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( 'revision', 'DISTINCT rev_user_text',
                        array(
                                'rev_page' => $this->getArticleID(),
-                               'rev_id > ' . (int)$fromRevId,
-                               'rev_id < ' . (int)$toRevId
+                               'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
+                               'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
                        ), __METHOD__,
-                       array( 'LIMIT' => $limit )
+                       array( 'LIMIT' => $limit + 1 ) // add one so caller knows it was truncated
                );
-               return (int)$db->numRows( $res );
+               return (int)$dbr->numRows( $res );
        }
 
        /**
@@ -3704,6 +3762,9 @@ class Title {
        /**
         * Callback for usort() to do title sorts by (namespace, title)
         *
+        * @param $a Title
+        * @param $b Title
+        *
         * @return Integer: result of string comparison, or namespace comparison
         */
        public static function compare( $a, $b ) {
@@ -3763,7 +3824,7 @@ class Title {
                                return (bool)wfFindFile( $this );
                        case NS_SPECIAL:
                                // valid special page
-                               return SpecialPage::exists( $this->getDBkey() );
+                               return SpecialPageFactory::exists( $this->getDBkey() );
                        case NS_MAIN:
                                // selflink, possibly with fragment
                                return $this->mDbkeyform == '';
@@ -3990,7 +4051,7 @@ class Title {
         */
        public function isSpecial( $name ) {
                if ( $this->getNamespace() == NS_SPECIAL ) {
-                       list( $thisName, /* $subpage */ ) = SpecialPage::resolveAliasWithSubpage( $this->getDBkey() );
+                       list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() );
                        if ( $name == $thisName ) {
                                return true;
                        }
@@ -4006,9 +4067,9 @@ class Title {
         */
        public function fixSpecialName() {
                if ( $this->getNamespace() == NS_SPECIAL ) {
-                       $canonicalName = SpecialPage::resolveAlias( $this->mDbkeyform );
+                       list( $canonicalName, /*...*/ ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform );
                        if ( $canonicalName ) {
-                               $localName = SpecialPage::getLocalNameFor( $canonicalName );
+                               $localName = SpecialPageFactory::getLocalNameFor( $canonicalName );
                                if ( $localName != $this->mDbkeyform ) {
                                        return Title::makeTitle( NS_SPECIAL, $localName );
                                }
@@ -4116,6 +4177,10 @@ class Title {
         * @return array applicable restriction types
         */
        public function getRestrictionTypes() {
+               if ( $this->getNamespace() == NS_SPECIAL ) {
+                       return array();
+               }
+
                $types = self::getFilteredRestrictionTypes( $this->exists() );
 
                if ( $this->getNamespace() != NS_FILE ) {
@@ -4124,15 +4189,15 @@ class Title {
                }
 
                wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) );
-               
-               wfDebug( __METHOD__ . ': applicable restriction types for ' . 
-                       $this->getPrefixedText() . ' are ' . implode( ',', $types ) );
+
+               wfDebug( __METHOD__ . ': applicable restriction types for ' .
+                       $this->getPrefixedText() . ' are ' . implode( ',', $types ) . "\n" );
 
                return $types;
        }
        /**
-        * Get a filtered list of all restriction types supported by this wiki. 
-        * @param bool $exists True to get all restriction types that apply to 
+        * Get a filtered list of all restriction types supported by this wiki.
+        * @param bool $exists True to get all restriction types that apply to
         * titles that do exist, False for all restriction types that apply to
         * titles that do not exist
         * @return array
@@ -4142,7 +4207,7 @@ class Title {
                $types = $wgRestrictionTypes;
                if ( $exists ) {
                        # Remove the create restriction for existing titles
-                       $types = array_diff( $types, array( 'create' ) );                       
+                       $types = array_diff( $types, array( 'create' ) );
                } else {
                        # Only the create and upload restrictions apply to non-existing titles
                        $types = array_intersect( $types, array( 'create', 'upload' ) );
@@ -4172,4 +4237,69 @@ class Title {
                }
                return $unprefixed;
        }
+
+       /**
+        * Get the language in which the content of this page is written.
+        * Defaults to $wgContLang, but in certain cases it can be e.g.
+        * $wgLang (such as special pages, which are in the user language).
+        *
+        * @return object Language
+        */
+       public function getPageLanguage() {
+               global $wgLang;
+               if ( $this->getNamespace() == NS_SPECIAL ) {
+                       // special pages are in the user language
+                       return $wgLang;
+               } elseif ( $this->isRedirect() ) {
+                       // the arrow on a redirect page is aligned according to the user language
+                       return $wgLang;
+               } elseif ( $this->isCssOrJsPage() ) {
+                       // css/js should always be LTR and is, in fact, English
+                       return wfGetLangObj( 'en' );
+               } elseif ( $this->getNamespace() == NS_MEDIAWIKI ) {
+                       // Parse mediawiki messages with correct target language
+                       list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $this->getText() );
+                       return wfGetLangObj( $lang );
+               }
+               global $wgContLang;
+               // If nothing special, it should be in the wiki content language
+               $pageLang = $wgContLang;
+               // Hook at the end because we don't want to override the above stuff
+               wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) );
+               return wfGetLangObj( $pageLang );
+       }
+}
+
+/**
+ * A BadTitle is generated in MediaWiki::parseTitle() if the title is invalid; the
+ * software uses this to display an error page.  Internally it's basically a Title
+ * for an empty special page
+ */
+class BadTitle extends Title {
+       public function __construct(){
+               $this->mTextform = '';
+               $this->mUrlform = '';
+               $this->mDbkeyform = '';
+               $this->mNamespace = NS_SPECIAL; // Stops talk page link, etc, being shown
+       }
+
+       public function exists(){
+               return false;
+       }
+
+       public function getPrefixedText(){
+               return '';
+       }
+
+       public function getText(){
+               return '';
+       }
+
+       public function getPrefixedURL(){
+               return '';
+       }
+
+       public function getPrefixedDBKey(){
+               return '';
+       }
 }