summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/ext-browser-content.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/extensions/ext-browser-content.js
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/extensions/ext-browser-content.js')
-rw-r--r--toolkit/components/extensions/ext-browser-content.js350
1 files changed, 350 insertions, 0 deletions
diff --git a/toolkit/components/extensions/ext-browser-content.js b/toolkit/components/extensions/ext-browser-content.js
new file mode 100644
index 0000000000..56fbdfed87
--- /dev/null
+++ b/toolkit/components/extensions/ext-browser-content.js
@@ -0,0 +1,350 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+"use strict";
+
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
+ clearTimeout: "resource://gre/modules/Timer.jsm",
+ ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm",
+ setTimeout: "resource://gre/modules/Timer.jsm",
+});
+
+/* eslint-env mozilla/frame-script */
+
+// Minimum time between two resizes.
+const RESIZE_TIMEOUT = 100;
+
+/**
+ * Check if the provided color is fully opaque.
+ *
+ * @param {string} color
+ * Any valid CSS color.
+ * @returns {boolean} true if the color is opaque.
+ */
+const isOpaque = function(color) {
+ try {
+ if (/(rgba|hsla)/i.test(color)) {
+ // Match .123456, 123.456, 123456 with an optional % sign.
+ let numberRe = /(\.\d+|\d+\.?\d*)%?/g;
+ // hsla/rgba, opacity is the last number in the color string (can be a percentage).
+ let opacity = color.match(numberRe)[3];
+
+ // Convert to [0, 1] space if the opacity was expressed as a percentage.
+ if (opacity.includes("%")) {
+ opacity = opacity.slice(0, -1);
+ opacity = opacity / 100;
+ }
+
+ return opacity * 1 >= 1;
+ } else if (/^#[a-f0-9]{4}$/i.test(color)) {
+ // Hex color with 4 characters, opacity is one if last character is F
+ return color.toUpperCase().endsWith("F");
+ } else if (/^#[a-f0-9]{8}$/i.test(color)) {
+ // Hex color with 8 characters, opacity is one if last 2 characters are FF
+ return color.toUpperCase().endsWith("FF");
+ }
+ } catch (e) {
+ // Invalid color.
+ }
+ return true;
+};
+
+const BrowserListener = {
+ init({
+ allowScriptsToClose,
+ blockParser,
+ fixedWidth,
+ maxHeight,
+ maxWidth,
+ stylesheets,
+ isInline,
+ }) {
+ this.fixedWidth = fixedWidth;
+ this.stylesheets = stylesheets || [];
+
+ this.isInline = isInline;
+ this.maxWidth = maxWidth;
+ this.maxHeight = maxHeight;
+
+ this.blockParser = blockParser;
+ this.needsResize = fixedWidth || maxHeight || maxWidth;
+
+ this.oldBackground = null;
+
+ if (allowScriptsToClose) {
+ content.windowUtils.allowScriptsToClose();
+ }
+
+ // Force external links to open in tabs.
+ docShell.isAppTab = true;
+
+ if (this.blockParser) {
+ this.blockingPromise = new Promise(resolve => {
+ this.unblockParser = resolve;
+ });
+ addEventListener("DOMDocElementInserted", this, true);
+ }
+
+ addEventListener("load", this, true);
+ addEventListener("DOMWindowCreated", this, true);
+ addEventListener("DOMContentLoaded", this, true);
+ addEventListener("MozScrolledAreaChanged", this, true);
+ },
+
+ destroy() {
+ if (this.blockParser) {
+ removeEventListener("DOMDocElementInserted", this, true);
+ }
+
+ removeEventListener("load", this, true);
+ removeEventListener("DOMWindowCreated", this, true);
+ removeEventListener("DOMContentLoaded", this, true);
+ removeEventListener("MozScrolledAreaChanged", this, true);
+ },
+
+ receiveMessage({ name, data }) {
+ if (name === "Extension:InitBrowser") {
+ this.init(data);
+ } else if (name === "Extension:UnblockParser") {
+ if (this.unblockParser) {
+ this.unblockParser();
+ this.blockingPromise = null;
+ }
+ } else if (name === "Extension:GrabFocus") {
+ content.window.requestAnimationFrame(() => {
+ Services.focus.focusedWindow = content.window;
+ });
+ }
+ },
+
+ loadStylesheets() {
+ let { windowUtils } = content;
+
+ for (let url of this.stylesheets) {
+ windowUtils.addSheet(
+ ExtensionCommon.stylesheetMap.get(url),
+ windowUtils.AGENT_SHEET
+ );
+ }
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "DOMDocElementInserted":
+ if (this.blockingPromise) {
+ event.target.blockParsing(this.blockingPromise);
+ }
+ break;
+
+ case "DOMWindowCreated":
+ if (event.target === content.document) {
+ this.loadStylesheets();
+ }
+ break;
+
+ case "DOMContentLoaded":
+ if (event.target === content.document) {
+ sendAsyncMessage("Extension:BrowserContentLoaded", {
+ url: content.location.href,
+ });
+
+ if (this.needsResize) {
+ this.handleDOMChange(true);
+ }
+ }
+ break;
+
+ case "load":
+ if (event.target.contentWindow === content) {
+ // For about:addons inline <browser>s, we currently receive a load
+ // event on the <browser> element, but no load or DOMContentLoaded
+ // events from the content window.
+
+ // Inline browsers don't receive the "DOMWindowCreated" event, so this
+ // is a workaround to load the stylesheets.
+ if (this.isInline) {
+ this.loadStylesheets();
+ }
+ sendAsyncMessage("Extension:BrowserContentLoaded", {
+ url: content.location.href,
+ });
+ } else if (event.target !== content.document) {
+ break;
+ }
+
+ if (!this.needsResize) {
+ break;
+ }
+
+ // We use a capturing listener, so we get this event earlier than any
+ // load listeners in the content page. Resizing after a timeout ensures
+ // that we calculate the size after the entire event cycle has completed
+ // (unless someone spins the event loop, anyway), and hopefully after
+ // the content has made any modifications.
+ Promise.resolve().then(() => {
+ this.handleDOMChange(true);
+ });
+
+ // Mutation observer to make sure the panel shrinks when the content does.
+ new content.MutationObserver(this.handleDOMChange.bind(this)).observe(
+ content.document.documentElement,
+ {
+ attributes: true,
+ characterData: true,
+ childList: true,
+ subtree: true,
+ }
+ );
+ break;
+
+ case "MozScrolledAreaChanged":
+ if (this.needsResize) {
+ this.handleDOMChange();
+ }
+ break;
+ }
+ },
+
+ // Resizes the browser to match the preferred size of the content (debounced).
+ handleDOMChange(ignoreThrottling = false) {
+ if (ignoreThrottling && this.resizeTimeout) {
+ clearTimeout(this.resizeTimeout);
+ this.resizeTimeout = null;
+ }
+
+ if (this.resizeTimeout == null) {
+ this.resizeTimeout = setTimeout(() => {
+ try {
+ if (content) {
+ this._handleDOMChange("delayed");
+ }
+ } finally {
+ this.resizeTimeout = null;
+ }
+ }, RESIZE_TIMEOUT);
+
+ this._handleDOMChange();
+ }
+ },
+
+ _handleDOMChange(detail) {
+ let doc = content.document;
+
+ let body = doc.body;
+ if (!body || doc.compatMode === "BackCompat") {
+ // In quirks mode, the root element is used as the scroll frame, and the
+ // body lies about its scroll geometry, and returns the values for the
+ // root instead.
+ body = doc.documentElement;
+ }
+
+ let result;
+ const zoom = content.browsingContext.fullZoom;
+ if (this.fixedWidth) {
+ // If we're in a fixed-width area (namely a slide-in subview of the main
+ // menu panel), we need to calculate the view height based on the
+ // preferred height of the content document's root scrollable element at the
+ // current width, rather than the complete preferred dimensions of the
+ // content window.
+
+ // Compensate for any offsets (margin, padding, ...) between the scroll
+ // area of the body and the outer height of the document.
+ // This calculation is hard to get right for all cases, so take the lower
+ // number of the combination of all padding and margins of the document
+ // and body elements, or the difference between their heights.
+ let getHeight = elem => elem.getBoundingClientRect(elem).height;
+ let bodyPadding = getHeight(doc.documentElement) - getHeight(body);
+
+ if (body !== doc.documentElement) {
+ let bs = content.getComputedStyle(body);
+ let ds = content.getComputedStyle(doc.documentElement);
+
+ let p =
+ parseFloat(bs.marginTop) +
+ parseFloat(bs.marginBottom) +
+ parseFloat(ds.marginTop) +
+ parseFloat(ds.marginBottom) +
+ parseFloat(ds.paddingTop) +
+ parseFloat(ds.paddingBottom);
+ bodyPadding = Math.min(p, bodyPadding);
+ }
+
+ let height = Math.ceil((body.scrollHeight + bodyPadding) * zoom);
+
+ result = { height, detail };
+ } else {
+ let background = doc.defaultView.getComputedStyle(body).backgroundColor;
+ if (!isOpaque(background)) {
+ // Ignore non-opaque backgrounds.
+ background = null;
+ }
+
+ if (background === null || background !== this.oldBackground) {
+ sendAsyncMessage("Extension:BrowserBackgroundChanged", { background });
+ }
+ this.oldBackground = background;
+
+ // Adjust the size of the browser based on its content's preferred size.
+ let { contentViewer } = docShell;
+ let ratio = content.devicePixelRatio;
+
+ let w = {},
+ h = {};
+ contentViewer.getContentSizeConstrained(
+ this.maxWidth * ratio,
+ this.maxHeight * ratio,
+ w,
+ h
+ );
+
+ let width = Math.ceil((w.value * zoom) / ratio);
+ let height = Math.ceil((h.value * zoom) / ratio);
+
+ result = { width, height, detail };
+ }
+
+ sendAsyncMessage("Extension:BrowserResized", result);
+ },
+};
+
+addMessageListener("Extension:InitBrowser", BrowserListener);
+addMessageListener("Extension:UnblockParser", BrowserListener);
+addMessageListener("Extension:GrabFocus", BrowserListener);
+
+var WebBrowserChrome = {
+ onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab) {
+ // isAppTab is the value for the docShell that received the click. We're
+ // handling this in the top-level frame and want traversal behavior to
+ // match the value for this frame rather than any subframe, so we pass
+ // through the docShell.isAppTab value rather than what we were handed.
+ return BrowserUtils.onBeforeLinkTraversal(
+ originalTarget,
+ linkURI,
+ linkNode,
+ docShell.isAppTab
+ );
+ },
+
+ shouldLoadURI(docShell, URI, referrerInfo, hasPostData, triggeringPrincipal) {
+ return true;
+ },
+
+ shouldLoadURIInThisProcess(URI) {
+ return true;
+ },
+};
+
+if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ let tabchild = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIBrowserChild);
+ tabchild.webBrowserChrome = WebBrowserChrome;
+}
+
+// This is a temporary hack to prevent regressions (bug 1471327).
+void content;