Hide title if DELETED_ACTION is on, but don't worry about type/action, which isn...
[lhc/web/wiklou.git] / includes / Title.php
index 4f82e95..06ba76d 100644 (file)
@@ -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,13 +382,11 @@ class Title {
                                        $m[1] = urldecode( ltrim( $m[1], ':' ) );
                                }
                                $title = Title::newFromText( $m[1] );
-                               // Redirects to some special pages are not permitted
-                               if( $title instanceof Title 
-                                               && !$title->isSpecial( 'Userlogout' )
-                                               && !$title->isSpecial( 'Filepath' ) ) 
-                               {
-                                       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;
@@ -451,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' );
        }
 
 #----------------------------------------------------------------------------
@@ -764,7 +828,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 ) {
@@ -990,7 +1056,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());
        }
 
        /**
@@ -1022,7 +1088,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
@@ -1092,9 +1158,10 @@ 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();
@@ -1104,7 +1171,7 @@ class Title {
                        wfProfileOut( __METHOD__ );
                        return $result ? array() : array( array( 'badaccess-group0' ) );
                }
-
+               // 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
@@ -1115,6 +1182,12 @@ class Title {
                        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;
+               }
+               // 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
@@ -1125,13 +1198,20 @@ class Title {
                        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;
+               }
                
-               // TODO: document
+               # 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');
                }
 
+               # Check $wgNamespaceProtection for restricted namespaces
                if( $this->isNamespaceProtected() ) {
                        $ns = $this->getNamespace() == NS_MAIN ?
                                wfMsg( 'nstab-main' ) : $this->getNsText();
@@ -1139,7 +1219,7 @@ class Title {
                                array('protectedinterface') : array( 'namespaceprotected',  $ns );
                }
 
-               # 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')
@@ -1148,6 +1228,32 @@ class Title {
                        $errors[] = array('customcssjsprotected');
                }
 
+               # 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
@@ -1170,26 +1276,10 @@ 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' ) {
@@ -1226,6 +1316,10 @@ class Title {
                                // 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' );
+                       }
                        // Check for immobile pages
                        if( !MWNamespace::isMovable( $this->getNamespace() ) ) {
                                // Specific message for this case
@@ -1312,7 +1406,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' ).')';
@@ -1406,7 +1500,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' ) ) {
@@ -1960,7 +2054,6 @@ class Title {
         * @return \type{\bool} true if the update succeded
         */
        public function invalidateCache() {
-               global $wgUseFileCache;
                if( wfReadOnly() ) {
                        return;
                }
@@ -1970,10 +2063,7 @@ class Title {
                        $this->pageCond(), 
                        __METHOD__
                );
-               if( $wgUseFileCache) {
-                       $cache = new HTMLFileCache( $this );
-                       @unlink( $cache->fileCacheName() );
-               }
+               HTMLFileCache::clearFileCache( $this );
                return $success;
        }
 
@@ -2062,14 +2152,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.
@@ -2557,8 +2655,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?
                }
 
@@ -2699,10 +2797,12 @@ 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_FILE ) {
                        $file = wfLocalFile( $this );
@@ -2717,7 +2817,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 ) {
@@ -2794,10 +2894,12 @@ 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_FILE ) {
                        $file = wfLocalFile( $this );
@@ -2812,7 +2914,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 );
@@ -3027,6 +3129,28 @@ class Title {
                );
        }
        
+       /**
+        * 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
         *
@@ -3170,6 +3294,15 @@ class Title {
        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;
+       }
 
        /**
         * Update page_touched timestamps and send squid purge messages for
@@ -3381,4 +3514,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;
+       }
 }