User:Mr. Stradivarius/gadgets/SpamUserPage.js

//

/*

* SpamUserPage

*

* This gadget deletes a user page, blocks the user, and notifies them on their

* talk page. See User:Mr. Stradivarius/gadgets/SpamUserPage for

* documentation.

*

* To install the script, add the following to your personal .js page:

importScript( 'User:Mr. Stradivarius/gadgets/SpamUserPage.js' ); // Linkback: User:Mr. Stradivarius/gadgets/SpamUserPage.js

* Author: Mr. Stradivarius

* Licence: MIT

*

* The MIT License (MIT)

*

* Copyright (c) 2015 Mr. Stradivarius

*

* Permission is hereby granted, free of charge, to any person obtaining a copy

* of this software and associated documentation files (the "Software"), to deal

* in the Software without restriction, including without limitation the rights

* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell

* copies of the Software, and to permit persons to whom the Software is

* furnished to do so, subject to the following conditions:

*

* The above copyright notice and this permission notice shall be included in

* all copies or substantial portions of the Software.

*

* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER

* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,

* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN

* THE SOFTWARE.

*/

mw.loader.using( [

'mediawiki.api',

'mediawiki.Title',

'mediawiki.util',

'oojs-ui'

], function () {

"use strict";

var config = mw.config.get( [

'wgTitle',

'wgNamespaceNumber',

'wgUserName'

] );

// Exit if we are not in user or user talk space.

if ( config.wgNamespaceNumber !== 2 && config.wgNamespaceNumber !== 3 ) {

return;

}

/**************************************************************************

* ApiManager class

**************************************************************************/

var ApiManager = function () {

var i, len, preset, key, customPresetIds, defaultPreset, hasUserPreference;

this.api = new mw.Api();

this.currentTitle = new mw.Title( config.wgTitle, config.wgNamespaceNumber );

this.userName = this.currentTitle.getMainText().replace( /\/.*$/, '' );

this.userTalkTitle = new mw.Title( this.userName, 3 );

this.userPreferences = typeof window.SpamUserPage === 'object' ? window.SpamUserPage : {};

// Set up caches

this.menuItemCache = {};

// Get presets

this.presets = {};

this.presetIds = [];

customPresetIds = []; // Track custom preset IDs separately so that we can put them first.

function setPreset( obj, thisArg ) {

var ret = thisArg.presets[ obj.id ] || {};

ret.id = obj.id;

for ( key in ApiManager.static.presetDefaults ) {

if ( ApiManager.static.presetDefaults.hasOwnProperty( key ) ) {

if ( obj[ key ] !== undefined ) {

ret[ key ] = obj[ key ];

}

if ( ret[ key ] === undefined ) {

ret[ key ] = ApiManager.static.presetDefaults[ key ];

}

}

}

thisArg.presets[ ret.id ] = ret;

}

for ( i = 0, len = ApiManager.static.presets.length; i < len; i++ ) {

preset = ApiManager.static.presets[ i ];

this.presetIds.push( preset.id );

setPreset( preset, this );

}

if ( $.isArray( this.userPreferences.presets ) ) {

for ( i = 0, len = this.userPreferences.presets.length; i < len; i++ ) {

preset = this.userPreferences.presets[ i ];

if ( typeof preset === 'object' ) {

if ( typeof preset.id === 'string' ) {

if ( !this.presets[ preset.id ] ) {

customPresetIds.push( preset.id );

}

setPreset( preset, this );

} else {

throw new Error( "missing or invalid 'id' field in custom preset #" + i );

}

}

}

}

this.presetIds = customPresetIds.concat( this.presetIds );

// Get defaults

this.defaults = {};

hasUserPreference = false;

if ( typeof this.userPreferences.preset === 'string' ) {

defaultPreset = this.userPreferences.preset;

if ( !this.presets[ defaultPreset ] ) {

throw new Error( "'" + defaultPreset + "' is not a valid preset ID" );

}

} else {

defaultPreset = ApiManager.static.otherDefaults.preset;

}

function setDefaults( obj, thisArg ) {

for ( key in obj ) {

if ( obj.hasOwnProperty( key ) ) {

if ( thisArg.userPreferences[ key ] !== undefined ) {

hasUserPreference = true;

thisArg.defaults[ key ] = thisArg.userPreferences[ key ];

} else if ( thisArg.presets[ defaultPreset ][ key ] !== undefined ) {

thisArg.defaults[ key ] = thisArg.presets[ defaultPreset ][ key ];

} else {

thisArg.defaults[ key ] = ApiManager.static.otherDefaults[ key ];

}

}

}

}

setDefaults( ApiManager.static.presetDefaults, this );

setDefaults( ApiManager.static.otherDefaults, this );

if ( hasUserPreference ) {

this.defaults.preset = null;

} else {

this.defaults.preset = defaultPreset;

}

};

OO.initClass( ApiManager );

ApiManager.static.presets = [

{

id: 'spamublock',

label: 'Spam-only account, username violation',

deletesummary: 'G11: Unambiguous advertising or promotion',

blocksummary: '{{uw-spamublock}}',

editsummary: 'You have been indefinitely blocked from editing because your account is being ' +

'used only for spam or advertising and your username is a violation of the ' +

'username policy.',

template: '{{subst:uw-spamublock|sig=yes}}'

},

{

id: 'soablock',

label: 'Spam-only account',

deletesummary: 'G11: Unambiguous advertising or promotion',

blocksummary: 'Spam / advertising-only account',

editsummary: 'You have been indefinitely blocked from editing because your account is being ' +

'used only for spam, advertising, or promotion',

template: '{{subst:uw-soablock|sig=yes}}'

},

{

id: 'sblock',

label: 'Spamming',

deletesummary: 'G11: Unambiguous advertising or promotion',

expiry: '31 hours',

blocksummary: 'Using Wikipedia for spam purposes',

editsummary: 'You have been blocked from editing for using Wikipedia for spam purposes',

template: '{{subst:uw-sblock|sig=yes}}'

},

{

id: 'vaublock',

label: 'Vandalism-only account, username violation',

deletesummary: 'G3: Vandalism',

blocksummary: '{{uw-vaublock}}',

editsummary: 'You have been indefinitely blocked from editing because your account is being ' +

'used only for vandalism and your username is a blatant violation of the ' +

'username policy',

template: '{{subst:uw-vaublock|sig=yes}}'

},

{

id: 'voablock',

label: 'Vandalism-only account',

deletesummary: 'G3: Vandalism',

blocksummary: 'Vandalism-only account',

editsummary: 'You have been indefinitely blocked from editing because your account is being ' +

'used only for vandalism',

template: '{{subst:uw-voablock|sig=yes}}'

},

{

id: 'vblock',

label: 'Vandalism',

deletesummary: 'G3: Vandalism',

expiry: '31 hours',

blocksummary: 'Vandalism',

editsummary: 'You have been blocked from editing for persistent vandalism',

template: '{{subst:uw-vblock|sig=yes}}'

},

{

id: 'softerblock',

label: 'Promotional username, soft block',

deletesummary: 'G11: Unambiguous advertising or promotion',

blocksummary: '{{uw-softerblock}}',

editsummary: 'You have been indefinitely blocked from editing because your username ' +

'gives the impression that the account represents a group, organization or website.',

template: '{{subst:uw-softerblock|sig=yes}}',

nocreate: false,

autoblock: false

},

{

id: 'causeblock',

label: 'Username of a non-profit, soft block',

deletesummary: 'G11: Unambiguous advertising or promotion',

blocksummary: '{{uw-causeblock}}',

editsummary: 'You have been indefinitely blocked from editing because your username ' +

'gives the impression that the account represents a group, organization or website.',

template: '{{subst:uw-causeblock|sig=yes}}',

nocreate: false,

autoblock: false

},

{

id: 'copyrightblock',

label: 'Copyright violations',

deletesummary: 'G12: Unambiguous copyright infringement',

expiry: '24 hours',

blocksummary: 'Copyright violations',

editsummary: 'You have been blocked from editing for continued copyright infringement',

template: '{{subst:uw-copyrightblock|sig=yes}}'

},

{

id: 'myblock',

label: 'Using Wikipedia as a blog or web host',

deletesummary: 'U5: Misuse of Wikipedia as a web host',

expiry: '24 hours',

blocksummary: 'Using Wikipedia as a blog, web host, social networking site or forum',

editsummary: 'You have been blocked from editing for using user and/or article pages ' +

'as a blog, web host, social networking site or forum',

template: '{{subst:uw-myblock|sig=yes}}'

},

{

id: 'npblock',

label: 'Creating nonsense pages',

deletesummary: 'G1: Patent nonsense, meaningless, or incomprehensible',

expiry: '24 hours',

blocksummary: 'Creating patent nonsense or other inappropriate pages',

editsummary: 'You have been blocked from editing for creating nonsense pages',

template: '{{subst:uw-npblock|sig=yes}}'

}

];

ApiManager.static.menuDefaults = {

expiry: [

'indefinite',

'3 hours',

'12 hours',

'24 hours',

'31 hours',

'36 hours',

'48 hours',

'60 hours',

'72 hours',

'1 week',

'2 weeks',

'1 month',

'3 months',

'6 months',

'1 year',

'2 years',

'3 years'

],

deletesummary: [

// If you change the order of these, please also update the number of the

// default deletion summary in ApiManager.static.presetDefaults.

'G1: Patent nonsense, meaningless, or incomprehensible',

'G2: Test page',

'G3: Vandalism',

'G3: Blatant hoax',

'G4: Recreation of a page that was deleted per a deletion discussion',

'G5: Creation by a blocked or banned user in violation of block or ban',

'G6: Housekeeping and routine (non-controversial) cleanup',

'G7: One author who has requested deletion or blanked the page',

'G8: Page dependent on a deleted or nonexistent page',

'G10: Attack page or negative unsourced BLP',

'G11: Unambiguous advertising or promotion',

'G12: Unambiguous copyright infringement',

'G13: Abandoned Article for creation? to retrieve it, see WP:REFUND/G13',

'U1: User request to delete page in own userspace',

'U2: Userpage or subpage of a nonexistent user',

'U3: Non-free gallery',

'U5: Misuse of Wikipedia as a web host'

],

blocksummary: [

'{{uw-spamublock}}',

'Spam / advertising-only account',

'Using Wikipedia for spam or advertising purposes',

'{{uw-vaublock}}',

'Vandalism-only account',

'Vandalism',

'Copyright violations',

'{{uw-softerblock}}',

'{{uw-causeblock}}',

'Creating attack pages',

'Violations of the Biographies of living persons policy',

'Creating patent nonsense or other inappropriate pages',

'Disruptive editing',

'Personal attacks or harassment',

'Block evasion',

'Abusing multiple accounts',

'Long-term abuse',

],

editsummary: [

'You have been blocked from editing because your account ' +

'is being used only for spam or advertising and your ' +

'username is a violation of the username policy.',

'You have been blocked from editing for advertising or self-promotion',

'You have been blocked from editing for using Wikipedia for spam purposes',

'You have been blocked from editing because your account is being ' +

'used only for spam, advertising, or promotion',

'You have been indefinitely blocked from editing because your account ' +

'is being used only for vandalism and your username is ' +

'a blatant violation of the username policy',

'You have been blocked from editing for persistent vandalism',

'You have been blocked from editing because your account is being ' +

'used only for vandalism',

'You have been indefinitely blocked from editing because your username ' +

'gives the impression that the account represents a group, organization or website.',

'You have been blocked from editing for continued copyright infringement',

'You have been blocked from editing for using user and/or article ' +

'pages as a blog, web host, social networking site or forum',

],

template: [

'{{subst:uw-spamublock|sig=yes}}',

'{{subst:uw-sblock|sig=yes}}',

'{{subst:uw-soablock|sig=yes}}',

'{{subst:uw-adblock|sig=yes}}',

'{{subst:uw-aoablock|sig=yes}}',

'{{subst:uw-vaublock|sig=yes}}',

'{{subst:uw-vblock|sig=yes}}',

'{{subst:uw-voablock|sig=yes}}',

'{{subst:uw-softerblock|sig=yes}}',

'{{subst:uw-causeblock|sig=yes}}',

'{{subst:uw-copyrightblock|sig=yes}}',

'{{subst:uw-myblock|sig=yes}}'

]

};

ApiManager.static.presetDefaults = {

label: '

deletesummary: ApiManager.static.menuDefaults.deletesummary[ 10 ],

expiry: ApiManager.static.menuDefaults.expiry[ 0 ],

blocksummary: ApiManager.static.menuDefaults.blocksummary[ 0 ],

nocreate: true,

autoblock: true,

noemail: false,

nousertalk: false,

template: ApiManager.static.menuDefaults.template[ 0 ],

editsummary: ApiManager.static.menuDefaults.editsummary[ 0 ]

};

ApiManager.static.otherDefaults = {

preset :ApiManager.static.presets[ 0 ].id,

watchlist: 'preferences',

menulocation: 'p-cactions',

menuposition: null,

oneclick: false

};

ApiManager.static.months = [

'January',

'February',

'March',

'April',

'May',

'June',

'July',

'August',

'September',

'October',

'November',

'December'

];

ApiManager.static.summarySuffix = '(SUPG)';

ApiManager.prototype.getCurrentTitle = function () {

return this.currentTitle;

};

ApiManager.prototype.getUserName = function () {

return this.userName;

};

ApiManager.prototype.getUserTalkTitle = function () {

return this.userTalkTitle;

};

ApiManager.prototype._getNotificationDate = function () {

// We cache the result of this method so that we will always get the

// same date string as we used for the talk notification, even if a

// user happened to have the page open over a month boundary.

var date, month;

if ( !this.notificationDate ) {

date = new Date();

month = this.constructor.static.months[ date.getMonth() ];

this.notificationDate = month + ' ' + date.getFullYear();

}

return this.notificationDate;

};

ApiManager.prototype.getDefault = function ( key ) {

return this.defaults[ key ];

};

ApiManager.prototype.getPresetIds = function () {

return this.presetIds;

};

ApiManager.prototype.getPresetValue = function ( presetId, key ) {

return this.presets[ presetId ][ key ];

};

ApiManager.prototype.getMenuItems = function ( key ) {

var menuItems, pref, keyItems, isDupe, i, len, val;

if ( typeof key !== 'string' ) {

throw new TypeError( "argument #1 to 'getMenuItems' was not a string" );

}

function copyArray ( arr ) {

var ret = [];

for ( var i = 0, len = arr.length; i < len; i++ ) {

ret[ i ] = arr[ i ];

}

return ret;

}

if ( this.menuItemCache[ key ] === undefined ) {

menuItems = this.constructor.static.menuDefaults[ key ];

if ( menuItems ) {

this.menuItemCache[ key ] = menuItems;

pref = this.userPreferences[ key ];

if ( pref ) {

isDupe = false;

for ( i = 0, len = menuItems.length; i < len; i++ ) {

val = menuItems[ i ];

if ( val === pref ) {

isDupe = true;

break;

}

}

if ( !isDupe ) {

// Create a copy of the static array so we can manipulate it.

menuItems = copyArray( menuItems );

menuItems.unshift( pref );

this.menuItemCache[ key ] = menuItems;

}

}

} else {

this.menuItemCache[ key ] = null;

}

}

return this.menuItemCache[ key ];

};

ApiManager.prototype.addPortletLink = function () {

return mw.util.addPortletLink(

this.getDefault( 'menulocation' ),

'#',

'Delete and block',

'ca-spamuserpage',

'Delete this page and block this user',

null,

this.getDefault( 'menuposition' )

);

};

ApiManager.prototype.openTalkPage = function () {

window.open(

this.getUserTalkTitle().getUrl() +

'#' +

mw.util.wikiUrlencode( this._getNotificationDate() ),

'_top'

);

};

ApiManager.prototype._deleteCurrentPage = function ( options ) {

options = options || {};

var reason = options.deletesummary !== undefined ?

options.deletesummary :

this.getDefault( 'deletesummary' );

reason += ' ' + this.constructor.static.summarySuffix;

return this.api.postWithToken( 'delete', {

format: 'json',

action: 'delete',

title: this.currentTitle.getPrefixedText(),

reason: reason,

watchlist: options.watchlist || this.getDefault( 'watchlist' )

} );

};

ApiManager.prototype._blockUser = function ( options, deletePromise ) {

var deleteFinishedPromise, reason;

var apiManager = this;

options = options || {};

// Make a new promise that is resolved when the delete promise is

// either resolved or rejected.

deleteFinishedPromise = $.Deferred( function ( deferred ) {

deletePromise.always( function () {

deferred.resolve();

} );

} ).promise();

reason = options.blocksummary !== undefined ?

options.blocksummary :

apiManager.getDefault( 'blocksummary' );

reason += ' ';

return deleteFinishedPromise.then( function () {

return apiManager.api.postWithToken( 'block', {

format: 'json',

action: 'block',

user: apiManager.getUserName(),

expiry: options.expiry !== undefined ?

options.expiry :

apiManager.getDefault( 'expiry' ),

reason: reason,

nocreate: options.nocreate ? '' : undefined,

autoblock: options.autoblock ? '' : undefined,

noemail: options.noemail ? '' : undefined,

allowusertalk: !options.nousertalk ? '' : undefined

} );

} );

};

ApiManager.prototype._postTalkNotification = function ( options, blockPromise ) {

var self = this;

options = options || {};

return $.Deferred( function ( deferred ) {

// Reject the deferred if the block failed.

blockPromise.fail( function () {

return deferred.reject( 'usernotblocked', { error: {

id: 'usernotblocked',

info: 'There was an error while trying to block the user, ' +

'so notification was aborted'

} } );

} );

blockPromise.done( function () {

// Get the talk page content. We will use this to check if there is

// already a section heading for the current month.

return self.api.get( {

format: 'json',

action: 'query',

prop: 'revisions',

rvprop: 'content',

indexpageids: '',

titles: self.getUserTalkTitle().getPrefixedText(),

redirects: ''

} ).then( function ( obj ) {

var title, content, headings, lastHeading,

notificationDate, editPromise, summary;

var template = options.template !== undefined ?

options.template :

self.getDefault( 'template' );

var pageid = obj.query.pageids[ 0 ];

// If we got a redirect from the GET request, follow it.

if ( obj.query.redirects ) {

title = obj.query.redirects[ 0 ].to;

} else {

title = self.getUserTalkTitle().getPrefixedText();

}

if ( pageid === '-1' ) {

// The page doesn't exist.

content = '';

} else {

// The page exists; get the content.

content = obj.query.pages[ pageid ].revisions[ 0 ][ '*' ];

// Separate our notification from whatever the previous content was.

if ( content.match( /\S/ ) ) {

content += '\n\n';

}

}

// Add a heading for the current month if it is not already the

// last heading on the page.

headings = content.match( /^==[^=].*==[ \t]*$/gm );

if ( headings ) {

lastHeading = headings[ headings.length - 1 ].slice( 2, -2 ).trim();

}

notificationDate = self._getNotificationDate();

if ( !lastHeading || lastHeading !== notificationDate ) {

content += '== ' + notificationDate + ' ==\n\n';

}

// Add the block template.

content += template;

// Generate the edit summary.

summary = options.editsummary !== undefined ?

options.editsummary :

self.getDefault( 'editsummary' );

summary += ' ' + self.constructor.static.summarySuffix;

// Overwrite the page with our new content.

editPromise = self.api.postWithToken( 'edit', {

format: 'json',

action: 'edit',

title: title,

summary: summary,

notminor: '',

text: content,

watchlist: options.watchlist || self.getDefault( 'watchlist' )

} );

editPromise.done( function () {

return deferred.resolve();

} );

editPromise.fail( function ( id, obj ) {

return deferred.reject( id, obj );

} );

} );

} );

} ).promise();

};

ApiManager.prototype.submit = function ( options ) {

var promises = {};

options = options || {};

promises[ 'delete' ] = this._deleteCurrentPage( options );

promises.block = this._blockUser( options, promises[ 'delete' ] );

promises.notify = this._postTalkNotification( options, promises.block );

promises.all = $.when(

promises[ 'delete' ],

promises.block,

promises.notify

);

return promises;

};

/**************************************************************************

* Dialog class

**************************************************************************/

var Dialog = function ( options ) {

options = options || {};

this.apiManager = new ApiManager();

this.hasBeenSubmitted = false;

Dialog.super.call( this, options );

};

OO.inheritClass( Dialog, OO.ui.ProcessDialog );

Dialog.static.name = 'SpamUserPageDialog';

Dialog.static.title = 'Delete, block and notify';

Dialog.static.actions = [

{ action: 'submit', label: 'Submit', flags: [ 'primary', 'destructive' ] },

{ label: 'Cancel', flags: 'safe' }

];

Dialog.prototype.getApiManager = function () {

return this.apiManager;

};

Dialog.prototype.getBodyHeight = function () {

return 440;

};

Dialog.prototype.initialize = function () {

Dialog.super.prototype.initialize.apply( this, arguments );

var apiManager = this.getApiManager();

// Converts an array of data from ApiManager into a format usable with

// OOjs-ui menus.

function makeMenu( arr, labelCallback ) {

var i, len;

var items = [];

for ( i = 0, len = arr.length; i < len; i++ ) {

items[ i ] = new OO.ui.MenuOptionWidget( {

data: arr[ i ],

label: labelCallback ? labelCallback( arr[ i ] ) : arr[ i ]

} );

}

return { items: items };

}

// Initialize edit panel

this.editPanel = new OO.ui.PanelLayout( {

expanded: false

} );

this.editFieldset = new OO.ui.FieldsetLayout( {

classes: [ 'container' ]

} );

this.editPanel.$element.append( this.editFieldset.$element );

// Initialize preset widget

this.presetDropdown = new OO.ui.DropdownWidget( {

data: { label: 'Select a preset' },

menu: makeMenu( apiManager.getPresetIds(), function ( id ) {

return apiManager.getPresetValue( id, 'label' );

} )

} );

if ( apiManager.getDefault( 'preset' ) ) {

this.presetDropdown.getMenu().selectItemByData(

apiManager.getDefault( 'preset' )

);

} else {

this.presetDropdown.setLabel( this.presetDropdown.getData().label );

}

this.editFieldset.addItems( [

new OO.ui.FieldLayout( this.presetDropdown, {

label: 'Preset',

} )

] );

// Initialize deletion widgets

this.deleteSummaryInput = new OO.ui.ComboBoxInputWidget( {

value: apiManager.getDefault( 'deletesummary' ),

menu: makeMenu( apiManager.getMenuItems( 'deletesummary' ) )

} );

this.editFieldset.addItems( [

new OO.ui.FieldLayout( this.deleteSummaryInput, {

label: 'Deletion summary'

} )

] );

// Initialize notification widgets

this.editSummaryInput = new OO.ui.ComboBoxInputWidget( {

value: apiManager.getDefault( 'editsummary' ),

menu: makeMenu( apiManager.getMenuItems( 'editsummary' ) )

} );

this.templateInput = new OO.ui.ComboBoxInputWidget( {

value: apiManager.getDefault( 'template' ),

menu: makeMenu( apiManager.getMenuItems( 'template' ) )

} );

this.editFieldset.addItems( [

new OO.ui.FieldLayout( this.templateInput, {

label: 'Notification template'

} ),

new OO.ui.FieldLayout( this.editSummaryInput, {

label: 'Edit summary'

} )

] );

// Initialize block widgets

this.blockSummaryInput = new OO.ui.ComboBoxInputWidget( {

value: apiManager.getDefault( 'blocksummary' ),

menu: makeMenu( apiManager.getMenuItems( 'blocksummary' ) )

} );

this.expiryInput = new OO.ui.ComboBoxInputWidget( {

value: apiManager.getDefault( 'expiry' ),

menu: makeMenu( apiManager.getMenuItems( 'expiry' ) )

} );

this.nocreateToggleButton = new OO.ui.ToggleButtonWidget( {

label: 'Block account creation',

value: apiManager.getDefault( 'nocreate' )

} );

this.noemailToggleButton = new OO.ui.ToggleButtonWidget( {

label: 'Block email',

value: apiManager.getDefault( 'noemail' )

} );

this.nousertalkToggleButton = new OO.ui.ToggleButtonWidget( {

label: 'Block talk',

value: apiManager.getDefault( 'nousertalk' )

} );

this.autoblockToggleButton = new OO.ui.ToggleButtonWidget( {

label: 'Autoblock',

value: apiManager.getDefault( 'autoblock' )

} );

this.blockButtonGroup = new OO.ui.ButtonGroupWidget( {

items: [

this.nocreateToggleButton,

this.noemailToggleButton,

this.nousertalkToggleButton,

this.autoblockToggleButton

]

} );

this.editFieldset.addItems( [

new OO.ui.FieldLayout( this.blockSummaryInput, {

label: 'Block summary'

} ),

new OO.ui.FieldLayout( this.expiryInput, {

label: 'Block expiry'

} ),

new OO.ui.FieldLayout( this.blockButtonGroup, {

label: 'Block options',

align: 'top'

} ),

] );

// Initialize watchlist widgets

this.watchlistOptions = {};

this.watchlistOptions.watch = new OO.ui.ButtonOptionWidget( {

data: 'watch',

label: 'Watch',

title: 'Watch option'

} );

this.watchlistOptions.unwatch = new OO.ui.ButtonOptionWidget( {

data: 'unwatch',

label: 'Unwatch',

title: 'Unwatch option'

} );

this.watchlistOptions.preferences = new OO.ui.ButtonOptionWidget( {

data: 'preferences',

label: 'Follow preferences',

title: 'Follow preferences option'

} );

this.watchlistOptions.nochange = new OO.ui.ButtonOptionWidget( {

data: 'nochange',

label: 'No change',

title: 'No change option'

} );

this.watchlistButtonSelect = new OO.ui.ButtonSelectWidget( {

items: [

this.watchlistOptions.watch,

this.watchlistOptions.unwatch,

this.watchlistOptions.preferences,

this.watchlistOptions.nochange

]

} );

this.watchlistButtonSelect.selectItemByData(

apiManager.getDefault( 'watchlist' )

);

this.editFieldset.addItems( [

new OO.ui.FieldLayout( this.watchlistButtonSelect, {

label: 'Watchlist options',

align: 'top'

} )

] );

// Initialize submit panel

this.submitPanel = new OO.ui.PanelLayout( {

$: this.$,

expanded: false

} );

this.submitFieldset = new OO.ui.FieldsetLayout( {

classes: [ 'container' ]

} );

this.submitPanel.$element.append( this.submitFieldset.$element );

this.deleteProgressLabel = new OO.ui.LabelWidget();

this.deleteProgressField = new OO.ui.FieldLayout( this.deleteProgressLabel );

this.blockProgressLabel = new OO.ui.LabelWidget();

this.blockProgressField = new OO.ui.FieldLayout( this.blockProgressLabel );

this.notifyProgressLabel = new OO.ui.LabelWidget();

this.notifyProgressField = new OO.ui.FieldLayout( this.notifyProgressLabel );

this.openTalkProgressField = new OO.ui.FieldLayout( new OO.ui.LabelWidget() );

this.submitFieldset.addItems( [

this.deleteProgressField,

this.blockProgressField,

this.notifyProgressField,

this.openTalkProgressField

] );

// Initialize stack widget

this.stackLayout= new OO.ui.StackLayout( {

items: [ this.editPanel, this.submitPanel ],

padded: true

} );

// Add widgets to the DOM

this.$body.append( this.stackLayout.$element );

// Add event handlers

if ( apiManager.getDefault( 'preset' ) ) {

this.changeInputEventHandlers( 'on', this.onFirstChangeAfterPreset, null, this );

}

this.presetDropdown.getMenu().on( 'select', this.onPresetSelect, null, this );

};

Dialog.prototype.onPresetSelect = function () {

var presetId = this.presetDropdown.getMenu().findSelectedItem().getData();

var apiManager = this.getApiManager();

// Detach any input event handlers. If they are still attached, they will

// clear the preset selection, which we don't want.

this.changeInputEventHandlers( 'off', this.onFirstChangeAfterPreset, this );

// Set values

this.deleteSummaryInput.getInput().setValue(

apiManager.getPresetValue( presetId, 'deletesummary' )

);

this.deleteSummaryInput.getMenu().toggle( false );

this.expiryInput.getInput().setValue(

apiManager.getPresetValue( presetId, 'expiry' )

);

this.expiryInput.getMenu().toggle( false );

this.templateInput.getInput().setValue(

apiManager.getPresetValue( presetId, 'template' )

);

this.templateInput.getMenu().toggle( false );

this.editSummaryInput.getInput().setValue(

apiManager.getPresetValue( presetId, 'editsummary' )

);

this.editSummaryInput.getMenu().toggle( false );

this.blockSummaryInput.getInput().setValue(

apiManager.getPresetValue( presetId, 'blocksummary' )

);

this.blockSummaryInput.getMenu().toggle( false );

this.nocreateToggleButton.setValue(

apiManager.getPresetValue( presetId, 'nocreate' )

);

this.autoblockToggleButton.setValue(

apiManager.getPresetValue( presetId, 'autoblock' )

);

this.noemailToggleButton.setValue(

apiManager.getPresetValue( presetId, 'noemail' )

);

this.nousertalkToggleButton.setValue(

apiManager.getPresetValue( presetId, 'nousertalk' )

);

// Attach one-time event handler

this.changeInputEventHandlers( 'on', this.onFirstChangeAfterPreset, null, this );

};

Dialog.prototype.changeInputEventHandlers = function ( method, func, arg3, arg4 ) {

this.deleteSummaryInput.getInput()[ method ]( 'change', func, arg3, arg4 );

this.deleteSummaryInput.getMenu()[ method ]( 'select', func, arg3, arg4 );

this.expiryInput.getInput()[ method ]( 'change', func, arg3, arg4 );

this.expiryInput.getMenu()[ method ]( 'select', func, arg3, arg4 );

this.templateInput.getInput()[ method ]( 'change', func, arg3, arg4 );

this.templateInput.getMenu()[ method ]( 'select', func, arg3, arg4 );

this.editSummaryInput.getInput()[ method ]( 'change', func, arg3, arg4 );

this.editSummaryInput.getMenu()[ method ]( 'select', func, arg3, arg4 );

this.blockSummaryInput.getInput()[ method ]( 'change', func, arg3, arg4 );

this.blockSummaryInput.getMenu()[ method ]( 'select', func, arg3, arg4 );

this.nocreateToggleButton[ method ]( 'change', func, arg3, arg4 );

this.autoblockToggleButton[ method ]( 'change', func, arg3, arg4 );

this.noemailToggleButton[ method ]( 'change', func, arg3, arg4 );

this.nousertalkToggleButton[ method ]( 'change', func, arg3, arg4 );

};

Dialog.prototype.onFirstChangeAfterPreset = function () {

// Detach all input event handlers

this.changeInputEventHandlers( 'off', this.onFirstChangeAfterPreset, this );

// Clear the selected preset. Before doing this we detach its select event

// listener, as that will cause problems if it's still attached.

this.presetDropdown.getMenu().off( 'select', this.onPresetSelect, this );

this.presetDropdown.getMenu().selectItem();

this.presetDropdown.setLabel( this.presetDropdown.getData().label );

this.presetDropdown.getMenu().on( 'select', this.onPresetSelect, null, this );

};

Dialog.prototype.onSubmit = function () {

var self = this;

var options, promises;

var apiManager = this.getApiManager();

var title = apiManager.getCurrentTitle().getPrefixedText();

var user = apiManager.getUserName();

// Record that the dialog has been submitted. This is necessary to

// prevent the "Submit" button from being reactivated after the window

// is closed and reopened.

self.hasBeenSubmitted = true;

// Disable input

self.actions.setAbilities( { submit: false } );

// Increase the pending level by 4: one each for the delete, block,

// notify, and all promises.

self.pushPending();

self.pushPending();

self.pushPending();

self.pushPending();

// Set progress labels

self.deleteProgressField.setLabel( 'Deleting "' + title + '"...' );

self.blockProgressField.setLabel( 'Blocking ' + user + '...' );

self.notifyProgressField.setLabel( 'Notifying ' + user + '...' );

self.stackLayout.setItem( self.submitPanel );

// Get options and submit

options = {

watchlist: self.watchlistButtonSelect.findSelectedItem().getData(),

deletesummary: self.deleteSummaryInput.getInput().getValue(),

expiry: self.expiryInput.getInput().getValue(),

template: self.templateInput.getInput().getValue(),

editsummary: self.editSummaryInput.getInput().getValue(),

blocksummary: self.blockSummaryInput.getInput().getValue(),

nocreate: self.nocreateToggleButton.getValue(),

autoblock: self.autoblockToggleButton.getValue(),

noemail: self.noemailToggleButton.getValue(),

nousertalk: self.nousertalkToggleButton.getValue()

};

promises = self.getApiManager().submit( options );

// Update progress

// This involves editing the progress labels and reducing the pending

// level.

function updateProgress( promise, label ) {

promise.done( function () {

label.setLabel( $( '' )

.addClass( 'spamuserpage-success' )

.text( 'Done.' )

);

} );

promise.fail( function ( id, obj ) {

if ( obj && obj.error && obj.error.info ) {

label.setLabel( $( '' )

.addClass( 'spamuserpage-error' )

.text( 'Error: ' + obj.error.info + '.' )

);

} else {

label.setLabel( 'An unknown error occurred.' );

}

} );

promise.always( function () {

self.popPending();

} );

}

updateProgress( promises[ 'delete' ], self.deleteProgressLabel );

updateProgress( promises.block, self.blockProgressLabel );

updateProgress( promises.notify, self.notifyProgressLabel );

// Clean up when everything is done

promises.all.done( function () {

self.openTalkProgressField.setLabel(

'Opening "' +

apiManager.getUserTalkTitle().getPrefixedText() +

'"...'

);

apiManager.openTalkPage();

} );

promises.all.fail( function () {

// Pop pending only on failure, so that it still looks like we're

// loading something while the talk page is being opened.

self.popPending();

} );

};

Dialog.prototype.getReadyProcess = function ( data ) {

// Parent getReadyProcess method

return Dialog.super.prototype.getReadyProcess.call( this, data )

.next( function () {

if ( this.hasBeenSubmitted ) {

// The dialog has already been submitted when it was previously

// opened, so disable the Submit button.

this.actions.setAbilities( { submit: false } );

} else if ( this.getApiManager().getDefault( 'oneclick' ) ) {

this.executeAction( 'submit' );

}

}, this );

};

Dialog.prototype.getActionProcess = function ( action ) {

return Dialog.super.prototype.getActionProcess.call( this, action )

.next( function () {

if ( action === 'submit' ) {

return this.onSubmit();

} else {

return Dialog.super.prototype.getActionProcess.call( this, action );

}

}, this );

};

/**************************************************************************

* Main

**************************************************************************/

function main() {

var portletLink;

var supDialog = new Dialog( { size: 'large' } );

var apiManager = supDialog.getApiManager();

var userName = apiManager.getUserName();

if (

// Don't run in the current user's userspace or on any user talk pages.

userName !== config.wgUserName &&

!(

config.wgNamespaceNumber === 3 &&

config.wgTitle === userName

)

) {

// Load CSS

importStylesheet( "User:Mr. Stradivarius/gadgets/SpamUserPage.css" );

// Set up window manager

var windowManager = new OO.ui.WindowManager();

$( 'body' ).append( windowManager.$element );

windowManager.addWindows( [ supDialog ] );

// Add portlet link

portletLink = apiManager.addPortletLink();

$( portletLink ).click( function ( event ) {

event.preventDefault();

windowManager.openWindow( supDialog );

} );

}

}

main();

} );

//