Jump to content

User:Equazcion/csdhelper.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/* ============================================== *\
** CSD Helper - JavaScript CSD Script
**   for Wikipedia
**
** Created by Alex Barley [[User:Ale_jrb]]
**		Tracker: [[User:Ale_jrb/Scripts]]
**
**	You are advised to import this script to your
** monobook.js page - AVOID CREATING YOUR OWN 
** VERSION WHERE POSSIBLE.
**
**	Instructions for this script can be found at
** [[User:Ale_jrb/Scripts]] - refer to this for
** setting details.
\* ============================================== */

// NB. this script relies on [[User:Ale_jrb/Scripts/waLib.js]].
// the following settings are used in this script:
if (notifyByDefaultDec == null) 	var notifyByDefaultDec	= true;			// whether to check the 'notify tagger' box by default when declining a speedy deletion request
if (notifyByDefaultDel == null) 	var notifyByDefaultDel	= false;		// whether to check the 'notify tagger' box by default when changing a speedy deletion rationale
if (notifyByDefaultPrd == null) 	var notifyByDefaultPrd	= true;			// whether to check the 'notify tagger' box by default when converting a speedy deletion to PROD
if (notifyByDefaultNew == null) 	var notifyByDefaultNew	= true;			// whether to check the 'use newbie message' box by default
if (notifyLimit == null) 			var notifyLimit			= 12;			// how many revisions should be retrieved when determining who tagged the page
if ( notifyTemplate == null )		var notifyTemplate		= 'User:Ale_jrb/Scripts/CSDHelper'; // the template that should be substituted for notification messages

if (redirectAfterDel == null) 		var redirectAfterDel	= mw.config.get ( 'wgScript' ) + '?title=Category:Candidates_for_speedy_deletion&action=purge#Pages_in_category'; // where to redirect after deletion
if (myDeleteReasons == null)		var myDeleteReasons		= new Array(); 	// any addition speedy deletion reasons to add to the list

if (logOnDecline == null)			var logOnDecline		= false;
if (logOnDeclinePath == null)		var logOnDeclinePath	= '';
if (overwriteDeclineReasons == null)var overwriteDeclineReasons	= false; 		// whether to overwrite the in-build decline reasons with the user defined ones
if (overwriteDeleteReasons == null)var overwriteDeleteReasons	= false; 		// whether to overwrite the in-build delete reasons with the user defined ones
if (myDeclineReasons == null)		var myDeclineReasons		= new Array(); 	// any addition speedy deletion decline reasons to add to the list
if (myDeclineListing == null)		var myDeclineListing		= '%CRITERION%: %REASON%' // the appearance of the option in the drop-down box
if (myDeclineSummary == null)		var myDeclineSummary		= 'Speedy deletion %ACTION%. Criterion %CRITERION% does not apply: %REASON%';	// the summary to use when removing a deletion tag from a page because it has been declined or contested
if (myDeclineSummarySpecial == null)var myDeclineSummarySpecial	= 'Speedy deletion %ACTION%. %REASON%';	// the summary to use when removing a deletion tag from a page IN A SEPCIAL CASE. NOTE: %CRITERION% will be blank!

/* ============================================== *\
** WikiApps JavaScript GUI, API & AJAX Library
**      for MediaWiki v1.13 and above
**
** Created (c) by Alex Barley [[User:Ale_jrb]]
**		version 1.0.10m
\* ============================================== */

// mediawiki objects
function wa_mediawikiUser ( who ) {
	if ( ! who ) return false;
	var waUserObj			= this;
	this.rootApi			= wgScriptPath + '/api.php';
	
	this.getUserGroup = function(group, onDone) {
		if (who == 'self') {
			for (var i = 0; i < wgUserGroups.length; i ++) {
				if (wgUserGroups[i] == group) return true;
			}
			return false;
		} else {
			this.ajax 				= new wa_ajaxcall();
			this.ajax.requestUrl 	= this.rootApi + '?format=xml&action=query&list=users&usprop=groups&ususers=' + encodeURIComponent(who);
			this.ajax.doRequest		(function() { 
										wa_mediawikiUser.apiResponse = waUserObj.ajax.response;
										waUserGroups = wa_mediawikiUser.apiResponse.getElementsByTagName('g');
										for (var i = 0; i < waUserGroups.length; i ++) {
											if (waUserGroups[i].childNodes[0].nodeValue == group) { onDone(true); return true; }
										}
										onDone(false);
									
									});
		}
	}
	
	this.getUserContribs = function ( number, onDone ) {
		if (who == 'self') 		{ var user = wgUserName; } else { var user = who; }
		if (number == 0) 		number = 1;
		if (number > 100) 		number = 100;
		
		this.ajax 				= new wa_ajaxcall();
		this.ajax.requestUrl 	= this.rootApi + '?format=xml&action=query&list=usercontribs&uclimit=' + number + '&ucuser=' + encodeURIComponent(who) + '&ucprop=ids|title|timestamp|comment';
		this.ajax.doRequest		(function() { 
									wa_mediawikiUser.apiResponse = waUserObj.ajax.response; 
									
									waUserObj.editDetails = [];
									if (wa_mediawikiUser.apiResponse.getElementsByTagName('item').length == 0) {
										waUserObj.editDetails[0] = new Object();
										waUserObj.editDetails[0]['pageid'] 		= false; waUserObj.editDetails[0]['revid'] 		= false;
										waUserObj.editDetails[0]['title'] 		= false; waUserObj.editDetails[0]['timestamp'] 	= false;
									}
									for (var i = 0; i < wa_mediawikiUser.apiResponse.getElementsByTagName('item').length; i ++) { // for each revision
										var tempData = wa_mediawikiUser.apiResponse.getElementsByTagName('item')[i];
										waUserObj.editDetails[i] = new Object();
										
										waUserObj.editDetails[i]['pageid'] 		= tempData.getAttribute('pageid');
										waUserObj.editDetails[i]['revid'] 		= tempData.getAttribute('revid');
										waUserObj.editDetails[i]['title'] 		= tempData.getAttribute('title');
										waUserObj.editDetails[i]['timestamp'] 	= tempData.getAttribute('timestamp');
									}
									
									onDone();
									
								});
	}
	
	this.getUserLogs = function(number, onDone) {
		if (who == 'self') 		{ var user = wgUserName; } else { var user = who; }
		if (number == 0) 		number = 1;
		if (number > 100) 		number = 100;
		
		this.ajax 				= new wa_ajaxcall();
		this.ajax.requestUrl 	= this.rootApi + '?format=xml&action=query&list=logevents&lelimit=' + number + '&leuser=' + encodeURIComponent(who) + '&leprop=ids|title|timestamp|comment|type';
		this.ajax.doRequest		(function() { 
									wa_mediawikiUser.apiResponse = waUserObj.ajax.response; 
									
									waUserObj.logDetails = [];
									if (wa_mediawikiUser.apiResponse.getElementsByTagName('item').length == 0) {
										waUserObj.logDetails[0] = new Object();
										waUserObj.logDetails[0]['pageid'] 		= false; waUserObj.logDetails[0]['logid'] 		= false;
										waUserObj.logDetails[0]['title'] 		= false; waUserObj.logDetails[0]['timestamp'] 	= false;
										waUserObj.logDetails[0]['type'] 		= false; waUserObj.logDetails[0]['action'] 		= false;
									}
									for (var i = 0; i < wa_mediawikiUser.apiResponse.getElementsByTagName('item').length; i ++) { // for each revision
										var tempData = wa_mediawikiUser.apiResponse.getElementsByTagName('item')[i];
										waUserObj.logDetails[i] = new Object();
										
										waUserObj.logDetails[i]['pageid'] 		= tempData.getAttribute('pageid');
										waUserObj.logDetails[i]['logid'] 		= tempData.getAttribute('logid');
										waUserObj.logDetails[i]['title'] 		= tempData.getAttribute('title');
										waUserObj.logDetails[i]['timestamp'] 	= tempData.getAttribute('timestamp');
										waUserObj.logDetails[i]['type'] 		= tempData.getAttribute('type');
										waUserObj.logDetails[i]['action'] 		= tempData.getAttribute('action');
									}
									
									onDone();
									
								});
	}
	
	// construct
	if (who == 'self') {
		this.isSysop = this.getUserGroup('sysop');
		this.isRollback = this.getUserGroup('rollback');
		this.isAutoconfirmed = this.getUserGroup('autoconfirmed');
	}
}
var waUser = new wa_mediawikiUser('self');

function wa_mediawikiApi() {
	// this function handles a multitude of Wiki API calls.
	var wa_mediaWiki		= this;	// callback
	
	this.rootApi			= wgScriptPath + '/api.php';
	
	this.apiResponse		= false; 			// actual response from API - allows manual parsing, if desired
	this.apiPage			= new Object;
	this.apiPage['plain']	= false; 			// the provided name of the last page called in this object
	this.apiPage['enc']		= false; 			// the encoded name of the last page called in this object
	this.data				= new Object; 		// general response of the method called - associative array filled with requested data
	this.ajax				= false; 			// the ajax object - allows manual access to the ajax object/functions, if desired
	
	this.onCompleteAction 	= function() { return true; } 	// onCompleteAction is the function that will be called whenever the current operation is complete
	this.internalOnComplete	= function() { return true; } 	// internalOnComplete is the function that will be called (callback) when an internal operation completes
	this.internalRequest	= false;						// internalRequest specifies whether this is an internal callback or not
	
	this.getToken = function(token, page) {
		// set vars
		this.apiPage['plain'] 		= page;
		if (page.indexOf('%20') > -1) { this.apiPage['enc'] = page; } else { this.apiPage['enc'] = encodeURIComponent(page); }
		page 						= this.apiPage['enc'];
		token 						= token.toLowerCase();
		
		// verification
		if (token.match(/(edit|delete|protect|move|block|unblock|rollback)/i) == null) return false;
		
		// check rollback
		if (token.match(/rollback/i) != null) {
			this.getPage(page, 1, 'rollback-int');
			
			return true;
		}
		
		// go
		var requestUrl 			= this.rootApi + '?action=query&format=xml&prop=info&inprop=talkid&intoken=' + token + '&titles=' + page;
		this.ajax 				= new wa_ajaxcall();
		this.ajax.requestUrl 	= requestUrl;
		this.ajax.doRequest		(function() { 
									var internal = [];
									
									wa_mediaWiki.apiResponse 			= wa_mediaWiki.ajax.response; 
									wa_mediaWiki.data['token']			= [];
									wa_mediaWiki.data['token'].push		(wa_mediaWiki.apiPage['enc'], encodeURIComponent(wa_mediaWiki.apiResponse.getElementsByTagName('page')[0].getAttribute(token+'token')));
									if (wa_mediaWiki.apiResponse.getElementsByTagName('page')[0].getAttribute('missing') != null) { internal[0] = true; } else { internal[0] = false; }
									if (wa_mediaWiki.apiResponse.getElementsByTagName('page')[0].getAttribute('talkid') != null) { 
										internal[1] = wa_mediaWiki.apiResponse.getElementsByTagName('page')[0].getAttribute('talkid'); } else { internal[1] = false; }
									
									if (wa_mediaWiki.internalRequest == false) {wa_mediaWiki.onCompleteAction(wa_mediaWiki.data); } else 
										{ wa_mediaWiki.internalRequest = false; wa_mediaWiki.internalOnComplete(wa_mediaWiki.data, internal); }
								});
		
		return true;
	}
	
	this.getPage = function(page, revisions, properties) {
		// set vars
		if (properties == 'rollback-int') {
			properties = 'rollback';
		} else {
			this.apiPage['plain'] 	= page;
			if (page.indexOf('%20') > -1) { this.apiPage['enc'] = page; } else { this.apiPage['enc'] = encodeURIComponent(page); }
			page 					= this.apiPage['enc'];
		}
		
		// verification
		if (revisions > 500) revisions = 500;
		if (properties.match(/^(?:(?:ids|flag|timestamp|user|size|comment|content|rollback)\|?)*$/i) == null) properties = 'ids|user|content';
		
		// go
		if (properties.match(/rollback/i) != null) {
			var rollbackRequest 	= true;
			
			properties = properties.replace(/\|rollback\|/ig, '');
			properties = properties.replace(/rollback\|/ig, '');
			properties = properties.replace(/\|rollback/ig, '');
			properties = properties.replace(/\|\|/ig, '|');
			properties = properties.replace(/rollback/ig, '');
			
			var requestUrl 			= this.rootApi + '?action=query&format=xml&prop=revisions&titles=' + page + '&rvtoken=rollback&rvprop=' + properties + '&rvlimit=' + revisions;
		} else {
			var rollbackRequest 	= false;
			var requestUrl 			= this.rootApi + '?action=query&format=xml&prop=revisions&titles=' + page + '&rvprop=' + properties + '&rvlimit=' + revisions;
		}
		
		this.ajax 				= new wa_ajaxcall();
		this.ajax.requestUrl 	= requestUrl;
		this.ajax.doRequest		(function() { 
									wa_mediaWiki.apiResponse 			= wa_mediaWiki.ajax.response; 
									wa_mediaWiki.data					= new Object;
									
									if (wa_mediaWiki.apiResponse.getElementsByTagName('rev')[0] == null) { wa_mediaWiki.data['page'] = new Object; wa_mediaWiki.data['page']['status'] = 'E'; } else {
										wa_mediaWiki.data['page'] 				= new Object; 
										wa_mediaWiki.data['page']['revisions']	= [];
										wa_mediaWiki.data['page']['status'] 	= 'OK';
										
										for (var i = 0; i < wa_mediaWiki.apiResponse.getElementsByTagName('rev').length; i ++) { // for each revision
											wa_mediaWiki.data['page']['revisions'][i] = new Object;
										
											// get details
											if (properties.match(/ids/i) != null) wa_mediaWiki.data['page']['revisions'][i]['id'] 				
														= wa_mediaWiki.apiResponse.getElementsByTagName('rev')[i].getAttribute('revid');
											if (properties.match(/size/i) != null) wa_mediaWiki.data['page']['revisions'][i]['size'] 			
														= wa_mediaWiki.apiResponse.getElementsByTagName('rev')[i].getAttribute('size');
											if (properties.match(/user/i) != null) wa_mediaWiki.data['page']['revisions'][i]['user'] 			
														= wa_mediaWiki.apiResponse.getElementsByTagName('rev')[i].getAttribute('user');
											if (properties.match(/comment/i) != null) wa_mediaWiki.data['page']['revisions'][i]['comment']		 
														= wa_mediaWiki.apiResponse.getElementsByTagName('rev')[i].getAttribute('comment');
											if (properties.match(/timestamp/i) != null) wa_mediaWiki.data['page']['revisions'][i]['timestamp'] 	
														= wa_mediaWiki.apiResponse.getElementsByTagName('rev')[i].getAttribute('timestamp');
											if ( (rollbackRequest == true) && (i == 0) ) { 
												wa_mediaWiki.data['token']		= [];
												wa_mediaWiki.data['token'][1] 	= encodeURIComponent(wa_mediaWiki.apiResponse.getElementsByTagName('rev')[i].getAttribute('rollbacktoken'));
												
												var internal = [];
												if (wa_mediaWiki.apiResponse.getElementsByTagName('page')[0].getAttribute('missing') != null) { internal[0] = true; } else { internal[0] = false; }
											}
										
											// get content
											if (properties.match(/content/i) != null) {
												wa_mediaWiki.data['page']['revisions'][i]['content'] = '';
												for (var j = 0; j < wa_mediaWiki.apiResponse.getElementsByTagName('rev')[i].childNodes.length; j ++) {
													wa_mediaWiki.data['page']['revisions'][i]['content'] += wa_mediaWiki.apiResponse.getElementsByTagName('rev')[i].childNodes[j].nodeValue;
												}
											}
										}
									}
									
									if (wa_mediaWiki.internalRequest == false) {wa_mediaWiki.onCompleteAction(wa_mediaWiki.data); } else 
										{ if (typeof internal == 'undefined') { var internal = false; } wa_mediaWiki.internalRequest = false; wa_mediaWiki.internalOnComplete(wa_mediaWiki.data, internal); }
								});
			
		return true;
	}
	
	this.getLastNotUser = function(page, excludeWho) {
		// this function gets the username of the most recent editor to the page who ISN'T excludeWho
		if (typeof excludeWho != 'string') return false;
		
		this.apiPage['plain'] 	= page;
		if (page.indexOf('%20') > -1) { this.apiPage['enc'] = page; } else { this.apiPage['enc'] = encodeURIComponent(page); }
		page 					= this.apiPage['enc'];
		
		var requestUrl 			= this.rootApi + '?action=query&format=xml&prop=revisions&titles=' + page + '&rvprop=user&rvlimit=1&rvexcludeuser=' + encodeURIComponent(excludeWho);
		this.ajax 				= new wa_ajaxcall();
		this.ajax.requestUrl 	= requestUrl;
		this.ajax.doRequest		(function() { 
									var ret;
									wa_mediaWiki.apiResponse 			= wa_mediaWiki.ajax.response; 
									if (wa_mediaWiki.apiResponse.getElementsByTagName('rev')[0] == null) { 
										// an error occurred - sort it
										if (wa_mediaWiki.apiResponse.getElementsByTagName('rev')[0].getAttribute('missing') != null) { ret = 'missing'; } else {
											ret = 'no-other-user'; }
									} else {
										ret = wa_mediaWiki.apiResponse.getElementsByTagName('rev')[0].getAttribute('user');
									}
									wa_mediaWiki.onCompleteAction(ret);
								});
		return true;
	}
	
	this.editPage = function(title, text, summary, minor, addWhere, token) {
		// shortcut to performAction('edit')
		var details = [];
		details[0]	= text;
		details[1]	= summary;
		details[2]	= minor;
		details[3]	= addWhere;
		
		wa_mediaWiki.performAction('edit', title, details, token);
		return true;
	}
	
	this.deletePage = function(title, reason, token) {
		// shortcut to performAction('delete')
		
		var details = reason;
		
		wa_mediaWiki.performAction('delete', title, details, token);
		return true;
	}
	
	this.rollbackPage = function(title, user, summary, token) {
		// shortcut to performAction('rollback')
		// NB. a user must be provided; rollback will only occur if they are the last editor to the page.
		
		var details = [];
		details[0] 	= user;
		if (summary !== false) { details[1] = summary; } else { details[1] = ''; }
		
		wa_mediaWiki.performAction('rollback', title, details, token);
	}
	
	this.performAction = function(action, target, details, token, internal) {
		// this module only accepts block, edit, rollback and deletion requests. Other actions must be performed manually, though the getToken method allows the
		// easy retrieval of the correct token for almost any action.
		// --
		// NB. internal is an internal parameter, allowing library to communicate internal data between functions. Modifying it will result in unexpected actions.
		
		// verification
		if ( (token == null) || (token == '') || (token == 0) || (token == false) ) {
			// get a token
			this.internalRequest = true;
			this.internalOnComplete = function(passToken, internal) { wa_mediaWiki.performAction(action, target, details, passToken['token'][1], internal); };
			this.getToken(action, target);
			return false;
		}
		
		// set vars
		this.apiPage['plain'] 			= target;
		if (target.indexOf('%20') > -1) { this.apiPage['enc'] = target; } else { this.apiPage['enc'] = encodeURIComponent(target); }
		target 							= this.apiPage['enc'];
		if (details == null) 			{ details = ''; } else if (typeof(details) == 'object') { for (x in details) { if (typeof details[x] == 'string') details[x] = encodeURIComponent(details[x]); } } else { details = encodeURIComponent(details); }
		
		// go
		switch ( action ) {
			case 'edit': // shortcut to this method from editPage method
				if (typeof(details) != 'object') 		return false; 		// we need an array: 
				if (typeof(details[0]) != 'string') 	details[0] = '';	//details[0] - text
				if (typeof(details[1]) != 'string') 	details[1] = '';	//details[1] - edit summary
				if (typeof(details[2]) != 'boolean') 	details[2] = false;	//details[2] - minor edit?
				if (typeof(details[3]) != 'string') 	details[3] = 'text';//details[3] - appendtext, prependtext, text
				
				if (details[2] == true) { var minor = 'minor=true'; } else { var minor = 'notminor=true'; }
				if (internal[0] == true) details[3] = 'text';
				if (details[3] == 'appendtext') { details[0] = encodeURIComponent('\n\n') + details[0]; }
			
				this.ajax					= new wa_ajaxcall();
				this.ajax.requestUrl		= this.rootApi;
				this.ajax.postParams		= 'format=xml&action=edit&title=' + target + '&summary=' + details[1] + '&' + details[3] + '=' + details[0] + '&' + minor + '&token=' + token;
				this.ajax.post				(function() {
												wa_mediaWiki.apiResponse = wa_mediaWiki.ajax.response;
												wa_mediaWiki.onCompleteAction();
												return true;
											});
				break;
				
			case 'rollback':
				if ( internal[0] == true ) { wa_mediaWiki.onCompleteAction(false); return false; }
			
				if ( (details[1] == '') || (details[1] == null) || (typeof details[1] == 'undefined') ) { var useCustomSummary = false; } else { var useCustomSummary = true; }
				
				var params = 'format=xml&action=rollback&title=' + target + '&user=' + details[0] + '&token=' + token;
				if (useCustomSummary == true) params += '&summary=' + details[1];
				
				this.ajax					= new wa_ajaxcall();
				this.ajax.requestUrl		= this.rootApi;
				this.ajax.postParams		= params;
				this.ajax.post				(function() {
												wa_mediaWiki.apiResponse = wa_mediaWiki.ajax.response;
												if ( (wa_mediaWiki.apiResponse.getElementsByTagName('error')[0] == null) || (wa_mediaWiki.apiResponse.getElementsByTagName('error')[0].getAttribute('code') == null) )
													{ 
														//if (wa_mediaWiki.apiResponse.getElementsByTagName('rollback')[0].getAttribute('revid') == wa_mediaWiki.apiResponse.getElementsByTagName('rollback')[0].getAttribute('old_revid')) {
														//	var r = false;
														//} else {
															var r = wa_mediaWiki.apiResponse.getElementsByTagName('rollback')[0].getAttribute('revid');
														//}
													} else { var r = false; }
													  
												wa_mediaWiki.onCompleteAction(r);
												return true;
											});
				break;
			
			case 'delete':
				// we have the required token. Perform the deletion!
				if (internal[0] == true) 	return false;
				
				this.ajax					= new wa_ajaxcall();
				this.ajax.requestUrl		= this.rootApi;
				this.ajax.postParams		= 'format=xml&action=delete&title=' + target + '&reason=' + details + '&token=' + token;
				this.ajax.post				(function() {
												wa_mediaWiki.apiResponse = wa_mediaWiki.ajax.response;
												wa_mediaWiki.onCompleteAction(internal[1]);
												return true;
											});
				return true;
				break;
				
			case 'block':
				// additional verification for blocking
				if (typeof(details) != 'object') 		return false; 		// we need an array: 
				if (typeof(details[0]) != 'string') 	return false;		//details[0] - expiry as string - you must provide this, or the block will not happen
				if (typeof(details[1]) != 'boolean') 	return false;		//details[1] - anonymous only - you must provide this, or the block will not happen
				if (typeof(details[2]) != 'string') 	details[2] = '';	//details[2] - reason as string 
				if (typeof(details[3]) != 'boolean') 	details[3] = true;	//details[3] - prevent account creation
				if (typeof(details[4]) != 'boolean') 	details[4] = true;	//details[4] - autoblock - default, hardblock
				
				// build valid syntax
				if (details[1] == true) { var anonOnly = '&anononly'; } else { var anonOnly = ''; }
				if (details[3] == true) { var createAccount = '&nocreate'; } else { var createAccount = ''; }
				if (details[4] == true) { var autoblock = '&autoblock'; } else { var autoblock = ''; }
			
				// we have the required token
				this.ajax					= new wa_ajaxcall();
				this.ajax.requestUrl		= this.rootApi;
				this.ajax.postParams		= 'format=xml&action=block&user=' + target + '&expiry=' + details[0] + '&reason=' + details[2] + anonOnly + createAccount + 
														autoblock + '&token=' + token;
				this.ajax.post				(function() {
												wa_mediaWiki.apiResponse = wa_mediaWiki.ajax.response;
												wa_mediaWiki.onCompleteAction();
												return true;
											});
				break;
			
			default:
				return false;
				break;
		}
	}
}

// non-gui objects
function wa_ajaxcall () {
	var waMyAjax = this;
	
	this.requestType		= 'GET';
	this.responseType		= 'xml';
	this.requestUrl			= '';
	waMyAjax.pageRequest 	= false;
	
	this.postParams			= '';
	
	this.response 			= false;
	
	this.abort = function () {
		if ( waMyAjax.pageRequest != false ) {
			waMyAjax.pageRequest.abort ();
			return true;
		}
	}
	
	this.post = function ( runOnComplete ) {
		waMyAjax.requestType = 'POST';
		waMyAjax.doRequest ( runOnComplete );
	}
	
	this.get = function ( runOnComplete ) {
		waMyAjax.requestType = 'GET';
		waMyAjax.doRequest ( runOnComplete );
	}
	
	this.doRequest = function ( runOnComplete ) {
		if ( this.requestUrl == '' ) return false;
		if ( window.XMLHttpRequest ) { // if good browser
			waMyAjax.pageRequest = new XMLHttpRequest ();
		}
		else if ( window.ActiveXObject ) { // if IE
			try { // try request 1
				waMyAjax.pageRequest = new ActiveXObject ( "Msxml2.XMLHTTP" );
			} 
			catch ( e ) { // it failed.
				try {	// try request 2
					waMyAjax.pageRequest = new ActiveXObject ( "Microsoft.XMLHTTP" );
				}
				catch ( e ) { return false; }
			}
		}
		else
		{
			return false;
		}
		
		
		waMyAjax.pageRequest.onreadystatechange = function () {
			if ( waMyAjax.pageRequest.readyState == 4 ) { 
				if ( waMyAjax.pageRequest.status == 200 ) {
					if ( waMyAjax.responseType == 'xml' ) {
						waMyAjax.response = waMyAjax.pageRequest.responseXML;
					} else {
						waMyAjax.response = waMyAjax.pageRequest.responseText;
					}
					if ( waMyAjax.pageRequest.responseXML ) waMyAjax.responseXML = waMyAjax.pageRequest.responseXML;
					if ( waMyAjax.pageRequest.responseText ) waMyAjax.responseText = waMyAjax.pageRequest.responseText;
					
					runOnComplete ();
				}
			}
		}
		
		if ( this.requestType == 'GET' ) {
			// do get request
			waMyAjax.pageRequest.open('GET', this.requestUrl, true);
			switch (this.responseType) {
				default: case 'xml':
					if ( waMyAjax.pageRequest.overrideMimeType ) { waMyAjax.pageRequest.overrideMimeType ( 'text/xml' ); } else {
						waMyAjax.pageRequest.setRequestHeader ( 'Content-type', 'text/xml' ); }
					break;
					
				case 'html':
					if ( waMyAjax.pageRequest.overrideMimeType ) waMyAjax.pageRequest.overrideMimeType ( 'text/html' );
					break;
			}
			waMyAjax.pageRequest.send ( null );
		}
		else if ( this.requestType == 'POST' )
		{
			// do post request
			waMyAjax.pageRequest.open ( 'POST', this.requestUrl, true );
			waMyAjax.pageRequest.setRequestHeader ( "Content-type", "application/x-www-form-urlencoded" );
			waMyAjax.pageRequest.setRequestHeader ( "Content-length", this.postParams.length );
			waMyAjax.pageRequest.setRequestHeader ( "Connection", "close" );
			waMyAjax.pageRequest.send ( this.postParams );
		}
		else
		{ /* unrecognised */ }
		
	};
	
	return true;
}

// drawing objects
function wa_document () {
	// document -- main interface representation. grabs required document ids for easy use. not used directly. sits above wiki interface, to allow override
	
	// vars
	this.wk_base 				= document.getElementsByTagName('body')[0];
	this.wk_content_base 		= document.getElementById('bodyContent');
	
	this.wk_top_links_port		= document.getElementById('p-personal');
	this.wk_top_links			= document.getElementById('pt-userpage').parentNode;
	this.wk_pref_link			= document.getElementById('pt-preferences');
	
	this.root					= wgArticlePath;
	this.user					= wgUserName;
	this.page					= wgTitle;
	
	return true;
}

function wa_window ( parent_opt ) {
	// window -- main interface compontent. wikiapps gui is built of windows. they're esentially divs, attached to a parent div. child of document.
		// providing a parent_opt object (must be another window) will result in that object being the parent node. otherwise, the default document 
		// object is used.
	
	// vars - enable quick window creation by setting CSS style defaults
	this.win_fill 		= false; 			this.win_bd_wd 		= 0;
	this.win_top 		= 0; 				this.win_bd_rt		= '';
	this.win_left 		= 0; 				this.win_bd_lf		= '';
	this.win_width 		= 0; 				this.win_bd_bt		= '';
	this.win_height 	= 0; 				this.win_bd_tp		= '';
	this.win_bg 		= '#000000'; 		this.win_class 		= '';
	this.win_bd 		= '#000000'; 		this.win_alpha 		= 1;
	this.win_disp		= 'block';			this.win_obj 		= document.createElement ( 'div' );
	this.win_z			= '9999999';
	this.win_pos		= 'absolute';		this.win_handler	= 'click';
	this.win_func		= function() {  };	this.win_fade		= 'visible';
	this.win_attached	= false;			this.win_cursor		= 'auto';
	this.win_padding	= 3;				this.win_content	= '';
	this.win_margin		= 0;				this.win_id 		= '';
	this.win_talign		= 'left';			this.win_overflow	= 'visible';
	this.win_right		= false;			this.win_bottom		= false;
	this.win_maintfill	= true;				this.hidden			= false;
	this.win_fontsize	= 10;
	
	if ( parent_opt == null ) { this.parentObj = this.wk_base; } else { 
		if ( typeof parent_opt.win_obj !== 'undefined' ) {
			this.parentObj = parent_opt.win_obj; 
		} else if ( parent_opt != null ) {
			this.parentObj = parent_opt;
		}
	}
	
	
	
	// methods
	this.applyAll = function() {
			// applyAll - method
			// applyAll applies current settings to the window object. if createNew is set as true, a new window will be created and appended to the base.
			// if not provided, the current object's settings will be updated. special behaviour: setting win_fill to true will cause the window to 
			// automatically maintain the shape of the window. setting it to false will disable auto updating, and unfill the screen.
		this.win_obj.style.position 						= this.win_pos;
		this.win_obj.style.zIndex 							= this.win_z;
		
		// special behaviour - fill screen, usage background cover etc. only 1 per page
		if (this.win_fill == true) {
			this.win_obj.style.position						= 'fixed';
			this.win_obj.style.top							= '0px';
			this.win_obj.style.left							= '0px';
			this.win_obj.style.width						= document.documentElement.clientWidth + 'px';
			this.win_obj.style.height						= document.documentElement.clientHeight + 'px';
			
			// fill screen - attach updater to window resize
			var wa_selfFill		= this;
			if (this.win_maintfill == true) {
				if (window.addEventListener) {
					window.addEventListener('resize', function() {  
						wa_selfFill.applyAll();
					}, false);
				}
				else
				{
					window.attachEvent('onresize', function() {  
						wa_selfFill.applyAll();
					});
				}
			}
		}
		else
		{
			if (this.win_right !== false) { this.win_obj.style.right = this.win_right + 'px'; } else { this.win_obj.style.left = this.win_left + 'px'; }
			if (this.win_bottom !== false) { this.win_obj.style.bottom = this.win_bottom + 'px'; } else { this.win_obj.style.top = this.win_top + 'px'; }
			if (this.win_width != 0) { this.win_obj.style.width	 = this.win_width 	+ 'px'; } else { this.win_obj.style.width 	= 'auto'; }
			if (this.win_height != 0) { this.win_obj.style.height= this.win_height 	+ 'px'; } else { this.win_obj.style.height 	= 'auto'; }
		}
		
		if (this.win_obj.addEventListener) 					{ this.win_obj.addEventListener(this.win_handler, this.win_func, false); } 
																else { this.win_obj.attachEvent('on'+this.win_handler, this.win_func); }
		this.win_obj.style.backgroundColor					= this.win_bg;
		this.win_obj.style.padding							= this.win_padding	+ 'px';
		if (this.win_margin != 'auto') { this.win_obj.style.margin = this.win_margin + 'px'; } else { this.win_obj.style.margin = 'auto'; }
		this.win_obj.style.border							= this.win_bd_wd 	+ 'px solid ' + this.win_bd;
		if (this.win_bd_rt != '') this.win_obj.style.borderRight 	= this.win_bd_rt;
		if (this.win_bd_tp != '') this.win_obj.style.borderTop 		= this.win_bd_tp;
		if (this.win_bd_bt != '') this.win_obj.style.borderBottom 	= this.win_bd_bt;
		if (this.win_bd_lf != '') this.win_obj.style.borderLeft 	= this.win_bd_lf;
		this.win_obj.style.cursor							= this.win_cursor;
		this.win_obj.style.overflow							= this.win_overflow;
		this.win_obj.style.opacity							= this.win_alpha.toString();                      
    	this.win_obj.style.MozOpacity						= this.win_alpha.toString();                   
    	this.win_obj.style.filter							= 'alpha(opacity='+ (this.win_alpha * 100) +')';
		this.win_obj.style.textAlign						= this.win_talign;
		this.win_obj.style.fontSize							= this.win_fontsize	+ 'px';
		// compatibility with 'hide' module
		if (this.hidden == false) this.win_obj.style.display = this.win_disp;
		
		this.win_obj.innerHTML 								= this.win_content;
		
		if (this.win_attached == false) { this.parentObj.appendChild(this.win_obj); this.win_attached = true; }
		
		
		return true; // successful init
	};
	
	// special methods - effects for windows
	this.center = function(centerPositions, maintainCenter, offset) {
		// center - places the window in the centre of the user's screen. set maintainCenter to true and this position will be kept even if
		// the window is resized.
		if (((this.win_pos != 'fixed') && (this.win_pos != 'absolute')) || (this.win_fill == true)) { return false; }
		
		var screenWidth 			= document.documentElement.clientWidth;
		var screenHeight 			= document.documentElement.clientHeight;
		var myWidth 				= this.win_obj.offsetWidth;
		var myHeight 				= this.win_obj.offsetHeight;
		
		var leftPos					= ((screenWidth / 2) - (myWidth / 2));
		var topPos					= ((screenHeight / 2) - (myHeight / 2));
		
		if (typeof offset == 'object') {
			leftPos += offset[0];
			topPos	+= offset[1];
		}
		
		if ((centerPositions == 'left') || (centerPositions == 'both')) this.win_obj.style.left = leftPos + 'px';
		if ((centerPositions == 'top')  || (centerPositions == 'both')) this.win_obj.style.top 	= topPos + 'px';
		
		if (maintainCenter == true) {
			var wa_selfCenter = this;
			if (window.addEventListener) {
				window.addEventListener('resize', function() {  
					wa_selfCenter.center(centerPositions);
				}, false);
			}
			else
			{
				window.attachEvent('onresize', function() {  
					wa_selfCenter.center(centerPositions);
				});
			}
		}
		
		return true;
	};
	
	this.fade = function(fadeSpeed, opacityLimit, runWhenFinished) {
		// fade - toggle method - the object will be faded in if currently hidden, and faded out if currently visible.
		
		var stepDefault	= 20;
		var stepNumber	= fadeSpeed * stepDefault;
		var stepSize	= 1 / stepNumber;
		var wa_selfFade = this;
		
		if (opacityLimit == null) opacityLimit = 0;
		if (interval != null) clearInterval(interval);
		
		// user call - prepare fade
		if (this.win_fade == 'visible') {
			// start fade out
			var tempAlpha = 1; // just in case
			wa_selfFade.win_alpha					= tempAlpha;
			wa_selfFade.win_obj.style.opacity 		= tempAlpha;
			wa_selfFade.win_obj.style.MozOpacity	= tempAlpha;
			wa_selfFade.win_obj.style.filter		= 'alpha(opacity='+ (tempAlpha * 100) +')';
			
			var interval = setInterval(function() {
				tempAlpha = parseFloat(wa_selfFade.win_obj.style.opacity);
				tempAlpha = tempAlpha - stepSize;
				
				wa_selfFade.win_alpha					= tempAlpha;
				wa_selfFade.win_obj.style.opacity 		= tempAlpha;
				wa_selfFade.win_obj.style.MozOpacity	= tempAlpha;
				wa_selfFade.win_obj.style.filter		= 'alpha(opacity='+ (tempAlpha * 100) +')';
				
				if (tempAlpha <= (0 + opacityLimit)) {
					tempAlpha = (0 + opacityLimit);
					
					wa_selfFade.win_alpha					= tempAlpha;
					wa_selfFade.win_obj.style.opacity 		= tempAlpha;
					wa_selfFade.win_obj.style.MozOpacity	= tempAlpha;
					wa_selfFade.win_obj.style.filter		= 'alpha(opacity='+ (tempAlpha * 100) +')';
					
					wa_selfFade.win_obj.style.display	= 'none';
					wa_selfFade.win_fade				= 'invisible';
					
					if (runWhenFinished != null) runWhenFinished();
					clearInterval				(interval);
				}
			}, (1000 / stepDefault));
		}
		else
		{
			// start fade in
			var tempAlpha = 0; // just in case
			wa_selfFade.win_alpha					= tempAlpha;
			wa_selfFade.win_obj.style.opacity 		= tempAlpha;
			wa_selfFade.win_obj.style.MozOpacity	= tempAlpha;
			wa_selfFade.win_obj.style.filter		= 'alpha(opacity='+ (tempAlpha * 100) +')';
			
			this.win_obj.style.display	= 'block';
			
			var interval = setInterval(function() {
				tempAlpha = parseFloat(wa_selfFade.win_obj.style.opacity);
				tempAlpha = tempAlpha + stepSize;
				
				wa_selfFade.win_alpha					= tempAlpha;
				wa_selfFade.win_obj.style.opacity 		= tempAlpha;
				wa_selfFade.win_obj.style.MozOpacity	= tempAlpha;
				wa_selfFade.win_obj.style.filter		= 'alpha(opacity='+ (tempAlpha * 100) +')';
				
				if (tempAlpha >= (1 - opacityLimit)) {
					tempAlpha = (1 - opacityLimit);
					
					wa_selfFade.win_alpha					= tempAlpha;
					wa_selfFade.win_obj.style.opacity 		= tempAlpha;
					wa_selfFade.win_obj.style.MozOpacity	= tempAlpha;
					wa_selfFade.win_obj.style.filter		= 'alpha(opacity='+ (tempAlpha * 100) +')';
					
					wa_selfFade.win_fade				= 'visible';
					if (runWhenFinished != null) runWhenFinished();
					clearInterval				(interval);
				}
			}, (1000 / stepDefault));
		}
		
		return true;
	};
	
	this.setLocation = function( toLeft, toTop, domMove ) {
		// this is a shortcut for setting the position of a window
		if ( ! domMove ) { 
			this.win_top = toTop;
			this.win_left = toLeft;
			this.applyAll();
		} else {
			this.win_obj.style.left = toLeft + 'px';
			this.win_obj.style.top = toTop + 'px';
		}
		
		return true;
	};
	
	this.move = function(toTop, toLeft, time, runWhenFinished) {
		// time is the length of time for the move. To be smooth, the move needs about a frame per 5 pixels of movement, regardless of time.
		if (runWhenFinished == null) runWhenFinished = function() {  };
		if ( (toTop == this.win_top) && (toLeft == this.win_left) ) return false;
		
		// first, calculate the distance to be travelled on both sides
		var topDis = toTop - this.win_top;
		var leftDis = toLeft - this.win_left;
		
		// pick the bigger one
		if (Math.abs(topDis) >= Math.abs(leftDis)) { var moveDis = Math.abs(topDis); } else { var moveDis = Math.abs(leftDis); }
		
		// divide by the time to get how many pixels we have to move per second
		var pps = moveDis / time;
		var smoothSteps = ((1 / time) * 4);
		var fps = pps / smoothSteps;
		
		// we know the number of frames per second. Now, we need to know how far to move in each direction per step. Multiply the fps by the time,
		// to get the total steps then divide the total distance by the total steps to get a value.
		var totalSteps = fps * time;
		if (topDis > 0) { var topMove = smoothSteps; } else if (topDis < 0) { var topMove = (smoothSteps * -1); } else { var topMove = 0; }
		if (leftDis > 0) { var leftMove = smoothSteps; } else if (leftDis < 0) { var leftMove = (smoothSteps * -1); } else { var leftMove = 0; }
		
		var wa_selfMove = this;
		var i = 0;
		var interval = setInterval(function() {
			var newTop = wa_selfMove.win_top + topMove;
			var newLeft = wa_selfMove.win_left + leftMove;
			
			wa_selfMove.setLocation(newTop, newLeft);
			
			if (i >= totalSteps) { wa_selfMove.setLocation(toTop, toLeft); clearInterval(interval); runWhenFinished(); }
			i ++;
		}, (1000 / fps));
	};
	
	this.hide = function () {
		this.hidden = true;
		this.win_obj.style.display = 'none';
		
		return true;
	};
	
	this.show = function () {
		this.hidden = false;
		this.win_obj.style.display = 'block';
		
		return true;
	};
	
	this.addScriptEvent = function(eventHandler, eventFunction) {
		if (this.win_obj.addEventListener) { 
			this.win_obj.addEventListener(eventHandler, eventFunction, false); 
		} 
		else 
		{ 
			this.win_obj.attachEvent('on' + eventHandler, eventFunction); 
		}
	};
	
	return true;
}

function wa_element(elementType) {
	// element -- wa building block. elements can be of any type, but if a div, use of wa_window is recommended. element offers a greater level of control, but less automation than
	// 				windows. elementType must be a valid html element.
	if (elementType == null) return false;
	this.ele_obj = document.createElement(elementType);
	
	this.attach = function(attachTo, attachWhere) {
		// attachWhere can be [append, before]. if blank, append is used.
		if (attachTo == null) return false;
		if (attachWhere == null) var attachWhere = 'append';
		
		switch (attachWhere) {
			case 'after':
				attachTo.parentNode.appendChild(this.ele_obj);
				break;
				
			case 'before':
				attachTo.parentNode.insertBefore(this.ele_obj, attachTo);
				break;
			
			default:
				return false;
				break;
		}
		
		return true;
	};
	
	this.addScriptEvent = function(eventHandler, eventFunction) {
		if (this.ele_obj.addEventListener) { 
			this.ele_obj.addEventListener(eventHandler, eventFunction, false); 
		} 
		else 
		{ 
			this.ele_obj.attachEvent('on' + eventHandler, eventFunction); 
		}
	};
	
	this.destroy = function() {
		var selfElement = this.ele_obj;
		selfElement.parentNode.removeChild(selfElement);
		selfElement = null;
		
		return false;
	};
	
	return true;
}

$(document).ready( function() { 
	wa_window.prototype = new wa_document;
	wa_element.prototype = new wa_document;
});


// handy functions
// arrays
function in_array ( needle, haystack, recursive ) {
	if ( recursive == true ) {
		for ( var i in haystack ) { //var i = 0; i < haystack.length; i ++ ) {
			if ( ( typeof haystack[i] == 'object' ) && ( typeof haystack[i] != 'function' ) ) {
				var t = in_array ( needle, haystack[i], true );
				if ( t == true ) return t;
			} else {
				if ( haystack[i] == needle ) return true;
			}
		}
		return false;
	} else {
		if ( typeof haystack != 'object' ) return false;
	
		for ( var i = 0; i < haystack.length; i ++ ) {
			if ( haystack[i] == needle ) return true;
		}
		return false;
	}
}

function sort_array_multi(array, id, direction) {
	// this function sorts a multi-dimentional array into numerical order based
	// on an id field in one of the sets.
		// e.g. [0] = [1, hi]
		// 		[1] = [3, test]
		//		[2] = [2, boo]
	// where [#][0] is the id field would be sorted to [0], [2], [1] is acsending order etc.
	
	if (typeof array != 'object') return false;
	if ( (direction != 'ascending') && (direction != 'descending') ) { direction = 'descending'; }
	if (typeof id != 'number') id = 0;
	
	var index = []; // the index array is what keeps track of the ids in the array before sorting
	for (var i = 0; i < array.length; i ++) {
		index[i] = array[i][id] + '::' + i;
	}
	
	// first, sort the index array into the correct order.
	index.sort(function(a,b) { 
								var ta =  a; var tb = b;
								ta = ta.substr(0, ta.indexOf('::'));
								tb = tb.substr(0, tb.indexOf('::'));
								
								if (direction == 'ascending') { return (ta - tb); } else { return (tb - ta); }
							});
	
	// the index array is now in the right order. build a new array with full content based off the order in the index array.
	var newArray = [];
	for (var i = 0; i < index.length; i ++) {
		var aid = index[i].substr(index[i].indexOf('::') + 2);
		
		newArray[i] = array[aid];
	}
	
	return newArray;
}

function echo_nodes_recursive ( parent ) {
	var nex = '';
	
	if ( ! parent.childNodes ) { 
		if (parent.nodeValue) if (parent.nodeValue != '') return parent.nodeValue; 
		return false; 
	} else
	
	if (parent.childNodes.length == 0) { 
		if (parent.nodeValue) if (parent.nodeValue != '') return parent.nodeValue; 
		return false; 
	}
	
	for (var i = 0; i < parent.childNodes.length; i ++) {
		var nex2 = echo_nodes_recursive(parent.childNodes[i]);
		if (nex2 != false) nex = nex + nex2;
	}
	
	return nex;
}

// internet explorer
function ie_create_document() {
	if (typeof ActiveXObject == 'undefined') return false;
	var implementations = ['Microsoft.XMLDOM', 'Msxml2.DOMDocument.3.0', 'MSXML2.DOMDocument', 'MSXML.DOMDocument'];
	for (var ii in implementations) {
		try {
			var r = new ActiveXObject(implementations[ii]);
			return r;
		} catch (e) {}
	}
	
	return false;
}

function ie_getElementById(parent, id) {
	// parent should be document or an AJAX return etc.
	if (parent.childNodes.length == 0) return false;
	
	for (var i = 0; i < parent.childNodes.length; i ++) {
		if (parent.childNodes[i].nodeType == 1){
			var at = parent.childNodes[i].attributes;
			at = at.getNamedItem('id');
			if (at != null) if (at.value == id) return parent.childNodes[i];
		}

		var t = ie_getElementById(parent.childNodes[i], id);
		if (typeof t == 'object') return t;
	}
	
	return false;
}

function ie_cloneNode(node, cloned) {
	// clone a node to avoid the stupid IE no such interface error
	var current;
	if (!cloned) {
		current = document.createElement(node.nodeName);
	} else {
		current = cloned.appendChild(document.createElement(node.nodeName));
	}
	
	
	for (var j = 0; j < node.attributes.length; j++) {
		current.setAttribute(node.attributes[j].nodeName, node.attributes[j].nodeValue);
	}
	
	for (var i = 0; i < node.childNodes.length; i++) {
		if (node.childNodes[i].nodeType == 1) {
			ie_cloneNode(node.childNodes[i], current);
		} else if (node.childNodes[i].nodeType == 3) {
			var text = document.createTextNode(node.childNodes[i].nodeValue);
			current.appendChild(text);
		}
	}
	
	return current;
} 

// events
function wa_attach(object, eventHandler, eventFunction, useCapture) {
	if ( useCapture == null ) useCapture = false;
	
	if (object.addEventListener) { 
		if ( eventHandler == 'mouseenter' ) {
			object.addEventListener('mouseover', mouseMove( eventFunction ), useCapture); 
		} else if ( eventHandler == 'mouseleave' ) {
			object.addEventListener('mouseout', mouseMove( eventFunction ), useCapture); 
		} else {
			object.addEventListener(eventHandler, eventFunction, useCapture);
		}
	} 
	else 
	{ 
		object.attachEvent('on' + eventHandler, eventFunction); 
	}
}

function mouseMove ( eventFunction ) {
	return function (e) {
		var target = e.relatedTarget;
		if ( ( this === target ) || ( wa_isChild ( target, this) ) ) { return; }
		eventFunction.call ( this, e );
	};
}

function wa_isChild ( childTest, parentTest ) {
	if ( childTest === parentTest ) return false;
	while ( childTest && ( childTest !== parentTest ) ) {
		try { childTest = childTest.parentNode; }
		catch ( e ) { return true; }
	}
	return ( childTest === parentTest );
}

function wa_getObjPos ( object ) {
	var curleft = curtop = 0;
	
	if ( object.offsetParent ) {
		do {
			curleft += object.offsetLeft;
			curtop  += object.offsetTop;
		} while ( object = object.offsetParent );
	}
	
	return [ curleft, curtop ];
}

function mousex ( e ) {
	// read event, return mouse x pos
	if ( !e ) var e = window.event;
	if ( e.pageX ) {
		var r = e.pageX;
	} 
	else if ( e.clientX ) {
		var r = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
	}
	
	return r;
}

function mousey ( e ) {
	// read event, return mouse x pos
	if ( !e ) var e = window.event;
	if ( e.pageY ) {
		var r = e.pageY;
	} 
	else if ( e.clientY ) {
		var r = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
	}
	
	return r;
}

var wikiapps = true;

// Document ready here?

//main script
function csdHelper() {
	this.launch = function() {
		// launch helper. check whether there is a deletion tag on this page.
		if ((document.getElementById('delete-criterion') != null) || (document.body.innerHTML.indexOf('speedy deletion of this page is contested.') > -1)) {
			if (wgNamespaceNumber == 10) { return false; // do nothing!
			} else {
				// all checks OK :).
				// launch controller.
				this.control	= new csdH_controller;
				this.control.attachLinks();
				return true;
			}
		} else { return false; /* do nothing!*/ }
	}
}

function csdH_controller() {
	csdHController 		= this;
	
	this.notifyExempt	= new Array('SDPatrolBot','Ale jrb 2'); // these editors are exempt from being notified (if they added the tag, notification will fail)
	
	csdHController.deleteRegex = /[\s]*\{\{(?:db|speedy ?delet(?:e|ion)|speedy|d|rm|del(?:ete)? ?(?:because)?|csd|nn)(?:-(?:.+?))?(?:\|(?:.+?))?\}\}[\s]*/gi;
	csdHController.hangonRegex = /[\s]*\{\{(?:hang|hold)(?: |-)?oo?n(?:\|.+?)?\}\}[\s]*/gi;
	
	csdHController.doNotifyDec = ''; csdHController.doNotifyDel = ''; csdHController.doNotifyPrd = ''; 
	if (notifyByDefaultDec == true) csdHController.doNotifyDec = ' checked';
	if (notifyByDefaultDel == true) csdHController.doNotifyDel = ' checked';
	if (notifyByDefaultPrd == true) csdHController.doNotifyPrd = ' checked';
	if (notifyByDefaultNew == true) csdHController.doNotifyNew = ' checked';
	
	if (waUser.isSysop == true) { csdHController.isSysop = 'yes'; csdHController.decAction = 'declined'; } else { csdHController.isSysop = 'no'; csdHController.decAction = 'contested'; }
	
	// default arrays - it's fine to edit these, but if you simply wish to add additional decline reasons, use the
	// setting for additional options described above.
	this.declineReasons = new Array(
										['G1', 		'Not nonsense - there is meaningful content'],
										['G2', 		'Not a test page'],
										['G3', 		'Not blatantly vandalism or a hoax'],
										['G4', 		'Not previously been deleted via a deletion discussion'],
										['G5', 		'Not created by a banned user, or the page does not violate the user\'s ban'],
										['G6', 		'Deletion of this page may be controversial or is under discussion'],
										['G7', 		'Author has not requested deletion, or other users have added substantial content'],
										['G8', 		'Does not rely on a page that does not exist'],
										['G9', 		'G9 can only be used at the request of, or by, the Wikimedia Foundation'],
										['G10', 	'Not blatantly an attack page or negative, unsourced BLP'],
										['G11', 	'Not unambiguously promotional'],
										['G12', 	'Not an unambiguous copyright infringement, or there is other content to save'],
										
										['A1', 		'There is sufficient context to identify the subject of the article'],
										['A2', 		'The article is in English, or does not exist at a foreign project'],
										['A3', 		'Contains sufficient content to be a stub'],
										['A5', 		'The article has not been transwikied to another project'],
										['A7', 		'The article makes a credible assertion of importance or significance, sufficient to pass A7'],
										['A7', 		'A7 does not apply to schools'],
										['A7', 		'A7 does not apply to software'],
										['A9', 		'The article makes a credible assertion of importance or significance, or is not a musical recording'],
										
										['R2', 		'Does not redirect to a different or incorrect namespace'],
										['R3', 		'Is a plausible, useful redirect or is not a redirect at all'],
										['R3',		'Not a recently created redirect - consider [[WP:RfD]]'],
										
										['U1', 		'Not a user page'],
										['U1', 		'Does not apply to user talk pages'],
										['U2', 		'User does exist, or this is not a user page']
									);
	this.deleteReasons = new Array(
								   		['N/A', 	'You must select a rationale...'],
										['G1', 		'[[WP:PN|Patent nonsense]], meaningless, or incomprehensible'],
										['G2', 		'Test page'],
										['G3', 		'[[WP:VANDAL|Vandalism]]'],
										['G3', 		'[[WP:VANDAL|Vandalism]] - blatant hoax or misinformation'],
										['G4', 		'Recreation of a page that was [[WP:DEL|deleted]] per a [[WP:XFD|deletion discussion]]'],
										['G5', 		'Creation by a [[WP:BAN|banned]] user in violation of ban'],
										['G6', 		'Housekeeping and routine (non-controversial) cleanup'],
										['G7', 		'One author who has requested deletion or blanked the page'],
										['G8', 		'Page dependent on a deleted or nonexistent page'],
										['G8',		'Talk page of a deleted page'],
										['G10', 	'[[WP:ATP|Attack page]] or negative unsourced [[WP:BLP|BLP]] that serves no purpose but to threaten or disparage its subject'],
										['G11', 	'Unambiguous [[WP:ADS|advertising]] or promotion'],
										['G12', 	'Unambiguous [[WP:C|copyright infringement]]'],
										['G13', 	'Rejected or unsubmitted Articles for creation page that has not been edited in over six months'],
																				
										['A1', 		'Not enough context to identify article\'s subject'],
										['A2', 		'Article in a foreign language that exists on another project'],
										['A3', 		'Article that has no meaningful, substantive content'],
										['A5', 		'Article that has been transwikied to another project'],
										['A7', 		'No indication that the article may meet the guidelines for inclusion'],
										['A7',		'Article about a real person, which does not indicate the importance or significance of the subject'],
										['A7',		'Article about a band, singer, musician, or musical ensemble that does not indicate the importance or significance of the subject'],
										['A7',		'Article about a web site, blog, web forum, webcomic, podcast, browser game, or similar web content, which does not indicate the importance or significance of the subject'],
										['A7',		'Article about a company, corporation, organization, or group which does not indicate the importance or significance of the subject'],
										['A7',		'Article about a group or club, which does not indicate the importance or significance of the subject'],
										['A7',		'Article about an organized event, which does not indicate the importance or significance of the subject'],
										['A7',		'Article about an individual animal, which does not indicate the importance or significance of the subject'],
										['A9', 		'Article about a musical recording, which does not indicate the importance or significance of the subject and where the artist has no article'],
										['A10', 	'Article where the only content is already existing in another article and where a redirect to the existing article would be implausible'],
										
										['R2', 		'Cross-[[WP:NS|namespace]] [[WP:R|redirect]] from mainspace'],
										['R3', 		'Recently-created, implausible [[WP:R|redirect]]'],
										
										['U1', 		'User request to delete pages in own userspace'],
										['U2', 		'Userpage or subpage of a nonexistent user'],
										['U3', 		'[[WP:NFC|Non-free]] [[Help:Gallery|gallery]]']
									);
	
	
	// Handle user defined content...
	// declining
	if (overwriteDeclineReasons == true) {
		this.declineReasons.length = 0; this.declineReasons = myDeclineReasons;
	} else {
		this.declineReasons = this.declineReasons.concat(myDeclineReasons);
	}
	// deleting
	if (overwriteDeleteReasons == true) {
		this.deleteReasons.length = 0; this.deleteReasons = myDeleteReasons;
	} else {
		this.deleteReasons = this.deleteReasons.concat(myDeleteReasons);
	}
	// append necessary options to decline reasons
	var declineReasonsEnd = new Array(
									  	['INVALID', 		'The reason given is not a valid [[WP:CSD|speedy deletion criterion]]'],	// don't touch this
										['DONTPROVIDE', 	'No reason given.'], 						// don't touch this
										['OTHER', 			'Other - provide your own reason below']	// don't touch this
									);
	this.declineReasons = this.declineReasons.concat(declineReasonsEnd);
	
	
	// main GUI function
	this.showcsdHWindow = function() {
		// grab position of button
		var offsetL = 0, offsetT = 0, thisObject = document.getElementById('ca-speedy');
		
		if ( thisObject.offsetParent ) {
			do {
				offsetL += thisObject.offsetLeft;
				offsetT += thisObject.offsetTop;
			} while ( thisObject = thisObject.offsetParent );
		}
		
		// build window to show user
		if (waUser.isSysop == true) { var showSys = '<div style="margin: auto; width: 95%; cursor: pointer; background: #dfdfdf; border: 1px solid #cfcfcf; height: 22px; margin-top: 20px;" onclick="csdHController.deletePage();" onmouseover="this.style.border = \'1px solid #333333\';" onmouseout="this.style.border = \'1px solid #cfcfcf\';">Delete Page</div>' } else { var showSys = ''; }
		if (this.interface == null) { 
			this.interface = new wa_window(document.getElementById('content')); this.visible = true; this.csdHelperLink.ele_obj.setAttribute('class', 'selected'); 
			this.interface.win_content = ''+
								'<div><div style="width: 596px; border-bottom: 1px solid #aaaaaa; padding: 2px; font-weight: bold;">Handle Speedy Deletion</div>'+
								'<div style="font-size: 11px; margin-left: 7px;">What do you want to do to this page?</div>'+
								'<div style="font-size: 11px; text-align: center; width: 100%;">'+
									showSys+
									'<div style="margin: auto; width: 80%; cursor: pointer; background: #dfdfdf; border: 1px solid #cfcfcf; height: 16px; margin-top: 20px;" onclick="csdHController.declinePage();" onmouseover="this.style.border = \'1px solid #333333\';" onmouseout="this.style.border = \'1px solid #cfcfcf\';">Decline Speedy</div>'+
									'<div style="margin: auto; width: 80%; cursor: pointer; background: #dfdfdf; border: 1px solid #cfcfcf; height: 16px; margin-top:  5px;" onclick="csdHController.prodPage();" onmouseover="this.style.border = \'1px solid #333333\';" onmouseout="this.style.border = \'1px solid #cfcfcf\';">Change to PROD</div>'+
								'</div>'+
								'';
		} else {
			this.interface.win_content = ''+
								'<div><div style="width: 596px; border-bottom: 1px solid #aaaaaa; padding: 2px; font-weight: bold;">Handle Speedy Deletion</div>'+
								'<div style="font-size: 11px; margin-left: 7px;">What do you want to do to this page?</div>'+
								'<div style="font-size: 11px; text-align: center; width: 100%;">'+
									showSys+
									'<div style="margin: auto; width: 80%; cursor: pointer; background: #dfdfdf; border: 1px solid #cfcfcf; height: 16px; margin-top: 20px;" onclick="csdHController.declinePage();" onmouseover="this.style.border = \'1px solid #333333\';" onmouseout="this.style.border = \'1px solid #cfcfcf\';">Decline Speedy</div>'+
									'<div style="margin: auto; width: 80%; cursor: pointer; background: #dfdfdf; border: 1px solid #cfcfcf; height: 16px; margin-top:  5px;" onclick="csdHController.prodPage();" onmouseover="this.style.border = \'1px solid #333333\';" onmouseout="this.style.border = \'1px solid #cfcfcf\';">Change to PROD</div>'+
								'</div>'+
								'';
			
			if (this.visible == true) { 
				this.csdHelperLink.ele_obj.setAttribute('class', '');
				this.interface.win_disp = 'none'; 
				this.interface.applyAll(); 
				this.visible = false; 
				return true; 
			} else { 
				this.csdHelperLink.ele_obj.setAttribute('class', 'selected');
				this.interface.win_disp = 'block';
				this.interface.win_height = 170;
				this.interface.applyAll();
				this.visible = true; 
				return true;
			}
		}
		
		if ( mw.config.get ( 'skin' ) == 'vector' ) {
			this.interface.win_left = offsetL - 17;
			this.interface.win_top = offsetT + 39;
		} else {
			this.interface.win_left = document.getElementById('ca-speedy').offsetLeft - 17;
			this.interface.win_top = -1;
		}
		this.interface.win_width = 600;
		this.interface.win_height = 170;
		this.interface.win_bg = '#fff';
		this.interface.win_bd = '#aaaaaa';
		this.interface.win_bd_wd = 1;
		this.interface.applyAll();
	}
	
	this.declinePage = function() {
		// build the selection options
		var declineOptions = ''; var optionSelected = false;
		for (var i = 0; i < this.declineReasons.length; i++) {
			var selected = '';
			// determine whether this option should be selected
			if (optionSelected == false) {
				if (document.getElementById('delete-criterion') != null) { // if this matches the criterion provided, select it.
					if (document.getElementById('delete-criterion').innerHTML == this.declineReasons[i][0]) { selected = ' selected '; optionSelected = true; }
					if ( (this.declineReasons[i][0] == 'INVALID') && (optionSelected == false) ) { selected = ' selected '; optionSelected = true; }
				} else { // if no criterion was selected, wait until 'other' is selected.
					if (this.declineReasons[i][0] == 'OTHER') { selected = ' selected '; optionSelected = true; }
				}
			}
			
			// build the visible message for use in the drop-down.
			var tempVisible = myDeclineListing.replace(/%CRITERION%/gi, this.declineReasons[i][0]); tempVisible = tempVisible.replace(/%REASON%/gi, this.declineReasons[i][1]);
			declineOptions += '<option'+selected+'>'+tempVisible+'</option>';
		}
		
		if ( mw.config.get ( 'skin' ) == 'vector' ) { var skinPos = '3'; } else { var skinPos = '10'; }
		this.interface.win_content = ''+
								'<div><div style="width: 596px; border-bottom: 1px solid #aaaaaa; padding: 2px; font-weight: bold;">Decline Speedy Deletion</div>'+
								'<div style="font-size: 11px; margin-left: 7px; padding: 0px;">Select the reason for declining the deletion from the list. This text will be used as the edit summary.</div>'+
								'<select id="declineReason" style="font-size: 11px; margin-left: 9px;" onchange="document.getElementById(\'declineText\').value = \'\'">'+declineOptions+'</select>'+
								'<div style="font-size: 11px; margin-left: 7px; margin-top: 8px; padding: 0px;">Or provide your own reason and summary below.</div>'+
								'<input id="declineText" style="margin-left: 9px; font-size: 11px; width: 576px;" type="text" onkeyup="document.getElementById(\'declineReason\').selectedIndex = csdHController.declineReasons.length-1;" />'+
								'<div style="margin-top: 13px; float: right; margin-right: 300px; font-size: 11px; ">-- <a href="#" onclick="csdHController.declineDo();">decline speedy deletion</a> --</div>'+
								'<div style="margin-top: '+skinPos+'px; font-size: 11px; margin-left: 20px; vertical-align: middle; padding: 0px;">notify tagger <input id="notifyTagger"'+csdHController.doNotifyDec+' style="position: relative; top: 3px; " type="checkbox" /></div>'+
								'<div style="margin-top: '+skinPos+'px; font-size: 11px; margin-left: 20px; vertical-align: middle; padding: 0px;">use newbie message <input id="notifyNewbie"'+csdHController.doNotifyNew+' style="position: relative; top: 3px; " type="checkbox" /></div>'+
								'</div>'+
								'';
		this.interface.applyAll();
	}
	this.declineDo = function(callback) {
		// get page content
		if (!callback) var callback = 0;
		switch (callback) {
			default:
				// main vars
				csdHController.declineText = 	document.getElementById('declineText').value;
				csdHController.declineSel = 	document.getElementById('declineReason').selectedIndex;
				csdHController.declineNotify = 	document.getElementById('notifyTagger').checked;
				csdHController.notifyNewbie = 	document.getElementById('notifyNewbie').checked;
			
				if ((csdHController.declineText == '') && (csdHController.declineSel == csdHController.declineReasons.length-1)) {
					// if no reason is typed, byt 'Other' is selected, use the 'No reason provided' option.
					csdHController.declineSel = csdHController.declineReasons.length - 2;
				}
				
				csdHController.declineCategory 	= 	csdHController.declineReasons[csdHController.declineSel][0];
				csdHController.declineReason 	=	csdHController.declineReasons[csdHController.declineSel][1];
				
				// build all messages
				if ( (csdHController.declineCategory == 'INVALID') || (csdHController.declineCategory == 'DONTPROVIDE') ) { // if it's a 'special' case, use the 'special' summary
					var tempSummary = myDeclineSummarySpecial;
					tempSummary = tempSummary.replace(/%ACTION%/gi, csdHController.decAction); tempSummary = tempSummary.replace(/%REASON%/gi, csdHController.declineReason);
					csdHController.editSummary = tempSummary;
				} else if (csdHController.declineCategory == 'OTHER') { // if they've typed a reason, use that
					var tempSummary = myDeclineSummarySpecial;
					tempSummary = tempSummary.replace(/%ACTION%/gi, csdHController.decAction); tempSummary = tempSummary.replace(/%REASON%/gi, csdHController.declineText);
					csdHController.editSummary = tempSummary;
				} else { // otherwise, use the 'normal' summary
					var tempSummary = myDeclineSummary;
					tempSummary = tempSummary.replace(/%ACTION%/gi, csdHController.decAction); tempSummary = tempSummary.replace(/%CRITERION%/gi, csdHController.declineCategory); tempSummary = tempSummary.replace(/%REASON%/gi, csdHController.declineReason);
					csdHController.editSummary = tempSummary;
				}
				csdHController.editSummary += ' ([[User:Ale_jrb/Scripts|CSDH]])';
				
				// start
				this.interface.win_content = ''+
								'<div><div style="width: 596px; border-bottom: 1px solid #aaaaaa; padding: 2px; font-weight: bold;">Working...</div>'+
								'<div style="font-size: 11px; margin-left: 7px;">Please wait while CSDHelper performs the requested operations. This can take several seconds.</div>'+
								'<div id="workingStatus" style="font-size: 11px; margin-left: 15px;">- Retrieving existing content...<br /></div>'+
								'</div>'+
								'';
				
				this.interface.applyAll();
			
				csdHController.pageReq = new wa_mediawikiApi();
				csdHController.pageReq.onCompleteAction = function() { csdHController.declineDo('1'); };
				csdHController.pageReq.getPage(wgPageName, notifyLimit, 'user|content');
				break;
				
			case '1':
				document.getElementById('workingStatus').innerHTML += '- Removing tags...<br />';
				csdHController.pageContent = csdHController.pageReq.data['page']['revisions'][0]['content'];
				
				var regReplace = csdHController.deleteRegex;
				csdHController.pageContent = csdHController.pageContent.replace(regReplace, '');
				var regReplace = csdHController.hangonRegex;
				csdHController.pageContent = csdHController.pageContent.replace(regReplace, '');
				csdHController.newContent = csdHController.pageContent; // grab a record of that
				
				document.getElementById('workingStatus').innerHTML += '- Updating page...<br />';
				
				csdHController.editReq = new wa_mediawikiApi();
				csdHController.editReq.onCompleteAction = function() { csdHController.declineDo('2'); };
				csdHController.editReq.editPage(wgPageName, csdHController.newContent, csdHController.editSummary, false, 'text');
				break;
				
			case '2':
				// check for notify
				if ( (csdHController.declineNotify == true) || (logOnDecline == true) ) {
					document.getElementById('workingStatus').innerHTML += '- Determining user who tagged page...<br />';
					
					var tags = 0; csdHController.tagger = '';
				
					for (var i = 0; i < notifyLimit; i ++) {
						var thisPage = csdHController.pageReq.data['page']['revisions'][i]['content'];
						
						// count tags
						var regTest = csdHController.deleteRegex;
						var k = 1; var count = 0;
						while (k != null) {
							k = regTest.exec(thisPage);
							if (k != null) count ++;
						}
						regTest.lastIndex = 0;
						
						// check if we have fewer than last time (1 was added on that rev)
						if (count < tags) {
							csdHController.tagger = csdHController.pageReq.data['page']['revisions'][i - 1]['user'];
							break;
						} else {
							tags = count;
						}
					}
					if (csdHController.tagger == '') { document.getElementById('workingStatus').innerHTML += '- Could not determine tagger: will not log or notify. Finished.<br />'; break; }
					
					for (var i = 0; i < csdHController.notifyExempt.length; i ++) {
						if (csdHController.tagger == csdHController.notifyExempt[i]) {
							document.getElementById('workingStatus').innerHTML += '- Tagger ('+csdHController.tagger+') is on the tagger exempt list, and will not be notified. Finished.<br />';
							if (logOnDecline == true) { csdHController.declineNotify = false; csdHController.declineDo('3');  } else { return true; }
						}
					}
					
					if ( (logOnDecline == true) && (logOnDeclinePath != '') ) { csdHController.declineDo('3'); } else { csdHController.declineDo('5'); }
					
				} else { window.location.reload(true); csdHController.showcsdHWindow(); }
				break;
				
			case '3':
				// log decline action where relevant
				document.getElementById('workingStatus').innerHTML += '- Logging decline action to \''+logOnDeclinePath+'\' - retrieving page... ';
				
				csdHController.pageReq = new wa_mediawikiApi();
				csdHController.pageReq.onCompleteAction = function() { csdHController.declineDo('4'); };
				csdHController.pageReq.getPage(logOnDeclinePath, 1, 'content');
				
				break;
				
			case '4':
				// we have retrieved the data regarding the log page; move to edit it
				document.getElementById('workingStatus').innerHTML += 'modifying page...<br />';
				//var logOnDeclinePath = logOnDeclinePath.replace(/ /g,'_');
				
				// check whether there is existing content
				var pageData = csdHController.pageReq.data['page'];
				if (pageData['status'] == 'OK') {
					var oldContent = pageData['revisions'][0]['content'];
					if ( oldContent === '' ) oldContent = "{| class=\"sortable wikitable\" style=\"font-size: 80%;\" border=\"2\" cellpadding=\"1\" background:#f9f9f9;\"|\n|-\n! style=\"text-align: left\" | Article\n! Tagger\n! Criterion\n! Decline reason\n! Date\n|}";
				} else {
					var oldContent = "{| class=\"sortable wikitable\" style=\"font-size: 80%;\" border=\"2\" cellpadding=\"1\" background:#f9f9f9;\"|\n|-\n! style=\"text-align: left\" | Article\n! Tagger\n! Criterion\n! Decline reason\n! Date\n|}";
				}
				
				// message
				var pageName = wgPageName.replace(/_/g, ' ');
				var reason = (csdHController.declineCategory == 'OTHER') ? csdHController.declineText : csdHController.declineReason;
				var crit = document.getElementById('delete-criterion').innerHTML;
				var message = "|-\n| [[:" + pageName + "]] || [[User:" + csdHController.tagger + "|" + csdHController.tagger + "]] || [[WP:CSD#" + crit + "|CSD " + crit + "]] || " +reason + " || " + "~~" + "~~" + "~\n";
				
				// add the new row to the table
				var newContent = oldContent.replace('|}', message + '|}');
				
				// build vars
				var editSummary = 'Adding [[' + pageName + ']] to speedy decline log ([[User:Ale_jrb/Scripts|CSDH]])';
				
				// perform the edit
				csdHController.editReq = new wa_mediawikiApi();
				csdHController.editReq.onCompleteAction = function() { csdHController.declineDo('5'); };
				csdHController.editReq.editPage(logOnDeclinePath, newContent, editSummary, true, 'text');
				break;
				
			case '5':
				// check notify
				if (csdHController.declineNotify != true) { window.location.reload(true); csdHController.showcsdHWindow(); break; }
			
				// output
				document.getElementById('workingStatus').innerHTML += '- Tagged by \''+csdHController.tagger+'\' - notifying user...<br />';
			
				// edit summary
				csdHController.editSummary = 'Notifying about '+csdHController.decAction+' speedy deletion ([[User:Ale_jrb/Scripts|CSDH]])';
				
				// decide whether to use newbie message
				if (csdHController.notifyNewbie) { var useNewbie = 'yes'; } else { var useNewbie = 'no'; }
				
				// fix message - handle other special case
				if (csdHController.declineCategory == 'OTHER') csdHController.declineReason = csdHController.declineText;
				// fix message - handle punctuation at the end of the message (it must be added if absent).
				var p = csdHController.declineReason.substr(csdHController.declineReason.length - 1);
				if ( (p != '.') && (p != '!') && (p != '?') ) csdHController.declineReason += '.';
				
				// message
				var message = '== Speedy deletion '+csdHController.decAction+': [[:'+wgPageName.replace(/_/g, ' ')+']] =='+"\n"+'{{subst:'+notifyTemplate+'|action=decline|page='+wgPageName.replace(/_/g, ' ')+'|tagger='+csdHController.tagger+'|declinetext='+csdHController.declineReason+'|admin='+csdHController.isSysop+'|newbie='+useNewbie+'}} ~~'+'~~';
				
				csdHController.saveReq = new wa_mediawikiApi();
				csdHController.saveReq.onCompleteAction = function() { window.location.reload(true); csdHController.showcsdHWindow(); };
				csdHController.saveReq.editPage('User_talk:'+csdHController.tagger, message, csdHController.editSummary, false, 'appendtext');
				break;
			
		}
		
	}
	
	this.prodPage = function() {
		var prodOptions = '';
		
		var content = document.getElementById('bodyContent');
		var regTest = /criteria for speedy deletion<\/a><\/i> because (.*?)\.<\/b> <i>For valid criteria,/i
		var rationale = regTest.exec(content);
		if (rationale != null) { rationale = rationale[1]; } else { rationale = ''; }
		
		this.interface.win_content = ''+
								'<div><div style="width: 596px; border-bottom: 1px solid #aaaaaa; padding: 2px; font-weight: bold;">Convert to PROD</div>'+
								'<div style="font-size: 11px; margin-left: 7px;">You are asserting that the page cannot be speedy deleted, but <em>can</em> be deleted by proposed deletion. '+
								'Enter the reason for deletion below - if the tagger provided one, it will be filled automatically.</div>'+
								'<input id="prodReason" style="margin-left: 9px; font-size: 11px; width: 576px;" type="text" value="'+rationale+'" />'+
								'<div id="notifyTaggerDiv" style="margin-top: 10px; font-size: 11px; margin-left: 20px; float: left; vertical-align: middle;">notify tagger of conversion <input id="notifyTagger"'+csdHController.doNotifyPrd+' style="position: relative; top: 3px; " type="checkbox" /><br />'+
								'use newbie message <input id="notifyNewbie"'+csdHController.doNotifyNew+' style="position: relative; top: 3px; " type="checkbox" /></div>'+
								'<div style="margin: auto; margin-top: 13px; font-size: 11px; width: 200px; text-align: center;">-- <a href="#" onclick="csdHController.prodDo();">convert to PROD</a> --</div>'+
								'</div>'+
								'';
		this.interface.applyAll();
	}
	this.prodDo = function(callback) {
		if (!callback) var callback = 0;
		switch (callback) {
			default:
				csdHController.prodReason = document.getElementById('prodReason').value;
				csdHController.prodNotify = document.getElementById('notifyTagger').checked;
				csdHController.notifyNewbie = document.getElementById('notifyNewbie').checked;
				
				this.interface.win_content = ''+
								'<div><div style="width: 596px; border-bottom: 1px solid #aaaaaa; padding: 2px; font-weight: bold;">Working...</div>'+
								'<div style="font-size: 11px; margin-left: 7px;">Please wait while CSDHelper performs the requested operations. This can take several seconds.</div>'+
								'<div id="workingStatus" style="font-size: 11px; margin-left: 15px;"></div>'+
								'</div>'+
								'';
				
				this.interface.applyAll();
			
				if (csdHController.prodNotify == true) {
					document.getElementById('workingStatus').innerHTML += '- Searching for tagger...<br />';
					
					if (notifyLimit > 15) notifyLimit = 15;
					csdHController.pageReq = new wa_mediawikiApi();
					csdHController.pageReq.onCompleteAction = function() { csdHController.prodDo('1'); };
					csdHController.pageReq.getPage(wgPageName, notifyLimit + 1, 'content|user');
				} else { 
					csdHController.pageReq = new wa_mediawikiApi();
					csdHController.pageReq.onCompleteAction = function() { csdHController.prodDo('3'); };
					csdHController.pageReq.getPage(wgPageName, 1, 'content|user');
				}
				break;
				
			case '1':
				var tags = 0; csdHController.tagger = '';
				
				for (var i = 0; i < notifyLimit; i ++) {
					var thisPage = csdHController.pageReq.data['page']['revisions'][i]['content'];
					
					// count tags
					var regTest = csdHController.deleteRegex;
					var k = 1; var count = 0;
					while (k != null) {
						k = regTest.exec(thisPage);
						if (k != null) count ++;
					}
					regTest.lastIndex = 0;
					
					// check if we have fewer than last time (1 was added on that rev)
					if (count < tags) {
						csdHController.tagger = csdHController.pageReq.data['page']['revisions'][i - 1]['user'];
						break;
					} else {
						tags = count;
					}
				}
				
				if (csdHController.tagger == '') { document.getElementById('workingStatus').innerHTML += '- Could not determine tagger. Moving to tag page...<br />'; csdHController.prodDo('3'); break; }
				
				for (var i = 0; i < csdHController.notifyExempt.length; i ++) {
					if (csdHController.tagger == csdHController.notifyExempt[i]) {
						document.getElementById('workingStatus').innerHTML += '- Tagger ('+csdHController.tagger+') is on the tagger exempt list, and will not be notified. Moving to tag page...<br />';
						csdHController.prodDo('3');
						return true;
					}
				}
				
				csdHController.prodDo('2');
				break;
				
			case '2': // notify tagger
				document.getElementById('workingStatus').innerHTML += '- Tagged by \''+csdHController.tagger+'\' - notifying user...<br />';
			
				// edit summary
				csdHController.editSummary = 'Notifying about speedy deletion converted to PROD ([[User:Ale_jrb/Scripts|CSDH]])';
				
				// decide whether to use newbie message
				if (csdHController.notifyNewbie) { var useNewbie = 'yes'; } else { var useNewbie = 'no'; }
				
				// message
				var message = '== Speedy deletion converted to PROD: [[:'+wgPageName.replace(/_/g, ' ')+']] =='+"\n"+'{{subst:'+notifyTemplate+'|action=convert|page='+wgPageName.replace(/_/g, ' ')+'|tagger='+csdHController.tagger+'|admin='+csdHController.isSysop+'|newbie='+useNewbie+'}} ~~'+'~~';
				
				csdHController.notifyReq = new wa_mediawikiApi();
				csdHController.notifyReq.onCompleteAction = function() { csdHController.prodDo('3'); };
				csdHController.notifyReq.editPage('User_talk:'+csdHController.tagger, message, csdHController.editSummary, false, 'appendtext');
				break;
				
			case '3':
				document.getElementById('workingStatus').innerHTML += '- Converting tags...<br />';
				csdHController.pageContent = csdHController.pageReq.data['page']['revisions'][0]['content'];
				
				var regReplace = csdHController.deleteRegex;
				csdHController.pageContent = csdHController.pageContent.replace(regReplace, '');
				var regReplace = csdHController.hangonRegex;
				csdHController.pageContent = csdHController.pageContent.replace(regReplace, '');
				
				if (csdHController.prodReason != 'nn') { csdHController.pageContent = '{'+'{subst:prod|'+csdHController.prodReason+'}'+'}\n' + csdHController.pageContent; } else {
					csdHController.pageContent = '{'+'{subst:prod-nn}'+'}\n' + csdHController.pageContent; }
				
				csdHController.newContent = csdHController.pageContent; // grab a record of that
				csdHController.editSummary = 'Speedy deletion converted to PROD ([[User:Ale_jrb/Scripts|CSDH]])';
				
				document.getElementById('workingStatus').innerHTML += '- Updating page...<br />';
				
				csdHController.editReq = new wa_mediawikiApi();
				csdHController.editReq.onCompleteAction = function() { window.location.reload(true); csdHController.showcsdHWindow(); };
				csdHController.editReq.editPage(wgPageName, csdHController.newContent, csdHController.editSummary, false, 'text');
				break;
		}
	}
	
	this.deletePage = function() {
		// build the selection options
		var deleteOptions = ''; var optionSelected = false;
		for (var i = 0; i < this.deleteReasons.length; i++) {
			var selected = '';
			if ( (document.getElementById('delete-criterion') != null) && (optionSelected == false) ) { 
				if (document.getElementById('delete-criterion').innerHTML == this.deleteReasons[i][0]) { csdHController.initialRationale = this.deleteReasons[i][0]; selected = ' selected'; optionSelected = true; }
			} else {
				if (this.deleteReasons[i][0] == 'N/A') { selected = ' selected '; optionSelected = true; }
			}
			
			// general
			var visibleReasoning = this.deleteReasons[i][1].replace(/\[\[(?:.*?\|)(.+?)\]\]/g, '$1');
			deleteOptions += '<option value="'+i+'"'+selected+'>'+this.deleteReasons[i][0]+': '+visibleReasoning+'</option>';
		}
		
		if (document.getElementById('delete-criterion') == null) { var displayTagCommand = 'display: none;'; } else { var displayTagCommand = 'display: block;'; }
		
		this.interface.win_content = ''+
								'<div><div style="width: 596px; border-bottom: 1px solid #aaaaaa; padding: 2px; font-weight: bold;">Perform Speedy Deletion</div>'+
								'<div style="font-size: 11px; margin-left: 7px;">Select the reason for deletion from the list. The rationale specified by the tagger is automatically selected. If you select a reason that does not '+
								'match the tag, will be given the option of notifying the tagger.</div>'+
								'<select id="deleteReason" style="font-size: 11px; margin-left: 9px; width: 576px;" onchange="'+
									'if (document.getElementById(\'delete-criterion\') != null) {'+
										'if (csdHController.deleteReasons[document.getElementById(\'deleteReason\').selectedIndex][0] != document.getElementById(\'delete-criterion\').innerHTML) { '+
											'if (csdHController.deleteReasons[document.getElementById(\'deleteReason\').selectedIndex][0] == \'N/A\') { document.getElementById(\'notifyTaggerDiv\').style.display = \'none\'; } else { document.getElementById(\'notifyTaggerDiv\').style.display = \'block\'; }'+
										'} else {'+
											'document.getElementById(\'notifyTaggerDiv\').style.display = \'none\'; '+
										'}'+
									'} else { document.getElementById(\'notifyTaggerDiv\').style.display = \'none\'; }'+
									
									'if (csdHController.deleteReasons[document.getElementById(\'deleteReason\').selectedIndex][0] == \'N/A\') {'+
										'document.getElementById(\'performDeletionDiv\').style.display = \'none\'; '+
									'} else {'+
										'document.getElementById(\'performDeletionDiv\').style.display = \'block\'; '+
									'}'+
								'">'+deleteOptions+'</select>'+
								'<div id="notifyTaggerDiv" style="display: none; margin-top: 10px; font-size: 11px; margin-left: 20px; float: left; vertical-align: middle;">notify tagger of rationale change <input id="notifyTagger"'+csdHController.doNotifyDel+' style="position: relative; top: 3px; " type="checkbox" /><br />'+
								'use newbie message <input id="notifyNewbie"'+csdHController.doNotifyNew+' style="position: relative; top: 3px; " type="checkbox" /></div>'+
								'<div id="performDeletionDiv" style="margin: auto; margin-top: 13px; font-size: 11px; width: 200px; text-align: center; '+displayTagCommand+'">-- <a href="#" onclick="csdHController.deleteDo();">perform speedy deletion</a> --</div>'+
								'</div>'+
								'';
		this.interface.applyAll();
	}
	this.deleteDo = function(callback) {
		if (!callback) var callback = 0;
		switch (callback) {
			default:
				csdHController.deleteSel 		= document.getElementById('deleteReason').selectedIndex;
				csdHController.deleteNotify 	= document.getElementById('notifyTagger').checked;
				csdHController.notifyNewbie		= document.getElementById('notifyNewbie').checked;
				if (document.getElementById('delete-criterion') == null) {
					csdHController.allowDelNotify = false;
				} else { csdHController.allowDelNotify = (csdHController.deleteReasons[csdHController.deleteSel][0] != document.getElementById('delete-criterion').innerHTML); }
				
				this.interface.win_content = ''+
								'<div><div style="width: 596px; border-bottom: 1px solid #aaaaaa; padding: 2px; font-weight: bold;">Working...</div>'+
								'<div style="font-size: 11px; margin-left: 7px;">Please wait while CSDHelper performs the requested operations. This can take several seconds.</div>'+
								'<div id="workingStatus" style="font-size: 11px; margin-left: 15px;"></div>'+
								'</div>'+
								'';
				
				this.interface.applyAll();
			
				if ((csdHController.deleteNotify == true) && (csdHController.allowDelNotify == true)) {
					document.getElementById('workingStatus').innerHTML += '- Searching for tagger...<br />';
					
					if (notifyLimit > 15) notifyLimit = 15;
					csdHController.pageReq = new wa_mediawikiApi();
					csdHController.pageReq.onCompleteAction = function() { csdHController.deleteDo('1'); };
					csdHController.pageReq.getPage(wgPageName, notifyLimit + 1, 'content|user');
				} else { csdHController.deleteDo('3'); }
				break;
				
			case '1':
				var tags = 0; csdHController.tagger = '';
				
				for (var i = 0; i < notifyLimit; i ++) {
					var thisPage = csdHController.pageReq.data['page']['revisions'][i]['content'];
					
					// count tags
					var regTest = csdHController.deleteRegex;
					var k = 1; var count = 0;
					while (k != null) {
						k = regTest.exec(thisPage);
						if (k != null) count ++;
					}
					regTest.lastIndex = 0;
					
					// check if we have fewer than last time (1 was added on that rev)
					if (count < tags) {
						csdHController.tagger = csdHController.pageReq.data['page']['revisions'][i - 1]['user'];
						break;
					} else {
						tags = count;
					}
				}
				
				if (csdHController.tagger == '') { document.getElementById('workingStatus').innerHTML += '- Could not determine tagger. Moving to deletion...<br />'; csdHController.deleteDo('3'); break; }
				
				for (var i = 0; i < csdHController.notifyExempt.length; i ++) {
					if (csdHController.tagger == csdHController.notifyExempt[i]) {
						document.getElementById('workingStatus').innerHTML += '- Tagger ('+csdHController.tagger+') is on the tagger exempt list, and will not be notified. Moving to deletion...<br />';
						csdHController.deleteDo('3');
						return true;
					}
				}
				
				csdHController.deleteDo('2');
				break;
				
			case '2': // notify tagger
				document.getElementById('workingStatus').innerHTML += '- Tagged by \''+csdHController.tagger+'\' - notifying user...<br />';
			
				// edit summary
				csdHController.editSummary = 'Notifying about altered speedy deletion rationale ([[User:Ale_jrb/Scripts|CSDH]])';
				
				// decide whether to use newbie message
				if (csdHController.notifyNewbie) { var useNewbie = 'yes'; } else { var useNewbie = 'no'; }
				
				// message
				var message = '== Altered speedy deletion rationale: [[:'+wgPageName.replace(/_/g, ' ')+']] =='+"\n"+'{{subst:'+notifyTemplate+'|action=change|page='+wgPageName.replace(/_/g, ' ')+'|tagger='+csdHController.tagger+'|newbie='+useNewbie+'}} ~~'+'~~';
				
				csdHController.notifyReq = new wa_mediawikiApi();
				csdHController.notifyReq.onCompleteAction = function() { csdHController.deleteDo('3'); };
				csdHController.notifyReq.editPage('User_talk:'+csdHController.tagger, message, csdHController.editSummary, false, 'appendtext');
				break;
				
			case '3':
				// perform the edit
				document.getElementById('workingStatus').innerHTML += '- Deleting page...<br />';
				var deleteReason = '[[WP:CSD#'+csdHController.deleteReasons[csdHController.deleteSel][0]+'|'+csdHController.deleteReasons[csdHController.deleteSel][0]+']]: ' + csdHController.deleteReasons[csdHController.deleteSel][1] + ' ([[User:Ale_jrb/Scripts|CSDH]])';
				
				csdHController.deleteReq = new wa_mediawikiApi();
				csdHController.deleteReq.onCompleteAction = function(callback) { 
																					if ( (callback == null) || (callback == false) || (wgNamespaceNumber != 0) ) { // no talk page
																						window.location = redirectAfterDel; csdHController.showcsdHWindow(); 
																					} else { // talk page exists
																						csdHController.talkpageId = callback;
																						document.getElementById('workingStatus').innerHTML = ''+
																							'<strong>Warning: this page now has an orphaned talk page. Do you wish to delete it under G8?</strong><br /><br />'+
																							'<div style="width: 100%; text-align: center;"><a href="#" onclick="document.getElementById(\'workingStatus\').innerHTML = \'- Deleting talk page...<br />\'; csdHController.deleteDo(\'4\');">Delete</a> | <a href="#" onclick="window.location = redirectAfterDel; csdHController.showcsdHWindow(); ">Ignore</a></div>'+
																						'';
																					}
																				};
				csdHController.deleteReq.performAction('delete', wgPageName, deleteReason);
				break;
			
			case '4':
				// delete the talk page - get the title
				var talkPage = 'Talk:' + wgPageName;
				
				csdHController.deleteReq = new wa_mediawikiApi();
				csdHController.deleteReq.onCompleteAction = function() { window.location = redirectAfterDel; csdHController.showcsdHWindow(); };
				csdHController.deleteReq.performAction('delete', talkPage, '[[WP:CSD#G8|G8]]: Talk page of deleted page. ([[User:Ale_jrb/Scripts|CSDH]])');
				break;
		}
	}
	
	this.displayError = function() {
		this.interface.win_content = ''+
								'<div><div style="width: 596px; border-bottom: 1px solid #aaaaaa; padding: 2px; font-weight: bold;">An error occurred...</div>'+
								'</div>'+
								'';
		this.interface.applyAll();
	}
	
	this.attachLinks = function() {
		
		
		this.csdHelperLink 						= new wa_element('li');
		this.csdHelperLink.ele_obj.id			= 'ca-speedy';
		this.csdHelperLink.addScriptEvent('click', function() { csdHController.showcsdHWindow(); });
		
		if ( mw.config.get ( 'skin' ) == 'vector' ) {
			this.csdHelperLink.ele_obj.innerHTML	= '<span><a href="#" title="handle speedy deletion">Speedy</a></span>';
			this.csdHelperLink.attach(document.getElementById('ca-talk'), 'after');
		} else {
			this.csdHelperLink.ele_obj.innerHTML	= '<a href="#" title="handle speedy deletion">speedy</a>';
			this.csdHelperLink.attach(document.getElementById('ca-move'), 'before');
		}
	};
}

function launchCsdHelper () {
	// lib proto
	wa_window.prototype = new wa_document;
	wa_element.prototype = new wa_document;
	
	// init object
	var obj_csdHelper = new csdHelper;
	obj_csdHelper.launch ();
	
	return true;
}

$(document).ready( function() { launchCsdHelper() });