User:The Earwig/revdel-responder.js

Source: Wikipedia, the free encyclopedia.
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>