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
108 changes: 59 additions & 49 deletions src/app/service/service_worker/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getMetadataStr,
getUserConfigStr,
obtainBlackList,
isFirefox,
} from "@App/pkg/utils/utils";
import { cacheInstance } from "@App/app/cache";
import { UrlMatch } from "@App/pkg/utils/match";
Expand Down Expand Up @@ -53,7 +54,7 @@ const runtimeGlobal = {
messageFlag: "PENDING",
scriptLoadComplete: "PENDING",
envLoadComplete: "PENDING",
} as MessageFlags,
} satisfies MessageFlags & Record<string, string>,
};

export class RuntimeService {
Expand Down Expand Up @@ -305,8 +306,8 @@ export class RuntimeService {

let registered = false;
try {
const res = await chrome.userScripts.getScripts({ ids: ["scriptcat-content", "scriptcat-inject"] });
registered = res.length === 2;
const res = await chrome.userScripts.getScripts({ ids: ["scriptcat-inject"] });
registered = res.length === 1;
} finally {
// 考虑 UserScripts API 不可使用等情况
runtimeGlobal.registered = registered;
Expand Down Expand Up @@ -608,6 +609,7 @@ export class RuntimeService {
runtimeGlobal.messageFlags = this.generateMessageFlags();
await Promise.allSettled([
chrome.userScripts.unregister(),
chrome.scripting.unregisterContentScripts(),
this.localStorageDAO.save({ key: "scriptInjectMessageFlags", value: runtimeGlobal.messageFlags }),
]);
}
Expand Down Expand Up @@ -781,32 +783,56 @@ export class RuntimeService {
// do nothing
}
}
const retScript: chrome.userScripts.RegisteredUserScript[] = [];
const contentJs = await this.getContentJsCode();
if (contentJs) {
retScript.push({
id: "scriptcat-content",
js: [{ code: `(function (MessageFlags) {\n${contentJs}\n})(${JSON.stringify(messageFlags)})` }],
matches: ["<all_urls>"],
allFrames: true,
runAt: "document_start",
world: "USER_SCRIPT",
excludeMatches,
excludeGlobs,
});
}

let retContent: chrome.scripting.RegisteredContentScript[] = [];
let retInject: chrome.userScripts.RegisteredUserScript[] = [];
// inject.js
const injectJs = await this.getInjectJsCode();
if (injectJs) {
const apiScripts = this.compileInjectUserScript(injectJs, messageFlags, {
excludeMatches,
excludeGlobs,
});
retScript.push(...apiScripts);
// 构建inject.js的脚本注册信息
const code = `(function (MessageFlags) {\n${injectJs}\n})(${JSON.stringify(messageFlags)})`;
retInject = [
{
id: "scriptcat-inject",
js: [{ code }],
matches: ["<all_urls>"],
allFrames: true,
world: "MAIN",
runAt: "document_start",
excludeMatches: excludeMatches,
excludeGlobs: excludeGlobs,
} satisfies chrome.userScripts.RegisteredUserScript,
];
}
// Note: Chrome does not support file.js?query
// 注意:Chrome 不支持 file.js?query
if (isFirefox()) {
retContent = [
{
id: "scriptcat-content",
js: [`/src/content.js?FlagsStart&${`${new URLSearchParams(messageFlags)}`}&FlagsEnd`],
matches: ["<all_urls>"],
allFrames: true,
runAt: "document_start",
excludeMatches,
} satisfies chrome.scripting.RegisteredContentScript,
];
} else {
const contentJs = await this.getContentJsCode();
if (contentJs) {
retInject.push({
id: "scriptcat-content",
js: [{ code: `(function () {\n${contentJs}\n})(${JSON.stringify(messageFlags)})` }],
matches: ["<all_urls>"],
allFrames: true,
runAt: "document_start",
world: "USER_SCRIPT",
excludeMatches,
excludeGlobs,
} satisfies chrome.userScripts.RegisteredUserScript);
}
}

return retScript;
return { content: retContent, inject: retInject };
}

// 如果是重复注册,需要先调用 unregisterUserscripts
Expand All @@ -818,8 +844,8 @@ export class RuntimeService {
if (runtimeGlobal.registered) {
// 异常情况
// 检查scriptcat-content和scriptcat-inject是否存在
const res = await chrome.userScripts.getScripts({ ids: ["scriptcat-content", "scriptcat-inject"] });
if (res.length === 2) {
const res = await chrome.userScripts.getScripts({ ids: ["scriptcat-inject"] });
if (res.length === 1) {
return;
}
// scriptcat-content/scriptcat-inject不存在的情况
Expand All @@ -843,9 +869,9 @@ export class RuntimeService {
const particularScriptList = await this.getParticularScriptList(options);
// getContentAndInjectScript依赖loadScriptMatchInfo
// 需要等getParticularScriptList完成后再执行
const generalScriptList = await this.getContentAndInjectScript(options);
const { inject: injectScripList, content: contentScriptList } = await this.getContentAndInjectScript(options);

const list: chrome.userScripts.RegisteredUserScript[] = [...particularScriptList, ...generalScriptList];
const list: chrome.userScripts.RegisteredUserScript[] = [...particularScriptList, ...injectScripList];

runtimeGlobal.registered = true;
try {
Expand All @@ -870,6 +896,11 @@ export class RuntimeService {
}
}
}
try {
await chrome.scripting.registerContentScripts(contentScriptList);
} catch (e: any) {
this.logger.error("register content.js error", Logger.E(e));
}
}

// 给指定tab发送消息
Expand Down Expand Up @@ -1200,27 +1231,6 @@ export class RuntimeService {
return await runScript(this.msgSender, res);
}

compileInjectUserScript(
injectJs: string,
messageFlags: MessageFlags,
{ excludeMatches, excludeGlobs }: { excludeMatches: string[] | undefined; excludeGlobs: string[] | undefined }
) {
// 构建inject.js的脚本注册信息
const code = `(function (MessageFlags) {\n${injectJs}\n})(${JSON.stringify(messageFlags)})`;
const script: chrome.userScripts.RegisteredUserScript = {
id: "scriptcat-inject",
js: [{ code }],
matches: ["<all_urls>"],
allFrames: true,
world: "MAIN",
runAt: "document_start",
excludeMatches: excludeMatches,
excludeGlobs: excludeGlobs,
};

return [script] as chrome.userScripts.RegisteredUserScript[];
}

scriptMatchEntry(
scriptRes: ScriptRunResource,
o: {
Expand Down
14 changes: 10 additions & 4 deletions src/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ import { CustomEventMessage } from "@Packages/message/custom_event_message";
import { Server } from "@Packages/message/server";
import ContentRuntime from "./app/service/content/content";
import { ScriptExecutor } from "./app/service/content/script_executor";
import { randomMessageFlag } from "./pkg/utils/utils";
import { randomMessageFlag, getUspMessageFlags } from "./pkg/utils/utils";
import type { Message } from "@Packages/message/types";

/* global MessageFlags */
// @ts-ignore
const MessageFlags: MessageFlags | null = (typeof arguments === "object" && arguments?.[0]) || getUspMessageFlags();

if (typeof chrome?.runtime?.onMessage?.addListener !== "function") {
// Firefox MV3 之类好像没有 chrome.runtime.onMessage.addListener ?
if (!MessageFlags) {
console.error("MessageFlags is unavailable.");
} else if (typeof chrome?.runtime?.onMessage?.addListener !== "function") {
// Firefox userScripts.RegisteredUserScript does not provide chrome.runtime.onMessage.addListener
// Firefox scripting.RegisteredContentScript does provide chrome.runtime.onMessage.addListener
// Firefox 的 userScripts.RegisteredUserScript 不提供 chrome.runtime.onMessage.addListener
// Firefox 的 scripting.RegisteredContentScript 提供 chrome.runtime.onMessage.addListener
console.error("chrome.runtime.onMessage.addListener is not a function");
} else {
// 建立与service_worker页面的连接
Expand Down
37 changes: 34 additions & 3 deletions src/pkg/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,37 @@ export function randomMessageFlag(): string {
return `-${Date.now().toString(36)}.${randNum(8e11, 2e12).toString(36)}`;
}

export const getUspMessageFlags = () => {
const s = new Error().stack;
if (s) {
const search1 = "content.js?FlagsStart&";
const len1 = search1.length;
const idx1 = s.indexOf(search1);
if (idx1 > 0) {
const search2 = "&FlagsEnd";
const idx2 = s.indexOf(search2, idx1 + len1);
if (idx2 > 0) {
const param = s.substring(idx1 + len1, idx2);
try {
const ret: Record<string, string> = {};
const usp = new URLSearchParams(param);
// ⚠️ In Firefox content scripts, Xray wrappers strip the iterator from URLSearchParams.entries()
// → Do not use for...of or [...usp.entries()] here
// ⚠️ 在 Firefox 内容脚本中,Xray 包装器会移除 URLSearchParams.entries() 的迭代器
// → 此处不要使用 for...of 或 [...usp.entries()]
usp.forEach((value, key) => {
ret[key] = value;
});
return ret as unknown as MessageFlags;
} catch (e) {
console.error(e);
}
}
}
}
return null;
};

export function isFirefox() {
//@ts-ignore
return typeof mozInnerScreenX !== "undefined";
Expand Down Expand Up @@ -178,7 +209,7 @@ export async function checkUserScriptsAvailable() {
// Method call which throws if API permission or toggle is not enabled.
chrome.userScripts;
const ret: chrome.userScripts.RegisteredUserScript[] | any = await chrome.userScripts.getScripts({
ids: ["scriptcat-content", "undefined-id-3"],
ids: ["scriptcat-inject", "undefined-id-3"],
});
// 返回结果不是阵列的话表示API不可使用
if (ret === undefined || ret === null || typeof ret[Symbol.iterator] !== "function") {
Expand All @@ -187,10 +218,10 @@ export async function checkUserScriptsAvailable() {

if (ret[0]) {
// API内部处理实际给予扩展权限才会有返回Script
// 含有 "scriptcat-content" 或 "undefined-id-3"
// 含有 "scriptcat-inject" 或 "undefined-id-3"
return true;
} else {
// 没有 "scriptcat-content" 和 "undefined-id-3"
// 没有 "scriptcat-inject" 和 "undefined-id-3"
// 进行 "undefined-id-3" 的注册反注册测试
// Chrome MV3 的一部分浏览器(如 Vivaldi )没正确处理 MV3 UserScripts API 权限问题 (API内部处理没有给予扩展权限)
// 此时会无法注册 (1. register 报错)
Expand Down
Loading