From: Tim Eulitz Date: Tue, 5 Feb 2019 13:31:53 +0000 (+0100) Subject: Show confirmation prompt on rollback links X-Git-Tag: 1.34.0-rc.0~2441 X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_ecrire%28%22calendrier%22%2C%22type=semaine%22%29%20.%20%22?a=commitdiff_plain;h=341320457cd67;p=lhc%2Fweb%2Fwiklou.git Show confirmation prompt on rollback links Bug: T215020 Change-Id: Ic831888e30808a20a04397912498fe2ca04f80ba --- diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 34b2796242..5f93abe0a1 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -4877,6 +4877,7 @@ $wgDefaultUserOptions = [ 'rows' => 25, // @deprecated since 1.29 No longer used in core 'showhiddencats' => 0, 'shownumberswatching' => 1, + 'showrollbackconfirmation' => 0, 'skin' => false, 'stubthreshold' => 0, 'thumbsize' => 5, @@ -9004,15 +9005,6 @@ $wgActorTableSchemaMigrationStage = SCHEMA_COMPAT_OLD; */ $wgEnablePartialBlocks = false; -/** - * Enable confirmation prompt for rollback actions to prevent accidental rollbacks. - * May be disabled to reduce number of clicks needed to perform rollbacks. - * - * @since 1.33 - * @var bool - */ -$wgEnableRollbackConfirmationPrompt = true; - /** * Enable stats monitoring when Block Notices are displayed in different places around core * and extensions. diff --git a/includes/Linker.php b/includes/Linker.php index 3e50ac64d0..6f11c1c8b5 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -1768,6 +1768,10 @@ class Linker { $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped(); } + if ( $context->getUser()->getBoolOption( 'showrollbackconfirmation' ) ) { + $context->getOutput()->addModules( 'mediawiki.page.rollback.confirmation' ); + } + return '' . $inner . ''; } @@ -1869,6 +1873,7 @@ class Linker { $attrs = [ 'data-mw' => 'interface', 'title' => $context->msg( 'tooltip-rollback' )->text(), + 'data-rollback-count' => (int)$editCount ]; $options = [ 'known', 'noclasses' ]; diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 28e0b052a1..3b0c524f30 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -2389,6 +2389,9 @@ "deleting-backlinks-warning": "Warning: [[Special:WhatLinksHere/{{FULLPAGENAME}}|Other pages]] link to or transclude the page you are about to delete.", "deleting-subpages-warning": "Warning: The page you are about to delete has [[Special:PrefixIndex/{{FULLPAGENAME}}/|{{PLURAL:$1|a subpage|$1 subpages|51=over 50 subpages}}]].", "rollback": "Roll back edits", + "rollback-confirmation-confirm": "Rollback of {{PLURAL:$1|0=these edits|one edit|$1 edits}}?", + "rollback-confirmation-yes": "Rollback", + "rollback-confirmation-no": "Cancel", "rollbacklink": "rollback", "rollbacklinkcount": "rollback $1 {{PLURAL:$1|edit|edits}}", "rollbacklinkcount-morethan": "rollback more than $1 {{PLURAL:$1|edit|edits}}", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 790633bf7a..b9b59dae1c 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -252,7 +252,7 @@ "tog-norollbackdiff": "Option in [[Special:Preferences]], 'Misc' tab. Only shown for users with the rollback right. By default a diff is shown below the return screen of a rollback. Checking this preference toggle will suppress that. {{Gender}}\n{{Identical|Rollback}}", "tog-useeditwarning": "Used as label for the checkbox in [[Special:Preferences#mw-prefsection-editing|Special:Preferences]].", "tog-prefershttps": "Toggle option used in [[Special:Preferences]] that indicates if the user wants to use a secure connection when logged in", - "tog-showrollbackconfirmation": "Toggle option used in [[Special:Preferences]] to enable/disable rollback confirmation prompt. Should be visible only to users with rollback rights", + "tog-showrollbackconfirmation": "Toggle option used in [[Special:Preferences]] to enable/disable rollback confirmation prompt. Should be visible only to users with rollback rights.", "underline-always": "Used in [[Special:Preferences#mw-prefsection-rendering|Preferences]].\n\nThis option means \"always underline links\", there are also options {{msg-mw|Underline-never}} and {{msg-mw|Underline-default}}.\n\n{{Gender}}\n{{Identical|Always}}", "underline-never": "Used in [[Special:Preferences#mw-prefsection-rendering|Preferences]].\n\nThis option means \"never underline links\", there are also options {{msg-mw|Underline-always}} and {{msg-mw|Underline-default}}.\n\n{{Gender}}\n{{Identical|Never}}", "underline-default": "Used in [[Special:Preferences#mw-prefsection-rendering|Preferences]].\n\nThis option means \"underline links as in your user skin or your browser\", there are also options {{msg-mw|Underline-never}} and {{msg-mw|Underline-always}}.\n\n{{Gender}}\n{{Identical|Browser default}}", @@ -2595,6 +2595,9 @@ "deleting-backlinks-warning": "A warning shown when a page that is being deleted has at least one link to it or is transcluded in at least one page.", "deleting-subpages-warning": "A warning shown when a page that is being deleted has at least one subpage. $1 is the number of subpages of the page. For any number of subpages over 50, $1 will be 51.\nSee also:\n* {{msg-mw|Deleting-backlinks-warning}}", "rollback": "{{Identical|Rollback}}", + "rollback-confirmation-confirm": "Question shown to user to confirm that they want to proceed with the rollback.\n\nParameters:\n* $1 - number of edits that will be rolled back.", + "rollback-confirmation-yes": "Button text to confirm that a rollback should be executed.", + "rollback-confirmation-no": "Button text to cancel a rollback instead of executing it.", "rollbacklink": "{{Doc-actionlink}}\nThis link text appears on the recent changes page to users who have the \"rollback\" right.\nThis message has a tooltip {{msg-mw|tooltip-rollback}}\n{{Identical|Rollback}}", "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", diff --git a/resources/Resources.php b/resources/Resources.php index aa6e7550f2..5e5f3087f3 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1776,6 +1776,17 @@ return [ 'actioncomplete', ], ], + 'mediawiki.page.rollback.confirmation' => [ + 'scripts' => 'resources/src/mediawiki.rollback.confirmation.js', + 'dependencies' => [ + 'jquery.confirmable' + ], + 'messages' => [ + 'rollback-confirmation-confirm', + 'rollback-confirmation-yes', + 'rollback-confirmation-no', + ], + ], 'mediawiki.page.image.pagination' => [ 'scripts' => 'resources/src/mediawiki.page.image.pagination.js', 'dependencies' => [ diff --git a/resources/src/mediawiki.rollback.confirmation.js b/resources/src/mediawiki.rollback.confirmation.js new file mode 100644 index 0000000000..55d78d5415 --- /dev/null +++ b/resources/src/mediawiki.rollback.confirmation.js @@ -0,0 +1,14 @@ +/*! + * JavaScript for rollback confirmation prompt + */ +$( function () { + $( '.mw-rollback-link a' ).each( function () { + $( this ).confirmable( { + i18n: { + confirm: mw.msg( 'rollback-confirmation-confirm', $( this ).data( 'rollback-count' ) ), + yes: mw.msg( 'rollback-confirmation-yes' ), + no: mw.msg( 'rollback-confirmation-no' ) + } + } ); + } ); +} ); diff --git a/tests/phpunit/includes/LinkerTest.php b/tests/phpunit/includes/LinkerTest.php index bc9bafa02f..438d3e7bca 100644 --- a/tests/phpunit/includes/LinkerTest.php +++ b/tests/phpunit/includes/LinkerTest.php @@ -285,6 +285,48 @@ class LinkerTest extends MediaWikiLangTestCase { ); } + /** + * @covers Linker::generateRollback + * @dataProvider provideCasesForRollbackGeneration + */ + public function testGenerateRollback( $rollbackEnabled, $expectedModules ) { + $this->markTestSkippedIfDbType( 'postgres' ); + + $context = RequestContext::getMain(); + $user = $context->getUser(); + $user->setOption( 'showrollbackconfirmation', $rollbackEnabled ); + + $pageData = $this->insertPage( 'Rollback_Test_Page' ); + $page = WikiPage::factory( $pageData['title'] ); + + $updater = $page->newPageUpdater( $user ); + $updater->setContent( \MediaWiki\Revision\SlotRecord::MAIN, + new TextContent( 'Technical Wishes 123!' ) + ); + $summary = CommentStoreComment::newUnsavedComment( 'Some comment!' ); + $updater->saveRevision( $summary ); + + $rollbackOutput = Linker::generateRollback( $page->getRevision(), $context ); + $modules = $context->getOutput()->getModules(); + + $this->assertEquals( $expectedModules, $modules ); + $this->assertContains( 'rollback 1 edit', $rollbackOutput ); + } + + public static function provideCasesForRollbackGeneration() { + return [ + [ + true, + [ 'mediawiki.page.rollback.confirmation' ] + + ], + [ + false, + [] + ] + ]; + } + public static function provideCasesForFormatLinksInComment() { // phpcs:disable Generic.Files.LineLength return [ diff --git a/tests/selenium/.eslintrc.json b/tests/selenium/.eslintrc.json index 38cd062a90..dd766c85fb 100644 --- a/tests/selenium/.eslintrc.json +++ b/tests/selenium/.eslintrc.json @@ -7,7 +7,8 @@ "mocha": true }, "globals": { - "browser": false + "browser": false, + "mw": false }, "rules": { "no-console": 0 diff --git a/tests/selenium/pageobjects/history.page.js b/tests/selenium/pageobjects/history.page.js index acaf3ea0fa..6b45e66014 100644 --- a/tests/selenium/pageobjects/history.page.js +++ b/tests/selenium/pageobjects/history.page.js @@ -1,11 +1,40 @@ -const Page = require( 'wdio-mediawiki/Page' ); +const Page = require( 'wdio-mediawiki/Page' ), + Api = require( 'wdio-mediawiki/Api' ); class HistoryPage extends Page { + get heading() { return browser.element( '#firstHeading' ); } + get headingText() { return browser.getText( '#firstHeading' ); } get comment() { return browser.element( '#pagehistory .comment' ); } + get rollback() { return browser.element( '.mw-rollback-link' ); } + get rollbackConfirmable() { return browser.element( '.mw-rollback-link .jquery-confirmable-text' ); } + get rollbackConfirmableYes() { return browser.element( '.mw-rollback-link .jquery-confirmable-button-yes' ); } + get rollbackConfirmableNo() { return browser.element( '.mw-rollback-link .jquery-confirmable-button-no' ); } open( title ) { super.openTitle( title, { action: 'history' } ); } + + vandalizePage( name, content ) { + let vandalUsername = 'Evil_' + browser.options.username; + + browser.call( function () { + return Api.edit( name, content ); + } ); + + browser.call( function () { + return Api.createAccount( + vandalUsername, browser.options.password + ); + } ); + + browser.call( function () { + Api.edit( + name, + 'Vandalized: ' + content, + vandalUsername + ); + } ); + } } module.exports = new HistoryPage(); diff --git a/tests/selenium/specs/page.js b/tests/selenium/specs/page.js index 3b2429808f..d35843b884 100644 --- a/tests/selenium/specs/page.js +++ b/tests/selenium/specs/page.js @@ -5,7 +5,7 @@ const assert = require( 'assert' ), EditPage = require( '../pageobjects/edit.page' ), HistoryPage = require( '../pageobjects/history.page' ), UndoPage = require( '../pageobjects/undo.page' ), - UserLoginPage = require( '../pageobjects/userlogin.page' ), + UserLoginPage = require( 'wdio-mediawiki/LoginPage' ), Util = require( 'wdio-mediawiki/Util' ); describe( 'Page', function () { @@ -91,7 +91,7 @@ describe( 'Page', function () { // check HistoryPage.open( name ); - assert.strictEqual( HistoryPage.comment.getText(), `(Created page with "${content}")` ); + assert.strictEqual( HistoryPage.comment.getText(), `(Created or updated page with "${content}")` ); } ); it( 'should be deletable', function () { diff --git a/tests/selenium/specs/rollback.js b/tests/selenium/specs/rollback.js new file mode 100644 index 0000000000..c52eca1210 --- /dev/null +++ b/tests/selenium/specs/rollback.js @@ -0,0 +1,114 @@ +const assert = require( 'assert' ), + HistoryPage = require( '../pageobjects/history.page' ), + UserLoginPage = require( 'wdio-mediawiki/LoginPage' ), + Util = require( 'wdio-mediawiki/Util' ); + +describe( 'Rollback with confirmation', function () { + var content, + name; + + before( function () { + // disable VisualEditor welcome dialog + browser.deleteCookie(); + UserLoginPage.open(); + browser.localStorage( 'POST', { key: 've-beta-welcome-dialog', value: '1' } ); + + // Enable rollback confirmation for admin user + // Requires user to log in again, handled by deleteCookie() call in beforeEach function + UserLoginPage.loginAdmin(); + + browser.pause( 300 ); + browser.execute( function () { + return ( new mw.Api() ).saveOption( + 'showrollbackconfirmation', + '1' + ); + } ); + } ); + + beforeEach( function () { + browser.deleteCookie(); + + content = Util.getTestString( 'beforeEach-content-' ); + name = Util.getTestString( 'BeforeEach-name-' ); + + HistoryPage.vandalizePage( name, content ); + + UserLoginPage.loginAdmin(); + HistoryPage.open( name ); + } ); + + it( 'should offer rollback options for admin users', function () { + assert.strictEqual( HistoryPage.rollback.getText(), 'rollback 1 edit' ); + + HistoryPage.rollback.click(); + + assert.strictEqual( HistoryPage.rollbackConfirmable.getText(), 'Rollback of one edit?' ); + assert.strictEqual( HistoryPage.rollbackConfirmableYes.getText(), 'Rollback' ); + assert.strictEqual( HistoryPage.rollbackConfirmableNo.getText(), 'Cancel' ); + } ); + + it( 'should offer a way to cancel rollbacks', function () { + HistoryPage.rollback.click(); + HistoryPage.rollbackConfirmableNo.click(); + + browser.pause( 500 ); + + assert.strictEqual( HistoryPage.heading.getText(), 'Revision history of "' + name + '"' ); + } ); + + it( 'should perform rollbacks after confirming intention', function () { + HistoryPage.rollback.click(); + HistoryPage.rollbackConfirmableYes.click(); + + // waitUntil indirectly asserts that the content we are looking for is present + browser.waitUntil( function () { + return browser.getText( '#firstHeading' ) === 'Action complete'; + }, 5000, 'Expected rollback page to appear.' ); + } ); +} ); + +describe( 'Rollback without confirmation', function () { + var content, + name; + + before( function () { + // disable VisualEditor welcome dialog + browser.deleteCookie(); + UserLoginPage.open(); + browser.localStorage( 'POST', { key: 've-beta-welcome-dialog', value: '1' } ); + + // Disable rollback confirmation for admin user + // Requires user to log in again, handled by deleteCookie() call in beforeEach function + UserLoginPage.loginAdmin(); + + browser.pause( 300 ); + browser.execute( function () { + return ( new mw.Api() ).saveOption( + 'showrollbackconfirmation', + '0' + ); + } ); + } ); + + beforeEach( function () { + browser.deleteCookie(); + + content = Util.getTestString( 'beforeEach-content-' ); + name = Util.getTestString( 'BeforeEach-name-' ); + + HistoryPage.vandalizePage( name, content ); + + UserLoginPage.loginAdmin(); + HistoryPage.open( name ); + } ); + + it( 'should perform rollback without asking the user to confirm', function () { + HistoryPage.rollback.click(); + + // waitUntil indirectly asserts that the content we are looking for is present + browser.waitUntil( function () { + return HistoryPage.headingText === 'Action complete'; + }, 5000, 'Expected rollback page to appear.' ); + } ); +} ); diff --git a/tests/selenium/wdio-mediawiki/Api.js b/tests/selenium/wdio-mediawiki/Api.js index f68fee95bf..7947ff504c 100644 --- a/tests/selenium/wdio-mediawiki/Api.js +++ b/tests/selenium/wdio-mediawiki/Api.js @@ -5,22 +5,31 @@ const MWBot = require( 'mwbot' ); module.exports = { /** * Shortcut for `MWBot#edit( .. )`. + * Default username, password and base URL is used unless specified * * @since 1.0.0 * @see * @param {string} title * @param {string} content + * @param {string} username - Optional + * @param {string} password - Optional + * @param {baseUrl} baseUrl - Optional * @return {Object} Promise for API action=edit response data. */ - edit( title, content ) { + edit( title, + content, + username = browser.options.username, + password = browser.options.password, + baseUrl = browser.options.baseUrl + ) { let bot = new MWBot(); return bot.loginGetEditToken( { - apiUrl: `${browser.options.baseUrl}/api.php`, - username: browser.options.username, - password: browser.options.password + apiUrl: `${baseUrl}/api.php`, + username: username, + password: password } ).then( function () { - return bot.edit( title, content, `Created page with "${content}"` ); + return bot.edit( title, content, `Created or updated page with "${content}"` ); } ); },