// 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: Delete talk // if (typeof(AjaxQuickDelete) == 'undefined') { window.AjaxQuickDelete = { /** ** Set up the AjaxQuickDelete object and add the toolbox link. Called via $(document).ready() during page loading. **/ install: function () { this.insertTagButtons = [{ label: this.i18n.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 a 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) this.insertTagButtons = this.insertTagButtons.concat(window.AjaxDeleteExtraButtons); // Import stylesheet importStylesheet('MediaWiki:AjaxQuickDelete.css'); // Don't install link if the user opted out, or when were on a special page if (window.AjaxDeleteOptOut || wgNamespaceNumber < 0) return; // Set up toolbox link if (wgNamespaceNumber != 14) { addPortletLink('p-tb', 'javascript:AjaxQuickDelete.nominateForDeletion();', this.i18n.toolboxLinkDelete, 't-ajaxquickdelete', null); } else { 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 (wgUserGroups && (' ' + wgUserGroups.join(' ') + ' ').indexOf(' sysop ') != -1) { this.userRights = 'sysop'; } else if (wgUserGroups && (' ' + wgUserGroups.join(' ') + ' ').indexOf(' filemover ') != -1) { this.userRights = 'filemover'; } // Install AjaxMoveButton if ((this.userRights == 'filemover' || this.userRights == 'sysop') && wgNamespaceNumber == 6) { // Also add a "Move & Replace" button to dropdown menu addPortletLink('p-cactions', 'javascript:AjaxQuickDelete.moveFile("", "");', this.i18n.dropdownMove, 'ca-quickmove', 'ca-move'); //Add quicklinks to template if ($('#AjaxRenameLink').length) { $('#AjaxRenameLink') .append('Move file and replace all usage') .append(' Decline request'); } } if (this.userRights == 'sysop' && wgNamespaceNumber == 6) { if ($('#AjaxDupeProcess').length) $('#AjaxDupeProcess') .append('Process Duplicates').show(); } // Define optional buttons if (window.QuickDeleteEnhanced && wgNamespaceNumber == 6) { $.each(this.insertTagButtons, function (k, v) { 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('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 || ''), 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').attr('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, page) { this.pageName = (page === undefined) ? wgPageName.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... this.tasks = []; // get token this.addTask('findCreator'); this.addTask('prependTemplate'); if (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 }], prompt_text || this.i18n.reasonForDeletion); } else { this.nextTask(); } }, discussCategory: function () { this.pageName = wgPageName; 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 (page) { this.pageName = (page === undefined) ? wgPageName : page; this.startDate = new Date(); // set up some page names we'll need later this.requestPage = this.requestPagePrefix + this.pageName; this.dailyLogPage = this.requestPagePrefix + this.formatDate("YYYY/MM/DD"); this.tag = "{{delete|reason=%PARAMETER%|subpage=" + this.pageName + this.formatDate("|year=YYYY|month=MON|day=DAY}}\n"); this.img_summary = 'Nominating for deletion'; this.talk_tag = "{\{subst:idw|" + this.pageName + "}}"; this.talk_summary = "[[:" + this.pageName + "]] 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'); // ...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 = 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\n=== [[:" + this.pageName + "]] ===\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) { if (user == 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 () { if (wgNamespaceNumber == 6) { var query = { action: 'query', prop: 'imageinfo|revisions|info', rvprop: 'content|timestamp', intoken: 'edit', iiprop: 'user|sha1|comment', iilimit: 50, titles: this.pageName }; } else { var query = { action: 'query', prop: 'info|revisions', rvprop: 'user|timestamp', rvlimit: 1, rvdir: 'newer', intoken: 'edit', titles: this.pageName }; } 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 (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; // 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) { // Make sure the username is in canonical form match = match.replace(/^[\s_]+/, "").replace(/[\s_]+$/, "").replace(/[\s_]+/g, " "); match = match.substr(0, 1).toUpperCase() + match.substr(1); this.uploaders[match] = true; } } } } this.nextTask(); }, 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 = (this.destination || 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'; if (!this.details) this.reason = '[[Commons:File renaming|File renamed]]: ' + this.reason; page.text = '\n{{universal replace|' + wgPageName.replace('File:', '') + '|' + this.destination.replace('File:', '') + '|reason=' + 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 = $('
').html('
').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('