diff --git a/Update.json b/Update.json index 51653d1f..11af94c9 100644 --- a/Update.json +++ b/Update.json @@ -3977,6 +3977,17 @@ } ], "Notes": "Change dev domain to https://dev.xmoj-script.uk/" + }, + "4.0.4": { + "UpdateDate": 1791375486861, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 1064, + "Description": "Authenticate to the backend with our own session token" + } + ], + "Notes": "脚本现在使用后端签发的会话令牌与服务器通信,不再在每次请求中发送 XMOJ 的 PHPSESSID。论坛、短消息和通知的加载更快,也不再受 XMOJ 每天自动登出的影响。
\n在后端更新前仍可正常使用。
\n新增「注销所有设备」:使所有设备上的插件登录凭证失效。它不会让其他设备退出 XMOJ 本身,因此如果怀疑账号被他人使用,请先修改 XMOJ 密码,再使用此功能。" } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index e46a9b37..5cd70f6a 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 4.0.3 +// @version 4.0.4 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen @@ -1851,6 +1851,11 @@ let clearCredential = async () => { } } }; +// Backend auth. We swap the xmoj PHPSESSID for a token of our own once, so +// requests don't hand the backend the user's cookie or make it check that +// cookie against xmoj, which is slow. The token stands for the user, not +// for the PHPSESSID, so it survives xmoj's daily logouts and our auto-login. +// It lives in GM storage, which the page itself can't read. let RequestAPI = (Action, Data, CallBack, ErrorCallBack) => { let completed = false; let Fail = (message) => { @@ -1859,85 +1864,314 @@ let RequestAPI = (Action, Data, CallBack, ErrorCallBack) => { if (ErrorCallBack) ErrorCallBack(message); else console.error("[XMOJ-Script] Request " + Action + ": " + message); }; - try { - let Session = ""; - let Temp = document.cookie.split(";"); - for (let i = 0; i < Temp.length; i++) { - if (Temp[i].includes("PHPSESSID")) { - Session = Temp[i].split("=")[1]; - } + let Timeout = ErrorCallBack ? 15000 : 30000; + let InternalError = (e) => { + Fail("请求失败,请重试"); + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); } - if (Session === "") { //The cookie is httpOnly - GM.cookie.set({ - name: 'PHPSESSID', - value: (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)).substring(0, 28), - path: "/" - }) - .then(() => { - console.log('Reset PHPSESSID successfully.'); - location.reload(); //Refresh the page to auth with the new PHPSESSID - }) - .catch((error) => { - console.error(error); - }); + }; + let Send = (Authentication, Retried) => { + try { + PostAPI(Action, Authentication, Data, Timeout, !!ErrorCallBack, (result) => OnResult(result, Authentication, Retried), Fail); + } catch (e) { + InternalError(e); } - let PostData = { - "Authentication": { - "SessionID": Session, "Username": CurrentUsername, - }, "Data": Data, "Version": GM_info.script.version, "DebugMode": UtilityEnabled("DebugMode") - }; - let DataString = JSON.stringify(PostData); - if (UtilityEnabled("DebugMode")) { - console.log("Sent for", Action + ":", DataString); - } - GM_xmlhttpRequest({ - method: "POST", - url: (UtilityEnabled("SuperDebug") ? "http://127.0.0.1:8787/" : "https://api.xmoj-script.uk/") + Action, - headers: { - "Content-Type": "application/json", - "Cache-Control": "no-cache", - "XMOJ-UserID": CurrentUsername, - "XMOJ-Script-Version": GM_info.script.version, - "DebugMode": UtilityEnabled("DebugMode") - }, - data: DataString, - // Preserve the stashed general request limit and faster badge retries. - timeout: ErrorCallBack ? 15000 : 30000, - onerror: () => Fail("网络错误,请重试"), - ontimeout: () => Fail("请求超时,请重试"), - onabort: () => Fail("请求已取消,请重试"), - onload: (Response) => { - if (completed) return; - if (UtilityEnabled("DebugMode")) { - console.log("Received for", Action + ":", Response.responseText); - } - if (ErrorCallBack && (Response.status < 200 || Response.status >= 300)) { - Fail("请求失败(HTTP " + Response.status + "),请重试"); + }; + let OnResult = (result, Authentication, Retried) => { + if (completed) return; + // A body like `null` is the caller's to report, so only look inside + // real results. + // A backend rolled back to before tokens rejects the field outright. + // Use the old auth for the rest of this page load; the token is kept + // for when the new backend is back. + if (result && !result.Success && result.Message === "参数Token未知" && Authentication.Token && !Retried.Legacy) { + BackendLegacyAuth = true; + Send(LegacyAuthentication(), {Token: true, Session: true, Legacy: true}); + return; + } + if (result && !result.Success && result.Data) { + // The backend forgot the token (expired, or logged out elsewhere). + if (result.Data.TokenInvalid && !Retried.Token) { + StoreBackendToken(""); + GetBackendToken().then((Token) => Send(Token !== "" ? {"Token": Token} : LegacyAuthentication(), {...Retried, Token: true}), Fail); + return; + } + // This request has to read xmoj as us, so it needs the cookie + // after all. Only these requests ever get it. + if (result.Data.SessionRequired && !Retried.Session) { + let Session = GetPHPSESSIDOrReset(); + if (Session === "") { + Fail("无法读取登录状态,请刷新页面"); return; } - let result; - try { - result = JSON.parse(Response.responseText); - } catch (Error) { - console.log(Response.responseText); - Fail("服务器响应异常,请重试"); + Send({"Token": Authentication.Token, "SessionID": Session}, {...Retried, Session: true}); + return; + } + } + completed = true; + try { + if (CallBack) CallBack(result); + } catch (Error) { + console.error(Error); + } + }; + try { + if (UnauthenticatedActions.includes(Action)) { + Send({}, {Token: true, Session: true}); + return; + } + // Usually we already have a token; send right away rather than + // after a promise tick. + let Token = BackendLegacyAuth ? "" : GetStoredBackendToken(); + if (Token !== "") { + Send({"Token": Token}, {}); + return; + } + GetBackendToken().then((Token) => Send(Token !== "" ? {"Token": Token} : LegacyAuthentication(), {}), Fail); + } catch (e) { + InternalError(e); + } +}; +// These are answered before the backend looks at Authentication, and are +// needed on pages where the user isn't logged in. +const UnauthenticatedActions = ["GetNotice", "GetAddOnScript"]; +let BackendTokenRequest = null; +// Set when the backend predates tokens (no Login endpoint): this page load +// then authenticates the old way, as that backend expects. +let BackendLegacyAuth = false; +// A failed exchange is remembered briefly, so a page full of requests +// doesn't send the PHPSESSID once per request while xmoj is struggling. +let BackendTokenFailure = null; +const BackendTokenRetryDelay = 30000; +// Set once logout begins. From then on no new exchange starts, and a token +// that still arrives (from a Login or the socket already under way) is +// revoked instead of stored. +let BackendLoggingOut = false; +// Debug logs get copied into chats and issues; keep credentials out of them. +// On by default; developers who need the raw values can turn RedactLogs off. +let RedactCredentials = (Text) => !UtilityEnabled("RedactLogs") ? String(Text) : String(Text).replace(/("(?:Token|SessionID|token)":")[^"]*"/g, '$1"'); +let AcceptIssuedToken = (Token) => { + if (BackendLoggingOut) { + PostAPI("Logout", {"Token": Token}, {}, 2000, false, () => {}, () => {}); + return false; + } + StoreBackendToken(Token); + return true; +}; +let GetPHPSESSID = () => { + let Session = ""; + let Temp = document.cookie.split(";"); + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].includes("PHPSESSID")) { + Session = Temp[i].split("=")[1]; + break; + } + } + return Session; +}; +let GetPHPSESSIDOrReset = () => { + let Session = GetPHPSESSID(); + if (Session === "") { //The cookie is httpOnly + GM.cookie.set({ + name: 'PHPSESSID', + value: (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)).substring(0, 28), + path: "/" + }) + .then(() => { + console.log('Reset PHPSESSID successfully.'); + location.reload(); //Refresh the page to auth with the new PHPSESSID + }) + .catch((error) => { + console.error(error); + }); + } + return Session; +}; +let GetStoredBackendToken = () => { + let Stored = GM_getValue("BackendToken", null); + if (Stored && Stored.Token && CurrentUsername && Stored.Username === CurrentUsername) { + return Stored.Token; + } + return ""; +}; +let StoreBackendToken = (Token) => { + GM_setValue("BackendToken", Token ? {"Token": Token, "Username": CurrentUsername} : null); +}; +// The one place requests reach the backend. OnResult gets the parsed JSON, +// OnFail a message for the user. +let PostAPI = (Action, Authentication, Data, Timeout, StrictStatus, OnResult, OnFail) => { + let DataString = JSON.stringify({ + "Authentication": Authentication, "Data": Data, "Version": GM_info.script.version, "DebugMode": UtilityEnabled("DebugMode") + }); + if (UtilityEnabled("DebugMode")) { + console.log("Sent for", Action + ":", RedactCredentials(DataString)); + } + GM_xmlhttpRequest({ + method: "POST", + url: (UtilityEnabled("SuperDebug") ? "http://127.0.0.1:8787/" : "https://api.xmoj-script.uk/") + Action, + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "XMOJ-UserID": CurrentUsername, + "XMOJ-Script-Version": GM_info.script.version, + "DebugMode": UtilityEnabled("DebugMode") + }, + data: DataString, + timeout: Timeout, + onerror: () => OnFail("网络错误,请重试"), + ontimeout: () => OnFail("请求超时,请重试"), + onabort: () => OnFail("请求已取消,请重试"), + onload: (Response) => { + if (UtilityEnabled("DebugMode")) { + console.log("Received for", Action + ":", RedactCredentials(Response.responseText)); + } + if (StrictStatus && (Response.status < 200 || Response.status >= 300)) { + OnFail("请求失败(HTTP " + Response.status + "),请重试"); + return; + } + let result; + try { + result = JSON.parse(Response.responseText); + } catch (Error) { + console.log(Response.responseText); + OnFail("服务器响应异常,请重试"); + return; + } + OnResult(result); + } + }); +}; +let ExchangeBackendToken = () => { + return new Promise((resolve, reject) => { + if (!CurrentUsername) { + reject("用户未登录"); + return; + } + let Session = GetPHPSESSIDOrReset(); + if (Session === "") { + // The cookie is httpOnly; GetPHPSESSIDOrReset is already fixing + // that with a reload. + reject("无法读取登录状态,请刷新页面"); + return; + } + PostAPI("Login", {"SessionID": Session, "Username": CurrentUsername}, {}, 15000, true, (Response) => { + if (Response && Response.Success && Response.Data && typeof Response.Data.Token === "string" && Response.Data.Token !== "") { + if (!AcceptIssuedToken(Response.Data.Token)) { + reject("正在退出登录"); return; } - completed = true; - try { - if (CallBack) CallBack(result); - } catch (Error) { - console.error(Error); - } + resolve(Response.Data.Token); + } else if (Response && Response.Message === "访问的页面不存在") { + BackendLegacyAuth = true; + resolve(""); + } else { + reject(Response && Response.Message ? Response.Message : "服务器响应异常,请重试"); } + }, reject); + }).catch((Message) => { + BackendTokenFailure = {"Time": new Date().getTime(), "Message": Message}; + throw Message; + }); +}; +// How to authenticate when we have no token: only against a backend that +// has never heard of tokens. +let LegacyAuthentication = () => { + return {"SessionID": GetPHPSESSIDOrReset(), "Username": CurrentUsername}; +}; +// Resolves to our token, exchanging the PHPSESSID for one if we have none. +// Everyone waiting at once shares one exchange, including one the +// notification socket is doing; if that one comes back empty, we fall +// back to Login. +// Resolves to "" when the backend predates tokens; callers then use +// LegacyAuthentication. +let GetBackendToken = () => { + if (BackendLegacyAuth) { + return Promise.resolve(""); + } + let Token = GetStoredBackendToken(); + if (Token !== "") { + return Promise.resolve(Token); + } + if (BackendLoggingOut) { + return Promise.reject("正在退出登录"); + } + if (BackendTokenFailure && new Date().getTime() - BackendTokenFailure.Time < BackendTokenRetryDelay) { + return Promise.reject(BackendTokenFailure.Message); + } + if (BackendTokenRequest) { + return BackendTokenRequest.then((Token) => Token !== "" ? Token : GetBackendToken()); + } + BackendTokenRequest = ExchangeBackendToken().finally(() => { + BackendTokenRequest = null; + }); + return BackendTokenRequest; +}; +// Revokes every token this user holds, on every device. Devices still logged +// in to xmoj quietly exchange their PHPSESSID for a new one. +let RevokeAllBackendTokens = (CallBack) => { + RequestAPI("LogoutAll", {}, (Response) => { + if (Response && Response.Success) { + StoreBackendToken(""); + } + CallBack(Response || {"Success": false, "Message": "服务器响应异常,请重试"}); + }, (Message) => CallBack({"Success": false, "Message": Message})); +}; +// Forgets the token here and on the backend. Never trades the PHPSESSID for +// a token just to throw it away. +let RevokeBackendToken = (CallBack) => { + BackendLoggingOut = true; + let Done = false; + let Finish = () => { + if (Done) return; + Done = true; + CallBack(); + }; + // An exchange under way would store its token after we cleared storage. + // Give it a moment to land so it is revoked with the rest; anything later + // is caught by AcceptIssuedToken. + let Pending = BackendTokenRequest ? BackendTokenRequest.catch(() => {}) : Promise.resolve(); + Promise.race([Pending, new Promise((resolve) => setTimeout(resolve, 3000))]).then(() => { + let Token = GetStoredBackendToken(); + StoreBackendToken(""); + if (Token === "") { + Finish(); + return; + } + PostAPI("Logout", {"Token": Token}, {}, 2000, false, Finish, Finish); + }); +}; +// The site's own logout, the classic logout.php link or the /web app's +// POST /api/logout, knows nothing of our token, which outlives xmoj +// sessions on purpose. Revoke it on the way out. +let InterceptNativeLogout = () => { + document.addEventListener("click", (Event) => { + let Link = Event.target && Event.target.closest ? Event.target.closest('a[href*="logout.php"]') : null; + if (!Link || GetStoredBackendToken() === "") return; + Event.preventDefault(); + Event.stopImmediatePropagation(); + RevokeBackendToken(() => { + location.href = Link.href; }); - } catch (e) { - Fail("请求失败,请重试"); - console.error(e); - if (UtilityEnabled("DebugMode")) { - SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + }, true); + let PageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; + let NativeFetch = PageWindow.fetch; + if (typeof NativeFetch !== "function") return; + PageWindow.fetch = function (Resource, Options) { + let Target = typeof Resource === "string" || Resource instanceof URL ? String(Resource) : (Resource && Resource.url) || ""; + let Method = String((Options && Options.method) || (Resource && Resource.method) || "GET").toUpperCase(); + let Arguments = arguments; + let IsLogout = false; + try { + IsLogout = Method === "POST" && new URL(Target, location.href).pathname === "/api/logout"; + } catch (e) { } - } + if (IsLogout && GetStoredBackendToken() !== "") { + return new Promise((resolve) => RevokeBackendToken(resolve)).then(() => NativeFetch.apply(this, Arguments)); + } + return NativeFetch.apply(this, Arguments); + }; }; let SyncSettingsToCloud = (CallBack) => { if (!CurrentUsername) { @@ -1999,17 +2233,7 @@ let NotificationSocketReconnectDelay = 1000; let NotificationSocketPingInterval = null; let NotificationSocketReconnectTimer = null; -function GetPHPSESSID() { - let Session = ""; - let Temp = document.cookie.split(";"); - for (let i = 0; i < Temp.length; i++) { - if (Temp[i].includes("PHPSESSID")) { - Session = Temp[i].split("=")[1]; - break; - } - } - return Session; -} +let NotificationSocketTokenFailures = 0; function ConnectNotificationSocket() { try { @@ -2019,23 +2243,46 @@ function ConnectNotificationSocket() { NotificationSocketReconnectTimer = null; } - let Session = GetPHPSESSID(); - if (Session === "") { - if (UtilityEnabled("DebugMode")) { - console.log("WebSocket: PHPSESSID not available, skipping connection"); - } + let Token = BackendLegacyAuth ? "" : GetStoredBackendToken(); + if (Token === "" && BackendTokenRequest) { + // Someone is already getting us a token; connect with it instead of + // minting a second one. + BackendTokenRequest.then(() => ConnectNotificationSocket(), () => ConnectNotificationSocket()); return; } - - let wsUrl = (UtilityEnabled("SuperDebug") ? "ws://127.0.0.1:8787" : "wss://api.xmoj-script.uk") + "/ws/notifications?SessionID=" + Session; + let wsUrl = (UtilityEnabled("SuperDebug") ? "ws://127.0.0.1:8787" : "wss://api.xmoj-script.uk") + "/ws/notifications?"; + let ResolveMintedToken = null; + if (Token !== "") { + wsUrl += "Token=" + Token; + } else { + let Session = GetPHPSESSID(); + if (Session === "") { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: PHPSESSID not available, skipping connection"); + } + return; + } + // No token yet: the socket gets one for everyone. Requests made + // meanwhile wait for it rather than running their own Login. + wsUrl += "SessionID=" + Session + "&IssueToken=1"; + BackendTokenRequest = new Promise((resolve) => { + ResolveMintedToken = resolve; + setTimeout(() => resolve(""), 15000); + }).finally(() => { + BackendTokenRequest = null; + }); + } if (UtilityEnabled("DebugMode")) { - console.log("WebSocket: Connecting to", wsUrl); + console.log("WebSocket: Connecting", Token !== "" ? "with token" : "with PHPSESSID"); } + let Opened = false; NotificationSocket = new WebSocket(wsUrl); NotificationSocket.onopen = () => { + Opened = true; + NotificationSocketTokenFailures = 0; if (UtilityEnabled("DebugMode")) { console.log("WebSocket: Connected successfully"); } @@ -2059,7 +2306,12 @@ function ConnectNotificationSocket() { }; NotificationSocket.onmessage = (event) => { - HandleNotificationMessage(event); + let notification = HandleNotificationMessage(event); + // An older backend sends no token; resolve anyway so nobody waits + // out the timeout before falling back to Login. + if (ResolveMintedToken && notification && notification.type === 'connected') { + ResolveMintedToken(notification.token || ""); + } }; NotificationSocket.onerror = (error) => { @@ -2075,6 +2327,24 @@ function ConnectNotificationSocket() { if (NotificationSocketPingInterval) { clearInterval(NotificationSocketPingInterval); } + // 4001: the user logged out everywhere and this token is gone. + // Reconnecting proves the xmoj session again for a new one. + if (event.code === 4001 && Token !== "" && GetStoredBackendToken() === Token) { + StoreBackendToken(""); + } + // Closed before handing over a token; whoever is waiting falls + // back to Login. A no-op if the token already arrived. + if (ResolveMintedToken) { + ResolveMintedToken(""); + } + // A browser can't tell a refused token from being offline, so after + // a few failed handshakes ask the backend over HTTP. Only a + // TokenInvalid answer drops the token (RequestAPI does that and + // gets a new one); a network error leaves it alone. + if (Token !== "" && !Opened && ++NotificationSocketTokenFailures >= 3) { + NotificationSocketTokenFailures = 0; + RequestAPI("SendData", {}); + } ReconnectNotificationSocket(); }; } catch (e) { @@ -2101,13 +2371,16 @@ function HandleNotificationMessage(event) { const notification = JSON.parse(event.data); if (UtilityEnabled("DebugMode")) { - console.log("WebSocket: Received message", notification); + console.log("WebSocket: Received message", RedactCredentials(event.data)); } if (notification.type === 'connected') { if (UtilityEnabled("DebugMode")) { console.log("WebSocket: Server confirmed connection at timestamp", notification.timestamp); } + if (notification.token) { + AcceptIssuedToken(notification.token); + } } else if (notification.type === 'bbs_mention') { if (UtilityEnabled("BBSPopup")) { CreateAndShowBBSMentionToast(notification.data); @@ -2121,8 +2394,10 @@ function HandleNotificationMessage(event) { console.log("WebSocket: Received pong"); } } + return notification; } catch (e) { console.error("WebSocket: Failed to handle message", e); + return null; } } @@ -2840,6 +3115,24 @@ function InitializeAccountBadgeEditor() { }); } +function LogOut() { + clearCredential(); + RevokeBackendToken(() => { + location.href = "https://www.xmoj.tech/logout.php"; + }); + GM.cookie.set({ + name: 'PHPSESSID', + value: (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)).substring(0, 28), + path: "/" + }) + .then(() => { + console.log('Reset PHPSESSID successfully.'); + }) + .catch((error) => { + console.error(error); + }); //We can no longer rely of the server to set the cookie for us +} + // The ResetType user menu. Shared by the legacy navbar and the /web app so both // menus offer the same entries and animate the same way. function CreateUserMenuItems() { @@ -2849,20 +3142,25 @@ function CreateUserMenuItems() { ["短消息", () => { location.href = "https://www.xmoj.tech/mail.php"; }], ["插件设置", () => { location.href = "https://www.xmoj.tech/index.php?ByUserScript=1"; }], ["插件更新日志", () => { location.href = "https://www.xmoj.tech/modify_user_info.php?ByUserScript=1"; }], - ["注销", () => { - clearCredential(); - GM.cookie.set({ - name: 'PHPSESSID', - value: (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)).substring(0, 28), - path: "/" - }) - .then(() => { - console.log('Reset PHPSESSID successfully.'); - }) - .catch((error) => { - console.error(error); - }); //We can no longer rely of the server to set the cookie for us - location.href = "https://www.xmoj.tech/logout.php"; + ["注销", LogOut], + ["注销所有设备", () => { + // Spell out the limits: this ends plugin sessions, not XMOJ ones, + // so anyone who still has the password can simply log back in. + if (!confirm("如果你怀疑账号被他人使用,请先修改 XMOJ 密码(菜单中的「修改帐号」),再使用此功能。否则对方仍可用你的密码重新登录,并重新获得插件的访问权限。\n\n" + + "注销所有设备会:\n" + + "· 使你在所有设备上的插件登录凭证失效\n" + + "· 断开所有设备的插件实时通知\n" + + "· 退出当前设备的登录\n\n" + + "注销所有设备不会:\n" + + "· 让其他设备退出 XMOJ 本身。仍登录着 XMOJ 的设备会自动重新获得插件凭证,可以继续使用\n\n" + + "确定要注销所有设备吗?")) return; + RevokeAllBackendTokens((Response) => { + if (!Response.Success) { + SmartAlert("注销所有设备失败:" + Response.Message); + return; + } + LogOut(); + }); }] ]; return Entries.map(([Text, Action]) => { @@ -3808,6 +4106,7 @@ class NavbarStyler { // which executes userscripts as classic scripts (not ES modules). (async () => { if (GetContestWebRedirect()) return; +InterceptNativeLogout(); if (GetAccountSettingsRedirect()) return; if (document.readyState === "loading") { await new Promise(r => document.addEventListener("DOMContentLoaded", r, { once: true })); @@ -4469,6 +4768,8 @@ async function main() { "ID": "CloudSync", "Type": "A", "Name": "将设置同步至云端(跨设备同步)" }, { "ID": "DebugMode", "Type": "A", "Name": "调试模式(仅供开发者使用)" + }, { + "ID": "RedactLogs", "Type": "A", "Name": "调试日志中隐藏登录凭证(分享日志前请保持开启)" }, { "ID": "SuperDebug", "Type": "A", "Name": "本地调试模式(仅供开发者使用) (未经授权的擅自开启将导致大部分功能不可用!)" }])); diff --git a/package.json b/package.json index 81f9715e..31f6ef39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "4.0.3", + "version": "4.0.4", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { diff --git a/tests/account-settings.test.cjs b/tests/account-settings.test.cjs index e32c1ca9..9596727d 100644 --- a/tests/account-settings.test.cjs +++ b/tests/account-settings.test.cjs @@ -48,6 +48,8 @@ test('account-page migration browser regressions', {timeout: 60000}, async t => window.GetUserInfo = async () => ({EmailHash: 'hash'}); window.apiCalls = []; document.cookie = 'PHPSESSID=fixture; path=/'; + // A logged-in user already holds a backend token. + window.GM_getValue = () => ({Token: 'f'.repeat(64), Username: window.CurrentUsername}); window.GM_info = {script: {version: 'test'}}; window.badgeFailureMode = options.badgeFailureMode; window.badgeLoadFailureMode = options.badgeLoadFailureMode; @@ -371,6 +373,7 @@ test('ordinary API requests retain the stashed timeout and log failures once', ( const context = vm.createContext({ document: {cookie: 'PHPSESSID=fixture'}, CurrentUsername: 'Viewer', GM_info: {script: {version: 'test'}}, UtilityEnabled: () => false, + GM_getValue: () => ({Token: 'f'.repeat(64), Username: 'Viewer'}), GM_xmlhttpRequest: options => { request = options; }, console: {error: message => errors.push(message)}, saveResponse: response => responses.push(response) diff --git a/tests/profile-page.test.cjs b/tests/profile-page.test.cjs index 6949d22d..ddb3a678 100644 --- a/tests/profile-page.test.cjs +++ b/tests/profile-page.test.cjs @@ -27,6 +27,8 @@ test('profile page browser regressions', {timeout: 60000}, async t => { window.CurrentUsername = 'Viewer'; window.GM_info = {script: {version: 'test'}}; document.cookie = 'PHPSESSID=fixture; path=/'; + // A logged-in user already holds a backend token. + window.GM_getValue = () => ({Token: 'f'.repeat(64), Username: 'Viewer'}); window.UtilityEnabled = name => ['Rating', 'ReplaceLinks', 'ReplaceXM'].includes(name) || (name === 'RemoveUseless' && options.removeActivity !== false); window.CryptoJS = {MD5: email => { window.avatarEmail = email; return {toString: () => '123456789012345678901234567890abcf'}; }}; window.GetUserInfo = window.GetUserBadge = () => { throw new Error('Profile must not wait for these APIs'); };