MediaWiki:Gadget-globalmassblock.js

/** * Adds a client-side Special:MassGlobalBlock (404!) until a server-side solution is implemented. * See https://phabricator.wikimedia.org/T124607 * Usage: Enable the gadget at Special:Gadgets and go to Special:MassGlobalBlock. */

var SpecialMassGlobalBlock = { name: 'Mass global block', api: null, $content: $( '#mw-content-text' ), expiryTimes: [ '1 day', '3 days', '1 week', '2 weeks', '1 month', '6 months', '1 year', '2 years', '3 years' ], blockReasons: [ 'Cross-wiki spam: spambot', 'Open proxy: See the help page if you are affected', 'Open proxy/Webhost: See the help page if you are affected ' ], MAX_LIMIT: 20000,

execute: function { SpecialMassGlobalBlock.api = new mw.Api; document.title = this.name + ' - ' + mw.config.get( 'wgSiteName' ); $( '#firstHeading' ).text( this.name ); this.$content.empty; this.$content.append(			' This page allows doing mass global blocks on lots of IP addresses or ranges at once. You can block '				+ 'a maximum of ' + this.MAX_LIMIT + ' targets in one submission. Please use this tool with care. '				+ ' Try not to flood StewardBot out of the IRC channel! '		); this.$content.append( this.getFormPanel.$element ); this.submit.on( 'click', this.onSubmit ); },

initFormWidgets: function { // Validation callback for text inputs var isEmpty = function( val ) { if ( val.trim === '' ) { return false; }			return true; };

this.targets = new OO.ui.MultilineTextInputWidget( {			id: 'mw-mgblock-targets',			multiline: true,			required: true,			rows: 20,			maxRows: 20000,			autocomplete: false,			placeholder: 'List of IP addresses or ranges separated by newline',			validate: isEmpty		} );

this.expiry = new OO.ui.ComboBoxInputWidget( {			id: 'mw-mgblock-expiry',			required: true,			options: this.expiryTimes.map( function( expiry ) { return { data: expiry }; } ),			validate: isEmpty		} );

this.reason = new OO.ui.ComboBoxInputWidget( {			id: 'mw-mgblock-reason',			required: true,			options: this.blockReasons.map( function( reason ) { return { data: reason }; } ),			validate: isEmpty		} );

this.checkboxes = new OO.ui.CheckboxMultiselectInputWidget( {			id: 'mw-mgblock-checkboxes',			options: [ {				data: 'anononly',				label: 'Block anonymous users only'			}, {				data: 'alsolocal',				label: 'Also block the given IP address locally on this wiki'			}, {				data: 'localblockstalk',				label: 'Block user from editing their own talk page locally'			}, {				data: 'modify',				label: 'Modify any existing blocks'			} ]		} );

this.submit = new OO.ui.ButtonInputWidget( {			id: 'mw-mgblock-submit',			label: 'Submit',			flags: [ 'primary', 'destructive' ]		} ); },

getFormFields: function { return [ { widget: this.targets, config: { label: 'List of IP addresses and ranges to block', align: 'top' }		}, {			widget: this.expiry, config: { label: 'Expiry', align: 'top', }		}, {			widget: this.reason, config: { label: 'Reason', align: 'top', }		}, {			widget: this.checkboxes, config: { align: 'inline' }		} ];	},

getFormPanel: function { this.initFormWidgets; var formFields = this.getFormFields .map( function( field ) {				return new OO.ui.FieldLayout( field.widget, field.config );			} ); var fieldset = new OO.ui.FieldsetLayout( {			items: formFields.concat( new OO.ui.FieldLayout( this.submit ) ),		} ); return new OO.ui.PanelLayout( {			id: 'mw-mgblock-form',			expanded: false,			$content: fieldset.$element		} ); },

disableForm: function( state ) { this.submit.setDisabled( state ); this.targets.setReadOnly( state ); this.expiry.setReadOnly( state ); this.reason.setReadOnly( state ); this.checkboxes.setDisabled( state ); },

/**	 * Click handler for the submit button. Does input validation, controls form state, * and show errors to the user whenever necesarry. If everything seem fine, attempt the API requests. */	onSubmit: function { var self = SpecialMassGlobalBlock, textWidgets = [ self.targets, self.expiry, self.reason ], enableForm = function { self.disableForm( false ); },			showError = function( errorMsg ) { OO.ui.alert( errorMsg ).done( enableForm ); };

self.disableForm( true );

// Check whether all text input fields are not blank var blank = false; $.each( textWidgets, function( i, widget ) {			if ( widget.getValue.trim === '' ) {				blank = true;				return false;			}		} ); if ( blank ) { showError( 'All fields are required. Please enter valid input.' ); return; }

// Split the target field input by new lines and: // - strip whitespace // - add to targets array if not already present (to avoid dupes) var targets = [], targetLines = self.targets.getValue.split( '\n' ); for ( var i = 0; i < targetLines.length; i++ ) { var line = targetLines[ i ].trim; if ( line !== '' && $.inArray( line, targets ) === -1 ) { targets.push( line ); }		}

var targetsCount = targets.length; for ( i = 0; i < targetsCount; i++ ) { if ( mw.util.isIPAddress( targets[ i ], true ) === false ) { showError( 'Invalid IP address or IP range: ' + targets[ i ] ); return; }		}

if ( targetsCount > self.MAX_LIMIT ) { showError( 'Maximum number of target IPs exceeded. You entered ' + targetsCount + ' IPs.' ); return; }

var blockSettings = { action: 'globalblock', expiry: self.expiry.getValue, reason: self.reason.getValue };		self.checkboxes.getValue.forEach( function( value ) {			blockSettings[ value ] = true;		} );

var progressBar = new OO.ui.ProgressBarWidget( {			progress: 0		} ); var progressField = new OO.ui.FieldLayout(			progressBar,			{ label: 'Progress:' }		); self.$content.append( progressField.$element );

// Initialize and start sending API requests. The requests are sent one after another. // If the API throws an error, this will stop sending future requests and will // tell the user about it. var iterator = new self.Iterator( targets, {			onIteration: function( me, ip, curIndex, count ) {				self.doApiRequest( Object.assign( blockSettings, { target: ip } ) )					.done( function { progressBar.setProgress( Math.round( ( curIndex / count ) * 100 ) ); setTimeout( me.next, 10 ); } )					.fail( function( errorMsg ) { me.error( errorMsg ); } );			},			onError: function( me, current, errMsg ) {				progressField.$element.remove;				showError( 'Error occured in API request while attempting to block ' + current + '. Please check whether your input is valid. Script has been terminated.' );				enableForm;			},			onComplete: function( me, last, count ) {				progressBar.setProgress( 100 );				OO.ui.alert( 'Finished. Successfully blocked ' + count + ' IPs.' );			}		} ); iterator.start;

},

doApiRequest: function( params ) { return this.api.postWithToken( 'csrf', params ) .then( function {				return true;			}, function( data ) {				return data;			} ); },

/**	 * Based on mw.siteMatrix.Iterator at mw:User:Krinkle/Snippets/Iterate_SiteMatrix_in_JavaScript */	Iterator: function( array, funcs ) { var self = this, arrLength = array.length, i, current;

self.next = function { if ( i < arrLength ) { current = array[ i ]; funcs.onIteration( self, current, i, arrLength ); i++; } else { funcs.onComplete( self, current, arrLength ); }		};		self.error = function( errMsg ) { console.log( current, errMsg ); funcs.onError( self, current, errMsg ); };		self.start = function { i = 0; self.next; };		return self; } };

if ( mw.config.get( 'wgNamespaceNumber' ) === -1 && mw.config.get( 'wgTitle' ) === 'MassGlobalBlock' && mw.config.get( 'wgGlobalGroups' ).indexOf( 'steward' ) > -1 ) { // Load dependencies conditionally as we just want those on one page only mw.loader.using( [ 'oojs-ui', 'mediawiki.util', 'mediawiki.api' ], function {		SpecialMassGlobalBlock.execute;	} ); } else if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'GlobalBlock' ) { var $a = $( '' ) .attr( 'href', mw.config.get( 'wgServer' ) + '/wiki/Special:MassGlobalBlock' ) .text( 'Mass global block' ); $( '#contentSub > a:last-child' ).after( ' | ', $a ); }