Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
  Support for quick deletions and closing of deletion requests at the Commons.
 
  Authors: [[User:Lupo]], October 2007 - January 2008,
           [[User:DieBuche]], February 2011
           [[User:Perhelion]], January 2016
  Revision: 23:02, 25 January 2016 (UTC)

  License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
 
  Choose whichever license of these you like best :-)

  IE not supported 
 * required modules: user.options, mediawiki.util, jquery.blockUI
**/
//<nowiki>
/*global mediaWiki:false, jQuery:false, prompt:false, alert:false*/
/*jshint bitwise:true, curly:false, eqeqeq:true, forin:false, laxbreak:true,
          trailing:true, undef:true, unused:true, white:false, smarttabs:true */

(function($, mw) {
'use strict';
// Guard against double inclusions // Enable the whole shebang only for sysops.
if ('object' === typeof DelReqHandler || -1 === $.inArray('filemover', mw.config.get('wgUserGroups'))) return;

var DelReqHandler = window.DelReqHandler = {
	/*------------------------------------------------------------------------------------------
	Deletion request closing: add "[del]" and "[keep]" links to the left of the section edit
	links of a deletion request. [del] and [keep] prompt for an (optional) reason, then 
	add "delh" and "delf" with "Deleted." or "Kept." plus the reason and signature (four tildes).

	Links are added to every non-deleted image mentioned on a deletion request page. The "[del]" link
	triggers deletion (auto-completed!) of the image, with a deletion summary linking to the
	deletion request. If the image has a talk page, it is deleted as well. The "[keep]" link
	automatically removes the "delete" template from the image page and adds the "kept" template
	to the image talk page, both linking back to the deletion request.
	------------------------------------------------------------------------------------------*/
	running: [],
	parse: function () {
		var $content = $('#bodyContent, #mw_contentholder');
		if (!$content.length) return;
		//mw.util.addCSS('.reqHandlerLinks {font-size: 85%;}'); replaced by class navbar
		var linkRegex = /Commons:Deletion_requests\/.*?&section=(T-)?[1-4]$/;
		
		$content.find('h3').each(function () {
			var $t = $(this);
			var headLine = $t.find('span.mw-headline').eq(0);
			var requestHref = $t.find('span.mw-editsection a').not('.mw-editsection-visualeditor').eq(0).attr('href');
			// It's really an edit lk to a deletion request subpage, and not a section
			// edit for a daily subpage or something else
			if (!linkRegex.test(requestHref)) return true;
			var headLink = headLine.find('a').not('.new').eq(0);
			var title = (headLink.length) ? DelReqHandler.titleFromHref(headLink.attr('href')) : "";
			var requestPage = DelReqHandler.titleFromHref(requestHref);
			var discussion = $t.nextUntil('h3, .printfooter, .delh');
			var wholeDiscussion = discussion.add($t);
			if (!$t.parents('.delh').length)
				DelReqHandler.addLinks(requestPage, headLine, title, true, wholeDiscussion);
			discussion.find('a').not('.new').add(headLink).each(function () {
				var title = DelReqHandler.titleFromHref(this.href);
				if (title.indexOf('File:') === 0 && title.indexOf('/') < 0)
					//We have an image link
					DelReqHandler.addLinks(requestPage, $(this), title, false, wholeDiscussion);
			});
		});
	},
	titleFromHref: function (href) {
		if (href) {
			var title = mw.util.getParamValue('title', href);
			if (title) return title.replace(/_/g, ' ');
			var prefix = mw.config.get('wgArticlePath').replace('$1', "");
			// Fully expanded URL?
			if (href.indexOf(prefix) !== 0) prefix = mw.config.get('wgServer') + prefix;
			if (href.indexOf(prefix) !== 0 && prefix.indexOf('//') === 0) prefix = document.location.protocol + prefix; // protocol-relative wgServer?
			if (href.indexOf(prefix) === 0) return decodeURIComponent(href.substring(prefix.length)).replace(/_/g, ' ');
		}
		return "";
	},
	addLinks: function (requestPage, location, imagePage, closeRequest, discussion) {
		var span = $('<span/>', { 'class': 'navbar' });
		
		function _click (e) {
			// Use link.name for keep boolean
			e.preventDefault();
			e = new DelReqHandler.process(e.target.name, closeRequest, requestPage, imagePage, [location, span, discussion]);
			DelReqHandler.running.push(e);
		}
		
		var linkK = $('<a/>', { href: '#', 'name': 1, 'click': _click }),
			linkD = $('<a/>', { href: '#', 'click': _click });

		if (closeRequest) {
			linkK.text('Close: Kept');
			linkD.text('Close: Deleted');
			span.addClass('reqHandlerLinks2');
		} else {
			linkK.text('keep');
			linkD.text('del');
			span.addClass('reqHandlerLinks');
		}
		span.append(' [').append(linkK).append('] [').append(linkD).append(']');
		location.after(span);
	},
	setup: function () {
		if (mw.config.get('wgPageName').indexOf('Commons:Deletion_requests/') !== -1 && mw.config.get('wgAction') === 'view' && document.URL.search(/[?&]oldid=/) === -1) {
			// We're on COM:DEL or one of its daily subpages
			// Don't do anything if we're not viewing the current version of the page
			this.parse();
		}
	},
	process: function (keep, closeRequestBool, requestPage, imagePage, domElements) {
		//Merge the page processing functions into our new process
		$.extend(this, DelReqHandler.processHelpers);
		var delReqReason = window.delReqReason || "per nomination";
		var keepReqReason = window.keepReqReason || "no valid reason for deletion";
		this.tasks = [];
		this.requestPage = requestPage.replace(/_/g, ' ');
		this.keep = keep;
		this.closeRequestBool = closeRequestBool;
		this.imagePage = imagePage;
		this.imageTalkPage = imagePage.replace(/^File:/, 'File talk:');
		this.summary = 'Per [[' + requestPage + ']]';
		this.domElements = domElements;
		//getToken
		this.addTask('getPages');
		if (closeRequestBool) {
			if (keep) {
				this.reason = prompt('Why did you decide to keep this file?', keepReqReason);
				//User canceled
				if (!this.reason) return;
				this.pagesToGet = [requestPage, imagePage];
				//this.addTask('markAsKept');
				//this.addTask('getDate');
			} else {
				this.reason = prompt('Why did you decide to delete this file?', delReqReason);
				//User canceled
				if (!this.reason) return;

				this.pagesToGet = [requestPage];
				//if (imagePage != "") {
				//	this.addTask('deleteFile');
				//	this.addTask('deleteFileTalk');
				//}
			}
			this.addTask('closeRequest');
		} else {
			this.pagesToGet = [imagePage];
			if (keep) {
				this.addTask('markAsKept');
				this.addTask('getDate');
				//first letter lowercase
				this.summary = 'Kept p' + this.summary.slice(1);
			} else {
				this.addTask('deleteFile');
				this.addTask('nothing');
			}
			this.summary = prompt("Summary:", this.summary);
			//User canceled
			if (!this.summary) return;

		}
		this.addTask('fakeReload');
		this.nextTask();
		this.showProgress();
	}
};
DelReqHandler.processHelpers = {
	getPages: function () {
		var query = {
			action: 'query',
			prop: 'revisions|info',
			rvprop: 'content|timestamp',
			intoken: 'edit',
			titles: this.pagesToGet.join('|')
		};
		this.doAPICall(query, 'getPagesCallback');
	},
	getPagesCallback: function (result) {
		var pages = result.query.pages;
		for (var id in pages) { // there should be only one, but we don't know it's ID
			if (pages.hasOwnProperty(id)) {
				// The edittoken only changes between logins
				this.edittoken = pages[id].edittoken;
				var type;
				switch (pages[id].title) {
				case this.imagePage:
					type = 'imagePage';
					break;
				case this.requestPage:
					type = 'requestPage';
					break;
				default:
					type = 'unknown';
					break;
				}
				this[type + 'Result'] = {
					pageContent: pages[id].revisions[0]['*'],
					starttimestamp: pages[id].starttimestamp,
					timestamp: pages[id].revisions[0].timestamp
				};
			}
		}
		this.nextTask();
	},
	closeRequest: function () {
		var text = this.requestPageResult.pageContent,
			watchFor = '<noinclude>[[Category:MobileUpload-related deletion requests', replace = ']]</noinclude>';
		this.decision = (this.keep) ? 'Kept' : 'Deleted';
		text = text.replace(watchFor + replace, watchFor + '/' + this.decision.toLowerCase() + replace);
		
		// Check for second nomination (we always load the full page)
		var sec = text.lastIndexOf('{{delf}}\n') + 9;   // Additional more accurately: text.substr(sec).search(/^==+/m) but not really needed
		text = (sec > 51) ? // minimum text-size
			text.slice(0, sec) + '{{delh}}\n' + $.trim(text.slice(sec)) : '{{delh}}\n' + $.trim(text);
		text += '\n----\n';
		// Add dashes on 'lesser' individual signatures
		var uSig = (mw.user.options.get('fancysig') && mw.user.options.get('nickname').search(/^[ ']*\[\[/) !== 0)?
			'' : '--';
		if (this.reason) {
			this.decision += ':';
			this.reason = this.reason.replace(/[.\s-]*$/, '. ');
		}
		else this.decision += '.';
		
		text += "'''" + this.decision + "''' " + this.reason + uSig + '~~\~~\n{{delf}}';

		var page = {
			title: this.requestPage,
			text: text,
			summary: this.decision + ' ' + this.reason,
			editType: 'text'
		};
		this.savePage(page, 'nextTask');
	},

	markAsKept: function () {
		var text = this.imagePageResult.pageContent;
		text = this.removeTemplate(text);
		var page = {
			title: this.imagePage,
			text: text,
			summary: this.summary,
			editType: 'text'
		};
		this.savePage(page, 'nextTask');

	},
	removeTemplate: function (text) {
		var start = text.search(/\{\{[dD]elete/);
		if (start >= 0) {
			var level = 0;
			var curr = start + 2;
			var end = 0;
			while (curr < text.length && end === 0) {
				var opening = text.indexOf('{{', curr);
				var closing = text.indexOf('}}', curr);
				if (opening >= 0 && opening < closing) {
					level = level + 1;
					curr = opening + 2;
				} else {
					if (closing < 0) {
						// No closing braces found
						curr = text.length;
					} else {
						if (level > 0) level = level - 1;
						else end = closing + 2;
						curr = closing + 2;
					}
				}
			}
			if (end > start) {
				// Also strip whitespace after the "delete" template
				if (start > 0)
					text = text.substring(0, start) + text.substring(end).replace(/^\s*/, '');
				else
					text = text.substring(end).replace(/^\s*/, '');
				return text;
			}
		}
		alert('Couldn’t remove the {{delete}} template, please check the file ' + this.imagePage + ' manually.');
		return text;
	},

	getDate: function () {
		var query = {
			action: 'query',
			prop: 'revisions',
			rvlimit: 1,
			rvprop: 'timestamp',
			rvdir: 'newer',
			titles: this.requestPage
		};
		this.doAPICall(query, 'addKeepToTalk');
	},

	addKeepToTalk: function (result) {
		var pages = result.query.pages;
		var date = "";
		for (var id in pages) {
			if (pages.hasOwnProperty(id)) {
				// there should be only one, but we don't know it's ID
				var ts = pages[id].revisions[0].timestamp;
				if (ts) {
					// Extract year, month, and day from the timestamp. 
					// We don't care about the exact time.
					var year = ts.substr(0, 4);
					var month = ts.substr(5, 2);
					var day = ts.substr(8, 2);
					date = year + '-' + month + '-' + day;
				}
			}
		}
		var page = {
			title: this.imageTalkPage,
			text: '{{kept|' + date + '|' + this.requestPage + '}}\n',
			summary: 'Adding {{kept}}',
			editType: 'prependtext'
		};
		this.savePage(page, 'nextTask');
	},
	reload: function () {
		window.location.reload();
	},
	fakeReload: function () {
		var dE = this.domElements;
		dE[3].unblock();
		//Remove links
		dE[1].remove();
		if (this.closeRequestBool) {
			dE[3].toggleClass('delh delreqworking');
			dE[2].eq(0).before('<i>This deletion debate is now closed. Please do not make any edits to this archive.</i>');
			dE[2].eq(-1).after('<br><span style="color:green">Saved successfully.<br>This is just an approximate rendering. Reload to see the actual request.</span>');
			dE[2].eq(-1).after('<b>' + this.decision + '</b> ' + this.reason + ' --' + mw.config.get('wgUserName'));
			dE[2].eq(-1).after('<hr>');
		} else {
			//Color link red
			if (!this.keep) dE[0].addClass('new');
		}
	},

	apiURL: mw.util.wikiScript('api'),

	/**
	** Simple task queue.  addTask() adds a new task to the queue, nextTask() executes
	** the next scheduled task.  Tasks are specified as method names to call.
	**/
	// list of pending tasks
	currentTask: '',
	// current task, for error reporting
	addTask: function (task) {
		this.tasks.push(task);
	},
	nextTask: function () {
		var task = this.currentTask = this.tasks.shift();
		try {
			this[task]();
		} catch (e) {
			this.fail(e);
		}
	},
	deleteFile: function () {
		var edit = {
			action: 'delete',
			reason: this.summary,
			title: this.imagePage,
			token: this.edittoken,
			recreate: ''
		};
		this.doAPICall(edit, 'nextTask');
		edit = {
			action: 'delete',
			reason: "Talk page of deleted image",
			title: this.imageTalkPage,
			token: this.edittoken,
			recreate: ''
		};
		this.doAPICall(edit, 'nextTask', true);
	},
	savePage: function (page, callback) {
		var edit = {
			action: 'edit',
			summary: page.summary,
			title: page.title,
			token: this.edittoken
		};

		edit[page.editType] = page.text;
		this.doAPICall(edit, callback);
	},
	fail: function (e) {
		alert(e);
	},
	doAPICall: function (params, callback, ignoreErrors) {
		var k = this;
		params.format = 'json';
		$.ajax({
			url: this.apiURL,
			cache: false,
			dataType: 'json',
			data: params,
			type: 'POST',
			success: function (result, status, x) {
				if (ignoreErrors) {
					k[callback](result);
					return;
				}
				if (!result) return k.fail("Receive empty API response:\n" + x.responseText);
				// In case we get the mysterious 231 unknown error, just try again
				if (result.error && result.error.info.indexOf('231') !== -1) return setTimeout(function () {
					k.doAPICall(params, callback);
				}, 500);
				if (result.error) return k.fail("API request failed (" + result.error.code + "): " + result.error.info);
				k[callback](result);
			},
			error: function (x, status, error) {
				return k.fail("API request returned code " + x.status + " " + status + "Error code is " + error);
			}
		});
	},
	showProgress: function () {
		if (this.closeRequestBool){
			this.domElements[2].wrapAll('<div class="delreqworking">');
			this.domElements[3] = this.domElements[2].parent('.delreqworking');
			this.domElements[3].block({ 
				message: '<img src="https://upload.wikimedia.org/wikipedia/commons/3/39/Spinning_wheel_throbber_blue.gif" /> Closing request…', 
				css: { border: '3px solid #9C3', fontSize: '135%' } 
			});
		} else {
			this.domElements[3] = this.domElements[0].parent();
			this.domElements[3].block({ 
				message: '<img src="https://upload.wikimedia.org/wikipedia/commons/f/f8/Ajax-loader%282%29.gif" /> Working…', 
				css: { color: '#9C3', fontWeight: 'bold', background:'none', border:'none' } 
			});
		}
	},
	nothing: function () {
		//nothing
	}
};

mw.loader.using('user.options', function () { $(DelReqHandler.setup()); });

})(jQuery, mediaWiki);
//</nowiki>