Skip to content

Commit b8497df

Browse files
cyfung1031CopilotCodFrm
authored
🐛 修复 GM_addElement 问题,将操作放到content环境 (#1233)
* 配合 1.3 scripting, 重构 `GM_addElement` * Update scripting.ts * Create gm_add_element.js * Update src/app/service/content/gm_api/gm_api.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * API parameter for GM.addElement * event.detail.createElement as string * if (!(frag instanceof DocumentFragment)) throw Error * 测试改一下 * 整合测试用例 * 重构(删除了native、insertBefore、和特定属性) * 优化lint和测试问题 * 删除不兼容的示例 * 根据copilot的处理 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: 王一之 <yz@ggnb.top>
1 parent 2fa4778 commit b8497df

17 files changed

Lines changed: 448 additions & 149 deletions

example/gm_add_element.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,36 @@
2222
* 2. 元素标签名
2323
* 3. 属性对象
2424
*/
25-
const el = GM_addElement(document.querySelector('.BorderGrid-cell'), "img", {
26-
src: "https://bbs.tampermonkey.net.cn/uc_server/avatar.php?uid=4&size=small&ts=1"
25+
26+
// ------------- 基础用法 ----------------
27+
28+
const el = GM_addElement(document.querySelector(".BorderGrid-cell"), "img", {
29+
src: "https://bbs.tampermonkey.net.cn/uc_server/avatar.php?uid=4&size=small&ts=1",
2730
});
2831

2932
// 打印创建出来的 DOM 元素
3033
console.log(el);
34+
35+
// ------------- 基础用法 - textContent ----------------
36+
37+
const span3 = GM_addElement("span", {
38+
textContent: "Hello",
39+
});
40+
41+
console.log(`span text: ${span3.textContent}`);
42+
43+
// ------------- 基础用法 - onload & onerror ----------------
44+
45+
new Promise((resolve, reject) => {
46+
img = GM_addElement(document.body, "img", {
47+
src: "https://www.tampermonkey.net/favicon.ico",
48+
onload: resolve,
49+
onerror: reject,
50+
});
51+
})
52+
.then(() => {
53+
console.log("img insert ok");
54+
})
55+
.catch(() => {
56+
console.log("img insert failed");
57+
});

example/tests/gm_api_test.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// ==UserScript==
22
// @name GM API 完整测试
33
// @namespace https://docs.scriptcat.org/
4-
// @version 1.0.0
4+
// @version 1.1.0
55
// @description 全面测试ScriptCat的所有GM API功能
66
// @author ScriptCat
77
// @match https://content-security-policy.com/
@@ -234,7 +234,7 @@
234234
});
235235

236236
// ============ GM_addElement 测试 ============
237-
test("GM_addElement - 创建元素", () => {
237+
await testAsync("GM_addElement - 创建元素", async () => {
238238
assert("function", typeof GM_addElement, "GM_addElement 应该是函数");
239239

240240
const div = GM_addElement("div", {
@@ -244,8 +244,39 @@
244244
assert(true, div && div.tagName === "DIV", "应该返回 div 元素");
245245
console.log("添加的元素:", div);
246246

247+
// 创建脚本元素测试
248+
const script = GM_addElement("script", {
249+
textContent: 'window.foo = "bar";',
250+
});
251+
assert(true, script && script.tagName === "SCRIPT", "应该返回 script 元素");
252+
assert("bar", unsafeWindow.foo, "脚本内容应该执行,unsafeWindow.foo 应该是 'bar'");
253+
console.log("添加的脚本元素:", script);
254+
255+
document.querySelector(".container").insertBefore(script, document.querySelector(".masthead"));
256+
257+
// onload 和 onerror 测试 - 插入图片元素
258+
let img;
259+
await new Promise((resolve, reject) => {
260+
img = GM_addElement(document.body, "img", {
261+
src: "https://www.tampermonkey.net/favicon.ico",
262+
onload: () => {
263+
console.log("图片加载成功");
264+
resolve();
265+
},
266+
onerror: (error) => {
267+
reject(new Error("图片加载失败: " + error));
268+
},
269+
});
270+
});
271+
assert(true, img && img.tagName === "IMG", "应该返回 img 元素");
272+
console.log("添加的图片元素:", img);
273+
247274
// 3秒后移除
248-
setTimeout(() => div.remove(), 3000);
275+
setTimeout(() => {
276+
script.remove();
277+
div.remove();
278+
img.remove();
279+
}, 3000);
249280
});
250281

251282
// ============ GM_getResourceText/URL 测试 ============

packages/message/common.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export const pageDispatchEvent = performanceClone.dispatchEvent.bind(performance
1414
export const pageAddEventListener = performanceClone.addEventListener.bind(performanceClone);
1515
export const pageRemoveEventListener = performanceClone.removeEventListener.bind(performanceClone);
1616
const detailClone = typeof cloneInto === "function" ? cloneInto : null;
17-
export const pageDispatchCustomEvent = (eventType: string, detail: any) => {
18-
if (detailClone && detail) detail = detailClone(detail, performanceClone);
17+
export const pageDispatchCustomEvent = <T = any>(eventType: string, detail: T) => {
18+
if (detailClone && detail) detail = <T>detailClone(detail, performanceClone);
1919
const ev = new CustomEventClone(eventType, {
2020
detail,
2121
cancelable: true,

packages/message/custom_event_message.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export class CustomEventMessage implements Message {
5151
event.preventDefault(); // 告知另一端这边已准备好
5252
this.readyWrap.setReady(); // 两端已准备好,则 setReady()
5353
} else if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) {
54+
if (event.cancelable) event.preventDefault(); // 告知另一端
5455
relatedTargetMap.set(event.movementX, event.relatedTarget);
5556
} else if (event instanceof CustomEventClone) {
5657
this.messageHandle(event.detail, new CustomEventPostMessage(this));

src/app/service/content/create_context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const createContext = (
1414
GMInfo: any,
1515
envPrefix: string,
1616
message: Message,
17+
contentMsg: Message,
1718
scriptGrants: Set<string>
1819
) => {
1920
// 按照GMApi构建
@@ -31,6 +32,7 @@ export const createContext = (
3132
const context = createGMBase({
3233
prefix: envPrefix,
3334
message,
35+
contentMsg,
3436
scriptRes,
3537
valueChangeListener,
3638
EE,

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

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,13 @@ const envInfo: GMInfoEnv = {
2828
isIncognito: false,
2929
};
3030

31-
// @ts-ignore
32-
const noneExec = new ExecScript(scriptRes, undefined, undefined, nilFn, envInfo);
31+
const noneExec = new ExecScript(scriptRes, {
32+
envPrefix: "scripting",
33+
message: undefined as any,
34+
contentMsg: undefined as any,
35+
code: nilFn,
36+
envInfo,
37+
});
3338

3439
const scriptRes2 = {
3540
id: 0,
@@ -42,8 +47,13 @@ const scriptRes2 = {
4247
value: {},
4348
} as unknown as ScriptLoadInfo;
4449

45-
// @ts-ignore
46-
const sandboxExec = new ExecScript(scriptRes2, undefined, undefined, nilFn, envInfo);
50+
const sandboxExec = new ExecScript(scriptRes2, {
51+
envPrefix: "scripting",
52+
message: undefined as any,
53+
contentMsg: undefined as any,
54+
code: nilFn,
55+
envInfo,
56+
});
4757

4858
describe.concurrent("GM_info", () => {
4959
it.concurrent("none", async () => {
@@ -503,8 +513,13 @@ describe("沙盒环境测试", async () => {
503513

504514
it.concurrent("RegExp", async () => {
505515
const script = Object.assign({}, scriptRes2) as ScriptLoadInfo;
506-
// @ts-ignore
507-
const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo);
516+
const exec = new ExecScript(script, {
517+
envPrefix: "scripting",
518+
message: undefined as any,
519+
contentMsg: undefined as any,
520+
code: nilFn,
521+
envInfo,
522+
});
508523
script.code = `const str = "12345";
509524
const reg = /(123)/;
510525
return [str.match(reg), RegExp.$1];`;
@@ -516,24 +531,39 @@ return [str.match(reg), RegExp.$1];`;
516531
it.concurrent("沙盒之间不应该共享变量", async () => {
517532
const script = Object.assign({}, scriptRes2) as ScriptLoadInfo;
518533
script.code = `this.testVar = "ok"; ttest1 = "ok"; return {testVar: this.testVar, testVar2: this.testVar2, ttest1: typeof ttest1, ttest2: typeof ttest2};`;
519-
// @ts-ignore
520-
const exec1 = new ExecScript(script, undefined, undefined, nilFn, envInfo);
534+
const exec1 = new ExecScript(script, {
535+
envPrefix: "scripting",
536+
message: undefined as any,
537+
contentMsg: undefined as any,
538+
code: nilFn,
539+
envInfo,
540+
});
521541
exec1.scriptFunc = compileScript(compileScriptCode(script));
522542
const ret1 = await exec1.exec();
523543
expect(ret1).toEqual({ testVar: "ok", testVar2: undefined, ttest1: "string", ttest2: "number" });
524544

525545
const script2 = Object.assign({}, scriptRes2) as ScriptLoadInfo;
526546
script2.code = `this.testVar2 = "ok"; ttest2 = "ok"; return {testVar: this.testVar, testVar2: this.testVar2, ttest1: typeof ttest1, ttest2: typeof ttest2};`;
527-
// @ts-ignore
528-
const exec2 = new ExecScript(script2, undefined, undefined, nilFn, envInfo);
547+
const exec2 = new ExecScript(script2, {
548+
envPrefix: "scripting",
549+
message: undefined as any,
550+
contentMsg: undefined as any,
551+
code: nilFn,
552+
envInfo,
553+
});
529554
exec2.scriptFunc = compileScript(compileScriptCode(script2));
530555
const ret2 = await exec2.exec();
531556
expect(ret2).toEqual({ testVar: undefined, testVar2: "ok", ttest1: "number", ttest2: "string" });
532557

533558
const script3 = Object.assign({}, scriptRes2) as ScriptLoadInfo;
534559
script3.code = `onload = function (){return 123}; return {onload, thisOnload: this.onload, winOnload: window.onload};`;
535-
// @ts-ignore
536-
const exec3 = new ExecScript(script3, undefined, undefined, nilFn, envInfo);
560+
const exec3 = new ExecScript(script3, {
561+
envPrefix: "scripting",
562+
message: undefined as any,
563+
contentMsg: undefined as any,
564+
code: nilFn,
565+
envInfo,
566+
});
537567
exec3.scriptFunc = compileScript(compileScriptCode(script3));
538568
const ret3 = await exec3.exec();
539569
expect(ret3.onload).toEqual(expect.any(Function));
@@ -545,8 +575,13 @@ return [str.match(reg), RegExp.$1];`;
545575

546576
const script4 = Object.assign({}, scriptRes2) as ScriptLoadInfo;
547577
script4.code = `onload = function (){return 456}; return {onload, thisOnload: this.onload, winOnload: window.onload};`;
548-
// @ts-ignore
549-
const exec4 = new ExecScript(script4, undefined, undefined, nilFn, envInfo);
578+
const exec4 = new ExecScript(script4, {
579+
envPrefix: "scripting",
580+
message: undefined as any,
581+
contentMsg: undefined as any,
582+
code: nilFn,
583+
envInfo,
584+
});
550585
exec4.scriptFunc = compileScript(compileScriptCode(script4));
551586
const ret4 = await exec4.exec();
552587
expect(ret4.onload).toEqual(expect.any(Function));
@@ -564,16 +599,26 @@ return [str.match(reg), RegExp.$1];`;
564599
it.concurrent("沙盒之间能用unsafeWindow(及全局作用域)共享变量", async () => {
565600
const script = Object.assign({}, scriptRes2) as ScriptLoadInfo;
566601
script.code = `unsafeWindow.testSVar1 = "shareA"; ggaa1 = "ok"; return {testSVar1: unsafeWindow.testSVar1, testSVar2: unsafeWindow.testSVar2, ggaa1: typeof ggaa1, ggaa2: typeof ggaa2};`;
567-
// @ts-ignore
568-
const exec1 = new ExecScript(script, undefined, undefined, nilFn, envInfo);
602+
const exec1 = new ExecScript(script, {
603+
envPrefix: "scripting",
604+
message: undefined as any,
605+
contentMsg: undefined as any,
606+
code: nilFn,
607+
envInfo,
608+
});
569609
exec1.scriptFunc = compileScript(compileScriptCode(script));
570610
const ret1 = await exec1.exec();
571611
expect(ret1).toEqual({ testSVar1: "shareA", testSVar2: undefined, ggaa1: "string", ggaa2: "undefined" });
572612

573613
const script2 = Object.assign({}, scriptRes2) as ScriptLoadInfo;
574614
script2.code = `unsafeWindow.testSVar2 = "shareB"; ggaa2 = "ok"; return {testSVar1: unsafeWindow.testSVar1, testSVar2: unsafeWindow.testSVar2, ggaa1: typeof ggaa1, ggaa2: typeof ggaa2};`;
575-
// @ts-ignore
576-
const exec2 = new ExecScript(script2, undefined, undefined, nilFn, envInfo);
615+
const exec2 = new ExecScript(script2, {
616+
envPrefix: "scripting",
617+
message: undefined as any,
618+
contentMsg: undefined as any,
619+
code: nilFn,
620+
envInfo,
621+
});
577622
exec2.scriptFunc = compileScript(compileScriptCode(script2));
578623
const ret2 = await exec2.exec();
579624
expect(ret2).toEqual({ testSVar1: "shareA", testSVar2: "shareB", ggaa1: "string", ggaa2: "string" });
@@ -582,8 +627,13 @@ return [str.match(reg), RegExp.$1];`;
582627
it.concurrent("测试SC沙盒与TM沙盒有相近的特殊处理", async () => {
583628
const script1 = Object.assign({}, scriptRes2) as ScriptLoadInfo;
584629
script1.code = `onfocus = function(){}; onresize = 123; onblur = "123"; const ret = {onfocus, onresize, onblur}; onfocus = null; onresize = null; onblur = null; return ret;`;
585-
// @ts-ignore
586-
const exec1 = new ExecScript(script1, undefined, undefined, nilFn, envInfo);
630+
const exec1 = new ExecScript(script1, {
631+
envPrefix: "scripting",
632+
message: undefined as any,
633+
contentMsg: undefined as any,
634+
code: nilFn,
635+
envInfo,
636+
});
587637
exec1.scriptFunc = compileScript(compileScriptCode(script1));
588638
const ret1 = await exec1.exec();
589639
expect(ret1.onfocus).toEqual(expect.any(Function));
@@ -592,8 +642,13 @@ return [str.match(reg), RegExp.$1];`;
592642

593643
const script2 = Object.assign({}, scriptRes2) as ScriptLoadInfo;
594644
script2.code = `window.onfocus = function(){}; window.onresize = 123; window.onblur = "123"; const {onfocus, onresize, onblur} = window; const ret = {onfocus, onresize, onblur}; window.onfocus = null; window.onresize = null; window.onblur = null; return ret;`;
595-
// @ts-ignore
596-
const exec2 = new ExecScript(script2, undefined, undefined, nilFn, envInfo);
645+
const exec2 = new ExecScript(script2, {
646+
envPrefix: "scripting",
647+
message: undefined as any,
648+
contentMsg: undefined as any,
649+
code: nilFn,
650+
envInfo,
651+
});
597652
exec2.scriptFunc = compileScript(compileScriptCode(script2));
598653
const ret2 = await exec2.exec();
599654
expect(ret2.onfocus).toEqual(expect.any(Function));

src/app/service/content/exec_script.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ export default class ExecScript {
2525

2626
constructor(
2727
scriptRes: TScriptInfo,
28-
envPrefix: "scripting" | "offscreen",
29-
message: Message,
30-
code: string | ScriptFunc,
31-
envInfo: GMInfoEnv,
32-
globalInjection?: { [key: string]: any } // 主要是全域API. @grant none 时无效
28+
options: {
29+
envPrefix: string;
30+
message: Message;
31+
contentMsg: Message;
32+
code: string | ScriptFunc;
33+
envInfo: GMInfoEnv;
34+
globalInjection?: { [key: string]: any }; // 主要是全域API. @grant none 时无效
35+
}
3336
) {
37+
const { envPrefix, message, contentMsg, code, envInfo, globalInjection } = options;
3438
this.scriptRes = scriptRes;
3539
this.logger = LoggerCore.getInstance().logger({
3640
component: "exec",
@@ -52,7 +56,7 @@ export default class ExecScript {
5256
this.named = { GM: { info: GM_info }, GM_info };
5357
} else {
5458
// 构建脚本GM上下文
55-
this.sandboxContext = createContext(scriptRes, GM_info, envPrefix, message, grantSet);
59+
this.sandboxContext = createContext(scriptRes, GM_info, envPrefix, message, contentMsg, grantSet);
5660
if (globalInjection) {
5761
Object.assign(this.sandboxContext, globalInjection);
5862
}

src/app/service/content/exec_warp.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,14 @@ export class BgExecScriptWarp extends ExecScript {
7373
},
7474
isIncognito: false,
7575
};
76-
super(scriptRes, "offscreen", message, scriptRes.code, envInfo, thisContext);
76+
super(scriptRes, {
77+
envPrefix: "offscreen",
78+
message: message,
79+
contentMsg: message,
80+
code: scriptRes.code,
81+
envInfo,
82+
globalInjection: thisContext,
83+
});
7784
this.setTimeout = setTimeout;
7885
this.setInterval = setInterval;
7986
}

src/app/service/content/global.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export const Native = {
77
structuredClone: typeof structuredClone === "function" ? structuredClone : unsupportedAPI,
88
jsonStringify: JSON.stringify.bind(JSON),
99
jsonParse: JSON.parse.bind(JSON),
10+
createElement: Document.prototype.createElement,
11+
ownFragment: new DocumentFragment(),
1012
} as const;
1113

1214
export const customClone = (o: any) => {

0 commit comments

Comments
 (0)