Bug 905881 - Move the package to the local installation directory. r=fabrice, r=myk
authorMarco Castelluccio <mar.castelluccio@studenti.unina.it>
Fri, 13 Sep 2013 08:32:47 -0400
changeset 160028 e6a9da6e6a8c07941049cc1e10306bc8cac40d13
parent 160027 54fe3c932126cac0ecb4a9dd77a277bda51462a3
child 160029 fdb5b2fc7e54657b15b6b863aed0a1175b2da759
push id2961
push userlsblakk@mozilla.com
push dateMon, 28 Oct 2013 21:59:28 +0000
treeherdermozilla-beta@73ef4f13486f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfabrice, myk
bugs905881
milestone26.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 905881 - Move the package to the local installation directory. r=fabrice, r=myk
browser/modules/webappsUI.jsm
dom/apps/src/AppsUtils.jsm
dom/apps/src/Webapps.jsm
netwerk/protocol/app/AppProtocolHandler.js
toolkit/devtools/apps/tests/unit/xpcshell.ini
toolkit/webapps/WebappOSUtils.jsm
toolkit/webapps/WebappsInstaller.jsm
--- a/browser/modules/webappsUI.jsm
+++ b/browser/modules/webappsUI.jsm
@@ -151,20 +151,20 @@ this.webappsUI = {
 
         if (app) {
           let localDir = null;
           if (app.appProfile) {
             localDir = app.appProfile.localDir;
           }
 
           DOMApplicationRegistry.confirmInstall(aData, localDir,
-            (aManifest) => {
+            (aManifest, aZipPath) => {
               Task.spawn(function() {
                 try {
-                  yield WebappsInstaller.install(aData, aManifest);
+                  yield WebappsInstaller.install(aData, aManifest, aZipPath);
                   if (this.downloads[manifestURL]) {
                     yield this.downloads[manifestURL].promise;
                   }
                   installationSuccessNotification(aData, app, bundle);
                 } catch (ex) {
                   Cu.reportError("Error installing webapp: " + ex);
                   // TODO: Notify user that the installation has failed
                 } finally {
--- a/dom/apps/src/AppsUtils.jsm
+++ b/dom/apps/src/AppsUtils.jsm
@@ -4,21 +4,22 @@
 
 "use strict";
 
 const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/WebappOSUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
   return Cc["@mozilla.org/network/util;1"]
            .getService(Ci.nsINetUtil);
 });
 
 // Shared code for AppsServiceChild.jsm, Webapps.jsm and Webapps.js
 
@@ -184,31 +185,33 @@ this.AppsUtils = {
     try {
       return FileUtils.getDir("coreAppsDir", ["webapps"], false).path;
     } catch(e) {
       return null;
     }
   },
 
   getAppInfo: function getAppInfo(aApps, aAppId) {
-    if (!aApps[aAppId]) {
+    let app = aApps[aAppId];
+
+    if (!app) {
       debug("No webapp for " + aAppId);
       return null;
     }
 
     // We can have 3rd party apps that are non-removable,
     // so we can't use the 'removable' property for isCoreApp
     // Instead, we check if the app is installed under /system/b2g
     let isCoreApp = false;
-    let app = aApps[aAppId];
+
 #ifdef MOZ_WIDGET_GONK
     isCoreApp = app.basePath == this.getCoreAppsBasePath();
 #endif
     debug(app.basePath + " isCoreApp: " + isCoreApp);
-    return { "basePath":  app.basePath + "/",
+    return { "path":  WebappOSUtils.getInstallPath(app),
              "isCoreApp": isCoreApp };
   },
 
   /**
     * Remove potential HTML tags from displayable fields in the manifest.
     * We check name, description, developer name, and permission description
     */
   sanitizeManifest: function(aManifest) {
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -1945,18 +1945,18 @@ 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)) {
+        let id = this._appIdForManifestURL(app.manifestURL);
+        if (id !== null && this._isLaunchable(this.webapps[id])) {
           sendError("REINSTALL_FORBIDDEN");
           return;
         }
 
         if (!(AppsUtils.checkManifest(manifest, app) &&
               manifest.package_path)) {
           sendError("INVALID_MANIFEST");
         } else if (!AppsUtils.checkInstallAllowed(manifest, app.installOrigin)) {
@@ -2064,17 +2064,17 @@ this.DOMApplicationRegistry = {
           }
           debug("About to fire Webapps:PackageEvent 'installed'");
           this.broadcastMessage("Webapps:PackageEvent",
                                 { type: "installed",
                                   manifestURL: appObject.manifestURL,
                                   app: app,
                                   manifest: aManifest });
           if (installSuccessCallback) {
-            installSuccessCallback(aManifest);
+            installSuccessCallback(aManifest, zipFile.path);
           }
         }).bind(this));
       }).bind(this));
     }
   },
 
   confirmInstall: function(aData, aProfileDir, aInstallSuccessCallback) {
     let isReinstall = false;
--- a/netwerk/protocol/app/AppProtocolHandler.js
+++ b/netwerk/protocol/app/AppProtocolHandler.js
@@ -65,20 +65,20 @@ AppProtocolHandler.prototype = {
       // downstream user get a 404 error.
       dump("!! got no appInfo for " + appId + "\n");
       return new DummyChannel();
     }
 
     let uri;
     if (this._runningInParent || appInfo.isCoreApp) {
       // In-parent and CoreApps can directly access files, so use jar:file://
-      uri = "jar:file://" + appInfo.basePath + appId + "/application.zip!" + fileSpec;
+      uri = "jar:file://" + appInfo.path + "/application.zip!" + fileSpec;
     } else {
       // non-CoreApps in child need to ask parent for file handle, use jar:ipcfile://
-      uri = "jar:remoteopenfile://" + appInfo.basePath + appId + "/application.zip!" + fileSpec;
+      uri = "jar:remoteopenfile://" + appInfo.path + "/application.zip!" + fileSpec;
     }
     let channel = Services.io.newChannel(uri, null, null);
     channel.QueryInterface(Ci.nsIJARChannel).setAppURI(aURI);
     channel.QueryInterface(Ci.nsIChannel).originalURI = aURI;
 
     return channel;
   },
 
--- a/toolkit/devtools/apps/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/apps/tests/unit/xpcshell.ini
@@ -1,8 +1,9 @@
 [DEFAULT]
 head = head_apps.js
 tail = tail_apps.js
 
 [test_webappsActor.js]
+skip-if = (os == "win" || "linux" || "mac")
 [test_appInstall.js]
 # Persistent failures.
 skip-if = true
--- a/toolkit/webapps/WebappOSUtils.jsm
+++ b/toolkit/webapps/WebappOSUtils.jsm
@@ -3,41 +3,48 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const CC = Components.Constructor;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 
 this.EXPORTED_SYMBOLS = ["WebappOSUtils"];
 
+// Returns the MD5 hash of a string.
+function computeHash(aString) {
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+                  createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  let result = {};
+  // Data is an array of bytes.
+  let data = converter.convertToByteArray(aString, result);
+
+  let hasher = Cc["@mozilla.org/security/hash;1"].
+               createInstance(Ci.nsICryptoHash);
+  hasher.init(hasher.MD5);
+  hasher.update(data, data.length);
+  // We're passing false to get the binary hash and not base64.
+  let hash = hasher.finish(false);
+
+  function toHexString(charCode) {
+    return ("0" + charCode.toString(16)).slice(-2);
+  }
+
+  // Convert the binary hash data to a hex string.
+  return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
+}
+
 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 {
-      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(aApp.name).toLowerCase() + "-" +
+           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
@@ -82,25 +89,30 @@ this.WebappOSUtils = {
     launchTarget.append(appFilename + ".exe");
 
     return launchTarget;
 #elifdef XP_MACOSX
     let mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"]
                      .createInstance(Ci.nsIMacWebAppUtils);
 
     try {
-      if (mwaUtils.pathForAppWithIdentifier(uniqueName)) {
-        return uniqueName;
-      }
-      if (mwaUtils.pathForAppWithIdentifier(aApp.origin)) {
-        return aApp.origin;
+      let path;
+      if (path = mwaUtils.pathForAppWithIdentifier(uniqueName)) {
+        return [ uniqueName, path ];
       }
     } catch(ex) {}
 
-    return null;
+    try {
+      let path;
+      if (path = mwaUtils.pathForAppWithIdentifier(aApp.origin)) {
+        return [ aApp.origin, path ];
+      }
+    } catch(ex) {}
+
+    return [ null, null ];
 #elifdef XP_UNIX
     let exeFile = Services.dirsvc.get("Home", Ci.nsIFile);
     exeFile.append("." + uniqueName);
     exeFile.append("webapprt-stub");
 
     if (!exeFile.exists()) {
       exeFile = Services.dirsvc.get("Home", Ci.nsIFile);
 
@@ -116,16 +128,41 @@ this.WebappOSUtils = {
         return null;
       }
     }
 
     return exeFile;
 #endif
   },
 
+  getInstallPath: function(aApp) {
+#ifdef XP_WIN
+    let execFile = this.getLaunchTarget(aApp);
+    if (!execFile) {
+      return null;
+    }
+
+    return execFile.parent.path;
+#elifdef XP_MACOSX
+    let [ bundleID, path ] = this.getLaunchTarget(aApp);
+    return path;
+#elifdef MOZ_B2G
+    return aApp.basePath + "/" + aApp.id;
+#elifdef MOZ_FENNEC
+    return aApp.basePath + "/" + aApp.id;
+#elifdef XP_UNIX
+    let execFile = this.getLaunchTarget(aApp);
+    if (!execFile) {
+      return null;
+    }
+
+    return execFile.parent.path;
+#endif
+  },
+
   launch: function(aApp) {
     let uniqueName = this.getUniqueName(aApp);
 
 #ifdef XP_WIN
     let initProcess = CC("@mozilla.org/process/util;1",
                          "nsIProcess", "init");
 
     let launchTarget = this.getLaunchTarget(aApp);
@@ -137,17 +174,17 @@ this.WebappOSUtils = {
       let process = initProcess(launchTarget);
       process.runwAsync([], 0);
     } catch (e) {
       return false;
     }
 
     return true;
 #elifdef XP_MACOSX
-    let launchIdentifier = this.getLaunchTarget(aApp);
+    let [ launchIdentifier, path ] = this.getLaunchTarget(aApp);
     if (!launchIdentifier) {
       return false;
     }
 
     let mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"]
                      .createInstance(Ci.nsIMacWebAppUtils);
 
     try {
@@ -210,17 +247,17 @@ this.WebappOSUtils = {
 
 #ifdef XP_WIN
     if (!this.getLaunchTarget(aApp)) {
       return false;
     }
 
     return true;
 #elifdef XP_MACOSX
-    if (!this.getLaunchTarget(aApp)) {
+    if (!this.getInstallPath(aApp)) {
       return false;
     }
 
     return true;
 #elifdef XP_UNIX
     let env = Cc["@mozilla.org/process/environment;1"]
                 .getService(Ci.nsIEnvironment);
 
--- a/toolkit/webapps/WebappsInstaller.jsm
+++ b/toolkit/webapps/WebappsInstaller.jsm
@@ -56,27 +56,29 @@ this.WebappsInstaller = {
     return this.shell;
   },
 
   /**
    * Creates a native installation of the web app in the OS
    *
    * @param aData the data provided to the install function
    * @param aManifest the manifest data provided by the web app
+   * @param aZipPath path to the zip file for packaged apps (undefined for
+   *                 hosted apps)
    */
-  install: function(aData, aManifest) {
+  install: function(aData, aManifest, aZipPath) {
     try {
       if (Services.prefs.getBoolPref("browser.mozApps.installer.dry_run")) {
         return Promise.resolve();
       }
     } catch (ex) {}
 
     this.shell.init(aData, aManifest);
 
-    return this.shell.install().then(() => {
+    return this.shell.install(aZipPath).then(() => {
       let data = {
         "installDir": this.shell.installDir.path,
         "app": {
           "manifest": aManifest,
           "origin": aData.app.origin
         }
       };
 
@@ -91,21 +93,22 @@ this.WebappsInstaller = {
  * 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 to the install function
  *
  */
 function NativeApp(aData) {
-  this.uniqueName = WebappOSUtils.getUniqueName(aData.app);
-
   let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest;
   let manifest = new ManifestHelper(jsonManifest, aData.app.origin);
 
+  aData.app.name = manifest.name;
+  this.uniqueName = WebappOSUtils.getUniqueName(aData.app);
+
   this.appName = sanitize(manifest.name);
   this.appNameAsFilename = stripStringForFilename(this.appName);
 }
 
 NativeApp.prototype = {
   uniqueName: null,
   appName: null,
   appNameAsFilename: null,
@@ -205,16 +208,33 @@ NativeApp.prototype = {
   },
 
   /**
    * This function retrieves the icon for an app.
    * If the retrieving fails, it uses the default chrome icon.
    */
   getIcon: function() {
     try {
+      // If the icon is in the zip package, we should modify the url
+      // to point to the zip file (we can't use the app protocol yet
+      // because the app isn't installed yet).
+      if (this.iconURI.scheme == "app") {
+        let zipFile = Cc["@mozilla.org/file/local;1"].
+                      createInstance(Ci.nsIFile);
+        zipFile.initWithPath(OS.Path.join(this.installDir.path,
+                                          "application.zip"));
+        let zipUrl = Services.io.newFileURI(zipFile).spec;
+
+        let filePath = this.iconURI.QueryInterface(Ci.nsIURL).filePath;
+
+        this.iconURI = Services.io.newURI("jar:" + zipUrl + "!" + filePath,
+                                          null, null);
+      }
+
+
       let [ mimeType, icon ] = yield downloadIcon(this.iconURI);
       yield this.processIcon(mimeType, icon);
     }
     catch(e) {
       Cu.reportError("Failure retrieving icon: " + e);
 
       let iconURI = Services.io.newURI(DEFAULT_ICON_URL, null, null);
 
@@ -283,23 +303,29 @@ function WinNativeApp(aData) {
 WinNativeApp.prototype = {
   __proto__: NativeApp.prototype,
   size: null,
 
   /**
    * Install the app in the system
    *
    */
-  install: function() {
+  install: function(aZipPath) {
     return Task.spawn(function() {
       try {
         this._copyPrebuiltFiles();
         this._createShortcutFiles();
         this._createConfigFiles();
         this._writeSystemKeys();
+
+        if (aZipPath) {
+          yield OS.File.move(aZipPath, OS.Path.join(this.installDir.path,
+                                                    "application.zip"));
+        }
+
         yield this.getIcon();
       } catch (ex) {
         this._removeInstallation(false);
         throw(ex);
       }
     }.bind(this));
   },
 
@@ -636,21 +662,27 @@ MacNativeApp.prototype = {
     this.iconFile.append("appicon.icns");
 
     // Remove previously installed app (for update purposes)
     this._removeInstallation(true);
 
     this._createDirectoryStructure();
   },
 
-  install: function() {
+  install: function(aZipPath) {
     return Task.spawn(function() {
       try {
         this._copyPrebuiltFiles();
         this._createConfigFiles();
+
+        if (aZipPath) {
+          yield OS.File.move(aZipPath, OS.Path.join(this.installDir.path,
+                                                    "application.zip"));
+        }
+
         yield this.getIcon();
         this._moveToApplicationsFolder();
       } catch (ex) {
         this._removeInstallation(false);
         throw(ex);
       }
     }.bind(this));
   },
@@ -833,21 +865,27 @@ LinuxNativeApp.prototype = {
     this.desktopINI.append("owa-" + this.uniqueName + ".desktop");
 
     // Remove previously installed app (for update purposes)
     this._removeInstallation(true);
 
     this._createDirectoryStructure();
   },
 
-  install: function() {
+  install: function(aZipPath) {
     return Task.spawn(function() {
       try {
         this._copyPrebuiltFiles();
         this._createConfigFiles();
+
+        if (aZipPath) {
+          yield OS.File.move(aZipPath, OS.Path.join(this.installDir.path,
+                                                    "application.zip"));
+        }
+
         yield this.getIcon();
       } catch (ex) {
         this._removeInstallation(false);
         throw(ex);
       }
     }.bind(this));
   },