User:JBW/sandbox/Gadget-Massblock.js

/* jshint maxerr: 10000 */ /* global mediaWiki, jQuery, OO, window, document */ /** * Tool for mass-blocking a list of IPs/users. * Adapted from en:User:Timotheus Canens/massblock.js. * Go to Special:Massblock to use it. * * In order to use it, please IMPORT this page, and then add some code calling * mw.messages.set with a list of localized messages if you wish. For an example, * see it:MediaWiki:Gadget-Massblock-it.js * * Memo: only then can return a new promise object, done and fail can't. * * @author (i.e. blame him) it:User:Daimona Eaytoy */ ( function( mw, $ ) {	'use strict';

// Default messages mw.messages.set( {		'massblock-toolbar-text': 'Massblock',		'massblock-document-title': 'Mass-blocking tool - Wikipedia, the free encyclopedia',		'massblock-page-title': 'Mass-blocking tool',		'massblock-abuse-disclaimer': 'If you abuse this tool, it\'s your fault, not mine.',		'massblock-blockusers': 'Users to block (one on each line, please):',		'massblock-talkmsg': 'Replace talk page with, or text to add if the expiry is not infinite (leave blank to leave no message):',		'massblock-upmsg': 'Replace user page text with (leave blank for no change):',		'massblock-block-options-label': 'Block options',		'massblock-further-options-label': 'Further options',		'massblock-common-reasons': 'Common reasons:',		'massblock-other-reason': 'Other reason',		'massblock-extra-reason': 'Other/additional reason:',		'massblock-exptime': 'Expiration time, in english (blank for indefinite):',		'massblock-summary-default': 'Blocked user.', 'massblock-talksummary': 'Edit summary for talk page edit:', 'massblock-talkprotect': 'Protect the talk (sysop level, infinite):', 'massblock-upsummary': 'Edit summary for user page edit:', 'massblock-upprotect': 'Protect the user page (sysop level, infinite):', 'massblock-protect-reason-label': 'Reason for protection:', 'massblock-protect-reason-default': 'Blocked user.', 'massblock-anononly': 'Block anonymous users only (IPs only):', 'massblock-autoblock': 'Enable autoblock (accounts only):', 'massblock-nocreate': 'Block account creation:', 'massblock-noemail': 'Block email:', 'massblock-notalk': 'Remove talk page access:', 'massblock-override': 'Override existing blocks:', 'massblock-submit-text': 'Block', 'massblock-result-alert': 'Blocked $1 users. Edited $2 talk pages and $3 user pages. Protected $4 talk pages and $5 user pages.', 'massblock-failed-actions': 'Failed actions with errors:', 'massblock-failure-help': 'help', 'massblock-init-failure': 'Unable to load the Massblock gadget. Error:' } );

// OOUI element var submitBtn, // Ideally this would use $wgBlockAllowsUTEdit, but it's not available. blockAllowsTalkEdit = window.location.href.indexOf( 'it.wikipedia' ) === -1, mwapi = new mw.Api;

var ErrorHandler = { errors: {}, /**		 * Process an error in any part of the process *		 * @param {string} e The error code * @param {string} user The user we're processing * @param {string} action The action we're doing. This is of the form *  {$actionname}-{$page}, where $action name is the name of the API module *  being used (e.g. 'block' or 'edit') and $page is either 'talk' or 'user', *  representing the target of the action. The only special case is 'block', *  which has no hyphen and no page. */		add: function( e, user, action ) { var obj = { action: e }; if ( !this.errors[ user ] ) { this.errors[ user ] = [ obj ]; } else { this.errors[ user ].push( obj ); }		},		getCodesForUser: function( user ) { var cb = function( el ) { var key = Object.keys( el )[ 0 ], action = key.split( "-" )[ 0 ], link = '//mediawiki.org/wiki/API:' + action + '#Possible_errors'; return key + ':  (' +					msg( 'failure-help' ) + ')'; };			return this.errors[ user ].map( cb ); }	};

var FormData = { init: function { this.dropdownReason = $( "#wpMassBlockReasons select :selected" ).val.trim; this.otherReason = $( "#wpMassBlockReason input" ).val.trim; this.anononly = $( "#wpMassBlockAnononly input" ).prop( 'checked' ); this.nocreate = $( "#wpMassBlockNocreate input" ).prop( 'checked' ); this.noEmail = $( "#wpMassBlockEmail input" ).prop( 'checked' ); this.autoblock = $( "#wpMassBlockAutoblock input" ).prop( 'checked' ); this.talkpageBlocked = blockAllowsTalkEdit ? $( "#wpMassBlockTalkpage input" ).prop( 'checked' ) : true; this.reblock = $( "#wpMassBlockReblock input" ).prop( 'checked' ); this.talkMessage = $( "#wpMassBlockMessage textarea" ).val.trim; this.expiry = $( "#wpMassBlockExpiry input" ).val.trim; this.talkSummary = $( "#wpMassBlockSummaryTalk input" ).val.trim; this.upSummary = $( "#wpMassBlockSummaryUser input" ).val.trim; this.protectReason = $( "#wpMassBlockProtectReason input" ).val.trim;

this.isInfty = isInfinity( this.expiry ); // Several actions can only be executed if the block is infinite this.protectTalk = $( "#wpMassBlockProtectTalk input" ).prop( 'checked' ) && this.isInfty; this.protectUser = $( "#wpMassBlockProtectUser input" ).prop( 'checked' ) && this.isInfty; this.upMessage = this.isInfty ? $( "#wpMassBlockTag textarea" ).val.trim : ''; }	};

var Main = { blocked: 0, talkpageedited: 0, userpageedited: 0, talkpageprotected: 0, userpageprotected: 0,

_protect: function( page, exists, counter, user ) { var prType = exists ? 'edit=sysop|move=sysop' : 'create=sysop'; return doProtectPage( page, 'edit=sysop|move=sysop' ) .done( function {					this[counter]++;				} ) .fail( function( e ) {					ErrorHandler.add( e, user, "protect-talk" );				} ) .always( function {					return $.when;				} ); },		editTalk: function( user ) { var talkPage = getTalkTitle( user ); return doEditPage( talkPage, FormData.talkMessage, FormData.talkSummary, !FormData.isInfty ) .then(					function {						this.talkpageedited++;						if ( FormData.protectTalk ) {							return this._protect( talkPage, true, 'talkpageprotected', user );						}					}.bind( this ),					function( e ) {						ErrorHandler.add( e, user, "edit-talk" );						return $.when;					}				); },		protectTalk: function( user ) { var talkPage = getTalkTitle( user ); mwapi.get( {				action: 'query',				titles: talkPage			} ) .then(					function( data ) {						var exists = Object.keys( data.query.pages )[ 0 ] !== -1;						return this._protect( talkPage, exists, 'talkpageprotected', user );					}.bind( this ),					function( e ) {						ErrorHandler.add( e, user, "query-talk" );						return $.when;					}				); },		editUP: function( user ) { var userPage = getUPTitle( user ); doEditPage( userPage, FormData.upMessage, FormData.upSummary ) .then(					function {						this.userpageedited++;						if ( FormData.protectUser ) {							return this._protect( userPage, true, 'userpageprotected', user );						}					}.bind( this ),					function( e ) {						ErrorHandler.add( e, user, "edit-user" );						return $.when;					}				); },		protectUP: function( user ) { var userPage = getUPTitle( user ); return mwapi.get( {					action: 'query',					titles: userPage				} ) .then(					function( data ) {						var exists = Object.keys( data.query.pages )[ 0 ] !== -1;						return this._protect( userPage, exists, 'userpageprotected', user );					}.bind( this ),					function( e ) {						ErrorHandler.add( e, user, "query-user" );						return $.when;					}				); },		getAlertText: function { return msg( 'result-alert' ) .replace( '$1', this.blocked ) .replace( '$2', this.talkpageedited ) .replace( '$3', this.userpageedited ) .replace( '$4', this.talkpageprotected ) .replace( '$5', this.userpageprotected ); }	};

function getTalkTitle( user ) { return 'User talk:' + user; }	function getUPTitle( user ) { return 'User:' + user; }

/**	 * The post-block handler. Performs edits and protections. *	 * @param {string} user * @return {Promise} A promise which is resolved after all actions are done *  for all pages. This will never be rejected. */	function successHandler( user ) { // @var {Promise} These track the state for all actions (edit and protect) // on every page. They are resolved as soon as a page is processed, with or // without a failure. This way the final $.when will resolve after all promises // have been processed, and not after the first rejection. var talkDone, userDone;

Main.blocked++;

if ( FormData.talkMessage !== "" ) { talkDone = Main.editTalk( user ); } else if ( FormData.protectTalk ) { talkDone = Main.protectTalk( user ); } else { talkDone = $.when; }

if ( FormData.upMessage !== "" ) { userDone = Main.editUP( user ); } else if ( FormData.protectUser ) { userDone = Main.protectUP( user ); } else { userDone = $.when; }

return $.when( talkDone, userDone ); }

/**	 * Main form processing routine, called on form submit */	function doMassBlock { var users = $( "#wpMassBlockUsers textarea" ).val.split( "\n" );

users = users // First trim everything .map( function( s ) {				return s.trim;			} ) // Then remove blanks and duplicates .filter( function( el, index, me ) {				return el !== '' && me.indexOf( el ) === index;			} );

if ( users.length === 0 ) { // Easy return; }		submitBtn.setDisabled( true );

FormData.init;

// Array of Promises, one for each user. Each one is resolved after the user // is processed, even in case of failure. var deferreds = [];

users.forEach( function( user ) {			deferreds.push( doBlock( user ) .then(						function {							return successHandler( user );						},						function( e ) {							ErrorHandler.add( e, user, "block" );							// Return so that this counts as resolved and won't leave							// other promises unresolved.							return $.when;						}					) );		} );

$.when.apply( $, deferreds ).always( doPostBlockActions ); }

/**	 * Executed after all users have been processed. */	function doPostBlockActions { var errors = ErrorHandler.errors; if ( Object.keys( errors ).length > 0 ) { var linkedList = '';

for ( var user in errors ) { var codes = ErrorHandler.getCodesForUser( user ); linkedList += "" + user + ": " + codes.join( "; " ) + ""; }			$( "#wpMassBlockFailedContainer" ).html(				' ' + msg( 'failed-actions' ) + ' ' + linkedList + ' '			); }

OO.ui.alert( Main.getAlertText ); }

/**	 * Perform a single block via API *	 * @param {string} user The user to block * @return {Promise} */	function doBlock( user ) { return mwapi.postWithToken( "csrf", {			action: 'block',			allowusertalk: !FormData.talkpageBlocked,			autoblock: FormData.autoblock,			nocreate: FormData.nocreate,			expiry: FormData.expiry === "" ? "indefinite" : FormData.expiry,			anononly: FormData.anononly,			noemail: FormData.noEmail,			reblock: FormData.reblock,			reason: FormData.dropdownReason === "other" ? FormData.otherReason : FormData.dropdownReason + ( FormData.otherReason ? ": " + FormData.otherReason : "" ),			user: user		} ); }

/**	 * Edit the given page. *	 * @param {string} title The title of the page * @param {string} text The text to add * @param {string} summary The summary to use * @param {bool} append Whether to append the text or replace the whole page content * @return {Promise} */	function doEditPage( title, text, summary, append ) { var appendText = append || false, params = { action: 'edit', title: title, summary: summary, watchlist: 'nochange' };		if ( appendText ) { params.appendtext = text; } else { params.text = text; }		return mwapi.postWithEditToken( params ); }

/**	 * Protect the given page *	 * @param {string} title The page to protect * @param {string} protections As accepted by the Protect API module * @return {Promise} */	function doProtectPage( title, protections ) { return mwapi.postWithToken( 'csrf', {			action: 'protect',			title: title,			protections: protections,			reason: FormData.protectReason,			watchlist: 'nochange'		} ); }

/**	 * Get a localised messages, or the default one as fallback. *	 * @param {string} msg The key of the message to get * @return {string} */	function msg( msg ) { return mw.messages.get( 'massblock-' + msg ); }

/**	 * Build the form */	function massblockform { var reasons = mw.msg( 'Ipbreason-dropdown' ).split( '**' ), // OOUI elements talkTextField, userTextField, talkProtectCb, talkSummaryField, userSummaryField, userProtectCb, protectReasonField;

$( "h1" ).first.html( msg( 'page-title' ) ); document.title = msg( 'document-title' );

var form = new OO.ui.FormLayout( {			id: 'wpMassBlock'		} );

talkTextField = new OO.ui.MultilineTextInputWidget( {			rows: 10		} );

userTextField = new OO.ui.MultilineTextInputWidget( {			rows: 10		} );

var mainField = new OO.ui.FieldsetLayout( {			label: msg( 'blockusers' ),			items: [				new OO.ui.MultilineTextInputWidget( { rows: 10, id: 'wpMassBlockUsers' } )			]		} );

var reasonOpts = [ { optgroup: msg( 'other-reason' ) },			{				data: 'other', label: msg( 'other-reason' ) },			{				optgroup: msg( 'common-reasons' ) }		];		for ( var i = 1, j = reasons.length; i < j; i++ ) { reasonOpts.push( {				data: reasons[ i ],				label: reasons[ i ]			} ); }

var expiryField = new OO.ui.TextInputWidget( {			maxLength: 255,			id: 'wpMassBlockExpiry'		} );

var otherReasonField = new OO.ui.TextInputWidget( {			maxLength: 255,			id: 'wpMassBlockReason'		} );

var reasonsDropdown = new OO.ui.DropdownInputWidget( {			options: reasonOpts,			id: 'wpMassBlockReasons'		} ).on( 'change', function {			var reason = reasonsDropdown.getValue,				maxlength = ( reason === "other" ? 255 : ( 255 - ': '.length ) - reason.length );

$( '#wpMassBlockReason input' ).attr( "maxlength", maxlength ); } );

var blockOptsArray = [ new OO.ui.FieldLayout(				reasonsDropdown, {					label: msg( 'common-reasons' )				}			),

new OO.ui.FieldLayout(				otherReasonField, {					label: msg( 'extra-reason' )				}			),

new OO.ui.FieldLayout( expiryField, {				label: msg( 'exptime' )			} ),

new OO.ui.FieldLayout(				new OO.ui.CheckboxInputWidget( { id: 'wpMassBlockAnononly', selected: true } ), {					label: msg( 'anononly' )				}			), new OO.ui.FieldLayout(				new OO.ui.CheckboxInputWidget( { id: 'wpMassBlockAutoblock', selected: true } ), {					label: msg( 'autoblock' )				}			), new OO.ui.FieldLayout(				new OO.ui.CheckboxInputWidget( { id: 'wpMassBlockNocreate', selected: true } ), {					label: msg( 'nocreate' )				}			), new OO.ui.FieldLayout(				new OO.ui.CheckboxInputWidget( { id: 'wpMassBlockEmail' } ), {					label: msg( 'noemail' )				}			) ];

if ( blockAllowsTalkEdit ) { blockOptsArray.push( new OO.ui.FieldLayout( new OO.ui.CheckboxInputWidget( {					id: 'wpMassBlockTalkpage'				} ), { label: msg( 'notalk' ) }			) );		}

blockOptsArray.push( new OO.ui.FieldLayout( new OO.ui.CheckboxInputWidget( {				id: 'wpMassBlockReblock'			} ), { label: msg( 'override' ) }		) );

var blockOpts = new OO.ui.FieldsetLayout( {			label: msg( 'block-options-label' ),			items: blockOptsArray		} );

talkSummaryField = new OO.ui.TextInputWidget( {				maxLength: 255,				id: 'wpMassBlockSummaryTalk',				value: msg( 'summary-default' ),				// The text is empty by default				disabled: true			} );

talkTextField.on( 'change', function {			talkSummaryField.setDisabled( talkTextField.getValue.trim === '' );		} );

userSummaryField = new OO.ui.TextInputWidget( {				maxLength: 255,				id: 'wpMassBlockSummaryUser',				value: msg( 'summary-default' ),				// The text is empty by default				disabled: true			} );

var toggleUserTextField = function { var disabled = userTextField.getValue.trim === '' || !isInfinity( $( '#wpMassBlockExpiry input' ).val.trim ); userSummaryField.setDisabled( disabled ); };

userTextField.on( 'change', toggleUserTextField );

talkProtectCb = new OO.ui.CheckboxInputWidget( {				id: 'wpMassBlockProtectTalk'			} );

userProtectCb = new OO.ui.CheckboxInputWidget( {				id: 'wpMassBlockProtectUser'			} );

protectReasonField = new OO.ui.TextInputWidget( {				maxLength: 255,				id: 'wpMassBlockProtectReason',				value: msg( 'protect-reason-default' ),				disabled: true			} );

/**		 * Toggle protection fields */		var toggleProtectionFields = function { if (				!( talkProtectCb.isSelected || userProtectCb.isSelected ) ||				!isInfinity( $( '#wpMassBlockExpiry input' ).val.trim )			) { protectReasonField.setDisabled( true ); } else { protectReasonField.setDisabled( false ); }		};

talkProtectCb.on( 'change', toggleProtectionFields ); userProtectCb.on( 'change', toggleProtectionFields );

var furtherOpts = new OO.ui.FieldsetLayout( {			label: msg( 'further-options-label' ),			items: [				new OO.ui.HorizontalLayout( { items: [ new OO.ui.FieldLayout(							talkTextField,							{								label: msg( 'talkmsg' ),								align: 'top',								id: 'wpMassBlockMessage',								classes: [ 'massblock-horiz-element' ]							}						), new OO.ui.FieldLayout(							userTextField,							{								label: msg( 'upmsg' ),								align: 'top',								id: 'wpMassBlockTag',								classes: [ 'massblock-horiz-element' ]							}						) ]				} ),				new OO.ui.FieldLayout( talkSummaryField, { label: msg( 'talksummary' ) } ),				new OO.ui.FieldLayout( userSummaryField, { label: msg( 'upsummary' ) } ),				new OO.ui.FieldLayout( talkProtectCb, { label: msg( 'talkprotect' ) } ),				new OO.ui.FieldLayout( userProtectCb, { label: msg( 'upprotect' ) } ),				new OO.ui.FieldLayout( protectReasonField, { label: msg( 'protect-reason-label' ) } )			]		} );

expiryField.on( 'change', function {			// Several fields cannot be used if the expiry isn't infinite			var enable = isInfinity( $( '#wpMassBlockExpiry input' ).val.trim ),				// These are OOUI elements				disableEls = [					userTextField,					talkProtectCb,					userProtectCb				];

for ( var el in disableEls ) { disableEls[ el ].setDisabled( !enable ); }

toggleProtectionFields; toggleUserTextField; } );

submitBtn = new OO.ui.ButtonInputWidget( {				label: msg( 'submit-text' ),				title: msg( 'submit-text' ),				id: 'wpMassBlockSubmit',				flags: [					'primary',					'progressive'				]			} ) .on( 'click', doMassBlock );

form.addItems( [ mainField, blockOpts, furtherOpts, new OO.ui.FieldLayout( submitBtn ) ] );

var bodyContentID = ( mw.config.get( 'skin' ) === "cologneblue" ? "#article" : "#bodyContent" ); $( bodyContentID ).html( ' ' + msg( 'abuse-disclaimer' ) + ' ' ); $( bodyContentID ).append( form.$element ); }

/**	 * Utility function to tell if an expiry is infinite *	 * @param {string} expiry * @return {bool} */	function isInfinity( expiry ) { return /^(indefinite|infinite|infinity|never|)$/i.test( expiry ); }

$( function {		if ( mw.config.get( "wgNamespaceNumber" ) === -1 && ( mw.config.get( "wgTitle" ) === "Massblock" || mw.config.get( "wgTitle" ) === "MassBlock" ) && ( /sysop/ ).test( mw.config.get( "wgUserGroups" ) ) ) {			var style ='.massblock-horiz-element{ width: 40%; }';			mw.util.addCSS( style );			mw.loader.using( [ 'mediawiki.jqueryMsg', 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows' ], $.ready )				.done( function loadMsg { mwapi.loadMessagesIfMissing( [ 'Ipbreason-dropdown' ] ) .done( massblockform ) .fail( function ( e ) {							mw.log.error( msg( 'init-failure' ) + ' ' + e );						} ); } )				.fail( function ( e ) { mw.log.error( msg( 'init-failure' ) + ' ' + e ); } );		} else {			mw.util.addPortletLink( 'p-tb', mw.util.getUrl( 'Special:Massblock' ), msg( 'toolbar-text' ) );		}	} ); }( mediaWiki, jQuery ) );