merging latest master
[lhc/web/wiklou.git] / includes / Title.php
index 046fadb..08b8f0a 100644 (file)
@@ -65,6 +65,7 @@ class Title {
        var $mFragment;                   // /< Title fragment (i.e. the bit after the #)
        var $mArticleID = -1;             // /< Article ID, fetched from the link cache on demand
        var $mLatestID = false;           // /< ID of most recent revision
+       var $mContentModel = false;       // /< ID of the page's content model, i.e. one of the CONTENT_MODEL_XXX constants
        private $mEstimateRevisions;      // /< Estimated number of revisions; null of not loaded
        var $mRestrictions = array();     // /< Array of groups allowed to edit this article
        var $mOldRestrictions = false;
@@ -200,6 +201,27 @@ class Title {
                }
        }
 
+       /**
+        * Returns a list of fields that are to be selected for initializing Title objects or LinkCache entries.
+        * Uses $wgContentHandlerUseDB to determine whether to include page_content_model.
+        *
+        * @return array
+        */
+       protected static function getSelectFields() {
+               global $wgContentHandlerUseDB;
+
+               $fields = array(
+                       'page_namespace', 'page_title', 'page_id',
+                       'page_len', 'page_is_redirect', 'page_latest',
+               );
+
+               if ( $wgContentHandlerUseDB ) {
+                       $fields[] = 'page_content_model';
+               }
+
+               return $fields;
+       }
+
        /**
         * Create a new Title from an article ID
         *
@@ -211,10 +233,7 @@ class Title {
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                $row = $db->selectRow(
                        'page',
-                       array(
-                               'page_namespace', 'page_title', 'page_id',
-                               'page_len', 'page_is_redirect', 'page_latest',
-                       ),
+                       self::getSelectFields(),
                        array( 'page_id' => $id ),
                        __METHOD__
                );
@@ -240,10 +259,7 @@ class Title {
 
                $res = $dbr->select(
                        'page',
-                       array(
-                               'page_namespace', 'page_title', 'page_id',
-                               'page_len', 'page_is_redirect', 'page_latest',
-                       ),
+                       self::getSelectFields(),
                        array( 'page_id' => $ids ),
                        __METHOD__
                );
@@ -283,11 +299,16 @@ class Title {
                                $this->mRedirect = (bool)$row->page_is_redirect;
                        if ( isset( $row->page_latest ) )
                                $this->mLatestID = (int)$row->page_latest;
+                       if ( isset( $row->page_content_model ) )
+                               $this->mContentModel = strval( $row->page_content_model );
+                       else
+                               $this->mContentModel = false; # initialized lazily in getContentModel()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
+                       $this->mContentModel = false; # initialized lazily in getContentModel()
                }
        }
 
@@ -313,6 +334,7 @@ class Title {
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = str_replace( '_', ' ', $title );
+               $t->mContentModel = false; # initialized lazily in getContentModel()
                return $t;
        }
 
@@ -363,9 +385,11 @@ class Title {
         *
         * @param $text String: Text with possible redirect
         * @return Title: The corresponding Title
+        * @deprecated since 1.WD, use Content::getRedirectTarget instead.
         */
        public static function newFromRedirect( $text ) {
-               return self::newFromRedirectInternal( $text );
+               $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+               return $content->getRedirectTarget();
        }
 
        /**
@@ -376,10 +400,11 @@ class Title {
         *
         * @param $text String Text with possible redirect
         * @return Title
+        * @deprecated since 1.WD, use Content::getUltimateRedirectTarget instead.
         */
        public static function newFromRedirectRecurse( $text ) {
-               $titles = self::newFromRedirectArray( $text );
-               return $titles ? array_pop( $titles ) : null;
+               $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+               return $content->getUltimateRedirectTarget();
        }
 
        /**
@@ -390,71 +415,11 @@ class Title {
         *
         * @param $text String Text with possible redirect
         * @return Array of Titles, with the destination last
+        * @deprecated since 1.WD, use Content::getRedirectChain instead.
         */
        public static function newFromRedirectArray( $text ) {
-               global $wgMaxRedirects;
-               $title = self::newFromRedirectInternal( $text );
-               if ( is_null( $title ) ) {
-                       return null;
-               }
-               // recursive check to follow double redirects
-               $recurse = $wgMaxRedirects;
-               $titles = array( $title );
-               while ( --$recurse > 0 ) {
-                       if ( $title->isRedirect() ) {
-                               $page = WikiPage::factory( $title );
-                               $newtitle = $page->getRedirectTarget();
-                       } else {
-                               break;
-                       }
-                       // Redirects to some special pages are not permitted
-                       if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
-                               // the new title passes the checks, so make that our current title so that further recursion can be checked
-                               $title = $newtitle;
-                               $titles[] = $newtitle;
-                       } else {
-                               break;
-                       }
-               }
-               return $titles;
-       }
-
-       /**
-        * Really extract the redirect destination
-        * Do not call this function directly, use one of the newFromRedirect* functions above
-        *
-        * @param $text String Text with possible redirect
-        * @return Title
-        */
-       protected static function newFromRedirectInternal( $text ) {
-               global $wgMaxRedirects;
-               if ( $wgMaxRedirects < 1 ) {
-                       //redirects are disabled, so quit early
-                       return null;
-               }
-               $redir = MagicWord::get( 'redirect' );
-               $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( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
-                               // Strip preceding colon used to "escape" categories, etc.
-                               // and URL-decode links
-                               if ( strpos( $m[1], '%' ) !== false ) {
-                                       // Match behavior of inline link parsing here;
-                                       $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
-                               if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
-                                       return null;
-                               }
-                               return $title;
-                       }
-               }
-               return null;
+               $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+               return $content->getRedirectChain();
        }
 
        /**
@@ -702,6 +667,38 @@ class Title {
                return $this->mNamespace;
        }
 
+       /**
+        * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
+        *
+        * @return String: Content model id
+        */
+       public function getContentModel() {
+               if ( !$this->mContentModel ) {
+                       $linkCache = LinkCache::singleton();
+                       $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
+               }
+
+               if ( !$this->mContentModel ) {
+                       $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
+               }
+
+               if( !$this->mContentModel ) {
+                       throw new MWException( "failed to determin content model!" );
+               }
+
+               return $this->mContentModel;
+       }
+
+       /**
+        * Convenience method for checking a title's content model name
+        *
+        * @param int $id
+        * @return Boolean true if $this->getContentModel() == $id
+        */
+       public function hasContentModel( $id ) {
+               return $this->getContentModel() == $id;
+       }
+
        /**
         * Get the namespace text
         *
@@ -825,7 +822,7 @@ class Title {
 
        /**
         * Returns true if the title is inside the specified namespace.
-        * 
+        *
         * Please make use of this instead of comparing to getNamespace()
         * This function is much more resistant to changes we may make
         * to namespaces than code that makes direct comparisons.
@@ -945,22 +942,31 @@ class Title {
         * @return Bool
         */
        public function isWikitextPage() {
-               $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
-               wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
-               return $retval;
+               return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
        }
 
        /**
-        * Could this page contain custom CSS or JavaScript, based
-        * on the title?
+        * Could this page contain custom CSS or JavaScript for the global UI.
+        * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
+        * or CONTENT_MODEL_JAVASCRIPT.
+        *
+        * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage() for that!
+        *
+        * Note that this method should not return true for pages that contain and show "inactive" CSS or JS.
         *
         * @return Bool
         */
        public function isCssOrJsPage() {
-               $retval = $this->mNamespace == NS_MEDIAWIKI
-                       && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
-               wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
-               return $retval;
+               $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
+                       && ( $this->hasContentModel( CONTENT_MODEL_CSS )
+                               || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
+
+               #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure
+               #      hook funktions can force this method to return true even outside the mediawiki namespace.
+
+               wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) );
+
+               return $isCssOrJsPage;
        }
 
        /**
@@ -968,7 +974,9 @@ class Title {
         * @return Bool
         */
        public function isCssJsSubpage() {
-               return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
+               return ( NS_USER == $this->mNamespace && $this->isSubpage()
+                               && ( $this->hasContentModel( CONTENT_MODEL_CSS )
+                                       || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
        }
 
        /**
@@ -991,7 +999,8 @@ class Title {
         * @return Bool
         */
        public function isCssSubpage() {
-               return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
+               return ( NS_USER == $this->mNamespace && $this->isSubpage()
+                       && $this->hasContentModel( CONTENT_MODEL_CSS ) );
        }
 
        /**
@@ -1000,7 +1009,8 @@ class Title {
         * @return Bool
         */
        public function isJsSubpage() {
-               return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
+               return ( NS_USER == $this->mNamespace && $this->isSubpage()
+                       && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
        }
 
        /**
@@ -1270,9 +1280,11 @@ class Title {
         * See getLocalURL for the arguments.
         *
         * @see self::getLocalURL
+        * @see wfExpandUrl
+        * @param $proto Protocol type to use in URL
         * @return String the URL
         */
-       public function getFullURL( $query = '', $query2 = false ) {
+       public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
                $query = self::fixUrlQueryArgs( $query, $query2 );
 
                # Hand off all the decisions on urls to getLocalURL
@@ -1281,7 +1293,7 @@ class Title {
                # Expand the url to make it a full url. Note that getLocalURL has the
                # potential to output full urls for a variety of reasons, so we use
                # wfExpandUrl instead of simply prepending $wgServer
-               $url = wfExpandUrl( $url, PROTO_RELATIVE );
+               $url = wfExpandUrl( $url, $proto );
 
                # Finally, add the fragment.
                $url .= $this->getFragmentForURL();
@@ -1516,6 +1528,7 @@ class Title {
        /**
         * Is $wgUser watching this page?
         *
+        * @deprecated in 1.20; use User::isWatched() instead.
         * @return Bool
         */
        public function userIsWatching() {
@@ -1881,7 +1894,7 @@ class Title {
                                        $title_protection['pt_create_perm'] = 'protect'; // B/C
                                }
                                if( $title_protection['pt_create_perm'] == '' ||
-                                       !$user->isAllowed( $title_protection['pt_create_perm'] ) ) 
+                                       !$user->isAllowed( $title_protection['pt_create_perm'] ) )
                                {
                                        $errors[] = array( 'titleprotected', User::whoIs( $title_protection['pt_user'] ), $title_protection['pt_reason'] );
                                }
@@ -2824,8 +2837,16 @@ class Title {
                if ( !$this->getArticleID( $flags ) ) {
                        return $this->mRedirect = false;
                }
+
                $linkCache = LinkCache::singleton();
-               $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
+               $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
+               if ( $cached === null ) { # check the assumption that the cache actually knows about this title
+                       # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
+                       #      as a stop gap, perhaps log this, but don't throw an exception?
+                       throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
+               }
+
+               $this->mRedirect = (bool)$cached;
 
                return $this->mRedirect;
        }
@@ -2846,7 +2867,14 @@ class Title {
                        return $this->mLength = 0;
                }
                $linkCache = LinkCache::singleton();
-               $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) );
+               $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
+               if ( $cached === null ) { # check the assumption that the cache actually knows about this title
+                       # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
+                       #      as a stop gap, perhaps log this, but don't throw an exception?
+                       throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
+               }
+
+               $this->mLength = intval( $cached );
 
                return $this->mLength;
        }
@@ -2858,7 +2886,7 @@ class Title {
         * @return Int or 0 if the page doesn't exist
         */
        public function getLatestRevID( $flags = 0 ) {
-               if ( $this->mLatestID !== false ) {
+               if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
                        return intval( $this->mLatestID );
                }
                # Calling getArticleID() loads the field from cache as needed
@@ -2866,7 +2894,14 @@ class Title {
                        return $this->mLatestID = 0;
                }
                $linkCache = LinkCache::singleton();
-               $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) );
+               $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
+               if ( $cached === null ) { # check the assumption that the cache actually knows about this title
+                       # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
+                       #      as a stop gap, perhaps log this, but don't throw an exception?
+                       throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
+               }
+
+               $this->mLatestID = intval( $cached );
 
                return $this->mLatestID;
        }
@@ -2877,7 +2912,7 @@ class Title {
         *
         * - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow
         * loading of the new page_id. It's also called from
-        * WikiPage::doDeleteArticle()
+        * WikiPage::doDeleteArticleReal()
         *
         * @param $newid Int the new Article ID
         */
@@ -2895,6 +2930,7 @@ class Title {
                $this->mRedirect = null;
                $this->mLength = -1;
                $this->mLatestID = false;
+               $this->mContentModel = false;
                $this->mEstimateRevisions = null;
        }
 
@@ -3133,7 +3169,7 @@ class Title {
 
                $res = $db->select(
                        array( 'page', $table ),
-                       array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+                       self::getSelectFields(),
                        array(
                                "{$prefix}_from=page_id",
                                "{$prefix}_namespace" => $this->getNamespace(),
@@ -3183,6 +3219,8 @@ class Title {
         * @return Array of Title objects linking here
         */
        public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
+               global $wgContentHandlerUseDB;
+
                $id = $this->getArticleID();
 
                # If the page doesn't exist; there can't be any link from this page
@@ -3199,9 +3237,12 @@ class Title {
                $namespaceFiled = "{$prefix}_namespace";
                $titleField = "{$prefix}_title";
 
+               $fields = array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' );
+               if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
+
                $res = $db->select(
                        array( $table, 'page' ),
-                       array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+                       $fields,
                        array( "{$prefix}_from" => $id ),
                        __METHOD__,
                        $options,
@@ -3284,16 +3325,14 @@ class Title {
         * @return Array of String the URLs
         */
        public function getSquidURLs() {
-               global $wgContLang;
-
                $urls = array(
                        $this->getInternalURL(),
                        $this->getInternalURL( 'action=history' )
                );
 
-               // purge variant urls as well
-               if ( $wgContLang->hasVariants() ) {
-                       $variants = $wgContLang->getVariants();
+               $pageLang = $this->getPageLanguage();
+               if ( $pageLang->hasVariants() ) {
+                       $variants = $pageLang->getVariants();
                        foreach ( $variants as $vCode ) {
                                $urls[] = $this->getInternalURL( '', $vCode );
                        }
@@ -3475,6 +3514,10 @@ class Title {
                        $wgUser->spreadAnyEditBlock();
                        return $err;
                }
+               // Check suppressredirect permission
+               if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
+                       $createRedirect = true;
+               }
 
                // If it is a file, move it first.
                // It is done before all other moving stuff is done because it's hard to revert.
@@ -3571,8 +3614,8 @@ class Title {
         *
         * @param $nt Title the page to move to, which should be a redirect or nonexistent
         * @param $reason String The reason for the move
-        * @param $createRedirect Bool Whether to leave a redirect at the old title.  Ignored
-        *   if the user doesn't have the suppressredirect right
+        * @param $createRedirect Bool Whether to leave a redirect at the old title. Does not check
+        *   if the user has the suppressredirect right
         * @throws MWException
         */
        private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) {
@@ -3586,7 +3629,7 @@ class Title {
                        $logType = 'move';
                }
 
-               $redirectSuppressed = !$createRedirect && $wgUser->isAllowed( 'suppressredirect' );
+               $redirectSuppressed = !$createRedirect;
 
                $logEntry = new ManualLogEntry( 'move', $logType );
                $logEntry->setPerformer( $wgUser );
@@ -3763,10 +3806,16 @@ class Title {
         * @return Bool
         */
        public function isSingleRevRedirect() {
+               global $wgContentHandlerUseDB;
+
                $dbw = wfGetDB( DB_MASTER );
+
                # Is it a redirect?
+               $fields = array( 'page_is_redirect', 'page_latest', 'page_id' );
+               if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
+
                $row = $dbw->selectRow( 'page',
-                       array( 'page_is_redirect', 'page_latest', 'page_id' ),
+                       $fields,
                        $this->pageCond(),
                        __METHOD__,
                        array( 'FOR UPDATE' )
@@ -3775,6 +3824,7 @@ class Title {
                $this->mArticleID = $row ? intval( $row->page_id ) : 0;
                $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
                $this->mLatestID = $row ? intval( $row->page_latest ) : false;
+               $this->mContentModel = $row && isset( $row->page_content_model ) ? strval( $row->page_content_model ) : false;
                if ( !$this->mRedirect ) {
                        return false;
                }
@@ -3819,24 +3869,24 @@ class Title {
                if( !is_object( $rev ) ){
                        return false;
                }
-               $text = $rev->getText();
+               $content = $rev->getContent();
                # Does the redirect point to the source?
                # Or is it a broken self-redirect, usually caused by namespace collisions?
-               $m = array();
-               if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) {
-                       $redirTitle = Title::newFromText( $m[1] );
-                       if ( !is_object( $redirTitle ) ||
-                               ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
-                               $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) {
+               $redirTitle = $content->getRedirectTarget();
+
+               if ( $redirTitle ) {
+                       if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
+                               $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
                                wfDebug( __METHOD__ . ": redirect points to other page\n" );
                                return false;
+                       } else {
+                               return true;
                        }
                } else {
-                       # Fail safe
-                       wfDebug( __METHOD__ . ": failsafe\n" );
+                       # Fail safe (not a redirect after all. strange.)
+                       wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() . " is a redirect, but it doesn't contain a valid redirect.\n" );
                        return false;
                }
-               return true;
        }
 
        /**
@@ -4076,30 +4126,60 @@ class Title {
        }
 
        /**
-        * Get the number of authors between the given revision IDs.
+        * Get the number of authors between the given revisions or revision IDs.
         * Used for diffs and other things that really need it.
         *
-        * @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
-        * @return Int Number of revision authors between these revisions.
-        */
-       public function countAuthorsBetween( $old, $new, $limit ) {
+        * @param $old int|Revision Old revision or rev ID (first before range by default)
+        * @param $new int|Revision New revision or rev ID (first after range by default)
+        * @param $limit int Maximum number of authors
+        * @param $options string|array (Optional): Single option, or an array of options:
+        *     'include_old' Include $old in the range; $new is excluded.
+        *     'include_new' Include $new in the range; $old is excluded.
+        *     'include_both' Include both $old and $new in the range.
+        *     Unknown option values are ignored.
+        * @return int Number of revision authors in the range; zero if not both revisions exist
+        */
+       public function countAuthorsBetween( $old, $new, $limit, $options = array() ) {
                if ( !( $old instanceof Revision ) ) {
                        $old = Revision::newFromTitle( $this, (int)$old );
                }
                if ( !( $new instanceof Revision ) ) {
                        $new = Revision::newFromTitle( $this, (int)$new );
                }
+               // XXX: what if Revision objects are passed in, but they don't refer to this title?
+               // Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID()
+               // in the sanity check below?
                if ( !$old || !$new ) {
                        return 0; // nothing to compare
                }
+               $old_cmp = '>';
+               $new_cmp = '<';
+               $options = (array) $options;
+               if ( in_array( 'include_old', $options ) ) {
+                       $old_cmp = '>=';
+               }
+               if ( in_array( 'include_new', $options ) ) {\r
+                       $new_cmp = '<=';\r
+               }\r
+               if ( in_array( 'include_both', $options ) ) {\r
+                       $old_cmp = '>=';\r
+                       $new_cmp = '<=';
+               }
+               // No DB query needed if $old and $new are the same or successive revisions:
+               if ( $old->getId() === $new->getId() ) {
+                       return ( $old_cmp === '>' && $new_cmp === '<' ) ? 0 : 1;
+               } else if ( $old->getId() === $new->getParentId() ) {
+                       if ( $old_cmp === '>' || $new_cmp === '<' ) {
+                               return ( $old_cmp === '>' && $new_cmp === '<' ) ? 0 : 1;
+                       }
+                       return ( $old->getRawUserText() === $new->getRawUserText() ) ? 1 : 2;
+               }\r
                $dbr = wfGetDB( DB_SLAVE );
                $res = $dbr->select( 'revision', 'DISTINCT rev_user_text',
                        array(
                                'rev_page' => $this->getArticleID(),
-                               'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
-                               'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
+                               "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
+                               "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
                        ), __METHOD__,
                        array( 'LIMIT' => $limit + 1 ) // add one so caller knows it was truncated
                );
@@ -4503,17 +4583,17 @@ class Title {
                if ( $this->isSpecialPage() ) {
                        // special pages are in the user language
                        return $wgLang;
-               } elseif ( $this->isCssOrJsPage() || $this->isCssJsSubpage() ) {
-                       // 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;
+
+               //TODO: use the LinkCache to cache this!
+               //NOTE: ContentHandler::getPageLanguage() may need to load the content to determine the page language!
+               $contentHandler = ContentHandler::getForTitle( $this );
+               $pageLang = $contentHandler->getPageLanguage( $this );
+
                // Hook at the end because we don't want to override the above stuff
                wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) );
                return wfGetLangObj( $pageLang );