User:MTLskyline/monobook.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
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.
// 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
// @rev 23:30, 2 November 2018 (UTC)
// 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>

/* eslint indent:["error","tab",{"outerIIFEBody":0}] */
/* eslint-disable one-var, vars-on-top, camelcase, no-underscore-dangle, valid-jsdoc */ // extends:wikimedia
/* global jQuery:true, mediaWiki:false, require */
( function ( $, mw ) {
'use strict';
// Guard against multiple inclusions
if ( window.AjaxQuickDelete ) { return; }

var AQD,
	conf = mw.config.get( [
		'wgArticleId',
		'wgCanonicalNamespace',
		'wgCanonicalSpecialPageName',
		'wgCategories',
		'wgFormattedNamespaces',
		'wgNamespaceNumber',
		'wgPageName',
		'wgRestrictionEdit',
		'wgUserGroups',
		'wgUserLanguage',
		'wgUserName',
		'wgIsRedirect'
	] ),
	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 ];
		}
	}

}
$.ucFirst = function ( s ) {
	return s[ 0 ].toUpperCase() + s.slice( 1 );
};

// Create the AjaxQuickDelete-singleton (object literal)
AQD = window.AjaxQuickDelete = {
	// When maintaining this script always bump this number!
	version: '1.0.6',
	/**
	 ** 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.</div>' );
		}

		AQD.doNothing = ( !conf.wgArticleId || nsNumber < 0 || /^Commons:Deletion/.test( pageName ) );

		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 ( $.inArray( AQD.userRights, [ 'filemover', 'sysop' ] ) !== -1 && 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.' ).on( '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', '#', AQD.i18n.toolboxLinkDiscuss, 't-ajaxquickdiscusscat' ) ).on( 'click', function ( e ) {
					e.preventDefault();
					AQD.discussCategory();
				} );
			} else {
				// On other pages, the nominate-for-deletion link
				$( mw.util.addPortletLink( 'p-tb', '#', AQD.i18n.toolboxLinkDelete, 't-ajaxquickdelete' ) ).on( 'click', function ( e ) {
					e.preventDefault();
					AQD.nominateForDeletion();
				} );
			}

			// 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' ).on( 'click', AQD._convertToDR );
				$( '.ctdr-btn-remove' ).on( '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: '#',
						text: AQD.i18n.processDupes,
						style: 'font-weight:bold',
						click: function ( e ) {
							e.preventDefault();
							AQD.processDupes();
						}
					} ) ).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: '#',
								text: '[' + AQD.i18n.processDupes + ']',
								style: 'background:#CEB',
								click: function ( e ) {
									e.preventDefault();
									AQD.processDupes();
								}
							} ) ) );
					}
				}
			}
			// 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;
		this.declineReason = undefined;
		this.notifyUser = true;
	},

	/**
	 ** 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() );
		}

		// Let's be sure we have a fresh token and the latest MIME-Info
		this.addTask( 'getMoveToken' );

		var linkstoimage = $( '#mw-imagepage-section-linkstoimage' );
		if ( $( '#globalusage' ).length ||
		( linkstoimage.length && linkstoimage.find( 'a' ).not( '.mw-redirect' ).length - linkstoimage.find( '.mw-imagepage-linkstoimage-ns2 a[href^="/wiki/User:OgreBot/Uploads"]' ).length ) ) {
			this.inUse = true;
			this.addTask( 'chkPreMoveDecline' );
		}

		this.addTask( 'promptForMoveTarget' );

		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();
		mw.hook( 'aqd.prompt' ).add( function ( AQD ) {
			if ( AQD.inUse || AQD.userRights === 'filemover' ) {
				$( '#AjaxQuestion2' ).prop( 'disabled', true );
			} else if (
				$( '#mw-imagepage-section-filehistory' ).find( 'tr' ).length === 2 &&
				( new Date( AQD.timestamp ) - new Date( AQD.imagetimestamp ) ) / 3600000 < 48
			) {
				// Younger than 2 days!?
				$( '#AjaxQuestion2' ).prop( 'checked', false );
			}
		} );
		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 );
	},

	/* Warn other filemovers */
	chkPreMoveDecline: function () {
		$( '#mw-imagepage-section-linkstoimage' ).find( 'a' ).each( function () {
			if ( $( this ).text() === 'Commons:File renaming/Recently declined rename requests' ) {
				// eslint-disable-next-line no-alert
				alert( AQD.i18n.warnRename );
				return false;
			}
		} );
		this.nextTask();
	},

	/**
	 ** 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' );

		// TODO extend the reason (for summary)
		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, optin_notify ) {
		this.initialize();

		this.pageName = ( page || pageName ).replace( /_/g, ' ' );
		this.tag = tag.replace( '%USER%', conf.wgUserName ) + '\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() && /(?:copyvio|nsd|npd|nld)/.test( tag ) ) {
			this.addTask( 'listMobileUploadSpeedy' );
		}

		var prompt = [];

		// 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;

			prompt.push( {
				message: this.i18n.notifyUser,
				prefill: true,
				returnvalue: 'notifyUser',
				type: 'checkbox'
			} );
			this.addTask( 'notifyUploaders' );
		}
		this.addTask( 'reloadPage' );

		if ( tag.indexOf( '%PARAMETER%' ) !== -1 ) {
			prompt.push( {
				message: '',
				prefill: '',
				returnvalue: 'reason',
				cleanUp: true,
				noEmpty: true,
				minLength: 1
			} );

			this.prompt( prompt, prompt_text || this.i18n.reasonForDeletion );
		} else if ( optin_notify && prompt.length && this.talk_summary ) {
			this.prompt( prompt, this.talk_summary /* || this.i18n.notifyingUploader.replace('%USER%', 'User') */ );
		} 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();
		// eslint-disable-next-line no-useless-escape
		this.tag = '{\{subst:cfd}}';
		this.img_summary = 'This category needs discussion';
		// eslint-disable-next-line no-useless-escape
		this.talk_tag = '{\{subst:cdw|1=' + 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( 'prependTemplate' );
		this.addTask( 'createRequestSubpage' );
		this.addTask( 'listRequestSubpage' );
		this.addTask( 'notifyUploaders' );

		// 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 () {
			o.pageName = ( page || pageName ).replace( /_/g, ' ' );
			o.startDate = new Date();

			// set up some page names we'll need later
			var requestPage = o.pageName,
				mwString = require( 'mediawiki.String' );
			// MediaWiki has an ugly limit of 255 bytes per title, excluding the namespace
			while ( mwString.byteLength( requestPage ) + mwString.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;
			case 828: // Lua comments
				o.tag = '\n--[=[ ' + o.tag + ' ]=]\n';
			}
			o.img_summary = 'Nominating for deletion';
			// eslint-disable-next-line no-useless-escape
			o.talk_tag = '{\{subst:idw|1=' + requestPage + '}}';
			o.talk_summary = '[[:' + o.pageName + ']] has been nominated for deletion';
			o.subpage_summary = 'Starting deletion request';
			if (conf.wgIsRedirect) {
				// without \n it breaks the redirect syntax
				o.tag += '\n';
			}

			// 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( {
				prop: 'templates',
				titles: this.talkPage,
				tltemplates: 'Template:Kept',
				tllimit: 1
			}, 'formerDRTalk' );
		}
		this.queryAPI( {
			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
		} ).on( '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( this.i18n.templateRegExp );
			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 () {
		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 = {
			prop: 'revisions',
			rvprop: 'content|user',
			titles: pageName.replace( /_/g, ' ' ),
			rvlimit: 50
		};
		this.queryAPI( query, 'findTemplateAdderCB' );
	},
	findTemplateAdderCB: function ( result ) {
		var m,
			reason,
			user,
			pgRevs, // for debug
			template;
		$.each( result.query.pages, function ( id, pg ) {
			pgRevs = pg.revisions;
			$.each( pgRevs, 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 ) {
			mw.log.warn( pgRevs );
			throw new Error( this.i18n.findTemplateAdderErr );
		}
		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 () {
		this.queryAPI( {
			curtimestamp: 1,
			meta: 'tokens',
			prop: 'imageinfo|revisions|info',
			rvprop: 'content|timestamp',
			inprop: 'watched',
			iiprop: 'sha1|size|url',
			iiurlwidth: 365,
			redirects: 1,
			titles: pageName.replace( /_/g, ' ' ) + '|' + this.destination
		}, 'getDupeDetailsCB' );
		this.showProgress( 'Fetching details' );
	},

	getDupeDetailsCB: function ( result ) {
		var q = result.query,
			id,
			pg,
			ii,
			n,
			pages = q.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;
					throw new Error( ( ( $.trim( pg.title ) === '{{{1}}}' ) ?
						this.i18n.dupeParaErr :
						this.i18n.dupeExistErr.replace( '%TITLE%', pg.title ) ) + ' (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: result.curtimestamp
				};
				this.details.push( n );
				this.csrftoken = q.tokens.csrftoken;

				if ( pg.watched !== undefined ) {
					this.pageWasWatched = true;
				}

			}
		}
		if ( this.details.length < 2 ) {
			this.disableReport = true;
			throw new Error( this.i18n.noPageFound );
		}
		// 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;
		// eslint-disable-next-line no-useless-escape
		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( this.i18n.listingMobile );

		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( this.i18n.listingMobile );

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

	/**
	 ** Check the users talkpage is not blocked indefinite
	 **/
	verifyUserNotify: function ( user ) {
		this.queryAPI( {
			prop: 'info',
			titles: this.userTalkPrefix + user,
			redirects: 1,
			inprop: 'protection'
		}, 'verifyUserNotifyCB' );
	},
	verifyUserNotifyCB: function ( result ) {
		var page;
		this.notifyUser = true; // reset
		if ( !result || !result.query || !result.query.pages ) {
			mw.log.warn( 'Verify user: result.query.pages is undefined. ', result );
			return this.uploaderNotified();
		}

		result = result.query.pages;
		for ( var pg in result ) {
			pg = result[ pg ];
			page = pg.title;
			pg = pg.protection;
			if ( pg ) {
				for ( var p = 0; p < pg.length; p++ ) {
					var pt = pg[ p ];
					if ( pt && pt.type === 'edit' && pt.level === 'sysop' ) {
					// Disable report for protected userpages
						this.disableReport = true;
						// Disable notify for indefinite protected
						if ( pt.expiry === 'infinity' ) {
							this.notifyUser = false;
						}
					}
				}
			}
		}
		if ( this.notifyUser && page ) {
			page = {
				title: page,
				// eslint-disable-next-line no-useless-escape
				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' );
		} else {
			this.uploaderNotified();
		}
	},

	/**
	 ** Notify any uploaders/creators of this page using {{idw}}.
	 **/
	notifyUploaders: function () {
		this.uploadersToNotify = 0;
		if ( this.notifyUser ) {
			for ( var user in this.uploaders ) {
				if ( this.uploaders.hasOwnProperty( user ) ) {
					if ( user === conf.wgUserName ) {
						// notifying yourself is pointless
						continue;
					}
					this.verifyUserNotify( user );
					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 q = {
			curtimestamp: 1,
			meta: 'tokens',
			rvdir: 'newer',
			rvlimit: 1,
			titles: this.pageName
		};

		if ( nsNumber === 6 ) {
			$.extend( q, {
				prop: 'imageinfo|info|revisions',
				rvlimit: 1,
				rvprop: 'content|timestamp|user',
				iiprop: 'comment|sha1|user',
				iilimit: 50
			} );
		} else {
			q.prop = 'info|revisions';
			q.rvprop = 'timestamp|user';
		}
		this.showProgress( this.i18n.preparingToEdit );
		this.queryAPI( q, 'findCreatorCB' );
	},

	findCreatorCB: function ( r ) {
		this.uploaders = {};
		if ( !r || !r.query || !r.query.pages ) {
			this.disableReport = true;
			throw new Error( this.i18n.noPageFound );
		}
		var q = r.query,
			pg = _firstItem( q.pages ),
			rv;

		if ( !pg || !pg.revisions ) {
			throw new Error( this.i18n.noCreatorFound );
		}

		// The csrftoken only changes between sessions
		this.csrftoken = q.tokens.csrftoken;
		rv = pg.revisions[ 0 ];

		// First handle non-file pages
		if ( nsNumber !== 6 || !pg.imageinfo ) {
			this.pageCreator = rv.user;
			this.starttimestamp = r.curtimestamp;
			this.timestamp = rv.timestamp;

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

		} else {
			var info = pg.imageinfo,
				content = rv[ '*' ],
				seenHashes = {};

			for ( var i = info.length - 1; i >= 0; i-- ) {
				// iterate in reverse order
				var iii = info[ i ];
				if ( iii.sha1 && seenHashes[ iii.sha1 ] ) {
					// skip reverts
					continue;
				}
				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 () {
		this.showProgress( this.i18n.preparingToEdit );
		var query = {
			curtimestamp: 1,
			prop: 'info|revisions',
			meta: 'tokens',
			rvprop: 'content|timestamp',
			inprop: 'watched',
			titles: this.pageName || pageName.replace( /_/g, ' ' )
		};
		if ( !this.declineReason ) {
			query.prop += '|imageinfo';
			query.iiprop = 'mediatype|mime|timestamp';
		}
		this.queryAPI( query, 'getMoveTokenCB' );
	},

	/**
	 * @brief [callback] Prepare page for saving before
	 * @param [in] result of query
	 * @return csrftoken, pageContent, starttimestamp, timestamp, imagetimestamp, mimeFileExtension, pageWasWatched
	 **/
	getMoveTokenCB: function ( result ) {
		var q = result.query || {},
			pg = _firstItem( q.pages );
		if ( !pg || !pg.revisions ) {
			this.disableReport = true;
			throw new Error( this.i18n.noPageFound );
		}
		// The csrftoken only changes between sessions
		$.extend( this, {
			csrftoken: q.tokens.csrftoken,
			pageContent: pg.revisions[ 0 ][ '*' ],
			starttimestamp: result.curtimestamp,
			timestamp: pg.revisions[ 0 ].timestamp
		} );

		if ( pg.watched !== undefined ) {
			this.pageWasWatched = true;
		}

		var ii = pg.imageinfo;
		if ( ii && ii.length && ii[ 0 ].mime ) {
			ii = ii[ 0 ];
			this.imagetimestamp = ii.timestamp;
			this.mimeFileExtension = ii.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 ( ii.mediatype ) {
				case 'AUDIO':
					this.mimeFileExtension = 'oga';
					break;
				case 'VIDEO':
					this.mimeFileExtension = 'ogv';
					break;
				}
			}
		}
		this.nextTask();
	},

	doesFileExist: function () {
		if ( !this.destination ) {
			// eslint-disable-next-line no-alert
			return alert( this.i18n.moveDestination );
		}
		this.destination = this.cleanFileName( this.destination );
		var 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 nextTask 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 filename: result.query.pages is undefined. ' + this.destination );
		}
		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 || 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 () {
		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.
		var newText = this.pageContent.replace( ( this.templateRegExp || /(?:([^=])\n)?\{\{(?:rename|rename media|move)\s*\|.*?\}\}(?:\n([^=]))?/i ), this.replaceWith );
		if ( newText === this.pageContent ) {
			return this.nextTask();
		}
		this.showProgress( this.i18n.removingTemplate );
		// If nothing remains, add the no-license-template (this also to prevent abuse filter blocking this edit because of page blanking)
		this.savePage( {
			title: ( this.destination || pageName ),
			text: $.trim( newText ) || '{{subst:nld}}',
			editType: 'text',
			starttimestamp: this.starttimestamp,
			timestamp: this.timestamp,
			watchlist: 'nochange'
		}, ( this.declineReason || 'Removing template; rename done' ), 'nextTask' );
	},

	replaceUsage: function () {
		if ( !this.inUse ) {
			return this.nextTask();
		}
		this.showProgress( this.i18n.replacingUsage );
		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 );
						mw.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 () {
		mw.loader.using( 'mediawiki.api' ).then( function () {
			return new mw.Api().loadMessagesIfMissing( [
				'Whatlinkshere'
			] );
		} ).then( function () {
			AQD.showProgress( mw.msg( 'Whatlinkshere' ) );
			// TODO: Replace also the redirects inclusions!?
			AQD.queryAPI( {
				generator: 'backlinks',
				gblfilterredir: 'redirects',
				prop: 'revisions',
				rvprop: 'content',
				gbltitle: AQD.pageName || pageName.replace( /_/g, ' ' )
			}, AQD.queryRedirectsCB ?
				AQD.queryRedirectsCB :
				'updateRedirects' );
		} );
	},

	updateRedirects: function ( result ) {
		AQD.redirectsToUpdate = 0;
		if ( result.query && result.query.pages ) {
			this.showProgress( this.i18n.updRedir );
			$.each( result.query.pages, function ( id, pg ) {
				var rv = pg.revisions[ 0 ];
				if ( !rv || !rv[ '*' ] ) {
					return;
				}
				// Update only redirects with same mimetype
				if ( AQD.checkFileExt( pg.title, AQD.destination, true ) ) {
					return mw.log( 'Redirect skipped, not same mimetype.', pg.title );
				}

				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 = document.getElementById( 'AjaxQuestion' + i );
				response = ( v.type === 'checkbox' ) ? response.checked : response.value;
				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 )
						.replace( '%PARAMETER-LINKED%', '[[:' + response + ']]' );
				}
			} );
			$( this ).dialog( 'close' );
			AQD.nextTask();
		};
		dlgButtons[ this.i18n.cancelButtonLabel ] = function () {
			$( this ).dialog( 'close' );
		};

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

		var _parseReason = function () {
			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 ) {
				$submitButton.button( 'option', 'disabled', $.trim( $el.val() ).length < ( v.minLength || 10 ) );
			}

			if (
				( $el.prop( 'nodeName' ) !== 'TEXTAREA' ) &&
				( event.which === 13 ) &&
				( v.enterToSubmit !== false ) &&
				!$submitButton.button( 'option', 'disabled' )
			) {
				$submitButton.click();
			}
		};

		var _convertToTextarea = function () {
			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();
		};

		$.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.prop( 'checked', v.prefill ).attr( 'style', 'margin-left: 5px' );
			}
		} );

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

		$( '<div>' ).append( $AjaxDeleteContainer ).dialog( {
			width: ( width || 600 ),
			modal: true,
			title: title,
			dialogClass: 'wikiEditor-toolbar-dialog',
			close: function () {
				$( this ).dialog( 'destroy' ).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' );
				$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 ).on( 'click', _convertToTextarea );
			}
		} );

		$( '#AjaxQuestion0' ).focus().select();
		mw.hook( 'aqd.prompt' ).fire( o );
	},

	/**
	 ** 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 ],
			$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();
			}, 20 );
		};
		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 );
		$( '<div>' ).append( $AjaxDupeContainer ).dialog( {
			width: 800,
			modal: true,
			title: this.i18n.compareDetails,
			draggable: false,
			dialogClass: 'wikiEditor-toolbar-dialog',
			close: function () {
				$( this ).dialog( 'destroy' ).remove();
			},
			buttons: dlgButtons,
			open: function () {
				var $buttons = $( this ).parent().find( '.ui-dialog-buttonpane button' );
				$buttons.eq( 0 ).specialButton( 'proceed' );
				$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', ( ( $swapButton.css( 'float' ) === 'left' ) ? 'right' : 'left' ) );
				$overlayButton.css( 'float', ( ( $overlayButton.css( 'float' ) === 'left' ) ? '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 filenames (with extension)
	 ** Either a filename 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 ( fn, ignoreMIME ) {
		// Remove Namespace
		fn = fn.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
			// eslint-disable-next-line no-control-regex
			.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, '(' );

		fn = this.checkFileExt( pageName, fn, ignoreMIME ) || fn;

		// Capitalize the first letter and prefix the namespace
		return 'File:' + $.ucFirst( fn ); // own prototype
	},

	/**
	 *  @brief Compare mimetype
	 *  @param [in] of old filename
	 *  @param [in] fn new filename
	 *  @param [in] boolean
	 *  @return false if same, otherwise new file with old extension
	 */
	checkFileExt: function ( of, fn, ignoreMIME ) {
		var currentExt = ( !ignoreMIME && this.mimeFileExtension ) ?
			this.mimeFileExtension :
			of.replace( /.*?\.(\w{2,5})$/, '$1' ).toLowerCase().replace( 'jpeg', 'jpg' );

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

		// If new filename is without (same) extension, add the one from the old name
		if ( !reCurrentExt.test( fn ) ) {
			// First, try to replace the old extension
			fn = fn.replace( reDestExt, '.' + currentExt );
			if ( !reCurrentExt.test( fn ) ) {
			// If this did not work, then simply append the old extension
				fn += '.' + currentExt;
			}
		} else {
			fn = false;
		} // is equal

		return fn;
	},

	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' ).remove();
				}
			} );
		}

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

		$.extend( true, page, {
			cb: function ( r ) {
				AQD.secureCall( callback, r );
			},
			// text, result, query
			errCb: function ( t, r ) {
				if ( AQD.uploadersToNotify ) {
				// If user notify fails don't break next task (e.q. redirect to protected page, very rare)
					AQD.secureCall( callback, r );
				}
				AQD.fail( t, r );
			},
			summary: summary
		} );

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

	movePage: function () {
		mw.user.tokens.set( 'csrfToken', AQD.csrftoken );
		// 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();
				},
				// text, r-result, query
				errCb: function ( t, r ) {
					AQD.fail( t, r );
				},
				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( 'csrfToken', AQD.csrftoken );
		mw.loader.using( [ 'ext.gadget.libAPI' ], function () {
			AQD.showProgress( AQD.i18n.deletingFile );
			mw.libs.commons.api.deletePage( {
				cb: function () {
					AQD.nextTask();
				},
				// text, result, query
				errCb: function ( t, r ) {
					AQD.fail( t, r );
				},
				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).
	 **/
	queryAPI: function ( params, callback ) {
		mw.loader.using( [ 'ext.gadget.libAPI' ], function () {
			params.action = params.action || 'query';
			mw.libs.commons.api.query( params, {
				method: 'GET',
				cache: false,
				cb: function ( r ) {
					AQD.secureCall( callback, r );
				},
				// text, result, query
				errCb: function ( t, r ) {
					AQD.fail( t, r );
				}
			} );
		} );
	},

	/**
	 ** Method to catch errors and report where they occurred
	 **/
	secureCall: function ( fn, r ) {
		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 ( typeof fn === 'string' ) {
				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, r );
		}
	},

	/**
	 ** 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 , r ) {
		var o = this;
		if ( typeof err === 'object' ) {
			var stErr = err.message + ' \n\n ' + err.name;
			if ( err.lineNumber ) {
				stErr += ' @line' + err.lineNumber;
			}
			err = stErr;
		}
		if ( typeof r === 'object' ) {
			err += '\n' + JSON.stringify( r );
		}

		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 ( $.inArray( o.currentTask, [ 'movePage', 'deletePage', 'notifyUploaders' ] ) !== -1 && ( /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 +
				'\nAQD version: ' + o.version +
				'\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 [[User:{{subst:REVISIONUSER}}|]] ~~~~~\n';
				$.post( o.apiURL, {
					action: 'edit',
					format: 'json',
					title: 'MediaWiki talk:Gadget-AjaxQuickDelete.js/auto-errors',
					summary: '/*Autoreport by AjaxQuickDelete ' + randomId + '*/ error with random id',
					appendtext: toSend,
					token: ( o.csrftoken || 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.replace( /%BUTTON%/, '<tt>' + this.i18n.reportButtonLabel + '</tt>' ) + '</a>' )
		} ) ).dialog( {
			width: 550,
			modal: true,
			closeOnEscape: false,
			title: this.i18n.errorDlgTitle,
			dialogClass: 'ajaxDeleteError',
			buttons: dlgButtons,
			close: function () {
				$( this ).dialog( 'destroy' ).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() ) );
	},

	// 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: 'Nominate for deletion',
		toolboxLinkDiscuss: 'Nominate category for discussion',

		// GUI reason prompt form
		reasonForDeletion: 'Why should this file be deleted?',
		reasonForDiscussion: 'Why does this category need discussion?',
		moreInformation: 'More information',
		loading: 'Loading…',

		keptAfterDR: 'This page was kept after a deletion request. Please contact the administrator who kept it before re-nominating.',
		hasTalkpage: 'There is a talk page. Consider reading it or adding your remarks.',
		mentionedInDR: 'Consider reading the deletion debate –%PAGE%– that links to this page.',
		mentionedInForum: 'On %PAGE%, this page is part of a discussion.',

		// Labels
		previewLabel: 'Preview:',
		submitButtonLabel: 'Proceed',
		cancelButtonLabel: 'Cancel',
		abortButtonLabel: 'Abort',
		reportButtonLabel: 'Report automatically',
		retryButtonLabel: 'Retry',
		ignoreButtonLabel: 'Ignore and continue',
		inverseButtonLabel: 'Inverse. Keep this delete other',
		swapImagesButtonLabel: 'Swap to compare',
		overlayButtonLabel: 'Overlay to compare',
		expandToTextarea: 'Expand to textarea',
		notifyUser: 'Notify users',

		// GUI progress messages
		preparingToEdit: 'Preparing to edit pages… ',
		creatingNomination: 'Creating nomination page… ',
		listingNomination: 'Adding nomination page to daily list… ',
		addingAnyTemplate: 'Adding template to ' + conf.wgCanonicalNamespace.toLowerCase() + ' page… ',
		notifyingUploader: 'Notifying %USER%… ',
		listingMobile: 'Listing mobile upload',
		updRedir: 'Updating redirects',

		// Extended version
		toolboxLinkSource: 'No source',
		toolboxLinkLicense: 'No license',
		toolboxLinkPermission: 'No permission',
		toolboxLinkCopyvio: 'Report copyright violation',
		reasonForCopyvio: 'Why is this file a copyright violation?',

		// For moving files
		notAllowed: 'You do not have the neccessary rights to move files',
		reasonForMove: 'Why do you want to move this file?',
		moveDestination: 'What should be the new filename?',
		moveOtherDestination: 'The name you have specified exists. Choose a new name, please.',
		checkFileExists: 'Checking whether file exists',
		movingFile: 'Moving file',
		replacingUsage: 'Ordering CommonsDelinker to replace all usage',
		dropdownMove: 'Move & Replace',
		leaveRedirect: 'Leave a redirect behind:',
		moveAndReplace: 'Move file and replace all usage',
		warnRename: 'File renaming was recently declined, be prudent!',

		// For declining any request
		removingTemplate: 'Removing template',
		declineRequest: 'Why do you want to decline the request?',
		anyDecline: 'Decline request',

		// For Duplicates
		useCORSForReplace: 'Try to replace usage immediately using your user account:',
		deletingFile: 'Deleting file',
		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',
		processDupes: 'Process Duplicates',

		// Errors
		errorDlgTitle: 'Error',
		genericFailure: 'An error occurred while trying to do the requested action. ',
		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 %BUTTON% to send an automatic error-report.',
		tagWas: 'The tag to be inserted into this page was ',

		// Minor errors/warnings
		templateRegExp: 'The template does not expose a valid regular expression for {{X-To-DR}}. Go the the template and fix it there.',
		findTemplateAdderErr: '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.',
		dupeParaErr: 'Error in the duplicate-template, check your language version!',
		dupeExistErr: 'Retrieving information about %TITLE% failed. It is possible that it is deleted, the last revision is corrupt or the file is a redirect.',
		noCreatorFound: 'The page you are attempting to add a tag to was deleted or moved. Unable to retrieve the content.',
		noPageFound: 'The page you are attempting to modify or move is corrupted, was deleted or moved: Unable to retrieve history and contents.'
	}
};

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>