સભ્ય:Harsh4101991/Gadget-AjaxQuickDelete.js

વિકિપીડિયામાંથી

નોંધ: પાનું પ્રકાશિત કર્યા પછી, તમારે તમારા બ્રાઉઝરની કૅશ બાયપાસ કરવાની આવશ્યકતા પડી શકે છે.

  • ફાયરફોક્સ / સફારી: શીફ્ટ દબાવેલી રાખીને રિલોડ પર ક્લિક કરો, અથવા તો Ctrl-F5 કે Ctrl-R દબાવો (મેક પર ⌘-R)
  • ગુગલ ક્રોમ: Ctrl-Shift-R દબાવો (મેક પર ⌘-Shift-R)
  • ઈન્ટરનેટ એક્સપ્લોરર/એજ: Ctrl દબાવેલી રાખીને રિફ્રેશ પર ક્લિક કરો, અથવા Ctrl-F5 દબાવો
  • Opera: Ctrl-F5 દબાવો
// Original code written by [[User:Ilmari Karonen]]
// Rewritten & extended by [[User:DieBuche]]. Botdetection and encoding fixer by [[User:Lupo]]
// Validation and further development [[User:Rillke]], 2011-2012
//
// Ajax-based replacement for [[MediaWiki:Quick-delete-code.js]]
//
// TODO: Fix problems with moves of videos
// TODO: Delete talk
//<nowiki>

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

(function($, mw) {

'use strict';

var namespaceNumber = mw.config.get('wgNamespaceNumber');
var pageName = mw.config.get('wgPageName');
var canonicalNS = mw.config.get('wgCanonicalNamespace');

var AjaxQuickDelete, AQD;

if (typeof AjaxQuickDelete !== 'undefined' || namespaceNumber < 0) return;

// utility method: Should be moved out into some global site code since used everywhere
$.createIcon = function (iconClass) {
	return $('<span>', { 'class': 'ui-icon ' + iconClass + ' ajaxInlineIcon', text: ' ' });
};

AjaxQuickDelete = AQD = window.AjaxQuickDelete = {

   /**
    ** Set up the AjaxQuickDelete object and add the toolbox link.  Called via $(document).ready() during page loading.
    **/
   install: function() {

      // Disallow performing operations on empty pages
      if (0 === mw.config.get('wgArticleId')) return;

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

      // wait for document.readyState
      $(function() {
         $(document).triggerHandler('scriptLoaded', ['AjaxQuickDelete']);

         // Set up toolbox link
         if (namespaceNumber !== 14) {
            mw.util.addPortletLink('p-tb', 'javascript:AjaxQuickDelete.nominateForDeletion();', AQD.i18n.toolboxLinkDelete, 't-ajaxquickdelete', null);
         } else {
            mw.util.addPortletLink('p-tb', 'javascript:AjaxQuickDelete.discussCategory();', AQD.i18n.toolboxLinkDiscuss, 't-ajaxquickdiscusscat', null);
         }

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

         // Install AjaxMoveButton
         if ((AQD.userRights === 'filemover' || AQD.userRights === 'sysop') && namespaceNumber === 6) {

            // Also add a "Move & Replace" button to dropdown menu
            mw.util.addPortletLink('p-cactions', 'javascript:AjaxQuickDelete.moveFile("", "");', AQD.i18n.dropdownMove, 'ca-quickmove', 'ca-move');

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

            // Install x-To-DR
            $('.ctdr-btn-convert').click(AQD._convertToDR);
            $('.ctdr-btn-remove').click(AQD._removeAnyTag);
            $('.convert-to-dr').show();
         }
         if (AQD.userRights === 'sysop' && namespaceNumber === 6) {
            if ($('#AjaxDupeProcess').length) {
               $('#AjaxDupeProcess').append('<a href="javascript:AjaxQuickDelete.processDupes();">Process Duplicates</a>').show();
            }
         }
         // Extra buttons
         if ("1" === mw.user.options.get('gadget-QuickDelete')) {
            // Wait until the user's js was loaded and executed
            mw.loader.using(['ext.gadget.QuickDelete', 'user'], function() {
               AQD.doInsertTagButtons();
            });
         }
      });
   },
   
   /**
    ** Ensure that all variables are in a good state
    ** You must call this method before doing anything!
    **/
   initialize: function(undefined) {
      pageName = mw.config.get('wgPageName');
      this.tasks = [];
      this.destination = undefined;
      this.details = undefined;
   },

   fileExists: function() {
      this.i18n.moveDestination = this.i18n.moveOtherDestination;
      this.moveFile();
   },

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

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

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

      this.addTask('doesFileExist');
      this.fileNameExistsCB = 'fileExists';
      this.addTask('getMoveToken');
      this.addTask('movePage');
      this.addTask('removeTemplate');
      if (this.inUse) this.addTask('replaceUsage');

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

      this.prompt([{
         message: this.i18n.moveDestination,
         prefill: (this.possibleDestination || this.cleanFileName(pageName)),
         returnvalue: 'destination',
         cleanUp: true,
         noEmpty: true
      }, {
         message: this.i18n.reasonForMove,
         prefill: (this.reason || this.possibleReason || ''),
         returnvalue: 'reason',
         cleanUp: true,
         noEmpty: false
      }, {
         message: this.i18n.leaveRedirect,
         prefill: true,
         returnvalue: 'wpLeaveRedirect',
         cleanUp: false,
         noEmpty: false,
         type: 'checkbox'
      }], this.i18n.movingFile);
      if (this.inUse || this.userRights === 'filemover') $('#AjaxQuestion2').prop('disabled', true);
   },

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

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

      // finally reload the page to show the template was removed
      this.addTask('reloadPage');
      
      // extend the reason
      switch (reason) {
         case 'move':
            reason = 'No valid reason stated, see the [[COM:MOVE|rename guidelines]]';
            break;
      }

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

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

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

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

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

      this.addTask('prependTemplate');

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

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

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

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

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

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

      // first schedule some API queries to fetch the info we need...
      this.addTask('findCreator');
      
      // ...then schedule the actual edits
      this.addTask('notifyUploaders');
      this.addTask('prependTemplate');
      this.addTask('createRequestSubpage');
      this.addTask('listRequestSubpage');

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

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

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

   },
   nominateForDeletion: function(page) {
      var o = this;
   
      // reset task list in case an earlier error left it non-empty
      this.initialize(); 

      mw.loader.using(['jquery.byteLength', 'jquery.ui'], function() {
         o.pageName = (page === undefined) ? pageName.replace(/_/g, ' ') : page.replace(/_/g, ' ');
         o.startDate = new Date();

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

         o.tag = "{{delete|કારણ=%PARAMETER%|subpage=" + requestPage + o.formatDate("|year=YYYY|month=MON|day=DAY}}\n");
         
         switch (namespaceNumber) {
            // On MediaWiki pages, wrap inside comments (for css and js)
            case 8:
               o.tag = '/*' + o.tag + '*/';
               break;
            // On templates and creator/institution-templates: Wrap inside <noinclude>s.
            case 10:
            case 100:
            case 106:
               o.tag = '<noinclude>' + o.tag + '</noinclude>';
               break;
         }
         o.img_summary = 'Nominating for deletion';
         o.talk_tag = '{' + '{subst:idw|' + requestPage + '}}';
         o.talk_summary = "[[:" + o.pageName + "]] has been nominated for deletion";
         o.subpage_summary = 'Starting deletion request';

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

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

         // 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.prompt([{
            message: '',
            prefill: o.reason || '',
            returnvalue: 'reason',
            cleanUp: true,
            noEmpty: true,
            appendNode: lazyLoadNode,
            parseReason: true
         }], o.i18n.reasonForDeletion);
      });
   },
   
   renderNode: function($node, remotecontent, selector) {
      if (selector) selector = ' ' + selector;
      
      $node.load(mw.config.get('wgScript') + '?' + $.param({
         'action': 'render',
         'title': remotecontent,
         'uselang': mw.config.get('wgUserLanguage')
      }) + (selector || ''), function() {
         $node.find('a').each(function(i, el) {
            var $el = $(el);
            $el.attr('href', $el.attr('href').replace('MediaWiki:Anoneditwarning', mw.config.get('wgPageName')));
         });
      });
      return $node;
   },

   createLazyLoadNode: function(label, page, selector) {
      return $('<div>', {
         style: 'min-height:40px;'
      }).append($('<a>', {
         'href': '#',
         'text': label
      }).click(function(e) {
         e.preventDefault();
         var $content = $(this).parent().find('.ajaxDeleteLazyLoad');
         var $contentInner = $content.find('.ajax-quick-delete-loading');
         if ($contentInner.length) {
            // first time invoked, do the XHR to load the content
            AQD.renderNode($content, $contentInner.data('aqdPage'), selector);
         }
         $content.toggle('fast');
      }), $('<div>', {
         'class': 'ajaxDeleteLazyLoad',
         'style': 'display:none;'
      }).append($('<span>', {
         'class': 'ajax-quick-delete-loading',
         'text': this.i18n.loading
      }).data('aqdPage', page)));
   },
   extractFromHTML: function(DOMElement) {
      var $el = $(DOMElement);
      
      // ...extract the regular expression from html
      this.templateRegExp = $el.parent().find('.ctdr-regex').text();
      var m = this.templateRegExp.match(/^\/(.+)\/(i)?$/);
      if (!m || !m[1]) {
         var err = new Error('The template does not expose a valid regular expression for {{X-To-DR}}. Go the the template and fix it there.');
         this.fail(err);
         throw err;
      }
      this.templateRegExp = new RegExp(m[1], m[2]);
      
      // ...and the template name itself
      var template = $el.parent().find('.ctdr-template-name').text();
      this.reason = "This file was initially tagged by %USER%" + (template ? (" as '''" + template + "'''") : "");
      
      // ...and the decline reason
      this.declineReason = $el.parent().find('.ctdr-template-decline-reason').text();
   },
   removeProgress: function() {
      this.showProgress();
      return this.nextTask();
   },
   /**
    ** Remove any tag
    ** @context DOM-Element
    ** This function must be called with the DOM-Element as this-arg!
    **/ 
   _removeAnyTag: function(e) {
      AQD.extractFromHTML(this);
      AQD.removeAnyTag();
      return false;
   },
   removeAnyTag: function() {
      this.initialize();
      this.addTask('declineRequest');
      this.nextTask();
   },
   /**
    ** Convert any tag to a deletion request
    ** @context DOM-Element
    ** This function must be called with the DOM-Element as this-arg!
    **/ 
   _convertToDR: function(e) {
      AQD.extractFromHTML(this);
      AQD.convertToDR();
      return false;
   },
   convertToDR: function() {
      // reset task list in case an earlier error left it non-empty
      this.initialize(); 

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

      // ...then schedule the actual edits
      this.addTask('removeTemplate');
      this.addTask('removeProgress');
      this.addTask('nominateForDeletion');
      
      this.declineReason = "This file does not qualify for [[COM:SPEEDY|speedy-deletion]] and a regular deletion request will be started.";
      
      // Hide the buttons to prevent attempts of duplicate removal
      $('.convert-to-dr').hide();

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

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

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

      this.addTask('getDupeDetails');

      this.addTask('compareDetails');
      this.addTask('mergeDescriptions');
      this.addTask('saveDescription');
      if (this.inUse) this.addTask('replaceUsage');
      this.addTask('deletePage');
      this.addTask('redirectPage');

      this.addTask('reloadPage');

      this.destination = $('#AjaxDupeDestination').text();
      this.nextTask();

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

      pages = result.query.pages;
      this.details = [];

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

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

      this.showProgress(this.i18n.creatingNomination);

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

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

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

      this.showProgress(this.i18n.listingNomination);

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

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

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

            this.uploadersToNotify++;
         }
      }
      if (this.uploadersToNotify === 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 (namespaceNumber === 6) {
         query = {
            action: 'query',
            prop: 'imageinfo|revisions|info',
            rvprop: 'content|timestamp',
            intoken: 'edit',
            iiprop: 'user|sha1|comment',
            iilimit: 50,
            titles: this.pageName
         };

      } else {
         query = {
            action: 'query',
            prop: 'info|revisions',
            rvprop: 'user|timestamp',
            rvlimit: 1,
            rvdir: 'newer',
            intoken: 'edit',
            titles: this.pageName
         };
      }
      this.showProgress(this.i18n.preparingToEdit);
      this.doAPICall(query, 'findCreatorCB');
   },
   findCreatorCB: function(result) {
      this.uploaders = {};
      var pages = result.query.pages;
      for (var id in pages) { // there should be only one, but we don't know its ID
         if (pages.hasOwnProperty(id)) {
            // The edittoken only changes between sessions
            this.edittoken = pages[id].edittoken;
            
            if (!pages[id].revisions) {
               this.disableReport = true;
               throw new Error('The page you are attempting to add a tag to was deleted or moved. Unable to retrieve the content.');
            }

            //First handle non-file pages
            if (namespaceNumber !== 6 || !pages[id].imageinfo) {

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

               if (typeof this.pageCreator !== 'undefined') {
                  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]) continue; // skip reverts
                  seenHashes[info[i].sha1] = true;
                  // Now exclude bots which only reupload a new version:
                  this.excludedBots = ['FlickreviewR', 'Rotatebot', 'Cropbot', 'Picasa Review Bot', 'Reedy RotateBot'];
                  if (-1 !== $.inArray(info[i].user, this.excludedBots)) continue;

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

   getMoveToken: function() {
      var query = {
         action: 'query',
         prop: 'info|revisions|imageinfo',
         rvprop: 'content|timestamp',
         iiprop: 'mime',
         intoken: 'edit|move',
         titles: pageName
      };

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

   getMoveTokenCB: function(result) {
      var pages = result.query.pages;
      for (var id in pages) { // there should be only one, but we don't know its ID
         if (pages.hasOwnProperty(id)) {
            var pg = pages[id];
            if (!pg.revisions) {
               this.disableReport = true;
               throw new Error('The page you are attempting to modify or move was deleted or moved. Unable to history and contents.');
            }
            // The edittoken only changes between sessions
            this.edittoken = pg.edittoken;
            this.movetoken = pg.movetoken;
            this.pageContent = pg.revisions[0]['*'];
            this.starttimestamp = pg.starttimestamp;
            this.timestamp = pg.revisions[0].timestamp;
            if (pg.imageinfo && pg.imageinfo.length && pg.imageinfo[0].mime) {
               this.fileMime = pg.imageinfo[0].mime
                  .replace('image/jpeg', 'jpg')
                  .replace(/image\/(?:(png)|(gif)|x-(xcf)|vnd\.(djvu)|(svg)\+xml|(tif)f)/, '$1')
                  .replace(/application\/(ogg|pdf)/, '$1')
                  .replace('audio\/midi', 'mid');
               if (this.fileMime.length > 5) this.fileMime = '';
            }
         }
      }

      this.nextTask();
   },
   
   doesFileExist: function() {
      var toCheck = this.cleanFileName(this.destination).replace(/^File:/, '');
      var query = {
         'action': 'query',
         'list': 'allpages',
         'apfrom': toCheck,
         'apto': toCheck,
         'apnamespace': 6
      };
      this.showProgress(this.i18n.checkFileExists);
      this.doAPICall(query, 'doesFileExistCB');
   },
   
   doesFileExistCB: function(result) {
      if (!result || !result.query || !result.query.allpages) throw new Error('Checking file name: result.query.allpages is undefined.');
      if (result.query.allpages[0]) {
         if (this.fileNameExistsCB) this[this.fileNameExistsCB](result.query.allpages[0].title.replace(/^File:/, ''));
         return;
      }
      this.nextTask();
   },

   removeTemplate: function() {
      var page = {};
      this.replaceWith = (this.replaceWith || (this.templateRegExp ? '' : '$1$2' ));
      page.title = (this.destination || pageName);
      page.text = $.trim(this.pageContent.replace((this.templateRegExp || /(?:([^\=])\n)?\{\{(?:rename|rename media|move)\|.*?\}\}(?:\n([^\=]))?/i), this.replaceWith));
      page.editType = 'text';
      page.starttimestamp = this.starttimestamp;
      page.timestamp = this.timestamp;

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

   replaceUsage: function() {
      var page = {};
      page.title = 'User:CommonsDelinker/commands';
      if (this.userRights === 'filemover') {
         page.title = 'User:CommonsDelinker/commands/filemovers';
         this.reason = this.reason.replace(/\{/g, '&#123;').replace(/\}/g, '&#125;').replace(/\=/g, '&#61;');
      }
      if (!this.details) this.reason = '[[COM:FR|File renamed]]: ' + this.reason.replace(/\[\[Commons:File[_ ]renaming[^\[\]]*\]\]:? ?/i, '');
      page.text = '\n{{universal replace|' + pageName.replace('File:', '') + '|' + this.destination.replace('File:', '') + '|reason=' + this.reason + '}}';
      page.editType = 'appendtext';
      page.watchlist = 'nochange';

      this.showProgress(this.i18n.replacingUsage);
      this.savePage(page, 'universal replace: [[:' + pageName + ']] → [[:' + this.destination + ']]', 'nextTask');
   },
   redirectPage: function() {
      var page = {};
      page.title = pageName;
      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 ([[' + pageName + ']])', 'nextTask');
   },


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

      var $submitButton, $cancelButton;
      var $AjaxDeleteContainer = $('<div>', {
         id: 'AjaxDeleteContainer'
      });
      
      var _convertToTextarea = function(e) {
         var $el = $(this),
            $input = $el.data('toConvert'),
            $tarea = $('<textarea>', { id: $input.attr('id'), style: 'height:10em; width:98%; display:none;' });
            
         $el.unbind();
         $el.fadeOut();
         $input.parent().prepend(
            $tarea
               .data('v', $input.data('v')).data('parserResultNode', $input.data('parserResultNode'))
               .val($input.val()).keyup(_parseReason).bind('keyup input', _validateInput));
         $tarea.slideDown();
         $input.remove();
      };
      
      var _parseReason = function(event) {
         var $el = $(this),
            parsertimeout = $el.data('parsertimeout'),
            parserjqXHR = $el.data('parserjqXHR'),
            $parserResultNode = $el.data('parserResultNode'),
            delay = 1000;

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

         parsertimeout = parsertimeout || 0;
         if (parserjqXHR) parserjqXHR.abort();

         var gotJSON = function(d) {
               try {
                  $parserResultNode.html(d.parse.text['*']);
                  $parserResultNode.css('color', '#000');
               } catch (ex) {}
            };

         var parseIt = function() {
               var toParse = $el.val();
               if (!toParse || !/(?:<|\/\/|\[|\'\{|~~)/.test(toParse)) {
                  gotJSON({
                     parse: {
                        text: {
                           '*': toParse || ''
                        }
                     }
                  });
                  return;
               }
               var query = {
                  format: 'json',
                  action: 'parse',
                  uselang: mw.config.get('wgUserLanguage'),
                  redirects: true,
                  prop: 'text',
                  pst: true,
                  text: toParse
               };
               $el.data('parserjqXHR', 
                  $.getJSON(mw.util.wikiScript('api'), query, function(text) {
                     gotJSON(text);
                     delay += 65;
                  })
               );
            };
         clearTimeout(parsertimeout);
         $el.data('parsertimeout', 
            setTimeout(parseIt, Math.min(3500, delay))
         );
      };
      
      var _validateInput = function(event) {
         var $el = $(this),
            v = $el.data('v');
            
         if (v.noEmpty) {
            if ($.trim($el.val()).length < (v.minLength || 10)) {
               $submitButton.button('option', 'disabled', true);
            } else {
               $submitButton.button('option', 'disabled', false);
            }
         }
         if (('TEXTAREA' !== $el.prop('nodeName')) && 
            ((event.keyCode - 0) === 13) && 
            (v.enterToSubmit !== false) && 
            !$submitButton.button('option', 'disabled')
         ) $submitButton.click();
      };

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

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

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

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

         curQuestion.data('v', v);
         curQuestion.bind('keyup input', _validateInput);
         
         // SECURITY: prefill could contain evil jsCode. Never use it unescaped!
         // Use .val() or { value: prefill } or '<input value="' + mw.html.escape() + '" ...>
         curQuestion.val(v.prefill);
         if (v.type === 'checkbox') curQuestion.attr('checked', v.prefill).attr('style', 'margin-left: 5px');
      });
      
      if (mw.user.isAnon()) {
         AQD.renderNode($('<div>', { id: 'ajaxDeleteAnonwarning' }), 'MediaWiki:Anoneditwarning').appendTo($AjaxDeleteContainer);
      }

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

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

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

   /**
    ** Pseudo-Modal JS windows.
    **/
   compareDetails: function() {
      var d = this.details[0],
         f = this.details[1],
         $submitButton, $inverseButton, $cancelButton, $swapButton, $overlayButton;

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

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

      dlgButtons[this.i18n.submitButtonLabel] = function() {
         $(this).dialog("close");
         AQD.nextTask();
      };
      dlgButtons[this.i18n.inverseButtonLabel] = function() {
         $(this).dialog("close");
         AQD.destination = pageName.replace(/_/g, ' ');
         pageName = f.title;
         AQD.details.reverse();
         setTimeout(function() {
            AQD.compareDetails();
         }, 10);
      };
      dlgButtons[this.i18n.cancelButtonLabel] = function() {
         $(this).dialog("close");
      };
      dlgButtons[this.i18n.swapImagesButtonLabel] = function() {
         if ($imgD[0].nextSibling === $imgF[0]) {
            $imgD.before($imgF);
         } else {
            $imgF.before($imgD);
         }
      };
      var $fClone;
      dlgButtons[this.i18n.overlayButtonLabel] = function() {
         if ($fClone) {
            $fClone.remove();
            $fClone = 0;
         } else {
            $fClone = $imgF.clone().appendTo($imgF.parent());
            $fClone.css('position', 'absolute');
            var pos = $imgD.position();
            $fClone.css('top', pos.top - 1);
            $fClone.css('left', pos.left - 1);
            $fClone.fadeTo(0, 0.65);
            // These modules should be already loaded for the dialog but let's be sure
            mw.loader.using(['jquery.ui'], function() {
               // Set width to auto because AjaxQuickDelete.css sets it to a fixed size
               $fClone.css('background', 'rgba(200, 200, 200, 0.5)').css('width', 'auto').css('border', '1px solid #0c9').draggable();
               $fClone.find('img').resizable();
               // In IE, opacity is not fully inerhited
               $fClone.children('div').fadeTo(0, 0.7);
            });
         }
      };
      var $AjaxDupeContainer = $('<div>', {
         id: 'AjaxDupeContainer'
      }).append($imgD, $imgF);
      var $dialog = $('<div></div>').append($AjaxDupeContainer).dialog({
         width: 800,
         modal: true,
         title: this.i18n.compareDetails,
         draggable: false,
         dialogClass: "wikiEditor-toolbar-dialog",
         close: function() {
            $(this).dialog("destroy");
            $(this).remove();
         },
         buttons: dlgButtons,
			open: function() {
				var $buttons = $(this).parent().find('.ui-dialog-buttonpane button');
				$submitButton = $buttons.eq(0).button({ icons: { primary: 'ui-icon-circle-check' } }).addClass('ui-button-green');
            $inverseButton = $buttons.eq(1).button({ icons: { primary: 'ui-icon-refresh' } });
				$cancelButton = $buttons.eq(2).button({ icons: { primary: 'ui-icon-circle-close' } }).addClass('ui-button-red');
            $swapButton = $buttons.eq(3).button({ icons: { primary: 'ui-icon-transfer-e-w' } });
            $overlayButton = $buttons.eq(4).button({ icons: { primary: 'ui-icon-newwin' } });
            $swapButton.css('float', (('left' === $swapButton.css('float')) ? 'right' : 'left'));
            $overlayButton.css('float', (('left' === $overlayButton.css('float')) ? 'right' : 'left'));
			}
      });
   },

   mergeDescriptions: function() {
      this.prompt([{
         message: '',
         prefill: this.details[0].content,
         returnvalue: 'discard',
         cleanUp: false,
         noEmpty: false,
         type: 'textarea',
         enterToSubmit: false
      }, {
         message: '',
         prefill: this.details[1].content,
         returnvalue: 'newPageText',
         cleanUp: false,
         noEmpty: false,
         type: 'textarea',
         enterToSubmit: false
      }], this.i18n.mergeDescription, 800);
      this.destination = this.details[1].title;
      this.reason = 'Exact or scaled-down duplicate: [[:' + this.destination + ']]';
   },

   cleanFileName: function(uncleanName) {
      // Remove Namespace
      uncleanName = uncleanName.replace(/^(?:Image|File):/i, '');
      // Convert extension to lower case
      uncleanName = uncleanName.replace(/\.\w{3,4}$/, function($e) { return $e.toLowerCase(); });
      // jpeg -> jpg
      uncleanName = uncleanName.replace(/\.jpe*g$/, '.jpg');
      
      // First cleanUp from Flinfo (FlinfoOut.php) by Flominator and Lupo
      uncleanName = uncleanName.replace(/~{3,}/g, '')
         .replace(/\s+|_/g, ' ')
         .replace(/[\x00-\x1f\x7f]/g, '')
         .replace(/%([0-9A-Fa-f]{2})/g, '% $1')
         .replace(/&(([A-Za-z0-9\x80-\xff]+|#[0-9]+|#x[0-9A-Fa-f]+);)/g, '& $1')
         .replace(/[:\/|#]/g, '-')
         .replace(/[\]\}>]/g, ')')
         .replace(/[\[\{<]/g, '(');

      var currentExt = pageName.toLowerCase().replace(/.*?\.(\w{3,4})$/, '$1').replace('jpeg', 'jpg');
      // If the current mime-type is available to the script, check it; 
      // MediaWiki sometimes allows uploading mismatching mimetypes but not moving
      if (this.fileMime) {
         currentExt = ('ogg' === this.fileMime && ('oga' === currentExt || 'ogv' === currentExt)) ? currentExt : this.fileMime;
      }
      var reCurrentExt = new RegExp('\\.' + currentExt + '$', 'i');

      // If new file name is without extension, add the one from the old name
      if (!reCurrentExt.test(uncleanName.toLowerCase())) uncleanName += '.' + currentExt;
      // Capitalize the first letter and prefix the namespace
      return 'File:' + uncleanName.replace(/^\w/, function($0) { return $0.toUpperCase(); });
   },
   cleanReason: function(uncleanReason) {
      // trim whitespace
      uncleanReason = uncleanReason.replace(/^\s*(.+)\s*$/, '$1');
      // remove signature
      uncleanReason = uncleanReason.replace(/(?:\-\-|–|—)? ?~{3,5}$/, '').replace(/^~{3,5} ?/, '');
      return uncleanReason;
   },

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

         this.progressDialog = $('<div></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',
            open: function() {
               $(this).parent().find('.ui-dialog-titlebar').hide();
            },
            close: function() {
               $(this).dialog("destroy");
               $(this).remove();
            }
         });
      }

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

   movePage: function() {
      // Some users don't get it: They want to move pages to itself.
      if (AQD.cleanFileName(pageName) === AQD.destination) return AQD.nextTask();
      mw.loader.using(['ext.gadget.libAPI'], function() {
         mw.user.tokens.set('moveToken', AQD.movetoken);
         var moveArgs = {
            cb: function() {
               AQD.nextTask();
            },
            // r-result, query, text
            errCb: function(r, q, t) {
               AQD.fail(t);
            },
            from: pageName,
            to: AQD.destination,
            reason: AQD.reason,
            movetalk: true
         };
         // Option to not leave a redirect behind, MediaWiki default does leave one behind
         // Just like movetalk, an empty parameter sets it to true (true to not leave a redirect behind)
         if (AQD.wpLeaveRedirect === false) {
            moveArgs.noredirect = true;
         }
         AQD.showProgress(AQD.i18n.movingFile);
         mw.libs.commons.api.movePage(moveArgs);
      });
   },

   deletePage: function() {
      var edit = {
         action: 'delete',
         reason: this.reason,
         title: pageName,
         token: this.deletetoken,
         recreate: ''
      };
      this.showProgress(this.i18n.deletingFile);
      this.doAPICall(edit, 'nextTask');
   },

   setCurrentDate: function(x) {
      try {
         var dat = x.getResponseHeader('date').match(/\D+(\d\d) (\D{3}) (\d{4}) (\d\d):(\d\d):(\d\d)/);
         var monthNamesShort = [ "", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ];
         this.currentDate = new Date(dat[3], $.inArray(dat[2], monthNamesShort) - 1, dat[1], dat[4], dat[5], dat[6]);
         // The date is initialized/ constructed in local time but the server returned GMT-Time, so remove the offset
         // According to w3c under- and overflow (<0, >60) are handled by the date-object itself
         this.currentDate.setMinutes(this.currentDate.getMinutes() - this.currentDate.getTimezoneOffset());
      } catch (ex) {
         this.currentDate = this.startDate || new Date();
      }
   },

   /**
    ** 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,
          newParams = { format: 'json' };

      // At least let's try to send the format first and the token last
      // If the POST-request is cut off, we get "invalid token" or other errors
      $.extend(newParams, params);
      if ('edit' === newParams.action) newParams.token = this.edittoken;
      
      var retry = function(timeout, errText) {
         o.apiErrorThreshold--;
         if (0 === o.apiErrorThreshold) {
            return o.fail(errText);
         } else {
            return setTimeout(function () {
               o.doAPICall(params, callback);
            }, timeout);
         }
      };
      
      $.ajax({
         url: this.apiURL,
         cache: false,
         dataType: 'json',
         data: newParams,
         type: 'POST',
         success: function(result, status, x) {
            if (!o.currentDate && x && x.getResponseHeader) o.setCurrentDate(x);
            if (!result && 'query' === newParams.action) return retry(1500, "Received empty API response:\n" + x.responseText);
            if (!result) return o.fail("Received 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 retry(500, "mysterious 231 unknown error");
            
            if (result.error && 'editconflict' === result.error.code && (params.prependtext || params.appendtext)) return retry(750, "edit conflict");
            
            if (result.error) {
               // In some cases, we just don't want to know. If users have protected their talk-page it's their problem.
               if (-1 !== $.inArray(result.error.code, ['protectedpage', 'missingtitle'])) this.disableReport = true;
               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) {
            if ('query' === newParams.action) return retry(1500, "API request returned code " + x.status + " " + status + ". Error code is " + 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);
      }
   },
   retryTask: function() {
      try {
         this[this.currentTask]();
      } catch (e) {
         this.fail(e);
      }
   },

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

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

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

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

      var dlgButtons = {};
      dlgButtons[this.i18n.retryButtonLabel] = function() {
         $(this).remove();
         o.retryTask();
      };
      if (-1 !== $.inArray(o.currentTask, ['movePage', 'deletePage', 'notifyUploaders']) && (/code 50\d/.test(err) || /missingtitle/.test(err))) {
         dlgButtons[this.i18n.ignoreButtonLabel] = function() {
            $(this).remove();
            o.nextTask();
         };
      }
      if (!this.disableReport) {
         dlgButtons[this.i18n.reportButtonLabel] = function() {
            $('#feedbackContainer').contents().remove();
            $('#feedbackContainer').append($('<img>', {
               src: '/w/skins/common/images/ajax-loader.gif'
            })).css('text-align', 'center');
            var randomId = Math.round(Math.random()*1099511627776);
            var toSend = '\n== Autoreport by AjaxQuickDelete ' + randomId + ' ==\n' + err + '\n++++\n:Task: ' + o.currentTask + '\n:NextTask: ' + o.tasks[0] + '\n:LastTask: ' + o.tasks[o.tasks.length - 1] + 
               '\n:Page: ' + (o.pageName || pageName) + '\n:Skin: ' + mw.user.options.get('skin') + '\n:[{{fullurl:Special:Contributions|target={{subst:urlencode:{{subst:REVISIONUSER}}}}&offset={{subst:REVISIONTIMESTAMP}}}} Contribs before error]';
            $.post(o.apiURL, {
               'action': 'edit',
               'format': 'json',
               'title': 'MediaWiki talk:Gadget-AjaxQuickDelete.js/auto-errors',
               'summary': '[[#Autoreport by AjaxQuickDelete ' + randomId + '|Reporting an AjaxQuickDelete error.]] Random ID=' + randomId,
               'appendtext': toSend,
               'token': (o.edittoken || mw.user.tokens.get('csrfToken'))
            }, function() {
               o.reloadPage();
            });
         };
      }
      dlgButtons[this.i18n.abortButtonLabel] = function() {
         $(this).remove();
      };
      
      this.disableReport = false;
      this.showProgress();
      this.progressDialog = $('<div>').append($('<div>', {
         id: 'feedbackContainer',
         html: (msg + ' ' + fix + '<br>' + this.i18n.errorDetails + '<br>' + mw.html.escape(err) + '<br>' + (this.tag ? (this.i18n.tagWas + this.tag) : '') + '<br><a href="' + mw.config.get('wgServer') + '/wiki/MediaWiki_talk:AjaxQuickDelete.js" >' + this.i18n.errorReport + '</a>')
      })).dialog({
         width: 550,
         modal: true,
         closeOnEscape: false,
         title: this.i18n.errorDlgTitle,
         dialogClass: "ajaxDeleteError",
         buttons: dlgButtons,
         close: function() {
            $(this).dialog("destroy");
            $(this).remove();
         }
      });
   },

   /**
    ** 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.currentDate || 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, mw.config.get('wgMonthNames')[date.getUTCMonth() + 1]);
      fmt = fmt.replace(/DAY/g, date.getUTCDate());
      return fmt;
   },

   // Constants
   // DR subpage prefix
   requestPagePrefix: "Deletion notice/",
   // user talk page prefix
   userTalkPrefix: mw.config.get('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: "પાનું દૂર કરવા વિનંતી",
      toolboxLinkDiscuss: "Nominate category for discussion",

      // GUI reason prompt form
      reasonForDeletion: "શા માટે આ પાનું દૂર કરવું?",
      reasonForDiscussion: "Why does this category need discussion?",
      moreInformation: "More information",
      loading: "Loading...",

      // 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",

      // GUI progress messages
      preparingToEdit: "Preparing to edit pages... ",
      creatingNomination: "Creating nomination page... ",
      listingNomination: "Adding nomination page to daily list... ",
      addingAnyTemplate: "Adding template to " + canonicalNS.toLowerCase() + " page... ",
      notifyingUploader: "Notifying %USER%... ",

      // 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 file name?",
      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",
      
      // For declining any request
      removingTemplate: "Removing template",
      declineRequest: "Why do you want to decline the request?",
      anyDecline: "Decline request",

      //For Duplicates
      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",

      // Errors
      errorDlgTitle: "Error",
      genericFailure: "An error occurred while trying to do the requested action. ",
      taskFailure: {
         listUploaders: "An error occurred while determining the " + (namespaceNumber === 6 ? " uploader(s) of this file" : "creator of this page") + ".",
         loadPages: "An error occurred while preparing to nominate this " + canonicalNS.toLowerCase() + " for deletion.",
         prependDeletionTemplate: "An error occurred while adding the {{delete}} template to this " + canonicalNS.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 " + (namespaceNumber === 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 " + canonicalNS.toLowerCase() + " for deletion, please edit the page to add the {{delete}} template and follow the instructions shown on it.",
      completeRequestByHand: "Please follow the instructions on the deletion notice to complete the request.",
      errorDetails: "A detailed description of the error is shown below:",
      errorReport: "Manually report the error here or click on <tt>Report automatically</tt> to send an automatic error-report.",
      tagWas: "The tag to be inserted into this page was "
   }
};

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


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