From: Timo Tijhof Date: Tue, 29 Sep 2015 02:53:20 +0000 (-0700) Subject: RollbackAction: Implement AJAX interface and require POST X-Git-Tag: 1.31.0-rc.0~6854^2 X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_ecrire%28%22suivi_revisions%22%2C%22id_auteur=%24connecte%22%29%20.%20%22?a=commitdiff_plain;h=9af38c046c86a51f82891225e3ea6833566302d4;p=lhc%2Fweb%2Fwiklou.git 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 --- diff --git a/RELEASE-NOTES-1.28 b/RELEASE-NOTES-1.28 index e3654869c4..76728c8121 100644 --- a/RELEASE-NOTES-1.28 +++ b/RELEASE-NOTES-1.28 @@ -13,7 +13,7 @@ production. === 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 6a869dd45f..b81218f6d1 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -1872,7 +1872,9 @@ class Linker { * 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 * @@ -1902,6 +1904,8 @@ class Linker { $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped(); } + $context->getOutput()->addModules( 'mediawiki.page.rollback' ); + return '' . $inner . ''; } @@ -1996,11 +2000,13 @@ class Linker { $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 @@ class Linker { } 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 d002da8e89..e32582e5d2 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 @@ class RollbackAction extends FormlessAction { return 'rollback'; } - public function onView() { - // TODO: use $this->useTransactionalTimeLimit(); when POST only - wfTransactionalTimeLimit(); + protected function preText() { + return $this->msg( 'confirm-rollback-top' )->parse(); + } + + 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 ); + } + } + } - $details = null; + /** + * 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 @@ class RollbackAction extends FormlessAction { } } - 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 @@ class RollbackAction extends FormlessAction { ); $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 55f7143719..b9911da138 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -45,16 +45,6 @@ class ApiRollback extends ApiBase { $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 @@ class ApiRollback extends ApiBase { $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,10 +102,23 @@ class ApiRollback extends ApiBase { '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 ); } @@ -148,13 +166,6 @@ class ApiRollback extends ApiBase { return 'rollback'; } - protected function getWebUITokenSalt( array $params ) { - return [ - $this->getRbTitle( $params )->getPrefixedText(), - $this->getRbUser( $params ) - ]; - } - /** * @param array $params * diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 870215672d..cf533d6e80 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -2993,6 +2993,7 @@ class WikiPage implements Page, IDBAccessObject { * 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 @@ class WikiPage implements Page, IDBAccessObject { $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 3000a54848..2ae80e27f1 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: $1.", "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": "; ", "comma-separator": ", ", "colon-separator": ": ", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index f6d2f41799..c301616804 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 $wgShowRollbackEditCount (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 9a5931f5f9..5dde2f2829 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -933,6 +933,12 @@ return [ '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 @@ return [ '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 0000000000..eb2b3fc1f5 --- /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 ec68b3c935..30b73e4c61 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 0000000000..d973d07d98 --- /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 a57d5c77b8..c59f5ba97a 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();