Bug 1255562 Allow applications property to be in mozilla.json r?mossop draft
authorAndrew Swan <aswan@mozilla.com>
Thu, 17 Mar 2016 04:56:31 -0700
changeset 344617 a1c7bc8194047a6ee5e36cf1a93886cbc788c90d
parent 344616 ab10aef83718f44aa6d12efb95ae263b888c8dce
child 517005 70635d1619034dd5c38fa3b1245322b035830a39
push id13881
push useraswan@mozilla.com
push dateThu, 24 Mar 2016 23:39:25 +0000
reviewersmossop
bugs1255562
milestone48.0a1
Bug 1255562 Allow applications property to be in mozilla.json r?mossop MozReview-Commit-ID: 8w6s4bVgbxE
toolkit/components/extensions/Extension.jsm
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -665,26 +665,44 @@ ExtensionData.prototype = {
           resolve(JSON.parse(text));
         } catch (e) {
           reject(e);
         }
       });
     });
   },
 
-  // Reads the extension's |manifest.json| file, and stores its
-  // parsed contents in |this.manifest|.
+  // Reads the extension's |manifest.json| file and optional |mozilla.json|,
+  // and stores the parsed and merged contents in |this.manifest|.
   readManifest() {
     return Promise.all([
       this.readJSON("manifest.json"),
+      this.readJSON("mozilla.json").catch(err => null),
       Management.lazyInit(),
-    ]).then(([manifest]) => {
+    ]).then(([manifest, mozManifest]) => {
       this.manifest = manifest;
       this.rawManifest = manifest;
 
+      if (mozManifest) {
+        if (typeof mozManifest != "object") {
+          this.logger.warn(`Loading extension '${this.id}': mozilla.json has unexpected type ${typeof mozManifest}`);
+        } else {
+          Object.keys(mozManifest).forEach(key => {
+            if (key != "applications") {
+              throw new Error(`Illegal property "${key}" in mozilla.json`);
+            }
+            if (key in manifest) {
+              this.logger.warn(`Ignoring property "${key}" from mozilla.json`);
+            } else {
+              manifest[key] = mozManifest[key];
+            }
+          });
+        }
+      }
+
       if (manifest && manifest.default_locale) {
         return this.initLocale();
       }
     }).then(() => {
       let context = {
         url: this.baseURI && this.baseURI.spec,
 
         principal: this.principal,
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -268,43 +268,44 @@ function createAppInfo(ID, name, version
     crashReporter: true,
     extraProps: {
       browserTabsRemoteAutostart: false,
     },
   });
   gAppInfo = tmp.getAppInfo();
 }
 
-function getManifestURIForBundle(file) {
+function getManifestURIForBundle(file, manifest="manifest.json") {
   if (file.isDirectory()) {
-    file.append("install.rdf");
-    if (file.exists()) {
-      return NetUtil.newURI(file);
+    let path = file.clone();
+    path.append("install.rdf");
+    if (path.exists()) {
+      return NetUtil.newURI(path);
     }
 
-    file.leafName = "manifest.json";
-    if (file.exists()) {
-      return NetUtil.newURI(file);
+    path.leafName = manifest;
+    if (path.exists()) {
+      return NetUtil.newURI(path);
     }
 
     throw new Error("No manifest file present");
   }
 
   let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
             createInstance(AM_Ci.nsIZipReader);
   zip.open(file);
   try {
     let uri = NetUtil.newURI(file);
 
     if (zip.hasEntry("install.rdf")) {
       return NetUtil.newURI("jar:" + uri.spec + "!/" + "install.rdf");
     }
 
-    if (zip.hasEntry("manifest.json")) {
-      return NetUtil.newURI("jar:" + uri.spec + "!/" + "manifest.json");
+    if (zip.hasEntry(manifest)) {
+      return NetUtil.newURI("jar:" + uri.spec + "!/" + manifest);
     }
 
     throw new Error("No manifest file present");
   }
   finally {
     zip.close();
   }
 }
@@ -336,18 +337,22 @@ let getIDForManifest = Task.async(functi
                      getService(AM_Ci.nsIRDFService);
 
     let rdfID = ds.GetTarget(rdfService.GetResource("urn:mozilla:install-manifest"),
                              rdfService.GetResource("http://www.mozilla.org/2004/em-rdf#id"),
                              true);
     return rdfID.QueryInterface(AM_Ci.nsIRDFLiteral).Value;
   }
   else {
-    let manifest = JSON.parse(data);
-    return manifest.applications.gecko.id;
+    try {
+      let manifest = JSON.parse(data);
+      return manifest.applications.gecko.id;
+    } catch (err) {
+      return null;
+    }
   }
 });
 
 let gUseRealCertChecks = false;
 function overrideCertDB(handler) {
   // Unregister the real database. This only works because the add-ons manager
   // hasn't started up and grabbed the certificate database yet.
   let registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar);
@@ -375,16 +380,25 @@ function overrideCertDB(handler) {
       return;
     }
 
     try {
       let manifestURI = getManifestURIForBundle(file);
 
       let id = yield getIDForManifest(manifestURI);
 
+      if (!id) {
+        manifestURI = getManifestURIForBundle(file, "mozilla.json");
+        id = yield getIDForManifest(manifestURI);
+      }
+
+      if (!id) {
+        throw new Error("Cannot find addon ID");
+      }
+
       // Make sure to close the open zip file or it will be locked.
       if (file.isFile()) {
         Services.obs.notifyObservers(file, "flush-cache-entry", "cert-override");
       }
 
       let fakeCert = {
         commonName: id
       }
@@ -1107,54 +1121,68 @@ function writeInstallRDFForExtension(aDa
  * @param   aManifest
  *          The data to write
  * @param   aDir
  *          The install directory to add the extension to
  * @param   aId
  *          An optional string to override the default installation aId
  * @return  A file pointing to where the extension was installed
  */
-function writeWebManifestForExtension(aData, aDir, aId = undefined) {
+function writeWebManifestForExtension(aData, aDir, aId = undefined, aMozData = undefined) {
   if (!aId)
     aId = aData.applications.gecko.id;
 
   if (TEST_UNPACKED) {
     let dir = aDir.clone();
     dir.append(aId);
     if (!dir.exists())
       dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
 
-    let file = dir.clone();
-    file.append("manifest.json");
-    if (file.exists())
-      file.remove(true);
+    function writeOne(filename, raw) {
+      let file = dir.clone();
+      file.append(filename);
+      if (file.exists())
+        file.remove(true);
 
-    let data = JSON.stringify(aData);
-    let fos = AM_Cc["@mozilla.org/network/file-output-stream;1"].
-              createInstance(AM_Ci.nsIFileOutputStream);
-    fos.init(file,
-             FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
-             FileUtils.PERMS_FILE, 0);
-    fos.write(data, data.length);
-    fos.close();
+      let data = JSON.stringify(raw);
+      let fos = AM_Cc["@mozilla.org/network/file-output-stream;1"].
+          createInstance(AM_Ci.nsIFileOutputStream);
+      fos.init(file,
+               FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
+               FileUtils.PERMS_FILE, 0);
+      fos.write(data, data.length);
+      fos.close();
+    }
+
+    writeOne("manifest.json", aData);
+    if (aMozData) {
+      writeOne("mozilla.json", aMozData);
+    }
 
     return dir;
   }
   else {
     let file = aDir.clone();
     file.append(aId + ".xpi");
 
     let stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
                  createInstance(AM_Ci.nsIStringInputStream);
     stream.setData(JSON.stringify(aData), -1);
     let zipW = AM_Cc["@mozilla.org/zipwriter;1"].
                createInstance(AM_Ci.nsIZipWriter);
     zipW.open(file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
     zipW.addEntryStream("manifest.json", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
                         stream, false);
+    if (aMozData) {
+      let mozStream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
+                      createInstance(AM_Ci.nsIStringInputStream);
+      mozStream.setData(JSON.stringify(aMozData), -1);
+      zipW.addEntryStream("mozilla.json", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
+                          mozStream, false);
+    }
     zipW.close();
 
     return file;
   }
 }
 
 /**
  * Writes an install.rdf manifest into a packed extension using the properties passed
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
@@ -154,16 +154,51 @@ add_task(function*() {
   let file = getFileForAddon(profileDir, ID);
   do_check_true(file.exists());
 
   addon.uninstall();
 
   yield promiseRestartManager();
 });
 
+// applications.gecko.id may be in mozilla.json
+add_task(function* test_mozilla_json() {
+  writeWebManifestForExtension({
+    name: "Web Extension Name",
+    version: "1.0",
+    manifest_version: 2,
+  }, profileDir, ID, {
+    applications: {
+      gecko: {
+        id: ID
+      }
+    }
+  });
+
+  yield promiseRestartManager();
+
+  let addon = yield promiseAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_eq(addon.version, "1.0");
+  do_check_eq(addon.name, "Web Extension Name");
+  do_check_true(addon.isCompatible);
+  do_check_false(addon.appDisabled);
+  do_check_true(addon.isActive);
+  do_check_false(addon.isSystem);
+  do_check_eq(addon.type, "extension");
+  do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+  let file = getFileForAddon(profileDir, ID);
+  do_check_true(file.exists());
+
+  addon.uninstall();
+
+  yield promiseRestartManager();
+});
+
 add_task(function* test_manifest_localization() {
   const ID = "webextension3@tests.mozilla.org";
 
   yield promiseInstallAllFiles([do_get_addon("webextension_3")], true);
   yield promiseAddonStartup();
 
   let addon = yield promiseAddonByID(ID);
   addon.userDisabled = true;