User:Jon Kolbert/AQD.js
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.
![]() | Documentation for this user script can be added at User:Jon Kolbert/AQD. |
// 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]]
//
// Invoke automated jsHint-validation on save: A feature on Wikimedia Commons
// Interested? See [[c:MediaWiki:JSValidator.js]] or [[c:Help:JSValidator]].
//
// 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, nomen:false */
(function ($, mw) {
'use strict';
// Guard against multiple inclusions
if (window.AjaxQuickDelete) return;
var AQD,
conf = mw.config.get([
'wgArticleId',
'wgCanonicalNamespace',
'wgCanonicalSpecialPageName',
'wgCategories',
'wgFormattedNamespaces',
'wgNamespaceNumber',
'wgPageName',
'wgRestrictionEdit',
'wgUserGroups',
'wgUserLanguage',
'wgUserName'
]),
nsNumber = conf.wgNamespaceNumber,
pageName = conf.wgPageName;
// A bunch of helper functions
function firstItem(o) {
for (var i in o) {
if (o.hasOwnProperty(i)) {
return o[i];
}
}
}
function ucFirst(s) {
return s[0].toUpperCase() + s.slice(1);
}
// Create the AjaxQuickDelete-singleton (object literal)
AQD = window.AjaxQuickDelete = {
/**
** Runs before $(document).ready() and before translation is available
** (important event-binders should be attached as fast as possible)
**/
preinstall: function () {
// Promote our gadget when user opened old move page
if (conf.wgCanonicalSpecialPageName === 'Movepage' && Number($('select[name="wpNewTitleNs"]').val()) === 6) {
$('#mw-movepage-table').before(
'<div class="warningbox">Consider using <i>Move & Replace</i> from the menu on file pages (open with a single click) when moving files to care for global usage and redirects.</span>');
}
AQD.doNothing = (!conf.wgArticleId || nsNumber < 0);
if (AQD.doNothing)
return;
// Check user group
if ($.inArray('sysop', conf.wgUserGroups) !== -1) {
AQD.userRights = 'sysop';
} else if ($.inArray('filemover', conf.wgUserGroups) !== -1) {
AQD.userRights = 'filemover';
}
if ((AQD.userRights === 'filemover' || AQD.userRights === 'sysop') && nsNumber === 6) {
// Change "Move" to "Move & Replace"
var $moveLink = $('#ca-move'),
$moveLanchor = $moveLink.find('a');
AQD.$moveLink = $moveLink = $moveLanchor.length ? $moveLanchor : $moveLink;
var _onMoveClick = function (e) {
e.preventDefault();
AQD.moveFile();
};
$moveLink.text($moveLink.text() + ' & Replace').attr('title', 'Click in order to ' + $moveLink.attr('title') + ' and replace usage. Default form though new tab.').click(_onMoveClick);
}
},
/**
** Set up the AjaxQuickDelete object and add the toolbox link. Called via $(document).ready() during page loading.
**/
install: function () {
// Disallow performing operations on empty or special pages
if (AQD.doNothing)
return;
// Check edit restrictions and do not install anything if protected
if (conf.wgRestrictionEdit && conf.wgRestrictionEdit.length) {
if ($.inArray(conf.wgRestrictionEdit[0], conf.wgUserGroups) === -1) {
return;
}
}
// wait for document.readyState
$(function () {
// Trigger a jQuery event for other scripts that like to know
// when this script has loaded all translations and is ready to install
$(document).triggerHandler('scriptLoaded', ['AjaxQuickDelete']);
// Set up toolbox link
if (nsNumber === 14) {
// In categories the discuss-category link
mw.util.addPortletLink('p-tb', 'javascript:AjaxQuickDelete.discussCategory();', AQD.i18n.toolboxLinkDiscuss, 't-ajaxquickdiscusscat');
} else {
// On other pages, the nominate-for-deletion link
mw.util.addPortletLink('p-tb', 'javascript:AjaxQuickDelete.nominateForDeletion();', AQD.i18n.toolboxLinkDelete, 't-ajaxquickdelete');
}
// Install AjaxMoveButton for filemovers and administrators
if (AQD.$moveLink) {
// Change Move & Replace link to fully localized text
AQD.$moveLink.text(AQD.i18n.dropdownMove);
//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. See [[Template:X-To-DR]]; currently filemover rights required
$('.ctdr-btn-convert').click(AQD._convertToDR);
$('.ctdr-btn-remove').click(AQD._removeAnyTag);
$('.convert-to-dr').show();
}
// Install "Process Duplicates"-Link (either in template
// or if no template was detected and MediaWiki found dupes, behind the link in the dupe-section)
if (AQD.userRights === 'sysop' && nsNumber === 6) {
if ($('#AjaxDupeProcess').length) {
$('#AjaxDupeProcess').append($('<a>', {
href: 'javascript:AjaxQuickDelete.processDupes();',
text: "Process Duplicates"
})).show();
} else {
var dupeSection = $('.mw-imagepage-duplicates');
if (dupeSection.length) {
dupeSection.find('li:first')
.append($('<span>', {
style: 'display:none',
id: 'AjaxDupeDestination',
text: dupeSection.find('a').attr('title')
}))
.append(' ', $('<sup>').append($('<a>', {
href: 'javascript:AjaxQuickDelete.processDupes();',
text: "[Process Duplicates]",
style: 'background:#CEEDB4'
})));
}
}
}
// Extra buttons
if (mw.user.options.get('gadget-QuickDelete') === '1') {
// 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!
** TODO: Never override pageName, always clean task queue
**/
initialize: function () {
pageName = conf.wgPageName;
this.tasks = [];
this.destination = undefined;
this.details = undefined;
},
/**
** If a file exists, exchange the messages (very hackish)
** so the user is prompted to choose another destination
** TODO: Develop an improved solution
**/
fileExists: function () {
this.i18n.moveDestination = this.i18n.moveOtherDestination;
this.moveFile();
},
/**
** For moving files
**/
moveFile: function () {
this.initialize();
this.showProgress();
if ($('#AjaxRenameLink').length) {
this.possibleDestination = $('#AjaxRenameDestination').text();
this.possibleReason = this.cleanReason($('#AjaxRenameReason').text());
}
if ($('#globalusage').length || !$('#mw-imagepage-nolinkstoimage').length)
this.inUse = true;
this.addTask('getMoveToken');
this.addTask('promptForMoveTarget');
// Let's be sure we have a fresh token and the latest MIME-Info
this.addTask('getMoveToken');
this.addTask('doesFileExist');
this.fileNameExistsCB = 'fileExists';
this.addTask('movePage');
this.addTask('removeTemplate');
this.addTask('queryRedirects');
this.addTask('replaceUsage');
// finally reload the page to show changed page
this.addTask('reloadPage');
this.nextTask();
},
promptForMoveTarget: function () {
this.showProgress();
this.prompt([{
message: this.i18n.moveDestination,
prefill: this.cleanFileName(this.possibleDestination || pageName),
returnvalue: 'destination',
cleanUp: true,
noEmpty: true
}, {
message: this.i18n.reasonForMove,
prefill: $.trim((this.reason || this.possibleReason || '').replace(/'{2,}/g, '').replace(/\s{2,}/g, ' ')),
returnvalue: 'reason',
cleanUp: true,
noEmpty: false
}, {
message: this.i18n.leaveRedirect,
prefill: true,
returnvalue: 'wpLeaveRedirect',
cleanUp: false,
noEmpty: false,
type: 'checkbox'
}, {
message: this.i18n.useCORSForReplace,
prefill: !window.aqdCORSOptOut,
returnvalue: 'replaceUsingCORS',
cleanUp: false,
noEmpty: false,
type: 'checkbox'
}
], this.i18n.movingFile);
if (this.inUse || this.userRights === 'filemover')
$('#AjaxQuestion2').attr('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 = 'rename request declined: does not comply with [[COM:FR|renaming 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 || pageName).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');
if (this.isMobile()) {
if (/(?:copyvio|nsd|npd|nld)/.test(tag))
this.addTask('listMobileUploadSpeedy');
}
// 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 || pageName).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|reason=%PARAMETER%|subpage=" + requestPage + o.formatDate("|year=YYYY|month=MON|day=DAY}}\n");
switch (nsNumber) {
// 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;
// Lua comments
case 828:
o.tag = '\n--[=[ ' + o.tag + ' ]=]\n';
}
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('purge');
o.addTask('notifyUploaders');
if (o.isMobile()) {
o.addTask('listMobileUpload');
}
// finally reload the page to show the deletion tag
o.addTask('reloadPage');
var lazyLoadNode = o.createLazyLoadNode(o.i18n.moreInformation, 'MediaWiki:Gadget-AjaxQuickDelete.js/DeleteInfo', '#AjaxQuickDeleteDeleteInfo');
o.prevDRNode = $('<ul>').attr('id', 'AjaxDeletePrevRequests');
o.secureCall('checkForFormerDR');
var toAppend = $('<div>').append($('<div>').attr('class', 'ajaxDeleteLazyLoad').css({
'max-height': Math.max(Math.round($(window).height() / 2) - 250, 100),
'min-height': 0,
overflow: 'auto'
}).append(o.prevDRNode), '<br>', lazyLoadNode);
o.prompt([{
message: '',
prefill: o.reason || '',
returnvalue: 'reason',
cleanUp: true,
noEmpty: true,
appendNode: toAppend,
parseReason: true
}
], o.i18n.reasonForDeletion);
});
},
// Check whether there was a deletion request for the same title in the past
checkForFormerDR: function () {
// Don't search for "kept" when nominating talk pages
if (nsNumber % 2 === 0) {
this.talkPage = conf.wgFormattedNamespaces[nsNumber + 1] + ':' + this.pageName.replace(conf.wgCanonicalNamespace + ':', '');
this.queryAPI({
action: 'query',
prop: 'templates',
titles: this.talkPage,
tltemplates: 'Template:Kept',
tllimit: 1
}, 'formerDRTalk');
}
this.queryAPI({
action: 'query',
list: 'backlinks',
bltitle: this.pageName,
blnamespace: 4,
blfilterredir: 'nonredirects',
bllimit: 500
}, 'formerDRRequestpage');
},
formerDRTalk: function (r) {
var pgs = r.query.pages;
$.each(pgs, function (id, pg) {
if ($.isArray(pg.templates)) {
$('<li>').append($('<a>', {
text: AQD.i18n.keptAfterDR,
href: mw.util.getUrl(AQD.talkPage)
})).prependTo(AQD.prevDRNode);
} else if (pg.missing === undefined) {
$('<li>').append($('<a>', {
text: AQD.i18n.hasTalkpage,
href: mw.util.getUrl(AQD.talkPage)
})).appendTo(AQD.prevDRNode);
}
});
},
formerDRRequestpage: function (r) {
var bls = r.query.backlinks;
var _addItem = function (t, m, bl) {
$('<li>').append($('<a>', {
text: t.replace('%PAGE%', bl.title),
href: mw.util.getUrl(bl.title)
}))[m](AQD.prevDRNode);
};
$.each(bls, function (i, bl) {
if (this.requestPage === bl.title) {
_addItem(AQD.i18n.mentionedInDR, 'prependTo', bl);
} else if (/^Commons:Deletion requests\/\D/.test(bl.title)) {
_addItem(AQD.i18n.mentionedInDR, 'appendTo', bl);
} else if (/^Commons:Village pump\//.test(bl.title)) {
_addItem(AQD.i18n.mentionedInForum, 'appendTo', bl);
}
});
},
renderNode: function ($node, remotecontent, selector) {
if (selector)
selector = ' ' + selector;
$node.load(mw.util.wikiScript() + '?' + $.param({
'action': 'render',
'title': remotecontent,
'uselang': conf.wgUserLanguage
}) + (selector || ''), function () {
$node.find('a').each(function (i, el) {
var $el = $(el);
$el.attr('href', $el.attr('href').replace('MediaWiki:Anoneditwarning', conf.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.queryAPI(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');
this.addTask('replaceUsage');
this.addTask('queryRedirects');
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',
inprop: 'watched',
iiprop: 'size|sha1|url',
iiurlwidth: 365,
titles: pageName.replace(/_/g, ' ') + '|' + this.destination
};
this.queryAPI(query, 'getDupeDetailsCB');
this.showProgress('Fetching details');
},
getDupeDetailsCB: function (result) {
var pages,
id,
pg,
ii,
n;
pages = result.query.pages;
this.details = [];
for (id in pages) {
if (pages.hasOwnProperty(id)) {
pg = pages[id];
if (!pg.imageinfo) {
// Nothing we can change so prevent users reporting
this.disableReport = true;
if ($.trim(pg.title) === '{{{1}}}') {
throw new Error("Error in the duplicate-template, check your language version! (pg.imageinfo is undefined)");
} else {
throw new Error("Retrieving information about " + pg.title + " failed. It is possible that it is deleted, the last revision is corrupt or the file is a redirect. (pg.imageinfo is undefined)");
}
}
ii = pg.imageinfo[0];
n = {
title: pg.title,
size: ii.size,
width: ii.width,
height: ii.height,
thumburl: ii.thumburl,
thumbwidth: ii.thumbwidth,
thumbheight: ii.thumbheight,
descriptionurl: ii.descriptionurl,
sha1: ii.sha1,
content: pg.revisions[0]['*'],
starttimestamp: pg.starttimestamp
};
this.details.push(n);
this.edittoken = pg.edittoken;
this.deletetoken = pg.deletetoken;
if (pg.watched !== undefined) {
this.pageWasWatched = true;
}
}
}
// If order (old=0, new=1) is incorrect: Reverse
if (this.details[0].title !== pageName.replace(/_/g, ' '))
this.details.reverse();
this.nextTask();
},
/**
** Edit the current page to add the specified tag. Assumes that the page hasn't
** been tagged yet; if it is, a duplicate tag will be added.
**/
prependTemplate: function () {
var page = {
title: this.pageName,
text: this.tag,
editType: 'prependtext',
minor: false
};
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';
if (this.isMobile()) {
page.text += "\n<noinclude>[[Category:MobileUpload-related deletion requests]]</noinclude>";
}
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');
},
isMobile: function () {
var isMobile = false,
cats = conf.wgCategories;
for (var i = 0, len = cats.length; i < len; i++) {
isMobile = isMobile || /^Uploaded with Mobile/.test(cats[i]);
}
return isMobile;
},
listMobileUpload: function () {
var page = {
title: 'Commons:Deletion requests/mobile tracking',
text: '\n{{' + this.requestPage + '}}\n',
watchlist: 'nochange',
editType: 'appendtext'
};
this.showProgress("Listing mobile upload");
this.savePage(page, "Listing [[" + this.requestPage + "]]", 'nextTask');
},
listMobileUploadSpeedy: function () {
var page = {
title: 'Commons:Mobile app/deletion request tracking',
text: '\n# [[:' + this.pageName + ']]',
watchlist: 'nochange',
editType: 'appendtext'
};
this.showProgress("Listing mobile upload");
this.savePage(page, "Listing [[" + this.pageName + "]]", '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 === conf.wgUserName)
continue; // notifying yourself is pointless
var page = {
title: this.userTalkPrefix + user,
text: '\n' + this.talk_tag + ' ~~' + '~~\n',
editType: 'appendtext',
redirect: true,
minor: false
};
if (window.AjaxDeleteWatchUserTalk)
page.watchlist = 'watch';
this.savePage(page, this.talk_summary, 'uploaderNotified');
this.showProgress(this.i18n.notifyingUploader.replace('%USER%', user));
this.uploadersToNotify++;
}
}
if (!this.uploadersToNotify)
this.nextTask();
},
uploaderNotified: function () {
this.uploadersToNotify--;
if (!this.uploadersToNotify)
this.nextTask();
},
/**
** Compile a list of uploaders to notify. Users who have only reverted the file to an
** earlier version will not be notified.
** DONE: notify creator of non-file pages
**/
findCreator: function () {
var query;
if (nsNumber === 6) {
query = {
action: 'query',
prop: 'imageinfo|revisions|info',
rvprop: 'content|timestamp|user',
rvdir: 'newer',
rvlimit: 1,
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.queryAPI(query, 'findCreatorCB');
},
findCreatorCB: function (result) {
this.uploaders = {};
var pages = result.query.pages,
pg = firstItem(pages),
rv;
// The edittoken only changes between sessions
this.edittoken = pg.edittoken;
if (!pg.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.');
}
rv = pg.revisions[0];
//First handle non-file pages
if (nsNumber !== 6 || !pg.imageinfo) {
this.pageCreator = rv.user;
this.starttimestamp = pg.starttimestamp;
this.timestamp = rv.timestamp;
if (this.pageCreator !== undefined) {
this.uploaders[this.pageCreator] = true;
}
} else {
var info = pg.imageinfo;
var content = rv['*'];
var seenHashes = {};
for (var i = info.length - 1; i >= 0; i--) { // iterate in reverse order
var iii = info[i];
if (iii.sha1 && seenHashes[iii.sha1])
continue; // skip reverts
seenHashes[iii.sha1] = true;
// Now exclude bots which only reupload a new version:
if (mw.libs.commons.isSmallChangesBot(iii.user))
continue;
// outsourced to [[MediaWiki:Gadget-libCommons.js]]
var match = mw.libs.commons.getUploadBotUser(iii.user, content, iii.comment, rv.user);
if (match) {
this.uploaders[match] = true;
}
}
}
this.nextTask();
},
getMoveToken: function () {
var query = {
action: 'query',
prop: 'info|revisions|imageinfo',
rvprop: 'content|timestamp',
iiprop: 'mime|mediatype',
intoken: 'edit|move',
inprop: 'watched',
titles: pageName.replace(/_/g, ' ')
};
this.showProgress(this.i18n.preparingToEdit);
this.queryAPI(query, 'getMoveTokenCB');
},
getMoveTokenCB: function (result) {
var pages = result.query.pages,
pg = firstItem(pages);
if (!pg.revisions) {
this.disableReport = true;
throw new Error("The page you are attempting to modify or move is corrupted, was deleted or moved: Unable to retrieve history and contents.");
}
// The edittoken only changes between sessions
$.extend(this, {
edittoken: pg.edittoken,
movetoken: pg.movetoken,
pageContent: pg.revisions[0]['*'],
starttimestamp: pg.starttimestamp,
timestamp: pg.revisions[0].timestamp
});
if (pg.watched !== undefined) {
this.pageWasWatched = true;
}
var ii = pg.imageinfo;
if (ii && ii.length && ii[0].mime) {
var mime = ii[0].mime,
mediaType = ii[0].mediatype;
this.mimeFileExtension = mime
.toLowerCase()
.replace('image/jpeg', 'jpg')
.replace(/image\/(?:x\-|vnd\.)?(png|gif|xcf|djvu|svg|tiff)(?:\+xml)?/, '$1')
.replace(/application\/(ogg|pdf)/, '$1')
.replace(/video\/(webm)/, '$1')
.replace('audio\/midi', 'mid')
.replace(/audio\/(?:x\-|vnd\.)?wave?/, 'wav')
.replace(/audio\/(?:x\-)?flac/, 'flac');
if (this.mimeFileExtension.length > 5) {
this.mimeFileExtension = '';
} else if (this.mimeFileExtension === 'ogg') {
switch (mediaType) {
case 'AUDIO':
this.mimeFileExtension = 'oga';
break;
case 'VIDEO':
this.mimeFileExtension = 'ogv';
break;
}
}
}
this.nextTask();
},
doesFileExist: function () {
if (!this.destination)
return alert(this.i18n.moveDestination);
this.destination = this.cleanFileName(this.destination);
var query = {
'action': 'query',
'prop': 'info|revisions',
'titles': this.destination,
'rvprop': 'content',
'rvlimit': 2
};
// usually you would use 'redirects': 1, to detect the redirect target but
// in this case you would get the revisions for the target and not the redirect
this.showProgress(this.i18n.checkFileExists);
this.queryAPI(query, 'doesFileExistCB');
},
/**
* Return false if the page does not exist
* or it is a redirect with one revision to the source
*/
doesFileExistCB: function (result) {
if (!result || !result.query || !result.query.pages)
throw new Error('Checking file name: result.query.pages is undefined.');
var exists = true,
pg = firstItem(result.query.pages),
getRedirRegExp = function (title) {
title = title.replace(/^(File|Image):/, '').replace(/_/g, ' ');
return new RegExp('^\\s*#REDIRECT\\s*\\[\\[File\\:[' +
mw.RegExp.escape(title[0].toUpperCase()) + mw.RegExp.escape(title[0].toLowerCase()) + ']' +
mw.RegExp.escape(title.slice(1)).replace(/ /g, '[ _]') +
'\\s*\\]\\]',
'');
};
if (pg.missing !== undefined) {
exists = false;
} else if (pg.revisions.length === 1 && getRedirRegExp(pageName).test(pg.revisions[0]['*'].replace('Image:', 'File:'))) {
// There seems to be no way to find out whether a title is a redirect
// and whether the redirect only consists of one revision
exists = false;
}
if (exists) {
if (this.fileNameExistsCB)
this[this.fileNameExistsCB](pg.title.replace(/^File:/, ''));
return;
}
this.nextTask();
},
removeTemplate: function () {
var page,
newText;
this.replaceWith = (this.replaceWith || (this.templateRegExp ? '' : '$1$2'));
// Remove the template from the text. In case there is an empty line before, remove this also.
newText = this.pageContent.replace((this.templateRegExp || /(?:([^\=])\n)?\{\{(?:rename|rename media|move)\s*\|.*?\}\}(?:\n([^\=]))?/i), this.replaceWith);
if (newText === this.pageContent) {
return this.nextTask();
}
// If nothing remains, add the no-license-template (this also to prevent abuse filter blocking this edit because of page blanking)
page = {
title: (this.destination || pageName),
text: $.trim(newText) || '{{subst:nld}}',
editType: 'text',
starttimestamp: this.starttimestamp,
timestamp: this.timestamp,
watchlist: 'nochange'
};
this.showProgress(this.i18n.removingTemplate);
this.savePage(page, (this.declineReason || "Removing template; rename done"), 'nextTask');
},
replaceUsage: function () {
this.showProgress(this.i18n.replacingUsage);
if (!this.inUse)
return this.nextTask();
var reasonShort = '[[COM:Duplicate|Duplicate]]:';
if (!this.details) {
AQD.reason = AQD.reason.replace(/\[\[Commons:File[_ ]renaming[^\[\]]*\]\]:? ?/i, '');
reasonShort = '[[COM:FR|File renamed]]:';
}
mw.loader.using('ext.gadget.libGlobalReplace', function () {
if (AQD.replaceUsingCORS) {
mw.libs.globalReplace(pageName, AQD.destination, reasonShort,
AQD.reason)
.fail(function (err) {
throw new Error(err);
})
.done(function () {
AQD.nextTask();
})
.progress(function (r) {
AQD.showProgress(r);
if (window.console && console.log) {
console.log(r);
}
});
} else {
mw.libs.globalReplaceDelinker(pageName, AQD.destination, reasonShort + ' ' + AQD.reason, function () {
AQD.nextTask();
}, function (err) {
throw new Error(err);
});
}
});
},
redirectPage: function () {
var page = {
title: pageName,
text: '#REDIRECT [[' + this.destination + ']]',
editType: 'text',
watchlist: AQD.pageWasWatched ? 'watch' : 'nochange'
};
this.showProgress(this.i18n.redirectingFile);
this.savePage(page, 'Redirecting to duplicate file', 'nextTask');
},
saveDescription: function () {
var page = {
title: this.destination,
text: this.newPageText,
editType: 'text',
watchlist: AQD.pageWasWatched ? 'watch' : 'nochange'
};
this.showProgress(this.i18n.savingDescription);
this.savePage(page, 'Merging details from duplicate ([[' + pageName + ']])', 'nextTask');
},
/**
** Updates the redirects to the current page
** when moving or processing dupes immediately
** to prevent double redirects
**/
queryRedirects: function () {
this.queryAPI({
action: 'query',
generator: 'backlinks',
gblfilterredir: 'redirects',
prop: 'revisions',
rvprop: 'content',
gbltitle: pageName.replace(/_/g, ' ')
}, 'updateRedirects');
this.showProgress("What-links-here");
},
updateRedirects: function (result) {
AQD.redirectsToUpdate = 0;
if (result.query) {
this.showProgress("Updating redirects");
$.each(result.query.pages, function (id, pg) {
var rv = pg.revisions[0];
if (!rv || !rv['*'])
return;
var page = {
title: pg.title,
text: rv['*'].replace(/\#\s*REDIRECT\s*\[\[.+/, '#REDIRECT [[' + AQD.destination + ']]'),
editType: 'text',
watchlist: 'nochange'
};
AQD.savePage(page, 'Updating redirect while processing [[' + pageName.replace(/_/g, ' ') + ']]', 'updateRedirectsCB');
AQD.redirectsToUpdate++;
});
}
if (!AQD.redirectsToUpdate)
AQD.nextTask();
},
updateRedirectsCB: function () {
AQD.redirectsToUpdate--;
if (!AQD.redirectsToUpdate)
AQD.nextTask();
},
/**
** Pseudo-Modal JS windows.
**/
prompt: function (questions, title, width) {
var o = this;
var dlgButtons = {};
dlgButtons[this.i18n.submitButtonLabel] = function () {
$.each(questions, function (i, v) {
var response = $('#AjaxQuestion' + i).val();
if (v.type === 'checkbox')
response = $('#AjaxQuestion' + i)[0].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.off();
$el.fadeOut();
$input.parent().prepend(
$tarea
.data('v', $input.data('v')).data('parserResultNode', $input.data('parserResultNode'))
.val($input.val()).keyup(_parseReason).on('keyup input', _validateInput));
$tarea.slideDown();
$input.remove();
};
var _parseReason = function (event) {
var $el = $(this),
$parserResultNode = $el.data('parserResultNode');
if (!$parserResultNode)
return;
$parserResultNode.css('color', '#877');
var _gotParsedText = function (r) {
try {
$parserResultNode.html(r);
$parserResultNode.css('color', '#000');
} catch (ex) {}
};
mw.loader.using(['ext.gadget.libAPI'], function () {
mw.libs.commons.api.parse($el.val(), conf.wgUserLanguage, pageName, _gotParsedText);
});
};
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 (($el.prop('nodeName') !== 'TEXTAREA') &&
(event.which === 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: ' '
});
$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 (typeof v.byteLimit === 'number') {
mw.loader.using('jquery.lengthLimit', function () {
curQuestion.byteLimit(v.byteLimit);
});
}
curQuestion.data('v', v);
curQuestion.on('keyup input', _validateInput);
// SECURITY: prefill could contain evil jsCode. Never use it unescaped!
// Use .val() or { value: prefill } or '<input value="' + mw.html.escape() + '" ...>
curQuestion.val(v.prefill);
if (v.type === 'checkbox')
curQuestion.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>').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).specialButton('proceed');
$cancelButton = $buttons.eq(1).specialButton('cancel');
}
});
$.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', AQD.i18n.expandToTextarea);
$('<span>', {
'class': 'ajaxTextareaConverter'
}).append($i).appendTo($q).data('toConvert', curQuestion).click(_convertToTextarea);
}
});
$('#AjaxQuestion0').focus().select();
},
/**
** Open a jQuery dialog with preview-images and some options
** and information to compare the two files
**/
compareDetails: function () {
var d = this.details[0],
f = this.details[1],
$submitButton,
$inverseButton,
$swapButton,
$overlayButton;
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) + ' KiB <br>' + d.width + '×' + 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) + ' KiB <br>' + f.width + '×' + 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();
AQD.inUse = true;
setTimeout(function () {
AQD.compareDetails();
}, 10);
};
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);
});
}
};
this.showProgress();
var $AjaxDupeContainer = $('<div>', {
id: 'AjaxDupeContainer'
}).append($imgD, $imgF);
var $dialog = $('<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).specialButton('proceed');
$inverseButton = $buttons.eq(1).button({
icons: {
primary: 'ui-icon-refresh'
}
});
$swapButton = $buttons.eq(2).button({
icons: {
primary: 'ui-icon-transfer-e-w'
}
});
$overlayButton = $buttons.eq(3).button({
icons: {
primary: 'ui-icon-newwin'
}
});
$swapButton.css('float', (('left' === $swapButton.css('float')) ? 'right' : 'left'));
$overlayButton.css('float', (('left' === $overlayButton.css('float')) ? 'right' : 'left'));
}
});
mw.loader.load(['ext.gadget.libGlobalReplace', 'ext.gadget.libWikiDOM']);
},
mergeDescriptions: function () {
var newPageText = this.details[1].content;
mw.loader.using(['ext.gadget.libGlobalReplace', 'ext.gadget.libWikiDOM'], function () {
newPageText = mw.libs.wikiDOM.nowikiEscaper(newPageText).doCleanUp();
AQD.showProgress();
AQD.prompt([{
message: '',
prefill: AQD.details[0].content,
returnvalue: 'discard',
cleanUp: false,
noEmpty: false,
type: 'textarea',
enterToSubmit: false
}, {
message: '',
prefill: newPageText,
returnvalue: 'newPageText',
cleanUp: false,
noEmpty: false,
type: 'textarea',
enterToSubmit: false
}, {
message: AQD.i18n.useCORSForReplace,
prefill: !window.aqdCORSOptOut,
returnvalue: 'replaceUsingCORS',
cleanUp: false,
noEmpty: false,
type: 'checkbox'
}
], AQD.i18n.mergeDescription, 800);
AQD.destination = AQD.details[1].title;
AQD.reason = 'Exact or scaled-down duplicate: [[:' + AQD.destination + ']]';
});
},
/**
** Correct the MIME-Type; Accepts only valid file names (with extension)
** Either a file name is passed or the destination property is used
**/
correctMIME: function (fn) {
// If the current mime-type is available to the script, check it;
// MediaWiki sometimes allows uploading mismatching mimetypes but not moving
var f = fn || this.destination;
if (this.mimeFileExtension) {
f = f.replace(/\.\w{2,5}$/, '.' + this.mimeFileExtension);
}
if (!fn) {
this.destination = f;
return this.nextTask();
} else {
return f;
}
},
cleanFileName: function (uncleanName, ignoreMIME) {
// Remove Namespace
uncleanName = uncleanName.replace(/^(?:Image|File):/i, '')
// Convert extension to lower case
.replace(/\.\w{2,5}$/, function ($e) {
return $e.toLowerCase();
})
// jpeg -> jpg
.replace(/\.jpe*g$/, '.jpg')
// First cleanUp from Flinfo (FlinfoOut.php) by Flominator and Lupo
.replace(/~{3,}/g, '') // "signature"
.replace(/[\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]/, ' ') // remove NBSP and other unusual spaces
.replace(/\s+|_/g, ' ') // (multiple) whitespace
.replace(/[\x00-\x1f\x7f]/g, '')
.replace(/%([0-9A-Fa-f]{2})/g, '% $1') // URL encoding stuff
.replace(/&(([A-Za-z0-9\x80-\xff]+|#[0-9]+|#x[0-9A-Fa-f]+);)/g, '& $1') // URL-params?
.replace(/''/g, '\"')
.replace(/[:\/|#]/g, '-')
.replace(/[\]\}>]/g, ')')
.replace(/[\[\{<]/g, '(');
// Check file extension
var currentExt = pageName.toLowerCase().replace(/.*?\.(\w{2,5})$/, '$1').replace('jpeg', 'jpg');
if (!ignoreMIME)
currentExt = this.mimeFileExtension || currentExt;
var reCurrentExt = new RegExp('\\.' + mw.RegExp.escape(currentExt) + '$', 'i'),
reOldExt = new RegExp('\\.' + mw.RegExp.escape(uncleanName.replace(/.*?\.(\w{2,5})$/, '$1')) + '$', 'i');
// If new file name is without (right) extension, add the one from the old name
if (!reCurrentExt.test(uncleanName)) {
// First, try to replace the old extension
uncleanName = uncleanName.replace(reOldExt, '.' + currentExt);
if (!reCurrentExt.test(uncleanName)) {
// If this did not work, then simply append the new extention
uncleanName += '.' + currentExt;
}
}
// Capitalize the first letter and prefix the namespace
return 'File:' + ucFirst(uncleanName);
},
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>').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',
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) {
if (AQD.edittoken)
mw.user.tokens.set('editToken', AQD.edittoken);
$.extend(true, page, {
cb: function (r) {
AQD.secureCall(callback, r);
},
// text, r-result, query
errCb: function (t, r, q) {
AQD.fail(t);
},
summary: summary
});
mw.loader.using(['ext.gadget.libAPI'], function () {
mw.libs.commons.api.editPage(page);
});
},
movePage: function () {
mw.user.tokens.set('moveToken', AQD.movetoken);
// Some users don't get it: They want to move pages to themselves.
if (pageName.replace(/_/g, ' ') === AQD.destination)
return AQD.nextTask();
mw.loader.using(['ext.gadget.libAPI'], function () {
var moveArgs = {
cb: function () {
AQD.nextTask();
},
// r-result, query, text
errCb: function (t, r, q) {
AQD.fail(t);
},
from: pageName,
to: AQD.destination,
reason: AQD.reason,
movetalk: true,
// Nochange won't watch the file under the new location
// even if it was watched under the old location
watchlist: AQD.pageWasWatched ? 'watch' : 'nochange'
};
// 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 () {
mw.user.tokens.set('deleteToken', AQD.deletetoken);
mw.loader.using(['ext.gadget.libAPI'], function () {
AQD.showProgress(AQD.i18n.deletingFile);
mw.libs.commons.api.deletePage({
cb: function () {
AQD.nextTask();
},
// r-result, query, text
errCb: function (t, r, q) {
AQD.fail(t);
},
title: pageName,
reason: AQD.reason
});
});
},
purge: function () {
// No need for checking success, showing progress, nor for waiting for task to complete
this.nextTask();
$.post(this.apiURL, {
format: 'json',
action: 'purge',
forcelinkupdate: 1,
titles: pageName
});
},
/**
** Does a MediaWiki API request and passes the result to the supplied callback (method name).
** Uses POST requests for everything for simplicity.
**/
queryAPI: function (params, callback) {
mw.loader.using(['ext.gadget.libAPI'], function () {
mw.libs.commons.api.query(params, {
method: 'POST',
cache: false,
cb: function (r) {
AQD.secureCall(callback, r);
},
// r-result, query, text
errCb: function (t, r, q) {
AQD.fail(t);
}
});
});
},
/**
** Method to catch errors and report where they occurred
**/
secureCall: function (fn) {
var o = AQD;
try {
o.currentTask = arguments[0];
if ($.isFunction(fn)) {
return fn.apply(o, Array.prototype.slice.call(arguments, 1)); // arguments is not of type array so we can't just write arguments.slice
} else if ('string' === typeof fn) {
return o[fn].apply(o, Array.prototype.slice.call(arguments, 1));
} else {
mw.log.warn(fn, this.tasks);
o.fail('This is not a function!');
}
} catch (ex) {
o.fail(ex);
}
},
/**
** Simple task queue. addTask() adds a new task to the queue, nextTask() executes
** the next scheduled task. Tasks are specified as method names to call.
**/
tasks: [],
// list of pending tasks
currentTask: '',
// current task, for error reporting
addTask: function (task) {
this.tasks.push(task);
},
nextTask: function () {
this.secureCall(this.tasks.shift());
},
retryTask: function () {
this.secureCall(this.currentTask);
},
/**
** Once we're all done, reload the page.
**/
reloadPage: function () {
this.showProgress();
if (this.pageName && this.pageName.replace(/ /g, '_') !== pageName)
return;
location.href = mw.util.getUrl(this.destination || pageName);
},
/**
** Error handler. Throws an alert at the user and give him
** the possibility to retry or autoreport the error-message.
**/
fail: function (err) {
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: '//upload.wikimedia.org/wikipedia/commons/d/de/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: {{Page|1=' + (o.pageName || pageName) + '}}\n:Skin: ' + mw.user.options.get('skin') +
'\n:[{{fullurl:Special:Contributions|target={{subst:urlencode:{{subst:REVISIONUSER}}}}&offset={{subst:REVISIONTIMESTAMP}}}} Contribs] ' +
'[{{fullurl:Special:Log|user={{subst:urlencode:{{subst:REVISIONUSER}}}}&offset={{subst:REVISIONTIMESTAMP}}}} Log] ' +
'before error' + ' [[User:{{subst:REVISIONUSER}}|]] ' + '~~~~~';
$.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.util.getUrl('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();
}
});
if (mw.log.warn) mw.log.warn(err);
},
/**
** Very simple date formatter. Replaces the substrings "YYYY", "MM" and "DD" in a
** given string with the UTC year, month and day numbers respectively.
** Also replaces "MON" with the English full month name and "DAY" with the unpadded day.
**/
formatDate: function (fmt, date) {
return mw.libs.commons.formatDate(fmt, date, (mw.libs.commons.api && mw.libs.commons.api.getCurrentDate() || new Date()));
},
// Constants
// DR subpage prefix
requestPagePrefix: "Commons:Deletion requests/",
// user talk page prefix
userTalkPrefix: conf.wgFormattedNamespaces[3] + ":",
// MediaWiki API script URL
apiURL: mw.util.wikiScript('api'),
// Max number of errors that are allowed for silent retry
apiErrorThreshold: 10,
// Translatable strings
i18n: {
toolboxLinkDelete: "Nominate for deletion",
toolboxLinkDiscuss: "Nominate category for discussion",
// GUI reason prompt form
reasonForDeletion: "Why should this file be deleted?",
reasonForDiscussion: "Why does this category need discussion?",
moreInformation: "More information",
loading: "Loading...",
keptAfterDR: "This page was kept after a deletion request. Please contact the administrator who kept it before re-nominating.",
hasTalkpage: "There is a talk page. Consider reading it or adding your remarks.",
mentionedInDR: "Consider reading the deletion debate –%PAGE%– that links to this page.",
mentionedInForum: "On %PAGE%, this page is part of a discussion.",
// Labels
previewLabel: "Preview:",
submitButtonLabel: "Proceed",
cancelButtonLabel: "Cancel",
abortButtonLabel: "Abort",
reportButtonLabel: "Report automatically",
retryButtonLabel: "Retry",
ignoreButtonLabel: "Ignore and continue",
inverseButtonLabel: "Inverse. Keep this delete other",
swapImagesButtonLabel: "Swap to compare",
overlayButtonLabel: "Overlay to compare",
expandToTextarea: "Expand to textarea",
// GUI progress messages
preparingToEdit: "Preparing to edit pages... ",
creatingNomination: "Creating nomination page... ",
listingNomination: "Adding nomination page to daily list... ",
addingAnyTemplate: "Adding template to " + conf.wgCanonicalNamespace.toLowerCase() + " page... ",
notifyingUploader: "Notifying %USER%... ",
// 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
useCORSForReplace: "Try to replace usage immediately using your user account:",
deletingFile: "Deleting file",
compareDetails: "Please compare the images before merging the descriptions. The image with the bold text will be deleted.",
mergeDescription: "Please now merge the file descriptions",
redirectingFile: "Redirecting file",
savingDescription: "Saving new details",
// Errors
errorDlgTitle: "Error",
genericFailure: "An error occurred while trying to do the requested action. ",
taskFailure: {
listUploaders: "An error occurred while determining the " + (nsNumber === 6 ? " uploader(s) of this file" : "creator of this page") + ".",
loadPages: "An error occurred while preparing to nominate this " + conf.wgCanonicalNamespace.toLowerCase() + " for deletion.",
prependDeletionTemplate: "An error occurred while adding the {{delete}} template to this " + conf.wgCanonicalNamespace.toLowerCase() + ".",
createRequestSubpage: "An error occurred while creating the request subpage.",
listRequestSubpage: "An error occurred while adding the deletion request to today's log.",
notifyUploaders: "An error occurred while notifying the " + (nsNumber === 6 ? " uploader(s) of this file" : "creator of this page") + ".",
movePage: "Error while moving the page.",
deletePage: "Error deleting the page."
},
addTemplateByHand: "To nominate this " + conf.wgCanonicalNamespace.toLowerCase() + " for deletion, please edit the page to add the {{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 "
}
};
AQD.preinstall();
if (conf.wgUserLanguage !== 'en') {
$.ajax({
url: mw.util.wikiScript(),
dataType: 'script',
data: {
title: 'MediaWiki:Gadget-AjaxQuickDelete.js/' + conf.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>