diff options
Diffstat (limited to 'toolkit/components/webextensions/LegacyExtensionsUtils.jsm')
-rw-r--r-- | toolkit/components/webextensions/LegacyExtensionsUtils.jsm | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/toolkit/components/webextensions/LegacyExtensionsUtils.jsm b/toolkit/components/webextensions/LegacyExtensionsUtils.jsm new file mode 100644 index 000000000..7632548e3 --- /dev/null +++ b/toolkit/components/webextensions/LegacyExtensionsUtils.jsm @@ -0,0 +1,250 @@ +/* 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 = ["LegacyExtensionsUtils"]; + +/* exported LegacyExtensionsUtils, LegacyExtensionContext */ + +/** + * This file exports helpers for Legacy Extensions that want to embed a webextensions + * and exchange messages with the embedded WebExtension. + */ + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Extension", + "resource://gre/modules/Extension.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +Cu.import("resource://gre/modules/ExtensionChild.jsm"); +Cu.import("resource://gre/modules/ExtensionCommon.jsm"); + +var { + BaseContext, +} = ExtensionCommon; + +var { + Messenger, +} = ExtensionChild; + +/** + * Instances created from this class provide to a legacy extension + * a simple API to exchange messages with a webextension. + */ +var LegacyExtensionContext = class extends BaseContext { + /** + * Create a new LegacyExtensionContext given a target Extension instance. + * + * @param {Extension} targetExtension + * The webextension instance associated with this context. This will be the + * instance of the newly created embedded webextension when this class is + * used through the EmbeddedWebExtensionsUtils. + */ + constructor(targetExtension) { + super("legacy_extension", targetExtension); + + // Legacy Extensions (xul overlays, bootstrap restartless and Addon SDK) + // runs with a systemPrincipal. + let addonPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + Object.defineProperty( + this, "principal", + {value: addonPrincipal, enumerable: true, configurable: true} + ); + + let cloneScope = Cu.Sandbox(this.principal, {}); + Cu.setSandboxMetadata(cloneScope, {addonId: targetExtension.id}); + Object.defineProperty( + this, "cloneScope", + {value: cloneScope, enumerable: true, configurable: true, writable: true} + ); + + let sender = {id: targetExtension.uuid}; + let filter = {extensionId: targetExtension.id}; + // Legacy addons live in the main process. Messages from other addons are + // Messages from WebExtensions are sent to the main process and forwarded via + // the parent process manager to the legacy extension. + this.messenger = new Messenger(this, [Services.cpmm], sender, filter); + + this.api = { + browser: { + runtime: { + onConnect: this.messenger.onConnect("runtime.onConnect"), + onMessage: this.messenger.onMessage("runtime.onMessage"), + }, + }, + }; + } + + /** + * This method is called when the extension shuts down or is unloaded, + * and it nukes the cloneScope sandbox, if any. + */ + unload() { + if (this.unloaded) { + throw new Error("Error trying to unload LegacyExtensionContext twice."); + } + super.unload(); + Cu.nukeSandbox(this.cloneScope); + this.cloneScope = null; + } +}; + +var EmbeddedExtensionManager; + +/** + * Instances of this class are used internally by the exported EmbeddedWebExtensionsUtils + * to manage the embedded webextension instance and the related LegacyExtensionContext + * instance used to exchange messages with it. + */ +class EmbeddedExtension { + /** + * Create a new EmbeddedExtension given the add-on id and the base resource URI of the + * container add-on (the webextension resources will be loaded from the "webextension/" + * subdir of the base resource URI for the legacy extension add-on). + * + * @param {Object} containerAddonParams + * An object with the following properties: + * @param {string} containerAddonParams.id + * The Add-on id of the Legacy Extension which will contain the embedded webextension. + * @param {nsIURI} containerAddonParams.resourceURI + * The nsIURI of the Legacy Extension container add-on. + */ + constructor({id, resourceURI}) { + this.addonId = id; + this.resourceURI = resourceURI; + + // Setup status flag. + this.started = false; + } + + /** + * Start the embedded webextension. + * + * @returns {Promise<LegacyContextAPI>} A promise which resolve to the API exposed to the + * legacy context. + */ + startup() { + if (this.started) { + return Promise.reject(new Error("This embedded extension has already been started")); + } + + // Setup the startup promise. + this.startupPromise = new Promise((resolve, reject) => { + let embeddedExtensionURI = Services.io.newURI("webextension/", null, this.resourceURI); + + // This is the instance of the WebExtension embedded in the hybrid add-on. + this.extension = new Extension({ + id: this.addonId, + resourceURI: embeddedExtensionURI, + }); + + // This callback is register to the "startup" event, emitted by the Extension instance + // after the extension manifest.json has been loaded without any errors, but before + // starting any of the defined contexts (which give the legacy part a chance to subscribe + // runtime.onMessage/onConnect listener before the background page has been loaded). + const onBeforeStarted = () => { + this.extension.off("startup", onBeforeStarted); + + // Resolve the startup promise and reset the startupError. + this.started = true; + this.startupPromise = null; + + // Create the legacy extension context, the legacy container addon + // needs to use it before the embedded webextension startup, + // because it is supposed to be used during the legacy container startup + // to subscribe its message listeners (which are supposed to be able to + // receive any message that the embedded part can try to send to it + // during its startup). + this.context = new LegacyExtensionContext(this.extension); + + // Destroy the LegacyExtensionContext cloneScope when + // the embedded webextensions is unloaded. + this.extension.callOnClose({ + close: () => { + this.context.unload(); + }, + }); + + // resolve startupPromise to execute any pending shutdown that has been + // chained to it. + resolve(this.context.api); + }; + + this.extension.on("startup", onBeforeStarted); + + // Run ambedded extension startup and catch any error during embedded extension + // startup. + this.extension.startup().catch((err) => { + this.started = false; + this.startupPromise = null; + this.extension.off("startup", onBeforeStarted); + + reject(err); + }); + }); + + return this.startupPromise; + } + + /** + * Shuts down the embedded webextension. + * + * @returns {Promise<void>} a promise that is resolved when the shutdown has been done + */ + shutdown() { + EmbeddedExtensionManager.untrackEmbeddedExtension(this); + + // If there is a pending startup, wait to be completed and then shutdown. + if (this.startupPromise) { + return this.startupPromise.then(() => { + this.extension.shutdown(); + }); + } + + // Run shutdown now if the embedded webextension has been correctly started + if (this.extension && this.started && !this.extension.hasShutdown) { + this.extension.shutdown(); + } + + return Promise.resolve(); + } +} + +// Keep track on the created EmbeddedExtension instances and destroy +// them when their container addon is going to be disabled or uninstalled. +EmbeddedExtensionManager = { + // Map of the existent EmbeddedExtensions instances by addon id. + embeddedExtensionsByAddonId: new Map(), + + untrackEmbeddedExtension(embeddedExtensionInstance) { + // Remove this instance from the tracked embedded extensions + let id = embeddedExtensionInstance.addonId; + if (this.embeddedExtensionsByAddonId.get(id) == embeddedExtensionInstance) { + this.embeddedExtensionsByAddonId.delete(id); + } + }, + + getEmbeddedExtensionFor({id, resourceURI}) { + let embeddedExtension = this.embeddedExtensionsByAddonId.get(id); + + if (!embeddedExtension) { + embeddedExtension = new EmbeddedExtension({id, resourceURI}); + // Keep track of the embedded extension instance. + this.embeddedExtensionsByAddonId.set(id, embeddedExtension); + } + + return embeddedExtension; + }, +}; + +this.LegacyExtensionsUtils = { + getEmbeddedExtensionFor: (addon) => { + return EmbeddedExtensionManager.getEmbeddedExtensionFor(addon); + }, +}; |