MediaWiki:Gadget-DelReqHandler.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.
This user script seems to have a documentation page at MediaWiki:Gadget-DelReqHandler. |
/**
@description: Support for quick deletions and closing of deletion requests at the Commons.
@author: [[User:Lupo]], October 2007 - January 2008
@author: [[User:DieBuche]], February 2011
@author: [[User:Rillke]], April 2012; jsHint-validation, outsourcing
@author: [[User:Perhelion]], 2016; performance tuning
@revision: 21:11, 11 August 2019 (UTC)
@license: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
Choose whichever license of these you like best :-)
IE not supported
@required modules: user.options, mediawiki.util, jquery.blockUI, jquery.tipsy
* TODO: replacement for deprecated Tipsy
**/
// <nowiki>
/* global mediaWiki:false, jQuery:false, prompt:false, alert:false*/
/* jshint bitwise:true, curly:false, eqeqeq:true, forin:false, laxbreak:true */
/* eslint-env es5*/
(function ($, mw) {
'use strict';
// Guard against double inclusions // Enable the whole shebang only for sysops.
if (window.DelReqHandler || mw.config.get('wgUserGroups').indexOf('sysop') === -1) return;
// window.delReqGlobalUsage = 1;
var DRH = window.DelReqHandler = {
/* ------------------------------------------------------------------------------------------
Deletion request closing: add "[del]" and "[keep]" links to the left of the section edit
links of a deletion request. [del] and [keep] prompt for an (optional) reason, then
add "delh" and "delf" with "Deleted." or "Kept." plus the reason and signature (four tildes).
Links are added to every non-deleted image mentioned on a deletion request page. The "[del]" link
triggers deletion (auto-completed!) of the image, with a deletion summary linking to the
deletion request. If the image has a talk page, it is deleted as well. The "[keep]" link
automatically removes the "delete" template from the image page and adds the "kept" template
to the image talk page, both linking back to the deletion request.
Additional there is a quick delete link [qd] without any prompt.
------------------------------------------------------------------------------------------*/
running: [], // for race event?
titleFromHref: function (href) {
href = decodeURI(href.getAttribute('href')); // only Wikilinks
if (/^\/wiki\//.test(href)) // faster than indexOf
return RegExp.rightContext || href.substring(6);
return '';
},
spanFragC: $('<span class="navbar reqHandlerLinks2 mw-editsection-bracket"> [<a name="1" href="#">Close: Kept</a>] [<a href="#">Close: Deleted</a>]</span>')[0],
spanFragA: $('<span class="navbar reqHandlerLinks2 mw-editsection-bracket"> [<a name="1" href="#" title="Mass handle only here selected">MASS process</a>]\
<a href="#" class="new" style="display:none"><s>Del all</s></a></span>')[0],
spanFragF: $('<span class="navbar reqHandlerLinks mw-editsection-bracket"> [<a name="1" href="#">keep</a>] [<a href="#" class="new">del</a>] \
[<a href="#" onclick="DelReqHandler.quickDeleteFile(event);" title="QuickDelete" class="new">qd</a>]</span>')[0],
quickDeleteFile: function (e) {
e.preventDefault();
e = e.target;
// take the function from the adjacent del link
$(e).prev().attr('title', e.title).trigger('click');
return false;
},
nextUntilH3: function (cur) {
var matched = [cur];
cur = cur.nextElementSibling;
// https://www.mediawiki.org/wiki/Heading_HTML_changes
while (cur && !(cur.nodeName === 'H3' || (cur.nodeName === 'DIV' && cur.className === 'delh') || cur.classList.contains('mw-heading3'))) {
matched.push(cur);
cur = cur.nextElementSibling;
}
return matched;
},
parse: function () {
var $content = $('#mw-content-text');
if (!$content.length) return;
if (window.delReqGlobalUsage && $.fn.badge) {
this.spanFragF.appendChild(
$('<a>', {
'title': 'GlobalUsage',
'onclick': 'DelReqHandler._onBadge(event)',
'class': 'guGU'
}).badge('?', 'inline', true).get(0));
} else if (window.delReqGlobalUsage) {
// module not ready yet, try once again
return setTimeout(function () {
DRH.parse();
setTimeout(function () {
window.delReqGlobalUsage = 0;
}, 300);
}, 200);
}
// var parent = $content.parent();
// $content.detach(); // speedup DOM manipulation?
var h3 = $content[0].getElementsByTagName('H3'),
h = h3.length,
linkReg = /Deletion_requests\/[^\n]*?§ion=(T-)?\d$/;
/*
* Main DOM loop: use as less as possibly operations, especially omit jQuery,
* as we could scan over 10.000 links.
*/
while (h--) {
var th = h3[h],
discussion = [],
headLine, requestPage;
// https://www.mediawiki.org/wiki/Heading_HTML_changes
headLine = th.querySelector('span.mw-headline') || th;
th = th.closest('.mw-heading3') || th;
requestPage = th.querySelector('span.mw-editsection a');
// For some reason, not all h3 have a link, e.q.: [[Commons:Deletion_requests/Files_in_Category:Liquor_bottles]]
if (requestPage) requestPage = requestPage.getAttribute('href');
// It’s really an editlink to a deletion request subpage, and not a section
// edit for a daily subpage or something else
if (!requestPage || !linkReg.test(requestPage)) continue;
discussion = this.nextUntilH3(th); // .printfooter?
if (th.parentNode.className !== 'delh')
this.addLinks(requestPage, headLine, /* title*/ '', true, discussion);
var links = [],
d = 0,
i = discussion.length;
while (i--) {
var al = discussion[i].getElementsByTagName('A'),
l = al.length;
while (l--) {
var a = al[l];
if (a.className !== 'new') {
links[d] = a;
d++;
}
}
}
i = links.length;
// Probably last link is topic
if (i > 16 && !/^File:/.test(this.titleFromHref(links[i - 1]))) { // We have a non image link
this.addLinks(requestPage, links.pop(), '', false, discussion); // Add mass links
i--;
}
while (i--) {
var link = links[i],
title = this.titleFromHref(link);
if (/^File:/.test(title) && !/\//.test(title) && link.className !== 'internal') { // We have an image link
this.addLinks(requestPage, link, title, false, discussion);
}
}
}
mw.util.addCSS(
'.reqHandlerLinks a,.reqHandlerLinks2 a, input.reqHandlerBox {margin:0 .25em}\n\
input.reqHandlerBox {vertical-align:middle}');
// parent.append( $content );
},
/**
* Adds links to each headline.
*
* @param {string} requestPage The href property containing the URL.
* @param {HTMLElement} element The HTMLAnchorElement
* @param {string} imagePage If image href
* @param {boolean} closeRequest Keep/Del
* @param {NodeList} discussion The whole DR discussion section
*/
addLinks: function (requestPage, element, imagePage, closeRequest, discussion) {
// jQuery is too slow here! // with vars tiny faster
var frag = document.createDocumentFragment(),
span = (closeRequest ? this.spanFragC : (imagePage ? this.spanFragF : this.spanFragA)).cloneNode(1),
click = function (e) {
e.preventDefault();
// Use link.name for keep boolean // link.title for quick boolean
e = new DRH.Process(e.target, closeRequest, requestPage, imagePage, element, span, discussion);
DRH.running.push(e); // for race event?
},
lks = span.children;
lks[0].onclick = click;
lks[1].onclick = click;
frag.appendChild(span);
element.parentNode.insertBefore(frag, element.nextSibling);
},
Process: function (e, closeRequestBool, requestPage, imagePage, element, span, discussion) {
// Merge the page processing functions into our new process
$.extend(this, DRH.processHelpers);
this.keep = e.name;
var reason = this.keep ?
['keep', window.keepReqReason || 'no valid reason for deletion'] :
['delete', window.delReqReason || 'per nomination'],
why = 'Why did you decide to %1 this file?';
this.tasks = [];
this.requestPage = this.titleFromTitle(requestPage);
this.closeRequestBool = closeRequestBool;
this.imagePage = decodeURIComponent(imagePage);
this.summary = 'per [[' + this.requestPage + ']]';
this.domElements = [$(element), $(span), $(discussion)];
this.pageIDs = [];
// getToken
this.addTask('getPages');
if (closeRequestBool) {
this.reason = prompt(why.replace(/%1/, reason[0]), reason[1]);
// User canceled
if (!this.reason)
return;
this.pagesToGet = [this.requestPage];
this.sectionCount = this.getSectionCount(requestPage);
this.addTask('closeRequest');
} else if (this.imagePage) {
this.pagesToGet = [this.imagePage];
this.redirect = this.domElements[0].hasClass('mw-redirect');
if (this.keep) {
this.addTask('markAsKept');
this.addTask('getDate'); // runs addKeepToTalk
this.summary = 'Kept ' + this.summary;
} else {
this.addTask('deleteFile');
// this.addTask('nothing'); // ?
}
this.summary = (e.title === 'QuickDelete') ? this.summary : prompt('Summary:', this.summary);
// User canceled
if (!this.summary)
return;
} else {
this.tasks.pop(); // remove normal getPages
// Merge more functions into our new process
$.extend(this, {
setMassCheckBoxes: DRH.setMassCheckBoxes,
processAll: DRH.processAll,
processAllChunks: DRH.processAllChunks
});
return this.setMassCheckBoxes();
}
this.showProgress();
this.addTask('fakeReload');
this.nextTask();
},
setMassCheckBoxes: function () {
var checkFrag = $('<input class="reqHandlerBox" type="checkbox" checked>')[0],
$lks = this.domElements[1].children(),
$lk2 = $lks.eq(1);
// e.preventDefault();
if ($lk2.is(':hidden')) {
$lk2.after('] ');
$lk2.before(' [');
$lk2.show();
$lks.eq(0).text('Keep all');
this.domElements[1].css('background-color', '#FB9');
// Get all page links from relevant discussion section
$(this.domElements[2]).find('.reqHandlerLinks').each(function (a) {
var li = this.parentNode;
if (li.tagName === 'LI') {
a = li.firstChild;
if (a.tagName === 'A' && a.className !== 'new')
li.insertBefore(checkFrag.cloneNode(), a);
}
});
delete DRH.running[0];
} else { this.processAll(); }
// return false;
},
processAll: function () {
var allPages = [],
cSize = 50; // Max chunk size for API, bots 500
this.chunkPagesToGet = []; // list of arrays
if (this.keep)
this.processTasks = ['markAsKept']; // 'getDate' add msg on talk on mass?
else
this.processTasks = ['deleteFile'];
this.summary = prompt('Summary:', this.summary);
if (!this.summary) {
if (this.domElements[3]) this.domElements[3].unblock();
return;
}
// :checkbox
$(this.domElements[2]).find('input.reqHandlerBox:checked').each(function (a) {
a = DRH.titleFromHref(this.nextSibling);
if (a) allPages.push(a);
this.parentNode.removeChild(this);
});
// this.redirect = 1;
// Make chunks due the API limit
for (var p = 0; p < allPages.length; p += cSize)
this.chunkPagesToGet.push(allPages.slice(p, p + cSize));
this.showProgress();
this.addTask('processAllChunks');
this.nextTask();
},
processAllChunks: function () {
this.pagesToGet = this.chunkPagesToGet.pop();
if (this.pagesToGet) {
this.addTask('getPages');
this.addTask(this.processTasks[0]); // currently only one
// this.tasks.concat(this.processTasks);
this.addTask('processAllChunks');
} else { this.addTask('fakeReload'); }
this.nextTask();
},
_onBadge: function (e) {
var query = {},
$gu = $(e.target).closest('a.guGU'),
t = $gu.closest('span.reqHandlerLinks').prev('a');
t = window.DelReqHandler.titleFromHref(t[0]);
$gu[0].onclick = null;
if (!t) return;
t = decodeURIComponent(t).replace(/_/g, ' ');
query[t] = $gu;
$gu = mw.libs.GlobalUsage(5, 5);
$gu.tipsyGravity = $('body').is('.rtl') ? 'sw' : 'se';
$gu.query(query);
},
setup: function () {
var title = mw.config.get('wgTitle');
if (mw.config.get('wgNamespaceNumber') === 4 &&
/^Deletion requests\/|\/Deletion requests$/.test(title) &&
mw.config.get('wgAction') === 'view' &&
document.URL.search(/[?&]oldid=/) === -1) {
// We’re on COM:DEL or one of its daily subpages
// Don’t do anything if we're not viewing the current version of the page
var ext = ['user.options', 'mediawiki.util'];
if (window.delReqGlobalUsage)
ext.push('ext.gadget.jquery.badge');
$.when(mw.loader.using(ext), $.ready).done(function () {
DRH.parse();
setTimeout(function () { // not needed at startup
ext = ['ext.gadget.jquery.blockUI'];
if (window.delReqGlobalUsage)
ext = ext.concat(['ext.gadget.GlobalUsage', 'ext.gadget.tipsyDeprecated']);
mw.loader.load(ext);
}, 500);
});
}
}
};
DRH.processHelpers = {
titleFromTitle: function (title) {
if (title) {
title = mw.util.getParamValue('title', title);
if (title)
return title.replace(/_/g, ' ');
}
return '';
},
getSectionCount: function (title) {
if (title) {
title = mw.util.getParamValue('section', title);
if (title) {
title = parseInt(title.replace(/T-/g, ''));
if (!isNaN(title))
return title;
}
}
return '';
},
getPages: function () {
var query = {
action: 'query',
prop: 'revisions|info',
rvprop: 'content|timestamp',
// inprop: 'talkid', not needed if we only handle files
titles: this.pagesToGet.join('|'),
redirects: this.redirect,
meta: 'tokens'
};
this.doAPICall(query, 'getPagesCallback');
},
getPagesCallback: function (result) {
var pages = result.query.pages,
task = this.tasks.shift();
this.unknownResult = {};
this.imagePageResult = {};
this.requestPageResult = {};
// The edittoken only changes between logins
this.edittoken = result.query.tokens.csrftoken;
for (var id in pages) { // there should be only one, but we don't know it's ID
if (pages.hasOwnProperty(id)) {
var page = pages[id];
// FIXME better fail handling
if (!page.revisions) continue;
this.pageIDs.push(id); // For mulitple pages
var type = 'unknown';
switch (page.ns) {
case 6:
type = 'imagePage';
// if (this.redirect) this.imagePage = page.title;
break;
case 4:
type = 'requestPage';
break;
}
this.tasks.unshift(task); // Add much as pages
this[type + 'Result'][id] = {
title: page.title,
pageContent: page.revisions[0]['*'],
starttimestamp: page.starttimestamp,
timestamp: page.revisions[0].timestamp
};
}
}
this.nextTask();
},
closeRequest: function () {
// (we always load the whole page)
var text = this.requestPageResult[this.pageIDs.pop()].pageContent,
watchFor = '<noinclude>[[Category:MobileUpload-related deletion requests',
c = 0,
hRegex = /^=+.+=+.*$/gm,
sec = ']]</noinclude>';
this.decision = this.keep ? 'Kept' : 'Deleted';
text = text.replace(watchFor + sec, watchFor + '/' + this.decision.toLowerCase() + sec);
// Multiple nominations
if ((sec = this.sectionCount)) {
while ((watchFor = hRegex.exec(text)) !== null) {
c++;
if (c === sec) {
sec = watchFor.index;
break;
}
}
c = 0;
if (watchFor[0]) {
c = text.indexOf('{{delh}}\n', hRegex.lastIndex);
if (c === -1) c = text.indexOf(watchFor[0], hRegex.lastIndex);
if (c !== -1) {
// closed section at end
if (!(watchFor = text.slice(c))) c = 0;
} else { c = 0; } // last section so skip to default
}
}
if (!c) c = undefined;
if (!sec && !c) // Check anyway for a second previous nomination
sec = text.lastIndexOf('{{delf}}\n') + 9; // Additional more accurately: text.substr(sec).search(/^==+/m) but not really needed
text = (sec > 51 || c) ? // minimum text-size
text.slice(0, sec) + '{{delh}}\n' + text.slice(sec, c).trim() :
'{{delh}}\n' + text.trim(); // the whole page
text += '\n----\n';
// Add dashes on 'lesser' individual signatures
sec = (mw.user.options.get('fancysig') && mw.user.options.get('nickname').search(/^[ ']*\[\[/) !== 0) ?
'' : '--';
if (this.reason) {
this.decision += ':';
this.reason = this.reason.replace(/[.\s-]*$/, '. ');
} else { this.decision += '.'; }
text += '\'\'\'' + this.decision + '\'\'\' ' + this.reason + sec + '~~~~\n{{delf}}\n';
if (c) text += watchFor;
this.savePage({
title: this.requestPage,
text: text,
summary: this.decision + ' ' + this.reason,
editType: 'text'
}, 'nextTask');
},
markAsKept: function () {
var text = this.pageIDs.pop(); // id
this.imagePage = this.imagePageResult[text].title;
this.imageTalkPage = this.imagePage.replace(/^File:/, 'File_talk:');
text = this.removeTemplate(this.imagePageResult[text].pageContent);
if (text) {
this.savePage({
text: text,
title: this.imagePage, // pageid: id,
summary: this.summary,
editType: 'text'
}, 'nextTask');
} else { this.nextTask(); }
},
removeTemplate: function (text) {
var start = text.search(/\{\{[dD]elete/),
level = 0,
curr = start + 2,
end = 0,
opening = -1,
closing = -1;
if (start >= 0) {
while (curr < text.length && !end) {
opening = text.indexOf('{{', curr);
closing = text.indexOf('}}', curr);
if (opening >= 0 && opening < closing) {
level++;
curr = opening + 2;
} else {
if (closing < 0) {
// No closing braces found
curr = text.length;
} else {
if (level > 0)
level--;
else
end = closing + 2;
curr = closing + 2;
}
}
}
if (end) {
// Also strip whitespace after the "delete" template
end = text.substring(end).replace(/^\s+/, '');
return start ? text.substring(0, start) + end : end;
}
}
end = 'Couldn’t remove the {{delete}} template, please check the ' + this.imagePage + ' manually.';
mw.log.warn(end);
if (!this.processAllChunks) alert(end);
},
// Get start date of the DR
getDate: function (c) {
var query = {
action: 'query',
prop: 'revisions',
titles: this.requestPage,
// rvprop: 'comment|timestamp',
rvlimit: 50
};
if (c)
query.rvcontinue = c;
this.doAPICall(query, 'addKeepToTalk');
},
addKeepToTalk: function (result) {
var cont = result['continue']; // parse error on this line if not as bracket selector
if (!result.hasOwnProperty('batchcomplete') && cont && cont.rvcontinue)
cont = cont.rvcontinue;
var date = '',
pages = result.query.pages,
rev = {},
revLen;
for (var id in pages) {
// There should be only one, but we don't know it's ID
if (pages.hasOwnProperty(id) && pages[id].revisions) {
rev = pages[id].revisions;
revLen = rev.length;
for (var i = 0; i < revLen; i++) {
if (rev[i].comment === 'Starting deletion request') {
date = rev[i].timestamp;
if (date)
break;
}
}
}
}
if (!date && cont) { this.getDate(cont); } else {
if (!date) { // Fallback first edit if no appropriate comment?
date = rev[revLen - 1].timestamp;
}
// Extract year, month, and day from the timestamp.
date = date.substr(0, 4) + '-' + date.substr(5, 2) + '-' + date.substr(8, 2);
this.savePage({
title: this.imageTalkPage,
text: '{{kept|' + date + '|' + this.requestPage + '}}\n',
summary: 'Adding {{kept}}',
editType: 'prependtext'
}, 'nextTask');
}
},
reload: function () {
window.location.reload();
},
fakeReload: function () {
var dE = this.domElements;
if (dE[3]) dE[3].unblock(); // showProgress
// Remove links with keep width for following links position
dE[1].css('opacity', '0').find('a').removeAttr('href onclick title').css('cursor', 'default');
if (this.closeRequestBool) {
dE[3].toggleClass('delh delreqworking');
dE[2].eq(0).before('<i>This deletion debate is now closed. Please do not make any edits to this archive.</i>');
dE[2].eq(-1).after('<br><span class="success">Saved successfully.\
<br>This is just an approximate rendering. Reload to see the actual request.</span>');
dE[2].eq(-1).after('<b>' + this.decision + '</b> ' + this.reason + ' --' + mw.config.get('wgUserName'));
dE[2].eq(-1).after('<hr>');
} else {
if (!this.keep)
dE[0].addClass('new'); // Color link red
}
},
/**
* 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.
**/
// 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);
}
},
deleteFile: function () {
var imagePage = this.imagePageResult[this.pageIDs.pop()].title;
var edit = {
action: 'delete',
reason: this.summary,
title: imagePage,
recreate: ''
};
this.doAPICall(edit, 'nothing');
edit = {
action: 'delete',
reason: 'Talk page of deleted image',
title: imagePage.replace(/^File:/, 'File talk:'),
recreate: ''
};
this.doAPICall(edit, 'nextTask', true);
},
savePage: function (page, callback) {
var edit = {
action: 'edit',
summary: page.summary,
notminor: 1,
watchlist: window.AjaxDeleteWatchFile ? 'watch' : 'nochange',
title: page.title
};
edit[page.editType] = page.text;
this.doAPICall(edit, callback);
},
fail: function (e) {
mw.notify(e, { title: 'DelReqHandler', type: 'error' });
},
doAPICall: function (params, callback, ignoreErrors) {
var k = this;
params.format = 'json';
params.token = this.edittoken;
$.ajax({
url: mw.util.wikiScript('api'),
cache: false,
dataType: 'json',
data: params,
type: 'POST',
success: function (result, status, x) {
if (ignoreErrors) {
k[callback](result);
return;
}
if (!result)
return k.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 () {
k.doAPICall(params, callback);
}, 500);
}
if (result.error)
return k.fail('API request failed (' + result.error.code + '): ' + result.error.info);
k[callback](result);
},
error: function (x, status, error) {
return k.fail('API request returned code ' + x.status + ' ' + status + 'Error code is ' + error);
}
});
},
showProgress: function () {
var dE = this.domElements;
if (this.closeRequestBool) {
dE[2].wrapAll('<div class="delreqworking">');
dE[3] = dE[2].parent('.delreqworking');
dE[3].block({
message: '<img src="https://upload.wikimedia.org/wikipedia/commons/3/39/Spinning_wheel_throbber_blue.gif"/> Closing request…',
css: {
border: '3px solid #9C3',
fontSize: '135%'
}
});
} else {
dE[3] = dE[0].parent();
dE[3].block({
message: '<img src="https://upload.wikimedia.org/wikipedia/commons/f/f8/Ajax-loader%282%29.gif"/> Working…',
css: {
color: '#9C3',
fontWeight: 'bold',
background: 'none',
border: 'none'
}
});
}
},
nothing: function () {}
};
DRH.setup();
}(jQuery, mediaWiki));
// </nowiki> EOF