MediaWiki:Gadget-ajaxQuickDelete.js (MediaWiki&Gadget-ajaxQuickDeleteQjs)

Перейти к навигации Перейти к поиску
JS-код ниже относится к гаджету «Добавить ссылки для пометки проблемных файлов (бета)» (править описание). Связанный CSS-файл: MediaWiki:Gadget-ajaxQuickDelete.css. Его использует около 2000 учётных записей.

После сохранения или недавних изменений очистите кэш браузера.

// Original code written by [[User:Ilmari Karonen]]
// Rewritten & extended by [[User:DieBuche]]. Botdetection and encoding fixer by [[User:Lupo]]
//
// Ajax-based replacement for [[MediaWiki:Quick-delete-code.js]]
//<nowiki>
if ( typeof( AjaxQuickDelete ) === 'undefined' ) {

window.AjaxQuickDelete = {

	/**
	 ** Set up the AjaxQuickDelete object and add the toolbox link.  Called via $( document ).ready() during page loading.
	 **/
	install: function ( ) {
		this.insertTagButtons = [{
				label: this.i18n.toolboxLinkAuthor,
				tag: '{\{subst:nad}}',
				talkTag: '{\{subst:Запрос о статусе файла|1=%FILE%}}',
				imgSummary: 'не указан автор файла',
				talkSummary: '[[ВП:ППФ|автоматическое]] уведомление об отсутствии автора для %FILE%'
			}, {
				label: this.i18n.toolboxLinkSource,
				tag: '{\{subst:nsd}}',
				talkTag: '{\{subst:Запрос о статусе файла|1=%FILE%}}',
				imgSummary: 'не указан источник файла',
				talkSummary: '[[ВП:ППФ|автоматическое]] уведомление об отсутствии источника для %FILE%'
			}, {
				label: this.i18n.toolboxLinkLicense,
				tag: '{\{subst:nld}}',
				talkTag: '{\{subst:Запрос о статусе файла|1=%FILE%}}',
				imgSummary: 'не указана лицензия файла',
				talkSummary: '[[ВП:ППФ|автоматическое]] уведомление об отсутствии лицензии для %FILE%'
			}, {
				label: this.i18n.toolboxLinkPermission,
				tag: '{\{subst:npd}}',
				talkTag: '{\{subst:Запрос о статусе файла|1=%FILE%}}',
				imgSummary: 'отсутствует [[ВП:ДОБРО|разрешение]] на использование файла',
				talkSummary: '[[ВП:ППФ|автоматическое]] уведомление об отсутствии [[ВП:ДОБРО|разрешения]] на использование %FILE%'
			}, {
				label: this.i18n.toolboxLinkDisputed,
				tag: '{\{subst:dd}}',
				talkTag: '{\{subst:Запрос о статусе файла|1=%FILE%}}',
				imgSummary: 'сомнительный статус файла',
				talkSummary: '[[ВП:ППФ|автоматическое]] уведомление о сомнительном статусе %FILE%'
			}, {
				label: this.i18n.toolboxLinkDisputedFairuse,
				tag: '{\{subst:dfud%FUC1%%FUC2%%FUC3%%FUC4%%FUC9%%FUC10%}}',
				talkTag: '{\{subst:Запрос о соответствии КДИ|1=%FILE%}}',
				imgSummary: 'файл не соответствует [[ВП:КДИ]]',
				talkSummary: '[[ВП:ППФ|автоматическое]] уведомление о несоответствии [[ВП:КДИ]] %FILE%',
				promptText: {
					title: this.i18n.reasonForDisputedFairuse,
					questions: [{
						message: '1 — может быть заменено свободным эквивалентом;',
						type: 'checkbox',
						prefill: '|1',
						returnvalue: 'FUC1'
					}, {
						message: '2 — разрешение/качество наносит имущественный ущерб правообладателю;',
						type: 'checkbox',
						prefill: '|2',
						returnvalue: 'FUC2'
					}, {
						message: '3 — не отвечает критерию минимального использования или не имеет значимости;',
						type: 'checkbox',
						prefill: '|3',
						returnvalue: 'FUC3'
					}, {
						message: '4 — не публиковалось ранее или не имеет проверяемого источника;',
						type: 'checkbox',
						prefill: '|4',
						returnvalue: 'FUC4'
					}, {
						message: '9 — используется вне пространства статей или не используется в статьях;',
						type: 'checkbox',
						prefill: '|9',
						returnvalue: 'FUC9'
					}, {
						message: '10 — не имеет описания и/или обоснования добросовестности использования.',
						type: 'checkbox',
						prefill: '|10',
						returnvalue: 'FUC10'
					}]
				}
			}, {
				label: this.i18n.toolboxLinkOrphanedFairuse,
				tag: '{\{subst:ofud}}',
				imgSummary: 'неиспользуемый несвободный файл'
			}, {
				label: this.i18n.toolboxLinkNotHost,
				tag: '{\{subst:nothost}}',
				imgSummary: 'неиспользуемый файл, не имеющий энциклопедической ценности'
			}];
		if ( typeof window.AjaxDeleteExtraButtons !== 'undefined' ) {
			this.insertTagButtons = this.insertTagButtons.concat( window.AjaxDeleteExtraButtons );
		}

		// Define optional buttons
		if ( mw.config.get( 'wgNamespaceNumber' ) === 6 && $( '#shared-image-desc' ).length === 0 ) {
			$.each( this.insertTagButtons, function ( k, v ) {
				mw.util.addPortletLink( 'p-tb', 'javascript:AjaxQuickDelete.insertTagOnPage("' + v.tag + '","'
				  + v.imgSummary + '","' + v.talkTag + '","' + v.talkSummary + '","'
				  + ( typeof v.promptText !== 'undefined' ? JSON.stringify( v.promptText ).replace( /"/g, '\\"' ) : v.promptText )
				  + '");', v.label );
			} );
		}
	},

	insertTagOnPage: function ( tag, imgSummary, talkTag, talkSummary, promptText, page ) {
		this.pageName = ( page === undefined ) ? mw.config.get( 'wgPageName' ).replace( /_/g, ' ' ) : page.replace( /_/g, ' ' );
		this.tag = tag + '\n';
		this.imgSummary = imgSummary;

		// first schedule some API queries to fetch the info we need...
		this.tasks = [];

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

		this.addTask( 'prependTemplate' );

		if ( talkTag !== 'undefined' ) {
			this.talkTag = talkTag.replace( '%FILE%', this.pageName );
			this.talkSummary = talkSummary.replace( '%FILE%', '[[:' + this.pageName + ']]' );

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

		if ( promptText !== 'undefined' ) {
			promptText = JSON.parse( promptText );
			if ( typeof promptText === 'string' ) {
				this.prompt( [{
					message: '',
					prefill: '',
					returnvalue: 'reason',
					cleanUp: true,
					noEmpty: true
				}], promptText || this.i18n.reasonForDeletion );
			} else {
				this.prompt( promptText.questions, promptText.title );
			}
		} else {
			this.nextTask();
		}
	},

	nominateForDeletion: function ( page ) {
		this.pageName = ( page === undefined ) ? mw.config.get( 'wgPageName' ) : page;
		this.startDate = new Date();

		// set up some page names we'll need later
		this.requestPage = this.requestPagePrefix + this.pageName;
		this.dailyLogPage = this.requestPagePrefix + this.formatDate( 'YYYY/MM/DD' );

		this.tag = '{{delete|reason=%PARAMETER%|subpage=' + this.pageName + this.formatDate( '|year=YYYY|month=MON|day=DAY}}\n' );
		// On templates: Wrap inside <noinclude>s. Thanks Rillke
		if ( mw.config.get( 'wgNamespaceNumber' ) === 10 ) {
		  this.tag = '<noinclude>' + this.tag + '</noinclude>';
		}
		this.imgSummary = 'Nominating for deletion';
		this.talkTag = '{\{subst:idw|' + this.pageName + '}}';
		this.talkSummary = '[[:' + this.pageName + ']] has been nominated for deletion';
		this.subpageSummary = 'Starting deletion request';

		// first schedule some API queries to fetch the info we need...
		this.tasks = []; // reset task list in case an earlier error left it non-empty
		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' );

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

	/**
	 ** 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 = [];
		page.title = this.pageName;
		page.text = this.tag;
		page.editType = 'prependtext';
		if ( window.AjaxDeleteWatchFile ) {
			page.watchlist = 'watch';
		}

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

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

		this.showProgress( this.i18n.creatingNomination );

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

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

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

		this.showProgress( this.i18n.listingNomination );

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

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

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

			this.uploadersToNotify++;
		}
		if ( this.uploadersToNotify === 0 ) {
			this.nextTask();
		}
	},

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

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

		} else {
			query = {
				action: 'query',
				prop: 'info|revisions',
				rvprop: 'user|timestamp',
				rvlimit: 1,
				rvdir: 'newer',
				meta: 'tokens',
				type: 'csrf',
				titles: this.pageName
			};
		}
		this.showProgress();
		this.doAPICall( query, 'findCreatorCB' );
	},
	findCreatorCB: function ( result ) {
		this.uploaders = {};
		this.edittoken = result.query.tokens.csrftoken;
		
		var pages = result.query.pages;
		for ( var id in pages ) { // there should be only one, but we don't know its ID

			//First handle non-file pages
			if ( mw.config.get( 'wgNamespaceNumber' ) !== 6 ) {

				this.pageCreator = pages[id].revisions[0].user;
				this.starttimestamp = pages[id].starttimestamp;
				this.timestamp = pages[id].revisions[0].timestamp;

				this.uploaders[this.pageCreator] = true;

			} else {
				var info = pages[id].imageinfo;

				var content = pages[id].revisions[0]['*'];

				var seenHashes = {};
				for ( var i = info.length - 1; i >= 0; i-- ) { // iterate in reverse order
					if ( info[i].sha1 && seenHashes[info[i].sha1] ) {
						// skip reverts
						continue;
					}
					seenHashes[info[i].sha1] = true;
					// Now exclude bots which only reupload a new version:
					this.excludedBots = 'FlickreviewR, Rotatebot, Cropbot, Picasa Review Bot';
					if ( this.excludedBots.indexOf( info[i].user ) !== -1 ) {
						continue;
					}

					// Handle some special cases, most of the code by [[User:Lupo]]
					var match;
					if ( info[i].user === 'File Upload Bot (Magnus Manske)' ) {
						// CommonsHelper
						match = /transferred to Commons by \[\[User:([^\]\|]*)(\|([^\]]*))?\]\] using/.exec( info[i].comment );

						// geograph_org2commons, regex accounts for typo ("transferd") and it's possible future correction
						if ( !match ) {
						  match = /geograph.org.uk\]; transferr?e?d by \[\[User:([^\]\|]*)(\|([^\]]*))?\]\] using/.exec( info[i].comment );
						}

						// flickr2commons
						if ( !match ) {
							match = /\* Uploaded by \[\[User:([^\]\|]*)(\|([^\]]*))?\]\]/.exec( content );
						}

						if ( match ) {
							match = match[1];
						}
						// Really necessary?
						match = this.fixDoubleEncoding( match );
					} else if ( info[i].user === 'FlickrLickr' ) {
						match = /\n\|reviewer=\s*(.*)\n/.exec( content );
						if ( match ) {
							match = match[1];
						}
					} else if ( info[i].user === 'Flickr upload bot' ) {
						// Check for the bot's upload template
						match = /\{\{User:Flickr upload bot\/upload(\|[^\|\}]*)?\|reviewer=([^\}]*)\}\}/.exec( content );
						if ( match ) {
							match = match[2];
						}
					} else {
						// No special case applies, just continue;
						this.uploaders[info[i].user] = true;
						continue;
					}
					if ( match ) {
						// Make sure the username is in canonical form
						match = match
							.replace( /^[\s_]+/, '' )
							.replace( /[\s_]+$/, '' )
							.replace( /[\s_]+/g, ' ' );
						match = match.substr( 0, 1 ).toUpperCase() + match.substr( 1 );
						this.uploaders[match] = true;
					}
				}
			}
		}
		this.nextTask();
	},

	removeTemplate: function () {
		var page = [];
		page.title = ( this.destination || mw.config.get( 'wgPageName' ) );
		page.text = this.pageContent.replace( /\{\{(rename|rename media|move)\|.*?\}\}/i, '' );
		page.editType = 'text';
		page.starttimestamp = this.starttimestamp;
		page.timestamp = this.timestamp;

		this.showProgress( this.i18n.removingTemplate );
		this.savePage( page, ( this.declineReason || this.i18n.renameDone ), 'nextTask' );
	},

	redirectPage: function () {
		var page = [];
		page.title = mw.config.get( 'wgPageName' );
		page.text = '#REDIRECT [[' + this.destination + ']]';
		page.editType = 'text';

		this.showProgress( this.i18n.redirectingFile );
		this.savePage( page, 'Redirecting to duplicate file', 'nextTask' );
	},
	saveDescription: function () {
		var page = [];
		page.title = this.destination;
		page.text = this.newPageText;
		page.editType = 'text';

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


	/**
	 ** Pseudo-Modal JS windows.
	 **/
	prompt: function ( questions, title, width ) {
		var dlgButtons = {};
		dlgButtons[this.i18n.submitButtonLabel] = function () {
			$.each( questions, function ( i, v ) {
				var response = $( '#AjaxQuestion' + i ).val();
				if ( v.type === 'checkbox' ) {
					if ( response === 'on' ) {
						// no value
						response = $( '#AjaxQuestion' + i ).prop( 'checked' );
					}
					else if ( !$( '#AjaxQuestion' + i ).prop( 'checked' ) ) {
						// unchecked
						response = '';
					}
				}
				if ( v.cleanUp ) {
					if ( v.returnvalue === 'reason' ) {
						response = AjaxQuickDelete.cleanReason( response );
					}
					if ( v.returnvalue === 'destination' ) {
						response = AjaxQuickDelete.cleanFileName( response );
					}
				}
				AjaxQuickDelete[v.returnvalue] = response;
				if ( v.returnvalue === 'reason' && AjaxQuickDelete.tag ) {
					AjaxQuickDelete.tag = AjaxQuickDelete.tag.replace( '%PARAMETER%', response );
					AjaxQuickDelete.imgSummary = AjaxQuickDelete.imgSummary.replace( '%PARAMETER%', response );
					AjaxQuickDelete.imgSummary = AjaxQuickDelete.imgSummary.replace( '%PARAMETER-LINKED%', '[[:' + response + ']]' );
				}
				var param = '%' + v.returnvalue.toUpperCase() + '%';
				AjaxQuickDelete.tag = AjaxQuickDelete.tag.replace( param, response );
				AjaxQuickDelete.imgSummary = AjaxQuickDelete.imgSummary.replace( param, response );
			} );
			$( this ).dialog( 'close' );
			AjaxQuickDelete.nextTask();
		};
		dlgButtons[this.i18n.cancelButtonLabel] = function () {
			$( this ).dialog( 'close' );
		};

		$( '<div>' )
			.html( $('<div>').attr('id', 'AjaxDeleteContainer') )
			.dialog( {
				width: ( width || 600 ),
				modal: true,
				title: title,
				draggable: false,
				dialogClass: 'wikiEditor-toolbar-dialog',
				close: function () {
					$( this ).dialog( 'destroy' );
					$( this ).remove();
				},
				buttons: dlgButtons
			} );
		var submitButton = $( '.ui-dialog-buttonpane button:first' );

		$.each( questions, function ( i, v ) {
			if ( v.type === 'textarea' ) {
				$( '#AjaxDeleteContainer' )
					.append( '<label for="AjaxQuestion' + i + '">' + v.message + '</label>' )
					.append( '<textarea rows=20 id="AjaxQuestion' + i + '"><br/><br/>' );
			} else {
				$( '#AjaxDeleteContainer' )
					.append( '<label for="AjaxQuestion' + i + '">' + v.message + '</label>' )
					.append( '<input type="' + ( v.type || 'text' ) + '" id="AjaxQuestion' + i + '" style="width: 98%;"><br/><br/>' );
			}
			var curQuestion = $( '#AjaxQuestion' + i );
			curQuestion.keyup( function ( event ) {
				if ( v.noEmpty ) {
					if ( $( this ).val().length < ( v.minLength || 4 ) ) {
						submitButton.addClass( 'ui-state-disabled' );
					} else {
						submitButton.removeClass( 'ui-state-disabled' );
					}
				}
				if ( event.keyCode === '13' && v.enterToSubmit !== false ) {
					submitButton.click();
				}
			} );
			curQuestion.val( v.prefill );
			if ( v.type === 'checkbox' ) {
				$( '#AjaxQuestion' + i )
					.prop( 'checked', ( typeof v.checked !== 'undefined' ? v.checked : false ) )
					.attr( 'style', 'float:left; margin-right:5px' );
			}
			curQuestion.keyup();
		} );
		$( '#AjaxQuestion0' ).focus();
	},

	/**
	 ** Pseudo-Modal JS windows.
	 **/
	compareDetails: function () {
		var d = this.details[0];
		var f = this.details[1];
		document.body.style.cursor = 'default';
		this.progressDialog.remove();
		if ( d.sha1 === f.sha1 ) {
			this.exactDupes = true;
			this.nextTask();
			return;
		}
		var dlgButtons = {};
		dlgButtons[this.i18n.submitButtonLabel] = function () {

			$( this ).dialog( 'close' );
			AjaxQuickDelete.nextTask();
		};
		dlgButtons[this.i18n.cancelButtonLabel] = function () {
			$( this ).dialog( 'close' );
		};
		$( '<div>' )
			.html( $('<div>').attr('id', 'AjaxDeleteContainer') )
			.dialog( {
				width: 800,
				modal: true,
				title: 'title',
				draggable: false,
				dialogClass: 'wikiEditor-toolbar-dialog',
				close: function () {
					$( this ).dialog( 'destroy' );
					$( this ).remove();
				},
				buttons: dlgButtons
			} );
		$( '#AjaxDupeContainer' )
			.append( '<div><img src="' + d.thumburl + '"></div>' )
			.append( '<div><img src="' + f.thumburl + '"></div>' )
			.append( '<div>' + Math.round( d.size / 1000 ) + ' KB <br/>' + d.width + 'x' + d.height + '</div>' )
			.append( '<div>' + Math.round( f.size / 1000 ) + ' KB <br/>' + f.width + 'x' + f.height + '</div>' );
	},

	/**
	 ** Double encoding fixer by Lupo. This is necessary for some older uploads of Magnus' bot.
	 **/
	fixDoubleEncoding: function ( match ) {
		if ( !match ) {
			return match;
		}
		var utf8 = /[u00C2-u00F4][u0080-u00BF][u0080-u00BF]?[u0080-u00BF]?/g;
		if ( !utf8.test( match ) ) {
			return match;
		}
		// Looks like we have a double encoding. At least it contains character
		// sequences that might be legal UTF-8 encodings. Translate them into %-
		// syntax and try to decode again.
		var temp = '',
			curr = 0,
			m,
			hexDigit = '0123456789ABCDEF';
		var str = match.replace( /%/g, '%25' );
		utf8.lastIndex = 0;
		// Reset regexp to beginning of string
		try {
			while ( ( m = utf8.exec( str ) ) !== null ) {
				temp += str.substring( curr, m.index );
				m = m[0];
				for ( var i = 0; i < m.length; i++ ) {
					temp += '%' + hexDigit.charAt( m.charCodeAt( i ) / 16 ) + hexDigit.charAt( m.charCodeAt( i ) % 16 );
				}
				curr = utf8.lastIndex;
			}
			if ( curr < str.length ) {
				temp += str.substring( curr );
			}
			temp = decodeURIComponent( temp );
			return temp;
		} catch ( e ) {}
		return match;
	},

	cleanFileName: function ( uncleanName ) {
		uncleanName = uncleanName
			.replace( /^Image:/i, 'File:' )
			.replace( /\.jpe*g$/i, '.jpg' )
			.replace( /\.png$/i, '.png' )
			.replace( /\.gif$/i, '.gif' );

		var currentExtension = mw.config.get( 'wgPageName' ).toLowerCase().replace( /.*?\.(\w{3,4})$/, '$1' ).replace( 'jpeg', 'jpg' );

		// If new file name is without extension, add the one from the old name
		if ( uncleanName.toLowerCase().indexOf( currentExtension ) === -1 ) {
			uncleanName += '.' + currentExtension;
		}
		if ( uncleanName.indexOf( 'File:' ) === -1 ) {
			uncleanName = 'File:' + uncleanName;
		}
		return uncleanName;
	},
	cleanReason: function ( uncleanReason ) {
		// trim whitespace
		uncleanReason = uncleanReason.replace( /^\s*(.+)\s*$/, '$1' );
		// remove signature
		uncleanReason = uncleanReason.replace( /(.+)(--)?~{3,5}$/, '$1' );
		return uncleanReason;
	},

	/**
	 ** For display of progress messages.
	 **/
	showProgress: function ( message ) {
		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: 90,
					minHeight: 90,
					modal: true,
					resizable: false,
					draggable: false,
					closeOnEscape: false,
					dialogClass: 'ajaxDeleteFeedback'
				} );
			$( '.ui-dialog-titlebar' ).hide();
		}

	},
	/**
	 ** Submit an edited page.
	 **/
	savePage: function ( page, summary, callback ) {
		var edit = {
			action: 'edit',
			summary: summary,
			watchlist: ( page.watchlist || 'preferences' ),
			title: page.title,
			token: this.edittoken
		};
		if ( page.redirect ) {
			edit.redirect = '';
		}
		edit[page.editType] = page.text;
		this.doAPICall( edit, callback );
	},

	/**
	 ** Does a MediaWiki API request and passes the result to the supplied callback (method name).
	 ** Uses POST requests for everything for simplicity.
	 **/
	doAPICall: function ( params, callback ) {
		var o = this;

		params.format = 'json';
		$.ajax( {
			url: this.apiURL,
			cache: false,
			dataType: 'json',
			data: params,
			type: 'POST',
			success: function ( result, status, x ) {
				if ( !result ) {
					return o.fail( 'Receive empty API response:\n' + x.responseText );
				}

				// In case we get the mysterious 231 unknown error, just try again
				if ( result.error && result.error.info.indexOf( '231' ) !== -1 ) {
					return setTimeout( function () {
						o.doAPICall( params, callback );
					}, 500 );
				}
				if ( result.error ) {
					return o.fail( 'API request failed (' + result.error.code + '): ' + result.error.info );
				}
				if ( result.edit && result.edit.spamblacklist ) {
					  return o.fail( 'The edit failed because ' + result.edit.spamblacklist + ' is on the Spam Blacklist' );
				}
				try {
					o[callback]( result );
				} catch ( e ) {
					return o.fail( e );
				}
			},
			error: function ( x, status, error ) {
				return o.fail( 'API request returned code ' + x.status + ' ' + status + 'Error code is ' + error );
			}
		} );
	},

	/**
	 ** 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 () {
		var task = this.currentTask = this.tasks.shift();
		try {
			this[task]();
		} catch ( e ) {
			this.fail( e );
		}
	},

	/**
	 ** Once we're all done, reload the page.
	 **/
	reloadPage: function () {
		this.progressDialog.remove();
		if ( this.pageName && this.pageName.replace( / /g, '_' ) !== mw.config.get( 'wgPageName' ) ) {
			return;
		}
		var encTitle = ( this.destination || mw.config.get( 'wgPageName' ) );
		encTitle = encodeURIComponent( encTitle.replace( / /g, '_' ) ).replace( /%2F/ig, '/' ).replace( /%3A/ig, ':' );
		location.href = mw.config.get( 'wgServer' ) + mw.config.get( 'wgArticlePath' ).replace( '$1', encTitle );
	},

	/**
	 ** Crude error handler. Just throws an alert at the user and
	 ** (if we managed to add the { { delete } } tag) reloads the page.
	 **/
	 fail: function ( err ) {
		if ( 'object' === typeof err ) {
			var stErr = err.message + '<br/>' + err.name;
			if ( err.lineNumber ) {
				stErr += ' @line' + err.lineNumber;
			}
			err = stErr;
		}

		document.body.style.cursor = 'default';
		var msg = this.i18n.taskFailure[this.currentTask] || this.i18n.genericFailure;

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

		$( '#feedbackContainer' ).html( msg + ' ' + fix + '<br/>' + this.i18n.errorDetails + '<br/>' + err
			+ '<br/><a href=' + mw.config.get( 'wgServer' ) + '/wiki/MediaWiki_talk:AjaxQuickDelete.js>' + this.i18n.errorReport + '</a>' );
		$( '.ui-dialog-content' ).height( 'auto' );
		$( '.ui-dialog' ).addClass( 'ajaxDeleteError' );
		// Allow some time to read the message
		if ( this.templateAdded ) {
			setTimeout( this.reloadPage(), 5000 );
		}
	},

	/**
	 ** 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 ) {
		var pad0 = function ( s ) {
			s = '' + s;
			return ( s.length > 1 ? s : '0' + s );
		}; // zero-pad to two digits
		if ( !date ) {
			date = this.startDate;
		}
		fmt = fmt.replace( /YYYY/g, date.getUTCFullYear() );
		fmt = fmt.replace( /MM/g, pad0( date.getUTCMonth() + 1 ) );
		fmt = fmt.replace( /DD/g, pad0( date.getUTCDate() ) );
		fmt = fmt.replace( /MON/g, this.months[date.getUTCMonth()] );
		fmt = fmt.replace( /DAY/g, date.getUTCDate() );
		return fmt;
	},
	months: 'January February March April May June July August September October November December'.split( ' ' ),

	// Constants
	// DR subpage prefix
	requestPagePrefix: 'Commons:Deletion requests/',
	// user talk page prefix
	userTalkPrefix: mw.config.get( 'wgFormattedNamespaces' )[3] + ':',
	// MediaWiki API script URL
	apiURL: ( /^\/\//.test( mw.config.get( 'wgServer' ) ) ? document.location.protocol : '' ) + mw.config.get( 'wgServer' ) + mw.util.wikiScript( 'api' ),


	// 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?',
		reasonForDisputedFairuse: 'Каким критериям не соответствует файл?',
		submitButtonLabel: 'Подтвердить',
		cancelButtonLabel: 'Отменить',

		// GUI progress messages
		preparingToEdit: 'Подготовка к редактированию страниц… ',
		creatingNomination: 'Creating nomination page... ',
		listingNomination: 'Adding nomination page to daily list... ',
		addingAnyTemplate: 'Добавление шаблона на страницу' + ( mw.config.get( 'wgNamespaceNumber' ) === 6 ? ' файла' : '' ) + '… ',
		notifyingUploader: 'Уведомление участника %USER%… ',

		// Extended version
		toolboxLinkAuthor: 'Нет автора',
		toolboxLinkLicense: 'Нет лицензии',
		toolboxLinkSource: 'Нет источника',
		toolboxLinkPermission: 'Нет разрешения',
		toolboxLinkDisputed: 'Сомнительный',
		toolboxLinkDisputedFairuse: 'Не соответствует КДИ',
		toolboxLinkOrphanedFairuse: 'Неисп. несвободный',
		toolboxLinkNotHost: 'Не хостинг',
		toolboxLinkCopyvio: 'Report copyright violation',
		reasonForCopyvio: 'Why is this file a copyright violation?',

		// For moving files
		renameDone: 'Removing template; rename done',
		removingTemplate: 'Removing rename template',
		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 file name?',
		movingFile: 'Moving file',
		replacingUsage: 'Ordering CommonsDelinker to replace all usage',
		declineMove: 'Why do you want to decline the request?',
		leaveRedirect: 'Leave a redirect behind:',

		//For Duplicates
		deletingFile: 'Deleting file',
		mergeDescription: 'Please now merge the file descriptions',
		redirectingFile: 'Redirecting file',
		savingDescription: 'Saving new details',

		// Errors
		genericFailure: 'An error occurred while trying to do the requested action. ',
		taskFailure: {
			listUploaders: 'An error occurred while determining the '
				+ ( 6 === mw.config.get( 'wgNamespaceNumber' ) ? ' uploader(s) of this file' : 'creator of this page' ) + '.',
			loadPages: 'An error occurred while preparing to nominate this '
				+ mw.config.get( 'wgCanonicalNamespace' ).toLowerCase() + ' for deletion.',
			prependDeletionTemplate: 'An error occurred while adding the {{delete}} template to this '
				+ mw.config.get( '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 '
				+ ( 6 === mw.config.get( 'wgNamespaceNumber' ) ? ' uploader(s) of this file' : 'creator of this page' ) + '.'
		},
		addTemplateByHand: 'To nominate this ' + mw.config.get( '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: 'Report the error here'
	}
};

mediaWiki.loader.using( 'jquery.ui', function () {
	$( document ).ready( function () {
		AjaxQuickDelete.install();
	} );
} );


} // end if (guard)
// </nowiki>