Jump to content

User:Suffusion of Yellow/effp-helper-dev.js

From 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.
/* 
 * WP:EF/FP helper (development version)
 *
 * Do not import this directly. Use [[User:Suffusion of Yellow/effp-helper.js]] instead.
 */

// jshint esnext: false, esversion: 8
// <nowiki>
(function() {
	'use strict';

	let userMessages = {
		"en": {
			'effp-makeedit': "Make edit",
			'effp-makeedit-more': "Perform edit on behalf of user",
			'effp-pagedeleted': "Page deleted",
			'effp-pagedeleted-more': "Page should be undeleted first, for attribution",
			'effp-pagenevercreated': "Page does not exist",
			'effp-pagemoved': "Page moved",
			'effp-pagemoved-more': "Page moved from \"$1\" to \"$2\"",
			'effp-sigsfound': "Contains signatures",
			'effp-editmade': "Edit already made by $1",
			'effp-editmadecurrent': "Edit already made by $1 and is current",
			'effp-nolateredits': "No later edits",
			'effp-somelateredits': "$1 later {{PLURAL:$1|edit|edits}}",
			'effp-manylateredits': "At least $1 later edits",
			'effp-editsby': "Edits by $1",
			'effp-diff': "diff",
			'effp-noconflict': "No edit conflict",
			'effp-editconflict': "Edit conflict",
			'effp-whatsthis': "What's this?"
		}
	};

	let siteMessages = {
		"en": {
			'effp-spam' : "([[:en:User:Suffusion of Yellow/effp-helper|effp-helper]])",
			'effp-sig-user': "[[User:$1|$1]] ([[User talk:$1#top|talk]])",
			'effp-sig-ip': "[[Special:Contribs/$1|$1]] ([[User talk:$1#top|talk]])",
			'effp-sig-timestamp' : "$1, $2 $3 $4 (UTC)",
			'effp-sum': "Edit made on behalf of $1 because it was [[Special:AbuseLog/$2|disallowed]] by an edit filter $3",
			'effp-sum-withorigsum': "Edit made on behalf of $1 because it was [[Special:AbuseLog/$2|disallowed]] by an edit filter. Original summary was \"$3\" $4",
		}
	};

	function getLogId() {
		switch (mw.config.get('wgCanonicalSpecialPageName')) {
		case "AbuseLog":
			let match = mw.config.get('wgTitle').match(/\/([0-9]+)$/);
			return match ? match[1] : null;
		case "AbuseFilter":
			// Don't run when examining a recent change
			let examine = mw.config.get('abuseFilterExamine');
			return examine.type == "log" ? examine.id : null;
		default:
			return null;
		}

	}

	async function SHA1(str) {
		let hb = byte => byte.toString(16).padStart(2, "0");
		let buf = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(str));

		return [...(new Uint8Array(buf))].map(hb).join("");
	}

	function formatTS(unixTime) {
		return (new Date(unixTime * 1000)).toISOString().slice(0, 19) + "Z";
	}

	function formatTSForSig(unixTime) {
		let d = new Date(unixTime * 1000);

		return mw.msg('effp-sig-timestamp',
					  d.toISOString().slice(11, 16),
					  d.getUTCDate(),
					  mw.config.get('wgMonthNames')[d.getUTCMonth() + 1],
					  d.getUTCFullYear());
	}

	function formatSig(user) {
		return mw.util.isIPAddress(user) ? mw.msg('effp-sig-ip', user) : mw.msg('effp-sig-user', user);
	}

	function buildSummary(user, id, origsum) {
		if (!origsum.length)
			return mw.msg('effp-sum', formatSig(user), id, mw.msg('effp-spam'));

		let space = 500 - mw.msg('effp-sum-withorigsum', formatSig(user), id, "").length;

		if (origsum.length > space)
			origsum = origsum.slice(0, space - 3) + "...";

		return mw.msg('effp-sum-withorigsum', formatSig(user), id, origsum, mw.msg('effp-spam'));
	}

	/*
	 * Attempt to sign comments with the user's name, not our own!
	 *
	 * Can't use action=parse directly, because that will substitue OUR sig.
	 * Can't just replace the ~~~~s because that won't catch nowiki, etc.
	 *
	 * Instead use action=parse to find all the places where ~~~~ ISN'T
	 * substed, and skip those when replacing the ~~~~s.
	 * Is there a less hacky way to do this?
	 */
	async function substTildes(text, user, unixTime) {
		let munged = text.replace(/~{3,5}/g, (m, o) => ("<<" + o + ">>" + m));
		let pst = (await (new mw.Api()).post({
			action : "parse",
			text : munged,
			onlypst : 1
		})).parse.text["*"];

		let isFake = o => pst.includes("<<" + o + ">>~~~");

		let sig = formatSig(user);
		let ts = formatTSForSig(unixTime);

		text = text.replace(/~~~~~/g, (m, o) => isFake(o) ? m : ts);
		text = text.replace(/~~~~/g, (m, o) => isFake(o) ? m : sig + " " + ts);
		text = text.replace(/~~~/g, (m, o) => isFake(o) ? m : sig);

		return text;
	}

	/*
	 * Post an edit to web interface. Can't use the API, because the user
	 * might need to make fixes, or resolve an edit conflict.
	 */
	function postEdit(title, wikitext, summary,
					  revid, editTS, startTS,
					  newTab) {
		let input = (name, value) =>
			$('<input>', {type: "hidden", name, value});

		let $form = $('<form></form>', {
			style: 'display: none !important;',
			action: mw.config.get('wgScript') + "?title="
				+ mw.util.wikiUrlencode(title) + "&action=submit",
			method: "POST"
		});

		if (newTab)
			$form.attr("target", "_blank");

		$('<textarea name="wpTextbox1"></textarea>').val(wikitext).appendTo($form);

		input("wpSummary", summary).appendTo($form);

		/* Don't show preview without opt-in, might contain NSFW content */
		if (window.effpPreviewOnFirstClick)
			input("wpPreview", "1").appendTo($form);
		else
			input("wpDiff", "1").appendTo($form);

		input("wpEdittime", editTS.replace(/[^0-9]/g, "")).appendTo($form);
		input("wpStarttime", startTS.replace(/[^0-9]/g, "")).appendTo($form);

		if (revid) {
			input("editRevId", revid).appendTo($form);
			input("baseRevId", revid).appendTo($form);
			input("parentRevId", revid).appendTo($form);
		}

		if (mw.user.options.get('watchdefault'))
			input("wpWatchthis", "1").appendTo($form);

		input("wpUltimateParam", "1").appendTo($form);

		//No edit token needed for preview

		$form.appendTo('body').submit();
	}

	/*
	 * Check if the edit will conflict. This depends on an undocumented internal
	 * feature, so an error here shouldn't cause any other problems.
	 */

	function checkEditConflict(title, text, revision) {
		(new mw.Api()).postWithToken('csrf', {
			action: "stashedit",
			text: text,
			title: title,
			baserevid: revision,
			contentmodel: "wikitext",
			contentformat: "text/x-wiki"
		}).catch().then(r => {
			if (r && r.stashedit) {
				if (r.stashedit.status == "stashed")
					$('.effp-ecinfo').text(" (" + mw.msg('effp-noconflict') + ")");
				else if (r.stashedit.status == "editconflict")
					$('.effp-ecinfo').text(" (" + mw.msg('effp-editconflict') + ")");
			}
		}, () => null);
	}

	async function setupLink(vars, newer, older, text) {
		let revisions = null, page, curTitle, curPageId;
		let noLink = false, info = "", moreInfo = "";
		let	diff = null;
		let baseRev, timestamp;

		try {
			curPageId = Object.keys(newer.query.pages)[0];
			page = newer.query.pages[curPageId];
			curTitle = page.title;

			let oldRev = older.query.pages[curPageId].revisions || [];
			let newRevs = page.revisions || [];

			if (oldRev[0]) {
				baseRev = oldRev[0].revid;
				timestamp = oldRev[0].timestamp;
			} else {
				baseRev = null;
				timestamp = formatTS(vars.timestamp);
			}

			if (oldRev[0] && newRevs[0] && oldRev[0].revid === newRevs[0].revid)
				revisions = newRevs;
			else
				revisions = oldRev.concat(newRevs);
		} catch(e) {
			console.error("effp-helper: Bad response from API");
			return;
		}

		if (page.missing !== undefined) {
			curPageId = 0;
			curTitle = vars.page_prefixedtitle;
		}

		// Let's not.
		if (page.contentmodel != "wikitext" || page.ns == 8)
			noLink = true;

		if (curPageId === 0) {
			if (vars.page_id == 0) {
				info = mw.msg('effp-pagenevercreated');
			} else {
				info = mw.msg('effp-pagedeleted');
				moreInfo = mw.msg('effp-pagedeleted-more');
				noLink = true;
			}
		} else {
			let sha1 = await SHA1(text);
			let cnt = revisions.length;
			let haveAll = (page.lastrevid == revisions[cnt - 1].revid);
			let skip = (revisions[0].timestamp < formatTS(vars.timestamp));
			let trueCnt = cnt - skip;
			let editMadeBy = null, users = new Set();

			for(let i = 0; i < cnt; i++) {
				if (editMadeBy === null && revisions[i].sha1 == sha1) {
					editMadeBy = revisions[i].user;
					diff = revisions[i].revid;
				}

				if (i >= skip)
					users.add(revisions[i].user);
			}

			if (editMadeBy !== null) {
				if (haveAll && revisions[cnt - 1].sha1 == sha1)
					info = mw.msg('effp-editmadecurrent', editMadeBy);
				else
					info = mw.msg('effp-editmade', editMadeBy);
			} else if (trueCnt === 0) {
				info = mw.msg('effp-nolateredits');
			} else {
				info = haveAll ?
					mw.msg('effp-somelateredits', trueCnt) :
					mw.msg('effp-manylateredits', trueCnt);

				moreInfo = mw.msg('effp-editsby', [...users].join(", "));

				checkEditConflict(curTitle, text, baseRev);
			}
		}

		if (!vars.new_pst && vars.new_wikitext != text)
			info =  mw.msg('effp-sigsfound') + ") (" + info;

		if (vars.page_prefixedtitle !== curTitle) {
			info = mw.msg('effp-pagemoved') + ") (" + info;
			moreInfo = mw.msg('effp-pagemoved-more', vars.page_prefixedtitle, curTitle) + ". " + moreInfo;
		}

		let $info = $('<span></span>', {
			class: 'effp-info',
			text: " (" + info + ")",
			title: moreInfo
		});

		if (diff) {
			$info.append(" (",
						 $('<a></a>', {
							 href: mw.config.get('wgScript') + "?diff=" + diff,
							 text: mw.msg('effp-diff')
						 }),
						 ")");

			noLink = true;
		}

		$('#firstHeading').append($info);

		// Temporary, people won't know which script this is coming from
		$('#firstHeading').append(" ",
					 $("<a></a>", {
						 style: "font-size: 50%;",
						 href: "https://en.wikipedia.org/wiki/User:Suffusion of Yellow/effp-helper",
						 text: mw.msg("effp-whatsthis")
					 })
					);

		if (noLink)
			return;

		$info.append('<span class="effp-ecinfo"></span>');

		let link = mw.util.addPortletLink(
			'p-cactions',
			'#',
			mw.msg('effp-makeedit'),
			'ca-makeedit' ,
			mw.msg('effp-makeedit-more')
		);

		let summary = buildSummary(vars.user_name, getLogId(), vars.summary);

		$(link).on("click auxclick", (e) => {
			if (e.button <= 1) {
				e.preventDefault();

				postEdit(curTitle,
						 text,
						 summary,
						 baseRev,
						 timestamp,
						 formatTS(vars.timestamp),
						 e.button == 1 || e.CtrlKey);
			}
		});

	}

	function startup() {
		mw.messages.set(Object.assign(
			{},
			userMessages.en,
			siteMessages.en,
			userMessages[mw.config.get('wgUserLanguage')] || {},
			siteMessages[mw.config.get('wgContentLanguage')] || {}
		));

		mw.util.addCSS(".effp-info { font-size: 75%; font-style: italic; }");

		let vars = mw.config.get('wgAbuseFilterVariables');

		// Private log entry and no permissions, or action != "edit"
		if (!getLogId() || !vars ||
			vars.page_id === undefined ||
			vars.page_prefixedtitle === undefined ||
			vars.new_wikitext === undefined ||
			vars.timestamp === undefined)
			return;

		let newer = {
			action: 'query',
			prop: 'info|revisions',
			rvprop: "ids|timestamp|user|sha1",
			rvstart: formatTS(vars.timestamp),
			rvdir: "newer",
			rvlimit: 500
		};

		let older = {
			action: 'query',
			prop: 'revisions',
			rvprop: "ids|timestamp|user|sha1",
			rvstart: formatTS(vars.timestamp),
			rvdir: "older",
			rvlimit: 1
		};

		// Always use the page id if available, in case page moved
		if (vars.page_id !== 0)
			older.pageids = newer.pageids = vars.page_id;
		else
			older.titles = newer.titles = vars.page_prefixedtitle;

		let textP;

		/*
		 * Use new_pst if available, otherwise if the text looks like
		 * it contains a signature, attempt to subst it properly.
		 */
		if (vars.new_pst)
			textP = vars.new_pst;
		else if (!vars.new_wikitext.includes("~~~"))
			textP = vars.new_wikitext;
		else
			textP = substTildes(vars.new_wikitext, vars.user_name, vars.timestamp);

		let api = new mw.Api();

		Promise.all([
			api.get(newer),
			api.get(older),
			textP,
			$.ready
		]).then(r => setupLink(vars, r[0], r[1], r[2]));
	}

	if (mw.config.get('wgAbuseFilterVariables'))
		mw.loader.using(['mediawiki.interface.helpers.styles',
						 'mediawiki.api',
						 'mediawiki.util']).then(startup);
})();
// </nowiki>