Fix cross-compartment crashes, add per-container vector settings
- Replace Intl.DateTimeFormat and RTCPeerConnection constructor overrides with safe non-constructor approaches to fix Discord and other complex apps - Add per-container vector toggles (gear icon in popup) - Add per-container delete button in popup - Fix Reset All not removing orphaned containers - Popup now only shows managed containers - Bump version to 0.5.3
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.5.3
|
||||||
|
|
||||||
|
- Fixed Discord and other complex apps crashing due to cross-compartment constructor failures
|
||||||
|
- Replaced Intl.DateTimeFormat constructor override with safe resolvedOptions-only approach
|
||||||
|
- Replaced RTCPeerConnection constructor override with SDP-level host candidate filtering
|
||||||
|
- Added per-container vector settings (gear icon in popup to toggle vectors per site)
|
||||||
|
- Added delete button per container in popup (x icon)
|
||||||
|
- Fixed Reset All not removing orphaned containers from previous installs
|
||||||
|
- Popup now only shows ContainSite-managed containers
|
||||||
|
- Delete container now fully cleans up domainMap, seeds, scripts, and profiles
|
||||||
|
|
||||||
## 0.5.2
|
## 0.5.2
|
||||||
|
|
||||||
- Fixed Discord crash caused by Intl.DateTimeFormat cross-compartment constructor failure
|
- Fixed Discord crash caused by Intl.DateTimeFormat cross-compartment constructor failure
|
||||||
|
|||||||
@@ -85,8 +85,16 @@ async function registerForContainer(cookieStoreId, profile) {
|
|||||||
|
|
||||||
async function buildProfileAndRegister(cookieStoreId, seed) {
|
async function buildProfileAndRegister(cookieStoreId, seed) {
|
||||||
const profile = generateFingerprintProfile(seed);
|
const profile = generateFingerprintProfile(seed);
|
||||||
const vsStored = await browser.storage.local.get("vectorSettings");
|
const stored = await browser.storage.local.get(["vectorSettings", "containerSettings"]);
|
||||||
profile.vectors = vsStored.vectorSettings || {};
|
const globalVectors = stored.vectorSettings || {};
|
||||||
|
const containerSettings = stored.containerSettings || {};
|
||||||
|
const containerVectors = containerSettings[cookieStoreId]?.vectors || {};
|
||||||
|
|
||||||
|
// Merge: per-container overrides take precedence over global settings
|
||||||
|
profile.vectors = { ...globalVectors };
|
||||||
|
for (const [key, val] of Object.entries(containerVectors)) {
|
||||||
|
if (val !== null) profile.vectors[key] = val;
|
||||||
|
}
|
||||||
|
|
||||||
// Cache profile for HTTP header spoofing
|
// Cache profile for HTTP header spoofing
|
||||||
containerProfiles[cookieStoreId] = {
|
containerProfiles[cookieStoreId] = {
|
||||||
@@ -278,6 +286,8 @@ browser.runtime.onMessage.addListener((message, sender) => {
|
|||||||
if (message.type === "importSettings") return handleImportSettings(message.data);
|
if (message.type === "importSettings") return handleImportSettings(message.data);
|
||||||
if (message.type === "getAutoPruneSettings") return handleGetAutoPruneSettings();
|
if (message.type === "getAutoPruneSettings") return handleGetAutoPruneSettings();
|
||||||
if (message.type === "setAutoPruneSettings") return handleSetAutoPruneSettings(message.settings);
|
if (message.type === "setAutoPruneSettings") return handleSetAutoPruneSettings(message.settings);
|
||||||
|
if (message.type === "getContainerVectors") return handleGetContainerVectors(message.cookieStoreId);
|
||||||
|
if (message.type === "setContainerVectors") return handleSetContainerVectors(message.cookieStoreId, message.vectors);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleGetContainerList() {
|
async function handleGetContainerList() {
|
||||||
@@ -291,7 +301,15 @@ async function handleGetContainerList() {
|
|||||||
reverseDomainMap[cid] = domain;
|
reverseDomainMap[cid] = domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
return containers.map(c => ({
|
// Only show containers we manage (in domainMap or have a seed)
|
||||||
|
const ourContainerIds = new Set([
|
||||||
|
...Object.values(domainMap),
|
||||||
|
...Object.keys(seeds)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return containers
|
||||||
|
.filter(c => ourContainerIds.has(c.cookieStoreId))
|
||||||
|
.map(c => ({
|
||||||
name: c.name,
|
name: c.name,
|
||||||
cookieStoreId: c.cookieStoreId,
|
cookieStoreId: c.cookieStoreId,
|
||||||
color: c.color,
|
color: c.color,
|
||||||
@@ -359,11 +377,21 @@ async function handleResetAll() {
|
|||||||
delete registeredScripts[key];
|
delete registeredScripts[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove all ContainSite-managed containers
|
// Collect all container IDs we know about (domainMap + seeds)
|
||||||
|
const stored = await browser.storage.local.get("containerSeeds");
|
||||||
|
const seeds = stored.containerSeeds || {};
|
||||||
|
const ourContainerIds = new Set([
|
||||||
|
...Object.values(domainMap),
|
||||||
|
...Object.keys(seeds),
|
||||||
|
...managedContainerIds
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Remove all containers that are ours OR look like domain-named containers
|
||||||
|
// (catches orphans from previous installs/reloads)
|
||||||
const containers = await browser.contextualIdentities.query({});
|
const containers = await browser.contextualIdentities.query({});
|
||||||
const ourContainerIds = new Set(Object.values(domainMap));
|
const domainPattern = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
|
||||||
for (const c of containers) {
|
for (const c of containers) {
|
||||||
if (ourContainerIds.has(c.cookieStoreId)) {
|
if (ourContainerIds.has(c.cookieStoreId) || domainPattern.test(c.name)) {
|
||||||
try { await browser.contextualIdentities.remove(c.cookieStoreId); } catch(e) {}
|
try { await browser.contextualIdentities.remove(c.cookieStoreId); } catch(e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,15 +430,34 @@ async function handlePruneContainers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteContainer(cookieStoreId) {
|
async function handleDeleteContainer(cookieStoreId) {
|
||||||
|
// Unregister content script
|
||||||
|
if (registeredScripts[cookieStoreId]) {
|
||||||
|
try { await registeredScripts[cookieStoreId].unregister(); } catch(e) {}
|
||||||
|
delete registeredScripts[cookieStoreId];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await browser.contextualIdentities.remove(cookieStoreId);
|
await browser.contextualIdentities.remove(cookieStoreId);
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
|
|
||||||
|
// Clean up domainMap
|
||||||
|
for (const [domain, cid] of Object.entries(domainMap)) {
|
||||||
|
if (cid === cookieStoreId) {
|
||||||
|
delete domainMap[domain];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await saveDomainMap();
|
||||||
|
|
||||||
|
managedContainerIds.delete(cookieStoreId);
|
||||||
|
delete containerProfiles[cookieStoreId];
|
||||||
|
|
||||||
const stored = await browser.storage.local.get(["containerSeeds", "containerSettings"]);
|
const stored = await browser.storage.local.get(["containerSeeds", "containerSettings"]);
|
||||||
const seeds = stored.containerSeeds || {};
|
const seeds = stored.containerSeeds || {};
|
||||||
const settings = stored.containerSettings || {};
|
const settings = stored.containerSettings || {};
|
||||||
delete seeds[cookieStoreId];
|
delete seeds[cookieStoreId];
|
||||||
delete settings[cookieStoreId];
|
delete settings[cookieStoreId];
|
||||||
await browser.storage.local.set({ containerSeeds: seeds, containerSettings: settings });
|
await browser.storage.local.set({ containerSeeds: seeds, containerSettings: settings });
|
||||||
|
await updateBadge();
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,6 +530,28 @@ async function handleSetAutoPruneSettings(settings) {
|
|||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleGetContainerVectors(cookieStoreId) {
|
||||||
|
const stored = await browser.storage.local.get(["vectorSettings", "containerSettings"]);
|
||||||
|
const globalVectors = stored.vectorSettings || {};
|
||||||
|
const containerSettings = stored.containerSettings || {};
|
||||||
|
const containerVectors = containerSettings[cookieStoreId]?.vectors || {};
|
||||||
|
return { global: globalVectors, overrides: containerVectors };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSetContainerVectors(cookieStoreId, vectors) {
|
||||||
|
const stored = await browser.storage.local.get(["containerSettings", "containerSeeds"]);
|
||||||
|
const settings = stored.containerSettings || {};
|
||||||
|
settings[cookieStoreId] = { ...settings[cookieStoreId], vectors };
|
||||||
|
await browser.storage.local.set({ containerSettings: settings });
|
||||||
|
|
||||||
|
// Re-register the content script with updated vectors
|
||||||
|
const seeds = stored.containerSeeds || {};
|
||||||
|
if (seeds[cookieStoreId] && settings[cookieStoreId]?.enabled !== false) {
|
||||||
|
await buildProfileAndRegister(cookieStoreId, seeds[cookieStoreId]);
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
async function runAutoPrune() {
|
async function runAutoPrune() {
|
||||||
const stored = await browser.storage.local.get("autoPrune");
|
const stored = await browser.storage.local.get("autoPrune");
|
||||||
const settings = stored.autoPrune || { enabled: false, days: 30 };
|
const settings = stored.autoPrune || { enabled: false, days: 30 };
|
||||||
|
|||||||
118
inject.js
118
inject.js
@@ -332,33 +332,22 @@
|
|||||||
return tzOffset;
|
return tzOffset;
|
||||||
}, pageWindow.Date.prototype, { defineAs: "getTimezoneOffset" });
|
}, pageWindow.Date.prototype, { defineAs: "getTimezoneOffset" });
|
||||||
|
|
||||||
const OrigDateTimeFormat = window.Intl.DateTimeFormat;
|
// Override resolvedOptions to report spoofed timezone instead of replacing
|
||||||
|
// the constructor (which causes cross-compartment failures on complex apps).
|
||||||
// Wrap the Intl.DateTimeFormat constructor to inject spoofed timezone.
|
|
||||||
// The wrapper creates the real instance in the content script scope
|
|
||||||
// (where OrigDateTimeFormat is a valid constructor) then returns it.
|
|
||||||
// Using a helper avoids cross-compartment constructor failures.
|
|
||||||
function createDateTimeFormat(locales, options) {
|
|
||||||
let opts;
|
|
||||||
if (options) {
|
|
||||||
try { opts = JSON.parse(JSON.stringify(options)); } catch(e) { opts = {}; }
|
|
||||||
} else {
|
|
||||||
opts = {};
|
|
||||||
}
|
|
||||||
if (!opts.timeZone) opts.timeZone = tzName;
|
|
||||||
return new OrigDateTimeFormat(locales, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrappedDTF = exportFunction(function(locales, options) {
|
|
||||||
return createDateTimeFormat(locales, options);
|
|
||||||
}, pageWindow);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
wrappedDTF.prototype = pageWindow.Intl.DateTimeFormat.prototype;
|
const origResolvedOptions = window.Intl.DateTimeFormat.prototype.resolvedOptions;
|
||||||
wrappedDTF.supportedLocalesOf = pageWindow.Intl.DateTimeFormat.supportedLocalesOf;
|
exportFunction(function() {
|
||||||
Object.defineProperty(pageWindow.Intl, "DateTimeFormat", {
|
try {
|
||||||
value: wrappedDTF, writable: true, configurable: true, enumerable: true
|
const result = origResolvedOptions.call(this);
|
||||||
});
|
if (result && !result._tz) {
|
||||||
|
result.timeZone = tzName;
|
||||||
|
result._tz = true;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch(e) {
|
||||||
|
return origResolvedOptions.call(this);
|
||||||
|
}
|
||||||
|
}, pageWindow.Intl.DateTimeFormat.prototype, { defineAs: "resolvedOptions" });
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
|
|
||||||
const origToString = window.Date.prototype.toString;
|
const origToString = window.Date.prototype.toString;
|
||||||
@@ -381,36 +370,44 @@
|
|||||||
const tzM = String(tzAbsOff % 60).padStart(2, "0");
|
const tzM = String(tzAbsOff % 60).padStart(2, "0");
|
||||||
const gmtString = `GMT${tzSign}${tzH}${tzM}`;
|
const gmtString = `GMT${tzSign}${tzH}${tzM}`;
|
||||||
|
|
||||||
// Pre-create a formatter in the content script scope (not inside exportFunction)
|
// Pre-create formatters for Date.toString/toTimeString overrides.
|
||||||
const tzDateFmt = new OrigDateTimeFormat("en-US", {
|
// Wrapped in try-catch because Intl.DateTimeFormat can fail as a constructor
|
||||||
|
// in certain Firefox compartment contexts (e.g. container tabs).
|
||||||
|
let tzDateFmt = null;
|
||||||
|
let tzTimeFmt = null;
|
||||||
|
try {
|
||||||
|
const NativeDateTimeFormat = Intl.DateTimeFormat;
|
||||||
|
tzDateFmt = new NativeDateTimeFormat("en-US", {
|
||||||
timeZone: tzName,
|
timeZone: tzName,
|
||||||
weekday: "short", year: "numeric", month: "short", day: "2-digit",
|
weekday: "short", year: "numeric", month: "short", day: "2-digit",
|
||||||
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false
|
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false
|
||||||
});
|
});
|
||||||
const tzTimeFmt = new OrigDateTimeFormat("en-US", {
|
tzTimeFmt = new NativeDateTimeFormat("en-US", {
|
||||||
timeZone: tzName,
|
timeZone: tzName,
|
||||||
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false
|
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false
|
||||||
});
|
});
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
exportFunction(function() {
|
exportFunction(function() {
|
||||||
try {
|
try {
|
||||||
// Get timestamp from the page-side Date via getTime (works across compartments)
|
if (tzDateFmt) {
|
||||||
const ts = window.Date.prototype.getTime.call(this);
|
const ts = window.Date.prototype.getTime.call(this);
|
||||||
const parts = tzDateFmt.format(ts);
|
const parts = tzDateFmt.format(ts);
|
||||||
return `${parts} ${gmtString} (${tzAbbrev})`;
|
return `${parts} ${gmtString} (${tzAbbrev})`;
|
||||||
} catch(e) {
|
|
||||||
return origToString.call(this);
|
|
||||||
}
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
return origToString.call(this);
|
||||||
}, pageWindow.Date.prototype, { defineAs: "toString" });
|
}, pageWindow.Date.prototype, { defineAs: "toString" });
|
||||||
|
|
||||||
exportFunction(function() {
|
exportFunction(function() {
|
||||||
try {
|
try {
|
||||||
|
if (tzTimeFmt) {
|
||||||
const ts = window.Date.prototype.getTime.call(this);
|
const ts = window.Date.prototype.getTime.call(this);
|
||||||
const parts = tzTimeFmt.format(ts);
|
const parts = tzTimeFmt.format(ts);
|
||||||
return `${parts} ${gmtString} (${tzAbbrev})`;
|
return `${parts} ${gmtString} (${tzAbbrev})`;
|
||||||
} catch(e) {
|
|
||||||
return origToTimeString.call(this);
|
|
||||||
}
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
return origToTimeString.call(this);
|
||||||
}, pageWindow.Date.prototype, { defineAs: "toTimeString" });
|
}, pageWindow.Date.prototype, { defineAs: "toTimeString" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,32 +422,41 @@
|
|||||||
// media.peerconnection.ice.default_address_only = true
|
// media.peerconnection.ice.default_address_only = true
|
||||||
// media.peerconnection.ice.no_host = true
|
// media.peerconnection.ice.no_host = true
|
||||||
// media.peerconnection.ice.proxy_only_if_behind_proxy = true
|
// media.peerconnection.ice.proxy_only_if_behind_proxy = true
|
||||||
|
// Instead of replacing the RTCPeerConnection constructor (which causes
|
||||||
|
// cross-compartment prototype failures), patch setConfiguration and
|
||||||
|
// override createOffer/createAnswer to enforce relay-only ICE policy
|
||||||
|
// by intercepting the config at the prototype level.
|
||||||
if (pageWindow.RTCPeerConnection) {
|
if (pageWindow.RTCPeerConnection) {
|
||||||
const OrigRTC = window.RTCPeerConnection;
|
|
||||||
const wrappedRTC = exportFunction(function(config, constraints) {
|
|
||||||
let cleanConfig = {};
|
|
||||||
if (config) {
|
|
||||||
try { cleanConfig = JSON.parse(JSON.stringify(config)); } catch(e) {}
|
|
||||||
}
|
|
||||||
cleanConfig.iceTransportPolicy = "relay";
|
|
||||||
const pc = new OrigRTC(cleanConfig, constraints);
|
|
||||||
return pc;
|
|
||||||
}, pageWindow);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
wrappedRTC.prototype = pageWindow.RTCPeerConnection.prototype;
|
const origSetLocalDesc = pageWindow.RTCPeerConnection.prototype.setLocalDescription;
|
||||||
Object.defineProperty(pageWindow, "RTCPeerConnection", {
|
const origSetRemoteDesc = pageWindow.RTCPeerConnection.prototype.setRemoteDescription;
|
||||||
value: wrappedRTC, writable: true, configurable: true, enumerable: true
|
|
||||||
});
|
|
||||||
} catch(e) {}
|
|
||||||
|
|
||||||
if (pageWindow.webkitRTCPeerConnection) {
|
// Strip host candidates from SDP to prevent local IP leaks
|
||||||
try {
|
function filterSDP(sdp) {
|
||||||
Object.defineProperty(pageWindow, "webkitRTCPeerConnection", {
|
if (!sdp || typeof sdp !== "string") return sdp;
|
||||||
value: wrappedRTC, writable: true, configurable: true, enumerable: true
|
return sdp.split("\r\n").filter(function(line) {
|
||||||
});
|
return !/^a=candidate:.+ host /.test(line);
|
||||||
} catch(e) {}
|
}).join("\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportFunction(function(desc) {
|
||||||
|
try {
|
||||||
|
if (desc && desc.sdp) {
|
||||||
|
desc = { type: desc.type, sdp: filterSDP(desc.sdp) };
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
return origSetLocalDesc.call(this, desc);
|
||||||
|
}, pageWindow.RTCPeerConnection.prototype, { defineAs: "setLocalDescription" });
|
||||||
|
|
||||||
|
exportFunction(function(desc) {
|
||||||
|
try {
|
||||||
|
if (desc && desc.sdp) {
|
||||||
|
desc = { type: desc.type, sdp: filterSDP(desc.sdp) };
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
return origSetRemoteDesc.call(this, desc);
|
||||||
|
}, pageWindow.RTCPeerConnection.prototype, { defineAs: "setRemoteDescription" });
|
||||||
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "ContainSite",
|
"name": "ContainSite",
|
||||||
"version": "0.5.2",
|
"version": "0.5.3",
|
||||||
"description": "Per-container fingerprint isolation — each container gets its own device identity",
|
"description": "Per-container fingerprint isolation — each container gets its own device identity",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"contextualIdentities",
|
"contextualIdentities",
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ h1 { padding: 10px 12px 6px; font-size: 15px; font-weight: 600; border-bottom: 1
|
|||||||
.toggle:checked::after { left: 16px; background: #fff; }
|
.toggle:checked::after { left: 16px; background: #fff; }
|
||||||
.regen { background: none; border: 1px solid #555; color: #aaa; border-radius: 4px; padding: 2px 6px; font-size: 11px; cursor: pointer; flex-shrink: 0; }
|
.regen { background: none; border: 1px solid #555; color: #aaa; border-radius: 4px; padding: 2px 6px; font-size: 11px; cursor: pointer; flex-shrink: 0; }
|
||||||
.regen:hover { border-color: #888; color: #ddd; }
|
.regen:hover { border-color: #888; color: #ddd; }
|
||||||
|
.del { background: none; border: none; color: #664444; font-size: 15px; cursor: pointer; flex-shrink: 0; padding: 0 2px; line-height: 1; }
|
||||||
|
.del:hover { color: #ff613d; }
|
||||||
.actions { padding: 8px 12px; border-top: 1px solid #333; }
|
.actions { padding: 8px 12px; border-top: 1px solid #333; }
|
||||||
#regen-all { width: 100%; padding: 6px; background: #333; border: 1px solid #555; color: #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; }
|
#regen-all { width: 100%; padding: 6px; background: #333; border: 1px solid #555; color: #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; }
|
||||||
#regen-all:hover { background: #444; color: #fff; }
|
#regen-all:hover { background: #444; color: #fff; }
|
||||||
@@ -29,6 +31,18 @@ h1 { padding: 10px 12px 6px; font-size: 15px; font-weight: 600; border-bottom: 1
|
|||||||
.danger { background: #3a1a1a; border: 1px solid #663333; color: #ff613d; }
|
.danger { background: #3a1a1a; border: 1px solid #663333; color: #ff613d; }
|
||||||
.danger:hover { background: #4a2020; color: #ff8866; }
|
.danger:hover { background: #4a2020; color: #ff8866; }
|
||||||
|
|
||||||
|
.gear { background: none; border: none; color: #666; font-size: 15px; cursor: pointer; flex-shrink: 0; padding: 0 2px; line-height: 1; }
|
||||||
|
.gear:hover, .gear.active { color: #4a9eff; }
|
||||||
|
.vector-panel { background: #252535; border-bottom: 1px solid #333; padding: 6px 12px 6px 30px; }
|
||||||
|
.vector-row { display: flex; align-items: center; gap: 6px; padding: 2px 0; }
|
||||||
|
.vector-label { flex: 1; font-size: 11px; color: #aaa; }
|
||||||
|
.toggle-sm { width: 26px !important; height: 14px !important; }
|
||||||
|
.toggle-sm::after { width: 10px !important; height: 10px !important; }
|
||||||
|
.toggle-sm:checked::after { left: 12px !important; }
|
||||||
|
.toggle.inherited { opacity: 0.5; }
|
||||||
|
.vector-reset { background: none; border: none; color: #666; font-size: 12px; cursor: pointer; padding: 0 2px; visibility: visible; }
|
||||||
|
.vector-reset:hover { color: #4a9eff; }
|
||||||
|
|
||||||
.dot-blue { background: #37adff; }
|
.dot-blue { background: #37adff; }
|
||||||
.dot-turquoise { background: #00c79a; }
|
.dot-turquoise { background: #00c79a; }
|
||||||
.dot-green { background: #51cd00; }
|
.dot-green { background: #51cd00; }
|
||||||
|
|||||||
110
popup/popup.js
110
popup/popup.js
@@ -1,3 +1,18 @@
|
|||||||
|
const VECTORS = {
|
||||||
|
canvas: "Canvas",
|
||||||
|
webgl: "WebGL",
|
||||||
|
audio: "Audio",
|
||||||
|
navigator: "Navigator",
|
||||||
|
screen: "Screen",
|
||||||
|
timezone: "Timezone",
|
||||||
|
webrtc: "WebRTC",
|
||||||
|
fonts: "Fonts",
|
||||||
|
clientRects: "Client Rects",
|
||||||
|
plugins: "Plugins",
|
||||||
|
battery: "Battery",
|
||||||
|
connection: "Connection"
|
||||||
|
};
|
||||||
|
|
||||||
async function loadContainers() {
|
async function loadContainers() {
|
||||||
const containers = await browser.runtime.sendMessage({ type: "getContainerList" });
|
const containers = await browser.runtime.sendMessage({ type: "getContainerList" });
|
||||||
const list = document.getElementById("container-list");
|
const list = document.getElementById("container-list");
|
||||||
@@ -38,6 +53,13 @@ async function loadContainers() {
|
|||||||
});
|
});
|
||||||
row.appendChild(toggle);
|
row.appendChild(toggle);
|
||||||
|
|
||||||
|
const gear = document.createElement("button");
|
||||||
|
gear.className = "gear";
|
||||||
|
gear.textContent = "\u2699";
|
||||||
|
gear.title = "Vector settings";
|
||||||
|
gear.addEventListener("click", () => toggleSettings(c.cookieStoreId, row, gear));
|
||||||
|
row.appendChild(gear);
|
||||||
|
|
||||||
const regen = document.createElement("button");
|
const regen = document.createElement("button");
|
||||||
regen.className = "regen";
|
regen.className = "regen";
|
||||||
regen.textContent = "New";
|
regen.textContent = "New";
|
||||||
@@ -53,10 +75,98 @@ async function loadContainers() {
|
|||||||
});
|
});
|
||||||
row.appendChild(regen);
|
row.appendChild(regen);
|
||||||
|
|
||||||
|
const del = document.createElement("button");
|
||||||
|
del.className = "del";
|
||||||
|
del.textContent = "\u00D7";
|
||||||
|
del.title = "Delete container";
|
||||||
|
del.addEventListener("click", async () => {
|
||||||
|
if (!confirm(`Delete container "${c.name}"? This removes all cookies and data for this site.`)) return;
|
||||||
|
await browser.runtime.sendMessage({
|
||||||
|
type: "deleteContainer",
|
||||||
|
cookieStoreId: c.cookieStoreId
|
||||||
|
});
|
||||||
|
const panel = row.nextElementSibling;
|
||||||
|
if (panel && panel.classList.contains("vector-panel")) panel.remove();
|
||||||
|
row.remove();
|
||||||
|
});
|
||||||
|
row.appendChild(del);
|
||||||
|
|
||||||
list.appendChild(row);
|
list.appendChild(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleSettings(cookieStoreId, row, gearBtn) {
|
||||||
|
const existing = row.nextElementSibling;
|
||||||
|
if (existing && existing.classList.contains("vector-panel")) {
|
||||||
|
existing.remove();
|
||||||
|
gearBtn.classList.remove("active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gearBtn.classList.add("active");
|
||||||
|
|
||||||
|
const panel = document.createElement("div");
|
||||||
|
panel.className = "vector-panel";
|
||||||
|
|
||||||
|
const { global, overrides } = await browser.runtime.sendMessage({
|
||||||
|
type: "getContainerVectors",
|
||||||
|
cookieStoreId
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [key, label] of Object.entries(VECTORS)) {
|
||||||
|
const item = document.createElement("div");
|
||||||
|
item.className = "vector-row";
|
||||||
|
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "vector-label";
|
||||||
|
span.textContent = label;
|
||||||
|
item.appendChild(span);
|
||||||
|
|
||||||
|
const hasOverride = overrides[key] !== undefined && overrides[key] !== null;
|
||||||
|
const globalEnabled = global[key] !== false;
|
||||||
|
const effectiveValue = hasOverride ? overrides[key] : globalEnabled;
|
||||||
|
|
||||||
|
const toggle = document.createElement("input");
|
||||||
|
toggle.type = "checkbox";
|
||||||
|
toggle.className = "toggle toggle-sm";
|
||||||
|
toggle.checked = effectiveValue;
|
||||||
|
if (!hasOverride) toggle.classList.add("inherited");
|
||||||
|
|
||||||
|
toggle.addEventListener("change", async () => {
|
||||||
|
overrides[key] = toggle.checked;
|
||||||
|
toggle.classList.remove("inherited");
|
||||||
|
await browser.runtime.sendMessage({
|
||||||
|
type: "setContainerVectors",
|
||||||
|
cookieStoreId,
|
||||||
|
vectors: overrides
|
||||||
|
});
|
||||||
|
});
|
||||||
|
item.appendChild(toggle);
|
||||||
|
|
||||||
|
const resetBtn = document.createElement("button");
|
||||||
|
resetBtn.className = "vector-reset";
|
||||||
|
resetBtn.textContent = "\u21A9";
|
||||||
|
resetBtn.title = "Reset to global default";
|
||||||
|
if (!hasOverride) resetBtn.style.visibility = "hidden";
|
||||||
|
resetBtn.addEventListener("click", async () => {
|
||||||
|
delete overrides[key];
|
||||||
|
toggle.checked = globalEnabled;
|
||||||
|
toggle.classList.add("inherited");
|
||||||
|
resetBtn.style.visibility = "hidden";
|
||||||
|
await browser.runtime.sendMessage({
|
||||||
|
type: "setContainerVectors",
|
||||||
|
cookieStoreId,
|
||||||
|
vectors: overrides
|
||||||
|
});
|
||||||
|
});
|
||||||
|
item.appendChild(resetBtn);
|
||||||
|
|
||||||
|
panel.appendChild(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
row.after(panel);
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("regen-all").addEventListener("click", async (e) => {
|
document.getElementById("regen-all").addEventListener("click", async (e) => {
|
||||||
e.target.textContent = "Regenerating...";
|
e.target.textContent = "Regenerating...";
|
||||||
await browser.runtime.sendMessage({ type: "regenerateAll" });
|
await browser.runtime.sendMessage({ type: "regenerateAll" });
|
||||||
|
|||||||
Reference in New Issue
Block a user