User:Mr. Stradivarius/gadgets/SpamUserPage.js

From Wikipedia, the free encyclopedia
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.
// <nowiki>
/*
 * 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: '[[WP:CSD#G11|G11]]: Unambiguous [[WP:NOTADVERTISING|advertising]] or promotion',
			blocksummary: '{{uw-spamublock}}',
			editsummary: 'You have been indefinitely blocked from editing because your account is being ' +
				'used only for [[WP:SPAM|spam or advertising]] and your username is a violation of the ' +
				'[[WP:U|username policy]].',
			template: '{{subst:uw-spamublock|sig=yes}}'
		},
		{
			id: 'soablock',
			label: 'Spam-only account',
			deletesummary: '[[WP:CSD#G11|G11]]: Unambiguous [[WP:NOTADVERTISING|advertising]] or promotion',
			blocksummary: '[[WP:Spam|Spam]] / [[WP:NOTADVERTISING|advertising]]-only account',
			editsummary: 'You have been indefinitely blocked from editing because your account is being ' +
				'used only for [[WP:SPAM|spam, advertising, or promotion]]',
			template: '{{subst:uw-soablock|sig=yes}}'
		},
		{
			id: 'sblock',
			label: 'Spamming',
			deletesummary: '[[WP:CSD#G11|G11]]: Unambiguous [[WP:NOTADVERTISING|advertising]] or promotion',
			expiry: '31 hours',
			blocksummary: 'Using Wikipedia for [[WP:SPAM|spam]] purposes',
			editsummary: 'You have been blocked from editing for using Wikipedia for [[WP:SPAM|spam]] purposes',
			template: '{{subst:uw-sblock|sig=yes}}'
		},
		{
			id: 'vaublock',
			label: 'Vandalism-only account, username violation',
			deletesummary: '[[WP:CSD#G3|G3]]: [[WP:Vandalism|Vandalism]]',
			blocksummary: '{{uw-vaublock}}',
			editsummary: 'You have been indefinitely blocked from editing because your account is being ' +
				'[[WP:VOA|used only for vandalism]] and your username is a blatant violation of the ' +
				'[[WP:U|username policy]]',
			template: '{{subst:uw-vaublock|sig=yes}}'
		},
		{
			id: 'voablock',
			label: 'Vandalism-only account',
			deletesummary: '[[WP:CSD#G3|G3]]: [[WP:Vandalism|Vandalism]]',
			blocksummary: '[[WP:Vandalism-only account|Vandalism-only account]]',
			editsummary: 'You have been indefinitely blocked from editing because your account is being ' +
				'[[WP:VOA|used only for vandalism]]',
			template: '{{subst:uw-voablock|sig=yes}}'
		},
		{
			id: 'vblock',
			label: 'Vandalism',
			deletesummary: '[[WP:CSD#G3|G3]]: [[WP:Vandalism|Vandalism]]',
			expiry: '31 hours',
			blocksummary: '[[WP:Vandalism|Vandalism]]',
			editsummary: 'You have been blocked from editing for persistent [[WP:VAND|vandalism]]',
			template: '{{subst:uw-vblock|sig=yes}}'
		},
		{
			id: 'softerblock',
			label: 'Promotional username, soft block',
			deletesummary: '[[WP:CSD#G11|G11]]: Unambiguous [[WP:NOTADVERTISING|advertising]] or promotion',
			blocksummary: '{{uw-softerblock}}',
			editsummary: 'You have been indefinitely blocked from editing because your [[WP:U|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: '[[WP:CSD#G11|G11]]: Unambiguous [[WP:NOTADVERTISING|advertising]] or promotion',
			blocksummary: '{{uw-causeblock}}',
			editsummary: 'You have been indefinitely blocked from editing because your [[WP:U|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: '[[WP:CSD#G12|G12]]: Unambiguous [[WP:CV|copyright infringement]]',
			expiry: '24 hours',
			blocksummary: '[[WP:Copyright violations|Copyright violations]]',
			editsummary: 'You have been blocked from editing for continued [[WP:COPYVIO|copyright infringement]]',
			template: '{{subst:uw-copyrightblock|sig=yes}}'
		},
		{
			id: 'myblock',
			label: 'Using Wikipedia as a blog or web host',
			deletesummary: '[[WP:CSD#U5|U5]]: [[WP:NOTWEBHOST|Misuse of Wikipedia as a web host]]',
			expiry: '24 hours',
			blocksummary: 'Using Wikipedia as a [[WP:NOTMYSPACE|blog, web host, social networking site or forum]]',
			editsummary: 'You have been blocked from editing for using user and/or article pages ' +
				'as a [[WP:NOTMYSPACE|blog, web host, social networking site or forum]]',
			template: '{{subst:uw-myblock|sig=yes}}'
		},
		{
			id: 'npblock',
			label: 'Creating nonsense pages',
			deletesummary: '[[WP:CSD#G1|G1]]: [[WP:PN|Patent nonsense]], meaningless, or incomprehensible',
			expiry: '24 hours',
			blocksummary: 'Creating [[WP:Patent nonsense|patent nonsense]] or other inappropriate pages',
			editsummary: 'You have been blocked from editing for creating [[WP:PN|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.
			'[[WP:CSD#G1|G1]]: [[WP:PN|Patent nonsense]], meaningless, or incomprehensible',
			'[[WP:CSD#G2|G2]]: Test page',
			'[[WP:CSD#G3|G3]]: [[WP:Vandalism|Vandalism]]',
			'[[WP:CSD#G3|G3]]: Blatant [[WP:Do not create hoaxes|hoax]]',
			'[[WP:CSD#G4|G4]]: Recreation of a page that was [[WP:DEL|deleted]] per a [[WP:XFD|deletion discussion]]',
			'[[WP:CSD#G5|G5]]: Creation by a [[WP:BLOCK|blocked]] or [[WP:BAN|banned]] user in violation of block or ban',
			'[[WP:CSD#G6|G6]]: Housekeeping and routine (non-controversial) cleanup',
			'[[WP:CSD#G7|G7]]: One author who has requested deletion or blanked the page',
			'[[WP:CSD#G8|G8]]: Page dependent on a deleted or nonexistent page',
			'[[WP:CSD#G10|G10]]: [[WP:ATP|Attack page]] or negative unsourced [[WP:BLP|BLP]]',
			'[[WP:CSD#G11|G11]]: Unambiguous [[WP:NOTADVERTISING|advertising]] or promotion',
			'[[WP:CSD#G12|G12]]: Unambiguous [[WP:CV|copyright infringement]]',
			'[[WP:CSD#G13|G13]]: Abandoned [[WP:AFC|Article for creation]]? to retrieve it, see [[WP:REFUND/G13]]',
			'[[WP:CSD#U1|U1]]: User request to delete page in own userspace',
			'[[WP:CSD#U2|U2]]: Userpage or subpage of a nonexistent user',
			'[[WP:CSD#U3|U3]]: [[WP:NFC|Non-free]] [[Help:Gallery|gallery]]',
			'[[WP:CSD#U5|U5]]: [[WP:NOTWEBHOST|Misuse of Wikipedia as a web host]]'
		],
		blocksummary: [
			'{{uw-spamublock}}',
			'[[WP:Spam|Spam]] / [[WP:NOTADVERTISING|advertising]]-only account',
			'Using Wikipedia for [[WP:Spam|spam]] or [[WP:NOTADVERTISING|advertising]] purposes',
			'{{uw-vaublock}}',
			'[[WP:Vandalism-only account|Vandalism-only account]]',
			'[[WP:Vandalism|Vandalism]]',
			'[[WP:Copyright violations|Copyright violations]]',
			'{{uw-softerblock}}',
			'{{uw-causeblock}}',
			'Creating [[WP:Attack page|attack pages]]',
			'Violations of the [[WP:Biographies of living persons|Biographies of living persons]] policy',
			'Creating [[WP:Patent nonsense|patent nonsense]] or other inappropriate pages',
			'[[WP:Disruptive editing|Disruptive editing]]',
			'[[WP:No personal attacks|Personal attacks]] or [[WP:Harassment|harassment]]',
			'[[WP:Blocking policy#Evasion of blocks|Block evasion]]',
			'Abusing [[WP:Sock puppetry|multiple accounts]]',
			'[[WP:Long-term abuse|Long-term abuse]]',
		],
		editsummary: [
			'You have been blocked from editing because your account ' +
				'is being used only for [[WP:SPAM|spam or advertising]] and your ' +
				'username is a violation of the [[WP:U|username policy]].',
			'You have been blocked from editing for [[WP:SOAP|advertising or self-promotion]]',
			'You have been blocked from editing for using Wikipedia for [[WP:SPAM|spam]] purposes',
			'You have been blocked from editing because your account is being ' +
				'used only for [[WP:SPAM|spam, advertising, or promotion]]',
			'You have been indefinitely blocked from editing because your account ' +
				'is being [[WP:VOA|used only for vandalism]] and your username is ' +
				'a blatant violation of the [[WP:U|username policy]]',
			'You have been blocked from editing for persistent [[WP:VAND|vandalism]]',
			'You have been blocked from editing because your account is being ' +
				'[[WP:VOA|used only for vandalism]]',
			'You have been indefinitely blocked from editing because your [[WP:U|username]] ' +
				'gives the impression that the account represents a group, organization or website.',
			'You have been blocked from editing for continued [[WP:COPYVIO|copyright infringement]]',
			'You have been blocked from editing for using user and/or article ' +
				'pages as a [[WP:NOTMYSPACE|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: '<label missing>',
		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 = '([[WP:SUPG|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 += ' <!-- ' + apiManager.constructor.static.summarySuffix + ' -->';

		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( $( '<span>' )
					.addClass( 'spamuserpage-success' )
					.text( 'Done.' )
				);
			} );
			promise.fail( function ( id, obj ) {
				if ( obj && obj.error && obj.error.info ) {
					label.setLabel( $( '<span>' )
						.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();
} );

// </nowiki>