* Separating UI code and DB code in Article::rollback()
authorRoan Kattouw <catrope@users.mediawiki.org>
Fri, 29 Jun 2007 19:55:46 +0000 (19:55 +0000)
committerRoan Kattouw <catrope@users.mediawiki.org>
Fri, 29 Jun 2007 19:55:46 +0000 (19:55 +0000)
* Adding API rollback functionality

includes/Article.php
includes/AutoLoader.php
includes/Defines.php
includes/api/ApiMain.php
includes/api/ApiQueryInfo.php
includes/api/ApiRollback.php [new file with mode: 0644]

index 9f3795e..681a56f 100644 (file)
@@ -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( '<h2>' . htmlspecialchars( $newComment ) . "</h2>\n<hr />\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( '<h2>' . htmlspecialchars( $info['summary'] ) . "</h2>\n<hr />\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
index fbd01a1..670bd06 100644 (file)
@@ -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__ );
index c923c25..3148c76 100644 (file)
@@ -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
index fda6f88..b12a325 100644 (file)
@@ -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();
        }
 
        /**
index 715c4a2..b67754f 100644 (file)
@@ -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 (file)
index 0000000..c2ba38f
--- /dev/null
@@ -0,0 +1,134 @@
+<?php
+
+/*
+ * Created on Jun 20, 2007
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@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 $';
+       }
+}
+?>