/* 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"; const Cu = Components.utils; const Cc = Components.classes; const Ci = Components.interfaces; this.EXPORTED_SYMBOLS = ["UserCustomizations"]; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/AppsUtils.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "ppmm", "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster"); XPCOMUtils.defineLazyServiceGetter(this, "cpmm", "@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender"); XPCOMUtils.defineLazyServiceGetter(this, "console", "@mozilla.org/consoleservice;1", "nsIConsoleService"); /** * Customization scripts and CSS stylesheets can be specified in an * application manifest with the following syntax: * "customizations": [ * { * "filter": "http://youtube.com", * "css": ["file1.css", "file2.css"], * "scripts": ["script1.js", "script2.js"] * } * ] */ function debug(aMsg) { if (!UserCustomizations._debug) { return; } dump("-*-*- UserCustomizations (" + (UserCustomizations._inParent ? "parent" : "child") + "): " + aMsg + "\n"); } function log(aStr) { console.logStringMessage(aStr); } this.UserCustomizations = { _debug: false, _items: [], _loaded : {}, // Keep track per manifestURL of css and scripts loaded. _windows: null, // Set of currently opened windows. _enabled: false, _addItem: function(aItem) { debug("_addItem: " + uneval(aItem)); this._items.push(aItem); if (this._inParent) { ppmm.broadcastAsyncMessage("UserCustomizations:Add", [aItem]); } }, _removeItem: function(aHash) { debug("_removeItem: " + aHash); let index = -1; this._items.forEach((script, pos) => { if (script.hash == aHash ) { index = pos; } }); if (index != -1) { this._items.splice(index, 1); } if (this._inParent) { ppmm.broadcastAsyncMessage("UserCustomizations:Remove", aHash); } }, register: function(aManifest, aApp) { debug("Starting customization registration for " + aApp.manifestURL); if (!this._enabled || !aApp.enabled || aApp.role != "addon") { debug("Rejecting registration (global enabled=" + this._enabled + ") (app role=" + aApp.role + ", enabled=" + aApp.enabled + ")"); debug(uneval(aApp)); return; } let customizations = aManifest.customizations; if (customizations === undefined || !Array.isArray(customizations)) { return; } let base = Services.io.newURI(aApp.origin, null, null); customizations.forEach(item => { // The filter property is mandatory. if (!item.filter || (typeof item.filter !== "string")) { log("Mandatory filter property not found in this customization item: " + uneval(item) + " in " + aApp.manifestURL); return; } // Create a new object with resolved urls and a hash that we reuse to // remove items. let custom = { filter: item.filter, status: aApp.appStatus, manifestURL: aApp.manifestURL, css: [], scripts: [] }; custom.hash = AppsUtils.computeObjectHash(item); if (item.css && Array.isArray(item.css)) { item.css.forEach((css) => { custom.css.push(base.resolve(css)); }); } if (item.scripts && Array.isArray(item.scripts)) { item.scripts.forEach((script) => { custom.scripts.push(base.resolve(script)); }); } this._addItem(custom); }); this._updateAllWindows(); }, _updateAllWindows: function() { debug("UpdateWindows"); if (this._inParent) { ppmm.broadcastAsyncMessage("UserCustomizations:UpdateWindows", {}); } // Inject in all currently opened windows. this._windows.forEach(this._injectInWindow.bind(this)); }, unregister: function(aManifest, aApp) { if (!this._enabled) { return; } debug("Starting customization unregistration for " + aApp.manifestURL); let customizations = aManifest.customizations; if (customizations === undefined || !Array.isArray(customizations)) { return; } customizations.forEach(item => { this._removeItem(AppsUtils.computeObjectHash(item)); }); this._unloadForManifestURL(aApp.manifestURL); }, _unloadForManifestURL: function(aManifestURL) { debug("_unloadForManifestURL " + aManifestURL); if (this._inParent) { ppmm.broadcastAsyncMessage("UserCustomizations:Unload", aManifestURL); } if (!this._loaded[aManifestURL]) { return; } if (this._loaded[aManifestURL].scripts && this._loaded[aManifestURL].scripts.length > 0) { // We can't rollback script changes, so don't even try to unload in this // situation. return; } this._loaded[aManifestURL].css.forEach(aItem => { try { debug("unloading " + aItem.uri.spec); let utils = aItem.window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); utils.removeSheet(aItem.uri, Ci.nsIDOMWindowUtils.AUTHOR_SHEET); } catch(e) { log("Error unloading stylesheet " + aItem.uri.spec + " : " + e); } }); this._loaded[aManifestURL] = null; }, _injectItem: function(aWindow, aItem, aInjected) { debug("Injecting item " + uneval(aItem) + " in " + aWindow.location.href); let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); let manifestURL = aItem.manifestURL; // Load the stylesheets only in this window. aItem.css.forEach(aCss => { if (aInjected.indexOf(aCss) !== -1) { debug("Skipping duplicated css: " + aCss); return; } let uri = Services.io.newURI(aCss, null, null); try { utils.loadSheet(uri, Ci.nsIDOMWindowUtils.AUTHOR_SHEET); if (!this._loaded[manifestURL]) { this._loaded[manifestURL] = { css: [], scripts: [] }; } this._loaded[manifestURL].css.push({ window: aWindow, uri: uri }); aInjected.push(aCss); } catch(e) { log("Error loading stylesheet " + aCss + " : " + e); } }); let sandbox; if (aItem.scripts.length > 0) { sandbox = Cu.Sandbox([aWindow], { wantComponents: false, sandboxPrototype: aWindow }); } // Load the scripts using a sandbox. aItem.scripts.forEach(aScript => { debug("Sandboxing " + aScript); if (aInjected.indexOf(aScript) !== -1) { debug("Skipping duplicated script: " + aScript); return; } try { let options = { target: sandbox, charset: "UTF-8", async: true } Services.scriptloader.loadSubScriptWithOptions(aScript, options); if (!this._loaded[manifestURL]) { this._loaded[manifestURL] = { css: [], scripts: [] }; } this._loaded[manifestURL].scripts.push({ sandbox: sandbox, uri: aScript }); aInjected.push(aScript); } catch(e) { log("Error sandboxing " + aScript + " : " + e); } }); // Makes sure we get rid of the sandbox. if (sandbox) { aWindow.addEventListener("unload", () => { Cu.nukeSandbox(sandbox); sandbox = null; }); } }, _injectInWindow: function(aWindow) { debug("_injectInWindow"); if (!aWindow || !aWindow.document) { return; } let principal = aWindow.document.nodePrincipal; debug("principal status: " + principal.appStatus); let href = aWindow.location.href; // The list of resources loaded in this window, used to filter out // duplicates. let injected = []; this._items.forEach((aItem) => { // We only allow customizations to apply to apps with an equal or lower // privilege level. if (principal.appStatus > aItem.status) { return; } let regexp = new RegExp(aItem.filter, "g"); if (regexp.test(href)) { this._injectItem(aWindow, aItem, injected); debug("Currently injected: " + injected.toString()); } }); }, observe: function(aSubject, aTopic, aData) { if (aTopic === "content-document-global-created") { let window = aSubject.QueryInterface(Ci.nsIDOMWindow); let href = window.location.href; if (!href || href == "about:blank") { return; } let id = window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils) .currentInnerWindowID; this._windows.set(id, window); debug("document created: " + href); this._injectInWindow(window); } else if (aTopic === "inner-window-destroyed") { let winId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; this._windows.delete(winId); } }, init: function() { this._enabled = false; try { this._enabled = Services.prefs.getBoolPref("dom.apps.customization.enabled"); } catch(e) {} if (!this._enabled) { return; } this._windows = new Map(); // Can't be a WeakMap because we need to enumerate. this._inParent = Cc["@mozilla.org/xre/runtime;1"] .getService(Ci.nsIXULRuntime) .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; debug("init"); Services.obs.addObserver(this, "content-document-global-created", /* ownsWeak */ false); Services.obs.addObserver(this, "inner-window-destroyed", /* ownsWeak */ false); if (this._inParent) { ppmm.addMessageListener("UserCustomizations:List", this); } else { cpmm.addMessageListener("UserCustomizations:Add", this); cpmm.addMessageListener("UserCustomizations:Remove", this); cpmm.addMessageListener("UserCustomizations:Unload", this); cpmm.addMessageListener("UserCustomizations:UpdateWindows", this); cpmm.sendAsyncMessage("UserCustomizations:List", {}); } }, receiveMessage: function(aMessage) { let name = aMessage.name; let data = aMessage.data; switch(name) { case "UserCustomizations:List": aMessage.target.sendAsyncMessage("UserCustomizations:Add", this._items); break; case "UserCustomizations:Add": data.forEach(this._addItem, this); break; case "UserCustomizations:Remove": this._removeItem(data); break; case "UserCustomizations:Unload": this._unloadForManifestURL(data); break; case "UserCustomizations:UpdateWindows": this._updateAllWindows(); break; } } } UserCustomizations.init();