diff --git a/example/gm_cookie.js b/example/gm_cookie.js index 926438a..01ee04a 100644 --- a/example/gm_cookie.js +++ b/example/gm_cookie.js @@ -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" diff --git a/example/gm_download.js b/example/gm_download.js index 0cfa78a..517253e 100644 --- a/example/gm_download.js +++ b/example/gm_download.js @@ -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/", diff --git a/example/gm_value/gm_value_2.js b/example/gm_value/gm_value_2.js index 4baa2a0..c54008a 100644 --- a/example/gm_value/gm_value_2.js +++ b/example/gm_value/gm_value_2.js @@ -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(() => { diff --git a/example/gm_value/gm_value_2_bg.js b/example/gm_value/gm_value_2_bg.js index fb6750b..cb7a6b2 100644 --- a/example/gm_value/gm_value_2_bg.js +++ b/example/gm_value/gm_value_2_bg.js @@ -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(() => { diff --git a/package.json b/package.json index 33bffe2..82139c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scriptcat", - "version": "0.17.0-alpha.2", + "version": "0.17.0-alpha.1", "description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!", "author": "CodFrm", "license": "GPLv3", diff --git a/packages/message/client.ts b/packages/message/client.ts index b1c5920..92e83a3 100644 --- a/packages/message/client.ts +++ b/packages/message/client.ts @@ -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; } } diff --git a/packages/message/server.ts b/packages/message/server.ts index 515b060..805628a 100644 --- a/packages/message/server.ts +++ b/packages/message/server.ts @@ -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); + } }); } diff --git a/src/app/cache.ts b/src/app/cache.ts index 4c74986..1cc0a4c 100644 --- a/src/app/cache.ts +++ b/src/app/cache.ts @@ -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 { @@ -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!; diff --git a/src/app/repo/dao.ts b/src/app/repo/dao.ts index eb1351c..45fdee8 100644 --- a/src/app/repo/dao.ts +++ b/src/app/repo/dao.ts @@ -75,9 +75,9 @@ export abstract class DAO { } const resp = await this.table.update(id, val); if (resp) { - return Promise.resolve(id); + return id; } - return Promise.reject(ErrSaveError); + throw ErrSaveError; } public findById(id: number) { diff --git a/src/app/service/content/content.ts b/src/app/service/content/content.ts index 0e97142..bc35274 100644 --- a/src/app/service/content/content.ts +++ b/src/app/service/content/content.ts @@ -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"); diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index d254b6b..d2d7be2 100644 --- a/src/app/service/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -215,7 +215,22 @@ export default class GMApi { @GMContext.API() public async CAT_fetchDocument(url: string): Promise { const data = await this.sendMessage("CAT_fetchDocument", [url]); - return (this.message).getAndDelRelatedTarget(data.relatedTarget); + return (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 { + 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(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 = 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) { diff --git a/src/app/service/offscreen/gm_api.ts b/src/app/service/offscreen/gm_api.ts index d54fc69..b73f2e8 100644 --- a/src/app/service/offscreen/gm_api.ts +++ b/src/app/service/offscreen/gm_api.ts @@ -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: 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, 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)); } } diff --git a/src/app/service/sandbox/runtime.ts b/src/app/service/sandbox/runtime.ts index cab680e..6d3e4a4 100644 --- a/src/app/service/sandbox/runtime.ts +++ b/src/app/service/sandbox/runtime.ts @@ -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) { diff --git a/src/app/service/service_worker/gm_api.ts b/src/app/service/service_worker/gm_api.ts index 7ee3d6f..6e87e9f 100644 --- a/src/app/service/service_worker/gm_api.ts +++ b/src/app/service/service_worker/gm_api.ts @@ -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 { 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 = 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 = 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 { + 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: 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 { + try { + await chrome.tabs.remove(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 = { @@ -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(id, options); } + @PermissionVerify.API() + async GM_download(request: Request, sender: GetSender) { + const params = 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}`); + } + }); } } diff --git a/src/app/service/service_worker/permission_verify.ts b/src/app/service/service_worker/permission_verify.ts index 5f97471..95cdd60 100644 --- a/src/app/service/service_worker/permission_verify.ts +++ b/src/app/service/service_worker/permission_verify.ts @@ -36,8 +36,6 @@ export interface ApiParam { background?: boolean; // 是否需要弹出页面让用户进行确认 confirm?: (request: Request) => Promise; - // 监听方法 - 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, diff --git a/src/app/service/service_worker/popup.ts b/src/app/service/service_worker/popup.ts index 3ff02ea..677ca1d 100644 --- a/src/app/service/service_worker/popup.ts +++ b/src/app/service/service_worker/popup.ts @@ -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; }); }); // 监听页面切换加载菜单 diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 8ef93e9..39b6a1e 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -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 { diff --git a/src/manifest.json b/src/manifest.json index 98722fa..00181fe 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -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" ], diff --git a/src/types/main.d.ts b/src/types/main.d.ts index 490c50a..f34d2ac 100644 --- a/src/types/main.d.ts +++ b/src/types/main.d.ts @@ -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 { diff --git a/src/types/scriptcat.d.ts b/src/types/scriptcat.d.ts index a9d139a..31030ea 100644 --- a/src/types/scriptcat.d.ts +++ b/src/types/scriptcat.d.ts @@ -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 {