Jump to content

User:Suffusion of Yellow/mark-reverted.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.
/*
 * 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>
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy