Bug 982874 - Import / export API for apps : Part 2: dom/webapps implementation r=marco,sicking
authorFabrice Desré <fabrice@mozilla.com>
Tue, 14 Oct 2014 22:55:14 -0700
changeset 210401 e1ed7edd0166b2c5dbb599ef45903d10ad9ffe5c
parent 210400 56ba1d2ec26445891f0cb3fed60d68bca103250c
child 210402 1ec76d3b14bec5f06e9298e9c9729e0a7d04424e
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmarco, sicking
bugs982874
milestone36.0a1
Bug 982874 - Import / export API for apps : Part 2: dom/webapps implementation r=marco,sicking
content/base/public/File.h
content/base/test/chrome/test_fileconstructor_tempfile.xul
dom/apps/ImportExport.jsm
dom/apps/Webapps.js
dom/apps/Webapps.jsm
dom/apps/moz.build
dom/apps/tests/mochitest.ini
dom/apps/tests/test_import_export.html
dom/tests/mochitest/webapps/test_list_api.xul
dom/webidl/Apps.webidl
--- a/content/base/public/File.h
+++ b/content/base/public/File.h
@@ -747,16 +747,17 @@ public:
                                       ErrorResult& aRv) MOZ_OVERRIDE;
   virtual nsresult GetInternalStream(nsIInputStream**) MOZ_OVERRIDE;
 
   void SetPath(const nsAString& aFullPath);
 
 protected:
   virtual ~FileImplFile() {
     if (mFile && mIsTemporary) {
+      NS_WARNING("In temporary ~FileImplFile");
       // Ignore errors if any, not much we can do. Clean-up will be done by
       // https://mxr.mozilla.org/mozilla-central/source/xpcom/io/nsAnonymousTemporaryFile.cpp?rev=6c1c7e45c902#127
 #ifdef DEBUG
       nsresult rv =
 #endif
       mFile->Remove(false);
       NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Failed to remove temporary DOMFile.");
     }
--- a/content/base/test/chrome/test_fileconstructor_tempfile.xul
+++ b/content/base/test/chrome/test_fileconstructor_tempfile.xul
@@ -69,17 +69,17 @@ try {
                  0666, 0);
   outStream.write(fileData, fileData.length);
   outStream.close();
 
   // Create a scoped DOMFile so the gc will happily get rid of it.
   {
     let dirfile = new File(tmp, { temporary: true });
     ok(true, "Temporary File() created");
-    var reader = new FileReader();
+    let reader = new FileReader();
     reader.readAsArrayBuffer(dirfile);
     reader.onload = function(event) {
       let buffer = event.target.result;
       ok(buffer.byteLength > 0,
          "Blob size should be > 0 : " + buffer.byteLength);
       cleanup(tmp);
     }
   }
new file mode 100644
--- /dev/null
+++ b/dom/apps/ImportExport.jsm
@@ -0,0 +1,488 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Webapps.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+  "resource://gre/modules/FileUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+  "resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PermissionsInstaller",
+  "resource://gre/modules/PermissionsInstaller.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
+
+this.EXPORTED_SYMBOLS = ["ImportExport"];
+
+const kAppArchiveMimeType  = "application/openwebapp+zip";
+const kAppArchiveExtension = ".wpk"; // Webapp Package
+const kAppArchiveVersion   = 1;
+
+// From prio.h
+const PR_RDWR        = 0x04;
+const PR_CREATE_FILE = 0x08;
+const PR_TRUNCATE    = 0x20;
+
+function debug(aMsg) {
+//#ifdef DEBUG
+  dump("-*- ImportExport.jsm : " + aMsg + "\n");
+//#endif
+}
+
+/*
+The full meta-data for an app looks like the properties set by
+the mozIApplication constructor in AppsUtils.jsm.  We don't export anything
+that can't be recreated from the manifest or by using sane defaults.
+*/
+
+// Reads a JSON object from a zip.
+function readObjectFromZip(aZipReader, aPath) {
+  if (!aZipReader.hasEntry(aPath)) {
+    debug("ZIP doesn't have entry " + aPath);
+    return;
+  }
+
+  let istream = aZipReader.getInputStream(aPath);
+
+  // Obtain a converter to read from a UTF-8 encoded input stream.
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                    .createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+
+  let res;
+  try {
+    res = JSON.parse(converter.ConvertToUnicode(
+      NetUtil.readInputStreamToString(istream, istream.available()) || ""));
+  } catch(e) {
+    debug("error reading " + aPath + " from ZIP: " + e);
+  }
+  return res;
+}
+
+this.ImportExport = {
+  getUUID: function() {
+    let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
+                          .getService(Ci.nsIUUIDGenerator);
+    return uuidGenerator.generateUUID().toString();
+  },
+
+  // Exports to a Blob. Returns a promise that is resolved with the Blob and
+  // rejected with an error string.
+  // Possible errors are:
+  // NoSuchApp, AppNotFullyInstalled, CertifiedAppExportForbidden, ZipWriteError,
+  // NoAppDirectory
+  export: Task.async(function*(aApp) {
+    if (!aApp) {
+      // Should not happen!
+      throw "NoSuchApp";
+    }
+
+    debug("Exporting " + aApp.manifestURL);
+
+    if (aApp.installState != "installed") {
+      throw "AppNotFullyInstalled";
+    }
+
+    // Exporting certified apps is forbidden, as it is to import them.
+    // We *have* to do this check in the parent process.
+    if (aApp.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
+      throw "CertifiedAppExportForbidden";
+    }
+
+    // Add the metadata we'll need to recreate the app object.
+    let meta = {
+      installOrigin: aApp.InstallOrigin,
+      manifestURL: aApp.manifestURL,
+      version: kAppArchiveVersion
+    };
+
+    // Add all the needed files in the app's base path to the archive.
+
+    debug("Adding files from " + aApp.basePath + "/" + aApp.id);
+    let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+    dir.initWithPath(aApp.basePath);
+    dir.append(aApp.id);
+    if (!dir.exists() || !dir.isDirectory()) {
+      throw "NoAppDirectory";
+    }
+
+    let files = [];
+    if (aApp.origin.startsWith("app://")) {
+      files.push("update.webapp");
+      files.push("application.zip");
+    } else {
+      files.push("manifest.webapp");
+    }
+
+    // Creates the archive and adds the application meta-data.
+    // Using the app id as the file name prevents name collisions.
+    let zipWriter = Cc["@mozilla.org/zipwriter;1"]
+                      .createInstance(Ci.nsIZipWriter);
+    let uuid = this.getUUID();
+
+    // We create the file in ${TempDir}/mozilla-temp-files to make sure we
+    // can remove it once the blob has been used even on windows.
+    // See https://mxr.mozilla.org/mozilla-central/source/xpcom/io/nsAnonymousTemporaryFile.cpp?rev=6c1c7e45c902#127
+    let zipFile = FileUtils.getFile("TmpD",
+      ["mozilla-temp-files", uuid + kAppArchiveExtension]);
+    debug("Creating archive " + zipFile.path);
+
+    zipWriter.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
+
+    let blob;
+
+    try {
+      debug("Adding metadata.json to exported blob.");
+      let stream = Cc["@mozilla.org/io/string-input-stream;1"]
+                     .createInstance(Ci.nsIStringInputStream);
+      let s = JSON.stringify(meta);
+      stream.setData(s, s.length);
+      zipWriter.addEntryStream("metadata.json", Date.now(),
+                               Ci.nsIZipWriter.COMPRESSION_DEFAULT,
+                               stream, false);
+
+      files.forEach((aName) => {
+        let file = dir.clone();
+        file.append(aName);
+        debug("Adding " + file.leafName + " to export blob.");
+        zipWriter.addEntryFile(file.leafName,
+                               Ci.nsIZipWriter.COMPRESSION_DEFAULT,
+                               file, false);
+      });
+
+      zipWriter.close();
+      // Reads back the file as Blob.
+      // File is only available on Window and Worker objects, so we need
+      // to get a window there...
+      let win = Services.wm.getMostRecentWindow("navigator:browser");
+      if (!win) {
+        throw "NoWindowAvailable";
+      }
+      blob = new win.File(zipFile, { name: aApp.id + kAppArchiveExtension,
+                                     type: kAppArchiveMimeType,
+                                     temporary: true });
+    } catch(e) {
+      debug("Error: " + e);
+      zipWriter.close();
+      zipFile.remove(false);
+      throw "ZipWriteError";
+    }
+
+    return blob;
+  }),
+
+  // Returns the manifest for this hosted app.
+  _importHostedApp: function(aZipReader, aManifestURL) {
+    debug("Importing hosted app " + aManifestURL);
+
+    if (!aZipReader.hasEntry("manifest.webapp")) {
+      throw "NoManifestFound";
+    }
+
+    let manifest = readObjectFromZip(aZipReader, "manifest.webapp");
+    if (!manifest) {
+      throw "NoManifestFound";
+    }
+
+    return manifest;
+  },
+
+  // Returns the manifest for this packaged app.
+  _importPackagedApp: function(aZipReader, aManifestURL, aDir) {
+    debug("Importing packaged app " + aManifestURL);
+
+    if (!aZipReader.hasEntry("update.webapp")) {
+      throw "NoUpdateManifestFound";
+    }
+
+    if (!aZipReader.hasEntry("application.zip")) {
+      throw "NoPackageFound";
+    }
+
+    // Extract application.zip and update.webapp
+    // We get manifest.webapp from application.zip itself.
+    let file;
+    ["update.webapp", "application.zip"].forEach((aName) => {
+      file = aDir.clone();
+      file.append(aName);
+      aZipReader.extract(aName, file);
+    });
+
+    // |file| now points to application.zip, open it.
+    let appZipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
+                         .createInstance(Ci.nsIZipReader);
+    appZipReader.open(file);
+    if (!appZipReader.hasEntry("manifest.webapp")) {
+      throw "NoManifestFound";
+    }
+
+    return [readObjectFromZip(appZipReader, "manifest.webapp"), file];
+  },
+
+  // Imports a blob, returning a Promise that resolves to
+  // [manifestURL, manifest]
+  // Possible errors are:
+  // NoBlobFound, UnsupportedBlobArchive, MissingMetadataFile, IncorrectVersion,
+  // AppAlreadyInstalled, DontImportCertifiedApps, InvalidManifest,
+  // InvalidPrivilegeLevel, InvalidOrigin, DuplicateOrigin
+  import: Task.async(function*(aBlob) {
+
+    // First, do we even have a blob?
+    if (!aBlob || !aBlob instanceof Ci.nsIDOMBlob) {
+      throw "NoBlobFound";
+    }
+
+    let isFile = aBlob instanceof Ci.nsIDOMFile;
+    if (!isFile) {
+      // XXX: TODO Store the blob on disk.
+      throw "UnsupportedBlobArchive";
+    }
+
+    // We can't QI the DOMFile to nsIFile, so we need to create one.
+    let zipFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+    zipFile.initWithPath(aBlob.mozFullPath);
+
+    debug("Importing from " + zipFile.path);
+
+    let meta;
+    let appDir;
+    let manifest;
+    let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
+                      .createInstance(Ci.nsIZipReader);
+    zipReader.open(zipFile);
+    try {
+      // Do some sanity checks on the metadata.json and manifest.webapp files.
+      if (!zipReader.hasEntry("metadata.json")) {
+        throw "MissingMetadataFile";
+      }
+
+      meta = readObjectFromZip(zipReader, "metadata.json");
+      if (!meta) {
+        throw "NoMetadata";
+      }
+      debug("metadata: " + uneval(meta));
+
+      // Bail out if that comes from an unsupported archive version.
+      if (meta.version !== 1) {
+        throw "IncorrectVersion";
+      }
+
+      // Check if we already have an app installed for this manifest url.
+      let app = DOMApplicationRegistry.getAppByManifestURL(meta.manifestURL);
+      if (app) {
+        throw "AppAlreadyInstalled";
+      }
+
+      // Create a new app id & localId.
+      // TODO: stop accessing internal methods of other objects.
+      meta.localId = DOMApplicationRegistry._nextLocalId();
+      meta.id = this.getUUID();
+      meta.basePath = FileUtils.getDir(DOMApplicationRegistry.dirKey,
+                                       ["webapps"], false).path;
+
+      appDir = FileUtils.getDir(DOMApplicationRegistry.dirKey,
+                                ["webapps", meta.id], true);
+
+      let isPackage = zipReader.hasEntry("application.zip");
+
+      let appFile;
+
+      if (isPackage) {
+        [manifest, appFile] =
+          this._importPackagedApp(zipReader, meta.manifestURL, appDir);
+      } else {
+        manifest = this._importHostedApp(zipReader, meta.manifestURL);
+      }
+
+      if (!AppsUtils.checkManifest(manifest)) {
+        throw "InvalidManifest";
+      }
+
+      let manifestFile = appDir.clone();
+      manifestFile.append("manifest.webapp");
+
+      let manifestString = JSON.stringify(manifest);
+
+      // We now have the correct manifest. Save it.
+      // TODO: stop accessing internal methods of other objects.
+      yield DOMApplicationRegistry._writeFile(manifestFile.path,
+                                              manifestString)
+      .then(() => { debug("Manifest saved."); },
+            aError => { debug("Error saving manifest: " + aError )});
+
+      // Default values for the common fields.
+      // TODO: share this code with the main install flow and with
+      // DOMApplicationRegistry::addInstalledApp
+      meta.name = manifest.name;
+      meta.csp = manifest.csp;
+      meta.installTime = Date.now();
+      meta.removable = true;
+      meta.progress = 0.0;
+      meta.installState = "installed";
+      meta.downloadAvailable = false;
+      meta.downloading = false;
+      meta.readyToApplyDownload = false;
+      meta.downloadSize = 0;
+      meta.lastUpdateCheck = Date.now();
+      meta.updateTime = Date.now();
+      meta.manifestHash = AppsUtils.computeHash(manifestString);
+      meta.installerAppId = Ci.nsIScriptSecurityManager.NO_APP_ID;
+      meta.installerIsBrowser = false;
+      meta.role = manifest.role;
+
+      // Set the appropriate metadata for hosted and packaged apps.
+      if (isPackage) {
+        meta.origin = "app://" + meta.id;
+        // Signature check
+        // TODO: stop accessing internal methods of other objects.
+        let [reader, isSigned] =
+          yield DOMApplicationRegistry._openPackage(appFile, meta, false);
+        let maxStatus = isSigned ? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED
+                                 : Ci.nsIPrincipal.APP_STATUS_INSTALLED;
+        meta.appStatus = AppsUtils.getAppManifestStatus(manifest);
+        debug("Signed app? " + isSigned);
+        if (meta.appStatus > maxStatus) {
+          throw "InvalidPrivilegeLevel";
+        }
+
+        // Custom origin.
+        // We unfortunately can't reuse _checkOrigin here.
+        if (isSigned &&
+            meta.appStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED &&
+            manifest.origin) {
+          let uri;
+          try {
+            uri = Services.io.newURI(aManifest.origin, null, null);
+          } catch(e) {
+            throw "InvalidOrigin";
+          }
+          if (uri.scheme != "app") {
+            throw "InvalidOrigin";
+          }
+          meta.id = uri.prePath.substring(6); // "app://".length
+          if (meta.id in DOMApplicationRegistry.webapps) {
+            throw "DuplicateOrigin";
+          }
+          meta.origin = uri.prePath;
+        }
+      } else {
+        let uri = Services.io.newURI(meta.manifestURL, null, null);
+        meta.origin = uri.resolve("/");
+        meta.appStatus = Ci.nsIPrincipal.APP_STATUS_INSTALLED;
+        if (manifest.appcache_path) {
+          // We don't export the content of the appcache, so set the app
+          // in the state that will trigger download.
+          meta.installState = "pending";
+          meta.downloadAvailable = true;
+        }
+      }
+      meta.kind = DOMApplicationRegistry.appKind(meta, manifest);
+
+      DOMApplicationRegistry.webapps[meta.id] = meta;
+
+      // Set permissions and handlers
+      PermissionsInstaller.installPermissions(
+        {
+          origin: meta.origin,
+          manifestURL: meta.manifestURL,
+          manifest: manifest
+        },
+        false,
+        null);
+      DOMApplicationRegistry.updateAppHandlers(null /* old manifest */,
+                                               manifest,
+                                               meta);
+
+      // Save the app registry, and sends the various notifications.
+      // TODO: stop accessing internal methods of other objects.
+      yield DOMApplicationRegistry._saveApps();
+
+      app = AppsUtils.cloneAppObject(meta);
+      app.manifest = manifest;
+      DOMApplicationRegistry.broadcastMessage("Webapps:AddApp",
+                                              { id: meta.id, app: app });
+      DOMApplicationRegistry.broadcastMessage("Webapps:Install:Return:OK",
+                                              { app: app });
+      Services.obs.notifyObservers(null, "webapps-installed",
+        JSON.stringify({ manifestURL: meta.manifestURL }));
+
+    } catch(e) {
+      debug("Import failed: " + e);
+      if (appDir && appDir.exists()) {
+        appDir.remove(true);
+      }
+      throw e;
+    } finally {
+      zipReader.close();
+    }
+
+    return [meta.manifestURL, manifest];
+  }),
+
+  // Extracts the manifest from a blob, returning a Promise that resolves to
+  // the manifest
+  // Possible errors are:
+  // NoBlobFound, UnsupportedBlobArchive, MissingMetadataFile.
+  extractManifest: Task.async(function*(aBlob) {
+    // First, do we even have a blob?
+    if (!aBlob || !aBlob instanceof Ci.nsIDOMBlob) {
+      throw "NoBlobFound";
+    }
+
+    let isFile = aBlob instanceof Ci.nsIDOMFile;
+    if (!isFile) {
+      // XXX: TODO Store the blob on disk.
+      throw "UnsupportedBlobArchive";
+    }
+
+    // We can't QI the DOMFile to nsIFile, so we need to create one.
+    let zipFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+    zipFile.initWithPath(aBlob.mozFullPath);
+    debug("extractManifest from " + zipFile.path);
+
+    // Do some sanity checks on the metadata.json and manifest.webapp files.
+    let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
+                      .createInstance(Ci.nsIZipReader);
+    zipReader.open(zipFile);
+
+    let manifest;
+    try {
+      if (zipReader.hasEntry("manifest.webapp")) {
+        manifest = readObjectFromZip(zipReader, "manifest.webapp");
+        if (!manifest) {
+          throw "NoManifest";
+        }
+      } else if (zipReader.hasEntry("application.zip")) {
+        // That's a packaged app, we need to extract from the inner zip.
+        let innerReader = Cc["@mozilla.org/libjar/zip-reader;1"]
+                            .createInstance(Ci.nsIZipReader);
+        innerReader.openInner(zipReader, "application.zip");
+        manifest = readObjectFromZip(innerReader, "manifest.webapp");
+        innerReader.close();
+        if (!manifest) {
+          throw "NoManifest";
+        }
+      } else {
+        throw "MissingManifestFile";
+      }
+    } finally {
+      zipReader.close();
+    }
+
+    return manifest;
+  }),
+};
--- a/dom/apps/Webapps.js
+++ b/dom/apps/Webapps.js
@@ -543,16 +543,30 @@ WebappsApplication.prototype = {
                                                       newReceipt: newReceipt,
                                                       oldReceipt: oldReceipt,
                                                       oid: this._id,
                                                       requestID: this.getRequestId(request) });
 
     return request;
   },
 
+  export: function() {
+    this.addMessageListeners(["Webapps:Export:Return"]);
+    return this.createPromise((aResolve, aReject) => {
+      cpmm.sendAsyncMessage("Webapps:Export",
+        { manifestURL: this.manifestURL,
+          oid: this._id,
+          requestID: this.getPromiseResolverId({
+            resolve: aResolve,
+            reject: aReject
+          })
+        });
+    });
+  },
+
   _prepareForContent: function() {
     if (this.__DOM_IMPL__) {
       return this.__DOM_IMPL__;
     }
     return this._window.DOMApplication._create(this._window, this.wrappedJSObject);
   },
 
   uninit: function() {
@@ -579,17 +593,18 @@ WebappsApplication.prototype = {
              : Services.DOMRequest.fireSuccess(req, msg.result);
   },
 
   receiveMessage: function(aMessage) {
     let msg = aMessage.json;
     let req;
     if (aMessage.name == "Webapps:Connect:Return:OK" ||
         aMessage.name == "Webapps:Connect:Return:KO" ||
-        aMessage.name == "Webapps:GetConnections:Return:OK") {
+        aMessage.name == "Webapps:GetConnections:Return:OK" ||
+        aMessage.name == "Webapps:Export:Return") {
       req = this.takePromiseResolver(msg.requestID);
     } else {
       req = this.takeRequest(msg.requestID);
     }
 
     if (msg.oid !== this._id || !req) {
       return;
     }
@@ -667,16 +682,24 @@ WebappsApplication.prototype = {
         this._proxy.receipts = msg.receipts;
         Services.DOMRequest.fireSuccess(req, null);
         break;
       case "Webapps:ReplaceReceipt:Return:KO":
         this.removeMessageListeners(["Webapps:ReplaceReceipt:Return:OK",
                                      "Webapps:ReplaceReceipt:Return:KO"]);
         Services.DOMRequest.fireError(req, msg.error);
         break;
+      case "Webapps:Export:Return":
+        this.removeMessageListeners(["Webapps:Export:Return"]);
+        if (msg.success) {
+          req.resolve(Cu.cloneInto(msg.blob, this._window));
+        } else {
+          req.reject(new this._window.DOMError(msg.error || ""));
+        }
+        break;
     }
   },
 
   classID: Components.ID("{723ed303-7757-4fb0-b261-4f78b1f6bd22}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
                                          Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference])
@@ -692,17 +715,19 @@ function WebappsApplicationMgmt() {
 WebappsApplicationMgmt.prototype = {
   __proto__: DOMRequestIpcHelper.prototype,
 
   init: function(aWindow) {
     this.initDOMRequestHelper(aWindow, ["Webapps:Uninstall:Return:OK",
                                         "Webapps:Uninstall:Broadcast:Return:OK",
                                         "Webapps:Uninstall:Return:KO",
                                         "Webapps:Install:Return:OK",
-                                        "Webapps:GetNotInstalled:Return:OK"]);
+                                        "Webapps:GetNotInstalled:Return:OK",
+                                        "Webapps:Import:Return",
+                                        "Webapps:ExtractManifest:Return"]);
     cpmm.sendAsyncMessage("Webapps:RegisterForMessages",
                           {
                             messages: ["Webapps:Install:Return:OK",
                                        "Webapps:Uninstall:Return:OK",
                                        "Webapps:Uninstall:Broadcast:Return:OK"]
                           }
                          );
   },
@@ -750,16 +775,40 @@ WebappsApplicationMgmt.prototype = {
 
   getNotInstalled: function() {
     let request = this.createRequest();
     cpmm.sendAsyncMessage("Webapps:GetNotInstalled", { oid: this._id,
                                                        requestID: this.getRequestId(request) });
     return request;
   },
 
+  import: function(aBlob) {
+    return this.createPromise((aResolve, aReject) => {
+      cpmm.sendAsyncMessage("Webapps:Import",
+        { blob: aBlob,
+          oid: this._id,
+          requestID: this.getPromiseResolverId({
+            resolve: aResolve,
+            reject: aReject
+          })});
+    });
+  },
+
+  extractManifest: function(aBlob) {
+    return this.createPromise((aResolve, aReject) => {
+      cpmm.sendAsyncMessage("Webapps:ExtractManifest",
+        { blob: aBlob,
+          oid: this._id,
+          requestID: this.getPromiseResolverId({
+            resolve: aResolve,
+            reject: aReject
+          })});
+    });
+  },
+
   get oninstall() {
     return this.__DOM_IMPL__.getEventHandler("oninstall");
   },
 
   get onuninstall() {
     return this.__DOM_IMPL__.getEventHandler("onuninstall");
   },
 
@@ -767,25 +816,34 @@ WebappsApplicationMgmt.prototype = {
     this.__DOM_IMPL__.setEventHandler("oninstall", aCallback);
   },
 
   set onuninstall(aCallback) {
     this.__DOM_IMPL__.setEventHandler("onuninstall", aCallback);
   },
 
   receiveMessage: function(aMessage) {
-    var msg = aMessage.json;
-    let req = this.getRequest(msg.requestID);
+    let msg = aMessage.data;
+    let req;
+    if (["Webapps:Import:Return",
+         "Webapps:ExtractManifest:Return"]
+         .indexOf(aMessage.name) != -1) {
+      req = this.takePromiseResolver(msg.requestID);
+    } else {
+      req = this.getRequest(msg.requestID);
+    }
+
     // We want Webapps:Install:Return:OK and Webapps:Uninstall:Broadcast:Return:OK
     // to be broadcasted to all instances of mozApps.mgmt.
     if (!((msg.oid == this._id && req) ||
           aMessage.name == "Webapps:Install:Return:OK" ||
           aMessage.name == "Webapps:Uninstall:Broadcast:Return:OK")) {
       return;
     }
+
     switch (aMessage.name) {
       case "Webapps:GetNotInstalled:Return:OK":
         Services.DOMRequest.fireSuccess(req, convertAppsArray(msg.apps, this._window));
         break;
       case "Webapps:Install:Return:OK":
         {
           let app = createContentApplicationObject(this._window, msg.app);
           let event =
@@ -802,16 +860,30 @@ WebappsApplicationMgmt.prototype = {
         }
         break;
       case "Webapps:Uninstall:Return:OK":
         Services.DOMRequest.fireSuccess(req, msg.manifestURL);
         break;
       case "Webapps:Uninstall:Return:KO":
         Services.DOMRequest.fireError(req, "NOT_INSTALLED");
         break;
+      case "Webapps:Import:Return":
+        if (msg.success) {
+          req.resolve(createContentApplicationObject(this._window, msg.app));
+        } else {
+          req.reject(new this._window.DOMError(msg.error || ""));
+        }
+        break;
+      case "Webapps:ExtractManifest:Return":
+        if (msg.success) {
+          req.resolve(Cu.cloneInto(msg.manifest, this._window));
+        } else {
+          req.reject(new this._window.DOMError(msg.error || ""));
+        }
+        break;
     }
     if (aMessage.name !== "Webapps:Uninstall:Broadcast:Return:OK") {
       this.removeRequest(msg.requestID);
     }
   },
 
   classID: Components.ID("{8c1bca96-266f-493a-8d57-ec7a95098c15}"),
 
--- a/dom/apps/Webapps.jsm
+++ b/dom/apps/Webapps.jsm
@@ -62,16 +62,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/NetUtil.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ScriptPreloader",
                                   "resource://gre/modules/ScriptPreloader.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "TrustedHostedAppsUtils",
                                   "resource://gre/modules/TrustedHostedAppsUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "ImportExport",
+                                  "resource://gre/modules/ImportExport.jsm");
+
 #ifdef MOZ_WIDGET_GONK
 XPCOMUtils.defineLazyGetter(this, "libcutils", function() {
   Cu.import("resource://gre/modules/systemlibs.js");
   return libcutils;
 });
 #endif
 
 #ifdef MOZ_WIDGET_ANDROID
@@ -162,30 +165,33 @@ this.DOMApplicationRegistry = {
 
   // Path to the webapps.json file where we store the registry data.
   appsFile: null,
   webapps: { },
   children: [ ],
   allAppsLaunchable: false,
   _updateHandlers: [ ],
   _pendingUninstalls: {},
+  dirKey: DIRECTORY_NAME,
 
   init: function() {
     this.messages = ["Webapps:Install", "Webapps:Uninstall",
                      "Webapps:GetSelf", "Webapps:CheckInstalled",
                      "Webapps:GetInstalled", "Webapps:GetNotInstalled",
                      "Webapps:Launch",
                      "Webapps:InstallPackage",
                      "Webapps:GetList", "Webapps:RegisterForMessages",
                      "Webapps:UnregisterForMessages",
                      "Webapps:CancelDownload", "Webapps:CheckForUpdate",
                      "Webapps:Download", "Webapps:ApplyDownload",
                      "Webapps:Install:Return:Ack", "Webapps:AddReceipt",
                      "Webapps:RemoveReceipt", "Webapps:ReplaceReceipt",
                      "Webapps:RegisterBEP",
+                     "Webapps:Export", "Webapps:Import",
+                     "Webapps:ExtractManifest",
                      "child-process-shutdown"];
 
     this.frameMessages = ["Webapps:ClearBrowserData"];
 
     this.messages.forEach((function(msgName) {
       ppmm.addMessageListener(msgName, this);
     }).bind(this));
 
@@ -1170,20 +1176,23 @@ this.DOMApplicationRegistry = {
   },
 
   receiveMessage: function(aMessage) {
     // nsIPrefBranch throws if pref does not exist, faster to simply write
     // the pref instead of first checking if it is false.
     Services.prefs.setBoolPref("dom.mozApps.used", true);
 
     // We need to check permissions for calls coming from mozApps.mgmt.
-    // These are: getNotInstalled(), applyDownload() and uninstall().
+    // These are: getNotInstalled(), applyDownload(), uninstall(), import() and
+    // extractManifest().
     if (["Webapps:GetNotInstalled",
          "Webapps:ApplyDownload",
-         "Webapps:Uninstall"].indexOf(aMessage.name) != -1) {
+         "Webapps:Uninstall",
+         "Webapps:Import",
+         "Webapps:ExtractManifest"].indexOf(aMessage.name) != -1) {
       if (!aMessage.target.assertPermission("webapps-manage")) {
         debug("mozApps message " + aMessage.name +
         " from a content process with no 'webapps-manage' privileges.");
         return null;
       }
     }
     // And RegisterBEP requires "browser" permission...
     if ("Webapps:RegisterBEP" == aMessage.name) {
@@ -1299,16 +1308,25 @@ this.DOMApplicationRegistry = {
           this.removeReceipt(msg, mm);
           break;
         case "Webapps:ReplaceReceipt":
           this.replaceReceipt(msg, mm);
           break;
         case "Webapps:RegisterBEP":
           this.registerBrowserElementParentForApp(msg, mm);
           break;
+        case "Webapps:Export":
+          this.doExport(msg, mm);
+          break;
+        case "Webapps:Import":
+          this.doImport(msg, mm);
+          break;
+        case "Webapps:ExtractManifest":
+          this.doExtractManifest(msg, mm);
+          break;
       }
     });
   },
 
   getAppInfo: function getAppInfo(aAppId) {
     return AppsUtils.getAppInfo(this.webapps, aAppId);
   },
 
@@ -1366,18 +1384,20 @@ this.DOMApplicationRegistry = {
     let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                       .createInstance(Ci.nsIScriptableUnicodeConverter);
     converter.charset = "UTF-8";
 
     // Asynchronously copy the data to the file.
     let istream = converter.convertToInputStream(aData);
     NetUtil.asyncCopy(istream, ostream, function(aResult) {
       if (!Components.isSuccessCode(aResult)) {
-        deferred.reject()
+        debug("Error saving " + aPath + " : " + aResult);
+        deferred.reject(aResult)
       } else {
+        debug("Success saving " + aPath);
         deferred.resolve();
       }
     });
 
     return deferred.promise;
   },
 
   /**
@@ -1406,16 +1426,109 @@ this.DOMApplicationRegistry = {
 
     let thread = Services.tm.currentThread;
     while (!done) {
       thread.processNextEvent(/* mayWait */ true);
     }
     return { webapps: this.webapps, manifests: res };
   },
 
+  doExport: function(aMsg, aMm) {
+
+    function sendError(aError) {
+      aMm.sendAsyncMessage("Webapps:Export:Return",
+        { requestID: aMsg.requestID,
+          oid: aMsg.oid,
+          error: aError,
+          success: false
+        });
+    }
+
+    let app = this.getAppByManifestURL(aMsg.manifestURL);
+    if (!app) {
+      sendError("NoSuchApp");
+      return;
+    }
+
+    ImportExport.export(app).then(
+      aBlob => {
+        debug("exporting " + aBlob);
+        aMm.sendAsyncMessage("Webapps:Export:Return",
+          { requestID: aMsg.requestID,
+            oid: aMsg.oid,
+            blob: aBlob,
+            success: true
+          });
+      },
+      aError => sendError(aError));
+  },
+
+  doImport: function(aMsg, aMm) {
+    function sendError(aError) {
+      aMm.sendAsyncMessage("Webapps:Import:Return",
+        { requestID: aMsg.requestID,
+          oid: aMsg.oid,
+          error: aError,
+          success: false
+        });
+    }
+
+    if (!aMsg.blob || !aMsg.blob instanceof Ci.nsIDOMBlob) {
+      sendError("NoBlobFound");
+      return;
+    }
+
+    ImportExport.import(aMsg.blob).then(
+      ([aManifestURL, aManifest]) => {
+        let app = this.getAppByManifestURL(aManifestURL);
+        app.manifest = aManifest;
+        aMm.sendAsyncMessage("Webapps:Import:Return",
+          { requestID: aMsg.requestID,
+            oid: aMsg.oid,
+            app: app,
+            success: true
+          });
+      },
+      aError => sendError(aError));
+  },
+
+  doExtractManifest: function(aMsg, aMm) {
+    function sendError() {
+      aMm.sendAsyncMessage("Webapps:ExtractManifest:Return",
+        { requestID: aMsg.requestID,
+          oid: aMsg.oid,
+          error: aError,
+          success: false
+        });
+    }
+
+    if (!aMsg.blob || !aMsg.blob instanceof Ci.nsIDOMBlob) {
+      sendError("NoBlobFound");
+      return;
+    }
+
+    ImportExport.extractManifest(aMsg.blob).then(
+      aManifest => {
+        aMm.sendAsyncMessage("Webapps:ExtractManifest:Return",
+          { requestID: aMsg.requestID,
+            oid: aMsg.oid,
+            manifest: aManifest,
+            success: true
+          });
+      },
+      aError => {
+        aMm.sendAsyncMessage("Webapps:ExtractManifest:Return",
+          { requestID: aMsg.requestID,
+            oid: aMsg.oid,
+            error: aError,
+            success: false
+          });
+      }
+    );
+  },
 
   doLaunch: function (aData, aMm) {
     this.launch(
       aData.manifestURL,
       aData.startPoint,
       aData.timestamp,
       function onsuccess() {
         aMm.sendAsyncMessage("Webapps:Launch:Return:OK", aData);
--- a/dom/apps/moz.build
+++ b/dom/apps/moz.build
@@ -37,16 +37,17 @@ EXTRA_JS_MODULES += [
     'OfflineCacheInstaller.jsm',
     'PermissionsInstaller.jsm',
     'PermissionsTable.jsm',
     'StoreTrustAnchor.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'AppsUtils.jsm',
+    'ImportExport.jsm',
     'OperatorApps.jsm',
     'ScriptPreloader.jsm',
     'TrustedHostedAppsUtils.jsm',
     'Webapps.jsm',
 ]
 
 FAIL_ON_WARNINGS = True
 
--- a/dom/apps/tests/mochitest.ini
+++ b/dom/apps/tests/mochitest.ini
@@ -19,16 +19,17 @@ support-files =
   signed_app_template.webapp
   signed/*
   test_packaged_app_common.js
   marketplace/*
   pkg_install_iframe.html
 
 [test_app_update.html]
 [test_bug_795164.html]
+[test_import_export.html]
 [test_install_multiple_apps_origin.html]
 [test_install_receipts.html]
 [test_marketplace_pkg_install.html]
 skip-if = buildapp == "b2g" || toolkit == "android" # see bug 989806
 [test_packaged_app_install.html]
 [test_packaged_app_update.html]
 [test_receipt_operations.html]
 [test_signed_pkg_install.html]
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/test_import_export.html
@@ -0,0 +1,315 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id={982874}
+-->
+<head>
+  <title>Test for Bug {982874}</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="test_packaged_app_common.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id={982874}">Mozilla Bug {982874}</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+var gManifestURL = "http://test/tests/dom/apps/tests/file_app.sjs?apptype=hosted&getmanifest=true";
+var gGenerator = runTest();
+
+function runApp(aApp, aCallback) {
+  var ifr = document.createElement('iframe');
+  ifr.setAttribute('mozbrowser', 'true');
+  ifr.setAttribute('mozapp', aApp.manifestURL);
+  ifr.src = aApp.origin + aApp.manifest.launch_path;
+
+  ifr.addEventListener('mozbrowsershowmodalprompt', function onAlert(e) {
+    var message = e.detail.message;
+    info("Got message " + message);
+
+    if (message.startsWith("OK: ")) {
+      ok(true, message.substring(4, message.length));
+    } else if (message.startsWith("ERROR: ")) {
+      ok(false, message.substring(7, message.length));
+    } else if (message == "DONE") {
+      ifr.removeEventListener('mozbrowsershowmodalprompt', onAlert, false);
+      document.body.removeChild(ifr);
+      aCallback();
+    }
+  }, false);
+
+  document.body.appendChild(ifr);
+}
+
+function go() {
+  SpecialPowers.pushPermissions(
+    [{ "type": "webapps-manage", "allow": 1, "context": document },
+     { "type": "browser", "allow": 1, "context": document },
+     { "type": "embed-apps", "allow": 1, "context": document }],
+    function() {
+      SpecialPowers.pushPrefEnv({'set': [["dom.mozBrowserFramesEnabled", true]]},  continueTest) });
+}
+
+function continueTest() {
+  try {
+    gGenerator.next();
+  } catch (e if e instanceof StopIteration) {
+    finish();
+  }
+}
+
+function finish() {
+  SimpleTest.finish();
+}
+
+function cbError(aEvent) {
+  ok(false, "Error callback invoked " +
+            aEvent.target.error.name + " " + aEvent.target.error.message);
+  finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+/**
+  * Install 2 apps from the same origin and uninstall them.
+  */
+function runTest() {
+  SpecialPowers.setAllAppsLaunchable(true);
+
+  SpecialPowers.autoConfirmAppInstall(continueTest);
+  yield undefined;
+
+  SpecialPowers.autoConfirmAppUninstall(continueTest);
+  yield undefined;
+
+  // Check how many apps we are starting with.
+  request = navigator.mozApps.mgmt.getAll();
+  request.onerror = cbError;
+  request.onsuccess = continueTest;
+  yield undefined;
+  var initialAppsCount = request.result.length;
+  info("Starting with " + initialAppsCount + " apps installed.");
+
+  // Install a hosted app.
+  var request = navigator.mozApps.install(gManifestURL, { });
+  request.onerror = cbError;
+  request.onsuccess = continueTest;
+  yield undefined;
+
+  var app = request.result;
+  ok(app, "App is non-null");
+  is(app.manifestURL, gManifestURL, "App manifest url is correct.");
+
+  // Export the hosted app.
+  var exported;
+
+  app.export().then(blob => {
+    exported = blob;
+    ok(blob !== null, "Exported blob is not null");
+    ok(blob.size > 0, "Exported blob size is > 0");
+    continueTest();
+  }, error => {
+    info(error);
+    ok(false, "Export 1 should not fail");
+  });
+
+  yield undefined;
+
+  // Try to import the same blob. That will fails since the app is
+  // already installed.
+  navigator.mozApps.mgmt.import(exported)
+    .then((app) => {
+      ok(false, "Can't import an app already installed!");
+    }, (error) => {
+      is(error.name, "AppAlreadyInstalled", "Reject import of already installed app.");
+      continueTest();
+    }
+  );
+
+  yield undefined;
+
+  // Get some information from the manifest.
+  navigator.mozApps.mgmt.extractManifest(exported)
+    .then((manifest) => {
+      is(manifest.name, "Really Rapid Release (hosted)",
+         "Check manifest.name");
+      continueTest();
+    }, (error) => {
+      ok(false, "Should not fail to extract the manifest.");
+    }
+  );
+
+  yield undefined;
+
+  // Uninstall the hosted app.
+  navigator.mozApps.mgmt.onuninstall = function(event) {
+    var app = event.application;
+    is(app.manifestURL, gManifestURL, "App uninstall event ok.");
+    is(app.manifest.name, "Really Rapid Release (hosted)",
+       "App uninstall manifest ok.");
+    continueTest();
+  }
+  request = navigator.mozApps.mgmt.uninstall(app);
+  request.onerror = cbError;
+  request.onsuccess = continueTest;
+  yield undefined;
+  yield undefined;
+  is(request.result, gManifestURL, "Hosted App uninstalled.");
+  navigator.mozApps.mgmt.onuninstall = null;
+
+  // Re-import the app. This time this will succeed.
+  navigator.mozApps.mgmt.import(exported)
+    .then((imported) => {
+      ok(imported !== null, "Imported app is not null.");
+      is(imported.manifest.name, "Really Rapid Release (hosted)",
+         "Verifying manifest name");
+      app = imported;
+      continueTest();
+    }, (error) => {
+      ok(false, "We should not fail to import!");
+    }
+  );
+
+  yield undefined;
+
+  // Launch the imported hosted app.
+  info("Running " + app.manifestURL);
+  runApp(app, continueTest);
+  yield undefined;
+
+  // Uninstall the imported app to cleanup after ourself.
+  navigator.mozApps.mgmt.onuninstall = function(event) {
+    var app = event.application;
+    is(app.manifestURL, gManifestURL, "App uninstall event ok.");
+    is(app.manifest.name, "Really Rapid Release (hosted)",
+       "App uninstall manifest ok.");
+    continueTest();
+  }
+  request = navigator.mozApps.mgmt.uninstall(app);
+  request.onerror = cbError;
+  request.onsuccess = continueTest;
+  yield undefined;
+  yield undefined;
+  is(request.result, gManifestURL, "Hosted App imported uninstalled.");
+  navigator.mozApps.mgmt.onuninstall = null;
+
+  // Install a packaged app.
+  PackagedTestHelper.setAppVersion(2, continueTest);
+
+  yield undefined;
+
+  var miniManifestURL = PackagedTestHelper.gSJS +
+                        "?getManifest=true";
+  info("Install packaged app from " + miniManifestURL);
+  navigator.mozApps.mgmt.oninstall = function(evt) {
+    info("Got oninstall event");
+    PackagedTestHelper.gApp = evt.application;
+    PackagedTestHelper.gApp.ondownloaderror = function() {
+      ok(false, "Package Download error " +
+                PackagedTestHelper.gApp.downloadError.name);
+      PackagedTestHelper.finish();
+    };
+    PackagedTestHelper.gApp.ondownloadsuccess = function() {
+      info("Packaged App downloaded");
+      var expected = {
+        name: PackagedTestHelper.gAppName,
+        manifestURL: miniManifestURL,
+        installOrigin: PackagedTestHelper.gInstallOrigin,
+        progress: 0,
+        installState: "installed",
+        downloadAvailable: false,
+        downloading: false,
+        downloadSize: 0,
+        size: 0,
+        readyToApplyDownload: false
+      };
+      PackagedTestHelper.checkAppState(PackagedTestHelper.gApp, 2, expected,
+                                       true, false, continueTest);
+    };
+  };
+
+  var request = navigator.mozApps.installPackage(miniManifestURL);
+  request.onerror = PackagedTestHelper.mozAppsError;
+  request.onsuccess = function() {
+    info("Packaged Application installed");
+  };
+
+  yield undefined;
+
+  // Export the packaged app.
+  PackagedTestHelper.gApp.export().then((blob) => {
+    exported = blob;
+    ok(blob !== null, "Exported blob is not null");
+    info("blob size is " + blob.size);
+    ok(blob.size > 0, "Exported blob size is > 0");
+    continueTest();
+  }, (error) => {
+    ok(false, "Export 2 should not fail");
+  });
+
+  yield undefined;
+
+  // Uninstall the packaged app.
+  navigator.mozApps.mgmt.onuninstall = function(event) {
+    var app = event.application;
+    is(app.manifestURL, miniManifestURL, "Packaged App uninstall event ok.");
+    is(app.manifest.name, "appname", "Packaged App uninstall manifest ok.");
+    continueTest();
+  }
+  request = navigator.mozApps.mgmt.uninstall(PackagedTestHelper.gApp);
+  request.onerror = cbError;
+  request.onsuccess = continueTest;
+  yield undefined;
+  yield undefined;
+  is(request.result, miniManifestURL, "Packaged App uninstalled.");
+  navigator.mozApps.mgmt.onuninstall = null;
+
+  // Import the packaged app.
+  navigator.mozApps.mgmt.import(exported)
+    .then((imported) => {
+      ok(imported !== null, "Imported app is not null.");
+      is(imported.manifest.name, "appname", "Verifying imported app name");
+      app = imported;
+      continueTest();
+    }, (error) => {
+      ok(false, "We should not fail to import!");
+    }
+  );
+
+  yield undefined;
+
+  // Uninstall the imported packaged app.
+  navigator.mozApps.mgmt.onuninstall = function(event) {
+    var app = event.application;
+    is(app.manifestURL, miniManifestURL, "Packaged App uninstall event ok.");
+    is(app.manifest.name, "appname", "Packaged App uninstall manifest ok.");
+    continueTest();
+  }
+  request = navigator.mozApps.mgmt.uninstall(PackagedTestHelper.gApp);
+  request.onerror = cbError;
+  request.onsuccess = continueTest;
+  yield undefined;
+  yield undefined;
+  is(request.result, miniManifestURL, "Packaged App uninstalled.");
+  navigator.mozApps.mgmt.onuninstall = null;
+
+  // Check that we restored the app registry.
+  request = navigator.mozApps.mgmt.getAll();
+  request.onerror = cbError;
+  request.onsuccess = continueTest;
+  yield undefined;
+  is(request.result.length, initialAppsCount, "All apps are uninstalled.");
+}
+
+addLoadEvent(go);
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/tests/mochitest/webapps/test_list_api.xul
+++ b/dom/tests/mochitest/webapps/test_list_api.xul
@@ -42,16 +42,18 @@ var mgmtProps = {
   getAll: "function",
   getNotInstalled: "function",
   uninstall: "function",
   oninstall: "object",
   onuninstall: "object",
   ownerGlobal: "object",
   removeEventListener: "function",
   setEventHandler: "function",
+  extractManifest: "function",
+  import: "function"
 };
 
 isDeeply([p for (p in navigator.mozApps.mgmt)].sort(),
          Object.keys(mgmtProps).sort(),
          "navigator.mozApps.mgmt has only the expected properties");
 
 for (var p in mgmtProps) {
   is(typeof navigator.mozApps.mgmt[p], mgmtProps[p], "typeof mgmt." + p);
--- a/dom/webidl/Apps.webidl
+++ b/dom/webidl/Apps.webidl
@@ -74,22 +74,28 @@ interface DOMApplication : EventTarget {
 
    Promise<sequence<MozInterAppMessagePort>> getConnections();
 
     // Receipts handling functions.
     DOMRequest addReceipt(optional DOMString receipt);
     DOMRequest removeReceipt(optional DOMString receipt);
     DOMRequest replaceReceipt(optional DOMString oldReceipt,
                               optional DOMString newReceipt);
+
+    // Export this app as a shareable Blob.
+    Promise<Blob> export();
 };
 
 [JSImplementation="@mozilla.org/webapps/manager;1",
  ChromeOnly,
  CheckPermissions="webapps-manage"]
 interface DOMApplicationsManager : EventTarget {
   DOMRequest getAll();
   DOMRequest getNotInstalled();
   void applyDownload(DOMApplication app);
   DOMRequest uninstall(DOMApplication app);
 
+  Promise<DOMApplication> import(Blob blob);
+  Promise<any> extractManifest(Blob blob);
+
   attribute EventHandler oninstall;
   attribute EventHandler onuninstall;
 };