From 60b5092c93291f8ef5a5d0d9d99aa3406f5750b8 Mon Sep 17 00:00:00 2001 From: adamp01 Date: Wed, 27 Jul 2022 11:12:16 +0100 Subject: [PATCH] feat: add functionality to load overlays into browser files (cherry picked from commit 7561b2434a3258c302ba7a0573bb97831ba5aa64) --- waterfox/browser/components/WaterfoxGlue.jsm | 58 ++- waterfox/browser/components/chrome.manifest | 0 waterfox/browser/components/jar.mn | 3 + waterfox/browser/components/moz.build | 2 + .../browser/extensions/common/Overlays.jsm | 421 +++++++++--------- 5 files changed, 281 insertions(+), 203 deletions(-) create mode 100644 waterfox/browser/components/chrome.manifest create mode 100644 waterfox/browser/components/jar.mn diff --git a/waterfox/browser/components/WaterfoxGlue.jsm b/waterfox/browser/components/WaterfoxGlue.jsm index 211d636e34f3..f17f46db3ef0 100644 --- a/waterfox/browser/components/WaterfoxGlue.jsm +++ b/waterfox/browser/components/WaterfoxGlue.jsm @@ -6,6 +6,62 @@ const EXPORTED_SYMBOLS = ["WaterfoxGlue"]; +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + ChromeManifest: "resource:///modules/ChromeManifest.jsm", + Overlays: "resource:///modules/Overlays.jsm", +}); + +XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]); + const WaterfoxGlue = { - init() {}, + async init() { + // Parse chrome.manifest + this.startupManifest = await this.getChromeManifest("startup"); + + // Observe chrome-document-loaded topic to detect window open + Services.obs.addObserver(this, "chrome-document-loaded"); + }, + + async getChromeManifest(manifest) { + let uri; + switch (manifest) { + case "startup": + uri = "resource://waterfox/overlays/chrome.manifest"; + break; + } + let chromeManifest = new ChromeManifest(async () => { + let res = await fetch(uri); + return res.text(); + }, this.options); + await chromeManifest.parse(); + return chromeManifest; + }, + + options: { + application: Services.appinfo.ID, + appversion: Services.appinfo.version, + platformversion: Services.appinfo.platformVersion, + os: Services.appinfo.OS, + osversion: Services.sysinfo.getProperty("version"), + abi: Services.appinfo.XPCOMABI, + }, + + async observe(subject, topic, data) { + switch (topic) { + case "chrome-document-loaded": + // Only load overlays in new browser windows + // baseURI for about:blank is also browser.xhtml, so use URL + if (subject.URL.includes("browser.xhtml")) { + const window = subject.defaultView; + Overlays.load(this.startupManifest, window); + } + break; + } + }, }; diff --git a/waterfox/browser/components/chrome.manifest b/waterfox/browser/components/chrome.manifest new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/waterfox/browser/components/jar.mn b/waterfox/browser/components/jar.mn new file mode 100644 index 000000000000..dd63b1842319 --- /dev/null +++ b/waterfox/browser/components/jar.mn @@ -0,0 +1,3 @@ +browser.jar: +% resource waterfox %waterfox/ contentaccessible=yes + waterfox/overlays/chrome.manifest (chrome.manifest) diff --git a/waterfox/browser/components/moz.build b/waterfox/browser/components/moz.build index 3cb6f2377034..34924ffe83e1 100644 --- a/waterfox/browser/components/moz.build +++ b/waterfox/browser/components/moz.build @@ -7,3 +7,5 @@ EXTRA_JS_MODULES += [ "WaterfoxGlue.jsm", ] + +JAR_MANIFESTS += ["jar.mn"] diff --git a/waterfox/browser/extensions/common/Overlays.jsm b/waterfox/browser/extensions/common/Overlays.jsm index 0ff6b7ebb0d2..9ad0747a78c4 100644 --- a/waterfox/browser/extensions/common/Overlays.jsm +++ b/waterfox/browser/extensions/common/Overlays.jsm @@ -1,33 +1,23 @@ -/* 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/. */ - /** * Load overlays in a similar way as XUL did for legacy XUL add-ons. */ +/* exported Overlays */ + "use strict"; -this.EXPORTED_SYMBOLS = ["Overlays"]; +const EXPORTED_SYMBOLS = ["Overlays"]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const lazy = {}; -const { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm"); ChromeUtils.defineModuleGetter( - this, - "Services", - "resource://gre/modules/Services.jsm" -); -ChromeUtils.defineModuleGetter( - this, + lazy, "setTimeout", "resource://gre/modules/Timer.jsm" ); -let oconsole = new ConsoleAPI({ - prefix: "Overlays.jsm", - consoleID: "overlays-jsm", - maxLogLevelPref: "extensions.overlayloader.loglevel", -}); - /** * The overlays class, providing support for loading overlays like they used to work. This class * should likely be called through its static method Overlays.load() @@ -42,10 +32,11 @@ class Overlays { * @param {DOMWindow} window The window to load into */ static load(overlayProvider, window) { - let instance = new Overlays(overlayProvider, window); + const instance = new Overlays(overlayProvider, window); - let urls = overlayProvider.overlay.get(instance.location, false); - instance.load(urls); + const urls = overlayProvider.overlay.get(instance.location, false); + console.debug(instance.location); + return instance.load(urls); } /** @@ -76,35 +67,36 @@ class Overlays { * Loads the given urls into the window, recursively loading further overlays as provided by the * overlayProvider. * - * @param {string[]} urls The urls to load + * @param {String[]} urls The urls to load */ - load(urls) { - let unloadedOverlays = this._collectOverlays(this.document).concat(urls); + async load(urls) { + const unloadedOverlays = new Set( + this._collectOverlays(this.document).concat(urls) + ); let forwardReferences = []; - let unloadedScripts = []; - let unloadedSheets = []; + this.unloadedScripts = []; + const unloadedSheets = []; this._toolbarsToResolve = []; - let xulStore = Services.xulStore; + const xulStore = Services.xulStore; this.persistedIDs = new Set(); // Load css styles from the registry - for (let sheet of this.overlayProvider.style.get(this.location, false)) { + for (const sheet of this.overlayProvider.style.get(this.location, false)) { unloadedSheets.push(sheet); } - if (!unloadedOverlays.length && !unloadedSheets.length) { + if (!unloadedOverlays.size && !unloadedSheets.length) { return; } - while (unloadedOverlays.length) { - let url = unloadedOverlays.shift(); - let xhr = this.fetchOverlay(url); - let doc = xhr.responseXML; + for (const url of unloadedOverlays) { + unloadedOverlays.delete(url); + const doc = await this.fetchOverlay(url); - oconsole.debug(`Applying ${url} to ${this.location}`); + console.debug(`Applying ${url} to ${this.location}`); // clean the document a bit - let emptyNodes = doc.evaluate( + const emptyNodes = doc.evaluate( "//text()[normalize-space(.) = '']", doc, null, @@ -112,23 +104,31 @@ class Overlays { null ); for (let i = 0, len = emptyNodes.snapshotLength; i < len; ++i) { - let node = emptyNodes.snapshotItem(i); + const node = emptyNodes.snapshotItem(i); node.remove(); } - let commentNodes = doc.evaluate("//comment()", doc, null, 7, null); + const commentNodes = doc.evaluate("//comment()", doc, null, 7, null); for (let i = 0, len = commentNodes.snapshotLength; i < len; ++i) { - let node = commentNodes.snapshotItem(i); + const node = commentNodes.snapshotItem(i); node.remove(); } + // Force a re-evaluation of inline styles to work around an issue + // causing inline styles to be initially ignored. + const styledNodes = doc.evaluate("//*[@style]", doc, null, 7, null); + for (let i = 0, len = styledNodes.snapshotLength; i < len; ++i) { + const node = styledNodes.snapshotItem(i); + node.style.display = node.style.display; // eslint-disable-line no-self-assign + } + // Load css styles from the registry - for (let sheet of this.overlayProvider.style.get(url, false)) { + for (const sheet of this.overlayProvider.style.get(url, false)) { unloadedSheets.push(sheet); } // Load css processing instructions from the overlay - let stylesheets = doc.evaluate( + const stylesheets = doc.evaluate( "/processing-instruction('xml-stylesheet')", doc, null, @@ -136,33 +136,44 @@ class Overlays { null ); for (let i = 0, len = stylesheets.snapshotLength; i < len; ++i) { - let node = stylesheets.snapshotItem(i); - let match = node.nodeValue.match(/href=["']([^"']*)["']/); + const node = stylesheets.snapshotItem(i); + const match = node.nodeValue.match(/href=["']([^"']*)["']/); if (match) { unloadedSheets.push(new URL(match[1], node.baseURI).href); } } + const t_unloadedOverlays = []; // Prepare loading further nested xul overlays from the overlay - unloadedOverlays.push(...this._collectOverlays(doc)); + t_unloadedOverlays.push(...this._collectOverlays(doc)); // Prepare loading further nested xul overlays from the registry - for (let overlayUrl of this.overlayProvider.overlay.get(url, false)) { - unloadedOverlays.push(overlayUrl); + for (const overlayUrl of this.overlayProvider.overlay.get(url, false)) { + t_unloadedOverlays.push(overlayUrl); } + t_unloadedOverlays.forEach(o => unloadedOverlays.add(o)); + // Run through all overlay nodes on the first level (hookup nodes). Scripts will be deferred // until later for simplicity (c++ code seems to process them earlier?). - for (let node of doc.documentElement.children) { + const t_forwardReferences = []; + for (const node of doc.documentElement.children) { if (node.localName == "script") { - unloadedScripts.push(node); + this.unloadedScripts.push(node); } else { - forwardReferences.push(node); + t_forwardReferences.push(node); } } + forwardReferences.unshift(...t_forwardReferences); } - let ids = xulStore.getIDsEnumerator(this.location); + // We've resolved all the forward references we can, we can now go ahead and load the scripts + this.deferredLoad = []; + for (const script of this.unloadedScripts) { + this.deferredLoad.push(...this.loadScript(script)); + } + + const ids = xulStore.getIDsEnumerator(this.location); while (ids.hasMore()) { this.persistedIDs.add(ids.getNext()); } @@ -172,9 +183,9 @@ class Overlays { let previous = 0; while (forwardReferences.length && forwardReferences.length != previous) { previous = forwardReferences.length; - let unresolved = []; + const unresolved = []; - for (let ref of forwardReferences) { + for (const ref of forwardReferences) { if (!this._resolveForwardReference(ref)) { unresolved.push(ref); } @@ -184,76 +195,58 @@ class Overlays { } if (forwardReferences.length) { - oconsole.warn( + console.warn( `Could not resolve ${forwardReferences.length} references`, forwardReferences ); } // Loading the sheets now to avoid race conditions with xbl bindings - for (let sheet of unloadedSheets) { + for (const sheet of unloadedSheets) { this.loadCSS(sheet); } this._decksToResolve = new Map(); - for (let id of this.persistedIDs.values()) { - let element = this.document.getElementById(id); + for (const id of this.persistedIDs.values()) { + const element = this.document.getElementById(id); if (element) { - let attrNames = xulStore.getAttributeEnumerator(this.location, id); + const attrNames = xulStore.getAttributeEnumerator(this.location, id); while (attrNames.hasMore()) { - let attrName = attrNames.getNext(); - let attrValue = xulStore.getValue(this.location, id, attrName); + const attrName = attrNames.getNext(); + const attrValue = xulStore.getValue(this.location, id, attrName); if (attrName == "selectedIndex" && element.localName == "deck") { this._decksToResolve.set(element, attrValue); - } else { + } else if ( + (element != this.document.documentElement || + !["height", "screenX", "screenY", "sizemode", "width"].includes( + attrName + )) && + element.getAttribute(attrName) != attrValue.toString() + ) { element.setAttribute(attrName, attrValue); } } } } - // We've resolved all the forward references we can, we can now go ahead and load the scripts - let deferredLoad = []; - for (let script of unloadedScripts) { - deferredLoad.push(...this.loadScript(script)); - } - if (this.document.readyState == "complete") { - let sheet; - let overlayTrigger = this.document.createXULElement("overlayTrigger"); - overlayTrigger.addEventListener( - "bindingattached", - () => { - oconsole.debug("XBL binding attached, continuing with load"); - if (sheet) { - sheet.remove(); + lazy.setTimeout(() => { + this._finish(); + + // Now execute load handlers since we are done loading scripts + const bubbles = []; + for (const { listener, useCapture } of this.deferredLoad) { + if (useCapture) { + this._fireEventListener(listener); + } else { + bubbles.push(listener); } - overlayTrigger.remove(); + } - setTimeout(() => { - this._finish(); - - // Now execute load handlers since we are done loading scripts - let bubbles = []; - for (let { listener, useCapture } of deferredLoad) { - if (useCapture) { - this._fireEventListener(listener); - } else { - bubbles.push(listener); - } - } - - for (let listener of bubbles) { - this._fireEventListener(listener); - } - }, 0); - }, - { once: true } - ); - this.document.documentElement.appendChild(overlayTrigger); - if (overlayTrigger.parentNode) { - sheet = this.loadCSS("chrome://messenger/content/overlayBindings.css"); - } + for (const listener of bubbles) { + this._fireEventListener(listener); + } + }); } else { this.document.defaultView.addEventListener( "load", @@ -264,18 +257,18 @@ class Overlays { } _finish() { - for (let [deck, selectedIndex] of this._decksToResolve.entries()) { + for (const [deck, selectedIndex] of this._decksToResolve.entries()) { deck.setAttribute("selectedIndex", selectedIndex); } - for (let bar of this._toolbarsToResolve) { - let currentset = Services.xulStore.getValue( + for (const bar of this._toolbarsToResolve) { + const currentSet = Services.xulStore.getValue( this.location, bar.id, "currentset" ); - if (currentset) { - bar.currentSet = currentset; + if (currentSet) { + bar.currentSet = currentSet; } else if (bar.getAttribute("defaultset")) { bar.currentSet = bar.getAttribute("defaultset"); } @@ -285,12 +278,12 @@ class Overlays { /** * Gets the overlays referenced by processing instruction on a document. * - * @param {DOMDocument} document The document to read instuctions from - * @returns {string[]} URLs of the overlays from the document + * @param {DOMDocument} document The document to read instructions from + * @return {String[]} URLs of the overlays from the document */ _collectOverlays(doc) { - let urls = []; - let instructions = doc.evaluate( + const urls = []; + const instructions = doc.evaluate( "/processing-instruction('xul-overlay')", doc, null, @@ -298,8 +291,8 @@ class Overlays { null ); for (let i = 0, len = instructions.snapshotLength; i < len; ++i) { - let node = instructions.snapshotItem(i); - let match = node.nodeValue.match(/href=["']([^"']*)["']/); + const node = instructions.snapshotItem(i); + const match = node.nodeValue.match(/href=["']([^"']*)["']/); if (match) { urls.push(match[1]); } @@ -313,13 +306,13 @@ class Overlays { * @param {EventListener|Function} listener The event listener to call */ _fireEventListener(listener) { - let fakeEvent = new this.window.UIEvent("load", { view: this.window }); + const fakeEvent = new this.window.UIEvent("load", { view: this.window }); if (typeof listener == "function") { listener(fakeEvent); } else if (listener && typeof listener == "object") { listener.handleEvent(fakeEvent); } else { - oconsole.error("Unknown listener type", listener); + console.error("Unknown listener type", listener); } } @@ -329,36 +322,27 @@ class Overlays { * level. * * @param {Element} node The DOM Element to resolve in the target document. - * @returns {boolean} True, if the node was merged/inserted, false otherwise + * @return {Boolean} True, if the node was merged/inserted, false otherwise */ _resolveForwardReference(node) { if (node.id) { - let target = this.document.getElementById(node.id); + const target = this.document.getElementById(node.id); + // if (node.localName == "template") { + // this._insertElement(this.document.documentElement, node); + // return true; + // } else if (node.localName == "toolbarpalette") { - let box; - if (target) { - box = target.closest("toolbox"); - } else { - // These vanish from the document but still exist via the palette property - let boxes = [...this.document.getElementsByTagName("toolbox")]; - box = boxes.find(box => box.palette && box.palette.id == node.id); - let palette = box ? box.palette : null; - - if (!palette) { - oconsole.debug( - `The palette for ${node.id} could not be found, deferring to later` - ); - return false; - } - - target = palette; - } - - this._toolbarsToResolve.push( - ...box.querySelectorAll('toolbar:not([type="menubar"])') - ); + console.error("toolbarpalette is unsupported type", node.id); + return false; } else if (!target) { - oconsole.debug( + if ( + node.hasAttribute("insertafter") || + node.hasAttribute("insertbefore") + ) { + this._insertElement(this.document.documentElement, node); + return true; + } + console.debug( `The node ${node.id} could not be found, deferring to later` ); return false; @@ -380,7 +364,7 @@ class Overlays { _insertElement(parent, node) { // These elements need their values set before they are added to // the document, or bad things happen. - for (let element of node.querySelectorAll("menulist, radiogroup")) { + for (const element of node.querySelectorAll("menulist")) { if (element.id && this.persistedIDs.has(element.id)) { element.setAttribute( "value", @@ -389,6 +373,17 @@ class Overlays { } } + if (node.localName == "toolbar") { + this._toolbarsToResolve.push(node); + } else { + this._toolbarsToResolve.push(...node.querySelectorAll("toolbar")); + } + + const nodes = node.querySelectorAll("script"); + for (const script of nodes) { + this.deferredLoad.push(...this.loadScript(script)); + } + let wasInserted = false; let pos = node.getAttribute("insertafter"); let after = true; @@ -399,12 +394,12 @@ class Overlays { } if (pos) { - for (let id of pos.split(",")) { - let targetchild = this.document.getElementById(id); - if (targetchild && targetchild.parentNode == parent) { - parent.insertBefore( + for (const id of pos.split(",")) { + const targetChild = this.document.getElementById(id); + if (targetChild && parent.contains(targetChild.parentNode)) { + targetChild.parentNode.insertBefore( node, - after ? targetchild.nextSibling : targetchild + after ? targetChild.nextElementSibling : targetChild ); wasInserted = true; break; @@ -414,9 +409,9 @@ class Overlays { if (!wasInserted) { // position is 1-based - let position = parseInt(node.getAttribute("position"), 10); - if (position > 0 && position - 1 <= parent.childNodes.length) { - parent.insertBefore(node, parent.childNodes[position - 1]); + const position = parseInt(node.getAttribute("position"), 10); + if (position > 0 && position - 1 <= parent.children.length) { + parent.insertBefore(node, parent.children[position - 1]); wasInserted = true; } } @@ -433,36 +428,65 @@ class Overlays { * * @param {Element} target The node to merge into * @param {Element} node The node that is being merged + * @param {bool} shadow If the target node is in the shadow DOM */ - _mergeElement(target, node) { - for (let attribute of node.attributes) { - if (attribute.name == "id") { - continue; - } + _mergeElement(target, node, shadow = false) { + for (const attribute of node.attributes) { + if (attribute.name !== "id") { + if (attribute.name == "removeelement" && attribute.value == "true") { + target.remove(); + return; + } - if (attribute.name == "removeelement" && attribute.value == "true") { - target.remove(); - return; + target.setAttributeNS( + attribute.namespaceURI, + attribute.name, + attribute.value + ); } + } - target.setAttributeNS( - attribute.namespaceURI, - attribute.name, - attribute.value - ); + for (const nodes of node.children) { + if (nodes.localName == "script") { + this.deferredLoad.push(...this.loadScript(nodes)); + } + } + + // Get the DocumentFragment + if (node.localName === "template") { + node = node.content; } for (let i = 0, len = node.childElementCount; i < len; i++) { - let child = node.firstElementChild; + const child = node.firstElementChild; child.remove(); let elementInDocument = child.id ? this.document.getElementById(child.id) : null; - let parentId = elementInDocument ? elementInDocument.parentNode.id : null; + const parentId = elementInDocument + ? elementInDocument.parentNode.id + : null; + // Strictly to working with shadow DOM elements + if (target.content) { + elementInDocument = target.content.children.namedItem(child.id); + } else if (shadow && target.children.length === 1) { + // This condition is very specific to adding appMenu items, if we need another + // use case we will have to re-visit + elementInDocument = target.children[0]; + } + + // Need an else if nodeName is doc fragment if (parentId && parentId == target.id) { this._mergeElement(elementInDocument, child); + } else if ( + elementInDocument && + (elementInDocument.parentNode.nodeName === "#document-fragment" || + shadow) + ) { + // Merging for shadow DOM elements, as getElementById does not work + this._mergeElement(elementInDocument, child, true); } else { this._insertElement(target, child); } @@ -470,11 +494,10 @@ class Overlays { } /** - * Fetches the overlay from the given chrome:// or resource:// URL. This happen synchronously so - * we have a chance to complete before the load event. + * Fetches the overlay from the given chrome:// or resource:// URL. * - * @param {string} srcUrl The URL to load - * @returns {XMLHttpRequest} The completed XHR. + * @param {String} srcUrl The URL to load + * @return {Promise} Returns a promise that is resolved with the XML document. */ fetchOverlay(srcUrl) { if (!srcUrl.startsWith("chrome://") && !srcUrl.startsWith("resource://")) { @@ -483,24 +506,28 @@ class Overlays { ); } - let xhr = new XMLHttpRequest(); - xhr.overrideMimeType("application/xml"); - xhr.open("GET", srcUrl, false); + return new Promise((resolve, reject) => { + const xhr = new this.window.XMLHttpRequest(); + xhr.overrideMimeType("application/xml"); + xhr.open("GET", srcUrl, true); - // Elevate the request, so DTDs will work. Should not be a security issue since we - // only load chrome, resource and file URLs, and that is our privileged chrome package. - try { - xhr.channel.owner = Services.scriptSecurityManager.getSystemPrincipal(); - } catch (ex) { - oconsole.error( - "Failed to set system principal while fetching overlay " + srcUrl - ); - xhr.close(); - throw new Error("Failed to set system principal"); - } + // Elevate the request, so DTDs will work. Should not be a security issue since we + // only load chrome, resource and file URLs, and that is our privileged chrome package. + try { + xhr.channel.owner = Services.scriptSecurityManager.getSystemPrincipal(); + } catch (ex) { + console.error( + `Failed to set system principal while fetching overlay ${srcUrl}` + ); + xhr.close(); + reject("Failed to set system principal"); + } - xhr.send(null); - return xhr; + xhr.onload = () => resolve(xhr.responseXML); + xhr.onerror = () => + reject(`Failed to load ${srcUrl} to ${this.location}`); + xhr.send(null); + }); } /** @@ -508,14 +535,14 @@ class Overlays { * be an inline script with textContent. * * @param {Element} node The