Skip to content
Open
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
20 changes: 19 additions & 1 deletion src/app/service/content/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,27 @@ describe("utils", () => {

expect(result).toBeDefined();
expect(result).toContain(
`GM_registerMenuCommand(("ScriptCat's demo for \\"context-menu\\""), ()=>{let GM_registerMenuCommand=window.GM_registerMenuCommand=GM.registerMenuCommand=undefined;\nconsole.log(567); // testing\n}, {nested:false});\n`
`GM_registerMenuCommand(("ScriptCat's demo for \\"context-menu\\""), ()=>{\nconsole.log(567); // testing\n}, {nested:false});\n`
);
});

it.concurrent("@run-at context-menu 的包装不得屏蔽脚本体自己的 GM_registerMenuCommand", () => {
const scriptRes = createMockScriptRes({
name: "menu registering script",
code: 'GM_registerMenuCommand("Own Item", () => {});',
metadata: {
"run-at": ["context-menu"],
},
});

const result = compileScriptCode(scriptRes);

// 曾经在回调开头把 GM_registerMenuCommand 连同 window./GM. 上的引用一起置为 undefined,
// 于是任何在脚本体里注册菜单的脚本一点菜单就 TypeError 中断,自己的菜单项也永远注册不上
expect(result).not.toContain("GM_registerMenuCommand=undefined");
expect(result).not.toContain("window.GM_registerMenuCommand");
expect(result).not.toContain("GM.registerMenuCommand");
});
});

describe("compileScript", () => {
Expand Down
3 changes: 2 additions & 1 deletion src/app/service/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ export function compileScriptCodeByResource(resource: CompileScriptCodeResource)
// 在UserScripts API中,由于执行不是在物件导向里呼叫,使用arrow function的话会把this改变。须使用 .call(this) [ 或 .bind(this)() ]

if (resource.isContextMenu) {
code = `GM_registerMenuCommand((${JSON.stringify(resource.name)}), ()=>{let GM_registerMenuCommand=window.GM_registerMenuCommand=GM.registerMenuCommand=undefined;\n${code}\n}, {nested:false});\n`;
// 脚本体整体延后到菜单回调里执行,它自己的 GM_registerMenuCommand 也随之推迟到点击后才注册
code = `GM_registerMenuCommand((${JSON.stringify(resource.name)}), ()=>{\n${code}\n}, {nested:false});\n`;
}

const joinedCode = [
Expand Down
6 changes: 5 additions & 1 deletion src/app/service/service_worker/gm_api/gm_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import type {
} from "../types";
import type { TScriptMenuRegister, TScriptMenuUnregister } from "../../queue";
import type { NotificationOptionCache } from "../utils";
import { BrowserNoSupport, notificationsUpdate } from "../utils";
import { BrowserNoSupport, getCombinedMeta, notificationsUpdate } from "../utils";
import {
getSkillScriptGrantsByUuid,
getSkillScriptNameByUuid,
Expand Down Expand Up @@ -416,6 +416,10 @@ export default class GMApi {
if (!script) {
throw new Error("script is not found");
}
// 设置面板改的运行时机等只写 selfMetadata,GM API 校验要看合并后的生效值(#1649)
if (script.selfMetadata) {
script = { ...script, metadata: getCombinedMeta(script.metadata, script.selfMetadata) };
}
}
// 订阅脚本的 connect 使用订阅声明的 connect 覆盖脚本自身的
if (script.subscribeUrl) {
Expand Down
81 changes: 81 additions & 0 deletions src/app/service/service_worker/gm_api/gm_api_self_metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, it, expect, beforeEach } from "vitest";
import { ScriptDAO, SCRIPT_TYPE_NORMAL, SCRIPT_STATUS_ENABLE, SCRIPT_RUN_STATUS_COMPLETE } from "@App/app/repo/scripts";
import type { Script } from "@App/app/repo/scripts";
import GMApi, { MockGMExternalDependencies } from "./gm_api";
import { initTestEnv } from "@Tests/utils";
import { MockMessage } from "@Packages/message/mock_message";
import { Server, type IGetSender } from "@Packages/message/server";
import type { ExtMessageSender } from "@Packages/message/types";
import EventEmitter from "eventemitter3";
import { MessageQueue } from "@Packages/message/message_queue";
import { SystemConfig } from "@App/pkg/config/config";
import PermissionVerify, { PermissionVerifyApiGet } from "../permission_verify";
import type { ValueService } from "../value";

initTestEnv();

const makeSender = (): IGetSender => ({
getSender: () => ({}) as chrome.runtime.MessageSender,
getType: () => 0,
isType: (_type: any) => false,
getExtMessageSender: () => null as unknown as ExtMessageSender,
getConnect: () => undefined,
});

const createGMApi = () => {
const ee = new EventEmitter<string, any>();
const message = new MockMessage(ee);
const messageQueue = new MessageQueue();
const systemConfig = new SystemConfig(messageQueue);
const server = new Server("serviceWorker", message);
const permissionVerify = new PermissionVerify(server.group("permissionVerify"), messageQueue);
const gmApi = new GMApi(
systemConfig,
permissionVerify,
server.group("runtime"),
message,
messageQueue,
{} as ValueService,
new MockGMExternalDependencies()
);
return { gmApi, permissionVerify };
};

// 设置面板把运行时机改成 context-menu 只写 selfMetadata,脚本自带 metadata 不变;
// GM API 请求若只带自带 metadata,免 @grant 的菜单豁免就判不出来,菜单项注册不上(#1649)。
describe("parseRequest 用户覆写的 metadata", () => {
let scriptDAO: ScriptDAO;

beforeEach(() => {
scriptDAO = new ScriptDAO();
});

it("selfMetadata 覆写 run-at=context-menu 的脚本,GM_registerMenuCommand 无需 @grant 即可通过校验", async () => {
const script: Script = {
uuid: "uuid-context-menu-override",
name: "test-script",
namespace: "test",
metadata: { grant: ["none"], "run-at": ["document-idle"] },
selfMetadata: { "run-at": ["context-menu"] },
type: SCRIPT_TYPE_NORMAL,
status: SCRIPT_STATUS_ENABLE,
sort: 0,
runStatus: SCRIPT_RUN_STATUS_COMPLETE,
createtime: Date.now(),
checktime: Date.now(),
};
await scriptDAO.save(script);
const { gmApi, permissionVerify } = createGMApi();

const req = await gmApi.parseRequest({
uuid: script.uuid,
api: "GM_registerMenuCommand",
runFlag: "",
params: [],
});

await expect(
permissionVerify.verify(req, PermissionVerifyApiGet("GM_registerMenuCommand")!, makeSender(), gmApi)
).resolves.toBe(true);
});
});
80 changes: 80 additions & 0 deletions src/app/service/service_worker/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1192,3 +1192,83 @@ describe("MQ 事件处理效果(enableScripts / deleteScripts / sortedScripts
expect((runtime as any).cachedPatterns.has(uuid)).toBe(false);
});
});

// ─────────────────────────────────────────────────────────────────────────────

describe("restoreJSCodeFromCompiledResource 还原代码时的生效 metadata", () => {
// 设置面板改运行时机只写 selfMetadata,脚本自带 metadata 原封不动;
// 还原路径若只看自带 metadata,重新注册后用户覆写就会静默失效(#1649)。
const createContext = (script: Script) => {
const { runtime, mockScriptService } = _createRuntimeContext();
const scriptRes = _createScriptRunResource(script);
const compiledResource: CompiledResource = {
name: script.name,
flag: `#-${script.uuid}`,
uuid: script.uuid,
require: [],
matches: ["https://www.example.com/*"],
includeGlobs: [],
excludeMatches: [],
excludeGlobs: [],
allFrames: false,
world: "MAIN",
runAt: "document_idle",
scriptUrlPatterns: scriptURLPatternResults(scriptRes)!.scriptUrlPatterns,
originalUrlPatterns: null,
};
mockScriptService.buildScriptRunResource.mockResolvedValue(scriptRes);
(runtime as any).script = {
...mockScriptService,
scriptCodeDAO: { get: vi.fn().mockResolvedValue({ code: "console.log(1);" }) },
};
(runtime as any).resource = { resourceDAO: { get: vi.fn().mockResolvedValue(undefined) } };
return { runtime, compiledResource };
};

it("selfMetadata 覆写 run-at=context-menu 时,还原的代码应包裹 GM_registerMenuCommand", async () => {
const script = _createMockScript({
metadata: { match: ["https://www.example.com/*"], "run-at": ["document-idle"] },
selfMetadata: { "run-at": ["context-menu"] },
});
const { runtime, compiledResource } = createContext(script);

const code = await runtime.restoreJSCodeFromCompiledResource(script, compiledResource);

expect(code).toContain("GM_registerMenuCommand");
});

it("selfMetadata 覆写为 early-start 时,还原的代码应走预注入编译", async () => {
const script = _createMockScript({
metadata: { match: ["https://www.example.com/*"], "run-at": ["document-idle"] },
selfMetadata: { "early-start": [""], "run-at": ["document-start"] },
});
const { runtime, compiledResource } = createContext(script);

const code = await runtime.restoreJSCodeFromCompiledResource(script, compiledResource);

expect(code).toContain("performance.dispatchEvent");
});
});

describe("pushValueUpdate 判断是否需要为 early-start 脚本重新编译", () => {
// early-start 会把 GM 值编进预注入代码,值变了必须重编;
// 该脚本的 early-start 可能来自用户覆写,不能只看脚本自带 metadata。
it("selfMetadata 覆写为 early-start 的脚本,值更新后应重新编译注册", async () => {
const { runtime } = _createRuntimeContext();
const script = _createMockScript({
metadata: { match: ["https://www.example.com/*"], "run-at": ["document-idle"] },
selfMetadata: { "early-start": [""], "run-at": ["document-start"] },
});
const updateSpy = vi.spyOn(runtime, "updateResourceOnScriptChange").mockResolvedValue(undefined);

await runtime.pushValueUpdate(script, {
entries: [],
uuid: script.uuid,
storageName: "test-storage",
sender: { runFlag: "", tabId: -1 },
valueUpdated: true,
});

expect(updateSpy).toHaveBeenCalledWith(script);
});
});
16 changes: 12 additions & 4 deletions src/app/service/service_worker/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { runScript, stopScript } from "../offscreen/client";
import {
buildScriptRunResourceBasic,
compileInjectionCode,
getCombinedMeta,
getUserScriptRegister,
parseUrlSRI,
scriptURLPatternResults,
Expand Down Expand Up @@ -485,7 +486,10 @@ export class RuntimeService {

// valueUpdate 消息用于 early script 的处理
if (sendData.valueUpdated) {
if (script.status === SCRIPT_STATUS_ENABLE && isEarlyStartScript(script.metadata)) {
if (
script.status === SCRIPT_STATUS_ENABLE &&
isEarlyStartScript(getCombinedMeta(script.metadata, script.selfMetadata))
) {
// 如果是预加载脚本,需要更新脚本代码重新注册
// scriptMatchInfo 里的 value 改变 => compileInjectionCode -> injectionCode 改变
await this.updateResourceOnScriptChange(script);
Expand Down Expand Up @@ -902,15 +906,19 @@ export class RuntimeService {

// 从CompiledResource中还原脚本代码
async restoreJSCodeFromCompiledResource(script: Script, result: CompiledResource) {
// 用户在设置面板改运行时机只写 selfMetadata,脚本自带 metadata 不变,
// 所以编译分支必须按合并后的生效 metadata 选,否则重新注册会丢掉覆写(#1649)
const metadata = getCombinedMeta(script.metadata, script.selfMetadata);

// 如果是 Scriptlet (unwrap) 脚本,需要另外的处理方式
if (isScriptletUnwrap(script.metadata)) {
if (isScriptletUnwrap(metadata)) {
const scriptRes = await this.script.buildScriptRunResource(script);
if (!scriptRes) return "";
return compileScriptletCode(scriptRes, scriptRes.code, result.scriptUrlPatterns);
}

// 如果是预加载脚本,需要另外的处理方式
if (isEarlyStartScript(script.metadata)) {
if (isEarlyStartScript(metadata)) {
const scriptRes = await this.script.buildScriptRunResource(script);
if (!scriptRes) return "";
return compileInjectionCode(scriptRes, scriptRes.code, result.scriptUrlPatterns);
Expand All @@ -931,7 +939,7 @@ export class RuntimeService {
name: result.name,
code: originalCode?.code || "",
require,
isContextMenu: isContextMenuScript(script.metadata),
isContextMenu: isContextMenuScript(metadata),
})
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/service/service_worker/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export async function notificationsUpdate(
}
}

export function getCombinedMeta(metaBase: SCMetadata, metaCustom: SCMetadata): SCMetadata {
export function getCombinedMeta(metaBase: SCMetadata, metaCustom: SCMetadata | undefined): SCMetadata {
const metaRet = { ...metaBase };
if (!metaCustom) {
return metaRet;
Expand Down
Loading