no-bug: Block loading glance from unmatching principals (gh-13237)

This commit is contained in:
mr. m
2026-04-13 20:33:01 +02:00
committed by GitHub
parent adc8c92816
commit d540c6cddf
3 changed files with 165 additions and 106 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;