From: Roan Kattouw Date: Fri, 29 Jun 2007 19:55:46 +0000 (+0000) Subject: * Separating UI code and DB code in Article::rollback() X-Git-Tag: 1.31.0-rc.0~52321 X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_ecrire%28%22articles%22%2C%22id_article=%24id_article%22%29%20.%20%22?a=commitdiff_plain;h=1a2e663a40c9ac585d97459867b6a90a16c27acf;p=lhc%2Fweb%2Fwiklou.git * Separating UI code and DB code in Article::rollback() * Adding API rollback functionality --- diff --git a/includes/Article.php b/includes/Article.php index 9f3795e749..681a56f701 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -2155,119 +2155,152 @@ class Article { return true; } - /** - * Revert a modification - */ - function rollback() { - global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol; - - if( $wgUser->isAllowed( 'rollback' ) ) { - if( $wgUser->isBlocked() ) { - $wgOut->blockedPage(); - return; - } - } else { - $wgOut->permissionRequired( 'rollback' ); - return; - } - - if ( wfReadOnly() ) { - $wgOut->readOnlyPage( $this->getContent() ); - return; - } - if( !$wgUser->matchEditToken( $wgRequest->getVal( 'token' ), - array( $this->mTitle->getPrefixedText(), - $wgRequest->getVal( 'from' ) ) ) ) { - $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); - $wgOut->addWikiText( wfMsg( 'sessionfailure' ) ); - return; - } - $dbw = wfGetDB( DB_MASTER ); - - # Enhanced rollback, marks edits rc_bot=1 - $bot = $wgRequest->getBool( 'bot' ); - - # Replace all this user's current edits with the next one down - - # Get the last editor - $current = Revision::newFromTitle( $this->mTitle ); - if( is_null( $current ) ) { - # Something wrong... no page? - $wgOut->addHTML( wfMsg( 'notanarticle' ) ); - return; - } - - $from = str_replace( '_', ' ', $wgRequest->getVal( 'from' ) ); - if( $from != $current->getUserText() ) { - $wgOut->setPageTitle( wfMsg('rollbackfailed') ); - $wgOut->addWikiText( wfMsg( 'alreadyrolled', - htmlspecialchars( $this->mTitle->getPrefixedText()), - htmlspecialchars( $from ), - htmlspecialchars( $current->getUserText() ) ) ); - if( $current->getComment() != '') { - $wgOut->addHTML( - wfMsg( 'editcomment', - $wgUser->getSkin()->formatComment( $current->getComment() ) ) ); - } - return; - } - - # Get the last edit not by this guy - $user = intval( $current->getUser() ); - $user_text = $dbw->addQuotes( $current->getUserText() ); - $s = $dbw->selectRow( 'revision', - array( 'rev_id', 'rev_timestamp' ), + /** Backend rollback implementation. UI logic is in rollback() + * @param string $user - Name of the user whose edits to rollback. + * @param string $token - Rollback token. + * @param bool $bot - If true, mark all reverted edits as bot. + * @param string $summary - Custom summary. Set to default summary if empty. + * @param array $info - Reference to associative array that will be set to contain the revision ID, edit summary, etc. + * @return ROLLBACK_SUCCES on succes, ROLLBACK_* on failure + */ + public function doRollback($user, $token, $bot = false, $summary = "", &$info = NULL) + { + global $wgUser, $wgUseRCPatrol; + if(!$wgUser->isAllowed('rollback')) + return ROLLBACK_PERM; + if($wgUser->isBlocked()) + return ROLLBACK_BLOCKED; + if(wfReadOnly()) + return ROLLBACK_READONLY; + + // Check token first + if(!$wgUser->matchEditToken($token, array($this->mTitle->getPrefixedText(), $user))) + return ROLLBACK_BADTOKEN; + + $dbw = wfGetDB(DB_MASTER); + $current = Revision::newFromTitle($this->mTitle); + if(is_null($current)) + return ROLLBACK_BADARTICLE; + + // Check if someone else was there first + if($user != $current->getUserText()) + { + $info['usertext'] = $current->getUserText(); + $info['comment'] = $current->getComment(); + return ROLLBACK_ALREADYROLLED; + } + // Get the last edit not by $user + $userid = intval($current->getUser()); + $s = $dbw->selectRow('revision', + array('rev_id', 'rev_timestamp'), array( 'rev_page' => $current->getPage(), - "rev_user <> {$user} OR rev_user_text <> {$user_text}" + "rev_user <> $userid OR rev_user_text <> {$dbw->addQuotes($user)}" ), __METHOD__, array( 'USE INDEX' => 'page_timestamp', - 'ORDER BY' => 'rev_timestamp DESC' ) - ); - if( $s === false ) { - # Something wrong - $wgOut->setPageTitle(wfMsg('rollbackfailed')); - $wgOut->addHTML( wfMsg( 'cantrollback' ) ); - return; - } + 'ORDER BY' => 'rev_timestamp DESC' + )); + if($s === false) + return ROLLBACK_ONLYAUTHOR; + $target = Revision::newFromID($s->rev_id); + // If the reverted edits should be marked bot or patrolled, do so $set = array(); - if ( $bot ) { - # Mark all reverted edits as bot + if($bot) $set['rc_bot'] = 1; - } - if ( $wgUseRCPatrol ) { - # Mark all reverted edits as patrolled + if($wgUseRCPatrol) $set['rc_patrolled'] = 1; - } - - if ( $set ) { - $dbw->update( 'recentchanges', $set, - array( /* WHERE */ - 'rc_cur_id' => $current->getPage(), - 'rc_user_text' => $current->getUserText(), - "rc_timestamp > '{$s->rev_timestamp}'", - ), __METHOD__ - ); - } - - # Get the edit summary - $target = Revision::newFromId( $s->rev_id ); - $newComment = wfMsgForContent( 'revertpage', $target->getUserText(), $from ); - $newComment = $wgRequest->getText( 'summary', $newComment ); - - # Save it! - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $wgOut->addHTML( '

' . htmlspecialchars( $newComment ) . "

\n
\n" ); + if($set) + $dbw->update('recentchanges', $set, + array( + 'rc_cur_id' => $current->getPage(), + 'rc_user_text' => $user, + "rc_timestamp > '{$s->rev_timestamp}'" + ), __METHOD__ + ); - $this->updateArticle( $target->getText(), $newComment, 1, $this->mTitle->userIsWatching(), $bot ); + // Generate an edit summary + if(empty($summary)) + $summary = wfMsgForContent('revertpage', $target->getUserText(), $user); + + // Now we *finally* get to commit the edit + $flags = EDIT_UPDATE | EDIT_MINOR; + if($bot) + $flags |= EDIT_FORCE_BOT; + if(!$this->doEdit($target->getText(), $summary, $flags)) + return ROLLBACK_EDITFAILED; + + if(is_null($info)) + // Save time + return ROLLBACK_SUCCESS; + + $info['title'] = $this->mTitle->getPrefixedText(); + $info['pageid'] = $current->getPage(); + $info['summary'] = $summary; + // NOTE: If the rollback turned out to be a null edit, revid and old_revid will be equal + $info['revid'] = $this->mTitle->getLatestRevID(); // The revid of your rollback + $info['old_revid'] = $current->getId(); // The revid of the last edit before your rollback + $info['last_revid'] = $s->rev_id; // The revid of the last edit that was not rolled back + $info['user'] = $user; // The name of the victim + $info['userid'] = $userid; // And their userid + $info['to'] = $target->getUserText(); // The user whose last version was reverted to + if($bot) + $info['bot'] = ""; + return ROLLBACK_SUCCESS; + } + + /** UI entry point for rollbacks. Relies on doRollback() to do the hard work */ + function rollback() { + global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol; - $wgOut->returnToMain( false ); + // Basically, we just call doRollback() and interpret its return value + $info = array(); + $retval = $this->doRollback($wgRequest->getVal('from'), $wgRequest->getVal('token'), $wgRequest->getBool('bot'), + $wgRequest->getText('summary'), &$info); + switch($retval) + { + case ROLLBACK_SUCCESS: + case ROLLBACK_EDITFAILED: // Is ignored + $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->addHTML( '

' . htmlspecialchars( $info['summary'] ) . "

\n
\n" ); + $this->doRedirect(true); + $wgOut->returnToMain(false); + return; + case ROLLBACK_PERM: + $wgOut->permissionRequired('rollback'); + return; + case ROLLBACK_BLOCKED: + $wgOut->blockedPage(); + return; + case ROLLBACK_READONLY: + $wgOut->readOnlyPage($this->getContent()); + return; + case ROLLBACK_BADTOKEN: + $wgOut->setPageTitle(wfMsg('rollbackfailed')); + $wgOut->addWikiText(wfMsg('sessionfailure')); + return; + case ROLLBACK_BADARTICLE: + $wgOut->addHTML(wfMsg('notanarticle')); + return; + case ROLLBACK_ALREADYROLLED: + $wgOut->setPageTitle(wfMsg('rollbackfailed')); + $wgOut->addWikiText(wfMsg('alreadyrolled', + htmlspecialchars($this->mTitle->getPrefixedText()), + htmlspecialchars($wgRequest->getVal('from')), + htmlspecialchars($info['usertext']))); + if($info['comment'] != '') + $wgOut->addHTML(wfMsg('editcomment', + $wgUser->getSkin()->formatComment($info['comment']))); + return; + case ROLLBACK_ONLYAUTHOR: + $wgOut->setPageTitle(wfMsg('rollbackfailed')); + $wgOut->addHTML(wfMsg('cantrollback')); + return; + } } - /** * Do standard deferred updates after page view * @private diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index fbd01a127b..670bd06f74 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -322,6 +322,7 @@ function __autoload($className) { 'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php', 'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php', 'ApiResult' => 'includes/api/ApiResult.php', + 'ApiRollback' => 'includes/api/ApiRollback.php' ); wfProfileIn( __METHOD__ ); diff --git a/includes/Defines.php b/includes/Defines.php index c923c25605..3148c76b4b 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -195,6 +195,19 @@ define( 'EDIT_DEFER_UPDATES', 32 ); define( 'EDIT_AUTOSUMMARY', 64 ); /**#@-*/ +/**#@+ + * Article::doRollback() return values + */ +define('ROLLBACK_SUCCES', 0); +define('ROLLBACK_PERM', 1); // Permission denied +define('ROLLBACK_BLOCKED', 2); // User has been blocked +define('ROLLBACK_READONLY', 3); // Wiki is in read-only mode +define('ROLLBACK_BADTOKEN', 4); // Invalid token specified +define('ROLLBACK_BADARTICLE', 5); // $article is not a valid Article +define('ROLLBACK_ALREADYROLLED', 6); // Someone else already rolled this back. $info['usertext'] and $info['comment'] will be set +define('ROLLBACK_ONLYAUTHOR', 7); // User is the only author of the page +define('ROLLBACK_EDITFAILED', 8); // Article::doEdit() failed. This is a very weird error + /** * Flags for Database::makeList() * These are also available as Database class constants diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index fda6f884e0..b12a325e8a 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -54,6 +54,7 @@ class ApiMain extends ApiBase { private static $Modules = array ( 'login' => 'ApiLogin', 'query' => 'ApiQuery', + 'rollback' => 'ApiRollback', 'opensearch' => 'ApiOpenSearch', 'feedwatchlist' => 'ApiFeedWatchlist', 'help' => 'ApiHelp', @@ -104,6 +105,18 @@ class ApiMain extends ApiBase { $this->mRequest = & $request; $this->mSquidMaxage = 0; + + global $wgUser, $wgCookiePrefix; + if(session_id() == '') + wfSetupSession(); + // Reinit $wgUser with info from lg* or the session data. The former overrides the latter + if(isset($_REQUEST['lguserid']) && isset($_REQUEST['lgusername']) && isset($_REQUEST['lgtoken'])) + { + $_SESSION['wsUserID'] = $_REQUEST['lguserid']; + $_SESSION['wsUserName'] = $_REQUEST['lgusername']; + $_SESSION['wsToken'] = $_REQUEST['lgtoken']; + } + $wgUser = User::newFromSession(); } /** diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index 715c4a204e..b67754f13e 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -49,13 +49,17 @@ class ApiQueryInfo extends ApiQueryBase { } public function execute() { + global $wgUser; $params = $this->extractRequestParams(); $fld_protection = false; if(!is_null($params['prop'])) { $prop = array_flip($params['prop']); $fld_protection = isset($prop['protection']); + $fld_lastrevby = isset($prop['lastrevby']); } + if(!is_null($params['tokens'])) + $params['tokens'] = array_flip($params['tokens']); $pageSet = $this->getPageSet(); $titles = $pageSet->getGoodTitles(); @@ -85,13 +89,18 @@ class ApiQueryInfo extends ApiQueryBase { $db->freeResult($res); } - foreach ( $titles as $pageid => $unused ) { + foreach ( $titles as $pageid => $title ) { $pageInfo = array ( 'touched' => wfTimestamp(TS_ISO_8601, $pageTouched[$pageid]), 'lastrevid' => intval($pageLatest[$pageid]), 'counter' => intval($pageCounter[$pageid]), - 'length' => intval($pageLength[$pageid]), + 'length' => intval($pageLength[$pageid]) ); + if(isset($params['tokens']) || $fld_lastrevby) + { + $lastrev = Revision::newFromId($pageInfo['lastrevid']); + $pageInfo['lastrevby'] = $lastrev->getUserText(); + } if ($pageIsRedir[$pageid]) $pageInfo['redirect'] = ''; @@ -108,6 +117,31 @@ class ApiQueryInfo extends ApiQueryBase { } } + $tokenArr = array(); + foreach($params['tokens'] as $token => $unused) + switch($token) + { + case 'rollback': + $tokenArr[$token] = $wgUser->editToken(array($title->getPrefixedText(), $pageInfo['lastrevby'])); + break; + case 'edit': + case 'move': + case 'delete': + case 'undelete': + case 'protect': + case 'unprotect': + if($wgUser->isAnon()) + $tokenArr[$token] = EDIT_TOKEN_SUFFIX; + else + $tokenArr[$token] = $wgUser->editToken(); + // default: can't happen, ignore it if it does happen in some weird way + } + if(count($tokenArr) > 0) + { + $pageInfo['tokens'] = $tokenArr; + $result->setIndexedTagName($pageInfo['tokens'], 't'); + } + $result->addValue(array ( 'query', 'pages' @@ -121,7 +155,20 @@ class ApiQueryInfo extends ApiQueryBase { ApiBase :: PARAM_DFLT => NULL, ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => array ( - 'protection' + 'protection', + 'lastrevby' + )), + 'tokens' => array( + ApiBase :: PARAM_DFLT => NULL, + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array( + 'edit', + 'move', + 'delete', + 'undelete', + 'rollback', + 'protect', + 'unprotect' )) ); } @@ -130,8 +177,10 @@ class ApiQueryInfo extends ApiQueryBase { return array ( 'prop' => array ( 'Which additional properties to get:', - ' "protection" - List the protection level of each page' - ) + ' "protection" - List the protection level of each page', + ' "lastrevby" - The name of the user who made the last edit. You may need this for action=rollback.' + ), + 'tokens' => 'Which tokens to get.' ); } @@ -143,7 +192,8 @@ class ApiQueryInfo extends ApiQueryBase { protected function getExamples() { return array ( 'api.php?action=query&prop=info&titles=Main%20Page', - 'api.php?action=query&prop=info&inprop=protection&titles=Main%20Page' + 'api.php?action=query&prop=info&inprop=protection&titles=Main%20Page', + 'api.php?action=query&prop=info&intokens=edit|rollback&titles=Main%20Page' ); } diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php new file mode 100644 index 0000000000..c2ba38ff0f --- /dev/null +++ b/includes/api/ApiRollback.php @@ -0,0 +1,134 @@ +.@home.nl + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + +/** + * @addtogroup API + */ +class ApiRollback extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + global $wgUser; + $params = $this->extractRequestParams(); + + $titleObj = NULL; + if(!isset($params['title'])) + $this->dieUsage('The title parameter must be set', 'notarget'); + if(!isset($params['user'])) + $this->dieUsage('The user parameter must be set', 'nouser'); + if(!isset($params['token'])) + $this->dieUsage('The token parameter must be set', 'notoken'); + + // doRollback() also checks for these, but we wanna save some work + if(!$wgUser->isAllowed('rollback')) + $this->dieUsage('You don\'t have permission to rollback', 'permissiondenied'); + if($wgUser->isBlocked()) + $this->dieUsage('You have been blocked from editing', 'blocked'); + if(wfReadOnly()) + $this->dieUsage('The wiki is in read-only mode', 'readonly'); + + $titleObj = Title::newFromText($params['title']); + if(!$titleObj) + $this->dieUsage("bad title {$params['title']}", 'invalidtitle'); + + $articleObj = new Article($titleObj); + $summary = (isset($params['summary']) ? $params['summary'] : ""); + $info = array(); + $retval = $articleObj->doRollback($params['user'], $params['token'], isset($params['markbot']), $summary, &$info); + + switch($retval) + { + case ROLLBACK_SUCCESS: + break; // We'll deal with that later + case ROLLBACK_PERM: + $this->dieUsage('You don\'t have permission to rollback', 'permissiondenied'); + case ROLLBACK_BLOCKED: // If we get BLOCKED or PERM that's very weird, but it's possible + $this->dieUsage('You have been blocked from editing', 'blocked'); + case ROLLBACK_READONLY: + $this->dieUsage('The wiki is in read-only mode', 'readonly'); + case ROLLBACK_BADTOKEN: + $this->dieUsage('Invalid token', 'badtoken'); + case ROLLBACK_BADARTICLE: + $this->dieUsage("The article ``{$params['title']}'' doesn't exist", 'missingtitle'); + case ROLLBACK_ALREADYROLLED: + $this->dieUsage('The edit(s) you tried to rollback is/are already rolled back', 'alreadyrolled'); + case ROLLBACK_ONLYAUTHOR: + $this->dieUsage("{$params['user']} is the only author of the page", 'onlyauthor'); + case ROLLBACK_EDITFAILED: + $this->dieDebug(__METHOD__, 'Article::doEdit() failed'); + default: + // rollback() has apparently invented a new error, which is extremely weird + $this->dieDebug(__METHOD__, "rollback() returned an unknown error ($retval)"); + } + // $retval has to be ROLLBACK_SUCCESS if we get here + $this->getResult()->addValue(null, 'rollback', $info); + } + + protected function getAllowedParams() { + return array ( + 'title' => null, + 'user' => null, + 'token' => null, + 'summary' => null, + 'markbot' => null + ); + } + + protected function getParamDescription() { + return array ( + 'title' => 'Title of the page you want to rollback.', + 'user' => 'Name of the user whose edits are to be rolled back. If set incorrectly, you\'ll get a badtoken error.', + 'token' => 'A rollback token previously retrieved through prop=info', + 'summary' => 'Custom edit summary. If not set, default summary will be used.', + 'markbot' => 'Mark the reverted edits and the revert as bot edits' + ); + } + + protected function getDescription() { + return array( + 'Undoes the last edit to the page. If the last user who edited the page made multiple edits in a row,', + 'they will all be rolled back. You need to be logged in as a sysop to use this function, see also action=login.' + ); + } + + protected function getExamples() { + return array ( + 'api.php?action=rollback&title=Main%20Page&user=Catrope&token=123ABC', + 'api.php?action=rollback&title=Main%20Page&user=217.121.114.116&token=123ABC&summary=Reverting%20vandalism&markbot=1' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiRollback.php 22289 2007-05-20 23:31:44Z yurik $'; + } +} +?>