--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -636,17 +636,18 @@ var AddonTestUtils = {
var rdf = '<?xml version="1.0"?>\n';
rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' +
' xmlns:em="http://www.mozilla.org/2004/em-rdf#">\n';
rdf += '<Description about="urn:mozilla:install-manifest">\n';
let props = ["id", "version", "type", "internalName", "updateURL", "updateKey",
"optionsURL", "optionsType", "aboutURL", "iconURL", "icon64URL",
- "skinnable", "bootstrap", "unpack", "strictCompatibility", "multiprocessCompatible"];
+ "skinnable", "bootstrap", "unpack", "strictCompatibility",
+ "multiprocessCompatible", "hasEmbeddedWebExtension"];
rdf += this._writeProps(data, props);
rdf += this._writeLocaleStrings(data);
for (let platform of data.targetPlatforms || [])
rdf += escaped`<em:targetPlatform>${platform}</em:targetPlatform>\n`;
for (let app of data.targetApplications || []) {
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -54,16 +54,18 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "ProductAddonChecker",
"resource://gre/modules/addons/ProductAddonChecker.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
"resource://gre/modules/UpdateUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "isAddonPartOfE10SRollout",
"resource://gre/modules/addons/E10SAddonsRollout.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LegacyExtensionsUtils",
+ "resource://gre/modules/LegacyExtensionsUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "Blocklist",
"@mozilla.org/extensions/blocklist;1",
Ci.nsIBlocklistService);
XPCOMUtils.defineLazyServiceGetter(this,
"ChromeRegistry",
"@mozilla.org/chrome/chrome-registry;1",
"nsIChromeRegistry");
@@ -172,17 +174,17 @@ const XPI_PERMISSION =
const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest";
const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
const TOOLKIT_ID = "toolkit@mozilla.org";
const XPI_SIGNATURE_CHECK_PERIOD = 24 * 60 * 60;
-XPCOMUtils.defineConstant(this, "DB_SCHEMA", 17);
+XPCOMUtils.defineConstant(this, "DB_SCHEMA", 18);
const NOTIFICATION_TOOLBOXPROCESS_LOADED = "ToolboxProcessLoaded";
// Properties that exist in the install manifest
const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL",
"updateKey", "optionsURL", "optionsType", "aboutURL",
"iconURL", "icon64URL"];
const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
@@ -798,16 +800,17 @@ function EM_R(aProperty) {
function createAddonDetails(id, aAddon) {
return {
id: id || aAddon.id,
type: aAddon.type,
version: aAddon.version,
multiprocessCompatible: aAddon.multiprocessCompatible,
runInSafeMode: aAddon.runInSafeMode,
dependencies: aAddon.dependencies,
+ hasEmbeddedWebExtension: aAddon.hasEmbeddedWebExtension,
};
}
/**
* Converts an internal add-on type to the type presented through the API.
*
* @param aType
* The internal add-on type
@@ -1147,16 +1150,17 @@ function loadManifestFromRDF(aUri, aStre
addon.strictCompatibility = !(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) ||
getRDFProperty(ds, root, "strictCompatibility") == "true";
// Only read these properties for extensions.
if (addon.type == "extension") {
addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true";
addon.multiprocessCompatible = getRDFProperty(ds, root, "multiprocessCompatible") == "true";
+ addon.hasEmbeddedWebExtension = getRDFProperty(ds, root, "hasEmbeddedWebExtension") == "true";
if (addon.optionsType &&
addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG &&
addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE &&
addon.optionsType != AddonManager.OPTIONS_TYPE_TAB &&
addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_INFO) {
throw new Error("Install manifest specifies unknown type: " + addon.optionsType);
}
}
@@ -4887,16 +4891,27 @@ this.XPIProvider = {
};
if (aExtraParams) {
for (let key in aExtraParams) {
params[key] = aExtraParams[key];
}
}
+ if (aAddon.hasEmbeddedWebExtension) {
+ if (aMethod == "startup") {
+ const webExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor(params);
+ params.webExtension = {
+ startup: () => webExtension.startup(),
+ };
+ } else if (aMethod == "shutdown") {
+ LegacyExtensionsUtils.getEmbeddedExtensionFor(params).shutdown();
+ }
+ }
+
logger.debug("Calling bootstrap method " + aMethod + " on " + aAddon.id + " version " +
aAddon.version);
try {
method(params, aReason);
}
catch (e) {
logger.warn("Exception running bootstrap method " + aMethod + " on " + aAddon.id, e);
}
@@ -6932,16 +6947,17 @@ AddonInternal.prototype = {
/**
* @property {Array<string>} dependencies
* An array of bootstrapped add-on IDs on which this add-on depends.
* The add-on will remain appDisabled if any of the dependent
* add-ons is not installed and enabled.
*/
dependencies: Object.freeze([]),
+ hasEmbeddedWebExtension: false,
get selectedLocale() {
if (this._selectedLocale)
return this._selectedLocale;
let locale = Locale.findClosestLocale(this.locales);
this._selectedLocale = locale ? locale : this.defaultLocale;
return this._selectedLocale;
},
@@ -7249,16 +7265,20 @@ AddonWrapper.prototype = {
get __AddonInternal__() {
return AppConstants.DEBUG ? addonFor(this) : undefined;
},
get seen() {
return addonFor(this).seen;
},
+ get hasEmbeddedWebExtension() {
+ return addonFor(this).hasEmbeddedWebExtension;
+ },
+
markAsSeen: function() {
addonFor(this).seen = true;
XPIDatabase.saveChanges();
},
get type() {
return getExternalType(addonFor(this).type);
},
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -82,17 +82,17 @@ const PROP_JSON_FIELDS = ["id", "syncGUI
"optionsType", "aboutURL", "icons", "iconURL", "icon64URL",
"defaultLocale", "visible", "active", "userDisabled",
"appDisabled", "pendingUninstall", "descriptor", "installDate",
"updateDate", "applyBackgroundUpdates", "bootstrap",
"skinnable", "size", "sourceURI", "releaseNotesURI",
"softDisabled", "foreignInstall", "hasBinaryComponents",
"strictCompatibility", "locales", "targetApplications",
"targetPlatforms", "multiprocessCompatible", "signedState",
- "seen", "dependencies"];
+ "seen", "dependencies", "hasEmbeddedWebExtension"];
// Properties that should be migrated where possible from an old database. These
// shouldn't include properties that can be read directly from install.rdf files
// or calculated
const DB_MIGRATE_METADATA= ["installDate", "userDisabled", "softDisabled",
"sourceURI", "applyBackgroundUpdates",
"releaseNotesURI", "foreignInstall", "syncGUID"];
@@ -2151,16 +2151,17 @@ this.XPIDatabaseReconcile = {
if (currentAddon.bootstrap && currentAddon.active) {
XPIProvider.bootstrappedAddons[id] = {
version: currentAddon.version,
type: currentAddon.type,
descriptor: currentAddon._sourceBundle.persistentDescriptor,
multiprocessCompatible: currentAddon.multiprocessCompatible,
runInSafeMode: canRunInSafeMode(currentAddon),
dependencies: currentAddon.dependencies,
+ hasEmbeddedWebExtension: currentAddon.hasEmbeddedWebExtension,
};
}
if (currentAddon.active && currentAddon.internalName == XPIProvider.selectedSkin)
sawActiveTheme = true;
}
// Pass over the set of previously visible add-ons that have now gone away
--- a/toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm
@@ -7,17 +7,19 @@ function notify(event, originalMethod, d
event,
data: Object.assign({}, data, {
installPath: data.installPath.path,
resourceURI: data.resourceURI.spec,
}),
reason
};
- Services.obs.notifyObservers(null, "bootstrapmonitor-event", JSON.stringify(info));
+ let subject = {wrappedJSObject: {data}};
+
+ Services.obs.notifyObservers(subject, "bootstrapmonitor-event", JSON.stringify(info));
// If the bootstrap scope already declares a method call it
if (originalMethod)
originalMethod(data, reason);
}
// Allows a simple one-line bootstrap script:
// Components.utils.import("resource://xpcshelldata/bootstrapmonitor.jsm").monitor(this);
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -231,16 +231,27 @@ this.BootstrapMonitor = {
do_check_false(this.installed.has(id));
},
observe(subject, topic, data) {
let info = JSON.parse(data);
let id = info.data.id;
let installPath = new FileUtils.File(info.data.installPath);
+ if (subject && subject.wrappedJSObject) {
+ // NOTE: in some of the new tests, we need to received the real objects instead of
+ // their JSON representations, but most of the current tests expect intallPath
+ // and resourceURI to have been converted to strings.
+ const {installPath, resourceURI} = info.data;
+ info.data = Object.assign({}, subject.wrappedJSObject.data, {
+ installPath,
+ resourceURI,
+ });
+ }
+
// If this is the install event the add-ons shouldn't already be installed
if (info.event == "install") {
this.checkAddonNotInstalled(id);
this.installed.set(id, info);
for (let resolve of this.installPromises)
resolve();
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js
@@ -0,0 +1,235 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+BootstrapMonitor.init();
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49");
+startupManager();
+
+// NOTE: the following import needs to be called after the `createAppInfo`
+// or it will fail Extension.jsm internally imports AddonManager.jsm and
+// AddonManager will raise a ReferenceError exception because it tried to
+// access an undefined `Services.appinfo` object.
+const { Management } = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+
+const {
+ EmbeddedExtensionManager,
+ LegacyExtensionsUtils,
+} = Components.utils.import("resource://gre/modules/LegacyExtensionsUtils.jsm");
+
+// Wait the startup of the embedded webextension.
+function promiseWebExtensionStartup() {
+ return new Promise(resolve => {
+ let listener = (event, extension) => {
+ Management.off("startup", listener);
+ resolve(extension);
+ };
+
+ Management.on("startup", listener);
+ });
+}
+
+function promiseWebExtensionShutdown() {
+ return new Promise(resolve => {
+ let listener = (event, extension) => {
+ Management.off("shutdown", listener);
+ resolve(extension);
+ };
+
+ Management.on("shutdown", listener);
+ });
+}
+
+const BOOTSTRAP = String.raw`
+ Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
+`;
+
+const EMBEDDED_WEBEXT_MANIFEST = JSON.stringify({
+ name: "embedded webextension addon",
+ manifest_version: 2,
+ version: "1.0",
+});
+
+/**
+ * This test case checks that an addon with hasEmbeddedWebExtension set to true
+ * in its install.rdf gets the expected `embeddedWebExtension` object in the
+ * parameters of its bootstrap methods.
+ */
+add_task(function* run_embedded_webext_bootstrap() {
+ const ID = "embedded-webextension-addon2@tests.mozilla.org";
+
+ const xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ hasEmbeddedWebExtension: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ }, {
+ "bootstrap.js": BOOTSTRAP,
+ "webextension/manifest.json": EMBEDDED_WEBEXT_MANIFEST,
+ });
+
+ yield AddonManager.installTemporaryAddon(xpiFile);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+ equal(addon.hasEmbeddedWebExtension, true,
+ "Got the expected hasEmbeddedWebExtension value");
+
+ // Check that the addon has been installed and started.
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+
+ let installInfo = BootstrapMonitor.installed.get(ID);
+ ok(!("webExtension" in installInfo.data),
+ "No webExtension property is expected in the install bootstrap method params");
+
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ let startupInfo = BootstrapMonitor.started.get(ID);
+
+ ok(("webExtension" in startupInfo.data),
+ "Got an webExtension property in the startup bootstrap method params");
+
+ ok(("startup" in startupInfo.data.webExtension),
+ "Got the expected 'startup' property in the webExtension object");
+
+ const waitForWebExtensionStartup = promiseWebExtensionStartup();
+
+ const embeddedAPI = yield startupInfo.data.webExtension.startup();
+
+ // WebExtension startup should have been fully resolved.
+ yield waitForWebExtensionStartup;
+
+ Assert.deepEqual(
+ Object.keys(embeddedAPI.browser.runtime).sort(),
+ ["onConnect", "onMessage"],
+ `Got the expected 'runtime' in the 'browser' API object`
+ );
+
+ // Uninstall the addon and wait that the embedded webextension has been stopped and
+ // test the params of the shutdown and uninstall bootstrap method.
+ let waitForWebExtensionShutdown = promiseWebExtensionShutdown();
+ let waitUninstall = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitForWebExtensionShutdown;
+ yield waitUninstall;
+
+ BootstrapMonitor.checkAddonNotStarted(ID, "1.0");
+
+ let shutdownInfo = BootstrapMonitor.stopped.get(ID);
+ ok(!("webExtension" in shutdownInfo.data),
+ "No webExtension property is expected in the shutdown bootstrap method params");
+
+ let uninstallInfo = BootstrapMonitor.uninstalled.get(ID);
+ ok(!("webExtension" in uninstallInfo.data),
+ "No webExtension property is expected in the uninstall bootstrap method params");
+});
+
+/**
+ * This test case checks that an addon with hasEmbeddedWebExtension can be reloaded
+ * without raising unexpected exceptions due to race conditions.
+ */
+add_task(function* reload_embedded_webext_bootstrap() {
+ const ID = "embedded-webextension-addon2@tests.mozilla.org";
+
+ // No embedded webextension should be currently around.
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
+ "No embedded extension instance should be tracked here");
+
+ const xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ hasEmbeddedWebExtension: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ }, {
+ "bootstrap.js": BOOTSTRAP,
+ "webextension/manifest.json": EMBEDDED_WEBEXT_MANIFEST,
+ });
+
+ yield AddonManager.installTemporaryAddon(xpiFile);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+ equal(addon.isActive, true, "The Addon is active");
+ equal(addon.appDisabled, false, "The addon is not app disabled");
+ equal(addon.userDisabled, false, "The addon is not user disabled");
+
+ // Check that the addon has been installed and started.
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ // Only one embedded extension.
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
+ "Got the expected number of tracked extension instances");
+
+ const embeddedWebExtension = EmbeddedExtensionManager.embeddedExtensionsByAddonId.get(ID);
+
+ let startupInfo = BootstrapMonitor.started.get(ID);
+ yield startupInfo.data.webExtension.startup();
+
+ const waitForAddonDisabled = promiseAddonEvent("onDisabled");
+ addon.userDisabled = true;
+ yield waitForAddonDisabled;
+
+ // No embedded webextension should be currently around.
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
+ "No embedded extension instance should be tracked here");
+
+ const waitForAddonEnabled = promiseAddonEvent("onEnabled");
+ addon.userDisabled = false;
+ yield waitForAddonEnabled;
+
+ // Only one embedded extension.
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
+ "Got the expected number of tracked extension instances");
+
+ const embeddedWebExtensionAfterEnabled = EmbeddedExtensionManager.embeddedExtensionsByAddonId.get(ID);
+ notEqual(embeddedWebExtensionAfterEnabled, embeddedWebExtension,
+ "Got a new EmbeddedExtension instance after the addon has been disabled and then enabled");
+
+ startupInfo = BootstrapMonitor.started.get(ID);
+ yield startupInfo.data.webExtension.startup();
+
+ const waitForReinstalled = promiseAddonEvent("onInstalled");
+ addon.reload();
+ yield waitForReinstalled;
+
+ // No leaked embedded extension after the previous reloads.
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
+ "Got the expected number of tracked extension instances");
+
+ const embeddedWebExtensionAfterReload = EmbeddedExtensionManager.embeddedExtensionsByAddonId.get(ID);
+ notEqual(embeddedWebExtensionAfterReload, embeddedWebExtensionAfterEnabled,
+ "Got a new EmbeddedExtension instance after the addon has been reloaded");
+
+ startupInfo = BootstrapMonitor.started.get(ID);
+ yield startupInfo.data.webExtension.startup();
+
+ // Uninstall the test addon
+ let waitUninstalled = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitUninstalled;
+
+ // No leaked embedded extension after uninstalling.
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
+ "No embedded extension instance should be tracked after the addon uninstall");
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
@@ -310,16 +310,19 @@ run-sequentially = Uses global XCurProcD
skip-if = appname == "thunderbird"
tags = webextensions
[test_webextension.js]
skip-if = appname == "thunderbird"
tags = webextensions
[test_webextension_install.js]
skip-if = appname == "thunderbird"
tags = webextensions
+[test_webextension_embedded.js]
+skip-if = appname == "thunderbird"
+tags = webextensions
[test_bootstrap_globals.js]
[test_bug1180901_2.js]
skip-if = os != "win"
[test_bug1180901.js]
skip-if = os != "win"
[test_e10s_restartless.js]
[test_switch_os.js]
# Bug 1246231