'rows' => 25, // @deprecated since 1.29 No longer used in core
'showhiddencats' => 0,
'shownumberswatching' => 1,
+ 'showrollbackconfirmation' => 0,
'skin' => false,
'stubthreshold' => 0,
'thumbsize' => 5,
*/
$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.
$inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
}
+ if ( $context->getUser()->getBoolOption( 'showrollbackconfirmation' ) ) {
+ $context->getOutput()->addModules( 'mediawiki.page.rollback.confirmation' );
+ }
+
return '<span class="mw-rollback-link">' . $inner . '</span>';
}
$attrs = [
'data-mw' => 'interface',
'title' => $context->msg( 'tooltip-rollback' )->text(),
+ 'data-rollback-count' => (int)$editCount
];
$options = [ 'known', 'noclasses' ];
"deleting-backlinks-warning": "<strong>Warning:</strong> [[Special:WhatLinksHere/{{FULLPAGENAME}}|Other pages]] link to or transclude the page you are about to delete.",
"deleting-subpages-warning": "<strong>Warning:</strong> 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}}",
"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}}",
"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 <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",
'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' => [
--- /dev/null
+/*!
+ * 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' )
+ }
+ } );
+ } );
+} );
);
}
+ /**
+ * @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 [
"mocha": true
},
"globals": {
- "browser": false
+ "browser": false,
+ "mw": false
},
"rules": {
"no-console": 0
-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();
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 () {
// 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 () {
--- /dev/null
+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.' );
+ } );
+} );
module.exports = {
/**
* Shortcut for `MWBot#edit( .. )`.
+ * Default username, password and base URL is used unless specified
*
* @since 1.0.0
* @see <https://www.mediawiki.org/wiki/API:Edit>
* @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}"` );
} );
},