MediaWiki:Gadget-MassGlobalBlock.js

/** * Script that allows for mass (global) blocking via Special:MassGlobalBlock * To use, add the following to your common.js: * mw.loader.load( 'https://meta.miraheze.org/w/index.php?title=User:Void/massGBlock.js&action=raw&ctype=text/javascript' ); * Then, navigate to Special:MassGlobalBlock * Only supports IP addresses and IP ranges (IPv4 and IPv6 supported!) */ /* jslint strict:false */ // mw.loader.using( ['mediawiki.api', 'oojs-ui'], function {	mw.util.addPortletLink( 'p-tb', '//meta.miraheze.org/wiki/Special:MassGlobalBlock', 'Mass Global Block', 't-massgblock', 'Globally block mutiple ips.' );

if( mw.config.get( 'wgPageName' ) !== 'Special:MassGlobalBlock' ) { return; }

var api = new mw.Api;

function GroupEnforcerProc ( config ) { GroupEnforcerProc.super.call( this, config ); }	OO.inheritClass( GroupEnforcerProc, OO.ui.ProcessDialog );

GroupEnforcerProc.static.name = 'gEnforceProc'; GroupEnforcerProc.static.title = 'Group Enforcer'; GroupEnforcerProc.static.actions = [ {			action: 'save', label: 'Proceed', flags: ['primary','progressive'] },		{			label: 'Cancel', flags: 'safe' }	];

var gepEditFlag = new OO.ui.CheckboxInputWidget( {		id: 'gep-edit-flag'	} ); gepEditFlag.on( 'change', function ( selected ) {		var disable = !selected;		// gepGroup.setDisabled( disable );		gepReason.setDisabled( disable );		gepRevertReason.setDisabled( disable );	} );

var gepGroup = new OO.ui.TextInputWidget( {		id: 'gep-group',		value: 'flood',		disabled: true,		required: true,		validate: 'not-empty'	} );

var gepReason = new OO.ui.TextInputWidget( {		id: 'gep-reason',		value: 'Performing mass blocking',		disabled: true,		required: true,		validate: 'not-empty'	} );

var gepRevertReason = new OO.ui.TextInputWidget( {		id: 'gep-revert-reason',		value: 'Done',		disabled: true,		required: true,		validate: 'not-empty'	} );

var gepRevertButton = new OO.ui.ButtonWidget( {		id: 'gep-revert-button',		label: 'Remove group',		flags: [ 'destructive', 'primary' ]	} ); gepRevertButton.on( 'click', function {		modifyGroups( false );		windowManager.closeWindow( groupEnforcer );	} );

GroupEnforcerProc.prototype.initialize = function { GroupEnforcerProc.super.prototype.initialize.apply( this, arguments );

this.content = new OO.ui.FieldsetLayout( {			label: 'Enforcement options'		} ); this.content.addItems( [			new OO.ui.FieldLayout( gepEditFlag, { label: 'Enable editing of options', align: 'inline' } ),			new OO.ui.FieldLayout( gepGroup, { label: 'Group:', align: 'inline' } ),			new OO.ui.FieldLayout( gepReason, { label: 'Reason:', align: 'inline', help: 'Only used if adding the group!', helpInline: true } ),			new OO.ui.FieldLayout( gepRevertReason, { label: 'Removal reason:', align: 'inline', help: 'Only used if you use this form to remove yourself from the enforced group!', helpInline: true } ),			new OO.ui.FieldLayout( gepRevertButton ),		] );		this.content.$element.css( 'padding', '1em' );

this.$body.append( this.content.$element ); };

GroupEnforcerProc.prototype.getActionProcess = function ( action ) { var dialog = this; if ( action ) { return new OO.ui.Process( function {				if ( action == 'save' ) {					modifyGroups( true );				}

dialog.close( {					action: action				} ); } );		}		return GroupEnforcerProc.super.prototype.getActionProcess.call( this, action );	};

GroupEnforcerProc.prototype.getBodyHeight = function { return this.content.$element.outerHeight( true ); };

var progressBar = new OO.ui.ProgressBarWidget; var progressField = new OO.ui.FieldLayout(		progressBar,		{			label: "Blocking...",			align: "top"		}	);

function ProgressDialog ( config ) { ProgressDialog.super.call( this, config ); }	OO.inheritClass( ProgressDialog, OO.ui.Dialog );

ProgressDialog.static.name = 'ProcessDialog';

ProgressDialog.prototype.initialize = function { ProgressDialog.super.prototype.initialize.call( this ); this.content = new OO.ui.FieldsetLayout; this.content.addItems([ progressField ]); this.$body.append( this.content.$element ); };

ProgressDialog.prototype.getBodyHeight = function { return this.content.$element.outerHeight( true ); };

var complete = 0; var total = 0;

var windowManager = new OO.ui.WindowManager; var groupEnforcer = new GroupEnforcerProc( {		size: 'small'	} ); var progress = new ProgressDialog( {	   size: 'large'	} ); windowManager.addWindows( [ groupEnforcer, progress ] );

var inEnforcerButton = new OO.ui.ButtonWidget( {		id: 'mgb-enforcer-button',		label: 'Group enforcer',		value: 1	} ); inEnforcerButton.on( 'click', function {		windowManager.openWindow( groupEnforcer );	} );

var inRanges = new OO.ui.MultilineTextInputWidget( {		id: 'mgb-ranges',		disabled: true,		required: true,		rows: 10,		validate: 'not-empty'	} );

var inExpiry = new OO.ui.ComboBoxInputWidget( {		id: 'mgb-expiry',		disabled: true,		options: [			{ data: '1 month' },			{ data: '3 months' },			{ data: '6 months' },			{ data: '1 year' },			{ data: '2 years' }		],		required: true,		value: '2 years',		validate: 'not-empty'	} );

var inReason = new OO.ui.ComboBoxInputWidget( {		id: 'mgb-reason',		disabled: true,		options: [			{ data: 'Crosswiki spamming. Contact cvt[at]miraheze.org if affected.' },			{ data: 'Crosswiki abuse. Contact cvt[at]miraheze.org if affected.' },			{ data: 'Vandalism. Contact cvt[at]miraheze.org if affected.' },			{ data: 'Long term abuse. Contact cvt[at]miraheze.org if affected.' },			{ data: 'Web host or proxy. Contact cvt[at]miraheze.org if affected.' },			// Trust and Safety team use only \/			{ data: 'Trust and Safety enforcement action. Direct enquiries to ts[at]miraheze.org.' }		],		required: true,		value: 'Web host or proxy. Contact cvt[at]miraheze.org if affected.',		validate: 'not-empty'	} );

var inAnonOnly = new OO.ui.CheckboxInputWidget( {		id: 'mgb-anononly',		disabled: true,		selected: true,		value: 1	} );

var inGlobalBlocks = new OO.ui.CheckboxInputWidget( {		id: 'mgb-globalblocks',		disabled: true,		selected: true,		value: 1	} );

var inLocalBlocks = new OO.ui.CheckboxInputWidget( {		id: 'mgb-localblocks',		disabled: true,		selected: true,		value: 1	} );

var inSubmit = new OO.ui.ButtonWidget( {		id: 'mgb-submit',		disabled: true,		label: 'Submit',		flags: ['destructive', 'primary']	} ); inSubmit.on( 'click', doSubmit );

var mgbForm = new OO.ui.FieldsetLayout; mgbForm.addItems( [		new OO.ui.FieldLayout( inEnforcerButton ),		new OO.ui.FieldLayout( inRanges, { label: 'IP Ranges:', align: 'top', help: 'List only one IP or IP range per line', helpInline: true } ),		new OO.ui.FieldLayout( inExpiry, { label: 'Expiry:', align: 'top' } ),		new OO.ui.FieldLayout( inReason, { label: 'Reason', align: 'top' } ),		new OO.ui.FieldLayout( inAnonOnly, { label: 'Block anonymous users only', align: 'inline' } ),		new OO.ui.FieldLayout( inGlobalBlocks, { label: 'Block ranges globally', align: 'inline' } ),		new OO.ui.FieldLayout( inLocalBlocks, { label: 'Block ranges locally', align: 'inline' } ),		new OO.ui.FieldLayout( inSubmit ),	] );	function ConsolePage ( name, label, id, config ) { ConsolePage.super.call( this, name, config ); this._label = label; this._id = id; this.$element.append(" "); }	OO.inheritClass( ConsolePage, OO.ui.PageLayout ); ConsolePage.prototype.setupOutlineItem = function { this.outlineItem.setLabel( this._label ); };	var consoleGood = new ConsolePage( 'one', 'Output log', 'console-output' ), consoleBad = new ConsolePage( 'two', 'Errors', 'console-errors' ), console = new OO.ui.BookletLayout( { outlined: true } ); console.addPages( [ consoleGood, consoleBad ] );

var tabInterface = new OO.ui.TabPanelLayout( 'one', {			label: 'Mass blocking interface',			content: [ mgbForm ]		} ), tabConsole = new OO.ui.TabPanelLayout( 'two', {			label: 'Console',			content: [ console ]		} ), index = new OO.ui.IndexLayout( { expanded: true } );

tabInterface.$element.css( 'height', 'fit-content' ); tabConsole.$element.css( 'height', 'fit-content' ); index.addTabPanels( [ tabInterface, tabConsole ] ); function log ( type, message ) { message += '\n'; // Additional newline because yes if( type === "error" ) { $("#console-errors").append(message); } else { $("#console-output").append(message); }	}

var IPRange = function IPR ( value ) { this.getRange = function { if( !this.range ) { this.range = this.ip + (this.cidr == this.cidrMax ? '' : '/' + this.cidr); }			return this.range; };

this.rangeBreakdown = function { if( this.rangelist ) { return this.rangelist; } else if( parseInt( this.cidr ) >= parseInt( this.cidrMin ) ) { this.rangelist = [ this.getRange ]; return this.rangelist; } else if( parseInt( this.cidr ) >= 8 + this.ipv6 ? 4 : 0 ) { var final = this.getNext; var breakdown = []; var base = this.ipv6 ? 16 : 10;				var priv = new IPRange( this.ip + '/' + this.cidrMin ); breakdown.push( priv.getRange ); var makeStr = function ( e, i, orig ) { orig[i] = e.toString( base ); };				for( var i = 1; priv.getNext < final; i++ ) { var next = priv.getNext.slice; next.forEach( makeStr ); var ip = next.join( this.ipv6 ? ':' : '.' ); priv = new IPRange( ip + '/' + this.cidrMin ); breakdown.push( priv.getRange ); }				this.rangelist = breakdown; return this.rangelist; } else { throw this.getRange + ' is too large!'; }		};

this.getDecimal = function { if( this.decimal ) { return this.decimal; }			var dots = this.ip.split( this.ipv6 ? ':' : '.' ); dots = dots.concat( Array( ( this.ipv6 ? 8 : 4 ) - dots.length ).fill( '' ) ); var base = this.ipv6 ? 16 : 10;			for( var i = 0; i < dots.length; i++ ) { var num = parseInt( dots[i], base ); dots[i] = num ? num : 0; // num can be NaN }			this.decimal = dots; return this.decimal; };

this.getNext = function { if( this.nextR ) { return this.nextR; }			var decimal = this.getDecimal.slice; // Copy var exp = this.ipv6 ? 16 : 8;			var ind = Math.floor( parseInt( this.cidr ) / exp ); var rem = parseInt( this.cidr ) % exp; var jump = rem === 0 ? 1 : Math.pow( 2, exp - rem ); ind -= 1 - ( rem !== 0 ); decimal[ind] += jump; this.nextR = decimal; return this.nextR; };

this.combine = function ( other ) { var ourDecimal = this.getDecimal.slice, otherDecimal = other.getDecimal.slice; var ourNext = this.getNext.slice, otherNext = other.getNext.slice; var pad = this.ipv6 ? 5 : 3;			function stringAndPad ( e, i, orig ) { orig[i] = e.toString.padStart( pad, '0' ); }

ourDecimal.forEach( stringAndPad ); otherDecimal.forEach( stringAndPad ); ourNext.forEach( stringAndPad ); otherNext.forEach( stringAndPad );

if( ourDecimal > otherDecimal ) { return other.combine( this ); // Impossible, but let's account for it anyway }

var ip = null, cidr = null; if( ourDecimal.toString === otherDecimal.toString ) { ip = this.ip; cidr = ( parseInt( this.cidr ) < parseInt( other.cidr ) ? this.cidr : other.cidr ); } else if( ourDecimal < otherDecimal && ourNext >= otherNext ) { ip = this.ip; cidr = this.cidr; } else if( ourNext.toString === otherDecimal.toString && this.cidr === other.cidr ) { ip = this.ip; cidr = '' + ( parseInt( this.cidr ) - 1 ); } else { throw 'Cannot combine ' + this.getRange + ' with ' + other.getRange; }			return new IPRange( ip + '/' + cidr ); };

var vlist = value.split( '/' ); if( vlist.length > 2 ) { throw 'Invalid IP range format'; }		this.ip = vlist[0]; if( !mw.util.isIPAddress( this.ip ) ) { throw 'Invalid IP'; }		this.ipv6 = mw.util.isIPv6Address( this.ip ); this.cidrMax = this.ipv6 ? '128' : '32';		this.cidrMin = this.ipv6 ? '19' : '16';		this.cidr = vlist.length == 2 ? vlist[1] : this.cidrMax; if( this.cidr.padStart( 3 ) > this.cidrMax.padStart( 3 ) ) { throw 'Invalid CIDR'; }

var decimal = this.getDecimal; var exp = this.ipv6 ? 16 : 8;		var ind = Math.floor( parseInt( this.cidr ) / exp ); var rem = parseInt( this.cidr ) % exp; var jump = rem === 0 ? 1 : Math.pow( 2, exp - rem ); ind -= 1 - ( rem !== 0 );

function check ( e ) { return !e; }		if( decimal[ind] % jump || !decimal.slice( ind + 1 ).every( check ) ) { throw 'Invalid IP range'; }	};

function consolidateIPRanges ( ranges ) { var cleanList = []; for( var i = 0; i < ranges.length; i++ ) { try { cleanList.push( new IPRange( ranges[i] ) ); } catch( e ) { log( 'error', 'Invalid input: ' + ranges[i] ); log( 'error', e ); }		}		ranges = cleanList.sort( function ( a, b ) {			if( a.cidrMax !== b.cidrMax ) {				return parseInt( a.cidrMax ) - parseInt( b.cidrMax );			}			var adecimal = a.getDecimal, bdecimal = b.getDecimal;			if( adecimal.length != bdecimal.length ) {				throw 'Unexpected error, cannot sort ' + a.getRange + ' and ' + b.getRange;			}			for( var i = 0; i < adecimal.length; i++ ) {				if( adecimal[i] != bdecimal[i] ) {					return adecimal[i] - bdecimal[i];				}			}			return 0;		} );

var change = true; while( change ) { change = false; var cranges = []; for( var j = 0; j < ranges.length; j++ ) { try { cranges[cranges.length - 1] = cranges[cranges.length - 1].combine( ranges[j] ); change = true; } catch( e ) { cranges.push( ranges[j] ); }

var change2 = true; while( change2 ) { change2 = false; var range = cranges.pop; try { cranges[cranges.length - 1].combine( range ); change2 = true; } catch( e ) { cranges.push( range ); }				}			}			ranges = cranges; }

var finRanges = []; ranges.forEach( function ( e ) {			finRanges = finRanges.concat( e.rangeBreakdown );		} );

return finRanges; }

function doSubmit { if( !inGlobalBlocks.isSelected && !inLocalBlocks.isSelected ) { return alert( 'You must apply a block locally or globally!' ); }		if( !mgbForm.items.every( function ( e ) { return e.fieldWidget.value !== ''; } ) ) { return alert( 'Please fill all required parts of the form!' ); }

mgbForm.items.forEach( function ( e ) {			e.fieldWidget.setDisabled( true );		} );

windowManager.openWindow( progress );

var ranges = inRanges.value.split( '\n' ), deferred = null, conf = { reason: inReason.value, expiry: inExpiry.value, anononly: inAnonOnly.isSelected ? 1 : 0			};

ranges = consolidateIPRanges( ranges );

total = ranges.length * (inGlobalBlocks.isSelected + inLocalBlocks.isSelected); complete = 0; progressField.setLabel("Blocking... " + complete + " out of " + total);

if( inGlobalBlocks.isSelected ) { deferred = makeBlockFunc( true, ranges[0], conf ); }		if( inLocalBlocks.isSelected ) { if( deferred ) { deferred = deferred.then( makeBlockFunc( false, ranges[0], conf ) ); } else { deferred = makeBlockFunc( false, ranges[0], conf ); }		}

for( var i = 1; i < ranges.length; i++ ) { if( inGlobalBlocks.isSelected ) { deferred = deferred.then( makeBlockFunc( true, ranges[i], conf ) ); }			if( inLocalBlocks.isSelected ) { deferred = deferred.then( makeBlockFunc( false, ranges[i], conf ) ); }		}

$.when( deferred ).then( doFinish ); }

function makeBlockFunc ( global, range, config ) { return function { return $.Deferred( function ( deferred ) {				var data = {					format: 'json',					expiry: config.expiry,					reason: config.reason,					anononly: config.anononly				};

if ( global ) { data.action = 'globalblock'; data.target = range; } else { data.action = 'block'; data.user = range; data.nocreate = 1; }

var promise = api.postWithToken( 'csrf', data ); function finish { complete++; progressBar.setProgress( parseInt((complete / total) * 100) ); progressField.setLabel("Blocking... " + complete + " out of " + total); }				function attachHandlers( promise ) { promise.done( function {						log( 'info', ( global ? 'Globally' : 'Locally' ) + ' blocked ' + range );						finish;						deferred.resolve;					} ); promise.fail( function ( jqXHR, textStatus, error ) {						log( 'error', 'Could not ' + ( global ? 'globally' : 'locally' ) + ' block ' + range );						try {							errorMsg = global ? error.error.globalblock[0].message : error.error.info;							finish;							deferred.resolve;						} catch (e) {							errorMsg = "Network error, trying again after a delay....";							setTimeout(function { var newPromise = api.postWithToken( 'csrf', data ); attachHandlers( newPromise ); }, 1000 * 15); // Retry after 15 seconds						}						log( 'error', errorMsg );					} ); }				attachHandlers( promise ); } );		};	}

function doFinish { alert( 'Done blocking' ); mgbForm.items.forEach( function ( e ) { e.fieldWidget.setDisabled( false ); } ); windowManager.closeWindow( progress ); }

function modifyGroups ( adding ) { api.postWithToken( 'userrights', {			format: 'json',			action: 'userrights',			user: mw.config.get( 'wgUserName' ),			add: adding ? gepGroup.value : ,			remove: adding ?  : gepGroup.value,			reason: adding ? gepReason.value : gepRevertReason.value		} ).done( permChecks ); }

function permChecks { api.getUserInfo.done( function ( data ) {			if ( data.groups.includes( 'flood' ) ) {				inRanges.setDisabled( false );				inExpiry.setDisabled( false );				inReason.setDisabled( false );				inAnonOnly.setDisabled( false );				inGlobalBlocks.setDisabled( false );				inLocalBlocks.setDisabled( false );				inSubmit.setDisabled( false );			} else {				inRanges.setDisabled( true );				inExpiry.setDisabled( true );				inReason.setDisabled( true );				inAnonOnly.setDisabled( true );				inGlobalBlocks.setDisabled( true );				inLocalBlocks.setDisabled( true );				inSubmit.setDisabled( true );				windowManager.openWindow( groupEnforcer ); // Auto open because why not?			}		} ); }

// Create UI	var sheet = document.createElement('style'); sheet.innerHTML = ".oo-ui-menuLayout-expanded > .oo-ui-menuLayout-content { position: relative }\n.oo-ui-menuLayout-expanded { position: relative; }\n.oo-ui-panelLayout-expanded { position: relative; }"; document.body.appendChild(sheet); // Otherwise the tab layout sets its own height to like 10px??? $( '.firstHeading' ).text( 'Mass Global Block' ); $( '#bodyContent' ).text( '' ); $( '#bodyContent' ).append( windowManager.$element ); $( '#bodyContent' ).append( index.$element ); permChecks; } ); //