* (bug 13040) Gender-aware user namespace aliases
[lhc/web/wiklou.git] / includes / Title.php
index 0e493cf..649f764 100644 (file)
@@ -68,7 +68,7 @@ class Title {
        var $mWatched = null;             ///< Is $wgUser watching this page? null if unfilled, accessed through userIsWatching()
        var $mLength = -1;                ///< The page length, 0 for special pages
        var $mRedirect = null;            ///< Is the article at this title a redirect?
-       var $mNotificationTimestamp = array(); ///< Associative array of ID -> timestamp/NULL
+       var $mNotificationTimestamp = array(); ///< Associative array of user ID -> timestamp/false
        //@}
 
 
@@ -294,11 +294,77 @@ class Title {
        /**
         * Extract a redirect destination from a string and return the
         * Title, or null if the text doesn't contain a valid redirect
+        * 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
+        * @param $text \type{\string} Text with possible redirect
         * @return \type{Title} The corresponding Title
         */
        public static function newFromRedirect( $text ) {
+               return self::newFromRedirectInternal( $text );
+       }
+       
+       /**
+        * Extract a redirect destination from a string and return the
+        * Title, or null if the text doesn't contain a valid redirect
+        * This will recurse down $wgMaxRedirects times or until a non-redirect target is hit
+        * in order to provide (hopefully) the Title of the final destination instead of another redirect
+        *
+        * @param $text \type{\string} Text with possible redirect
+        * @return \type{Title} The corresponding Title
+        */
+       public static function newFromRedirectRecurse( $text ) {
+               $titles = self::newFromRedirectArray( $text );
+               return $titles ? array_pop( $titles ) : null;
+       }
+       
+       /**
+        * Extract a redirect destination from a string and return an
+        * array of Titles, or null if the text doesn't contain a valid redirect
+        * The last element in the array is the final destination after all redirects
+        * have been resolved (up to $wgMaxRedirects times)
+        *
+        * @param $text \type{\string} Text with possible redirect
+        * @return \type{\array} Array of Titles, with the destination last
+        */
+       public static function newFromRedirectArray( $text ) {
+               global $wgMaxRedirects;
+               // are redirects disabled?
+               if( $wgMaxRedirects < 1 )
+                       return null;
+               $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() ) {
+                               $article = new Article( $title, 0 );
+                               $newtitle = $article->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 \type{\string} Text with possible redirect
+        * @return \type{Title} The corresponding Title
+        */
+       protected static function newFromRedirectInternal( $text ) {
                $redir = MagicWord::get( 'redirect' );
                $text = trim($text);
                if( $redir->matchStartAndRemove( $text ) ) {
@@ -316,9 +382,11 @@ class Title {
                                        $m[1] = urldecode( ltrim( $m[1], ':' ) );
                                }
                                $title = Title::newFromText( $m[1] );
-                               // Redirects to Special:Userlogout are not permitted
-                               if( $title instanceof Title && !$title->isSpecial( 'Userlogout' ) )
-                                       return $title;
+                               // 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;
@@ -337,7 +405,10 @@ class Title {
        public static function nameOf( $id ) {
                $dbr = wfGetDB( DB_SLAVE );
 
-               $s = $dbr->selectRow( 'page', array( 'page_namespace','page_title' ),  array( 'page_id' => $id ), __METHOD__ );
+               $s = $dbr->selectRow( 'page',
+                       array( 'page_namespace','page_title' ),
+                       array( 'page_id' => $id ), 
+                       __METHOD__ );
                if ( $s === false ) { return NULL; }
 
                $n = self::makeName( $s->page_namespace, $s->page_title );
@@ -376,7 +447,7 @@ class Title {
 
                $t = preg_replace( "/\\s+/", ' ', $t );
 
-               if ( $ns == NS_IMAGE ) {
+               if ( $ns == NS_FILE ) {
                        $t = preg_replace( "/ (png|gif|jpg|jpeg|ogg)$/", "", $t );
                }
                return trim( $t );
@@ -444,13 +515,13 @@ class Title {
         * Escape a text fragment, say from a link, for a URL
         */
        static function escapeFragmentForURL( $fragment ) {
-               $fragment = str_replace( ' ', '_', $fragment );
-               $fragment = urlencode( Sanitizer::decodeCharReferences( $fragment ) );
-               $replaceArray = array(
-                       '%3A' => ':',
-                       '%' => '.'
-               );
-               return strtr( $fragment, $replaceArray );
+               global $wgEnforceHtmlIds;
+               # Note that we don't urlencode the fragment.  urlencoded Unicode
+               # fragments appear not to work in IE (at least up to 7) or in at least
+               # one version of Opera 9.x.  The W3C validator, for one, doesn't seem
+               # to care if they aren't encoded.
+               return Sanitizer::escapeId( $fragment,
+                       $wgEnforceHtmlIds ? 'noninitial' : 'xml' );
        }
 
 #----------------------------------------------------------------------------
@@ -483,7 +554,7 @@ class Title {
         * @return \type{\string} Namespace text
         */
        public function getNsText() {
-               global $wgContLang, $wgCanonicalNamespaceNames;
+               global $wgCanonicalNamespaceNames;
 
                if ( '' != $this->mInterwiki ) {
                        // This probably shouldn't even happen. ohh man, oh yuck.
@@ -496,6 +567,35 @@ class Title {
                                return $wgCanonicalNamespaceNames[$this->mNamespace];
                        }
                }
+
+               return $this->getNsTextInternal( $this->mNamespace );
+       }
+
+       function getNsTextInternal( $namespace) {
+               global $wgContLang, $wgSlowGenderAliases, $wgTitle, $title;
+               if( $namespace === NS_USER || $namespace === NS_USER_TALK ) {
+                       static $gender = null;
+
+                       $name = $this->getBaseText();
+                       if( !isset($gender[$name] ) ) {
+                               $gender[$name] = User::getDefaultOption( 'gender' );
+
+                               // wgTitle may not be defined
+                               $mytitle = isset($wgTitle) ? $wgTitle: Title::newFromText($title);
+
+                               // Check stuff
+                               if ( $wgSlowGenderAliases ||
+                                    // Needs to be checked always to produce desired
+                                    // effect when viewing user pages
+                                    ($mytitle && $name === $mytitle->getBaseText()) ) {
+
+                                       $user = User::newFromName( $name );
+                                       if ( $user ) $gender[$name] = $user->getOption( 'gender' );
+                               }
+                       }
+
+                       return $wgContLang->getGenderNsText( $this->mNamespace, $gender[$name] );
+               }
                return $wgContLang->getNsText( $this->mNamespace );
        }
        /**
@@ -510,16 +610,14 @@ class Title {
         * @return \type{\string} Namespace text
         */
        public function getSubjectNsText() {
-               global $wgContLang;
-               return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) );
+               return $this->getNsTextInternal( MWNamespace::getSubject( $this->mNamespace ) );
        }
        /**
         * Get the namespace text of the talk page
         * @return \type{\string} Namespace text
         */
        public function getTalkNsText() {
-               global $wgContLang;
-               return( $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ) );
+               return $this->getNsTextInternal( MWNamespace::getTalk( $this->mNamespace ) );
        }
        /**
         * Could this title have a corresponding talk page?
@@ -638,7 +736,6 @@ class Title {
        public function getSubpageUrlForm() {
                $text = $this->getSubpageText();
                $text = wfUrlencode( str_replace( ' ', '_', $text ) );
-               $text = str_replace( '%28', '(', str_replace( '%29', ')', $text ) ); # Clean up the URL; per below, this might not be safe
                return( $text );
        }
 
@@ -648,14 +745,7 @@ class Title {
         */
        public function getPrefixedURL() {
                $s = $this->prefix( $this->mDbkeyform );
-               $s = str_replace( ' ', '_', $s );
-
-               $s = wfUrlencode ( $s ) ;
-
-               # Cleaning up URL to make it look nice -- is this safe?
-               $s = str_replace( '%28', '(', $s );
-               $s = str_replace( '%29', ')', $s );
-
+               $s = wfUrlencode( str_replace( ' ', '_', $s ) );
                return $s;
        }
 
@@ -765,7 +855,9 @@ class Title {
                                                $query = $matches[1];
                                                if( isset( $matches[4] ) ) $query .= $matches[4];
                                                $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
-                                               if( $query != '' ) $url .= '?' . $query;
+                                               if( $query != '' ) {
+                                                       $url = wfAppendQuery( $url, $query );
+                                               }
                                        }
                                }
                                if ( $url === false ) {
@@ -991,7 +1083,7 @@ class Title {
         */
        public function userCan( $action, $doExpensiveQueries = true ) {
                global $wgUser;
-               return ( $this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries ) === array());
+               return ($this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries, true ) === array());
        }
 
        /**
@@ -1023,7 +1115,7 @@ class Title {
                }
 
                // Edit blocks should not affect reading. Account creation blocks handled at userlogin.
-               if ( $user->isBlockedFrom( $this ) && $action != 'read' && $action != 'createaccount' ) {
+               if ( $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this ) ) {
                        $block = $user->mBlock;
 
                        // This is from OutputPage::blockedPage
@@ -1093,69 +1185,156 @@ class Title {
         * @param $action \type{\string} action that permission needs to be checked for
         * @param $user \type{User} user to check
         * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
+        * @param $short \type{\bool} Set this to true to stop after the first permission error.
         * @return \type{\array} Array of arrays of the arguments to wfMsg to explain permissions problems.
         */
-       private function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true ) {
+       private function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries=true, $short=false ) {
                wfProfileIn( __METHOD__ );
 
                $errors = array();
 
+               // First stop is permissions checks, which fail most often, and which are easiest to test.
+               if ( $action == 'move' ) {
+                       if( !$user->isAllowed( 'move-rootuserpages' )
+                                       && $this->getNamespace() == NS_USER && !$this->isSubpage() )
+                       {
+                               // Show user page-specific message only if the user can move other pages
+                               $errors[] = array( 'cant-move-user-page' );
+                       }
+                       
+                       // Check if user is allowed to move files if it's a file
+                       if( $this->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
+                               $errors[] = array( 'movenotallowedfile' );
+                       }
+                       
+                       if( !$user->isAllowed( 'move' ) ) {
+                               // User can't move anything
+                               $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed');
+                       }
+               } elseif ( $action == 'create' ) {
+                       if( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
+                               ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) )
+                       {
+                               $errors[] = $user->isAnon() ? array ('nocreatetext') : array ('nocreate-loggedin');
+                       }
+               } elseif( $action == 'move-target' ) {
+                       if( !$user->isAllowed( 'move' ) ) {
+                               // User can't move anything
+                               $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed');
+                       } elseif( !$user->isAllowed( 'move-rootuserpages' )
+                               && $this->getNamespace() == NS_USER && !$this->isSubpage() )
+                       {
+                               // Show user page-specific message only if the user can move other pages
+                               $errors[] = array( 'cant-move-to-user-page' );
+                       }
+               } elseif( !$user->isAllowed( $action ) ) {
+                       $return = null;
+                       $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
+                               User::getGroupsWithPermission( $action ) );
+                       if( $groups ) {
+                               $return = array( 'badaccess-groups',
+                                       array( implode( ', ', $groups ), count( $groups ) ) );
+                       } else {
+                               $return = array( "badaccess-group0" );
+                       }
+                       $errors[] = $return;
+               }
+
+               # Short-circuit point
+               if( $short && count($errors) > 0 ) {
+                       wfProfileOut( __METHOD__ );
+                       return $errors;
+               }
+
                // Use getUserPermissionsErrors instead
-               if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) {
+               if( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) {
                        wfProfileOut( __METHOD__ );
                        return $result ? array() : array( array( 'badaccess-group0' ) );
                }
-
-               if (!wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) {
-                       if ($result != array() && is_array($result) && !is_array($result[0]))
+               // Check getUserPermissionsErrors hook
+               if( !wfRunHooks( 'getUserPermissionsErrors', array(&$this,&$user,$action,&$result) ) ) {
+                       if( is_array($result) && count($result) && !is_array($result[0]) )
                                $errors[] = $result; # A single array representing an error
-                       else if (is_array($result) && is_array($result[0]))
+                       else if( is_array($result) && is_array($result[0]) )
                                $errors = array_merge( $errors, $result ); # A nested array representing multiple errors
-                       else if ($result != '' && $result != null && $result !== true && $result !== false)
+                       else if( $result !== '' && is_string($result) )
                                $errors[] = array($result); # A string representing a message-id
-                       else if ($result === false )
+                       else if$result === false )
                                $errors[] = array('badaccess-group0'); # a generic "We don't want them to do that"
                }
-               if ($doExpensiveQueries && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) ) {
-                       if ($result != array() && is_array($result) && !is_array($result[0]))
+               # Short-circuit point
+               if( $short && count($errors) > 0 ) {
+                       wfProfileOut( __METHOD__ );
+                       return $errors;
+               }
+               // Check getUserPermissionsErrorsExpensive hook
+               if( $doExpensiveQueries && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array(&$this,&$user,$action,&$result) ) ) {
+                       if( is_array($result) && count($result) && !is_array($result[0]) )
                                $errors[] = $result; # A single array representing an error
-                       else if (is_array($result) && is_array($result[0]))
+                       else if( is_array($result) && is_array($result[0]) )
                                $errors = array_merge( $errors, $result ); # A nested array representing multiple errors
-                       else if ($result != '' && $result != null && $result !== true && $result !== false)
+                       else if( $result !== '' && is_string($result) )
                                $errors[] = array($result); # A string representing a message-id
-                       else if ($result === false )
+                       else if$result === false )
                                $errors[] = array('badaccess-group0'); # a generic "We don't want them to do that"
                }
+               # Short-circuit point
+               if( $short && count($errors) > 0 ) {
+                       wfProfileOut( __METHOD__ );
+                       return $errors;
+               }
                
+               # Only 'createaccount' and 'execute' can be performed on
+               # special pages, which don't actually exist in the DB.
                $specialOKActions = array( 'createaccount', 'execute' );
                if( NS_SPECIAL == $this->mNamespace && !in_array( $action, $specialOKActions) ) {
                        $errors[] = array('ns-specialprotected');
                }
 
-               if ( $this->isNamespaceProtected() ) {
-                       $ns = $this->getNamespace() == NS_MAIN
-                               ? wfMsg( 'nstab-main' )
-                               : $this->getNsText();
-                       $errors[] = (NS_MEDIAWIKI == $this->mNamespace
-                               ? array('protectedinterface')
-                               : array( 'namespaceprotected',  $ns ) );
+               # Check $wgNamespaceProtection for restricted namespaces
+               if( $this->isNamespaceProtected() ) {
+                       $ns = $this->getNamespace() == NS_MAIN ?
+                               wfMsg( 'nstab-main' ) : $this->getNsText();
+                       $errors[] = NS_MEDIAWIKI == $this->mNamespace ?
+                               array('protectedinterface') : array( 'namespaceprotected',  $ns );
                }
 
-               if( $this->mDbkeyform == '_' ) {
-                       # FIXME: Is this necessary? Shouldn't be allowed anyway...
-                       $errors[] = array('badaccess-group0');
-               }
-
-               # protect css/js subpages of user pages
+               # Protect css/js subpages of user pages
                # XXX: this might be better using restrictions
                # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssJsSubpage() from working
-               if( $this->isCssJsSubpage()
-                       && !$user->isAllowed('editusercssjs')
-                       && !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) ) {
+               if( $this->isCssJsSubpage() && !$user->isAllowed('editusercssjs')
+                       && !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) )
+               {
                        $errors[] = array('customcssjsprotected');
                }
 
-               if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) {
+               # Check against page_restrictions table requirements on this
+               # page. The user must possess all required rights for this action.
+               foreach( $this->getRestrictions($action) as $right ) {
+                       // Backwards compatibility, rewrite sysop -> protect
+                       if( $right == 'sysop' ) {
+                               $right = 'protect';
+                       }
+                       if( '' != $right && !$user->isAllowed( $right ) ) {
+                               // Users with 'editprotected' permission can edit protected pages
+                               if( $action=='edit' && $user->isAllowed( 'editprotected' ) ) {
+                                       // Users with 'editprotected' permission cannot edit protected pages
+                                       // with cascading option turned on.
+                                       if( $this->mCascadeRestriction ) {
+                                               $errors[] = array( 'protectedpagetext', $right );
+                                       }
+                               } else {
+                                       $errors[] = array( 'protectedpagetext', $right );
+                               }
+                       }
+               }
+               # Short-circuit point
+               if( $short && count($errors) > 0 ) {
+                       wfProfileOut( __METHOD__ );
+                       return $errors;
+               }
+               
+               if( $doExpensiveQueries && !$this->isCssJsSubpage() ) {
                        # We /could/ use the protection level on the source page, but it's fairly ugly
                        #  as we have to establish a precedence hierarchy for pages included by multiple
                        #  cascade-protected pages. So just restrict it to people with 'protect' permission,
@@ -1177,103 +1356,45 @@ class Title {
                                }
                        }
                }
-
-               foreach( $this->getRestrictions($action) as $right ) {
-                       // Backwards compatibility, rewrite sysop -> protect
-                       if ( $right == 'sysop' ) {
-                               $right = 'protect';
-                       }
-                       if( '' != $right && !$user->isAllowed( $right ) ) {
-                               //Users with 'editprotected' permission can edit protected pages
-                               if( $action=='edit' && $user->isAllowed( 'editprotected' ) ) {
-                                       //Users with 'editprotected' permission cannot edit protected pages
-                                       //with cascading option turned on.
-                                       if($this->mCascadeRestriction) {
-                                               $errors[] = array( 'protectedpagetext', $right );
-                                       } else {
-                                               //Nothing, user can edit!
-                                       }
-                               } else {
-                                       $errors[] = array( 'protectedpagetext', $right );
-                               }
-                       }
+               # Short-circuit point
+               if( $short && count($errors) > 0 ) {
+                       wfProfileOut( __METHOD__ );
+                       return $errors;
                }
 
-               if ($action == 'protect') {
-                       if ($this->getUserPermissionsErrors('edit', $user) != array()) {
+               if( $action == 'protect' ) {
+                       if( $this->getUserPermissionsErrors('edit', $user) != array() ) {
                                $errors[] = array( 'protect-cantedit' ); // If they can't edit, they shouldn't protect.
                        }
                }
 
-               if ($action == 'create') {
+               if( $action == 'create' ) {
                        $title_protection = $this->getTitleProtection();
+                       if( is_array($title_protection) ) {
+                               extract($title_protection); // is this extract() really needed?
 
-                       if (is_array($title_protection)) {
-                               extract($title_protection);
-
-                               if ($pt_create_perm == 'sysop')
-                                       $pt_create_perm = 'protect';
-
-                               if ($pt_create_perm == '' || !$user->isAllowed($pt_create_perm)) {
-                                       $errors[] = array ( 'titleprotected', User::whoIs($pt_user), $pt_reason );
+                               if( $pt_create_perm == 'sysop' ) {
+                                       $pt_create_perm = 'protect'; // B/C
+                               }
+                               if( $pt_create_perm == '' || !$user->isAllowed($pt_create_perm) ) {
+                                       $errors[] = array( 'titleprotected', User::whoIs($pt_user), $pt_reason );
                                }
                        }
-
-                       if( (  $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
-                               ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) {
-                               $errors[] = $user->isAnon() ? array ('nocreatetext') : array ('nocreate-loggedin');
-                       }
-
-               } elseif ( $action == 'move' ) {
-                       if ( !$user->isAllowed( 'move' ) ) {
-                               // User can't move anything
-                               $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed');
-                       } elseif ( !$user->isAllowed( 'move-rootuserpages' ) 
-                                       && $this->getNamespace() == NS_USER && !$this->isSubpage() ) 
-                       {
-                               // Show user page-specific message only if the user can move other pages
-                               $errors[] = array( 'cant-move-user-page' );
-                       }
-
+               } elseif( $action == 'move' ) {
                        // Check for immobile pages
-                       if ( !MWNamespace::isMovable( $this->getNamespace() ) ) {
+                       if( !MWNamespace::isMovable( $this->getNamespace() ) ) {
                                // Specific message for this case
                                $errors[] = array( 'immobile-source-namespace', $this->getNsText() );
-                       } elseif ( !$this->isMovable() ) {
+                       } elseif( !$this->isMovable() ) {
                                // Less specific message for rarer cases
                                $errors[] = array( 'immobile-page' );
                        }
-
-               } elseif ( $action == 'move-target' ) {
-                       if ( !$user->isAllowed( 'move' ) ) {
-                               // User can't move anything
-                               $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed');
-                       } elseif ( !$user->isAllowed( 'move-rootuserpages' ) 
-                                       && $this->getNamespace() == NS_USER && !$this->isSubpage() ) 
-                       {
-                               // Show user page-specific message only if the user can move other pages
-                               $errors[] = array( 'cant-move-to-user-page' );
-                       }
-                       if ( !MWNamespace::isMovable( $this->getNamespace() ) ) {
+               } elseif( $action == 'move-target' ) {
+                       if( !MWNamespace::isMovable( $this->getNamespace() ) ) {
                                $errors[] = array( 'immobile-target-namespace', $this->getNsText() );
-                       } elseif ( !$this->isMovable() ) {
+                       } elseif( !$this->isMovable() ) {
                                $errors[] = array( 'immobile-target-page' );
                        }
-
-               } elseif ( !$user->isAllowed( $action ) ) {
-                       $return = null;
-                       $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
-                               User::getGroupsWithPermission( $action ) );
-                       if ( $groups ) {
-                               $return = array( 'badaccess-groups',
-                                       array(
-                                               implode( ', ', $groups ),
-                                               count( $groups ) ) );
-                       }
-                       else {
-                               $return = array( "badaccess-group0" );
-                       }
-                       $errors[] = $return;
                }
 
                wfProfileOut( __METHOD__ );
@@ -1293,7 +1414,8 @@ class Title {
 
                $dbr = wfGetDB( DB_SLAVE );
                $res = $dbr->select( 'protected_titles', '*',
-                       array ('pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey()) );
+                       array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
+                       __METHOD__ );
 
                if ($row = $dbr->fetchRow( $res )) {
                        return $row;
@@ -1325,7 +1447,7 @@ class Title {
 
                $expiry_description = '';
                if ( $encodedExpiry != 'infinity' ) {
-                       $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) ).')';
+                       $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) , $wgContLang->date( $expiry ) , $wgContLang->time( $expiry ) ).')';
                }
                else {
                        $expiry_description .= ' (' . wfMsgForContent( 'protect-expiry-indefinite' ).')';
@@ -1363,7 +1485,8 @@ class Title {
                $dbw = wfGetDB( DB_MASTER );
 
                $dbw->delete( 'protected_titles',
-                       array ('pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey()), __METHOD__ );
+                       array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), 
+                       __METHOD__ );
        }
 
        /**
@@ -1400,8 +1523,7 @@ class Title {
         * @return \type{\bool} TRUE or FALSE
         */
        public function isMovable() {
-               return MWNamespace::isMovable( $this->getNamespace() )
-                       && $this->getInterwiki() == '';
+               return MWNamespace::isMovable( $this->getNamespace() ) && $this->getInterwiki() == '';
        }
 
        /**
@@ -1419,7 +1541,7 @@ class Title {
                }
 
                # Shortcut for public wikis, allows skipping quite a bit of code
-               if ($wgGroupPermissions['*']['read'])
+               if ( !empty( $wgGroupPermissions['*']['read'] ) )
                        return true;
 
                if( $wgUser->isAllowed( 'read' ) ) {
@@ -1518,10 +1640,36 @@ class Title {
                }
 
                $db = wfGetDB( DB_SLAVE );
-               return $this->mHasSubpages = (bool)$db->selectField( 'page', '1',
-                       "page_namespace = {$this->mNamespace} AND page_title LIKE '"
-                       . $db->escapeLike( $this->mDbkeyform ) . "/%'",
-                       __METHOD__
+               $subpages = $this->getSubpages( 1 );
+               if( $subpages instanceof TitleArray )
+                       return $this->mHasSubpages = (bool)$subpages->count();
+               return $this->mHasSubpages = false;
+       }
+       
+       /**
+        * Get all subpages of this page.
+        * @param $limit Maximum number of subpages to fetch; -1 for no limit
+        * @return mixed TitleArray, or empty array if this page's namespace
+        *  doesn't allow subpages
+        */
+       public function getSubpages($limit = -1) {
+               if( !MWNamespace::hasSubpages( $this->getNamespace() ) )
+                       return array();
+
+               $dbr = wfGetDB( DB_SLAVE );
+               $conds['page_namespace'] = $this->getNamespace();
+               $conds[] = 'page_title LIKE ' . $dbr->addQuotes(
+                               $dbr->escapeLike( $this->getDBkey() ) . '/%' );
+               $options = array();
+               if( $limit > -1 )
+                       $options['LIMIT'] = $limit;
+               return $this->mSubpages = TitleArray::newFromResult(
+                       $dbr->select( 'page',
+                               array( 'page_id', 'page_namespace', 'page_title' ),
+                               $conds,
+                               __METHOD__,
+                               $options
+                       )
                );
        }
 
@@ -1626,7 +1774,7 @@ class Title {
 
                $dbr = wfGetDB( DB_SLAVE );
 
-               if ( $this->getNamespace() == NS_IMAGE ) {
+               if ( $this->getNamespace() == NS_FILE ) {
                        $tables = array ('imagelinks', 'page_restrictions');
                        $where_clauses = array(
                                'il_to' => $this->getDBkey(),
@@ -1863,7 +2011,7 @@ class Title {
                        $dbr = wfGetDB( DB_SLAVE );
                        $n = $dbr->selectField( 'archive', 'COUNT(*)', array( 'ar_namespace' => $this->getNamespace(),
                                'ar_title' => $this->getDBkey() ), $fname );
-                       if( $this->getNamespace() == NS_IMAGE ) {
+                       if( $this->getNamespace() == NS_FILE ) {
                                $n += $dbr->selectField( 'filearchive', 'COUNT(*)',
                                        array( 'fa_name' => $this->getDBkey() ), $fname );
                        }
@@ -1879,6 +2027,9 @@ class Title {
         * @return \type{\int} the ID
         */
        public function getArticleID( $flags = 0 ) {
+               if( $this->getNamespace() < 0 ) {
+                       return $this->mArticleID = 0;
+               }
                $linkCache = LinkCache::singleton();
                if( $flags & GAID_FOR_UPDATE ) {
                        $oldUpdate = $linkCache->forUpdate( true );
@@ -1902,10 +2053,9 @@ class Title {
        public function isRedirect( $flags = 0 ) {
                if( !is_null($this->mRedirect) )
                        return $this->mRedirect;
-               # Zero for special pages.
-               # Also, calling getArticleID() loads the field from cache!
-               if( !$this->getArticleID($flags) || $this->getNamespace() == NS_SPECIAL ) {
-                       return false;
+               # Calling getArticleID() loads the field from cache as needed
+               if( !$this->getArticleID($flags) ) {
+                       return $this->mRedirect = false;
                }
                $linkCache = LinkCache::singleton();
                $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
@@ -1922,10 +2072,9 @@ class Title {
        public function getLength( $flags = 0 ) {
                if( $this->mLength != -1 )
                        return $this->mLength;
-               # Zero for special pages.
-               # Also, calling getArticleID() loads the field from cache!
-               if( !$this->getArticleID($flags) || $this->getNamespace() == NS_SPECIAL ) {
-                       return 0;
+               # Calling getArticleID() loads the field from cache as needed
+               if( !$this->getArticleID($flags) ) {
+                       return $this->mLength = 0;
                }
                $linkCache = LinkCache::singleton();
                $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) );
@@ -1943,9 +2092,7 @@ class Title {
                        return $this->mLatestID;
 
                $db = ($flags & GAID_FOR_UPDATE) ? wfGetDB(DB_MASTER) : wfGetDB(DB_SLAVE);
-               $this->mLatestID = $db->selectField( 'page', 'page_latest',
-                       array( 'page_namespace' => $this->getNamespace(), 'page_title' => $this->getDBKey() ),
-                       __METHOD__ );
+               $this->mLatestID = $db->selectField( 'page', 'page_latest', $this->pageCond(), __METHOD__ );
                return $this->mLatestID;
        }
 
@@ -1974,27 +2121,16 @@ class Title {
         * @return \type{\bool} true if the update succeded
         */
        public function invalidateCache() {
-               global $wgUseFileCache;
-
-               if ( wfReadOnly() ) {
+               if( wfReadOnly() ) {
                        return;
                }
-
                $dbw = wfGetDB( DB_MASTER );
                $success = $dbw->update( 'page',
-                       array( /* SET */
-                               'page_touched' => $dbw->timestamp()
-                       ), array( /* WHERE */
-                               'page_namespace' => $this->getNamespace() ,
-                               'page_title' => $this->getDBkey()
-                       ), 'Title::invalidateCache'
+                       array( 'page_touched' => $dbw->timestamp() ), 
+                       $this->pageCond(), 
+                       __METHOD__
                );
-
-               if ($wgUseFileCache) {
-                       $cache = new HTMLFileCache($this);
-                       @unlink($cache->fileCacheName());
-               }
-
+               HTMLFileCache::clearFileCache( $this );
                return $success;
        }
 
@@ -2055,8 +2191,7 @@ class Title {
                # Strip Unicode bidi override characters.
                # Sometimes they slip into cut-n-pasted page titles, where the
                # override chars get included in list displays.
-               $dbkey = str_replace( "\xE2\x80\x8E", '', $dbkey ); // 200E LEFT-TO-RIGHT MARK
-               $dbkey = str_replace( "\xE2\x80\x8F", '', $dbkey ); // 200F RIGHT-TO-LEFT MARK
+               $dbkey = preg_replace( '/\xE2\x80[\x8E\x8F\xAA-\xAE]/S', '', $dbkey );
 
                # Clean up whitespace
                #
@@ -2084,14 +2219,22 @@ class Title {
 
                # Namespace or interwiki prefix
                $firstPass = true;
+               $prefixRegexp = "/^(.+?)_*:_*(.*)$/S";
                do {
                        $m = array();
-                       if ( preg_match( "/^(.+?)_*:_*(.*)$/S", $dbkey, $m ) ) {
+                       if ( preg_match( $prefixRegexp, $dbkey, $m ) ) {
                                $p = $m[1];
-                               if ( $ns = $wgContLang->getNsIndex( $p )) {
+                               if ( $ns = $wgContLang->getNsIndex( $p ) ) {
                                        # Ordinary namespace
                                        $dbkey = $m[2];
                                        $this->mNamespace = $ns;
+                                       # For Talk:X pages, check if X has a "namespace" prefix
+                                       if( $ns == NS_TALK && preg_match( $prefixRegexp, $dbkey, $x ) ) {
+                                               if( $wgContLang->getNsIndex( $x[1] ) )
+                                                       return false; # Disallow Talk:File:x type titles...
+                                               else if( Interwiki::isValidInterwiki( $x[1] ) )
+                                                       return false; # Disallow Talk:Interwiki:x type titles...
+                                       }
                                } elseif( Interwiki::isValidInterwiki( $p ) ) {
                                        if( !$firstPass ) {
                                                # Can't make a local interwiki link to an interwiki link.
@@ -2436,6 +2579,9 @@ class Title {
                if( !$this->isMovable() ) {
                        $errors[] = array( 'immobile-source-namespace', $this->getNsText() );
                }
+               if ( $nt->getInterwiki() != '' ) {
+                       $errors[] = array( 'immobile-target-namespace-iw' );
+               }
                if ( !$nt->isMovable() ) {
                        $errors[] = array('immobile-target-namespace', $nt->getNsText() );
                }
@@ -2453,10 +2599,10 @@ class Title {
                }
 
                // Image-specific checks
-               if( $this->getNamespace() == NS_IMAGE ) {
+               if( $this->getNamespace() == NS_FILE ) {
                        $file = wfLocalFile( $this );
                        if( $file->exists() ) {
-                               if( $nt->getNamespace() != NS_IMAGE ) {
+                               if( $nt->getNamespace() != NS_FILE ) {
                                        $errors[] = array('imagenocrossnamespace');
                                }
                                if( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) {
@@ -2576,8 +2722,8 @@ class Title {
                        );
                        # Update the protection log
                        $log = new LogPage( 'protect' );
-                       $comment = wfMsgForContent('prot_1movedto2',$this->getPrefixedText(), $nt->getPrefixedText() );
-                       if( $reason ) $comment .= ': ' . $reason;
+                       $comment = wfMsgForContent( 'prot_1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() );
+                       if( $reason ) $comment .= wfMsgForContent( 'colon-separator' ) . $reason;
                        $log->addEntry( 'move_prot', $nt, $comment, array($this->getPrefixedText()) ); // FIXME: $params?
                }
 
@@ -2679,7 +2825,7 @@ class Title {
                $nullRevId = $nullRevision->insertOn( $dbw );
                
                $article = new Article( $this );
-               wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest) );
+               wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $wgUser) );
 
                # Change the name of the target page:
                $dbw->update( 'page',
@@ -2707,7 +2853,7 @@ class Title {
                        $redirectRevision->insertOn( $dbw );
                        $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
                        
-                       wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false) );
+                       wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false, $wgUser) );
 
                        # Now, we record the link from the redirect to the new title.
                        # It should have no other outgoing links...
@@ -2718,12 +2864,14 @@ class Title {
                                        'pl_namespace' => $nt->getNamespace(),
                                        'pl_title'     => $nt->getDBkey() ),
                                $fname );
+                       $redirectSuppressed = false;
                } else {
                        $this->resetArticleID( 0 );
+                       $redirectSuppressed = true;
                }
-               
+
                # Move an image if this is a file
-               if( $this->getNamespace() == NS_IMAGE ) {
+               if( $this->getNamespace() == NS_FILE ) {
                        $file = wfLocalFile( $this );
                        if( $file->exists() ) {
                                $status = $file->move( $nt );
@@ -2736,7 +2884,7 @@ class Title {
 
                # Log the move
                $log = new LogPage( 'move' );
-               $log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText() ) );
+               $log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) );
 
                # Purge squid
                if ( $wgUseSquid ) {
@@ -2776,7 +2924,7 @@ class Title {
                $nullRevId = $nullRevision->insertOn( $dbw );
                
                $article = new Article( $this );
-               wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest) );
+               wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $wgUser) );
 
                # Rename page entry
                $dbw->update( 'page',
@@ -2804,7 +2952,7 @@ class Title {
                        $redirectRevision->insertOn( $dbw );
                        $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
                        
-                       wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false) );
+                       wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false, $wgUser) );
 
                        # Record the just-created redirect's linking to the page
                        $dbw->insert( 'pagelinks',
@@ -2813,12 +2961,14 @@ class Title {
                                        'pl_namespace' => $nt->getNamespace(),
                                        'pl_title'     => $nt->getDBkey() ),
                                $fname );
+                       $redirectSuppressed = false;
                } else {
                        $this->resetArticleID( 0 );
+                       $redirectSuppressed = true;
                }
-               
+
                # Move an image if this is a file
-               if( $this->getNamespace() == NS_IMAGE ) {
+               if( $this->getNamespace() == NS_FILE ) {
                        $file = wfLocalFile( $this );
                        if( $file->exists() ) {
                                $status = $file->move( $nt );
@@ -2831,7 +2981,7 @@ class Title {
 
                # Log the move
                $log = new LogPage( 'move' );
-               $log->addEntry( 'move', $this, $reason, array( 1 => $nt->getPrefixedText()) );
+               $log->addEntry( 'move', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) );
 
                # Purge caches as per article creation
                Article::onArticleCreate( $nt );
@@ -2842,31 +2992,87 @@ class Title {
                
        }
        
+       /**
+        * Move this page's subpages to be subpages of $nt
+        * @param $nt Title Move target
+        * @param $auth bool Whether $wgUser's permissions should be checked
+        * @param $reason string The reason for the move
+        * @param $createRedirect bool Whether to create redirects from the old subpages to the new ones
+        *  Ignored if the user doesn't have the 'suppressredirect' right
+        * @return mixed array with old page titles as keys, and strings (new page titles) or
+        *  arrays (errors) as values, or an error array with numeric indices if no pages were moved
+        */
+       public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) {
+               global $wgUser, $wgMaximumMovedPages;
+               // Check permissions
+               if( !$this->userCan( 'move-subpages' ) )
+                       return array( 'cant-move-subpages' );
+               // Do the source and target namespaces support subpages?
+               if( !MWNamespace::hasSubpages( $this->getNamespace() ) )
+                       return array( 'namespace-nosubpages',
+                               MWNamespace::getCanonicalName( $this->getNamespace() ) );
+               if( !MWNamespace::hasSubpages( $nt->getNamespace() ) )
+                       return array( 'namespace-nosubpages',
+                               MWNamespace::getCanonicalName( $nt->getNamespace() ) );
+
+               $subpages = $this->getSubpages($wgMaximumMovedPages + 1);
+               $retval = array();
+               $count = 0;
+               foreach( $subpages as $oldSubpage ) {
+                       $count++;
+                       if( $count > $wgMaximumMovedPages ) {
+                               $retval[$oldSubpage->getPrefixedTitle()] =
+                                               array( 'movepage-max-pages',
+                                                       $wgMaximumMovedPages );
+                               break;
+                       }
+
+                       if( $oldSubpage->getArticleId() == $this->getArticleId() )
+                               // When moving a page to a subpage of itself,
+                               // don't move it twice
+                               continue;
+                       $newPageName = preg_replace(
+                                       '#^'.preg_quote( $this->getDBKey(), '#' ).'#',
+                                       $nt->getDBKey(), $oldSubpage->getDBKey() );
+                       if( $oldSubpage->isTalkPage() ) {
+                               $newNs = $nt->getTalkPage()->getNamespace();
+                       } else {
+                               $newNs = $nt->getSubjectPage()->getNamespace();
+                       }
+                       # Bug 14385: we need makeTitleSafe because the new page names may
+                       # be longer than 255 characters.
+                       $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
+
+                       $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect );
+                       if( $success === true ) {
+                               $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
+                       } else {
+                               $retval[$oldSubpage->getPrefixedText()] = $success;
+                       }
+               }
+               return $retval;
+       }
+       
        /**
         * Checks if this page is just a one-rev redirect.
         * Adds lock, so don't use just for light purposes.
         *
-        * @param $curId \type{int} page ID, optional
         * @return \type{\bool} TRUE or FALSE
         */
-       public function isSingleRevRedirect( $curId = 0 ) {
+       public function isSingleRevRedirect() {
                $dbw = wfGetDB( DB_MASTER );
-               $curId = $curId ? $curId : $this->getArticleId();
-               # Nothing here?
-               if( !$curId ) {
-                       return true;
-               }
                # Is it a redirect?
                $row = $dbw->selectRow( 'page',
-                       array( 'page_is_redirect', 'page_latest' ),
-                       array( 'page_id' => $curId ),
+                       array( 'page_is_redirect', 'page_latest', 'page_id' ),
+                       $this->pageCond(),
                        __METHOD__,
                        'FOR UPDATE'
                );
                # Cache some fields we may want
+               $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;
-               if( $this->mRedirect ) {
+               if( !$this->mRedirect ) {
                        return false;
                }
                # Does the article have a history?
@@ -2894,7 +3100,7 @@ class Title {
        public function isValidMoveTarget( $nt ) {
                $dbw = wfGetDB( DB_MASTER );
                # Is it an existsing file?
-               if( $nt->getNamespace() == NS_IMAGE ) {
+               if( $nt->getNamespace() == NS_FILE ) {
                        $file = wfLocalFile( $nt );
                        if( $file->exists() ) {
                                wfDebug( __METHOD__ . ": file exists\n" );
@@ -2934,8 +3140,7 @@ class Title {
         * @return \type{\bool} TRUE or FALSE
         */
        public function isWatchable() {
-               return !$this->isExternal()
-                       && MWNamespace::isWatchable( $this->getNamespace() );
+               return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
        }
 
        /**
@@ -3006,7 +3211,12 @@ class Title {
         * @return \type{\array} Selection array
         */
        public function pageCond() {
-               return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform );
+               if( $this->mArticleID > 0 ) {
+                       // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
+                       return array( 'page_id' => $this->mArticleID );
+               } else {
+                       return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform );
+               }
        }
 
        /**
@@ -3046,6 +3256,38 @@ class Title {
                        array( 'ORDER BY' => 'rev_id' )
                );
        }
+       
+       /**
+        * Get the first revision of the page
+        *
+        * @param $flags \type{\int} 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 );
+               $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 );
+               }
+       }
+       
+       /**
+        * Check if this is a new page
+        *
+        * @return bool
+        */
+       public function isNewPage() {
+               $dbr = wfGetDB( DB_SLAVE );
+               return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
+       }
 
        /**
         * Get the oldest revision timestamp of this page
@@ -3116,7 +3358,12 @@ class Title {
        }
 
        /**
-        * Check if page exists
+        * Check if page exists.  For historical reasons, this function simply
+        * checks for the existence of the title in the page table, and will
+        * thus return false for interwiki links, special pages and the like.
+        * If you want to know if a title can be meaningfully viewed, you should
+        * probably call the isKnown() method instead.
+        *
         * @return \type{\bool} TRUE or FALSE
         */
        public function exists() {
@@ -3124,21 +3371,65 @@ class Title {
        }
 
        /**
-        * Do we know that this title definitely exists, or should we otherwise
-        * consider that it exists?
+        * Should links to this title be shown as potentially viewable (i.e. as
+        * "bluelinks"), even if there's no record by this title in the page
+        * table?
+        *
+        * This function is semi-deprecated for public use, as well as somewhat
+        * misleadingly named.  You probably just want to call isKnown(), which
+        * calls this function internally.
+        *
+        * (ISSUE: Most of these checks are cheap, but the file existence check
+        * can potentially be quite expensive.  Including it here fixes a lot of
+        * existing code, but we might want to add an optional parameter to skip
+        * it and any other expensive checks.)
         *
         * @return \type{\bool} TRUE or FALSE
         */
        public function isAlwaysKnown() {
-               // 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.
-               // Also, extension messages which are not loaded, are shown as red, because
-               // we don't call MessageCache::loadAllMessages.
-               list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 );
-               return $this->isExternal()
-                       || ( $this->mNamespace == NS_MAIN && $this->mDbkeyform == '' )
-                       || ( $this->mNamespace == NS_MEDIAWIKI && wfMsgWeirdKey( $basename ) );
+               if( $this->mInterwiki != '' ) {
+                       return true;  // any interwiki link might be viewable, for all we know
+               }
+               switch( $this->mNamespace ) {                   
+               case NS_MEDIA:
+               case NS_FILE:
+                       return 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.
+                       // Also, extension messages which are not loaded, are shown as red, because
+                       // we don't call MessageCache::loadAllMessages.
+                       list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 );
+                       return wfMsgWeirdKey( $basename );  // known system message
+               default:
+                       return false;
+               }
+       }
+
+       /**
+        * Does this title refer to a page that can (or might) be meaningfully
+        * viewed?  In particular, this function may be used to determine if
+        * links to the title should be rendered as "bluelinks" (as opposed to
+        * "redlinks" to non-existent pages).
+        *
+        * @return \type{\bool} TRUE or FALSE
+        */
+       public function isKnown() {
+               return $this->exists() || $this->isAlwaysKnown();
+       }
+       
+       /**
+       * Is this in a namespace that allows actual pages?
+       *
+       * @return \type{\bool} TRUE or FALSE
+       */
+       public function canExist() {
+               return $this->mNamespace >= 0 && $this->mNamespace != NS_MEDIA;
        }
 
        /**
@@ -3163,12 +3454,7 @@ class Title {
         */
        public function getTouched( $db = NULL ) {
                $db = isset($db) ? $db : wfGetDB( DB_SLAVE );
-               $touched = $db->selectField( 'page', 'page_touched',
-                       array(
-                               'page_namespace' => $this->getNamespace(),
-                               'page_title' => $this->getDBkey()
-                       ), __METHOD__
-               );
+               $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
                return $touched;
        }
 
@@ -3263,8 +3549,8 @@ class Title {
                        case NS_PROJECT:
                        case NS_PROJECT_TALK:
                                return 'nstab-project';
-                       case NS_IMAGE:
-                       case NS_IMAGE_TALK:
+                       case NS_FILE:
+                       case NS_FILE_TALK:
                                return 'nstab-image';
                        case NS_MEDIAWIKI:
                        case NS_MEDIAWIKI_TALK:
@@ -3356,4 +3642,26 @@ class Title {
                }
                return $redirs;
        }
+       
+       /**
+        * Check if this Title is a valid redirect target
+        *
+        * @return \type{\bool} TRUE or FALSE
+        */
+       public function isValidRedirectTarget() {
+               global $wgInvalidRedirectTargets;
+               
+               // invalid redirect targets are stored in a global array, but explicity disallow Userlogout here
+               if( $this->isSpecial( 'Userlogout' ) ) {
+                       return false;
+               }
+               
+               foreach( $wgInvalidRedirectTargets as $target ) {
+                       if( $this->isSpecial( $target ) ) {
+                               return false;
+                       }
+               }
+               
+               return true;
+       }
 }