/**
* 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>