michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu, Constructor: CC } = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/FileUtils.jsm"); michael@0: Cu.import("resource://gre/modules/osfile.jsm"); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: michael@0: this.EXPORTED_SYMBOLS = ["WebappOSUtils"]; michael@0: michael@0: // Returns the MD5 hash of a string. michael@0: function computeHash(aString) { michael@0: let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. michael@0: createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: converter.charset = "UTF-8"; michael@0: let result = {}; michael@0: // Data is an array of bytes. michael@0: let data = converter.convertToByteArray(aString, result); michael@0: michael@0: let hasher = Cc["@mozilla.org/security/hash;1"]. michael@0: createInstance(Ci.nsICryptoHash); michael@0: hasher.init(hasher.MD5); michael@0: hasher.update(data, data.length); michael@0: // We're passing false to get the binary hash and not base64. michael@0: let hash = hasher.finish(false); michael@0: michael@0: function toHexString(charCode) { michael@0: return ("0" + charCode.toString(16)).slice(-2); michael@0: } michael@0: michael@0: // Convert the binary hash data to a hex string. michael@0: return [toHexString(hash.charCodeAt(i)) for (i in hash)].join(""); michael@0: } michael@0: michael@0: this.WebappOSUtils = { michael@0: getUniqueName: function(aApp) { michael@0: return this.sanitizeStringForFilename(aApp.name).toLowerCase() + "-" + michael@0: computeHash(aApp.manifestURL); michael@0: }, michael@0: michael@0: #ifdef XP_WIN michael@0: /** michael@0: * Returns the registry key associated to the given app and a boolean that michael@0: * specifies whether we're using the old naming scheme or the new one. michael@0: */ michael@0: getAppRegKey: function(aApp) { michael@0: let regKey = Cc["@mozilla.org/windows-registry-key;1"]. michael@0: createInstance(Ci.nsIWindowsRegKey); michael@0: michael@0: try { michael@0: regKey.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, michael@0: "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + michael@0: this.getUniqueName(aApp), Ci.nsIWindowsRegKey.ACCESS_READ); michael@0: michael@0: return { value: regKey, michael@0: namingSchemeVersion: 2}; michael@0: } catch (ex) {} michael@0: michael@0: // Fall back to the old installation naming scheme michael@0: try { michael@0: regKey.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, michael@0: "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + michael@0: aApp.origin, Ci.nsIWindowsRegKey.ACCESS_READ); michael@0: michael@0: return { value: regKey, michael@0: namingSchemeVersion: 1 }; michael@0: } catch (ex) {} michael@0: michael@0: return null; michael@0: }, michael@0: #endif michael@0: michael@0: /** michael@0: * Returns the executable of the given app, identifying it by its unique name, michael@0: * which is in either the new format or the old format. michael@0: * On Mac OS X, it returns the identifier of the app. michael@0: * michael@0: * The new format ensures a readable and unique name for an app by combining michael@0: * its name with a hash of its manifest URL. The old format uses its origin, michael@0: * which is only unique until we support multiple apps per origin. michael@0: */ michael@0: getLaunchTarget: function(aApp) { michael@0: #ifdef XP_WIN michael@0: let appRegKey = this.getAppRegKey(aApp); michael@0: michael@0: if (!appRegKey) { michael@0: return null; michael@0: } michael@0: michael@0: let appFilename, installLocation; michael@0: try { michael@0: appFilename = appRegKey.value.readStringValue("AppFilename"); michael@0: installLocation = appRegKey.value.readStringValue("InstallLocation"); michael@0: } catch (ex) { michael@0: return null; michael@0: } finally { michael@0: appRegKey.value.close(); michael@0: } michael@0: michael@0: installLocation = installLocation.substring(1, installLocation.length - 1); michael@0: michael@0: if (appRegKey.namingSchemeVersion == 1 && michael@0: !this.isOldInstallPathValid(aApp, installLocation)) { michael@0: return null; michael@0: } michael@0: michael@0: let initWithPath = CC("@mozilla.org/file/local;1", michael@0: "nsILocalFile", "initWithPath"); michael@0: let launchTarget = initWithPath(installLocation); michael@0: launchTarget.append(appFilename + ".exe"); michael@0: michael@0: return launchTarget; michael@0: #elifdef XP_MACOSX michael@0: let uniqueName = this.getUniqueName(aApp); michael@0: michael@0: let mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"]. michael@0: createInstance(Ci.nsIMacWebAppUtils); michael@0: michael@0: try { michael@0: let path; michael@0: if (path = mwaUtils.pathForAppWithIdentifier(uniqueName)) { michael@0: return [ uniqueName, path ]; michael@0: } michael@0: } catch(ex) {} michael@0: michael@0: // Fall back to the old installation naming scheme michael@0: try { michael@0: let path; michael@0: if ((path = mwaUtils.pathForAppWithIdentifier(aApp.origin)) && michael@0: this.isOldInstallPathValid(aApp, path)) { michael@0: return [ aApp.origin, path ]; michael@0: } michael@0: } catch(ex) {} michael@0: michael@0: return [ null, null ]; michael@0: #elifdef XP_UNIX michael@0: let uniqueName = this.getUniqueName(aApp); michael@0: michael@0: let exeFile = Services.dirsvc.get("Home", Ci.nsIFile); michael@0: exeFile.append("." + uniqueName); michael@0: exeFile.append("webapprt-stub"); michael@0: michael@0: // Fall back to the old installation naming scheme michael@0: if (!exeFile.exists()) { michael@0: exeFile = Services.dirsvc.get("Home", Ci.nsIFile); michael@0: michael@0: let origin = Services.io.newURI(aApp.origin, null, null); michael@0: let installDir = "." + origin.scheme + ";" + michael@0: origin.host + michael@0: (origin.port != -1 ? ";" + origin.port : ""); michael@0: michael@0: exeFile.append(installDir); michael@0: exeFile.append("webapprt-stub"); michael@0: michael@0: if (!exeFile.exists() || michael@0: !this.isOldInstallPathValid(aApp, exeFile.parent.path)) { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: return exeFile; michael@0: #endif michael@0: }, michael@0: michael@0: getInstallPath: function(aApp) { michael@0: #ifdef MOZ_B2G michael@0: // All b2g builds michael@0: return aApp.basePath + "/" + aApp.id; michael@0: michael@0: #elifdef MOZ_FENNEC michael@0: // All fennec michael@0: return aApp.basePath + "/" + aApp.id; michael@0: michael@0: #elifdef MOZ_PHOENIX michael@0: // Firefox michael@0: michael@0: #ifdef XP_WIN michael@0: let execFile = this.getLaunchTarget(aApp); michael@0: if (!execFile) { michael@0: return null; michael@0: } michael@0: michael@0: return execFile.parent.path; michael@0: #elifdef XP_MACOSX michael@0: let [ bundleID, path ] = this.getLaunchTarget(aApp); michael@0: return path; michael@0: #elifdef XP_UNIX michael@0: let execFile = this.getLaunchTarget(aApp); michael@0: if (!execFile) { michael@0: return null; michael@0: } michael@0: michael@0: return execFile.parent.path; michael@0: #endif michael@0: michael@0: #elifdef MOZ_WEBAPP_RUNTIME michael@0: // Webapp runtime michael@0: michael@0: #ifdef XP_WIN michael@0: let execFile = this.getLaunchTarget(aApp); michael@0: if (!execFile) { michael@0: return null; michael@0: } michael@0: michael@0: return execFile.parent.path; michael@0: #elifdef XP_MACOSX michael@0: let [ bundleID, path ] = this.getLaunchTarget(aApp); michael@0: return path; michael@0: #elifdef XP_UNIX michael@0: let execFile = this.getLaunchTarget(aApp); michael@0: if (!execFile) { michael@0: return null; michael@0: } michael@0: michael@0: return execFile.parent.path; michael@0: #endif michael@0: michael@0: #endif michael@0: // Anything unsupported, like Metro michael@0: throw new Error("Unsupported apps platform"); michael@0: }, michael@0: michael@0: getPackagePath: function(aApp) { michael@0: let packagePath = this.getInstallPath(aApp); michael@0: michael@0: // Only for Firefox on Mac OS X michael@0: #ifndef MOZ_B2G michael@0: #ifdef XP_MACOSX michael@0: packagePath = OS.Path.join(packagePath, "Contents", "Resources"); michael@0: #endif michael@0: #endif michael@0: michael@0: return packagePath; michael@0: }, michael@0: michael@0: launch: function(aApp) { michael@0: let uniqueName = this.getUniqueName(aApp); michael@0: michael@0: #ifdef XP_WIN michael@0: let launchTarget = this.getLaunchTarget(aApp); michael@0: if (!launchTarget) { michael@0: return false; michael@0: } michael@0: michael@0: try { michael@0: let process = Cc["@mozilla.org/process/util;1"]. michael@0: createInstance(Ci.nsIProcess); michael@0: michael@0: process.init(launchTarget); michael@0: process.runwAsync([], 0); michael@0: } catch (e) { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: #elifdef XP_MACOSX michael@0: let [ launchIdentifier, path ] = this.getLaunchTarget(aApp); michael@0: if (!launchIdentifier) { michael@0: return false; michael@0: } michael@0: michael@0: let mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"]. michael@0: createInstance(Ci.nsIMacWebAppUtils); michael@0: michael@0: try { michael@0: mwaUtils.launchAppWithIdentifier(launchIdentifier); michael@0: } catch(e) { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: #elifdef XP_UNIX michael@0: let exeFile = this.getLaunchTarget(aApp); michael@0: if (!exeFile) { michael@0: return false; michael@0: } michael@0: michael@0: try { michael@0: let process = Cc["@mozilla.org/process/util;1"] michael@0: .createInstance(Ci.nsIProcess); michael@0: michael@0: process.init(exeFile); michael@0: process.runAsync([], 0); michael@0: } catch (e) { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: #endif michael@0: }, michael@0: michael@0: uninstall: function(aApp) { michael@0: #ifdef XP_WIN michael@0: let appRegKey = this.getAppRegKey(aApp); michael@0: michael@0: if (!appRegKey) { michael@0: return Promise.reject("App registry key not found"); michael@0: } michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: try { michael@0: let uninstallerPath = appRegKey.value.readStringValue("UninstallString"); michael@0: uninstallerPath = uninstallerPath.substring(1, uninstallerPath.length - 1); michael@0: michael@0: let uninstaller = Cc["@mozilla.org/file/local;1"]. michael@0: createInstance(Ci.nsIFile); michael@0: uninstaller.initWithPath(uninstallerPath); michael@0: michael@0: let process = Cc["@mozilla.org/process/util;1"]. michael@0: createInstance(Ci.nsIProcess); michael@0: process.init(uninstaller); michael@0: process.runwAsync(["/S"], 1, (aSubject, aTopic) => { michael@0: if (aTopic == "process-finished") { michael@0: deferred.resolve(true); michael@0: } else { michael@0: deferred.reject("Uninstaller failed with exit code: " + aSubject.exitValue); michael@0: } michael@0: }); michael@0: } catch (e) { michael@0: deferred.reject(e); michael@0: } finally { michael@0: appRegKey.value.close(); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: #elifdef XP_MACOSX michael@0: let [ , path ] = this.getLaunchTarget(aApp); michael@0: if (!path) { michael@0: return Promise.reject("App not found"); michael@0: } michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: let mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"]. michael@0: createInstance(Ci.nsIMacWebAppUtils); michael@0: michael@0: mwaUtils.trashApp(path, (aResult) => { michael@0: if (aResult == Cr.NS_OK) { michael@0: deferred.resolve(true); michael@0: } else { michael@0: deferred.resolve("Error moving the app to the Trash: " + aResult); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: #elifdef XP_UNIX michael@0: let exeFile = this.getLaunchTarget(aApp); michael@0: if (!exeFile) { michael@0: return Promise.reject("App executable file not found"); michael@0: } michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: try { michael@0: let process = Cc["@mozilla.org/process/util;1"] michael@0: .createInstance(Ci.nsIProcess); michael@0: michael@0: process.init(exeFile); michael@0: process.runAsync(["-remove"], 1, (aSubject, aTopic) => { michael@0: if (aTopic == "process-finished") { michael@0: deferred.resolve(true); michael@0: } else { michael@0: deferred.reject("Uninstaller failed with exit code: " + aSubject.exitValue); michael@0: } michael@0: }); michael@0: } catch (e) { michael@0: deferred.reject(e); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: #endif michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if the given install path (in the old naming scheme) actually michael@0: * belongs to the given application. michael@0: */ michael@0: isOldInstallPathValid: function(aApp, aInstallPath) { michael@0: // Applications with an origin that starts with "app" are packaged apps and michael@0: // packaged apps have never been installed using the old naming scheme. michael@0: // After bug 910465, we'll have a better way to check if an app is michael@0: // packaged. michael@0: if (aApp.origin.startsWith("app")) { michael@0: return false; michael@0: } michael@0: michael@0: // Bug 915480: We could check the app name from the manifest to michael@0: // better verify the installation path. michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Checks if the given app is locally installed. michael@0: */ michael@0: isLaunchable: function(aApp) { michael@0: let uniqueName = this.getUniqueName(aApp); michael@0: michael@0: #ifdef XP_WIN michael@0: if (!this.getLaunchTarget(aApp)) { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: #elifdef XP_MACOSX michael@0: if (!this.getInstallPath(aApp)) { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: #elifdef XP_UNIX michael@0: let env = Cc["@mozilla.org/process/environment;1"] michael@0: .getService(Ci.nsIEnvironment); michael@0: michael@0: let xdg_data_home_env; michael@0: try { michael@0: xdg_data_home_env = env.get("XDG_DATA_HOME"); michael@0: } catch(ex) {} michael@0: michael@0: let desktopINI; michael@0: if (xdg_data_home_env) { michael@0: desktopINI = new FileUtils.File(xdg_data_home_env); michael@0: } else { michael@0: desktopINI = FileUtils.getFile("Home", [".local", "share"]); michael@0: } michael@0: desktopINI.append("applications"); michael@0: desktopINI.append("owa-" + uniqueName + ".desktop"); michael@0: michael@0: // Fall back to the old installation naming scheme michael@0: if (!desktopINI.exists()) { michael@0: if (xdg_data_home_env) { michael@0: desktopINI = new FileUtils.File(xdg_data_home_env); michael@0: } else { michael@0: desktopINI = FileUtils.getFile("Home", [".local", "share"]); michael@0: } michael@0: michael@0: let origin = Services.io.newURI(aApp.origin, null, null); michael@0: let oldUniqueName = origin.scheme + ";" + michael@0: origin.host + michael@0: (origin.port != -1 ? ";" + origin.port : ""); michael@0: michael@0: desktopINI.append("owa-" + oldUniqueName + ".desktop"); michael@0: michael@0: if (!desktopINI.exists()) { michael@0: return false; michael@0: } michael@0: michael@0: let installDir = Services.dirsvc.get("Home", Ci.nsIFile); michael@0: installDir.append("." + origin.scheme + ";" + origin.host + michael@0: (origin.port != -1 ? ";" + origin.port : "")); michael@0: michael@0: return isOldInstallPathValid(aApp, installDir.path); michael@0: } michael@0: michael@0: return true; michael@0: #endif michael@0: }, michael@0: michael@0: /** michael@0: * Sanitize the filename (accepts only a-z, 0-9, - and _) michael@0: */ michael@0: sanitizeStringForFilename: function(aPossiblyBadFilenameString) { michael@0: return aPossiblyBadFilenameString.replace(/[^a-z0-9_\-]/gi, ""); michael@0: } michael@0: }