Mypal/toolkit/components/webextensions/LegacyExtensionsUtils.jsm

251 lines
8.4 KiB
JavaScript

/* 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.id};
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);
},
};