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
2 changes: 1 addition & 1 deletion docs/references/terminology-zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
| 目前詞彙 | 建議判斷 | 目前受影響 key |
| --- | --- | --- |
| `目錄` | 指 filesystem directory 的介面動作用 `資料夾`;文章或文件的內容目錄仍用 `目錄`。 | `open_backup_dir`, `open_directory`, `script_operation_description`, `get_backup_dir_url_failed` |
| `恢復` | restore settings/default values 用 `還原`;resume operation 或 recover 依語意使用 `恢復` / `復原`。 | `exclude_on`, `restore_default_values` |
| `恢復` | restore settings/default values 用 `還原`;resume operation 或 recover 依語意使用 `恢復` / `復原`。 | `restore_default_values` |
| `拉取` | Git pull 可用 `拉取`;從雲端取得備份或資料的 UI 動作用 `下載` / `擷取` / `同步取得`。 | `pulling_data_from_cloud`, `pull_failed` |
| `保存` / `儲存` | UI 的 save 動作用 `儲存`;保存期限、保存證據或一般敘述仍可使用 `保存`。 | `tools_backup_content` |
| `設備` / `裝置` | 使用者持有、同步或連線的 device 介面用 `裝置`;equipment 或設備管理等語境仍可使用 `設備`。 | `setting_sync_content` |
Expand Down
9 changes: 5 additions & 4 deletions e2e/popup-matching-regressions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ async function verifyExcludeRoundTrip(
const script = initial.data.scriptList.find((item: { name: string }) => item.name === scriptName);
if (!script) throw new Error(`script missing initially: ${JSON.stringify(initial.data.scriptList)}`);

const host = new URL(url).host;
const exclude = await chrome.runtime.sendMessage({
action: "serviceWorker/script/excludeUrl",
data: { uuid: script.uuid, excludePattern: "*://sitea.test/*", remove: false },
action: "serviceWorker/script/excludeFromMatch",
data: { uuid: script.uuid, host, url },
});
if (exclude.code) throw new Error(`exclude failed: ${JSON.stringify(exclude)}`);

Expand All @@ -50,8 +51,8 @@ async function verifyExcludeRoundTrip(
if (!excluded) throw new Error("script disappeared from Popup after exclusion");

const unexclude = await chrome.runtime.sendMessage({
action: "serviceWorker/script/excludeUrl",
data: { uuid: script.uuid, excludePattern: "*://sitea.test/*", remove: true },
action: "serviceWorker/script/allowUrl",
data: { uuid: script.uuid, matchPattern: `*://${host}/*`, excludePattern: `*://${host}/*` },
});
if (unexclude.code) throw new Error(`unexclude failed: ${JSON.stringify(unexclude)}`);

Expand Down
8 changes: 2 additions & 6 deletions src/app/service/service_worker/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,6 @@ export class ScriptClient extends Client {
return this.doThrow("getScriptRunResourceByUUID", uuid);
}

excludeUrl(uuid: string, excludePattern: string, remove: boolean) {
return this.do("excludeUrl", { uuid, excludePattern, remove });
}

onlyRunOnUrl(uuid: string, matchPattern: string) {
return this.do("onlyRunOnUrl", { uuid, matchPattern });
}
Expand All @@ -113,8 +109,8 @@ export class ScriptClient extends Client {
return this.do("allowUrl", { uuid, matchPattern, excludePattern });
}

excludeFromMatch(uuid: string, matchPattern: string) {
return this.do("excludeFromMatch", { uuid, matchPattern });
excludeFromMatch(uuid: string, host: string, url: string) {
return this.do("excludeFromMatch", { uuid, host, url });
}

// 重置匹配项
Expand Down
21 changes: 18 additions & 3 deletions src/app/service/service_worker/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ describe.concurrent("RuntimeService - getPageScriptMatchingResultByUrl 脚本匹
});
});

it.concurrent("match 覆盖清空后不再保留此前的匹配规则", async () => {
it.concurrent("match 覆盖清空后此前的匹配规则不再生效,但仍以未生效列出", async () => {
const { runtime } = createRuntimeTestContext();
const script = createMockScript({
metadata: { match: ["https://www.example.com/*"] },
Expand All @@ -302,9 +302,24 @@ describe.concurrent("RuntimeService - getPageScriptMatchingResultByUrl 脚本匹
...script,
selfMetadata: { match: [] },
});
expect(await runtime.applyScriptMatchInfo(emptyMatchOverride)).toBeUndefined();
await runtime.applyScriptMatchInfo(emptyMatchOverride);

expect(runtime.getPageScriptMatchingResultByUrl("https://www.example.com/", true).has(script.uuid)).toBe(false);
expect(runtime.getPageScriptMatchingResultByUrl("https://www.example.com/").has(script.uuid)).toBe(false);
// 原始规则仍在匹配器内,Popup 才能把它列为未生效并给出「允许在此执行」的恢复入口
expect(runtime.getPageScriptMatchingResultByUrl("https://www.example.com/", true).get(script.uuid)?.effective).toBe(
false
);
});

it.concurrent("match 覆盖清空的脚本不应被注册(空规则会被 UserScripts API 退回成全站匹配)", async () => {
const { runtime } = createRuntimeTestContext();
(runtime as any).resource = { getScriptResourceValue: vi.fn().mockResolvedValue({}) };
const script = createMockScript({
metadata: { match: ["https://www.example.com/*"] },
selfMetadata: { match: [] },
});

expect(await runtime.buildAndSaveCompiledResourceFromScript(script)).toBeUndefined();
});

it.concurrent("空匹配覆盖时应删除持久化 CompiledResource 并注销旧注册", async () => {
Expand Down
11 changes: 7 additions & 4 deletions src/app/service/service_worker/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import Logger from "@App/app/logger/logger";
import type { GMInfoEnv, ValueUpdateDataEncoded } from "../content/types";
import { initLocalesPromise, localePath } from "@App/locales/locales";
import { DocumentationSite } from "@App/app/const";
import { extractUrlPatterns, RuleType, type URLRuleEntry } from "@App/pkg/utils/url_matcher";
import { extractUrlPatterns, RuleType, RuleTypeBit, type URLRuleEntry } from "@App/pkg/utils/url_matcher";
import { parseUserConfig } from "@App/pkg/utils/yaml";
import type { CompiledResource, Resource, ResourceType } from "@App/app/repo/resource";
import { CompiledResourceDAO, CompiledResourceNamespace } from "@App/app/repo/resource";
Expand Down Expand Up @@ -456,9 +456,9 @@ export class RuntimeService {
// 安装,启用,或earlyStartScript的value更新
const ret = await this.buildAndSaveCompiledResourceFromScript(script, true);
if (!ret) {
// 空匹配覆盖(match 与 include 均为空)时脚本不再匹配任何站点。
// 内存 matcher 已由 applyScriptMatchInfo 清空,这里再清掉持久化的 CompiledResource
// 并注销浏览器旧注册,否则 SW 重启后 waitInit 会信任旧资源、让旧范围复活。
// 空匹配覆盖(match 与 include 均为空)时脚本不再匹配任何站点。内存 matcher 里只剩
// 供 Popup 恢复用的原始规则,这里再清掉持久化的 CompiledResource 并注销浏览器旧注册,
// 否则 SW 重启后 waitInit 会信任旧资源、让旧范围复活。
await this.compiledResourceDAO.delete(script.uuid);
await this.unregistryPageScripts([script.uuid]);
return;
Expand Down Expand Up @@ -855,6 +855,9 @@ export class RuntimeService {
const resourceUrls = (script.metadata["require"] || []).map((res) => resources[res]?.url).filter((res) => res);
const scriptMatchInfo = await this.applyScriptMatchInfo(scriptRes);
if (!scriptMatchInfo) return undefined;
// 生效规则一条 inclusion 都不剩(用户把当前站点从匹配中移除后可能如此)时不能注册:
// getApiMatchesAndGlobs 对没有 match pattern 的规则集会退回 *://*/*,注册出去等于全站运行。
if (!scriptMatchInfo.scriptUrlPatterns.some((rule) => rule.ruleType & RuleTypeBit.INCLUSION)) return undefined;

const res = getUserScriptRegister(scriptMatchInfo);
const registerScript = res.registerScript;
Expand Down
164 changes: 76 additions & 88 deletions src/app/service/service_worker/script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,11 +723,11 @@ describe("ScriptClient 站点范围消息", () => {
const sendMessage = vi.fn().mockResolvedValue({ data: true });
const client = new ScriptClient({ sendMessage } as unknown as MessageSend);

await client.excludeFromMatch("script-uuid", "*://current.example/*");
await client.excludeFromMatch("script-uuid", "current.example", "https://current.example/page");

expect(sendMessage).toHaveBeenCalledWith({
action: "serviceWorker/script/excludeFromMatch",
data: { uuid: "script-uuid", matchPattern: "*://current.example/*" },
data: { uuid: "script-uuid", host: "current.example", url: "https://current.example/page" },
});
});
});
Expand Down Expand Up @@ -786,51 +786,10 @@ describe("ScriptService selfMetadata 用户覆盖", () => {
);
});

describe("excludeUrl - popup 排除/取消排除", () => {
it("取消最后一条排除后应保存空覆盖,而不是回落脚本自带的 exclude", async () => {
const script = createMockScript();
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://ads.script.com/*", remove: true });

expect(savedSelfMetadata()).toEqual({ exclude: [] });
});

it("取消排除后仍有其他规则时应保存剩余规则", async () => {
const script = createMockScript({
selfMetadata: { exclude: ["*://ads.script.com/*", "*://user.com/*"] },
});
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://user.com/*", remove: true });

expect(savedSelfMetadata()).toEqual({ exclude: ["*://ads.script.com/*"] });
});

it("排除新网站时应追加到覆盖中", async () => {
const script = createMockScript();
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://user.com/*", remove: false });

expect(savedSelfMetadata()).toEqual({ exclude: ["*://ads.script.com/*", "*://user.com/*"] });
});

it("已有用户排除覆盖时新增排除应同时保留作者与用户规则", async () => {
const script = createMockScript({
selfMetadata: { exclude: ["*://user-blocked.example/*"] },
});
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://new.example/*", remove: false });

expect(savedSelfMetadata()).toEqual({
exclude: ["*://ads.script.com/*", "*://user-blocked.example/*", "*://new.example/*"],
});
});
});

describe("popup 站点范围快捷操作", () => {
const host = "current.example";
const url = "https://current.example/page";

it("初始化时应注册排除已匹配站点操作", async () => {
const alarmsDescriptor = Object.getOwnPropertyDescriptor(chrome, "alarms");
Object.defineProperty(chrome, "alarms", {
Expand Down Expand Up @@ -894,17 +853,14 @@ describe("ScriptService selfMetadata 用户覆盖", () => {

const onlyRun = scriptService.onlyRunOnUrl({ uuid: stored.uuid, matchPattern: "*://current.example/*" });
await firstUpdateStarted;
const exclude = scriptService.excludeFromMatch({ uuid: stored.uuid, matchPattern: "*://current.example/*" });
const exclude = scriptService.excludeFromMatch({ uuid: stored.uuid, host, url });
await Promise.resolve();
await Promise.resolve();
releaseFirstUpdate();
await Promise.all([onlyRun, exclude]);

expect(stored.selfMetadata).toEqual({
match: [],
include: [],
exclude: ["*://ads.script.com/*", "*://current.example/*"],
});
// 串行执行才能让 excludeFromMatch 看到 onlyRunOnUrl 写入的匹配覆盖并把它移出
expect(stored.selfMetadata).toEqual({ match: [], include: [] });
});

it("并发 onlyRunOnUrl 与 resetMatch 应串行执行并保留两次读改写", async () => {
Expand Down Expand Up @@ -1012,86 +968,118 @@ describe("ScriptService selfMetadata 用户覆盖", () => {
});
});

it("排除已包含站点时应移出用户匹配并加入用户排除", async () => {
it("匹配中有当前站点的专属规则时应只移出匹配,不写入排除", async () => {
const script = createMockScript({
selfMetadata: {
match: ["*://allowed.example/*", "*://current.example/*"],
exclude: ["*://blocked.example/*"],
},
metadata: { match: ["*://current.example/*", "*://other.example/*"] },
selfMetadata: { exclude: ["*://blocked.example/*"] },
});
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" });
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });

// 作者 @exclude(ads.script.com)并入用户覆盖,避免用户覆盖整体替换作者规则
// 移出匹配后脚本已不在本站生效,无需再写排除,用户的排除列表保持原样
expect(savedSelfMetadata()).toEqual({
match: ["*://allowed.example/*"],
exclude: ["*://ads.script.com/*", "*://blocked.example/*", "*://current.example/*"],
match: ["*://other.example/*"],
exclude: ["*://blocked.example/*"],
});
});

it("排除最后一个用户匹配时应保留显式空匹配覆盖", async () => {
const script = createMockScript({ selfMetadata: { match: ["*://current.example/*"] } });
it("同一站点的多条路径匹配应一并移出", async () => {
const script = createMockScript({
metadata: { match: ["https://current.example/a*", "http://current.example/b*", "*://other.example/*"] },
});
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" });
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });

expect(savedSelfMetadata()).toEqual({
match: [],
exclude: ["*://ads.script.com/*", "*://current.example/*"],
});
expect(savedSelfMetadata()).toEqual({ match: ["*://other.example/*"] });
});

it("没有用户匹配覆盖时排除站点不应创建匹配覆盖", async () => {
const script = createMockScript();
it("移出的是最后一条匹配时应保留显式空匹配覆盖", async () => {
const script = createMockScript({ metadata: { match: ["*://current.example/*"] } });
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" });
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });

expect(savedSelfMetadata()).toEqual({ exclude: ["*://ads.script.com/*", "*://current.example/*"] });
expect(savedSelfMetadata()).toEqual({ match: [] });
});

it("已有空匹配覆盖时排除站点应保留空覆盖", async () => {
const script = createMockScript({ selfMetadata: { match: [] } });
it("通配匹配移不掉当前站点时应回退为写入排除", async () => {
const script = createMockScript({ metadata: { match: ["*://*/*"] } });
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" });
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });

expect(savedSelfMetadata()).toEqual({
match: [],
exclude: ["*://ads.script.com/*", "*://current.example/*"],
// 通配匹配删不掉单一站点,只有排除能真正关掉;同时不该创建匹配覆盖
expect(savedSelfMetadata()).toEqual({ exclude: ["*://current.example/*"] });
});

it("不应移除通配子域匹配,改以排除关掉当前子域", async () => {
const script = createMockScript({ metadata: { match: ["*://*.example.com/*"] } });
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

// 移除 *://*.example.com/* 会连兄弟子域一起关掉,超出「不在 www.example.com 执行」的范围
await scriptService.excludeFromMatch({
uuid: script.uuid,
host: "www.example.com",
url: "https://www.example.com/page",
});

expect(savedSelfMetadata()).toEqual({ exclude: ["*://www.example.com/*"] });
});

it("新增排除覆盖时应保留作者已有的排除规则", async () => {
it("@include 仍命中当前站点时应在移出匹配后补写排除", async () => {
const script = createMockScript({
metadata: { exclude: ["*://author-blocked.example/*"] },
selfMetadata: { match: ["*://current.example/*"] },
metadata: { match: ["*://current.example/*"], include: ["*://current.example/*"] },
});
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" });
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });

expect(savedSelfMetadata()).toEqual({
match: [],
exclude: ["*://author-blocked.example/*", "*://current.example/*"],
exclude: ["*://current.example/*"],
});
});

it("已有用户排除覆盖时排除站点应同时保留作者与用户排除规则", async () => {
it("仅在当前站点执行后再关掉当前站点应清空匹配并撤销来源标记", async () => {
const script = createMockScript({
metadata: { exclude: ["*://author-blocked.example/*"] },
selfMetadata: { match: ["*://current.example/*"], exclude: ["*://user-blocked.example/*"] },
selfMetadata: {
match: ["*://current.example/*"],
include: [],
[SELF_METADATA_ONLY_RUN_ON_URL]: ["*://current.example/*"],
},
});
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });

expect(savedSelfMetadata()).toEqual({ match: [], include: [] });
});

it("写入排除时应同时保留作者与用户已有的排除规则", async () => {
const script = createMockScript({
metadata: { match: ["*://*/*"], exclude: ["*://author-blocked.example/*"] },
selfMetadata: { exclude: ["*://user-blocked.example/*"] },
});
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeFromMatch({ uuid: script.uuid, matchPattern: "*://current.example/*" });
await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });

// 用户覆盖整体替换作者规则,因此写排除时须并入作者 @exclude,避免丢作者规则
expect(savedSelfMetadata()).toEqual({
match: [],
exclude: ["*://author-blocked.example/*", "*://user-blocked.example/*", "*://current.example/*"],
});
});

it("当前站点本就不在匹配范围内时不应写入任何覆盖", async () => {
const script = createMockScript({ metadata: { match: ["*://other.example/*"] } });
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeFromMatch({ uuid: script.uuid, host, url });

expect(mockScriptDAO.update).not.toHaveBeenCalled();
});
});

describe("resetMatch / resetExclude - 编辑器匹配列表", () => {
Expand Down
Loading
Loading