权限确认

This commit is contained in:
王一之 2025-04-15 18:06:31 +08:00
parent 2a0286e47d
commit 44b6f11b19
10 changed files with 307 additions and 45 deletions

View File

@ -35,6 +35,7 @@ export default defineConfig({
inject: `${src}/inject.ts`,
popup: `${src}/pages/popup/main.tsx`,
install: `${src}/pages/install/main.tsx`,
confirm: `${src}/pages/confirm/main.tsx`,
options: `${src}/pages/options/main.tsx`,
"editor.worker": "monaco-editor/esm/vs/editor/editor.worker.js",
"ts.worker": "monaco-editor/esm/vs/language/typescript/ts.worker.js",
@ -151,6 +152,15 @@ export default defineConfig({
minify: true,
chunks: ["install"],
}),
,
new rspack.HtmlRspackPlugin({
filename: `${dist}/ext/src/confirm.html`,
template: `${src}/pages/template.html`,
inject: "head",
title: "Confirm - ScriptCat",
minify: true,
chunks: ["confirm"],
}),
new rspack.HtmlRspackPlugin({
filename: `${dist}/ext/src/options.html`,
template: `${src}/pages/options.html`,

View File

@ -3,7 +3,7 @@ import Logger from "@App/app/logger/logger";
import { Script, ScriptDAO } from "@App/app/repo/scripts";
import { ExtMessageSender, GetSender, Group, MessageSend } from "@Packages/message/server";
import { ValueService } from "@App/app/service/service_worker/value";
import PermissionVerify from "./permission_verify";
import PermissionVerify, { ConfirmParam } from "./permission_verify";
import { connect, sendMessage } from "@Packages/message/client";
import Cache, { incr } from "@App/app/cache";
import EventEmitter from "eventemitter3";
@ -13,6 +13,7 @@ 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";
// GMApi,处理脚本的GM API调用请求
@ -357,8 +358,35 @@ export default class GMApi {
});
}
// TODO: maxRedirects实现
@PermissionVerify.API()
@PermissionVerify.API({
confirm: async (request: Request) => {
console.log("confirm", request);
const config = <GMSend.XHRDetails>request.params[0];
const url = new URL(config.url);
if (request.script.metadata.connect) {
const { connect } = request.script.metadata;
for (let i = 0; i < connect.length; i += 1) {
if (url.hostname.endsWith(connect[i])) {
return Promise.resolve(true);
}
}
}
const metadata: { [key: string]: string } = {};
metadata[i18next.t("script_name")] = i18nName(request.script);
metadata[i18next.t("request_domain")] = url.hostname;
metadata[i18next.t("request_url")] = config.url;
return Promise.resolve({
permission: "cors",
permissionValue: url.hostname,
title: i18next.t("script_accessing_cross_origin_resource"),
metadata,
describe: i18next.t("confirm_operation_description"),
wildcard: true,
permissionContent: i18next.t("domain"),
} as ConfirmParam);
},
})
async GM_xmlhttpRequest(request: Request, sender: GetSender) {
if (request.params.length === 0) {
throw new Error("param is failed");

View File

@ -183,6 +183,7 @@ export default class PermissionVerify {
}
return Promise.resolve(model);
});
console.log("confirm", request, confirm);
// 有查询到结果,进入判断,不再需要用户确认
if (ret) {
if (ret.allow) {
@ -249,9 +250,27 @@ export default class PermissionVerify {
// 弹出窗口让用户进行确认
async confirmWindow(script: Script, confirm: ConfirmParam): Promise<UserConfirm> {
return Promise.resolve({
allow: true,
type: 1,
return new Promise((resolve, reject) => {
const uuid = uuidv4();
// 超时处理
const timeout = setTimeout(() => {
this.confirmMap.delete(uuid);
reject(new Error("permission confirm timeout"));
}, 40 * 1000);
// 保存到map中
this.confirmMap.set(uuid, {
confirm,
script,
resolve: (value: UserConfirm) => {
clearTimeout(timeout);
resolve(value);
},
reject,
});
// 打开窗口
chrome.tabs.create({
url: chrome.runtime.getURL(`src/confirm.html?uuid=${uuid}`),
});
});
}
}

View File

@ -10,10 +10,11 @@ import zhTW from "./zh-TW/translation.json";
import achUG from "./ach-UG/translation.json";
import "dayjs/locale/zh-cn";
import "dayjs/locale/zh-tw";
import { systemConfig } from "@App/pages/store/global";
i18n.use(initReactI18next).init({
fallbackLng: "zh-CN",
lng: localStorage.language || chrome.i18n.getUILanguage(),
lng: chrome.i18n.getUILanguage(),
interpolation: {
escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
},
@ -26,22 +27,13 @@ i18n.use(initReactI18next).init({
},
});
if (!localStorage.language) {
chrome.i18n.getAcceptLanguages((lngs) => {
// 遍历数组寻找匹配语言
for (let i = 0; i < lngs.length; i += 1) {
const lng = lngs[i];
if (i18n.hasResourceBundle(lng, "translation")) {
localStorage.language = lng;
chrome.i18n.getAcceptLanguages((lngs) => {
systemConfig.getLanguage().then((lng) => {
i18n.changeLanguage(lng);
dayjs.locale(lng.toLocaleLowerCase());
break;
}
}
});
} else {
dayjs.locale((localStorage.language as string).toLocaleLowerCase());
}
});
dayjs.extend(relativeTime);
export function i18nName(script: { name: string; metadata: Metadata }) {

View File

@ -3,7 +3,8 @@ import { Button, Card, Collapse, Link, Message, Space, Typography } from "@arco-
import { useTranslation } from "react-i18next";
import FileSystemParams from "../FileSystemParams";
import { systemConfig } from "@App/pages/store/global";
import { FileSystemType } from "@Packages/filesystem/factory";
import FileSystemFactory, { FileSystemType } from "@Packages/filesystem/factory";
import { set } from "node_modules/yaml/dist/schema/yaml-1.1/set";
const CollapseItem = Collapse.Item;
@ -52,13 +53,13 @@ const GMApiSetting: React.FC = () => {
Message.error(`${t("account_validation_failed")}: ${e}`);
return;
}
const params = { ...systemConfig.catFileStorage.params };
const params = { ...fileSystemParams };
params[fileSystemType] = fileSystemParams;
systemConfig.catFileStorage = {
systemConfig.setCatFileStorage({
status: "success",
filesystem: fileSystemType,
params,
};
});
setStatus("success");
Message.success(t("save_success")!);
}}
@ -68,10 +69,14 @@ const GMApiSetting: React.FC = () => {
<Button
key="reset"
onClick={() => {
const config = systemConfig.catFileStorage;
config.status = "unset";
systemConfig.catFileStorage = config;
systemConfig.setCatFileStorage({
status: "unset",
filesystem: "webdav",
params: {},
});
setStatus("unset");
setFilesystemParam({});
setFilesystemType("webdav");
}}
type="primary"
status="danger"

142
src/pages/confirm/App.tsx Normal file
View File

@ -0,0 +1,142 @@
import { ConfirmParam } from "@App/app/service/service_worker/permission_verify";
import { Button, Message, Space } from "@arco-design/web-react";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
function App() {
const uuid = window.location.search.split("=")[1];
const [confirm, setConfirm] = React.useState<ConfirmParam>();
const [likeNum, setLikeNum] = React.useState(0);
const [second, setSecond] = React.useState(30);
const { t } = useTranslation();
if (second === 0) {
window.close();
}
setTimeout(() => {
setSecond(second - 1);
}, 1000);
useEffect(() => {
window.addEventListener("beforeunload", () => {
permissionCtrl.sendConfirm(uuid, {
allow: false,
type: 0,
});
});
permissionCtrl
.getConfirm(uuid)
.then((data) => {
setConfirm(data.confirm);
setLikeNum(data.likeNum);
})
.catch((e: any) => {
Message.error(e.message || t("get_confirm_error"));
});
}, []);
const handleConfirm = (allow: boolean, type: number) => {
return async () => {
try {
await permissionCtrl.sendConfirm(uuid, {
allow,
type,
});
window.close();
} catch (e: any) {
Message.error(e.message || t("confirm_error"));
setTimeout(() => {
window.close();
}, 3000);
}
};
};
return (
<div className="h-full">
<Space direction="vertical">
<span className="text-2xl font-500">{confirm?.title}</span>
{confirm &&
confirm.metadata &&
Object.keys(confirm.metadata).map((key) => (
<span className="text-base" key={key}>
{key}: {confirm!.metadata![key]}
</span>
))}
<span className="text-xl font-500">{confirm?.describe}</span>
<div>
<Button type="primary" onClick={handleConfirm(false, 1)}>
{t("ignore")} ({second})
</Button>
</div>
<div>
<Space>
<Button onClick={handleConfirm(true, 1)} status="success">
{t("allow_once")}
</Button>
<Button onClick={handleConfirm(true, 3)} status="success">
{t("temporary_allow", {
permissionContent: confirm?.permissionContent,
})}
</Button>
{likeNum > 2 && (
<Button onClick={handleConfirm(true, 2)} status="success">
{t("temporary_allow_all", {
permissionContent: confirm?.permissionContent,
})}
</Button>
)}
<Button onClick={handleConfirm(true, 5)} status="success">
{t("permanent_allow", {
permissionContent: confirm?.permissionContent,
})}
</Button>
{likeNum > 2 && (
<Button onClick={handleConfirm(true, 4)} status="success">
{t("permanent_allow_all", {
permissionContent: confirm?.permissionContent,
})}
</Button>
)}
</Space>
</div>
<div>
<Space>
<Button onClick={handleConfirm(false, 1)} status="danger">
{t("deny_once")}
</Button>
<Button onClick={handleConfirm(false, 3)} status="danger">
{t("temporary_deny", {
permissionContent: confirm?.permissionContent,
})}
</Button>
{likeNum > 2 && (
<Button onClick={handleConfirm(false, 2)} status="danger">
{t("temporary_deny_all", {
permissionContent: confirm?.permissionContent,
})}
</Button>
)}
<Button onClick={handleConfirm(false, 5)} status="danger">
{t("permanent_deny", {
permissionContent: confirm?.permissionContent,
})}
</Button>
{likeNum > 2 && (
<Button onClick={handleConfirm(false, 4)} status="danger">
{t("permanent_deny_all", {
permissionContent: confirm?.permissionContent,
})}
</Button>
)}
</Space>
</div>
</Space>
</div>
);
}
export default App;

View File

@ -0,0 +1,33 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import MainLayout from "../components/layout/MainLayout.tsx";
import "@arco-design/web-react/dist/css/arco.css";
import "@App/locales/locales";
import "@App/index.css";
import { Provider } from "react-redux";
import { store } from "@App/pages/store/store.ts";
import LoggerCore from "@App/app/logger/core.ts";
import migrate from "@App/app/migrate.ts";
import { LoggerDAO } from "@App/app/repo/logger.ts";
import DBWriter from "@App/app/logger/db_writer.ts";
// 初始化数据库
migrate();
// 初始化日志组件
const loggerCore = new LoggerCore({
writer: new DBWriter(new LoggerDAO()),
labels: { env: "install" },
});
loggerCore.logger().debug("page start");
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<Provider store={store}>
<MainLayout className="!flex-col !px-4 box-border">
<App />
</MainLayout>
</Provider>
</React.StrictMode>
);

View File

@ -10,9 +10,9 @@ import i18n from "@App/locales/locales";
import { useTranslation } from "react-i18next";
import dayjs from "dayjs";
import Logger from "@App/app/logger/logger";
import { systemConfig } from "@App/pages/store/global";
import { FileSystemType } from "@Packages/filesystem/factory";
import FileSystemFactory, { FileSystemType } from "@Packages/filesystem/factory";
import FileSystemParams from "@App/pages/components/FileSystemParams";
import { systemConfig } from "@App/pages/store/global";
function Setting() {
const [syncDelete, setSyncDelete] = useState<boolean>();
@ -46,8 +46,16 @@ function Setting() {
useEffect(() => {
const loadConfigs = async () => {
const [cloudSync, menuExpandNum, checkCycle, updateDisabled, silenceUpdate, eslintConfig, enableEslint] =
await Promise.all([
const [
cloudSync,
menuExpandNum,
checkCycle,
updateDisabled,
silenceUpdate,
eslintConfig,
enableEslint,
language,
] = await Promise.all([
systemConfig.getCloudSync(),
systemConfig.getMenuExpandNum(),
systemConfig.getCheckScriptUpdateCycle(),
@ -55,6 +63,7 @@ function Setting() {
systemConfig.getSilenceUpdateScript(),
systemConfig.getEslintConfig(),
systemConfig.getEnableEslint(),
systemConfig.getLanguage(),
]);
setSyncDelete(cloudSync.syncDelete);
@ -67,6 +76,7 @@ function Setting() {
setSilenceUpdateScript(silenceUpdate);
setEslintConfig(eslintConfig);
setEnableEslint(enableEslint);
setLanguage(language);
};
loadConfigs();
@ -96,9 +106,7 @@ function Setting() {
return;
}
setLanguage(value);
i18n.changeLanguage(value);
dayjs.locale(value.toLocaleLowerCase());
localStorage.language = value;
systemConfig.setLanguage(value);
Message.success(t("language_change_tip")!);
}}
>

View File

@ -3,6 +3,8 @@ import ChromeStorage from "./chrome_storage";
import { defaultConfig } from "../../../packages/eslint/linter-config";
import { FileSystemType } from "@Packages/filesystem/factory";
import { MessageQueue } from "@Packages/message/message_queue";
import i18n from "@App/locales/locales";
import dayjs from "dayjs";
export const SystamConfigChange = "systemConfigChange";
@ -214,4 +216,26 @@ export class SystemConfig {
setMenuExpandNum(val: number) {
this.set("menu_expand_num", val);
}
async getLanguage() {
const defaultLanguage = await new Promise<string>((resolve) => {
chrome.i18n.getAcceptLanguages((lngs) => {
// 遍历数组寻找匹配语言
for (let i = 0; i < lngs.length; i += 1) {
const lng = lngs[i];
if (i18n.hasResourceBundle(lng, "translation")) {
resolve(lng);
break;
}
}
});
});
return this.get("language", defaultLanguage || chrome.i18n.getUILanguage());
}
setLanguage(value: any) {
this.set("language", value);
i18n.changeLanguage(value);
dayjs.locale(value.toLocaleLowerCase());
}
}

View File

@ -58,7 +58,8 @@ async function main() {
// 初始化管理器
const message = new ExtensionMessage(true);
const server = new Server("serviceWorker", message);
const manager = new ServiceWorkerManager(server, new MessageQueue(), new ServiceWorkerMessageSend());
const messageQueue = new MessageQueue();
const manager = new ServiceWorkerManager(server, messageQueue, new ServiceWorkerMessageSend());
manager.initManager();
// 初始化沙盒环境
await setupOffscreenDocument();