summaryrefslogtreecommitdiffstats
path: root/content/OverlayManager.jsm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--content/OverlayManager.jsm514
1 files changed, 514 insertions, 0 deletions
diff --git a/content/OverlayManager.jsm b/content/OverlayManager.jsm
new file mode 100644
index 0000000..a0c18d6
--- /dev/null
+++ b/content/OverlayManager.jsm
@@ -0,0 +1,514 @@
+/*
+ * This file is part of TbSync.
+ *
+ * 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 EXPORTED_SYMBOLS = ["OverlayManager"];
+
+var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function OverlayManager(extension, options = {}) {
+ this.registeredOverlays = {};
+ this.overlays = {};
+ this.stylesheets = {};
+ this.options = {verbose: 0};
+ this.extension = extension;
+
+ let userOptions = Object.keys(options);
+ for (let i=0; i < userOptions.length; i++) {
+ this.options[userOptions[i]] = options[userOptions[i]];
+ }
+
+
+
+ this.windowListener = {
+ that: this,
+ onOpenWindow: function(xulWindow) {
+ let window = xulWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow);
+ this.that.injectAllOverlays(window);
+ },
+ onCloseWindow: function(xulWindow) { },
+ onWindowTitleChange: function(xulWindow, newTitle) { }
+ };
+
+
+
+
+ this.startObserving = function () {
+ let windows = Services.wm.getEnumerator(null);
+ while (windows.hasMoreElements()) {
+ let window = windows.getNext();
+ //inject overlays for this window
+ this.injectAllOverlays(window);
+ }
+
+ Services.wm.addListener(this.windowListener);
+ };
+
+ this.stopObserving = function () {
+ Services.wm.removeListener(this.windowListener);
+
+ let windows = Services.wm.getEnumerator(null);
+ while (windows.hasMoreElements()) {
+ let window = windows.getNext();
+ //remove overlays (if any)
+ this.removeAllOverlays(window);
+ }
+ };
+
+ this.hasRegisteredOverlays = function (window) {
+ return this.registeredOverlays.hasOwnProperty(window.location.href);
+ };
+
+ this.registerOverlay = async function (dst, overlay) {
+ if (overlay.startsWith("chrome://")) {
+ let xul = null;
+ try {
+ xul = await this.readChromeFile(overlay);
+ } catch (e) {
+ console.log("Error reading file <"+overlay+"> : " + e);
+ return;
+ }
+ let rootNode = this.getDataFromXULString(xul);
+
+ if (rootNode) {
+ //get urls of stylesheets to load them
+ let styleSheetUrls = this.getStyleSheetUrls(rootNode);
+ for (let i=0; i<styleSheetUrls.length; i++) {
+ //we must replace, since we do not know, if it changed - could have been an update
+ //if (!this.stylesheets.hasOwnProperty(styleSheetUrls[i])) {
+ this.stylesheets[styleSheetUrls[i]] = await this.readChromeFile(styleSheetUrls[i]);
+ //}
+ }
+
+ if (!this.registeredOverlays[dst]) this.registeredOverlays[dst] = [];
+ if (!this.registeredOverlays[dst].includes(overlay)) this.registeredOverlays[dst].push(overlay);
+
+ this.overlays[overlay] = rootNode;
+ }
+ } else {
+ console.log("Only chrome:// URIs can be registered as overlays.");
+ }
+ };
+
+ this.getDataFromXULString = function (str) {
+ let data = null;
+ let xul = "";
+ if (str == "") {
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: A provided XUL file is empty!");
+ return null;
+ }
+
+ let oParser = new DOMParser();
+ try {
+ xul = oParser.parseFromString(str, "application/xml");
+ } catch (e) {
+ //however, domparser does not throw an error, it returns an error document
+ //https://developer.mozilla.org/de/docs/Web/API/DOMParser
+ //just in case
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: A provided XUL file could not be parsed correctly, something is wrong.\n" + str);
+ return null;
+ }
+
+ //check if xul is error document
+ if (xul.documentElement.nodeName == "parsererror") {
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: A provided XUL file could not be parsed correctly, something is wrong.\n" + str);
+ return null;
+ }
+
+ if (xul.documentElement.nodeName != "overlay") {
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: A provided XUL file does not look like an overlay (root node is not overlay).\n" + str);
+ return null;
+ }
+
+ return xul;
+ };
+
+
+
+
+
+ this.injectAllOverlays = async function (window, _href = null) {
+ if (window.document.readyState != "complete") {
+ // Make sure the window load has completed.
+ await new Promise(resolve => {
+ window.addEventListener("load", resolve, { once: true });
+ });
+ }
+
+ let href = (_href === null) ? window.location.href : _href;
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] Injecting into new window: " + href);
+ let injectCount = 0;
+ for (let i=0; this.registeredOverlays[href] && i < this.registeredOverlays[href].length; i++) {
+ if (this.injectOverlay(window, this.registeredOverlays[href][i])) injectCount++;
+ }
+ if (injectCount > 0) {
+ // dispatch a custom event to indicate we finished loading the overlay
+ let event = new Event("DOMOverlayLoaded_" + this.extension.id);
+ window.document.dispatchEvent(event);
+ }
+ };
+
+ this.removeAllOverlays = function (window) {
+ if (!this.hasRegisteredOverlays(window))
+ return;
+
+ for (let i=0; i < this.registeredOverlays[window.location.href].length; i++) {
+ this.removeOverlay(window, this.registeredOverlays[window.location.href][i]);
+ }
+ };
+
+
+
+
+ this.injectOverlay = function (window, overlay) {
+ if (!window.hasOwnProperty("injectedOverlays")) window.injectedOverlays = [];
+
+ if (window.injectedOverlays.includes(overlay)) {
+ if (this.options.verbose>2) Services.console.logStringMessage("[OverlayManager] NOT Injecting: " + overlay);
+ return false;
+ }
+
+ let rootNode = this.overlays[overlay];
+
+ if (rootNode) {
+ let overlayNode = rootNode.documentElement;
+ if (overlayNode) {
+ //get and load scripts
+ let scripts = this.getScripts(rootNode, overlayNode);
+ for (let i=0; i < scripts.length; i++){
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Loading: " + scripts[i]);
+ try {
+ Services.scriptloader.loadSubScript(scripts[i], window);
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+ let omscopename = overlayNode.hasAttribute("omscope") ? overlayNode.getAttribute("omscope") : null;
+ let omscope = omscopename ? window[omscopename] : window;
+
+ let inject = true;
+ if (omscope.hasOwnProperty("onBeforeInject")) {
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Executing " + (omscopename ? omscopename : "window") + ".onBeforeInject()");
+ try {
+ inject = omscope.onBeforeInject(window);
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+ if (inject) {
+ if (this.options.verbose>2) Services.console.logStringMessage("[OverlayManager] Injecting: " + overlay);
+ window.injectedOverlays.push(overlay);
+
+ //get urls of stylesheets to add preloaded files
+ let styleSheetUrls = this.getStyleSheetUrls(rootNode);
+ for (let i=0; i<styleSheetUrls.length; i++) {
+ let namespace = overlayNode.lookupNamespaceURI("html");
+ let element = window.document.createElementNS(namespace, "style");
+ element.id = styleSheetUrls[i];
+ element.textContent = this.stylesheets[styleSheetUrls[i]];
+ window.document.documentElement.appendChild(element);
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Stylesheet: " + styleSheetUrls[i]);
+ }
+
+ this.insertXulOverlay(window, overlayNode.children);
+ if (omscope.hasOwnProperty("onInject")) {
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Executing " + (omscopename ? omscopename : "window") + ".onInject()");
+ try {
+ omscope.onInject(window);
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+ // add to injectCounter
+ return true;
+ }
+ }
+ }
+
+ // nothing injected, do not add to inject counter
+ return false;
+ };
+
+ this.removeOverlay = function (window, overlay) {
+ if (!window.hasOwnProperty("injectedOverlays")) window.injectedOverlays = [];
+
+ if (!window.injectedOverlays.includes(overlay)) {
+ if (this.options.verbose>2) Services.console.logStringMessage("[OverlayManager] NOT Removing: " + overlay);
+ return;
+ }
+
+ if (this.options.verbose>2) Services.console.logStringMessage("[OverlayManager] Removing: " + overlay);
+ window.injectedOverlays = window.injectedOverlays.filter(e => (e != overlay));
+
+ let rootNode = this.overlays[overlay];
+ if (rootNode) {
+ let overlayNode = rootNode.documentElement;
+ if (overlayNode) {
+ let omscopename = overlayNode.hasAttribute("omscope") ? overlayNode.getAttribute("omscope") : null;
+ let omscope = omscopename ? window[omscopename] : window;
+
+ if (omscope.hasOwnProperty("onRemove")) {
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Executing " + (omscopename ? omscopename : "window") + ".onRemove()");
+ try {
+ omscope.onRemove(window);
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+ this.removeXulOverlay(window, overlayNode.children);
+ }
+
+ //get urls of stylesheets to remove styte tag
+ let styleSheetUrls = this.getStyleSheetUrls(rootNode);
+ for (let i=0; i<styleSheetUrls.length; i++) {
+ let element = window.document.getElementById(styleSheetUrls[i]);
+ if (element) {
+ element.parentNode.removeChild(element);
+ }
+ }
+ }
+ };
+
+
+
+
+
+
+
+
+
+
+
+ this.getStyleSheetUrls = function (rootNode) {
+ let sheetsIterator = rootNode.evaluate("processing-instruction('xml-stylesheet')", rootNode, null, 0, null); //PathResult.ANY_TYPE = 0
+ let urls = [];
+
+ let sheet;
+ while (sheet = sheetsIterator.iterateNext()) { //returns object XMLStylesheetProcessingInstruction]
+ let attr=sheet.data.split(" ");
+ for (let a=0; a < attr.length; a++) {
+ if (attr[a].startsWith("href=")) urls.push(attr[a].substring(6,attr[a].length-1));
+ }
+ }
+ return urls;
+ };
+
+ this.getScripts = function(rootNode, overlayNode) {
+ let nodeIterator = rootNode.evaluate("./script", overlayNode, null, 0, null); //PathResult.ANY_TYPE = 0
+ let scripts = [];
+
+ let node;
+ while (node = nodeIterator.iterateNext()) {
+ if (node.hasAttribute("src") && node.hasAttribute("type") && node.getAttribute("type").toLowerCase().includes("javascript")) {
+ scripts.push(node.getAttribute("src"));
+ }
+ }
+ return scripts;
+ };
+
+
+
+
+
+
+
+
+
+
+ this.createXulElement = function (window, node, forcedNodeName = null) {
+ //check for namespace
+ let typedef = forcedNodeName ? forcedNodeName.split(":") : node.nodeName.split(":");
+ if (typedef.length == 2) typedef[0] = node.lookupNamespaceURI(typedef[0]);
+
+ let CE = {}
+ if (node.attributes && node.attributes.getNamedItem("is")) {
+ for (let i=0; i <node.attributes.length; i++) {
+ if (node.attributes[i].name == "is") {
+ CE = { "is" : node.attributes[i].value };
+ break;
+ }
+ }
+ }
+
+ let element = (typedef.length==2) ? window.document.createElementNS(typedef[0], typedef[1]) : window.document.createXULElement(typedef[0], CE);
+ if (node.attributes) {
+ for (let i=0; i <node.attributes.length; i++) {
+ element.setAttribute(node.attributes[i].name, node.attributes[i].value);
+ }
+ }
+
+ //add text child nodes as textContent
+ if (node.hasChildNodes) {
+ let textContent = "";
+ for (let child of node.childNodes) {
+ if (child.nodeType == "3") {
+ textContent += child.nodeValue;
+ }
+ }
+ if (textContent) element.textContent = textContent
+ }
+
+ return element;
+ };
+
+ this.insertXulOverlay = function (window, nodes, parentElement = null) {
+ /*
+ The passed nodes value could be an entire window.document in a single node (type 9) or a
+ single element node (type 1) as returned by getElementById. It could however also
+ be an array of nodes as returned by getElementsByTagName or a nodeList as returned
+ by childNodes. In that case node.length is defined.
+ */
+ let nodeList = [];
+ if (nodes.length === undefined) nodeList.push(nodes);
+ else nodeList = nodes;
+
+ // nodelist contains all childs
+ for (let node of nodeList) {
+ let element = null;
+ let hookMode = null;
+ let hookName = null;
+ let hookElement = null;
+
+ if (node.nodeName == "script" && node.hasAttribute("src")) {
+ //skip, since they are handled by getScripts()
+ } else if (node.nodeType == 1) {
+
+ if (!parentElement) { //misleading: if it does not have a parentElement, it is a top level element
+ //Adding top level elements without id is not allowed, because we need to be able to remove them!
+ if (!node.hasAttribute("id")) {
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: A top level <" + node.nodeName+ "> element does not have an ID. Skipped");
+ continue;
+ }
+
+ //check for inline script tags
+ if (node.nodeName == "script") {
+ let element = this.createXulElement(window, node, "html:script"); //force as html:script
+ window.document.documentElement.appendChild(element);
+ continue;
+ }
+
+ //check for inline style
+ if (node.nodeName == "style") {
+ let element = this.createXulElement(window, node, "html:style"); //force as html:style
+ window.document.documentElement.appendChild(element);
+ continue;
+ }
+
+ if (node.hasAttribute("appendto")) hookMode = "appendto";
+ if (node.hasAttribute("insertbefore")) hookMode ="insertbefore";
+ if (node.hasAttribute("insertafter")) hookMode = "insertafter";
+
+ if (hookMode) {
+ hookName = node.getAttribute(hookMode);
+ hookElement = window.document.getElementById(hookName);
+
+ if (!hookElement) {
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: The hook element <"+hookName+"> of top level overlay element <"+ node.nodeName+"> does not exist. Skipped");
+ continue;
+ }
+ } else {
+ hookMode = "appendto";
+ hookName = "ROOT";
+ hookElement = window.document.documentElement;
+ }
+ }
+
+ element = this.createXulElement(window, node);
+ if (node.hasChildNodes) this.insertXulOverlay(window, node.children, element);
+
+ if (parentElement) {
+ // this is a child level XUL element which needs to be added to to its parent
+ parentElement.appendChild(element);
+ } else {
+ // this is a toplevel element, which needs to be added at insertafter or insertbefore
+ switch (hookMode) {
+ case "appendto":
+ hookElement.appendChild(element);
+ break;
+ case "insertbefore":
+ hookElement.parentNode.insertBefore(element, hookElement);
+ break;
+ case "insertafter":
+ hookElement.parentNode.insertBefore(element, hookElement.nextSibling);
+ break;
+ default:
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: Top level overlay element <"+ node.nodeName+"> uses unknown hook type <"+hookMode+">. Skipped.");
+ continue;
+ }
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Adding <"+element.id+"> ("+element.tagName+") " + hookMode + " <" + hookName + ">");
+ }
+ }
+ }
+ };
+
+ this.removeXulOverlay = function (window, nodes, parentElement = null) {
+ //only scan toplevel elements and remove them
+ let nodeList = [];
+ if (nodes.length === undefined) nodeList.push(nodes);
+ else nodeList = nodes;
+
+ // nodelist contains all childs
+ for (let node of nodeList) {
+ let element = null;
+ switch(node.nodeType) {
+ case 1:
+ if (node.hasAttribute("id")) {
+ let element = window.document.getElementById(node.getAttribute("id"));
+ if (element) {
+ element.parentNode.removeChild(element);
+ }
+ }
+ break;
+ }
+ }
+ };
+
+
+
+
+
+
+
+
+
+
+ //read file from within the XPI package
+ this.readChromeFile = function (aURL) {
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Reading file: " + aURL);
+ return new Promise((resolve, reject) => {
+ let uri = Services.io.newURI(aURL);
+ let channel = Services.io.newChannelFromURI(uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+
+ NetUtil.asyncFetch(channel, (inputStream, status) => {
+ if (!Components.isSuccessCode(status)) {
+ reject(status);
+ return;
+ }
+
+ try {
+ let data = NetUtil.readInputStreamToString(inputStream, inputStream.available());
+ resolve(data);
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ });
+ };
+
+}