diff --git a/example/gm_add_element.js b/example/gm_add_element.js index b71ebf5a7..878d7ba13 100644 --- a/example/gm_add_element.js +++ b/example/gm_add_element.js @@ -22,9 +22,36 @@ * 2. 元素标签名 * 3. 属性对象 */ -const el = GM_addElement(document.querySelector('.BorderGrid-cell'), "img", { - src: "https://bbs.tampermonkey.net.cn/uc_server/avatar.php?uid=4&size=small&ts=1" + +// ------------- 基础用法 ---------------- + +const el = GM_addElement(document.querySelector(".BorderGrid-cell"), "img", { + src: "https://bbs.tampermonkey.net.cn/uc_server/avatar.php?uid=4&size=small&ts=1", }); // 打印创建出来的 DOM 元素 console.log(el); + +// ------------- 基础用法 - textContent ---------------- + +const span3 = GM_addElement("span", { + textContent: "Hello", +}); + +console.log(`span text: ${span3.textContent}`); + +// ------------- 基础用法 - onload & onerror ---------------- + +new Promise((resolve, reject) => { + img = GM_addElement(document.body, "img", { + src: "https://www.tampermonkey.net/favicon.ico", + onload: resolve, + onerror: reject, + }); +}) + .then(() => { + console.log("img insert ok"); + }) + .catch(() => { + console.log("img insert failed"); + }); diff --git a/example/tests/gm_api_test.js b/example/tests/gm_api_test.js index af8d7fa8d..a563ea5e7 100644 --- a/example/tests/gm_api_test.js +++ b/example/tests/gm_api_test.js @@ -1,7 +1,7 @@ // ==UserScript== // @name GM API 完整测试 // @namespace https://docs.scriptcat.org/ -// @version 1.0.0 +// @version 1.1.0 // @description 全面测试ScriptCat的所有GM API功能 // @author ScriptCat // @match https://content-security-policy.com/ @@ -234,7 +234,7 @@ }); // ============ GM_addElement 测试 ============ - test("GM_addElement - 创建元素", () => { + await testAsync("GM_addElement - 创建元素", async () => { assert("function", typeof GM_addElement, "GM_addElement 应该是函数"); const div = GM_addElement("div", { @@ -244,8 +244,39 @@ assert(true, div && div.tagName === "DIV", "应该返回 div 元素"); console.log("添加的元素:", div); + // 创建脚本元素测试 + const script = GM_addElement("script", { + textContent: 'window.foo = "bar";', + }); + assert(true, script && script.tagName === "SCRIPT", "应该返回 script 元素"); + assert("bar", unsafeWindow.foo, "脚本内容应该执行,unsafeWindow.foo 应该是 'bar'"); + console.log("添加的脚本元素:", script); + + document.querySelector(".container").insertBefore(script, document.querySelector(".masthead")); + + // onload 和 onerror 测试 - 插入图片元素 + let img; + await new Promise((resolve, reject) => { + img = GM_addElement(document.body, "img", { + src: "https://www.tampermonkey.net/favicon.ico", + onload: () => { + console.log("图片加载成功"); + resolve(); + }, + onerror: (error) => { + reject(new Error("图片加载失败: " + error)); + }, + }); + }); + assert(true, img && img.tagName === "IMG", "应该返回 img 元素"); + console.log("添加的图片元素:", img); + // 3秒后移除 - setTimeout(() => div.remove(), 3000); + setTimeout(() => { + script.remove(); + div.remove(); + img.remove(); + }, 3000); }); // ============ GM_getResourceText/URL 测试 ============ diff --git a/packages/message/common.ts b/packages/message/common.ts index 8b00e74cf..3806f53f4 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -14,8 +14,8 @@ export const pageDispatchEvent = performanceClone.dispatchEvent.bind(performance export const pageAddEventListener = performanceClone.addEventListener.bind(performanceClone); export const pageRemoveEventListener = performanceClone.removeEventListener.bind(performanceClone); const detailClone = typeof cloneInto === "function" ? cloneInto : null; -export const pageDispatchCustomEvent = (eventType: string, detail: any) => { - if (detailClone && detail) detail = detailClone(detail, performanceClone); +export const pageDispatchCustomEvent = (eventType: string, detail: T) => { + if (detailClone && detail) detail = detailClone(detail, performanceClone); const ev = new CustomEventClone(eventType, { detail, cancelable: true, diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index 5943bb632..32872d1e5 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -51,6 +51,7 @@ export class CustomEventMessage implements Message { event.preventDefault(); // 告知另一端这边已准备好 this.readyWrap.setReady(); // 两端已准备好,则 setReady() } else if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) { + if (event.cancelable) event.preventDefault(); // 告知另一端 relatedTargetMap.set(event.movementX, event.relatedTarget); } else if (event instanceof CustomEventClone) { this.messageHandle(event.detail, new CustomEventPostMessage(this)); diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index ea445e6ec..1bbe7f151 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -14,6 +14,7 @@ export const createContext = ( GMInfo: any, envPrefix: string, message: Message, + contentMsg: Message, scriptGrants: Set ) => { // 按照GMApi构建 @@ -31,6 +32,7 @@ export const createContext = ( const context = createGMBase({ prefix: envPrefix, message, + contentMsg, scriptRes, valueChangeListener, EE, diff --git a/src/app/service/content/exec_script.test.ts b/src/app/service/content/exec_script.test.ts index 4f927db46..3b82103e7 100644 --- a/src/app/service/content/exec_script.test.ts +++ b/src/app/service/content/exec_script.test.ts @@ -28,8 +28,13 @@ const envInfo: GMInfoEnv = { isIncognito: false, }; -// @ts-ignore -const noneExec = new ExecScript(scriptRes, undefined, undefined, nilFn, envInfo); +const noneExec = new ExecScript(scriptRes, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, +}); const scriptRes2 = { id: 0, @@ -42,8 +47,13 @@ const scriptRes2 = { value: {}, } as unknown as ScriptLoadInfo; -// @ts-ignore -const sandboxExec = new ExecScript(scriptRes2, undefined, undefined, nilFn, envInfo); +const sandboxExec = new ExecScript(scriptRes2, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, +}); describe.concurrent("GM_info", () => { it.concurrent("none", async () => { @@ -503,8 +513,13 @@ describe("沙盒环境测试", async () => { it.concurrent("RegExp", async () => { const script = Object.assign({}, scriptRes2) as ScriptLoadInfo; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); script.code = `const str = "12345"; const reg = /(123)/; return [str.match(reg), RegExp.$1];`; @@ -516,24 +531,39 @@ return [str.match(reg), RegExp.$1];`; it.concurrent("沙盒之间不应该共享变量", async () => { const script = Object.assign({}, scriptRes2) as ScriptLoadInfo; script.code = `this.testVar = "ok"; ttest1 = "ok"; return {testVar: this.testVar, testVar2: this.testVar2, ttest1: typeof ttest1, ttest2: typeof ttest2};`; - // @ts-ignore - const exec1 = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec1 = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec1.scriptFunc = compileScript(compileScriptCode(script)); const ret1 = await exec1.exec(); expect(ret1).toEqual({ testVar: "ok", testVar2: undefined, ttest1: "string", ttest2: "number" }); const script2 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script2.code = `this.testVar2 = "ok"; ttest2 = "ok"; return {testVar: this.testVar, testVar2: this.testVar2, ttest1: typeof ttest1, ttest2: typeof ttest2};`; - // @ts-ignore - const exec2 = new ExecScript(script2, undefined, undefined, nilFn, envInfo); + const exec2 = new ExecScript(script2, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec2.scriptFunc = compileScript(compileScriptCode(script2)); const ret2 = await exec2.exec(); expect(ret2).toEqual({ testVar: undefined, testVar2: "ok", ttest1: "number", ttest2: "string" }); const script3 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script3.code = `onload = function (){return 123}; return {onload, thisOnload: this.onload, winOnload: window.onload};`; - // @ts-ignore - const exec3 = new ExecScript(script3, undefined, undefined, nilFn, envInfo); + const exec3 = new ExecScript(script3, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec3.scriptFunc = compileScript(compileScriptCode(script3)); const ret3 = await exec3.exec(); expect(ret3.onload).toEqual(expect.any(Function)); @@ -545,8 +575,13 @@ return [str.match(reg), RegExp.$1];`; const script4 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script4.code = `onload = function (){return 456}; return {onload, thisOnload: this.onload, winOnload: window.onload};`; - // @ts-ignore - const exec4 = new ExecScript(script4, undefined, undefined, nilFn, envInfo); + const exec4 = new ExecScript(script4, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec4.scriptFunc = compileScript(compileScriptCode(script4)); const ret4 = await exec4.exec(); expect(ret4.onload).toEqual(expect.any(Function)); @@ -564,16 +599,26 @@ return [str.match(reg), RegExp.$1];`; it.concurrent("沙盒之间能用unsafeWindow(及全局作用域)共享变量", async () => { const script = Object.assign({}, scriptRes2) as ScriptLoadInfo; script.code = `unsafeWindow.testSVar1 = "shareA"; ggaa1 = "ok"; return {testSVar1: unsafeWindow.testSVar1, testSVar2: unsafeWindow.testSVar2, ggaa1: typeof ggaa1, ggaa2: typeof ggaa2};`; - // @ts-ignore - const exec1 = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec1 = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec1.scriptFunc = compileScript(compileScriptCode(script)); const ret1 = await exec1.exec(); expect(ret1).toEqual({ testSVar1: "shareA", testSVar2: undefined, ggaa1: "string", ggaa2: "undefined" }); const script2 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script2.code = `unsafeWindow.testSVar2 = "shareB"; ggaa2 = "ok"; return {testSVar1: unsafeWindow.testSVar1, testSVar2: unsafeWindow.testSVar2, ggaa1: typeof ggaa1, ggaa2: typeof ggaa2};`; - // @ts-ignore - const exec2 = new ExecScript(script2, undefined, undefined, nilFn, envInfo); + const exec2 = new ExecScript(script2, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec2.scriptFunc = compileScript(compileScriptCode(script2)); const ret2 = await exec2.exec(); expect(ret2).toEqual({ testSVar1: "shareA", testSVar2: "shareB", ggaa1: "string", ggaa2: "string" }); @@ -582,8 +627,13 @@ return [str.match(reg), RegExp.$1];`; it.concurrent("测试SC沙盒与TM沙盒有相近的特殊处理", async () => { const script1 = Object.assign({}, scriptRes2) as ScriptLoadInfo; script1.code = `onfocus = function(){}; onresize = 123; onblur = "123"; const ret = {onfocus, onresize, onblur}; onfocus = null; onresize = null; onblur = null; return ret;`; - // @ts-ignore - const exec1 = new ExecScript(script1, undefined, undefined, nilFn, envInfo); + const exec1 = new ExecScript(script1, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec1.scriptFunc = compileScript(compileScriptCode(script1)); const ret1 = await exec1.exec(); expect(ret1.onfocus).toEqual(expect.any(Function)); @@ -592,8 +642,13 @@ return [str.match(reg), RegExp.$1];`; const script2 = Object.assign({}, scriptRes2) as ScriptLoadInfo; 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;`; - // @ts-ignore - const exec2 = new ExecScript(script2, undefined, undefined, nilFn, envInfo); + const exec2 = new ExecScript(script2, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec2.scriptFunc = compileScript(compileScriptCode(script2)); const ret2 = await exec2.exec(); expect(ret2.onfocus).toEqual(expect.any(Function)); diff --git a/src/app/service/content/exec_script.ts b/src/app/service/content/exec_script.ts index 4c252e5b1..b00ca4c9d 100644 --- a/src/app/service/content/exec_script.ts +++ b/src/app/service/content/exec_script.ts @@ -25,12 +25,16 @@ export default class ExecScript { constructor( scriptRes: TScriptInfo, - envPrefix: "scripting" | "offscreen", - message: Message, - code: string | ScriptFunc, - envInfo: GMInfoEnv, - globalInjection?: { [key: string]: any } // 主要是全域API. @grant none 时无效 + options: { + envPrefix: string; + message: Message; + contentMsg: Message; + code: string | ScriptFunc; + envInfo: GMInfoEnv; + globalInjection?: { [key: string]: any }; // 主要是全域API. @grant none 时无效 + } ) { + const { envPrefix, message, contentMsg, code, envInfo, globalInjection } = options; this.scriptRes = scriptRes; this.logger = LoggerCore.getInstance().logger({ component: "exec", @@ -52,7 +56,7 @@ export default class ExecScript { this.named = { GM: { info: GM_info }, GM_info }; } else { // 构建脚本GM上下文 - this.sandboxContext = createContext(scriptRes, GM_info, envPrefix, message, grantSet); + this.sandboxContext = createContext(scriptRes, GM_info, envPrefix, message, contentMsg, grantSet); if (globalInjection) { Object.assign(this.sandboxContext, globalInjection); } diff --git a/src/app/service/content/exec_warp.ts b/src/app/service/content/exec_warp.ts index defffa41c..a965e91bc 100644 --- a/src/app/service/content/exec_warp.ts +++ b/src/app/service/content/exec_warp.ts @@ -73,7 +73,14 @@ export class BgExecScriptWarp extends ExecScript { }, isIncognito: false, }; - super(scriptRes, "offscreen", message, scriptRes.code, envInfo, thisContext); + super(scriptRes, { + envPrefix: "offscreen", + message: message, + contentMsg: message, + code: scriptRes.code, + envInfo, + globalInjection: thisContext, + }); this.setTimeout = setTimeout; this.setInterval = setInterval; } diff --git a/src/app/service/content/global.ts b/src/app/service/content/global.ts index e932f8c3c..5562212c6 100644 --- a/src/app/service/content/global.ts +++ b/src/app/service/content/global.ts @@ -7,6 +7,8 @@ export const Native = { structuredClone: typeof structuredClone === "function" ? structuredClone : unsupportedAPI, jsonStringify: JSON.stringify.bind(JSON), jsonParse: JSON.parse.bind(JSON), + createElement: Document.prototype.createElement, + ownFragment: new DocumentFragment(), } as const; export const customClone = (o: any) => { 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 3a7b61e59..2a76ed845 100644 --- a/src/app/service/content/gm_api/gm_api.test.ts +++ b/src/app/service/content/gm_api/gm_api.test.ts @@ -44,8 +44,13 @@ describe.concurrent("@grant GM", () => { "GM_log", "GM_notification", ]; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); script.code = `return { GM_getValue: this.GM_getValue, GM_getTab: this.GM_getTab, @@ -102,8 +107,13 @@ describe.concurrent("@grant GM", () => { "GM.log", "GM.notification", ]; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); script.code = `return { ["GM.getValue"]: GM.getValue, ["GM.getTab"]: GM.getTab, @@ -152,8 +162,13 @@ describe.concurrent("window.*", () => { const script = Object.assign({}, scriptRes) as ScriptLoadInfo; script.metadata.grant = ["window.close"]; script.code = `return window.close;`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual(expect.any(Function)); @@ -166,8 +181,13 @@ describe.concurrent("GM Api", () => { script.value = { test: "ok" }; script.metadata.grant = ["GM_getValue"]; script.code = `return GM_getValue("test");`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("ok"); @@ -177,8 +197,13 @@ describe.concurrent("GM Api", () => { script.value = { test: "ok" }; script.metadata.grant = ["GM.getValue"]; script.code = `return GM.getValue("test").then(v=>v+"!");`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("ok!"); @@ -189,8 +214,13 @@ describe.concurrent("GM Api", () => { script.value = { test1: "23", test2: "45", test3: "67" }; script.metadata.grant = ["GM_listValues"]; script.code = `return GM_listValues().join("-");`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("test1-test2-test3"); @@ -205,8 +235,13 @@ describe.concurrent("GM Api", () => { script.value.test1 = "40"; script.metadata.grant = ["GM_listValues"]; script.code = `return GM_listValues().join("-");`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("test5-test2-test3-test1"); // TM也没有sort @@ -217,8 +252,13 @@ describe.concurrent("GM Api", () => { script.value = { test1: "23", test2: "45", test3: "67" }; script.metadata.grant = ["GM.listValues"]; script.code = `return GM.listValues().then(v=>v.join("-"));`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("test1-test2-test3"); @@ -233,8 +273,13 @@ describe.concurrent("GM Api", () => { script.value.test1 = "40"; script.metadata.grant = ["GM.listValues"]; script.code = `return GM.listValues().then(v=>v.join("-"));`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual("test5-test2-test3-test1"); // TM也没有sort @@ -245,8 +290,13 @@ describe.concurrent("GM Api", () => { script.value = { test1: "23", test2: 45, test3: "67" }; script.metadata.grant = ["GM_getValues"]; script.code = `return GM_getValues(["test2", "test3", "test1"]);`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret.test1).toEqual("23"); @@ -266,8 +316,13 @@ describe.concurrent("GM Api", () => { script.value = { test1: "23", test2: 45, test3: "67" }; script.metadata.grant = ["GM.getValues"]; script.code = `return GM.getValues(["test2", "test3", "test1"]).then(v=>v);`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret.test1).toEqual("23"); @@ -283,8 +338,13 @@ describe.concurrent("early-script", () => { script.metadata["early-start"] = [""]; script.metadata["grant"] = ["CAT_scriptLoaded"]; script.code = `return CAT_scriptLoaded().then(()=>123);`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); // 抛出错误 await expect(exec.exec()).rejects.toThrowError(); @@ -296,8 +356,13 @@ describe.concurrent("early-script", () => { script.metadata["run-at"] = ["document-start"]; script.metadata["grant"] = ["CAT_scriptLoaded"]; script.code = `return CAT_scriptLoaded().then(()=>123);`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: undefined as any, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = exec.exec(); // 触发envInfo @@ -317,8 +382,13 @@ describe.concurrent("GM_menu", () => { const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); @@ -367,8 +437,13 @@ describe.concurrent("GM_menu", () => { const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "content", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = exec.exec(); // 验证 sendMessage 是否被调用 @@ -389,8 +464,13 @@ describe.concurrent("GM_menu", () => { const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); @@ -452,8 +532,13 @@ describe.concurrent("GM_menu", () => { const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "content", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual({ id1: "abc", id2: "abc", id3: 1, id4: 2, id5: "3", id6: 3, id7: 3, id8: 4 }); @@ -481,8 +566,13 @@ describe.concurrent("GM_value", () => { const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -611,8 +701,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "content", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -726,8 +821,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -849,8 +949,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -918,8 +1023,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -994,8 +1104,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); expect(mockSendMessage).toHaveBeenCalledTimes(1); @@ -1028,8 +1143,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); // remote = true const retPromise = exec.exec(); @@ -1054,8 +1174,13 @@ return { value1, value2, value3, values1,values2, allValues1, allValues2, value4 const mockMessage = { sendMessage: mockSendMessage, } as unknown as Message; - // @ts-ignore - const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, { + envPrefix: "scripting", + message: mockMessage, + contentMsg: undefined as any, + code: nilFn, + envInfo, + }); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index 874c4c1ff..61ff146be 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -69,6 +69,9 @@ class GM_Base implements IGM_Base { @GMContext.protected() protected message?: Message | null; + @GMContext.protected() + protected contentMsg!: Message; + // Extension Context 无效时释放 scriptRes @GMContext.protected() protected scriptRes?: ScriptRunResource | null; @@ -200,8 +203,9 @@ export default class GMApi extends GM_Base { constructor( public prefix: string, - public message: Message | undefined, - public scriptRes: ScriptRunResource | undefined + public message: Message, + public contentMsg: Message, + public scriptRes: ScriptRunResource ) { // testing only 仅供测试用 const valueChangeListener = new ListenerManager(); @@ -725,25 +729,22 @@ export default class GMApi extends GM_Base { if (typeof css !== "string") throw new Error("The parameter 'css' of GM_addStyle shall be a string."); // 与content页的消息通讯实际是同步,此方法不需要经过background // 这里直接使用同步的方式去处理, 不要有promise - const resp = (this.message).syncSendMessage({ - action: `${this.prefix}/runtime/gmApi`, + const resp = (this.contentMsg).syncSendMessage({ + action: `content/runtime/addElement`, data: { - uuid: this.scriptRes.uuid, - api: "GM_addElement", params: [ null, "style", { textContent: css, }, - isContent, ], }, }); if (resp.code) { throw new Error(resp.message); } - return (this.message).getAndDelRelatedTarget(resp.data) as Element; + return (this.contentMsg).getAndDelRelatedTarget(resp.data) as Element; } @GMContext.API({ depend: ["GM_addStyle"] }) @@ -758,41 +759,72 @@ export default class GMApi extends GM_Base { public GM_addElement( parentNode: Node | string, tagName: string | Record, - attrs: Record = {} + attrs: Record | null = {} ): Element | undefined { if (!this.message || !this.scriptRes) return; - // 与content页的消息通讯实际是同步,此方法不需要经过background + // 与content页的消息通讯实际是同步, 此方法不需要经过background // 这里直接使用同步的方式去处理, 不要有promise + // 在content脚本执行的话,与直接 DOM 无异 + // TrustedTypes 限制了对 DOM 的 innerHTML/outerHTML 的操作 (TrustedHTML) + // TrustedTypes 限制了对 script 的 innerHTML/outerHTML/textContent/innerText 的操作 (TrustedScript) + // CSP 限制了对 appendChild/insertChild/replaceChild/insertAdjacentElement ... 等DOM插入移除操作 + let parentNodeId: number | null; if (typeof parentNode !== "string") { - const id = (this.message).sendRelatedTarget(parentNode); + const id = (this.contentMsg).sendRelatedTarget(parentNode); parentNodeId = id; } else { parentNodeId = null; attrs = (tagName || {}) as Record; tagName = parentNode as string; } + if (typeof tagName !== "string") throw new Error("The parameter 'tagName' of GM_addElement shall be a string."); - if (typeof attrs !== "object") throw new Error("The parameter 'attrs' of GM_addElement shall be an object."); - const resp = (this.message).syncSendMessage({ - action: `${this.prefix}/runtime/gmApi`, + if (attrs !== null && typeof attrs !== "object") { + throw new Error("The parameter 'attrs' of GM_addElement shall be an object."); + } + + // 控制传送参数,避免参数出现 non-json-selizable + const attrsCT = {} as Record; + const setAttr = {} as Record; + for (const [key, value] of Object.entries(attrs as Record)) { + if (typeof value === "string" || typeof value === "number") { + // 数字不是标准的 attribute value type, 但常见于实际使用 + attrsCT[key] = value; + } else { + // property setter for non attribute (e.g. Function, Symbol, boolean, etc) + // Function, Symbol 无法跨环境传递 + setAttr[key] = value; + } + } + + // 使用contentMsg同步发送消息到content脚本,由content脚本创建元素并返回 + // 不使用message,因为message是在scripting环境处理的,会因为扩展的 CSP 而无法操作 DOM + const resp = (this.contentMsg).syncSendMessage({ + action: `content/runtime/addElement`, data: { - uuid: this.scriptRes.uuid, - api: "GM_addElement", - params: [parentNodeId, tagName, attrs, isContent], + params: [parentNodeId, tagName, attrsCT], }, }); if (resp.code) { throw new Error(resp.message); } - return (this.message).getAndDelRelatedTarget(resp.data) as Element; + + const el = (this.contentMsg).getAndDelRelatedTarget(resp.data) as Element; + // 设置属性 + for (const [key, value] of Object.entries(setAttr)) { + (el as any)[key] = value; + } + + // 回传元素 + return el; } @GMContext.API({ depend: ["GM_addElement"] }) public "GM.addElement"( parentNode: Node | string, tagName: string | Record, - attrs: Record = {} + attrs: Record | null = {} ): Promise { return new Promise((resolve) => { const ret = this.GM_addElement(parentNode, tagName, attrs); diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index 21b005450..c231213f6 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -32,7 +32,10 @@ export class ScriptExecutor { earlyScriptFlag: Set = new Set(); execScriptMap: Map = new Map(); - constructor(private msg: Message) {} + constructor( + private msg: Message, + private contentMsg: Message // 用于 content <-> content/inject 通讯 + ) {} emitEvent(data: EmitEventRequest) { // 转发给脚本 @@ -132,7 +135,13 @@ export class ScriptExecutor { execScriptEntry(scriptEntry: ExecScriptEntry) { const { scriptLoadInfo, scriptFunc, envInfo } = scriptEntry; - const execScript = new ExecScript(scriptLoadInfo, "scripting", this.msg, scriptFunc, envInfo); + const execScript = new ExecScript(scriptLoadInfo, { + envPrefix: "scripting", + message: this.msg, + contentMsg: this.contentMsg, + code: scriptFunc, + envInfo, + }); this.execScriptMap.set(scriptLoadInfo.uuid, execScript); const metadata = scriptLoadInfo.metadata || {}; const resource = scriptLoadInfo.resource; diff --git a/src/app/service/content/script_runtime.ts b/src/app/service/content/script_runtime.ts index e2aa00fb4..b93aa515d 100644 --- a/src/app/service/content/script_runtime.ts +++ b/src/app/service/content/script_runtime.ts @@ -6,16 +6,52 @@ import type { EmitEventRequest } from "../service_worker/types"; import type { GMInfoEnv, ValueUpdateDataEncoded } from "./types"; import type { ScriptEnvTag } from "@Packages/message/consts"; import { onInjectPageLoaded } from "./external"; +import type { CustomEventMessage } from "@Packages/message/custom_event_message"; export class ScriptRuntime { constructor( private readonly scripEnvTag: ScriptEnvTag, private readonly server: Server, private readonly msg: Message, - private readonly scriptExecutor: ScriptExecutor, - private readonly messageFlag: string + private readonly scriptExecutor: ScriptExecutor ) {} + // content环境的特殊初始化 + contentInit() { + this.server.on("runtime/addElement", (data: { params: [number | null, string, Record | null] }) => { + const [parentNodeId, tagName, tmpAttr] = data.params; + + const msg = this.msg as CustomEventMessage; + + // 取回 parentNode(如果存在) + let parentNode: Node | undefined; + if (parentNodeId) { + parentNode = msg.getAndDelRelatedTarget(parentNodeId) as Node | undefined; + } + + // 创建元素并设置属性 + const el = document.createElement(tagName); + const attr = tmpAttr ? { ...tmpAttr } : {}; + let textContent = ""; + if (attr.textContent) { + textContent = attr.textContent; + delete attr.textContent; + } + for (const key of Object.keys(attr)) { + el.setAttribute(key, attr[key]); + } + if (textContent) el.textContent = textContent; + + // 优先挂到 parentNode,否则挂到 head/body/任意节点 + const node = parentNode || document.head || document.body || document.querySelector("*"); + node.appendChild(el); + + // 返回节点引用 id,供另一侧再取回 + const nodeId = msg.sendRelatedTarget(el); + return nodeId; + }); + } + init() { this.server.on("runtime/emitEvent", (data: EmitEventRequest) => { // 转发给脚本 diff --git a/src/app/service/content/scripting.ts b/src/app/service/content/scripting.ts index dd24079ea..fa802ce52 100644 --- a/src/app/service/content/scripting.ts +++ b/src/app/service/content/scripting.ts @@ -95,39 +95,6 @@ export default class ScriptingRuntime { xhr.send(); }); } - case "GM_addElement": { - const [parentNodeId, tagName, tmpAttr, isContent] = data.params; - - // 根据来源选择不同的消息桥(content / inject) - const msg = isContent ? this.senderToContent : this.senderToInject; - - // 取回 parentNode(如果存在) - let parentNode: Node | undefined; - if (parentNodeId) { - parentNode = msg.getAndDelRelatedTarget(parentNodeId) as Node | undefined; - } - - // 创建元素并设置属性 - const el = document.createElement(tagName); - const attr = tmpAttr ? { ...tmpAttr } : {}; - let textContent = ""; - if (attr.textContent) { - textContent = attr.textContent; - delete attr.textContent; - } - for (const key of Object.keys(attr)) { - el.setAttribute(key, attr[key]); - } - if (textContent) el.textContent = textContent; - - // 优先挂到 parentNode,否则挂到 head/body/任意节点 - const node = parentNode || document.head || document.body || document.querySelector("*"); - node.appendChild(el); - - // 返回节点引用 id,供另一侧再取回 - const nodeId = msg.sendRelatedTarget(el); - return nodeId; - } case "GM_log": // 拦截 GM_log:直接打印到控制台(某些页面可能劫持 console.log) switch (data.params.length) { diff --git a/src/content.ts b/src/content.ts index 64c84ec13..397f99dc6 100644 --- a/src/content.ts +++ b/src/content.ts @@ -25,7 +25,8 @@ getEventFlag(messageFlag, (eventFlag: string) => { logger.logger().debug("content start"); const server = new Server("content", msg); - const scriptExecutor = new ScriptExecutor(msg); - const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, messageFlag); + const scriptExecutor = new ScriptExecutor(msg, new CustomEventMessage(`${eventFlag}${scriptEnvTag}`, true)); + const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor); + runtime.contentInit(); runtime.init(); }); diff --git a/src/inject.ts b/src/inject.ts index 82d391e4e..17ecaf9fd 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -25,8 +25,8 @@ getEventFlag(messageFlag, (eventFlag: string) => { logger.logger().debug("inject start"); const server = new Server("inject", msg); - const scriptExecutor = new ScriptExecutor(msg); - const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, messageFlag); + const scriptExecutor = new ScriptExecutor(msg, new CustomEventMessage(`${eventFlag}${ScriptEnvTag.content}`, true)); + const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor); runtime.init(); // inject环境,直接判断白名单,注入对外接口 diff --git a/tests/runtime/gm_api.test.ts b/tests/runtime/gm_api.test.ts index acfdc137c..e1840b447 100644 --- a/tests/runtime/gm_api.test.ts +++ b/tests/runtime/gm_api.test.ts @@ -135,7 +135,7 @@ describe.concurrent("测试GMApi环境 - XHR", async () => { addTestPermission(script.uuid); await new ScriptDAO().save(script); - const gmApi = new GMApi("serviceWorker", msg, { + const gmApi = new GMApi("serviceWorker", msg, undefined as any, { uuid: script.uuid, }); it.concurrent("test GM xhr - plain text", async () => { @@ -341,7 +341,7 @@ describe.concurrent("测试GMApi环境 - XHR", async () => { describe.concurrent("GM xmlHttpRequest", () => { const msg = initTestGMApi(); - const gmApi = new GMApi("serviceWorker", msg, { + const gmApi = new GMApi("serviceWorker", msg, undefined as any, { uuid: script.uuid, }); it.concurrent("get", () => { @@ -424,7 +424,7 @@ describe.concurrent("GM xmlHttpRequest", () => { describe("GM download", () => { const msg = initTestGMApi(); - const gmApi = new GMApi("serviceWorker", msg, { + const gmApi = new GMApi("serviceWorker", msg, undefined as any, { uuid: script.uuid, }); it("simple download", async () => {