Jump to content

User:Evad37/XFDcloser/v3.js: Difference between revisions

From Wikipedia, the free encyclopedia
Content deleted Content added
Version 3.5.4: Prevent query http errors when retrieving page information (by just rejecting those with more than 50 pages)
Fix script
Line 1,751: Line 1,751:
),
),
// Rcats
// Rcats
/*
$('<div>').append(
$('<div>').append(
$('<input>').attr({
$('<input>').attr({
Line 1,767: Line 1,768:
'{{R to anchor}}". Leave blank if unsure which rcat to use.')
'{{R to anchor}}". Leave blank if unsure which rcat to use.')
),
),
self.rcatSelector.$element.detach()
//self.rcatSelector.$element.detach()
)
)*/
),
),
Line 2,052: Line 2,053:
if ( config.xfd.type !== 'ffd' && !self.discussion.isBasicMode() ) {
if ( config.xfd.type !== 'ffd' && !self.discussion.isBasicMode() ) {
this.rcatSelector = extraJs.makeRcatCapsuleMultiselect(self.storeRcatData, self);
//this.rcatSelector = extraJs.makeRcatCapsuleMultiselect(self.storeRcatData, self);
}
}
Line 2,072: Line 2,073:
} else {
} else {
if ( config.xfd.type === 'afd' ) {
if ( config.xfd.type === 'afd' ) {
this.rcatSelector.setItemsFromData(['{{R to related topic}}']);
//this.rcatSelector.setItemsFromData(['{{R to related topic}}']);
}
}
this.addToBody(this.makeAfterActions());
this.addToBody(this.makeAfterActions());
Line 2,093: Line 2,094:
if ( config.xfd.type !== 'ffd' && !self.discussion.isBasicMode() ) {
if ( config.xfd.type !== 'ffd' && !self.discussion.isBasicMode() ) {
this.rcatSelector = extraJs.makeRcatCapsuleMultiselect(self.storeRcatData, self);
//this.rcatSelector = extraJs.makeRcatCapsuleMultiselect(self.storeRcatData, self);
}
}
Line 2,109: Line 2,110:
this.addToBody(this.makeAfterActions(true));
this.addToBody(this.makeAfterActions(true));
if ( config.xfd.type === 'afd' ) {
if ( config.xfd.type === 'afd' ) {
this.rcatSelector.setItemsFromData(['{{R to related topic}}']);
//this.rcatSelector.setItemsFromData(['{{R to related topic}}']);
}
}
this.addToBody(this.makeOptions(true));
this.addToBody(this.makeOptions(true));

Revision as of 23:53, 20 October 2018

/*******************************************************************************
 XFDcloser --- by Evad37
 > A script that assists in closing AFD, CFD, FFD, MFD, RFD, and TFD discussions
--------------------------------------------------------------------------------
* See [[User:Evad37/XFDcloser]] for installation details and documentation
--------------------------------------------------------------------------------
* Version 3.5.4-sandbox
* See page history for change log
--------------------------------------------------------------------------------
* IMPORTANT NOTE:
  You are responsible for any edits/actions performed using XFDcloser! Please
  make sure you understand relevant Wikipedia policies and procedures, and use 
  this tool accordingly.
  (Some pertinent pages are: [[WP:CON]], [[WP:CLOSE]], [[WP:NAC]])
--------------------------------------------------------------------------------
* Licencing note:
  Originally published on the English Wikipedia at
  < https://en.wikipedia.org/wiki/User:Evad37/XFDcloser.js > ( and/or 
  < https://en.wikipedia.org/wiki/User:Evad37/XFDcloser/sandbox.js > and/or
  < https://en.wikipedia.org/wiki/User:Evad37/XFDcloser/v3.js > )
  and available under Creative Commons Attribution-ShareAlike 3.0 Unported
  License (CC BY-SA 3.0) < https://creativecommons.org/licenses/by-sa/3.0/ > and
  GNU Free Documentation License (GFDL) < http://www.gnu.org/copyleft/fdl.html >
* Attribution note:
  This script incorporates code derived from, and is a direct
  replacement for, these other scripts also written by Evad37:
  - https://en.wikipedia.org/wiki/User:Evad37/CFDcloser.js
  - https://en.wikipedia.org/wiki/User:Evad37/FFDcloser.js
  - https://en.wikipedia.org/wiki/User:Evad37/TFDcloser.js
  Futhermore, it incorporates code copied/derived from:
  - https://en.wikipedia.org/wiki/User:Mr.Z-man/closeAFD2.js
  - https://en.wikipedia.org/wiki/MediaWiki:Gadget-twinkleunlink.js
  - https://en.wikipedia.org/wiki/MediaWiki:Gadget-morebits.js
  Further notes:
   - See these pages' "History" pages for their authors.
   - All of the these pages are also published on Wikipedia, and thus also
     available under the terms of the CC BY-SA 3.0 license and GFDL
--------------------------------------------------------------------------------
	Have fun, and happy editing! - Evad37
*******************************************************************************/
/* <nowiki> */

/* ========== Dependencies ====================================================================== */
// Resource loader modules
mw.loader.using( ['mediawiki.util', 'mediawiki.api', 'mediawiki.Title', 'mediawiki.RegExp',
	'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows', 'jquery.ui.dialog'], function () {
// Load Morebits gadget if not already available
if ( window.Morebits == null ) {
	importScript('MediaWiki:Gadget-morebits.js');
	importStylesheet( 'MediaWiki:Gadget-morebits.css' );
}
// Load extra.js if not already available
if ( window.extraJs == null ) {
	importScript('User:Evad37/extra.js');
}
// Wait for page load
$( function($) {

/* ========== Config ============================================================================ */
// A global object that stores all the page and user configuration and settings
var config = {};
// Script info
config.script = {
	// Advert to append to edit summaries
	advert:  ' ([[WP:XFDC|XFDcloser]])',
	version: '3.5.4'
};
// MediaWiki configuration values
config.mw = mw.config.get( [
	'wgPageName',
	'wgUserGroups',
	'wgUserName',
	'wgFormattedNamespaces',
	'wgMonthNames'
] );

/* --------- Quick checks that script should be running ----------------------------------------- */
if ( /(?:\?|&)(?:action|diff|oldid)=/.test(window.location.href) ) {
	console.log('[XFDcloser] Page is in edit, history, diff, or oldid mode');
	return;
}

if (
	-1 === $.inArray('extendedconfirmed', config.mw.wgUserGroups) &&
	-1 === $.inArray('sysop', config.mw.wgUserGroups)
) {
	console.log('[XFDcloser] User is not extendedconfirmed or sysop');
	return;
}

var xfdpage_regex = /(Articles_for_deletion\/|Miscellany_for_deletion|User:Cyberbot_I\/AfD's_requiring_attention|Wikipedia:WikiProject_Deletion_sorting\/(?!(Flat|Compact)$)|(Categories|Files|Templates|Redirects)_for_discussion(?!\/(Working|Holding_cell)))(?!\/?Administrator_instructions$)/;
if ( !xfdpage_regex.test(config.mw.wgPageName) ) {
	console.log('[XFDcloser] Current page is not an XfD page');
	return;
}

/* --------- CSS rules -------------------------------------------------------------------------- */
mw.util.addCSS(
	[	// Inline links
		'.xfdc-status { font-size:85%; margin-left:13px; font-weight:normal; }',
		'.xfdc-action { margin-left:13px; }',
		'.xfdc-action a { cursor:pointer; }',
		'.xfdc-qc-cancel { cursor: pointer; border: 1px solid #777; border-radius: 10px;'+
			'font-weight: bold; font-size: 90%; color: #777; padding: 0px; margin: 0px 1px; }',
			
		// Dialog
		'#xfdc-dialog { background-color:#f0f0f0; padding:0.5em 1em; font-size:110%; }',
		'.xfdc-dialog-option { white-space:nowrap; min-width:8em; margin-right:0.3em; '+
			'display:block; float:left; }',
		'.xfdc-dialog-bracketedOption { white-space:nowrap; margin-left:2em; }',
		'.xfdc-dialog-bracketedOption::before, .xfdc-bracketed::before { content: "(\\200A "; }',
		'.xfdc-dialog-bracketedOption::after,  .xfdc-bracketed::after  { content: "\\200A )"; }',
		'.xfdc-dialog-container { float:left; max-width:34%; margin-left:0.5em; }',
		'.xfdc-dialog-disabled { color:#777; }',
		'.xfdc-dialog-actionInfo { border:2px solid #fff; padding:0.2em; margin:0.1em; }',
		'.xfdc-dialog-actionInfo > strong { display:block; }',
		'.xfdc-dialog-actionInfo > span { margin-left:1.5em; }',
		'#closeXFD-interface-buttons > button { margin:1em; }',
		'#closeXFD-preview-output { background-color:#fff; border:1px solid #777; '+
			'margin-top: 0px; padding:0px 10px; }',
		'.closeXFD-errorNote { border:1px solid #ff0000 }',
		
		// Task notices
		'.xfdc-notices { width:80%; font-size:95%; padding-left:2.5em; }',
		'.xfdc-notices > p { margin:0; line-height:1.1em; }',
		'.xfdc-notice-error { color:#D00000; font-size:92% }',
		'.xfdc-notice-warning { color:#9900A2; font-size:92% }',
		'.xfdc-notice-error::before, .xfdc-notice-warning::before { content: " ["; }',
		'.xfdc-notice-error::after,  .xfdc-notice-warning::after  { content: "]"; }',
		'.xfdc-task-waiting { color:#595959; }',
		'.xfdc-task-started { color:#0000D0; }',
		'.xfdc-task-done { color:#006800; }',
		'.xfdc-task-skipped { color:#697000; }',
		'.xfdc-task-aborted { color:#C00049; }',
		'.xfdc-task-failed { color:#D00000; }',
		
		// Show/hide box
		'#XFDcloser-showhide { bottom:0; display:block; position:fixed; right:0; z-index:100; '+
			'padding:5px; box-shadow:0 2px 4px rgba(0,0,0,0.5); background-color:#FEF9E6; '+
			'border:1px solid #aaa; border-radius:5px; font-size:85% }'
	]
	.join('\n')
);

/* --------- Additional configuration ----------------------------------------------------------- */
// Set description for namespace 0
config.mw.wgFormattedNamespaces['0'] = 'article';
// Remove empty string from start of wgMonthNames array
config.mw.wgMonthNames = config.mw.wgMonthNames.slice(1);
// Set aliases
config.mw.namespaces = config.mw.wgFormattedNamespaces;
config.mw.monthNames = config.mw.wgMonthNames;
// Set sysop status
if ( -1 !== $.inArray('sysop', config.mw.wgUserGroups) ) {
	config.user = {
		'isSysop': true,
		'sig': '~~~~'
	};
} else {
	config.user = {
		'isSysop': false,
		'sig': '<small>[[Wikipedia:NACD|(non-admin closure)]]</small> ~~~~'
	};
}
// Start time, for detecting edit conflicts
config.startTime = new Date();
// Variables for tracking across multiple discussions
config.track = {
	// Track Afd logpage edits using deferred objects, to know when it is safe to read the wikitext
	'afdLogEdit': [$.Deferred().resolve()],
	// Track how many closes/relists have been started and completed
	'started':  0,
	'finished': 0,
	'discussions': []
};

//Warn if unloading before closes/relists are completed
$(window).on('beforeunload', function(e) {
	if ( config.track.started > config.track.finished ) {
		e.returnValue = '';
		return '';		
	}
});

var makeErrorMsg = function(code, jqxhr) {
	var details = '';
	if ( code === 'http' && jqxhr.textStatus === 'error' ) {
		details = 'HTTP error ' + jqxhr.xhr.status;
	} else if ( code === 'http' ) {
		details = 'HTTP error: ' + jqxhr.textStatus;
	} else if ( code === 'ok-but-empty' ) {
		details = 'Error: Got an empty response from the server';
	} else {
		details = 'API error: ' + code;
	}
	return details;
};

/* ========== XfdVenue class ====================================================================
   Each instance represents an XfD venue, with properties/function specific to that venue
   ---------------------------------------------------------------------------------------------- */
// Constructor
var XfdVenue = function(type, settings) {
	this.type = type;
	for ( var key in settings ) {
		this[key] = settings[key];
	}
};
// ---------- XfdVenue prototype  --------------------------------------------------------------- */
XfdVenue.prototype.hasNomTemplate = function(wikitext) {
	var pattern = new RegExp(this.regex.nomTemplate);
	return pattern.test(wikitext);
};
XfdVenue.prototype.removeNomTemplate = function(wikitext) {
	var pattern = new RegExp(this.regex.nomTemplate);
	return wikitext.replace(pattern, '');
};
// ---------- Venue-specific classes  ----------------------------------------------------------- */
// MFD
var MfdVenue = function() {
	XfdVenue.call(this,	'mfd', {
		path:		 'Wikipedia:Miscellany for deletion',
		subpagePath: 'Wikipedia:Miscellany for deletion/',
		ns_number:	 null,
		html: {
			head:			'h4',
			list:			'dl',
			listitem:		'dd'
		},
		wikitext: {
			closeTop:		"{{subst:Mfd top|'''__RESULT__'''}}__TO_TARGET____RATIONALE__ __SIG__",
			closeBottom:	"{{subst:Mfd bottom}}",
			oldXfd:			"{{Old MfD |date=__DATE__ |result='''__RESULT__''' |page=__SUBPAGE__}}"+
				"\n",
			mergeFrom:		"{{mfd-mergefrom|__NOMINATED__|__DEBATE__|__DATE__}}\n",
			mergeTo:		"{{mfd-mergeto|__TARGET__|__DEBATE__|__DATE__|__TARGETTALK__}}\n",
			alreadyClosed:	"{{#ifeq:{{FULLPAGENAME}}|Wikipedia:Miscellany for deletion|"+
				"{{collapse bottom}}|}}"
		},
		regex: {
			nomTemplate:	/(?:<noinclude>\s*)?(?:{{mfd[^}}]*}}|<span id="mfd".*?<\/span>&nbsp;\[\[Category:Miscellaneous pages for deletion\|?.*\]\]\s*)(?:\s*<\/noinclude>)?/gi
		}
	});
};
MfdVenue.prototype = Object.create(XfdVenue.prototype);

// CFD
var CfdVenue = function() {
	XfdVenue.call(this,	'cfd', {
		path:		 'Wikipedia:Categories for discussion/Log/',
		ns_number:	 [14],
		html: {
			head:			'h4',
			list:			'ul',
			listitem:		'li',
			nthSpan:		'2'
		},
		wikitext: {
			closeTop:		"{{subst:cfd top}} '''__RESULT__'''__TO_TARGET____RATIONALE__ __SIG__",
			closeBottom:	"{{subst:cfd bottom}}",
			oldXfd:			"{{Old CfD |__SECTION__ |date=__DATE__ |action=__ACTION__ "+
				"|result=__RESULT__}}\n",
			alreadyClosed:	"<!-- Template:Cfd top -->"		
		}
	});
};
CfdVenue.prototype = Object.create(XfdVenue.prototype);

// FFD
var FfdVenue = function() {
	XfdVenue.call(this,	'ffd', {
		path:		 'Wikipedia:Files for discussion/',
		ns_number:	 [6],
		ns_unlink:   ['0', '10', '100', '118'], // main, Template, Portal, Draft
		html: {
			head:			'h4',
			list:			'dl',
			listitem:		'dd',
			nthSpan:		'1'
		},
		wikitext: {
			closeTop:		"{{subst:ffd top|'''__RESULT__'''}}__TO_TARGET____RATIONALE__ __SIG__",
			closeBottom:	"{{subst:ffd bottom}}",
			oldXfd:			"{{oldffdfull |date=__DATE__ |result='''__RESULT__''' "+
				"|page=__SECTION__}}\n",
			pagelinks:		"{{subst:ffd2|__PAGE__|multi=yes}}\n",
			relistReplace:	"{{ffd|log=__TODAY__",
			alreadyClosed:	"<!--Template:Ffd top-->"		
		},
		regex: {
			nomTemplate:	/{{ffd[^}}]*}}/gi,
			relistPattern:	/{{\s*ffd\s*\|\s*log\s*=\s*[^|}}]*/gi
		}
	});
};
FfdVenue.prototype = Object.create(XfdVenue.prototype);

// TFD
var TfdVenue = function() {
	XfdVenue.call(this,	'tfd', {
		path:		 'Wikipedia:Templates for discussion/Log/',
		subpagePath: 'Wikipedia:Templates for discussion/',
		ns_number:	 [10, 828],
		html: {
			head:			'h4',
			list:			'ul',
			listitem:		'li',
			nthSpan:		'1'
		},
		wikitext: {
			closeTop:		"{{subst:Tfd top|'''__RESULT__'''}}__TO_TARGET____RATIONALE__ __SIG__",
			closeBottom:	"{{subst:Tfd bottom}}",
			oldXfd:			"{{oldtfdfull|date= __DATE__ |result=__RESULT__ |disc=__SECTION__}}\n",
			pagelinks:		"* {{tfd links|__PAGE__}}\n",
			relistReplace:	"Wikipedia:Templates for discussion/Log/__TODAY__#",
			alreadyClosed:	"<!-- Tfd top -->"
		},
		regex: {
			nomTemplate:	/(<noinclude>[\n\s]*)?{{(?:Template for discussion|Tfm)\/dated[^{}]*(?:{{[^}}]*}}[^}}]*)*?}}([\n\s]*<\/noinclude>)?(\n)?/gi,
			relistPattern:	/Wikipedia:Templates(_|\s){1}for(_|\s){1}discussion\/Log\/\d{4}(_|\s){1}\w*(_|\s){1}\d{1,2}#(?=[^}]*}{2})/gi
		},
		holdingCellSectionNumber: {
			"review":			2,
			"convert":			11,
			"substitute":		12,
			"orphan":			13,
			"ready":			14,	// (ready for deletion)
			"merge-arts":		4,
			"merge-geopolgov":	5,	// (geography, politics and governance)
			"merge-religion":	6,
			"merge-sports":		7,
			"merge-trasnport":	8,
			"merge-other":		9,
			"merge-meta":		10
		}
	});
};
TfdVenue.prototype = Object.create(XfdVenue.prototype);
TfdVenue.prototype.removeNomTemplate = function(wikitext) {
	var pattern = new RegExp(config.xfd.regex.nomTemplate);
	var matches = pattern.exec(wikitext);
	var logical_xor = function(first, second) {
		return (first ? true : false) !== (second ? true : false);
	};
	var unbalancedNoincludeTags = logical_xor(matches[1], matches[2]); 
	var replacement = ( unbalancedNoincludeTags ) ? "$1$2" : "";					
	return wikitext.replace(pattern, replacement);
};

// RFD
var RfdVenue = function() {
	XfdVenue.call(this,	'rfd', {
		type:		 'rfd',
		path:		 'Wikipedia:Redirects for discussion/Log/',
		ns_number:	 null,
		html: {
			head:			'h4',
			list:			'ul',
			listitem:		'li'
		},
		wikitext: {
			closeTop:		"{{subst:Rfd top|'''__RESULT__'''}}__TO_TARGET____RATIONALE__ __SIG__",
			closeBottom:	"{{subst:Rfd bottom}}",
			oldXfd:			"{{Old RfD |date={{subst:date|__FIRSTDATE__}} |result='''__RESULT__'''"+
				" |page=__DATE__#__SECTION__}}\n",
			alreadyClosed:	"<!-- Template:Rfd top-->"		
		},
		regex: {
			nomTemplate:		/(^{{.*#invoke:RfD(?:.|\n)*?-->\|content=\n?|\n?<!-- Don't add anything after this line.*? -->\n}}|\[\[Category:Temporary maintenance holdings\]\]\n?)/g,
			fullNomTemplate:	/(^{{.*#invoke:RfD(?:.|\n)*?<!-- Don't add anything after this line.*? -->\n}}|\[\[Category:Temporary maintenance holdings\]\]\n?)/g
		},
	});
};
RfdVenue.prototype = Object.create(XfdVenue.prototype);

// AFD
var AfdVenue = function(transcludedOnly) {
	XfdVenue.call(this,	'afd', {
		type:		 'afd',
		path:		 'Wikipedia:Articles for deletion/Log/',
		subpagePath: 'Wikipedia:Articles for deletion/',
		ns_number:	 [0], // main
		ns_logpages: 4, // Wikipedia
		ns_unlink:   ['0', '10', '100', '118'], // main, Template, Portal, Draft
		html: {
			head:			'h3',
			list:			'dl',
			listitem:		'dd',
			nthSpan:		'2'
		},
		wikitext: {
			closeTop:		"{{subst:Afd top|'''__RESULT__'''}}__TO_TARGET____RATIONALE__ __SIG__",
			closeBottom:	"{{subst:Afd bottom}}",
			mergeFrom:		"{{Afd-merge from|__NOMINATED__|__DEBATE__|__DATE__}}\n",
			mergeTo:		"{{Afd-merge to|__TARGET__|__DEBATE__|__DATE__}}\n",
			alreadyClosed:	"<!--Template:Afd bottom-->"		
		},
		regex: {
			nomTemplate:	/(?:{{[Aa]rticle for deletion\/dated|<!-- Please do not remove or change this AfD message)(?:.|\n)*?}}(?:(?:.|\n)+this\ point -->)?\s*/g
		},
		transcludedOnly:	transcludedOnly
	});
};
AfdVenue.prototype = Object.create(XfdVenue.prototype);

// Create xfd venue object
var isAfd = /(Articles_for_deletion|User:Cyberbot_I|Wikipedia:WikiProject_Deletion_sorting)/.test(config.mw.wgPageName);
var afdTranscludedOnly = /(User:Cyberbot_I|Wikipedia:WikiProject_Deletion_sorting)/.test(config.mw.wgPageName);

if ( config.mw.wgPageName.includes('Wikipedia:Miscellany_for_deletion') ) {
	config.xfd = new MfdVenue();
} else if ( config.mw.wgPageName.includes('Categories_for_discussion/') ) {
	config.xfd = new CfdVenue();
} else if ( config.mw.wgPageName.includes('Files_for_discussion') ) {
	config.xfd = new FfdVenue();
} else if ( config.mw.wgPageName.includes('Templates_for_discussion') ) {
	config.xfd = new TfdVenue();
} else if ( config.mw.wgPageName.includes('Redirects_for_discussion') ) {
	config.xfd = new RfdVenue();
} else if ( isAfd ) {
	config.xfd = new AfdVenue(afdTranscludedOnly);
} else {
	return;
}

/* ========== API =============================================================================== */
var API = new mw.Api( {
    ajax: {
        headers: { 
			'Api-User-Agent': 'XFDcloser/' + config.script.version + 
				' ( https://en.wikipedia.org/wiki/WP:XFDC )'
		}
    }
} );

/* ========== Page class ========================================================================
   An extended/modified version of the mw.Title class. Each instance
   represents a page nominated in an XfD discussion.                          
   ---------------------------------------------------------------------------------------------- */
// Constructor
var Page = function(title) {
	try {
		mw.Title.call(this, decodeURIComponent(title));
	} catch(e) {
		throw new Error('Unable to parse title "'+title+'"'); 
	}
	this.exists = null;
	this.talk = null;
	this.talkExists = null;
};
//Constructor with a null return instead of an exception for invalid titles.
Page.newFromText = function(t) {
	if ( mw.Title.newFromText(t) ) {
		return new Page(t);
	} else {
		return null;
	}
};

// ---------- Page prototype  ------------------------------------------------------------------- */
// Inherited from mw.Title
Page.prototype = Object.create(mw.Title.prototype);
Page.prototype.constructor = Page;
// Set aliases
Page.prototype.getTitle = Page.prototype.getPrefixedText;
// Additional functions
Page.prototype.getTalk = function() {
	if ( this.talk === null ) {
		// talk page not yet set, so set it now
		if ( this.getNamespaceId()%2 === 1 ) {
			// Page is itself a talk page - set to empty string
			this.talk = '';
		} else {
			this.talk = mw.Title.newFromText(
				this.getMain(),
				this.getNamespaceId()+1
			).getPrefixedText();
		}
	}
	return this.talk;
};
Page.prototype.hasCorrectNamespace = function() {
	if (
		config.xfd.ns_number === null ||
		config.xfd.ns_number.indexOf(this.getNamespaceId()) !== -1
	) {
		return true;
	} else {
		return false;
	}
};


/* ========== Discusssion class =================================================================
   Each instance represents an XfD discussion.                          
   ---------------------------------------------------------------------------------------------- */
// Constructor
var Discussion = function(uniqueID, nomPage, sectionHeader, sectionNumber, pageObjects, firstDate) {
	this.id = uniqueID;
	this.nomPage = nomPage;
	this.sectionHeader = sectionHeader;
	this.sectionNumber = sectionNumber;
	this.pages = pageObjects; // or false if in in 'basic' mode
	this.nomDate = null;
	this.firstDate = firstDate;
	this.deferred = {};
	//this.notices = $('<div>')
	//.attr({'id':uniqueID+'-notices', 'class':'xfdc-notices'})
	//.after($('#'+this.id).closest(config.xfd.html.head));
};
// Construct from headline span element
Discussion.newFromHeadlineSpan = function (headingIndex, context) {
	var $headlineSpan = $(context);
	var $heading = $headlineSpan.parent();
	
	// Fix for "Auto-number headings" preference
	$('.mw-headline-number', context).prependTo($heading);

	// Get section header
	var sectionHeader = $headlineSpan.text().trim();

	// Check if already closed. Closed AfDs and MfDs have the box above the heading.
	if ( /(afd|mfd)/.test(config.xfd.type) && $heading.parent().attr('class') && $heading.parent().attr('class').includes('xfd-closed') ) {
		// Skip 
		return;
	} else if ( !/(afd|mfd)/.test(config.xfd.type) && $heading.next().attr('class') ) {
		// Only for closed discussion will the next element after the heading have any class set
		// Skip, add class to enable hiding of closed discussions
		$heading.addClass('xfd-closed');
		return;
	}

	var sectionlink = $heading.find('.mw-editsection a')
		.not('.mw-editsection-visualeditor, .autoCloserButton').attr('href');
	var editsection = sectionlink.split('section=')[1].split('&')[0];
	var nompage = '';
	if ( /T/.test(editsection) ) {
		// Section is transcluded from another page
		nompage = Page.newFromText(
			decodeURIComponent(sectionlink.split("title=")[1].split("&")[0])
		).getTitle();
		if ( -1 !== $.inArray(nompage, [
				'Wikipedia:Redirects for discussion/Header',
				'Wikipedia:Redirect/Deletion reasons',
				'Wikipedia:Templates for discussion/Holding cell'
			])
		) {
			// ignore headings transcuded from these pages
			return;
		}
		// remove "T-" from section number
		editsection = editsection.substr(2);
	} else {
		// Section is on current page, not transcluded
		if ( config.xfd.transcludedOnly ) {
			return;
		}
		nompage = Page.newFromText( config.mw.wgPageName ).getTitle();		
	}

	var pages=[];
	var firstdate = ( config.xfd.type === 'rfd' ) ? '' : null;

	if ( config.xfd.type === 'cfd' ) {
		//CFDs have basic closure only
		pages = false;
	} else if ( config.xfd.type === 'rfd' || config.xfd.type === 'mfd' ) {
		// For MFD, closed discussion are within a collapsed table
		$('table.collapsible').has('div.xfd-closed').addClass('xfd-closed');
		// MFD & RFD have nominated page links prior to span with classes plainlinks, lx
		pages = $heading
		.nextUntil(config.xfd.html.head + ', div.xfd-closed, table.xfd-closed')
		.find(config.xfd.html.listitem)
		.has('span.plainlinks.lx')
		.children('span')
		.filter(':first-child')
		.children('a, span.plainlinks:not(.lx)')
		.filter(':first-child')
		.map(function() { return Page.newFromText($(this).text()); })
		.get();
		if ( config.xfd.type === 'rfd' ) {
			var discussionNodes = $heading
			.nextUntil(config.xfd.html.head + ', div.xfd-closed, table.xfd-closed')
			.clone();
			
			// Fix for "Comments in Local Time" gadget
			discussionNodes.find('span.localcomments').each(function(){
				var utcTime = $(this).attr('title');
				$(this).text(utcTime);
			});
			
			var discussionText = discussionNodes.text();
			// Ignore relisted discussions, and non-boxed closed discussions
			if (
				discussionText.includes('Relisted, see Wikipedia:Redirects for discussion') ||
				discussionText.includes('Closed discussion, see full discussion')
			) {
				return;
			}
			// Find first timestamp date
			var firstdatePatt = /(?:\d\d:\d\d, )(\d{1,2} \w+ \d{4})(?: \(UTC\))/;
			var firstdateMatch = firstdatePatt.exec(discussionText);
			firstdate = firstdateMatch && firstdateMatch[1];
		}
	} else {
		// AFD, FFD, TFD: nominated page links inside span with classes plainlinks, nourlexpansion
		pages = $heading
		.nextUntil(config.xfd.html.head + ', div.xfd-closed')
		.find(config.xfd.html.listitem + ' > span.plainlinks.nourlexpansion')
		.filter(':nth-of-type(' + config.xfd.html.nthSpan + ')')
		.children('a')
		.filter(':first-child')
		.map(function() { return Page.newFromText($(this).text()); })
		.get();
	}
	
	// Sanity check - check if any pages are null
	if ( !pages || pages.length === 0 || pages.some(function(p) { return !p; }) ) {
		//Still offer a "basic" close using the section header
		pages = false;
	}
	
	// Create status span and notices div with unique id based on headingIndex
	var uniqueID = 'XFDC' + headingIndex;
	$headlineSpan.after(
		$('<span>')
		.attr({'id':uniqueID, 'class':'xfdc-status'})
		.text('[XFDcloser loading...]')
	);
	$heading.after(
		$('<div>')
		.attr({'id':uniqueID+'-notices', 'class':'xfdc-notices'})
	);
	
	// Create discussion object
	return new Discussion(uniqueID, nompage, sectionHeader, editsection, pages, firstdate);
};
// ---------- Discusssion prototype ------------------------------------------------------------- */
// Get status element (jQuery object)
Discussion.prototype.get$status = function() {
	return $('#'+this.id);
};
// Set status
Discussion.prototype.setStatus = function($status) {
	this.get$status().empty().append($status);
};
// Open dialog
Discussion.prototype.openDialog = function(relisting) {
	this.dialog = new Dialog(this, !!relisting);
	this.dialog.setup();
};
// Mark as finished
Discussion.prototype.setFinished = function(aborted) {
	var self = this;
	var msg;
	
	if ( aborted != null ) {
		msg = [
			$('<strong>').text( ( self.dialog && self.dialog.relisting ) ? 'Aborted relist' : 'Aborted close' ),
			( aborted === '' ) ? '' : ': ' + aborted
		];		
	} else if ( self.dialog && self.dialog.relisting ) {
		msg = [
			'Discussion ',
			$('<strong>').text('relisted'),
			' (reload page to see the actual relist)'
		];
	} else {
		msg = [
			'Closed as ',
			$('<strong>').text(self.taskManager.inputData.getResult()),
			' (reload page to see the actual close)'
		];
	}
	self.setStatus(msg);
	self.get$status().prev().css('text-decoration', 'line-through');
};
// Get notices element (jQuery object)
Discussion.prototype.get$notices = function() {
	return $('#'+this.id+'-notices');
};
// Set notices element
Discussion.prototype.setNotices = function($content) {
	this.get$notices().empty().append($content);
};
// Get an array of page titles
Discussion.prototype.getPageTitles = function(pagearray, options) {
	var titles = (pagearray || this.pages).map(function(p) { 
		return p.getTitle();
	});
	if ( options && options.moduledocs ) {
		return titles.map(function(t) {
			var isModule = ( t.indexOf('Module:') === 0 );
			return ( isModule ) ? t + '/doc' : t;
		});
	}
	return titles;
};
// Get an array of page' talkpage titles (excluding pages which are themselves talkpages)
Discussion.prototype.getTalkTitles = function(pagearray) {
	return (pagearray || this.pages).map(function(p) { 
		return p.getTalk();
	}).filter(function(t) { return t !== ''; });
};
// Get link text for a wikiink to the discussion - including anchor, except for AfDs 
Discussion.prototype.getNomPageLink = function() {
	if (config.xfd.type === 'afd') {
		return this.nomPage;
	} else {
		return this.nomPage + '#' + mw.util.wikiUrlencode(this.sectionHeader);
	}
};
// Get nomination subpage
Discussion.prototype.getNomSubpage = function() {
	return this.nomPage.replace(config.xfd.subpagePath, '');
};
// Get page object by matching the title
Discussion.prototype.getPageByTitle = function(title, options) {
	var convertModuleDoc = ( options && options.moduledocs && title.indexOf('Module:') === 0 );
	var titleToCheck = ( convertModuleDoc ) ? title.replace(/\/doc$/,'') : title;

	var search = mw.Title.newFromText(decodeURIComponent(titleToCheck)).getPrefixedText();
	for ( var i=0; i<this.pages.length; i++ ) {
		if ( search === this.pages[i].getTitle() ) {
			return this.pages[i];
		}
	}
	return false;
};
// Get page object by matching the talkpage's title
Discussion.prototype.getPageByTalkTitle = function(t) {
	var search = mw.Title.newFromText(decodeURIComponent(t)).getPrefixedText();
	for ( var i=0; i<this.pages.length; i++ ) {
		if ( search === this.pages[i].getTalk() ) {
			return this.pages[i];
		}
	}
	return false;
};

// Show links for closing/relisting
Discussion.prototype.showLinks = function(additonal) {
	// Preserve reference to self object
	var self = this;
	
	// Close link
	var $close = $('<span>')
	.addClass('xfdc-action')
	.append(
		'[',
		$('<a>')
		.attr('title', 'Close discussion...')
		.text('Close'),
		']'
	)
	.click(function() {
		self.setStatus('Closing...');
		self.openDialog();
	});
	
	// Relist link
	var $relist = $('<span>')
	.addClass('xfdc-action')
	.append(
		'[',
		$('<a>')
		.attr({title:'Relist discussion...', class:'XFDcloser-link-relist'})
		.text('Relist'),
		']'
	)
	.click(function() {
		self.setStatus('Relisting...');
		self.openDialog(true);
	});
	
	// quickKeep
	var $qk = $('<a>')
	.attr('title', 'quickKeep: close as "keep", remove nomination templates, '+
		'add old xfd templates to talk pages')
	.text('qK')
	.click(function(){
		var inputData = new InputData(self);
		inputData.result = 'keep';
		inputData.after = 'doKeepActions';
		
		self.setStatus('Closing...');
		self.taskManager = new TaskManager(self, inputData);
		self.taskManager.start();
	});

	// quickDelete
	var $qd = ( !config.user.isSysop && config.xfd.type !== 'tfd' ) ? '' : $('<a>')
	.attr({
		'title': 'quickDelete: close as "delete", delete nominated pages & their talk pages'+
			(( config.xfd.type === 'rfd' ) ? '' :' & redirects')+
			(( config.xfd.type === 'afd' || config.xfd.type === 'ffd' ) ? ', optionally '+
				'unlink backlinks' : ''),
		'class': 'xfdc-qd'
		})
	.text('qD');
	if ( !config.user.isSysop && config.xfd.type == 'tfd' ) {
		$qd.attr('title', 'quickDelete: close as "delete", tag nominated templates with '+
			'{{being deleted}}, add nominated templates to the holding cell as "orphan"')
		.click(function(){
			var inputData = new InputData(self);
			inputData.result = 'delete';
			inputData.after = 'holdingCell';
			inputData.holdcell = 'orphan';
			
			self.setStatus('Closing...');
			self.taskManager = new TaskManager(self, inputData);
			self.taskManager.start();
		});
	} else if ( config.user.isSysop ) {
		$qd.click(function(){
			var inputData = new InputData(self);
			inputData.result = 'delete';
			inputData.after = 'doDeleteActions';
			inputData.deleteredir = ( config.xfd.type === 'rfd' ) ? null : true;
			inputData.unlinkbackl = ( config.xfd.type === 'afd' ||
				config.xfd.type === 'ffd' ) ? true : null;

			self.setStatus('Closing...');
			self.taskManager = new TaskManager(self, inputData);
			self.taskManager.start();
		});
	}
	
	// quickClose links
	var $quick = $('<span>')
	.addClass('xfdc-action')
	.css('font-size', '92%')
	.append(
		'[',
		$('<a>')
			.attr('title', 'quickClose discussion...')
			.text('quickClose')
			.click(function(){
				$(this).hide().next().show();
			}),
		$('<span>')
			.hide()
			.append(
				'&nbsp;',
				$qk,
				" ",
				$('<strong>').html('&middot;'),
				" ",
				$qd,
				'&nbsp;',
				$('<span>')
					.attr({title: 'Cancel', class: 'xfdc-qc-cancel'})
					.html("&nbsp;x&nbsp;")
					.click(function(){
						$(this).parent().hide().prev().show();
					})
			),
		']'
	);


	//Add links in place of status
	self.setStatus([
		$close,
		( self.pages === false ) ? '' : $quick,
		( config.xfd.type==='cfd') ? '' : $relist,
		additonal || ''
	]);
};
	
// Retrieve extra information - pages' existance, nomination date(s)
Discussion.prototype.retrieveExtraInfo = function() {
	// Preserve reference to discussion object
	var self = this;

	// If already in basic mode, just show links
	if ( self.isBasicMode() ) {
		self.showLinks();
		return;
	}
	
	// Use Deferred objects to track when both queries are done, or either has failed
	self.deferred.extraInfoExists = $.Deferred();
	self.deferred.extraInfoDates = $.Deferred();
	$.when(self.deferred.extraInfoExists, self.deferred.extraInfoDates)
	.done( function() {
		//Show links
		self.showLinks();
	} )
	.fail( function(code, jqxhr) {
		//Set basic mode
		self.pages = false;
		//Show basic-mode links
		failDetails = $('<span>').addClass('xfdc-notice-error').append(
			'Error retrieving page information (reload the page to try again) ',
			$('<span>').addClass('xfdc-notice-error').append(
				makeErrorMsg(code, jqxhr)
			)
		);
		self.showLinks(failDetails);
	} );
	
	// Check if pages and talk pages exist
	apiCallback_getPagesInfo = function(result) {
		if ( !result.query || !result.query.pageids ) {
			self.deferred.extraInfoExists.reject();
			return;
		}
		var page_ids = result.query.pageids;
		for (var i=0; i<page_ids.length; i++) {
			title = result.query.pages[ page_ids[i] ].title;
			var pageObject = self.getPageByTitle(title);
			if ( !pageObject ) {
				self.deferred.extraInfoExists.reject();
				return;
			}
			pageObject.exists = page_ids[i] > 0;
			pageObject.talkExists = result.query.pages[ page_ids[i] ].talkid > 0;
		}
		self.deferred.extraInfoExists.resolve();
	};
	// get page info from api
	if ( self.getPageTitles().length > 50 ) {
		self.deferred.extraInfoExists.reject('Too many pages');
	} else {
		API.get( {
			action: 'query',
			titles: self.getPageTitles().join('|'),
			prop: 'info',
			inprop: 'talkid',
			indexpageids: 1,
			rawcontinue: ''
		} )
		.done( apiCallback_getPagesInfo )
		.fail( function(code, jqxhr) {
			self.deferred.extraInfoExists.reject(code, jqxhr);
		} );
	}
	// Get nomination date:
	if ( config.xfd.type !== "afd" && config.xfd.type !== "mfd" ) {
		self.nomDate = self.nomPage.split(config.xfd.path)[1];
		if ( config.xfd.type === "rfd" && !self.firstDate ) {
			// For an RfD with no first comment date detected, use the nom page date in dmy format
			self.firstDate = self.nomDate.replace(/(\d+) (\w*) (\d+)/g, "$3 $2 $1");
		}
		self.deferred.extraInfoDates.resolve();			
	} else {
		//Api query to get nomination page content
		apiCallback_getNomInfo = function(result) {
			//get nomination date from when nom page was created
			var p_id = result.query.pageids[0];
			var timestamp = result.query.pages[p_id].revisions[0].timestamp;
			var d = new Date(timestamp);
			self.nomDate = d.getUTCDate().toString() + ' ' + config.mw.monthNames[d.getUTCMonth()] + 
			' ' + d.getUTCFullYear().toString();
			self.deferred.extraInfoDates.resolve();
		};
		API.get( {
			action: 'query',
			titles: self.nomPage,
			prop: 'revisions',
			rvprop: 'timestamp',
			rvdir: 'newer',
			rvlimit: '1',
			indexpageids: 1,
			rawcontinue: ''
		} )
		.done( apiCallback_getNomInfo )
		.fail( function(code, jqxhr) {
			self.deferred.extraInfoDates.reject(code, jqxhr);
		} ); 
	}
};

// Check if discussion is in 'basic' mode - i.e. no pages
Discussion.prototype.isBasicMode = function() {
	return !this.pages;
};


/* ========== Dialog class ======================================================================
   The user interface for closing/relisting XfD discussions 
   ---------------------------------------------------------------------------------------------- */
// Constructor
var Dialog = function(discussion, relist) {
	this.discussion = discussion;
	this.relisting = !!relist;
	
	// Make an new, empty, not-displayed dialog/interface window	
	this.interfaceWindow = new Morebits.simpleWindow(
		Math.min(900, Math.floor(window.innerWidth*0.8)),
		Math.floor(window.innerHeight*0.9)
	);
	this.interfaceWindow.setTitle( 'XFDcloser' );
	this.interfaceWindow.setScriptName('(v' + config.script.version + ')');
	this.interfaceWindow.addFooterLink('script documentation', 'WP:XFDC');
	this.interfaceWindow.addFooterLink('feedback', 'WT:XFDC');
	this.interfaceWindow.setContent(
		$('<div>')
		.attr('id', 'xfdc-dialog')
		.append(
			$('<div>').attr('id', 'xfdc-dialog-header'),
			$('<div>').attr('id', 'xfdc-dialog-body'),
			$('<div>').attr('id', 'xfdc-dialog-footer')
		)
		.get(0)
	);
	$('a.ui-dialog-titlebar-close.ui-corner-all').remove();
	$('#xfdc-dialog').parent().css('background-color', '#f0f0f0');
};

// ---------- Dialog prototype ------------------------------------------------------------------ */

// --- Basic manipulation: ---
// Append content to header
Dialog.prototype.addToHeader = function($content) {
	$('#xfdc-dialog-header').append($content);
};
// Append content to body
Dialog.prototype.addToBody = function($content) {
	$('#xfdc-dialog-body').append($content);
};
// Append content to footer
Dialog.prototype.addToFooter = function($content) {
	$('#xfdc-dialog-footer').append($content);
};
// Clear dialog
Dialog.prototype.emptyContent = function() {
	$('#xfdc-dialog-header, #xfdc-dialog-body, #xfdc-dialog-footer').empty();
};
// Display dialog
Dialog.prototype.display = function() {
	this.interfaceWindow.display();
};
// Reset height
Dialog.prototype.resetHeight = function() {
	this.interfaceWindow.setHeight(Math.floor(window.innerHeight*0.9));
};
// Close dialog
Dialog.prototype.close = function() {
	this.interfaceWindow.close();
	$('#ejs-rcats-overlay').remove();
};

// --- Make interface elements: ---
Dialog.prototype.makeHeader = function(multimode) {
	var self = this;
	
	// Title, pagecount
	var $header = $('<h4>')
	.attr('id', 'closeXFD-interface-header')
	.append(
		$('<span>')
		.attr('id', 'closeXFD-interface-header-action')
		.append(
			( self.relisting ) ? 'Relisting discussion ' : 'Closing discussion ',
			$('<em>').text(self.discussion.sectionHeader),
			' ',
			$('<span>')
			.attr({'id':'closeXFD-pagecount', 'class':'xfdc-bracketed'})
			.css('font-size', '80%')
			.append(
				$('<span>')
					.attr('id', 'closeXFD-pagecount-pages')
					.css({'background':'#ff8', 'padding':'1px'})
					.text(
						( self.discussion.isBasicMode() ) ? 'basic mode '+
							'only' : self.discussion.pages.length +
						( ( self.discussion.pages.length === 1 ) ? ' page' : ' pages' )
					),
				( multimode ) ? '' : ' ',
				( multimode ) ? '' : $('<a>').attr('id', 'closeXFD-pagecount-show').text('[show]'),
				( multimode ) ? '' : $('<a>').attr('id', 'closeXFD-pagecount-hide').text('[hide]').hide()
			)
		)
	);
	$header.find('#closeXFD-pagecount-show, #closeXFD-pagecount-hide').click(function() {
		$('#closeXFD-nominatedpages, #closeXFD-pagecount-show, #closeXFD-pagecount-hide').toggle();
	});	
	
	// Multi/single-mode button
	if ( !self.relisting && self.discussion.pages && self.discussion.pages.length > 1 ) {
		$header.prepend(
			$('<button>')
			.css('float', 'right')
			.text(( multimode ) ? 'Single result...' : 'Multiple results...')
			.click(function() {
				if ( multimode ) {
					self.setup();
				} else {
					self.setupForMultiClose();
				}
			})
		);
	}
	
	return $header;
};

Dialog.prototype.makePagesList = function(multimode) {
	var self = this;

	// Template for per-page actions for multimode
	var $action_dropdown = $('<select>')
	.attr('class', 'closeXFD-pagelist-actionDropdown')
	.css('margin-right', '0.5em')
	.append(
		$('<option>').attr('value', 'default').text(''),
		$('<option>').attr('value', 'keep').text('Keep'),
		( config.xfd.type === 'ffd' ) ? '' : $('<option>')
			.attr('value', 'redirect').text('Redirect'),
		( config.xfd.type === 'ffd' || config.xfd.type === 'rfd' ) ? '' : $('<option>')
			.attr('value', 'merge').text('Merge'),
		( config.xfd.type !== 'rfd' ) ? '' : $('<option>')
			.attr('value', 'disambig').text('Disambiguate'),
		$('<option>').attr('value', 'del').text('Delete'),
		$('<option>').attr('value', 'na').text('(no action)')
	);
	var $target = $('<span>')
	.addClass('closeXFD-pagelist-target')
	.append(
		' to:',
		$('<input>').attr('type', 'text')
	)
	.css('display', 'none');

	
	// List of pages (or info on basic mode)
	var $pagesList = $('<ul>')
	.attr('id', 'closeXFD-nominatedpages')
	.css('font-size', '95%');
	if ( self.discussion.pages ) {
		for ( var i=0; i<self.discussion.pages.length; i++ ) {
			var pageTitle = self.discussion.pages[i].getTitle();
			$('<li>')
			.append(
				( !multimode ) ? '' : $action_dropdown
					.clone()
					.attr('id', 'closeXFD-pagelist-action-'+i)
					.data('pageTitle', pageTitle),
				$('<span>').addClass('closeXFD-pagelist-title').text(pageTitle),
				' ',
				( !multimode ) ? '' : $target
					.clone()
					.attr('id', 'closeXFD-pagelist-target-'+i)
					.data('pageTitle', pageTitle)
			)
			.appendTo($pagesList);
			if ( multimode ) {
				self.inputData.pageActions[pageTitle] = { 'id':'closeXFD-pagelist-action-'+i };
			}
		}
	} else {
		$('<li>')
		.append(
			'Nominated pages were not detected. You can still ',
			( self.relisting ) ? 'relist' : 'close',
			' this ',
			config.xfd.type.toUpperCase(),
			' discussion, but updating the nomination templates will need to be done manually '+
			'&ndash; see ',
			extraJs.makeLink('WP:'+config.xfd.type.toUpperCase()+'AI'),
			' for instructions.'
		)
		.appendTo($pagesList);
	}
	
	// On changing a multimode action, show/hide corresponding options as appropriate
	$pagesList.find('select').change(function(){
		if ( $(this).children().first().val() === 'default' ) {
			$(this).children().first().remove();
		}
		var $li = $(this).parent();
		var v = $(this).val();
		var t = $li.find('.closeXFD-pagelist-title').text();
		self.inputData.pageActions[t].action = v;
		if ( v === 'redirect' || v === 'merge' ) {
			$li.find('.closeXFD-pagelist-target').show();
		} else {
			$li.find('.closeXFD-pagelist-target').val('').change().hide();
		}
		self.updateMultimodeOptions();
	});
	$pagesList.find('input').change(function(){
		var t = $(this).parent().parent().find('.closeXFD-pagelist-title').text();
		self.inputData.pageActions[t].target = Page.newFromText(
			$(this).val().trim()
		);
	});
	if ( !multimode ) {
		$pagesList.hide();
	}
	return $pagesList;
};

Dialog.prototype.makeNonAdminNote = function() {
	var $NACD_note = $('<div>')
	.css({'text-align':'center', 'clear':'both'})
	.append(
		$('<em>')
		.append(
			'See the ',
			extraJs.makeLink('WP:NACD'),
			' guideline for advice on appropriate and inappropriate closures'
		),
		$('<hr>')
	);
	return $NACD_note;
};

Dialog.prototype.makeCloseResult = function() {
	var self = this;
	
	$resultContainer = $('<div>')
	.attr('id', 'closeXFD-resultContainer')
	.css({'float':'left', 'width':'99%', 'padding-bottom':'1%'})
	.append(
		$('<div>').append(
			$('<strong>').text('Result'),
			// Speedy
			$('<span>').addClass('xfdc-dialog-bracketedOption').append(
				$('<input>')
				.attr({'type':'checkbox', 'name':'closeXFD-speedy', 'id':'closeXFD-speedy'})
				.change(function(){
					self.inputData.speedy = ( $(this).prop('checked') );
				}),
				$('<label>').attr('for', 'closeXFD-speedy').text('Speedy')
			)
		),
		$('<div>').attr('id', 'closeXFD-resultOptions').css('overflow','hidden').append(
			// Keep
			$('<span>').attr('class', 'xfdc-dialog-option').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
					'class':'closeXFD-keepResult', 'id':'closeXFD-result-keep', 'value':'keep'}),
				$('<label>').attr('for', 'closeXFD-result-keep').text('Keep')
			),
			// Redirect
			( config.xfd.type === 'ffd' ) ? '' : $('<span>')
			.attr('class', 'xfdc-dialog-option').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
					'class':'closeXFD-keepResult', 'id':'closeXFD-result-redir',
					'value':(( config.xfd.type === 'rfd' ) ? 'retarget' : 'redirect')}),
				$('<label>')
				.attr('for', 'closeXFD-result-redir')
				.text(( config.xfd.type === 'rfd' ) ? 'Retarget' : 'Redirect'),
				( !config.user.isSysop ) ? '' : $('<label>')
				.attr('for', 'closeXFD-result-redir')
				.text(( config.xfd.type === 'rfd' ) ? 'Delete and retarget' : 'Delete and redirect')
				.hide()
			),
			// Soft redirect
			( config.xfd.type !== 'rfd' ) ? '' : $('<span>')
			.attr('class', 'xfdc-dialog-option').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
					'class':'closeXFD-keepResult', 'id':'closeXFD-result-softredir',
					'value':'soft redirect'}),
				$('<label>').attr('for', 'closeXFD-result-softredir').text('Soft redirect'),
				( !config.user.isSysop ) ? '' : $('<label>')
				.attr('for', 'closeXFD-result-softredir').text('Delete and soft redirect').hide()
			),	
			// No consensus
			$('<span>').attr('class', 'xfdc-dialog-option').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
					'class':'closeXFD-keepResult', 'id':'closeXFD-result-nocon',
					'value':'no consensus'}),
				$('<label>').attr('for', 'closeXFD-result-nocon').text('No consensus')
			),
			// Merge
			( config.xfd.type === 'ffd' || config.xfd.type === 'rfd' ) ? '' : $('<span>')
			.attr('class', 'xfdc-dialog-option').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
					'id':'closeXFD-result-merge', 'value':'merge'}),
				$('<label>').attr('for', 'closeXFD-result-merge').text('Merge')
			),	
			// Disambiguate
			( config.xfd.type !== 'rfd' ) ? '' : $('<span>')
			.attr('class', 'xfdc-dialog-option').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
					'id':'closeXFD-result-disambig', 'value':'disambiguate'}),
				$('<label>').attr('for', 'closeXFD-result-disambig').text('Disambiguate')
			),			
			// Delete
			( !config.user.isSysop && config.xfd.type !== 'tfd') ? '' : $('<span>')
			.attr('class', 'xfdc-dialog-option').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
				'class':'closeXFD-deleteResult', 'id':'closeXFD-result-delete', 'value':'delete'}),
				$('<label>').attr('for', 'closeXFD-result-delete').text('Delete')
			),
			// Delete (disabled)
			( config.user.isSysop || config.xfd.type === 'tfd') ? '' : $('<span>')
			.attr('class', 'xfdc-dialog-option').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
					'class':'closeXFD-deleteResult', 'id':'closeXFD-result-delete-disabled',
					'disabled':'disabled'}),
				$('<label>').attr('for', 'closeXFD-result-delete-disabled').append(
					'Delete ',
					extraJs.makeTooltip('Non-admin closure is not appropriate when the result '+
						'will require action by an administrator (per [[WP:BADNAC]])')
				)
			),
			// Soft delete
			( !config.user.isSysop || config.xfd.type === 'rfd' ) ? '' : $('<span>')
			.attr({'class':'xfdc-dialog-option', 'id':'closeXFD-resultContainer-softdel'}).append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
					'class':'closeXFD-deleteResult', 'id':'closeXFD-result-softdel',
					'value':'soft delete'}),
				$('<label>').attr('for', 'closeXFD-result-softdel').text('Soft delete')
			),
			// Custom
			$('<span>').attr('class', 'xfdc-dialog-option').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-result',
					'id':'closeXFD-result-custom', 'value':'custom'}),
				$('<label>').attr('for', 'closeXFD-result-custom').text('Custom')
			)
		),
		$('<div>')
		.attr({'id':'closeXFD-resultContainer-custom'})
		.css({'clear':'both', 'padding-bottom':'1%', 'display':'none'})
		.append(
			$('<label>').attr('for', 'closeXFD-result-custom-input').text('Custom result:'),
			$('<input>')
			.attr({'type':'text', 'name':'closeXFD-result-custom-input',
				'id':'closeXFD-result-custom-input'})
			.change(function(){
				self.inputData.customResult = $('#closeXFD-result-custom-input').val().trim();
			})
		),
		// Target page
		$('<div>')
		.attr({'id':'closeXFD-resultContainer-target'})
		.css({'clear':'both', 'padding-bottom':'1%', 'display':'none'})
		.append(
			$('<label>').attr({'id':'closeXFD-result-target-label', 'for':'closeXFD-result-target'})
			.text('Target:'),
			$('<input>')
			.attr({'type':'text', 'name':'closeXFD-result-target', 'id':'closeXFD-result-target'})
			.change(function(){
				self.inputData.target = Page.newFromText(
					$('#closeXFD-result-target').val().trim()
				);
			})
		)
	);
	
	$resultContainer.find('span.xfdc-dialog-option').after('<wbr>');
	
	$resultContainer.find("input[type=radio][name='closeXFD-result']").change(function(){
		var v = $(this).val();
		self.inputData.result = v;
		var currentAfter = $('input[name="closeXFD-after"]:checked').val() || false;
		// Show/hide options
		switch(v) {
			default:
			case 'keep':
			case 'no consensus':
				$('.closeXFD-keepOptions').show();
				$('.closeXFD-redirectOptions, .closeXFD-mergeOptions, .closeXFD-disambigOptions, .closeXFD-deleteOptions, '+
				'#closeXFD-customOptions, #closeXFD-resultContainer-target, '+
				'#closeXFD-resultContainer-custom').hide();
				// default options:
				if (
					!currentAfter ||
					( currentAfter !== 'doKeepActions' && currentAfter !== 'noAction' )
				) {
					$('#closeXFD-after-doKeepActions').prop('checked', true).change();
				}
				break;
			case 'retarget':
			case 'redirect':
			case 'soft redirect':
				$('.closeXFD-redirectOptions, #closeXFD-resultContainer-target').show();
				$('.closeXFD-keepOptions, .closeXFD-mergeOptions, .closeXFD-disambigOptions, .closeXFD-deleteOptions, '+
					'#closeXFD-customOptions, #closeXFD-resultContainer-custom').hide();
				$('#closeXFD-result-target-label').text(extraJs.toSentenceCase(v) + ' to: ');
				// default options:
				if (
					!currentAfter ||
					( currentAfter !== 'doRedirectActions' && currentAfter !== 'noAction' )
				) {
					$('#closeXFD-after-doRedirectActions')
					.prop('checked', true).change();
				}
				break;
			case 'merge':
				$('.closeXFD-mergeOptions, #closeXFD-resultContainer-target').show();
				$('.closeXFD-keepOptions, .closeXFD-redirectOptions, .closeXFD-disambigOptions, .closeXFD-deleteOptions, '+
					'#closeXFD-customOptions, #closeXFD-resultContainer-custom').hide();
				$('#closeXFD-result-target-label').text('Merge to:');
				// default options:
				if (
					!currentAfter ||
					( currentAfter !== 'doMergeActions' && currentAfter !== 'noAction' )
				) {
					$('#closeXFD-after-doMergeActions').prop('checked', true).change();
				}
				break;
			case 'disambiguate':
				$('.closeXFD-disambigOptions').show();
				$('.closeXFD-keepOptions, .closeXFD-redirectOptions, .closeXFD-mergeOptions, .closeXFD-deleteOptions, '+
				'#closeXFD-customOptions, #closeXFD-resultContainer-target, '+
				'#closeXFD-resultContainer-custom').hide();
				// default options:
				if (
					!currentAfter ||
					( currentAfter !== 'doDisambigActions' && currentAfter !== 'noAction' )
				) {
					$('#closeXFD-after-doDisambigActions').prop('checked', true).change();
				}
				break;
			case 'delete':
			case 'soft delete':
				$('.closeXFD-deleteOptions').show();
				$('.closeXFD-keepOptions, .closeXFD-redirectOptions, .closeXFD-mergeOptions, .closeXFD-disambigOptions, '+
					'#closeXFD-customOptions, #closeXFD-resultContainer-target, '+
					'#closeXFD-resultContainer-custom').hide();
				// default options:
				if (
					!currentAfter ||
					( currentAfter !== 'doDeleteActions' &&
					currentAfter !== 'holdingCell' &&
					currentAfter !== 'noAction' )
				) {
					$('#closeXFD-after-' +
					(( config.user.isSysop ) ? 'doDeleteActions' : 'holdingCell') +
					', #closeXFD-del-deleteredir, #closeXFD-del-unlinkbackl')
					.prop('checked', true).change();
				}
				// When soft delete is selected, prepend rationale with REFUND message
				if ( v === 'soft delete' ) {
					var old_rationale = $('#closeXFD-rationale').val();
					if (!old_rationale.includes('[[WP:REFUND]]')) {
						$('#closeXFD-rationale').val('[[WP:REFUND]] applies. ' + old_rationale)
						.change();
					}
				}
				break;
			case 'custom':
				$('#closeXFD-customOptions, #closeXFD-resultContainer-custom, '+
				'.closeXFD-keepOptions, .closeXFD-deleteOptions').show();
				$('.closeXFD-redirectOptions, .closeXFD-mergeOptions, .closeXFD-disambigOptions, '+
				'#closeXFD-resultContainer-target').hide();
				// default options:
				if ( !currentAfter || currentAfter !==  'noAction' ) {
					$('#closeXFD-after-noAction').prop('checked', true).change();
				}
		}
		self.resetHeight();
	});
	
	return $resultContainer;

};

Dialog.prototype.makeRationaleBox = function(multimode) {
	var self = this;
	
	var $rationale = $('<div>')
	.css('clear', 'both')
	.append(
		$('<strong>').attr('id', 'closeXFD-rationale-label')
		.text( ( self.relisting ) ? 'Relist comment' : (( multimode ) ? 'Rationale' : 'Additional '+
			'rationale')),
		( !multimode ) ? ' (optional):' : $('<a>')
		.addClass('xfdc-dialog-bracketedOption')
		.text('copy from above')
		.click(function(){
			var results = '';
			for ( var p in self.inputData.pageActions ) {
				var a = self.inputData.pageActions[p].action;
				if ( a ) {
					a = a
					.replace('del', 'delete')
					.replace(/(default|na)/, ' ');
				} else {
					a = ' ';
				}
				var t = ( /(merge|redirect)/.test(a) ) ? ' to [[' +
					self.inputData.pageActions[p].target + ']]\n' : '\n';
				results += "*'''" +	extraJs.toSentenceCase(a) + "''' [[" + p + "]]" + t;
			}
			$('#closeXFD-rationale').val(results + self.inputData.rationale).change();
		}),
		$('<br>'),
		$('<textarea>')
		.attr({'id':'closeXFD-rationale', 'rows':(( self.relisting ) ? 2 : 4)})
		.css('width', '99%')
		.change(function(){
			self.inputData.rationale = $(this).val().trim();
		})
		.change(),
		( self.relisting ) ? '' : $('<div>')
		.css({'clear':'both', 'padding-bottom':'1%'})
		.append(
			$('<input>')
			.attr({'type':'checkbox', 'name':'closeXFD-rationale-sentence',
				'id':'closeXFD-rationale-sentence'})
			.change(function(){
				if ( $(this).prop('checked') ) {
					self.inputData.resultPunct = '';
				} else {
					self.inputData.resultPunct = '.';
				}
			})
			.change(),
			$('<label>').attr('for', 'closeXFD-rationale-sentence')
			.text('Rationale is not a new sentence')
		)
	);
	
	if ( multimode ) {
		$('<div>')
		.css({'clear':'both', 'padding-bottom':'1%'})
		.append(
			$('<label>').attr('for', 'closeXFD-resultSummary-input').append(
				'Result summary',
				extraJs.makeTooltip('Appears in bold text before the rationale; also used in '+
					'edit summaries, and in Old ' + config.xfd.type + ' templates'),
				': '
			),
			$('<input>')
			.attr({'type':'text', 'name':'closeXFD-resultSummary-input',
				'id':'closeXFD-resultSummary-input'})
			.change(function(){
				self.inputData.result = $(this).val().trim();
			})
		)
		.prependTo($rationale);
	}
	
	return $rationale;
};

Dialog.prototype.makeAfterActions = function(multimode) {
	var self = this;

	var $after = $('<div>')
	.attr('id', 'closeXFD-afterContainer')
	.addClass('xfdc-dialog-container')
	.css('max-width', ( multimode ) ? '30%' : '40%')
	.append(
		$('<strong>').text('After closing:').css('display','block'),
		// KeepActions
		$('<div>')
		.addClass('closeXFD-keepOptions')
		.hide()
		.append(
			( multimode ) ? $('<strong>')
			.text('"Keep" pages: ') : $('<input>')
			.attr({
				'type':'radio',
				'name':'closeXFD-after',
				'id':'closeXFD-after-doKeepActions',
				'value':'doKeepActions'
			}),
			$(( multimode ) ? '<span>' : '<label>')
			.attr('for', 'closeXFD-after-doKeepActions')
			.append(
				'Update pages and talk pages ',
				extraJs.makeTooltip('Remove nomination templates, and add old xfd templates '+
					'to talk pages')
			)
		),
		// RedirectActions
		$('<div>')
		.addClass('closeXFD-redirectOptions')
		.hide()
		.append(
			( multimode ) ? $('<strong>')
			.text('"Redirect" pages: ') : $('<input>').attr({
				'type':'radio',
				'name':'closeXFD-after',
				'id':'closeXFD-after-doRedirectActions',
				'value': 'doRedirectActions'
			}),
			$(( multimode ) ? '<span>' : '<label>')
			.attr('for', 'closeXFD-after-doRedirectActions')
			.append(
				'Redirect pages and update talk pages ',
				extraJs.makeTooltip('Redirect nominated pages to the specified target, '+
					'and add old xfd templates to talk pages')
			)
		),
		// MergeActions
		$('<div>')
		.addClass('closeXFD-mergeOptions')
		.hide()
		.append(
			( multimode ) ? $('<strong>')
			.text('"Merge" pages: ') : $('<input>').attr({
				'type':'radio',
				'name':'closeXFD-after',
				'id':'closeXFD-after-doMergeActions',
				'value': 'doMergeActions'
			}),
			$(( multimode ) ? '<span>' : '<label>')
			.attr('for', 'closeXFD-after-doMergeActions')
			.append(
				'Update pages and talk pages ',
				extraJs.makeTooltip('Replace nomination templates with merge templates, '+
					'and add old xfd templates to talk pages')
			)
		),
		// DisambigActions
		$('<div>')
		.addClass('closeXFD-disambigOptions')
		.hide()
		.append(
			( multimode ) ? $('<strong>')
			.text('"Disambiguate" pages: ') : $('<input>')
			.attr({
				'type':'radio',
				'name':'closeXFD-after',
				'id':'closeXFD-after-doDisambigActions',
				'value':'doDisambigActions'
			}),
			$(( multimode ) ? '<span>' : '<label>')
			.attr('for', 'closeXFD-after-doDisambigActions')
			.append(
				'Update pages and talk pages ',
				extraJs.makeTooltip('Remove nomination templates, and add old xfd templates '+
					'to talk pages')
			)
		),
		// Delete actions
		( multimode || !config.user.isSysop ) ? '' : $('<div>')
		.addClass('closeXFD-deleteOptions')
		.hide()
		.append(
			$('<input>').attr({
				'type':'radio',
				'name':'closeXFD-after',
				'id':'closeXFD-after-doDeleteActions',
				'value': 'doDeleteActions'
			}),
			$('<label>').attr('for', 'closeXFD-after-doDeleteActions').append(
				'Delete pages ',
				extraJs.makeTooltip('Delete nominated pages and (unless otherwise specified) '+
					'their talk pages')
			)
		),
		// Holding cell
		( multimode || config.xfd.type !== 'tfd' ) ? '' : $('<div>')
		.addClass('closeXFD-deleteOptions')
		.hide()
		.append(
			$('<input>').attr({
				'type':'radio',
				'name':'closeXFD-after',
				'id':'closeXFD-after-holdingCell',
				'value':'holdingCell'
			}),
			$('<label>').attr('for', 'closeXFD-after-holdingCell').append(
				'List pages at holding cell ',
				extraJs.makeTooltip('Replace nomination template with {{Being deleted}}, '+
					'and list at the specified holding cell section')
			)
		),
		// Multimode delete/holding cell
		( !multimode ) ? '' :  $('<div>')
		.addClass('closeXFD-deleteOptions')
		.hide()
		.append(
			$('<strong>').text('"Delete" pages: '),
			$('<span>').append(
				( !config.user.isSysop ) ? '' : 'Delete pages ',
				( !config.user.isSysop ) ? '' : extraJs.makeTooltip('Delete nominated pages and '+
					'(unless otherwise specified) their talk pages'),
				( config.xfd.type !== 'tfd' ) ? '' : (( config.user.isSysop ) ? ' or l' : 'L') +
					'ist pages at holding cell ',
				( config.xfd.type !== 'tfd' ) ? '' : extraJs.makeTooltip('Replace nomination '+
					'template with {{Being deleted}}, and list at the specified '+
					'holding cell section')
			)
		),
		// No actions
		$('<div>')
		.attr('id', 'closeXFD-noAction')
		.toggle(!multimode)
		.append(
			( multimode ) ? $('<strong>')
			.text('"(no action)" pages: ') : $('<input>').attr({
				'type':'radio',
				'name':'closeXFD-after',
				'id':'closeXFD-after-noAction',
				'value':'noAction'
			}),
			$(( multimode ) ? '<span>' : '<label>')
			.attr('for', 'closeXFD-after-noAction').text('No automated actions')
		)
	)
	.find('input').change(function() {
		self.inputData.after = $(this).val();
		// Hide options if no action selected, or 'doKeepActions' is selected
		$('#closeXFD-optionsContainer').toggle(!/(noAction|doKeepActions)/.test(self.inputData.after));
		// Show or hide holding cell sections and 'delete redirects', if holding cell selected
		if ( config.xfd.type === 'tfd' ) {
			$('#closeXFD-del-holdcell').toggle(self.inputData.after === 'holdingCell');
			$('#closeXFD-del-deleteredir').parent()
			.toggle(self.inputData.after === 'doDeleteActions');
			// Disable 'don't delete talk' option for holding cell (except 'ready for deletion')
			if ( self.inputData.after === 'holdingCell' && self.inputData.holdcell !== 'ready' ) {
				$('#closeXFD-del-deletetalk').prop('disabled', true)
				.next().addClass('xfdc-dialog-disabled');
			} else {
				$('#closeXFD-del-deletetalk').prop('disabled', false)
				.next().removeClass('xfdc-dialog-disabled');
			}
		}
	} )
	.end();
	
	if ( multimode ) {
		$after.children('div').addClass('xfdc-dialog-actionInfo');
	}
	
	return $after;

};

Dialog.prototype.makeOptions = function(multimode) {
	var self = this;
	
	var $options = $('<div>')
	.attr('id', 'closeXFD-optionsContainer')
	.append(
		// Redirect options:
		( config.xfd.type === 'ffd' ) ? '' : $('<div>')
		.addClass('closeXFD-redirectOptions')
		.hide()
		.append(
			$('<strong>').text(( multimode ) ? 'Redirect options' : 'Options')
			.css('display','block'),
			// Delete before redirecting
			( !config.user.isSysop ) ? '' : $('<div>').append(
				$('<input>')
				.attr({
					'type':'checkbox',
					'name':'closeXFD-redir-deleteFirst',
					'id':'closeXFD-redir-deleteFirst'
				})
				.change(function(){
					self.inputData.deleteFirst = $(this).prop('checked');
					if ( !multimode ) {
						// toggle result labels
						$('#closeXFD-result-redir').nextAll().toggle();
						$('#closeXFD-result-softredir').nextAll().toggle();
					}
				}),
				$('<label>').attr('for', 'closeXFD-redir-deleteFirst')
				.text('Delete before redirecting')
			),
			// Rcats
			/*
			$('<div>').append(
				$('<input>').attr({
					'type':'checkbox',
					'name':'closeXFD-redir-addRcats',
					'id': 'closeXFD-redir-rcats',
					'disabled': true
				}).prop('checked', ( config.xfd.type === 'afd' )),
				$('<label>').attr('for', 'closeXFD-redir-rcats').append(
					$('<a>')
					.attr({'href':'https://en.wikipedia.org/wiki/Template:R_template_index',
						'target':'_blank'}).text('Rcats'),
					": ",
					extraJs.makeTooltip('Full markup required, e.g. "{{R to section}}";. '+
						'Multiple rcats can be specified, e.g. "{{R from book}}'+
						'{{R to anchor}}". Leave blank if unsure which rcat to use.')
				),
				//self.rcatSelector.$element.detach()
			)*/
		),
		
		// Merge options: 
		( config.xfd.type !== 'tfd' ) ? '' : $('<div>')
		.attr('id', 'closeXFD-merge-holdcell')
		.addClass('closeXFD-mergeOptions')
		.hide()
		.append(
			$('<strong>').text('Holding cell merge subsection:').css('display','block'),
			// Arts
			$('<div>').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-merge-holdingcell',
					'id':'closeXFD-holdingcell-merge-arts', 'value':'merge-arts'}),
				$('<label>').attr('for', 'closeXFD-holdingcell-merge-arts').text('Merge (Arts)')
			),
			// Geography, politics and governance
			$('<div>').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-merge-holdingcell',
					'id':'closeXFD-holdingcell-merge-geopolgov', 'value':'merge-geopolgov'}),
				$('<label>').attr('for', 'closeXFD-holdingcell-merge-geopolgov')
				.text('Merge (Geography, politics and governance)')
			),
			// Religion
			$('<div>').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-merge-holdingcell',
					'id':'closeXFD-holdingcell-merge-religion', 'value':'merge-religion'}),
				$('<label>').attr('for', 'closeXFD-holdingcell-merge-religion')
				.text('Merge (Religion)')
			),
			// Sports
			$('<div>').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-merge-holdingcell',
					'id':'closeXFD-holdingcell-merge-sports', 'value':'merge-sports'}),
				$('<label>').attr('for', 'closeXFD-holdingcell-merge-sports')
				.text('Merge (Sports)')
			),
			// Other
			$('<div>').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-merge-holdingcell',
					'id':'closeXFD-holdingcell-merge-other', 'value':'merge-other'}),
				$('<label>').attr('for', 'closeXFD-holdingcell-merge-other')
				.text('Merge (Other)')
			),
			// Meta
			$('<div>').append(
				$('<input>').attr({'type':'radio', 'name':'closeXFD-merge-holdingcell',
					'id':'closeXFD-holdingcell-merge-meta', 'value':'merge-meta'}),
				$('<label>').attr('for', 'closeXFD-holdingcell-merge-meta')
				.text('Merge (Meta)')
			)
		)
		.find("input[type=radio][name='closeXFD-merge-holdingcell']").change(function(){
			self.inputData.mergeHoldcell = $(this).val();
		})
		.end(),
		
		// Deletion options
		( !config.user.isSysop && config.xfd.type !== 'tfd' ) ? '' : $('<div>')
		.addClass('closeXFD-deleteOptions')
		.hide()
		.append(
			$('<strong>')
			.text(( multimode ) ? 'Delete options' : 'Options').css('display','block'),
			// Don't delete talk pages
			( !config.user.isSysop && config.xfd.type !== 'tfd' ) ? '' : $('<div>').append(
				$('<input>').attr({'type':'checkbox', 'name':'closeXFD-del-deletetalk',
					'id':'closeXFD-del-deletetalk', 'value':'dontdeletetalk'}),
				$('<label>').attr('for', 'closeXFD-del-deletetalk').append(
					( config.user.isSysop ) ? 'Do not delete talk pages' : 'Do not tag talk '+
						'pages for speedy deletion'
				)
			),
			// Delete redirects
			( !config.user.isSysop || config.xfd.type === 'rfd' ) ? '' : $('<div>').append(
				$('<input>').attr({'type':'checkbox', 'name':'closeXFD-del-deleteredir',
					'id':'closeXFD-del-deleteredir', 'value':'deleteredir',
					'checked':'checked'}),
				$('<label>').attr('for', 'closeXFD-del-deleteredir')
				.text('Also delete redirects')
			),
			// Unlink backlinks
			( config.xfd.type !== 'afd' && config.xfd.type !== 'ffd' ) ? '' : $('<div>')
			.append(
				$('<input>').attr({'type':'checkbox', 'name':'closeXFD-del-unlinkbackl',
					'id':'closeXFD-del-unlinkbackl', 'value':'unlinkbackl',
					'checked':'checked'}),
				$('<label>').attr('for', 'closeXFD-del-unlinkbackl').text('Unlink backlinks')
			),
			// Delete and redirect
			( multimode || !config.user.isSysop || config.xfd.type === 'ffd' ) ? '' : $('<div>')
			.append(
				// Hidden checkbox, so that text lines up
				$('<input>').attr('type', 'checkbox').css('visibility', 'hidden'),
				// Shortcut for switching to 'Redirect' and selects 'Delete before redirecting'
				$('<a>').attr('id', 'closeXFD-del-deleteAndRedir')
				.text('Delete and redirect...')
				.click(function(){
					$('#closeXFD-result-redir').prop('checked', true).change();
					$('#closeXFD-redir-deleteFirst').prop('checked', true);
					// Switch result labels to 'Delete and ...' versions, if needed
					if ( $('#closeXFD-result-redir').next().is(':visible') ) {
						$('#closeXFD-redir-deleteFirst').change();
					}
				} )
			),
			// Holding cell
			( config.xfd.type !== 'tfd' ) ? '' : $('<div>')
			.attr('id', 'closeXFD-del-holdcell')
			.append(
				$('<strong>').css('display', 'block').append(
					( !multimode ) ? 'Holding cell section:' : $('<input>')
					.attr({'type':'checkbox', 'name':'closeXFD-del-useHoldingCell',
						'id':'closeXFD-del-useHoldingCell', 'value':'useHoldingCell'})
					.on('change', function(){
						self.inputData.useHoldingCell = $(this).prop('checked');
						if ( self.inputData.useHoldingCell ) {
							$('#closeXFD-del-deletetalk, #closeXFD-del-deleteredir')
							.prop('disabled', true)
							.next().addClass('xfdc-dialog-disabled');
							$('#closeXFD-del-holdcell').children('div').show();
						} else {
							$('#closeXFD-del-deletetalk, #closeXFD-del-deleteredir')
							.prop('disabled', false)
							.next().removeClass('xfdc-dialog-disabled');
							$('#closeXFD-del-holdcell').children('div').hide();
						}
					})
					.prop('checked', !config.user.isSysop).change()
					.prop('disabled', !config.user.isSysop)
					.toggle(config.user.isSysop),
					( !multimode ) ? '' : $('<label>')
					.attr('for', 'closeXFD-del-useHoldingCell')
					.text((config.user.isSysop) ? 'Add to holding cell '+
						'instead of deleting:' : 'Holding cell section:')
				),
				// Review
				$('<div>').append(
					$('<input>').attr({'type':'radio', 'name':'closeXFD-holdingcell',
						'id':'closeXFD-holdingcell-review', 'value':'review'}),
					$('<label>').attr('for', 'closeXFD-holdingcell-review').text('Review')
				),
				// Convert
				$('<div>').append(
					$('<input>').attr({'type':'radio', 'name':'closeXFD-holdingcell',
						'id':'closeXFD-holdingcell-convert', 'value':'convert'}),
					$('<label>').attr('for', 'closeXFD-holdingcell-convert').text('Convert')
				),
				// Substitute
				$('<div>').append(
					$('<input>').attr({'type':'radio', 'name':'closeXFD-holdingcell',
						'id':'closeXFD-holdingcell-substitute', 'value':'substitute'}),
					$('<label>').attr('for', 'closeXFD-holdingcell-substitute').text('Substitute')
				),			
				// Orphan
				$('<div>').append(
					$('<input>').attr({'type':'radio', 'name':'closeXFD-holdingcell',
						'id':'closeXFD-holdingcell-orphan', 'value':'orphan'}),
					$('<label>').attr('for', 'closeXFD-holdingcell-orphan').text('Orphan')
				),
				// Ready for deletion
				$('<div>').append(
					$('<input>').attr({'type':'radio', 'name':'closeXFD-holdingcell',
						'id':'closeXFD-holdingcell-ready', 'value':'ready'}),
					$('<label>').attr('for', 'closeXFD-holdingcell-ready')
					.text('Ready for deletion')
				)
			)
			.children('div').toggle(!multimode || !config.user.isSysop).end()
			.find("input[type=radio][name='closeXFD-holdingcell']").change(function() {
				self.inputData.holdcell = $(this).val();
				// Disable 'don't delete talk' option unless 'ready for deletion' selected
				if ( self.inputData.holdcell !== 'ready' ) {
					$('#closeXFD-del-deletetalk').prop('disabled', true)
					.next().addClass('xfdc-dialog-disabled');
				} else {
					$('#closeXFD-del-deletetalk').prop('disabled', false)
					.next().removeClass('xfdc-dialog-disabled');
				}
			} )
			.end()
		)
	)
	.find('input[type=checkbox]')
		.not('#closeXFD-redir-deleteFirst, #closeXFD-del-useHoldingCell')
		.change(function(){
			var v = $(this).val();
			self.inputData[v] = $(this).prop('checked');
		})
		.change()
		.end()
	.end();
	
	if ( multimode ) {
		$options.children().addClass('xfdc-dialog-container');
	} else {
		$options.css({'float':'right', 'width':'59%'});
	}

	return $options;
};

Dialog.prototype.makeButtons = function(multimode) {
	var self = this;
	
	var $buttons = $('<div>')
	.attr('id', 'closeXFD-interface-buttons')
	.css({'text-align':'center', 'clear':'both'})
	.append(
		$('<button>')
		.attr('id', 'closeXFD-interface-cancel')
		.text('Cancel')
		.click(function() {
			self.close();
			self.discussion.showLinks();
		})
	);
	if ( !self.relisting ) {
		$buttons.prepend(
			$('<button>')
			.attr('id', 'closeXFD-interface-closedisc')
			.text('Close Discussion')
			.click(function() {
				if ( multimode ) {
					self.evaluateMultimodeClose();
				} else {
					self.evaluateClose();
				}
			}),
			$('<button>')
			.attr('id', 'closeXFD-interface-preview')
			.text('Preview')
			.click(function() {
				if ( multimode ) {
					self.evaluateMultimodeClose(true);
				} else {
					self.evaluateClose(true);
				}
			})
		);
	} else {
		$buttons.prepend(
			$('<button>').attr('id', 'closeXFD-interface-relistdisc').text('Relist Discussion')
			.click(function() {
				self.close();
				self.discussion.taskManager = new TaskManager(self.discussion);
				self.discussion.taskManager.start();
			}),
			$('<button>').attr('id', 'closeXFD-interface-previewRelist').text('Preview')
			.click(function() {
				self.showPreview(true);
			})
		);
	}

	return $buttons;
};

Dialog.prototype.makePreviewBox = function() {
	return $('<p>').attr('id', 'closeXFD-preview-output').hide();
};

Dialog.prototype.updateMultimodeOptions = function() {
	$('.closeXFD-keepOptions').toggle(this.inputData.inPageActions('keep'));
	$('.closeXFD-redirectOptions').toggle(this.inputData.inPageActions('redirect'));
	$('.closeXFD-mergeOptions').toggle(this.inputData.inPageActions('merge'));
	$('.closeXFD-disambigOptions').toggle(this.inputData.inPageActions('disambig'));
	$('.closeXFD-deleteOptions').toggle(this.inputData.inPageActions('del'));
	$('#closeXFD-noAction').toggle(this.inputData.inPageActions('na'));
	this.resetHeight();
};

Dialog.prototype.storeRcatData = function(rcatData) {
	this.inputData.rcats = rcatData.join('\n').trim();
	$('#closeXFD-redir-rcats').prop('checked', this.inputData.rcats !== '');
};

// --- Set up  for close or relist: ---
Dialog.prototype.setup = function() {
	var self = this;
	
	this.emptyContent();
	
	if ( config.xfd.type !== 'ffd' && !self.discussion.isBasicMode() ) {
		//this.rcatSelector = extraJs.makeRcatCapsuleMultiselect(self.storeRcatData, self);
	}
	
	this.inputData = new InputData(this.discussion);
	
	this.addToHeader(this.makeHeader());
	
	this.addToBody(this.makePagesList());
	if ( this.relisting ) {
		this.addToBody(this.makeRationaleBox());
	} else {
		if ( !config.user.isSysop ) {
			this.addToBody(this.makeNonAdminNote());
		}
		this.addToBody(this.makeCloseResult());
		this.addToBody(this.makeRationaleBox());
		if ( this.discussion.isBasicMode() ) {
			this.inputData.after = 'noAction';
		} else {
			if ( config.xfd.type === 'afd' ) {
				//this.rcatSelector.setItemsFromData(['{{R to related topic}}']);
			}
			this.addToBody(this.makeAfterActions());
			this.addToBody(this.makeOptions());
		}
	}
	
	this.addToFooter(this.makeButtons());
	this.addToFooter(this.makePreviewBox());
	
	this.resetHeight();
	this.display();
};

// --- Set up for multimode close: ---
Dialog.prototype.setupForMultiClose = function() {
	var self = this;
	
	this.emptyContent();
	
	if ( config.xfd.type !== 'ffd' && !self.discussion.isBasicMode() ) {
		//this.rcatSelector = extraJs.makeRcatCapsuleMultiselect(self.storeRcatData, self);
	}
	
	this.inputData = new InputData(this.discussion);
	this.inputData.initialiseMultimode();
	
	this.addToHeader(this.makeHeader(true));
	
	if ( !config.user.isSysop ) {
		this.addToBody(this.makeNonAdminNote());
	}
	
	this.addToBody(this.makePagesList(true));
	this.addToBody(this.makeRationaleBox(true));
	this.addToBody(this.makeAfterActions(true));
	if ( config.xfd.type === 'afd' ) {
		//this.rcatSelector.setItemsFromData(['{{R to related topic}}']);
	}
	this.addToBody(this.makeOptions(true));
	
	this.addToFooter(this.makeButtons(true));
	this.addToFooter(this.makePreviewBox());
	
	this.resetHeight();
	this.display();	
};

// Preview
Dialog.prototype.showPreview = function(relist) {

	var preview_wikitext = '';

	if ( relist ) {
		preview_wikitext = '{{Relist|1=' + this.inputData.getRelistComment() + '}}';
	} else {
		preview_wikitext = "The result of the discussion was '''" +
		this.inputData.getResult()+
		"'''" +
		(( this.inputData.getTargetLink() ) ? ' to ' + this.inputData.getTargetLink() : '') +
		( (this.inputData.getRationale()) || '.' ) +
		' ' +
		config.user.sig;
	}

	API.get({
		action: 'parse',
		contentmodel: 'wikitext',
		text: preview_wikitext,
	})
	.done(function(result) {
		preview_html = result.parse.text['*'];
		$('#closeXFD-preview-output').html(preview_html).show().find('a').attr('target', '_blank');
	})
	.fail(function(code, jqxhr) {
		$('#closeXFD-preview-output')
		.empty()
		.append(
			$('<strong>')
			.css('color', '#f00')
			.append(
				'Error: Preview failed.',
				$('<br>'),
				'Details: ',		
				makeErrorMsg(code, jqxhr)
			)
		)
		.show();
	});
};


// --- Evaluate if required form elements have been completed ---

// Show errors for anything required but not completed
Dialog.prototype.showErrors = function(element_ids) {
	
	// remove any old error messages
	//$('.closeXFD-errorNote').parent().css('border', '').end().remove(); 
	
	
	$(element_ids.join(', '))
	.filter(':visible')
	.addClass('closeXFD-errorNote')
	.fadeTo('fast', 0.33).fadeTo('fast', 0.8).fadeTo('fast', 0.4).fadeTo('fast', 1)
	.on('change.errorNote', function() {
		$(this)
		.removeClass('closeXFD-errorNote')
		.off('change.errorNote');
	});
};

// Evaluate standard close
Dialog.prototype.evaluateClose = function(preview) {
	self = this;
	var errors = [];
	
	// Result
	if ( self.inputData.result ) {
		switch ( self.inputData.result ) {
			case 'custom':
				if ( !self.inputData.customResult ) {
					// Custom result not specified
					errors.push('#closeXFD-resultContainer-custom');
				}
				break;
			case 'retarget':
			case 'redirect':
			case 'soft redirect':
			case 'merge':
				if ( $('#closeXFD-result-target').val().trim() === '' ) {
					// Target not specified
					errors.push('#closeXFD-resultContainer-target');
				} else {
					if ( !self.inputData.target ) {
						// Invalid target
						alert('Bad ' + self.inputData.result +
							' target: the title is invalid.');
						errors.push('#closeXFD-resultContainer-target');
					}
				}
				break;
			default:
				break;
		}
	} else {
		// Result not selected
		errors.push('#closeXFD-resultOptions');
	}
	
	// Rationale
	var r = self.inputData.rationale;
	if ( r ) {
		// Prepend newline if it starts with a * or #
		r = r.replace(/^(\*|#)/, '\n$1');
		// Append a newline if the last line starts with a * or #
		var n = r.lastIndexOf('\n');
		if ( n > 0 && /(\*|#)/.test(r.slice(n+1,n+2) ) ) {
			r += '\n:';
		}
		self.inputData.rationale = r;
	}
	
	// After actions & options
	if ( self.inputData.after ) {
		if ( self.inputData.after === 'holdingCell' && !self.inputData.holdcell ) {
			// Holding cell section not selected
			errors.push('#closeXFD-del-holdcell');
		} else if (
			config.xfd.type === 'tfd' &&
			self.inputData.after === 'doMergeActions' &&
			!self.inputData.mergeHoldcell
		) {
			// Holding cell merge subsection not selected
			errors.push('#closeXFD-merge-holdcell');
		}
	} else {
		// After action not selected
		errors.push('#closeXFD-afterContainer');
	}
	
	if ( errors.length > 0 ) {
		// Show errors
		self.showErrors(errors);
	} else if ( preview ) {
		// Show preview
		self.showPreview();
	} else {
		// Start closing
		if ( 
			( config.xfd.type === 'tfd' && self.inputData.after === 'doMergeActions' ) ||
			( self.inputData.after === 'holdingCell' && self.inputData.holdcell !== 'ready' )
		) {
			// Don't delete/tag talkpages when using holding cell (except for 'ready for deletion')
			self.inputData.dontdeletetalk = true;
		}
		self.close();
		self.discussion.taskManager = new TaskManager(self.discussion);
		self.discussion.taskManager.start();
	}
};

// Evaluate multimode close
Dialog.prototype.evaluateMultimodeClose = function(preview) {
	self = this;
	var errors = [];

	// Check result summary specified
	if ( !self.inputData.result ) {
		errors.push('#closeXFD-resultSummary-input');
	}

	// Rationale
	var r = self.inputData.rationale;
	if ( r ) {
		// Prepend newline if it starts with a * or #
		r = r.replace(/^(\*|#)/, '\n$1');
		// Append a newline if the last line starts with a * or #
		var n = r.lastIndexOf('\n');
		if ( n > 0 && /(\*|#)/.test(r.slice(n+1,n+2) ) ) {
			r += '\n:';
		}
		self.inputData.rationale = r;
	}
	
	// Check each page action / target
	for ( var p in self.inputData.pageActions ) {
		// Check action specified
		if ( !self.inputData.pageActions[p].action ) {
			errors.push('#'+self.inputData.pageActions[p].id);
		} else if ( 
			self.inputData.pageActions[p] === 'redirect' ||
			self.inputData.pageActions[p] === 'merge'
		) {
			// Check target specified
			var targetInputId = self.inputData.pageActions[p].id.replace('action', 'tagert');
			if ( $('#'+targetInputId).val().trim() === '' ) {
				errors.push('#'+targetInputId);
			} else if ( !self.inputData.pageActions[p].target ) {
				alert('Bad ' + self.inputData.pageActions[p].action + 'target for ' +
				p +' – the title is invalid.');
				errors.push('#'+targetInputId);
			}
		}
	}

	// Check holding cell selection (merge)
	if (
		config.xfd.type === 'tfd' &&
		self.inputData.inPageActions('merge') &&
		!self.inputData.mergeHoldcell
	) {
		errors.push('#closeXFD-merge-holdcell');
	}
	// Check holding cell selection (delete)
	if (
		config.xfd.type === 'tfd' &&
		self.inputData.inPageActions('del') &&
		self.inputData.useHoldingCell &&
		!self.inputData.holdcell
	) {
		errors.push('#closeXFD-del-holdcell');
	}
	
	if ( errors.length > 0 ) {
		// Show errors
		self.showErrors(errors);
	} else if ( preview ) {
		// Show preview
		self.showPreview();
	} else {
		// Start closing
		if ( self.inputData.useHoldingCell && self.inputData.holdcell !== 'ready' ) {
			// Don't delete/tag talkpages when using holding cell (except for 'ready for deletion')
			self.inputData.dontdeletetalk = true;
		}
		self.close();
		self.discussion.taskManager = new TaskManager(self.discussion);
		self.discussion.taskManager.start();
	}	
};


/* ========== InputData class ===================================================================
   The raw data that the user entered in a dialog, and prototype get
   functions to obtain ready to use data
   ---------------------------------------------------------------------------------------------- */
// Constructor
var InputData = function(discussion) {
	this.discussion = discussion;
/* Defined by user interaction with dialog:
	this.speedy : {Boolean}
	this.customResult : {String}
	this.target : {Page object}
	this.result : {String}
	this.rationale : {String}
	this.resultPunct : {String}
	this.after : {String}
	this.deleteFirst : {Boolean}
	this.rcats : {String|Boolean false}
	this.useHoldingCell : {Boolean}
	this.holdcell : {String}
	this.mergeHoldcell : {String}
	this.dontdeletetalk : {Boolean}
	this.deleteredir : {Boolean}
	this.unlinkbackl : {Boolean}
*/
};

// ---------- InputData prototype --------------------------------------------------------------- */
// Setup for multimode close
InputData.prototype.initialiseMultimode = function() {
	this.multimode = true;
	this.pageActions = {};
};

// Check if an action has been specified for any of the pages (for multimode)
InputData.prototype.inPageActions = function(action) {
	var self = this;
	if ( !self.multimode ) return false;
	for ( var p in self.pageActions ) {
		if ( self.pageActions[p].action === action ) return true;
	}
	return false;
};

// --- Get functions ---

// Get array of Page objects for a result (for multimode)
InputData.prototype.getPages = function(result, result2, result3) {
	var self = this;
	
	if ( !self.multimode ) return false;
	
	if ( result2 == null ) result2 = false;
	if ( result3 == null ) result3 = false;
	
	var output = [];
	for ( var p in self.pageActions ) {
		if (
			self.pageActions[p].action === result ||
			self.pageActions[p].action === result2 ||
			self.pageActions[p].action === result3
		) {
			output.push(self.discussion.getPageByTitle(p));
		}
	}
	return output;
};

// Get the relist comment, escaping pipes which aren't within wikilinks or templates
InputData.prototype.getRelistComment = function() {
	return this.rationale.replace(/(\|)(?!(?:[^\[]*]|[^\{]*}))/g, '&#124;');
};

// Get the full result, for use as bolded result text or in edit summaries etc.
InputData.prototype.getResult = function() {
	if ( this.multimode ) {
		return this.result;
	} else {
		var output = '';
		if ( this.speedy ) {
			output += 'speedy ';
		}
		if (
			this.deleteFirst &&
			/(?:retarget|redirect|soft redirect)/.test(this.result)
		) {
			output += 'delete and ';
		}
		output += ( this.result === 'custom') ? this.customResult : this.result;
		return output;
	}
};

// Get the page title of the target - for a particular page if in multimode
InputData.prototype.getTarget = function(p) {
	if ( this.multimode && p ) {
		return this.pageActions[p].target.getPrefixedText();
	} else if ( /(?:retarget|redirect|soft redirect|merge)/.test(this.result) ) {
		return this.target && this.target.getPrefixedText();
	} else {
		return false;
	}
};

// Multimode: Get an array of page titles of targets for a particular action ('redirect' or 'merge') 
// Single result: Return an array with one item, the target page title
InputData.prototype.getTargetsArray = function(action) {
	var self = this;
	var output = [];
	if ( self.multimode ) {	
		for ( var p in self.pageActions ) {
			if ( self.pageActions[p] && self.pageActions[p].action === action ) {
				output.push(self.pageActions[p].target.getPrefixedText());
			}
		}
	} else {
		output.push(self.target.getPrefixedText());
	}
	return output;
};

// Multimode: Get an array of Titles objects of targets, for either 'redirect' or 'merge'
// Single result: Return an array with one item, the Title object for the target
InputData.prototype.getTargetsObjectsArray = function() {
	var self = this;
	var output = [];
	if ( self.multimode ) {	
		for ( var p in self.pageActions ) {
			if ( self.pageActions[p] && /(redirect|merge)/.test(self.pageActions[p].action) ) {
				output.push(self.pageActions[p].target);
			}
		}
	} else {
		output.push(self.target);
	}
	return output;
};

// Get an array of page titles to be merged to a particlar target (for multimode)
InputData.prototype.getPagesToBeMerged = function(target) {
	var self = this;
	var output = [];
	for ( var p in self.pageActions ) {
		if (
			self.pageActions[p] &&
			self.pageActions[p].action === 'merge' &&
			self.pageActions[p].target.getPrefixedText() === target
		) {
			output.push(p);
		}
	}
	return output;
};

// Get a link to the target - for a particular page if multimode - including fragment.
// Is formatted as a wikilink, with preceding colon if needed, unless raw is true.
InputData.prototype.getTargetLink = function(p, raw) {
	var targetObject = ( this.multimode && p ) ? this.pageActions[p].target : this.target;
	
	if ( !targetObject ) {
		return null;
	}

	var targetFrag = ( targetObject.getFragment() ) ? '#' + targetObject.getFragment() : '';
	var targetNS = targetObject.getNamespaceId();
	if ( raw ) {
		return targetObject.getPrefixedText() + targetFrag;
	} else if ( targetNS === 6 || targetNS === 14 ) {
		return "[[:" + targetObject.getPrefixedText() + targetFrag + "]]";
	} else {
		return "[[" + targetObject.getPrefixedText() + targetFrag + "]]";
	}
};

// Gets the rational, preceded by a period (unless its not a new sentence) and a space
InputData.prototype.getRationale = function() {
	return ( this.rationale ) ? this.resultPunct + ' ' + this.rationale : '';
};


/* ========== TaskManager class =================================================================
   Manages tasks - chooses which tasks to do, what data to pass to them, and
   keeps track of when (all) tasks are completed
   ---------------------------------------------------------------------------------------------- */
// Constructor
var TaskManager = function(discussion, inputDataObject) {
	this.discussion = discussion;
	if ( inputDataObject != null ) {
		this.inputData = inputDataObject;
	} else {
		this.inputData = discussion.dialog.inputData;
	}
	this.tasks = [];
	this.dfd = {};
};

// ---------- TaskManager prototype ------------------------------------------------------------- */
// Sanity checks - confirm with user if there are a lot of pages or pages in unexpected namespaces
TaskManager.prototype.makeSanityCheckWarnings = function() {
	var self = this;
	
	var relisting = !self.inputData.result;
	var multimode = self.inputData.multimode;
	var warnings = [];
	
	// Check for mass actions when closing:
	if ( !relisting ) {
		// Only if nominated pages will be edited
		if (
			( relisting && /(ffd|tfd)/.test(config.xfd.type) ) ||
			( !relisting && !multimode && self.inputData.after !== 'noAction' ) ||
			( multimode && self.inputData.getPages('na').length !== self.discussion.pages.length )
		) {
			if ( self.discussion.pages.length > 3 ) {
				warnings.push('Mass actions will be peformed (' + self.discussion.pages.length +
				' nominated pages detected).');
				if ( self.discussion.pages.length > 50 ) {
					warnings.push('Only the first 50 pages may be processed, depending on the ' +
					'closing options selected.');
				}
			}
		}
	}
	
	// Check target page namespace:
	var targetObjectsArray = self.inputData.getTargetsObjectsArray();
	if ( targetObjectsArray ) {
		$.each(targetObjectsArray, function(_i, t) {
			if ( t && !t.hasCorrectNamespace()) {
				warnings.push( 'Target page [["' + t.getPrefixedText() + '"]] is not in the ' +
				config.mw.namespaces[(config.xfd.ns_number[0]).toString()] + ' namespace.');
			}
		});
	}
	
	//Check namespaces of nominated pages
	var wrongNamespacePages = self.discussion.pages && self.discussion.pages.filter(function(p) {
		return !p.hasCorrectNamespace();
	});
	if ( wrongNamespacePages.length > 0 ) {
		warnings.push(
			'The following pages are not in the ' +
			config.mw.namespaces[(config.xfd.ns_number[0]).toString()] + 
			' namespace:<ul><li>[[' +
			wrongNamespacePages.map(function(p){ return p.getTitle(); }).join(']]</li><li>[[') +
			']]</li>'
		);
	}

	if ( warnings.length === 0 ) {
		return false;
	} else {
		return '<p>' + warnings.join('</p><p>') + '</p>';
	}
};

// Resolve redirects:
// If nominated pages are redirects (at venues other than RfD), the script can't know if this was
// appropriate, where results such as 'Delete' should be applied to the target, or an out-of-process
// redirection, where results such as 'Delete' should be applied to the redirect 
TaskManager.prototype.resolveRedirects = function(callback) {
	var self = this;
	
	// No need to process for RfD, or if basic closure, or no after actions
	var relisting = !self.inputData.result;
	var multimode = self.inputData.multimode;
	if (
		// At RfD
		config.xfd.type === 'rfd' ||
		// Basic mode (no pages detected)
		!self.discussion.pages ||
		// Relisting for other than FfD/TfD
		( relisting && !/(ffd|tfd)/.test(config.xfd.type) ) ||
		// Not multimode, no after actions
		( !relisting && !multimode && self.inputData.after === 'noAction' ) ||
		// Multimode, no action for every page
		( multimode && self.inputData.getPages('na').length === self.discussion.pages.length )
	) {
		// Nominate pages expected to be redirects, no need to resolve them
		callback.call(self);
		return;
	}
	
	var processPages = function(result) {
		if ( result.query && result.query.redirects ) {
			// Redirects are present, need to ask user what to do

			var redir_from = new Array(result.query.redirects.length);
			var redir_to = new Array(result.query.redirects.length);
			var redir_list = new Array(result.query.redirects.length);
			for (var i=0; i<result.query.redirects.length; i++) {
				redir_from[i] = result.query.redirects[i].from;
				redir_to[i] = result.query.redirects[i].to;
				redir_list[i] = '[[' + redir_from[i] + ']] → [[' + redir_to[i] + ']]';
			}
			var redirects_msg = "The following nominated pages are redirects to other pages:<ul><li>" +
				redir_list.join("</li><li>") + "</li></ul>";
				
			// Proccess user's choice
			processRedirects = function(action) {
				if ( action === 'accept' ) {
					// Build a reference object linking titles to their page ids
					var title_ids = {};
					for (var tt=0; tt<result.query.pageids.length; tt++) {
						title_ids[ result.query.pageids[tt] ] = result.query.pages[ result.query.pageids[tt] ].title;
					}
					// Update discussion's page data
					self.discussion.pages = self.discussion.pages.map(function(p) {
						var rIndex = $.inArray(p.getTitle(), redir_from);
						if ( rIndex === -1 ) {
							// This is not a redirect
							return p;
						} else {
							// This is a redirect - create new page object for target
							var targetPage = Page.newFromText(redir_to[rIndex]);
							var targetId = extraJs.val2key(redir_to[rIndex], title_ids);
							targetPage.exists = targetId > 0;
							targetPage.talkExists = result.query.pages[targetId].talkid > 0;
							return targetPage;
						}
					});
					// Execute callback
					callback.call(self);
				} else if ( action === 'reject' ) {
					// Just get on with it: execute callback 
					callback.call(self);
				} else {
					// Abort closing, reset discussion links
					self.discussion.showLinks();
					return;
				}
			};
			
			// Ask for user confirmation
			extraJs.multiButtonConfirm('Use redirects or targets?', redirects_msg, [
					{ label: 'Cancel', flags: 'safe' },
					{ label: 'Use redirects', action: 'reject' },
					{ label: 'Use targets', action: 'accept', flags: 'progressive' }
				], processRedirects, {'verbose': true, 'unescape': true, 'wikilinks': true} );

		} else {
			// No redirects present, just get on with it: execute callback
			callback.call(self);
		}
	};
	
	// Get redirect status / targets, if any, from Api
	API.get( {
		action: 'query',
		titles: self.discussion.getPageTitles().join('|'),
		redirects: 1,
		prop: 'info',
		inprop: 'talkid',
		indexpageids: 1
	} ).done( processPages )
	.fail( function() {
		// TODO....
		
	} );	
};

// --- Initialise ---
// Set up for relist
TaskManager.prototype.initialiseForRelist = function() {
	var self = this;

	self.tasks.push(new Task('getRelistInfo', self.discussion));
	
	// Object to store gathered info
	self.relistInfo = {};
	
	switch ( config.xfd.type ) {
		case 'afd':
			Array.prototype.push.apply(self.tasks, [
				new Task('updateDiscussion', self.discussion),
				new Task('updateOldLogPage', self.discussion),
				new Task('updateNewLogPage', self.discussion)
			]);
			// Deferred for when this relist is finished, keep track of it via its array index
			self.afdLogEditIndex = config.track.afdLogEdit.push($.Deferred()) - 1;
			// Notify user
			self.discussion.setStatus('Waiting... (to avoid edit conflicts, previous relistings '+
			'need to be completed)');
			break;
		case 'mfd':
			self.tasks.push(new Task('updateDiscussion', self.discussion));
			break;
		case 'rfd':
			Array.prototype.push.apply(self.tasks, [
				new Task('updateOldLogPage', self.discussion),
				new Task('updateNewLogPage', self.discussion)
			]);
			break;
		default: // ffd, tfd
			Array.prototype.push.apply(self.tasks, [
				new Task('updateOldLogPage', self.discussion),
				new Task('updateNewLogPage', self.discussion),
				( self.discussion.pages ) ? new Task('updateNomTemplates', self.discussion) : ''
			]
			.filter(function(v){ return !!v; })
			);
			break;		
	}
	
	self.initialiseFinally();
};

TaskManager.prototype.initialiseForMultimodeClose = function() {
	// Set up for multimode
	var self = this;

	// Close discussion
	self.tasks.push(new Task('closeDisc', self.discussion));
	// Add Old xfd templates
	if (
		self.inputData.inPageActions('keep') ||
		self.inputData.inPageActions('redirect') ||
		( self.inputData.inPageActions('merge') && config.xfd.type !== 'tfd' ) ||
		self.inputData.inPageActions('disambig')	
	) {
		self.tasks.push(new Task('addOldXfd',
			self.discussion, self.inputData.getPages(
				'keep', 'redirect', ( /(afd|mfd)/.test(config.xfd.type) ) ? 'merge' : 'disambig'
			)
		));
	}
	// For keep results:
	if ( self.inputData.inPageActions('keep') ) {
		self.tasks.push(new Task('removeNomTemplates',
			self.discussion, self.inputData.getPages('keep')));
	}
	// For redirect results:
	if ( self.inputData.inPageActions('redirect') ) {
		self.tasks.push(new Task('redirect',
			self.discussion, self.inputData.getPages('redirect')));
	}
	// For merge (not holding cell) results:
	if ( config.xfd.type !== 'tfd' && self.inputData.inPageActions('merge') ) {
		self.tasks.push(new Task('addMergeTemplates',
			self.discussion, self.inputData.getPages('merge')));
	}
	// For disambiguate results:
	if ( self.inputData.inPageActions('disambig') ) {
		self.tasks.push(new Task('disambiguate',
			self.discussion, self.inputData.getPages('disambig')));
	}
	// For delete (not holding cell) results:
	if ( self.inputData.inPageActions('del') && !self.inputData.useHoldingCell ) {
		Array.prototype.push.apply(self.tasks, [
			new Task('peformDeletion', self.discussion, self.inputData.getPages('del')),
			( self.inputData.dontdeletetalk ) ? null : new Task('deleteTalk',
				self.discussion, self.inputData.getPages('del')),
			( self.inputData.deleteredir ) ? new Task('deleteRedirects',
				self.discussion, self.inputData.getPages('del')) : null,
			( self.inputData.unlinkbackl ) ? new Task('unlinkBacklinks',
				self.discussion, self.inputData.getPages('del')) : null
		]
		.filter(function(v){ return !!v; })
		);
	}
	// For TfD holding cell results
	if ( config.xfd.type === 'tfd' ) {
		var doHcMerge = self.inputData.inPageActions('merge');
		var doHcDelete = self.inputData.inPageActions('del') && self.inputData.useHoldingCell;
		if ( doHcMerge && doHcDelete ) {
			// Both 'merge' and 'delete' (via holding cell) results 
			Array.prototype.push.apply(self.tasks, [
				new Task('addBeingDeleted', self.discussion,
					self.inputData.getPages('merge', 'del')),
				new Task('addToHoldingCell', self.discussion,
					self.inputData.getPages('merge', 'del')),
				( self.inputData.dontdeletetalk ) ? null : new Task('tagTalkWithSpeedy',
					self.discussion, self.inputData.getPages('merge', 'del'))
			]
			.filter(function(v){ return !!v; })
			);			
		} else if ( doHcMerge ) {
			// Only 'merge' (via holding cell) results
			Array.prototype.push.apply(self.tasks, [
				new Task('addBeingDeleted', self.discussion, self.inputData.getPages('merge')),
				new Task('addToHoldingCell', self.discussion, self.inputData.getPages('merge'))
			]
			);
		} else if ( doHcDelete ) {
			// Only 'delete' (via holding cell) results
			Array.prototype.push.apply(self.tasks, [
				new Task('addBeingDeleted', self.discussion, self.inputData.getPages('del')),
				new Task('addToHoldingCell', self.discussion, self.inputData.getPages('del')),
				( self.inputData.dontdeletetalk ) ? null : new Task('tagTalkWithSpeedy',
					self.discussion, self.inputData.getPages('del'))
			]
			.filter(function(v){ return !!v; })
			);	
		}
	}
	
	self.initialiseFinally();
};
	
TaskManager.prototype.initialiseForClose = function() {
	var self = this;
	
	// Set up for close
	self.tasks.push(new Task('closeDisc', self.discussion));
	
	var addHoldcellTasks = function(){
		Array.prototype.push.apply(self.tasks, [
			new Task('addBeingDeleted', self.discussion),
			new Task('addToHoldingCell', self.discussion),
			( self.inputData.dontdeletetalk ) ? null : new Task('tagTalkWithSpeedy',
				self.discussion)
		]
		.filter(function(v){ return !!v; })
		);		
	};
	
	
	switch ( self.inputData.after ) {
		case 'doKeepActions':
			Array.prototype.push.apply(self.tasks, [
				new Task('addOldXfd', self.discussion),
				new Task('removeNomTemplates', self.discussion)
			]);
			break;
		case 'doRedirectActions':
			Array.prototype.push.apply(self.tasks, [
				new Task('addOldXfd', self.discussion),
				new Task('redirect', self.discussion),
				( self.inputData.result === 'soft redirect' ) ? null : new Task(
					'removeCircularLinks', self.discussion)
			]
			.filter(function(v){ return !!v; })
			);
			break;			
		case 'doMergeActions':
			if ( config.xfd.type === 'tfd' ) {
				addHoldcellTasks();
			} else {
				Array.prototype.push.apply(self.tasks, [
					new Task('addOldXfd', self.discussion),
					new Task('addMergeTemplates', self.discussion)
				]);
			}
			break;
		case 'doDisambigActions':
			Array.prototype.push.apply(self.tasks, [
				new Task('addOldXfd', self.discussion),
				new Task('disambiguate', self.discussion)
			]);
			break;
		case 'doDeleteActions':
			Array.prototype.push.apply(self.tasks, [
				new Task('peformDeletion', self.discussion),
				( self.inputData.dontdeletetalk ) ? null : new Task('deleteTalk',
					self.discussion),
				( self.inputData.deleteredir ) ? new Task('deleteRedirects',
					self.discussion) : null,
				( self.inputData.unlinkbackl ) ? new Task('unlinkBacklinks',
					self.discussion) : null
				
			]
			.filter(function(v){ return !!v; })
			);
			break;
		case 'holdingCell':
			addHoldcellTasks();
			break;
	}
	
	self.initialiseFinally();
	
};

// After initialising for relist/close, set up deferred objects and start initial task
TaskManager.prototype.initialiseFinally = function() {
	var self = this;
	
	// Deferred objects
	self.dfd.initialTask = $.Deferred().done(function() {
		// When initial task is done, start others (if any)
		if ( self.tasks.length > 1 ) {
			$.each(self.tasks.slice(1), function(_i, t) {
				if ( t.start ) {
					t.start();	// TODO: set task status, errors, and warning here, from the returned promise
				} else { // TODO: Investigate what the point of this is.
					self.tasks[_i+1].start();
				}
			});
		}
	});
	
	if ( self.getTaskByName('unlinkBacklinks') ) {
		// When unlinkBacklinks query is completed, it is okay to delete redirects
		self.dfd.ublQuery = $.Deferred();
	} else if ( self.getTaskByName('deleteRedirects') ) {
		self.dfd.ublQuery = $.Deferred().resolve();
	}
	
	// Start initial task
	if ( self.afdLogEditIndex ) {
		$.when( config.track.afdLogEdit[self.afdLogEditIndex-1] )
		.then( function() { 
			self.discussion.setStatus('Relisting...');
			self.tasks[0].start();
			} );
	} else {
		self.tasks[0].start();
	}
};

TaskManager.prototype.start = function() {
	var self = this;

	// Initialise
	var doAfterSanityCheck = function(action) {
		if ( action !== 'proceed' ) {
			// Reset discussion links
			self.discussion.showLinks();
			return;
		}
		// Resolve redirects, then initialise
		if ( !self.inputData.result ) {
			self.resolveRedirects(self.initialiseForRelist);
		} else if ( self.inputData.multimode ) {
			self.resolveRedirects(self.initialiseForMultimodeClose);
		} else {
			self.resolveRedirects(self.initialiseForClose);
		}
	};
	
	// Sanity checks
	var sanityCheckWarnings = self.makeSanityCheckWarnings();
	if ( sanityCheckWarnings ) {
		// Check with user before preceding
		extraJs.multiButtonConfirm('Warning', sanityCheckWarnings, [
				{ label: 'Cancel', flags: 'safe' },
				{ label: 'Continue', action: 'proceed', flags: 'progressive' }
			], doAfterSanityCheck, {'verbose': true, 'unescape': true, 'wikilinks':true} );		
	} else {
		// No need to check before preceding
		doAfterSanityCheck('proceed');
	}	
};

TaskManager.prototype.getTaskByName = function(name) {
	var self = this;
	for ( var i=0; i<self.tasks.length; i++ ) {
		if ( self.tasks[i].name === name ) {
			return self.tasks[i];
		}
	}
	return false;
};

TaskManager.prototype.makeTaskNote = function(task) {
	$notice = $('<p>')
	.addClass('xfdc-task-' + task.status)
	.addClass(task.name)
	.append(
		$('<span>').append(task.description),
		': ',
		$('<strong>').append(task.getStatusText()),
		$('<span>').append(task.errors),
		$('<span>').append(task.warnings)
	);
	return $notice;
};

TaskManager.prototype.updateTaskNotices = function(task, skipFinishedCheck) {
	var self = this;
	var $notices = self.discussion.get$notices();
	if ( task && $notices.find('.'+task.name).length ) {
		// Update specified task
		var $taskNotice = self.discussion.get$notices().find('.'+task.name);
		$taskNotice.after(self.makeTaskNote(task)).remove();
	} else {
		// Update all tasks
		self.discussion.setNotices(
			self.tasks.map(function(t) { return self.makeTaskNote(t); })
		);
	}
	var allFin = self.tasks.every(function(t) {
			return t.isFinished();
	});
	
	// If every task is finished
	if ( !skipFinishedCheck && allFin ) {
		if ( self.afdLogEditIndex ) {
			config.track.afdLogEdit[self.afdLogEditIndex].resolve();
		}
		self.discussion.setFinished();
	}
};

TaskManager.prototype.abortTasks = function(reason) {
	var self = this;
	
	$.each(self.tasks, function(_i, t) {
		t.status = 'aborted';
	});
	self.updateTaskNotices(null, true);
	if ( self.afdLogEditIndex ) {
		config.track.afdLogEdit[self.afdLogEditIndex].resolve();
	}
	self.discussion.setFinished(reason);
};

/* ========== Task class =========================================================================
   Tasks represent the action or series of actions the script will peform for
   the closer. Each task object also has a description, status, and (if any)
   error or warning messages, which the TaskManager uses to display the task
   notice on the page.   
   ---------------------------------------------------------------------------------------------- */
// Constructor
var Task = function(taskname, discussion, pages) {
	this.discussion = discussion;
	this.inputData = discussion.taskManager.inputData;
	this.name = taskname;
	
	this.status = 'waiting';
	this.errors = [];
	this.warnings = [];
	this.tracking = {};
	
	if ( pages != null ) {
		this.pages = pages;
	} else {
		this.pages = null;
	}

	var plural = ( this.pages ) ? this.pages.length > 1 : this.discussion.pages.length > 1;
	
	switch ( taskname ) {
		case 'closeDisc':
			this.description = 'Closing discussion';
			break;
		case 'addOldXfd':
			this.description = 'Updating talk page' + (( plural ) ? 's' : '');
			break;
		case 'removeNomTemplates':
		case 'addMergeTemplates':
		case 'disambiguate':
			this.description = 'Updating page' + (( plural ) ? 's' : '');
			break;
		case 'peformDeletion':
			this.description = 'Deleting page' + (( plural ) ? 's' : '');
			break;
		case 'addBeingDeleted':
			this.description = 'Updating template' + (( plural ) ? 's' : '');
			break;
		case 'addToHoldingCell':
			this.description = 'Listing at holding cell';
			break;
		case 'deleteTalk':
			this.description = 'Deleting talk page' + (( plural ) ? 's' : '');
			break;
		case 'tagTalkWithSpeedy':
			this.description = 'Tagging talk page' + (( plural ) ? 's' : '') +
			' for speedy deletion';
			break;
		case 'deleteRedirects':
			this.description = 'Deleting redirects';
			break;
		case 'unlinkBacklinks':
			this.description = 'Unlinking backlinks';
			break;
		case 'redirect':
			if ( discussion.dialog.inputData.deleteFirst ) {
				this.description = 'Deleting page' + (( plural ) ? 's' : '') +
				' and replacing with redirect' + (( plural ) ? 's' : '');
			} else if ( config.xfd.type === 'rfd' ) {
				this.description = 'Retargeting redirect' + (( plural ) ? 's' : '');
			} else {
				this.description = 'Making page' + (( plural ) ? 's' : '') +
				' into redirect' + (( plural ) ? 's' : '');
			}
			break;
		case 'removeCircularLinks':
			this.description = 'Unlinking circular links on redirect target';
			break;
		case 'getRelistInfo':
			this.description = 'Preparing to relist';
			break;
		case 'updateDiscussion':
			this.description = 'Updating discussion';
			break;
		case 'updateOldLogPage':
			this.description = 'Removing from old log page';
			break;
		case 'updateNewLogPage':
			this.description = 'Adding to today\'s log page';
			break;
		case 'updateNomTemplates':
			this.description = 'Updating link in nomination template' +
				(( plural ) ? 's' : '');
			break;
		default:
			this.description = taskname;
			break;
	}
};

// ---------- Task prototype -------------------------------------------------------------------- */
// Notice-related functions
Task.prototype.setDescription = function(d) {
	var self = this;
	this.description = d;
	this.discussion.taskManager.updateTaskNotices(self);
};
Task.prototype.setStatus = function(s) {
	var self = this;
	this.status = s;
	this.discussion.taskManager.updateTaskNotices(self);
};
Task.prototype.getStatusText = function() {
	var self = this;
	switch ( self.status ) {
		// Not yet started:
		case 'waiting':
			return 'Waiting...';
		// In progress:
		case 'started':
			return $('<img>').attr({
				'src':'//upload.wikimedia.org/wikipedia/commons/thumb/f/f8/Ajax-loader%282%29.gif/'+
					'40px-Ajax-loader%282%29.gif',
				'width':'20',
				'height':'5'
			});
		// Finished:
		case 'done':
			if (
				self.discussion.taskManager.dfd.initialTask.state() === 'pending' &&
				self.name === self.discussion.taskManager.tasks[0].name
			) {
				self.discussion.taskManager.dfd.initialTask.resolve();
			}
			return 'Done!';
		case 'aborted':
		case 'failed':
		case 'skipped':
			return extraJs.toSentenceCase(self.status) + '.';
		default:
			// unknown
			return '';
	}
};
Task.prototype.isFinished = function() {
	return $.inArray(this.status, ['done', 'aborted', 'failed', 'skipped']) !== -1;
};
Task.prototype.addError = function(e, critical) {
	var self = this;
	this.errors.push($('<span>').addClass('xfdc-notice-error').append(e));
	if ( critical ) {
		this.status = 'failed';
	}
	this.discussion.taskManager.updateTaskNotices(self);
};
Task.prototype.addWarning = function(w) {
	var self = this;
	this.warnings.push($('<span>').addClass('xfdc-notice-warning').append(w));
	this.discussion.taskManager.updateTaskNotices(self);
};
Task.prototype.addApiError = function(code, jqxhr, explanation, critical) {
	var self = this;
	self.addError([
		makeErrorMsg(code, jqxhr),
		' – ',
		$('<span>').append(explanation)
	], !!critical);
};
Task.prototype.setupTracking = function(key, total, allDoneCallback, allSkippedCallback) {
	var self = this;
	if ( allDoneCallback == null && allSkippedCallback == null ) {
		allDoneCallback = function() { this.setStatus('done'); };
		allSkippedCallback = function() { this.setStatus('skipped'); };
	}
	this.tracking[key] = {
		success: 0,
		skipped: 0,
		total: total,
		dfd: $.Deferred()
			.done($.proxy(allDoneCallback, self))
			.fail($.proxy(allSkippedCallback, self))
	};
};
Task.prototype.track = function(key, success) {
	if ( success ) {
		this.tracking[key].success++;
	} else {
		this.tracking[key].skipped++;
	}
	if ( this.tracking[key].skipped === this.tracking[key].total ) {
		this.tracking[key].dfd.reject();
	} else if ( this.tracking[key].success + this.tracking[key].skipped === this.tracking[key].total ) {
		this.tracking[key].dfd.resolve();
	}
};


Task.prototype.start = function() {
	return this.doTask[this.name](this);
};

// Code to actually do the tasks
Task.prototype.doTask = {};
// --- Closing tasks ---
// Close discussion
Task.prototype.doTask.closeDisc = function(self) {
	
	// Notify task is started -- current (to-be-deprecated) method 
	self.setStatus('started');
	
	// Get nomination page content and remove {Closing} etc templates if present
	return API.get( {
		action: 'query',
		titles: self.discussion.nomPage,
		prop: 'revisions',
		rvprop: 'content|timestamp',
		rvsection: self.discussion.sectionNumber,
		indexpageids: 1
	} )
	.then( function(response) {
		var id = response.query.pageids;
		return response.query.pages[ id ].revisions[ 0 ];
	} )
	.then( function(revision) {
		var contents = revision['*'];
		var lastEditTime = revision.timestamp;
		
		if ( contents.includes(config.xfd.wikitext.alreadyClosed) ) {
			return $.Deferred().reject('aborted', null, 'Discussion already closed (reload page to see '+
				'the actual close)');
		}

		var section_heading = contents.slice(0, contents.indexOf('\n'));
		
		var decodeHtml = function(t) {
			return $('<div>').html(t).text();
		};
		
		var plain_section_heading = decodeHtml(	section_heading
			.replace(/(?:^\s*=*\s*|\s*=*\s*$)/g, '') // remove heading markup
			.replace(/\[\[\:?(?:[^\]]+\|)?([^\]]+)\]\]/g, '$1') // replace link markup with link text
			.replace(/{{\s*[Tt]l[a-z]?\s*\|\s*([^}]+)}}/g, '{{$1}}') // replace tl templates
			.replace(/s*}}/, '}}') // remove any extra spaces after replacing tl templates
			.replace(/\s{2,}/g, ' ') // collapse multiple spaces into a single space
			.trim()
		);
		
		var isCorrectSection = plain_section_heading === self.discussion.sectionHeader;
		if ( !isCorrectSection ) {
			return $.Deferred().reject('aborted: possible edit conflict (found section heading `' +
			plain_section_heading + '`)');
		}

		var xfd_close_top = config.xfd.wikitext.closeTop
			.replace(/__RESULT__/, self.inputData.getResult() || '&thinsp;')
			.replace(/__TO_TARGET__/, ( self.inputData.getTarget() ) ? ' to ' +
				self.inputData.getTargetLink() : '')
			.replace(/__RATIONALE__/, ( self.inputData.getRationale() || '.'))
			.replace(/__SIG__/, config.user.sig);
		
		var section_contents = contents.slice(contents.indexOf('\n')+1)
			.replace(
				/({{closing}}|{{AfDh}}|{{AfDb}}|\{\{REMOVE THIS TEMPLATE WHEN CLOSING THIS AfD\|.?\}\}|<noinclude>\[\[Category:Relisted AfD debates\|.*?\]\]<\/noinclude>)/gi,
				'');
		var updated_top = ( config.xfd.type === 'afd' || config.xfd.type === 'mfd' ) ?
			xfd_close_top + '\n' + section_heading :
			section_heading + '\n' + xfd_close_top;
		var updated_section = updated_top + '\n' + section_contents.trim() + '\n' + config.xfd.wikitext.closeBottom;
		
		return $.Deferred().resolve(updated_section, lastEditTime);
	} )
	.then( function(updated_section, lastEditTime) {
		return API.postWithToken( 'csrf', {
			action: 'edit',
			title: self.discussion.nomPage,
			section: self.discussion.sectionNumber,
			text: updated_section,
			summary: '/* ' + self.discussion.sectionHeader + ' */ Closed as ' +
				self.inputData.getResult() + config.script.advert,
			basetimestamp: lastEditTime
		} );
	} )
	.then(
		function() {
			self.setStatus('done'); // Current (to-be-deprecated) method of setting status
			return 'done'; // Future method of setting status
		},
		function(code, jqxhr, abortReason) {
			var message = [
				'Could not edit page ',
				extraJs.makeLink(self.discussion.nomPage),
				'; could not close discussion'
			];
			self.addApiError(code, jqxhr, message);
			var abortMessage = ( code.indexOf("aborted") === 0 && abortReason ) || '';
			self.discussion.taskManager.abortTasks(abortMessage);
			return $.Deferred().reject(code, jqxhr, message);
		}
	);

};

// Add old xfd template to talk pages
Task.prototype.doTask.addOldXfd = function(self) {

	// Notify task is started
	self.setStatus('started');
	
	// Function to make an edit with the api.
	// Mode can be 'text' (overwrite existing content) or 'appendtext' or 'prependtext'
	var apiEditTalk = function (pageTitle, newWikitext, mode, redirect) {
		var req = {
			action: 'edit',
			title: pageTitle,
			section: '0',
			summary: 'Old ' + config.xfd.type.toUpperCase() + ' – ' + self.discussion.nomDate +
				': ' + self.inputData.getResult() + config.script.advert,
			redirect: !!redirect
		};
		req[mode] = ( mode === 'appendtext' ) ? '\n' + newWikitext : newWikitext;
		API.postWithToken( 'csrf', req )
		.done( function() {
			self.track('processed', true);
		})
		.fail( function(code, jqxhr) {
			self.track('processed', false);
			self.addApiError(code, jqxhr, [
				'Could not edit talk page ',
				extraJs.makeLink(pageTitle)
				]);
		} ); 
	};

	// Make wikitext of olf xfd template (non-AFD)
	var makeOldxfdWikitext = function(altpage) {
		result = config.xfd.wikitext.oldXfd
			.replace(/__DATE__/, self.discussion.nomDate)
			.replace(/__SECTION__/, self.discussion.sectionHeader)
			.replace(/__RESULT__/, self.inputData.getResult())
			.replace(/__FIRSTDATE__/, self.discussion.firstDate)
			.replace(/__SUBPAGE__/, self.discussion.getNomSubpage());
		if ( altpage ) {
			result = result.replace('}}', ' |altpage='+altpage+'}}');
		}
		return result;
	};
	
	// Add or update oldafdmulti template in section wikitext
	var makeNewWikitext = function(wikitext, pageTitle) {
		//Parts of this derived from https://en.wikipedia.org/wiki/User:Mr.Z-man/closeAFD2.js
		var titleObject = mw.Title.newFromText(pageTitle);
		var PAGENAME = titleObject.getMain();
		var SUBJECTPAGENAME = config.mw.monthNames[(titleObject.getNamespaceId()-1).toString()] +
			PAGENAME; 
		var oldafdmulti = '{{Old AfD multi';
		var numtest = /[A-z]+([0-9]+)/i;
		var count = 0;
		var i = 0;

		var makeTemplateRegexPatt = function(templateNameRegexString, flags, prevLine) {
			return new RegExp((( prevLine ) ? '\\n?' : '') +
			// start template
			'\\{\\{' + templateNameRegexString + '\\s*' +
			// start capturing group
			"(\\|" +
				// account for sub-templates within the template
				"(?:.|\\n)*?(?:(?:\\{\\{" +			
					// account for sub-sub-templates within sub-templates
					"(?:.|\\n)*?(?:(?:\\{\\{" +
						"(?:.|\\n)*?" +
					"\\}\\})(?:.|\\n)*?)*?" +
				"\\}\\})(?:.|\\n)*?)*" +
			// end capturing group, end template
			")\\}\\}\\n?", flags);
		};
		
		var makeParams = function(pattern) {
			var params = {};
			// Pattern to find `|param=value` or `|value`, where `value` can only contain a pipe
			// if within square brackets (i.e. wikilinks) or braces (i.e. templates)
			var partsPatt = /\|(?!(?:[^{]+}|[^\[]+]))(?:.|\s)*?(?=(?:\||$)(?!(?:[^{]+}|[^\[]+])))/g;
			// Execute pattern to obtain capturing group match (at index 1 of the resulting array)
			var old = pattern.exec(wikitext);
			if ( old ) {
				var unnamedParamCount = 0;
				var parts = old[1].match(partsPatt);
				for ( var i=0; i<parts.length; i++ ) {
					if ( parts[i].trim() === '|' ) {
						continue;
					}
					var equalsIndex = parts[i].indexOf('=');
					if ( equalsIndex === -1 ) {
						//unnamed parameter
						unnamedParamCount++;
						params[unnamedParamCount.toString()] = parts[i].slice(1).trim();
					} else {
						params[parts[i].slice(1, equalsIndex).trim()] = parts[i]
							.slice(equalsIndex+1).trim();
					}
				}
			}
			return params;
		};
		
		// Find old AFDs
		var oldAfdPatt = makeTemplateRegexPatt('(?:old|afd) ?(?:old|afd) ?(?:multi|full)?', 'i');
		var oldAfdParams = makeParams(oldAfdPatt);
		for (var p in oldAfdParams) {
			oldafdmulti += ' |' + p + '=' + oldAfdParams[p];
			var num = numtest.exec(p);
			var res = ( num ) ? parseInt(num[1]) : 1;
			if (res > count) {
				count = res;
			}
		}
		
		// Find old TFDs
		var oldTfdPatt = makeTemplateRegexPatt('(?:old|tfd|Previous) ?(?:tfd|tfd|end)(?:full)?', 'gi');
		var oldTfdTemplates = wikitext.match(oldTfdPatt);
		if ( oldTfdTemplates ) {
			for (i=0; i<oldTfdTemplates.length; i++) {
				count++;
				var oldTfdParams = makeParams(oldTfdPatt);
				var oldTfdDate = oldTfdParams.date;
				var oldTfdResult = oldTfdParams.result || 'keep';
				var oldTfdLink = "{{subst:#ifexist:Wikipedia:Templates for deletion/Log/"+
					( oldTfdParams.link || "{{subst:Date|"+oldTfdParams.date+"|ymd}}" )+
					"|Wikipedia:Templates for deletion/Log/" +
					( oldTfdParams.link || "{{subst:Date|"+oldTfdParams.date+"|ymd}}" )+
					"#" + ( oldTfdParams['1'] || oldTfdParams.disc || "Template:"+PAGENAME )+
					"|Wikipedia:Templates for discussion/Log/"+
					( oldTfdParams.link || "{{subst:Date|"+oldTfdParams.date+"|ymd}}" )+
					"#" + ( oldTfdParams['1'] || oldTfdParams.disc || "Template:"+PAGENAME )+
					"}}";
				oldafdmulti += " |date" + count + "=" + oldTfdDate +
				" |result" + count + "='''" + extraJs.toSentenceCase(oldTfdResult) + "'''" +
				" |link" + count + "={{canonicalurl:" + oldTfdLink + "}}";
				wikitext = wikitext.replace(oldTfdTemplates[i], '');
			}
		}
		
		// Find old FFDs
		var oldFfdPatt = makeTemplateRegexPatt('old ?(?:f|i)fd(?:full)?', 'gi');
		var oldFfdTemplates = wikitext.match(oldFfdPatt);			
		if ( oldFfdTemplates ) {
			for (i=0; i<oldFfdTemplates.length; i++) {
				count++;
				var oldFfdParams = makeParams(oldFfdPatt);
				var oldFfdDate = oldFfdParams.date || '';
				var oldFfdResult = oldFfdParams.result || 'keep';
				var oldFfdLink = "{{subst:#ifexist:Wikipedia:Images and media for deletion/"+
					"{{subst:#iferror:{{subst:#time:|"+oldFfdParams.date+"}}|"+oldFfdParams.date+
					"|{{subst:#time:Y F j|"+oldFfdParams.date+"}}}}"+
					"|Wikipedia:Images and media for deletion/{{subst:#iferror:{{subst:#time:|"+
					oldFfdParams.date+"}}|"+oldFfdParams.date+"|{{#time:Y F j|"+oldFfdParams.date+
					"}}}}#File:" + ( oldFfdParams.page || PAGENAME ) +
					"|{{subst:#ifexist:Wikipedia:Files for deletion/{{subst:#iferror:{{subst:#time:|"+
					oldFfdParams.date+"}}|"+oldFfdParams.date+"|{{subst:#time:Y F j|"+
					oldFfdParams.date+"}}}}"+
					"|Wikipedia:Files for deletion/{{subst:#iferror:{{subst:#time:|"+oldFfdParams.date+"}}|"+
					oldFfdParams.date+"|{{subst:#time:Y F j|"+oldFfdParams.date+"}}}}#" +
					( oldFfdParams.page || SUBJECTPAGENAME )+
					"|Wikipedia:Files for discussion/{{subst:#iferror:{{subst:#time:|"+oldFfdParams.date+"}}|"+
					oldFfdParams.date+"|{{subst:#time:Y F j|"+oldFfdParams.date+"}}}}#" +
					( oldFfdParams.page || SUBJECTPAGENAME )+
					"}}}}";
				oldafdmulti += " |date" + count + "=" + oldFfdDate +
				" |result" + count + "='''" +
				extraJs.toSentenceCase(oldFfdResult.replace(/'''/g, '')) + "'''" +
				" |link" + count + "={{canonicalurl:" + oldFfdLink + "}}";
				wikitext = wikitext.replace(oldFfdTemplates[i], '');
			}
		}
		
		// Find old MFDs
		var oldMfdPatt = makeTemplateRegexPatt('(?:old ?mfd|mfdend|mfdold)(?:full)?', 'gi');
		var oldMfdTemplates = wikitext.match(oldMfdPatt);			
		if ( oldMfdTemplates ) {
			for (i=0; i<oldMfdTemplates.length; i++) {
				count++;
				var oldMfdParams = makeParams(oldMfdPatt);
				var oldMfdDate = oldMfdParams.date || '';
				var oldMfdResult = oldMfdParams.result || 'keep';
				var oldMfdLink = "Wikipedia:Miscellany for deletion/" +
					( oldMfdParams.votepage || oldMfdParams.title ||
					oldMfdParams.page || SUBJECTPAGENAME );
				oldafdmulti += " |date" + count + "=" + oldMfdDate +
				" |result" + count + "='''" +
				extraJs.toSentenceCase(oldMfdResult.replace(/'''/g, '')) + "'''" +
				" |link" + count + "={{canonicalurl:" + oldMfdLink + "}}";
				wikitext = wikitext.replace(oldMfdTemplates[i], '');
			}
		}				

		// Find old RFDs
		var oldRfdPatt = makeTemplateRegexPatt('old?(?: |-)?rfd(?:full)?', 'gi');
		var oldRfdTemplates = wikitext.match(oldRfdPatt);			
		if ( oldRfdTemplates ) {
			for (i=0; i<oldRfdTemplates.length; i++) {
				count++;
				var oldRfdParams = makeParams(oldRfdPatt);
				var oldRfdDate = oldRfdParams.date || '';
				var oldRfdResult = oldRfdParams.result || 'keep';
				var oldRfdLink;
				if ( oldRfdParams.rawlink ) {
					oldRfdLink = oldRfdParams.rawlink.slice(2, oldRfdParams.rawlink.indexOf('|'));
				} else {
					oldRfdLink = "Wikipedia:Redirects for discussion/Log/" +
					( oldRfdParams.page || oldRfdParams.date + "#" + SUBJECTPAGENAME );
				}
				oldafdmulti += " |date" + count + "=" + oldRfdDate +
				" |result" + count + "='''" +
				extraJs.toSentenceCase(oldRfdResult.replace(/'''/g, '')) + "'''" +
				" |link" + count + "={{canonicalurl:" + oldRfdLink + "}}";
				wikitext = wikitext.replace(oldRfdTemplates[i], '');
			}
		}	

		// For non-AFDs, if no old banners were found, prepend process-specific banner to content
		if ( config.xfd.type !== 'afd' && count === 0 ) {
			return config.xfd.wikitext.oldXfd
				.replace(/__DATE__/, self.discussion.nomDate)
				.replace(/__SECTION__/, self.discussion.sectionHeader)
				.replace(/__RESULT__/, self.inputData.getResult())
				.replace(/__FIRSTDATE__/, self.discussion.firstDate)
				.replace(/__SUBPAGE__/, self.discussion.getNomSubpage()) + wikitext;
		}
		
		// Otherwise, add current discussion to oldafdmulti
		count++;
		var c = count.toString();
		var currentResult = self.inputData.getResult();
		if (count === 1) {
			c = '';
		} else {
			currentResult = extraJs.toSentenceCase(currentResult);
		}
		var currentNompageParamAndValue = ( config.xfd.type === 'afd' ) ? ' |page'+c + '=' +
			self.discussion.getNomSubpage() : ' |link'+c + '='+
			'{{canonicalurl:' + self.discussion.getNomPageLink() + '}}';
			
		oldafdmulti += ' |date'+c + '=' + self.discussion.nomDate +
			' |result'+c+ "='''" + currentResult + "'''" + currentNompageParamAndValue + '}}';
		if ( oldAfdPatt.test(wikitext) ) {
			// Override the existing oldafdmulti
			newtext = wikitext.replace(oldAfdPatt, oldafdmulti+'\n');
		} else {
			// Prepend to content ([[WP:Talk page layout]] is too complicated to automate)
			newtext = oldafdmulti+'\n'+wikitext;
		}
		return newtext;
	};
	
	var confirmRedirectReplacement = function(talkTitle) {
		OO.ui.confirm('"' + talkTitle + '" is currently a redirect. Okay ' +
		'to replace with Old RFD template?')
		.done( function ( confirmed ) {
			if ( confirmed ) {
				// Make the edit, replacing previous content
				apiEditTalk(talkTitle, makeOldxfdWikitext(), 'text');
			} else {
				// Skip
				self.addWarning([
					extraJs.makeLink(talkTitle),
					' skipped'
				]);
				self.track('processed', true);
			}
		} );
	};
	
	var processTalkTitles = function (result) {
		var page_ids = result.query.pageids;
		for ( var i=0; i < page_ids.length; i++ ) {
			var talkTitle = result.query.pages[ page_ids[i] ].title;
			// Check there's a corresponding nominated page
			var pageObj = self.discussion.getPageByTalkTitle(talkTitle);
			if ( !pageObj ) {
				self.addError([
					'API query result included unexpected talk page title ',
					extraJs.makeLink(talkTitle),
					'; this talk page will not be edited'
				]);
				self.track('processed', false);
				continue;
			}
			// Check corresponding page exists
			if ( !pageObj.exists ) {
				self.addError([
					'The subject page for ',
					extraJs.makeLink(talkTitle),
					' does not exist; this talk page will not be edited'
				]);
				self.track('processed', false);
				continue;
			}
			// Check redirect status
			if ( config.xfd.type !== 'afd' && result.query.pages[ page_ids[i] ].redirect === '' ) {
				// Is a redirect...
				if ( config.xfd.type === 'rfd' ) {
					// for RFD, ask what to do
					confirmRedirectReplacement(talkTitle);
				} else if ( config.xfd.type === 'mfd' ) {
					// For MFD, edit the redirect's target page, using the altpage parameter
					apiEditTalk(talkTitle, makeOldxfdWikitext(pageObj.getTitle()),
						'prependtext', true); 
				} else {
					// For other venues, append rather than prepend old xfd template
					apiEditTalk(talkTitle, makeOldxfdWikitext(), 'appendtext');
				}
			} else {
				// Not a redirect. Attempt to find and consolidate existing banners
				var oldwikitext = '';
				if ( parseInt(page_ids[i]) > 0 ) {
					oldwikitext = result.query.pages[ page_ids[i] ].revisions[0]['*'];
				}				
				apiEditTalk(talkTitle, makeNewWikitext(oldwikitext, talkTitle), 'text');
			}
		}
		
	};
		
	// Get talk pages
	var talkTitles = self.discussion.getTalkTitles(self.pages);
	if ( talkTitles.length === 0 ) {
		self.addWarning('none found');
		self.setStatus('done');
	} else {
		// get talk page redirect status from api
		self.setupTracking('processed', talkTitles.length);
		
		API.get( {
			action: 'query',
			prop: 'revisions|info',
			rvprop: 'content',
			rvsection: '0',
			titles: talkTitles,
			indexpageids: 1
		} )
		.done( processTalkTitles )
		.fail( function(code, jqxhr) {
			self.addApiError(code, jqxhr, 'Could not read contents of talk page' +
				( talkTitles.length > 1 ) ? 's' : '');
			self.setStatus('failed');
		} );
	}
	
};

Task.prototype.doTask.removeNomTemplates = function(self, mergeWikitext) {

	// Notify task is started
	self.setStatus('started');

	apiEditPage = function(pageTitle, updatedWikitext) {
		API.postWithToken( 'csrf', {
			action: 'edit',
			title: pageTitle,
			text: updatedWikitext,
			summary: config.xfd.type.toUpperCase() + ' closed as ' +
				self.inputData.getResult() + config.script.advert
		} )
		.done( function() {
			self.track('edit', true);
		} )
		.fail( function(code, jqxhr) {
			self.track('edit', false);
			self.addApiError(code, jqxhr, [
				'Could not edit page ',
				extraJs.makeLink(pageTitle)
			]);
		} );
	};
	
	var processPages = function (result) {
		//Get old wikitext, or make error if page doesn't exist, or if page is in wrong namespace
		var page_ids = result.query.pageids;
		for (var i = 0; i < page_ids.length; i++) {
			var pageTitle = result.query.pages[ page_ids[i] ].title;
			// Check there's a corresponding nominated page
			var pageObj = self.discussion.getPageByTitle(pageTitle, {'moduledocs': true});
			if ( !pageObj ) {
				self.addError([
					'API query result included unexpected title ',
					extraJs.makeLink(pageTitle),
					'; this page will not be edited'
				]);
				self.track('edit', false);
				continue;
			}
			// Check that page exists
			if ( parseInt(page_ids[i]) < 0 ) { 
				self.addError([
					extraJs.makeLink(pageTitle),
					' does not exist and will not be edited'
				]);
				self.track('edit', false);
				continue;
			}

			// Existing wikitext
			var oldWikitext = result.query.pages[ page_ids[i] ].revisions[0]['*'];
			
			// Start building updated wikitext
			var updatedWikitext = '';
			
			// For merging, unless the page is itself a merge target, prepend the mergeWikitext
			if (
				mergeWikitext != null &&
				$.inArray(pageTitle, self.inputData.getTargetsArray('merge')) === -1 
			) {
				updatedWikitext = mergeWikitext[pageTitle];
			}
			
			// Remove nom template if present, or warn if not found
			if ( config.xfd.hasNomTemplate(oldWikitext) ) {
				updatedWikitext += config.xfd.removeNomTemplate(oldWikitext);
			} else {
				self.addWarning([
					'Nomination template not found on page ',
					extraJs.makeLink(pageTitle)
				]);
				if ( updatedWikitext === '' ) {
					// Skip - nothing to change
					self.track('edit', false);
					continue;
				} else {
					updatedWikitext += oldWikitext;
				}
			}
			
			// Make the edit
			apiEditPage(pageTitle, updatedWikitext);
		}
	};

	//get each page's wikitext through api
	var pageTitles = self.discussion.getPageTitles(self.pages, {'moduledocs': true});
	if ( pageTitles.length === 0 ) {
		self.addWarning('none found');
		self.setStatus('failed');
		return;
	}
	
	self.setupTracking(
		'edit',
		pageTitles.length,
		( mergeWikitext ) ? function() { this.track('alldone', true); } : null,
		( mergeWikitext ) ? function() { this.track('alldone', false); } : null
	);

	API.get( {
		action: 'query',
		titles: pageTitles.join('|'),
		prop: 'revisions',
		rvprop: 'content',
		indexpageids: 1,
		rawcontinue: ''
	} )
	.done( processPages )
	.fail( function(code, jqxhr) {
		self.addApiError(code, jqxhr, 'Could not read contents of nominated page' +
			( pageTitles.length > 1 ) ? 's' : '');
		self.setStatus('failed');
	} );
};

Task.prototype.doTask.addMergeTemplates = function(self) {

	// Notify task is started
	self.setStatus('started');

	// Edit function
	editTargetTalk = function(pageTitle, prependWikitext) {
		API.postWithToken( 'csrf', {
			action: 'edit',
			title: pageTitle,
			prependtext: prependWikitext,
			summary: '[[:' + self.discussion.getNomPageLink() + ']] closed as ' +
				self.inputData.getResult() + config.script.advert
		} )
		.done( function() {
			self.track('editTargetTalk', true);
		})
		.fail( function(code, jqxhr) {
			self.track('editTargetTalk', false);
			self.addApiError(code, jqxhr, [
				'Could not edit page ',
				extraJs.makeLink(pageTitle)
			]);
		} );
	};
	
	// Get targets and their talk pages
	var targets = extraJs.uniqueArray(self.inputData.getTargetsArray('merge'));
	
	self.setupTracking('alldone', 2);
	self.setupTracking(
		'editTargetTalk',
		targets.length,
		function() { this.track('alldone', true); },
		function() { this.track('alldone', false); }
	);
	
	// Strings for merge templates
	var debate = self.discussion.getNomSubpage();
	var today = new Date();
	var curdate = today.getUTCDate().toString() + ' ' + config.mw.monthNames[today.getUTCMonth()] + ' ' +
		today.getUTCFullYear().toString();
	
	// Object to hold the 'merge to' template for each page
	var mergetoWikitext = {};
	
	// For each target...
	for ( var i=0; i<targets.length; i++ ) {
		// Pages to be merged to target
		var mergeTitles;
		if ( self.inputData.multimode ) {
			mergeTitles = self.inputData.getPagesToBeMerged(targets[i]);
		} else {
			mergeTitles = self.discussion.getPageTitles(self.pages);
		}

		// Make 'merge to' template for pages to be merged
		var mergetoTemplate = config.xfd.wikitext.mergeTo
			.replace(/__TARGET__/, targets[i])
			.replace(/__DEBATE__/, debate)
			.replace(/__DATE__/, curdate)		
			.replace(/__TARGETTALK__/, Page.newFromText(targets[i]).getTalk());

		// Make 'merge from' template for the target's talk page
		var mergefromTemplates = [];
		for (var ii=0; ii<mergeTitles.length; ii++) {
			mergefromTemplates.push(
				config.xfd.wikitext.mergeFrom
				.replace(/__NOMINATED__/, mergeTitles[ii])
				.replace(/__DEBATE__/, debate)
				.replace(/__DATE__/, curdate)
			);
			// Add 'merge to' template to holding object
			mergetoWikitext[mergeTitles[ii]] = mergetoTemplate;
		}
		
		// Check if the target is one of the nominated pages
		if ( $.inArray(targets[i], self.discussion.getPageTitles(self.pages)) !== -1 ) {
			// No need to add 'merge from' template - this was a nominated page, and will have an 
			// old xfd template put on its talkpage specify the merge result.
			self.track('editTargetTalk', false);
			continue;
			
		} else {
			// Edit target talkpage
			editTargetTalk(Page.newFromText(targets[i]).getTalk(), mergefromTemplates.join(''));
		}
	}
	
	// Replace nomination templates with 'merge to' templates
	self.doTask.removeNomTemplates(self, mergetoWikitext);
	
};

Task.prototype.doTask.disambiguate = function(self) {

	// Notify task is started
	self.setStatus('started');

	apiEditPage = function(pageTitle, updatedWikitext) {
		API.postWithToken( 'csrf', {
			action: 'edit',
			title: pageTitle,
			text: updatedWikitext,
			summary: config.xfd.type.toUpperCase() + ' closed as ' +
				self.inputData.getResult() + config.script.advert
		} )
		.done( function() {
			self.track('edit', true);
		} )
		.fail( function(code, jqxhr) {
			self.track('edit', false);
			self.addApiError(code, jqxhr, [
				'Could not edit page ',
				extraJs.makeLink(pageTitle)
			]);
		} );
	};
	
	var processPages = function (result) {
		//Get old wikitext, or make error if page doesn't exist, or if page is in wrong namespace
		var page_ids = result.query.pageids;
		for (var i = 0; i < page_ids.length; i++) {
			var pageTitle = result.query.pages[ page_ids[i] ].title;
			// Check there's a corresponding nominated page
			var pageObj = self.discussion.getPageByTitle(pageTitle);
			if ( !pageObj ) {
				self.addError([
					'API query result included unexpected title ',
					extraJs.makeLink(talkTitle),
					'; this page will not be edited'
				]);
				self.track('edit', false);
				continue;
			}
			// Check that page exists
			if ( parseInt(page_ids[i]) < 0 ) { 
				self.addError([
					extraJs.makeLink(talkTitle),
					' does not exist and will not be edited'
				]);
				self.track('edit', false);
				continue;
			}

			var oldWikitext = result.query.pages[ page_ids[i] ].revisions[0]['*'];
			var updatedWikitext = '';
			
			if ( config.xfd.regex.fullNomTemplate.test(oldWikitext) ) {
				updatedWikitext = oldWikitext.replace(config.xfd.regex.fullNomTemplate, '').trim();
			} else {
				self.addWarning([
					'Nomination template not found on page ',
					extraJs.makeLink(pageTitle)
				]);
				updatedWikitext = oldWikitext.replace(/^#REDIRECT/mi, '*');
			}
			
			if ( !/(?:disambiguation|disambig|dab|Mil-unit-dis|Numberdis)[^{]*}}/i.test(updatedWikitext) ) {
				updatedWikitext += '\n{{Disambiguation cleanup|{{subst:DATE}}}}';
				updatedWikitext.trim();
			}

			apiEditPage(pageTitle, updatedWikitext);
		}
	};

	//get each page's wikitext through api
	var pageTitles = self.discussion.getPageTitles(self.pages);
	if ( pageTitles.length === 0 ) {
		self.addWarning('none found');
		self.setStatus('failed');
		return;
	}
	
	self.setupTracking('edit', pageTitles.length);

	API.get( {
		action: 'query',
		titles: pageTitles.join('|'),
		prop: 'revisions',
		rvprop: 'content',
		indexpageids: 1,
		rawcontinue: ''
	} )
	.done( processPages )
	.fail( function(code, jqxhr) {
		self.addApiError(code, jqxhr, 'Could not read contents of nominated page' +
			( pageTitles.length > 1 ) ? 's' : '');
		self.setStatus('failed');
	} );
};

Task.prototype.doTask.peformDeletion = function(self) {

	// Notify task is started
	self.setStatus('started');
	
	// Delete with the api
	apiDeletePage = function(pageTitle) {
		API.postWithToken( 'csrf', {
			action: 'delete',
			title: pageTitle,
			reason: '[[' + self.discussion.getNomPageLink() + ']]' + config.script.advert
		} )
		.done( function() {
			self.track('del', true);
		} )
		.fail( function(code, jqxhr) {
			self.track('del', false);
			self.addApiError(code, jqxhr, [
				'Could not delete page ',
				extraJs.makeLink(pageTitle)
			]);
		} );
	};
	
	var pages = self.pages || self.discussion.pages;
	self.setupTracking('del', pages.length);
	// For each page, check it exists, then delete with api or add warning
	for ( var i=0; i<pages.length; i++ ) {
		if ( pages[i].exists ) {
			apiDeletePage(pages[i].getTitle());
		} else {
			self.addWarning([
				extraJs.makeLink(pages[i].getTitle()),
				' skipped: does not exist (may have already been deleted by others)'
			]);
			self.track('del', false);
		}
	}
};

Task.prototype.doTask.addBeingDeleted = function(self) {

	// Notify task is started
	self.setStatus('started');

	// Merge targets and pages to be merged (if any)
	var mergeTargets = [];
	var mergeTitles = [];
	if ( self.inputData.inPageActions('merge') || self.inputData.result === 'merge' ) {
		mergeTargets = self.inputData.getTargetsArray('merge');
		mergeTitles = self.discussion.getPageTitles( 
			(self.inputData.multimode ) ? self.inputData.getPages('merge') : self.pages
		);
	}

	// Edit with the Api
	var apiEditTemplate = function(pageTitle, newWikitext, editsum) {
		API.postWithToken( 'csrf', {
			action: 'edit',
			title: pageTitle,
			text: newWikitext,
			summary: editsum
		} )
		.done( function() {
			self.track('edit', true);
		} )
		.fail( function(code, jqxhr) {
			self.track('edit', false);
			self.addApiError(code, jqxhr, [
				'Could not edit page ',
				extraJs.makeLink(pageTitle)
			]);
		} );
	};

	var processTemplates = function (result) {
		var page_ids = result.query.pageids;
		for (var i = 0; i < page_ids.length; i++) {
			var pageTitle = result.query.pages[ page_ids[i] ].title;
			// Check there's a corresponding nominated page
			var pageObj = self.discussion.getPageByTitle(pageTitle, {'moduledocs': true});
			if ( !pageObj ) {
				self.addError([
					'API query result included unexpected page ',
					extraJs.makeLink(pageTitle),
					'; this page will not be edited'
				]);
				self.track('edit', false);
				continue;
			}
			// Check that page exists
			if ( parseInt(page_ids[i]) < 0 ) { 
				self.addError([
					extraJs.makeLink(pageTitle),
					' does not exist (may have been deleted)'
				]);
				self.track('edit', false);
				continue;
			}
			
			// Replace {Template for discussion/dated} or {Tfm/dated} (which
			// may or may not be noincluded)
			var isModule = ( pageTitle.indexOf('Module:') === 0 );
			var inclusionTag = ( isModule ) ? 'includeonly' : 'noinclude';
			var oldWikitext = result.query.pages[ page_ids[i] ].revisions[0]['*'];
			var updatedWikitext = '';
			var editsum = '';
			if ( $.inArray(pageTitle, mergeTargets) !== -1 ) {
				// If this is a merge target, don't add anything - just remove nom template
				updatedWikitext = config.xfd.removeNomTemplate(oldWikitext);
				editsum = '[[' + self.discussion.getNomPageLink() + ']] closed as ' +
					self.inputData.getResult() + config.script.advert;
			} else if ( self.inputData.holdcell === 'ready' ) {
				// If holding cell section is 'ready for deletion', tag for speedy deletion
				updatedWikitext = '<' + inclusionTag + '>{{Db-xfd|fullvotepage=' +
					self.discussion.getNomPageLink() + '}}</' + inclusionTag + '>' +
					config.xfd.removeNomTemplate(oldWikitext);
				editsum = '[[WP:G6|G6]] Speedy deletion nomination, per [[' +
					self.discussion.getNomPageLink() + ']]' + config.script.advert;
			} else {
				// Add Being deleted template, remove nom template
				updatedWikitext = '<' + inclusionTag + '>{{Being deleted|' + self.discussion.nomDate + '|' +
					mw.util.wikiUrlencode(self.discussion.sectionHeader);
				if ( $.inArray(pageTitle, mergeTitles) !== -1 ) {
					// If being merged, set merge parameter
					updatedWikitext += '|merge=' + self.inputData.getTarget(pageTitle);
				}
				updatedWikitext += '}}</' + inclusionTag + '>' + config.xfd.removeNomTemplate(oldWikitext);
				editsum = 'Per [[' + self.discussion.getNomPageLink() + ']], '+
				'added {{being deleted}}' + config.script.advert;
			}

			// Make the edit
			apiEditTemplate(pageTitle, updatedWikitext, editsum);

		}
		
	};

	var templateTitles = self.discussion.getPageTitles(self.pages, {'moduledocs': true});

	self.setupTracking('edit', templateTitles.length);
	//get page wikitext through api
	API.get( {
		action: 'query',
		titles: templateTitles.join('|'),
		prop: 'revisions',
		rvprop: 'content',
		indexpageids: 1,
		rawcontinue: ''
	} )
	.done( processTemplates )
	.fail( function(code, jqxhr) {
		self.addApiError(code, jqxhr, 'Could not read contents of nominated page' +
		( templateTitles.length > 1 ) ? 's' : '');
		self.setStatus('failed');
	} );
	
};

Task.prototype.doTask.addToHoldingCell = function(self) {

	// Notify task is started
	self.setStatus('started');
	
	holdCellSections = [
		self.inputData.holdcell || null,
		self.inputData.mergeHoldcell || null
	]
	.filter(function(v){ return !!v; })
	.map(function(v){ return config.xfd.holdingCellSectionNumber[v]; });
	
	self.setupTracking('processed', holdCellSections.length);

	//Function to make an edit
	var apiEditHoldingCell = function(newWikitext, sectionNum) {
		API.postWithToken( 'csrf', {
			action: 'edit',
			title: config.xfd.subpagePath + 'Holding cell',
			section: sectionNum,
			text: newWikitext,
			summary: 'Listing template(s) per [[:' + self.discussion.getNomPageLink() + ']]' +
			config.script.advert
		} )
		.done( function() {
			self.track('processed', true);
		})
		.fail( function(code, jqxhr) {
			self.track('processed', false);
			self.addApiError(code, jqxhr, 'Could not add templates to holding cell');
		} ); 
	};
	
	var processHoldingCellPage = function (result) {
		var p_id = result.query.pageids;
		// Get page contents, make all headings level 3 so sections can be counted
		var p_contents = result.query.pages[ p_id ].revisions[0]['*'].replace(/====/gi, '===');
		var sectionsArray = p_contents.split('===');
		
		for ( var i=0; i<holdCellSections.length; i++) {
			var isMergeSection = (4 <= holdCellSections[i]) && ( holdCellSections[i] <= 10);
			
			var listingPages;
			if ( !self.inputData.multimode ) {
				listingPages = self.discussion.pages;
			} else if ( isMergeSection ) {
				listingPages = self.inputData.getPages('merge');
			} else {
				listingPages = self.inputData.getPages('del');
			}
			
			var tfdlTemplates = '';
			for (ii = 0; ii < listingPages.length; ii++) {
				//Check namespace and existance
				if ( !listingPages[ii].hasCorrectNamespace() ) {
					self.addError([
						extraJs.makeLink(listingPages[ii]),
						' is not in the ' + config.mw.namespaces[config.xfd.ns_number[0].toString()] +
						' namespace, and will not be listed at the holding cell'
					]);
				} else if ( !listingPages[ii].exists ) {
					self.addError([
						extraJs.makeLink(listingPages[ii]),
						' does not exist, and will not be listed at the holding cell'
					]);
				} else {
					tfdlTemplates += '*{{tfdl|' + listingPages[ii].getMain() +
					'|' + self.discussion.nomDate +
					'|section=' + self.discussion.sectionHeader +
					(( holdCellSections[i] === 14 ) ? '|delete=1' : '') +
					(( listingPages[ii].getNamespaceId() === 828 ) ? '|ns=Module' : '') +
					'}}\n';
				}
			}
			
			if ( tfdlTemplates === '' ) {
				// If all don't exist or are in wrong namespace, then there's nothing to do
				self.track('processed', false);
				continue;
			}
			
			// Make new section wikitext
			var heading = sectionsArray[(holdCellSections[i]*2-1)];
			var contents = sectionsArray[(holdCellSections[i]*2)];
			// Remove "* ''None currently''" except if inside a <!--html comment-->
			contents = contents.replace(/\n*^\*\s*''None currently''\s*$(?![^<]*?-->)/gim, '');
			// Merge subsections have level-4 headings
			var headingLevel = ( (4 <= holdCellSections[i]) &&
				( holdCellSections[i] <= 10) ) ? '====' : '===';
			var newWikitext = headingLevel + heading + headingLevel + '\n' +
				contents.trim() + '\n' + tfdlTemplates;
				
			// If this isn't the first iteration of for-loop, wait a bit to avoid self-edit conflict
			if ( i > 0 ) {
				var start = new Date().getTime();
				var waited = 0;
				while ( waited < 1000 ) {
					waited = new Date().getTime() - start;
				}
			}
			apiEditHoldingCell(newWikitext, holdCellSections[i]);
		}
	};

	//get holding cell contents
	API.get( {
		action: 'query',
		titles: config.xfd.subpagePath + 'Holding cell',
		prop: 'revisions',
		rvprop: 'content',
		indexpageids: 1,
		rawcontinue: ''
	} )
	.done( processHoldingCellPage )
	.fail( function(code, jqxhr) {
		self.addApiError(code, jqxhr, 'Could not read contents of holding cell');
		self.setStatus('failed');
	} );
	
	
	
	
};

Task.prototype.doTask.deleteTalk = function(self) {

	// Notify task is started
	self.setStatus('started');
	
	// Delete with the api
	apiDeletePage = function(pageTitle) {
		API.postWithToken( 'csrf', {
			action: 'delete',
			title: pageTitle,
			reason: '[[WP:CSD#G8|G8]]: Talk page of deleted page.' + config.script.advert
		} )
		.done( function() {
			self.track('del', true);
		} )
		.fail( function(code, jqxhr) {
			self.track('del', false);
			self.addApiError(code, jqxhr, [
				'Could not delete page ',
				extraJs.makeLink(pageTitle)
			]);
		} );
	};

	// Get talk pages
	var talkTitles = self.discussion.getTalkTitles(self.pages);
	if ( talkTitles.length === 0 ) {
		self.addWarning('none found');
		self.setStatus('skipped');
	} else {
		// get talk page redirect status from api
		self.setupTracking('del', talkTitles.length);
		
		//For each talk page, check that it exists, then delete it
		for ( var i=0; i < talkTitles.length; i++ ) {
			var subjectPage = self.discussion.getPageByTalkTitle(talkTitles[i]);
			if ( subjectPage.talkExists ) {
				apiDeletePage(talkTitles[i]);
			} else {
				self.addWarning([
					extraJs.makeLink(talkTitles[i]),
					' skipped: does not exist (may have already been deleted by others)'
				]);
				self.track('del', false);
			}
		}
	}
};

Task.prototype.doTask.tagTalkWithSpeedy = function(self) {

	// Notify task is started
	self.setStatus('started');

	var apiTagPage = function(pageTitle) {
		API.postWithToken( 'csrf', {
			action: 'edit',
			title: pageTitle,
			prependtext: '{{Db-talk}}\n',
			summary: '[[WP:G8|G8]] Speedy deletion nomination, per [[:' +
				self.discussion.getNomPageLink() + ']]' + config.script.advert
		} )
		.done( function() {
			self.track('tag', true);
		} )
		.fail( function(code, jqxhr) {
			self.track('tag', false);
			self.addApiError(code, jqxhr, [
				'Could not delete page ',
				extraJs.makeLink(pageTitle)
			]);
		} );
	};
	
	// Get talk pages
	var talkTitles = self.discussion.getTalkTitles(self.pages);
	if ( talkTitles.length === 0 ) {
		self.addWarning('none found');
		self.setStatus('skipped');
	} else {
		// get talk page redirect status from api
		self.setupTracking('tag', talkTitles.length);
		
		//For each talk page, check that it exists, then tag it
		for ( var i=0; i < talkTitles.length; i++ ) {
			var subjectPage = self.discussion.getPageByTalkTitle(talkTitles[i]);
			if ( subjectPage.talkExists ) {
				apiTagPage(talkTitles[i]);
			} else {
				self.addWarning([
					extraJs.makeLink(talkTitles[i]),
					' skipped: does not exist (may have already been deleted by others)'
				]);
				self.track('tag', false);
			}
		}
	}
};

Task.prototype.doTask.deleteRedirects = function(self) {
	// If unlinking backlinks, wait until that task has the info it needs
	$.when(self.discussion.taskManager.dfd.ublQuery).then(function(){
		
		// Notify task is started
		self.setStatus('started');
		self.setupTracking('alldone', 2);
		
		var redirectTitles = [];
		var redirectTalkPageIds = [];
		
		// Delete redirect with the api
		apiDeleteRedir = function(_i, pageTitle) {
			API.postWithToken( 'csrf', {
				action: 'delete',
				title: pageTitle,
				reason: 'Delete redirect: [[' + self.discussion.getNomPageLink() + ']] closed as ' +
					self.inputData.getResult() + config.script.advert
			} )
			.done( function() {
				self.track('delRedir', true);
			} )
			.fail( function(code, jqxhr) {
				self.track('delRedir', false);
				self.addApiError(code, jqxhr, [
					'Could not delete redirect ',
					extraJs.makeLink(pageTitle)
				]);
			} );
		};

		// Delete redirect talkpage with the api
		apiDeleteRedirTalk = function(_i, talkpageid) {
			API.postWithToken( 'csrf', {
				action: 'delete',
				pageid: talkpageid,
				reason: '[[WP:CSD#G8|G8]]: Talk page of deleted page.' + config.script.advert
			} )
			.done( function() {
				self.track('delRedirTalk', true);
			} )
			.fail( function(code, jqxhr) {
				self.track('delRedirTalk', false);
				self.addApiError(code, jqxhr, [
					'Could not delete ',
					$('<a>').attr({
						'href':'https://en.wikipedia.org/wiki/?curid='+talkpageid,
						'target':'_blank'
					}).text('redirect talk page <'+talkpageid+'>')
				]);
			} );
		};
		
		// If okay to delete redirects
		var doDeleteRedirects = function() {
			// Set tracking
			self.setupTracking(
				'delRedir',
				redirectTitles.length,
				function() { this.track('alldone', true); },
				function() { this.track('alldone', false); }
			);
			// Delete each redirect
			$.each(redirectTitles, apiDeleteRedir);
					
			// Check if talk pages were found
			if ( redirectTalkPageIds.length === 0 ) {
				self.track('alldone', true);
				return;
			}
			// Set tracking
			self.setupTracking(
				'delRedirTalk',
				redirectTalkPageIds.length,
				function() { this.track('alldone', true); },
				function() { this.track('alldone', false); }
			);
			// Delete each talk page
			$.each(redirectTalkPageIds, apiDeleteRedirTalk);
		};
		
		// If user cancelled
		var doSkipRedirects = function() {
			self.addWarning('Cancelled by user');
			self.setStatus('skipped');
		};
		
		var processRedirects = function(result) {
			if ( !result.query || !result.query.pages ) {
				// No results
				self.addWarning('none found');
				self.setStatus('skipped');
				return;
			}
			// Gather redirect titles into array
			$.each(result.query.pages, function(_i, info) {
				redirectTitles.push(info.title);
				if ( info.talkid ) { redirectTalkPageIds.push(info.talkid); }
			});
			// Continue query if needed
			if ( result.continue ) {
				apiQuery($.extend(query, result.continue));
				return;
			}
			
			// Check if redirects were found
			if ( redirectTitles.length === 0 ) {
				self.addWarning('none found');
				self.setStatus('skipped');
				return;
			}
		
			// Warn if there will be mass action, allow user to cancel
			if ( redirectTitles.length >= 10 ) {
				var massActWarningCallback = function(action) {
					if ( action === 'accept' ) {
						doDeleteRedirects();
					} else if ( action === 'show' ) {
						extraJs.multiButtonConfirm(
							'Warning',
							'Mass action to be peformed: delete '+ redirectTitles.length +
							' redirects:<ul><li>[[' + redirectTitles.join(']]</li><li>[[') + ']]</li></ul>',
							[
								{ label:'Cancel', flags:'safe' },
								{ label:'Delete redirects', action:'accept', flags:'progressive' }
							],
							massActWarningCallback,
							{'verbose': true, 'unescape': true, 'wikilinks':true}
						);
					} else {
						doSkipRedirects();
					}
				};

				extraJs.multiButtonConfirm(
					'Warning',
					'Mass action to be peformed: delete '+ redirectTitles.length + ' redirects.',
					[
						{ label:'Cancel', flags:'safe' },
						{ label:'View redirects...', action:'show' },
						{ label:'Delete redirects', action:'accept', flags:'progressive' }
					],
					massActWarningCallback
				);
			} else {
				doDeleteRedirects();
			}
		};
		
		// Get redirects
		var query = {
			action: 'query',
			titles: self.discussion.getPageTitles(self.pages).join('|'),
			generator: 'redirects',
			grdlimit: 500,
			prop: 'info',
			inprop: 'talkid'
		};
		var apiQuery = function(q) {
			API.get( q )
			.done( processRedirects )
			.fail( function(code, jqxhr) {
				self.addApiError(code, jqxhr, 'Could not obtain redirects');
				self.setStatus('failed');
			} );
		};
		apiQuery(query);
	});
};

Task.prototype.doTask.unlinkBacklinks = function(self) {

	// Notify task is started
	self.setStatus('started');
	
	var pageTitles = self.discussion.getPageTitles(self.pages);
	var redirectTitles = [];
	var ignoreTitles = [
		'Template:WPUnited States Article alerts',
		'Template:Article alerts columns',
		'Template:Article alerts columns/doc'
	];
	var blresults = [];
	var iuresults = [];
	
	//convert results (arrays of objects) to titles (arrays of strings), removing duplicates
	var flattenToTitles = function(arr) {
		return arr.reduce(
			function(a, b) {
				if ( b.redirlinks ) {
					if ( $.inArray(b.title, redirectTitles) === -1 ) {
						redirectTitles.push(b.title);
					}
					return a.concat(
						b.redirlinks.reduce(
							function(aa, bb) {
								if (
									$.inArray(bb.title, a) !== -1 ||
									$.inArray(bb.title, pageTitles) !== -1 ||
									$.inArray(bb.title, ignoreTitles)!== -1
								) {
									return aa;
								} else {
									return aa.concat(bb.title);
								}
							},
							[]
						)
					);
				} else if (
					b.redirect === '' ||
					$.inArray(b.title, a) !== -1 ||
					$.inArray(b.title, pageTitles) !== -1 ||
					$.inArray(b.title, ignoreTitles)!== -1
				) {
					return a;
				} else {
					return a.concat(b.title);
				}
			},
			[]
		);
	};

	var apiEditPage = function(pageTitle, newWikitext) {
		API.postWithToken( 'csrf', {
			action: 'edit',
			title: pageTitle,
			text: newWikitext,
			summary: 'Removing link(s)' +
				(( config.xfd.type === 'ffd' ) ? ' / file usage(s)' : '' ) +
				': [[' + self.discussion.getNomPageLink() + ']] closed as ' +
				self.inputData.getResult() + config.script.advert,
			minor: 1,
			nocreate: 1
		} )
		.done( function() {
			self.track('unlink', true);
		} )
		.fail( function(code, jqxhr) {
			self.track('unlink', false);
			self.addApiError(code, jqxhr, [
				'Could not remove backlinks from ',
				extraJs.makeLink(pageTitle)
			]);
		} );
	};
	
	var checkListItems = function(pageTitle, wikitext) {
		// Find lines marked with {{subst:void}}, and the preceding section heading (if any)
		var toReview = /^{{subst:void}}(.*)$/m.exec(wikitext);
		if ( !toReview ) {
			// None found, so make the edit
			apiEditPage(pageTitle, wikitext);
		} else {
			// Find the preceding heading, if any
			var precendingText = wikitext.split('{{subst:void}}')[0];
			var allHeadings = precendingText.match(/^=+.+?=+$/gm);
			var heading = ( !allHeadings ) ? null : allHeadings[allHeadings.length - 1].replace(/(^=* *| *=*$)/g, '');
			// Prompt user
			extraJs.multiButtonConfirm(
				'Review unlinked list item',
				'[[' + pageTitle +
				( ( heading ) ? '#' +
					mw.util.wikiUrlencode(
						heading.replace(/\[\[([^\|\]]*?)\|([^\]]*?)\]\]/, '$2')
						.replace(/\[\[([^\|\]]*?)\]\]/, '$1')
					) + ']]' : ']]' ) +
				': ' +
				'<pre>' + toReview[1] + '</pre>',
				[{ label:'Keep item', action:'keep' }, { label:'Remove item', action:'remove'}],
				function(action) {
					if ( action === 'keep' ) {
						// Remove the void from the start of the line
						wikitext = wikitext.replace(/^{{subst:void}}/m, '');
					} else {
						// Remove the whole line
						wikitext = wikitext.replace(/^{{subst:void}}.*\n?/m, '');
					}
					// Iterate, in case there is more to be reviewed
					checkListItems(pageTitle, wikitext);
				},
				{ verbose:true, unescape:true, wikilinks:true }
			);
		}
	};
	
	var processUnlinkPages = function(result) {
		if ( !result.query || !result.query.pages ) {
			// No results
			self.addApiError('result.query.pages not found', null, 'Could not read contents of pages; '+
				'could not remove backlinks');
			console.log('[XFDcloser] API error: result.query.pages not found... result =');
			console.log(result);
			self.setStatus('failed');
			return;
		}
		// For each page, pass the wikitext through the unlink function
		$.each(result.query.pages, function(_i, page) {
			var oldWikitext = page.revisions[0]['*'];
			var newWikitext = extraJs.unlink(
				oldWikitext,
				pageTitles.concat(redirectTitles),
				page.ns,
				!!page.categories
			);
			if ( oldWikitext !== newWikitext ) {
				checkListItems(page.title, newWikitext);
			} else {
				self.addWarning(['Skipped ',
					extraJs.makeLink(page.title),
					' (no direct links)'
				]);
				self.track('unlink', false);
			}
		});
	};

	var apiReadFail = function(code, jqxhr) {
		self.addApiError(code, jqxhr, 'Could not read contents of pages; '+
			'could not remove backlinks');
		self.setStatus('failed');
	};
	
	var processResults = function() {
		// Flatten results arrays 
		if ( blresults.length !== 0 ) {
			blresults = flattenToTitles(blresults);
		}
		if ( iuresults.length !== 0 ) {
			iuresults = flattenToTitles(iuresults);
			// Remove image usage titles that are also in backlikns results 
			iuresults = iuresults.filter(function(t) { return $.inArray(t, blresults) === -1; });
		}

		// Check if, after flattening, there are still backlinks or image uses
		if ( blresults.length === 0 && iuresults.length === 0 ) {
			self.addWarning('none found');
			self.setStatus('skipped');
			return;
		}
		
		// Ask user for confirmation
		var heading = 'Unlink backlinks';
		if ( iuresults.length !== 0 ) {
			heading += '('; 
			if ( blresults.length !== 0 ) {
				heading += 'and ';
			}
			heading += 'file usage)';
		}
		heading += ':';
		var para = '<p>All '+ (blresults.length + iuresults.length) + ' pages listed below may be '+
			'edited (unless backlinks are only present due to transclusion of a template).</p>'+
			'<p>To process only some of these pages, use Twinkle\'s unlink tool instead.</p>'+
			'<p>Use with caution, after reviewing the pages listed below. '+
			'Note that the use of high speed, high volume editing software (such as this tool and '+
			'Twinkle\'s unlink tool) is subject to the Bot policy\'s Assisted editing guidelines '+
			'(WP:ASSISTED)</p><hr>';
		var list = '<ul>';
		if ( blresults.length !== 0 ) {
			list += '<li>[[' + blresults.join(']]</li><li>[[') + ']]</li>';
		}
		if ( iuresults.length !== 0 ) {
			list += '<li>[[' + iuresults.join(']]</li><li>[[') + ']]</li>';
		}
		list += '<ul>';
		
		extraJs.multiButtonConfirm(
			heading,
			para + list,
			[
				{ label: 'Cancel', flags: 'safe' },
				{ label: 'Remove backlinks', action: 'accept', flags: 'progressive' }
			],
			function(action) {
				if ( action ) {
					var unlinkTitles = iuresults.concat(blresults);
					self.setupTracking('unlink', unlinkTitles.length);
					// get wikitext of titles, check if disambig - in lots of 50 (max for Api)
					for (var ii=0; ii<unlinkTitles.length; ii+=50) {
						API.get( {
							action: 'query',
							titles: unlinkTitles.slice(ii, ii+49).join('|'),
							prop: 'categories|revisions',
							clcategories: 'Category:All disambiguation pages',
							rvprop: 'content',
							indexpageids: 1
						} )
						.done( processUnlinkPages )
						.fail( apiReadFail );
					}
				} else {
					self.addWarning('Cancelled by user');
					self.setStatus('skipped');
				}
			},
			{'verbose': true, 'unescape': true, 'wikilinks': true}
		);
	};

	// Queries
	var blParams = {
		list: 'backlinks',
		blfilterredir: 'nonredirects',
		bllimit: 'max',
		blnamespace: config.xfd.ns_unlink,
		blredirect: 1
	};
	var iuParams = {
		list: 'backlinks|imageusage',
		iutitle: '',
		iufilterredir: 'nonredirects',
		iulimit: 'max',
		iunamespace: config.xfd.ns_unlink,
		iuredirect: 1
	};
	var query = pageTitles.map(function(page) {
		return $.extend(
			{ action: 'query' },
			blParams,
			{ bltitle: page },
			( config.xfd.type === 'ffd' ) ? iuParams : null,
			( config.xfd.type === 'ffd' ) ? { iutitle: page } : null
		);
	});
	// Variable for incrementing current query
	var qIndex = 0;
	// Function to do Api query
	var apiQuery = function(q) {
		API.get( q )
		.done( processBacklinks )
		.fail( function(code, jqxhr) {
			self.addApiError(code, jqxhr, 'Could not retrieve backlinks');
			self.setStatus('failed');
			// Allow delete redirects task to begin
			self.discussion.taskManager.dfd.ublQuery.resolve();
		} );
	};
	// Process api callbacks
	var processBacklinks = function(result) {
		// Gather backlink results into array
		if ( result.query.backlinks ) {
			blresults = blresults.concat(result.query.backlinks);
		}
		// Gather image usage results into array
		if ( result.query.imageusage ) {
			iuresults = iuresults.concat(result.query.imageusage);
		}
		// Continue current query if needed
		if ( result.continue ) {
			apiQuery($.extend({}, query[qIndex], result.continue));
			return;
		}
		// Start next query, unless this is the final query
		qIndex++;
		if ( qIndex < query.length ) {
			apiQuery(query[qIndex]);
			return;
		}
		// Allow delete redirects task to begin
		self.discussion.taskManager.dfd.ublQuery.resolve();
		// Check if any backlinks or image uses were found
		if ( blresults.length === 0 && iuresults.length === 0 ) {
			self.addWarning('none found');
			self.setStatus('skipped');
			return;
		}
		// Process the results
		processResults();
	};
	// Get started
	apiQuery(query[qIndex]);
	
};

Task.prototype.doTask.redirect = function(self) {

	// Notify task is started
	self.setStatus('started');
	
	var pageTitles = self.discussion.getPageTitles(self.pages);
	var deleteFirst = self.inputData.deleteFirst;
	var softRedirect = self.inputData.result === 'soft redirect';
	var rcats = self.inputData.rcats;
	var rcatshell = ( rcats ) ? "\n\n{{Rcat shell|\n" + rcats + "\n}}" : '';

	self.setupTracking('redir', pageTitles.length);

	// Make a redirect
	apiMakeRedirect = function(pageTitle) {
		var newWikitext;
		if ( pageTitle.indexOf('Module:') === 0 ) {
			var targetPage = self.inputData.getTargetLink(pageTitle, true);
			if ( targetPage.indexOf('Module:') !== 0 ) {
				self.track('redir', false);
				self.addError([
					'Could not redirect ',
					extraJs.makeLink(pageTitle),
					' because target (',
					extraJs.makeLink(targetPage),
					') is not a module'
				], true);
				return false;
			}
			newWikitext = 'return require( "' + targetPage + '" )';
		} else if ( softRedirect ) {
			newWikitext = '{{Soft redirect|' + self.inputData.getTargetLink(pageTitle, true) +
			'}}' + rcatshell;
		} else {
			newWikitext = "#REDIRECT " + self.inputData.getTargetLink(pageTitle) + rcatshell;
		}
		
		API.postWithToken( 'csrf', {
			action: 'edit',
			title: pageTitle,
			text: newWikitext,
			summary: '[[:' + self.discussion.getNomPageLink() + ']] closed as ' +
				self.inputData.getResult() + config.script.advert
		} )
		.done( function() {
			self.track('redir', true);
		} )
		.fail( function(code, jqxhr) {
			self.track('redir', false);
			self.addApiError(code, jqxhr, [
				'Could not edit page ',
				extraJs.makeLink(pageTitle)
			]);
		} );
	};

	// Delete before redirecting
	apiDelAndRedir = function(pageTitle) {
		API.postWithToken( 'csrf', {
			action: 'delete',
			title: pageTitle,
			reason: '[[' + self.discussion.getNomPageLink() + ']]' + config.script.advert
		} )
		.done( function() {
			apiMakeRedirect(pageTitle);
		} )
		.fail( function(code, jqxhr) {
			self.track('redir', false);
			self.addApiError(code, jqxhr, [
				'Could not delete page ',
				extraJs.makeLink(pageTitle)
			]);
		} );
	};
	
	// For each page, check that it isn't a target, then redirect, or delete and redirect
	for ( var i=0; i<pageTitles.length; i++ ) {
		if ( $.inArray(pageTitles[i], self.inputData.getTargetsArray('redirect')) !== -1 ) {
			//Skip (don't make a page redirect to itself)
			self.track('redir', false);
		} else if ( deleteFirst ) {
			apiDelAndRedir(pageTitles[i]);
		} else {
			apiMakeRedirect(pageTitles[i]);
		}
	}
};

Task.prototype.doTask.removeCircularLinks = function(self) {

	// Notify task is started
	self.setStatus('started');

	var pageTitles = self.discussion.getPageTitles();	
	var targetTitles = self.inputData.getTargetsArray('redirect');
	self.setupTracking('uncircle', targetTitles.length);

	// Edit with the Api
	var apiEditPage = function(pageTitle, newWikitext) {
		API.postWithToken( 'csrf', {
			action: 'edit',
			title: pageTitle,
			text: newWikitext,
			summary: 'Unlinking circular redirects: [[:' + self.discussion.getNomPageLink() +
				']] closed as ' + self.inputData.getResult() + config.script.advert
		} )
		.done( function() {
			self.track('uncircle', true);
		} )
		.fail( function(code, jqxhr) {
			self.track('uncircle', false);
			self.addApiError(code, jqxhr, [
				'Could not edit page ',
				extraJs.makeLink(pageTitle)
			]);
		} );
	};
	
	processTargets = function (result) {
		$.each(result.query.pages, function(_i, page) {
			console.log('_i = ' + _i + '\npage.title = ' + page.title);
			var oldWikitext = page.revisions[ 0 ][ '*' ];
			// Don't remove selflinks
			var unlinkPageTitles = pageTitles.filter(function(t){
				return t !== page.title;
			});
			var newWikitext = extraJs.unlink(oldWikitext, unlinkPageTitles);
			
			// Check if any changes need to be made; if redirect target is a nominated page, remove
			// nomination template and adjust edit summary
			if ( newWikitext === oldWikitext ) {
				// No links to unlink
				self.addWarning('none found');
				self.track('uncircle', false);
			} else if ( $.inArray(page.title, pageTitles) !== -1 ) {
				// Target is one of the nominated pages - also remove nom template if present
				newWikitext = config.xfd.removeNomTemplate(newWikitext);
				apiEditPage(page.title, newWikitext);
			} else {
				apiEditPage(page.title, newWikitext);
			}
		});
	};
	
	// Get the wikitext of each target
	API.get( {
		action: 'query',
		titles: targetTitles.join('|'),
		prop: 'revisions',
		rvprop: 'content',
		indexpageids: 1,
		rawcontinue: ''
	} )	
	.done( processTargets )
	.fail( function(code, jqxhr) {
		self.track('redir', false);
		self.addApiError(code, jqxhr,
			'Could not read contents of redirect target' + ( targetTitles.length > 1 ) ? 's' : '');
	} );	
};

// --- Relisting tasks ---
Task.prototype.doTask.getRelistInfo = function(self) {

	// Notify task is started
	self.setStatus('started');
	
	self.setupTracking('done', 1);

	var now = new Date();
	var today = now.getUTCFullYear() + ' ' + config.mw.monthNames[now.getUTCMonth()] +
		' ' + now.getUTCDate();
	var todaysLogpage = config.xfd.path + today;
	
	// Set log page link in task message
	if ( config.xfd.type !== 'mfd' ) {
		self.discussion.taskManager.getTaskByName('updateNewLogPage').setDescription([
			'Adding to ',
			extraJs.makeLink(todaysLogpage, "today's log page")
		]);
	}
	
	var processAfdLogpages = function(result) {
		
		// Get wikitext
		var newlogtext = '';
		var oldlogtext = '';
		var ids = result.query.pageids;
		
		// Check how many log pages were found
		if ( ids.length === 1 ) {
			// Abort if only one log page found
			self.discussion.taskManager.abortTasks(
				'discussion already transcluded to today\'s log page'
			);
			return;
		}
		
		// Identify log pages
		if ( result.query.pages[ ids[0] ].title === todaysLogpage ) {
			// ids[0] is the new log page
			newlogtext = result.query.pages[ ids[0] ].revisions[0]['*'];
			oldlogtext = result.query.pages[ ids[1] ].revisions[0]['*'];
		} else {
			// ids[0] is the old log page
			newlogtext = result.query.pages[ ids[1] ].revisions[0]['*'];
			oldlogtext = result.query.pages[ ids[0] ].revisions[0]['*'];
		}
		
		// Abort if already relisted
		var t = mw.RegExp.escape(self.discussion.nomPage);
		var oldPatt = new RegExp('<!-- ?\\{\\{' + t + '\\}\\} ?-->', 'i');
		var newPatt = new RegExp('\\{\\{' + t + '\\}\\}', 'i');
		if ( oldPatt.test(oldlogtext) || newPatt.test(newlogtext) ) {
			self.discussion.taskManager.abortTasks('discussion has been relisted already');
			return;
		}
		
		// Updated new log wikitext:
		var newlogreg = new RegExp('<!-- Add new entries to the TOP of the following list -->','i');
		self.discussion.taskManager.relistInfo.newLogWikitext = newlogtext.replace(
			newlogreg,
			'<!-- Add new entries to the TOP of the following list -->\n{{' +
			self.discussion.nomPage + '}}<!--Relisted-->'
		);
		
		// Updated old log wikitext:
		var oldlogreg = new RegExp("(\\{\\{" + t + "\\}\\})", 'i' );
		self.discussion.taskManager.relistInfo.oldlogTransclusion = oldlogreg.test(oldlogtext);
		self.discussion.taskManager.relistInfo.oldLogWikitext = oldlogtext.replace(
			oldlogreg, '<!-- $1 -->');
		
		// Ready to proceed to next tasks
		self.track('done', true);
	};

	// TFD, RRD
	var processTodaysLogpage = function(result) {
		var _id = result.query.pageids;
		var _contents = result.query.pages[ _id ].revisions[ 0 ][ '*' ];
		var _h4 = _contents.match('====');
		if ( _h4 ) {
			// there is at least 1 level 4 heading on page - can prepend to section #2
			self.discussion.taskManager.relistInfo.newLogEditType = 'prependtext';
			self.discussion.taskManager.relistInfo.newLogSection = 2;
		} else {
			// there are no level 4 headings on page - can append to section #1
			self.discussion.taskManager.relistInfo.newLogEditType = 'appendtext';
			self.discussion.taskManager.relistInfo.newLogSection = 1;

		}
		// Ready to proceed to next tasks
		self.track('done', true);	
	};

	var processNomPage = function (result) {
		// Discussion wikitext
		var id = result.query.pageids;
		var oldWikitext = result.query.pages[ id ].revisions[0]['*'];
		var heading = oldWikitext.slice(0, oldWikitext.indexOf('\n'));

		// Abort if discussion is already closed
		if ( oldWikitext.indexOf('xfd-closed') !== -1 ) {
			self.discussion.taskManager.abortTasks('discussion has been closed');
			return;
		}
		
		// Relist template
		var relists = oldWikitext
			.match(/\[\[Wikipedia:Deletion process#Relisting discussions\|Relisted\]\]/g);
		var relistTemplate = '\n{{subst:Relist|1=' + self.inputData.getRelistComment() +
			'|2=' + (( relists ) ? relists.length + 1 : 1) + '}}\n';
		
		// New/relisted discussion
		var newWikitext = oldWikitext.trim() + relistTemplate;
		
		// Wikitext for old log page
		var oldLogWikitext = '';
		
		if ( config.xfd.type === 'afd' ) {
			// Update link to log page
			newWikitext = newWikitext.replace(
				/\[\[Wikipedia:Articles for deletion\/Log\/\d{4} \w+ \d{1,2}#/,
				'[[' + todaysLogpage + '#'
			);
		
		} else if ( config.xfd.type === 'ffd' || config.xfd.type === 'tfd' ) {
			// Discussion on old log page gets closed
			var xfdCloseTop = config.xfd.wikitext.closeTop
				.replace(/__RESULT__/, 'relisted')
				.replace(/__TO_TARGET__/, ' on [[' + todaysLogpage + '#' +
					self.discussion.sectionHeader + '|' + today + ']]')
				.replace(/__RATIONALE__/, '.')
				.replace(/__SIG__/, config.user.sig);
			// List of nominated pages
			var pagesList = '';
			if ( !self.discussion.isBasicMode() ) {
				pagesList = self.discussion.pages.reduce(function(list, page) {
					var namespaceParam = ( page.getNamespaceId() === 828 ) ? '|module=Module' : '';
					return list + config.xfd.wikitext.pagelinks.replace('__PAGE__',	page.getMain() + namespaceParam);
				}, '');
			}
			oldLogWikitext = heading + '\n' + xfdCloseTop + '\n' +
			pagesList + config.xfd.wikitext.closeBottom;
			
		} else if ( config.xfd.type === 'mfd' ) {
			// Find first linebreak after last pagelinks templats
			var splitIndex = newWikitext.indexOf('\n', newWikitext.lastIndexOf(':{{pagelinks'));
			// Add time stamp for bot to properly relist
			newWikitext = (newWikitext.slice(0, splitIndex).trim() +
			'\n{{subst:mfdr}}\n' + newWikitext.slice(splitIndex+1).trim());
		
		} else if ( config.xfd.type === 'rfd' ) {
			var topWikitext = '====' + self.discussion.sectionHeader + '====';
			if ( oldWikitext.indexOf('*<span id=') !== oldWikitext.lastIndexOf('*<span id=') ) {
				// Multiple redirects were nominted, so keep nominated redirects' anchors
				// Find linebreak prior to first span with an id
				var splitIndex1 = oldWikitext.indexOf('\n', oldWikitext.indexOf('*<span id=')-2);
				// Find linebreak after the last span with an id
				var splitIndex2 = oldWikitext.indexOf('\n', oldWikitext.lastIndexOf('*<span id='));
				var rfdAnchors = oldWikitext.slice(splitIndex1, splitIndex2)
					.replace(/\*<span/g, '<span') // remove bullets
					.replace(/^(?!<span).*$\n?/gm, '') // remove lines which don't start with a span
					.replace(/>.*$\s*/gm, '></span>') // remove content within or after span
					.trim();
				topWikitext += '\n<noinclude>' + rfdAnchors + '</noinclude>';
			}
			oldLogWikitext = topWikitext + '\n{{subst:rfd relisted|page=' + today +
			'|' + self.discussion.sectionHeader + '}}';
		}
		
		// Store wikitext in task manager
		self.discussion.taskManager.relistInfo = {
			'today': today,
			'newWikitext': newWikitext,
			'oldLogWikitext': oldLogWikitext
		};
		
		// For AfDs, check that it's still transcluded to old log page...		
		if ( config.xfd.type === 'afd' ) {
			// Get array of "embeddedin" pages which are logpages (should only be one)
			var eiLogpages = result.query.embeddedin.filter(function(ei) {
				return ei.title.indexOf(config.xfd.path) !== -1;
			});
			// Abort if none found
			if ( eiLogpages.length === 0 ) {
				self.addError('Old log page not found');
				self.discussion.taskManager.abortTasks('');
				return;
			}
			// Warn if multiple log pages were found
			if ( eiLogpages.length > 1 ) {
				for (var i = 1; i < eiLogpages.length; i++) {
					self.addWarning([
						'Note: transcluded on additional log page: ',
						extraJs.makeLink(
							eiLogpages[i].title,
							eiLogpages[i].title.replace(config.xfd.path, '')
						)
					]);
				}
			}
			// Set old log page 
			var oldLogpage = eiLogpages[0];
			// Abort if old log page is actually today's logpage
			if ( oldLogpage.title === todaysLogpage ) {
				self.addError('Already transcluded to today\'s log page');
				self.discussion.taskManager.abortTasks('');
				return;
			}
			// Add old log page link in task message
			self.discussion.taskManager.getTaskByName('updateOldLogPage').setDescription([
				'Removing from ',
				extraJs.makeLink(oldLogpage.title, 'old log page')
			]);
			// Store old log title
			self.discussion.taskManager.relistInfo.oldlogtitle = oldLogpage.title;
			self.discussion.taskManager.relistInfo.newLogEditType = 'text';
			// Get logpages' wikitext
			API.get( {
				action: 'query',
				titles: oldLogpage.title + '|' + todaysLogpage,
				prop: 'revisions',
				rvprop: 'content',
				indexpageids: 1,
				rawcontinue: ''
			} )
			.done( processAfdLogpages )
			.fail( function(code, jqxhr) {
				self.addApiError(code, jqxhr, 'Could not read contents of log pages');
				self.discussion.taskManager.abortTasks('');
			} );
			
		} else if ( config.xfd.type === 'tfd' || config.xfd.type === 'rfd' ) {
			//New discussions on top of log page, so need to check out current log page wikitext
			API.get( {
				action: 'query',
				titles: todaysLogpage,
				prop: 'revisions',
				rvprop: 'content',
				indexpageids: 1,
				rawcontinue: ''
			} )
			.done( processTodaysLogpage )
			.fail( function(code, jqxhr) {
				self.addApiError(code, jqxhr, [
					'Could not read contents of  ',
					extraJs.makeLink(todaysLogpage, "today's log page")
				]);
				self.discussion.taskManager.abortTasks('');
			} );
				
		} else { // ffd, mfd
			self.discussion.taskManager.relistInfo.newLogEditType = 'appendtext';
			// Ready to proceed to next task(s)
			self.track('done', true);
		}
	};
	
	var query = {
			action: 'query',
			titles: self.discussion.nomPage,
			prop: 'revisions',
			indexpageids: 1,
			rawcontinue: 1,
			rvprop: 'content',
			rvsection: self.discussion.sectionNumber
		};
	if ( config.xfd.type === 'afd' ) {
		$.extend(query, { 
			list: 'embeddedin',
			eititle: self.discussion.nomPage,
			einamespace: config.xfd.ns_logpages,
			eifilterredir: 'nonredirects',
			eilimit: 500
		});
		// Need to fetch whole page, in order to check if afd is already closed
		delete query.rvsection;
	}
	API.get( query )
	.done( processNomPage )
	.fail( function(code, jqxhr) {
		self.addApiError(code, jqxhr,
			[	'Could not read contents of page ',
				extraJs.makeLink(self.discussion.nomPage),
				'; could not relist discussion'	]
		);
		self.discussion.taskManager.abortTasks('');
	} );
	
};	

Task.prototype.doTask.updateDiscussion = function(self) {

	// Notify task is started
	self.setStatus('started');
	
	self.setupTracking('edit', 1);
	
	var relistInfo = self.discussion.taskManager.relistInfo;
	
	var params = {
		action: 'edit',
		title: self.discussion.nomPage,
		text: relistInfo.newWikitext,
		summary: 'Relisting discussion' + config.script.advert
	};
	if ( config.xfd.type === 'mfd' ) {
		params.section = self.discussion.sectionNumber;
	}
	
	API.postWithToken( 'csrf', params )
	.done( function() {
		self.track('edit', true);
	} )
	.fail( function(code, jqxhr) {
		self.track('edit', false);
		self.addApiError(code, jqxhr, 'Could not edit ' + config.xfd.type.toUpperCase() +
		' discussion');
	} );
};

Task.prototype.doTask.updateOldLogPage = function(self) {

	// Notify task is started
	self.setStatus('started');
	
	self.setupTracking('edit', 1);
	
	var relistInfo = self.discussion.taskManager.relistInfo;
	
	var params = {
		action: 'edit',
		title: ( config.xfd.type === 'afd' ) ? relistInfo.oldlogtitle : self.discussion.nomPage,
		text: relistInfo.oldLogWikitext,
		summary: (( config.xfd.type === 'afd' ) ? 'Relisting [[:' + self.discussion.nomPage +
			']]' : '/* ' + self.discussion.sectionHeader + ' */ Relisted on [[:' +
			config.xfd.path + relistInfo.today + '#' + self.discussion.sectionHeader +
			'|' + relistInfo.today + ']]') + config.script.advert
	};
	
	if ( config.xfd.type === 'afd' ) {
		// Skip if transclusion wasn't found
		if ( !relistInfo.oldlogTransclusion ) {
			self.track('edit', false);
			self.addError('Transclusion not found on old log page; could not be commented out');
			return;
		}
	} else {
		params.section = self.discussion.sectionNumber;
	}
	
	API.postWithToken( 'csrf', params )
	.done( function() {
		self.track('edit', true);
	} )
	.fail( function(code, jqxhr) {
		self.track('edit', false);
		self.addApiError(code, jqxhr, 'Could not edit old ' + config.xfd.type.toUpperCase() +
		' log page');
	} );
};

Task.prototype.doTask.updateNewLogPage = function(self) {

	// Notify task is started
	self.setStatus('started');
	
	self.setupTracking('edit', 1);
	
	var relistInfo = self.discussion.taskManager.relistInfo;
	if ( relistInfo.newLogEditType === 'appendtext' ) {
		relistInfo.newWikitext = '\n' + relistInfo.newWikitext;
	}
	
	var params = {
		action: 'edit',
		title: config.xfd.path + relistInfo.today,
		summary: 'Relisting ' + (( config.xfd.type === 'afd' ) ? '[[:' + self.discussion.nomPage +
		']]' : '"' + self.discussion.sectionHeader + '"') + config.script.advert
	};
	if ( config.xfd.type === 'afd' ) {
		params.text = relistInfo.newLogWikitext;
	} else {
		params[relistInfo.newLogEditType]  = relistInfo.newWikitext;
	}
	
	if ( /(tfd|rfd)/.test(config.xfd.type) ) {
		params.section = relistInfo.newLogSection;
	}
	
	API.postWithToken( 'csrf', params )
	.done( function() {
		self.track('edit', true);
	} )
	.fail( function(code, jqxhr) {
		self.track('edit', false);
		self.addApiError(code, jqxhr, 'Could not edit today\'s ' + config.xfd.type.toUpperCase() +
		' log page');
	} );
};

Task.prototype.doTask.updateNomTemplates = function(self) {

	// Notify task is started
	self.setStatus('started');

	var relistInfo = self.discussion.taskManager.relistInfo;
	
	var pageTitles = self.discussion.getPageTitles(null, {'moduledocs':true});
	self.setupTracking('edit', pageTitles.length);
	
	var apiEditPage = function(pageTitle, updatedWikitext) {
		API.postWithToken( 'csrf', {
			action: 'edit',
			title: pageTitle,
			text: updatedWikitext,
			summary: 'Updating ' + config.xfd.type.toUpperCase() +
				' template: discussion was relisted' + config.script.advert
		} )
		.done( function() {
			self.track('edit', true);
		} )
		.fail( function(code, jqxhr) {
			self.track('edit', false);
			self.addApiError(code, jqxhr, [
				'Could not edit page ',
				extraJs.makeLink(pageTitle)
			]);
		} );
	};
	
	var processPages = function(result) {
		//Get old wikitext, or make error if page doesn't exist, or if page is in wrong namespace
		var ids = result.query.pageids;
		for (var i = 0; i < ids.length; i++) {
			var pageTitle = result.query.pages[ ids[i] ].title;
			// Check there's a corresponding nominated page
			var pageObj = self.discussion.getPageByTitle(pageTitle, {'moduledocs':true});
			if ( !pageObj ) {
				self.addError([
					'API query result included unexpected title ',
					extraJs.makeLink(pageTitle),
					'; this page will not be edited'
				]);
				self.track('edit', false);
				continue;
			}
			// Check that page exists
			if ( parseInt(ids[i]) < 0 ) { 
				self.addError([
					extraJs.makeLink(pageTitle),
					' does not exist and will not be edited'
				]);
				self.track('edit', false);
				continue;
			}
			// Update wikitext
			var oldWikitext = result.query.pages[ ids[i] ].revisions[0]['*'];
			var newWikitext = oldWikitext.replace(
				config.xfd.regex.relistPattern,
				config.xfd.wikitext.relistReplace.replace("__TODAY__", relistInfo.today)
			);
			// Skip if no changes made
			if ( newWikitext === oldWikitext ) {
				self.track('edit', false);
				self.addWarning([
					'Skipped ',
					extraJs.makeLink(pageTitle),
					': nomination template not found'
				]);
				continue;				
			}
			// Make the edit
			apiEditPage(pageTitle, newWikitext);
		}
	};

	// Get each nominated page's wikitext
	new mw.Api().get( {
		action: 'query',
		titles: pageTitles.join('|'),
		prop: 'revisions',
		rvprop: 'content',
		indexpageids: 1
	} )
	.done( processPages )
	.fail( function(code, jqxhr) {
		self.addApiError(code, jqxhr, 'Could not read contents of nominated page' +
		( pageTitles.length > 1 ) ? 's' : '');
		self.setStatus('failed');
	} );
};


/* ========== ShowHideTag class =================================================================
   The 'tag' at the bottom of screen that toggles the visibility of closed discussions.
   ---------------------------------------------------------------------------------------------- */
// Constructor
var ShowHideTag = function() {
	// Determine previous state from localStorage
	try {
		if ( !!localStorage.getItem('xfdc-closedHidden') ) {
			this.isHidden = true;
		} else {
			this.isHidden = false;
		}
	} catch(e) {
		// If localStorage not available, default to not hidden
		this.isHidden = false;
	}
};

// ---------- ShowHideTag prototype ------------------------------------------------------------- */
ShowHideTag.prototype.hideClosed = function() {
	this.isHidden = true;
	try {
		localStorage.setItem('xfdc-closedHidden', true);
	}  catch(e) {}
	$('.xfd-closed, .tfd-closed, #XFDcloser-showhide-hide').hide();
	$('#XFDcloser-showhide-show').show();
};

ShowHideTag.prototype.showClosed = function() {
	this.isHidden = false;
	try {
		localStorage.setItem('xfdc-closedHidden', '');
	}  catch(e) {} 
	$('.xfd-closed, .tfd-closed, #XFDcloser-showhide-hide').show();
	$('#XFDcloser-showhide-show').hide();
};
	
ShowHideTag.prototype.initialise = function() {
	var self = this;
	$('<div>')
	.attr('id', 'XFDcloser-showhide')
	.append(
		$('<a>')
		.attr('id', 'XFDcloser-showhide-hide')
		.text('Hide closed discussions')
		.toggle(!self.isHidden)
		.on('click', self.hideClosed),
		$('<a>')
		.attr('id', 'XFDcloser-showhide-show')
		.text('Show closed discussions')
		.toggle(self.isHidden)
		.on('click', self.showClosed)
	)
	.appendTo('body');
};
   
/* ========== Get started ======================================================================= */
// Initialise show/hide closed discussions tag, unless there is only one discussion on the page
if ( $('#mw-content-text ' + config.xfd.html.head).length > 1 ) {
	config.showHide = new ShowHideTag();
	config.showHide.initialise();
}

// Set up discussion object for each discussion
$(config.xfd.html.head + ' > span.mw-headline')
.not('.XFDcloser-ignore')
.each(function(i) {
	var d = Discussion.newFromHeadlineSpan(i, this);
	if ( d ) {
		try {
			d.retrieveExtraInfo();
		} catch(e) {
			console.warn('[XFDcloser] Could not retrieve page info for ' + $(this).text() +
			' [see error below]');
			console.warn(e);
		}
	}
});

// If showHide state is hidden, hide any headings that may have had class 'xfd-closed' added
if ( config.showHide && config.showHide.isHidden ) {
	config.showHide.hideClosed();
}

/* ==========  End of full file closure wrappers ================================================ */
});
});
/* </nowiki> */