Made the page_count update defered, as for the site_stats update. I just removed...
[lhc/web/wiklou.git] / includes / Title.php
index 06aa98e..a5d9884 100644 (file)
@@ -13,7 +13,11 @@ if ( !class_exists( 'UtfNormal' ) ) {
        require_once( dirname( __FILE__ ) . '/normal/UtfNormal.php' );
 }
 
-define ( 'GAID_FOR_UPDATE', 1 );
+/**
+ * @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.
@@ -27,7 +31,6 @@ class Title {
        /** @name Static cache variables */
        // @{
        static private $titleCache = array();
-       static private $interwikiCache = array();
        // @}
 
        /**
@@ -37,6 +40,12 @@ class Title {
         */
        const CACHE_MAX = 1000;
 
+       /**
+        * Used to be GAID_FOR_UPDATE define. Used with getArticleId() and friends
+        * to use the master DB
+        */
+       const GAID_FOR_UPDATE = 1;
+
 
        /**
         * @name Private member variables
@@ -130,7 +139,7 @@ class Title {
                }
 
                /**
-                * Convert things like é ā or 〗 into normalized(bug 14952) text
+                * Convert things like é ā or 〗 into normalized (bug 14952) text
                 */
                $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
 
@@ -194,11 +203,11 @@ class Title {
         * Create a new Title from an article ID
         *
         * @param $id \type{\int} the page_id corresponding to the Title to create
-        * @param $flags \type{\int} use GAID_FOR_UPDATE to use master
+        * @param $flags \type{\int} use Title::GAID_FOR_UPDATE to use master
         * @return \type{Title} the new object, or NULL on an error
         */
        public static function newFromID( $id, $flags = 0 ) {
-               $db = ( $flags & GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
+               $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                $row = $db->selectRow( 'page', '*', array( 'page_id' => $id ), __METHOD__ );
                if ( $row !== false ) {
                        $title = Title::newFromRow( $row );
@@ -320,8 +329,8 @@ class Title {
         * This will only return the very next target, useful for
         * the redirect table and other checks that don't need full recursion
         *
-        * @param $text \type{\string} Text with possible redirect
-        * @return \type{Title} The corresponding Title
+        * @param $text String: Text with possible redirect
+        * @return Title: The corresponding Title
         */
        public static function newFromRedirect( $text ) {
                return self::newFromRedirectInternal( $text );
@@ -402,9 +411,7 @@ class Title {
                                // and URL-decode links
                                if ( strpos( $m[1], '%' ) !== false ) {
                                        // Match behavior of inline link parsing here;
-                                       // don't interpret + as " " most of the time!
-                                       // It might be safe to just use rawurldecode instead, though.
-                                       $m[1] = urldecode( ltrim( $m[1], ':' ) );
+                                       $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
                                }
                                $title = Title::newFromText( $m[1] );
                                // If the title is a redirect to bad special pages or is invalid, return null
@@ -586,21 +593,21 @@ class Title {
        /**
         * Get the main part with underscores
         *
-        * @return \type{\string} Main part of the title, with underscores
+        * @return 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 \type{\int} Namespace index
+        * @return Integer: Namespace index
         */
        public function getNamespace() { return $this->mNamespace; }
 
        /**
         * Get the namespace text
         *
-        * @return \type{\string} Namespace text
+        * @return String: Namespace text
         */
        public function getNsText() {
                global $wgContLang;
@@ -854,20 +861,12 @@ class Title {
         */
        public function getLocalURL( $query = '', $variant = false ) {
                global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
-               global $wgVariantArticlePath, $wgContLang, $wgUser;
+               global $wgVariantArticlePath, $wgContLang;
 
                if ( is_array( $query ) ) {
                        $query = wfArrayToCGI( $query );
                }
 
-               // internal links should point to same variant as current page (only anonymous users)
-               if ( !$variant && $wgContLang->hasVariants() && !$wgUser->isLoggedIn() ) {
-                       $pref = $wgContLang->getPreferredVariant( false );
-                       if ( $pref != $wgContLang->getCode() ) {
-                               $variant = $pref;
-                       }
-               }
-
                if ( $this->isExternal() ) {
                        $url = $this->getFullURL();
                        if ( $query ) {
@@ -1141,16 +1140,23 @@ class Title {
        }
 
        /**
-        * Determines if $wgUser is unable to edit this page because it has been protected
+        * Determines if $user is unable to edit this page because it has been protected
         * by $wgNamespaceProtection.
         *
+        * @param $user User object, $wgUser will be used if not passed
         * @return \type{\bool}
         */
-       public function isNamespaceProtected() {
-               global $wgNamespaceProtection, $wgUser;
+       public function isNamespaceProtected( User $user = null ) {
+               global $wgNamespaceProtection;
+
+               if ( $user === null ) {
+                       global $wgUser;
+                       $user = $wgUser;
+               }
+
                if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
                        foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
-                               if ( $right != '' && !$wgUser->isAllowed( $right ) ) {
+                               if ( $right != '' && !$user->isAllowed( $right ) ) {
                                        return true;
                                }
                        }
@@ -1182,14 +1188,6 @@ class Title {
         * @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 );
 
                // Remove the errors being ignored.
@@ -1261,8 +1259,6 @@ class Title {
                                $errors[] = array( 'cant-move-to-user-page' );
                        }
                } elseif ( !$user->isAllowed( $action ) ) {
-                       $return = null;
-
                        // We avoid expensive display logic for quickUserCan's and such
                        $groups = false;
                        if ( !$short ) {
@@ -1313,10 +1309,18 @@ class Title {
 
        /**
         * Check various permission hooks
-        * @see checkQuickPermissions for parameter information
+        *
+        * @param $action String the action to check
+        * @param $user User user to check
+        * @param $errors Array list of current errors
+        * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
+        * @param $short Boolean short circuit on first error
+        *
+        * @return Array list of errors
         */
        private function checkPermissionHooks( $action, $user, $errors, $doExpensiveQueries, $short ) {
                // Use getUserPermissionsErrors instead
+               $result = '';
                if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) {
                        return $result ? array() : array( array( 'badaccess-group0' ) );
                }
@@ -1335,7 +1339,14 @@ class Title {
 
        /**
         * Check permissions on special pages & namespaces
-        * @see checkQuickPermissions for parameter information
+        *
+        * @param $action String the action to check
+        * @param $user User user to check
+        * @param $errors Array list of current errors
+        * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
+        * @param $short Boolean short circuit on first error
+        *
+        * @return Array list of errors
         */
        private function checkSpecialsAndNSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
                # Only 'createaccount' and 'execute' can be performed on
@@ -1346,7 +1357,7 @@ class Title {
                }
 
                # Check $wgNamespaceProtection for restricted namespaces
-               if ( $this->isNamespaceProtected() ) {
+               if ( $this->isNamespaceProtected( $user ) ) {
                        $ns = $this->mNamespace == NS_MAIN ?
                                wfMsg( 'nstab-main' ) : $this->getNsText();
                        $errors[] = $this->mNamespace == NS_MEDIAWIKI ?
@@ -1358,7 +1369,14 @@ class Title {
 
        /**
         * Check CSS/JS sub-page permissions
-        * @see checkQuickPermissions for parameter information
+        *
+        * @param $action String the action to check
+        * @param $user User user to check
+        * @param $errors Array list of current errors
+        * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
+        * @param $short Boolean short circuit on first error
+        *
+        * @return Array list of errors
         */
        private function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
                # Protect css/js subpages of user pages
@@ -1382,7 +1400,14 @@ class Title {
         * Check against page_restrictions table requirements on this
         * page. The user must possess all required rights for this
         * action.
-        * @see checkQuickPermissions for parameter information
+        *
+        * @param $action String the action to check
+        * @param $user User user to check
+        * @param $errors Array list of current errors
+        * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
+        * @param $short Boolean short circuit on first error
+        *
+        * @return Array list of errors
         */
        private function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) {
                foreach ( $this->getRestrictions( $action ) as $right ) {
@@ -1409,7 +1434,14 @@ class Title {
 
        /**
         * Check restrictions on cascading pages.
-        * @see checkQuickPermissions for parameter information
+        * 
+        * @param $action String the action to check
+        * @param $user User user to check
+        * @param $errors Array list of current errors
+        * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
+        * @param $short Boolean short circuit on first error
+        *
+        * @return Array list of errors
         */
        private function checkCascadingSourcesRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) {
                if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) {
@@ -1441,7 +1473,14 @@ class Title {
 
        /**
         * Check action permissions not already checked in checkQuickPermissions
-        * @see checkQuickPermissions for parameter information
+        *
+        * @param $action String the action to check
+        * @param $user User user to check
+        * @param $errors Array list of current errors
+        * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
+        * @param $short Boolean short circuit on first error
+        *
+        * @return Array list of errors
         */
        private function checkActionPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
                if ( $action == 'protect' ) {
@@ -1480,10 +1519,17 @@ class Title {
 
        /**
         * Check that the user isn't blocked from editting.
-        * @see checkQuickPermissions for parameter information
+        *
+        * @param $action String the action to check
+        * @param $user User user to check
+        * @param $errors Array list of current errors
+        * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
+        * @param $short Boolean short circuit on first error
+        *
+        * @return Array list of errors
         */
        private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) {
-               if( $short ) {
+               if( $short && count( $errors ) > 0 ) {
                        return $errors;
                }
 
@@ -1610,6 +1656,10 @@ class Title {
                return $this->mTitleProtection;
        }
 
+       private function invalidateTitleProtectionCache() {
+               unset( $this->mTitleProtection );
+       }
+
        /**
         * Update the title protection status
         *
@@ -1658,6 +1708,8 @@ class Title {
                        $dbw->delete( 'protected_titles', array( 'pt_namespace' => $namespace,
                                'pt_title' => $title ), __METHOD__ );
                }
+               $this->invalidateTitleProtectionCache();
+
                # Update the protection log
                if ( $dbw->affectedRows() ) {
                        $log = new LogPage( 'protect' );
@@ -1684,6 +1736,7 @@ class Title {
                        array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
                        __METHOD__
                );
+               $this->invalidateTitleProtectionCache();
        }
 
        /**
@@ -1899,21 +1952,12 @@ class Title {
 
        /**
         * Is this a *valid* .css or .js subpage of a user page?
-        * Check that the corresponding skin exists
         *
         * @return \type{\bool}
+        * @deprecated
         */
        public function isValidCssJsSubpage() {
-               if ( $this->isCssJsSubpage() ) {
-                       $name = $this->getSkinFromCssJsSubpage();
-                       if ( $name == 'common' ) {
-                               return true;
-                       }
-                       $skinNames = Skin::getSkinNames();
-                       return array_key_exists( $name, $skinNames );
-               } else {
-                       return false;
-               }
+               return $this->isCssJsSubpage();
        }
 
        /**
@@ -2066,6 +2110,7 @@ class Title {
                }
                if ( $purgeExpired ) {
                        Title::purgeExpiredRestrictions();
+                       $this->invalidateTitleProtectionCache();
                }
 
                wfProfileOut( __METHOD__ );
@@ -2102,9 +2147,8 @@ class Title {
         */
        private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) {
                $rows = array();
-               $dbr = wfGetDB( DB_SLAVE );
 
-               while ( $row = $dbr->fetchObject( $res ) ) {
+               foreach ( $res as $row ) {
                        $rows[] = $row;
                }
 
@@ -2186,6 +2230,7 @@ class Title {
 
                        if ( $purgeExpired ) {
                                Title::purgeExpiredRestrictions();
+                               $this->invalidateTitleProtectionCache();
                        }
                }
 
@@ -2220,6 +2265,7 @@ class Title {
                                                $this->mRestrictions['create'] = explode( ',', trim( $title_protection['pt_create_perm'] ) );
                                        } else { // Get rid of the old restrictions
                                                Title::purgeExpiredRestrictions();
+                                               $this->invalidateTitleProtectionCache();
                                        }
                                } else {
                                        $this->mRestrictionsExpiry['create'] = Block::decodeExpiry( '' );
@@ -2326,7 +2372,7 @@ class Title {
         * Get the article ID for this Title from the link cache,
         * adding it if necessary
         *
-        * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select
+        * @param $flags \type{\int} a bit field; may be Title::GAID_FOR_UPDATE to select
         *  for update
         * @return \type{\int} the ID
         */
@@ -2335,7 +2381,7 @@ class Title {
                        return $this->mArticleID = 0;
                }
                $linkCache = LinkCache::singleton();
-               if ( $flags & GAID_FOR_UPDATE ) {
+               if ( $flags & self::GAID_FOR_UPDATE ) {
                        $oldUpdate = $linkCache->forUpdate( true );
                        $linkCache->clearLink( $this );
                        $this->mArticleID = $linkCache->addLinkObj( $this );
@@ -2352,7 +2398,7 @@ class Title {
         * Is this an article that is a redirect page?
         * Uses link cache, adding it if necessary
         *
-        * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update
+        * @param $flags \type{\int} a bit field; may be Title::GAID_FOR_UPDATE to select for update
         * @return \type{\bool}
         */
        public function isRedirect( $flags = 0 ) {
@@ -2373,8 +2419,8 @@ class Title {
         * What is the length of this page?
         * Uses link cache, adding it if necessary
         *
-        * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update
-        * @return \type{\bool}
+        * @param $flags \type{\int} a bit field; may be Title::GAID_FOR_UPDATE to select for update
+        * @return \type{\int}
         */
        public function getLength( $flags = 0 ) {
                if ( $this->mLength != -1 ) {
@@ -2393,7 +2439,7 @@ class Title {
        /**
         * What is the page_latest field for this page?
         *
-        * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update
+        * @param $flags \type{\int} a bit field; may be Title::GAID_FOR_UPDATE to select for update
         * @return \type{\int} or 0 if the page doesn't exist
         */
        public function getLatestRevID( $flags = 0 ) {
@@ -2414,6 +2460,10 @@ class Title {
         * This clears some fields in this object, and clears any associated
         * keys in the "bad links" section of the link cache.
         *
+        * - This is called from Article::doEdit() and Article::insertOn() to allow
+        * loading of the new page_id. It's also called from
+        * Article::doDeleteArticle()
+        *
         * @param $newid \type{\int} the new Article ID
         */
        public function resetArticleID( $newid ) {
@@ -2600,7 +2650,9 @@ class Title {
                                        $this->mInterwiki = $wgContLang->lc( $p );
 
                                        # Redundant interwiki prefix to the local wiki
-                                       if ( 0 == strcasecmp( $this->mInterwiki, $wgLocalInterwiki ) ) {
+                                       if ( $wgLocalInterwiki !== false
+                                               && 0 == strcasecmp( $this->mInterwiki, $wgLocalInterwiki ) ) 
+                                       {
                                                if ( $dbkey == '' ) {
                                                        # Can't have an empty self-link
                                                        return false;
@@ -2743,7 +2795,7 @@ class Title {
        /**
         * Get a Title object associated with the talk page of this article
         *
-        * @return \type{Title} the object for the talk page
+        * @return Title the object for the talk page
         */
        public function getTalkPage() {
                return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
@@ -2753,7 +2805,7 @@ class Title {
         * Get a title object associated with the subject page of this
         * talk page
         *
-        * @return \type{Title} the object for the subject page
+        * @return Title the object for the subject page
         */
        public function getSubjectPage() {
                // Is this the same title?
@@ -2799,13 +2851,13 @@ class Title {
                $retVal = array();
                if ( $db->numRows( $res ) ) {
                        foreach ( $res as $row ) {
-                               if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) {
+                               $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title );
+                               if ( $titleObj ) {
                                        $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect, $row->page_latest );
                                        $retVal[] = $titleObj;
                                }
                        }
                }
-               $db->freeResult( $res );
                return $retVal;
        }
 
@@ -3027,6 +3079,8 @@ class Title {
         * @return \type{\mixed} true on success, getUserPermissionsErrors()-like array on failure
         */
        public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
+               global $wgContLang;
+
                $err = $this->isValidMoveOperation( $nt, $auth, $reason );
                if ( is_array( $err ) ) {
                        return $err;
@@ -3059,24 +3113,19 @@ class Title {
                }
                $redirid = $this->getArticleID();
 
-               // Category memberships include a sort key which may be customized.
-               // If it's left as the default (the page title), we need to update
-               // the sort key to match the new title.
-               //
-               // Be careful to avoid resetting cl_timestamp, which may disturb
-               // time-based lists on some sites.
-               //
-               // Warning -- if the sort key is *explicitly* set to the old title,
-               // we can't actually distinguish it from a default here, and it'll
-               // be set to the new title even though it really shouldn't.
-               // It'll get corrected on the next edit, but resetting cl_timestamp.
+               // 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(
+                       'categorylinks',
+                       'cl_sortkey_prefix',
+                       array( 'cl_from' => $pageid ),
+                       __METHOD__
+               );
                $dbw->update( 'categorylinks',
                        array(
-                               'cl_sortkey' => $nt->getPrefixedText(),
+                               'cl_sortkey' => $wgContLang->convertToSortkey( $nt->getCategorySortkey( $prefix ) ),
                                'cl_timestamp=cl_timestamp' ),
-                       array(
-                               'cl_from' => $pageid,
-                               'cl_sortkey' => $this->getPrefixedText() ),
+                       array( 'cl_from' => $pageid ),
                        __METHOD__ );
 
                if ( $protected ) {
@@ -3139,9 +3188,8 @@ class Title {
                        $u->doUpdate();
                }
                # Update message cache for interface messages
-               if ( $nt->getNamespace() == NS_MEDIAWIKI ) {
-                       global $wgMessageCache;
-
+               global $wgMessageCache;
+               if ( $this->getNamespace() == NS_MEDIAWIKI ) {
                        # @bug 17860: old article can be deleted, if this the case,
                        # delete it from message cache
                        if ( $this->getArticleID() === 0 ) {
@@ -3150,7 +3198,8 @@ class Title {
                                $oldarticle = new Article( $this );
                                $wgMessageCache->replace( $this->getDBkey(), $oldarticle->getContent() );
                        }
-
+               }
+               if ( $nt->getNamespace() == NS_MEDIAWIKI ) {
                        $newarticle = new Article( $nt );
                        $wgMessageCache->replace( $nt->getDBkey(), $newarticle->getContent() );
                }
@@ -3300,7 +3349,6 @@ class Title {
                # Truncate for whole multibyte characters. +5 bytes for ellipsis
                $comment = $wgContLang->truncate( $comment, 250 );
 
-               $newid = $nt->getArticleID();
                $oldid = $this->getArticleID();
                $latest = $this->getLatestRevId();
 
@@ -3487,7 +3535,6 @@ class Title {
         * @return \type{\bool} TRUE or FALSE
         */
        public function isValidMoveTarget( $nt ) {
-               $dbw = wfGetDB( DB_MASTER );
                # Is it an existing file?
                if ( $nt->getNamespace() == NS_FILE ) {
                        $file = wfLocalFile( $nt );
@@ -3553,14 +3600,13 @@ class Title {
                         . " ORDER BY cl_sortkey";
 
                $res = $dbr->query( $sql );
+               $data = array();
 
                if ( $dbr->numRows( $res ) > 0 ) {
-                       foreach ( $res as $row )
+                       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();
+                       }
                }
                return $data;
        }
@@ -3587,10 +3633,9 @@ class Title {
                                        }
                                }
                        }
-                       return $stack;
-               } else {
-                       return array();
                }
+
+               return $stack;
        }
 
 
@@ -3613,11 +3658,11 @@ class Title {
         * Get the revision ID of the previous revision
         *
         * @param $revId \type{\int} Revision ID. Get the revision that was before this one.
-        * @param $flags \type{\int} GAID_FOR_UPDATE
+        * @param $flags \type{\int} Title::GAID_FOR_UPDATE
         * @return \twotypes{\int,\bool} Old revision ID, or FALSE if none exists
         */
        public function getPreviousRevisionID( $revId, $flags = 0 ) {
-               $db = ( $flags & GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
+               $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                return $db->selectField( 'revision', 'rev_id',
                        array(
                                'rev_page' => $this->getArticleId( $flags ),
@@ -3632,11 +3677,11 @@ class Title {
         * Get the revision ID of the next revision
         *
         * @param $revId \type{\int} Revision ID. Get the revision that was after this one.
-        * @param $flags \type{\int} GAID_FOR_UPDATE
+        * @param $flags \type{\int} Title::GAID_FOR_UPDATE
         * @return \twotypes{\int,\bool} Next revision ID, or FALSE if none exists
         */
        public function getNextRevisionID( $revId, $flags = 0 ) {
-               $db = ( $flags & GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
+               $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                return $db->selectField( 'revision', 'rev_id',
                        array(
                                'rev_page' => $this->getArticleId( $flags ),
@@ -3650,11 +3695,11 @@ class Title {
        /**
         * Get the first revision of the page
         *
-        * @param $flags \type{\int} GAID_FOR_UPDATE
+        * @param $flags \type{\int} Title::GAID_FOR_UPDATE
         * @return Revision (or NULL if page doesn't exist)
         */
        public function getFirstRevision( $flags = 0 ) {
-               $db = ( $flags & GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
+               $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                $pageId = $this->getArticleId( $flags );
                if ( !$pageId ) {
                        return null;
@@ -3708,12 +3753,35 @@ class Title {
         */
        public function countRevisionsBetween( $old, $new ) {
                $dbr = wfGetDB( DB_SLAVE );
-               return (int)$dbr->selectField( 'revision', 'count(*)',
-                       'rev_page = ' . intval( $this->getArticleId() ) .
-                       ' AND rev_id > ' . intval( $old ) .
-                       ' AND rev_id < ' . intval( $new ),
-                       __METHOD__
+               return (int)$dbr->selectField( 'revision', 'count(*)', array(
+                               'rev_page' => intval( $this->getArticleId() ),
+                               'rev_id > ' . intval( $old ),
+                               'rev_id < ' . intval( $new )
+                       ), __METHOD__
+               );
+       }
+
+       /**
+        * Get the number of authors between the given revision IDs.
+        * Used for diffs and other things that really need it.
+        *
+        * @param $fromRevId \type{\int} Revision ID (first before range)
+        * @param $toRevId \type{\int} Revision ID (first after range)
+        * @param $limit \type{\int} Maximum number of authors
+        * @param $flags \type{\int} Title::GAID_FOR_UPDATE
+        * @return \type{\int}
+        */
+       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',
+                       array(
+                               'rev_page' => $this->getArticleID(),
+                               'rev_id > ' . (int)$fromRevId,
+                               'rev_id < ' . (int)$toRevId
+                       ), __METHOD__,
+                       array( 'LIMIT' => $limit )
                );
+               return (int)$db->numRows( $res );
        }
 
        /**
@@ -3785,21 +3853,21 @@ class Title {
                        return true;  // any interwiki link might be viewable, for all we know
                }
                switch( $this->mNamespace ) {
-               case NS_MEDIA:
-               case NS_FILE:
-                       return (bool)wfFindFile( $this );  // file exists, possibly in a foreign repo
-               case NS_SPECIAL:
-                       return SpecialPage::exists( $this->getDBkey() );  // valid special page
-               case NS_MAIN:
-                       return $this->mDbkeyform == '';  // selflink, possibly with fragment
-               case NS_MEDIAWIKI:
-                       // If the page is form Mediawiki:message/lang, calling wfMsgWeirdKey causes
-                       // the full l10n of that language to be loaded. That takes much memory and
-                       // isn't needed. So we strip the language part away.
-                       list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 );
-                       return (bool)wfMsgWeirdKey( $basename );  // known system message
-               default:
-                       return false;
+                       case NS_MEDIA:
+                       case NS_FILE:
+                               return (bool)wfFindFile( $this );  // file exists, possibly in a foreign repo
+                       case NS_SPECIAL:
+                               return SpecialPage::exists( $this->getDBkey() );  // valid special page
+                       case NS_MAIN:
+                               return $this->mDbkeyform == '';  // selflink, possibly with fragment
+                       case NS_MEDIAWIKI:
+                               // If the page is form Mediawiki:message/lang, calling wfMsgWeirdKey causes
+                               // the full l10n of that language to be loaded. That takes much memory and
+                               // isn't needed. So we strip the language part away.
+                               list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 );
+                               return (bool)wfMsgWeirdKey( $basename );  // known system message
+                       default:
+                               return false;
                }
        }
 
@@ -3812,7 +3880,7 @@ class Title {
         * @return \type{\bool}
         */
        public function isKnown() {
-               return $this->exists() || $this->isAlwaysKnown();
+               return $this->isAlwaysKnown() || $this->exists();
        }
 
        /**
@@ -4031,7 +4099,7 @@ class Title {
         * In other words, is this a content page, for the purposes of calculating
         * statistics, etc?
         *
-        * @return \type{\bool}
+        * @return Boolean
         */
        public function isContentPage() {
                return MWNamespace::isContent( $this->getNamespace() );
@@ -4064,7 +4132,6 @@ class Title {
                        __METHOD__
                );
 
-
                foreach ( $res as $row ) {
                        $redirs[] = self::newFromRow( $row );
                }
@@ -4139,4 +4206,25 @@ class Title {
 
                return $types;
        }
+
+       /**
+        * Returns the raw sort key to be used for categories, with the specified
+        * prefix.  This will be fed to Language::convertToSortkey() to get a
+        * binary sortkey that can be used for actual sorting.
+        *
+        * @param $prefix string The prefix to be used, specified using
+        *   {{defaultsort:}} or like [[Category:Foo|prefix]].  Empty for no
+        *   prefix.
+        * @return string
+        */
+       public function getCategorySortkey( $prefix = '' ) {
+               $unprefixed = $this->getText();
+               if ( $prefix !== '' ) {
+                       # Separate with a null byte, so the unprefixed part is only used as
+                       # a tiebreaker when two pages have the exact same prefix -- null
+                       # sorts before everything else (hopefully).
+                       return "$prefix\0$unprefixed";
+               }
+               return $unprefixed;
+       }
 }