MediaWiki:Gadget-AjaxQuickDelete.js

/** * AjaxQuickDelete from  * * 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. */ //

/* 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(				' Consider using Move & Replace from the menu on file pages (open with a single click) when moving files to care for global usage and redirects. '			); }

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

if ( this.doNothing ) { return; }

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

if ( [ 'translationadmin', '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( $( ' ' ).text( this.i18n.dropdownMove ) ); // Add quicklinks to template $( '#AjaxRenameLink' ).append( '' + this.i18n.moveAndReplace + '' ) .append( '' + this.i18n.anyDecline + '' ); // 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( $( '', { 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( $( ' ', { style: 'display:none', id: 'AjaxDupeDestination', text: dupeSection.find( 'a' ).attr( 'title' ) } ) )						.append( ' ', $( ' ' ).append( $( '', {							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 = [ $( '' ) .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'				} ), ' ' ]; }		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 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 = '\n'; this.img_summary = 'This category needs discussion'; // eslint-disable-next-line no-useless-escape this.talk_tag =  + 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 = '\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 s.			case 10: case 100: case 106: o.tag = ' ' + o.tag + ' '; 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 =  + 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 = $( '' ).attr( 'id', 'AjaxDeletePrevRequests' ); o.secureCall( 'checkForFormerDR' ); var toAppend = $( ' ' ).append( $( ' ' ).attr( 'class', 'ajaxDeleteLazyLoad' ).css( { 'max-height': Math.max( Math.round( $( window ).height / 2 ) - 250, 100 ), 'min-height': 0, overflow: 'auto' } ).append( o.prevDRNode ), ' ', 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 ) ) {				$( '' ).append( $( '', {					text: AQD.i18n.keptAfterDR,					href: mw.util.getUrl( AQD.talkPage )				} ) ).prependTo( AQD.prevDRNode );			} else if ( pg.missing === undefined ) {				$( '' ).append( $( '', {					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 ) { $( '' ).append( $( '', { 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 $( ' ', { style: 'min-height:40px;' } ).append( $( '', { 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' ); } ), $( ' ', {			// eslint-disable-next-line quote-props 'class': 'ajaxDeleteLazyLoad', style: 'display:none;' } ).append( $( ' ', {			// 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 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;			pgRevs.forEach( function ( rv ) { var 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 +  ); 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 ) { 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 === '' ) ? 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 '; }

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 = ""; // add header to new log pages page.text = '\n\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;

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\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. */	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|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 || '' ); },

replaceUsage: function { if ( !this.inUse ) { return this.nextTask; }

this.showProgress( this.i18n.replacingUsage ); var reasonShort = 'Duplicate:';

if ( !this.details ) { AQD.reason = AQD.reason.replace( /\[\[Commons:File[_ ]renaming[^[\]]*\]\]:? ?/i, '' ); reasonShort = '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 = $( ' ', { 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 = $( ' ', {						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 + ' ' )					.append( '<textarea rows=20 id="AjaxQuestion' + i + '">' );			} else {				$AjaxDeleteContainer.append( '<label for="AjaxQuestion' + i + '">' + v.message + ' ' )					.append( '<input type="' + v.type + '" id="AjaxQuestion' + i + '" style="width:97%;">' );			}

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

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

curQuestion.data( 'parserResultNode', $parserResultNode ).keyup( _parseReason ); }			if ( v.type !== 'textarea' ) { $AjaxDeleteContainer.append( ' ' ); }

$AjaxDeleteContainer.append( v.appendNode ? 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( $( ' ', { id: 'ajaxDeleteAnonwarning' } ), 'MediaWiki:Anoneditwarning' ).appendTo( $AjaxDeleteContainer ); }

$( ' ' ).append( $AjaxDeleteContainer ).dialog( {			width: ( width || 600 ),			modal: true,			title: title,			dialogClass: 'wikiEditor-toolbar-dialog',			close: function {				$( this ).dialog( 'destroy' ).remove;				if ( AQD.currentTask === 'formerDRRequestpage' ) {					mw.util.$content.find( '.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( ' ' ).parent,					$i = $.createIcon( 'ui-icon-arrow-4-diag' ).attr( 'title', AQD.i18n.expandToTextarea );				$( ' ', { // 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 = $( ' ' ).append( $( ' ', { src: d.thumburl, height: d.thumbheight, width: d.thumbwidth } ), $( ' ', {				id: 'AjaxDeleteImgDel', html: Math.round( d.size / 1000 ) + ' KiB ' + d.width + '×' + d.height + ' ' } ).append( $( '', {					href: d.descriptionurl,					text: d.title,					target: '_blank'				} ) ) ), $imgF = $( ' ' ).append( $( ' ', { src: f.thumburl, height: f.thumbheight, width: f.thumbwidth } ), $( ' ', {				id: 'AjaxDeleteImgKeep', html: Math.round( f.size / 1000 ) + ' KiB ' + f.width + '×' + f.height + ' ' } ).append( $( '', {					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 = $( ' ', { id: 'AjaxDupeContainer' } ).append( $imgD, $imgF ); $( ' ' ).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 = $( ' ' ).html( ' ' + ( message || this.i18n.preparingToEdit ) + ' ' ).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: \n:Skin: ' + mw.user.options.get( 'skin' ) + '\n:[ Contribs] ' + '[ Log] ' + 'before error User: \n\n';

$( '#feedbackContainer' ).contents.remove.end .append( $( ' ', { 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 = $( ' ' ).append( $( ' ', { id: 'feedbackContainer', html: ( msg + ' ' + this.i18n.errorDetails + ' ' + mw.html.escape( err ) + ' ' +				( this.tag ? ( this.i18n.tagWas + this.tag ) : '' ) + ' ' + 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 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 ' + ( 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 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 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. 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;	} ); }

} ); //