User:The Earwig/revdel-responder.js

// /*	Adds buttons for admins to respond to 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);		}	} });

//