/* * 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 = globalThis.Services || ChromeUtils.import( "resource://gre/modules/Services.jsm" ).Services; 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; i1) 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; i3) 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; i1) 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); } }); }); }; }