Permalink
Cannot retrieve contributors at this time
514 lines (406 sloc)
17.9 KB
| /* | |
| * 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, | |
| Components.interfaces.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_DATA_INHERITS, | |
| Components.interfaces.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); | |
| } | |
| }); | |
| }); | |
| }; | |
| } |