MediaWiki:Gadget-AjaxQuickDelete.js

Na Galipedia, a Wikipedia en galego.
// Original code written by [[User:Ilmari Karonen]]
// Rewritten & extended by [[User:DieBuche]]. Botdetection and encoding fixer by [[User:Lupo]]
// Validation and further development [[User:Rillke]], 2011-2012
//
// Ajax-based replacement for [[MediaWiki:Quick-delete-code.js]]
//
// Invoke automated jsHint-validation on save: A feature on Wikimedia Commons
// Interested? See [[c:MediaWiki:JSValidator.js]] or [[c:Help:JSValidator]].
//
// TODO: Fix problems with moves of videos
// TODO: Delete talk
//<nowiki>

/*global jQuery:false, mediaWiki:false */
/*jshint curly:false, laxbreak:true, scripturl:true, onecase:true, nomen:false */

(function ($, mw) {
'use strict';
// Guard against multiple inclusions
if (window.AjaxQuickDelete instanceof Object) return;

var AQD,
	conf = mw.config.get([
			'wgArticleId',
			'wgCanonicalNamespace',
			'wgCanonicalSpecialPageName',
			'wgCategories',
			'wgFormattedNamespaces',
			'wgNamespaceNumber',
			'wgPageName',
			'wgRestrictionEdit',
			'wgUserGroups',
			'wgUserLanguage',
			'wgUserName'
		]),
nsNumber = conf.wgNamespaceNumber,
pageName = conf.wgPageName;

// A bunch of helper functions
function firstItem(o) {
	for (var i in o) {
		if (o.hasOwnProperty(i)) {
			return o[i];
		}
	}
}
function ucFirst(s) {
	return s[0].toUpperCase() + s.slice(1);
}

// Create the AjaxQuickDelete-singleton (object literal)
AQD = window.AjaxQuickDelete = {
	/**
	 ** Runs before $(document).ready() and before translation is available
	 ** (important event-binders should be attached as fast as possible)
	 **/
	preinstall: function () {
		// Promote our gadget when user opened old move page
		if (conf.wgCanonicalSpecialPageName === 'Movepage' && Number($('select[name="wpNewTitleNs"]').val()) === 6) {
			$('#mw-movepage-table').before(
				'<div class="warningbox">Consider using <i>Move & Replace</i> from the menu on file pages (open with a single click) when moving files to care for global usage and redirects.</span>');
		}

		AQD.doNothing = (!conf.wgArticleId || nsNumber < 0);

		if (AQD.doNothing)
			return;

		// Check user group
		if ($.inArray('sysop', conf.wgUserGroups) !== -1) {
			AQD.userRights = 'sysop';
		} else if ($.inArray('filemover', conf.wgUserGroups) !== -1) {
			AQD.userRights = 'filemover';
		}

		if ((AQD.userRights === 'filemover' || AQD.userRights === 'sysop') && nsNumber === 6) {
			// Change "Move" to "Move & Replace"
			var $moveLink = $('#ca-move'),
			$moveLanchor = $moveLink.find('a');
			AQD.$moveLink = $moveLink = $moveLanchor.length ? $moveLanchor : $moveLink;

			var _onMoveClick = function (e) {
				e.preventDefault();
				AQD.moveFile();
			};
			$moveLink.text($moveLink.text() + ' & Replace').attr('title', 'Click in order to ' + $moveLink.attr('title') + ' and replace usage. Default form though new tab.').click(_onMoveClick);
		}
	},
	/**
	 ** Set up the AjaxQuickDelete object and add the toolbox link.  Called via $(document).ready() during page loading.
	 **/
	install: function () {
		// Disallow performing operations on empty or special pages
		if (AQD.doNothing)
			return;

		// Check edit restrictions and do not install anything if protected
		if (conf.wgRestrictionEdit && conf.wgRestrictionEdit.length) {
			if ($.inArray(conf.wgRestrictionEdit[0], conf.wgUserGroups) === -1) {
				return;
			}
		}

		// wait for document.readyState
		$(function () {
			// Trigger a jQuery event for other scripts that like to know
			// when this script has loaded all translations and is ready to install
			$(document).triggerHandler('scriptLoaded', ['AjaxQuickDelete']);

			// Set up toolbox link
			if (nsNumber === 14) {
				// In categories the discuss-category link
				mw.util.addPortletLink('p-tb', 'javascript:AjaxQuickDelete.discussCategory();', AQD.i18n.toolboxLinkDiscuss, 't-ajaxquickdiscusscat');
			} else {
				// On other pages, the nominate-for-deletion link
				mw.util.addPortletLink('p-tb', 'javascript:AjaxQuickDelete.nominateForDeletion();', AQD.i18n.toolboxLinkDelete, 't-ajaxquickdelete');
			}

			// Install AjaxMoveButton for filemovers and administrators
			if (AQD.$moveLink) {

				// Change Move & Replace link to fully localized text
				AQD.$moveLink.text(AQD.i18n.dropdownMove);

				//Add quicklinks to template
				if ($('#AjaxRenameLink').length) {
					$('#AjaxRenameLink').append('<a href="javascript:AjaxQuickDelete.moveFile();">' + AQD.i18n.moveAndReplace + '</a>')
						.append('<a href="javascript:AjaxQuickDelete.declineRequest(\'move\');" class="ajaxDeleteDeclineMove"><sup> ' + AQD.i18n.anyDecline + '</sup></a>');
				}

				// Install x-To-DR. See [[Template:X-To-DR]]; currently filemover rights required
				$('.ctdr-btn-convert').click(AQD._convertToDR);
				$('.ctdr-btn-remove').click(AQD._removeAnyTag);
				$('.convert-to-dr').show();
			}

			// Install "Process Duplicates"-Link (either in template
			// or if no template was detected and MediaWiki found dupes, behind the link in the dupe-section)
			if (AQD.userRights === 'sysop' && nsNumber === 6) {
				if ($('#AjaxDupeProcess').length) {
					$('#AjaxDupeProcess').append($('<a>', {
							href: 'javascript:AjaxQuickDelete.processDupes();',
							text: "Process Duplicates"
						})).show();
				} else {
					var dupeSection = $('.mw-imagepage-duplicates');
					if (dupeSection.length) {
						dupeSection.find('li:first')
						.append($('<span>', {
								style: 'display:none',
								id: 'AjaxDupeDestination',
								text: dupeSection.find('a').attr('title')
							}))
						.append(' ', $('<sup>').append($('<a>', {
									href: 'javascript:AjaxQuickDelete.processDupes();',
									text: "[Process Duplicates]",
									style: 'background:#CEEDB4'
								})));
					}
				}
			}
			// Extra buttons
			if (mw.user.options.get('gadget-QuickDelete') === '1') {
				// Wait until the user's js was loaded and executed
				mw.loader.using(['ext.gadget.QuickDelete', 'user'], function () {
					AQD.doInsertTagButtons();
				});
			}
		});
	},

	/**
	 ** Ensure that all variables are in a good state
	 ** You must call this method before doing anything!
	 ** TODO: Never override pageName, always clean task queue
	 **/
	initialize: function () {
		pageName = conf.wgPageName;
		this.tasks = [];
		this.destination = undefined;
		this.details = undefined;
	},

	/**
	 ** If a file exists, exchange the messages (very hackish)
	 ** so the user is prompted to choose another destination
	 ** TODO: Develop an improved solution
	 **/
	fileExists: function () {
		this.i18n.moveDestination = this.i18n.moveOtherDestination;
		this.moveFile();
	},

	/**
	 ** For moving files
	 **/
	moveFile: function () {
		this.initialize();
		this.showProgress();

		if ($('#AjaxRenameLink').length) {
			this.possibleDestination = $('#AjaxRenameDestination').text();
			this.possibleReason = this.cleanReason($('#AjaxRenameReason').text());
		}

		if ($('#globalusage').length || !$('#mw-imagepage-nolinkstoimage').length)
			this.inUse = true;

		this.addTask('getMoveToken');
		this.addTask('promptForMoveTarget');
		// Let's be sure we have a fresh token and the latest MIME-Info
		this.addTask('getMoveToken');
		this.addTask('doesFileExist');
		this.fileNameExistsCB = 'fileExists';
		this.addTask('movePage');
		this.addTask('removeTemplate');
		this.addTask('queryRedirects');
		this.addTask('replaceUsage');

		// finally reload the page to show changed page
		this.addTask('reloadPage');

		this.nextTask();
	},

	promptForMoveTarget: function () {
		this.showProgress();
		this.prompt([{
					message: this.i18n.moveDestination,
					prefill: this.cleanFileName(this.possibleDestination || pageName),
					returnvalue: 'destination',
					cleanUp: true,
					noEmpty: true
				}, {
					message: this.i18n.reasonForMove,
					prefill: $.trim((this.reason || this.possibleReason || '').replace(/'{2,}/g, '').replace(/\s{2,}/g, '')),
					returnvalue: 'reason',
					cleanUp: true,
					noEmpty: false
				}, {
					message: this.i18n.leaveRedirect,
					prefill: true,
					returnvalue: 'wpLeaveRedirect',
					cleanUp: false,
					noEmpty: false,
					type: 'checkbox'
				}, {
					message: this.i18n.useCORSForReplace,
					prefill: !window.aqdCORSOptOut,
					returnvalue: 'replaceUsingCORS',
					cleanUp: false,
					noEmpty: false,
					type: 'checkbox'
				}
			], this.i18n.movingFile);
		if (this.inUse || this.userRights === 'filemover')
			$('#AjaxQuestion2').attr('disabled', true);
	},

	/**
	 ** For declining a request
	 **/
	declineRequest: function (reason) {
		// No valid reason stated, see the rename guidelines or not an exact duplicate
		this.initialize();

		this.addTask('getMoveToken');
		this.addTask('removeTemplate');

		// finally reload the page to show the template was removed
		this.addTask('reloadPage');

		// extend the reason
		switch (reason) {
		case 'move':
			reason = 'rename request declined: does not comply with [[COM:FR|renaming guidelines]]';
			break;
		}

		this.prompt([{
					message: '',
					prefill: reason || this.declineReason || '',
					returnvalue: 'declineReason',
					cleanUp: false,
					noEmpty: true,
					byteLimit: 250
				}
			], this.i18n.declineRequest);
	},

	insertTagOnPage: function (tag, img_summary, talk_tag, talk_summary, prompt_text, page) {
		this.initialize();

		this.pageName = (page || pageName).replace(/_/g, ' ');
		this.tag = tag + '\n';
		this.img_summary = img_summary;

		// first schedule some API queries to fetch the info we need...

		// get token
		this.addTask('findCreator');

		this.addTask('prependTemplate');

		if (this.isMobile()) {
			if (/(?:copyvio|nsd|npd|nld)/.test(tag))
				this.addTask('listMobileUploadSpeedy');
		}

		// Cave: insertTagOnPage is inserted as javascript link and therefore talk_tag can be "undefined"/string
		if (talk_tag && talk_tag !== 'undefined') {
			this.talk_tag = talk_tag.replace('%FILE%', this.pageName);
			this.talk_summary = talk_summary.replace('%FILE%', '[[:' + this.pageName + ']]');

			this.usersNeeded = true;
			this.addTask('notifyUploaders');
		}
		this.addTask('reloadPage');

		if (tag.indexOf('%PARAMETER%') !== -1) {
			this.prompt([{
						message: '',
						prefill: '',
						returnvalue: 'reason',
						cleanUp: true,
						noEmpty: true,
						minLength: 1
					}
				], prompt_text || this.i18n.reasonForDeletion);
		} else {
			this.nextTask();
		}
	},

	discussCategory: function () {
		// reset task list in case an earlier error left it non-empty
		this.initialize();

		this.pageName = pageName.replace(/_/g, ' ');
		this.startDate = new Date();
		this.tag = '{' + '{subst:cfd}}';
		this.img_summary = 'This category needs discussion';
		this.talk_tag = '{' + '{subst:cdw|' + pageName + '}}';
		this.talk_summary = "[[:" + pageName + "]] needs discussion";
		this.subpage_summary = 'Starting category discussion';

		// set up some page names we'll need later
		this.requestPage = 'Commons:Categories for discussion/' + this.formatDate("YYYY/MM/") + pageName;
		this.dailyLogPage = 'Commons:Categories for discussion/' + this.formatDate("YYYY/MM");

		// first schedule some API queries to fetch the info we need...
		this.addTask('findCreator');

		// ...then schedule the actual edits
		this.addTask('notifyUploaders');
		this.addTask('prependTemplate');
		this.addTask('createRequestSubpage');
		this.addTask('listRequestSubpage');

		// finally reload the page to show the deletion tag
		this.addTask('reloadPage');

		var lazyLoadNode = this.createLazyLoadNode(this.i18n.moreInformation, 'MediaWiki:Gadget-AjaxQuickDelete.js/DiscussCategoryInfo', '#AjaxQuickDeleteCatInfo');

		this.prompt([{
					message: '',
					prefill: '',
					returnvalue: 'reason',
					cleanUp: true,
					appendNode: lazyLoadNode,
					noEmpty: true,
					parseReason: true
				}
			], this.i18n.reasonForDiscussion);

	},
	nominateForDeletion: function (page) {
		var o = this;

		// reset task list in case an earlier error left it non-empty
		this.initialize();

		mw.loader.using(['mediawiki.String', 'jquery.ui'], function () {
			var mwStr = require('mediawiki.String');
			o.pageName = (page || pageName).replace(/_/g, ' ');
			o.startDate = new Date();

			// set up some page names we'll need later
			var requestPage = o.pageName;

			// MediaWiki has an ugly limit of 255 bytes per title, excluding the namespace
			while (mwStr.byteLength(requestPage) + mwStr.byteLength(o.requestPagePrefix.replace(/^.+?\:/, '')) >= 255) {
				requestPage = $.trim(requestPage.slice(0, requestPage.length - 1));
			}
			o.requestPage = o.requestPagePrefix + requestPage;
			o.dailyLogPage = o.requestPagePrefix + o.formatDate("YYYY/MM/DD");

			o.tag = "{{delete|reason=%PARAMETER%|subpage=" + requestPage + o.formatDate("|year=YYYY|month=MON|day=DAY}}\n");

			switch (nsNumber) {
				// On MediaWiki pages, wrap inside comments (for css and js)
			case 8:
				o.tag = '/*' + o.tag + '*/';
				break;
				// On templates and creator/institution-templates: Wrap inside <noinclude>s.
			case 10:
			case 100:
			case 106:
				o.tag = '<noinclude>' + o.tag + '</noinclude>';
				break;
				// Lua comments
			case 828:
				o.tag = '\n--[=[ ' + o.tag + ' ]=]\n';
			}
			o.img_summary = 'Nominating for deletion';
			o.talk_tag = '{' + '{subst:idw|' + requestPage + '}}';
			o.talk_summary = "[[:" + o.pageName + "]] has been nominated for deletion";
			o.subpage_summary = 'Starting deletion request';

			// first schedule some API queries to fetch the info we need...
			o.addTask('findCreator');

			// ...then schedule the actual edits
			o.addTask('prependTemplate');
			o.addTask('createRequestSubpage');
			o.addTask('listRequestSubpage');
			o.addTask('purge');
			o.addTask('notifyUploaders');
			if (o.isMobile()) {
				o.addTask('listMobileUpload');
			}

			// finally reload the page to show the deletion tag
			o.addTask('reloadPage');

			var lazyLoadNode = o.createLazyLoadNode(o.i18n.moreInformation, 'MediaWiki:Gadget-AjaxQuickDelete.js/DeleteInfo', '#AjaxQuickDeleteDeleteInfo');
			o.prevDRNode = $('<ul>').attr('id', 'AjaxDeletePrevRequests');
			o.secureCall('checkForFormerDR');
			var toAppend = $('<div>').append($('<div>').attr('class', 'ajaxDeleteLazyLoad').css({
						'max-height': Math.max(Math.round($(window).height() / 2) - 250, 100),
						'min-height': 0,
						overflow: 'auto'
					}).append(o.prevDRNode), '<br>', lazyLoadNode);

			o.prompt([{
						message: '',
						prefill: o.reason || '',
						returnvalue: 'reason',
						cleanUp: true,
						noEmpty: true,
						appendNode: toAppend,
						parseReason: true
					}
				], o.i18n.reasonForDeletion);
		});
	},

	// Check whether there was a deletion request for the same title in the past
	checkForFormerDR: function () {
		// Don't search for "kept" when nominating talk pages
		if (nsNumber % 2 === 0) {
			this.talkPage = conf.wgFormattedNamespaces[nsNumber + 1] + ':' + this.pageName.replace(conf.wgCanonicalNamespace + ':', '');
			this.queryAPI({
				action: 'query',
				prop: 'templates',
				titles: this.talkPage,
				tltemplates: 'Template:Kept',
				tllimit: 1
			}, 'formerDRTalk');
		}
		this.queryAPI({
			action: 'query',
			list: 'backlinks',
			bltitle: this.pageName,
			blnamespace: 4,
			blfilterredir: 'nonredirects',
			bllimit: 500
		}, 'formerDRRequestpage');
	},
	formerDRTalk: function (r) {
		var pgs = r.query.pages;
		$.each(pgs, function (id, pg) {
			if ($.isArray(pg.templates)) {
				$('<li>').append($('<a>', {
						text: AQD.i18n.keptAfterDR,
						href: mw.util.getUrl(AQD.talkPage)
					})).prependTo(AQD.prevDRNode);
			} else if (pg.missing === undefined) {
				$('<li>').append($('<a>', {
						text: AQD.i18n.hasTalkpage,
						href: mw.util.getUrl(AQD.talkPage)
					})).appendTo(AQD.prevDRNode);
			}
		});
	},
	formerDRRequestpage: function (r) {
		var bls = r.query.backlinks;
		var _addItem = function (t, m, bl) {
			$('<li>').append($('<a>', {
					text: t.replace('%PAGE%', bl.title),
					href: mw.util.getUrl(bl.title)
				}))[m](AQD.prevDRNode);
		};
		$.each(bls, function (i, bl) {
			if (this.requestPage === bl.title) {
				_addItem(AQD.i18n.mentionedInDR, 'prependTo', bl);
			} else if (/^Commons:Deletion requests\/\D/.test(bl.title)) {
				_addItem(AQD.i18n.mentionedInDR, 'appendTo', bl);
			} else if (/^Commons:Village pump\//.test(bl.title)) {
				_addItem(AQD.i18n.mentionedInForum, 'appendTo', bl);
			}
		});
	},

	renderNode: function ($node, remotecontent, selector) {
		if (selector)
			selector = ' ' + selector;

		$node.load(mw.util.wikiScript() + '?' + $.param({
				'action': 'render',
				'title': remotecontent,
				'uselang': conf.wgUserLanguage
			}) + (selector || ''), function () {
			$node.find('a').each(function (i, el) {
				var $el = $(el);
				$el.attr('href', $el.attr('href').replace('MediaWiki:Anoneditwarning', conf.wgPageName));
			});
		});
		return $node;
	},

	createLazyLoadNode: function (label, page, selector) {
		return $('<div>', {
			style: 'min-height:40px;'
		}).append($('<a>', {
				'href': '#',
				'text': label
			}).click(function (e) {
				e.preventDefault();
				var $content = $(this).parent().find('.ajaxDeleteLazyLoad');
				var $contentInner = $content.find('.ajax-quick-delete-loading');
				if ($contentInner.length) {
					// first time invoked, do the XHR to load the content
					AQD.renderNode($content, $contentInner.data('aqdPage'), selector);
				}
				$content.toggle('fast');
			}), $('<div>', {
				'class': 'ajaxDeleteLazyLoad',
				'style': 'display:none;'
			}).append($('<span>', {
					'class': 'ajax-quick-delete-loading',
					'text': this.i18n.loading
				}).data('aqdPage', page)));
	},
	extractFromHTML: function (DOMElement) {
		var $el = $(DOMElement);

		// ...extract the regular expression from html
		this.templateRegExp = $el.parent().find('.ctdr-regex').text();
		var m = this.templateRegExp.match(/^\/(.+)\/(i)?$/);
		if (!m || !m[1]) {
			var err = new Error('The template does not expose a valid regular expression for {{X-To-DR}}. Go the the template and fix it there.');
			this.fail(err);
			throw err;
		}
		this.templateRegExp = new RegExp(m[1], m[2]);

		// ...and the template name itself
		var template = $el.parent().find('.ctdr-template-name').text();
		this.reason = "This file was initially tagged by %USER%" + (template ? (" as '''" + template + "'''") : "");

		// ...and the decline reason
		this.declineReason = $el.parent().find('.ctdr-template-decline-reason').text();
	},
	removeProgress: function () {
		this.showProgress();
		return this.nextTask();
	},
	/**
	 ** Remove any tag
	 ** @context DOM-Element
	 ** This function must be called with the DOM-Element as this-arg!
	 **/
	_removeAnyTag: function (e) {
		AQD.extractFromHTML(this);
		AQD.removeAnyTag();
		return false;
	},
	removeAnyTag: function () {
		this.initialize();
		this.addTask('declineRequest');
		this.nextTask();
	},
	/**
	 ** Convert any tag to a deletion request
	 ** @context DOM-Element
	 ** This function must be called with the DOM-Element as this-arg!
	 **/
	_convertToDR: function (e) {
		AQD.extractFromHTML(this);
		AQD.convertToDR();
		return false;
	},
	convertToDR: function () {
		// reset task list in case an earlier error left it non-empty
		this.initialize();

		// first schedule a API query to fetch the info we need...
		this.addTask('findTemplateAdder');
		this.addTask('getMoveToken');

		// ...then schedule the actual edits
		this.addTask('removeTemplate');
		this.addTask('removeProgress');
		this.addTask('nominateForDeletion');

		this.declineReason = "This file does not qualify for [[COM:SPEEDY|speedy-deletion]] and a regular deletion request will be started.";

		// Hide the buttons to prevent attempts of duplicate removal
		$('.convert-to-dr').hide();

		// ... and go!
		this.nextTask();
	},
	findTemplateAdder: function () {
		var query = {
			action: 'query',
			prop: 'revisions',
			rvprop: 'user|content',
			titles: pageName.replace(/_/g, ' '),
			rvlimit: 50
		};
		this.queryAPI(query, 'findTemplateAdderCB');
	},
	findTemplateAdderCB: function (result) {
		var m,
		reason,
		user,
		template;
		$.each(result.query.pages, function (id, pg) {
			$.each(pg.revisions, function (iRv, rv) {
				m = rv['*'].match(AQD.templateRegExp);
				if (m) {
					user = rv.user;
					if (m.length > 1 && !template)
						template = m[1];
					if (m.length > 2 && !reason)
						reason = m[2];
				} else {
					return false;
				}
			});
		});
		if (!user)
			throw new Error("Unable to find the person who added the template. This can occur if the template was already removed, the page is deleted or a redirect to the template is used. In this case you must add the redirect to the RegExp of the target template.");
		this.reason = this.reason.replace('%USER%', "[[User:" + user + "|" + user + "]]");
		if (template)
			this.reason += " (" + template + ")";
		if (reason)
			this.reason += " and the most recent rationale was: <tt>" + reason + "</tt>";
		this.nextTask();
	},

	processDupes: function () {
		// reset task list in case an earlier error left it non-empty
		this.initialize();

		if ($('#globalusage').length || !$('#mw-imagepage-nolinkstoimage').length)
			this.inUse = true;

		this.addTask('getDupeDetails');
		this.addTask('compareDetails');
		this.addTask('mergeDescriptions');
		this.addTask('saveDescription');
		this.addTask('replaceUsage');
		this.addTask('queryRedirects');
		this.addTask('deletePage');
		this.addTask('redirectPage');
		this.addTask('reloadPage');
		this.destination = $('#AjaxDupeDestination').text();
		this.nextTask();
	},

	getDupeDetails: function () {
		var query = {
			action: 'query',
			prop: 'imageinfo|revisions|info',
			rvprop: 'content|timestamp',
			intoken: 'edit|delete',
			inprop: 'watched',
			iiprop: 'size|sha1|url',
			iiurlwidth: 365,
			titles: pageName.replace(/_/g, ' ') + '|' + this.destination
		};
		this.queryAPI(query, 'getDupeDetailsCB');
		this.showProgress('Fetching details');
	},

	getDupeDetailsCB: function (result) {
		var pages,
			id,
			pg,
			ii,
			n;
		pages = result.query.pages;
		this.details = [];

		for (id in pages) {
			if (pages.hasOwnProperty(id)) {
				pg = pages[id];
				if (!pg.imageinfo) {
					// Nothing we can change so prevent users reporting
					this.disableReport = true;
					if ($.trim(pg.title) === '{{{1}}}') {
						throw new Error("Error in the duplicate-template, check your language version! (pg.imageinfo is undefined)");
					} else {
						throw new Error("Retrieving information about " + pg.title + " failed. It is possible that it is deleted, the last revision is corrupt or the file is a redirect. (pg.imageinfo is undefined)");
					}
				}
				ii = pg.imageinfo[0];
				n = {
					title: pg.title,
					size: ii.size,
					width: ii.width,
					height: ii.height,
					thumburl: ii.thumburl,
					thumbwidth: ii.thumbwidth,
					thumbheight: ii.thumbheight,
					descriptionurl: ii.descriptionurl,
					sha1: ii.sha1,
					content: pg.revisions[0]['*'],
					starttimestamp: pg.starttimestamp
				};
				this.details.push(n);
				this.edittoken = pg.edittoken;
				this.deletetoken = pg.deletetoken;

				if (pg.watched !== undefined) {
					this.pageWasWatched = true;
				}
			}
		}
		// If order (old=0, new=1) is incorrect: Reverse
		if (this.details[0].title !== pageName.replace(/_/g, ' '))
			this.details.reverse();
		this.nextTask();
	},

	/**
	 ** Edit the current page to add the specified tag.  Assumes that the page hasn't
	 ** been tagged yet; if it is, a duplicate tag will be added.
	 **/
	prependTemplate: function () {
		var page = {
			title: this.pageName,
			text: this.tag,
			editType: 'prependtext',
			minor: false
		};

		if (window.AjaxDeleteWatchFile)
			page.watchlist = 'watch';

		this.showProgress(this.i18n.addingAnyTemplate);
		this.savePage(page, this.img_summary, 'nextTask');
	},

	/**
	 ** Create the DR subpage (or append a new request to an existing subpage).
	 ** The request page will always be watchlisted.
	 **/
	createRequestSubpage: function () {
		this.templateAdded = true; // we've got this far; if something fails, user can follow instructions on template to finish
		var page = {};
		page.title = this.requestPage;
		page.text = "\n=== [[:" + this.pageName + "]] ===\n" + this.reason + " ~~" + "~~\n";
		page.watchlist = 'watch';
		page.editType = 'appendtext';

		if (this.isMobile()) {
			page.text += "\n<noinclude>[[Category:MobileUpload-related deletion requests]]</noinclude>";
		}

		this.showProgress(this.i18n.creatingNomination);

		this.savePage(page, this.subpage_summary, 'nextTask');
	},

	/**
	 ** Transclude the nomination page onto today's DR log page, creating it if necessary.
	 ** The log page will never be watchlisted (unless the user is already watching it).
	 **/
	listRequestSubpage: function () {
		var page = {};
		page.title = this.dailyLogPage;

		// Impossible when using appendtext. Shouldn't not be severe though, since DRBot creates those pages before they are needed.
		// if (!page.text) page.text = "{{"+"subst:" + this.requestPagePrefix + "newday}}";  // add header to new log pages
		page.text = "\n{{" + this.requestPage + "}}\n";
		page.watchlist = 'nochange';
		page.editType = 'appendtext';

		this.showProgress(this.i18n.listingNomination);

		this.savePage(page, "Listing [[" + this.requestPage + "]]", 'nextTask');
	},

	isMobile: function () {
		var isMobile = false,
		cats = conf.wgCategories;

		for (var i = 0, len = cats.length; i < len; i++) {
			isMobile = isMobile || /^Uploaded with Mobile/.test(cats[i]);
		}
		return isMobile;
	},

	listMobileUpload: function () {
		var page = {
			title: 'Commons:Deletion requests/mobile tracking',
			text: '\n{{' + this.requestPage + '}}\n',
			watchlist: 'nochange',
			editType: 'appendtext'
		};

		this.showProgress("Listing mobile upload");

		this.savePage(page, "Listing [[" + this.requestPage + "]]", 'nextTask');
	},

	listMobileUploadSpeedy: function () {
		var page = {
			title: 'Commons:Mobile app/deletion request tracking',
			text: '\n# [[:' + this.pageName + ']]',
			watchlist: 'nochange',
			editType: 'appendtext'
		};

		this.showProgress("Listing mobile upload");

		this.savePage(page, "Listing [[" + this.pageName + "]]", 'nextTask');
	},

	/**
	 ** Notify any uploaders/creators of this page using {{idw}}.
	 **/
	notifyUploaders: function () {
		this.uploadersToNotify = 0;
		for (var user in this.uploaders) {
			if (this.uploaders.hasOwnProperty(user)) {
				if (user === conf.wgUserName)
					continue; // notifying yourself is pointless
				var page = {
					title: this.userTalkPrefix + user,
					text: '\n' + this.talk_tag + ' ~~' + '~~\n',
					editType: 'appendtext',
					redirect: true,
					minor: false
				};
				if (window.AjaxDeleteWatchUserTalk)
					page.watchlist = 'watch';
				this.savePage(page, this.talk_summary, 'uploaderNotified');

				this.showProgress(this.i18n.notifyingUploader.replace('%USER%', user));

				this.uploadersToNotify++;
			}
		}
		if (!this.uploadersToNotify)
			this.nextTask();
	},

	uploaderNotified: function () {
		this.uploadersToNotify--;
		if (!this.uploadersToNotify)
			this.nextTask();
	},

	/**
	 ** Compile a list of uploaders to notify.  Users who have only reverted the file to an
	 ** earlier version will not be notified.
	 ** DONE: notify creator of non-file pages
	 **/
	findCreator: function () {
		var query;
		if (nsNumber === 6) {
			query = {
				action: 'query',
				prop: 'imageinfo|revisions|info',
				rvprop: 'content|timestamp|user',
				rvdir: 'newer',
				rvlimit: 1,
				intoken: 'edit',
				iiprop: 'user|sha1|comment',
				iilimit: 50,
				titles: this.pageName
			};

		} else {
			query = {
				action: 'query',
				prop: 'info|revisions',
				rvprop: 'user|timestamp',
				rvlimit: 1,
				rvdir: 'newer',
				intoken: 'edit',
				titles: this.pageName
			};
		}
		this.showProgress(this.i18n.preparingToEdit);
		this.queryAPI(query, 'findCreatorCB');
	},
	findCreatorCB: function (result) {
		this.uploaders = {};
		var pages = result.query.pages,
		pg = firstItem(pages),
		rv;

		// The edittoken only changes between sessions
		this.edittoken = pg.edittoken;

		if (!pg.revisions) {
			this.disableReport = true;
			throw new Error('The page you are attempting to add a tag to was deleted or moved. Unable to retrieve the content.');
		}
		rv = pg.revisions[0];

		//First handle non-file pages
		if (nsNumber !== 6 || !pg.imageinfo) {

			this.pageCreator = rv.user;
			this.starttimestamp = pg.starttimestamp;
			this.timestamp = rv.timestamp;

			if (this.pageCreator !== undefined) {
				this.uploaders[this.pageCreator] = true;
			}

		} else {
			var info = pg.imageinfo;

			var content = rv['*'];

			var seenHashes = {};
			for (var i = info.length - 1; i >= 0; i--) { // iterate in reverse order
				var iii = info[i];
				if (iii.sha1 && seenHashes[iii.sha1])
					continue; // skip reverts
				seenHashes[iii.sha1] = true;
				// Now exclude bots which only reupload a new version:
				if (mw.libs.commons.isSmallChangesBot(iii.user))
					continue;

				// outsourced to [[MediaWiki:Gadget-libCommons.js]]
				var match = mw.libs.commons.getUploadBotUser(iii.user, content, iii.comment, rv.user);
				if (match) {
					this.uploaders[match] = true;
				}
			}
		}

		this.nextTask();
	},

	getMoveToken: function () {
		var query = {
			action: 'query',
			prop: 'info|revisions|imageinfo',
			rvprop: 'content|timestamp',
			iiprop: 'mime|mediatype',
			intoken: 'edit|move',
			inprop: 'watched',
			titles: pageName.replace(/_/g, ' ')
		};

		this.showProgress(this.i18n.preparingToEdit);
		this.queryAPI(query, 'getMoveTokenCB');
	},

	getMoveTokenCB: function (result) {
		var pages = result.query.pages,
		pg = firstItem(pages);

		if (!pg.revisions) {
			this.disableReport = true;
			throw new Error("The page you are attempting to modify or move is corrupted, was deleted or moved: Unable to retrieve history and contents.");
		}
		// The edittoken only changes between sessions
		$.extend(this, {
			edittoken: pg.edittoken,
			movetoken: pg.movetoken,
			pageContent: pg.revisions[0]['*'],
			starttimestamp: pg.starttimestamp,
			timestamp: pg.revisions[0].timestamp
		});

		if (pg.watched !== undefined) {
			this.pageWasWatched = true;
		}
		var ii = pg.imageinfo;
		if (ii && ii.length && ii[0].mime) {
			var mime = ii[0].mime,
			mediaType = ii[0].mediatype;

			this.mimeFileExtension = mime
				.toLowerCase()
				.replace('image/jpeg', 'jpg')
				.replace(/image\/(?:x\-|vnd\.)?(png|gif|xcf|djvu|svg|tiff)(?:\+xml)?/, '$1')
				.replace(/application\/(ogg|pdf)/, '$1')
				.replace(/video\/(webm)/, '$1')
				.replace('audio\/midi', 'mid')
				.replace(/audio\/(?:x\-|vnd\.)?wave?/, 'wav')
				.replace(/audio\/(?:x\-)?flac/, 'flac');
			if (this.mimeFileExtension.length > 5) {
				this.mimeFileExtension = '';
			} else if (this.mimeFileExtension === 'ogg') {
				switch (mediaType) {
				case 'AUDIO':
					this.mimeFileExtension = 'oga';
					break;
				case 'VIDEO':
					this.mimeFileExtension = 'ogv';
					break;
				}
			}
		}
		this.nextTask();
	},

	doesFileExist: function () {
		if (!this.destination)
			return alert(this.i18n.moveDestination); 
		this.destination = this.cleanFileName(this.destination);
		var query = {
			'action': 'query',
			'prop': 'info|revisions',
			'titles': this.destination,
			'rvprop': 'content',
			'rvlimit': 2
		};
		// usually you would use 'redirects': 1, to detect the redirect target but
		// in this case you would get the revisions for the target and not the redirect

		this.showProgress(this.i18n.checkFileExists);
		this.queryAPI(query, 'doesFileExistCB');
	},

	/**
	 *  Return false if the page does not exist
	 *  or it is a redirect with one revision to the source
	 */
	doesFileExistCB: function (result) {
		if (!result || !result.query || !result.query.pages)
			throw new Error('Checking file name: result.query.pages is undefined.');
		var exists = true,
		pg = firstItem(result.query.pages),
		getRedirRegExp = function (title) {
			title = title.replace(/^(File|Image):/, '').replace(/_/g, ' ');
			return new RegExp('^\\s*#REDIRECT\\s*\\[\\[File\\:[' +
				mw.RegExp.escape(title[0].toUpperCase()) + mw.RegExp.escape(title[0].toLowerCase()) + ']' +
				mw.RegExp.escape(title.slice(1)).replace(/ /g, '[ _]') +
				'\\s*\\]\\]',
				'');
		};

		if (pg.missing !== undefined) {
			exists = false;
		} else if (pg.revisions.length === 1 && getRedirRegExp(pageName).test(pg.revisions[0]['*'].replace('Image:', 'File:'))) {
			// There seems to be no way to find out whether a title is a redirect
			// and whether the redirect only consists of one revision
			exists = false;
		}

		if (exists) {
			if (this.fileNameExistsCB)
				this[this.fileNameExistsCB](pg.title.replace(/^File:/, ''));
			return;
		}

		this.nextTask();
	},

	removeTemplate: function () {
		var page,
		newText;
		this.replaceWith = (this.replaceWith || (this.templateRegExp ? '' : '$1$2'));
		// Remove the template from the text. In case there is an empty line before, remove this also.
		newText = this.pageContent.replace((this.templateRegExp || /(?:([^\=])\n)?\{\{(?:rename|rename media|move)\s*\|.*?\}\}(?:\n([^\=]))?/i), this.replaceWith);
		if (newText === this.pageContent) {
			return this.nextTask();
		}
		// If nothing remains, add the no-license-template (this also to prevent abuse filter blocking this edit because of page blanking)
		page = {
			title: (this.destination || pageName),
			text: $.trim(newText) || '{{subst:nld}}',
			editType: 'text',
			starttimestamp: this.starttimestamp,
			timestamp: this.timestamp,
			watchlist: 'nochange'
		};

		this.showProgress(this.i18n.removingTemplate);
		this.savePage(page, (this.declineReason || "Removing template; rename done"), 'nextTask');
	},

	replaceUsage: function () {
		this.showProgress(this.i18n.replacingUsage);

		if (!this.inUse)
			return this.nextTask();

		var reasonShort = '[[COM:Duplicate|Duplicate]]:';

		if (!this.details) {
			AQD.reason = AQD.reason.replace(/\[\[Commons:File[_ ]renaming[^\[\]]*\]\]:? ?/i, '');
			reasonShort = '[[COM:FR|File renamed]]:';
		}
		mw.loader.using('ext.gadget.libGlobalReplace', function () {
			if (AQD.replaceUsingCORS) {
				mw.libs.globalReplace(pageName, AQD.destination, reasonShort,
					AQD.reason)
				.fail(function (err) {
					throw new Error(err);
				})
				.done(function () {
					AQD.nextTask();
				})
				.progress(function (r) {
					AQD.showProgress(r);
					if (window.console && console.log) {
						console.log(r);
					}
				});
			} else {
				mw.libs.globalReplaceDelinker(pageName, AQD.destination, reasonShort + ' ' + AQD.reason, function () {
					AQD.nextTask();
				}, function (err) {
					throw new Error(err);
				});
			}
		});
	},
	redirectPage: function () {
		var page = {
			title: pageName,
			text: '#REDIRECT [[' + this.destination + ']]',
			editType: 'text',
			watchlist: AQD.pageWasWatched ? 'watch' : 'nochange'
		};

		this.showProgress(this.i18n.redirectingFile);
		this.savePage(page, 'Redirecting to duplicate file', 'nextTask');
	},
	saveDescription: function () {
		var page = {
			title: this.destination,
			text: this.newPageText,
			editType: 'text',
			watchlist: AQD.pageWasWatched ? 'watch' : 'nochange'
		};

		this.showProgress(this.i18n.savingDescription);
		this.savePage(page, 'Merging details from duplicate ([[' + pageName + ']])', 'nextTask');
	},

	/**
	 **  Updates the redirects to the current page
	 **  when moving or processing dupes immediately
	 **  to prevent double redirects
	 **/
	queryRedirects: function () {
		this.queryAPI({
			action: 'query',
			generator: 'backlinks',
			gblfilterredir: 'redirects',
			prop: 'revisions',
			rvprop: 'content',
			gbltitle: pageName.replace(/_/g, ' ')
		}, 'updateRedirects');
		this.showProgress("What-links-here");
	},
	updateRedirects: function (result) {
		AQD.redirectsToUpdate = 0;

		if (result.query) {
			this.showProgress("Updating redirects");
			$.each(result.query.pages, function (id, pg) {
				var rv = pg.revisions[0];
				if (!rv || !rv['*'])
					return;

				var page = {
					title: pg.title,
					text: rv['*'].replace(/\#\s*REDIRECT\s*\[\[.+/, '#REDIRECT [[' + AQD.destination + ']]'),
					editType: 'text',
					watchlist: 'nochange'
				};

				AQD.savePage(page, 'Updating redirect while processing [[' + pageName.replace(/_/g, ' ') + ']]', 'updateRedirectsCB');
				AQD.redirectsToUpdate++;
			});
		}
		if (!AQD.redirectsToUpdate)
			AQD.nextTask();
	},
	updateRedirectsCB: function () {
		AQD.redirectsToUpdate--;
		if (!AQD.redirectsToUpdate)
			AQD.nextTask();
	},

	/**
	 ** Pseudo-Modal JS windows.
	 **/
	prompt: function (questions, title, width) {
		var o = this;
		var dlgButtons = {};
		dlgButtons[this.i18n.submitButtonLabel] = function () {
			$.each(questions, function (i, v) {
				var response = $('#AjaxQuestion' + i).val();
				if (v.type === 'checkbox')
					response = $('#AjaxQuestion' + i)[0].checked;
				if (v.cleanUp) {
					if (v.returnvalue === 'reason')
						response = AQD.cleanReason(response);
					if (v.returnvalue === 'destination')
						response = AQD.cleanFileName(response);
				}
				AQD[v.returnvalue] = response;
				if (v.returnvalue === 'reason' && AQD.tag) {
					AQD.tag = AQD.tag.replace('%PARAMETER%', response);
					if (AQD.talk_tag)
						AQD.talk_tag = AQD.talk_tag.replace('%PARAMETER%', response);
					AQD.img_summary = AQD.img_summary.replace('%PARAMETER%', response);
					AQD.img_summary = AQD.img_summary.replace('%PARAMETER-LINKED%', '[[:' + response + ']]');
				}
			});
			$(this).dialog('close');
			AQD.nextTask();
		};
		dlgButtons[this.i18n.cancelButtonLabel] = function () {
			$(this).dialog('close');
		};

		var $submitButton,
		$cancelButton;
		var $AjaxDeleteContainer = $('<div>', {
				id: 'AjaxDeleteContainer'
			});

		var _convertToTextarea = function (e) {
			var $el = $(this),
			$input = $el.data('toConvert'),
			$tarea = $('<textarea>', {
					id: $input.attr('id'),
					style: 'height:10em; width:98%; display:none;'
				});

			$el.off();
			$el.fadeOut();
			$input.parent().prepend(
				$tarea
				.data('v', $input.data('v')).data('parserResultNode', $input.data('parserResultNode'))
				.val($input.val()).keyup(_parseReason).on('keyup input', _validateInput));
			$tarea.slideDown();
			$input.remove();
		};

		var _parseReason = function (event) {
			var $el = $(this),
			$parserResultNode = $el.data('parserResultNode');

			if (!$parserResultNode)
				return;

			$parserResultNode.css('color', '#877');

			var _gotParsedText = function (r) {
				try {
					$parserResultNode.html(r);
					$parserResultNode.css('color', '#000');
				} catch (ex) {}
			};
			mw.loader.using(['ext.gadget.libAPI'], function () {
				mw.libs.commons.api.parse($el.val(), conf.wgUserLanguage, pageName, _gotParsedText);
			});
		};

		var _validateInput = function (event) {
			var $el = $(this),
			v = $el.data('v');

			if (v.noEmpty) {
				if ($.trim($el.val()).length < (v.minLength || 10)) {
					$submitButton.button('option', 'disabled', true);
				} else {
					$submitButton.button('option', 'disabled', false);
				}
			}
			if (($el.prop('nodeName') !== 'TEXTAREA') &&
				(event.which === 13) &&
				(v.enterToSubmit !== false) &&
				!$submitButton.button('option', 'disabled'))
				$submitButton.click();
		};

		$.each(questions, function (i, v) {
			v.type = (v.type || 'text');
			if (v.type === 'textarea') {
				$AjaxDeleteContainer.append('<label for="AjaxQuestion' + i + '">' + v.message + '</label>').append('<textarea rows=20 id="AjaxQuestion' + i + '">');
			} else {
				$AjaxDeleteContainer.append('<label for="AjaxQuestion' + i + '">' + v.message + '</label>').append('<input type="' + v.type + '" id="AjaxQuestion' + i + '" style="width:97%;">');
			}

			var curQuestion = $AjaxDeleteContainer.find('#AjaxQuestion' + i);

			if (v.parseReason) {
				var $parserResultNode = $('<div>', {
						id: 'AjaxQuestionParse' + i,
						html: '&nbsp;'
					});
				$AjaxDeleteContainer.append('<br><label for="AjaxQuestionParse' + i + '">' + o.i18n.previewLabel + '</label>').append($parserResultNode);

				curQuestion.data('parserResultNode', $parserResultNode).keyup(_parseReason);
			}
			if (v.type !== 'textarea')
				$AjaxDeleteContainer.append('<br><br>');
			if (v.appendNode) {
				$AjaxDeleteContainer.append(v.appendNode);
			}
			if (typeof v.byteLimit === 'number') {
				mw.loader.using('jquery.lengthLimit', function () {
					curQuestion.byteLimit(v.byteLimit);
				});
			}

			curQuestion.data('v', v);
			curQuestion.on('keyup input', _validateInput);

			// SECURITY: prefill could contain evil jsCode. Never use it unescaped!
			// Use .val() or { value: prefill } or '<input value="' + mw.html.escape() + '" ...>
			curQuestion.val(v.prefill);
			if (v.type === 'checkbox')
				curQuestion.attr('checked', v.prefill).attr('style', 'margin-left: 5px');
		});

		if (mw.user.isAnon()) {
			AQD.renderNode($('<div>', {
					id: 'ajaxDeleteAnonwarning'
				}), 'MediaWiki:Anoneditwarning').appendTo($AjaxDeleteContainer);
		}

		var $dialog = $('<div>').append($AjaxDeleteContainer).dialog({
				width: (width || 600),
				modal: true,
				title: title,
				dialogClass: "wikiEditor-toolbar-dialog",
				close: function () {
					$(this).dialog("destroy");
					$(this).remove();
				},
				buttons: dlgButtons,
				open: function () {
					// Look out for http://bugs.jqueryui.com/ticket/6830 / jQuery UI 1.9
					var $buttons = $(this).parent().find('.ui-dialog-buttonpane button');
					$submitButton = $buttons.eq(0).specialButton('proceed');
					$cancelButton = $buttons.eq(1).specialButton('cancel');
				}
			});

		$.each(questions, function (i, v) {
			var curQuestion = $AjaxDeleteContainer.find('#AjaxQuestion' + i);
			curQuestion.keyup();
			if (v.type === 'text') {
				var $q = curQuestion.wrap('<div style="position:relative;">').parent();
				var $i = $.createIcon('ui-icon-arrow-4-diag').attr('title', AQD.i18n.expandToTextarea);
				$('<span>', {
					'class': 'ajaxTextareaConverter'
				}).append($i).appendTo($q).data('toConvert', curQuestion).click(_convertToTextarea);
			}
		});

		$('#AjaxQuestion0').focus().select();
	},

	/**
	 ** Open a jQuery dialog with preview-images and some options
	 ** and information to compare the two files
	 **/
	compareDetails: function () {
		var d = this.details[0],
		f = this.details[1],
		$submitButton,
		$inverseButton,
		$swapButton,
		$overlayButton;

		if (d.sha1 === f.sha1) {
			this.exactDupes = true;
			this.nextTask();
			return;
		}

		var $imgD = $('<div>').append($('<img>', {
					src: d.thumburl,
					height: d.thumbheight,
					width: d.thumbwidth
				}), $('<div>', {
					id: 'AjaxDeleteImgDel',
					html: Math.round(d.size / 1000) + ' KiB <br>' + d.width + '×' + d.height + '<br>'
				}).append(
					$('<a>', {
						href: d.descriptionurl,
						text: d.title,
						target: '_blank'
					})));
		var $imgF = $('<div>').append($('<img>', {
					src: f.thumburl,
					height: f.thumbheight,
					width: f.thumbwidth
				}), $('<div>', {
					id: 'AjaxDeleteImgKeep',
					html: Math.round(f.size / 1000) + ' KiB <br>' + f.width + '×' + f.height + '<br>'
				}).append(
					$('<a>', {
						href: f.descriptionurl,
						text: f.title,
						target: '_blank'
					})));
		var dlgButtons = {};

		dlgButtons[this.i18n.submitButtonLabel] = function () {
			$(this).dialog("close");
			AQD.nextTask();
		};
		dlgButtons[this.i18n.inverseButtonLabel] = function () {
			$(this).dialog("close");
			AQD.destination = pageName.replace(/_/g, ' ');
			pageName = f.title;
			AQD.details.reverse();
			AQD.inUse = true;
			setTimeout(function () {
				AQD.compareDetails();
			}, 10);
		};
		dlgButtons[this.i18n.swapImagesButtonLabel] = function () {
			if ($imgD[0].nextSibling === $imgF[0]) {
				$imgD.before($imgF);
			} else {
				$imgF.before($imgD);
			}
		};
		var $fClone;
		dlgButtons[this.i18n.overlayButtonLabel] = function () {
			if ($fClone) {
				$fClone.remove();
				$fClone = 0;
			} else {
				$fClone = $imgF.clone().appendTo($imgF.parent());
				$fClone.css('position', 'absolute');
				var pos = $imgD.position();
				$fClone.css('top', pos.top - 1);
				$fClone.css('left', pos.left - 1);
				$fClone.fadeTo(0, 0.65);
				// These modules should be already loaded for the dialog but let's be sure
				mw.loader.using(['jquery.ui'], function () {
					// Set width to auto because AjaxQuickDelete.css sets it to a fixed size
					$fClone.css('background', 'rgba(200, 200, 200, 0.5)').css('width', 'auto').css('border', '1px solid #0c9').draggable();
					$fClone.find('img').resizable();
					// In IE, opacity is not fully inerhited
					$fClone.children('div').fadeTo(0, 0.7);
				});
			}
		};

		this.showProgress();

		var $AjaxDupeContainer = $('<div>', {
				id: 'AjaxDupeContainer'
			}).append($imgD, $imgF);
		var $dialog = $('<div>').append($AjaxDupeContainer).dialog({
				width: 800,
				modal: true,
				title: this.i18n.compareDetails,
				draggable: false,
				dialogClass: "wikiEditor-toolbar-dialog",
				close: function () {
					$(this).dialog("destroy");
					$(this).remove();
				},
				buttons: dlgButtons,
				open: function () {
					var $buttons = $(this).parent().find('.ui-dialog-buttonpane button');
					$submitButton = $buttons.eq(0).specialButton('proceed');
					$inverseButton = $buttons.eq(1).button({
							icons: {
								primary: 'ui-icon-refresh'
							}
						});
					$swapButton = $buttons.eq(2).button({
							icons: {
								primary: 'ui-icon-transfer-e-w'
							}
						});
					$overlayButton = $buttons.eq(3).button({
							icons: {
								primary: 'ui-icon-newwin'
							}
						});
					$swapButton.css('float', (('left' === $swapButton.css('float')) ? 'right' : 'left'));
					$overlayButton.css('float', (('left' === $overlayButton.css('float')) ? 'right' : 'left'));
				}
			});
		mw.loader.load(['ext.gadget.libGlobalReplace', 'ext.gadget.libWikiDOM']);
	},

	mergeDescriptions: function () {
		var newPageText = this.details[1].content;
		mw.loader.using(['ext.gadget.libGlobalReplace', 'ext.gadget.libWikiDOM'], function () {
			newPageText = mw.libs.wikiDOM.nowikiEscaper(newPageText).doCleanUp();

			AQD.showProgress();
			AQD.prompt([{
						message: '',
						prefill: AQD.details[0].content,
						returnvalue: 'discard',
						cleanUp: false,
						noEmpty: false,
						type: 'textarea',
						enterToSubmit: false
					}, {
						message: '',
						prefill: newPageText,
						returnvalue: 'newPageText',
						cleanUp: false,
						noEmpty: false,
						type: 'textarea',
						enterToSubmit: false
					}, {
						message: AQD.i18n.useCORSForReplace,
						prefill: !window.aqdCORSOptOut,
						returnvalue: 'replaceUsingCORS',
						cleanUp: false,
						noEmpty: false,
						type: 'checkbox'
					}
				], AQD.i18n.mergeDescription, 800);

			AQD.destination = AQD.details[1].title;
			AQD.reason = 'Exact or scaled-down duplicate: [[:' + AQD.destination + ']]';
		});
	},

	/**
	 ** Correct the MIME-Type; Accepts only valid file names (with extension)
	 ** Either a file name is passed or the destination property is used
	 **/
	correctMIME: function (fn) {
		// If the current mime-type is available to the script, check it;
		// MediaWiki sometimes allows uploading mismatching mimetypes but not moving
		var f = fn || this.destination;
		if (this.mimeFileExtension) {
			f = f.replace(/\.\w{2,5}$/, '.' + this.mimeFileExtension);
		}
		if (!fn) {
			this.destination = f;
			return this.nextTask();
		} else {
			return f;
		}
	},

	cleanFileName: function (uncleanName, ignoreMIME) {
		// Remove Namespace
		uncleanName = uncleanName.replace(/^(?:Image|File):/i, '')
			// Convert extension to lower case
			.replace(/\.\w{2,5}$/, function ($e) {
				return $e.toLowerCase();
			})
			// jpeg -> jpg
			.replace(/\.jpe*g$/, '.jpg')
			// First cleanUp from Flinfo (FlinfoOut.php) by Flominator and Lupo
			.replace(/~{3,}/g, '') // "signature"
			.replace(/[\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]/, ' ') // remove NBSP and other unusual spaces
			.replace(/\s+|_/g, ' ') // (multiple) whitespace
			.replace(/[\x00-\x1f\x7f]/g, '')
			.replace(/%([0-9A-Fa-f]{2})/g, '% $1') // URL encoding stuff
			.replace(/&(([A-Za-z0-9\x80-\xff]+|#[0-9]+|#x[0-9A-Fa-f]+);)/g, '& $1') // URL-params?
			.replace(/''/g, '\"')
			.replace(/[:\/|#]/g, '-')
			.replace(/[\]\}>]/g, ')')
			.replace(/[\[\{<]/g, '(');

		// Check file extension
		var currentExt = pageName.toLowerCase().replace(/.*?\.(\w{2,5})$/, '$1').replace('jpeg', 'jpg');
		if (!ignoreMIME)
			currentExt = this.mimeFileExtension || currentExt;

		var reCurrentExt = new RegExp('\\.' + mw.RegExp.escape(currentExt) + '$', 'i'),
		reOldExt = new RegExp('\\.' + mw.RegExp.escape(uncleanName.replace(/.*?\.(\w{2,5})$/, '$1')) + '$', 'i');

		// If new file name is without (right) extension, add the one from the old name
		if (!reCurrentExt.test(uncleanName)) {
			// First, try to replace the old extension
			uncleanName = uncleanName.replace(reOldExt, '.' + currentExt);
			if (!reCurrentExt.test(uncleanName)) {
				// If this did not work, then simply append the new extention
				uncleanName += '.' + currentExt;
			}
		}

		// Capitalize the first letter and prefix the namespace
		return 'File:' + ucFirst(uncleanName);
	},
	cleanReason: function (uncleanReason) {
		// trim whitespace
		uncleanReason = uncleanReason.replace(/^\s*(.+)\s*$/, '$1');
		// remove signature
		uncleanReason = uncleanReason.replace(/(?:\-\-|–|—)? ?~{3,5}$/, '').replace(/^~{3,5} ?/, '');
		return uncleanReason;
	},

	/**
	 ** For display of progress messages.
	 **/
	showProgress: function (message) {
		if (!message) {
			if (this.progressDialog)
				this.progressDialog.remove();
			this.progressDialog = 0;
			document.body.style.cursor = 'default';
			return;
		}
		if ($('#feedbackContainer').length) {
			$('#feedbackContainer').html(message);
		} else {
			document.body.style.cursor = 'wait';

			this.progressDialog = $('<div>').html('<div id="feedbackContainer">' + (message || this.i18n.preparingToEdit) + '</div>').dialog({
					width: 450,
					height: 'auto',
					minHeight: 90,
					modal: true,
					resizable: false,
					draggable: false,
					closeOnEscape: false,
					dialogClass: 'ajaxDeleteFeedback',
					open: function () {
						$(this).parent().find('.ui-dialog-titlebar').hide();
					},
					close: function () {
						$(this).dialog("destroy");
						$(this).remove();
					}
				});
		}

	},
	/**
	 ** Submit an edited page.
	 **/
	savePage: function (page, summary, callback) {
		if (AQD.edittoken)
			mw.user.tokens.set('editToken', AQD.edittoken);

		$.extend(true, page, {
			cb: function (r) {
				AQD.secureCall(callback, r);
			},
			// r-result, query, text
			errCb: function (t, r, q) {
				AQD.fail(t);
			},
			summary: summary
		});

		mw.loader.using(['ext.gadget.libAPI'], function () {
			mw.libs.commons.api.editPage(page);
		});
	},

	movePage: function () {
		mw.user.tokens.set('moveToken', AQD.movetoken);

		// Some users don't get it: They want to move pages to themselves.
		if (pageName.replace(/_/g, ' ') === AQD.destination)
			return AQD.nextTask();
		mw.loader.using(['ext.gadget.libAPI'], function () {
			var moveArgs = {
				cb: function () {
					AQD.nextTask();
				},
				// r-result, query, text
				errCb: function (t, r, q) {
					AQD.fail(t);
				},
				from: pageName,
				to: AQD.destination,
				reason: AQD.reason,
				movetalk: true,
				// Nochange won't watch the file under the new location
				// even if it was watched under the old location
				watchlist: AQD.pageWasWatched ? 'watch' : 'nochange'
			};
			// Option to not leave a redirect behind, MediaWiki default does leave one behind
			// Just like movetalk, an empty parameter sets it to true (true to not leave a redirect behind)
			if (AQD.wpLeaveRedirect === false) {
				moveArgs.noredirect = true;
			}
			AQD.showProgress(AQD.i18n.movingFile);
			mw.libs.commons.api.movePage(moveArgs);
		});
	},

	deletePage: function () {
		mw.user.tokens.set('deleteToken', AQD.deletetoken);

		mw.loader.using(['ext.gadget.libAPI'], function () {
			AQD.showProgress(AQD.i18n.deletingFile);
			mw.libs.commons.api.deletePage({
				cb: function () {
					AQD.nextTask();
				},
				// r-result, query, text
				errCb: function (t, r, q) {
					AQD.fail(t);
				},
				title: pageName,
				reason: AQD.reason
			});
		});
	},

	purge: function () {
		// No need for checking success, showing progress, nor for waiting for task to complete
		this.nextTask();
		$.post(this.apiURL, {
			format: 'json',
			action: 'purge',
			forcelinkupdate: 1,
			titles: pageName
		});
	},

	/**
	 ** Does a MediaWiki API request and passes the result to the supplied callback (method name).
	 ** Uses POST requests for everything for simplicity.
	 **/
	queryAPI: function (params, callback) {
		mw.loader.using(['ext.gadget.libAPI'], function () {
			mw.libs.commons.api.query(params, {
				method: 'POST',
				cache: false,
				cb: function (r) {
					AQD.secureCall(callback, r);
				},
				// r-result, query, text
				errCb: function (t, r, q) {
					AQD.fail(t);
				}
			});
		});
	},

	/**
	 ** Method to catch errors and report where they occurred
	 **/
	secureCall: function (fn) {
		var o = AQD;
		try {
			o.currentTask = arguments[0];
			if ($.isFunction(fn)) {
				return fn.apply(o, Array.prototype.slice.call(arguments, 1)); // arguments is not of type array so we can't just write arguments.slice
			} else if ('string' === typeof fn) {
				return o[fn].apply(o, Array.prototype.slice.call(arguments, 1));
			} else {
				mw.log.warn(fn, this.tasks);
				o.fail('This is not a function!');
			}
		} catch (ex) {
			o.fail(ex);
		}
	},

	/**
	 ** 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.
	 **/
	tasks: [],
	// list of pending tasks
	currentTask: '',
	// current task, for error reporting
	addTask: function (task) {
		this.tasks.push(task);
	},
	nextTask: function () {
		this.secureCall(this.tasks.shift());
	},
	retryTask: function () {
		this.secureCall(this.currentTask);
	},

	/**
	 ** Once we're all done, reload the page.
	 **/
	reloadPage: function () {
		this.showProgress();
		if (this.pageName && this.pageName.replace(/ /g, '_') !== pageName)
			return;
		location.href = mw.util.getUrl(this.destination || pageName);
	},

	/**
	 ** Error handler. Throws an alert at the user and give him
	 ** the possibility to retry or autoreport the error-message.
	 **/
	fail: function (err) {
		var o = this;
		if (typeof err === 'object') {
			var stErr = err.message + ' \n\n ' + err.name;
			if (err.lineNumber)
				stErr += ' @line' + err.lineNumber;
			err = stErr;
		}

		var msg = this.i18n.taskFailure[this.currentTask] || this.i18n.genericFailure;

		//TODO: Needs cleanup
		var fix = '';
		if (this.img_summary === 'Nominating for deletion') {
			fix = (this.templateAdded ? this.i18n.completeRequestByHand : this.i18n.addTemplateByHand);
		}

		var dlgButtons = {};
		dlgButtons[this.i18n.retryButtonLabel] = function () {
			$(this).remove();
			o.retryTask();
		};
		if (-1 !== $.inArray(o.currentTask, ['movePage', 'deletePage', 'notifyUploaders']) && (/code 50\d/.test(err) || /missingtitle/.test(err))) {
			dlgButtons[this.i18n.ignoreButtonLabel] = function () {
				$(this).remove();
				o.nextTask();
			};
		}
		if (!this.disableReport) {
			dlgButtons[this.i18n.reportButtonLabel] = function () {
				$('#feedbackContainer').contents().remove();
				$('#feedbackContainer').append($('<img>', {
						src: '//upload.wikimedia.org/wikipedia/commons/d/de/Ajax-loader.gif'
					})).css('text-align', 'center');
				var randomId = Math.round(Math.random() * 1099511627776);
				var toSend = '\n== Autoreport by AjaxQuickDelete ' + randomId + ' ==\n' + err + '\n++++\n:Task: ' + o.currentTask + '\n:NextTask: ' + o.tasks[0] + '\n:LastTask: ' + o.tasks[o.tasks.length - 1] +
					'\n:Page: {{Page|1=' + (o.pageName || pageName) + '}}\n:Skin: ' + mw.user.options.get('skin') +
					'\n:[{{fullurl:Special:Contributions|target={{subst:urlencode:{{subst:REVISIONUSER}}}}&offset={{subst:REVISIONTIMESTAMP}}}} Contribs] ' +
					'[{{fullurl:Special:Log|user={{subst:urlencode:{{subst:REVISIONUSER}}}}&offset={{subst:REVISIONTIMESTAMP}}}} Log] ' +
					'before error';
				$.post(o.apiURL, {
					'action': 'edit',
					'format': 'json',
					'title': 'MediaWiki talk:Gadget-AjaxQuickDelete.js/auto-errors',
					'summary': '[[#Autoreport by AjaxQuickDelete ' + randomId + '|Reporting an AjaxQuickDelete error.]] Random ID=' + randomId,
					'appendtext': toSend,
					'token': (o.edittoken || mw.user.tokens.get('csrfToken'))
				}, function () {
					o.reloadPage();
				});
			};
		}
		dlgButtons[this.i18n.abortButtonLabel] = function () {
			$(this).remove();
		};

		this.disableReport = false;
		this.showProgress();
		this.progressDialog = $('<div>').append($('<div>', {
					id: 'feedbackContainer',
					html: (msg + ' ' + fix + '<br>' + this.i18n.errorDetails + '<br>' + mw.html.escape(err) + '<br>' + (this.tag ? (this.i18n.tagWas + this.tag) : '') + '<br><a href="' + mw.util.getUrl('MediaWiki talk:AjaxQuickDelete.js') + '" >' + this.i18n.errorReport + '</a>')
				})).dialog({
				width: 550,
				modal: true,
				closeOnEscape: false,
				title: this.i18n.errorDlgTitle,
				dialogClass: "ajaxDeleteError",
				buttons: dlgButtons,
				close: function () {
					$(this).dialog("destroy");
					$(this).remove();
				}
			});
		if (mw.log.warn) mw.log.warn(err);
	},

	/**
	 ** Very simple date formatter.  Replaces the substrings "YYYY", "MM" and "DD" in a
	 ** given string with the UTC year, month and day numbers respectively.
	 ** Also replaces "MON" with the English full month name and "DAY" with the unpadded day.
	 **/
	formatDate: function (fmt, date) {
		return mw.libs.commons.formatDate(fmt, date, (mw.libs.commons.api && mw.libs.commons.api.getCurrentDate() || new Date()));
	},

	// Constants
	// DR subpage prefix
	requestPagePrefix: "Commons:Deletion requests/",
	// user talk page prefix
	userTalkPrefix: conf.wgFormattedNamespaces[3] + ":",
	// MediaWiki API script URL
	apiURL: mw.util.wikiScript('api'),
	// Max number of errors that are allowed for silent retry
	apiErrorThreshold: 10,

	// Translatable strings
	i18n: {
		toolboxLinkDelete: "Nominar para borrado",
		toolboxLinkDiscuss: "Nominar categoría para discusión",

		// GUI reason prompt form
		reasonForDeletion: "Por que debería borrarse este ficheiro?",
		reasonForDiscussion: "Por que esta categoría necesita discusión?",
		moreInformation: "Máis información",
		loading: "Cargando...",

		keptAfterDR: "Esta páxina mantívose despois dunha petición de borrado. Por favor, contacta co administrador que a mantivo antes de renominala.",
		hasTalkpage: "Hai unha páxina de conversa. Considera lela ou engadir os teus comentarios.",
		mentionedInDR: "Considera ler o debate de borrado –%PAGE%– que liga a esta páxina.",
		mentionedInForum: "En %PAGE%, esta páxina é parte dunha conversa.",

		// Labels
		previewLabel: "Previsualizar:",
		submitButtonLabel: "Proceder",
		cancelButtonLabel: "Cancelar",
		abortButtonLabel: "Abortar",
		reportButtonLabel: "Avisar automaticamente",
		retryButtonLabel: "Reintentar",
		ignoreButtonLabel: "Ignorar e continuar",
		inverseButtonLabel: "Inversa. Deixar este borrar outro",
		swapImagesButtonLabel: "Cambiar para comparar",
		overlayButtonLabel: "Recubrir para comparar",
		expandToTextarea: "Expandir a área de texto",

		// GUI progress messages
		preparingToEdit: "Preparando para editar páxinas... ",
		creatingNomination: "Creando páxina de nomeamento... ",
		listingNomination: "Engadindo páxina de nomeamento á lista diaria... ",
		addingAnyTemplate: "Engadindo modelo a " + conf.wgCanonicalNamespace.toLowerCase() + " page... ",
		notifyingUploader: "Avisando a %USER%... ",

		// Extended version
		toolboxLinkSource: "Sen fonte",
		toolboxLinkLicense: "Sen licenza",
		toolboxLinkPermission: "Sen permisos",
		toolboxLinkCopyvio: "Avisar de violación de copyright",
		reasonForCopyvio: "Por que este ficheiro é unha violación de copyright?",

		// For moving files
		notAllowed: "Non ten os permisos necesarios para mover ficheiros",
		reasonForMove: "Por que quere mover este ficheiro?",
		moveDestination: "Cal debe ser o novo nome?",
		moveOtherDestination: "O nome que incou xa existe. Por favor, escolla outro nome.",
		checkFileExists: "Revisando se existe o ficheiro",
		movingFile: "Movendo o ficheiro",
		replacingUsage: "Mandando a CommonsDelinker substituír tódolos usos",
		dropdownMove: "Mover e substituír",
		leaveRedirect: "Deixar unha redirección:",
		moveAndReplace: "Mover ficheiro e substituír tódolos usos",

		// For declining any request
		removingTemplate: "Eliminando modelo",
		declineRequest: "Por que queres rechazar a petición?",
		anyDecline: "Rechazar petición",

		//For Duplicates
		useCORSForReplace: "Intentar substituír inmediatamente o ficheiro usando a súa conta de usuario:",
		deletingFile: "Borrando ficheiro",
		compareDetails: "Please compare the images before merging the descriptions. The image with the bold text will be deleted.",
		mergeDescription: "Please now merge the file descriptions",
		redirectingFile: "Redirecting file",
		savingDescription: "Saving new details",

		// Errors
		errorDlgTitle: "Erro",
		genericFailure: "Houbo un erro mentres se intentaba realizar a acción solicitada. ",
		taskFailure: {
			listUploaders: "An error occurred while determining the " + (nsNumber === 6 ? " uploader(s) of this file" : "creator of this page") + ".",
			loadPages: "An error occurred while preparing to nominate this " + conf.wgCanonicalNamespace.toLowerCase() + " for deletion.",
			prependDeletionTemplate: "An error occurred while adding the {{delete}} template to this " + conf.wgCanonicalNamespace.toLowerCase() + ".",
			createRequestSubpage: "An error occurred while creating the request subpage.",
			listRequestSubpage: "An error occurred while adding the deletion request to today's log.",
			notifyUploaders: "An error occurred while notifying the " + (nsNumber === 6 ? " uploader(s) of this file" : "creator of this page") + ".",
			movePage: "Error while moving the page.",
			deletePage: "Error deleting the page."
		},
		addTemplateByHand: "To nominate this " + conf.wgCanonicalNamespace.toLowerCase() + " for deletion, please edit the page to add the {{delete}} template and follow the instructions shown on it.",
		completeRequestByHand: "Please follow the instructions on the deletion notice to complete the request.",
		errorDetails: "A detailed description of the error is shown below:",
		errorReport: "Manually report the error here or click on <tt>Report automatically</tt> to send an automatic error-report.",
		tagWas: "The tag to be inserted into this page was "
	}
};

AQD.preinstall();
if (conf.wgUserLanguage !== 'en') {
	$.ajax({
		url: mw.util.wikiScript(),
		dataType: 'script',
		data: {
			title: 'MediaWiki:Gadget-AjaxQuickDelete.js/' + conf.wgUserLanguage + '.js',
			action: 'raw',
			ctype: 'text/javascript',
			// Allow caching for 28 days
			maxage: 2419200,
			smaxage: 2419200
		},
		cache: true,
		success: AQD.install,
		error: AQD.install
	});
} else {
	AQD.install();
}

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