MediaWiki:Gadget-AjaxQuickDelete.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.
/**
 * AjaxQuickDelete from <https://commons.wikimedia.org/wiki/MediaWiki:Gadget-AjaxQuickDelete.js>
 *
 * Changelog:
 * - 2010: Created in 2010 by [[User:Ilmari Karonen]] as Ajax-based replacement for [[MediaWiki:Quick-delete-code.js]].
 * - Rewritten by [[User:DieBuche]].
 * - Botdetection and encoding fixer by [[User:Lupo]].
 * - 2011-2012: Validation and further development [[User:Rillke]], 2011-2012.
 * - 2018: dropped IE8 support.
 *
 * TODO:
 * - Fix problems with moves of videos.
 * - Support move of timed text for videos.
 *
 * This pages has automated validation on save. Interested? See [[:commons:Commons:User scripts/reports]].
 */
// <nowiki>

/* eslint indent:[error,tab,{outerIIFEBody:0}] */
/* eslint-disable one-var, vars-on-top, camelcase, no-underscore-dangle, valid-jsdoc */
/* global $:false, mw:false, importScript:false */

( function () {
'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'
	] ),
	nsNr = conf.wgNamespaceNumber,
	pageName = conf.wgPageName;

// A bunch of helper functions
function _firstItem( o ) {
	for ( var i in o ) {
		if ( Object.prototype.hasOwnProperty.call( o, 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.1.3',
	/**
	* 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="mw-message-box mw-message-box-warning">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>'
			);
		}

		this.doNothing = ( !conf.wgArticleId || nsNr < 0 || /^Commons:Deletion/.test( pageName ) || /^Commons:Categories/.test( pageName ) );

		if ( this.doNothing ) {
			return;
		}

		// Check user group
		if ( conf.wgUserGroups.indexOf( 'sysop' ) !== -1 ) {
			this.userRights = 'sysop';
		} else if ( conf.wgUserGroups.indexOf( 'filemover' ) !== -1 ) {
			this.userRights = 'filemover';
		} else {
			this.userRights = [ 'autopatrolled', 'patroller', 'image-reviewer' ].filter( function ( g ) {
				return conf.wgUserGroups.indexOf( g ) !== -1;
			} )[ 0 ];
		}

		if ( [ 'filemover', 'sysop' ].indexOf( this.userRights ) !== -1 && nsNr === 6 ) {
			// Change "Move" to "Move & Replace"
			var $moveLink = $( '#ca-move' ),
				$moveLanchor = $moveLink.find( 'a' );
			this.$moveLink = $moveLink = $moveLanchor.length ? $moveLanchor : $moveLink;

			$moveLink.text( $moveLink.text() + ' & Replace' )
				.attr( 'title', 'Click in order to ' + $moveLink.attr( 'title' ) + ' and replace usage. Default form though new tab.' )
				.on( 'click', function ( e ) {
					e.preventDefault();
					AQD.moveFile();
				} );
		}
	},
	/**
	* 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 ( this.doNothing ) {
			return;
		}

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

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

		var link;
		// Set up toolbox link
		if ( nsNr === 14 ) {
			// In categories the discuss-category link
			link = mw.util.addPortletLink( 'p-tb', '#', this.i18n.toolboxLinkDiscuss, 't-ajaxquickdiscusscat' );
			if ( link ) {
				link.addEventListener( 'click', function ( e ) {
					e.preventDefault();
					mw.loader.using( 'jquery.ui' ).then( function () {
						AQD.discussCategory();
					} );
				} );
			}
		} else {
			// On other pages, the nominate-for-deletion link
			link = mw.util.addPortletLink( 'p-tb', '#', this.i18n.toolboxLinkDelete, 't-ajaxquickdelete' );
			if ( link ) {
				link.addEventListener( 'click', function ( e ) {
					e.preventDefault();
					AQD.nominateForDeletion();
				} );
			}
		}

		// Install AjaxMoveButton for filemovers and administrators
		if ( this.$moveLink ) {
			// Change Move & Replace link to fully localized text
			this.$moveLink.html( $( '<span>' ).text( this.i18n.dropdownMove ) );
			// Add quicklinks to template
			$( '#AjaxRenameLink' ).append( '<a href="javascript:AjaxQuickDelete.moveFile();">' + this.i18n.moveAndReplace + '</a>' )
				.append( '<a href="javascript:AjaxQuickDelete.loadAndDeclineRequest(\'move\');" class="ajaxDeleteDeclineMove"><sup> ' + this.i18n.anyDecline + '</sup></a>' );
			// Install x-To-DR. See [[Template:X-To-DR]]
			$( '#mw-imagepage-content .convert-to-dr' )
				.find( '.ctdr-btn-convert' ).on( 'click', this._convertToDR ).show().end()
			// Currently filemover rights required
				.find( '.ctdr-btn-remove' ).on( 'click', this._removeAnyTag ).show();
		} else if ( this.userRights ) {
			$( '#mw-imagepage-content .convert-to-dr .ctdr-btn-convert' ).on( 'click', this._convertToDR ).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 ( this.userRights === 'sysop' && nsNr === 6 ) {
			var dupeSection = $( '#AjaxDupeProcess' );
			if ( dupeSection.length ) {
				dupeSection.append( $( '<a>', {
					href: '#',
					text: this.i18n.processDupes,
					style: 'font-weight:bold',
					click: function ( e ) {
						e.preventDefault();
						AQD.processDupes();
					}
				} ) ).show();
			} else {
				dupeSection = $( '#mw-imagepage-section-duplicates .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: '[' + this.i18n.processDupes + ']',
							style: 'background:#CEB',
							click: function ( e ) {
								e.preventDefault();
								AQD.processDupes();
							}
						} ) ) );
				}
			}
		}
		// Extra buttons
		// Wait until the user's js was loaded and executed
		mw.loader.using( 'user', function () {
			if ( mw.user.options.get( 'gadget-QuickDelete' ) ) {
				mw.loader.using( 'ext.gadget.QuickDelete' ).always( 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;
		this.watchlist = 'preferences';
	},

	/**
	* 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 () {
		var o = this;

		o.initialize();

		mw.loader.using( [ 'jquery.ui' ] ).then( function () {
			o.showProgress();

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

			// Let's be sure we have a fresh token and the latest MIME-Info
			o.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
			) ) {
				o.inUse = true;
				o.addTask( 'chkPreMoveDecline' );
			}

			o.addTask( 'promptForMoveTarget' );

			o.addTask( 'doesFileExist' );
			o.fileNameExistsCB = 'fileExists';
			o.addTask( 'movePage' );
			o.addTask( 'removeTemplate' );
			o.addTask( 'queryRedirects' );
			o.addTask( 'replaceUsage' );

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

			o.nextTask();
		} );
	},

	promptForMoveTargetCB: function ( AQD ) {
		if ( AQD.inUse ) {
			$( '#AjaxQuestion2' ).prop( 'disabled', true );
		}
	},

	promptForMoveTarget: function () {
		var toAppend;
		this.showProgress();

		if ( mw.user.options.get( 'gadget-RenameLink' ) ) {
			toAppend = [ $( '<a>' )
				.text( this.i18n.moreInformation )
				.on( 'click', function () {
					if ( !AQD.rGetPolicy ) {
						importScript( 'MediaWiki:RenameRequest.js' );
						mw.hook( 'aqd.renamerequest.i18n' ).fire();
					}
					mw.hook( 'aqd.renamerequest.run' ).fire( { exec: this } );
				} ).button( {
					icons: { primary: 'ui-icon-script' },
					showLabel: true,
					text: false
				} ).css( {
					fontSize: '.6em',
					margin: '0',
					width: '2.5em',
					'float': 'right'
				} ), '<br>' ];
		}
		mw.hook( 'aqd.prompt' ).remove( this.promptForMoveTargetCB ).add( this.promptForMoveTargetCB );

		this.prompt( [ {
			message: this.i18n.moveDestination,
			prefill: this.cleanFileName( this.possibleDestination || pageName ),
			returnvalue: 'destination',
			appendNode: toAppend,
			cleanUp: true,
			noEmpty: true
		}, {
			message: this.i18n.reasonForMove,
			prefill: ( ( this.reason || this.possibleReason || '' ).trim().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 loading jquery UI to decline a request
	*/
	loadAndDeclineRequest: function ( reason ) {
		var that = this;
		mw.loader.using('jquery.ui').then(function () {
			that.declineRequest.call( that, reason );
		} );
	},

	/**
	* For declining a request
	*/
	declineRequest: function ( reason ) {
		reason = reason || this.declineReason;
		// 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]]';
			if ( window.AjaxDeclineMoveWatchFile ) {
				this.watchlist = 'watch';
			}
			break;
		}

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

	},

	// This method is generlaly used directly by user scripts without going
	// through other methods or setup functions. Needs to be standalone,
	// and ensures loading of its own dependencies.
	insertTagOnPage: function ( tag, img_summary, talk_tag, talk_summary, prompt_text, page, optin_notify ) {
		var o = this;
		this.initialize();

		mw.loader.using( [ 'jquery.ui' ], function () {
			o.pageName = ( page || pageName ).replace( /_/g, ' ' );
			o.tag = tag.replace( '%USER%', conf.wgUserName ) + '\n';
			o.img_summary = img_summary;

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

			if ( o.isMobile() && /(?:copyvio|nsd|npd|nld)/.test( tag ) ) {
				o.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' ) {
				o.talk_tag = talk_tag.replace( '%FILE%', o.pageName );
				o.talk_summary = talk_summary.replace( '%FILE%', '[[:' + o.pageName + ']]' );

				o.usersNeeded = true;

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

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

				o.prompt( prompt, prompt_text || o.i18n.reasonForDeletion );
			} else if ( optin_notify && prompt.length && o.talk_summary ) {
				o.prompt( prompt, o.talk_summary );
			} else {
				o.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}}\n';
		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 = requestPage.slice( 0, requestPage.length - 1 ).trim();
			}

			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 ( nsNr ) {
			// 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';
			}

			if ( o.templateReplace ) {
				o.declineReason = o.img_summary;
			}
			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';
			// without \n it breaks the redirect syntax
			if ( conf.wgIsRedirect ) {
				o.tag += '\n';
			}

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

			// …then schedule the actual edits
			o.addTask( o.templateReplace ? 'replaceTemplate' : '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 ( nsNr % 2 === 0 ) {
			this.talkPage = conf.wgFormattedNamespaces[ nsNr + 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 ( Array.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,
			_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' ).attr( 'href', function ( i, v ) {
				return v.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' ),
				$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>', {
			// eslint-disable-next-line quote-props
			'class': 'ajaxDeleteLazyLoad',
			style: 'display:none;'
		} ).append( $( '<span>', {
			// eslint-disable-next-line quote-props
			'class': 'ajax-quick-delete-loading',
			text: this.i18n.loading
		} ).data( 'aqdPage', page ) ) );
	},
	extractFromHTML: function ( $el ) {
		$el = $( $el ).parent();

		// …extract the regular expression from html
		this.templateRegExp = $el.find( '.ctdr-regex' ).text();

		var m = this.templateRegExp.match( /^\/(.+)\/(i)?$/ );
		if ( !m || !m[ 1 ] ) {
			m = new Error( this.i18n.templateRegExp );
			this.fail( m );
			throw m;
		}
		this.templateRegExp = new RegExp( m[ 1 ], m[ 2 ] );

		// …and the decline reason
		this.declineReason = $el.find( '.ctdr-template-decline-reason' ).text();

		// …and the template name itself
		m = $el.find( '.ctdr-template-name' ).text();

		this.reason = 'This file was initially tagged by %USER%' + ( m ? ( ' as \'\'\'' + m + '\'\'\'' ) : '' );

	},
	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( e.currentTarget || 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 ) {
		e = e.currentTarget || this;
		AQD.extractFromHTML( e );
		AQD.convertToDR( e );
		return false;
	},
	convertToDR: function ( el ) {
		// reset task list in case an earlier error left it non-empty
		this.initialize();
		this.declineReason = 'This file does not qualify for [[COM:SPEEDY|speedy-deletion]] and a regular deletion request will be started.';
		this.templateReplace = true;
		// first schedule a API query to fetch the info we need…
		this.addTask( 'findTemplateAdder' );
		this.addTask( 'getMoveToken' );
		// prompt before conversion for user decision
		this.addTask( 'removeTemplate' );
		this.addTask( 'removeProgress' );
		this.addTask( 'nominateForDeletion' );
		// Hide the buttons to prevent attempts of duplicate removal
		$( el ).closest( '.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 reason,
			user,
			pgRevs, // for debug
			template;
		$.each( result.query.pages, function ( id, pg ) {
			pgRevs = pg.revisions;
			var lastM = null;
			for (var i = pgRevs.length - 1; i >= 0; i--) {
				var m = pgRevs[ i ][ '*' ].match( AQD.templateRegExp );
				if ( m ) {
					if ( !lastM ) { // require tag to be an addition not there before
						user = pgRevs[ i ].user;
						if ( m.length > 1 && !template ) {
							template = m[ 1 ];
						}
						if ( m.length > 2 && !reason ) {
							reason = m[ 2 ];
						}
					}
					lastM = m;
				} else {
					lastM = null;
				}
			}
		} );
		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: <span style="font-family: monospace">' + reason + '</span>';
		}

		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 ) {
		this.details = [];
		if ( result ) {
			var q = result.query,
				id,
				pg,
				ii,
				n,
				pages = q.pages;

			for ( id in pages ) {
				if ( Object.prototype.hasOwnProperty.call( pages, id ) ) {
					pg = pages[ id ];
					if ( !pg.imageinfo ) {
						// Nothing we can change so prevent users reporting
						this.disableReport = true;
						throw new Error( ( ( pg.title.trim() === '{{{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',
			watchlist: window.AjaxDeleteWatchFile ? 'watch' : this.watchlist,
			minor: false
		};

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

	/**
	* Edit the current page to add the specified tag and the changed content.
	*/
	replaceTemplate: function ( text ) {
		var page = {
			title: this.destination || pageName,
			text: text || this.tag + this.pageContent,
			editType: 'text',
			starttimestamp: this.starttimestamp,
			timestamp: this.timestamp,
			watchlist: window.AjaxDeleteWatchFile ? 'watch' : this.watchlist,
			minor: false
		};

		this.templateReplace = false;
		this.showProgress( this.i18n.addingAnyTemplate );
		this.savePage( page, ( this.declineReason || 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 = {
			title: this.requestPage,
			// eslint-disable-next-line no-useless-escape
			text: '\n=== [[:' + this.pageName + ']] ===\n' + this.reason + ' ~~~~\n',
			watchlist: 'watch',
			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 = 'preferences';
		page.editType = 'appendtext';

		this.showProgress( this.i18n.listingNomination );

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

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

		// On mobile categories are not defined. Exit early to avoid .length error.
		if (!cats) {
			return true;
		}
		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: 'preferences',
			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: 'preferences',
			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 ( Object.prototype.hasOwnProperty.call( this.uploaders, 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
	* DONE: notify fileimporter (2019-09-09)
	*/
	findCreator: function () {
		var q = {
			curtimestamp: 1,
			meta: 'tokens',
			prop: 'revisions',
			titles: this.pageName,
			rvprop: 'timestamp|user',
			rvdir: 'newer',
			rvslots: 'main',
			rvlimit: 1
		};

		if ( nsNr === 6 ) {
			$.extend( q, {
				prop: q.prop + '|imageinfo',
				rvprop: q.rvprop + '|content',
				iiprop: 'user|comment|sha1',
				iilimit: '50',
				list: 'logevents',
				letype: 'import',
				leprop: 'user',
				letitle: this.pageName
			} );
		}

		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 ( nsNr !== 6 || !pg.imageinfo ) {
			this.starttimestamp = r.curtimestamp;
			this.timestamp = rv.timestamp;

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

		} else {
			var info = pg.imageinfo,
				i = info.length,
				content = rv.slots.main[ '*' ],
				seenHashes = {};
			// Iterate in reverse order (sha1 check)
			for ( --i; i >= 0; i-- ) {
				var iii = info[ i ],
					rev = seenHashes[ iii.sha1 ];
				// Skip reverts and remove users
				if ( iii.sha1 && rev ) {
					for ( rev; rev > i; rev-- ) {
						var u = info[ rev - 1 ].user;
						if ( this.uploaders[ u ] < rev ) {
							delete this.uploaders[ u ];
						}

					}
					continue;
				}
				seenHashes[ iii.sha1 ] = i + 1;
				// No need to check again the whole shebang
				if ( this.uploaders[ iii.user ] ) {
					continue;
				}

				// Now exclude bots which only reupload a new version:
				if ( mw.libs.commons.isSmallChangesBot( iii.user ) ) {
					continue;
				}

				// Outsourced to MediaWiki:Gadget-libCommons.js
				iii = mw.libs.commons.getUploadBotUser( iii.user, content, iii.comment, rv.user );
				if ( iii ) {
					this.uploaders[ iii ] = i + 1;
				}

			}
			// Get fileimporter
			if ( ( info = q.logevents ) && ( i = info.length ) ) {
				for ( --i; i >= 0; i-- ) {
					if ( info[ i ].user ) {
						this.uploaders[ info[ i ].user ] = 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.util.escapeRegExp( title[ 0 ].toUpperCase() ) + mw.util.escapeRegExp( title[ 0 ].toLowerCase() ) + ']' +
			mw.util.escapeRegExp( 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|rename image|move)\s*\|.*?\}\}(?:\n([^=]))?/i ),
				this.replaceWith
			);
		if ( newText === this.pageContent ) {
			return this.nextTask();
		}

		this.showProgress( this.i18n.removingTemplate );
		newText = this.pageContent = newText.trim();

		// We have another save request
		if ( this.templateReplace ) {
			this.img_summary = this.declineReason;
			return this.nextTask();
		}
		if ( !this.declineReason ) {
			this.img_summary = 'Removing template; rename done';
		}

		// If nothing remains, add the no-license-template (this also to prevent abuse filter blocking this edit because of page blanking)
		this.replaceTemplate( newText || '{{subst:nld}}' );
	},

	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' : 'preferences'
		};

		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' : 'preferences'
		};

		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: 'preferences'
				};

				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,
			dlgButtons = {};
		dlgButtons[ this.i18n.submitButtonLabel ] = function () {
			questions.forEach( function ( v, i ) {
				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,
			$AjaxDeleteContainer = $( '<div>', { id: 'AjaxDeleteContainer' } ),
			_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 );
				} );
			},

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

				if ( v.noEmpty ) {
					$submitButton.button( 'option', 'disabled', $el.val().trim().length < ( v.minLength || 8 ) );
				}

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

			},

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

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

		questions.forEach( function ( v, i ) {
			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>' );
			}

			$AjaxDeleteContainer.append( v.appendNode ? v.appendNode : '<br>' );

			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();
				if ( AQD.currentTask === 'formerDRRequestpage' ) {
					$('.convert-to-dr' ).show();
				}
			},
			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' );
			}
		} );

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

		$( '#AjaxQuestion0' ).trigger( 'focus' ).trigger( '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'
				} ) ) ),
			$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'
				} ) ) ),
			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, left: pos.left - 1
				} )
					.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)',
						width: 'auto',
						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' ),

			reCurrentExt = new RegExp( '\\.' + mw.util.escapeRegExp( currentExt ) + '$', 'i' ),
			reDestExt = new RegExp( '\\.' + mw.util.escapeRegExp( 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 ) {
		return uncleanReason
			.trim() // whitespace
			.replace( /(?:--|–|—)? ?~{3,5} ?/, '' ) // remove signature
			.replace( /\|\s/g, '&#124; ' ); // pipes
	},

	/**
	* 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 ) {
					if ( r && r.error && /articleexists/.test( r.error.code ) ) {
						AQD.disableReport = true;
					}

					AQD.fail( t, r );
				},
				from: pageName,
				to: AQD.destination,
				reason: AQD.reason,
				movetalk: true,
				watchlist: AQD.pageWasWatched ? 'watch' : 'preferences'
			};
				// Option to not leave a redirect behind, MediaWiki default does leave one behind
				// Just like movetalk, an empty parameter sets it to true
			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 ( typeof fn === 'function' ) {
				// arguments is not of type array so we can't take .slice
				return fn.apply( o, Array.prototype.slice.call( arguments, 1 ) );
			} 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,
			msg,
			dlgButtons = {};

		if ( typeof err === 'object' ) {
			msg = err.message + ' \n\n ' + err.name;
			if ( err.lineNumber ) {
				msg += ' @line' + err.lineNumber;
			}

			err = msg;
		}
		// Mostly the same as err
		if ( typeof r === 'object' ) {
			if ( r.error && /tpt-target-page|readonly/.test( r.error.code ) ) {
				this.disableReport = true;
			}
		}

		// err += '\n' + JSON.stringify( r );

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

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

		dlgButtons[ this.i18n.retryButtonLabel ] = function () {
			$( this ).remove();
			o.retryTask();
		};

		if ( [ 'movePage', 'deletePage', 'notifyUploaders' ].indexOf( o.currentTask ) !== -1 &&
			( /code 50\d|missingtitle/.test( err ) ) ) {
			dlgButtons[ this.i18n.ignoreButtonLabel ] = function () {
				$( this ).remove();
				o.nextTask();
			};
		}

		if ( !this.disableReport ) {
			dlgButtons[ this.i18n.reportButtonLabel ] = function () {
				var randomId = Math.round( Math.random() * 1099511627776 ),
					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\n';

				$( '#feedbackContainer' ).contents().remove().end()
					.append( $( '<img>', { src: '//upload.wikimedia.org/wikipedia/commons/d/de/Ajax-loader.gif' } ) ).css( 'text-align', 'center' );

				$.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 + '<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() || 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: '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 user 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 ' + ( nsNr === 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 ' + ( nsNr === 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' ) {
	$.when( mw.loader.getScript( mw.util.wikiScript() +
		'?title=MediaWiki:Gadget-AjaxQuickDelete.js/' +
		conf.wgUserLanguage + '.js&action=raw&ctype=text/javascript'
	), $.ready ).always( function () { AQD.install(); } );
} else {
	$( function () {
		AQD.install();
	} );
}

}() );
// </nowiki>