User:The Earwig/revdel-responder.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
This user script seems to have a documentation page at User:The Earwig/revdel-responder. |
// <nowiki>
/*
Adds buttons for admins to respond to {{Copyvio-revdel}} requests;
useful with [[User:Enterprisey/url-select-revdel]].
Full documentation is available at: [[User:The Earwig/revdel-responder]].
Install by adding:
importScript('User:The Earwig/revdel-responder.js'); // [[User:The Earwig/revdel-responder.js]]
to your [[Special:MyPage/common.js]].
*/
function revdelResponder(mboxes) {
this.mboxes = mboxes;
this.ui = [];
this.document = null;
this.templates = null;
this.etag = null;
}
revdelResponder.SCRIPT_NAME = 'User:The Earwig/revdel-responder';
revdelResponder.MBOX_SELECTOR = '.box-Copyvio-revdel';
revdelResponder.prototype.getUrl = function() {
return mw.util.getUrl(revdelResponder.SCRIPT_NAME);
};
revdelResponder.prototype.notifyDisabled = function() {
mw.notify($('<span>')
.append('You have the ')
.append($('<a>', {href: this.getUrl()}).text('revdel-responder'))
.append(' script loaded, but you are not an administrator, ' +
'so it cannot be used.'));
};
revdelResponder.prototype.parseContent = function(raw) {
const parser = new DOMParser();
this.document = parser.parseFromString(raw, 'text/html');
const mboxes = this.document.querySelectorAll(revdelResponder.MBOX_SELECTOR);
const cleanWikitext = function(wt) {
// This can be more robust
return wt.replace(/<!--.*?-->/g, '').trim();
};
this.templates = Array.from(mboxes).map(function(e) {
e = e.closest("[about]");
if (
e === null ||
e.getAttribute("typeof") !== "mw:Transclusion" ||
e.dataset.mw === undefined
) {
return null;
}
try {
const info = JSON.parse(e.dataset.mw);
const tmpl = info.parts[0].template;
Object.keys(tmpl.params).forEach(function(k) {
tmpl.params[k] = cleanWikitext(tmpl.params[k].wt);
});
return tmpl.params;
} catch (err) {
return null;
}
});
};
revdelResponder.prototype.withParsedContent = function(callback) {
const url = '/api/rest_v1/page/html/' +
mw.util.rawurlencode(mw.config.get('wgPageName')) + '/' +
mw.util.rawurlencode(mw.config.get('wgRevisionId')) + '?stash=true';
const raw = $.ajax({
url: url,
context: this,
dataType: 'html',
}).done(function(raw, status, xhr) {
this.etag = xhr.getResponseHeader('ETag');
this.parseContent(raw);
callback();
}).fail(function(xhr) {
mw.log.error('Error while parsing page content:', xhr);
mw.notify($('<span>')
.append('Sorry! ')
.append($('<a>', {href: this.getUrl()}).text('revdel-responder'))
.append(' failed to parse the page content. ' +
'Check the console for more info.'));
});
};
revdelResponder.prototype.getRevIds = function(i) {
const tmpl = this.templates[i];
if (!tmpl) {
return [];
}
const ranges = [];
let idx = 1, start = tmpl.start || tmpl.start1, end = tmpl.end || tmpl.end1;
while (start) {
ranges.push(end ? [start, end] : [start]);
idx++;
start = tmpl['start' + idx];
end = tmpl['end' + idx];
}
return ranges;
};
revdelResponder.prototype.getSourceUrl = function(i) {
const tmpl = this.templates[i];
if (!tmpl) {
return null;
}
return tmpl.url;
};
revdelResponder.prototype.getSourceUrls = function(i) {
const tmpl = this.templates[i];
if (!tmpl) {
return null;
}
const maxLen = 256;
const urls = [];
let idx = 1, url = tmpl.url, curLen = -2;
while (url && curLen + url.length + 2 <= maxLen) {
urls.push(url);
idx++;
curLen += url.length + 2;
url = tmpl['url' + idx];
}
return urls.join(', ');
};
revdelResponder.prototype.doHistory = function(i) {
const revIds = this.getRevIds(i).map(function(r) {
return r.length === 1 ? r[0] : r[0] + '..' + r[1];
}).join('|');
const url = mw.config.get('wgScript') + '?action=history&title=' +
mw.util.wikiUrlencode(mw.config.get('wgPageName')) + '&revdel_select=' +
mw.util.rawurlencode(revIds) + '&revdel_urls=' +
mw.util.rawurlencode(this.getSourceUrls(i));
window.open(url, '_blank');
};
revdelResponder.prototype.doCompare = function(i) {
const revIds = this.getRevIds(i);
const revId = (revIds.length > 0) ? revIds[0][0] : mw.config.get('wgRevisionId');
const url = 'https://copyvios.toolforge.org/?' + $.param({
lang: mw.config.get('wgContentLanguage'),
project: mw.config.get('wgSiteName').toLowerCase(),
title: mw.config.get('wgPageName'),
oldid: revId,
action: 'compare',
url: this.getSourceUrl(i) || '',
});
window.open(url, '_blank');
};
revdelResponder.prototype.removeTemplate = function(i) {
let mbox = this.document.querySelectorAll(revdelResponder.MBOX_SELECTOR)[i];
if (mbox !== undefined) {
mbox = mbox.closest("[about]");
}
if (!mbox) {
mw.notify('Error: Couldn\'t find the template in the page source?');
return;
}
// Remove by transclusion ID, otherwise we might leave the category behind
const about = mbox.getAttribute('about');
this.document.querySelectorAll('[about="' + about + '"]').forEach(function(el) {
// Need to remove a single newline if immediately following this element
const next = el.nextSibling;
if (next !== null && next.nodeType === Node.TEXT_NODE &&
next.textContent.startsWith('\n')) {
next.textContent = next.textContent.substr(1);
}
el.remove();
});
};
revdelResponder.prototype.transformWikicode = function(callback) {
const url = '/api/rest_v1/transform/html/to/wikitext/' +
mw.util.rawurlencode(mw.config.get('wgPageName')) + '/' +
mw.util.rawurlencode(mw.config.get('wgRevisionId'));
const payload = this.document.documentElement.outerHTML;
const raw = $.ajax({
url: url,
context: this,
method: 'POST',
data: {html: payload},
dataType: 'html',
headers: {'If-Match': this.etag},
}).done(callback)
.fail(function(xhr) {
mw.log.error('Error while transforming wikicode:', xhr);
mw.notify('Error: Couldn\'t transform wikicode. ' +
'Check the console for more info.');
});
};
revdelResponder.prototype.savePage = function(text, summary, callback) {
new mw.Api().postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
text: text,
summary: summary + ' ([[' + revdelResponder.SCRIPT_NAME + '|RR]])',
formatversion: '2',
baserevid: mw.config.get('wgRevisionId'),
nocreate: true,
assert: 'user',
}).done(callback)
.fail(function(code, result) {
const errcode = result.error && result.error.code;
const errinfo = result.error && result.error.info || 'Check the console for more info.';
mw.log.error('Error while saving:', result);
mw.notify('Error: Couldn\'t save the page: ' + errinfo);
});
};
revdelResponder.prototype.indicateRefresh = function(i) {
const ui = this.ui[i];
ui.buttons.forEach(function(button) {
button.$element.remove();
});
ui.elem.append($('<span>', {addClass: 'revdel-responder-status'}).text('Page saved!'));
ui.elem.append(new OO.ui.ButtonWidget({
label: 'Refresh',
icon: 'reload',
title: 'Reload the page',
}).on('click', function() {
window.location.reload();
}).$element);
};
revdelResponder.prototype.transformAndSave = function(i, summary) {
this.transformWikicode(function(text) {
this.savePage(text, summary, this.indicateRefresh.bind(this, i));
});
};
revdelResponder.prototype.doCompleteReal = function(i) {
this.removeTemplate(i);
this.transformAndSave(i, 'Copyvio revdel completed');
};
revdelResponder.prototype.doWarnComplete = function(i) {
const that = this;
const prompt = 'The requested revisions have not been deleted. Still remove the template?';
OO.ui.confirm(prompt).done(function(confirmed) {
if (confirmed) {
that.doCompleteReal(i);
} else {
that.enableInterface();
}
});
};
revdelResponder.prototype.doComplete = function(i) {
this.disableInterface();
var newest = null, oldest = null;
this.getRevIds(i).forEach(function(revs) {
revs.forEach(function(revId) {
if (newest === null || revId > newest) {
newest = revId;
}
if (oldest === null || revId < oldest) {
oldest = revId;
}
});
});
const that = this;
const api = new mw.Api();
const params = {
action: 'query',
prop: 'revisions',
pageids: mw.config.get('wgArticleId'),
rvprop: 'sha1',
rvdir: 'older',
rvlimit: 500,
formatversion: '2',
};
if (newest !== null && oldest !== null) {
params.rvstartid = newest;
params.rvendid = oldest;
}
api.get(params).done(function(result) {
const page = result && result.query && result.query.pages && result.query.pages[0];
const revs = page && page.revisions || [];
if (revs.length === 0 || revs.some(function(rev) { return rev.sha1hidden; })) {
that.doCompleteReal(i);
} else {
that.doWarnComplete(i);
}
}).fail(function(xhr) {
mw.log.error('Error while verifying redacted revisions:', xhr);
mw.notify('Error: Couldn\'t verify redacted revisions. ' +
'Check the console for more info.');
});
};
revdelResponder.prototype.doDeclineSave = function(i, reason) {
const ui = this.ui[i];
this.disableInterface();
this.removeTemplate(i);
var summary = 'Copyvio revdel declined';
if (reason) summary += ': ' + reason;
this.transformAndSave(i, summary);
};
revdelResponder.prototype.doDecline = function(i) {
const that = this;
OO.ui.prompt('Enter a decline reason:', {
size: 'medium',
textInput: {placeholder: 'Reason'},
}).done(function(reason) {
if (reason !== null) {
that.doDeclineSave(i, reason);
}
});
};
revdelResponder.prototype.doDelete = function(i) {
const reason = '[[WP:CSD#G12|G12]]: Unambiguous [[WP:CV|copyright infringement]]';
const url = mw.config.get('wgScript') + '?action=delete&title=' +
mw.util.wikiUrlencode(mw.config.get('wgPageName')) + '&wpDeleteReasonList=' +
mw.util.rawurlencode(reason) + '&wpReason=' +
mw.util.rawurlencode(this.getSourceUrls(i));
window.location.href = url;
};
revdelResponder.prototype.setupInterface = function() {
const ui = $('<div>', {addClass: 'revdel-responder-ui'})
.append($('<i>').append($('<a>', {href: this.getUrl()}).text('RR')).append(': '));
ui.append($('<span>', {
addClass: 'revdel-responder-loading revdel-responder-status',
}).text('Loading...'));
return {
elem: ui,
buttons: null,
};
};
revdelResponder.prototype.disableInterface = function() {
this.ui.forEach(function(ui) {
ui.buttons.forEach(function(button) {
button.setDisabled(true);
});
});
};
revdelResponder.prototype.enableInterface = function() {
this.ui.forEach(function(ui) {
ui.buttons.forEach(function(button) {
button.setDisabled(false);
});
});
};
revdelResponder.prototype.buildInterface = function(ui, i) {
ui.elem.find('.revdel-responder-loading').remove();
ui.buttons = [
new OO.ui.ButtonWidget({
label: 'History',
icon: 'history',
title: 'View page history, with revisions highlighted',
}).on('click', this.doHistory.bind(this, i)),
new OO.ui.ButtonWidget({
label: 'Compare',
icon: 'search',
title: 'Compare oldest revision with first source URL using Earwig\'s Copyvio Detector',
}).on('click', this.doCompare.bind(this, i)),
new OO.ui.ButtonWidget({
label: 'Complete',
flags: ['progressive'],
icon: 'check',
title: 'Remove the template after completing the revdel request',
}).on('click', this.doComplete.bind(this, i)),
new OO.ui.ButtonWidget({
label: 'Decline',
flags: ['destructive'],
icon: 'cancel',
title: 'Decline the revdel request',
}).on('click', this.doDecline.bind(this, i)),
new OO.ui.ButtonWidget({
label: 'Delete',
flags: ['destructive'],
icon: 'trash',
title: 'Delete the page',
}).on('click', this.doDelete.bind(this, i)),
];
ui.buttons.forEach(function(button) {
ui.elem.append(button.$element);
})
};
revdelResponder.prototype.render = function() {
const that = this;
mw.util.addCSS(
'.revdel-responder-ui { min-height: 32px; }' +
'.revdel-responder-ui > * { vertical-align: middle; }' +
'.revdel-responder-status { font-style: italic; margin-right: 1em; }'
);
this.mboxes.find('.mbox-text').each(function(i, e) {
const ui = that.setupInterface();
that.ui.push(ui);
$(e).append(ui.elem);
});
mw.loader.using([
'mediawiki.api',
'oojs-ui-core',
'oojs-ui.styles.icons-content',
'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-moderation',
'oojs-ui-windows',
], function() {
that.withParsedContent(function() {
that.ui.forEach(function(e, i) {
that.buildInterface(e, i);
});
});
});
};
revdelResponder.prototype.setupHistory = function(urls) {
$('#mw-history-revisionactions').append($('<input>', {
type: 'hidden',
name: 'wpRevDeleteReasonList',
value: '[[WP:RD1|RD1]]: Violations of [[Wikipedia:Copyright violations|copyright policy]]',
})).append($('<input>', {
type: 'hidden',
name: 'wpReason',
value: urls,
})).append($('<input>', {
type: 'hidden',
name: 'wpHidePrimary',
value: '1',
}));
};
revdelResponder.prototype.setupRevdel = function(hidePrimary) {
$('input[name="wpHidePrimary"]').filter(function(i, e) {
return $(e).prop('value') === hidePrimary;
}).prop('checked', true);
};
$.when(mw.loader.using('mediawiki.util'), $.ready).then(function() {
if (mw.config.get('wgAction') === 'view') {
if (mw.util.getParamValue('action') === 'revisiondelete') {
const hidePrimary = mw.util.getParamValue('wpHidePrimary');
if (hidePrimary !== null) {
new revdelResponder().setupRevdel(hidePrimary);
}
return;
}
if (mw.config.get('wgRevisionId') !== mw.config.get('wgCurRevisionId')) {
return;
}
const mboxes = $(revdelResponder.MBOX_SELECTOR);
if (mboxes.length > 0) {
const rr = new revdelResponder(mboxes);
const groups = mw.config.get('wgUserGroups');
if (groups === null || !groups.includes('sysop')) {
rr.notifyDisabled();
return;
}
rr.render();
}
} else if (mw.config.get('wgAction') === 'history') {
const urls = mw.util.getParamValue('revdel_urls');
if (urls !== null) {
new revdelResponder().setupHistory(urls);
}
}
});
// </nowiki>