jenkins-bot has submitted this change and it was merged.

Change subject: RollbackAction: Implement AJAX interface and require POST
......................................................................


RollbackAction: Implement AJAX interface and require POST

Similar to WatchAction (converted in commit 77cdf1919).

* Make FormAction::getFormFields not abstract.
  In most cases this will just be an empty array.

* Convert RollbackAction from FormlessAction to FormAction and implement the
  required error handling scenarios (mostly moved out of from the old method, or
  duplicated from the WikiPage method where necessary).

* In most cases the in-between form is never used since a JavaScript handler
  takes over the link and uses the API over AJAX instead. In the no-js fallback
  (as well as for any existing tokenless rollback links) copy the GET parameters
  into the form for re-submission as POST (plus token, added by HTMLForm).

* Remove the distinction between WebUI and API tokens. This stronger token salt 
made it
  unnecessarily complex and was only there because it used GET until now. This 
streamlining of
  tokens matches what we already do for 'watch', 'edit', 'patrol' and other 
actions.

* Fix form submission bugs when 'from' query parameter is missing.

  - Ensure the required 'from' query parameter is present before showing a form.
    No need for the user to submit a form we know will fail.

  - Plain GET request to action=rollback (with no parameters) is now a 400 Bad 
Request
    instead of a form that would fail when submitted.

  - Submitting the form without 'form' field now correctly says why it failed.
    Previously it emitted a session error, which was a lie.

Bug: T88044
Change-Id: Ia457802fec2e90573c8e7d552bc1f3cee258f10b
---
M RELEASE-NOTES-1.28
M includes/Linker.php
M includes/actions/RollbackAction.php
M includes/api/ApiRollback.php
M includes/page/WikiPage.php
M languages/i18n/en.json
M languages/i18n/qqq.json
M resources/Resources.php
A resources/src/mediawiki/api/rollback.js
M resources/src/mediawiki/page/patrol.ajax.js
A resources/src/mediawiki/page/rollback.js
M resources/src/mediawiki/page/watch.js
12 files changed, 247 insertions(+), 70 deletions(-)

Approvals:
  Aaron Schulz: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/RELEASE-NOTES-1.28 b/RELEASE-NOTES-1.28
index e365486..76728c8 100644
--- a/RELEASE-NOTES-1.28
+++ b/RELEASE-NOTES-1.28
@@ -13,7 +13,7 @@
 === New features in 1.28 ===
 * User::isBot() method for checking if an account is a bot role account.
 * Added a new hook, 'UserIsBot', to aid in determining if a user is a bot.
-
+* (T88044) Implemented one-click rollback handling via AJAX.
 
 === External library changes in 1.28 ===
 
diff --git a/includes/Linker.php b/includes/Linker.php
index 6a869dd..b81218f 100644
--- a/includes/Linker.php
+++ b/includes/Linker.php
@@ -1872,7 +1872,9 @@
         * work if $wgShowRollbackEditCount is disabled, so this can only 
function
         * as an additional check.
         *
-        * If the option noBrackets is set the rollback link wont be enclosed 
in []
+        * If the option noBrackets is set the rollback link wont be enclosed 
in "[]".
+        *
+        * See the "mediawiki.page.rollback" module for the client-side 
handling of this link.
         *
         * @since 1.16.3. $context added in 1.20. $options added in 1.21
         *
@@ -1901,6 +1903,8 @@
                if ( !in_array( 'noBrackets', $options, true ) ) {
                        $inner = $context->msg( 'brackets' )->rawParams( $inner 
)->escaped();
                }
+
+               $context->getOutput()->addModules( 'mediawiki.page.rollback' );
 
                return '<span class="mw-rollback-link">' . $inner . '</span>';
        }
@@ -1996,11 +2000,13 @@
                $query = [
                        'action' => 'rollback',
                        'from' => $rev->getUserText(),
-                       'token' => $context->getUser()->getEditToken( [
-                               $title->getPrefixedText(),
-                               $rev->getUserText()
-                       ] ),
                ];
+               $attrs = [
+                       'data-mw' => 'interface',
+                       'title' => $context->msg( 'tooltip-rollback' )->text(),
+               ];
+               $options = [ 'known', 'noclasses' ];
+
                if ( $context->getRequest()->getBool( 'bot' ) ) {
                        $query['bot'] = '1';
                        $query['hidediff'] = '1'; // bug 15999
@@ -2025,27 +2031,16 @@
                        }
 
                        if ( $editCount > $wgShowRollbackEditCount ) {
-                               $editCount_output = $context->msg( 
'rollbacklinkcount-morethan' )
+                               $html = $context->msg( 
'rollbacklinkcount-morethan' )
                                        ->numParams( $wgShowRollbackEditCount 
)->parse();
                        } else {
-                               $editCount_output = $context->msg( 
'rollbacklinkcount' )->numParams( $editCount )->parse();
+                               $html = $context->msg( 'rollbacklinkcount' 
)->numParams( $editCount )->parse();
                        }
 
-                       return self::link(
-                               $title,
-                               $editCount_output,
-                               [ 'title' => $context->msg( 'tooltip-rollback' 
)->text() ],
-                               $query,
-                               [ 'known', 'noclasses' ]
-                       );
+                       return self::link( $title, $html, $attrs, $query, 
$options );
                } else {
-                       return self::link(
-                               $title,
-                               $context->msg( 'rollbacklink' )->escaped(),
-                               [ 'title' => $context->msg( 'tooltip-rollback' 
)->text() ],
-                               $query,
-                               [ 'known', 'noclasses' ]
-                       );
+                       $html = $context->msg( 'rollbacklink' )->escaped();
+                       return self::link( $title, $html, $attrs, $query, 
$options );
                }
        }
 
diff --git a/includes/actions/RollbackAction.php 
b/includes/actions/RollbackAction.php
index d002da8..e32582e 100644
--- a/includes/actions/RollbackAction.php
+++ b/includes/actions/RollbackAction.php
@@ -25,7 +25,7 @@
  *
  * @ingroup Actions
  */
-class RollbackAction extends FormlessAction {
+class RollbackAction extends FormAction {
 
        public function getName() {
                return 'rollback';
@@ -35,39 +35,79 @@
                return 'rollback';
        }
 
-       public function onView() {
-               // TODO: use $this->useTransactionalTimeLimit(); when POST only
-               wfTransactionalTimeLimit();
+       protected function preText() {
+               return $this->msg( 'confirm-rollback-top' )->parse();
+       }
 
-               $details = null;
+       protected function alterForm( HTMLForm $form ) {
+               $form->setSubmitTextMsg( 'confirm-rollback-button' );
+               $form->setTokenSalt( 'rollback' );
+
+               // Copy parameters from GET to confirmation form
+               $from = $this->getRequest()->getVal( 'from' );
+               if ( $from === null ) {
+                       throw new BadRequestError( 'rollbackfailed', 
'rollback-missingparam' );
+               }
+               foreach ( [ 'from', 'bot', 'hidediff', 'summary' ] as $param ) {
+                       $val = $this->getRequest()->getVal( $param );
+                       if ( $val !== null ) {
+                               $form->addHiddenField( $param, $val );
+                       }
+               }
+       }
+
+       /**
+        * This must return true so that HTMLForm::show() will not display the 
form again after
+        * submission. For rollback, display either the form or the result 
(success/error)
+        * not both.
+        *
+        * @return bool
+        * @throws ErrorPageError
+        */
+       public function onSubmit( $data ) {
+               $this->useTransactionalTimeLimit();
 
                $request = $this->getRequest();
                $user = $this->getUser();
+               $from = $request->getVal( 'from' );
+               $rev = $this->page->getRevision();
+               if ( $from === null || $from === '' ) {
+                       throw new ErrorPageError( 'rollbackfailed', 
'rollback-missingparam' );
+               }
+               if ( $from !== $rev->getUserText() ) {
+                       throw new ErrorPageError( 'rollbackfailed', 
'alreadyrolled', [
+                               $this->getTitle()->getPrefixedText(),
+                               $from,
+                               $rev->getUserText()
+                       ] );
+               }
 
-               $result = $this->page->doRollback(
-                       $request->getVal( 'from' ),
+               $data = null;
+               $errors = $this->page->doRollback(
+                       $from,
                        $request->getText( 'summary' ),
-                       $request->getVal( 'token' ),
+                       // Provided by HTMLForm
+                       $request->getVal( 'wpEditToken' ),
                        $request->getBool( 'bot' ),
-                       $details,
+                       $data,
                        $this->getUser()
                );
 
-               if ( in_array( [ 'actionthrottledtext' ], $result ) ) {
+               if ( in_array( [ 'actionthrottledtext' ], $errors ) ) {
                        throw new ThrottledError;
                }
 
-               if ( isset( $result[0][0] ) &&
-                       ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 
'cantrollback' )
+               if ( isset( $errors[0][0] ) &&
+                       ( $errors[0][0] == 'alreadyrolled' || $errors[0][0] == 
'cantrollback' )
                ) {
                        $this->getOutput()->setPageTitle( $this->msg( 
'rollbackfailed' ) );
-                       $errArray = $result[0];
+                       $errArray = $errors[0];
                        $errMsg = array_shift( $errArray );
                        $this->getOutput()->addWikiMsgArray( $errMsg, $errArray 
);
 
-                       if ( isset( $details['current'] ) ) {
+                       if ( isset( $data['current'] ) ) {
                                /** @var Revision $current */
-                               $current = $details['current'];
+                               $current = $data['current'];
 
                                if ( $current->getComment() != '' ) {
                                        $this->getOutput()->addHTML( 
$this->msg( 'editcomment' )->rawParams(
@@ -75,25 +115,24 @@
                                }
                        }
 
-                       return;
+                       return true;
                }
 
                # NOTE: Permission errors already handled by 
Action::checkExecute.
-
-               if ( $result == [ [ 'readonlytext' ] ] ) {
+               if ( $errors == [ [ 'readonlytext' ] ] ) {
                        throw new ReadOnlyError;
                }
 
                # XXX: Would be nice if ErrorPageError could take multiple 
errors, and/or a status object.
-               #     Right now, we only show the first error
-               foreach ( $result as $error ) {
+               #      Right now, we only show the first error
+               foreach ( $errors as $error ) {
                        throw new ErrorPageError( 'rollbackfailed', $error[0], 
array_slice( $error, 1 ) );
                }
 
                /** @var Revision $current */
-               $current = $details['current'];
-               $target = $details['target'];
-               $newId = $details['newid'];
+               $current = $data['current'];
+               $target = $data['target'];
+               $newId = $data['newid'];
                $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' 
) );
                $this->getOutput()->setRobotPolicy( 'noindex,nofollow' );
 
@@ -121,6 +160,12 @@
                        );
                        $de->showDiff( '', '' );
                }
+               return true;
+       }
+
+       public function onSuccess() {
+               // Required by parent class, but redundant because onSubmit 
already shows
+               // the success message when needed.
        }
 
        protected function getDescription() {
diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php
index 55f7143..b9911da 100644
--- a/includes/api/ApiRollback.php
+++ b/includes/api/ApiRollback.php
@@ -45,16 +45,6 @@
                $user = $this->getUser();
                $params = $this->extractRequestParams();
 
-               // WikiPage::doRollback needs a Web UI token, so get one of 
those if we
-               // validated based on an API rollback token.
-               $token = $params['token'];
-               if ( $user->matchEditToken( $token, 'rollback', 
$this->getRequest() ) ) {
-                       $token = $this->getUser()->getEditToken(
-                               $this->getWebUITokenSalt( $params ),
-                               $this->getRequest()
-                       );
-               }
-
                $titleObj = $this->getRbTitle( $params );
                $pageObj = WikiPage::factory( $titleObj );
                $summary = $params['summary'];
@@ -72,15 +62,30 @@
                $retval = $pageObj->doRollback(
                        $this->getRbUser( $params ),
                        $summary,
-                       $token,
+                       $params['token'],
                        $params['markbot'],
                        $details,
                        $user,
                        $params['tags']
                );
 
+               // We don't care about multiple errors, just report one of them
                if ( $retval ) {
-                       // We don't care about multiple errors, just report one 
of them
+                       if ( isset( $retval[0][0] ) &&
+                               ( $retval[0][0] == 'alreadyrolled' || 
$retval[0][0] == 'cantrollback' )
+                       ) {
+                               $error = $retval[0];
+                               $userMessage = $this->msg( $error[0], 
array_slice( $error, 1 ) );
+                               // dieUsageMsg() doesn't support $extraData
+                               $errorCode = $error[0];
+                               $errorInfo = isset( 
ApiBase::$messageMap[$errorCode] ) ?
+                                       
ApiBase::$messageMap[$errorCode]['info'] :
+                                       $errorCode;
+                               $this->dieUsage( $errorInfo, $errorCode, 0, [
+                                       'messageHtml' => 
$userMessage->parseAsBlock()
+                               ] );
+                       }
+
                        $this->dieUsageMsg( reset( $retval ) );
                }
 
@@ -97,9 +102,22 @@
                        'pageid' => intval( $details['current']->getPage() ),
                        'summary' => $details['summary'],
                        'revid' => intval( $details['newid'] ),
+                       // The revision being reverted (previously the current 
revision of the page)
                        'old_revid' => intval( $details['current']->getID() ),
+                       // The revision being restored (the last revision 
before revision(s) by the reverted user)
                        'last_revid' => intval( $details['target']->getID() )
                ];
+
+               $oldUser = $details['current']->getUserText( 
Revision::FOR_THIS_USER );
+               $lastUser = $details['target']->getUserText( 
Revision::FOR_THIS_USER );
+               $diffUrl = $titleObj->getFullURL( [
+                       'diff' => $info['revid'],
+                       'oldid' => $info['old_revid'],
+                       'diffonly' => '1'
+               ] );
+               $info['messageHtml'] = $this->msg( 'rollback-success-notify' )
+                       ->params( $oldUser, $lastUser, $diffUrl )
+                       ->parseAsBlock();
 
                $this->getResult()->addValue( null, $this->getModuleName(), 
$info );
        }
@@ -146,13 +164,6 @@
 
        public function needsToken() {
                return 'rollback';
-       }
-
-       protected function getWebUITokenSalt( array $params ) {
-               return [
-                       $this->getRbTitle( $params )->getPrefixedText(),
-                       $this->getRbUser( $params )
-               ];
        }
 
        /**
diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php
index 8702156..cf533d6 100644
--- a/includes/page/WikiPage.php
+++ b/includes/page/WikiPage.php
@@ -2993,6 +2993,7 @@
         * to do the dirty work
         *
         * @todo Separate the business/permission stuff out from backend code
+        * @todo Remove $token parameter. Already verified by RollbackAction 
and ApiRollback.
         *
         * @param string $fromP Name of the user whose edits to rollback.
         * @param string $summary Custom summary. Set to default summary if 
empty.
@@ -3023,7 +3024,7 @@
                $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 
'rollback', $user );
                $errors = array_merge( $editErrors, wfArrayDiff2( 
$rollbackErrors, $editErrors ) );
 
-               if ( !$user->matchEditToken( $token, [ 
$this->mTitle->getPrefixedText(), $fromP ] ) ) {
+               if ( !$user->matchEditToken( $token, 'rollback' ) ) {
                        $errors[] = [ 'sessionfailure' ];
                }
 
diff --git a/languages/i18n/en.json b/languages/i18n/en.json
index 3000a54..2ae80e2 100644
--- a/languages/i18n/en.json
+++ b/languages/i18n/en.json
@@ -2131,12 +2131,14 @@
        "rollbacklinkcount": "rollback $1 {{PLURAL:$1|edit|edits}}",
        "rollbacklinkcount-morethan": "rollback more than $1 
{{PLURAL:$1|edit|edits}}",
        "rollbackfailed": "Rollback failed",
+       "rollback-missingparam": "Missing required parameters on request.",
        "cantrollback": "Cannot revert edit;\nlast contributor is only author 
of this page.",
        "alreadyrolled": "Cannot rollback last edit of [[:$1]] by 
[[User:$2|$2]] ([[User 
talk:$2|talk]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]);\nsomeone
 else has edited or rolled back the page already.\n\nThe last edit to the page 
was by [[User:$3|$3]] ([[User 
talk:$3|talk]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
        "editcomment": "The edit summary was: <em>$1</em>.",
        "revertpage": "Reverted edits by [[Special:Contributions/$2|$2]] 
([[User talk:$2|talk]]) to last revision by [[User:$1|$1]]",
        "revertpage-nouser": "Reverted edits by a hidden user to last revision 
by {{GENDER:$1|[[User:$1|$1]]}}",
        "rollback-success": "Reverted edits by $1;\nchanged back to last 
revision by $2.",
+       "rollback-success-notify": "Reverted edits by $1;\nchanged back to last 
revision by $2. [$3 Show changes]",
        "sessionfailure-title": "Session failure",
        "sessionfailure": "There seems to be a problem with your login 
session;\nthis action has been canceled as a precaution against session 
hijacking.\nGo back to the previous page, reload that page and then try again.",
        "changecontentmodel" : "Change content model of a page",
@@ -3381,6 +3383,8 @@
        "confirm-watch-top": "Add this page to your watchlist?",
        "confirm-unwatch-button": "OK",
        "confirm-unwatch-top": "Remove this page from your watchlist?",
+       "confirm-rollback-button": "OK",
+       "confirm-rollback-top": "Revert edits to this page?",
        "semicolon-separator": ";&#32;",
        "comma-separator": ",&#32;",
        "colon-separator": ":&#32;",
diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json
index f6d2f41..c301616 100644
--- a/languages/i18n/qqq.json
+++ b/languages/i18n/qqq.json
@@ -2310,12 +2310,14 @@
        "rollbacklinkcount": "{{doc-actionlink}}\nText of the rollback link 
showing the number of edits to be rolled back. See also 
{{msg-mw|rollbacklink}}.\n\nParameters:\n* $1 - the number of edits that will 
be rolled back. If $1 is over the value of 
<code>$wgShowRollbackEditCount</code> (default: 10) 
{{msg-mw|rollbacklinkcount-morethan}} is used.\n\nThe rollback link is 
displayed with a tooltip {{msg-mw|Tooltip-rollback}}",
        "rollbacklinkcount-morethan": "{{doc-actionlink}}\nText of the rollback 
link when a greater number of edits is to be rolled back. See also 
{{msg-mw|rollbacklink}}.\n\nWhen the number of edits rolled back is smaller 
than 
[[mw:Special:MyLanguage/Manual:$wgShowRollbackEditCount|$wgShowRollbackEditCount]],
 {{msg-mw|rollbacklinkcount}} is used instead.\n\nParameters:\n* $1 - number of 
edits",
        "rollbackfailed": "{{Identical|Rollback}}",
-       "cantrollback": "Used as error message when rolling back.\n\nSee 
also:\n* 
{{msg-mw|Notvisiblerev}}\n{{Identical|Revert}}\n{{Identical|Rollback}}",
+       "rollback-missingparam": "Used as error message rollback is accessed 
without the required parameters\n\nSee also:\n* {{msg-mw|Rollbackfailed}}\n.",
+       "cantrollback": "Used as error message when rollback fails due to there 
not being a valid revision to revert back to.\n\nSee also:\n* 
{{msg-mw|Notvisiblerev}}\n{{Identical|Revert}}\n{{Identical|Rollback}}",
        "alreadyrolled": "Appear when there's rollback and/or edit 
collision.\n\nRefers to:\n* {{msg-mw|Pipe-separator}}\n* 
{{msg-mw|Contribslink}}\nParameters:\n* $1 - the page to be rolled back\n* $2 - 
the editor to be rolled-back of that page\n* $3 - the editor that cause 
collision\n{{Identical|Rollback}}",
        "editcomment": "Only shown if there is an edit {{msg-mw|Summary}}. 
Parameters:\n* $1 - the edit summary",
        "revertpage": "Parameters:\n* $1 - username 1\n* $2 - username 2\n* $3 
- (Optional) revision ID of the revision reverted to\n* $4 - (Optional) 
timestamp of the revision reverted to\n* $5 - (Optional) revision ID of the 
revision reverted from\n* $6 - (Optional) timestamp of the revision reverted 
from\nSee also:\n* {{msg-mw|Revertpage-nouser}}\n{{Identical|Revert}}",
        "revertpage-nouser": "This is a confirmation message a user sees after 
reverting, when the username of the version is hidden with 
RevisionDelete.\n\nIn other cases the message {{msg-mw|Revertpage}} is 
used.\n\nParameters:\n* $1 - username 1, can be used for GENDER\n* $2 - 
(Optional) username 2\n* $3 - (Optional) revision ID of the revision reverted 
to\n* $4 - (Optional) timestamp of the revision reverted to\n* $5 - (Optional) 
revision ID of the revision reverted from\n* $6 - (Optional) timestamp of the 
revision reverted from",
        "rollback-success": "This message shows up on screen after successful 
revert (generally visible only to admins). $1 describes user whose changes have 
been reverted, $2 describes user which produced version, which replaces 
reverted version.\n{{Identical|Revert}}\n{{Identical|Rollback}}",
+       "rollback-success-notify": "Notification shown after a successful 
revert.\n* $1 - User whose changes have been reverted\n* $2 - User that made 
the edit that was restored\n* $3 - Url to the diff of the rollback\nSee 
also:\n*{{mw-msg|showdiff}}\n{{Identical|rollback-success}}\n{{Format|jquerymsg}}",
        "sessionfailure-title": "Used as title of the error message 
{{msg-mw|Sessionfailure}}.",
        "sessionfailure": "Used as error message.\n\nThe title for this error 
message is {{msg-mw|Sessionfailure-title}}.",
        "changecontentmodel": "Title of the change content model special page",
@@ -3560,6 +3562,8 @@
        "confirm-watch-top": "Used as confirmation message.",
        "confirm-unwatch-button": "Used as Submit button 
text.\n{{Identical|OK}}",
        "confirm-unwatch-top": "Used as confirmation message.",
+       "confirm-rollback-button": "Used as Submit button 
text.\n{{Identical|OK}}",
+       "confirm-rollback-top": "Used as confirmation message.",
        "semicolon-separator": "{{optional}}",
        "comma-separator": "{{optional}}\n\nWarning: languages have different 
usages of punctuation, and sometimes they are swapped (e.g. openining and 
closing quotation marks, or full stop and colon in Armenian), or change their 
form (the full stop in Chinese and Japanese, the prefered \"colon\" in Armenian 
used in fact as the regular full stop, the comma in Arabic, Armenian, and 
Chinese...)\n\nTheir spacing (before or after) may also vary across languages 
(for example French requires a non-breaking space, preferably narrow if the 
browser supports NNBSP, on the inner side of some punctuations like 
quotation/question/exclamation marks, colon, and semicolons).",
        "colon-separator": "{{optional}}\nChange it only if your language uses 
another character for ':' or it needs an extra space before the colon.",
diff --git a/resources/Resources.php b/resources/Resources.php
index 9a5931f..5dde2f2 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -933,6 +933,12 @@
                        'mediawiki.api',
                ],
        ],
+       'mediawiki.api.rollback' => [
+               'scripts' => 'resources/src/mediawiki/api/rollback.js',
+               'dependencies' => [
+                       'mediawiki.api',
+               ],
+       ],
        'mediawiki.content.json' => [
                'position' => 'top',
                'styles' => 
'resources/src/mediawiki/mediawiki.content.json.css',
@@ -1692,6 +1698,18 @@
                        'watcherrortext',
                ],
        ],
+       'mediawiki.page.rollback' => [
+               'scripts' => 'resources/src/mediawiki/page/rollback.js',
+               'dependencies' => [
+                       'mediawiki.api.rollback',
+                       'mediawiki.notify',
+                       'jquery.spinner',
+               ],
+               'messages' => [
+                       'rollbackfailed',
+                       'actioncomplete',
+               ],
+       ],
        'mediawiki.page.image.pagination' => [
                'scripts' => 'resources/src/mediawiki/page/image-pagination.js',
                'dependencies' => [
diff --git a/resources/src/mediawiki/api/rollback.js 
b/resources/src/mediawiki/api/rollback.js
new file mode 100644
index 0000000..eb2b3fc
--- /dev/null
+++ b/resources/src/mediawiki/api/rollback.js
@@ -0,0 +1,34 @@
+/**
+ * @class mw.Api.plugin.rollback
+ * @since 1.27
+ */
+( function ( mw, $ ) {
+
+       $.extend( mw.Api.prototype, {
+               /**
+                * Convenience method for `action=rollback`.
+                *
+                * @param {string|mw.Title} page
+                * @param {string} user
+                * @param {Object} [params] Additional parameters
+                * @return {jQuery.Promise}
+                */
+               rollback: function ( page, user, params ) {
+                       return this.postWithToken( 'rollback', $.extend( {
+                               action: 'rollback',
+                               title: String( page ),
+                               user: user,
+                               uselang: mw.config.get( 'wgUserLanguage' )
+                       }, params ) )
+                       .then( function ( data ) {
+                               return data.rollback;
+                       } );
+               }
+       } );
+
+       /**
+        * @class mw.Api
+        * @mixins mw.Api.plugin.rollback
+        */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/page/patrol.ajax.js 
b/resources/src/mediawiki/page/patrol.ajax.js
index ec68b3c..30b73e4 100644
--- a/resources/src/mediawiki/page/patrol.ajax.js
+++ b/resources/src/mediawiki/page/patrol.ajax.js
@@ -16,7 +16,7 @@
                $patrolLinks.on( 'click', function ( e ) {
                        var $spinner, rcid, apiRequest;
 
-                       // Start preloading the notification module (normally 
loaded by mw.notify())
+                       // Preload the notification module for mw.notify
                        mw.loader.load( 'mediawiki.notification' );
 
                        // Hide the link and create a spinner to show it inside 
the brackets.
diff --git a/resources/src/mediawiki/page/rollback.js 
b/resources/src/mediawiki/page/rollback.js
new file mode 100644
index 0000000..d973d07
--- /dev/null
+++ b/resources/src/mediawiki/page/rollback.js
@@ -0,0 +1,66 @@
+/*!
+ * Enhance rollback links by using asynchronous API requests,
+ * rather than navigating to an action page.
+ *
+ * @since 1.27
+ * @author Timo Tijhof
+ */
+( function ( mw, $ ) {
+
+       $( function () {
+               $( '.mw-rollback-link' ).on( 'click', 'a[data-mw="interface"]', 
function ( e ) {
+                       var api, $spinner,
+                               $link = $( this ),
+                               url = this.href,
+                               page = mw.util.getParamValue( 'title', url ),
+                               user = mw.util.getParamValue( 'from', url );
+
+                       if ( !page || !user ) {
+                               // Let native browsing handle the link
+                               return true;
+                       }
+
+                       // Preload the notification module for mw.notify
+                       mw.loader.load( 'mediawiki.notification' );
+
+                       // Remove event handler so that next click (re-try) 
uses server action
+                       $( e.delegateTarget ).off( 'click' );
+
+                       // Hide the link and create a spinner to show it inside 
the brackets.
+                       $spinner = $.createSpinner( { size: 'small', type: 
'inline' } );
+                       $link.hide().after( $spinner );
+
+                       api = new mw.Api();
+                       api.rollback( page, user )
+                               .then( function ( data ) {
+                                       mw.notify( $.parseHTML( 
data.messageHtml ), {
+                                               title: mw.msg( 'actioncomplete' 
)
+                                       } );
+
+                                       // Remove link container and the 
subsequent text node containing " | ".
+                                       if ( e.delegateTarget.nextSibling && 
e.delegateTarget.nextSibling.nodeType === Node.TEXT_NODE ) {
+                                               $( e.delegateTarget.nextSibling 
).remove();
+                                       }
+                                       $( e.delegateTarget ).remove();
+                               }, function ( errorCode, data ) {
+                                       var message = data && data.error && 
data.error.messageHtml
+                                               ? $.parseHTML( 
data.error.messageHtml )
+                                               : mw.msg( 'rollbackfailed' ),
+                                               type = errorCode === 
'alreadyrolled' ? 'warn' : 'error';
+
+                                       mw.notify( message, {
+                                               type: type,
+                                               title: mw.msg( 'rollbackfailed' 
),
+                                               autoHide: false
+                                       } );
+
+                                       // Restore the link (enables user to 
try again)
+                                       $spinner.remove();
+                                       $link.show();
+                               } );
+
+                       e.preventDefault();
+               } );
+       } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/page/watch.js 
b/resources/src/mediawiki/page/watch.js
index a57d5c7..c59f5ba 100644
--- a/resources/src/mediawiki/page/watch.js
+++ b/resources/src/mediawiki/page/watch.js
@@ -108,14 +108,13 @@
                $links.click( function ( e ) {
                        var action, api, $link;
 
-                       // Start preloading the notification module (normally 
loaded by mw.notify())
+                       // Preload the notification module for mw.notify
                        mw.loader.load( 'mediawiki.notification' );
 
                        action = mwUriGetAction( this.href );
 
                        if ( action !== 'watch' && action !== 'unwatch' ) {
-                               // Could not extract target action from link 
url,
-                               // let native browsing handle it further
+                               // Let native browsing handle the link
                                return true;
                        }
                        e.preventDefault();

-- 
To view, visit https://gerrit.wikimedia.org/r/242050
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ia457802fec2e90573c8e7d552bc1f3cee258f10b
Gerrit-PatchSet: 27
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Krinkle <krinklem...@gmail.com>
Gerrit-Reviewer: Aaron Schulz <asch...@wikimedia.org>
Gerrit-Reviewer: Alex Monk <kren...@gmail.com>
Gerrit-Reviewer: Bartosz DziewoƄski <matma....@gmail.com>
Gerrit-Reviewer: Fomafix
Gerrit-Reviewer: Jforrester <jforres...@wikimedia.org>
Gerrit-Reviewer: Krinkle <krinklem...@gmail.com>
Gerrit-Reviewer: Legoktm <legoktm.wikipe...@gmail.com>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to