Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "scriptcat",
"version": "1.2.5",
"version": "1.2.6",
"description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!",
"author": "CodFrm",
"license": "GPLv3",
Expand Down
36 changes: 36 additions & 0 deletions src/app/service/content/global.ts
Original file line number Diff line number Diff line change
@@ -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;
};
109 changes: 103 additions & 6 deletions src/app/service/content/gm_api/gm_api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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(
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand All @@ -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(
Expand Down Expand Up @@ -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 () => {
Expand Down
27 changes: 18 additions & 9 deletions src/app/service/content/gm_api/gm_api.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

上面的 a.sendMessage("GM_setValue", [id, key]); 会有 delete a.scriptRes.value[key]; 但这个是 a.scriptRes.value[key] = undefined;

你要不要统一一下呀?

Copy link
Member

@CodFrm CodFrm Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

故意为之,兼容TM,删除了key,但是这一次通过 GM_getValues 可以获取到 key,但是刷新页面就没有这个key了

} else {
a.sendMessage("GM_setValue", [id, key, value]);
}
}
return id;
}
Expand All @@ -295,20 +301,23 @@ export default class GMApi extends GM_Base {
valueChangePromiseMap.set(id, promise);
}
const valueStore = a.scriptRes.value;
const sendingValues = {} as Record<string, any>;
for (const [key, value] of Object.entries(values)) {
let value_ = value;
if (value_ === undefined) {
if (valueStore[key]) delete valueStore[key];
} else {
// 对object的value进行一次转化
if (value_ && typeof value_ === "object") {
value_ = structuredClone(value_);
value_ = customClone(value_);
}
// customClone 可能返回 undefined
valueStore[key] = value_;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

上面有 delete valueStore[key];
这里是 valueStore[key] = undefined;

你要不要统一一下呀?

}
sendingValues[key] = value_;
}
// 避免undefined 等空值流失,先进行映射处理
const valuesNew = encodeMessage(values);
const valuesNew = encodeMessage(sendingValues);
a.sendMessage("GM_setValues", [id, valuesNew]);
return id;
}
Expand Down Expand Up @@ -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)) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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]);
}
Expand Down Expand Up @@ -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]);
}
Expand Down
3 changes: 2 additions & 1 deletion src/app/service/content/gm_api/gm_xhr.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -344,7 +345,7 @@ export function GM_xmlhttpRequest(
let o = undefined;
if (text) {
try {
o = JSON.parse(text);
o = Native.jsonParse(text);
} catch {
// ignored
}
Expand Down
2 changes: 1 addition & 1 deletion src/manifest.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
Loading