-
Notifications
You must be signed in to change notification settings - Fork 324
Expand file tree
/
Copy pathscript_executor.ts
More file actions
172 lines (161 loc) · 6.66 KB
/
script_executor.ts
File metadata and controls
172 lines (161 loc) · 6.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import type { Message } from "@Packages/message/types";
import { getStorageName } from "@App/pkg/utils/utils";
import type { EmitEventRequest } from "../service_worker/types";
import ExecScript from "./exec_script";
import type { GMInfoEnv, ScriptFunc, ValueUpdateDataEncoded } from "./types";
import { addStyleSheet, definePropertyListener, waitBody } from "./utils";
import type { ScriptLoadInfo, TScriptInfo } from "@App/app/repo/scripts";
import { DefinedFlags } from "../service_worker/runtime.consts";
import { pageAddEventListener, pageDispatchEvent } from "@Packages/message/common";
import { isUrlExcluded } from "@App/pkg/utils/match";
import type { ScriptEnvTag } from "@Packages/message/consts";
export type ExecScriptEntry = {
scriptLoadInfo: TScriptInfo;
scriptFlag: string;
envInfo: any;
scriptFunc: any;
};
export const initEnvInfo = {
/** userAgentData - 从全局变量获取 */
userAgentData: typeof UserAgentData === "object" ? UserAgentData : {},
/** sandboxMode - 预留字段,当前固定为 raw */
sandboxMode: "raw",
/** isIncognito - inject/content 环境下无法判断,固定为 false */
/** 使用者可透过 「 await navigator.storage.persisted() 」来判断,但ScriptCat不会主动执行此代码来判断 */
isIncognito: false,
} satisfies GMInfoEnv;
// 脚本执行器
export class ScriptExecutor {
earlyScriptFlag: Set<string> = new Set();
execScriptMap: Map<string, ExecScript> = new Map();
constructor(
private msg: Message,
private contentMsg: Message // 用于 content <-> content/inject 通讯
) {}
emitEvent(data: EmitEventRequest) {
// 转发给脚本
const exec = this.execScriptMap.get(data.uuid);
if (exec) {
exec.emitEvent(data.event, data.eventId, data.data);
}
}
valueUpdate(data: ValueUpdateDataEncoded) {
const { uuid, storageName } = data;
for (const val of this.execScriptMap.values()) {
if (val.scriptRes.uuid === uuid || getStorageName(val.scriptRes) === storageName) {
val.valueUpdate(data);
}
}
}
startScripts(scripts: TScriptInfo[], envInfo: GMInfoEnv) {
const loadExec = (script: TScriptInfo, scriptFunc: any) => {
this.execScriptEntry({
scriptLoadInfo: script,
scriptFlag: script.flag,
scriptFunc,
envInfo: envInfo,
});
};
// 监听脚本加载
scripts.forEach((script) => {
const flag = script.flag;
// 如果是EarlyScriptFlag,处理沙盒环境
if (this.earlyScriptFlag.has(flag)) {
for (const val of this.execScriptMap.values()) {
if (val.scriptRes.flag === flag) {
// 处理早期脚本的沙盒环境
val.updateEarlyScriptGMInfo(envInfo);
return;
}
}
}
definePropertyListener(window, flag, (val: ScriptFunc) => {
loadExec(script, val);
});
});
}
checkEarlyStartScript(scriptEnvTag: ScriptEnvTag, envInfo: GMInfoEnv) {
const eventNamePrefix = `evt${process.env.SC_RANDOM_KEY}.${scriptEnvTag}`; // 仅用于early-start初始化
const scriptLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.scriptLoadComplete}`;
const envLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.envLoadComplete}`;
// 监听 脚本加载
// 适用于此「通知环境加载完成」代码执行后的脚本加载
const scriptLoadCompleteHandler: EventListener = (ev: Event) => {
const detail = (ev as CustomEvent).detail as {
scriptFlag: string;
scriptInfo: ScriptLoadInfo;
};
const scriptFlag = detail?.scriptFlag;
if (typeof scriptFlag === "string") {
ev.preventDefault(); // dispatchEvent 会回传 false -> 分离环境也能得知环境加载代码已执行
// 检查是否有 urlPattern,有则执行匹配再决定是否略过注入
if (detail.scriptInfo.scriptUrlPatterns) {
// 以 REGEX 情况为例
// "@include /REGEX/" 的情况下,MV3 UserScripts API 基础匹配范围扩大,会比实际需要的广阔,然后在 earlyScript 把不符合 REGEX 的除去
// (All @include = false -> 除去)
// 注:如果 @include 混合了 regex 跟 一般的,即使 regex 的 @include 不匹对当前网址,但匹对了一般 @include 也视为有效
// 相反如果 @include 混合了 regex 跟 一般的,regex 的 @include 匹对了即可
// "@exclude /REGEX/" 的情况下,MV3 UserScripts API 基础匹配范围不会扩大,然后在 earlyScript 把符合 REGEX 的匹配除去
// (Any @exclude = true -> 除去)
// 注:如果一早已被除排,根本不会被 MV3 UserScripts API 注入。所以只考虑排除「多余的匹配」。(略过注入)
try {
if (isUrlExcluded(window.location.href, detail.scriptInfo.scriptUrlPatterns)) {
// 「多余的匹配」-> 略过注入
return;
}
} catch (e) {
console.warn("Unexpected match error", e);
}
}
this.execEarlyScript(scriptFlag, detail.scriptInfo, envInfo);
}
};
pageAddEventListener(scriptLoadCompleteEvtName, scriptLoadCompleteHandler);
// 通知 环境 加载完成
// 适用于此「通知环境加载完成」代码执行前的脚本加载
const ev = new CustomEvent(envLoadCompleteEvtName);
pageDispatchEvent(ev);
}
execEarlyScript(flag: string, scriptInfo: TScriptInfo, envInfo: GMInfoEnv) {
const scriptFunc = (window as any)[flag] as ScriptFunc;
this.execScriptEntry({
scriptLoadInfo: scriptInfo,
scriptFunc: scriptFunc,
scriptFlag: flag,
envInfo: envInfo,
});
this.earlyScriptFlag.add(flag);
}
execScriptEntry(scriptEntry: ExecScriptEntry) {
const { scriptLoadInfo, scriptFunc, envInfo } = scriptEntry;
const execScript = new ExecScript(scriptLoadInfo, {
envPrefix: "scripting",
message: this.msg,
contentMsg: this.contentMsg,
code: scriptFunc,
envInfo,
});
this.execScriptMap.set(scriptLoadInfo.uuid, execScript);
const metadata = scriptLoadInfo.metadata || {};
const resource = scriptLoadInfo.resource;
// 注入css
if (metadata["require-css"] && resource) {
for (const val of metadata["require-css"]) {
const res = resource[val];
if (res) {
addStyleSheet(res.content);
}
}
}
if (metadata["run-at"] && metadata["run-at"][0] === "document-body") {
// 等待页面加载完成
waitBody(execScript.exec);
} else {
try {
execScript.exec();
} catch {
// 屏蔽错误,防止脚本报错导致后续脚本无法执行
}
}
}
}