Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <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>
You must be logged in to post a comment.