Skip to content

Commit a77effb

Browse files
CodFrmcyfung1031
andauthored
🐛 early脚本处理url匹配问题 (#1096)
* early脚本处理url匹配问题 * 处理lint问题 * 根据copilot意见修改 * 代码整理及修正 * 单元测试加多点 rules * 优化代码 --------- Co-authored-by: cyfung1031 <44498510+cyfung1031@users.noreply.github.com>
1 parent 324ce51 commit a77effb

File tree

6 files changed

+179
-39
lines changed

6 files changed

+179
-39
lines changed

src/app/repo/scripts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Repo } from "./repo";
22
import type { Resource } from "./resource";
33
import type { SCMetadata } from "./metadata";
44
import type { GMInfoEnv } from "../service/content/types";
5+
import type { URLRuleEntry } from "@App/pkg/utils/url_matcher";
56

67
// 脚本模型
78
export type SCRIPT_TYPE = 1 | 2 | 3;
@@ -113,6 +114,7 @@ export interface ScriptLoadInfo extends ScriptRunResource {
113114
userConfigStr: string;
114115
/** 用户配置对象(可选) */
115116
userConfig?: UserConfig;
117+
scriptUrlPatterns?: URLRuleEntry[];
116118
}
117119

118120
/**

src/app/service/content/script_executor.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import type { EmitEventRequest } from "../service_worker/types";
44
import ExecScript from "./exec_script";
55
import type { GMInfoEnv, ScriptFunc, ValueUpdateDataEncoded } from "./types";
66
import { addStyleSheet, definePropertyListener } from "./utils";
7-
import type { TScriptInfo } from "@App/app/repo/scripts";
7+
import type { ScriptLoadInfo, TScriptInfo } from "@App/app/repo/scripts";
88
import { DefinedFlags } from "../service_worker/runtime.consts";
9+
import { isUrlExcluded } from "@App/pkg/utils/match";
910

1011
export type ExecScriptEntry = {
1112
scriptLoadInfo: TScriptInfo;
@@ -91,10 +92,28 @@ export class ScriptExecutor {
9192
// 监听 脚本加载
9293
// 适用于此「通知环境加载完成」代码执行后的脚本加载
9394
performance.addEventListener(scriptLoadCompleteEvtName, (ev) => {
94-
const detail = (ev as CustomEvent).detail;
95+
const detail = (ev as CustomEvent).detail as {
96+
scriptFlag: string;
97+
scriptInfo: ScriptLoadInfo;
98+
};
9599
const scriptFlag = detail?.scriptFlag;
96100
if (typeof scriptFlag === "string") {
97101
ev.preventDefault(); // dispatchEvent 会回传 false -> 分离环境也能得知环境加载代码已执行
102+
// 检查是否有 urlPattern,有则执行匹配再决定是否略过注入
103+
if (detail.scriptInfo.scriptUrlPatterns) {
104+
// 以 REGEX 情况为例
105+
// "@include /REGEX/" 的情况下,MV3 UserScripts API 基础匹配范围扩大,会比实际需要的广阔,然后在 earlyScript 把不符合 REGEX 的除去
106+
// (All @include = false -> 除去)
107+
// 注:如果 @include 混合了 regex 跟 一般的,即使 regex 的 @include 不匹对当前网址,但匹对了一般 @include 也视为有效
108+
// 相反如果 @include 混合了 regex 跟 一般的,regex 的 @include 匹对了即可
109+
// "@exclude /REGEX/" 的情况下,MV3 UserScripts API 基础匹配范围不会扩大,然后在 earlyScript 把符合 REGEX 的匹配除去
110+
// (Any @exclude = true -> 除去)
111+
// 注:如果一早已被除排,根本不会被 MV3 UserScripts API 注入。所以只考虑排除「多余的匹配」。(略过注入)
112+
if (isUrlExcluded(window.location.href, detail.scriptInfo.scriptUrlPatterns)) {
113+
// 「多余的匹配」-> 略过注入
114+
return;
115+
}
116+
}
98117
this.execEarlyScript(scriptFlag, detail.scriptInfo, envInfo);
99118
}
100119
});

src/app/service/service_worker/runtime.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import { scriptToMenu, type TPopupPageLoadInfo } from "./popup_scriptmenu";
5353

5454
// 避免使用版本号控制导致代码理解混乱
5555
// 用来清除 UserScript API 里的旧缓存
56-
const USERSCRIPTS_REGISTER_CONTROL = "92292a62-4e81-4dc3-87d0-cb0f0cb9883d";
56+
const USERSCRIPTS_REGISTER_CONTROL = "a5564f38-d9b3-43d0-8520-3a2950d6a61d";
5757

5858
const ORIGINAL_URLMATCH_SUFFIX = "{ORIGINAL}"; // 用于标记原始URLPatterns的后缀
5959

@@ -666,7 +666,12 @@ export class RuntimeService {
666666

667667
let jsCode = "";
668668
if (withCode) {
669-
const code = compileInjectionCode(this.getMessageFlag(), scriptRes, scriptRes.code);
669+
const code = compileInjectionCode(
670+
this.getMessageFlag(),
671+
scriptRes,
672+
scriptRes.code,
673+
scriptMatchInfo.scriptUrlPatterns
674+
);
670675
registerScript.js[0].code = jsCode = code;
671676
}
672677

@@ -709,7 +714,7 @@ export class RuntimeService {
709714
if (earlyScript) {
710715
const scriptRes = await this.script.buildScriptRunResource(script);
711716
if (!scriptRes) return "";
712-
return compileInjectionCode(this.getMessageFlag(), scriptRes, scriptRes.code);
717+
return compileInjectionCode(this.getMessageFlag(), scriptRes, scriptRes.code, result.scriptUrlPatterns);
713718
}
714719

715720
const originalCode = await this.script.scriptCodeDAO.get(result.uuid);
@@ -1032,7 +1037,7 @@ export class RuntimeService {
10321037
// 该网址没有任何脚本匹配,包括排除匹配
10331038
if (!matchingResult.size) return null;
10341039

1035-
const enableScriptList = [] as ScriptLoadInfo[];
1040+
const enableScriptList = [] as (ScriptLoadInfo & { scriptUrlPatterns: URLRuleEntry[] })[];
10361041

10371042
const uuids = [...matchingResult.keys()];
10381043

@@ -1196,7 +1201,12 @@ export class RuntimeService {
11961201
const scriptRes = scriptsWithUpdatedResources.get(targetUUID);
11971202
const scriptDAOCode = scriptCodes[targetUUID];
11981203
if (scriptRes && scriptDAOCode) {
1199-
const scriptInjectCode = compileInjectionCode(this.getMessageFlag(), scriptRes, scriptDAOCode);
1204+
const scriptInjectCode = compileInjectionCode(
1205+
this.getMessageFlag(),
1206+
scriptRes,
1207+
scriptDAOCode,
1208+
scriptRes.scriptUrlPatterns!
1209+
);
12001210
scriptRegisterInfo.js = [
12011211
{
12021212
code: scriptInjectCode,

src/app/service/service_worker/utils.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export const BrowserNoSupport = new Error("browserNoSupport");
2-
import type { SCMetadata, Script, ScriptRunResource } from "@App/app/repo/scripts";
2+
import type { SCMetadata, Script, ScriptLoadInfo, ScriptRunResource } from "@App/app/repo/scripts";
33
import { getMetadataStr, getUserConfigStr } from "@App/pkg/utils/utils";
4-
import type { ScriptLoadInfo, ScriptMatchInfo } from "./types";
4+
import type { ScriptMatchInfo } from "./types";
55
import {
66
compileInjectScript,
77
compilePreInjectScript,
@@ -169,22 +169,41 @@ export function selfMetadataUpdate(script: Script, key: string, valueSet: Set<st
169169
return script;
170170
}
171171

172-
export function parseScriptLoadInfo(script: ScriptRunResource): ScriptLoadInfo {
172+
export function parseScriptLoadInfo(script: ScriptRunResource, scriptUrlPatterns: URLRuleEntry[]): ScriptLoadInfo {
173173
const metadataStr = getMetadataStr(script.code) || "";
174174
const userConfigStr = getUserConfigStr(script.code) || "";
175+
// 判断是否有正则表达式类型的 URLPattern
176+
let hasRegex = false;
177+
for (const pattern of scriptUrlPatterns) {
178+
if (pattern.ruleType === RuleType.REGEX_INCLUDE || pattern.ruleType === RuleType.REGEX_EXCLUDE) {
179+
hasRegex = true;
180+
break;
181+
}
182+
}
175183
return {
176184
...script,
177185
metadataStr,
178186
userConfigStr,
187+
// 如有 regex, 需要在 runtime 期间对整个 scriptUrlPatterns (包括但不限于 REGEX )进行测试
188+
scriptUrlPatterns: hasRegex ? scriptUrlPatterns : undefined,
179189
};
180190
}
181191

182-
export function compileInjectionCode(messageFlag: string, scriptRes: ScriptRunResource, scriptCode: string) {
192+
export function compileInjectionCode(
193+
messageFlag: string,
194+
scriptRes: ScriptRunResource,
195+
scriptCode: string,
196+
scriptUrlPatterns: URLRuleEntry[]
197+
): string {
183198
const preDocumentStartScript = isEarlyStartScript(scriptRes.metadata);
184199
let scriptInjectCode;
185200
scriptCode = compileScriptCode(scriptRes, scriptCode);
186201
if (preDocumentStartScript) {
187-
scriptInjectCode = compilePreInjectScript(messageFlag, parseScriptLoadInfo(scriptRes), scriptCode);
202+
scriptInjectCode = compilePreInjectScript(
203+
messageFlag,
204+
parseScriptLoadInfo(scriptRes, scriptUrlPatterns),
205+
scriptCode
206+
);
188207
} else {
189208
scriptInjectCode = compileInjectScript(scriptRes, scriptCode);
190209
}

src/pkg/utils/match.test.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "vitest";
2-
import { UrlMatch } from "./match";
2+
import { isUrlExcluded, isUrlIncluded, UrlMatch } from "./match";
33
import { v4 as uuidv4 } from "uuid";
44
import { extractUrlPatterns } from "./url_matcher";
55

@@ -177,7 +177,7 @@ describe.concurrent("UrlMatch-google", () => {
177177
url.addMatch("https://example.org/foo/bar.html", "ok4");
178178
url.addMatch("http://127.0.0.1/*", "ok5");
179179
url.addMatch("*://mail.google.com/*", "ok6");
180-
url.exclude("https://example-2.org/foo/bar.html", "ok1");
180+
url.addExclude("https://example-2.org/foo/bar.html", "ok1");
181181
it.concurrent("match1", () => {
182182
expect(url.urlMatch("https://www.google.com/")).toEqual(["ok1"]);
183183
expect(url.urlMatch("https://example.org/foo/bar.html")).toEqual(["ok1", "ok2", "ok4"]);
@@ -386,7 +386,7 @@ describe.concurrent("UrlMatch-exclude", () => {
386386
it.concurrent("exclue-port", () => {
387387
const url = new UrlMatch<string>();
388388
url.addInclude("*://*/*", "ok3");
389-
url.exclude("*:5244*", "ok3");
389+
url.addExclude("*:5244*", "ok3");
390390
expect(url.urlMatch("http://test.list.ggnb.top:5244/search")).toEqual([]);
391391
expect(url.urlMatch("http://test.list.ggnb.top:80/search")).toEqual(["ok3"]);
392392
});
@@ -704,9 +704,9 @@ describe.concurrent("UrlInclude-1", () => {
704704
url.clearRules("ok10");
705705
url.addMatch("*://*.x.com/*", "ok10"); // @match *://*.x.com/*
706706
expect(url.urlMatch("https://x.com/trump_chinese")).toEqual(["ok10"]); // 与TM一致
707-
url.exclude("*://*.x.com/*", "ok10"); // @exclude *://*.x.com/*
707+
url.addExclude("*://*.x.com/*", "ok10"); // @exclude *://*.x.com/*
708708
expect(url.urlMatch("https://x.com/trump_chinese")).toEqual(["ok10"]); // 与TM一致
709-
url.exclude("*://*x.com/*", "ok10"); // @exclude *://*x.com/*
709+
url.addExclude("*://*x.com/*", "ok10"); // @exclude *://*x.com/*
710710
expect(url.urlMatch("https://x.com/trump_chinese")).toEqual([]); // 与TM一致
711711
});
712712

@@ -832,3 +832,60 @@ describe.concurrent("@include * (all)", () => {
832832
expect(url.urlMatch("http://109.70.80.1:40/?#page")).toEqual(["ok1"]);
833833
});
834834
});
835+
836+
describe.concurrent("urlExclude urlMatch 1", () => {
837+
const url = new UrlMatch<string>();
838+
url.addInclude("*://*.example.com/*", "ok1");
839+
url.addExclude("*://sub.example.com/*", "ok1");
840+
it.concurrent("exclude-subdomain", () => {
841+
expect(isUrlExcluded("http://www.example.com/", url.rulesMap.get("ok1")!)).toEqual(false);
842+
expect(isUrlExcluded("http://sub.example.com/", url.rulesMap.get("ok1")!)).toEqual(true);
843+
844+
expect(isUrlIncluded("http://www.example.com/", url.rulesMap.get("ok1")!)).toEqual(true);
845+
expect(isUrlIncluded("http://sub.example.com/", url.rulesMap.get("ok1")!)).toEqual(false);
846+
});
847+
});
848+
849+
describe.concurrent("@exclude /REGEX/", () => {
850+
const url = new UrlMatch<string>();
851+
url.addInclude("*://*.dummy1.com/*", "ok1");
852+
url.addExclude("*://sub.dummy1.com/*", "ok1");
853+
url.addInclude("*://*.dummy2.com/*", "ok2");
854+
url.addExclude("*://sub.dummy2.com/*", "ok2");
855+
url.addInclude("*://*.example.com/*", "ok1");
856+
url.addExclude("*://sub.example.com/*", "ok1");
857+
url.addExclude("/h\\d\\.example\\.com/", "ok1");
858+
it.concurrent("test R1", () => {
859+
expect(isUrlExcluded("http://www.example.com/", url.rulesMap.get("ok1")!)).toEqual(false);
860+
expect(isUrlExcluded("http://sub.example.com/", url.rulesMap.get("ok1")!)).toEqual(true);
861+
expect(isUrlExcluded("http://h7.example.com/", url.rulesMap.get("ok1")!)).toEqual(true);
862+
expect(isUrlExcluded("http://hl.example.com/", url.rulesMap.get("ok1")!)).toEqual(false);
863+
864+
expect(isUrlIncluded("http://www.example.com/", url.rulesMap.get("ok1")!)).toEqual(true);
865+
expect(isUrlIncluded("http://sub.example.com/", url.rulesMap.get("ok1")!)).toEqual(false);
866+
expect(isUrlIncluded("http://h7.example.com/", url.rulesMap.get("ok1")!)).toEqual(false);
867+
expect(isUrlIncluded("http://hl.example.com/", url.rulesMap.get("ok1")!)).toEqual(true);
868+
});
869+
});
870+
871+
describe.concurrent("@include /REGEX/", () => {
872+
const url = new UrlMatch<string>();
873+
url.addInclude("*://*.dummy1.com/*", "ok1");
874+
url.addExclude("*://sub.dummy1.com/*", "ok1");
875+
url.addInclude("*://*.dummy2.com/*", "ok2");
876+
url.addExclude("*://sub.dummy2.com/*", "ok2");
877+
url.addInclude("*://*.example.com/*", "ok1");
878+
url.addExclude("*://sub.example.com/*", "ok1");
879+
url.addInclude("/\\.h\\dample\\.com/", "ok1");
880+
it.concurrent("test R2", () => {
881+
expect(isUrlExcluded("http://www.example.com/", url.rulesMap.get("ok1")!)).toEqual(false);
882+
expect(isUrlExcluded("http://sub.example.com/", url.rulesMap.get("ok1")!)).toEqual(true);
883+
expect(isUrlExcluded("http://www.h7ample.com/", url.rulesMap.get("ok1")!)).toEqual(false);
884+
expect(isUrlExcluded("http://www.hlample.com/", url.rulesMap.get("ok1")!)).toEqual(true);
885+
886+
expect(isUrlIncluded("http://www.example.com/", url.rulesMap.get("ok1")!)).toEqual(true);
887+
expect(isUrlIncluded("http://sub.example.com/", url.rulesMap.get("ok1")!)).toEqual(false);
888+
expect(isUrlIncluded("http://www.h7ample.com/", url.rulesMap.get("ok1")!)).toEqual(true);
889+
expect(isUrlIncluded("http://www.hlample.com/", url.rulesMap.get("ok1")!)).toEqual(false);
890+
});
891+
});

src/pkg/utils/match.ts

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,12 @@ export class UrlMatch<T> {
1616
public urlMatch(url: string): T[] {
1717
const cacheMap = this.cacheMap;
1818
if (cacheMap.has(url)) return cacheMap.get(url) as T[];
19-
const s = new Set<T>();
20-
for (const [uuid, rules] of this.rulesMap.entries()) {
21-
let ruleIncluded = false;
22-
let ruleExcluded = false;
23-
for (const rule of rules) {
24-
if (rule.ruleType & RuleTypeBit.INCLUSION) {
25-
// include
26-
if (!ruleIncluded && isUrlMatch(url, rule)) {
27-
ruleIncluded = true;
28-
}
29-
} else {
30-
// exclude
31-
if (!ruleExcluded && !isUrlMatch(url, rule)) {
32-
ruleExcluded = true;
33-
break;
34-
}
35-
}
36-
}
37-
if (ruleIncluded && !ruleExcluded) {
38-
s.add(uuid);
19+
const res: T[] = [];
20+
for (const [uuid, rules] of this.rulesMap) {
21+
if (isUrlIncluded(url, rules)) {
22+
res.push(uuid);
3923
}
4024
}
41-
const res = [...s];
4225
const sorter = this.sorter;
4326
if (sorter !== null && typeof sorter === "object" && typeof res[0] === "string") {
4427
(res as string[]).sort((a, b) => {
@@ -74,7 +57,7 @@ export class UrlMatch<T> {
7457
}
7558

7659
// 测试用
77-
public exclude(rulePattern: string, uuid: T) {
60+
public addExclude(rulePattern: string, uuid: T) {
7861
// @exclude xxxxx
7962
const rules = extractUrlPatterns([rulePattern].map((e) => `@exclude ${e}`));
8063
this.addRules(uuid, rules);
@@ -88,6 +71,56 @@ export class UrlMatch<T> {
8871
}
8972
}
9073

74+
// 检查单一网址是否符合 Inclusion 原则
75+
// 即匹配任一@include/@match且不匹配任何@exclude
76+
export function isUrlIncluded(url: string, rules: URLRuleEntry[]): boolean {
77+
let anyInclusionRule = false;
78+
let anyExclusionRule = false;
79+
for (const rule of rules) {
80+
if (rule.ruleType & RuleTypeBit.INCLUSION) {
81+
// include
82+
if (!anyInclusionRule && isUrlMatch(url, rule)) {
83+
// 符合 inclusion
84+
anyInclusionRule = true;
85+
}
86+
} else {
87+
// exclude
88+
if (!isUrlMatch(url, rule)) {
89+
// 符合 exclusion
90+
anyExclusionRule = true;
91+
break;
92+
}
93+
}
94+
}
95+
// true 条件: ( Any @include/@match = true ) AND ( All @exclude = false )
96+
return anyInclusionRule && !anyExclusionRule;
97+
}
98+
99+
// 检查单一网址是否符合 Exclusion 原则
100+
// 即匹配任何@exclude或所有@include/@match皆不匹配
101+
export function isUrlExcluded(url: string, rules: URLRuleEntry[]): boolean {
102+
let anyInclusionRule = false;
103+
let anyExclusionRule = false;
104+
for (const rule of rules) {
105+
if (rule.ruleType & RuleTypeBit.INCLUSION) {
106+
// include
107+
if (!anyInclusionRule && isUrlMatch(url, rule)) {
108+
// 符合 inclusion
109+
anyInclusionRule = true;
110+
}
111+
} else {
112+
// exclude
113+
if (!isUrlMatch(url, rule)) {
114+
// 符合 exclusion
115+
anyExclusionRule = true;
116+
break;
117+
}
118+
}
119+
}
120+
// true 条件: ( All @include/@match = false ) OR ( Any @exclude = true )
121+
return !anyInclusionRule || anyExclusionRule;
122+
}
123+
91124
export const blackListSelfCheck = (blacklist: string[] | null | undefined) => {
92125
blacklist = blacklist || [];
93126

0 commit comments

Comments
 (0)