Bug 777402 - Implement support for packaged apps via the installPackage function in the mozapps DOM API in desktop web runtime. r=myk,wesj,fabrice
authorMarco Castelluccio <mar.castelluccio@studenti.unina.it>
Thu, 01 Aug 2013 17:00:39 -0700
changeset 141093 668bb7634d04e1ef8b7d3f82fcf751078180ee47
parent 141092 fe6643be288de2cdf84dfb627299078ae03c8f28
child 141094 18b2dd40f9a0f854bfb4bec8593bd65888bd4028
push id2013
push usergijskruitbosch@gmail.com
push dateFri, 02 Aug 2013 13:48:28 +0000
treeherderfx-team@bcbc5a0ed405 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmyk, wesj, fabrice
bugs777402
milestone25.0a1
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
testing/mozbase/mozprofile/mozprofile/webapps.py
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
@@ -6717,35 +6717,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();
@@ -6755,36 +6756,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/testing/mozbase/mozprofile/mozprofile/webapps.py
+++ b/testing/mozbase/mozprofile/mozprofile/webapps.py
@@ -61,16 +61,17 @@ class Webapp(dict):
             if key not in self:
                 raise WebappFormatException("Webapp object missing required key '%s'" % key)
 
 
 class WebappCollection(object):
     """A list-like object that collects webapps and updates the webapp manifests"""
 
     json_template = Template(""""$name": {
+  "name": "$name",
   "origin": "$origin",
   "installOrigin": "$origin",
   "receipt": null,
   "installTime": 132333986000,
   "manifestURL": "$manifestURL",
   "localId": $localId,
   "id": "$name",
   "appStatus": $appStatus,
--- 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);