mirror of
https://github.com/zen-browser/desktop.git
synced 2026-04-16 20:42:57 +00:00
no-bug: Adress possible memory leaks (gh-13242)
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
value: "alt" # ctrl, alt, shift
|
||||
|
||||
- name: zen.glance.animation-duration
|
||||
value: 350 # in milliseconds
|
||||
value: 300 # in milliseconds
|
||||
|
||||
- name: zen.glance.deactivate-docshell-during-animation
|
||||
value: true
|
||||
|
||||
@@ -318,7 +318,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
data.width,
|
||||
data.height
|
||||
);
|
||||
return await this.#imageBitmapToBase64(
|
||||
return await this.#imageBitmapToObjectURL(
|
||||
await window.browsingContext.currentWindowGlobal.drawSnapshot(
|
||||
rect,
|
||||
1,
|
||||
@@ -785,7 +785,6 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
|
||||
// Batch all style/attribute writes together to avoid interleaved
|
||||
// read/write layout thrashing.
|
||||
this.browserWrapper.style.transformOrigin = "";
|
||||
this.browserWrapper.style.height = "100%";
|
||||
this.browserWrapper.style.width = "80%";
|
||||
this.browserWrapper.removeAttribute("animate");
|
||||
@@ -987,16 +986,31 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
}
|
||||
}
|
||||
|
||||
async #imageBitmapToBase64(imageBitmap) {
|
||||
// Use OffscreenCanvas + blob URL to avoid blocking the main thread
|
||||
// with synchronous base64 encoding from toDataURL().
|
||||
async #imageBitmapToObjectURL(imageBitmap) {
|
||||
// OffscreenCanvas + convertToBlob avoids the synchronous PNG re-encode
|
||||
// and base64 string copy that toDataURL performs on the main thread.
|
||||
// Callers must release the URL via #deleteGlance when the glance entry
|
||||
// is removed so the blob can be freed.
|
||||
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(imageBitmap, 0, 0);
|
||||
const blob = await canvas.convertToBlob({ type: "image/png" });
|
||||
imageBitmap.close();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
#deleteGlance(glanceID) {
|
||||
const entry = this.#glances.get(glanceID);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
this.#glances.delete(glanceID);
|
||||
const url = entry.elementData ?? entry.elementImageData;
|
||||
if (typeof url === "string") {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate parent background restoration
|
||||
*
|
||||
@@ -1196,7 +1210,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
*/
|
||||
#resetGlanceState(setNewID) {
|
||||
this.#currentParentTab.removeAttribute("glance-id");
|
||||
this.#glances.delete(this.#currentGlanceID);
|
||||
this.#deleteGlance(this.#currentGlanceID);
|
||||
this.#currentGlanceID = setNewID;
|
||||
this.#duringOpening = false;
|
||||
}
|
||||
@@ -1545,7 +1559,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
this.animatingFullOpen = false;
|
||||
const glanceID = this.#currentGlanceID;
|
||||
this.closeGlance({ noAnimation: true, skipPermitUnload: true });
|
||||
this.#glances.delete(glanceID);
|
||||
this.#deleteGlance(glanceID);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,13 +22,15 @@ export class ZenGlanceParent extends JSWindowActorParent {
|
||||
break;
|
||||
}
|
||||
case "ZenGlance:CloseGlance": {
|
||||
const params = {
|
||||
// Explicitly allowlist fields from content; never forward
|
||||
// skipPermitUnload or other privileged flags.
|
||||
const { noAnimation, setNewID, hasFocused } = message.data ?? {};
|
||||
this.browsingContext.topChromeWindow.gZenGlanceManager.closeGlance({
|
||||
onTabClose: true,
|
||||
...message.data,
|
||||
};
|
||||
this.browsingContext.topChromeWindow.gZenGlanceManager.closeGlance(
|
||||
params
|
||||
);
|
||||
noAnimation: !!noAnimation,
|
||||
setNewID: typeof setNewID === "string" ? setNewID : null,
|
||||
hasFocused: !!hasFocused,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "ZenGlance:RecordLinkClickData": {
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
nsZenMultiWindowFeature,
|
||||
} from "chrome://browser/content/zen-components/ZenCommonUtils.mjs";
|
||||
|
||||
const DOT_RE = /\./g;
|
||||
const WHITESPACE_RE = /\s/g;
|
||||
const NON_NAME_RE = /[^A-Za-z_-]+/g;
|
||||
|
||||
/**
|
||||
* Zen Mods Manager, handles downloading, updating and applying Zen Mods.
|
||||
*
|
||||
@@ -171,47 +175,57 @@ class nsZenMods extends nsZenPreloadedFeature {
|
||||
}
|
||||
|
||||
#writeToDom(modsWithPreferences) {
|
||||
for (const browser of nsZenMultiWindowFeature.browsers) {
|
||||
// Precompute per-mod data once; values are global prefs and sanitized
|
||||
// names don't vary per browser window, so this hoists O(windows) work.
|
||||
const prepared = modsWithPreferences.map(
|
||||
// eslint-disable-next-line no-shadow
|
||||
for (const { enabled, preferences, name } of modsWithPreferences) {
|
||||
const sanitizedName = this.sanitizeModName(name);
|
||||
({ enabled, preferences, name }) => ({
|
||||
enabled,
|
||||
sanitizedName: this.sanitizeModName(name),
|
||||
prefs: preferences.map(({ property, type }) => ({
|
||||
property,
|
||||
type,
|
||||
sanitizedProperty: property?.replaceAll(DOT_RE, "-"),
|
||||
value:
|
||||
enabled === undefined || enabled
|
||||
? Services.prefs.getStringPref(property, "")
|
||||
: "",
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
for (const browser of nsZenMultiWindowFeature.browsers) {
|
||||
const doc = browser.document;
|
||||
const root = doc.documentElement;
|
||||
|
||||
for (const { enabled, sanitizedName, prefs } of prepared) {
|
||||
if (enabled !== undefined && !enabled) {
|
||||
const element = browser.document.getElementById(sanitizedName);
|
||||
|
||||
const element = doc.getElementById(sanitizedName);
|
||||
if (element) {
|
||||
element.remove();
|
||||
}
|
||||
|
||||
for (const { property } of preferences.filter(
|
||||
({ type }) => type !== "checkbox"
|
||||
)) {
|
||||
const sanitizedProperty = property?.replaceAll(/\./g, "-");
|
||||
|
||||
browser.document
|
||||
.querySelector(":root")
|
||||
.style.removeProperty(`--${sanitizedProperty}`);
|
||||
for (const { type, sanitizedProperty } of prefs) {
|
||||
if (type === "checkbox") {
|
||||
continue;
|
||||
}
|
||||
root.style.removeProperty(`--${sanitizedProperty}`);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const { property, type } of preferences) {
|
||||
const value = Services.prefs.getStringPref(property, "");
|
||||
const sanitizedProperty = property?.replaceAll(/\./g, "-");
|
||||
|
||||
for (const { type, sanitizedProperty, value } of prefs) {
|
||||
switch (type) {
|
||||
case "dropdown": {
|
||||
if (value !== "") {
|
||||
let element = browser.document.getElementById(sanitizedName);
|
||||
let element = doc.getElementById(sanitizedName);
|
||||
|
||||
if (!element) {
|
||||
element = browser.document.createElement("div");
|
||||
|
||||
element = doc.createElement("div");
|
||||
element.style.display = "none";
|
||||
element.setAttribute("id", sanitizedName);
|
||||
|
||||
browser.document.body.appendChild(element);
|
||||
doc.body.appendChild(element);
|
||||
}
|
||||
|
||||
element.setAttribute(sanitizedProperty, value);
|
||||
@@ -221,13 +235,9 @@ class nsZenMods extends nsZenPreloadedFeature {
|
||||
|
||||
case "string": {
|
||||
if (value === "") {
|
||||
browser.document
|
||||
.querySelector(":root")
|
||||
.style.removeProperty(`--${sanitizedProperty}`);
|
||||
root.style.removeProperty(`--${sanitizedProperty}`);
|
||||
} else {
|
||||
browser.document
|
||||
.querySelector(":root")
|
||||
.style.setProperty(`--${sanitizedProperty}`, value);
|
||||
root.style.setProperty(`--${sanitizedProperty}`, value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -308,6 +318,19 @@ class nsZenMods extends nsZenPreloadedFeature {
|
||||
}
|
||||
|
||||
async #downloadUrlToFile(url, path, maxRetries = 3, retryDelayMs = 500) {
|
||||
// Mod assets must come over HTTPS. Without a signing/hash scheme this
|
||||
// is the minimum guard against MITM or a store-hosted HTTP redirect
|
||||
// serving attacker-controlled CSS/JSON into the chrome profile.
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(url);
|
||||
} catch {
|
||||
throw new Error(`[ZenMods]: Invalid mod asset URL: ${url}`);
|
||||
}
|
||||
if (parsed.protocol !== "https:") {
|
||||
throw new Error(`[ZenMods]: Refusing non-HTTPS mod asset URL: ${url}`);
|
||||
}
|
||||
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
@@ -377,8 +400,7 @@ class nsZenMods extends nsZenPreloadedFeature {
|
||||
|
||||
sanitizeModName(aName) {
|
||||
// Do not change to "mod-" for backwards compatibility
|
||||
// eslint-disable-next-line no-shadow
|
||||
return `theme-${aName?.replaceAll(/\s/g, "-")?.replaceAll(/[^A-Za-z_-]+/g, "")}`;
|
||||
return `theme-${aName?.replaceAll(WHITESPACE_RE, "-")?.replaceAll(NON_NAME_RE, "")}`;
|
||||
}
|
||||
|
||||
get updatePref() {
|
||||
|
||||
Reference in New Issue
Block a user