summaryrefslogtreecommitdiffstats
path: root/toolkit/components/webextensions/ExtensionManagement.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/webextensions/ExtensionManagement.jsm')
-rw-r--r--toolkit/components/webextensions/ExtensionManagement.jsm321
1 files changed, 321 insertions, 0 deletions
diff --git a/toolkit/components/webextensions/ExtensionManagement.jsm b/toolkit/components/webextensions/ExtensionManagement.jsm
new file mode 100644
index 000000000..324c5b71b
--- /dev/null
+++ b/toolkit/components/webextensions/ExtensionManagement.jsm
@@ -0,0 +1,321 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["ExtensionManagement"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
+ "resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
+
+XPCOMUtils.defineLazyGetter(this, "UUIDMap", () => {
+ let {UUIDMap} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ return UUIDMap;
+});
+
+/*
+ * This file should be kept short and simple since it's loaded even
+ * when no extensions are running.
+ */
+
+// Keep track of frame IDs for content windows. Mostly we can just use
+// the outer window ID as the frame ID. However, the API specifies
+// that top-level windows have a frame ID of 0. So we need to keep
+// track of which windows are top-level. This code listens to messages
+// from ExtensionContent to do that.
+var Frames = {
+ // Window IDs of top-level content windows.
+ topWindowIds: new Set(),
+
+ init() {
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ return;
+ }
+
+ Services.mm.addMessageListener("Extension:TopWindowID", this);
+ Services.mm.addMessageListener("Extension:RemoveTopWindowID", this, true);
+ },
+
+ isTopWindowId(windowId) {
+ return this.topWindowIds.has(windowId);
+ },
+
+ // Convert an outer window ID to a frame ID. An outer window ID of 0
+ // is invalid.
+ getId(windowId) {
+ if (this.isTopWindowId(windowId)) {
+ return 0;
+ }
+ if (windowId == 0) {
+ return -1;
+ }
+ return windowId;
+ },
+
+ // Convert an outer window ID for a parent window to a frame
+ // ID. Outer window IDs follow the same convention that
+ // |window.top.parent === window.top|. The API works differently,
+ // giving a frame ID of -1 for the the parent of a top-level
+ // window. This function handles the conversion.
+ getParentId(parentWindowId, windowId) {
+ if (parentWindowId == windowId) {
+ // We have a top-level window.
+ return -1;
+ }
+
+ // Not a top-level window. Just return the ID as normal.
+ return this.getId(parentWindowId);
+ },
+
+ receiveMessage({name, data}) {
+ switch (name) {
+ case "Extension:TopWindowID":
+ // FIXME: Need to handle the case where the content process
+ // crashes. Right now we leak its top window IDs.
+ this.topWindowIds.add(data.windowId);
+ break;
+
+ case "Extension:RemoveTopWindowID":
+ this.topWindowIds.delete(data.windowId);
+ break;
+ }
+ },
+};
+Frames.init();
+
+var APIs = {
+ apis: new Map(),
+
+ register(namespace, schema, script) {
+ if (this.apis.has(namespace)) {
+ throw new Error(`API namespace already exists: ${namespace}`);
+ }
+
+ this.apis.set(namespace, {schema, script});
+ },
+
+ unregister(namespace) {
+ if (!this.apis.has(namespace)) {
+ throw new Error(`API namespace does not exist: ${namespace}`);
+ }
+
+ this.apis.delete(namespace);
+ },
+};
+
+function getURLForExtension(id, path = "") {
+ let uuid = UUIDMap.get(id, false);
+ if (!uuid) {
+ Cu.reportError(`Called getURLForExtension on unmapped extension ${id}`);
+ return null;
+ }
+ return `moz-extension://${uuid}/${path}`;
+}
+
+// This object manages various platform-level issues related to
+// moz-extension:// URIs. It lives here so that it can be used in both
+// the parent and child processes.
+//
+// moz-extension URIs have the form moz-extension://uuid/path. Each
+// extension has its own UUID, unique to the machine it's installed
+// on. This is easier and more secure than using the extension ID,
+// since it makes it slightly harder to fingerprint for extensions if
+// each user uses different URIs for the extension.
+var Service = {
+ initialized: false,
+
+ // Map[uuid -> extension].
+ // extension can be an Extension (parent process) or BrowserExtensionContent (child process).
+ uuidMap: new Map(),
+
+ init() {
+ let aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService);
+ aps = aps.wrappedJSObject;
+ this.aps = aps;
+ aps.setExtensionURILoadCallback(this.extensionURILoadableByAnyone.bind(this));
+ aps.setExtensionURIToAddonIdCallback(this.extensionURIToAddonID.bind(this));
+ },
+
+ // Called when a new extension is loaded.
+ startupExtension(uuid, uri, extension) {
+ if (!this.initialized) {
+ this.initialized = true;
+ this.init();
+ }
+
+ // Create the moz-extension://uuid mapping.
+ let handler = Services.io.getProtocolHandler("moz-extension");
+ handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
+ handler.setSubstitution(uuid, uri);
+
+ this.uuidMap.set(uuid, extension);
+ this.aps.setAddonHasPermissionCallback(extension.id, extension.hasPermission.bind(extension));
+ this.aps.setAddonLoadURICallback(extension.id, this.checkAddonMayLoad.bind(this, extension));
+ this.aps.setAddonLocalizeCallback(extension.id, extension.localize.bind(extension));
+ this.aps.setAddonCSP(extension.id, extension.manifest.content_security_policy);
+ this.aps.setBackgroundPageUrlCallback(uuid, this.generateBackgroundPageUrl.bind(this, extension));
+ },
+
+ // Called when an extension is unloaded.
+ shutdownExtension(uuid) {
+ let extension = this.uuidMap.get(uuid);
+ this.uuidMap.delete(uuid);
+ this.aps.setAddonHasPermissionCallback(extension.id, null);
+ this.aps.setAddonLoadURICallback(extension.id, null);
+ this.aps.setAddonLocalizeCallback(extension.id, null);
+ this.aps.setAddonCSP(extension.id, null);
+ this.aps.setBackgroundPageUrlCallback(uuid, null);
+
+ let handler = Services.io.getProtocolHandler("moz-extension");
+ handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
+ handler.setSubstitution(uuid, null);
+ },
+
+ // Return true if the given URI can be loaded from arbitrary web
+ // content. The manifest.json |web_accessible_resources| directive
+ // determines this.
+ extensionURILoadableByAnyone(uri) {
+ let uuid = uri.host;
+ let extension = this.uuidMap.get(uuid);
+ if (!extension || !extension.webAccessibleResources) {
+ return false;
+ }
+
+ let path = uri.QueryInterface(Ci.nsIURL).filePath;
+ if (path.length > 0 && path[0] == "/") {
+ path = path.substr(1);
+ }
+ return extension.webAccessibleResources.matches(path);
+ },
+
+ // Checks whether a given extension can load this URI (typically via
+ // an XML HTTP request). The manifest.json |permissions| directive
+ // determines this.
+ checkAddonMayLoad(extension, uri) {
+ return extension.whiteListedHosts.matchesIgnoringPath(uri);
+ },
+
+ generateBackgroundPageUrl(extension) {
+ let background_scripts = extension.manifest.background &&
+ extension.manifest.background.scripts;
+ if (!background_scripts) {
+ return;
+ }
+ let html = "<!DOCTYPE html>\n<body>\n";
+ for (let script of background_scripts) {
+ script = script.replace(/"/g, "&quot;");
+ html += `<script src="${script}"></script>\n`;
+ }
+ html += "</body>\n</html>\n";
+ return "data:text/html;charset=utf-8," + encodeURIComponent(html);
+ },
+
+ // Finds the add-on ID associated with a given moz-extension:// URI.
+ // This is used to set the addonId on the originAttributes for the
+ // nsIPrincipal attached to the URI.
+ extensionURIToAddonID(uri) {
+ let uuid = uri.host;
+ let extension = this.uuidMap.get(uuid);
+ return extension ? extension.id : undefined;
+ },
+};
+
+// API Levels Helpers
+
+// Find the add-on associated with this document via the
+// principal's originAttributes. This value is computed by
+// extensionURIToAddonID, which ensures that we don't inject our
+// API into webAccessibleResources or remote web pages.
+function getAddonIdForWindow(window) {
+ return Cu.getObjectPrincipal(window).originAttributes.addonId;
+}
+
+const API_LEVELS = Object.freeze({
+ NO_PRIVILEGES: 0,
+ CONTENTSCRIPT_PRIVILEGES: 1,
+ FULL_PRIVILEGES: 2,
+});
+
+// Finds the API Level ("FULL_PRIVILEGES", "CONTENTSCRIPT_PRIVILEGES", "NO_PRIVILEGES")
+// with a given a window object.
+function getAPILevelForWindow(window, addonId) {
+ const {NO_PRIVILEGES, CONTENTSCRIPT_PRIVILEGES, FULL_PRIVILEGES} = API_LEVELS;
+
+ // Non WebExtension URLs and WebExtension URLs from a different extension
+ // has no access to APIs.
+ if (!addonId || getAddonIdForWindow(window) != addonId) {
+ return NO_PRIVILEGES;
+ }
+
+ // Extension pages running in the content process always defaults to
+ // "content script API level privileges".
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ return CONTENTSCRIPT_PRIVILEGES;
+ }
+
+ let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+
+ // Handling of ExtensionPages running inside sub-frames.
+ if (docShell.sameTypeParent) {
+ let parentWindow = docShell.sameTypeParent.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+
+ // The option page iframe embedded in the about:addons tab should have
+ // full API level privileges. (see Bug 1256282 for rationale)
+ let parentDocument = parentWindow.document;
+ let parentIsSystemPrincipal = Services.scriptSecurityManager
+ .isSystemPrincipal(parentDocument.nodePrincipal);
+ if (parentDocument.location.href == "about:addons" && parentIsSystemPrincipal) {
+ return FULL_PRIVILEGES;
+ }
+
+ // The addon iframes embedded in a addon page from with the same addonId
+ // should have the same privileges of the sameTypeParent.
+ // (see Bug 1258347 for rationale)
+ let parentSameAddonPrivileges = getAPILevelForWindow(parentWindow, addonId);
+ if (parentSameAddonPrivileges > NO_PRIVILEGES) {
+ return parentSameAddonPrivileges;
+ }
+
+ // In all the other cases, WebExtension URLs loaded into sub-frame UI
+ // will have "content script API level privileges".
+ // (see Bug 1214658 for rationale)
+ return CONTENTSCRIPT_PRIVILEGES;
+ }
+
+ // WebExtension URLs loaded into top frames UI could have full API level privileges.
+ return FULL_PRIVILEGES;
+}
+
+this.ExtensionManagement = {
+ startupExtension: Service.startupExtension.bind(Service),
+ shutdownExtension: Service.shutdownExtension.bind(Service),
+
+ registerAPI: APIs.register.bind(APIs),
+ unregisterAPI: APIs.unregister.bind(APIs),
+
+ getFrameId: Frames.getId.bind(Frames),
+ getParentFrameId: Frames.getParentId.bind(Frames),
+
+ getURLForExtension,
+
+ // exported API Level Helpers
+ getAddonIdForWindow,
+ getAPILevelForWindow,
+ API_LEVELS,
+
+ APIs,
+};