Bug 1262005 Rework how WebExtensions IDs are determined r?rhelmer draft
authorAndrew Swan <aswan@mozilla.com>
Wed, 06 Apr 2016 07:30:51 -0700
changeset 354429 d3e6a395fae8d90943c6d330826db1b7b286fc06
parent 352861 67ac40fb8f680ea5e03805552187ba1b5e8392a1
child 519003 844b0b3b5c367113cf1c79654baa238857869ba8
push id16077
push useraswan@mozilla.com
push dateWed, 20 Apr 2016 21:06:15 +0000
reviewersrhelmer
bugs1262005
milestone48.0a1
Bug 1262005 Rework how WebExtensions IDs are determined r?rhelmer MozReview-Commit-ID: 37EujfhGh0U
toolkit/components/extensions/schemas/manifest.json
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi
toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
toolkit/mozapps/extensions/test/xpcshell/test_webextension_paths.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
toolkit/mozapps/extensions/test/xpcshell/xpcshell-unpack.ini
--- a/toolkit/components/extensions/schemas/manifest.json
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -10,38 +10,32 @@
           "manifest_version": {
             "type": "integer",
             "minimum": 2,
             "maximum": 2
           },
 
           "applications": {
             "type": "object",
+            "optional": true,
             "properties": {
               "gecko": {
-                "type": "object",
-                "properties": {
-                  "id": { "$ref": "ExtensionID" },
-
-                  "update_url": {
-                    "type": "string",
-                    "format": "url",
-                    "optional": true
-                  },
+                "$ref": "FirefoxSpecificProperties",
+                "optional": true
+              }
+            }
+          },
 
-                  "strict_min_version": {
-                    "type": "string",
-                    "optional": true
-                  },
-
-                  "strict_max_version": {
-                    "type": "string",
-                    "optional": true
-                  }
-                }
+          "browser_specific_settings": {
+            "type": "object",
+            "optional": true,
+            "properties": {
+              "gecko": {
+                "$ref": "FirefoxSpecificProperties",
+                "optional": true
               }
             }
           },
 
           "name": {
             "type": "string",
             "optional": false,
             "preprocess": "localize"
@@ -201,16 +195,42 @@
           },
           {
             "type": "string",
             "pattern": "(?i)^[a-z0-9-._]*@[a-z0-9-._]+$"
           }
         ]
       },
       {
+        "id": "FirefoxSpecificProperties",
+        "type": "object",
+        "properties": {
+          "id": {
+            "$ref": "ExtensionID",
+            "optional": true
+          },
+
+          "update_url": {
+            "type": "string",
+            "format": "url",
+            "optional": true
+          },
+
+          "strict_min_version": {
+            "type": "string",
+            "optional": true
+          },
+
+          "strict_max_version": {
+            "type": "string",
+            "optional": true
+          }
+        }
+      },
+      {
         "id": "MatchPattern",
         "choices": [
           {
             "type": "string",
             "enum": ["<all_urls>"]
           },
           {
             "type": "string",
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -875,27 +875,33 @@ var loadManifestFromWebManifest = Task.a
   // Read the list of available locales, and pre-load messages for
   // all locales.
   let locales = yield extension.initAllLocales();
 
   // If there were any errors loading the extension, bail out now.
   if (extension.errors.length)
     throw new Error("Extension is invalid");
 
+  let bss = (manifest.browser_specific_settings && manifest.browser_specific_settings.gecko)
+      || (manifest.applications && manifest.applications.gecko) || {};
+  if (manifest.browser_specific_settings && manifest.applications) {
+    logger.warn("Ignoring applications property in manifest");
+  }
+
   let addon = new AddonInternal();
-  addon.id = manifest.applications.gecko.id;
+  addon.id = bss.id;
   addon.version = manifest.version;
   addon.type = "webextension";
   addon.unpack = false;
   addon.strictCompatibility = true;
   addon.bootstrap = true;
   addon.hasBinaryComponents = false;
   addon.multiprocessCompatible = true;
   addon.internalName = null;
-  addon.updateURL = manifest.applications.gecko.update_url;
+  addon.updateURL = bss.update_url;
   addon.updateKey = null;
   addon.optionsURL = null;
   addon.optionsType = null;
   addon.aboutURL = null;
 
   if (manifest.options_ui) {
     addon.optionsURL = extension.getURL(manifest.options_ui.page);
     if (manifest.options_ui.open_in_tab)
@@ -932,19 +938,19 @@ var loadManifestFromWebManifest = Task.a
 
   addon.defaultLocale = getLocale(extension.defaultLocale);
   addon.locales = Array.from(locales.keys(), getLocale);
 
   delete addon.defaultLocale.locales;
 
   addon.targetApplications = [{
     id: TOOLKIT_ID,
-    minVersion: (manifest.applications.gecko.strict_min_version ||
+    minVersion: (bss.strict_min_version ||
                  AddonManagerPrivate.webExtensionsMinPlatformVersion),
-    maxVersion: manifest.applications.gecko.strict_max_version || "*",
+    maxVersion: bss.strict_max_version || "*",
   }];
 
   addon.targetPlatforms = [];
   addon.userDisabled = false;
   addon.softDisabled = addon.blocklistState == Blocklist.STATE_SOFTBLOCKED;
 
   return addon;
 });
@@ -1309,24 +1315,39 @@ var loadManifestFromDir = Task.async(fun
   let file = getManifestFileForDir(aDir);
   if (!file) {
     throw new Error("Directory " + aDir.path + " does not contain a valid " +
                     "install manifest");
   }
 
   let uri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
 
-  let addon = file.leafName == FILE_WEB_MANIFEST ?
-              yield loadManifestFromWebManifest(uri) :
-              loadFromRDF(uri);
+  let addon;
+  if (file.leafName == FILE_WEB_MANIFEST) {
+    addon = yield loadManifestFromWebManifest(uri);
+    if (!addon.id) {
+      if (aInstallLocation == TemporaryInstallLocation) {
+        let id = Cc["@mozilla.org/uuid-generator;1"]
+            .getService(Ci.nsIUUIDGenerator)
+            .generateUUID().toString();
+        logger.info(`Generated temporary id ${id} for ${aDir.path}`);
+        addon.id = id;
+      } else {
+        addon.id = aDir.leafName;
+      }
+    }
+  } else {
+    addon = loadFromRDF(uri);
+  }
 
   addon._sourceBundle = aDir.clone();
   addon._installLocation = aInstallLocation;
   addon.size = getFileSize(aDir);
-  addon.signedState = yield verifyDirSignedState(aDir, addon);
+  addon.signedState = yield verifyDirSignedState(aDir, addon)
+    .then(({signedState}) => signedState);
   addon.appDisabled = !isUsableAddon(addon);
 
   defineSyncGUID(addon);
 
   return addon;
 });
 
 /**
@@ -1375,29 +1396,35 @@ var loadManifestFromZipReader = Task.asy
   let entry = getManifestEntryForZipReader(aZipReader);
   if (!entry) {
     throw new Error("File " + aZipReader.file.path + " does not contain a valid " +
                     "install manifest");
   }
 
   let uri = buildJarURI(aZipReader.file, entry);
 
-  let addon = entry == FILE_WEB_MANIFEST ?
+  let isWebExtension = (entry == FILE_WEB_MANIFEST);
+
+  let addon = isWebExtension ?
               yield loadManifestFromWebManifest(uri) :
               loadFromRDF(uri);
 
   addon._sourceBundle = aZipReader.file;
   addon._installLocation = aInstallLocation;
 
   addon.size = 0;
   let entries = aZipReader.findEntries(null);
   while (entries.hasMore())
     addon.size += aZipReader.getEntry(entries.getNext()).realSize;
 
-  addon.signedState = yield verifyZipSignedState(aZipReader.file, addon);
+  let {signedState, cert} = yield verifyZipSignedState(aZipReader.file, addon);
+  addon.signedState = signedState;
+  if (isWebExtension && !addon.id && cert) {
+    addon.id = cert.commonName;
+  }
   addon.appDisabled = !isUsableAddon(addon);
 
   defineSyncGUID(addon);
 
   return addon;
 });
 
 /**
@@ -1566,32 +1593,32 @@ function verifyZipSigning(aZip, aCertifi
 }
 
 /**
  * Returns the signedState for a given return code and certificate by verifying
  * it against the expected ID.
  */
 function getSignedStatus(aRv, aCert, aAddonID) {
   let expectedCommonName = aAddonID;
-  if (aAddonID.length > 64) {
+  if (aAddonID && aAddonID.length > 64) {
     let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                     createInstance(Ci.nsIScriptableUnicodeConverter);
     converter.charset = "UTF-8";
     let data = converter.convertToByteArray(aAddonID, {});
 
     let crypto = Cc["@mozilla.org/security/hash;1"].
                  createInstance(Ci.nsICryptoHash);
     crypto.init(Ci.nsICryptoHash.SHA256);
     crypto.update(data, data.length);
     expectedCommonName = getHashStringForCrypto(crypto);
   }
 
   switch (aRv) {
     case Cr.NS_OK:
-      if (expectedCommonName != aCert.commonName)
+      if (expectedCommonName && expectedCommonName != aCert.commonName)
         return AddonManager.SIGNEDSTATE_BROKEN;
 
       let hotfixID = Preferences.get(PREF_EM_HOTFIX_ID, undefined);
       if (hotfixID && hotfixID == aAddonID && Preferences.get(PREF_EM_CERT_CHECKATTRIBUTES, false)) {
         // The hotfix add-on has some more rigorous certificate checks
         try {
           CertUtils.validateCert(aCert,
                                  CertUtils.readCertPrefs(PREF_EM_HOTFIX_CERTS));
@@ -1654,32 +1681,40 @@ let gCertDB = Cc["@mozilla.org/security/
 /**
  * Verifies that a zip file's contents are all correctly signed by an
  * AMO-issued certificate
  *
  * @param  aFile
  *         the xpi file to check
  * @param  aAddon
  *         the add-on object to verify
- * @return a Promise that resolves to an AddonManager.SIGNEDSTATE_* constant.
+ * @return a Promise that resolves to an object with properties:
+ *         signedState: an AddonManager.SIGNEDSTATE_* constant
+ *         cert: an nsIX509Cert
  */
 function verifyZipSignedState(aFile, aAddon) {
   if (!shouldVerifySignedState(aAddon))
-    return Promise.resolve(AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+    return Promise.resolve({
+      signedState: AddonManager.SIGNEDSTATE_NOT_REQUIRED,
+      cert: null
+    });
 
   let root = Ci.nsIX509CertDB.AddonsPublicRoot;
   if (!REQUIRE_SIGNING && Preferences.get(PREF_XPI_SIGNATURES_DEV_ROOT, false))
     root = Ci.nsIX509CertDB.AddonsStageRoot;
 
   return new Promise(resolve => {
     let callback = {
       openSignedAppFileFinished: function(aRv, aZipReader, aCert) {
         if (aZipReader)
           aZipReader.close();
-        resolve(getSignedStatus(aRv, aCert, aAddon.id));
+        resolve({
+          signedState: getSignedStatus(aRv, aCert, aAddon.id),
+          cert: aCert
+        });
       }
     };
     // This allows the certificate DB to get the raw JS callback object so the
     // test code can pass through objects that XPConnect would reject.
     callback.wrappedJSObject = callback;
 
     gCertDB.openSignedAppFileAsync(root, aFile, callback);
   });
@@ -1688,30 +1723,38 @@ function verifyZipSignedState(aFile, aAd
 /**
  * Verifies that a directory's contents are all correctly signed by an
  * AMO-issued certificate
  *
  * @param  aDir
  *         the directory to check
  * @param  aAddon
  *         the add-on object to verify
- * @return a Promise that resolves to an AddonManager.SIGNEDSTATE_* constant.
+ * @return a Promise that resolves to an object with properties:
+ *         signedState: an AddonManager.SIGNEDSTATE_* constant
+ *         cert: an nsIX509Cert
  */
 function verifyDirSignedState(aDir, aAddon) {
   if (!shouldVerifySignedState(aAddon))
-    return Promise.resolve(AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+    return Promise.resolve({
+      signedState: AddonManager.SIGNEDSTATE_NOT_REQUIRED,
+      cert: null,
+    });
 
   let root = Ci.nsIX509CertDB.AddonsPublicRoot;
   if (!REQUIRE_SIGNING && Preferences.get(PREF_XPI_SIGNATURES_DEV_ROOT, false))
     root = Ci.nsIX509CertDB.AddonsStageRoot;
 
   return new Promise(resolve => {
     let callback = {
       verifySignedDirectoryFinished: function(aRv, aCert) {
-        resolve(getSignedStatus(aRv, aCert, aAddon.id));
+        resolve({
+          signedState: getSignedStatus(aRv, aCert, aAddon.id),
+          cert: null,
+        });
       }
     };
     // This allows the certificate DB to get the raw JS callback object so the
     // test code can pass through objects that XPConnect would reject.
     callback.wrappedJSObject = callback;
 
     gCertDB.verifySignedDirectoryAsync(root, aDir, callback);
   });
@@ -1723,19 +1766,19 @@ function verifyDirSignedState(aDir, aAdd
  *
  * @param  aBundle
  *         the nsIFile for the bundle to check, either a directory or zip file
  * @param  aAddon
  *         the add-on object to verify
  * @return a Promise that resolves to an AddonManager.SIGNEDSTATE_* constant.
  */
 function verifyBundleSignedState(aBundle, aAddon) {
-  if (aBundle.isFile())
-    return verifyZipSignedState(aBundle, aAddon);
-  return verifyDirSignedState(aBundle, aAddon);
+  let promise = aBundle.isFile() ? verifyZipSignedState(aBundle, aAddon)
+      : verifyDirSignedState(aBundle, aAddon);
+  return promise.then(({signedState}) => signedState);
 }
 
 /**
  * Replaces %...% strings in an addon url (update and updateInfo) with
  * appropriate values.
  *
  * @param  aAddon
  *         The AddonInternal representing the add-on
new file mode 100644
index 0000000000000000000000000000000000000000..6b4abaa6919af1df56045f2fe71fdb37218f1c0b
GIT binary patch
literal 4182
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
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
@@ -185,35 +185,16 @@ add_task(function* test_manifest_localiz
   addon = yield promiseAddonByID(ID);
 
   equal(addon.name, "Web Extensiøn foo ☹");
   equal(addon.description, "Descriptïon bar ☹ of add-on");
 
   addon.uninstall();
 });
 
-// Missing ID should cause a failure
-add_task(function*() {
-  writeWebManifestForExtension({
-    name: "Web Extension Name",
-    version: "1.0",
-    manifest_version: 2,
-  }, profileDir, ID);
-
-  yield promiseRestartManager();
-
-  let addon = yield promiseAddonByID(ID);
-  do_check_eq(addon, null);
-
-  let file = getFileForAddon(profileDir, ID);
-  do_check_false(file.exists());
-
-  yield promiseRestartManager();
-});
-
 // Missing version should cause a failure
 add_task(function*() {
   writeWebManifestForExtension({
     name: "Web Extension Name",
     manifest_version: 2,
     applications: {
       gecko: {
         id: ID
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
@@ -0,0 +1,112 @@
+function run_test() {
+  run_next_test();
+}
+
+let profileDir;
+add_task(function* setup() {
+  profileDir = gProfD.clone();
+  profileDir.append("extensions");
+
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+  startupManager();
+});
+
+const IMPLICIT_ID_XPI = "data/webext-implicit-id.xpi";
+const IMPLICIT_ID_ID = "webext_implicit_id@tests.mozilla.org";
+
+// webext-implicit-id.xpi has a minimal manifest with no
+// applications or browser_specific_settings, so its id comes
+// from its signature, which should be the ID constant defined below.
+add_task(function* test_implicit_id() {
+  let addon = yield promiseAddonByID(IMPLICIT_ID_ID);
+  do_check_eq(addon, null);
+
+  let xpifile = do_get_file(IMPLICIT_ID_XPI);
+  yield promiseInstallAllFiles([xpifile]);
+
+  addon = yield promiseAddonByID(IMPLICIT_ID_ID);
+  do_check_neq(addon, null);
+
+  addon.uninstall();
+});
+
+// We should also be able to install webext-implicit-id.xpi temporarily
+// and it should look just like the regular install (ie, the ID should
+// come from the signature)
+add_task(function* test_implicit_id_temp() {
+  let addon = yield promiseAddonByID(IMPLICIT_ID_ID);
+  do_check_eq(addon, null);
+
+  let xpifile = do_get_file(IMPLICIT_ID_XPI);
+  yield AddonManager.installTemporaryAddon(xpifile);
+
+  addon = yield promiseAddonByID(IMPLICIT_ID_ID);
+  do_check_neq(addon, null);
+
+  addon.uninstall();
+});
+
+// Test that we can get the ID from browser_specific_settings
+add_task(function* test_bss_id() {
+  const ID = "webext_bss_id@tests.mozilla.org";
+
+  let manifest = {
+    name: "bss test",
+    description: "test that ID may be in browser_specific_settings",
+    manifest_version: 2,
+    version: "1.0",
+
+    browser_specific_settings: {
+      gecko: {
+        id: ID
+      }
+    }
+  };
+
+  let addon = yield promiseAddonByID(ID);
+  do_check_eq(addon, null);
+
+  writeWebManifestForExtension(manifest, profileDir, ID);
+  yield promiseRestartManager();
+
+  addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+
+  addon.uninstall();
+});
+
+// Test that if we have IDs in both browser_specific_settings and applications,
+// that we prefer the ID in browser_specific_settings.
+add_task(function* test_two_ids() {
+  const GOOD_ID = "two_ids@tests.mozilla.org";
+  const BAD_ID = "i_am_obsolete@tests.mozilla.org";
+
+  let manifest = {
+    name: "two id test",
+    description: "test a web extension with ids in both applications and browser_specific_settings",
+    manifest_version: 2,
+    version: "1.0",
+
+    applications: {
+      gecko: {
+        id: BAD_ID
+      }
+    },
+
+    browser_specific_settings: {
+      gecko: {
+        id: GOOD_ID
+      }
+    }
+  }
+
+  writeWebManifestForExtension(manifest, profileDir, GOOD_ID);
+  yield promiseRestartManager();
+
+  let addon = yield promiseAddonByID(BAD_ID);
+  do_check_eq(addon, null);
+  addon = yield promiseAddonByID(GOOD_ID);
+  do_check_neq(addon, null);
+
+  addon.uninstall();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_paths.js
@@ -0,0 +1,55 @@
+function run_test() {
+  run_next_test();
+}
+
+let profileDir;
+add_task(function* setup() {
+  profileDir = gProfD.clone();
+  profileDir.append("extensions");
+
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+  startupManager();
+});
+
+// When installing an unpacked addon we derive the ID from the
+// directory name.  Make sure that if the directoy name is not a valid
+// addon ID that we reject it.
+add_task(function* test_bad_unpacked_path() {
+  let MANIFEST_ID = "webext_bad_path@tests.mozilla.org";
+
+  let manifest = {
+    name: "path test",
+    description: "test of a bad directory name",
+    manifest_version: 2,
+    version: "1.0",
+
+    browser_specific_settings: {
+      gecko: {
+        id: MANIFEST_ID
+      }
+    }
+  };
+
+  const directories = [
+    "not a valid ID",
+    '"quotes"@tests.mozilla.org',
+  ];
+
+  for (let dir of directories) {
+    try {
+      writeWebManifestForExtension(manifest, profileDir, dir);
+    } catch (ex) {
+      // This can fail if the underlying filesystem (looking at you windows)
+      // doesn't handle some of the characters in the ID.  In that case,
+      // just ignore this test on this platform.
+      continue;
+    }
+    yield promiseRestartManager();
+
+    let addon = yield promiseAddonByID(dir);
+    do_check_eq(addon, null);
+    addon = yield promiseAddonByID(MANIFEST_ID);
+    do_check_eq(addon, null);
+  }
+});
+
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
@@ -300,16 +300,17 @@ run-sequentially = Uses global XCurProcD
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 run-sequentially = Uses global XCurProcD dir.
 [test_overrideblocklist.js]
 run-sequentially = Uses global XCurProcD dir.
 [test_sourceURI.js]
 [test_webextension_icons.js]
 [test_webextension.js]
+[test_webextension_install.js]
 [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
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-unpack.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-unpack.ini
@@ -1,9 +1,11 @@
  [DEFAULT]
 head = head_addons.js head_unpack.js
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 dupe-manifest =
 tags = addons
 
+[test_webextension_paths.js]
+
 [include:xpcshell-shared.ini]