diff --git a/package.json b/package.json index de80b562b..3ac49f3c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scriptcat", - "version": "1.2.5", + "version": "1.2.6", "description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!", "author": "CodFrm", "license": "GPLv3", diff --git a/src/app/service/content/global.ts b/src/app/service/content/global.ts new file mode 100644 index 000000000..e932f8c3c --- /dev/null +++ b/src/app/service/content/global.ts @@ -0,0 +1,36 @@ +// 避免在全局页面环境中,内置处理函数被篡改或重写 +const unsupportedAPI = () => { + throw "unsupportedAPI"; +}; + +export const Native = { + structuredClone: typeof structuredClone === "function" ? structuredClone : unsupportedAPI, + jsonStringify: JSON.stringify.bind(JSON), + jsonParse: JSON.parse.bind(JSON), +} as const; + +export const customClone = (o: any) => { + // 非对象类型直接返回(包含 Symbol、undefined、基本类型等) + // 接受参数:阵列、物件、null + if (typeof o !== "object") return o; + + try { + // 优先使用 structuredClone,支持大多数可克隆对象 + return Native.structuredClone(o); + } catch { + // 例如:被 Proxy 包装的对象(如 Vue 等框架处理过的 reactive 对象) + // structuredClone 可能会失败,忽略错误继续尝试其他方式 + } + + try { + // 退而求其次,使用 JSON 序列化方式进行深拷贝 + // 仅适用于可被 JSON 表示的普通对象 + return Native.jsonParse(Native.jsonStringify(o)); + } catch { + // 序列化失败,忽略错误 + } + + // 其他无法克隆的非法对象,例如 window、document 等 + console.error("customClone failed"); + return undefined; +}; diff --git a/src/app/service/content/gm_api/gm_api.test.ts b/src/app/service/content/gm_api/gm_api.test.ts index fd737de4a..73b26f97b 100644 --- a/src/app/service/content/gm_api/gm_api.test.ts +++ b/src/app/service/content/gm_api/gm_api.test.ts @@ -416,7 +416,12 @@ describe.concurrent("GM_value", () => { // 设置再删除 GM_setValue("a", undefined); let ret2 = GM_getValue("a", 456); - return {ret1, ret2}; + // 设置错误的对象 + GM_setValue("proxy-key", new Proxy({}, {})); + let ret3 = GM_getValue("proxy-key"); + GM_setValue("window",window); + let ret4 = GM_getValue("window"); + return {ret1, ret2, ret3, ret4}; `; const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 }); const mockMessage = { @@ -428,7 +433,7 @@ describe.concurrent("GM_value", () => { const ret = await exec.exec(); expect(mockSendMessage).toHaveBeenCalled(); - expect(mockSendMessage).toHaveBeenCalledTimes(2); + expect(mockSendMessage).toHaveBeenCalledTimes(4); // 第一次调用:设置值为 123 expect(mockSendMessage).toHaveBeenNthCalledWith( @@ -458,11 +463,45 @@ describe.concurrent("GM_value", () => { }) ); - expect(ret).toEqual({ ret1: 123, ret2: 456 }); + // 第三次调用:设置值为 Proxy 对象(应失败) + expect(mockSendMessage).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + action: "content/runtime/gmApi", + data: { + api: "GM_setValue", + params: [expect.any(String), "proxy-key", {}], // Proxy 会被转换为空对象 + runFlag: expect.any(String), + uuid: undefined, + }, + }) + ); + + // 第四次调用:设置值为 window 对象(应失败) + expect(mockSendMessage).toHaveBeenNthCalledWith( + 4, + expect.objectContaining({ + action: "content/runtime/gmApi", + data: { + api: "GM_setValue", + params: [expect.any(String), "window"], // window 会被转换为空对象 + runFlag: expect.any(String), + uuid: undefined, + }, + }) + ); + + expect(ret).toEqual({ + ret1: 123, + ret2: 456, + ret3: {}, + ret4: undefined, + }); }); it.concurrent("value引用问题 #1141", async () => { const script = Object.assign({}, scriptRes) as ScriptLoadInfo; + script.value = {}; script.metadata.grant = ["GM_getValue", "GM_setValue", "GM_getValues"]; script.code = ` const value1 = { @@ -622,7 +661,12 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 // 设置再删除 GM_setValues({"a": undefined, "c": undefined}); let ret2 = GM_getValues(["a","b","c"]); - return {ret1, ret2}; + // 设置错误的对象 + GM_setValues({"proxy-key": new Proxy({}, {})}); + let ret3 = GM_getValues(["proxy-key"]); + GM_setValues({"window": window}); + let ret4 = GM_getValues(["window"]); + return {ret1, ret2, ret3, ret4}; `; const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 }); const mockMessage = { @@ -634,7 +678,7 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const ret = await exec.exec(); expect(mockSendMessage).toHaveBeenCalled(); - expect(mockSendMessage).toHaveBeenCalledTimes(2); + expect(mockSendMessage).toHaveBeenCalledTimes(4); // 第一次调用:设置值为 123 expect(mockSendMessage).toHaveBeenNthCalledWith( @@ -687,7 +731,60 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 }) ); - expect(ret).toEqual({ ret1: { a: 123, b: 456, c: "789" }, ret2: { b: 456 } }); + // 第三次调用:设置值为 Proxy 对象(应失败) + expect(mockSendMessage).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + action: "content/runtime/gmApi", + data: { + api: "GM_setValues", + params: [ + // event id + expect.stringMatching(/^.+::\d+$/), + // the object payload + expect.objectContaining({ + k: expect.stringMatching(/^##[\d.]+##$/), + m: expect.objectContaining({ + "proxy-key": {}, + }), + }), + ], + runFlag: expect.any(String), + uuid: undefined, + }, + }) + ); + + // 第四次调用:设置值为 window 对象(应失败) + expect(mockSendMessage).toHaveBeenNthCalledWith( + 4, + expect.objectContaining({ + action: "content/runtime/gmApi", + data: { + api: "GM_setValues", + params: [ + // event id + expect.stringMatching(/^.+::\d+$/), + // the object payload + expect.objectContaining({ + k: expect.stringMatching(/^##[\d.]+##$/), + m: expect.objectContaining({ + window: expect.stringMatching(/^##[\d.]+##undefined$/), + }), + }), + ], + runFlag: expect.any(String), + uuid: undefined, + }, + }) + ); + + expect(ret).toEqual({ + ret1: { a: 123, b: 456, c: "789" }, + ret2: { b: 456 }, + ret3: { "proxy-key": {} }, + ret4: { window: undefined }, + }); }); it.concurrent("GM_deleteValue", async () => { diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index 539184992..ee37cabda 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -1,3 +1,4 @@ +import { customClone, Native } from "../global"; import type { Message, MessageConnect } from "@Packages/message/types"; import type { CustomEventMessage } from "@Packages/message/custom_event_message"; import type { @@ -236,7 +237,7 @@ export default class GMApi extends GM_Base { const ret = a.scriptRes.value[key]; if (ret !== undefined) { if (ret && typeof ret === "object") { - return structuredClone(ret); + return customClone(ret)!; } return ret; } @@ -275,10 +276,15 @@ export default class GMApi extends GM_Base { } else { // 对object的value进行一次转化 if (value && typeof value === "object") { - value = structuredClone(value); + value = customClone(value); } + // customClone 可能返回 undefined a.scriptRes.value[key] = value; - a.sendMessage("GM_setValue", [id, key, value]); + if (value === undefined) { + a.sendMessage("GM_setValue", [id, key]); + } else { + a.sendMessage("GM_setValue", [id, key, value]); + } } return id; } @@ -295,6 +301,7 @@ export default class GMApi extends GM_Base { valueChangePromiseMap.set(id, promise); } const valueStore = a.scriptRes.value; + const sendingValues = {} as Record; for (const [key, value] of Object.entries(values)) { let value_ = value; if (value_ === undefined) { @@ -302,13 +309,15 @@ export default class GMApi extends GM_Base { } else { // 对object的value进行一次转化 if (value_ && typeof value_ === "object") { - value_ = structuredClone(value_); + value_ = customClone(value_); } + // customClone 可能返回 undefined valueStore[key] = value_; } + sendingValues[key] = value_; } // 避免undefined 等空值流失,先进行映射处理 - const valuesNew = encodeMessage(values); + const valuesNew = encodeMessage(sendingValues); a.sendMessage("GM_setValues", [id, valuesNew]); return id; } @@ -369,7 +378,7 @@ export default class GMApi extends GM_Base { if (!this.scriptRes) return {}; if (!keysOrDefaults) { // Returns all values - return structuredClone(this.scriptRes.value); + return customClone(this.scriptRes.value)!; } const result: TGMKeyValue = {}; if (Array.isArray(keysOrDefaults)) { @@ -381,7 +390,7 @@ export default class GMApi extends GM_Base { // 对object的value进行一次转化 let value = this.scriptRes.value[key]; if (value && typeof value === "object") { - value = structuredClone(value); + value = customClone(value)!; } result[key] = value; } @@ -465,7 +474,7 @@ export default class GMApi extends GM_Base { GM_log(message: string, level: GMTypes.LoggerLevel = "info", ...labels: GMTypes.LoggerLabel[]) { if (this.isInvalidContext()) return; if (typeof message !== "string") { - message = JSON.stringify(message); + message = Native.jsonStringify(message); } this.sendMessage("GM_log", [message, level, labels]); } @@ -1258,7 +1267,7 @@ export default class GMApi extends GM_Base { GM_saveTab(obj: object) { if (this.isInvalidContext()) return; if (typeof obj === "object") { - obj = JSON.parse(JSON.stringify(obj)); + obj = customClone(obj); } this.sendMessage("GM_saveTab", [obj]); } diff --git a/src/app/service/content/gm_api/gm_xhr.ts b/src/app/service/content/gm_api/gm_xhr.ts index c7e5a1ca1..276bea83f 100644 --- a/src/app/service/content/gm_api/gm_xhr.ts +++ b/src/app/service/content/gm_api/gm_xhr.ts @@ -1,3 +1,4 @@ +import { Native } from "../global"; import type { CustomEventMessage } from "@Packages/message/custom_event_message"; import type GMApi from "./gm_api"; import { dataEncode } from "@App/pkg/utils/xhr/xhr_data"; @@ -344,7 +345,7 @@ export function GM_xmlhttpRequest( let o = undefined; if (text) { try { - o = JSON.parse(text); + o = Native.jsonParse(text); } catch { // ignored } diff --git a/src/manifest.json b/src/manifest.json index 154d305da..93d3e10fb 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "__MSG_scriptcat__", - "version": "1.2.5", + "version": "1.2.6", "author": "CodFrm", "description": "__MSG_scriptcat_description__", "options_ui": {