X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FTitle.php;h=06ba76d0dd355c6ae2f045ee9da9feaf937782af;hb=48993f18f923ae06b27496a272798813e4e5a7bd;hp=792f475901189e2799abceb5896ef20bc1e351ba;hpb=57073ee3b102a200ae015fa6e211c846f6014026;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/Title.php b/includes/Title.php index 792f475901..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. @@ -2414,6 +2512,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() ); } @@ -2554,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? } @@ -2696,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 ); @@ -2714,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 ) { @@ -2791,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 ); @@ -2809,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 ); @@ -2979,7 +3084,8 @@ class Title { */ public function pageCond() { if( $this->mArticleID > 0 ) { - return array( 'page_id' => $this->mArticleID ); // PK avoids secondary lookups + // 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 ); } @@ -3022,6 +3128,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 @@ -3092,7 +3230,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() { @@ -3100,21 +3243,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; } /** @@ -3327,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; + } }