Bug 777402 - Implement support for packaged apps via the installPackage function in the mozapps DOM API in desktop web runtime. r=myk,wesj,fabrice
☠☠ backed out by f57e629bb4c3 ☠ ☠
authorMarco Castelluccio <mar.castelluccio@studenti.unina.it>
Thu, 01 Aug 2013 17:00:39 -0700
changeset 154191 357e720b5a377ecf0e6fa6da9a82059dc5f98581
parent 154190 f1971c2f523249e5a369724fc870160225921a6f
child 154192 f42f732ce1fd8d3748d0f4a6ccb27b2fce71de23
push id382
push userakeybl@mozilla.com
push dateMon, 21 Oct 2013 21:47:13 +0000
treeherdermozilla-release@5f1868ee45cb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmyk, wesj, fabrice
bugs777402
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 777402 - Implement support for packaged apps via the installPackage function in the mozapps DOM API in desktop web runtime. r=myk,wesj,fabrice
browser/modules/webappsUI.jsm
dom/apps/src/Webapps.js
dom/apps/src/Webapps.jsm
dom/apps/src/moz.build
dom/interfaces/apps/nsIDOMApplicationRegistry.idl
dom/interfaces/apps/nsIDOMApplicationRegistry2.idl
dom/tests/mochitest/webapps/test_install_errors.xul
dom/tests/mochitest/webapps/test_list_api.xul
mobile/android/chrome/content/browser.js
toolkit/webapps/WebappOSUtils.jsm
toolkit/webapps/WebappsInstaller.jsm
webapprt/CommandLineHandler.js
webapprt/WebappRT.jsm
webapprt/WebappsHandler.jsm
webapprt/test/chrome/head.js
--- a/browser/modules/webappsUI.jsm
+++ b/browser/modules/webappsUI.jsm
@@ -101,33 +101,40 @@ this.webappsUI = {
 
   doInstall: function(aData, aBrowser, aWindow) {
     let bundle = aWindow.gNavigatorBundle;
 
     let mainAction = {
       label: bundle.getString("webapps.install"),
       accessKey: bundle.getString("webapps.install.accesskey"),
       callback: function() {
-        let app = WebappsInstaller.install(aData);
+        let app = WebappsInstaller.init(aData);
+
         if (app) {
           let localDir = null;
           if (app.appProfile) {
             localDir = app.appProfile.localDir;
           }
 
-          DOMApplicationRegistry.confirmInstall(aData, false, localDir);
-          installationSuccessNotification(aData, app, aWindow);
+          DOMApplicationRegistry.confirmInstall(aData, false, localDir, null,
+            function (aManifest) {
+              if (WebappsInstaller.install(aData, aManifest)) {
+                installationSuccessNotification(aData, app, aWindow);
+              }
+            }
+          );
         } else {
           DOMApplicationRegistry.denyInstall(aData);
         }
       }
     };
 
     let requestingURI = aWindow.makeURI(aData.from);
-    let manifest = new ManifestHelper(aData.app.manifest, aData.app.origin);
+    let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest;
+    let manifest = new ManifestHelper(jsonManifest, aData.app.origin);
 
     let host;
     try {
       host = requestingURI.host;
     } catch(e) {
       host = requestingURI.spec;
     }
 
--- a/dom/apps/src/Webapps.js
+++ b/dom/apps/src/Webapps.js
@@ -113,51 +113,57 @@ WebappsRegistry.prototype = {
         Services.DOMRequest.fireError(aRequest, "BACKGROUND_APP");
       }
     }
     Services.tm.currentThread.dispatch(runnable,
                                        Ci.nsIThread.DISPATCH_NORMAL);
     return false;
   },
 
-  // mozIDOMApplicationRegistry implementation
-
-  install: function(aURL, aParams) {
-    let uri = this._validateURL(aURL);
-
-    let request = this.createRequest();
-
-    if (!this._ensureForeground(request)) {
-      return request;
-    }
-
+  _prepareInstall: function(aURL, aRequest, aParams, isPackage) {
     let installURL = this._window.location.href;
-    let requestID = this.getRequestId(request);
+    let requestID = this.getRequestId(aRequest);
     let receipts = (aParams && aParams.receipts &&
                     Array.isArray(aParams.receipts)) ? aParams.receipts
                                                      : [];
     let categories = (aParams && aParams.categories &&
                       Array.isArray(aParams.categories)) ? aParams.categories
                                                          : [];
 
     let principal = this._window.document.nodePrincipal;
-    cpmm.sendAsyncMessage("Webapps:Install",
-                          { app: {
-                              installOrigin: this._getOrigin(installURL),
-                              origin: this._getOrigin(uri),
-                              manifestURL: uri,
-                              receipts: receipts,
-                              categories: categories
-                            },
-                            from: installURL,
-                            oid: this._id,
-                            requestID: requestID,
-                            appId: principal.appId,
-                            isBrowser: principal.isInBrowserElement
-                          });
+
+    return { app: {
+                    installOrigin: this._getOrigin(installURL),
+                    origin: this._getOrigin(aURL),
+                    manifestURL: aURL,
+                    receipts: receipts,
+                    categories: categories
+                  },
+
+             from: installURL,
+             oid: this._id,
+             requestID: requestID,
+             appId: principal.appId,
+             isBrowser: principal.isInBrowserElement,
+             isPackage: isPackage
+           };
+  },
+
+  // mozIDOMApplicationRegistry implementation
+
+  install: function(aURL, aParams) {
+    let uri = this._validateURL(aURL);
+
+    let request = this.createRequest();
+
+    if (this._ensureForeground(request)) {
+      cpmm.sendAsyncMessage("Webapps:Install",
+                            this._prepareInstall(uri, request, aParams, false));
+    }
+
     return request;
   },
 
   getSelf: function() {
     let request = this.createRequest();
     cpmm.sendAsyncMessage("Webapps:GetSelf", { origin: this._getOrigin(this._window.location.href),
                                                appId: this._window.document.nodePrincipal.appId,
                                                oid: this._id,
@@ -196,52 +202,26 @@ WebappsRegistry.prototype = {
   },
 
   uninit: function() {
     this._mgmt = null;
     cpmm.sendAsyncMessage("Webapps:UnregisterForMessages",
                           ["Webapps:Install:Return:OK"]);
   },
 
-  // mozIDOMApplicationRegistry2 implementation
-
   installPackage: function(aURL, aParams) {
     let uri = this._validateURL(aURL);
 
     let request = this.createRequest();
 
-    if (!this._ensureForeground(request)) {
-      return request;
+    if (this._ensureForeground(request)) {
+      cpmm.sendAsyncMessage("Webapps:InstallPackage",
+                            this._prepareInstall(uri, request, aParams, true));
     }
 
-    let installURL = this._window.location.href;
-    let requestID = this.getRequestId(request);
-    let receipts = (aParams && aParams.receipts &&
-                    Array.isArray(aParams.receipts)) ? aParams.receipts
-                                                     : [];
-    let categories = (aParams && aParams.categories &&
-                      Array.isArray(aParams.categories)) ? aParams.categories
-                                                         : [];
-
-    let principal = this._window.document.nodePrincipal;
-    cpmm.sendAsyncMessage("Webapps:InstallPackage",
-                          { app: {
-                              installOrigin: this._getOrigin(installURL),
-                              origin: this._getOrigin(uri),
-                              manifestURL: uri,
-                              receipts: receipts,
-                              categories: categories
-                            },
-                            from: installURL,
-                            oid: this._id,
-                            requestID: requestID,
-                            isPackage: true,
-                            appId: principal.appId,
-                            isBrowser: principal.isInBrowserElement
-                          });
     return request;
   },
 
   // nsIDOMGlobalPropertyInitializer implementation
   init: function(aWindow) {
     this.initDOMRequestHelper(aWindow, ["Webapps:Install:Return:OK", "Webapps:Install:Return:KO",
                               "Webapps:GetInstalled:Return:OK",
                               "Webapps:GetSelf:Return:OK",
@@ -261,32 +241,23 @@ WebappsRegistry.prototype = {
     // the mgmt object.
     this.hasMgmtPrivilege = perm == Ci.nsIPermissionManager.ALLOW_ACTION;
   },
 
   classID: Components.ID("{fff440b3-fae2-45c1-bf03-3b5a2e432270}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
                                          Ci.mozIDOMApplicationRegistry,
-#ifdef MOZ_B2G
                                          Ci.mozIDOMApplicationRegistry2,
-#elifdef MOZ_WIDGET_ANDROID
-                                         Ci.mozIDOMApplicationRegistry2,
-#endif
                                          Ci.nsIDOMGlobalPropertyInitializer]),
 
   classInfo: XPCOMUtils.generateCI({classID: Components.ID("{fff440b3-fae2-45c1-bf03-3b5a2e432270}"),
                                     contractID: "@mozilla.org/webapps;1",
                                     interfaces: [Ci.mozIDOMApplicationRegistry,
-#ifdef MOZ_B2G
-                                                 Ci.mozIDOMApplicationRegistry2,
-#elifdef MOZ_WIDGET_ANDROID
-                                                 Ci.mozIDOMApplicationRegistry2,
-#endif
-                                                 ],
+                                                 Ci.mozIDOMApplicationRegistry2],
                                     flags: Ci.nsIClassInfo.DOM_OBJECT,
                                     classDescription: "Webapps Registry"})
 }
 
 /**
   * mozIDOMApplication object
   */
 
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -1836,25 +1836,16 @@ this.DOMApplicationRegistry = {
 
     let sendError = function sendError(aError) {
       aData.error = aError;
       aMm.sendAsyncMessage("Webapps:Install:Return:KO", aData);
       Cu.reportError("Error installing packaged app from: " +
                      app.installOrigin + ": " + aError);
     }.bind(this);
 
-    // Disallow reinstalls from the same manifest URL for now.
-    // See https://bugzilla.mozilla.org/show_bug.cgi?id=821288
-    if (this.getAppLocalIdByManifestURL(app.manifestURL) !==
-        Ci.nsIScriptSecurityManager.NO_APP_ID ||
-        this._appId(app.origin) !== null) {
-      sendError("REINSTALL_FORBIDDEN");
-      return;
-    }
-
     let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
                 .createInstance(Ci.nsIXMLHttpRequest);
     xhr.open("GET", app.manifestURL, true);
     xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
     xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId,
                                                                aData.isBrowser);
     xhr.responseType = "json";
 
@@ -1867,16 +1858,23 @@ this.DOMApplicationRegistry = {
         }
 
         let manifest = app.updateManifest = xhr.response;
         if (!manifest) {
           sendError("MANIFEST_PARSE_ERROR");
           return;
         }
 
+        // Disallow reinstalls from the same manifest URL for now.
+        if (this._appIdForManifestURL(app.manifestURL) !== null &&
+            this._isLaunchable(app)) {
+          sendError("REINSTALL_FORBIDDEN");
+          return;
+        }
+
         if (!(AppsUtils.checkManifest(manifest, app) &&
               manifest.package_path)) {
           sendError("INVALID_MANIFEST");
         } else if (!AppsUtils.checkInstallAllowed(manifest, app.installOrigin)) {
           sendError("INSTALL_FROM_DENIED");
         } else {
           app.etag = xhr.getResponseHeader("Etag");
           app.manifestHash = this.computeManifestHash(manifest);
@@ -1983,17 +1981,17 @@ this.DOMApplicationRegistry = {
     appObject.installTime = app.installTime = Date.now();
     appObject.lastUpdateCheck = app.lastUpdateCheck = Date.now();
     let appNote = JSON.stringify(appObject);
     appNote.id = id;
 
     appObject.id = id;
     appObject.localId = localId;
     appObject.basePath = FileUtils.getDir(DIRECTORY_NAME, ["webapps"], true, true).path;
-    let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true);
+    let dir = this._getAppDir(id);
     let manFile = dir.clone();
     manFile.append(manifestName);
     let jsonManifest = aData.isPackage ? app.updateManifest : app.manifest;
     this._writeFile(manFile, JSON.stringify(jsonManifest), function() { });
 
     let manifest = new ManifestHelper(jsonManifest, app.origin);
 
     if (manifest.appcache_path) {
@@ -2057,29 +2055,29 @@ this.DOMApplicationRegistry = {
         this.broadcastMessage("Webapps:Install:Return:OK", aData);
         Services.obs.notifyObservers(this, "webapps-sync-install", appNote);
       }).bind(this));
     }
 
     if (!aData.isPackage) {
       this.updateAppHandlers(null, app.manifest, app);
       if (aInstallSuccessCallback) {
-        aInstallSuccessCallback(manifest);
+        aInstallSuccessCallback(app.manifest);
       }
     }
 
     if (manifest.package_path) {
       // origin for install apps is meaningless here, since it's app:// and this
       // can't be used to resolve package paths.
       manifest = new ManifestHelper(jsonManifest, app.manifestURL);
       this.downloadPackage(manifest, appObject, false, (function(aId, aManifest) {
         // Success! Move the zip out of TmpD.
         let app = DOMApplicationRegistry.webapps[aId];
         let zipFile = FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true);
-        let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true);
+        let dir = this._getAppDir(id);
         zipFile.moveTo(dir, "application.zip");
         let tmpDir = FileUtils.getDir("TmpD", ["webapps", aId], true, true);
         try {
           tmpDir.remove(true);
         } catch(e) { }
 
         // Save the manifest
         let manFile = dir.clone();
--- a/dom/apps/src/moz.build
+++ b/dom/apps/src/moz.build
@@ -2,23 +2,20 @@
 # vim: set filetype=python:
 # 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/.
 
 EXTRA_COMPONENTS += [
     'AppsService.js',
     'AppsService.manifest',
+    'Webapps.js',
     'Webapps.manifest',
 ]
 
-EXTRA_PP_COMPONENTS += [
-    'Webapps.js',
-]
-
 EXTRA_JS_MODULES += [
     'AppDownloadManager.jsm',
     'AppsServiceChild.jsm',
     'FreeSpaceWatcher.jsm',
     'OfflineCacheInstaller.jsm',
     'PermissionsInstaller.jsm',
     'PermissionsTable.jsm',
 ]
--- a/dom/interfaces/apps/nsIDOMApplicationRegistry.idl
+++ b/dom/interfaces/apps/nsIDOMApplicationRegistry.idl
@@ -133,17 +133,17 @@ interface mozIDOMApplicationMgmt : nsISu
    *
    * @param app : the app object of the web app to be uninstalled.
    * @returns   : A DOMRequest object, returning the app's origin in |result|
    *              if uninstall succeeds; returning "NOT_INSTALLED" error otherwise.
    */
   nsIDOMDOMRequest uninstall(in mozIDOMApplication app);
 };
 
-[scriptable, uuid(abfc6c15-8b92-4b9a-b892-52e6ae76f379)]
+[scriptable, uuid(52710c5f-b2a2-4b27-b5b9-f679a1bcc79b)]
 interface mozIDOMApplicationRegistry : nsISupports
 {
   /**
    * Install a web app.
    *
    * @param manifestUrl : the URL of the webapps manifest.
    * @param parameters  : A structure with optional information.
    *                      {
@@ -164,10 +164,23 @@ interface mozIDOMApplicationRegistry : n
    */
   nsIDOMDOMRequest checkInstalled(in DOMString manifestUrl);
 
   /**
    * the request will return the applications installed from this origin, or null.
    */
   nsIDOMDOMRequest getInstalled();
 
+  /**
+   * Install a packaged web app.
+   *
+   * @param packageUrl : the URL of the webapps manifest.
+   * @param parameters : A structure with optional information.
+   *                      {
+   *                       receipts: ...    Will be used to specify the payment receipts for this installation.
+   *                       categories: ...  Will be used to specify the categories of the webapp.
+   *                      }
+   * @returns          : A DOMRequest object, returning the app object in |result| if install succeeds.
+   */
+  nsIDOMDOMRequest installPackage(in DOMString packageUrl, [optional] in jsval parameters);
+
   readonly attribute mozIDOMApplicationMgmt mgmt;
 };
--- a/dom/interfaces/apps/nsIDOMApplicationRegistry2.idl
+++ b/dom/interfaces/apps/nsIDOMApplicationRegistry2.idl
@@ -1,24 +1,13 @@
 /* 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/. */
 
 #include "nsIDOMApplicationRegistry.idl"
 
 interface nsIDOMDOMRequest;
 
-[scriptable, uuid(34498a66-3aee-4b80-8b8b-a9c5d5ba32b6)]
+// This interface is still here for backwards compatibility.
+[scriptable, uuid(5bd838b2-cf3d-406e-bbef-f633cf9e68de)]
 interface mozIDOMApplicationRegistry2 : mozIDOMApplicationRegistry
 {
-  /**
-   * Install a packaged web app.
-   *
-   * @param packageUrl : the URL of the webapps manifest.
-   * @param parameters : A structure with optional information.
-   *                      {
-   *                       receipts: ...    Will be used to specify the payment receipts for this installation.
-   *                       categories: ...  Will be used to specify the categories of the webapp.
-   *                      }
-   * @returns          : A DOMRequest object, returning the app object in |result| if install succeeds.
-   */
-  nsIDOMDOMRequest installPackage(in DOMString packageUrl, [optional] in jsval parameters);
 };
--- a/dom/tests/mochitest/webapps/test_install_errors.xul
+++ b/dom/tests/mochitest/webapps/test_install_errors.xul
@@ -19,17 +19,16 @@
 <script>
 
 var steps = [
   noArgs,
   parseError,
   invalidManifest,
   permissionDenied,
   invalidContent,
-  installPackageNotImplemented,
   invalidLaunchPath,
   invalidLaunchPath2,
   invalidEntryPoint,
   invalidLocaleEntryPoint,
   invalidActivityHref,
   invalidActivityHref2,
   invalidMessage,
   fileURL,
@@ -162,22 +161,16 @@ function invalidMessage(next) {
   var url = "http://test/chrome/dom/tests/mochitest/webapps/apps/invalid_message.webapp";
 
   navigator.mozApps.install(url, null).onerror = function onInstallError() {
     is(this.error.name, "INVALID_MANIFEST", "Manifest has absolute message href");
     next();
   };
 }
 
-function installPackageNotImplemented(next) {
-  ok(!("installPackage" in navigator.mozApps),
-     "installPackage not in navigator.mozApps");
-  next();
-}
-
 function fileURL(next) {
   try {
     navigator.mozApps.install("file:///nonexistent");
     ok(false,
        "attempt to install nonexistent file: URL doesn't throw exception");
   } catch(ex) {
     is(ex.message, "INVALID_URL_SCHEME: 'file'; must be 'http' or 'https'",
        "attempt to install nonexistent file: URL throws exception");
--- a/dom/tests/mochitest/webapps/test_list_api.xul
+++ b/dom/tests/mochitest/webapps/test_list_api.xul
@@ -19,16 +19,17 @@
 <script>
 
 var props = {
   QueryInterface: "function",
   checkInstalled: "function",
   getInstalled: "function",
   getSelf: "function",
   install: "function",
+  installPackage: "function",
   mgmt: "object",
 };
 
 isDeeply([p for (p in navigator.mozApps)].sort(), Object.keys(props).sort(),
          "navigator.mozApps has only the expected properties");
 
 for (var p in props) {
   is(typeof navigator.mozApps[p], props[p], "typeof " + p);
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -6714,35 +6714,36 @@ var WebappsUI = {
     }
 
     return iconURI ? iconURI.spec : DEFAULT_ICON;
   },
 
   doInstall: function doInstall(aData) {
     let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest;
     let manifest = new ManifestHelper(jsonManifest, aData.app.origin);
-    let name = manifest.name ? manifest.name : manifest.fullLaunchPath();
     let showPrompt = true;
 
-    if (!showPrompt || Services.prompt.confirm(null, Strings.browser.GetStringFromName("webapps.installTitle"), name + "\n" + aData.app.origin)) {
+    if (!showPrompt || Services.prompt.confirm(null, Strings.browser.GetStringFromName("webapps.installTitle"), manifest.name + "\n" + aData.app.origin)) {
       // Get a profile for the app to be installed in. We'll download everything before creating the icons.
       let origin = aData.app.origin;
       let profilePath = sendMessageToJava({
         type: "WebApps:PreInstall",
         name: manifest.name,
         manifestURL: aData.app.manifestURL,
         origin: origin
       });
       if (profilePath) {
         let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
         file.initWithPath(profilePath);
-  
+
         let self = this;
         DOMApplicationRegistry.confirmInstall(aData, false, file, null,
-          function (manifest) {
+          function (aManifest) {
+            let localeManifest = new ManifestHelper(aManifest, aData.app.origin);
+
             // the manifest argument is the manifest from within the zip file,
             // TODO so now would be a good time to ask about permissions.
             self.makeBase64Icon(self.getBiggestIcon(manifest.icons, Services.io.newURI(aData.app.origin, null, null)),
               function(scaledIcon, fullsizeIcon) {
                 // if java returned a profile path to us, try to use it to pre-populate the app cache
                 // also save the icon so that it can be used in the splash screen
                 try {
                   let iconFile = file.clone();
@@ -6752,36 +6753,36 @@ var WebappsUI = {
                   persist.persistFlags |= Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
 
                   let source = Services.io.newURI(fullsizeIcon, "UTF8", null);
                   persist.saveURI(source, null, null, null, null, iconFile, null);
 
                   // aData.app.origin may now point to the app: url that hosts this app
                   sendMessageToJava({
                     type: "WebApps:PostInstall",
-                    name: manifest.name,
+                    name: localeManifest.name,
                     manifestURL: aData.app.manifestURL,
                     originalOrigin: origin,
                     origin: aData.app.origin,
                     iconURL: fullsizeIcon
                   });
                   if (!!aData.isPackage) {
                     // For packaged apps, put a notification in the notification bar.
                     let message = Strings.browser.GetStringFromName("webapps.alertSuccess");
                     let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
-                    alerts.showAlertNotification("drawable://alert_app", manifest.name, message, true, "", {
+                    alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", {
                       observe: function () {
                         self.openURL(aData.app.manifestURL, aData.app.origin);
                       }
                     }, "webapp");
                   }
                 } catch(ex) {
                   console.log(ex);
                 }
-                self.writeDefaultPrefs(file, manifest);
+                self.writeDefaultPrefs(file, localeManifest);
               }
             );
           }
         );
       }
     } else {
       DOMApplicationRegistry.denyInstall(aData);
     }
--- a/toolkit/webapps/WebappOSUtils.jsm
+++ b/toolkit/webapps/WebappOSUtils.jsm
@@ -16,23 +16,28 @@ this.EXPORTED_SYMBOLS = ["WebappOSUtils"
 this.WebappOSUtils = {
   getUniqueName: function(aApp) {
     let name;
 
     // During the installation of a new app, the aApp object
     // doesn't have a name property. We then need to use the manifest.
     // For some mozApps calls, the aApp object doesn't have a manifest
     // associated, and so we need to use the name property.
+    // They're guaranteed to be always identical to the application
+    // name in the user locale.
     if (aApp.name) {
       name = aApp.name;
     } else {
-      name = aApp.manifest.name;
+      let manifest =
+        new ManifestHelper(aApp.updateManifest || aApp.manifest, aApp.origin);
+      name = manifest.name;
     }
 
-    return this.sanitizeStringForFilename(name).toLowerCase() + "-" + AppsUtils.computeHash(aApp.manifestURL);
+    return this.sanitizeStringForFilename(name).toLowerCase() + "-" +
+           AppsUtils.computeHash(aApp.manifestURL);
   },
 
   /**
    * Returns the executable of the given app, identifying it by its unique name,
    * which is in either the new format or the old format.
    * On Mac OS X, it returns the identifier of the app.
    *
    * The new format ensures a readable and unique name for an app by combining
--- a/toolkit/webapps/WebappsInstaller.jsm
+++ b/toolkit/webapps/WebappsInstaller.jsm
@@ -12,122 +12,183 @@ const Cr = Components.results;
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/WebappOSUtils.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 
 this.WebappsInstaller = {
+  shell: null,
+
+  /**
+   * Initializes the app object that takes care of the installation
+   * and creates the profile directory for an application
+   *
+   * @param aData the data provided to the install function
+   *
+   * @returns NativeApp on success, null on error
+   */
+  init: function(aData) {
+#ifdef XP_WIN
+    this.shell = new WinNativeApp(aData);
+#elifdef XP_MACOSX
+    this.shell = new MacNativeApp(aData);
+#elifdef XP_UNIX
+    this.shell = new LinuxNativeApp(aData);
+#else
+    return null;
+#endif
+
+    try {
+      if (Services.prefs.getBoolPref("browser.mozApps.installer.dry_run")) {
+        return this.shell;
+      }
+    } catch (ex) {}
+
+    try {
+      this.shell.createAppProfile();
+    } catch (ex) {
+      Cu.reportError("Error installing app: " + ex);
+      return null;
+    }
+
+    return this.shell;
+  },
+
   /**
    * Creates a native installation of the web app in the OS
    *
-   * @param aData the manifest data provided by the web app
+   * @param aData the data provided to the install function
+   * @param aManifest the manifest data provided by the web app
    *
-   * @returns bool true on success, false if an error was thrown
+   * @returns true on success, false if an error was thrown
    */
-  install: function(aData) {
-
+  install: function(aData, aManifest) {
     try {
       if (Services.prefs.getBoolPref("browser.mozApps.installer.dry_run")) {
         return true;
       }
     } catch (ex) {}
 
-#ifdef XP_WIN
-    let shell = new WinNativeApp(aData);
-#elifdef XP_MACOSX
-    let shell = new MacNativeApp(aData);
-#elifdef XP_UNIX
-    let shell = new LinuxNativeApp(aData);
-#else
-    return false;
-#endif
+    this.shell.init(aData, aManifest);
 
     try {
-      shell.install();
+      this.shell.install();
     } catch (ex) {
       Cu.reportError("Error installing app: " + ex);
-      return null;
+      return false;
     }
 
     let data = {
-      "installDir": shell.installDir.path,
-      "app": aData.app
+      "installDir": this.shell.installDir.path,
+      "app": {
+        "manifest": aManifest,
+        "origin": aData.app.origin
+      }
     };
     Services.obs.notifyObservers(null, "webapp-installed", JSON.stringify(data));
 
-    return shell;
+    return true;
   }
 }
 
 /**
  * This function implements the common constructor for
- * the Windows, Mac and Linux native app shells. It reads and parses
- * the data from the app manifest and stores it in the NativeApp
- * object. It's meant to be called as NativeApp.call(this, aData)
- * from the platform-specific constructor.
+ * the Windows, Mac and Linux native app shells. It sets
+ * the app unique name. It's meant to be called as
+ * NativeApp.call(this, aData) from the platform-specific
+ * constructor.
  *
- * @param aData the data object provided by the web app with
- *              all the app settings and specifications.
+ * @param aData the data object provided to the install function
  *
  */
 function NativeApp(aData) {
-  let app = this.app = aData.app;
+  this.uniqueName = WebappOSUtils.getUniqueName(aData.app);
+
+  let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest;
+  let manifest = new ManifestHelper(jsonManifest, aData.app.origin);
+
+  this.appName = sanitize(manifest.name);
+  this.appNameAsFilename = stripStringForFilename(this.appName);
+}
 
-  this.uniqueName = WebappOSUtils.getUniqueName(app);
-
-  let origin = Services.io.newURI(app.origin, null, null);
+NativeApp.prototype = {
+  uniqueName: null,
+  appName: null,
+  appNameAsFilename: null,
+  iconURI: null,
+  developerName: null,
+  shortDescription: null,
+  categories: null,
+  webappJson: null,
+  runtimeFolder: null,
 
-  let biggestIcon = getBiggestIconURL(app.manifest.icons);
-  try {
-    let iconURI = Services.io.newURI(biggestIcon, null, null);
-    if (iconURI.scheme == "data") {
-      this.iconURI = iconURI;
-    }
-  } catch (ex) {}
+  /**
+   * This function reads and parses the data from the app
+   * manifest and stores it in the NativeApp object.
+   *
+   * @param aData the data object provided to the install function
+   * @param aManifest the manifest data provided by the web app
+   *
+   */
+  init: function(aData, aManifest) {
+    let manifest = new ManifestHelper(aManifest, aData.app.origin);
 
-  if (!this.iconURI) {
+    let origin = Services.io.newURI(aData.app.origin, null, null);
+
+    let biggestIcon = getBiggestIconURL(manifest.icons);
     try {
-      this.iconURI = Services.io.newURI(origin.resolve(biggestIcon), null, null);
-    }
-    catch (ex) {}
-  }
-
-  this.appName = sanitize(app.manifest.name);
-  this.appNameAsFilename = stripStringForFilename(this.appName);
+      let iconURI = Services.io.newURI(biggestIcon, null, null);
+      if (iconURI.scheme == "data") {
+        this.iconURI = iconURI;
+      }
+    } catch (ex) {}
 
-  if(app.manifest.developer && app.manifest.developer.name) {
-    let devName = app.manifest.developer.name.substr(0, 128);
-    devName = sanitize(devName);
-    if (devName) {
-      this.developerName = devName;
+    if (!this.iconURI) {
+      try {
+        this.iconURI = Services.io.newURI(origin.resolve(biggestIcon), null, null);
+      }
+      catch (ex) {}
     }
-  }
+
+    if (manifest.developer && manifest.developer.name) {
+      let devName = sanitize(manifest.developer.name.substr(0, 128));
+      if (devName) {
+        this.developerName = devName;
+      }
+    }
 
-  let shortDesc = this.appName;
-  if (app.manifest.description) {
-    let firstLine = app.manifest.description.split("\n")[0];
-    shortDesc = firstLine.length <= 256
-                ? firstLine
-                : firstLine.substr(0, 253) + "...";
-  }
-  this.shortDescription = sanitize(shortDesc);
+    if (manifest.description) {
+      let firstLine = manifest.description.split("\n")[0];
+      let shortDesc = firstLine.length <= 256
+                      ? firstLine
+                      : firstLine.substr(0, 253) + "…";
+      this.shortDescription = sanitize(shortDesc);
+    } else {
+      this.shortDescription = this.appName;
+    }
+
+    this.categories = aData.app.categories.slice(0);
 
-  // The app registry is the Firefox profile from which the app
-  // was installed.
-  this.registryFolder = Services.dirsvc.get("ProfD", Ci.nsIFile);
+    // The app registry is the Firefox profile from which the app
+    // was installed.
+    let registryFolder = Services.dirsvc.get("ProfD", Ci.nsIFile);
 
-  this.webappJson = {
-    "registryDir": this.registryFolder.path,
-    "app": app
-  };
+    this.webappJson = {
+      "registryDir": registryFolder.path,
+      "app": {
+        "manifest": aManifest,
+        "origin": aData.app.origin
+      }
+    };
 
-  this.runtimeFolder = Services.dirsvc.get("GreD", Ci.nsIFile);
-}
+    this.runtimeFolder = Services.dirsvc.get("GreD", Ci.nsIFile);
+  }
+};
 
 #ifdef XP_WIN
 /*************************************
  * Windows app installer
  *
  * The Windows installation process will generate the following files:
  *
  * ${FolderName} = sanitized app name + "-" + manifest url hash
@@ -146,40 +207,36 @@ function NativeApp(aData) {
  *
  * After the app runs for the first time, a profiles/ folder will also be
  * created which will host the user profile for this app.
  */
 
 /**
  * Constructor for the Windows native app shell
  *
- * @param aData the data object provided by the web app with
- *              all the app settings and specifications.
+ * @param aData the data object provided to the install function
  */
 function WinNativeApp(aData) {
   NativeApp.call(this, aData);
   this._init();
 }
 
 WinNativeApp.prototype = {
+  __proto__: NativeApp.prototype,
+
   /**
-   * Install the app in the system by creating the folder structure,
+   * Install the app in the system
    *
    */
   install: function() {
-    // Remove previously installed app (for update purposes)
-    this._removeInstallation(true);
-
     try {
-      this._createDirectoryStructure();
       this._copyPrebuiltFiles();
       this._createConfigFiles();
       this._createShortcutFiles();
       this._writeSystemKeys();
-      this._createAppProfile();
     } catch (ex) {
       this._removeInstallation(false);
       throw(ex);
     }
 
     getIconForApp(this, function() {});
   },
 
@@ -216,16 +273,21 @@ WinNativeApp.prototype = {
 
     this.iconFile = this.installDir.clone();
     this.iconFile.append("chrome");
     this.iconFile.append("icons");
     this.iconFile.append("default");
     this.iconFile.append("default.ico");
 
     this.uninstallSubkeyStr = this.uniqueName;
+
+    // Remove previously installed app (for update purposes)
+    this._removeInstallation(true);
+
+    this._createDirectoryStructure();
   },
 
   /**
    * Remove the current installation
    */
   _removeInstallation : function(keepProfile) {
     let uninstallKey;
     try {
@@ -264,25 +326,27 @@ WinNativeApp.prototype = {
 
     removeFiles(filesToRemove);
   },
 
   /**
    * Creates the main directory structure.
    */
   _createDirectoryStructure: function() {
-    if (!this.installDir.exists())
+    if (!this.installDir.exists()) {
       this.installDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
+    }
+
     this.uninstallDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
   },
 
   /**
    * Creates the profile to be used for this app.
    */
-  _createAppProfile: function() {
+  createAppProfile: function() {
     let profSvc = Cc["@mozilla.org/toolkit/profile-service;1"]
                     .getService(Ci.nsIToolkitProfileService);
 
     try {
       this.appProfile = profSvc.createDefaultProfileForApp(this.installDir.leafName,
                                                            null, null);
     } catch (ex if ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {}
   },
@@ -448,16 +512,18 @@ WinNativeApp.prototype = {
 #elifdef XP_MACOSX
 
 function MacNativeApp(aData) {
   NativeApp.call(this, aData);
   this._init();
 }
 
 MacNativeApp.prototype = {
+  __proto__: NativeApp.prototype,
+
   _init: function() {
     this.appSupportDir = Services.dirsvc.get("ULibDir", Ci.nsILocalFile);
     this.appSupportDir.append("Application Support");
 
     let filenameRE = new RegExp("[<>:\"/\\\\|\\?\\*]", "gi");
     this.appNameAsFilename = this.appNameAsFilename.replace(filenameRE, "");
     if (this.appNameAsFilename == "") {
       this.appNameAsFilename = "Webapp";
@@ -477,25 +543,27 @@ MacNativeApp.prototype = {
     this.macOSDir = this.contentsDir.clone();
     this.macOSDir.append("MacOS");
 
     this.resourcesDir = this.contentsDir.clone();
     this.resourcesDir.append("Resources");
 
     this.iconFile = this.resourcesDir.clone();
     this.iconFile.append("appicon.icns");
+
+    // Remove previously installed app (for update purposes)
+    this._removeInstallation(true);
+
+    this._createDirectoryStructure();
   },
 
   install: function() {
-    this._removeInstallation(true);
     try {
-      this._createDirectoryStructure();
       this._copyPrebuiltFiles();
       this._createConfigFiles();
-      this._createAppProfile();
     } catch (ex) {
       this._removeInstallation(false);
       throw(ex);
     }
 
     getIconForApp(this, this._moveToApplicationsFolder);
   },
 
@@ -505,25 +573,26 @@ MacNativeApp.prototype = {
     if (!keepProfile) {
       filesToRemove.push(this.appProfileDir);
     }
 
     removeFiles(filesToRemove);
   },
 
   _createDirectoryStructure: function() {
-    if (!this.appProfileDir.exists())
+    if (!this.appProfileDir.exists()) {
       this.appProfileDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
+    }
 
     this.contentsDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
     this.macOSDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
     this.resourcesDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
   },
 
-  _createAppProfile: function() {
+  createAppProfile: function() {
     let profSvc = Cc["@mozilla.org/toolkit/profile-service;1"]
                     .getService(Ci.nsIToolkitProfileService);
 
     try {
       this.appProfile = profSvc.createDefaultProfileForApp(this.appProfileDir.leafName,
                                                            null, null);
     } catch (ex if ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {}
   },
@@ -645,16 +714,18 @@ MacNativeApp.prototype = {
 #elifdef XP_UNIX
 
 function LinuxNativeApp(aData) {
   NativeApp.call(this, aData);
   this._init();
 }
 
 LinuxNativeApp.prototype = {
+  __proto__: NativeApp.prototype,
+
   _init: function() {
     // The ${InstallDir} and desktop entry filename are: sanitized app name +
     // "-" + manifest url hash
 
     this.installDir = Services.dirsvc.get("Home", Ci.nsIFile);
     this.installDir.append("." + this.uniqueName);
 
     this.iconFile = this.installDir.clone();
@@ -680,26 +751,27 @@ LinuxNativeApp.prototype = {
     else {
       this.desktopINI = Services.dirsvc.get("Home", Ci.nsIFile);
       this.desktopINI.append(".local");
       this.desktopINI.append("share");
     }
 
     this.desktopINI.append("applications");
     this.desktopINI.append("owa-" + this.uniqueName + ".desktop");
+
+    // Remove previously installed app (for update purposes)
+    this._removeInstallation(true);
+
+    this._createDirectoryStructure();
   },
 
   install: function() {
-    this._removeInstallation(true);
-
     try {
-      this._createDirectoryStructure();
       this._copyPrebuiltFiles();
       this._createConfigFiles();
-      this._createAppProfile();
     } catch (ex) {
       this._removeInstallation(false);
       throw(ex);
     }
 
     getIconForApp(this, function() {});
   },
 
@@ -724,17 +796,17 @@ LinuxNativeApp.prototype = {
   },
 
   _copyPrebuiltFiles: function() {
     let webapprtPre = this.runtimeFolder.clone();
     webapprtPre.append(this.webapprt.leafName);
     webapprtPre.copyTo(this.installDir, this.webapprt.leafName);
   },
 
-  _createAppProfile: function() {
+  createAppProfile: function() {
     let profSvc = Cc["@mozilla.org/toolkit/profile-service;1"]
                     .getService(Ci.nsIToolkitProfileService);
 
     try {
       this.appProfile = profSvc.createDefaultProfileForApp(this.installDir.leafName,
                                                            null, null);
     } catch (ex if ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {}
   },
@@ -765,17 +837,17 @@ LinuxNativeApp.prototype = {
       "travel": "Amusement",
       "reference": "Science;Education;Documentation",
       "maps-navigation": "Maps",
       "utilities": "Utility"
     };
 
     // The trailing semicolon is needed as written in the freedesktop specification
     let categories = "";
-    for (let category of this.app.categories) {
+    for (let category of this.categories) {
       let catLower = category.toLowerCase();
       if (catLower in translations) {
         categories += translations[catLower] + ";";
       }
     }
 
     return categories;
   },
--- a/webapprt/CommandLineHandler.js
+++ b/webapprt/CommandLineHandler.js
@@ -25,17 +25,17 @@ CommandLineHandler.prototype = {
     if (inTestMode) {
       // Open the mochitest shim window, which configures the runtime for tests.
       Services.ww.openWindow(null,
                              "chrome://webapprt/content/mochitest.xul",
                              "_blank",
                              "chrome,dialog=no",
                              args);
     } else {
-      args.setProperty("url", WebappRT.launchURI.spec);
+      args.setProperty("url", WebappRT.launchURI);
       Services.ww.openWindow(null,
                              "chrome://webapprt/content/webapp.xul",
                              "_blank",
                              "chrome,dialog=no,resizable,scrollbars,centerscreen",
                              args);
     }
   },
 
--- a/webapprt/WebappRT.jsm
+++ b/webapprt/WebappRT.jsm
@@ -5,27 +5,23 @@
 this.EXPORTED_SYMBOLS = ["WebappRT"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppsUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "FileUtils", function() {
   Cu.import("resource://gre/modules/FileUtils.jsm");
   return FileUtils;
 });
 
-XPCOMUtils.defineLazyGetter(this, "DOMApplicationRegistry", function() {
-  Cu.import("resource://gre/modules/Webapps.jsm");
-  return DOMApplicationRegistry;
-});
-
 this.WebappRT = {
   _config: null,
 
   get config() {
     if (this._config)
       return this._config;
 
     let config;
@@ -46,15 +42,13 @@ this.WebappRT = {
   // will have a reference to its global object, so our reference to it
   // will leak that object (per bug 780674).  The setter enables us to clone
   // the new value so we don't actually retain a reference to it.
   set config(newVal) {
     this._config = JSON.parse(JSON.stringify(newVal));
   },
 
   get launchURI() {
-    let url = Services.io.newURI(this.config.app.origin, null, null);
-    if (this.config.app.manifest.launch_path) {
-      url = Services.io.newURI(this.config.app.manifest.launch_path, null, url);
-    }
-    return url;
+    let manifest = new ManifestHelper(this.config.app.manifest,
+                                      this.config.app.origin);
+    return manifest.fullLaunchPath();
   }
 };
--- a/webapprt/WebappsHandler.jsm
+++ b/webapprt/WebappsHandler.jsm
@@ -7,16 +7,17 @@
 this.EXPORTED_SYMBOLS = ["WebappsHandler"];
 
 let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Webapps.jsm");
+Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import("resource://gre/modules/WebappsInstaller.jsm");
 Cu.import("resource://gre/modules/WebappOSUtils.jsm");
 
 this.WebappsHandler = {
   init: function() {
     Services.obs.addObserver(this, "webapps-ask-install", false);
     Services.obs.addObserver(this, "webapps-launch", false);
     Services.obs.addObserver(this, "webapps-uninstall", false);
@@ -37,17 +38,19 @@ this.WebappsHandler = {
         break;
       case "webapps-uninstall":
         WebappOSUtils.uninstall(data);
         break;
     }
   },
 
   doInstall: function(data, window) {
-    let {name} = data.app.manifest;
+    let jsonManifest = data.isPackage ? data.app.updateManifest : data.app.manifest;
+    let manifest = new ManifestHelper(jsonManifest, data.app.origin);
+    let name = manifest.name;
     let bundle = Services.strings.createBundle("chrome://webapprt/locale/webapp.properties");
 
     let choice = Services.prompt.confirmEx(
       window,
       bundle.formatStringFromName("webapps.install.title", [name], 1),
       bundle.formatStringFromName("webapps.install.description", [name], 1),
       // Set both buttons to strings with the cancel button being default
       Ci.nsIPromptService.BUTTON_POS_1_DEFAULT |
@@ -55,16 +58,30 @@ this.WebappsHandler = {
         Ci.nsIPromptService.BUTTON_TITLE_IS_STRING * Ci.nsIPromptService.BUTTON_POS_1,
       bundle.GetStringFromName("webapps.install.install"),
       bundle.GetStringFromName("webapps.install.dontinstall"),
       null,
       null,
       {});
 
     // Perform the install if the user allows it
-    if (choice == 0 && WebappsInstaller.install(data)) {
-      DOMApplicationRegistry.confirmInstall(data);
-    }
-    else {
+    if (choice == 0) {
+      let shell = WebappsInstaller.init(data);
+
+      if (shell) {
+        let localDir = null;
+        if (shell.appProfile) {
+          localDir = shell.appProfile.localDir;
+        }
+
+        DOMApplicationRegistry.confirmInstall(data, false, localDir, null,
+          function (aManifest) {
+            WebappsInstaller.install(data, aManifest);
+          }
+        );
+      } else {
+        DOMApplicationRegistry.denyInstall(data);
+      }
+    } else {
       DOMApplicationRegistry.denyInstall(data);
     }
   }
 };
--- a/webapprt/test/chrome/head.js
+++ b/webapprt/test/chrome/head.js
@@ -26,17 +26,17 @@ function loadWebapp(manifest, parameters
   let url = Services.io.newURI(manifest, null, MANIFEST_URL_BASE);
 
   becomeWebapp(url.spec, parameters, function onBecome() {
     function onLoadApp() {
       gAppBrowser.removeEventListener("load", onLoadApp, true);
       onLoad();
     }
     gAppBrowser.addEventListener("load", onLoadApp, true);
-    gAppBrowser.setAttribute("src", WebappRT.launchURI.spec);
+    gAppBrowser.setAttribute("src", WebappRT.launchURI);
   });
 
   registerCleanupFunction(function() {
     // We load DOMApplicationRegistry into a local scope to avoid appearing
     // to leak it.
     let scope = {};
     Cu.import("resource://gre/modules/Webapps.jsm", scope);
     scope.DOMApplicationRegistry.uninstall(url.spec);