MediaWiki:Gadget-globalmassblock.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/**
* 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', '[[m:Special:MyLanguage/NOP|Open proxy]]: See the [[m:WM:OP/H|help page]] if you are affected', '[[m:Special:MyLanguage/NOP|Open proxy/Webhost]]: See the [[m:WM:OP/H|help page]] if you are affected <!-- INSERT PROVIDER -->' ],
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(
'<p> 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.<br />'
+ '<em>Try not to flood StewardBot out of the IRC channel!</em> </p>'
);
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 = $( '<a>' )
.attr( 'href', mw.config.get( 'wgServer' ) + '/wiki/Special:MassGlobalBlock' )
.text( 'Mass global block' );
$( '#contentSub > #mw-content-subtitle > a:last-child' ).after( ' | ', $a );
}