2025-04-21 18:02:35 +08:00

950 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger";
import { Script, ScriptDAO } from "@App/app/repo/scripts";
import { ExtMessageSender, GetSender, Group, MessageSend } from "@Packages/message/server";
import { ValueService } from "@App/app/service/service_worker/value";
import PermissionVerify, { ConfirmParam } from "./permission_verify";
import { connect, sendMessage } from "@Packages/message/client";
import Cache, { incr } from "@App/app/cache";
import EventEmitter from "eventemitter3";
import { MessageQueue } from "@Packages/message/message_queue";
import { RuntimeService } from "./runtime";
import { getIcon, isFirefox } from "@App/pkg/utils/utils";
import { MockMessageConnect } from "@Packages/message/mock_message";
import i18next, { i18nName } from "@App/locales/locales";
import { SystemConfig } from "@App/pkg/config/config";
import FileSystemFactory from "@Packages/filesystem/factory";
import FileSystem from "@Packages/filesystem/filesystem";
import { isWarpTokenError } from "@Packages/filesystem/error";
import { joinPath } from "@Packages/filesystem/utils";
// GMApi,处理脚本的GM API调用请求
export type MessageRequest = {
uuid: string; // 脚本id
api: string;
runFlag: string;
params: any[];
};
export type Request = MessageRequest & {
script: Script;
};
export const unsafeHeaders: { [key: string]: boolean } = {
// 部分浏览器中并未允许
"user-agent": true,
// 这两个是前缀
"proxy-": true,
"sec-": true,
// cookie已经特殊处理
cookie: true,
"accept-charset": true,
"accept-encoding": true,
"access-control-request-headers": true,
"access-control-request-method": true,
connection: true,
"content-length": true,
date: true,
dnt: true,
expect: true,
"feature-policy": true,
host: true,
"keep-alive": true,
origin: true,
referer: true,
te: true,
trailer: true,
"transfer-encoding": true,
upgrade: true,
via: true,
};
type NotificationData = {
uuid: string;
details: GMTypes.NotificationDetails;
sender: ExtMessageSender;
};
export type Api = (request: Request, con: GetSender) => Promise<any>;
export default class GMApi {
logger: Logger;
scriptDAO: ScriptDAO = new ScriptDAO();
constructor(
private systemConfig: SystemConfig,
private permissionVerify: PermissionVerify,
private group: Group,
private send: MessageSend,
private mq: MessageQueue,
private value: ValueService,
private runtime: RuntimeService
) {
this.logger = LoggerCore.logger().with({ service: "runtime/gm_api" });
}
async handlerRequest(data: MessageRequest, sender: GetSender) {
this.logger.trace("GM API request", { api: data.api, uuid: data.uuid, param: data.params });
const api = PermissionVerify.apis.get(data.api);
if (!api) {
throw new Error("gm api is not found");
}
const req = await this.parseRequest(data);
try {
await this.permissionVerify.verify(req, api);
} catch (e) {
this.logger.error("verify error", { api: data.api }, Logger.E(e));
throw e;
}
return api.api.call(this, req, sender);
}
// 解析请求
async parseRequest(data: MessageRequest): Promise<Request> {
const script = await this.scriptDAO.get(data.uuid);
if (!script) {
throw new Error("script is not found");
}
const req: Request = <Request>data;
req.script = script;
return req;
}
@PermissionVerify.API({
confirm(request: Request) {
if (request.params[0] === "store") {
return Promise.resolve(true);
}
const detail = <GMTypes.CookieDetails>request.params[1];
if (!detail.url && !detail.domain) {
return Promise.reject(new Error("there must be one of url or domain"));
}
let url: URL = <URL>{};
if (detail.url) {
url = new URL(detail.url);
} else {
url.host = detail.domain || "";
url.hostname = detail.domain || "";
}
let flag = false;
if (request.script.metadata.connect) {
const { connect } = request.script.metadata;
for (let i = 0; i < connect.length; i += 1) {
if (url.hostname.endsWith(connect[i])) {
flag = true;
break;
}
}
}
if (!flag) {
return Promise.reject(new Error("hostname must be in the definition of connect"));
}
const metadata: { [key: string]: string } = {};
metadata[i18next.t("script_name")] = i18nName(request.script);
metadata[i18next.t("request_domain")] = url.host;
return Promise.resolve({
permission: "cookie",
permissionValue: url.host,
title: i18next.t("access_cookie_content")!,
metadata,
describe: i18next.t("confirm_script_operation")!,
permissionContent: i18next.t("cookie_domain")!,
uuid: "",
});
},
})
async GM_cookie(request: Request, sender: GetSender) {
const param = request.params;
if (param.length !== 2) {
throw new Error("there must be two parameters");
}
const detail = <GMTypes.CookieDetails>request.params[1];
// url或者域名不能为空
if (detail.url) {
detail.url = detail.url.trim();
}
if (detail.domain) {
detail.domain = detail.domain.trim();
}
if (!detail.url && !detail.domain) {
throw new Error("there must be one of url or domain");
}
// 处理tab的storeid
let tabId = sender.getExtMessageSender().tabId;
let storeId: string | undefined;
if (tabId !== -1) {
const stores = await chrome.cookies.getAllCookieStores();
const store = stores.find((val) => val.tabIds.includes(tabId));
if (store) {
storeId = store.id;
}
}
switch (param[0]) {
case "list": {
return chrome.cookies.getAll({
domain: detail.domain,
name: detail.name,
path: detail.path,
secure: detail.secure,
session: detail.session,
url: detail.url,
storeId: storeId,
});
}
case "delete": {
if (!detail.url || !detail.name) {
throw new Error("delete operation must have url and name");
}
await chrome.cookies.remove({
name: detail.name,
url: detail.url,
storeId: storeId,
});
break;
}
case "set": {
if (!detail.url || !detail.name) {
throw new Error("set operation must have name and value");
}
await chrome.cookies.set({
url: detail.url,
name: detail.name,
domain: detail.domain,
value: detail.value,
expirationDate: detail.expirationDate,
path: detail.path,
httpOnly: detail.httpOnly,
secure: detail.secure,
storeId: storeId,
});
break;
}
default: {
throw new Error("action can only be: get, set, delete, store");
}
}
}
@PermissionVerify.API()
GM_log(request: Request): Promise<boolean> {
const message = request.params[0];
const level = request.params[1] || "info";
const labels = request.params[2] || {};
LoggerCore.logger(labels).log(level, message, {
uuid: request.uuid,
name: request.script.name,
component: "GM_log",
});
return Promise.resolve(true);
}
@PermissionVerify.API()
async GM_setValue(request: Request, sender: GetSender) {
if (!request.params || request.params.length !== 2) {
throw new Error("param is failed");
}
const [key, value] = request.params;
await this.value.setValue(request.script.uuid, key, value, {
runFlag: request.runFlag,
tabId: sender.getSender().tab?.id,
});
}
@PermissionVerify.API()
CAT_userConfig(request: Request) {
chrome.tabs.create({
url: `/src/options.html#/?userConfig=${request.uuid}`,
active: true,
});
}
@PermissionVerify.API({
confirm: (request: Request) => {
const [action, details] = request.params;
if (action === "config") {
return Promise.resolve(true);
}
const dir = details.baseDir ? details.baseDir : request.script.uuid;
const metadata: { [key: string]: string } = {};
metadata[i18next.t("script_name")] = i18nName(request.script);
return Promise.resolve({
permission: "file_storage",
permissionValue: dir,
title: i18next.t("script_operation_title"),
metadata,
describe: i18next.t("script_operation_description", { dir }),
wildcard: false,
permissionContent: i18next.t("script_permission_content"),
} as ConfirmParam);
},
})
async CAT_fileStorage(request: Request, sender: GetSender): Promise<{ action: string; data: any } | boolean> {
const [action, details] = request.params;
if (action === "config") {
chrome.tabs.create({
url: `/src/options.html#/setting`,
active: true,
});
return true;
}
const fsConfig = await this.systemConfig.getCatFileStorage();
if (fsConfig.status === "unset") {
return { action: "error", data: { code: 1, error: "file storage is unset" } };
}
if (fsConfig.status === "error") {
return { action: "error", data: { code: 2, error: "file storage is error" } };
}
let fs: FileSystem;
const baseDir = `ScriptCat/app/${details.baseDir ? details.baseDir : request.script.uuid}`;
try {
fs = await FileSystemFactory.create(fsConfig.filesystem, fsConfig.params[fsConfig.filesystem]);
await FileSystemFactory.mkdirAll(fs, baseDir);
fs = await fs.openDir(baseDir);
} catch (e: any) {
if (isWarpTokenError(e)) {
fsConfig.status = "error";
this.systemConfig.setCatFileStorage(fsConfig);
return { action: "error", data: { code: 2, error: e.error.message } };
}
return { action: "error", data: { code: 8, error: e.message } };
}
switch (action) {
case "list":
try {
const list = await fs.list();
list.forEach((file) => {
(<any>file).absPath = file.path;
file.path = joinPath(file.path.substring(file.path.indexOf(baseDir) + baseDir.length));
});
return { action: "onload", data: list };
} catch (e: any) {
return { action: "error", data: { code: 3, error: e.message } };
}
case "upload":
try {
const w = await fs.create(details.path);
await w.write(await (await fetch(<string>details.data)).blob());
return { action: "onload", data: true };
} catch (e: any) {
return { action: "error", data: { code: 4, error: e.message } };
}
case "download":
try {
const info = <CATType.FileStorageFileInfo>details.file;
fs = await fs.openDir(`${info.path}`);
const r = await fs.open({
fsid: (<any>info).fsid,
name: info.name,
path: info.absPath,
size: info.size,
digest: info.digest,
createtime: info.createtime,
updatetime: info.updatetime,
});
const blob = await r.read("blob");
const url = URL.createObjectURL(blob);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 30 * 1000);
return { action: "onload", data: url };
} catch (e: any) {
return { action: "error", data: { code: 5, error: e.message } };
}
break;
case "delete":
try {
await fs.delete(`${details.path}`);
return { action: "onload", data: true };
} catch (e: any) {
return { action: "error", data: { code: 6, error: e.message } };
}
default:
throw new Error("action is not supported");
}
}
// 根据header生成dnr规则
async buildDNRRule(reqeustId: number, params: GMSend.XHRDetails): Promise<{ [key: string]: string }> {
// 检查是否有unsafe header,有则生成dnr规则
const headers = params.headers;
if (!headers) {
return {};
}
const requestHeaders = [
{
header: "X-Scriptcat-GM-XHR-Request-Id",
operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE,
},
] as chrome.declarativeNetRequest.ModifyHeaderInfo[];
Object.keys(headers).forEach((key) => {
const lowKey = key.toLowerCase();
if (headers[key]) {
if (unsafeHeaders[lowKey] || lowKey.startsWith("sec-") || lowKey.startsWith("proxy-")) {
if (headers[key]) {
requestHeaders.push({
header: key,
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
value: headers[key],
});
}
delete headers[key];
}
} else {
requestHeaders.push({
header: key,
operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE,
});
delete headers[key];
}
});
// 判断是否是anonymous
if (params.anonymous) {
// 如果是anonymous并且有cookie则设置为自定义的cookie
if (params.cookie) {
requestHeaders.push({
header: "cookie",
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
value: params.cookie,
});
} else {
// 否则删除cookie
requestHeaders.push({
header: "cookie",
operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE,
});
}
} else if (params.cookie) {
// 否则正常携带cookie header
headers["cookie"] = params.cookie;
}
const ruleId = reqeustId;
const rule = {} as chrome.declarativeNetRequest.Rule;
rule.id = ruleId;
rule.action = {
type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,
requestHeaders: requestHeaders,
};
rule.priority = 1;
const tabs = await chrome.tabs.query({});
const excludedTabIds: number[] = [];
tabs.forEach((tab) => {
if (tab.id) {
excludedTabIds.push(tab.id);
}
});
rule.condition = {
resourceTypes: [chrome.declarativeNetRequest.ResourceType.XMLHTTPREQUEST],
urlFilter: params.url,
requestMethods: [(params.method || "GET").toLowerCase() as chrome.declarativeNetRequest.RequestMethod],
excludedTabIds: excludedTabIds,
};
await chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [ruleId],
addRules: [rule],
});
return headers;
}
gmXhrHeadersReceived: EventEmitter = new EventEmitter();
dealFetch(config: GMSend.XHRDetails, response: Response, readyState: 0 | 1 | 2 | 3 | 4) {
let respHeader = "";
response.headers.forEach((value, key) => {
respHeader += `${key}: ${value}\n`;
});
const respond: GMTypes.XHRResponse = {
finalUrl: response.url || config.url,
readyState,
status: response.status,
statusText: response.statusText,
responseHeaders: respHeader,
responseType: config.responseType,
};
return respond;
}
CAT_fetch(config: GMSend.XHRDetails, con: GetSender, resultParam: { requestId: number; responseHeader: string }) {
const { url } = config;
let connect = con.getConnect();
return fetch(url, {
method: config.method || "GET",
body: <any>config.data,
headers: config.headers,
}).then((resp) => {
const send = this.dealFetch(config, resp, 1);
const reader = resp.body?.getReader();
if (!reader) {
throw new Error("read is not found");
}
const _this = this;
reader.read().then(function read({ done, value }) {
if (done) {
const data = _this.dealFetch(config, resp, 4);
data.responseHeaders = resultParam.responseHeader || data.responseHeaders;
connect.sendMessage({
action: "onreadystatechange",
data: data,
});
connect.sendMessage({
action: "onload",
data: data,
});
connect.sendMessage({
action: "onloadend",
data: data,
});
} else {
connect.sendMessage({
action: "onstream",
data: Array.from(value),
});
reader.read().then(read);
}
});
send.responseHeaders = resultParam.responseHeader || send.responseHeaders;
connect.sendMessage({
action: "onloadstart",
data: send,
});
send.readyState = 2;
connect.sendMessage({
action: "onreadystatechange",
data: send,
});
});
}
@PermissionVerify.API({
confirm: async (request: Request) => {
const config = <GMSend.XHRDetails>request.params[0];
const url = new URL(config.url);
if (request.script.metadata.connect) {
const { connect } = request.script.metadata;
for (let i = 0; i < connect.length; i += 1) {
if (url.hostname.endsWith(connect[i])) {
return Promise.resolve(true);
}
}
}
const metadata: { [key: string]: string } = {};
metadata[i18next.t("script_name")] = i18nName(request.script);
metadata[i18next.t("request_domain")] = url.hostname;
metadata[i18next.t("request_url")] = config.url;
return Promise.resolve({
permission: "cors",
permissionValue: url.hostname,
title: i18next.t("script_accessing_cross_origin_resource"),
metadata,
describe: i18next.t("confirm_operation_description"),
wildcard: true,
permissionContent: i18next.t("domain"),
} as ConfirmParam);
},
})
async GM_xmlhttpRequest(request: Request, sender: GetSender) {
if (request.params.length === 0) {
throw new Error("param is failed");
}
const params = request.params[0] as GMSend.XHRDetails;
// 先处理unsafe hearder
// 关联自己生成的请求id与chrome.webRequest的请求id
const requestId = 10000 + (await incr(Cache.getInstance(), "gmXhrRequestId", 1));
// 添加请求header
if (!params.headers) {
params.headers = {};
}
params.headers["X-Scriptcat-GM-XHR-Request-Id"] = requestId.toString();
params.headers = await this.buildDNRRule(requestId, request.params[0]);
let resultParam = {
requestId,
responseHeader: "",
};
// 等待response
this.gmXhrHeadersReceived.addListener(
"headersReceived:" + requestId,
(details: chrome.webRequest.WebResponseHeadersDetails) => {
details.responseHeaders?.forEach((header) => {
resultParam.responseHeader += header.name + ": " + header.value + "\n";
});
this.gmXhrHeadersReceived.removeAllListeners("headersReceived:" + requestId);
}
);
if (params.responseType === "stream" || params.fetch || params.redirect) {
// 只有fetch支持ReadableStream、redirect这些直接使用fetch
return this.CAT_fetch(params, sender, resultParam);
}
// 再发送到offscreen, 处理请求
const offscreenCon = await connect(this.send, "offscreen/gmApi/xmlHttpRequest", request.params[0]);
offscreenCon.onMessage((msg: { action: string; data: any }) => {
// 发送到content
// 替换msg.data.responseHeaders
msg.data.responseHeaders = resultParam.responseHeader || msg.data.responseHeaders;
sender.getConnect().sendMessage(msg);
});
sender.getConnect().onDisconnect(() => {
// 关闭连接
offscreenCon.disconnect();
});
}
@PermissionVerify.API()
GM_registerMenuCommand(request: Request, sender: GetSender) {
const [id, name, accessKey] = request.params;
// 触发菜单注册, 在popup中处理
this.mq.emit("registerMenuCommand", {
uuid: request.script.uuid,
id: id,
name: name,
accessKey: accessKey,
tabId: sender.getSender().tab?.id || -1,
frameId: sender.getSender().frameId,
documentId: sender.getSender().documentId,
});
}
@PermissionVerify.API()
GM_unregisterMenuCommand(request: Request, sender: GetSender) {
const [id] = request.params;
// 触发菜单取消注册, 在popup中处理
this.mq.emit("unregisterMenuCommand", {
uuid: request.script.uuid,
id: id,
tabId: sender.getSender().tab?.id || -1,
frameId: sender.getSender().frameId,
});
}
@PermissionVerify.API({})
async GM_openInTab(request: Request, sender: GetSender) {
const url = request.params[0];
const options = request.params[1] || {};
if (options.useOpen === true) {
// 发送给offscreen页面处理
const ok = await sendMessage(this.send, "offscreen/gmApi/openInTab", { url });
if (ok) {
// 由于window.open强制在前台打开标签因此获取状态为{ active:true }的标签即为新标签
const [tab] = await chrome.tabs.query({ active: true });
await Cache.getInstance().set(`GM_openInTab:${tab.id}`, {
uuid: request.uuid,
sender: sender.getExtMessageSender(),
});
return tab.id;
} else {
// 当新tab被浏览器阻止时window.open()会返回null 视为已经关闭
// 似乎在Firefox中禁止在background页面使用window.open()强制返回null
return false;
}
} else {
const tab = await chrome.tabs.create({ url, active: options.active });
await Cache.getInstance().set(`GM_openInTab:${tab.id}`, {
uuid: request.uuid,
sender: sender.getExtMessageSender(),
});
return tab.id;
}
}
@PermissionVerify.API({
link: "GM_openInTab",
})
async GM_closeInTab(request: Request): Promise<boolean> {
try {
await chrome.tabs.remove(<number>request.params[0]);
} catch (e) {
this.logger.error("GM_closeInTab", Logger.E(e));
}
return Promise.resolve(true);
}
@PermissionVerify.API({})
GM_getTab(request: Request, sender: GetSender) {
return Cache.getInstance()
.tx(`GM_getTab:${request.uuid}`, async (tabData: { [key: number]: any }) => {
return tabData || {};
})
.then((data) => {
return data[sender.getExtMessageSender().tabId];
});
}
@PermissionVerify.API()
GM_saveTab(request: Request, sender: GetSender) {
const data = request.params[0];
const tabId = sender.getExtMessageSender().tabId;
return Cache.getInstance()
.tx(`GM_getTab:${request.uuid}`, (tabData: { [key: number]: any }) => {
tabData = tabData || {};
tabData[tabId] = data;
return Promise.resolve(tabData);
})
.then(() => true);
}
@PermissionVerify.API()
GM_getTabs(request: Request) {
return Cache.getInstance().tx(`GM_getTab:${request.uuid}`, async (tabData: { [key: number]: any }) => {
return tabData || {};
});
}
@PermissionVerify.API({})
GM_notification(request: Request, sender: GetSender) {
if (request.params.length === 0) {
throw new Error("param is failed");
}
const details: GMTypes.NotificationDetails = request.params[0];
const options: chrome.notifications.NotificationOptions<true> = {
title: details.title || "ScriptCat",
message: details.text || "无消息内容",
iconUrl: details.image || getIcon(request.script) || chrome.runtime.getURL("assets/logo.png"),
type: isFirefox() || details.progress === undefined ? "basic" : "progress",
};
if (!isFirefox()) {
options.silent = details.silent;
options.buttons = details.buttons;
}
options.progress = options.progress && parseInt(details.progress as any, 10);
return new Promise((resolve) => {
chrome.notifications.create(options, (notificationId) => {
Cache.getInstance().set(`GM_notification:${notificationId}`, {
uuid: request.script.uuid,
details: details,
sender: sender.getExtMessageSender(),
});
if (details.timeout) {
setTimeout(() => {
chrome.notifications.clear(notificationId);
Cache.getInstance().del(`GM_notification:${notificationId}`);
}, details.timeout);
}
resolve(notificationId);
});
});
}
@PermissionVerify.API({
link: "GM_notification",
})
GM_closeNotification(request: Request) {
if (request.params.length === 0) {
throw new Error("param is failed");
}
const [notificationId] = request.params;
Cache.getInstance().del(`GM_notification:${notificationId}`);
chrome.notifications.clear(notificationId);
}
@PermissionVerify.API({
link: "GM_notification",
})
GM_updateNotification(request: Request) {
if (isFirefox()) {
throw new Error("firefox does not support this method");
}
const id = request.params[0];
const details: GMTypes.NotificationDetails = request.params[1];
const options: chrome.notifications.NotificationOptions = {
title: details.title,
message: details.text,
iconUrl: details.image,
type: details.progress === undefined ? "basic" : "progress",
silent: details.silent,
progress: details.progress && parseInt(details.progress as any, 10),
};
chrome.notifications.update(<string>id, options);
}
@PermissionVerify.API()
async GM_download(request: Request, sender: GetSender) {
const params = <GMTypes.DownloadDetails>request.params[0];
// blob本地文件直接下载
if (params.url.startsWith("blob:")) {
chrome.downloads.download(
{
url: params.url,
saveAs: params.saveAs,
filename: params.name,
},
() => {
sender.getConnect().sendMessage({ event: "onload" });
}
);
return;
}
// 使用xhr下载blob,再使用download api创建下载
const EE = new EventEmitter();
const mockConnect = new MockMessageConnect(EE);
EE.addListener("message", (data: any) => {
const xhr = data.data;
const respond: any = {
finalUrl: xhr.url,
readyState: xhr.readyState,
status: xhr.status,
statusText: xhr.statusText,
responseHeaders: xhr.responseHeaders,
};
switch (data.action) {
case "onload":
sender.getConnect().sendMessage({
action: "onload",
data: respond,
});
chrome.downloads.download({
url: xhr.response,
saveAs: params.saveAs,
filename: params.name,
});
break;
case "onerror":
sender.getConnect().sendMessage({
action: "onerror",
data: respond,
});
break;
case "onprogress":
respond.done = xhr.DONE;
respond.lengthComputable = xhr.lengthComputable;
respond.loaded = xhr.loaded;
respond.total = xhr.total;
respond.totalSize = xhr.total;
sender.getConnect().sendMessage({
action: "onprogress",
data: respond,
});
break;
case "ontimeout":
sender.getConnect().sendMessage({
action: "ontimeout",
});
break;
}
});
// 处理参数问题
request.params[0] = {
method: params.method || "GET",
url: params.url,
headers: params.headers,
timeout: params.timeout,
cookie: params.cookie,
anonymous: params.anonymous,
responseType: "blob",
} as GMSend.XHRDetails;
return this.GM_xmlhttpRequest(request, new GetSender(mockConnect));
}
@PermissionVerify.API()
async GM_setClipboard(request: Request) {
let [data, type] = request.params;
type = type || "text/plain";
await sendMessage(this.send, "offscreen/gmApi/setClipboard", { data, type });
}
handlerNotification() {
const send = async (event: string, notificationId: string, params?: any) => {
const ret = (await Cache.getInstance().get(`GM_notification:${notificationId}`)) as NotificationData;
if (ret) {
this.runtime.emitEventToTab(ret.sender, {
event: "GM_notification",
eventId: notificationId,
uuid: ret.uuid,
data: {
event,
params,
},
});
}
};
chrome.notifications.onClosed.addListener((notificationId, byUser) => {
send("close", notificationId, {
byUser,
});
Cache.getInstance().del(`GM_notification:${notificationId}`);
});
chrome.notifications.onClicked.addListener((notificationId) => {
send("click", notificationId);
});
chrome.notifications.onButtonClicked.addListener((notificationId, index) => {
send("buttonClick", notificationId, {
index: index,
});
});
}
// 处理GM_xmlhttpRequest请求
handlerGmXhr() {
chrome.webRequest.onBeforeSendHeaders.addListener(
(details) => {
if (details.tabId === -1) {
// 判断是否存在X-Scriptcat-GM-XHR-Request-Id
// 讲请求id与chrome.webRequest的请求id关联
if (details.requestHeaders) {
const requestId = details.requestHeaders.find((header) => header.name === "X-Scriptcat-GM-XHR-Request-Id");
if (requestId) {
Cache.getInstance().set("gmXhrRequest:" + details.requestId, requestId.value);
}
}
}
},
{
urls: ["<all_urls>"],
types: ["xmlhttprequest"],
},
["requestHeaders", "extraHeaders"]
);
chrome.webRequest.onHeadersReceived.addListener(
(details) => {
if (details.tabId === -1) {
// 判断请求是否与gmXhrRequest关联
Cache.getInstance()
.get("gmXhrRequest:" + details.requestId)
.then((requestId) => {
if (requestId) {
this.gmXhrHeadersReceived.emit("headersReceived:" + requestId, details);
// 删除关联与DNR
Cache.getInstance().del("gmXhrRequest:" + details.requestId);
chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [parseInt(requestId)],
});
}
});
}
},
{
urls: ["<all_urls>"],
types: ["xmlhttprequest"],
},
["responseHeaders", "extraHeaders"]
);
}
start() {
this.group.on("gmApi", this.handlerRequest.bind(this));
this.handlerGmXhr();
this.handlerNotification();
chrome.tabs.onRemoved.addListener(async (tabId) => {
// 处理GM_openInTab关闭事件
const sender = (await Cache.getInstance().get(`GM_openInTab:${tabId}`)) as {
uuid: string;
sender: ExtMessageSender;
};
if (sender) {
this.runtime.emitEventToTab(sender.sender, {
event: "GM_openInTab",
eventId: tabId.toString(),
uuid: sender.uuid,
data: {
event: "onclose",
tabId: tabId,
},
});
Cache.getInstance().del(`GM_openInTab:${tabId}`);
}
});
}
}