diff --git a/packages/filesystem/auth.ts b/packages/filesystem/auth.ts index 04598ba..a90cde8 100644 --- a/packages/filesystem/auth.ts +++ b/packages/filesystem/auth.ts @@ -1,5 +1,6 @@ import { ExtServer, ExtServerApi } from "@App/app/const"; import { WarpTokenError } from "./error"; +import { LocalStorageDAO } from "@App/app/repo/localStorage"; type NetDiskType = "baidu" | "onedrive"; @@ -8,11 +9,7 @@ export function GetNetDiskToken(netDiskType: NetDiskType): Promise<{ msg: string; data: { token: { access_token: string; refresh_token: string } }; }> { - return fetch(ExtServerApi + `auth/net-disk/token?netDiskType=${netDiskType}`) - .then((resp) => resp.json()) - .then((resp) => { - return resp.data; - }); + return fetch(ExtServerApi + `auth/net-disk/token?netDiskType=${netDiskType}`).then((resp) => resp.json()); } export function RefreshToken( @@ -32,27 +29,45 @@ export function RefreshToken( netDiskType, refreshToken, }), - }) - .then((resp) => resp.json()) - .then((resp) => { - return resp.data; - }); + }).then((resp) => resp.json()); } export function NetDisk(netDiskType: NetDiskType) { return new Promise((resolve) => { - const loginWindow = window.open(`${ExtServer}api/v1/auth/net-disk?netDiskType=${netDiskType}`); - const t = setInterval(() => { - try { - if (loginWindow!.closed) { + if (globalThis.window) { + const loginWindow = window.open(`${ExtServer}api/v1/auth/net-disk?netDiskType=${netDiskType}`); + const t = setInterval(() => { + try { + if (loginWindow!.closed) { + clearInterval(t); + resolve(); + } + } catch (e) { clearInterval(t); resolve(); } - } catch (e) { - clearInterval(t); - resolve(); - } - }, 1000); + }, 1000); + } else { + chrome.tabs + .create({ + url: `${ExtServer}api/v1/auth/net-disk?netDiskType=${netDiskType}`, + }) + .then(({ id: tabId }) => { + const t = setInterval(async () => { + try { + const tab = await chrome.tabs.get(tabId!); + console.log("query tab", tab); + if (!tab) { + clearInterval(t); + resolve(); + } + } catch (e) { + clearInterval(t); + resolve(); + } + }, 1000); + }); + } }); } @@ -64,8 +79,15 @@ export type Token = { export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) { let token: Token | undefined; + const localStorageDao = new LocalStorageDAO(); + const key = `netdisk:token:${netDiskType}`; try { - token = JSON.parse(localStorage[`netdisk:token:${netDiskType}`]); + token = await localStorageDao.get(key).then((resp) => { + if (resp) { + return resp.value; + } + return undefined; + }); } catch (e) { // ignore } @@ -83,7 +105,10 @@ export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) { createtime: Date.now(), }; invalid = false; - localStorage[`netdisk:token:${netDiskType}`] = JSON.stringify(token); + await localStorageDao.save({ + key, + value: token, + }); } // token过期或者失效 if (Date.now() >= token.createtime + 3600000 || invalid) { @@ -91,7 +116,7 @@ export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) { try { const resp = await RefreshToken(netDiskType, token.refreshToken); if (resp.code !== 0) { - localStorage.removeItem(`netdisk:token:${netDiskType}`); + await localStorageDao.delete(key); // 刷新失败,并且标记失效,尝试重新获取token if (invalid) { return AuthVerify(netDiskType); @@ -103,7 +128,11 @@ export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) { refreshToken: resp.data.token.refresh_token, createtime: Date.now(), }; - localStorage[`netdisk:token:${netDiskType}`] = JSON.stringify(token); + // 更新token + await localStorageDao.save({ + key, + value: token, + }); } catch (e) { // 报错返回原token return Promise.resolve(token.accessToken); diff --git a/packages/filesystem/baidu/baidu.ts b/packages/filesystem/baidu/baidu.ts index 0d25972..05f07df 100644 --- a/packages/filesystem/baidu/baidu.ts +++ b/packages/filesystem/baidu/baidu.ts @@ -55,12 +55,11 @@ export default class BaiduFileSystem implements FileSystem { }); } - // eslint-disable-next-line no-undef - request(url: string, config?: RequestInit) { + async request(url: string, config?: RequestInit) { config = config || {}; const headers = config.headers || new Headers(); // 处理请求匿名不发送cookie - chrome.declarativeNetRequest.updateDynamicRules({ + await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: [100], addRules: [ { diff --git a/packages/filesystem/baidu/rw.ts b/packages/filesystem/baidu/rw.ts index 88004e6..727f7d3 100644 --- a/packages/filesystem/baidu/rw.ts +++ b/packages/filesystem/baidu/rw.ts @@ -1,5 +1,3 @@ -/* eslint-disable max-classes-per-file */ -/* eslint-disable import/prefer-default-export */ import { calculateMd5 } from "@App/pkg/utils/utils"; import { MD5 } from "crypto-js"; import { File, FileReader, FileWriter } from "../filesystem"; diff --git a/packages/filesystem/error.ts b/packages/filesystem/error.ts index 5cf23ae..67b744d 100644 --- a/packages/filesystem/error.ts +++ b/packages/filesystem/error.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export, max-classes-per-file export class WarpTokenError { error: Error; diff --git a/src/app/repo/localStorage.ts b/src/app/repo/localStorage.ts new file mode 100644 index 0000000..c17978d --- /dev/null +++ b/src/app/repo/localStorage.ts @@ -0,0 +1,17 @@ +import { Repo } from "./repo"; + +export interface LocalStorageItem { + key: string; + value: any; +} + +// 由于service worker不能使用localStorage,这里新建一个类来实现localStorage的功能 +export class LocalStorageDAO extends Repo { + constructor() { + super("localStorage"); + } + + save(value: LocalStorageItem) { + return super._save(value.key, value); + } +} diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index d2d7be2..2ad70aa 100644 --- a/src/app/service/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -318,6 +318,52 @@ export default class GMApi { this.sendMessage("GM_unregisterMenuCommand", [id]); } + @GMContext.API() + CAT_userConfig() { + return this.sendMessage("CAT_userConfig", []); + } + + @GMContext.API({ + depend: ["CAT_fetchBlob", "CAT_createBlobUrl"], + }) + async CAT_fileStorage(action: "list" | "download" | "upload" | "delete" | "config", details: any) { + if (action === "config") { + this.sendMessage("CAT_fileStorage", ["config"]); + return; + } + const sendDetails: { [key: string]: string } = { + baseDir: details.baseDir || "", + path: details.path || "", + filename: details.filename, + file: details.file, + }; + if (action === "upload") { + const url = await this.CAT_createBlobUrl(details.data); + sendDetails.data = url; + } + this.sendMessage("CAT_fileStorage", [action, sendDetails]).then(async (resp: { action: string; data: any }) => { + switch (resp.action) { + case "onload": { + if (action === "download") { + // 读取blob + const blob = await this.CAT_fetchBlob(resp.data); + details.onload && details.onload(blob); + } else { + details.onload && details.onload(resp.data); + } + break; + } + case "error": { + if (typeof resp.data.code === "undefined") { + details.onerror && details.onerror({ code: -1, message: resp.data.message }); + return; + } + details.onerror && details.onerror(resp.data); + } + } + }); + } + // 用于脚本跨域请求,需要@connect domain指定允许的域名 @GMContext.API({ depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"], diff --git a/src/app/service/service_worker/gm_api.ts b/src/app/service/service_worker/gm_api.ts index 0bf953e..639e574 100644 --- a/src/app/service/service_worker/gm_api.ts +++ b/src/app/service/service_worker/gm_api.ts @@ -10,10 +10,13 @@ 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"; import i18next, { i18nName } from "@App/locales/locales"; +import { SystemConfig } from "@App/pkg/config/config"; +import FileSystemFactory from "@Packages/filesystem/factory"; +import FileSystem from "@Packages/filesystem/filesystem"; +import { isWarpTokenError } from "@Packages/filesystem/error"; +import { joinPath } from "@Packages/filesystem/utils"; // GMApi,处理脚本的GM API调用请求 @@ -71,6 +74,7 @@ export default class GMApi { scriptDAO: ScriptDAO = new ScriptDAO(); constructor( + private systemConfig: SystemConfig, private permissionVerify: PermissionVerify, private group: Group, private send: MessageSend, @@ -248,6 +252,119 @@ export default class GMApi { }); } + @PermissionVerify.API() + CAT_userConfig(request: Request) { + chrome.tabs.create({ + url: `/src/options.html#/?userConfig=${request.uuid}`, + active: true, + }); + } + + @PermissionVerify.API({ + confirm: (request: Request) => { + const [action, details] = request.params; + if (action === "config") { + return Promise.resolve(true); + } + const dir = details.baseDir ? details.baseDir : request.script.uuid; + const metadata: { [key: string]: string } = {}; + metadata[i18next.t("script_name")] = i18nName(request.script); + return Promise.resolve({ + permission: "file_storage", + permissionValue: dir, + title: i18next.t("script_operation_title"), + metadata, + describe: i18next.t("script_operation_description", { dir }), + wildcard: false, + permissionContent: i18next.t("script_permission_content"), + } as ConfirmParam); + }, + }) + async CAT_fileStorage(request: Request, sender: GetSender): Promise<{ action: string; data: any } | boolean> { + const [action, details] = request.params; + if (action === "config") { + chrome.tabs.create({ + url: `/src/options.html#/setting`, + active: true, + }); + return true; + } + const fsConfig = await this.systemConfig.getCatFileStorage(); + if (fsConfig.status === "unset") { + return { action: "error", data: { code: 1, error: "file storage is unset" } }; + } + if (fsConfig.status === "error") { + return { action: "error", data: { code: 2, error: "file storage is error" } }; + } + let fs: FileSystem; + const baseDir = `ScriptCat/app/${details.baseDir ? details.baseDir : request.script.uuid}`; + try { + fs = await FileSystemFactory.create(fsConfig.filesystem, fsConfig.params[fsConfig.filesystem]); + await FileSystemFactory.mkdirAll(fs, baseDir); + fs = await fs.openDir(baseDir); + } catch (e: any) { + if (isWarpTokenError(e)) { + fsConfig.status = "error"; + this.systemConfig.setCatFileStorage(fsConfig); + return { action: "error", data: { code: 2, error: e.error.message } }; + } + return { action: "error", data: { code: 8, error: e.message } }; + } + switch (action) { + case "list": + try { + const list = await fs.list(); + list.forEach((file) => { + (file).absPath = file.path; + file.path = joinPath(file.path.substring(file.path.indexOf(baseDir) + baseDir.length)); + }); + return { action: "onload", data: list }; + } catch (e: any) { + return { action: "error", data: { code: 3, error: e.message } }; + } + case "upload": + try { + const w = await fs.create(details.path); + await w.write(await (await fetch(details.data)).blob()); + return { action: "onload", data: true }; + } catch (e: any) { + return { action: "error", data: { code: 4, error: e.message } }; + } + case "download": + try { + const info = details.file; + fs = await fs.openDir(`${info.path}`); + const r = await fs.open({ + fsid: (info).fsid, + name: info.name, + path: info.absPath, + size: info.size, + digest: info.digest, + createtime: info.createtime, + updatetime: info.updatetime, + }); + const blob = await r.read("blob"); + const url = URL.createObjectURL(blob); + setTimeout(() => { + URL.revokeObjectURL(url); + }, 6000); + return { action: "onload", data: url }; + } catch (e: any) { + return { action: "error", data: { code: 5, error: e.message } }; + } + break; + case "delete": + try { + await fs.delete(`${details.path}`); + return { action: "onload", data: true }; + } catch (e: any) { + return { action: "error", data: { code: 6, error: e.message } }; + } + default: + throw new Error("action is not supported"); + } + } + // 根据header生成dnr规则 async buildDNRRule(reqeustId: number, params: GMSend.XHRDetails): Promise<{ [key: string]: string }> { // 检查是否有unsafe header,有则生成dnr规则 @@ -401,7 +518,6 @@ export default class GMApi { @PermissionVerify.API({ confirm: async (request: Request) => { - console.log("confirm", request); const config = request.params[0]; const url = new URL(config.url); if (request.script.metadata.connect) { diff --git a/src/app/service/service_worker/index.ts b/src/app/service/service_worker/index.ts index 587035a..e69b602 100644 --- a/src/app/service/service_worker/index.ts +++ b/src/app/service/service_worker/index.ts @@ -6,6 +6,7 @@ import { ValueService } from "./value"; import { RuntimeService } from "./runtime"; import { ServiceWorkerMessageSend } from "@Packages/message/window_message"; import { PopupService } from "./popup"; +import { SystemConfig } from "@App/pkg/config/config"; export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode"; @@ -24,12 +25,14 @@ export default class ServiceWorkerManager { this.mq.emit("preparationOffscreen", {}); }); + const systemConfig = new SystemConfig(this.mq); + const resource = new ResourceService(this.api.group("resource"), this.mq); resource.init(); const value = new ValueService(this.api.group("value"), this.sender); const script = new ScriptService(this.api.group("script"), this.mq, value, resource); script.init(); - const runtime = new RuntimeService(this.api.group("runtime"), this.sender, this.mq, value, script); + const runtime = new RuntimeService(systemConfig, this.api.group("runtime"), this.sender, this.mq, value, script); runtime.init(); const popup = new PopupService(this.api.group("popup"), this.mq, runtime); popup.init(); diff --git a/src/app/service/service_worker/permission_verify.ts b/src/app/service/service_worker/permission_verify.ts index 719f105..1898624 100644 --- a/src/app/service/service_worker/permission_verify.ts +++ b/src/app/service/service_worker/permission_verify.ts @@ -181,7 +181,6 @@ export default class PermissionVerify { } return Promise.resolve(model); }); - console.log("confirm", request, confirm); // 有查询到结果,进入判断,不再需要用户确认 if (ret) { if (ret.allow) { diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index bccd9ff..1401a13 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -25,6 +25,7 @@ import { PopupService } from "./popup"; import Logger from "@App/app/logger/logger"; import LoggerCore from "@App/app/logger/core"; import PermissionVerify from "./permission_verify"; +import { SystemConfig } from "@App/pkg/config/config"; // 为了优化性能,存储到缓存时删除了code与value export interface ScriptMatchInfo extends ScriptRunResouce { @@ -48,6 +49,7 @@ export class RuntimeService { scriptMatchCache: Map | null | undefined; constructor( + private systemConfig: SystemConfig, private group: Group, private sender: MessageSend, private mq: MessageQueue, @@ -58,7 +60,7 @@ export class RuntimeService { async init() { // 启动gm api const permission = new PermissionVerify(this.group.group("permission")); - const gmApi = new GMApi(permission, this.group, this.sender, this.mq, this.value, this); + const gmApi = new GMApi(this.systemConfig, permission, this.group, this.sender, this.mq, this.value, this); permission.init(); gmApi.start(); diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index 0c4ce23..9ac4f47 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -1,5 +1,6 @@ import { Metadata, Script } from "@App/app/repo/scripts"; import { CronTime } from "cron"; +import crypto from "crypto-js"; import dayjs from "dayjs"; import semver from "semver"; @@ -234,3 +235,18 @@ export function getIcon(script: Script): string | undefined { (script.metadata.icon64url && script.metadata.icon64url[0]) ); } + +export function calculateMd5(blob: Blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsArrayBuffer(blob); + reader.onloadend = () => { + if (!reader.result) { + reject(new Error("result is null")); + } else { + const wordArray = crypto.lib.WordArray.create(reader.result); + resolve(crypto.MD5(wordArray).toString()); + } + }; + }); +}