From 475c65534f93b5582fd777dd224aaace0f68b3a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Mon, 2 Nov 2026 10:51:27 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20=E6=8C=89=E5=B9=B6=E9=9B=86?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=E6=B3=A8=E5=86=8C=20matches=20=E4=B8=8E=20in?= =?UTF-8?q?cludeGlobs=EF=BC=8C@include=20=E8=84=9A=E6=9C=AC=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=B3=A8=E5=85=A5=E5=88=B0=E6=89=80=E6=9C=89=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=20(#1803)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chrome.userScripts 与 Firefox(134+)对 matches 和 includeGlobs 取并集:网址命中任一即注入。 getApiMatchesAndGlobs 原先按「matches 之后再用 includeGlobs 过滤」设计,遇到 @include glob 或 正则就把 matches 放宽成 *://*/* 再指望 includeGlobs 收回,结果所有带 @include 的脚本代码都挂到了 每个 http(s) 页面(执行仍由 service worker 的匹配把关,但源码对页面可见)。 - matches 只放脚本真正的 match pattern;只有 glob 时用 *://scriptcat.invalid/* 占位(API 要求 matches 非空) - glob 不以 * 结尾时补一条 #* 变体:浏览器拿带 hash 的完整网址匹配 glob,脚本猫匹配前会去掉 hash - 能抽出 scheme://网域 的正则转成小写的网域级 glob;抽不出的仍全站注册,并以 regexUnbounded 标记 - 修改 CompiledResourceNamespace,让已保存的旧注册信息失效重建 --- src/app/repo/resource.ts | 4 +- src/app/service/service_worker/runtime.ts | 2 +- src/app/service/service_worker/utils.ts | 2 +- src/pkg/utils/url_matcher.test.ts | 762 +++++++--------------- src/pkg/utils/url_matcher.ts | 162 ++--- 5 files changed, 275 insertions(+), 657 deletions(-) diff --git a/src/app/repo/resource.ts b/src/app/repo/resource.ts index 962622893..addbbaa43 100644 --- a/src/app/repo/resource.ts +++ b/src/app/repo/resource.ts @@ -83,7 +83,7 @@ export type CompiledResource = { uuid: string; require: string[]; // 仅存储url,节省空间 matches: string[]; // primary - includeGlobs: string[]; // includeGlobs applied after matches + includeGlobs: string[]; // 浏览器与 matches 取并集,见 getApiMatchesAndGlobs excludeMatches: string[]; excludeGlobs: string[]; allFrames: boolean; @@ -110,7 +110,7 @@ export class ResourceDAO extends Repo { } // CompiledResource结构或 matches 计算规则变更时,建议修改 CompiledResourceNamespace 以删除旧Cache -export const CompiledResourceNamespace = "39a5eaa9-1cd2-446b-82b8-dd7bd3719f52"; +export const CompiledResourceNamespace = "8f0d6c1e-3b4a-4f55-9d2e-6a7c1b9e5f13"; export class CompiledResourceDAO extends Repo { constructor() { diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index ec2ba95d6..d657658c7 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -869,7 +869,7 @@ export class RuntimeService { const scriptMatchInfo = await this.applyScriptMatchInfo(scriptRes); if (!scriptMatchInfo) return undefined; // 生效规则一条 inclusion 都不剩(用户把当前站点从匹配中移除后可能如此)时不能注册: - // getApiMatchesAndGlobs 对没有 match pattern 的规则集会退回 *://*/*,注册出去等于全站运行。 + // 这种规则集转换不出任何 matches,提前返回,免得后面把它当成「只匹配不支持的 scheme」记警告。 if (!scriptMatchInfo.scriptUrlPatterns.some((rule) => rule.ruleType & RuleTypeBit.INCLUSION)) return undefined; const res = getUserScriptRegister(scriptMatchInfo); diff --git a/src/app/service/service_worker/utils.ts b/src/app/service/service_worker/utils.ts index 45b2e77b5..0baff8f2c 100644 --- a/src/app/service/service_worker/utils.ts +++ b/src/app/service/service_worker/utils.ts @@ -225,7 +225,7 @@ export function getUserScriptRegister(scriptMatchInfo: ScriptMatchInfo) { id: scriptMatchInfo.uuid, js: [{ code: "" }], matches: matches, // primary - includeGlobs: includeGlobs, // includeGlobs applied after matches + includeGlobs: includeGlobs, // 浏览器与 matches 取并集,见 getApiMatchesAndGlobs excludeMatches: excludeMatches, excludeGlobs: excludeGlobs, allFrames: !scriptMatchInfo.metadata["noframes"], diff --git a/src/pkg/utils/url_matcher.test.ts b/src/pkg/utils/url_matcher.test.ts index e09063d9b..95424e6ed 100644 --- a/src/pkg/utils/url_matcher.test.ts +++ b/src/pkg/utils/url_matcher.test.ts @@ -4,38 +4,13 @@ import { getApiMatchesAndGlobs, extractUrlPatterns, RuleType, - extractMatchPatternsFromGlobs, extractSchemesOfGlobs, embeddedPatternCheckerString, getMatchPatternHost, + PLACEHOLDER_MATCH_PATTERN, } from "./url_matcher"; import { isUrlIncluded } from "./match"; -describe.concurrent("extractMatchPatternsFromGlobs", () => { - it.concurrent("test1", () => { - expect(extractMatchPatternsFromGlobs(["https://www.google.com/*"])).toEqual(["https://www.google.com/*"]); - expect(extractMatchPatternsFromGlobs(["http://www.google.com/*"])).toEqual(["http://www.google.com/*"]); - expect(extractMatchPatternsFromGlobs(["http*://www.google.com/*"])).toEqual(["*://www.google.com/*"]); - expect(extractMatchPatternsFromGlobs(["*://www.google.com/*"])).toEqual(["*://www.google.com/*"]); - expect(extractMatchPatternsFromGlobs(["*://www.g*le.com/*"])).toEqual([null]); - expect(extractMatchPatternsFromGlobs(["*://*.gle.com/*"])).toEqual([null]); - expect(extractMatchPatternsFromGlobs(["*://*ww.gle.com/*"])).toEqual([null]); - expect(extractMatchPatternsFromGlobs(["*://www.gle.cm*/*"])).toEqual([null]); - expect(extractMatchPatternsFromGlobs(["*://www.gll?e.com/*"])).toEqual([null]); - expect(extractMatchPatternsFromGlobs(["*://www.google.com/a?b*c*"])).toEqual(["*://www.google.com/*"]); - expect(extractMatchPatternsFromGlobs(["http*://www.google.com/a?b*c*"])).toEqual(["*://www.google.com/*"]); - expect(extractMatchPatternsFromGlobs(["https://www.google.com/a?b*c*"])).toEqual(["https://www.google.com/*"]); - expect(extractMatchPatternsFromGlobs(["http://www.google.com/a?b*c*"])).toEqual(["http://www.google.com/*"]); - expect(extractMatchPatternsFromGlobs(["file:///mydrive/t*.html"])).toEqual(["file:///*"]); - expect(extractMatchPatternsFromGlobs(["file:///my?ive/t*.html"])).toEqual(["file:///*"]); - expect(extractMatchPatternsFromGlobs(["www.g*le.com/*"])).toEqual([null]); - expect(extractMatchPatternsFromGlobs(["www.google.com"])).toEqual([null]); - expect(extractMatchPatternsFromGlobs(["*www.g*le.com/*"])).toEqual([null]); - expect(extractMatchPatternsFromGlobs(["*ww.google.com*"])).toEqual([null]); - expect(extractMatchPatternsFromGlobs(["*www.google.com/hello/*"])).toEqual([null]); - }); -}); - describe.concurrent("extractSchemesOfGlobs", () => { it.concurrent("test1", () => { expect(extractSchemesOfGlobs(["https://www.google.com/*"])).toEqual(["*://*/*"]); @@ -307,59 +282,92 @@ describe.concurrent("checkUrlMatch-1", () => { }); }); +const BILIBILI_MATCHES = [ + "*://www.bilibili.com/video/*", + "*://www.bilibili.com/list/*", + "*://www.bilibili.com/bangumi/play/*", + "*://www.bilibili.com/medialist/play/watchlater", + "*://www.bilibili.com/medialist/play/watchlater/*", + "*://www.bilibili.com/medialist/play/ml*", +]; +const BILIBILI_INCLUDES = BILIBILI_MATCHES.map((p) => `// @include ${p}`); + +// 浏览器把 matches 与 includeGlobs 取并集(任一命中即注入),所以 matches 里一旦出现 *://*/*, +// 注册范围就是全站;只有脚本规则本身覆盖全站,或正则无法收窄时才允许出现(#1803) describe.concurrent("getApiMatchesAndGlobs-1 (基础测试)", () => { - it.concurrent("match1", () => { - // 只有有效的match pattern,不需要glob pattern + it.concurrent("只有 match pattern 时原样注册,不需要 glob", () => { const scriptUrlPatterns = extractUrlPatterns([ "@match http://google.com/*", "@match https://google.com/*", "@match file:///mydir/myfile/001/*", ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); + const result = getApiMatchesAndGlobs(scriptUrlPatterns); - expect(matches).toEqual(["http://google.com/*", "https://google.com/*", "file:///mydir/myfile/001/*"]); - expect(includeGlobs).toEqual([]); + expect(result).toEqual({ + matches: ["http://google.com/*", "https://google.com/*", "file:///mydir/myfile/001/*"], + includeGlobs: [], + regexUnbounded: false, + }); }); - it.concurrent("match2", () => { - // 由于 *hello* ,故要 match 全部页面 - const scriptUrlPatterns = extractUrlPatterns(["@include *hello*"]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - expect(matches).toEqual(["*://*/*"]); - expect(includeGlobs).toEqual(["*hello*"]); + it.concurrent("只有 glob 时 matches 用占位 pattern,范围完全由 glob 决定", () => { + const result = getApiMatchesAndGlobs(extractUrlPatterns(["@include *hello*"])); + + expect(result).toEqual({ + matches: [PLACEHOLDER_MATCH_PATTERN], + includeGlobs: ["*hello*"], + regexUnbounded: false, + }); }); - it.concurrent("match3", () => { - // 由于 *hello* ,故要 match 全部页面 - const scriptUrlPatterns = extractUrlPatterns([ - "@match http://google.com/*", - "@match https://google.com/*", - "@include *hello*", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); + it.concurrent("match 与 glob 混用时各自保留,不再退回 *://*/*", () => { + const result = getApiMatchesAndGlobs( + extractUrlPatterns(["@match http://google.com/*", "@match https://google.com/*", "@include *hello*"]) + ); - expect(matches).toEqual(["*://*/*"]); - expect(includeGlobs).toEqual(["*hello*", "http://google.com/*", "https://google.com/*"]); + expect(result).toEqual({ + matches: ["http://google.com/*", "https://google.com/*"], + includeGlobs: ["*hello*"], + regexUnbounded: false, + }); }); - it.concurrent("match4", () => { - // 由于 *hello* ,故要 match 全部页面 - // @match 有 file:/// ,故追加 file:///* 至match - const scriptUrlPatterns = extractUrlPatterns([ - "@match http://google.com/*", - "@match https://google.com/*", - "@match file:///mydir/myfile/001/*", - "@include *hello*", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - - expect(matches).toEqual(["*://*/*", "file:///*"]); - expect(includeGlobs).toEqual([ - "*hello*", - "http://google.com/*", - "https://google.com/*", - "file:///mydir/myfile/001/*", - ]); + it.concurrent("file:/// 的 match pattern 直接保留,不需要额外追加 file:///*", () => { + const result = getApiMatchesAndGlobs( + extractUrlPatterns([ + "@match http://google.com/*", + "@match https://google.com/*", + "@match file:///mydir/myfile/001/*", + "@include *hello*", + ]) + ); + + expect(result.matches).toEqual(["http://google.com/*", "https://google.com/*", "file:///mydir/myfile/001/*"]); + expect(result.includeGlobs).toEqual(["*hello*"]); + }); + + it.concurrent("脚本本身写了 *://*/* 时照常全站注册", () => { + const result = getApiMatchesAndGlobs(extractUrlPatterns(["@match *://*/*", "@include *hello*"])); + + expect(result.matches).toEqual(["*://*/*"]); + expect(result.regexUnbounded).toBe(false); + }); + + it.concurrent( + "glob 不以 * 结尾时补一条 #* 变体:浏览器拿带 hash 的完整网址匹配 glob,脚本猫匹配前会去掉 hash", + () => { + const result = getApiMatchesAndGlobs(extractUrlPatterns(["@include https://*example.com/page"])); + + expect(result.matches).toEqual([PLACEHOLDER_MATCH_PATTERN]); + expect(result.includeGlobs).toEqual(["https://*example.com/page", "https://*example.com/page#*"]); + } + ); + + it.concurrent("glob 推导出的非 http scheme 追加到 matches", () => { + const result = getApiMatchesAndGlobs(extractUrlPatterns(["@include ftp://*example*/*"])); + + expect(result.matches).toEqual(["ftp://*/*"]); + expect(result.includeGlobs).toEqual(["ftp://*example*/*"]); }); }); @@ -383,19 +391,16 @@ describe.concurrent("getApiMatchesAndGlobs 浏览器不支持的 scheme", () => expect(matches).toEqual(["https://example.com/*", "ftp://example.com/*", "file:///tmp/*"]); }); - it.concurrent("glob 推导出的不支持 scheme 也要丢弃", () => { - const { matches, includeGlobs } = getApiMatchesAndGlobs(extractUrlPatterns(["@include edge://*"])); + it.concurrent("只有不支持 scheme 的 glob 时整条丢弃,没有可注册的 matches", () => { + const result = getApiMatchesAndGlobs(extractUrlPatterns(["@include edge://*"])); - expect(matches).toEqual(["*://*/*"]); - expect(includeGlobs).toEqual(["edge://*"]); + expect(result.matches).toEqual([]); + expect(result.includeGlobs).toEqual([]); }); }); describe.concurrent("getApiMatchesAndGlobs-2 (实际例子测试)", () => { - it.concurrent("match1", () => { - // 测试真实例子,验证解析结果 - const scriptUrlPatterns = extractUrlPatterns( - ` + const realWorldIncludes = ` // @include *://steamcommunity.com/* // @include *://meta.appinn.net/* // @include *://v2ex.com/* @@ -435,7 +440,6 @@ describe.concurrent("getApiMatchesAndGlobs-2 (实际例子测试)", () => { // @include *.cn/* // @include *mall* // @include *shop* -// @include /.*(? { // @include *:8080* // @include https://*.test.com/* ` - .trim() - .split(/[\r\n]+/) - ); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - - expect(matches).toEqual(["*://*/*"]); - expect(includeGlobs).toEqual([ - "*.bilibili.com/*", - "*.1688.com/*", - "*.taobao.com/*", - "*.tmall.com/*", - "*.jd.com/*", - "*.smzdm.com/*", - "*.zhihu.com/*", - "*.qq.com/*", - "*.live.com/*", - "*.windows.com/*", - "*.mi.com/*", - "*docs.google.com/*", - "*baike.baidu.com/*", - "*.wikipedia.org/*", - "*://*.amazon.??*/*", - "*.flightradar24.com/*", - "*.obsidian.md/*", - "*.runoob.com/*", - "*.themoviedb.org/*", - "*.youku.com/*", - "*.cn/*", - "*mall*", - "*shop*", - "*buy*", - "*tools*", - "*translate*", - "*/releases", - "*/releases/*", - "*:5244*", - "*:8080*", - "https://*.test.com/*", - "*ple*", - "http*://steamcommunity.com/*", - "http*://meta.appinn.net/*", - "http*://v2ex.com/*", - "http*://www.v2ex.com/*", - "http*://greasyfork.org/*", - "http*://bilibili.com/*", - "http*://www.douyin.com/*", - "http*://weibo.com/*", - // "http*://amazon.tld/*", - // "http*://*.amazon.tld/*", - "https://www.baidu.com/s?*", - "https://www.google.com/search*", - "https://www.bing.com/search*", - "https://www.so.com/s*", - "https://regex101.com/", - "https://discord.com/*", - "https://web.telegram.org/*", - "https://www.flipkart.com/*", - // "https://test.com/*", - // "https://*.test.com/*", - ]); - }); -}); - -describe.concurrent("getApiMatchesAndGlobs-3 (全面性测试)", () => { - it.concurrent("标准match格式", () => { - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.bilibili.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://www.bilibili.com/medialist/play/watchlater", - "// @include *://www.bilibili.com/medialist/play/watchlater/*", - "// @include *://www.bilibili.com/medialist/play/ml*", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - expect(matches).toEqual([ - "*://www.bilibili.com/video/*", - "*://www.bilibili.com/list/*", - "*://www.bilibili.com/bangumi/play/*", - "*://www.bilibili.com/medialist/play/watchlater", - "*://www.bilibili.com/medialist/play/watchlater/*", - "*://www.bilibili.com/medialist/play/ml*", - ]); - expect(includeGlobs).toEqual([]); + .trim() + .split(/[\r\n]+/); + + it.concurrent("大量 match/glob 混用时逐条精确注册", () => { + const result = getApiMatchesAndGlobs(extractUrlPatterns(realWorldIncludes)); + + expect(result).toEqual({ + matches: [ + "*://steamcommunity.com/*", + "*://meta.appinn.net/*", + "*://v2ex.com/*", + "*://www.v2ex.com/*", + "*://greasyfork.org/*", + "*://bilibili.com/*", + "*://www.douyin.com/*", + "*://weibo.com/*", + "https://www.baidu.com/s?*", + "https://www.google.com/search*", + "https://www.bing.com/search*", + "https://www.so.com/s*", + "https://regex101.com/", + "https://discord.com/*", + "https://web.telegram.org/*", + "https://www.flipkart.com/*", + ], + includeGlobs: [ + "*.bilibili.com/*", + "*.1688.com/*", + "*.taobao.com/*", + "*.tmall.com/*", + "*.jd.com/*", + "*.smzdm.com/*", + "*.zhihu.com/*", + "*.qq.com/*", + "*.live.com/*", + "*.windows.com/*", + "*.mi.com/*", + "*docs.google.com/*", + "*baike.baidu.com/*", + "*.wikipedia.org/*", + "*://*.amazon.??*/*", + "*.flightradar24.com/*", + "*.obsidian.md/*", + "*.runoob.com/*", + "*.themoviedb.org/*", + "*.youku.com/*", + "*.cn/*", + "*mall*", + "*shop*", + "*buy*", + "*tools*", + "*translate*", + "*/releases", + "*/releases#*", + "*/releases/*", + "*:5244*", + "*:8080*", + "https://*.test.com/*", + ], + regexUnbounded: false, + }); }); - it.concurrent("[A1] regex格式可抽出match网域 (https?:\\/\\/)", () => { - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.bilibili.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://www.bilibili.com/medialist/play/watchlater", - "// @include *://www.bilibili.com/medialist/play/watchlater/*", - "// @include *://www.bilibili.com/medialist/play/ml*", - "// @include /https?:\\/\\/live\\.bilibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - // 可以是 "*://*/*" 或者 *://*.bilibili.com/* 或 (*://www.bilibili.com/* 及 *://live.bilibili.com/*) - // expect(matches).toEqual(["*://*/*"]); - expect(matches.sort()).toEqual(["*://www.bilibili.com/*", "*://live.bilibili.com/*"].sort()); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/video/*", - "http*://www.bilibili.com/list/*", - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/medialist/play/watchlater", - "http*://www.bilibili.com/medialist/play/watchlater/*", - "http*://www.bilibili.com/medialist/play/ml*", - "http*://live.bilibili.com/*", - ].sort() + it.concurrent("夹带一条无法收窄的正则时退回全站注册并标记 regexUnbounded", () => { + const result = getApiMatchesAndGlobs( + extractUrlPatterns([...realWorldIncludes, "// @include /.*(? { - // 斜线没有反斜线不影响结果 - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.bilibili.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://www.bilibili.com/medialist/play/watchlater", - "// @include *://www.bilibili.com/medialist/play/watchlater/*", - "// @include *://www.bilibili.com/medialist/play/ml*", - "// @include /https?://live\\.bilibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - // 可以是 "*://*/*" - expect(matches).toEqual(["*://live.bilibili.com/*", "*://www.bilibili.com/*"]); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/video/*", - "http*://www.bilibili.com/list/*", - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/medialist/play/watchlater", - "http*://www.bilibili.com/medialist/play/watchlater/*", - "http*://www.bilibili.com/medialist/play/ml*", - "http*://live.bilibili.com/*", - ].sort() - ); + expect(result).toEqual({ matches: ["*://*/*"], includeGlobs: [], regexUnbounded: true }); }); - it.concurrent("[A3] regex格式可抽出match网域 (*://)", () => { - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.bilibili.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://www.bilibili.com/medialist/play/watchlater", - "// @include *://www.bilibili.com/medialist/play/watchlater/*", - "// @include *://www.bilibili.com/medialist/play/ml*", - "// @include /.*://live\\.bilibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - // 可以是 "*://*/*" - expect(matches).toEqual(["*://live.bilibili.com/*", "*://www.bilibili.com/*"]); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/video/*", - "http*://www.bilibili.com/list/*", - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/medialist/play/watchlater", - "http*://www.bilibili.com/medialist/play/watchlater/*", - "http*://www.bilibili.com/medialist/play/ml*", - "*://live.bilibili.com/*", - ].sort() + it.concurrent("issue #1803 的脚本:正则 @include 只扩展出它的网域 glob,不影响其他站点", () => { + const result = getApiMatchesAndGlobs( + extractUrlPatterns([ + "// @match *://*.douyin.com/*", + "// @exclude *://so.douyin.com/*", + "// @match *://*.bilibili.com/*", + "// @include /^https?:\\/\\/(greasy|sleazy)fork\\.org\\/[^/]+\\/scripts\\/\\d+-/", + "// @match *://*.zhihu.com/*", + ]) ); - }); - it.concurrent("[B1] regex转换成*://*/* (match & glob)", () => { - // /live\\.bilibili\\.com/ 可匹配 123live.bilibili.com, www.live.bilibili.com, myhome.com/live.bilibili.com - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.bilibili.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://www.bilibili.com/medialist/play/watchlater", - "// @include *://www.bilibili.com/medialist/play/watchlater/*", - "// @include *://www.bilibili.com/medialist/play/ml*", - "// @include /live\\.bilibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - // 可以是 "*://*/*" - expect(matches).toEqual(["*://*/*"]); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/video/*", - "http*://www.bilibili.com/list/*", - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/medialist/play/watchlater", - "http*://www.bilibili.com/medialist/play/watchlater/*", - "http*://www.bilibili.com/medialist/play/ml*", - "*://*/*", - ].sort() - ); + expect(result).toEqual({ + matches: ["*://*.douyin.com/*", "*://*.bilibili.com/*", "*://*.zhihu.com/*"], + includeGlobs: ["http*://?????yfork.org/*"], + regexUnbounded: false, + }); }); +}); - it.concurrent("[B2] regex转换成*://*/* (match only)", () => { - // /live\\.bilibili\\.com/ 可匹配 123live.bilibili.com, www.live.bilibili.com, myhome.com/live.bilibili.com - // 相对于 (1) ,regex为较简单,glob部份不需转换 - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.bilibili.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://www.bilibili.com/medialist/play/watchlater", - "// @include *://www.bilibili.com/medialist/play/watchlater/*", - "// @include *://www.bilibili.com/medialist/play/ml*", - "// @include /live\\.bilibili\\.com/", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - // 可以是 "*://*/*" - expect(matches).toEqual(["*://*/*"]); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/video/*", - "http*://www.bilibili.com/list/*", - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/medialist/play/watchlater", - "http*://www.bilibili.com/medialist/play/watchlater/*", - "http*://www.bilibili.com/medialist/play/ml*", - "live.bilibili.com", - ].sort() - ); +describe.concurrent("getApiMatchesAndGlobs-3 (正则 @include 的收窄)", () => { + it.concurrent("标准match格式", () => { + const result = getApiMatchesAndGlobs(extractUrlPatterns(BILIBILI_INCLUDES)); + expect(result).toEqual({ matches: BILIBILI_MATCHES, includeGlobs: [], regexUnbounded: false }); }); - it.concurrent("[C1] 无法从regex抽出match网域, 全域match配合glob (live.bilibili.???)", () => { - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.bilibili.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://www.bilibili.com/medialist/play/watchlater", - "// @include *://www.bilibili.com/medialist/play/watchlater/*", - "// @include *://www.bilibili.com/medialist/play/ml*", - "// @include /.*://live\\.bilibili\\.(com|net)\\/(blanc\\/)?\\d+([/?]|$)/", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - // 可以是 "*://*/*" - expect(matches).toEqual(["*://*/*"]); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/video/*", - "http*://www.bilibili.com/list/*", - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/medialist/play/watchlater", - "http*://www.bilibili.com/medialist/play/watchlater/*", - "http*://www.bilibili.com/medialist/play/ml*", - "*://live.bilibili.???/*", - ].sort() - ); - }); + it.concurrent.each([ + ["https?:\\/\\/", "/https?:\\/\\/live\\.bilibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/", "http*://live.bilibili.com/*"], + ["https?://", "/https?://live\\.bilibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/", "http*://live.bilibili.com/*"], + ["*://", "/.*://live\\.bilibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/", "*://live.bilibili.com/*"], + ["网域含单字通配", "/.*://live\\.bilibili\\.(com|net)\\/(blanc\\/)?\\d+([/?]|$)/", "*://live.bilibili.???/*"], + ["网域含 *.", "/https?://.*\\.bilibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/", "http*://*.bilibili.com/*"], + ["网域含可选字符", "/https?://www\\.bil?ibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/", "http*://www.bi*ibili.com/*"], + ["网域含大写分组", "/https?://www(AS).com/www#11?11/2.*/", "http*://wwwas?com/*"], + ])("[A] 能抽出 scheme://网域 的正则(%s)只追加网域级 glob", (_label, regex, glob) => { + const result = getApiMatchesAndGlobs(extractUrlPatterns([...BILIBILI_INCLUDES, `// @include ${regex}`])); - it.concurrent("[C2] 无法从regex抽出match网域, 全域match配合glob (*.bilibili.com)", () => { - // glob/regex 的 *.bilibili.com 可以匹配 google.com/my.bilibili.com, 因此无法转换成match网域 - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.bilibili.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://www.bilibili.com/medialist/play/watchlater", - "// @include *://www.bilibili.com/medialist/play/watchlater/*", - "// @include *://www.bilibili.com/medialist/play/ml*", - "// @include /https?://.*\\.bilibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - // 可以是 "*://*/*" - expect(matches).toEqual(["*://*/*"]); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/video/*", - "http*://www.bilibili.com/list/*", - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/medialist/play/watchlater", - "http*://www.bilibili.com/medialist/play/watchlater/*", - "http*://www.bilibili.com/medialist/play/ml*", - "http*://*.bilibili.com/*", - ].sort() - ); + expect(result).toEqual({ matches: BILIBILI_MATCHES, includeGlobs: [glob], regexUnbounded: false }); }); - it.concurrent("[C3] 无法从regex抽出match网域, 全域match配合glob (www.bil?bili.com)", () => { - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.bilibili.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://www.bilibili.com/medialist/play/watchlater", - "// @include *://www.bilibili.com/medialist/play/watchlater/*", - "// @include *://www.bilibili.com/medialist/play/ml*", - "// @include /https?://www\\.bil?ibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - // 可以是 "*://*/*" - expect(matches).toEqual(["*://*/*"]); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/video/*", - "http*://www.bilibili.com/list/*", - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/medialist/play/watchlater", - "http*://www.bilibili.com/medialist/play/watchlater/*", - "http*://www.bilibili.com/medialist/play/ml*", - "http*://www.bi*ibili.com/*", - ].sort() + it.concurrent("[A] 正则默认忽略大小写,抽出的网域 glob 统一转小写", () => { + const result = getApiMatchesAndGlobs( + extractUrlPatterns(["// @include /^https?:\\/\\/WWW\\.Example\\.COM\\/Path/"]) ); - }); - it.concurrent("[D1] 无法从regex抽出match网域, 全域match配合glob (fallback glob to *://*/*)", () => { - // 减低依赖 regexToGlob 所带来的问题,regex 转成 glob后,出现罕见 glob pattern 会fallback至 *://*/* - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.bilibili.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://www.bilibili.com/medialist/play/watchlater", - "// @include *://www.bilibili.com/medialist/play/watchlater/*", - "// @include *://www.bilibili.com/medialist/play/ml*", - "// @include /://www\\.bil?ibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - // 可以是 "*://*/*" - expect(matches).toEqual(["*://*/*"]); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/video/*", - "http*://www.bilibili.com/list/*", - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/medialist/play/watchlater", - "http*://www.bilibili.com/medialist/play/watchlater/*", - "http*://www.bilibili.com/medialist/play/ml*", - "*://*/*", - ].sort() - ); + expect(result).toEqual({ + matches: [PLACEHOLDER_MATCH_PATTERN], + includeGlobs: ["http*://www.example.com/*"], + regexUnbounded: false, + }); }); - it.concurrent("[D2] 无法从regex抽出match网域, 全域match配合glob (fallback glob to *://*/*)", () => { - // 减低依赖 regexToGlob 所带来的问题,regex 转成 glob后,出现罕见 glob pattern 会fallback至 *://*/* - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.bilibili.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://www.bilibili.com/medialist/play/watchlater", - "// @include *://www.bilibili.com/medialist/play/watchlater/*", - "// @include *://www.bilibili.com/medialist/play/ml*", - "// @include /https?://www(AS).com/www#11?11/2.*/", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - // 可以是 "*://*/*" - expect(matches).toEqual(["*://*/*"]); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/video/*", - "http*://www.bilibili.com/list/*", - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/medialist/play/watchlater", - "http*://www.bilibili.com/medialist/play/watchlater/*", - "http*://www.bilibili.com/medialist/play/ml*", - "*://*/*", - ].sort() - ); - }); + it.concurrent.each([ + ["没有 scheme 的局部正则", "/live\\.bilibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/"], + ["只有网域片段", "/live\\.bilibili\\.com/"], + ["以 :// 开头", "/://www\\.bil?ibili\\.com\\/(blanc\\/)?\\d+([/?]|$)/"], + ])("[B] 无法抽出网域的正则(%s)退回全站注册并标记 regexUnbounded", (_label, regex) => { + const result = getApiMatchesAndGlobs(extractUrlPatterns([...BILIBILI_INCLUDES, `// @include ${regex}`])); - it.concurrent("[E1] 混合 regex/glob pattern 测试解析正确性", () => { - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.google.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://live.bilibili.com/medialist/play/watchlater", - "// @include /https?://www(AS).com/www#11?11/2.*/", - "// @include /https?://www.google.com/search\\?q=\\w+&page=\\d+/", - "// @exclude /https?://www.google.com/search\\?q=\\w+&page=[123]/", - "// @include /http://www.bilibili.com/\\w+/", - "// @include /http://www.apple.com/abc/", - "// @include /www.myapple.com/def/", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - // 可以是 "*://*/*" - expect(matches).toEqual(["*://*/*"]); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/list/*", - "http*://www.google.com/video/*", - "http*://www?google?com/*", - "http://www?bilibili?com/*", - "http*://live.bilibili.com/medialist/play/watchlater", - "*://*/*", - "http://www?apple?com/*", - ].sort() - ); + expect(result).toEqual({ matches: ["*://*/*"], includeGlobs: [], regexUnbounded: true }); }); - it.concurrent("[E2] 混合 regex/glob pattern 测试解析正确性", () => { - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.google.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://live.bilibili.com/medialist/play/watchlater", - "// @include /https?://www\\.google\\.com/search?q=\\w+&page=\\d+/", - "// @exclude /https?://www\\.google\\.com/search?q=\\w+&page=[123]/", - "// @include /http://www\\.bilibili\\.com/\\w+/", - "// @include /http://www\\.apple\\.com/abc/", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - // 可以是 "*://*/*" - expect(matches).toEqual([ - "*://www.google.com/*", - "http://www.bilibili.com/*", - "http://www.apple.com/*", - "*://www.bilibili.com/*", - "*://live.bilibili.com/*", - ]); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/list/*", - "http*://www.google.com/video/*", - "http*://www.google.com/*", - "http://www.bilibili.com/*", - "http*://live.bilibili.com/medialist/play/watchlater", - "http://www.apple.com/*", - ].sort() + it.concurrent("[C] 多条正则都能收窄时,@exclude 正则不进入 includeGlobs", () => { + const result = getApiMatchesAndGlobs( + extractUrlPatterns([ + "// @include *://www.google.com/video/*", + "// @include *://www.bilibili.com/list/*", + "// @include *://www.bilibili.com/bangumi/play/*", + "// @include *://live.bilibili.com/medialist/play/watchlater", + "// @include /https?://www\\.google\\.com/search?q=\\w+&page=\\d+/", + "// @exclude /https?://www\\.google\\.com/search?q=\\w+&page=[123]/", + "// @include /http://www\\.bilibili\\.com/\\w+/", + "// @include /http://www\\.apple\\.com/abc/", + "// @include file:///myfile/*", + ]) ); - }); - it.concurrent("[F1] 混合 regex/glob pattern & file scheme 测试解析正确性", () => { - // 由于regex pattern 而fallback至全部页面 - // 含有 file:///,追加 "file:///*" - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.google.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://live.bilibili.com/medialist/play/watchlater", - "// @include /https?://www(AS).com/www#11?11/2.*/", - "// @include /https?://www.google.com/search\\?q=\\w+&page=\\d+/", - "// @exclude /https?://www.google.com/search\\?q=\\w+&page=[123]/", - "// @include /http://www.bilibili.com/\\w+/", - "// @include /http://www.apple.com/abc/", - "// @include file:///myfile/*", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - expect(matches).toEqual(["*://*/*", "file:///*"]); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/list/*", - "http*://www.google.com/video/*", - "http*://www?google?com/*", - "http://www?bilibili?com/*", - "http*://live.bilibili.com/medialist/play/watchlater", - "*://*/*", - "http://www?apple?com/*", + expect(result).toEqual({ + matches: [ + "*://www.google.com/video/*", + "*://www.bilibili.com/list/*", + "*://www.bilibili.com/bangumi/play/*", + "*://live.bilibili.com/medialist/play/watchlater", "file:///myfile/*", - ].sort() - ); + ], + includeGlobs: ["http*://www.google.com/*", "http://www.bilibili.com/*", "http://www.apple.com/*"], + regexUnbounded: false, + }); }); - it.concurrent("[F2] 混合 regex/glob pattern & file scheme 测试解析正确性", () => { - // regex pattern 不用 fallback 至全部页面 - const scriptUrlPatterns = extractUrlPatterns([ - "// @include *://www.google.com/video/*", - "// @include *://www.bilibili.com/list/*", - "// @include *://www.bilibili.com/bangumi/play/*", - "// @include *://live.bilibili.com/medialist/play/watchlater", - "// @include /https?://www\\.google\\.com/search?q=\\w+&page=\\d+/", - "// @exclude /https?://www\\.google\\.com/search?q=\\w+&page=[123]/", - "// @include /http://www\\.bilibili\\.com/\\w+/", - "// @include /http://www\\.apple\\.com/abc/", - "// @include file:///myfile/*", - ]); - const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns); - expect(matches).toEqual([ - "*://www.google.com/*", - "http://www.bilibili.com/*", - "http://www.apple.com/*", - "*://www.bilibili.com/*", - "*://live.bilibili.com/*", - "file:///*", - ]); - // 忽略次序 - expect(includeGlobs.sort()).toEqual( - [ - "http*://www.bilibili.com/bangumi/play/*", - "http*://www.bilibili.com/list/*", - "http*://www.google.com/video/*", - "http*://www.google.com/*", - "http://www.bilibili.com/*", - "http*://live.bilibili.com/medialist/play/watchlater", - "http://www.apple.com/*", - "file:///myfile/*", - ].sort() + it.concurrent("[C] 退回全站注册时保留非 http 的 match pattern", () => { + const result = getApiMatchesAndGlobs( + extractUrlPatterns([ + "// @include *://www.google.com/video/*", + "// @include /live\\.bilibili\\.com/", + "// @include file:///myfile/*", + ]) ); + + expect(result).toEqual({ matches: ["*://*/*", "file:///myfile/*"], includeGlobs: [], regexUnbounded: true }); }); }); diff --git a/src/pkg/utils/url_matcher.ts b/src/pkg/utils/url_matcher.ts index 76c367174..219b6dbc2 100644 --- a/src/pkg/utils/url_matcher.ts +++ b/src/pkg/utils/url_matcher.ts @@ -393,35 +393,6 @@ const isUrlMatchRegEx = (s: string, ruleContent: [string, string]): boolean => { return new RegExp(ruleContent[0], ruleContent[1] || "i").test(s); }; -export const addMatchesToGlobs = (matches: URLRuleEntry[], globs: string[]): void => { - for (const rule of matches) { - if (rule.ruleType !== 1) continue; - const [scheme0, host, path] = rule.ruleContent as string[]; - const scheme = scheme0 === "*" ? "http*" : scheme0; - if (host.charAt(0) !== ".") { - globs.push(`${scheme}://${host}/${path}`); - } else { - const h = host.substring(1); - globs.push(`${scheme}://${h}/${path}`); - globs.push(`${scheme}://*.${h}/${path}`); - } - } -}; - -export const extractMatchPatternsFromGlobs = (globs: string[]): (string | null)[] => { - return globs.map((glob) => { - if (glob.startsWith("http*://")) { - glob = `*://${glob.substring(8)}`; - } - const extMatch = checkUrlMatch(glob); - if (!extMatch) return null; - const [scheme, host] = extMatch; - // glob 的 *.google.com 可以匹配 www.google.com 跟 my-website.com/abc.google.com - if (host.charAt(0) === ".") return null; - return `${scheme}://${host}/*`; - }); -}; - export const extractSchemesOfGlobs = (globs: string[]): string[] => { const set = new Set(["*://*/*"]); for (const glob of globs) { @@ -456,97 +427,66 @@ export const isRegistrableMatchPattern = (pattern: string) => { return rule?.ruleType === RuleType.MATCH_INCLUDE && isApiSupportedMatchPattern(rule.patternString); }; -export const getApiMatchesAndGlobs = (scriptUrlPatterns: URLRuleEntry[]) => { - const urlMatching = scriptUrlPatterns.filter((e) => e.ruleType === RuleType.MATCH_INCLUDE); - const urlSpecificMatching = urlMatching.filter((e) => e.patternString !== "*://*/*"); - let matchAll: string[] | null = null; - - // 原始的UrlPatterns中,match pattern 为空,或包括至少一个 *://*/* match pattern 时,先预设 match 为 *://*/* - // 之后会再判断,例如追加 file:///* - if (urlSpecificMatching.length === 0 || urlSpecificMatching.length !== urlMatching.length) { - matchAll = ["*://*/*"]; // 包含 https 和 http - } - - let regConvFallback = false; - let regConvTryExtractDomain = false; - const apiIncludeGlobs = toUniquePatternStrings(scriptUrlPatterns.filter((e) => e.ruleType === RuleType.GLOB_INCLUDE)); - const rulesForRegexInclude = scriptUrlPatterns.filter((e) => e.ruleType === RuleType.REGEX_INCLUDE); - - // 含有 regex 时,先转化成 glob pattern 再决定如何配合 UserScript API 的 match/glob pattern 注入 - if (rulesForRegexInclude.length > 0) { - for (const rule of rulesForRegexInclude) { - // 尝试利用JS代码,先把 regex pattern 转至 glob pattern, 最终尝试转化成 match pattern - let globPattern = regexToGlob(rule.ruleContent[0]); - if (globPattern !== null) { - let m: RegExpExecArray | null = null; - if ((m = /^([-_a-z0-9.:*?]+)$/.exec(globPattern))) { - // 非网域的简单 globPattern (*apple*, *apple.com*) - // globPattern = globPattern; - regConvFallback = true; - } else if ((m = /^([-a-z*?]+):\/\/([-_a-z0-9.:*?]+)(\/|$)/.exec(globPattern))) { - // 简单 globPattern, 提取网域 ( *://www.google.com/search?q=*, https://www.apple.com/page=?1&g= ) - globPattern = `${m[1]}://${m[2]}/*`; - regConvTryExtractDomain = true; // 会尝试转化成 match pattern - } else { - // 其他 globPattern, 不转换成 glob pattern - globPattern = "*://*/*"; - regConvFallback = true; - } - } else { - // regex pattern 转至 glob pattern - globPattern = "*://*/*"; - regConvFallback = true; - } - - if (apiIncludeGlobs.includes(globPattern)) { - // 已存在,不重复添加 - continue; - } - apiIncludeGlobs.push(globPattern); - } - } +// userScripts API 要求 matches 非空。脚本只有 glob 时用一个不可能有页面的地址占位(.invalid 是 RFC 6761 保留域名), +// 注册范围完全由 includeGlobs 决定 +export const PLACEHOLDER_MATCH_PATTERN = "*://scriptcat.invalid/*"; - // 由于有 glob pattern, 会先假设需要全域匹配,确保 UserScript API 的注入有效 - if (apiIncludeGlobs.length > 0 && !matchAll) matchAll = ["*://*/*"]; +const isApiSupportedGlob = (glob: string) => { + const m = /^([-\w]+):\/\//.exec(glob); + return !m || isApiSupportedMatchPattern(`${m[1]}://*/*`); +}; - // 为了改变现有的 match pattern, 现有的 match pattern 全部转换至 glob pattern, 并添加在目前的 globs - if (matchAll && urlSpecificMatching.length > 0) { - addMatchesToGlobs(urlSpecificMatching, apiIncludeGlobs); +/** + * 把脚本的 URL 规则转换成 userScripts API 的 matches / includeGlobs。 + * + * Chrome 与 Firefox(134+)都把 matches 与 includeGlobs 取并集:网址命中任一即注入,而不是先 matches 再用 + * includeGlobs 过滤。所以 matches 只能放脚本真正的 match pattern,一旦放入全站 pattern,includeGlobs 就收不住范围, + * 脚本代码会被注入到所有页面(#1803)。 + * + * 正则 @include 能抽出 scheme://网域 时转成网域级 glob(比正则宽,页面里再按正则过滤);抽不出时只能全站注册, + * 以 regexUnbounded 告知调用方。 + */ +export const getApiMatchesAndGlobs = (scriptUrlPatterns: URLRuleEntry[]) => { + const includeGlobs: string[] = []; + const addGlob = (glob: string) => { + if (includeGlobs.includes(glob)) return; + includeGlobs.push(glob); + // 浏览器拿带 #hash 的完整网址匹配 glob,脚本猫的匹配会先去掉 hash;不以 * 结尾的 glob 需要补上 hash 的情况 + if (!glob.endsWith("*")) includeGlobs.push(`${glob}#*`); + }; + for (const glob of toUniquePatternStrings(scriptUrlPatterns.filter((e) => e.ruleType === RuleType.GLOB_INCLUDE))) { + if (isApiSupportedGlob(glob)) addGlob(glob); } - // 有 globs 的情况下,需要进一步解析 globs 的规则,来决定 - // 1) globs 能充分转换成 match 网域 -> 缩小match范围 - // 2) globs 包含 http/https 以外的 scheme -> 增加match范围 - let apiMatches = null; - if (apiIncludeGlobs.length > 0) { - let matches = null; - // 有 regex pattern 换成 glob pattern 的情况下,进一步分析 match pattern 的网域 - if (regConvTryExtractDomain && !regConvFallback) { - // 有 regex pattern 换成 glob pattern, 且没有 fallback 的 regex pattern - matches = new Set(extractMatchPatternsFromGlobs(apiIncludeGlobs)); - // 如果有部份glob无法抽出网域资料,维持 matchAll - if (matches.has(null) || matches.size === 0) matches = null; - } - if (matches !== null) { - // 所有 globs 能提取成 match 网域, 不用匹配所有网域 - apiMatches = [...matches] as string[]; + let regexUnbounded = false; + for (const rule of scriptUrlPatterns) { + if (rule.ruleType !== RuleType.REGEX_INCLUDE) continue; + // 正则默认忽略大小写,而 glob 区分大小写;只取 scheme 与网域,转小写后与浏览器规范化的网址一致 + const glob = regexToGlob(rule.ruleContent[0])?.toLowerCase(); + const m = glob ? /^([-a-z*?]+):\/\/([-_a-z0-9.:*?]+)(\/|$)/.exec(glob) : null; + if (m) { + addGlob(`${m[1]}://${m[2]}/*`); } else { - // match 需要匹配所有网域 - // 如有 file:///* 之类,追加至 *://*/* - matchAll = extractSchemesOfGlobs(apiIncludeGlobs); + regexUnbounded = true; } } - if (apiMatches === null) { - // 如没有特定要求( apiMatches 及 matchAll 皆为 null ),则探用原有的match - apiMatches = matchAll || toUniquePatternStrings(urlSpecificMatching); + const matches = toUniquePatternStrings(scriptUrlPatterns.filter((e) => e.ruleType === RuleType.MATCH_INCLUDE)); + // 非 http 的 glob(如 file:///)沿用整个 scheme 的 match pattern + for (const pattern of extractSchemesOfGlobs(includeGlobs)) { + if (pattern !== "*://*/*" && !matches.includes(pattern)) matches.push(pattern); } - - return { - // 放在最后过滤:过滤后为空时调用方会不注册该脚本;若提前过滤,上面的逻辑会把空列表当成「无 match」退回 *://*/* - matches: apiMatches.filter(isApiSupportedMatchPattern), // primary - includeGlobs: apiIncludeGlobs, // includeGlobs applied after matches - }; + const apiMatches = matches.filter(isApiSupportedMatchPattern); + + if (regexUnbounded && !apiMatches.includes("*://*/*")) { + return { + matches: ["*://*/*", ...apiMatches.filter((pattern) => !/^(\*|https?):\/\//.test(pattern))], + includeGlobs: [], + regexUnbounded: true, + }; + } + if (apiMatches.length === 0 && includeGlobs.length > 0) apiMatches.push(PLACEHOLDER_MATCH_PATTERN); + return { matches: apiMatches, includeGlobs, regexUnbounded: false }; }; export type EmbeddedURLRuleEntry = Pick; From 91f93aa995a14d4278898adf0b25083d78f30cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Mon, 2 Nov 2026 11:13:16 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20=E6=AD=A3=E5=88=99=20@includ?= =?UTF-8?q?e=20=E6=94=B6=E4=B8=8D=E7=AA=84=E7=9A=84=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E9=A2=84=E6=B3=A8=E5=86=8C=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E7=94=B1=20pageLoad=20=E4=B8=8B=E5=8F=91=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=20(#1803)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 能抽出 scheme://网域 的正则已转成网域 glob 精确注册;抽不出的(如没有 scheme 的局部正则)只能全站注册, 脚本代码仍会挂到所有页面。这类脚本改为不预注册:service worker 在 pageLoad 匹配命中后,把挂载 window[flag] 的注入代码放进 TScriptInfo.code 下发,scripting 转交给 content 环境,content 沿用旧版 createElementNS 插入