Skip to content

Commit 265e122

Browse files
cyfung1031CodFrm
andauthored
🐛 修复 1.2.5 structuredClone错误 (#1192)
* 修复 1.2.5 structuredClone错误 * Update global.ts * lint * update * Update gm_api.ts * Update global.ts * Update global.ts * lint * lint * 修改命名方式 * 添加单元测试 * 修改名字 * 兼容TM,失败设置undefined * 修复setValues的兼容性问题 * TM一致 * typescript * 不建议这样做但先这样吧 * 修改成 sendingValues 避免直接改变参数物件 * 修复单测 * release v1.2.6 * 添加单测 --------- Co-authored-by: 王一之 <yz@ggnb.top>
1 parent b68f98e commit 265e122

6 files changed

Lines changed: 161 additions & 18 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "scriptcat",
3-
"version": "1.2.5",
3+
"version": "1.2.6",
44
"description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!",
55
"author": "CodFrm",
66
"license": "GPLv3",

src/app/service/content/global.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// 避免在全局页面环境中,内置处理函数被篡改或重写
2+
const unsupportedAPI = () => {
3+
throw "unsupportedAPI";
4+
};
5+
6+
export const Native = {
7+
structuredClone: typeof structuredClone === "function" ? structuredClone : unsupportedAPI,
8+
jsonStringify: JSON.stringify.bind(JSON),
9+
jsonParse: JSON.parse.bind(JSON),
10+
} as const;
11+
12+
export const customClone = (o: any) => {
13+
// 非对象类型直接返回(包含 Symbol、undefined、基本类型等)
14+
// 接受参数:阵列、物件、null
15+
if (typeof o !== "object") return o;
16+
17+
try {
18+
// 优先使用 structuredClone,支持大多数可克隆对象
19+
return Native.structuredClone(o);
20+
} catch {
21+
// 例如:被 Proxy 包装的对象(如 Vue 等框架处理过的 reactive 对象)
22+
// structuredClone 可能会失败,忽略错误继续尝试其他方式
23+
}
24+
25+
try {
26+
// 退而求其次,使用 JSON 序列化方式进行深拷贝
27+
// 仅适用于可被 JSON 表示的普通对象
28+
return Native.jsonParse(Native.jsonStringify(o));
29+
} catch {
30+
// 序列化失败,忽略错误
31+
}
32+
33+
// 其他无法克隆的非法对象,例如 window、document 等
34+
console.error("customClone failed");
35+
return undefined;
36+
};

src/app/service/content/gm_api/gm_api.test.ts

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,12 @@ describe.concurrent("GM_value", () => {
416416
// 设置再删除
417417
GM_setValue("a", undefined);
418418
let ret2 = GM_getValue("a", 456);
419-
return {ret1, ret2};
419+
// 设置错误的对象
420+
GM_setValue("proxy-key", new Proxy({}, {}));
421+
let ret3 = GM_getValue("proxy-key");
422+
GM_setValue("window",window);
423+
let ret4 = GM_getValue("window");
424+
return {ret1, ret2, ret3, ret4};
420425
`;
421426
const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 });
422427
const mockMessage = {
@@ -428,7 +433,7 @@ describe.concurrent("GM_value", () => {
428433
const ret = await exec.exec();
429434

430435
expect(mockSendMessage).toHaveBeenCalled();
431-
expect(mockSendMessage).toHaveBeenCalledTimes(2);
436+
expect(mockSendMessage).toHaveBeenCalledTimes(4);
432437

433438
// 第一次调用:设置值为 123
434439
expect(mockSendMessage).toHaveBeenNthCalledWith(
@@ -458,11 +463,45 @@ describe.concurrent("GM_value", () => {
458463
})
459464
);
460465

461-
expect(ret).toEqual({ ret1: 123, ret2: 456 });
466+
// 第三次调用:设置值为 Proxy 对象(应失败)
467+
expect(mockSendMessage).toHaveBeenNthCalledWith(
468+
3,
469+
expect.objectContaining({
470+
action: "content/runtime/gmApi",
471+
data: {
472+
api: "GM_setValue",
473+
params: [expect.any(String), "proxy-key", {}], // Proxy 会被转换为空对象
474+
runFlag: expect.any(String),
475+
uuid: undefined,
476+
},
477+
})
478+
);
479+
480+
// 第四次调用:设置值为 window 对象(应失败)
481+
expect(mockSendMessage).toHaveBeenNthCalledWith(
482+
4,
483+
expect.objectContaining({
484+
action: "content/runtime/gmApi",
485+
data: {
486+
api: "GM_setValue",
487+
params: [expect.any(String), "window"], // window 会被转换为空对象
488+
runFlag: expect.any(String),
489+
uuid: undefined,
490+
},
491+
})
492+
);
493+
494+
expect(ret).toEqual({
495+
ret1: 123,
496+
ret2: 456,
497+
ret3: {},
498+
ret4: undefined,
499+
});
462500
});
463501

464502
it.concurrent("value引用问题 #1141", async () => {
465503
const script = Object.assign({}, scriptRes) as ScriptLoadInfo;
504+
script.value = {};
466505
script.metadata.grant = ["GM_getValue", "GM_setValue", "GM_getValues"];
467506
script.code = `
468507
const value1 = {
@@ -622,7 +661,12 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4
622661
// 设置再删除
623662
GM_setValues({"a": undefined, "c": undefined});
624663
let ret2 = GM_getValues(["a","b","c"]);
625-
return {ret1, ret2};
664+
// 设置错误的对象
665+
GM_setValues({"proxy-key": new Proxy({}, {})});
666+
let ret3 = GM_getValues(["proxy-key"]);
667+
GM_setValues({"window": window});
668+
let ret4 = GM_getValues(["window"]);
669+
return {ret1, ret2, ret3, ret4};
626670
`;
627671
const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 });
628672
const mockMessage = {
@@ -634,7 +678,7 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4
634678
const ret = await exec.exec();
635679

636680
expect(mockSendMessage).toHaveBeenCalled();
637-
expect(mockSendMessage).toHaveBeenCalledTimes(2);
681+
expect(mockSendMessage).toHaveBeenCalledTimes(4);
638682

639683
// 第一次调用:设置值为 123
640684
expect(mockSendMessage).toHaveBeenNthCalledWith(
@@ -687,7 +731,60 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4
687731
})
688732
);
689733

690-
expect(ret).toEqual({ ret1: { a: 123, b: 456, c: "789" }, ret2: { b: 456 } });
734+
// 第三次调用:设置值为 Proxy 对象(应失败)
735+
expect(mockSendMessage).toHaveBeenNthCalledWith(
736+
3,
737+
expect.objectContaining({
738+
action: "content/runtime/gmApi",
739+
data: {
740+
api: "GM_setValues",
741+
params: [
742+
// event id
743+
expect.stringMatching(/^.+::\d+$/),
744+
// the object payload
745+
expect.objectContaining({
746+
k: expect.stringMatching(/^##[\d.]+##$/),
747+
m: expect.objectContaining({
748+
"proxy-key": {},
749+
}),
750+
}),
751+
],
752+
runFlag: expect.any(String),
753+
uuid: undefined,
754+
},
755+
})
756+
);
757+
758+
// 第四次调用:设置值为 window 对象(应失败)
759+
expect(mockSendMessage).toHaveBeenNthCalledWith(
760+
4,
761+
expect.objectContaining({
762+
action: "content/runtime/gmApi",
763+
data: {
764+
api: "GM_setValues",
765+
params: [
766+
// event id
767+
expect.stringMatching(/^.+::\d+$/),
768+
// the object payload
769+
expect.objectContaining({
770+
k: expect.stringMatching(/^##[\d.]+##$/),
771+
m: expect.objectContaining({
772+
window: expect.stringMatching(/^##[\d.]+##undefined$/),
773+
}),
774+
}),
775+
],
776+
runFlag: expect.any(String),
777+
uuid: undefined,
778+
},
779+
})
780+
);
781+
782+
expect(ret).toEqual({
783+
ret1: { a: 123, b: 456, c: "789" },
784+
ret2: { b: 456 },
785+
ret3: { "proxy-key": {} },
786+
ret4: { window: undefined },
787+
});
691788
});
692789

693790
it.concurrent("GM_deleteValue", async () => {

src/app/service/content/gm_api/gm_api.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { customClone, Native } from "../global";
12
import type { Message, MessageConnect } from "@Packages/message/types";
23
import type { CustomEventMessage } from "@Packages/message/custom_event_message";
34
import type {
@@ -236,7 +237,7 @@ export default class GMApi extends GM_Base {
236237
const ret = a.scriptRes.value[key];
237238
if (ret !== undefined) {
238239
if (ret && typeof ret === "object") {
239-
return structuredClone(ret);
240+
return customClone(ret)!;
240241
}
241242
return ret;
242243
}
@@ -275,10 +276,15 @@ export default class GMApi extends GM_Base {
275276
} else {
276277
// 对object的value进行一次转化
277278
if (value && typeof value === "object") {
278-
value = structuredClone(value);
279+
value = customClone(value);
279280
}
281+
// customClone 可能返回 undefined
280282
a.scriptRes.value[key] = value;
281-
a.sendMessage("GM_setValue", [id, key, value]);
283+
if (value === undefined) {
284+
a.sendMessage("GM_setValue", [id, key]);
285+
} else {
286+
a.sendMessage("GM_setValue", [id, key, value]);
287+
}
282288
}
283289
return id;
284290
}
@@ -295,20 +301,23 @@ export default class GMApi extends GM_Base {
295301
valueChangePromiseMap.set(id, promise);
296302
}
297303
const valueStore = a.scriptRes.value;
304+
const sendingValues = {} as Record<string, any>;
298305
for (const [key, value] of Object.entries(values)) {
299306
let value_ = value;
300307
if (value_ === undefined) {
301308
if (valueStore[key]) delete valueStore[key];
302309
} else {
303310
// 对object的value进行一次转化
304311
if (value_ && typeof value_ === "object") {
305-
value_ = structuredClone(value_);
312+
value_ = customClone(value_);
306313
}
314+
// customClone 可能返回 undefined
307315
valueStore[key] = value_;
308316
}
317+
sendingValues[key] = value_;
309318
}
310319
// 避免undefined 等空值流失,先进行映射处理
311-
const valuesNew = encodeMessage(values);
320+
const valuesNew = encodeMessage(sendingValues);
312321
a.sendMessage("GM_setValues", [id, valuesNew]);
313322
return id;
314323
}
@@ -369,7 +378,7 @@ export default class GMApi extends GM_Base {
369378
if (!this.scriptRes) return {};
370379
if (!keysOrDefaults) {
371380
// Returns all values
372-
return structuredClone(this.scriptRes.value);
381+
return customClone(this.scriptRes.value)!;
373382
}
374383
const result: TGMKeyValue = {};
375384
if (Array.isArray(keysOrDefaults)) {
@@ -381,7 +390,7 @@ export default class GMApi extends GM_Base {
381390
// 对object的value进行一次转化
382391
let value = this.scriptRes.value[key];
383392
if (value && typeof value === "object") {
384-
value = structuredClone(value);
393+
value = customClone(value)!;
385394
}
386395
result[key] = value;
387396
}
@@ -465,7 +474,7 @@ export default class GMApi extends GM_Base {
465474
GM_log(message: string, level: GMTypes.LoggerLevel = "info", ...labels: GMTypes.LoggerLabel[]) {
466475
if (this.isInvalidContext()) return;
467476
if (typeof message !== "string") {
468-
message = JSON.stringify(message);
477+
message = Native.jsonStringify(message);
469478
}
470479
this.sendMessage("GM_log", [message, level, labels]);
471480
}
@@ -1258,7 +1267,7 @@ export default class GMApi extends GM_Base {
12581267
GM_saveTab(obj: object) {
12591268
if (this.isInvalidContext()) return;
12601269
if (typeof obj === "object") {
1261-
obj = JSON.parse(JSON.stringify(obj));
1270+
obj = customClone(obj);
12621271
}
12631272
this.sendMessage("GM_saveTab", [obj]);
12641273
}

src/app/service/content/gm_api/gm_xhr.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Native } from "../global";
12
import type { CustomEventMessage } from "@Packages/message/custom_event_message";
23
import type GMApi from "./gm_api";
34
import { dataEncode } from "@App/pkg/utils/xhr/xhr_data";
@@ -344,7 +345,7 @@ export function GM_xmlhttpRequest(
344345
let o = undefined;
345346
if (text) {
346347
try {
347-
o = JSON.parse(text);
348+
o = Native.jsonParse(text);
348349
} catch {
349350
// ignored
350351
}

src/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "__MSG_scriptcat__",
4-
"version": "1.2.5",
4+
"version": "1.2.6",
55
"author": "CodFrm",
66
"description": "__MSG_scriptcat_description__",
77
"options_ui": {

0 commit comments

Comments
 (0)