diff options
Diffstat (limited to 'toolkit/components/webextensions/ExtensionManagement.jsm')
-rw-r--r-- | toolkit/components/webextensions/ExtensionManagement.jsm | 321 |
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, """); + 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, +}; |