X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FTitle.php;h=06ba76d0dd355c6ae2f045ee9da9feaf937782af;hb=48993f18f923ae06b27496a272798813e4e5a7bd;hp=4f82e951053d455539273d0869efadf2f3efee88;hpb=8beabce505ce77bde0acabb36002c494c5246640;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/Title.php b/includes/Title.php index 4f82e95105..06ba76d0dd 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -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; + } }