Bug 1269342 - Integrate LegacyExtensionsUtils helpers into XPIProvider. r?aswan,kmag draft
authorLuca Greco <lgreco@mozilla.com>
Sun, 11 Sep 2016 15:37:54 +0200
changeset 412462 28015af78e9da0257bcee789d31dfb1e00acf864
parent 412448 7e873393cc11d326338779e5a3ed2da031e30936
child 530990 defbbcbe05845c0a3c56aa961ea0dbf6238d7e59
push id29177
push userluca.greco@alcacoop.it
push dateSun, 11 Sep 2016 16:58:33 +0000
reviewersaswan, kmag
bugs1269342
milestone51.0a1
Bug 1269342 - Integrate LegacyExtensionsUtils helpers into XPIProvider. r?aswan,kmag MozReview-Commit-ID: Iw47S7OMP5S
toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
--- 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