mirror of
https://github.com/zen-browser/desktop.git
synced 2026-06-13 15:03:41 +00:00
no-bug: Block loading glance from unmatching principals (gh-13237)
This commit is contained in:
@@ -34,7 +34,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
|
||||
// Arc animation configuration
|
||||
#ARC_CONFIG = Object.freeze({
|
||||
ARC_STEPS: 400, // Increased for smoother bounce
|
||||
ARC_STEPS: 80, // Browser interpolates between keyframes natively
|
||||
MAX_ARC_HEIGHT: 25,
|
||||
ARC_HEIGHT_RATIO: 0.2, // Arc height = distance * ratio (capped at MAX_ARC_HEIGHT)
|
||||
});
|
||||
@@ -78,7 +78,11 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
menuitem.setAttribute("data-l10n-id", "zen-open-link-in-glance");
|
||||
|
||||
menuitem.addEventListener("command", () =>
|
||||
this.openGlance({ url: gContextMenu.linkURL })
|
||||
this.openGlance({
|
||||
url: gContextMenu.linkURL,
|
||||
triggeringPrincipal:
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
})
|
||||
);
|
||||
|
||||
document
|
||||
@@ -171,19 +175,20 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
/**
|
||||
* Create a new browser element for a glance
|
||||
*
|
||||
* @param {string} url - The URL to load
|
||||
* @param {object} data - Glance data including URL and dimensions
|
||||
* @param {Tab} currentTab - The current tab
|
||||
* @param {Tab} existingTab - Optional existing tab to reuse
|
||||
* @param {Tab|null} existingTab - Optional existing tab to reuse
|
||||
* @returns {Browser} The created browser element
|
||||
*/
|
||||
#createBrowserElement(url, currentTab, existingTab = null) {
|
||||
const newTabOptions = this.#createTabOptions(currentTab);
|
||||
#createBrowserElement(data, currentTab, existingTab = null) {
|
||||
const url = data.url;
|
||||
const newTabOptions = this.#createTabOptions(currentTab, data);
|
||||
const newUUID = gZenUIManager.generateUuidv4();
|
||||
|
||||
currentTab._selected = true;
|
||||
const newTab =
|
||||
existingTab ??
|
||||
gBrowser.addTrustedTab(Services.io.newURI(url).spec, newTabOptions);
|
||||
gBrowser.addTab(Services.io.newURI(url).spec, newTabOptions);
|
||||
|
||||
this.#configureNewTab(newTab, currentTab, newUUID);
|
||||
this.#registerGlance(newTab, currentTab, newUUID);
|
||||
@@ -196,14 +201,18 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
* Create tab options for a new glance tab
|
||||
*
|
||||
* @param {Tab} currentTab - The current tab
|
||||
* @param {object} data - Glance data for the new tab
|
||||
* @returns {object} Tab options
|
||||
*/
|
||||
#createTabOptions(currentTab) {
|
||||
#createTabOptions(currentTab, data) {
|
||||
return {
|
||||
userContextId: currentTab.getAttribute("usercontextid") || "",
|
||||
skipBackgroundNotify: true,
|
||||
insertTab: true,
|
||||
skipLoad: false,
|
||||
skipAnimation: true,
|
||||
ownerTab: currentTab,
|
||||
triggeringPrincipal: data.triggeringPrincipal,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -364,7 +373,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
this.#setAnimationState(true);
|
||||
const currentTab = ownerTab ?? gBrowser.selectedTab;
|
||||
const browserElement = this.#createBrowserElement(
|
||||
data.url,
|
||||
data,
|
||||
currentTab,
|
||||
existingTab
|
||||
);
|
||||
@@ -393,7 +402,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
* @returns {Promise<Tab>} Promise that resolves to the glance tab
|
||||
*/
|
||||
#animateGlanceOpening(data, browserElement) {
|
||||
this.#prepareGlanceAnimation(data, browserElement);
|
||||
this.#prepareGlanceAnimation(data);
|
||||
// FIXME(cheffy): We *must* have the call back async (at least,
|
||||
// until a better solution is found). If we do it inside the requestAnimationFrame,
|
||||
// we see flashing and if we do it directly, the animation does not play at all.
|
||||
@@ -417,15 +426,13 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
* Prepare the glance for animation
|
||||
*
|
||||
* @param {object} data - Glance data
|
||||
* @param {Browser} browserElement - The browser element
|
||||
*/
|
||||
#prepareGlanceAnimation(data, browserElement) {
|
||||
#prepareGlanceAnimation(data) {
|
||||
this.quickOpenGlance();
|
||||
const newButtons = this.#createNewOverlayButtons();
|
||||
this.browserWrapper.appendChild(newButtons);
|
||||
|
||||
this.#setupGlancePositioning(data);
|
||||
this.#configureBrowserElement(browserElement);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -463,9 +470,6 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
|
||||
this.overlay.removeAttribute("fade-out");
|
||||
this.browserWrapper.setAttribute("animate", true);
|
||||
this.browserWrapper.style.transform = `translate(${left - width / 2}px, ${top - height / 2}px)`;
|
||||
this.browserWrapper.style.width = `${width}px`;
|
||||
this.browserWrapper.style.height = `${height}px`;
|
||||
|
||||
this.#storeOriginalPosition({ top, left, width, height });
|
||||
this.overlay.style.overflow = "visible";
|
||||
@@ -510,33 +514,6 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
return imageDataElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure browser element for animation
|
||||
*
|
||||
* @param {Browser} browserElement - The browser element
|
||||
*/
|
||||
#configureBrowserElement(browserElement) {
|
||||
const rect = window.windowUtils.getBoundsWithoutFlushing(
|
||||
this.browserWrapper.parentElement
|
||||
);
|
||||
const minWidth = rect.width * 0.8;
|
||||
const minHeight = rect.height * 0.8;
|
||||
|
||||
browserElement.style.minWidth = `${minWidth}px`;
|
||||
browserElement.style.minHeight = `${minHeight}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the transform origin for the animation
|
||||
*
|
||||
* @param {object} data - Glance data with position and dimensions
|
||||
* @returns {string} The transform origin CSS value
|
||||
*/
|
||||
#getTransformOrigin(data) {
|
||||
const { clientX, clientY } = data;
|
||||
return `${clientX}px ${clientY}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the main glance animation
|
||||
*
|
||||
@@ -547,11 +524,13 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
#executeGlanceAnimation(data, browserElement, resolve) {
|
||||
const imageDataElement = this.#handleElementPreview(data);
|
||||
|
||||
// Create curved animation sequence
|
||||
const arcSequence = this.#createGlanceArcSequence(data, "opening");
|
||||
const transformOrigin = this.#getTransformOrigin(data);
|
||||
|
||||
this.browserWrapper.style.transformOrigin = transformOrigin;
|
||||
// Create the curved animation sequence. The transform origin is handled
|
||||
// separately (for example via CSS on the wrapper).
|
||||
const arcSequence = this.#createGlanceArcSequence(
|
||||
data,
|
||||
"opening",
|
||||
imageDataElement
|
||||
);
|
||||
|
||||
// Only animate if there is element data, so we can apply a
|
||||
// nice fade-in effect to the content. But if it doesn't exist,
|
||||
@@ -601,10 +580,41 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
*
|
||||
* @param {object} data - Glance data with position and dimensions
|
||||
* @param {string} direction - 'opening' or 'closing'
|
||||
* @param {Element|null} imageDataElement - The image data element for preview (optional)
|
||||
* @returns {object} Animation sequence object
|
||||
*/
|
||||
#createGlanceArcSequence(data, direction) {
|
||||
const { clientX, clientY, width, height } = data;
|
||||
#createGlanceArcSequence(data, direction, imageDataElement = null) {
|
||||
let { clientX, clientY, width, height } = data;
|
||||
if (imageDataElement?.parentElement) {
|
||||
// Since we are animating scale transforms on the wrapper, we need to
|
||||
// adjust the width/height to match the scaled size of the element preview,
|
||||
// so the image preview properly matches the size of the animating browser
|
||||
// during the animation.
|
||||
// For example:
|
||||
// +-- wrapper --------------------------+
|
||||
// | |
|
||||
// | +--- element preview -------------+ |
|
||||
// | | | |
|
||||
// | +---------------------------------+ |
|
||||
// | |
|
||||
// +-------------------------------------+
|
||||
// We are scaling the wrapper while having only the element preview size
|
||||
// in mind, so we need to adjust the width/height to match the size of the element preview
|
||||
const rect = imageDataElement.getBoundingClientRect();
|
||||
const imageRect =
|
||||
imageDataElement.firstElementChild.getBoundingClientRect();
|
||||
const widthRatio = rect.width / imageRect.width;
|
||||
// Since the image hasn't loaded at this point, so the image's height is 0
|
||||
// we need to calculate the height ratio based on the original aspect ratio of the image
|
||||
const aspectRatio = width / height;
|
||||
const heightRatio = rect.height / (rect.width / aspectRatio);
|
||||
const originalWidth = width;
|
||||
const originalHeight = height;
|
||||
width *= widthRatio;
|
||||
height *= heightRatio;
|
||||
clientX -= (width - originalWidth) / 2;
|
||||
clientY -= (height - originalHeight) / 2;
|
||||
}
|
||||
|
||||
// Calculate start and end positions based on direction
|
||||
let startPosition, endPosition;
|
||||
@@ -643,6 +653,12 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
};
|
||||
}
|
||||
|
||||
// Reference size used as the scale(1, 1) baseline — this matches the
|
||||
// wrapper's natural CSS size (80% x 100% of the tab panels) so the
|
||||
// animation can run entirely on the compositor via transform.
|
||||
const refWidth = tabPanelsRect.width * widthPercent;
|
||||
const refHeight = tabPanelsRect.height;
|
||||
|
||||
// Calculate distance and arc parameters
|
||||
const distance = this.#calculateDistance(startPosition, endPosition);
|
||||
const { arcHeight, shouldArcDownward } = this.#calculateOptimalArc(
|
||||
@@ -652,9 +668,10 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
);
|
||||
|
||||
const sequence = {
|
||||
transform: [],
|
||||
width: [],
|
||||
height: [],
|
||||
x: [],
|
||||
y: [],
|
||||
scaleY: [],
|
||||
scaleX: [],
|
||||
};
|
||||
|
||||
const steps = this.#ARC_CONFIG.ARC_STEPS;
|
||||
@@ -684,6 +701,8 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
const currentHeight =
|
||||
startPosition.height +
|
||||
(endPosition.height - startPosition.height) * eased;
|
||||
const scaleX = currentWidth / refWidth;
|
||||
const scaleY = currentHeight / refHeight;
|
||||
|
||||
// Calculate position on arc
|
||||
const distanceX = endPosition.x - startPosition.x;
|
||||
@@ -695,11 +714,12 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
distanceY * eased +
|
||||
arcDirection * arcHeight * (1 - (2 * eased - 1) ** 2);
|
||||
|
||||
sequence.transform.push(
|
||||
`translate(${x - currentWidth / 2}px, ${y - currentHeight / 2}px)`
|
||||
);
|
||||
sequence.width.push(`${currentWidth}px`);
|
||||
sequence.height.push(`${currentHeight}px`);
|
||||
let translateX = x - currentWidth / 2;
|
||||
let translateY = y - currentHeight / 2;
|
||||
sequence.x.push(translateX);
|
||||
sequence.y.push(translateY);
|
||||
sequence.scaleX.push(scaleX);
|
||||
sequence.scaleY.push(scaleY);
|
||||
}
|
||||
|
||||
return sequence;
|
||||
@@ -763,19 +783,16 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
imageDataElement.remove();
|
||||
}
|
||||
|
||||
// Batch all style/attribute writes together to avoid interleaved
|
||||
// read/write layout thrashing.
|
||||
this.browserWrapper.style.transformOrigin = "";
|
||||
|
||||
browserElement.style.minWidth = "";
|
||||
browserElement.style.minHeight = "";
|
||||
|
||||
this.browserWrapper.style.height = "100%";
|
||||
this.browserWrapper.style.width = "80%";
|
||||
|
||||
gBrowser.tabContainer._invalidateCachedTabs();
|
||||
this.overlay.style.removeProperty("overflow");
|
||||
this.browserWrapper.removeAttribute("animate");
|
||||
this.browserWrapper.setAttribute("has-finished-animation", true);
|
||||
this.overlay.style.removeProperty("overflow");
|
||||
|
||||
gBrowser.tabContainer._invalidateCachedTabs();
|
||||
this.#setAnimationState(false);
|
||||
this.#currentTab.dispatchEvent(new Event("GlanceOpen", { bubbles: true }));
|
||||
resolve(this.#currentTab);
|
||||
@@ -970,19 +987,14 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
}
|
||||
}
|
||||
|
||||
#imageBitmapToBase64(imageBitmap) {
|
||||
// 1. Create a canvas with the same size as the ImageBitmap
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = imageBitmap.width;
|
||||
canvas.height = imageBitmap.height;
|
||||
|
||||
// 2. Draw the ImageBitmap onto the canvas
|
||||
async #imageBitmapToBase64(imageBitmap) {
|
||||
// Use OffscreenCanvas + blob URL to avoid blocking the main thread
|
||||
// with synchronous base64 encoding from toDataURL().
|
||||
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(imageBitmap, 0, 0);
|
||||
|
||||
// 3. Convert the canvas content to a Base64 string (PNG by default)
|
||||
const base64String = canvas.toDataURL("image/png");
|
||||
return base64String;
|
||||
const blob = await canvas.convertToBlob({ type: "image/png" });
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1027,12 +1039,20 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
this.#currentGlanceID
|
||||
).elementImageData;
|
||||
|
||||
this.#addElementPreview(elementImageData);
|
||||
const imageDataElement = this.#addElementPreview(elementImageData);
|
||||
|
||||
// Create curved closing animation sequence
|
||||
const closingData =
|
||||
this.#createClosingDataFromOriginalPosition(originalPosition);
|
||||
const arcSequence = this.#createGlanceArcSequence(closingData, "closing");
|
||||
const arcSequence = this.#createGlanceArcSequence(
|
||||
closingData,
|
||||
"closing",
|
||||
imageDataElement
|
||||
);
|
||||
|
||||
// Batch style writes before starting animation to avoid layout thrashing
|
||||
this.browserWrapper.style.width = "";
|
||||
this.browserWrapper.style.height = "";
|
||||
|
||||
gZenUIManager.motion
|
||||
.animate(this.browserWrapper, arcSequence, {
|
||||
@@ -1084,6 +1104,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
const imageDataElement =
|
||||
this.#createGlancePreviewElement(elementImageData);
|
||||
this.browserWrapper.prepend(imageDataElement);
|
||||
return imageDataElement;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1506,6 +1527,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
this.openGlance(
|
||||
{
|
||||
url: undefined,
|
||||
// No need for triggeringPrincipal here
|
||||
},
|
||||
tab,
|
||||
tab.owner
|
||||
@@ -1692,6 +1714,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
clientY: top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1871,6 +1894,8 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
...clickPosition,
|
||||
width: 0,
|
||||
height: 0,
|
||||
triggeringPrincipal:
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
},
|
||||
currentTab,
|
||||
parentTab
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
"blockJavascript",
|
||||
"browser.link.alternative_click.block_javascript",
|
||||
true
|
||||
);
|
||||
|
||||
export class ZenGlanceChild extends JSWindowActorChild {
|
||||
#activationMethod;
|
||||
|
||||
@@ -26,31 +41,27 @@ export class ZenGlanceChild extends JSWindowActorChild {
|
||||
return !(event.ctrlKey ^ event.altKey ^ event.shiftKey ^ event.metaKey);
|
||||
}
|
||||
|
||||
#openGlance(target) {
|
||||
let url = target.href;
|
||||
// Add domain to relative URLs
|
||||
if (!url.match(/^(?:[a-z]+:)?\/\//i)) {
|
||||
url = this.contentWindow.location.origin + url;
|
||||
}
|
||||
#openGlance(href, principal) {
|
||||
this.sendAsyncMessage("ZenGlance:OpenGlance", {
|
||||
url,
|
||||
url: href,
|
||||
triggeringPrincipal: principal,
|
||||
});
|
||||
}
|
||||
|
||||
#sendClickDataToParent(target, element) {
|
||||
if (!element && !target) {
|
||||
#sendClickDataToParent(node, originalTarget) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
if (!target) {
|
||||
target = element;
|
||||
}
|
||||
// Get the largest element we can get. If the `A` element
|
||||
// is a parent of the original target, use the anchor element,
|
||||
// otherwise use the original target.
|
||||
let rect = element.getBoundingClientRect();
|
||||
const anchorRect = target.getBoundingClientRect();
|
||||
if (anchorRect.width * anchorRect.height > rect.width * rect.height) {
|
||||
rect = anchorRect;
|
||||
let rect = node.getBoundingClientRect();
|
||||
const originalTargetRect = originalTarget.getBoundingClientRect();
|
||||
if (
|
||||
originalTargetRect.width * originalTargetRect.height >
|
||||
rect.width * rect.height
|
||||
) {
|
||||
rect = originalTargetRect;
|
||||
}
|
||||
this.sendAsyncMessage("ZenGlance:RecordLinkClickData", {
|
||||
clientX: rect.left,
|
||||
@@ -68,29 +79,50 @@ export class ZenGlanceChild extends JSWindowActorChild {
|
||||
*/
|
||||
#getTargetFromEvent(event) {
|
||||
// get closest A element
|
||||
const target = event.target.closest("A");
|
||||
const elementToRecord = event.originalTarget || event.target;
|
||||
let [href, node, principal] =
|
||||
lazy.BrowserUtils.hrefAndLinkNodeForClickEvent(event);
|
||||
return {
|
||||
target,
|
||||
elementToRecord,
|
||||
href,
|
||||
node,
|
||||
principal,
|
||||
};
|
||||
}
|
||||
|
||||
#checkSecurity(href, principal) {
|
||||
if (
|
||||
lazy.blockJavascript &&
|
||||
Services.io.extractScheme(href) == "javascript"
|
||||
) {
|
||||
// We don't want to open new tabs or windows for javascript: links.
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
|
||||
principal,
|
||||
href
|
||||
);
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
on_mousedown(event) {
|
||||
const { target, elementToRecord } = this.#getTargetFromEvent(event);
|
||||
const { node } = this.#getTargetFromEvent(event);
|
||||
// We record the link data anyway, even if the glance may be invoked
|
||||
// or not. We have some cases where glance would open, for example,
|
||||
// when clicking on a link with a different domain where glance would open.
|
||||
// The problem is that at that stage we don't know the rect or even what
|
||||
// element has been clicked, so we send the data here.
|
||||
this.#sendClickDataToParent(target, elementToRecord);
|
||||
this.#sendClickDataToParent(node, event.target);
|
||||
}
|
||||
|
||||
on_click(event) {
|
||||
const { target } = this.#getTargetFromEvent(event);
|
||||
const { node, href, principal } = this.#getTargetFromEvent(event);
|
||||
if (
|
||||
event.button !== 0 ||
|
||||
!target ||
|
||||
!node ||
|
||||
event.defaultPrevented ||
|
||||
this.#ensureOnlyKeyModifiers(event)
|
||||
) {
|
||||
@@ -106,9 +138,12 @@ export class ZenGlanceChild extends JSWindowActorChild {
|
||||
} else if (activationMethod === "meta" && !event.metaKey) {
|
||||
return;
|
||||
}
|
||||
if (this.#checkSecurity(href, principal)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.#openGlance(target);
|
||||
this.#openGlance(href, principal);
|
||||
}
|
||||
|
||||
on_keydown(event) {
|
||||
|
||||
@@ -120,14 +120,14 @@
|
||||
}
|
||||
|
||||
& .browserContainer {
|
||||
transform: translate(-50%, -50%);
|
||||
position: fixed;
|
||||
flex: unset !important;
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
|
||||
&:not([has-finished-animation="true"]) {
|
||||
will-change: width, height, transform;
|
||||
will-change: transform;
|
||||
transform-origin: 0 0;
|
||||
|
||||
#statuspanel {
|
||||
display: none;
|
||||
@@ -178,7 +178,6 @@
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
translate: -50% -50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user