Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/**
* Swiss Army UI (SAUI) is a multipurpose tool that can multitask per a dropdown menu
* Right Now this is pretty much based off of MassGlobalBlock
 */
/* jslint strict:false */
// <nowiki>
mw.loader.using( ['mediawiki.api', 'oojs-ui'], function () {
	mw.util.addPortletLink(
		'p-tb',
		'//en.wikipedia.org/wiki/Special:SAUI',
		'Swiss Army UI',
		't-massgblock',
		'Do stuff.'
	);

	if( mw.config.get( 'wgPageName' ) !== 'Special:SAUI' ) {
		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: '[[m:No open proxies policy|Web host or proxy]]. Contact cvt[at]miraheze.org if affected.' },
			// Trust and Safety team use only \/
			{ data: '[[m:Trust and Safety|Trust and Safety]] enforcement action. Direct enquiries to ts[at]miraheze.org.' }
		],
		required: true,
		value: '[[m:No open proxies policy|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("<pre id='"   id   "'></pre>");
	}
	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 ]
		} ),
  tabDeez = new OO.ui.TabPanelLayout( 'two', {
			label: 'totally not a virus',
			content: [ mgbForm ]
		} ),
		tabConsole = new OO.ui.TabPanelLayout( 'three', {
			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, tabDeez, 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( 'Welcome to the Beta ig :p' );
	$( '#bodyContent' ).text( '' );
	$( '#bodyContent' ).append( windowManager.$element );
	$( '#bodyContent' ).append( index.$element );
	permChecks();
} );
// </nowiki>