MediaWiki:AjaxQuickDelete-dev.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
// Original code written by [[User:Ilmari Karonen]]
// Rewritten & extended by [[User:DieBuche]]. Botdetection and encoding fixer by [[User:Lupo]]

// Ajax-based replacement for [[MediaWiki:Quick-delete-code.js]]

// TODO: Fix problems with moves of videos
// TODO: Wait for also https://bugzilla.wikimedia.org/show_bug.cgi?id=24330
// TODO: Delete talk
// <nowiki>

if (typeof AjaxQuickDelete == 'undefined' && mw.config.get('wgNamespaceNumber') >= 0) {
   var wgPageName = mw.config.get('wgPageName');
   window.AjaxQuickDelete = {

      /**
      ** Set up the AjaxQuickDelete object and add the toolbox link.  Called via $(document).ready() during page loading.
      **/
      install : function () {
          // Abort if the user opted out, or is using an antique skin & load the legacy version
          if (window.AjaxDeleteOptOut || (skin != 'vector' && skin != 'monobook')) {
              importScript('MediaWiki:Quick-delete-code.js');
              return;
          }
          // Import stylesheet
          importStylesheet('MediaWiki:AjaxQuickDelete.css');

          //jQuery UI is not loaded on all pages:
          if (jQuery.ui == undefined){
              // FIXME: Use mw.loader.using() to depend on jquery.ui something
          }
          // Set up toolbox link
          if (mw.config.get('wgNamespaceNumber') != 14) {
            mw.util.addPortletLink('p-tb', 'javascript:AjaxQuickDelete.nominateForDeletion();', this.i18n.toolboxLinkDelete, 't-ajaxquickdelete', null);
          } else {
            mw.util.addPortletLink('p-tb', 'javascript:AjaxQuickDelete.discussCategory();', this.i18n.toolboxLinkDiscuss, 't-ajaxquickdiscusscat', null);
          }

          // Check user group. Attention: Array.prototype.indexOf does not exist on IE! See also
          // https://bugzilla.wikimedia.org/show_bug.cgi?id=24083 .
          if (mw.config.get('wgUserGroups') && (' ' + mw.config.get('wgUserGroups').join(' ') + ' ').indexOf(' sysop ') != -1 ) {
             this.userRights='sysop';
          } else if (mw.config.get('wgUserGroups') && (' ' + mw.config.get('wgUserGroups').join(' ') + ' ').indexOf(' filemover ') != -1 ) {
             this.userRights='filemover';
          }

          // Install AjaxMoveButton
          if ((this.userRights == 'filemover' || this.userRights == 'sysop') && mw.config.get('wgNamespaceNumber') == 6) {

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

              //Add quicklinks to template
              if ($('#AjaxRenameLink').length) {
                $('#AjaxRenameLink').append('<a href="javascript:AjaxQuickDelete.moveFile();">Move file and replace all usage</a>')
                  .append('<a href="javascript:AjaxQuickDelete.declineMoveFile();" class="ajaxDeleteDeclineMove"><sup> Decline request</sup></a>');
              }
          }
          if (this.userRights == 'sysop' && mw.config.get('wgNamespaceNumber') == 6) {
            if ($('#AjaxDupeProcess').length) $('#AjaxDupeProcess').append('<a href="javascript:AjaxQuickDelete.processDupes();">Process Duplicates</a>').show();
          }
          // Define optional buttons
          if (window.QuickDeleteEnhanced && mw.config.get('wgNamespaceNumber')==6) {
              var insertTagButtons = [
              {
                  label       : this.i18n.toolboxLinkCopyvio,
                  tag         : '{{copyvio|1=%PARAMETER%}}',
                  talk_tag    : '{{subst:copyvionote|1=%FILE%}}',
                  img_summary : 'Marking as possible copyvio because %PARAMETER%',
                  talk_summary: 'Notification of possible copyright violation',
                  prompt_text : this.i18n.reasonForCopyvio

              },
              {
                  label        : this.i18n.toolboxLinkSource,
                  tag          : '{{subst:nsd}}',
                  talk_tag     : '{{subst:image source|1=%FILE%}}',
                  img_summary  : 'File has no source',
                  talk_summary : '%FILE% does not have a source'

              },

              {
                  label        : this.i18n.toolboxLinkPermission,
                  tag          : '{{subst:npd}}',
                  talk_tag     : '{{subst:image permission|1=%FILE%}}',
                  img_summary  : 'Missing permission',
                  talk_summary : 'Please send permission for %FILE% to [[COM:OTRS|OTRS]]'

              },
              {
                  label        : this.i18n.toolboxLinkLicense,
                  tag          : '{{subst:nld}}',
                  talk_tag     : '{{subst:image license|1=%FILE%}}',
                  img_summary  : 'Missing license',
                  talk_summary : '%FILE% does not have a license'

              }
              ];
              if (window.AjaxDeleteExtraButtons) insertTagButtons = insertTagButtons.concat(window.AjaxDeleteExtraButtons);

              $.each(insertTagButtons, function(k, v) {
                  mw.util.addPortletLink('p-tb', 'javascript:AjaxQuickDelete.insertTagOnPage("' + v.tag + '","' + v.img_summary + '","' + v.talk_tag + '","' + v.talk_summary + '","' + v.prompt_text + '");', v.label);
              });
          }
      },
      /**
      ** For moving files
      **/
      moveFile: function () {

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

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

          this.tasks = [];

          this.addTask('getMoveToken');
          this.addTask('removeTemplate');
          this.addTask('movePage');
          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 || ''),
              returnvalue: 'destination',
              cleanUp: true,
              noEmpty: true
            }, {
              message: this.i18n.reasonForMove,
              prefill: (this.possibleReason || ''),
              returnvalue: 'reason',
              cleanUp: true,
              noEmpty: false
            }, {
              message: this.i18n.leaveRedirect,
              prefill: true,
              returnvalue: 'wpLeaveRedirect',
              cleanUp: false,
              noEmpty: false,
              type: 'checkbox'
            }], 'Moving file');
            if (this.inUse || this.userRights == 'filemover') $('#AjaxQuestion2').prop('disabled', true);
      },

      /**
      ** For declining request
      **/
      declineMoveFile : function () {
          // No valid reason stated, see the rename guidelines
          this.tasks = [];

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

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

          this.prompt ([{
                message: '',
                prefill: 'No valid reason stated, see the [[COM:MOVE|rename guidelines]]',
                returnvalue: 'declineReason',
                cleanUp: false,
                noEmpty: true
              }], this.i18n.declineMove);
      },

      insertTagOnPage : function (tag, img_summary, talk_tag, talk_summary, prompt_text) {
           this.tag=tag + '\n';
           this.img_summary = img_summary;

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

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

           this.addTask('prependTemplate' );

           if (talk_tag != "undefined") {
               this.talk_tag=talk_tag.replace('%FILE%', wgPageName);
               this.talk_summary=talk_summary.replace('%FILE%', '[[:'+wgPageName+']]');

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

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

      discussCategory : function () {
          this.startDate = new Date ();
          this.tag = '{{subst:cfd}}';
          this.img_summary = 'This category needs discussion';
          this.talk_tag = "{{subst:cdw|" + wgPageName + "}}";
          this.talk_summary = "[[:" + wgPageName + "]] needs discussion";

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

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

          // ...then schedule the actual edits

          this.addTask('findCreator' );
          this.addTask('notifyUploaders' );
          this.addTask('prependTemplate' );
          this.addTask('createRequestSubpage' );
          this.addTask('listRequestSubpage' );

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

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

      },
      nominateForDeletion : function () {
          this.startDate = new Date ();

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

          this.tag = "{{delete|reason=%PARAMETER%|subpage=" + wgPageName + this.formatDate("|year=YYYY|month=MON|day=DAY}}\n");
          this.img_summary = 'Nominating for deletion';
          this.talk_tag = "{{subst:idw|" + wgPageName + "}}";
          this.talk_summary = "[[:" + wgPageName + "]] has been nominated for deletion";

          // 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' );
          this.addTask('resolveRedirects' );

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

      processDupes : function (){

          if ($('#globalusage').length || !$('#mw-imagepage-nolinkstoimage').length ) this.inUse=true;
          this.tasks = [];  // reset task list in case an earlier error left it non-empty
          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').html();
          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 : wgPageName +'|' + this.destination
              };
        this.doAPICall( query, 'getDupeDetailsCB' );
        this.showProgress('Fetching details');
      },
      getDupeDetailsCB : function (result) {
        var pages = result.query.pages;
        this.details = [];
        i = 0;

        for (var id in pages) {
            v = pages[id];
            n = this.details[i] = {};
            n.title    = v.title;
            n.size     = v.imageinfo[0].size;
            n.width    = v.imageinfo[0].width;
            n.height   = v.imageinfo[0].height;
            n.thumburl = v.imageinfo[0].thumburl;
            n.sha1     = v.imageinfo[0].sha1;
            n.content  = v.revisions[0]['*'];
            n.starttimestamp = v.starttimestamp;
            this.edittoken   = v.edittoken;
            this.deletetoken = v.deletetoken;
            i++;
        }
        //If ordner (old=0, new=1) not correct: Reverse the order
        if (this.details[0].title != wgPageName.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 = wgPageName;
        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\n=== [[:" + wgPageName + "]] ===\n" + this.reason + " ~~"+"~~\n";
        page.watchlist = 'watch';
        page.editType = 'appendtext';

        this.showProgress( this.i18n.creatingNomination );

        this.savePage( page, "Starting deletion request", '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) {
              var page = [];
              page.title = this.userTalkPrefix + user;
              page.text = "\n"+ this.talk_tag +  " ~~"+"~~\n";
              page.editType = 'appendtext';
              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 () {
          if (mw.config.get('wgNamespaceNumber') == 6) {
              var query = {
                action : 'query',
                prop : 'imageinfo|revisions|info',
                rvprop: 'content|timestamp',
                intoken : 'edit',
                iiprop : 'user|sha1|comment',
                iilimit : 50,
                titles : wgPageName
                };

          } else {
              var query = {
                action  : 'query',
                prop    : 'info|revisions',
                rvprop  : 'user|timestamp',
                rvlimit : 1,
                rvdir   : 'newer',
                intoken : 'edit',
                titles  : wgPageName
                };
          }
          this.showProgress();
          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

              // The edittoken only changes between logins
              this.edittoken=pages[id].edittoken;

              //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 ]) continue;  // skip reverts

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

                    // Now exclude the user itself:
                    if(mw.config.get('wgUserName') == info[i].user) continue;

                    // Handle some special cases, most of the code by [[User:Lupo]]
                    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){
                          this.uploaders[ match ] = true;
                      }
                }
              }
          }
          this.nextTask();
      },

      /**
      ** Now query the redirect status of all user talk pages, and, if necessary, replace them in this.uploaders
      ** Otherwise we would appendText to redirected pages, thus breaking them..
      **/
      resolveRedirects : function () {
          var pages = [];
          for (var user in this.uploaders) pages.push( this.userTalkPrefix + user );
          var query = {
              action : 'query',
              redirects : '',
              titles : pages.join('|') };
          this.doAPICall( query, 'resolveRedirectsCB' );
      },

      resolveRedirectsCB : function (result) {
          if (result.query && result.query.redirects) {
              for (var i in result.query.redirects) {
                  redirect = result.query.redirects[i];
                  delete this.uploaders[redirect['from'].replace(this.userTalkPrefix, '')];

                  this.uploaders[ redirect['to'].replace(this.userTalkPrefix, '') ] = true;
              }
          }
          this.nextTask();
      },

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

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

            // The edittoken only changes between logins
            this.edittoken=pages[id].edittoken;
            this.movetoken=pages[id].movetoken;
            this.pageContent = pages[id].revisions[0]['*'];
            this.starttimestamp = pages[id].starttimestamp;
            this.timestamp = pages[id].revisions[0]['timestamp'];
          }

        this.nextTask();
      },

      removeTemplate : function () {
          var page = [];
          page.title = 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');
      },

      replaceUsage : function () {
          var page = [];
          page.title = 'User:CommonsDelinker/commands';
          if (this.userRights=='filemover') page.title = 'User talk:CommonsDelinker/commands';

          page.text = '\n{{universal replace|' + wgPageName.replace("File:", "") + '|' + this.destination.replace("File:", "") + '|reason=[[Commons:File renaming|File renamed]]: ' + this.reason + '}}';
          page.editType = 'appendtext';

          this.showProgress( this.i18n.replacingUsage );
          this.savePage(page, 'universal replace: [[:' + wgPageName + ']] → [[:' + this.destination +']]', 'nextTask');
      },
      redirectPage : function () {
          var page = [];
          page.title = 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) {
                  response = $('#AjaxQuestion' + i).val();
                  if (v.type == 'checkbox') response = $('#AjaxQuestion' + i).attr('checked');
                  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.img_summary = AjaxQuickDelete.img_summary.replace('%PARAMETER%', response);
                      AjaxQuickDelete.img_summary = AjaxQuickDelete.img_summary.replace('%PARAMETER-LINKED%', '[[:' + response + ']]');
                  }
              });
              $(this).dialog("close");
              AjaxQuickDelete.nextTask();
          };
          dlgButtons[this.i18n.cancelButtonLabel] = function () {
              $(this).dialog("close");
          };

          var $dialog = $('<div></div>')
          .html('<div id="AjaxDeleteContainer"></div>')
          .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(v.message)
                .append('<textarea rows=20 id="AjaxQuestion' + i + '"><br><br>');
              } else {
                $('#AjaxDeleteContainer').append(v.message)
                .append('<input type="' + (v.type || 'text') + '" id="AjaxQuestion' + i + '" style="width: 98%;"><br><br>');
              }
              $('#AjaxQuestion' + i).keyup(function(event) {
                  if (v.noEmpty){
                    if ($(this).val().length < 4) {
                        submitButton.addClass('ui-state-disabled');
                    } else {
                        submitButton.removeClass('ui-state-disabled');
                    }
                  }
                  if (event.keyCode == '13' && v.enterToSubmit != false) submitButton.click();
              });
              $('#AjaxQuestion' + i).val(v.prefill);
              if (v.type == 'checkbox') $('#AjaxQuestion' + i).attr('checked', v.prefill).attr('style', 'margin-left: 5px');
              $('#AjaxQuestion' + i).keyup();
          });
          $('#AjaxQuestion0').focus();
      },

      /**
      ** Pseudo-Modal JS windows.
      **/
      compareDetails: function () {
          d=this.details[0];
          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");
          };
          var $dialog = $('<div></div>')
          .html('<div id="AjaxDupeContainer"></div>')
          .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><div><img src="' + f.thumburl + '"></div><br>')
            .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>');
      },

      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 + ']]';
      },
      /**
      ** 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,
          hex_digit = "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 += '%'
                      + hex_digit.charAt(m.charCodeAt(i) / 16)
                      + hex_digit.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:');
          uncleanName = uncleanName.replace(/.jpe*g/i, '.jpg');
          uncleanName = uncleanName.replace(/.png/i, '.png');
          uncleanName = uncleanName.replace(/.gif/i, '.gif');

          // If new file name is without extension, add the one from the old name
          if (uncleanName.toLowerCase().indexOf(wgPageName.toLowerCase().replace(/.*./, '').replace(/jpe*g/i, 'jpg')) == -1) {
              uncleanName += '.' + wgPageName.toLowerCase().replace(/.*./, '').replace(/jpe*g/i, 'jpg');
          }
          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></div>')
              .html('<div id="feedbackContainer">'+ (message || this.i18n.preparingToEdit) + '</div>')
              .dialog({
                  width: 450,
                  height: 'auto',
                  minHeight: 90,
                  modal: true,
                  resizable: false,
                  draggable: false,
                  closeOnEscape: false,
                  dialogClass: "ajaxDeleteFeedback"
              });
              $('.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
              };

          edit[page.editType]=page.text;
          this.doAPICall( edit, callback );
      },

      movePage : function () {
          var edit = {
              action   : 'move',
              reason   : this.reason,
              from     : wgPageName,
              to       : this.destination,
              movetalk : '',
              token    : this.movetoken
              };
          // 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 (this.wpLeaveRedirect === false) {
              edit.noredirect = '';
          }

          this.showProgress( this.i18n.movingFile );
          this.doAPICall( edit, 'nextTask');
      },

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

      /**
      ** 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 () {
          location.href = mw.config.get('wgServer') + wgArticlePath.replace("$1", encodeURIComponent(this.destination || wgPageName));
      },

      /**
      ** 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 ) {
          document.body.style.cursor = 'default';
          var msg = this.i18n.taskFailure[this.currentTask] || this.i18n.genericFailure;
          var 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: wgFormattedNamespaces[3] + ":",
      // MediaWiki API script URL
      apiURL: mw.config.get('wgServer') + wgScriptPath + "/api.php",

      // 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?",
        submitButtonLabel     : "Proceed",
        cancelButtonLabel     : "Cancel",

        // GUI progress messages
        preparingToEdit       : "Preparing to edit pages... ",
        creatingNomination    : "Creating nomination page... ",
        listingNomination     : "Adding nomination page to daily list... ",
        addingAnyTemplate     : "Adding template to " + mw.config.get('wgCanonicalNamespace').toLowerCase() + " description 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
        renameDone            : "Removing template; rename actioned",
        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?",
        dropdownMove          : "Move & Replace",
        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 nominating this " + mw.config.get('wgCanonicalNamespace').toLowerCase() + " for deletion.",
        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"
      }
 };

 if (wgUserLanguage != 'en') importScript( 'MediaWiki:AjaxQuickDelete.js/' + wgUserLanguage);
 $(document).ready(function() { AjaxQuickDelete.install(); });

} // end if (guard)

// </nowiki>