安全与权限: - 站点访问权限改为 optional_host_permissions,保存配置时逐域名授权;移除 host_permissions 与 tabs,安装提示不再出现全站数据访问与浏览记录 - 主口令不再写入 storage.session,改用 IndexedDB 中不可导出的 CryptoKey 句柄恢复解锁,锁定即丢弃 - content script 注入范围只注册已授权域名,并随权限变化即时收敛 自动登录: - 支持 iframe 内的登录表单:脚本注入所有帧,逐帧按自身地址匹配配置 - 手动填充改为逐帧探测,定向发送到真正含密码框的帧 - 限流修正:只在真正触发提交后计数;判定登录成功后立即清零;达到上限时页面给出可见提示;重置判断只看启用中的配置 - SPA 重试改为 DOM 变更门控 + 退避,并单独监听已发现的 Shadow Root 工程化与文档: - 引入 ESLint(扁平配置)与 Prettier,CI 增加 lint 与 format:check - 信息类日志改为 debugLog(默认静默,chrome.storage.local.debugLog 开关) - 测试 123 → 128 用例(新增权限、iframe、限流相关用例) - README / PRIVACY / CHANGELOG 同步
168 lines
5.2 KiB
TypeScript
168 lines
5.2 KiB
TypeScript
/**
|
|
* 添加配置表单:表单字段、草稿持久化(弹窗关闭后不丢失)、
|
|
* 添加按钮提交、"填入当前域名"按钮。
|
|
*/
|
|
import { t } from "../i18n.js";
|
|
import { resolveAlias } from "../ui-utils.js";
|
|
import type { DomainConfig } from "../types.js";
|
|
import { $ } from "./dom";
|
|
import type { PopupCtx } from "./context";
|
|
|
|
const domainInput = $("domain");
|
|
const aliasInput = $("alias");
|
|
const usernameInput = $("username");
|
|
const passwordInput = $("password");
|
|
const autoSubmitSelect = $("autoSubmit");
|
|
const enabledCheckbox = $("enabled");
|
|
const delayMsInput = $("delayMs");
|
|
const usernameSelectorInput = $("usernameSelector");
|
|
const passwordSelectorInput = $("passwordSelector");
|
|
const submitSelectorInput = $("submitSelector");
|
|
const addBtn = $("addBtn");
|
|
const fillDomainBtn = $("fillDomain");
|
|
|
|
const DRAFT_KEY = "formDraft";
|
|
|
|
const draftFields = [
|
|
domainInput,
|
|
aliasInput,
|
|
usernameInput,
|
|
passwordInput,
|
|
autoSubmitSelect,
|
|
usernameSelectorInput,
|
|
passwordSelectorInput,
|
|
submitSelectorInput,
|
|
enabledCheckbox,
|
|
delayMsInput,
|
|
];
|
|
|
|
export function initConfigForm(ctx: PopupCtx) {
|
|
function saveDraft() {
|
|
const draft = {
|
|
domain: domainInput.value,
|
|
alias: aliasInput.value,
|
|
username: usernameInput.value,
|
|
password: passwordInput.value,
|
|
autoSubmit: autoSubmitSelect.value,
|
|
usernameSelector: usernameSelectorInput.value,
|
|
passwordSelector: passwordSelectorInput.value,
|
|
submitSelector: submitSelectorInput.value,
|
|
enabled: enabledCheckbox.checked,
|
|
delayMs: delayMsInput.value,
|
|
};
|
|
chrome.storage.local.set({ [DRAFT_KEY]: draft });
|
|
}
|
|
|
|
async function loadDraft() {
|
|
const data = await chrome.storage.local.get(DRAFT_KEY);
|
|
const draft = data[DRAFT_KEY];
|
|
if (!draft) return;
|
|
domainInput.value = draft.domain || "";
|
|
aliasInput.value = draft.alias || "";
|
|
usernameInput.value = draft.username || "";
|
|
passwordInput.value = draft.password || "";
|
|
autoSubmitSelect.value = draft.autoSubmit || "true";
|
|
usernameSelectorInput.value = draft.usernameSelector || "";
|
|
passwordSelectorInput.value = draft.passwordSelector || "";
|
|
submitSelectorInput.value = draft.submitSelector || "";
|
|
enabledCheckbox.checked = draft.enabled !== false;
|
|
delayMsInput.value = draft.delayMs || "";
|
|
addBtn.textContent = t("addConfigBtn");
|
|
}
|
|
|
|
function clearDraft() {
|
|
chrome.storage.local.remove(DRAFT_KEY);
|
|
}
|
|
|
|
function resetForm() {
|
|
domainInput.value = "";
|
|
aliasInput.value = "";
|
|
usernameInput.value = "";
|
|
passwordInput.value = "";
|
|
autoSubmitSelect.value = "true";
|
|
usernameSelectorInput.value = "";
|
|
passwordSelectorInput.value = "";
|
|
submitSelectorInput.value = "";
|
|
enabledCheckbox.checked = true;
|
|
delayMsInput.value = "";
|
|
addBtn.textContent = t("addConfigBtn");
|
|
clearDraft();
|
|
}
|
|
|
|
// 表单内容实时存入草稿,弹窗关闭后不丢失
|
|
draftFields.forEach((el) => {
|
|
el.addEventListener("input", saveDraft);
|
|
el.addEventListener("change", saveDraft);
|
|
});
|
|
|
|
addBtn.addEventListener("click", async () => {
|
|
const domain = domainInput.value.trim();
|
|
const username = usernameInput.value.trim();
|
|
const password = passwordInput.value;
|
|
|
|
if (!domain || !username || !password) {
|
|
ctx.showToast(t("toastRequired"));
|
|
return;
|
|
}
|
|
|
|
// 申请站点访问权限必须紧跟用户手势,因此放在其它 await 之前
|
|
const authorized = await Perms.requestForDomain(domain);
|
|
|
|
const config: DomainConfig = {
|
|
id: EnvStore.genCfgId(),
|
|
domain,
|
|
alias: aliasInput.value.trim() || (await resolveAlias(domain)) || null,
|
|
username,
|
|
password,
|
|
autoSubmit: autoSubmitSelect.value === "true",
|
|
usernameSelector: usernameSelectorInput.value.trim() || null,
|
|
passwordSelector: passwordSelectorInput.value.trim() || null,
|
|
submitSelector: submitSelectorInput.value.trim() || null,
|
|
enabled: enabledCheckbox.checked,
|
|
delayMs: delayMsInput.value ? Number(delayMsInput.value) : null,
|
|
};
|
|
|
|
const configs = await EnvStore.loadConfigs();
|
|
|
|
configs.push(config);
|
|
ctx.showToast(authorized ? t("toastAdded") : t("toastUnauthorized"));
|
|
|
|
await EnvStore.saveConfigs(configs);
|
|
resetForm();
|
|
ctx.renderConfigList();
|
|
ctx.renderEnvSelect();
|
|
ctx.refreshEnvManage();
|
|
});
|
|
|
|
// 填入当前标签页域名
|
|
fillDomainBtn.addEventListener("click", async () => {
|
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
if (!tab) {
|
|
ctx.showToast(t("toastNoTab"));
|
|
return;
|
|
}
|
|
if (!tab.url) {
|
|
// 未授权时读不到地址:提示用户点扩展图标重新授予 activeTab
|
|
ctx.showToast(t("toastNoTabUrl"));
|
|
return;
|
|
}
|
|
try {
|
|
const u = new URL(tab.url);
|
|
const host = u.host;
|
|
if (!host) {
|
|
ctx.showToast(t("toastNoDomain"));
|
|
return;
|
|
}
|
|
const value = u.pathname && u.pathname !== "/" ? host + u.pathname : host;
|
|
domainInput.value = value;
|
|
if (tab.title) aliasInput.value = tab.title;
|
|
saveDraft();
|
|
ctx.showToast(t("toastDomainFilled", value));
|
|
} catch {
|
|
ctx.showToast(t("toastUrlParseFail"));
|
|
}
|
|
});
|
|
|
|
return { resetForm, saveDraft, loadDraft };
|
|
}
|