User:Suffusion of Yellow/mark-reverted.js

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.
/*
 * mark-reverted.js
 *
 * Highlights diffs and permalinks by status: live, reverted, or unknown.
 * Should work on any page. Based on revision SHA1 only.
 */
// <nowiki>
(function() {
	/* globals $, mw */
	'use strict';

	const MESSAGES = {
		'mr-activate-text' : "Mark reverted",
		'mr-activate-title' : "Highlight links by status (reverted, live, or unknown)",
		'mr-link-unknown-title' : "This status of this edit could not be determined",
		'mr-link-reverted-title' : "This edit has been reverted at least once",
		'mr-link-live-title' : "This edit has identical text with the current revision",
		'mr-link-error-title' : "An error occured while determining the status of this edit (see browser console)",
		'mr-disallow-toomany': "There are $1 unique pages linked from here. Try again on a page with fewer links.",
		'mr-warn-toomany' : "There are $1 unique pages linked from here. Continue?",
	};

	const WINDOW_SIZE = 30; // Always look AT LEAST this far ahead/back

	/*
	 * It's possible for there to be 5000 unique titles linked from
	 * one user contributions page. That would be 10000 API requests!
	 * Set some sensible limits.
	 */
	const SOFT_PAGE_LIMIT = 100; // Prompt first
	const HARD_PAGE_LIMIT = 1000; // Nope
	const MAX_CONCURRENT_REQUESTS = 10;

	const CSS_PAGE = "https://en.wikipedia.org/w/index.php?title=User:Suffusion_of_Yellow/mark-reverted.css&action=raw&ctype=text/css";

	const API_USER_AGENT = "mark-reverted/0.1 (https://en.wikipedia.org/wiki/User:Suffusion_of_Yellow/mark-reverted.js)";

	var api;

	/*
	 * Silently ignore API errors, but log them to the console.
	 * We are making LOTS of requests and there's no need to panic
	 * if one goes missing. Results will be "good enough".
	 */
	function handleApiError(code, details) {
        if (typeof code != 'string')
            throw code; // Something went very wrong

        if (code == "http" && details.textStatus == "abort")
            return; // Aborted by user, not an error

		console.log((code == "http") ?
					"HTTP error: " + details.textStatus :
					"API returned error \"" + code + "\": " + details.error.info);
    }

	/*
	 * Get a batch of revisions and mark all revisions as
	 * "live" if the rev_sha1 is same as the current rev_sha1
	 * "reverted" if:
	 *   Some later revision has a rev_sha1, S
	 *   AND some earlier revision has the same rev_sha1, S
	 *   AND the revision itself does NOT have rev_sha1 S
	 * "unknown" otherwise
	 */
	async function getRevisions(state, revid, dir) {
		let page = state.page, revlist = state.revlist;
		let revmap = state.revmap, shamap = state.shamap;
		let start = revid, idx = revmap.get(revid);

		if (idx !== undefined) {
			if (dir == "newer" ||
				revlist.length - idx > WINDOW_SIZE ||
				revlist[revlist.length - 1].parentid === 0)
				return; // Fully cached

			start = revlist[revlist.length - 1].revid; // Partly cached
		}

		let r = await api.get( {
			action : 'query',
			prop : 'revisions',
			pageids : page.pageid,
			rvprop : "ids|sha1",
			rvstartid : start,
			rvdir : dir,
			rvlimit : WINDOW_SIZE
		}).catch(handleApiError);

		let revisions;
		try {
			revisions = r.query.pages[page.pageid].revisions;

			if (dir == "newer")
				revisions.reverse();
		} catch(e) {
			return;
		}

		for (let rev of revisions) {
			if (revmap.get(rev.revid))
				continue;

			rev.status = "unknown";
			revmap.set(rev.revid, revlist.length);
			revlist.push(rev);

			if (rev.sha1 !== undefined) {
				if (revlist[0].revid == page.lastrevid &&
					rev.sha1 == revlist[0].sha1)
					rev.status = "live";

				let last = shamap.get(rev.sha1);

				if (last !== undefined)
					for (let j = last; j < revlist.length - 1; j++)
						if (revlist[j].status == "unknown" &&
							revlist[j].sha1 !== rev.sha1)
							revlist[j].status = "reverted";

				shamap.set(rev.sha1, revlist.length - 1);
			}
		}
	}

	/*
	 * Mark all links for a given page.
	 * NOT concurrent; makes caching tricky
	 */
	async function markAllForPage(page, links) {
		let state = {
			page : page,
			revlist : [],
			revmap : new Map(),
			shamap : new Map()
		};

		for (let rev of page.revisions) {
			await getRevisions(state, rev.revid, "newer");
			await getRevisions(state, rev.revid, "older");
		}

		for (let rev of page.revisions) {
			let r = state.revmap.get(rev.revid);
			let result = r !== undefined ? state.revlist[r].status : "error";

			links.get(rev.revid).addClass("mr-" + result);
			links.get(rev.revid).prop("title",
									  mw.msg("mr-link-" + result + "-title"));
		}
	}

	/*
	 * Concurrently mark all links for all pages
	 */
	async function markAll(pages, links) {
		let pending = [];

        for(let [id, page] of pages) {
            let idx = pending.length < MAX_CONCURRENT_REQUESTS ?
                pending.length : await Promise.race(pending);

			pending[idx] = markAllForPage(page, links).then(() => idx);
        }
	}

	/*
	 * Find out what page is associated with each revision,
	 * and create a list of revisions for each page.
	 */
	async function getPageInfo(links) {
		const BATCH_SIZE = 50;
		let pages = new Map();
		let revids = [...links.keys()];

		for(let i = 0; i < revids.length; i += BATCH_SIZE) {
			let response = await api.get({
				action : 'query',
				prop : 'revisions|info',
				rvprop : "ids|timestamp",
				revids : revids.slice(i, i + BATCH_SIZE).join("|")
			}).catch(handleApiError);

			if (!response.query || !response.query.pages)
				continue; // All the revids were bad, perhaps?

			for (let id in response.query.pages) {
				let page = pages.get(id);
				if (!page)
					pages.set(id, response.query.pages[id]);
				else {
					page.revisions.push(...response.query.pages[id].revisions);
				}
			}
		}

		/*
		 * Sort by timestamp (newest first), then by revid (largest first),
		 * and remove duplicates
		 */
		for (let [id, page] of pages) {
			let r = page.revisions.slice();

			r.sort((a, b) => {
				if (a.timestamp == b.timestamp)
					return a.revid == b.revid ? 0 : a.revid > b.revid ? -1 : 1;
				else
					return a.timestamp > b.timestamp ? -1 : 1;
			});

			page.revisions = [];

			for(let i = 0; i < r.length; i++)
				if (i == 0 || r[i].revid != r[i - 1].revid)
					page.revisions.push(r[i]);
		}

		return pages;
	}

	/*
	 * Extract revision ID from various forms of links,
	 * (...diff=prev&oldid=xxx, Special:Diff/xxx, etc.)
	 */
	function parseLink(link) {
		let diff, oldid, match, p;

		try {
			p = new mw.Uri(link);
		} catch(e) {
			return null;
		}

		if (p.host !== mw.config.get('wgServerName'))
			return null;

		if (p.path == mw.config.get('wgScript')) {
			diff = p.query.diff;
			oldid = p.query.oldid;
		} else if ((match = p.path.match(/^\/wiki\/Special:Diff\/([^\/]+)$/i))) {
			diff = match[1];
		} else if ((match = p.path.match(/^\/wiki\/Special:PermanentLink\/([^\/]+)$/i))) {
			oldid = match[1];
		} else if ((match = p.path.match(/^\/wiki\/Special:Diff\/([^\/]+)\/([^\/]+)$/i))) {
			oldid = match[1];
			diff = match[2];
		}

		switch(diff) {
		case undefined:
		case "prev":
			return parseInt(oldid) || null;
		case "cur":
		case "next":
			return null; // Not yet implemented
		default:
			return parseInt(diff) || null;
		}

		return null;
	}

	async function activate(event) {
		event.preventDefault();

		if (api)
			api.abort();
		else {
            api = new mw.Api({
                ajax: {
                    headers: {
                        'Api-User-Agent' : API_USER_AGENT
                    }
                }
            });

			mw.loader.load(CSS_PAGE, "text/css");
		}

		let links = new Map();

		/*
		 * Find any element with an associated revid, or any link
		 * that is NOT descended from an element with a revid
		 */
		let $elems =
			$('#mw-content-text [data-mw-revid], #mw-content-text a:not([data-mw-revid] a)');

		for (let e of $elems) {
			let $elem, revid = $(e).data('mwRevid') || parseLink(e.href);

			// Not a permalink or diff
			if (!revid)
				continue;

			// No data-mw-revid in ancestor <li> on AbuseLog
			if (mw.config.get('wgCanonicalSpecialPageName') == "AbuseLog")
				$elem = $(e).closest('li');
			else
				$elem = $(e);

			$elem.removeClass("external damaging mr-reverted mr-unknown mr-live mr-error");

			if (!links.get(revid))
				links.set(revid,  $elem);
			else
				links.set(revid, links.get(revid).add($elem));
		}

		let pages = await getPageInfo(links);

		if (pages.size > HARD_PAGE_LIMIT) {
			alert(mw.msg('mr-disallow-toomany', pages.size));
			return;
		} else if (pages.size > SOFT_PAGE_LIMIT) {
			if (!confirm(mw.msg('mr-warn-toomany', pages.size)))
				return;
		}

		markAll(pages, links);
	}

	$.when(mw.loader.using( ["mediawiki.util",
							 "mediawiki.api",
							 "mediawiki.Uri"] ),
		   $.ready).then(() => {
			   mw.messages.set(MESSAGES);

			   $(mw.util.addPortletLink(
				   "p-tb",
				   "#",
				   mw.msg('mr-activate-text'),
				   't-markreverted',
				   mw.msg('mr-activate-title')
			   )).click(activate);
		   });
})();
// </nowiki>
Retrieved from "https://en.wikipedia.org/w/index.php?title=User:Suffusion_of_Yellow/mark-reverted.js&oldid=1166667510"