storage.sync rejects any item over 8192 bytes, and the options page writes every field in one set(). An oversized template therefore made every later save fail, including changes to unrelated settings, with only a brief "Could not save settings". Cap the template at 1,000 UTF-16 units: maxlength on the input, and mergeSettings falls back to the default for a longer stored value, as it does for other out-of-range settings. Even a template of pasted control characters (six bytes each once JSON-escaped) stays under the quota; a test pins that arithmetic and another ties the HTML maxlength to the constant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
62 lines
2.5 KiB
JavaScript
62 lines
2.5 KiB
JavaScript
import { DEFAULT_SETTINGS, MAX_TEMPLATE_CHARS, OPEN_IN_VALUES } from "./defaults.js";
|
|
|
|
export function normalizeLanguage(value) {
|
|
if (typeof value !== "string") return "";
|
|
const trimmed = value.trim();
|
|
return [...trimmed].length <= 100 && !/[\r\n\x00-\x1f]/u.test(trimmed) ? trimmed : "";
|
|
}
|
|
|
|
// Pure. Layers stored values over the defaults, dropping unknown keys and
|
|
// falling back on wrong types or out-of-range values. A fresh or signed-out
|
|
// profile returns {} from storage, which yields the defaults.
|
|
export function mergeSettings(stored) {
|
|
const merged = { ...DEFAULT_SETTINGS };
|
|
if (stored === null || typeof stored !== "object") {
|
|
return merged;
|
|
}
|
|
merged.contextMenuMode = ["off", "simple", "advanced"].includes(stored.contextMenuMode)
|
|
? stored.contextMenuMode : stored.showContextMenu === false ? "off" : "simple";
|
|
if (typeof stored.showTranslate === "boolean") merged.showTranslate = stored.showTranslate;
|
|
if (["ask", "preferred"].includes(stored.translationMode)) merged.translationMode = stored.translationMode;
|
|
merged.translationLanguage = normalizeLanguage(stored.translationLanguage);
|
|
if (typeof stored.temporaryChat === "boolean") {
|
|
merged.temporaryChat = stored.temporaryChat;
|
|
}
|
|
if (typeof stored.webSearch === "boolean") {
|
|
merged.webSearch = stored.webSearch;
|
|
}
|
|
if (OPEN_IN_VALUES.includes(stored.openIn)) {
|
|
merged.openIn = stored.openIn;
|
|
}
|
|
if (typeof stored.promptTemplate === "string" && stored.promptTemplate.length <= MAX_TEMPLATE_CHARS) {
|
|
merged.promptTemplate = stored.promptTemplate;
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
// storage.sync works with no Mozilla account (it behaves as local storage and
|
|
// syncs later if the user signs in), so no capability probe is needed. Any
|
|
// rejection falls back to defaults rather than blocking the user's action.
|
|
// Firefox sanitises all non-quota backend errors to a single generic string, so
|
|
// there is nothing useful to branch on.
|
|
export async function load() {
|
|
try {
|
|
const stored = await browser.storage.sync.get([...Object.keys(DEFAULT_SETTINGS), "showContextMenu"]);
|
|
return mergeSettings(stored);
|
|
} catch {
|
|
return mergeSettings(null);
|
|
}
|
|
}
|
|
|
|
export async function save(partial) {
|
|
const normalized = mergeSettings(partial);
|
|
const patch = {};
|
|
if (partial && typeof partial === "object") {
|
|
for (const key of Object.keys(DEFAULT_SETTINGS)) {
|
|
if (Object.hasOwn(partial, key)) patch[key] = normalized[key];
|
|
}
|
|
if (Object.hasOwn(partial, "showContextMenu")) patch.contextMenuMode = normalized.contextMenuMode;
|
|
}
|
|
await browser.storage.sync.set(patch);
|
|
}
|