Bug 1190692: Split out manifest parsing code to support web extension manifests. r=billm
authorDave Townsend <dtownsend@oxymoronical.com>
Fri, 07 Aug 2015 13:15:11 -0700
changeset 256993 bce7e1052bff5199b37c9d9f75dae4092b928adb
parent 256992 1e52038911911ee778fc2837a21a658a7dcd621b
child 256994 b958b84f2fa0121763910a4257f19a548a65bcd5
push idunknown
push userunknown
push dateunknown
reviewersbillm
bugs1190692
milestone42.0a1
Bug 1190692: Split out manifest parsing code to support web extension manifests. r=billm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -112,17 +112,18 @@ const URI_EXTENSION_STRINGS           = 
 const STRING_TYPE_NAME                = "type.%ID%.name";
 
 const DIR_EXTENSIONS                  = "extensions";
 const DIR_STAGE                       = "staged";
 const DIR_TRASH                       = "trash";
 
 const FILE_DATABASE                   = "extensions.json";
 const FILE_OLD_CACHE                  = "extensions.cache";
-const FILE_INSTALL_MANIFEST           = "install.rdf";
+const FILE_RDF_MANIFEST               = "install.rdf";
+const FILE_WEB_MANIFEST               = "manifest.json";
 const FILE_XPI_ADDONS_LIST            = "extensions.ini";
 
 const KEY_PROFILEDIR                  = "ProfD";
 const KEY_APPDIR                      = "XCurProcD";
 const KEY_TEMPDIR                     = "TmpD";
 const KEY_APP_DISTRIBUTION            = "XREAppDist";
 
 const KEY_APP_PROFILE                 = "app-profile";
@@ -653,16 +654,35 @@ function createAddonDetails(id, aAddon) 
  * @return an external add-on type
  */
 function getExternalType(aType) {
   if (aType in TYPE_ALIASES)
     return TYPE_ALIASES[aType];
   return aType;
 }
 
+function getManifestFileForDir(aDir) {
+  let file = aDir.clone();
+  file.append(FILE_WEB_MANIFEST);
+  if (file.exists() && file.isFile())
+    return file;
+  file.leafName = FILE_RDF_MANIFEST;
+  if (file.exists() && file.isFile())
+    return file;
+  return null;
+}
+
+function getManifestEntryForZipReader(aZipReader) {
+  if (aZipReader.hasEntry(FILE_WEB_MANIFEST))
+    return FILE_WEB_MANIFEST;
+  if (aZipReader.hasEntry(FILE_RDF_MANIFEST))
+    return FILE_RDF_MANIFEST;
+  return null;
+}
+
 /**
  * Converts a list of API types to a list of API types and any aliases for those
  * types.
  *
  * @param  aTypes
  *         An array of types or null for all types
  * @return an array of types or null for all types
  */
@@ -713,16 +733,29 @@ function getRDFValue(aLiteral) {
  *         The property to read
  * @return a string if the property existed or null
  */
 function getRDFProperty(aDs, aResource, aProperty) {
   return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true));
 }
 
 /**
+ * Reads an AddonInternal object from a manifest stream.
+ *
+ * @param  aStream
+ *         An open stream to read the manifest from
+ * @return an AddonInternal object
+ * @throws if the install manifest in the stream is corrupt or could not
+ *         be read
+ */
+function loadManifestFromWebManifest(aStream) {
+  throw new Error("Web Extensions aren't supported yet.")
+}
+
+/**
  * Reads an AddonInternal object from an RDF stream.
  *
  * @param  aUri
  *         The URI that the manifest is being read from
  * @param  aStream
  *         An open stream to read the RDF from
  * @return an AddonInternal object
  * @throws if the install manifest in the RDF stream is corrupt or could not
@@ -973,48 +1006,49 @@ function loadManifestFromRDF(aUri, aStre
     addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
     addon.updateURL = null;
     addon.updateKey = null;
 
     addon.targetApplications = [];
     addon.targetPlatforms = [];
   }
 
+  return addon;
+}
+
+function defineSyncGUID(aAddon) {
   // Load the storage service before NSS (nsIRandomGenerator),
   // to avoid a SQLite initialization error (bug 717904).
   let storage = Services.storage;
 
   // Define .syncGUID as a lazy property which is also settable
-  Object.defineProperty(addon, "syncGUID", {
+  Object.defineProperty(aAddon, "syncGUID", {
     get: () => {
-
       // Generate random GUID used for Sync.
       // This was lifted from util.js:makeGUID() from services-sync.
       let rng = Cc["@mozilla.org/security/random-generator;1"].
         createInstance(Ci.nsIRandomGenerator);
       let bytes = rng.generateRandomBytes(9);
       let byte_string = [String.fromCharCode(byte) for each (byte in bytes)]
                         .join("");
       // Base64 encode
       let guid = btoa(byte_string).replace(/\+/g, '-')
         .replace(/\//g, '_');
 
-      delete addon.syncGUID;
-      addon.syncGUID = guid;
+      delete aAddon.syncGUID;
+      aAddon.syncGUID = guid;
       return guid;
     },
     set: (val) => {
-      delete addon.syncGUID;
-      addon.syncGUID = val;
+      delete aAddon.syncGUID;
+      aAddon.syncGUID = val;
     },
     configurable: true,
     enumerable: true,
   });
-
-  return addon;
 }
 
 /**
  * Loads an AddonInternal object from an add-on extracted in a directory.
  *
  * @param  aDir
  *         The nsIFile directory holding the add-on
  * @return an AddonInternal object
@@ -1032,43 +1066,52 @@ let loadManifestFromDir = Task.async(fun
     let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
     let entry;
     while ((entry = entries.nextFile))
       size += getFileSize(entry);
     entries.close();
     return size;
   }
 
-  let file = aDir.clone();
-  file.append(FILE_INSTALL_MANIFEST);
-  if (!file.exists() || !file.isFile())
+  function loadFromRDF(aFile, aStream) {
+    let addon = loadManifestFromRDF(Services.io.newFileURI(aFile), aStream);
+
+    let file = aDir.clone();
+    file.append("chrome.manifest");
+    let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file));
+    addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
+                                                             "binary-component");
+    return addon;
+  }
+
+  let file = getManifestFileForDir(aDir);
+  if (!file) {
     throw new Error("Directory " + aDir.path + " does not contain a valid " +
                     "install manifest");
+  }
 
   let fis = Cc["@mozilla.org/network/file-input-stream;1"].
             createInstance(Ci.nsIFileInputStream);
   fis.init(file, -1, -1, false);
   let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
             createInstance(Ci.nsIBufferedInputStream);
   bis.init(fis, 4096);
 
   try {
-    let addon = loadManifestFromRDF(Services.io.newFileURI(file), bis);
+    let addon = file.leafName == FILE_WEB_MANIFEST ?
+                loadManifestFromWebManifest(bis) :
+                loadFromRDF(file, bis);
+
     addon._sourceBundle = aDir.clone();
     addon.size = getFileSize(aDir);
-
-    file = aDir.clone();
-    file.append("chrome.manifest");
-    let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file));
-    addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
-                                                             "binary-component");
-
     addon.signedState = yield verifyDirSignedState(aDir, addon);
-
     addon.appDisabled = !isUsableAddon(addon);
+
+    defineSyncGUID(addon);
+
     return addon;
   }
   finally {
     bis.close();
     fis.close();
   }
 });
 
@@ -1076,44 +1119,61 @@ let loadManifestFromDir = Task.async(fun
  * Loads an AddonInternal object from an nsIZipReader for an add-on.
  *
  * @param  aZipReader
  *         An open nsIZipReader for the add-on's files
  * @return an AddonInternal object
  * @throws if the XPI file does not contain a valid install manifest
  */
 let loadManifestFromZipReader = Task.async(function* loadManifestFromZipReader(aZipReader) {
-  let zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST);
-  let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
-            createInstance(Ci.nsIBufferedInputStream);
-  bis.init(zis, 4096);
-
-  try {
-    let uri = buildJarURI(aZipReader.file, FILE_INSTALL_MANIFEST);
-    let addon = loadManifestFromRDF(uri, bis);
-    addon._sourceBundle = aZipReader.file;
-
-    addon.size = 0;
-    let entries = aZipReader.findEntries(null);
-    while (entries.hasMore())
-      addon.size += aZipReader.getEntry(entries.getNext()).realSize;
+  function loadFromRDF(aStream) {
+    let uri = buildJarURI(aZipReader.file, FILE_RDF_MANIFEST);
+    let addon = loadManifestFromRDF(uri, aStream);
 
     // Binary components can only be loaded from unpacked addons.
     if (addon.unpack) {
       uri = buildJarURI(aZipReader.file, "chrome.manifest");
       let chromeManifest = ChromeManifestParser.parseSync(uri);
       addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
                                                                "binary-component");
     } else {
       addon.hasBinaryComponents = false;
     }
 
+    return addon;
+  }
+
+  let entry = getManifestEntryForZipReader(aZipReader);
+  if (!entry) {
+    throw new Error("File " + aZipReader.file.path + " does not contain a valid " +
+                    "install manifest");
+  }
+
+  let zis = aZipReader.getInputStream(entry);
+  let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
+            createInstance(Ci.nsIBufferedInputStream);
+  bis.init(zis, 4096);
+
+  try {
+    let addon = entry == FILE_WEB_MANIFEST ?
+                loadManifestFromWebManifest(bis) :
+                loadFromRDF(bis);
+
+    addon._sourceBundle = aZipReader.file;
+
+    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);
-
     addon.appDisabled = !isUsableAddon(addon);
+
+    defineSyncGUID(addon);
+
     return addon;
   }
   finally {
     bis.close();
     zis.close();
   }
 });
 
@@ -1679,25 +1739,22 @@ XPIState.prototype = {
       let [modFile, modTime, items] = recursiveLastModifiedTime(aFile);
       XPIProvider._mostRecentlyModifiedFile[aId] = modFile;
       XPIProvider.setTelemetry(aId, "scan_items", items);
       if (modTime != this.scanTime) {
         this.scanTime = modTime;
         changed = true;
       }
     }
-    // if the add-on is disabled, modified time is the install.rdf time, if any.
-    // If {path}/install.rdf doesn't exist, we assume this is a packed .xpi and use
+    // if the add-on is disabled, modified time is the install manifest time, if
+    // any. If no manifest exists, we assume this is a packed .xpi and use
     // the time stamp of {path}
     try {
-      // Get the install.rdf update time, if any.
-      // XXX This will eventually also need to check for package.json or whatever
-      // the new manifest is named.
-      let maniFile = aFile.clone();
-      maniFile.append(FILE_INSTALL_MANIFEST);
+      // Get the install manifest update time, if any.
+      let maniFile = getManifestFileForDir(aFile);
       if (!(aId in XPIProvider._mostRecentlyModifiedFile)) {
         XPIProvider._mostRecentlyModifiedFile[aId] = maniFile.leafName;
       }
       let maniTime = maniFile.lastModifiedTime;
       if (maniTime != this.manifestTime) {
         this.manifestTime = maniTime;
         changed = true;
       }
@@ -2699,22 +2756,21 @@ this.XPIProvider = {
           seenFiles.push(stageDirEntry.leafName);
           continue;
         }
 
         changed = true;
 
         if (isDir) {
           // Check if the directory contains an install manifest.
-          let manifest = stageDirEntry.clone();
-          manifest.append(FILE_INSTALL_MANIFEST);
+          let manifest = getManifestFileForDir(stageDirEntry);
 
           // If the install manifest doesn't exist uninstall this add-on in this
           // install location.
-          if (!manifest.exists()) {
+          if (!manifest) {
             logger.debug("Processing uninstall of " + id + " in " + aLocation.name);
             try {
               aLocation.uninstallAddon(id);
               seenFiles.push(stageDirEntry.leafName);
             }
             catch (e) {
               logger.error("Failed to uninstall add-on " + id + " in " + aLocation.name, e);
             }