User:ShuQizhe/MarkRightsGlobal-Test Wiki.js
Appearance
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>