权限确认

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`, inject: `${src}/inject.ts`,
popup: `${src}/pages/popup/main.tsx`, popup: `${src}/pages/popup/main.tsx`,
install: `${src}/pages/install/main.tsx`, install: `${src}/pages/install/main.tsx`,
confirm: `${src}/pages/confirm/main.tsx`,
options: `${src}/pages/options/main.tsx`, options: `${src}/pages/options/main.tsx`,
"editor.worker": "monaco-editor/esm/vs/editor/editor.worker.js", "editor.worker": "monaco-editor/esm/vs/editor/editor.worker.js",
"ts.worker": "monaco-editor/esm/vs/language/typescript/ts.worker.js", "ts.worker": "monaco-editor/esm/vs/language/typescript/ts.worker.js",
@ -151,6 +152,15 @@ export default defineConfig({
minify: true, minify: true,
chunks: ["install"], 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({ new rspack.HtmlRspackPlugin({
filename: `${dist}/ext/src/options.html`, filename: `${dist}/ext/src/options.html`,
template: `${src}/pages/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 { Script, ScriptDAO } from "@App/app/repo/scripts";
import { ExtMessageSender, GetSender, Group, MessageSend } from "@Packages/message/server"; import { ExtMessageSender, GetSender, Group, MessageSend } from "@Packages/message/server";
import { ValueService } from "@App/app/service/service_worker/value"; import { ValueService } from "@App/app/service/service_worker/value";
import PermissionVerify from "./permission_verify"; import PermissionVerify, { ConfirmParam } from "./permission_verify";
import { connect, sendMessage } from "@Packages/message/client"; import { connect, sendMessage } from "@Packages/message/client";
import Cache, { incr } from "@App/app/cache"; import Cache, { incr } from "@App/app/cache";
import EventEmitter from "eventemitter3"; import EventEmitter from "eventemitter3";
@ -13,6 +13,7 @@ import { getIcon, isFirefox } from "@App/pkg/utils/utils";
import { PopupService } from "./popup"; import { PopupService } from "./popup";
import { act } from "react"; import { act } from "react";
import { MockMessageConnect } from "@Packages/message/mock_message"; import { MockMessageConnect } from "@Packages/message/mock_message";
import i18next, { i18nName } from "@App/locales/locales";
// GMApi,处理脚本的GM API调用请求 // 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) { async GM_xmlhttpRequest(request: Request, sender: GetSender) {
if (request.params.length === 0) { if (request.params.length === 0) {
throw new Error("param is failed"); throw new Error("param is failed");

View File

@ -183,6 +183,7 @@ export default class PermissionVerify {
} }
return Promise.resolve(model); return Promise.resolve(model);
}); });
console.log("confirm", request, confirm);
// 有查询到结果,进入判断,不再需要用户确认 // 有查询到结果,进入判断,不再需要用户确认
if (ret) { if (ret) {
if (ret.allow) { if (ret.allow) {
@ -249,9 +250,27 @@ export default class PermissionVerify {
// 弹出窗口让用户进行确认 // 弹出窗口让用户进行确认
async confirmWindow(script: Script, confirm: ConfirmParam): Promise<UserConfirm> { async confirmWindow(script: Script, confirm: ConfirmParam): Promise<UserConfirm> {
return Promise.resolve({ return new Promise((resolve, reject) => {
allow: true, const uuid = uuidv4();
type: 1, // 超时处理
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 achUG from "./ach-UG/translation.json";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import "dayjs/locale/zh-tw"; import "dayjs/locale/zh-tw";
import { systemConfig } from "@App/pages/store/global";
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
fallbackLng: "zh-CN", fallbackLng: "zh-CN",
lng: localStorage.language || chrome.i18n.getUILanguage(), lng: chrome.i18n.getUILanguage(),
interpolation: { interpolation: {
escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape 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) => {
chrome.i18n.getAcceptLanguages((lngs) => { systemConfig.getLanguage().then((lng) => {
// 遍历数组寻找匹配语言 i18n.changeLanguage(lng);
for (let i = 0; i < lngs.length; i += 1) { dayjs.locale(lng.toLocaleLowerCase());
const lng = lngs[i];
if (i18n.hasResourceBundle(lng, "translation")) {
localStorage.language = lng;
i18n.changeLanguage(lng);
dayjs.locale(lng.toLocaleLowerCase());
break;
}
}
}); });
} else { });
dayjs.locale((localStorage.language as string).toLocaleLowerCase());
}
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
export function i18nName(script: { name: string; metadata: Metadata }) { 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 { useTranslation } from "react-i18next";
import FileSystemParams from "../FileSystemParams"; import FileSystemParams from "../FileSystemParams";
import { systemConfig } from "@App/pages/store/global"; 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; const CollapseItem = Collapse.Item;
@ -52,13 +53,13 @@ const GMApiSetting: React.FC = () => {
Message.error(`${t("account_validation_failed")}: ${e}`); Message.error(`${t("account_validation_failed")}: ${e}`);
return; return;
} }
const params = { ...systemConfig.catFileStorage.params }; const params = { ...fileSystemParams };
params[fileSystemType] = fileSystemParams; params[fileSystemType] = fileSystemParams;
systemConfig.catFileStorage = { systemConfig.setCatFileStorage({
status: "success", status: "success",
filesystem: fileSystemType, filesystem: fileSystemType,
params, params,
}; });
setStatus("success"); setStatus("success");
Message.success(t("save_success")!); Message.success(t("save_success")!);
}} }}
@ -68,10 +69,14 @@ const GMApiSetting: React.FC = () => {
<Button <Button
key="reset" key="reset"
onClick={() => { onClick={() => {
const config = systemConfig.catFileStorage; systemConfig.setCatFileStorage({
config.status = "unset"; status: "unset",
systemConfig.catFileStorage = config; filesystem: "webdav",
params: {},
});
setStatus("unset"); setStatus("unset");
setFilesystemParam({});
setFilesystemType("webdav");
}} }}
type="primary" type="primary"
status="danger" 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 { useTranslation } from "react-i18next";
import dayjs from "dayjs"; import dayjs from "dayjs";
import Logger from "@App/app/logger/logger"; import Logger from "@App/app/logger/logger";
import { systemConfig } from "@App/pages/store/global"; import FileSystemFactory, { FileSystemType } from "@Packages/filesystem/factory";
import { FileSystemType } from "@Packages/filesystem/factory";
import FileSystemParams from "@App/pages/components/FileSystemParams"; import FileSystemParams from "@App/pages/components/FileSystemParams";
import { systemConfig } from "@App/pages/store/global";
function Setting() { function Setting() {
const [syncDelete, setSyncDelete] = useState<boolean>(); const [syncDelete, setSyncDelete] = useState<boolean>();
@ -46,16 +46,25 @@ function Setting() {
useEffect(() => { useEffect(() => {
const loadConfigs = async () => { const loadConfigs = async () => {
const [cloudSync, menuExpandNum, checkCycle, updateDisabled, silenceUpdate, eslintConfig, enableEslint] = const [
await Promise.all([ cloudSync,
systemConfig.getCloudSync(), menuExpandNum,
systemConfig.getMenuExpandNum(), checkCycle,
systemConfig.getCheckScriptUpdateCycle(), updateDisabled,
systemConfig.getUpdateDisableScript(), silenceUpdate,
systemConfig.getSilenceUpdateScript(), eslintConfig,
systemConfig.getEslintConfig(), enableEslint,
systemConfig.getEnableEslint(), language,
]); ] = await Promise.all([
systemConfig.getCloudSync(),
systemConfig.getMenuExpandNum(),
systemConfig.getCheckScriptUpdateCycle(),
systemConfig.getUpdateDisableScript(),
systemConfig.getSilenceUpdateScript(),
systemConfig.getEslintConfig(),
systemConfig.getEnableEslint(),
systemConfig.getLanguage(),
]);
setSyncDelete(cloudSync.syncDelete); setSyncDelete(cloudSync.syncDelete);
setEnableCloudSync(cloudSync.enable); setEnableCloudSync(cloudSync.enable);
@ -67,6 +76,7 @@ function Setting() {
setSilenceUpdateScript(silenceUpdate); setSilenceUpdateScript(silenceUpdate);
setEslintConfig(eslintConfig); setEslintConfig(eslintConfig);
setEnableEslint(enableEslint); setEnableEslint(enableEslint);
setLanguage(language);
}; };
loadConfigs(); loadConfigs();
@ -96,9 +106,7 @@ function Setting() {
return; return;
} }
setLanguage(value); setLanguage(value);
i18n.changeLanguage(value); systemConfig.setLanguage(value);
dayjs.locale(value.toLocaleLowerCase());
localStorage.language = value;
Message.success(t("language_change_tip")!); 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 { defaultConfig } from "../../../packages/eslint/linter-config";
import { FileSystemType } from "@Packages/filesystem/factory"; import { FileSystemType } from "@Packages/filesystem/factory";
import { MessageQueue } from "@Packages/message/message_queue"; import { MessageQueue } from "@Packages/message/message_queue";
import i18n from "@App/locales/locales";
import dayjs from "dayjs";
export const SystamConfigChange = "systemConfigChange"; export const SystamConfigChange = "systemConfigChange";
@ -214,4 +216,26 @@ export class SystemConfig {
setMenuExpandNum(val: number) { setMenuExpandNum(val: number) {
this.set("menu_expand_num", val); 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 message = new ExtensionMessage(true);
const server = new Server("serviceWorker", message); 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(); manager.initManager();
// 初始化沙盒环境 // 初始化沙盒环境
await setupOffscreenDocument(); await setupOffscreenDocument();