-
Notifications
You must be signed in to change notification settings - Fork 321
Expand file tree
/
Copy pathscript.ts
More file actions
264 lines (258 loc) · 8.7 KB
/
script.ts
File metadata and controls
264 lines (258 loc) · 8.7 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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
import { uuidv4 } from "@App/pkg/utils/uuid";
import type { SCMetadata, Script, ScriptCode, UserConfig } from "@App/app/repo/scripts";
import {
SCRIPT_RUN_STATUS_COMPLETE,
SCRIPT_STATUS_DISABLE,
SCRIPT_STATUS_ENABLE,
SCRIPT_TYPE_BACKGROUND,
SCRIPT_TYPE_CRONTAB,
SCRIPT_TYPE_NORMAL,
ScriptCodeDAO,
ScriptDAO,
} from "@App/app/repo/scripts";
import type { Subscribe } from "@App/app/repo/subscribe";
import { SubscribeStatusType, SubscribeDAO } from "@App/app/repo/subscribe";
import { nextTimeDisplay } from "./cron";
import { parseUserConfig } from "./yaml";
import { t as i18n_t } from "@App/locales/locales";
import { readBlobContent } from "@App/pkg/utils/encoding";
const HEADER_BLOCK = /\/\/[ \t]*==User(Script|Subscribe)==([\s\S]+?)\/\/[ \t]*==\/User\1==/m;
const META_LINE = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/gm;
// 从脚本代码抽出Metadata
export function parseMetadata(code: string): SCMetadata | null {
let isSubscribe = false;
let headerContent: string;
let m: RegExpExecArray | null;
if ((m = HEADER_BLOCK.exec(code))) {
isSubscribe = m[1] === "Subscribe";
headerContent = m[2];
} else {
return null;
}
const metadata: SCMetadata = {} as SCMetadata;
META_LINE.lastIndex = 0; // 重置正则表达式的lastIndex(用于复用)
while ((m = META_LINE.exec(headerContent)) !== null) {
const key = m[1].toLowerCase();
const val = m[2]?.trim() ?? "";
const values = metadata[key] || (metadata[key] = []);
values.push(val);
}
if (!metadata.name || Object.keys(metadata).length < 3) return null;
if (!metadata.namespace) metadata.namespace = [""];
if (isSubscribe) metadata.usersubscribe = []; // 如果是 user.sub.js, 在 metadata 会有一个额外的 usersubscribe
return metadata;
}
// 从网址取得脚本代码
export async function fetchScriptBody(url: string, signal?: AbortSignal): Promise<string> {
const resp = await fetch(url, {
signal,
headers: {
"Cache-Control": "no-cache",
},
});
if (resp.status !== 200) {
throw new Error("fetch script info failed");
}
if (resp.headers.get("content-type")?.includes("text/html")) {
throw new Error("url is html");
}
const body = await readBlobContent(resp, resp.headers.get("content-type"));
return body;
}
// 通过代码解析出脚本基本信息 (不含数据库查询)
export function parseScriptFromCode(code: string, origin: string, uuid?: string): Script {
const metadata = parseMetadata(code);
if (!metadata) {
throw new Error(i18n_t("error_metadata_invalid"));
}
// 不接受空白name
if (!metadata.name?.[0]) {
throw new Error(i18n_t("error_script_name_required"));
}
// 可接受空白namespace
if (metadata.namespace === undefined) {
throw new Error(i18n_t("error_script_namespace_required"));
}
// 可接受空白version
let type = SCRIPT_TYPE_NORMAL;
if (metadata.crontab !== undefined) {
type = SCRIPT_TYPE_CRONTAB;
try {
nextTimeDisplay(metadata.crontab[0]);
} catch {
throw new Error(i18n_t("error_cron_invalid", { expr: metadata.crontab[0] }));
}
} else if (metadata.background !== undefined) {
type = SCRIPT_TYPE_BACKGROUND;
}
let domain = "";
let checkUpdateUrl = "";
let downloadUrl = origin;
if (metadata.updateurl && metadata.downloadurl) {
[checkUpdateUrl] = metadata.updateurl;
[downloadUrl] = metadata.downloadurl;
} else {
checkUpdateUrl = origin.replace("user.js", "meta.js");
}
if (origin.startsWith("http://") || origin.startsWith("https://")) {
const u = new URL(origin);
domain = u.hostname;
}
const newUUID = uuid || uuidv4();
const config: UserConfig | undefined = parseUserConfig(code);
const now = Date.now();
return {
uuid: newUUID,
name: metadata.name[0],
author: metadata.author && metadata.author[0],
namespace: metadata.namespace[0], // 上面的代码已检查 meta.namespace, 不会为undefined
originDomain: domain,
origin,
checkUpdate: true,
checkUpdateUrl,
downloadUrl,
config,
metadata,
selfMetadata: {},
sort: -1,
type,
status: SCRIPT_STATUS_DISABLE,
runStatus: SCRIPT_RUN_STATUS_COMPLETE,
createtime: now,
updatetime: now,
checktime: now,
};
}
// 通过代码解析出脚本信息 (Script)
export async function prepareScriptByCode(
code: string,
origin: string,
uuid?: string,
override: boolean = false,
dao?: ScriptDAO,
options?: {
byEditor?: boolean; // 是否通过编辑器导入
byWebRequest?: boolean; // 是否通过網頁連結安裝或更新
}
): Promise<{ script: Script; oldScript?: Script; oldScriptCode?: string }> {
dao = dao ?? new ScriptDAO();
const script = parseScriptFromCode(code, origin, uuid);
let old: Script | undefined;
let oldCode: ScriptCode | undefined;
if (uuid) {
old = await dao.get(uuid);
}
if (!old && (!uuid || override)) {
old = await dao.findByNameAndNamespace(script.name, script.namespace);
}
if (!old && options?.byWebRequest) {
const test = await dao.searchExistingScript(script);
if (test.length === 1) {
const testCheckUrl = test[0]?.checkUpdateUrl;
if (testCheckUrl) {
// 尝试下载该脚本的url, 检查是否指向要求脚本
try {
const code = await fetchScriptBody(testCheckUrl);
const metadata = code ? parseMetadata(code) : null;
if (metadata && metadata.name![0] === script.name && (metadata.namespace?.[0] || "") === script.namespace) {
old = test[0];
}
} catch {
/* empty */
}
}
}
}
const hasGrantConflict = (metadata: SCMetadata | undefined | null) =>
metadata?.grant?.includes("none") && metadata?.grant?.some((s: string) => s.startsWith("GM"));
const hasDuplicatedMetaline = (metadata: SCMetadata | undefined | null) => {
if (metadata) {
for (const list of Object.values(metadata)) {
if (list && new Set(list).size !== list.length) return true;
}
}
};
if (options?.byEditor && hasGrantConflict(script.metadata) && (!old || !hasGrantConflict(old.metadata))) {
throw new Error(i18n_t("error_grant_conflict"));
}
if (options?.byEditor && hasDuplicatedMetaline(script.metadata) && (!old || !hasDuplicatedMetaline(old.metadata))) {
throw new Error(i18n_t("error_metadata_line_duplicated"));
}
if (old) {
if (
(old.type === SCRIPT_TYPE_NORMAL && script.type !== SCRIPT_TYPE_NORMAL) ||
(script.type === SCRIPT_TYPE_NORMAL && old.type !== SCRIPT_TYPE_NORMAL)
) {
throw new Error(i18n_t("error_script_type_mismatch"));
}
const scriptCode = await new ScriptCodeDAO().get(old.uuid);
if (!scriptCode) {
throw new Error(i18n_t("error_old_script_code_missing"));
}
oldCode = scriptCode;
const { uuid, createtime, lastruntime, error, sort, selfMetadata, subscribeUrl, checkUpdate, status } = old;
Object.assign(script, {
uuid,
createtime,
lastruntime,
error,
sort,
selfMetadata: selfMetadata || {},
subscribeUrl,
checkUpdate,
status,
});
} else {
// 前台脚本默认开启
if (script.type === SCRIPT_TYPE_NORMAL) {
script.status = SCRIPT_STATUS_ENABLE;
}
script.checktime = Date.now();
}
return { script, oldScript: old, oldScriptCode: oldCode?.code };
}
// 通过代码解析出脚本信息 (Subscribe)
export async function prepareSubscribeByCode(
code: string,
url: string
): Promise<{ subscribe: Subscribe; oldSubscribe?: Subscribe }> {
/*
// ==UserSubscribe==
// @name xxx
// @description 订阅xxx系列脚本
// @version 0.1.0
// @author You
// @connect www.baidu.com
// @scriptUrl https://script.tampermonkey.net.cn/48.user.js
// @scriptUrl https://script.tampermonkey.net.cn/49.user.js
// ==/UserSubscribe==
*/
const dao = new SubscribeDAO();
const metadata = parseMetadata(code);
if (!metadata) {
throw new Error(i18n_t("error_metadata_invalid"));
}
if (metadata.name === undefined) {
throw new Error(i18n_t("error_subscribe_name_required"));
}
const now = Date.now();
const subscribe: Subscribe = {
url, // url of the user.sub.js
name: metadata.name[0],
code,
author: (metadata.author && metadata.author[0]) || "",
scripts: {},
metadata: metadata,
status: SubscribeStatusType.enable,
createtime: now,
updatetime: now,
checktime: now,
};
const old = await dao.get(url); // 已存在 -> 把之前的 scripts, createtime, status 抽出来
if (old) {
const { url, scripts, createtime, status } = old;
// url 是一样的;Subscribe 不使用 name 和 namespace 判断,仅使用 url 作唯一键
Object.assign(subscribe, { url, scripts, createtime, status });
}
return { subscribe, oldSubscribe: old };
}