Jump to content

User:ShuQizhe/MarkRightsGlobal-Test Wiki.js

From Meta, a Wikimedia project coordination wiki

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
//<syntaxhighlight lang="javascript">
//MarkRights.js
//原作者[[User:暁月凛奈/MarkRightsGlobal]]
//该版本是为 https://testwiki.wiki 设置的分支
Promise.all([
	$.ready,
	mw.loader.using(["mediawiki.api", "mediawiki.storage", "mediawiki.Title", "mediawiki.util"]),
]).then(() => {
	mw.loader.load("//meta.wikimedia.org/w/index.php?title=User:ShuQizhe/MarkRightsGlobal-Test Wiki.css&action=raw&ctype=text/css", "text/css");
	/** @type Map<string, string[]> */
	const rightsMap = new Map();
	/** @type Map<string, string[]> */
	const globalRightsMap = new Map();

	/** @type {number} */
	const apiQueryUsersLimit = window.ujsMarkRightsApiQueryUsersLimit || 50;
	/** @type {boolean} */
	const disableCache = !!window.ujsMarkRightsDisableCache;

	const processGlobalStoreKey = "markrights.processGlobal";
	/** @type {string | boolean | null} */
	let processGlobal;
	if (window.ujsMarkRightsDoNotProcessGlobal) {
		processGlobal = false;
	} else if (window.ujsMarkRightsProcessGlobal !== undefined) {
		processGlobal = !!window.ujsMarkRightsProcessGlobal;
	} else {
		processGlobal = mw.storage.get(processGlobalStoreKey);
	}
	
	const shouldFilterUnknownRights = !!window.ujsMarkRightsFilterUnknownRights;

	const defaultMaxConcurrentRequests = 10;
	const defaultRequestWaitMs = 200;
	class ConcurrentApi extends mw.Api {
		/**
		 * @param {number} maxConcurrentRequests
		 * @param {number} requestWaitMs
		 */
		constructor(maxConcurrentRequests, requestWaitMs) {
			super();
			this.inFlightRequests = 0;
			/**
			 * @type {(() => JQuery.Promise<unknown>)[]}
			 */
			this.requestQueue = [];
			this.maxConcurrentRequests = maxConcurrentRequests ?? defaultMaxConcurrentRequests;
			this.requestWaitMs = requestWaitMs ?? defaultRequestWaitMs;
		}

		/**
		 * @param {Parameters<mw.Api['ajax']>} reqParams
		 * @return {JQuery.Promise<unknown>}
		 */
		// @ts-expect-error TS2416
		ajax(...reqParams) {
			const deferred = $.Deferred();
			this.requestQueue.push(() => super.ajax(...reqParams).then(deferred.resolve, deferred.reject));
			this.processQueue();
			return deferred.promise();
		}

		processQueue() {
			if (this.requestQueue.length > 0 && this.inFlightRequests < this.maxConcurrentRequests) {
				const request = this.requestQueue.shift();
				if (!request) {
					return;
				}
				this.inFlightRequests++;
				request().always(() => {
					this.inFlightRequests--;
					setTimeout(() => this.processQueue(), this.requestWaitMs);
				});
			}
		}
	}
	const api = new ConcurrentApi(window.ujsMarkRightsMaxConcurrentRequests, window.ujsMarkRightsRequestWaitMs);

	const pathReg = new RegExp(
		"^" + mw.util.escapeRegExp(mw.config.get("wgArticlePath")).replace(/\\\$1/g /* \$1 */, "([^\\/]+)") + "$",
		"i"
	);
	/**
	 * @param {string} title
	 * @return {string|false}
	 */
	function getUsernameFromTitle(title) {
		if (title.indexOf(":") > 0 && !title.includes("/")) {
			try {
				const mTitle = new mw.Title(title);
				if (mTitle.getNamespaceId() === 2) {
					const username = mTitle.getMainText();
					if (mw.util.isIPAddress(username)) {
						// IP 沒有所在群組可列
						return false;
					}
					return mTitle.getMainText();
				}
			} catch (e) {
				// ignore
			}
		}
		return false;
	}
	/**
	 * @param {string} href
	 * @return {string|false}
	 */
	function getUsernameInternal(href) {
		if (!href) {
			return false;
		}
		const url = new URL(href, window.location.href);
		const queryTitle = url.searchParams.has("title") ? url.searchParams.get("title") : null;
		if (queryTitle !== null) {
			return getUsernameFromTitle(queryTitle);
		}
		try {
			const mPathname = pathReg.exec(decodeURIComponent(url.pathname));
			if (mPathname) {
				return getUsernameFromTitle(mPathname[1]);
			}
		} catch (error) {
			// ignore
		}
		return false;
	}
	/** @type Map<string, string|false> */
	const getUsernameCache = new Map();
	/**
	 * @param {string} href
	 */
	function getUsername(href) {
		if (getUsernameCache.has(href)) {
			return getUsernameCache.get(href);
		}
		const result = getUsernameInternal(href);
		getUsernameCache.set(href, result);
		return result;
	}

	const currentUser = mw.config.get("wgUserName");
	if (currentUser) {
		rightsMap.set(currentUser, mw.config.get("wgUserGroups") || []);
	}

	/**
	 * @param {string[]} ususers
	 * @return {Promise<boolean>}
	 */
	async function loadLocalGroupsForUsersInternal(ususers) {
		try {
			const data = await api.get({
				format: "json",
				action: "query",
				list: "users",
				usprop: "groups",
				ususers,
				formatversion: "2",
			});
			const users = data?.query?.users;
			if (users) {
				for (const user of users) {
					if (user.groups) {
						rightsMap.set(user.name, user.groups);
					}
				}
				return true;
			}
			return false;
		} catch (error) {
			console.error("markrights.js", error);
			return false;
		}
	}

	/**
	 * @param {string[]} users
	 * @return {Promise<boolean>} 是否全部都查詢成功
	 */
	async function loadLocalGroupsForUsers(users) {
		users = users.filter((user) => !rightsMap.has(user));
		if (!users.length) {
			return true;
		}

		const promises = [];
		for (let i = 0; i < Math.ceil(users.length / apiQueryUsersLimit); i++) {
			promises.push(loadLocalGroupsForUsersInternal(users.slice(i * apiQueryUsersLimit, (i + 1) * apiQueryUsersLimit)));
		}
		return Promise.all(promises).then((values) => values.every((value) => !!value));
	}

	/**
	 * @param {string} [guiuser]
	 * @return {Promise<boolean>}
	 */
	async function loadGlobalGroups(guiuser) {
		if (guiuser && globalRightsMap.has(guiuser)) {
			return true;
		}

		try {
			const data = await api.get({
				format: "json",
				action: "query",
				meta: "globaluserinfo",
				guiuser,
				guiprop: "groups",
				formatversion: "2",
			});
			const user = data?.query?.globaluserinfo;
			if (user) {
				globalRightsMap.set(user.name, user.groups || []);
				return true;
			}
			return false;
		} catch (error) {
			console.error("markrights.js", error);
			return false;
		}
	}

	/**
	 * @param {string[]} users
	 * @return {Promise<boolean>}
	 */
	async function loadGlobalGroupsForUsers(users) {
		users = users.filter((user) => !rightsMap.has(user));
		if (!users.length) {
			return true;
		}

		const promises = users.map((user) => loadGlobalGroups(user));
		return Promise.all(promises).then((values) => values.every((value) => !!value));
	}

	/**
	 * @type {(elements: HTMLElement[]) => HTMLElement[]}
	 */
	let filterUnknownRights;
	if (shouldFilterUnknownRights) {
		/** @type {Map<string, boolean>} */
		const filterCache = new Map();
		const testContainer = document.createElement("div");
		testContainer.display = "none";
		document.body.appendChild(testContainer);
		filterUnknownRights = (elements) => {
			return elements.filter((element) => {
				const groupIdKey = element.dataset.mrGroupId;
				if (!groupIdKey) {
					return true;
				}
				if (filterCache.has(groupIdKey)) {
					const cloneElement = element.cloneNode();
					testContainer.appendChild(cloneElement);
					const style = window.getComputedStyle(cloneElement, '::after');
					filterCache.set(groupIdKey, style.content !== 'none');
					cloneElement.remove();
				}
				return !!filterCache.get(groupIdKey);
			});
		};
	} else {
		filterUnknownRights = (elements) => elements; // noop
	}

	/**
	 * @param {string} groupId
	 * @param {boolean} [isGlobal]
	 * @return {HTMLElement}
	 */
	function buildMarkRightElement(groupId, isGlobal = false) {
		const sup = document.createElement("sup");
		sup.classList.add("markrights");
		if (isGlobal) {
			sup.classList.add("markrights-global");
		}
		sup.classList.add("markrights-" + groupId);
		sup.dataset.mrGroupId = `${isGlobal ? 'global' : 'local'}|${groupId}`;
		return sup;
	}

	const loadInParseOutput = !!window.ujsMarkRightsLoadInParseOutput;
	const testSelectors = [
		"a.mw-userlink:not(.mw-anonuserlink)",
		"ul.mw-logevent-loglines > li > a.userlink",
		".mw-contributions-user-tools a", // Note: .mw-contributions-user-tools a.userlink didn't exist in some wiki
	].join(",");
	let initializing = false;
	async function markUG($content) {
		if (initializing) {
			// 防止重複執行導致多次加上標記
			return;
		}

		try {
			initializing = true;
	
			if (disableCache) {
				rightsMap.clear();
				globalRightsMap.clear();
			}

			/** @type HTMLAnchorElement[] */
			// @ts-expect-error TS2322
			let userElements = $content.find(testSelectors).get();
			if (!loadInParseOutput) {
				userElements = userElements.filter(el => !el.closest('.mw-parser-output'));
			}
			/** @type Set<string> */
			const usersToQuery = new Set();
			/** @type WeakMap<HTMLAnchorElement, string> */
			const elementUserMap = new WeakMap();
			for (const element of userElements) {
				const username = getUsername(element.href);
				if (username) {
					if (mw.util.isTemporaryUser?.(username)) {
						rightsMap.set(username, ['temp']);
						globalRightsMap.set(username, []);
					} else {
						usersToQuery.add(username);
					}

					elementUserMap.set(element, username);
				}
			}

			if (usersToQuery.size) {
				let promises = [loadLocalGroupsForUsers([...usersToQuery])];
	
				if (processGlobal === null) {
					const globalAvailable = await loadGlobalGroups();
					processGlobal = !!globalAvailable;
					mw.storage.set(processGlobalStoreKey, String(!!globalAvailable));
				}
				if (processGlobal === true || processGlobal === "true") {
					promises.push(loadGlobalGroupsForUsers([...usersToQuery]));
				}
	
				await Promise.all(promises);
			}

			for (const element of userElements) {
				const username = elementUserMap.get(element);

				if (username) {
					/** @type {HTMLElement[]} */
					const appends = [];

					for (const globalGroup of (globalRightsMap.get(username) || [])) {
						appends.unshift(buildMarkRightElement(globalGroup, true));
					}

					for (const group of (rightsMap.get(username) || [])) {
						if (
							group === "*" ||
							group === "user"
						) {
							// steward: https://w.wiki/CUjJ migrateStewards.php
							// *: any user
							// user: any register user
							continue;
						}
						appends.unshift(buildMarkRightElement(group));
					}

					if (appends.length) {
						$(element).append(...filterUnknownRights(appends));
					}
				}
			}
		} finally {
			initializing = false;
		}
	}

	const specialPage = mw.config.get("wgCanonicalSpecialPageName");
	if (specialPage === "Listgrouprights") {
		$(".mw-listgrouprights-table tr")
			.get()
			.forEach((e) => {
				const $e = $(e);
				const id = $e.attr("id");
				if (!id) {
					return;
				}
				const $exec = $e.children("td:first").children(":first");
				/** @type {"after" | "before"} */
				let method = "after";
				if (!$exec.length) {
					return;
					// @ts-expect-error TS2532
				} else if ($exec.get(0).tagName.toLowerCase() === "br") {
					method = "before";
				}
				$exec[method](buildMarkRightElement(id));
			});
	} else if (specialPage === "GlobalGroupPermissions") {
		const base = mw.util.getUrl(mw.config.get("wgPageName")) + "/";

		/**
		 * @param {JQuery<HTMLAnchorElement>} $a
		 * @return {string|false}
		 */
		function findGlobalGroupId($a) {
			for (const a of $a.get()) {
				const href = $(a).attr("href");
				if (href?.startsWith(base)) {
					return href.slice(base.length);
				}
			}
			return false;
		}

		$(".mw-centralauth-groups-table tr")
			.get()
			.forEach((e) => {
				const $e = $(e);
				const id = findGlobalGroupId($e.find("a"));
				if (!id) {
					return;
				}
				const $exec = $e.children("td:first").children(":first");
				/** @type {"after" | "before"} */
				let method = "after";
				if (!$exec.length) {
					return;
					// @ts-expect-error TS2532
				} else if ($exec.get(0).tagName.toLowerCase() === "br") {
					method = "before";
				}
				$exec[method](buildMarkRightElement(id, true));
			});
	} else if (specialPage === "Contributions") {
		markUG($(".mw-contributions-user-tools").parent());
	} else {
		let hasInit = false;
		mw.hook("wikipage.content").add(($content) => {
			if ($content.attr("id") === "mw-content-text" || $content.hasClass("mw-changeslist")) {
				hasInit = true;
				markUG($content);
			}
		});
		if (!hasInit) {
			markUG($("#mw-content-text"));
		}
	}
});
//</syntaxhighlight>