基本实现完成GM API

This commit is contained in:
王一之 2025-04-12 02:05:10 +08:00
parent 5c0d4a2560
commit d697928fb0
20 changed files with 631 additions and 99 deletions

View File

@ -1,5 +1,5 @@
// ==UserScript== // ==UserScript==
// @name New Userscript // @name GM cookie操作
// @namespace https://bbs.tampermonkey.net.cn/ // @namespace https://bbs.tampermonkey.net.cn/
// @version 0.1.0 // @version 0.1.0
// @description 可以控制浏览器的cookie, 必须指定@connect, 并且每次一个新的域调用都需要用户确定 // @description 可以控制浏览器的cookie, 必须指定@connect, 并且每次一个新的域调用都需要用户确定
@ -9,8 +9,6 @@
// @connect example.com // @connect example.com
// ==/UserScript== // ==/UserScript==
// GM_cookie("store") 方法请看storage_name/gm_value.js的例子, 可用于隐身窗口的操作
GM_cookie("set", { GM_cookie("set", {
url: "http://example.com/cookie", url: "http://example.com/cookie",
name: "cookie1", value: "value" name: "cookie1", value: "value"

View File

@ -9,7 +9,7 @@
// ==/UserScript== // ==/UserScript==
GM_download({ GM_download({
url: "https://scriptcat.org/api/v1/gm_crx/download/ScriptCat", url: "https://scriptcat.org/api/v2/open/crx-download/ndcooeababalnlpkfedmmbbbgkljhpjf",
name: "scriptcat.crx", name: "scriptcat.crx",
headers: { headers: {
"referer": "http://www.example.com/", "referer": "http://www.example.com/",

View File

@ -13,13 +13,8 @@
// @storageName example // @storageName example
// ==/UserScript== // ==/UserScript==
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote, tabid) { GM_addValueChangeListener("test_set", function (name, oldval, newval, remote) {
console.log("test_set change", name, oldval, newval, remote, tabid); console.log("test_set change", name, oldval, newval, remote);
// 可以通过tabid获取到触发变化的tab
// GM_cookie.store可以获取到对应的cookie storeId
GM_cookie("store", tabid, (storeId) => {
console.log("store", storeId);
});
}); });
setInterval(() => { setInterval(() => {

View File

@ -14,13 +14,8 @@
// ==/UserScript== // ==/UserScript==
return new Promise((resolve) => { return new Promise((resolve) => {
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote, tabid) { GM_addValueChangeListener("test_set", function (name, oldval, newval, remote) {
console.log("value change", name, oldval, newval, remote, tabid); console.log("value change", name, oldval, newval, remote);
// 可以通过tabid获取到触发变化的tab
// GM_cookie.store可以获取到对应的cookie storeId
GM_cookie("store", tabid, (storeId) => {
console.log("store", storeId);
});
}); });
setInterval(() => { setInterval(() => {

View File

@ -1,6 +1,6 @@
{ {
"name": "scriptcat", "name": "scriptcat",
"version": "0.17.0-alpha.2", "version": "0.17.0-alpha.1",
"description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!", "description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!",
"author": "CodFrm", "author": "CodFrm",
"license": "GPLv3", "license": "GPLv3",

View File

@ -6,9 +6,9 @@ export async function sendMessage(msg: MessageSend, action: string, data?: any):
LoggerCore.getInstance().logger().trace("sendMessage", { action, data, response: res }); LoggerCore.getInstance().logger().trace("sendMessage", { action, data, response: res });
if (res && res.code) { if (res && res.code) {
console.error(res); console.error(res);
return Promise.reject(res.message); throw res.message;
} else { } else {
return Promise.resolve(res.data); return res.data;
} }
} }

View File

@ -138,20 +138,7 @@ export function forwardMessage(
to: MessageSend, to: MessageSend,
middleware?: ApiFunctionSync middleware?: ApiFunctionSync
) { ) {
from.on(path, (params, fromCon) => { const handler = (params: any, fromCon: GetSender) => {
if (middleware) {
// 此处是为了处理CustomEventMessage的同步消息情况
const resp = middleware(params, fromCon) as any;
if (resp instanceof Promise) {
return resp.then((data) => {
if (data !== false) {
return data;
}
});
} else if (resp !== false) {
return resp;
}
}
const fromConnect = fromCon.getConnect(); const fromConnect = fromCon.getConnect();
if (fromConnect) { if (fromConnect) {
connect(to, prefix + "/" + path, params).then((toCon) => { connect(to, prefix + "/" + path, params).then((toCon) => {
@ -171,5 +158,22 @@ export function forwardMessage(
} else { } else {
return sendMessage(to, prefix + "/" + path, params); return sendMessage(to, prefix + "/" + path, params);
} }
};
from.on(path, (params, sender) => {
if (middleware) {
// 此处是为了处理CustomEventMessage的同步消息情况
const resp = middleware(params, sender) as any;
if (resp instanceof Promise) {
return resp.then((data) => {
if (data !== false) {
return data;
}
return handler(params, sender);
});
} else if (resp !== false) {
return resp;
}
return handler(params, sender);
}
}); });
} }

View File

@ -116,7 +116,7 @@ export default class Cache {
ret = await set(); ret = await set();
this.set(key, ret); this.set(key, ret);
} }
return Promise.resolve(ret); return ret;
} }
public set(key: string, value: any): Promise<void> { public set(key: string, value: any): Promise<void> {
@ -173,8 +173,9 @@ export default class Cache {
if (value) { if (value) {
newValue = value; newValue = value;
return this.set(key, value); return this.set(key, value);
} else if (value === undefined) {
return this.del(key);
} }
return Promise.resolve();
}); });
unlock(); unlock();
return newValue!; return newValue!;

View File

@ -75,9 +75,9 @@ export abstract class DAO<T> {
} }
const resp = await this.table.update(id, <any>val); const resp = await this.table.update(id, <any>val);
if (resp) { if (resp) {
return Promise.resolve(id); return id;
} }
return Promise.reject(ErrSaveError); throw ErrSaveError;
} }
public findById(id: number) { public findById(id: number) {

View File

@ -35,7 +35,7 @@ export default class ContentRuntime {
setTimeout(() => { setTimeout(() => {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}, 60 * 1000); }, 60 * 1000);
return Promise.resolve(url); return url;
} }
case "CAT_fetchBlob": { case "CAT_fetchBlob": {
return fetch(data.params[0]).then((res) => res.blob()); return fetch(data.params[0]).then((res) => res.blob());
@ -80,7 +80,7 @@ export default class ContentRuntime {
return nodeId; return nodeId;
} }
} }
return Promise.resolve(false); return false;
} }
); );
const client = new Client(this.msg, "inject"); const client = new Client(this.msg, "inject");

View File

@ -215,7 +215,22 @@ export default class GMApi {
@GMContext.API() @GMContext.API()
public async CAT_fetchDocument(url: string): Promise<Document | undefined> { public async CAT_fetchDocument(url: string): Promise<Document | undefined> {
const data = await this.sendMessage("CAT_fetchDocument", [url]); const data = await this.sendMessage("CAT_fetchDocument", [url]);
return (<CustomEventMessage>this.message).getAndDelRelatedTarget(data.relatedTarget); return (<CustomEventMessage>this.message).getAndDelRelatedTarget(data.relatedTarget) as Document;
}
@GMContext.API()
GM_cookie(
action: string,
details: GMTypes.CookieDetails,
done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void
) {
this.sendMessage("GM_cookie", [action, details])
.then((resp: any) => {
done && done(resp, undefined);
})
.catch((err) => {
done && done(undefined, err);
});
} }
@GMContext.API() @GMContext.API()
@ -479,7 +494,7 @@ export default class GMApi {
break; break;
default: default:
LoggerCore.logger().warn("GM_xmlhttpRequest resp is error", { LoggerCore.logger().warn("GM_xmlhttpRequest resp is error", {
action: data.action, data,
}); });
break; break;
} }
@ -497,6 +512,64 @@ export default class GMApi {
}; };
} }
@GMContext.API()
GM_download(url: GMTypes.DownloadDetails | string, filename?: string): GMTypes.AbortHandle<void> {
let details: GMTypes.DownloadDetails;
if (typeof url === "string") {
details = {
name: filename || "",
url,
};
} else {
details = url;
}
let connect: MessageConnect;
this.connect("GM_download", [
{
method: details.method,
url: details.url,
name: details.name,
headers: details.headers,
saveAs: details.saveAs,
timeout: details.timeout,
cookie: details.cookie,
anonymous: details.anonymous,
},
]).then((con) => {
connect = con;
connect.onMessage((data: { action: string; data: any }) => {
switch (data.action) {
case "onload":
details.onload && details.onload(data.data);
break;
case "onprogress":
details.onprogress && details.onprogress(<GMTypes.XHRProgress>data.data);
break;
case "ontimeout":
details.ontimeout && details.ontimeout();
break;
case "onerror":
details.onerror &&
details.onerror({
error: "unknown",
});
break;
default:
LoggerCore.logger().warn("GM_download resp is error", {
data,
});
break;
}
});
});
return {
abort: () => {
connect?.disconnect();
},
};
}
@GMContext.API({ @GMContext.API({
depend: ["GM_closeNotification", "GM_updateNotification"], depend: ["GM_closeNotification", "GM_updateNotification"],
}) })
@ -576,6 +649,88 @@ export default class GMApi {
this.sendMessage("GM_updateNotification", [id, details]); this.sendMessage("GM_updateNotification", [id, details]);
} }
@GMContext.API({ depend: ["GM_closeInTab"] })
public GM_openInTab(url: string, options?: GMTypes.OpenTabOptions | boolean): GMTypes.Tab {
let option: GMTypes.OpenTabOptions = {};
if (arguments.length === 1) {
option.active = true;
} else if (typeof options === "boolean") {
option.active = !options;
} else {
option = <GMTypes.OpenTabOptions>options;
}
if (option.active === undefined) {
option.active = true;
}
let tabid: any;
const ret: GMTypes.Tab = {
close: () => {
tabid && this.GM_closeInTab(tabid);
},
};
this.sendMessage("GM_openInTab", [url, option]).then((id) => {
if (id) {
tabid = id;
this.EE.addListener("GM_openInTab:" + id, (resp: any) => {
switch (resp.event) {
case "oncreate":
tabid = resp.tabId;
break;
case "onclose":
ret.onclose && ret.onclose();
ret.closed = true;
this.EE.removeAllListeners("GM_openInTab:" + id);
break;
default:
LoggerCore.logger().warn("GM_openInTab resp is error", {
resp,
});
break;
}
});
} else {
ret.onclose && ret.onclose();
ret.closed = true;
}
});
return ret;
}
@GMContext.API()
public GM_closeInTab(tabid: string) {
return this.sendMessage("GM_closeInTab", [tabid]);
}
@GMContext.API()
GM_getTab(callback: (data: any) => void) {
this.sendMessage("GM_getTab", []).then((data) => {
callback(data);
});
}
@GMContext.API()
GM_saveTab(obj: object) {
if (typeof obj === "object") {
obj = JSON.parse(JSON.stringify(obj));
}
this.sendMessage("GM_saveTab", [obj]);
}
@GMContext.API()
GM_getTabs(callback: (objs: { [key: string | number]: object }) => any) {
this.sendMessage("GM_getTabs", []).then((resp) => {
callback(resp);
});
}
@GMContext.API()
GM_setClipboard(data: string, info?: string | { type?: string; minetype?: string }) {
this.sendMessage("GM_setClipboard", [data, info]);
}
@GMContext.API() @GMContext.API()
GM_getResourceURL(name: string, isBlobUrl?: boolean): string | undefined { GM_getResourceURL(name: string, isBlobUrl?: boolean): string | undefined {
if (!this.scriptRes.resource) { if (!this.scriptRes.resource) {

View File

@ -15,7 +15,6 @@ export default class GMApi {
data?: any data?: any
) { ) {
const finalUrl = xhr.responseURL || details.url; const finalUrl = xhr.responseURL || details.url;
// 判断是否有headerFlag-final-url,有则替换finalUrl
let response: GMTypes.XHRResponse = { let response: GMTypes.XHRResponse = {
finalUrl, finalUrl,
readyState: <any>xhr.readyState, readyState: <any>xhr.readyState,
@ -173,7 +172,38 @@ export default class GMApi {
}); });
} }
openInTab({ url }: { url: string }) {
return Promise.resolve(window.open(url) !== undefined);
}
textarea: HTMLTextAreaElement = document.createElement("textarea");
clipboardData: { type?: string; data: string } | undefined;
async setClipboard({ data, type }: { data: string; type: string }) {
this.clipboardData = {
type,
data,
};
this.textarea.focus();
document.execCommand("copy", false, <any>null);
}
init() { init() {
this.textarea.style.display = "none";
document.documentElement.appendChild(this.textarea);
document.addEventListener("copy", (e: ClipboardEvent) => {
if (!this.clipboardData || !e.clipboardData) {
return;
}
e.preventDefault();
const { type, data } = this.clipboardData;
e.clipboardData.setData(type || "text/plain", data);
this.clipboardData = undefined;
});
this.group.on("xmlHttpRequest", this.xmlHttpRequest.bind(this)); this.group.on("xmlHttpRequest", this.xmlHttpRequest.bind(this));
this.group.on("openInTab", this.openInTab.bind(this));
this.group.on("setClipboard", this.setClipboard.bind(this));
} }
} }

View File

@ -206,7 +206,7 @@ export class Runtime {
} else { } else {
this.cronJob.set(script.uuid, cronJobList); this.cronJob.set(script.uuid, cronJobList);
} }
return Promise.resolve(!flag); return !flag;
} }
crontabExec(script: ScriptRunResouce, oncePos: number) { crontabExec(script: ScriptRunResouce, oncePos: number) {

View File

@ -4,12 +4,15 @@ import { Script, ScriptDAO } from "@App/app/repo/scripts";
import { ExtMessageSender, GetSender, Group, MessageSend } from "@Packages/message/server"; import { ExtMessageSender, GetSender, Group, MessageSend } from "@Packages/message/server";
import { ValueService } from "@App/app/service/service_worker/value"; import { ValueService } from "@App/app/service/service_worker/value";
import PermissionVerify from "./permission_verify"; import PermissionVerify from "./permission_verify";
import { connect } from "@Packages/message/client"; import { connect, sendMessage } from "@Packages/message/client";
import Cache, { incr } from "@App/app/cache"; import Cache, { incr } from "@App/app/cache";
import EventEmitter from "eventemitter3"; import EventEmitter from "eventemitter3";
import { MessageQueue } from "@Packages/message/message_queue"; import { MessageQueue } from "@Packages/message/message_queue";
import { RuntimeService } from "./runtime"; import { RuntimeService } from "./runtime";
import { getIcon, isFirefox } from "@App/pkg/utils/utils"; import { getIcon, isFirefox } from "@App/pkg/utils/utils";
import { PopupService } from "./popup";
import { act } from "react";
import { MockMessageConnect } from "@Packages/message/mock_message";
// GMApi,处理脚本的GM API调用请求 // GMApi,处理脚本的GM API调用请求
@ -82,14 +85,14 @@ export default class GMApi {
this.logger.trace("GM API request", { api: data.api, uuid: data.uuid, param: data.params }); this.logger.trace("GM API request", { api: data.api, uuid: data.uuid, param: data.params });
const api = PermissionVerify.apis.get(data.api); const api = PermissionVerify.apis.get(data.api);
if (!api) { if (!api) {
return Promise.reject(new Error("gm api is not found")); throw new Error("gm api is not found");
} }
const req = await this.parseRequest(data); const req = await this.parseRequest(data);
try { try {
await this.permissionVerify.verify(req, api); await this.permissionVerify.verify(req, api);
} catch (e) { } catch (e) {
this.logger.error("verify error", { api: data.api }, Logger.E(e)); this.logger.error("verify error", { api: data.api }, Logger.E(e));
return Promise.reject(e); throw e;
} }
return api.api.call(this, req, sender); return api.api.call(this, req, sender);
} }
@ -98,17 +101,103 @@ export default class GMApi {
async parseRequest(data: MessageRequest): Promise<Request> { async parseRequest(data: MessageRequest): Promise<Request> {
const script = await this.scriptDAO.get(data.uuid); const script = await this.scriptDAO.get(data.uuid);
if (!script) { if (!script) {
return Promise.reject(new Error("script is not found")); throw new Error("script is not found");
} }
const req: Request = <Request>data; const req: Request = <Request>data;
req.script = script; req.script = script;
return Promise.resolve(req); return req;
}
@PermissionVerify.API()
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() @PermissionVerify.API()
async GM_setValue(request: Request, sender: GetSender) { async GM_setValue(request: Request, sender: GetSender) {
if (!request.params || request.params.length !== 2) { if (!request.params || request.params.length !== 2) {
return Promise.reject(new Error("param is failed")); throw new Error("param is failed");
} }
const [key, value] = request.params; const [key, value] = request.params;
await this.value.setValue(request.script.uuid, key, value, { await this.value.setValue(request.script.uuid, key, value, {
@ -122,7 +211,7 @@ export default class GMApi {
// 检查是否有unsafe header,有则生成dnr规则 // 检查是否有unsafe header,有则生成dnr规则
const headers = params.headers; const headers = params.headers;
if (!headers) { if (!headers) {
return Promise.resolve({}); return {};
} }
const requestHeaders = [ const requestHeaders = [
{ {
@ -151,6 +240,26 @@ export default class GMApi {
delete headers[key]; 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 ruleId = reqeustId;
const rule = {} as chrome.declarativeNetRequest.Rule; const rule = {} as chrome.declarativeNetRequest.Rule;
rule.id = ruleId; rule.id = ruleId;
@ -176,16 +285,83 @@ export default class GMApi {
removeRuleIds: [ruleId], removeRuleIds: [ruleId],
addRules: [rule], addRules: [rule],
}); });
return Promise.resolve(headers); return headers;
} }
gmXhrHeadersReceived: EventEmitter = new EventEmitter(); 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,
});
});
}
// TODO: maxRedirects实现 // TODO: maxRedirects实现
@PermissionVerify.API() @PermissionVerify.API()
async GM_xmlhttpRequest(request: Request, con: GetSender) { async GM_xmlhttpRequest(request: Request, sender: GetSender) {
if (request.params.length === 0) { if (request.params.length === 0) {
return Promise.reject(new Error("param is failed")); throw new Error("param is failed");
} }
const params = request.params[0] as GMSend.XHRDetails; const params = request.params[0] as GMSend.XHRDetails;
// 先处理unsafe hearder // 先处理unsafe hearder
@ -197,26 +373,35 @@ export default class GMApi {
} }
params.headers["X-Scriptcat-GM-XHR-Request-Id"] = requestId.toString(); params.headers["X-Scriptcat-GM-XHR-Request-Id"] = requestId.toString();
params.headers = await this.buildDNRRule(requestId, request.params[0]); params.headers = await this.buildDNRRule(requestId, request.params[0]);
let responseHeader = ""; let resultParam = {
requestId,
responseHeader: "",
};
// 等待response // 等待response
this.gmXhrHeadersReceived.addListener( this.gmXhrHeadersReceived.addListener(
"headersReceived:" + requestId, "headersReceived:" + requestId,
(details: chrome.webRequest.WebResponseHeadersDetails) => { (details: chrome.webRequest.WebResponseHeadersDetails) => {
details.responseHeaders?.forEach((header) => { details.responseHeaders?.forEach((header) => {
responseHeader += header.name + ": " + header.value + "\r\n"; resultParam.responseHeader += header.name + ": " + header.value + "\n";
}); });
this.gmXhrHeadersReceived.removeAllListeners("headersReceived:" + requestId); 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, 处理请求 // 再发送到offscreen, 处理请求
const offscreenCon = await connect(this.send, "offscreen/gmApi/xmlHttpRequest", request.params[0]); const offscreenCon = await connect(this.send, "offscreen/gmApi/xmlHttpRequest", request.params[0]);
offscreenCon.onMessage((msg: { action: string; data: any }) => { offscreenCon.onMessage((msg: { action: string; data: any }) => {
// 发送到content // 发送到content
// 替换msg.data.responseHeaders // 替换msg.data.responseHeaders
if (responseHeader) { msg.data.responseHeaders = resultParam.responseHeader || msg.data.responseHeaders;
msg.data.responseHeaders = responseHeader; sender.getConnect().sendMessage(msg);
} });
con.getConnect().sendMessage(msg); sender.getConnect().onDisconnect(() => {
// 关闭连接
offscreenCon.disconnect();
}); });
} }
@ -247,10 +432,83 @@ export default class GMApi {
}); });
} }
@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({}) @PermissionVerify.API({})
GM_notification(request: Request, sender: GetSender) { GM_notification(request: Request, sender: GetSender) {
if (request.params.length === 0) { if (request.params.length === 0) {
return Promise.reject(new Error("param is failed")); throw new Error("param is failed");
} }
const details: GMTypes.NotificationDetails = request.params[0]; const details: GMTypes.NotificationDetails = request.params[0];
const options: chrome.notifications.NotificationOptions<true> = { const options: chrome.notifications.NotificationOptions<true> = {
@ -288,7 +546,7 @@ export default class GMApi {
}) })
GM_closeNotification(request: Request) { GM_closeNotification(request: Request) {
if (request.params.length === 0) { if (request.params.length === 0) {
return Promise.reject(new Error("param is failed")); throw new Error("param is failed");
} }
const [notificationId] = request.params; const [notificationId] = request.params;
Cache.getInstance().del(`GM_notification:${notificationId}`); Cache.getInstance().del(`GM_notification:${notificationId}`);
@ -300,7 +558,7 @@ export default class GMApi {
}) })
GM_updateNotification(request: Request) { GM_updateNotification(request: Request) {
if (isFirefox()) { if (isFirefox()) {
return Promise.reject(new Error("firefox does not support this method")); throw new Error("firefox does not support this method");
} }
const id = request.params[0]; const id = request.params[0];
const details: GMTypes.NotificationDetails = request.params[1]; const details: GMTypes.NotificationDetails = request.params[1];
@ -315,6 +573,91 @@ export default class GMApi {
chrome.notifications.update(<string>id, options); 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() { handlerNotification() {
const send = async (event: string, notificationId: string, params?: any) => { const send = async (event: string, notificationId: string, params?: any) => {
const ret = (await Cache.getInstance().get(`GM_notification:${notificationId}`)) as NotificationData; const ret = (await Cache.getInstance().get(`GM_notification:${notificationId}`)) as NotificationData;
@ -397,5 +740,25 @@ export default class GMApi {
this.group.on("gmApi", this.handlerRequest.bind(this)); this.group.on("gmApi", this.handlerRequest.bind(this));
this.handlerGmXhr(); this.handlerGmXhr();
this.handlerNotification(); 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}`);
}
});
} }
} }

View File

@ -36,8 +36,6 @@ export interface ApiParam {
background?: boolean; background?: boolean;
// 是否需要弹出页面让用户进行确认 // 是否需要弹出页面让用户进行确认
confirm?: (request: Request) => Promise<boolean | ConfirmParam>; confirm?: (request: Request) => Promise<boolean | ConfirmParam>;
// 监听方法
listener?: () => void;
// 别名 // 别名
alias?: string[]; alias?: string[];
// 关联 // 关联
@ -59,9 +57,6 @@ export default class PermissionVerify {
public static API(param: ApiParam = {}) { public static API(param: ApiParam = {}) {
return (target: any, propertyName: string, descriptor: PropertyDescriptor) => { return (target: any, propertyName: string, descriptor: PropertyDescriptor) => {
const key = propertyName; const key = propertyName;
if (param.listener) {
param.listener();
}
PermissionVerify.apis.set(key, { PermissionVerify.apis.set(key, {
api: descriptor.value, api: descriptor.value,
param, param,

View File

@ -183,8 +183,10 @@ export class PopupService {
const scriptMenu = script.map((script) => { const scriptMenu = script.map((script) => {
const run = runScript.find((item) => item.uuid === script.uuid); const run = runScript.find((item) => item.uuid === script.uuid);
if (run) { if (run) {
// 如果脚本已经存在,则不添加,赋值状态 // 如果脚本已经存在,则不添加,更新信息
run.enable = script.status === SCRIPT_STATUS_ENABLE; run.enable = script.status === SCRIPT_STATUS_ENABLE;
run.customExclude = script.customizeExcludeMatches || run.customExclude;
run.hasUserConfig = !!script.config;
return run; return run;
} }
return this.scriptToMenu(script); return this.scriptToMenu(script);
@ -196,6 +198,7 @@ export class PopupService {
scriptMenu.push(script); scriptMenu.push(script);
} }
}); });
console.log("popup脚本菜单", runScript);
// 后台脚本只显示开启或者运行中的脚本 // 后台脚本只显示开启或者运行中的脚本
return { scriptList: scriptMenu, backScriptList: await this.getScriptMenu(-1) }; return { scriptList: scriptMenu, backScriptList: await this.getScriptMenu(-1) };
} }
@ -335,9 +338,19 @@ export class PopupService {
// 监听tab开关 // 监听tab开关
chrome.tabs.onRemoved.addListener((tabId) => { chrome.tabs.onRemoved.addListener((tabId) => {
// 清理数据 // 清理数据tab关闭需要释放的数据
this.txUpdateScriptMenu(tabId, async () => { this.txUpdateScriptMenu(tabId, async (script) => {
return []; script.forEach((script) => {
// 处理GM_saveTab关闭事件, 由于需要用到tab相关的脚本数据所以需要在这里处理
// 避免先删除了数据获取不到
Cache.getInstance().tx(`GM_getTab:${script.uuid}`, (tabData: { [key: number]: any }) => {
if (tabData) {
delete tabData[tabId];
}
return Promise.resolve(tabData);
});
});
return undefined;
}); });
}); });
// 监听页面切换加载菜单 // 监听页面切换加载菜单

View File

@ -21,6 +21,7 @@ import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match";
import { ExtensionContentMessageSend } from "@Packages/message/extension_message"; import { ExtensionContentMessageSend } from "@Packages/message/extension_message";
import { sendMessage } from "@Packages/message/client"; import { sendMessage } from "@Packages/message/client";
import { compileInjectScript } from "../content/utils"; import { compileInjectScript } from "../content/utils";
import { PopupService } from "./popup";
// 为了优化性能存储到缓存时删除了code与value // 为了优化性能存储到缓存时删除了code与value
export interface ScriptMatchInfo extends ScriptRunResouce { export interface ScriptMatchInfo extends ScriptRunResouce {

View File

@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_scriptcat__", "name": "__MSG_scriptcat__",
"version": "0.17.0.1003", "version": "0.17.0.1002",
"author": "CodFrm", "author": "CodFrm",
"description": "__MSG_scriptcat_description__", "description": "__MSG_scriptcat_description__",
"options_ui": { "options_ui": {
@ -24,13 +24,16 @@
"permissions": [ "permissions": [
"tabs", "tabs",
"storage", "storage",
"cookies",
"offscreen", "offscreen",
"scripting", "scripting",
"downloads",
"activeTab", "activeTab",
"webRequest", "webRequest",
"userScripts", "userScripts",
"contextMenus", "contextMenus",
"notifications", "notifications",
"clipboardWrite",
"unlimitedStorage", "unlimitedStorage",
"declarativeNetRequest" "declarativeNetRequest"
], ],

3
src/types/main.d.ts vendored
View File

@ -30,7 +30,8 @@ declare namespace GMSend {
password?: string; password?: string;
nocache?: boolean; nocache?: boolean;
dataType?: "FormData" | "Blob"; dataType?: "FormData" | "Blob";
maxRedirects?: number; redirect?: "follow" | "error" | "manual";
maxRedirects?: number; // 为了与tm保持一致, 在v0.17.0后废弃, 使用redirect替代
} }
interface XHRFormData { interface XHRFormData {

View File

@ -139,21 +139,6 @@ declare function GM_cookie(
ondone: (cookie: GMTypes.Cookie[], error: unknown | undefined) => void ondone: (cookie: GMTypes.Cookie[], error: unknown | undefined) => void
): void; ): void;
/**
* GM_addValueChangeListener获取tabid
* tabid(,ValueChangeListener会返回tabid),storeid,.
* API,
* @param tabid tabid
* @param ondone
* @param callback.storeid storeid,GM_cookie使用
* @param callback.error
* @deprecated ,使GM_cookie("store", tabid)
*/
declare function GM_getCookieStore(
tabid: number,
ondone: (storeId: number | undefined, error: unknown | undefined) => void
): void;
/** /**
* *
* @deprecated ,beta版本中添加 * @deprecated ,beta版本中添加
@ -289,10 +274,7 @@ declare namespace CATType {
} }
declare namespace GMTypes { declare namespace GMTypes {
/* type CookieAction = "list" | "delete" | "set";
* store为获取隐身窗口之类的cookie,API,
*/
type CookieAction = "list" | "delete" | "set" | "store";
type LoggerLevel = "debug" | "info" | "warn" | "error"; type LoggerLevel = "debug" | "info" | "warn" | "error";
@ -308,17 +290,13 @@ declare namespace GMTypes {
path?: string; path?: string;
secure?: boolean; secure?: boolean;
session?: boolean; session?: boolean;
storeId?: string;
httpOnly?: boolean; httpOnly?: boolean;
expirationDate?: number; expirationDate?: number;
// store用
tabId?: number;
} }
interface Cookie { interface Cookie {
domain: string; domain: string;
name: string; name: string;
storeId: string;
value: string; value: string;
session: boolean; session: boolean;
hostOnly: boolean; hostOnly: boolean;
@ -341,7 +319,7 @@ declare namespace GMTypes {
active?: boolean; active?: boolean;
insert?: boolean; insert?: boolean;
setParent?: boolean; setParent?: boolean;
useOpen?: boolean; // 这是一个实验性/不兼容其他管理器/不兼容Firefox的功能 useOpen?: boolean; // 这是一个实验性/不兼容其他管理器/不兼容Firefox的功能 表示使用window.open打开新窗口 #178
} }
interface XHRResponse { interface XHRResponse {