基本实现完成GM API
This commit is contained in:
parent
5c0d4a2560
commit
d697928fb0
@ -1,5 +1,5 @@
|
||||
// ==UserScript==
|
||||
// @name New Userscript
|
||||
// @name GM cookie操作
|
||||
// @namespace https://bbs.tampermonkey.net.cn/
|
||||
// @version 0.1.0
|
||||
// @description 可以控制浏览器的cookie, 必须指定@connect, 并且每次一个新的域调用都需要用户确定
|
||||
@ -9,8 +9,6 @@
|
||||
// @connect example.com
|
||||
// ==/UserScript==
|
||||
|
||||
// GM_cookie("store") 方法请看storage_name/gm_value.js的例子, 可用于隐身窗口的操作
|
||||
|
||||
GM_cookie("set", {
|
||||
url: "http://example.com/cookie",
|
||||
name: "cookie1", value: "value"
|
||||
|
@ -9,7 +9,7 @@
|
||||
// ==/UserScript==
|
||||
|
||||
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",
|
||||
headers: {
|
||||
"referer": "http://www.example.com/",
|
||||
|
@ -13,13 +13,8 @@
|
||||
// @storageName example
|
||||
// ==/UserScript==
|
||||
|
||||
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote, tabid) {
|
||||
console.log("test_set change", name, oldval, newval, remote, tabid);
|
||||
// 可以通过tabid获取到触发变化的tab
|
||||
// GM_cookie.store可以获取到对应的cookie storeId
|
||||
GM_cookie("store", tabid, (storeId) => {
|
||||
console.log("store", storeId);
|
||||
});
|
||||
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote) {
|
||||
console.log("test_set change", name, oldval, newval, remote);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
|
@ -14,13 +14,8 @@
|
||||
// ==/UserScript==
|
||||
|
||||
return new Promise((resolve) => {
|
||||
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote, tabid) {
|
||||
console.log("value change", name, oldval, newval, remote, tabid);
|
||||
// 可以通过tabid获取到触发变化的tab
|
||||
// GM_cookie.store可以获取到对应的cookie storeId
|
||||
GM_cookie("store", tabid, (storeId) => {
|
||||
console.log("store", storeId);
|
||||
});
|
||||
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote) {
|
||||
console.log("value change", name, oldval, newval, remote);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scriptcat",
|
||||
"version": "0.17.0-alpha.2",
|
||||
"version": "0.17.0-alpha.1",
|
||||
"description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!",
|
||||
"author": "CodFrm",
|
||||
"license": "GPLv3",
|
||||
|
@ -6,9 +6,9 @@ export async function sendMessage(msg: MessageSend, action: string, data?: any):
|
||||
LoggerCore.getInstance().logger().trace("sendMessage", { action, data, response: res });
|
||||
if (res && res.code) {
|
||||
console.error(res);
|
||||
return Promise.reject(res.message);
|
||||
throw res.message;
|
||||
} else {
|
||||
return Promise.resolve(res.data);
|
||||
return res.data;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,20 +138,7 @@ export function forwardMessage(
|
||||
to: MessageSend,
|
||||
middleware?: ApiFunctionSync
|
||||
) {
|
||||
from.on(path, (params, fromCon) => {
|
||||
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 handler = (params: any, fromCon: GetSender) => {
|
||||
const fromConnect = fromCon.getConnect();
|
||||
if (fromConnect) {
|
||||
connect(to, prefix + "/" + path, params).then((toCon) => {
|
||||
@ -171,5 +158,22 @@ export function forwardMessage(
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ export default class Cache {
|
||||
ret = await set();
|
||||
this.set(key, ret);
|
||||
}
|
||||
return Promise.resolve(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public set(key: string, value: any): Promise<void> {
|
||||
@ -173,8 +173,9 @@ export default class Cache {
|
||||
if (value) {
|
||||
newValue = value;
|
||||
return this.set(key, value);
|
||||
} else if (value === undefined) {
|
||||
return this.del(key);
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
unlock();
|
||||
return newValue!;
|
||||
|
@ -75,9 +75,9 @@ export abstract class DAO<T> {
|
||||
}
|
||||
const resp = await this.table.update(id, <any>val);
|
||||
if (resp) {
|
||||
return Promise.resolve(id);
|
||||
return id;
|
||||
}
|
||||
return Promise.reject(ErrSaveError);
|
||||
throw ErrSaveError;
|
||||
}
|
||||
|
||||
public findById(id: number) {
|
||||
|
@ -35,7 +35,7 @@ export default class ContentRuntime {
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 60 * 1000);
|
||||
return Promise.resolve(url);
|
||||
return url;
|
||||
}
|
||||
case "CAT_fetchBlob": {
|
||||
return fetch(data.params[0]).then((res) => res.blob());
|
||||
@ -80,7 +80,7 @@ export default class ContentRuntime {
|
||||
return nodeId;
|
||||
}
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
return false;
|
||||
}
|
||||
);
|
||||
const client = new Client(this.msg, "inject");
|
||||
|
@ -215,7 +215,22 @@ export default class GMApi {
|
||||
@GMContext.API()
|
||||
public async CAT_fetchDocument(url: string): Promise<Document | undefined> {
|
||||
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()
|
||||
@ -479,7 +494,7 @@ export default class GMApi {
|
||||
break;
|
||||
default:
|
||||
LoggerCore.logger().warn("GM_xmlhttpRequest resp is error", {
|
||||
action: data.action,
|
||||
data,
|
||||
});
|
||||
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({
|
||||
depend: ["GM_closeNotification", "GM_updateNotification"],
|
||||
})
|
||||
@ -576,6 +649,88 @@ export default class GMApi {
|
||||
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()
|
||||
GM_getResourceURL(name: string, isBlobUrl?: boolean): string | undefined {
|
||||
if (!this.scriptRes.resource) {
|
||||
|
@ -15,7 +15,6 @@ export default class GMApi {
|
||||
data?: any
|
||||
) {
|
||||
const finalUrl = xhr.responseURL || details.url;
|
||||
// 判断是否有headerFlag-final-url,有则替换finalUrl
|
||||
let response: GMTypes.XHRResponse = {
|
||||
finalUrl,
|
||||
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() {
|
||||
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("openInTab", this.openInTab.bind(this));
|
||||
this.group.on("setClipboard", this.setClipboard.bind(this));
|
||||
}
|
||||
}
|
||||
|
@ -206,7 +206,7 @@ export class Runtime {
|
||||
} else {
|
||||
this.cronJob.set(script.uuid, cronJobList);
|
||||
}
|
||||
return Promise.resolve(!flag);
|
||||
return !flag;
|
||||
}
|
||||
|
||||
crontabExec(script: ScriptRunResouce, oncePos: number) {
|
||||
|
@ -4,12 +4,15 @@ 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 from "./permission_verify";
|
||||
import { connect } from "@Packages/message/client";
|
||||
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 { PopupService } from "./popup";
|
||||
import { act } from "react";
|
||||
import { MockMessageConnect } from "@Packages/message/mock_message";
|
||||
|
||||
// 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 });
|
||||
const api = PermissionVerify.apis.get(data.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);
|
||||
try {
|
||||
await this.permissionVerify.verify(req, api);
|
||||
} catch (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);
|
||||
}
|
||||
@ -98,17 +101,103 @@ export default class GMApi {
|
||||
async parseRequest(data: MessageRequest): Promise<Request> {
|
||||
const script = await this.scriptDAO.get(data.uuid);
|
||||
if (!script) {
|
||||
return Promise.reject(new Error("script is not found"));
|
||||
throw new Error("script is not found");
|
||||
}
|
||||
const req: Request = <Request>data;
|
||||
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()
|
||||
async GM_setValue(request: Request, sender: GetSender) {
|
||||
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;
|
||||
await this.value.setValue(request.script.uuid, key, value, {
|
||||
@ -122,7 +211,7 @@ export default class GMApi {
|
||||
// 检查是否有unsafe header,有则生成dnr规则
|
||||
const headers = params.headers;
|
||||
if (!headers) {
|
||||
return Promise.resolve({});
|
||||
return {};
|
||||
}
|
||||
const requestHeaders = [
|
||||
{
|
||||
@ -151,6 +240,26 @@ export default class GMApi {
|
||||
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;
|
||||
@ -176,16 +285,83 @@ export default class GMApi {
|
||||
removeRuleIds: [ruleId],
|
||||
addRules: [rule],
|
||||
});
|
||||
return Promise.resolve(headers);
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: maxRedirects实现
|
||||
@PermissionVerify.API()
|
||||
async GM_xmlhttpRequest(request: Request, con: GetSender) {
|
||||
async GM_xmlhttpRequest(request: Request, sender: GetSender) {
|
||||
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;
|
||||
// 先处理unsafe hearder
|
||||
@ -197,26 +373,35 @@ export default class GMApi {
|
||||
}
|
||||
params.headers["X-Scriptcat-GM-XHR-Request-Id"] = requestId.toString();
|
||||
params.headers = await this.buildDNRRule(requestId, request.params[0]);
|
||||
let responseHeader = "";
|
||||
let resultParam = {
|
||||
requestId,
|
||||
responseHeader: "",
|
||||
};
|
||||
// 等待response
|
||||
this.gmXhrHeadersReceived.addListener(
|
||||
"headersReceived:" + requestId,
|
||||
(details: chrome.webRequest.WebResponseHeadersDetails) => {
|
||||
details.responseHeaders?.forEach((header) => {
|
||||
responseHeader += header.name + ": " + header.value + "\r\n";
|
||||
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
|
||||
if (responseHeader) {
|
||||
msg.data.responseHeaders = responseHeader;
|
||||
}
|
||||
con.getConnect().sendMessage(msg);
|
||||
msg.data.responseHeaders = resultParam.responseHeader || msg.data.responseHeaders;
|
||||
sender.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({})
|
||||
GM_notification(request: Request, sender: GetSender) {
|
||||
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 options: chrome.notifications.NotificationOptions<true> = {
|
||||
@ -288,7 +546,7 @@ export default class GMApi {
|
||||
})
|
||||
GM_closeNotification(request: Request) {
|
||||
if (request.params.length === 0) {
|
||||
return Promise.reject(new Error("param is failed"));
|
||||
throw new Error("param is failed");
|
||||
}
|
||||
const [notificationId] = request.params;
|
||||
Cache.getInstance().del(`GM_notification:${notificationId}`);
|
||||
@ -300,7 +558,7 @@ export default class GMApi {
|
||||
})
|
||||
GM_updateNotification(request: Request) {
|
||||
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 details: GMTypes.NotificationDetails = request.params[1];
|
||||
@ -315,6 +573,91 @@ export default class GMApi {
|
||||
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;
|
||||
@ -397,5 +740,25 @@ export default class GMApi {
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -36,8 +36,6 @@ export interface ApiParam {
|
||||
background?: boolean;
|
||||
// 是否需要弹出页面让用户进行确认
|
||||
confirm?: (request: Request) => Promise<boolean | ConfirmParam>;
|
||||
// 监听方法
|
||||
listener?: () => void;
|
||||
// 别名
|
||||
alias?: string[];
|
||||
// 关联
|
||||
@ -59,9 +57,6 @@ export default class PermissionVerify {
|
||||
public static API(param: ApiParam = {}) {
|
||||
return (target: any, propertyName: string, descriptor: PropertyDescriptor) => {
|
||||
const key = propertyName;
|
||||
if (param.listener) {
|
||||
param.listener();
|
||||
}
|
||||
PermissionVerify.apis.set(key, {
|
||||
api: descriptor.value,
|
||||
param,
|
||||
|
@ -183,8 +183,10 @@ export class PopupService {
|
||||
const scriptMenu = script.map((script) => {
|
||||
const run = runScript.find((item) => item.uuid === script.uuid);
|
||||
if (run) {
|
||||
// 如果脚本已经存在,则不添加,赋值状态
|
||||
// 如果脚本已经存在,则不添加,更新信息
|
||||
run.enable = script.status === SCRIPT_STATUS_ENABLE;
|
||||
run.customExclude = script.customizeExcludeMatches || run.customExclude;
|
||||
run.hasUserConfig = !!script.config;
|
||||
return run;
|
||||
}
|
||||
return this.scriptToMenu(script);
|
||||
@ -196,6 +198,7 @@ export class PopupService {
|
||||
scriptMenu.push(script);
|
||||
}
|
||||
});
|
||||
console.log("popup脚本菜单", runScript);
|
||||
// 后台脚本只显示开启或者运行中的脚本
|
||||
return { scriptList: scriptMenu, backScriptList: await this.getScriptMenu(-1) };
|
||||
}
|
||||
@ -335,9 +338,19 @@ export class PopupService {
|
||||
|
||||
// 监听tab开关
|
||||
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||
// 清理数据
|
||||
this.txUpdateScriptMenu(tabId, async () => {
|
||||
return [];
|
||||
// 清理数据tab关闭需要释放的数据
|
||||
this.txUpdateScriptMenu(tabId, async (script) => {
|
||||
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;
|
||||
});
|
||||
});
|
||||
// 监听页面切换加载菜单
|
||||
|
@ -21,6 +21,7 @@ import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match";
|
||||
import { ExtensionContentMessageSend } from "@Packages/message/extension_message";
|
||||
import { sendMessage } from "@Packages/message/client";
|
||||
import { compileInjectScript } from "../content/utils";
|
||||
import { PopupService } from "./popup";
|
||||
|
||||
// 为了优化性能,存储到缓存时删除了code与value
|
||||
export interface ScriptMatchInfo extends ScriptRunResouce {
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_scriptcat__",
|
||||
"version": "0.17.0.1003",
|
||||
"version": "0.17.0.1002",
|
||||
"author": "CodFrm",
|
||||
"description": "__MSG_scriptcat_description__",
|
||||
"options_ui": {
|
||||
@ -24,13 +24,16 @@
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"storage",
|
||||
"cookies",
|
||||
"offscreen",
|
||||
"scripting",
|
||||
"downloads",
|
||||
"activeTab",
|
||||
"webRequest",
|
||||
"userScripts",
|
||||
"contextMenus",
|
||||
"notifications",
|
||||
"clipboardWrite",
|
||||
"unlimitedStorage",
|
||||
"declarativeNetRequest"
|
||||
],
|
||||
|
3
src/types/main.d.ts
vendored
3
src/types/main.d.ts
vendored
@ -30,7 +30,8 @@ declare namespace GMSend {
|
||||
password?: string;
|
||||
nocache?: boolean;
|
||||
dataType?: "FormData" | "Blob";
|
||||
maxRedirects?: number;
|
||||
redirect?: "follow" | "error" | "manual";
|
||||
maxRedirects?: number; // 为了与tm保持一致, 在v0.17.0后废弃, 使用redirect替代
|
||||
}
|
||||
|
||||
interface XHRFormData {
|
||||
|
26
src/types/scriptcat.d.ts
vendored
26
src/types/scriptcat.d.ts
vendored
@ -139,21 +139,6 @@ declare function GM_cookie(
|
||||
ondone: (cookie: GMTypes.Cookie[], error: unknown | undefined) => 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版本中添加
|
||||
@ -289,10 +274,7 @@ declare namespace CATType {
|
||||
}
|
||||
|
||||
declare namespace GMTypes {
|
||||
/*
|
||||
* store为获取隐身窗口之类的cookie,这是一个实验性质的API,后续可能会改变
|
||||
*/
|
||||
type CookieAction = "list" | "delete" | "set" | "store";
|
||||
type CookieAction = "list" | "delete" | "set";
|
||||
|
||||
type LoggerLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
@ -308,17 +290,13 @@ declare namespace GMTypes {
|
||||
path?: string;
|
||||
secure?: boolean;
|
||||
session?: boolean;
|
||||
storeId?: string;
|
||||
httpOnly?: boolean;
|
||||
expirationDate?: number;
|
||||
// store用
|
||||
tabId?: number;
|
||||
}
|
||||
|
||||
interface Cookie {
|
||||
domain: string;
|
||||
name: string;
|
||||
storeId: string;
|
||||
value: string;
|
||||
session: boolean;
|
||||
hostOnly: boolean;
|
||||
@ -341,7 +319,7 @@ declare namespace GMTypes {
|
||||
active?: boolean;
|
||||
insert?: boolean;
|
||||
setParent?: boolean;
|
||||
useOpen?: boolean; // 这是一个实验性/不兼容其他管理器/不兼容Firefox的功能
|
||||
useOpen?: boolean; // 这是一个实验性/不兼容其他管理器/不兼容Firefox的功能 表示使用window.open打开新窗口 #178
|
||||
}
|
||||
|
||||
interface XHRResponse {
|
||||
|
Loading…
x
Reference in New Issue
Block a user