User:The Earwig/revdel-responder.js

//

/*

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($('')

.append('You have the ')

.append($('', {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($('')

.append('Sorry! ')

.append($('', {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 + ' (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($('', {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 = 'G12: Unambiguous 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 = $('

', {addClass: 'revdel-responder-ui'})

.append($('').append($('', {href: this.getUrl()}).text('RR')).append(': '));

ui.append($('', {

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($('', {

type: 'hidden',

name: 'wpRevDeleteReasonList',

value: 'RD1: Violations of copyright policy',

})).append($('', {

type: 'hidden',

name: 'wpReason',

value: urls,

})).append($('', {

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);

}

}

});

//