Bug 789527 - Change the way we install packaged apps [r=gwagner]
authorFabrice Desré <fabrice@mozilla.com>
Thu, 27 Sep 2012 12:34:41 -0700
changeset 108612 eda2891c545b3ecbe1bef496d63876c4bcacddc2
parent 108611 d31d2479d685f203b45738dd92983b7eebd6f5f6
child 108613 9415ab3ff488d590619ceccc262f2b73df6025c9
push id82
push usershu@rfrn.org
push dateFri, 05 Oct 2012 13:20:22 +0000
reviewersgwagner
bugs789527
milestone18.0a1
Bug 789527 - Change the way we install packaged apps [r=gwagner]
dom/apps/src/Webapps.js
dom/apps/src/Webapps.jsm
--- a/dom/apps/src/Webapps.js
+++ b/dom/apps/src/Webapps.js
@@ -183,33 +183,76 @@ WebappsRegistry.prototype = {
   uninit: function() {
     this._mgmt = null;
     cpmm.sendAsyncMessage("Webapps:UnregisterForMessages",
                           ["Webapps:Install:Return:OK"]);
   },
 
   // mozIDOMApplicationRegistry2 implementation
 
-  installPackage: function(aPackageURL, aParams) {
-    this._validateScheme(aPackageURL);
+  installPackage: function(aURL, aParams) {
+    let installURL = this._window.location.href;
+    let installOrigin = this._getOrigin(installURL);
+    this._validateScheme(aURL);
 
     let request = this.createRequest();
     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 : [];
-    cpmm.sendAsyncMessage("Webapps:InstallPackage", { url: aPackageURL,
-                                                      receipts: receipts,
-                                                      categories: categories,
-                                                      requestID: requestID,
-                                                      oid: this._id,
-                                                      from: this._window.location.href,
-                                                      installOrigin: this._getOrigin(this._window.location.href) });
+    let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+    xhr.open("GET", aURL, true);
+    xhr.channel.loadFlags |= Ci.nsIRequest.VALIDATE_ALWAYS;
+
+    xhr.addEventListener("load", (function() {
+      if (xhr.status == 200) {
+        let manifest;
+        try {
+          manifest = JSON.parse(xhr.responseText, installOrigin);
+        } catch(e) {
+          Services.DOMRequest.fireError(request, "MANIFEST_PARSE_ERROR");
+          return;
+        }
+        if (!(AppsUtils.checkManifest(manifest, installOrigin) &&
+              manifest.package_path)) {
+          Services.DOMRequest.fireError(request, "INVALID_MANIFEST");
+        } else {
+          if (!this.checkAppStatus(manifest)) {
+            Services.DOMRequest.fireError(request, "INVALID_SECURITY_LEVEL");
+          } else {
+            let receipts = (aParams && aParams.receipts && Array.isArray(aParams.receipts)) ? aParams.receipts : [];
+            let categories = (aParams && aParams.categories && Array.isArray(aParams.categories)) ? aParams.categories : [];
+            let etag = xhr.getResponseHeader("Etag");
+            cpmm.sendAsyncMessage("Webapps:InstallPackage", { app: {
+                                                              installOrigin: installOrigin,
+                                                              origin: this._getOrigin(aURL),
+                                                              manifestURL: aURL,
+                                                              updateManifest: manifest,
+                                                              etag: etag,
+                                                              receipts: receipts,
+                                                              categories: categories },
+                                                              from: installURL,
+                                                              oid: this._id,
+                                                              requestID: requestID,
+                                                              isPackage: true });
+          }
+        }
+      }
+      else {
+        Services.DOMRequest.fireError(request, "MANIFEST_URL_ERROR");
+      }
+    }).bind(this), false);
+
+    xhr.addEventListener("error", (function() {
+      Services.DOMRequest.fireError(request, "NETWORK_ERROR");
+    }).bind(this), false);
+
+    xhr.send(null);
     return request;
   },
 
   // nsIDOMGlobalPropertyInitializer implementation
   init: function(aWindow) {
     this.initHelper(aWindow, ["Webapps:Install:Return:OK", "Webapps:Install:Return:KO",
                               "Webapps:GetInstalled:Return:OK",
                               "Webapps:GetSelf:Return:OK",
@@ -333,19 +376,21 @@ WebappsApplication.prototype = {
     this._ondownloadapplied = null;
 
     this._downloadError = null;
 
     this.initHelper(aWindow, ["Webapps:Uninstall:Return:OK",
                               "Webapps:Uninstall:Return:KO",
                               "Webapps:OfflineCache",
                               "Webapps:CheckForUpdate:Return:OK",
-                              "Webapps:CheckForUpdate:Return:KO"]);
+                              "Webapps:CheckForUpdate:Return:KO",
+                              "Webapps:PackageEvent"]);
     cpmm.sendAsyncMessage("Webapps:RegisterForMessages",
-                          ["Webapps:Uninstall:Return:OK", "Webapps:OfflineCache"]);
+                          ["Webapps:Uninstall:Return:OK", "Webapps:OfflineCache",
+                           "Webapps:PackageEvent"]);
   },
 
   set onprogress(aCallback) {
     this._onprogress = aCallback;
   },
 
   get onprogress() {
     return this._onprogress;
@@ -429,30 +474,33 @@ WebappsApplication.prototype = {
     if (browserChild) {
       browserChild.messageManager.sendAsyncMessage("Webapps:ClearBrowserData");
     }
   },
 
   uninit: function() {
     this._onprogress = null;
     cpmm.sendAsyncMessage("Webapps:UnregisterForMessages",
-                          ["Webapps:Uninstall:Return:OK", "Webapps:OfflineCache"]);
+                          ["Webapps:Uninstall:Return:OK", "Webapps:OfflineCache",
+                           "Webapps:PackageEvent"]);
   },
 
   _fireEvent: function(aName, aHandler) {
     if (aHandler) {
       let event = new this._window.MozApplicationEvent(aName, { application: this });
       aHandler.handleEvent(event);
     }
   },
 
   receiveMessage: function(aMessage) {
-    var msg = aMessage.json;
+    let msg = aMessage.json;
     let req = this.takeRequest(msg.requestID);
-    if ((msg.oid != this._id || !req) && aMessage.name !== "Webapps:OfflineCache")
+    if ((msg.oid != this._id || !req) &&
+        aMessage.name !== "Webapps:OfflineCache" &&
+        aMessage.name !== "Webapps:PackageEvent")
       return;
     switch (aMessage.name) {
       case "Webapps:Uninstall:Return:OK":
         Services.DOMRequest.fireSuccess(req, msg.origin);
         break;
       case "Webapps:Uninstall:Return:KO":
         Services.DOMRequest.fireError(req, "NOT_INSTALLED");
         break;
@@ -486,16 +534,53 @@ WebappsApplication.prototype = {
               Services.DOMRequest.fireSuccess(req, this.manifestURL);
               this._fireEvent("downloadapplied", this._ondownloadapplied);
             }
           }
           break;
         case "Webapps:CheckForUpdate:Return:KO":
           Services.DOMRequest.fireError(req, msg.error);
           break;
+        case "Webapps:PackageEvent":
+          if (msg.manifestURL != this.manifestURL)
+            return;
+          switch(msg.type) {
+            case "error":
+              this._downloadError = msg.error;
+              this._fireEvent("downloaderror", this._ondownloaderror);
+              break;
+            case "progress":
+              this.progress = msg.progress;
+              this._fireEvent("downloadprogress", this._onprogress);
+              break;
+            case "installed":
+              let app = msg.app;
+              this.progress = app.progress || 0;
+              this.downloadAvailable = app.downloadAvailable;
+              this.downloading = app.downloading;
+              this.readyToApplyDownload = app.readyToApplyDownload;
+              this.downloadSize = app.downloadSize || 0;
+              this.installState = app.installState;
+              this.manifest = app.manifest;
+              this._fireEvent("downloaded", this._ondownloaded);
+              this._fireEvent("downloadapplied", this._ondownloadapplied);
+              break;
+            case "canceled":
+              app = msg.app;
+              this.progress = app.progress || 0;
+              this.downloadAvailable = app.downloadAvailable;
+              this.downloading = app.downloading;
+              this.readyToApplyDownload = app.readyToApplyDownload;
+              this.downloadSize = app.downloadSize || 0;
+              this.installState = app.installState;
+              this._downloadError = msg.error;
+              this._fireEvent("downloaderror", this._ondownloaderror);
+              break;
+          }
+          break;
     }
   },
 
   classID: Components.ID("{723ed303-7757-4fb0-b261-4f78b1f6bd22}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.mozIDOMApplication]),
 
   classInfo: XPCOMUtils.generateCI({classID: Components.ID("{723ed303-7757-4fb0-b261-4f78b1f6bd22}"),
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -52,16 +52,17 @@ XPCOMUtils.defineLazyGetter(this, "msgmg
   const DIRECTORY_NAME = WEBAPP_RUNTIME ? "WebappRegD" : "ProfD";
 #endif
 
 let DOMApplicationRegistry = {
   appsFile: null,
   webapps: { },
   children: [ ],
   allAppsLaunchable: false,
+  downloads: { },
 
   init: function() {
     this.messages = ["Webapps:Install", "Webapps:Uninstall",
                      "Webapps:GetSelf", "Webapps:IsInstalled",
                      "Webapps:GetInstalled", "Webapps:GetNotInstalled",
                      "Webapps:Launch", "Webapps:GetAll",
                      "Webapps:InstallPackage", "Webapps:GetBasePath",
                      "Webapps:GetList", "Webapps:RegisterForMessages",
@@ -465,17 +466,18 @@ let DOMApplicationRegistry = {
         break;
       case "Webapps:GetAll":
         if (msg.hasPrivileges)
           this.getAll(msg, mm);
         else
           mm.sendAsyncMessage("Webapps:GetAll:Return:KO", msg);
         break;
       case "Webapps:InstallPackage":
-        this.installPackage(msg, mm);
+        // always ask for UI to install
+        Services.obs.notifyObservers(mm, "webapps-ask-install", JSON.stringify(msg));
         break;
       case "Webapps:GetBasePath":
         return this.webapps[msg.id].basePath;
         break;
       case "Webapps:RegisterForMessages":
         this.addMessageListener(msg, mm);
         break;
       case "Webapps:UnregisterForMessages":
@@ -561,16 +563,36 @@ let DOMApplicationRegistry = {
       return;
     }
 
     Services.obs.notifyObservers(aMm, "webapps-launch", JSON.stringify(aData));
   },
 
   cancelDownload: function cancelDowload(aManifestURL) {
     // We can't cancel appcache dowloads for now.
+    if (!this.downloads[aManifestURL]) {
+      return;
+    }
+    // This is a HTTP channel.
+    let download = this.downloads[aManifestURL]
+    download.channel.cancel(Cr.NS_BINDING_ABORTED);
+    let app = this.webapps[dowload.appId];
+
+    app.progress = 0;
+    app.installState = app.previousState;
+    app.dowloading = false;
+    app.dowloadavailable = false;
+    app.downloadSize = 0;
+    this._saveApps((function() {
+      this.broadcastMessage("Webapps:PackageEvent",
+                             { type: "canceled",
+                               manifestURL:  aApp.manifestURL,
+                               app: app,
+                               error: "DOWNLOAD_CANCELED" });
+    }).bind(this));
   },
 
   startOfflineCacheDownload: function startOfflineCacheDownload(aManifest, aApp, aProfileDir) {
     // if the manifest has an appcache_path property, use it to populate the appcache
     if (aManifest.appcache_path) {
       let appcacheURI = Services.io.newURI(aManifest.fullAppcachePath(), null, null);
       let updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"]
                             .getService(Ci.nsIOfflineCacheUpdateService);
@@ -709,113 +731,132 @@ let DOMApplicationRegistry = {
       }
     }
     aData.mm.sendAsyncMessage("Webapps:Install:Return:KO", aData);
   },
 
   confirmInstall: function(aData, aFromSync, aProfileDir, aOfflineCacheObserver) {
     let app = aData.app;
     app.removable = true;
+
+    let origin = Services.io.newURI(app.origin, null, null);
+    let manifestURL = origin.resolve(app.manifestURL);
+
     let id = app.syncId || this._appId(app.origin);
-    let localId = this.getAppLocalIdByManifestURL(app.manifestURL);
+    let localId = this.getAppLocalIdByManifestURL(manifestURL);
 
     // Installing an application again is considered as an update.
     if (id) {
       let dir = this._getAppDir(id);
       try {
         dir.remove(true);
       } catch(e) {
       }
     } else {
       id = this.makeAppId();
       localId = this._nextLocalId();
     }
 
-    if (app.packageId) {
+    let manifestName = "manifest.webapp";
+    if (aData.isPackage) {
       // Override the origin with the correct id.
       app.origin = "app://" + id;
+
+      // For packaged apps, keep the update manifest distinct from the app
+      // manifest.
+      manifestName = "update.webapp";
     }
 
     let appObject = AppsUtils.cloneAppObject(app);
     appObject.appStatus = app.appStatus || Ci.nsIPrincipal.APP_STATUS_INSTALLED;
     appObject.installTime = app.installTime = Date.now();
     appObject.lastUpdateCheck = app.lastUpdateCheck = Date.now();
     let appNote = JSON.stringify(appObject);
     appNote.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 manFile = dir.clone();
-    manFile.append("manifest.webapp");
-    this._writeFile(manFile, JSON.stringify(app.manifest), function() {
-      // If this a packaged app, move the zip file from the temp directory,
-      // and delete the temp directory.
-      if (app.packageId) {
-        let appFile = FileUtils.getFile("TmpD", ["webapps", app.packageId, "application.zip"],
-                                        true, true);
-        appFile.moveTo(dir, "application.zip");
-        let tmpDir = FileUtils.getDir("TmpD", ["webapps", app.packageId],
-                                        true, true);
-        try {
-          tmpDir.remove(true);
-        } catch(e) {
-        }
-      }
-    });
+    manFile.append(manifestName);
+    let jsonManifest = aData.isPackage ? app.updateManifest : app.manifest;
+    this._writeFile(manFile, JSON.stringify(jsonManifest), function() { });
 
-    let manifest = new DOMApplicationManifest(app.manifest, app.origin);
+    let manifest = new DOMApplicationManifest(jsonManifest, app.origin);
 
     if (manifest.appcache_path) {
       appObject.installState = "pending";
       appObject.downloadAvailable = true;
       appObject.downloading = true;
-      appObject.downloadsize = 0;
+      appObject.downloadSize = 0;
+      appObject.readyToApplyDownload = false;
+    } else if (manifest.package_path) {
+      appObject.installState = "pending";
+      appObject.downloadAvailable = true;
+      appObject.downloading = true;
+      appObject.downloadSize = manifest.size;
       appObject.readyToApplyDownload = false;
     } else {
       appObject.installState = "installed";
       appObject.downloadAvailable = false;
       appObject.downloading = false;
       appObject.readyToApplyDownload = false;
     }
 
-    appObject.name = app.manifest.name;
+    appObject.name = manifest.name;
 
     this.webapps[id] = appObject;
+    ["installState", "downloadAvailable",
+     "downloading", "downloadSize", "readyToApplyDownload"].forEach(function(aProp) {
+      aData.app[aProp] = appObject[aProp];
+     });
 
     if (!aFromSync)
       this._saveApps((function() {
         this.broadcastMessage("Webapps:Install:Return:OK", aData);
         Services.obs.notifyObservers(this, "webapps-sync-install", appNote);
         this.broadcastMessage("Webapps:AddApp", { id: id, app: appObject });
       }).bind(this));
 
 #ifdef MOZ_SYS_MSG
-    this._registerSystemMessages(app.manifest, app);
-    this._registerActivities(app.manifest, app);
+    if (!aData.isPackage) {
+      this._registerSystemMessages(app.manifest, app);
+      this._registerActivities(app.manifest, app);
+    }
 #endif
 
     this.startOfflineCacheDownload(manifest, appObject, aProfileDir);
+    if (manifest.package_path) {
+      this.downloadPackage(manifest, appObject);
+    }
   },
 
   _nextLocalId: function() {
     let id = Services.prefs.getIntPref("dom.mozApps.maxLocalId") + 1;
     Services.prefs.setIntPref("dom.mozApps.maxLocalId", id);
     return id;
   },
 
   _appId: function(aURI) {
     for (let id in this.webapps) {
       if (this.webapps[id].origin == aURI)
         return id;
     }
     return null;
   },
 
+  _appIdForManifestURL: function(aURI) {
+    for (let id in this.webapps) {
+      if (this.webapps[id].manifestURL == aURI)
+        return id;
+    }
+    return null;
+  },
+
   makeAppId: function() {
     let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
     return uuidGenerator.generateUUID().toString();
   },
 
   _saveApps: function(aCallback) {
     this._writeFile(this.appsFile, JSON.stringify(this.webapps, null, 2), function() {
       if (aCallback)
@@ -834,62 +875,56 @@ let DOMApplicationRegistry = {
 
     let index = aIndex || 0;
     let id = aData[index].id;
 
     // the manifest file used to be named manifest.json, so fallback on this.
     let baseDir = (this.webapps[id].removable ? DIRECTORY_NAME : "coreAppsDir");
     let file = FileUtils.getFile(baseDir, ["webapps", id, "manifest.webapp"], true);
     if (!file.exists()) {
+      file = FileUtils.getFile(baseDir, ["webapps", id, "update.webapp"], true);
+    }
+    if (!file.exists()) {
       file = FileUtils.getFile(baseDir, ["webapps", id, "manifest.json"], true);
     }
 
     this._loadJSONAsync(file, (function(aJSON) {
       aData[index].manifest = aJSON;
       if (index == aData.length - 1)
         aFinalCallback(aData);
       else
         this._readManifests(aData, aFinalCallback, index + 1);
     }).bind(this));
   },
 
-  installPackage: function(aData, aMm) {
+  downloadPackage: function(aManifest, aApp) {
     // Here are the steps when installing a package:
     // - create a temp directory where to store the app.
     // - download the zip in this directory.
     // - extract the manifest from the zip and check it.
     // - ask confirmation to the user.
     // - add the new app to the registry.
     // If we fail at any step, we backout the previous ones and return an error.
 
-    let id;
-    let manifestURL = "jar:" + aData.url + "!manifest.webapp";
-    // Check if we reinstall a known application.
-    for (let appId in this.webapps) {
-      if (this.webapps[appId].manifestURL == manifestURL) {
-        id = appId;
-      }
-    }
+    debug(JSON.stringify(aApp));
 
-    // New application.
-    if (!id) {
-      id = this.makeAppId();
-    }
-
-    let dir = FileUtils.getDir("TmpD", ["webapps", id], true, true);
+    let id = this._appIdForManifestURL(aApp.manifestURL);
+    let app = this.webapps[id];
 
     // Removes the directory we created, and sends an error to the DOM side.
     function cleanup(aError) {
+      debug("Cleanup: " + aError);
+      let dir = FileUtils.getDir("TmpD", ["webapps", id], true, true);
       try {
         dir.remove(true);
       } catch (e) { }
-      aMm.sendAsyncMessage("Webapps:Install:Return:KO",
-                           { oid: aData.oid,
-                             requestID: aData.requestID,
-                             error: aError });
+        this.broadcastMessage("Webapps:PackageEvent",
+                              { type: "error",
+                                manifestURL:  aApp.manifestURL,
+                                error: aError});
     }
 
     function getInferedStatus() {
       // XXX Update once we have digital signatures (bug 772365)
       return Ci.nsIPrincipal.APP_STATUS_INSTALLED;
     }
 
     function getAppManifestStatus(aManifest) {
@@ -923,71 +958,119 @@ let DOMApplicationRegistry = {
     function checkAppStatus(aManifest) {
       if (Services.prefs.getBoolPref("dom.mozApps.dev_mode")) {
         return true;
       }
 
       return (getAppManifestStatus(aManifest) <= getInferedStatus());
     }
 
-    NetUtil.asyncFetch(aData.url, function(aInput, aResult, aRequest) {
+    debug("About to download " + aManifest.fullPackagePath());
+
+    let requestChannel = NetUtil.newChannel(aManifest.fullPackagePath())
+                                .QueryInterface(Ci.nsIHttpChannel);
+    this.downloads[aApp.manifestURL] =
+      { channel:requestChannel,
+        appId: id,
+        previousState: "pending"
+      };
+    requestChannel.notificationCallbacks = {
+      QueryInterface: function notifQI(aIID) {
+        if (aIID.equals(Ci.nsISupports)          ||
+            aIID.equals(Ci.nsIProgressEventSink))
+          return this;
+
+        throw Cr.NS_ERROR_NO_INTERFACE;
+      },
+      getInterface: function notifGI(aIID) {
+        return this.QueryInterface(aIID);
+      },
+      onProgress: function notifProgress(aRequest, aContext,
+                                         aProgress, aProgressMax) {
+        debug("onProgress: " + aProgress + "/" + aProgressMax);
+        app.progress = aProgress;
+        DOMApplicationRegistry.broadcastMessage("Webapps:PackageEvent",
+                                                { type: "progress",
+                                                  manifestURL: aApp.manifestURL,
+                                                  progress: aProgress });
+      },
+      onStatus: function notifStatus(aRequest, aContext, aStatus, aStatusArg) { }
+    }
+
+    NetUtil.asyncFetch(requestChannel,
+    function(aInput, aResult, aRequest) {
       if (!Components.isSuccessCode(aResult)) {
         // We failed to fetch the zip.
         cleanup("NETWORK_ERROR");
         return;
       }
       // Copy the zip on disk.
       let zipFile = FileUtils.getFile("TmpD",
                                       ["webapps", id, "application.zip"], true);
       let ostream = FileUtils.openSafeFileOutputStream(zipFile);
       NetUtil.asyncCopy(aInput, ostream, function (aResult) {
         if (!Components.isSuccessCode(aResult)) {
           // We failed to save the zip.
           cleanup("DOWNLOAD_ERROR");
           return;
         }
-        // Build a data structure to call the webapps confirmation dialog :
-        // - load the manifest from the zip
-        // - set data.app.(origin, install_origin, manifestURL, manifest, receipts, categories)
-        // - call notifyObservers(this, "webapps-ask-install", JSON.stringify(msg));
-        let msg = {
-          from: aData.from,
-          oid: aData.oid,
-          requestID: aData.requestID,
-          app: {
-            packageId: id,
-            installOrigin: aData.installOrigin,
-            origin: "app://" + id,
-            manifestURL: manifestURL,
-            receipts: aData.receipts,
-            categories: aData.categories
-          }
-        }
+
         let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
                         .createInstance(Ci.nsIZipReader);
         try {
           zipReader.open(zipFile);
           if (!zipReader.hasEntry("manifest.webapp")) {
             throw "No manifest.webapp found.";
           }
 
           let istream = zipReader.getInputStream("manifest.webapp");
-          msg.app.manifest = JSON.parse(NetUtil.readInputStreamToString(istream,
-                                        istream.available()) || "");
-          if (!AppsUtils.checkManifest(msg.app.manifest, aData.installOrigin)) {
+
+          // Obtain a converter to read from a UTF-8 encoded input stream.
+          let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                          .createInstance(Ci.nsIScriptableUnicodeConverter);
+          converter.charset = "UTF-8";
+
+          let manifest = JSON.parse(converter.ConvertToUnicode(NetUtil.readInputStreamToString(istream,
+                                                               istream.available()) || ""));
+
+          if (!AppsUtils.checkManifest(manifest, aApp.installOrigin)) {
             throw "INVALID_MANIFEST";
           }
 
-          if (!checkAppStatus(msg.app.manifest)) {
+          if (!checkAppStatus(manifest)) {
             throw "INVALID_SECURITY_LEVEL";
           }
 
-          msg.app.appStatus = getAppStatus(msg.app.manifest);
-          Services.obs.notifyObservers(this, "webapps-ask-install",
-                                             JSON.stringify(msg));
+          // Success! Move the zip out of TmpD.
+          let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true);
+          zipFile.moveTo(dir, "application.zip");
+          let tmpDir = FileUtils.getDir("TmpD", ["webapps", id], true, true);
+          try {
+            tmpDir.remove(true);
+          } catch(e) { }
+
+          // Save the manifest
+          let manFile = dir.clone();
+          manFile.append("manifest.webapp");
+          DOMApplicationRegistry._writeFile(manFile,
+                                            JSON.stringify(manifest),
+                                            function() { });
+          // Set state and fire events.
+          app.installState = "installed";
+          app.dowloading = false;
+          app.dowloadavailable = false;
+          DOMApplicationRegistry._saveApps(function() {
+            debug("About to fire Webapps:PackageEvent");
+            DOMApplicationRegistry.broadcastMessage("Webapps:PackageEvent",
+                                                    { type: "installed",
+                                                      manifestURL: aApp.manifestURL,
+                                                      app: app,
+                                                      manifest: manifest });
+            delete DOMApplicationRegistry.downloads[aApp.manifestURL]
+          });
         } catch (e) {
           // XXX we may need new error messages.
           cleanup(e);
         } finally {
           zipReader.close();
         }
       });
     });
@@ -1137,17 +1220,17 @@ let DOMApplicationRegistry = {
     }).bind(this));
   },
 
   getManifestFor: function(aOrigin, aCallback) {
     if (!aCallback)
       return;
 
     let id = this._appId(aOrigin);
-    if (!id) {
+    if (!id || this.webapps[id].installState == "pending") {
       aCallback(null);
       return;
     }
 
     this._readManifests([{ id: id }], function(aResult) {
       aCallback(aResult[0].manifest);
     });
   },
@@ -1490,16 +1573,24 @@ DOMApplicationManifest.prototype = {
   get appcache_path() {
     return this._localeProp("appcache_path");
   },
 
   get orientation() {
     return this._localeProp("orientation");
   },
 
+  get package_path() {
+    return this._localeProp("package_path");
+  },
+
+  get size() {
+    return this._manifest["size"] || 0;
+  },
+
   iconURLForSize: function(aSize) {
     let icons = this._localeProp("icons");
     if (!icons)
       return null;
     let dist = 100000;
     let icon = null;
     for (let size in icons) {
       let iSize = parseInt(size);
@@ -1533,12 +1624,17 @@ DOMApplicationManifest.prototype = {
 
   resolveFromOrigin: function(aURI) {
     return this._origin.resolve(aURI);
   },
 
   fullAppcachePath: function() {
     let appcachePath = this._localeProp("appcache_path");
     return this._origin.resolve(appcachePath ? appcachePath : "");
+  },
+
+  fullPackagePath: function() {
+    let packagePath = this._localeProp("package_path");
+    return this._origin.resolve(packagePath ? packagePath : "");
   }
 };
 
 DOMApplicationRegistry.init();