Bug 777402 - Use app name + manifest url hash as unique name for apps but forbid multiple apps per origin and webapposutils refactoring. r=myk,fabrice

This commit is contained in:
Marco Castelluccio 2013-08-01 17:00:22 -07:00
parent 6c49f9ce91
commit cdfe88088a
4 changed files with 271 additions and 177 deletions

View File

@ -482,6 +482,30 @@ this.AppsUtils = {
// Nothing failed.
return true;
},
// Returns the MD5 hash of a string.
computeHash: function(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("");
}
}

View File

@ -20,6 +20,7 @@ Cu.import("resource://gre/modules/PermissionsInstaller.jsm");
Cu.import("resource://gre/modules/OfflineCacheInstaller.jsm");
Cu.import("resource://gre/modules/SystemMessagePermissionsChecker.jsm");
Cu.import("resource://gre/modules/AppDownloadManager.jsm");
Cu.import("resource://gre/modules/WebappOSUtils.jsm");
#ifdef MOZ_WIDGET_GONK
XPCOMUtils.defineLazyGetter(this, "libcutils", function() {
@ -1372,26 +1373,7 @@ this.DOMApplicationRegistry = {
// Returns the MD5 hash of the manifest.
computeManifestHash: function(aManifest) {
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(JSON.stringify(aManifest), 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("");
return AppsUtils.computeHash(JSON.stringify(aManifest));
},
// Updates the redirect mapping, activities and system message handlers.
@ -1776,13 +1758,6 @@ this.DOMApplicationRegistry = {
": " + aError);
}.bind(this);
// Disallow reinstalls from the same origin for now.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=821288
if (this._appId(app.origin) !== null && this._isLaunchable(app.origin)) {
sendError("REINSTALL_FORBIDDEN");
return;
}
// Hosted apps can't be trusted or certified, so just check that the
// manifest doesn't ask for those.
function checkAppStatus(aManifest) {
@ -1812,6 +1787,18 @@ this.DOMApplicationRegistry = {
return;
}
// Disallow multiple hosted apps installations from the same origin for now.
// We will remove this code after multiple apps per origin are supported (bug 778277).
// This will also disallow reinstalls from the same origin for now.
for (let id in this.webapps) {
if (this.webapps[id].origin == app.origin &&
!this.webapps[id].packageHash &&
this._isLaunchable(this.webapps[id])) {
sendError("MULTIPLE_APPS_PER_ORIGIN_FORBIDDEN");
return;
}
}
if (!AppsUtils.checkManifest(app.manifest, app)) {
sendError("INVALID_MANIFEST");
} else if (!AppsUtils.checkInstallAllowed(app.manifest, app.installOrigin)) {
@ -2827,7 +2814,7 @@ this.DOMApplicationRegistry = {
for (let id in this.webapps) {
if (this.webapps[id].origin == aData.origin &&
this.webapps[id].localId == aData.appId &&
this._isLaunchable(this.webapps[id].origin)) {
this._isLaunchable(this.webapps[id])) {
let app = AppsUtils.cloneAppObject(this.webapps[id]);
aData.apps.push(app);
tmp.push({ id: id });
@ -2874,7 +2861,7 @@ this.DOMApplicationRegistry = {
for (let id in this.webapps) {
if (this.webapps[id].installOrigin == aData.origin &&
this._isLaunchable(this.webapps[id].origin)) {
this._isLaunchable(this.webapps[id])) {
aData.apps.push(AppsUtils.cloneAppObject(this.webapps[id]));
tmp.push({ id: id });
}
@ -2892,7 +2879,7 @@ this.DOMApplicationRegistry = {
let tmp = [];
for (let id in this.webapps) {
if (!this._isLaunchable(this.webapps[id].origin)) {
if (!this._isLaunchable(this.webapps[id])) {
aData.apps.push(AppsUtils.cloneAppObject(this.webapps[id]));
tmp.push({ id: id });
}
@ -2919,7 +2906,7 @@ this.DOMApplicationRegistry = {
for (let id in this.webapps) {
let app = AppsUtils.cloneAppObject(this.webapps[id]);
if (!this._isLaunchable(app.origin))
if (!this._isLaunchable(app))
continue;
apps.push(app);
@ -3072,58 +3059,11 @@ this.DOMApplicationRegistry = {
this._saveApps(aCallback);
},
_isLaunchable: function(aOrigin) {
_isLaunchable: function(aApp) {
if (this.allAppsLaunchable)
return true;
#ifdef XP_WIN
let uninstallKey = Cc["@mozilla.org/windows-registry-key;1"]
.createInstance(Ci.nsIWindowsRegKey);
try {
uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER,
"SOFTWARE\\Microsoft\\Windows\\" +
"CurrentVersion\\Uninstall\\" +
aOrigin,
uninstallKey.ACCESS_READ);
uninstallKey.close();
return true;
} catch (ex) {
return false;
}
#elifdef XP_MACOSX
let mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"]
.createInstance(Ci.nsIMacWebAppUtils);
return !!mwaUtils.pathForAppWithIdentifier(aOrigin);
#elifdef XP_UNIX
let env = Cc["@mozilla.org/process/environment;1"]
.getService(Ci.nsIEnvironment);
let xdg_data_home_env;
try {
xdg_data_home_env = env.get("XDG_DATA_HOME");
} catch(ex) {
}
let desktopINI;
if (xdg_data_home_env) {
desktopINI = new FileUtils.File(xdg_data_home_env);
} else {
desktopINI = FileUtils.getFile("Home", [".local", "share"]);
}
desktopINI.append("applications");
let origin = Services.io.newURI(aOrigin, null, null);
let uniqueName = origin.scheme + ";" +
origin.host +
(origin.port != -1 ? ";" + origin.port : "");
desktopINI.append("owa-" + uniqueName + ".desktop");
return desktopINI.exists();
#else
return true;
#endif
return WebappOSUtils.isLaunchable(aApp);
},
_notifyCategoryAndObservers: function(subject, topic, data, msg) {

View File

@ -8,100 +8,255 @@ 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"];
this.WebappOSUtils = {
launch: function(aData) {
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.
if (aApp.name) {
name = aApp.name;
} else {
name = aApp.manifest.name;
}
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
* its name with a hash of its manifest URL. The old format uses its origin,
* which is only unique until we support multiple apps per origin.
*/
getLaunchTarget: function(aApp) {
let uniqueName = this.getUniqueName(aApp);
#ifdef XP_WIN
let appRegKey;
try {
let open = CC("@mozilla.org/windows-registry-key;1",
"nsIWindowsRegKey", "open");
let initWithPath = CC("@mozilla.org/file/local;1",
"nsILocalFile", "initWithPath");
let initProcess = CC("@mozilla.org/process/util;1",
"nsIProcess", "init");
appRegKey = open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" +
aData.origin, Ci.nsIWindowsRegKey.ACCESS_READ);
uniqueName, Ci.nsIWindowsRegKey.ACCESS_READ);
} catch (ex) {
try {
appRegKey = open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" +
aApp.origin, Ci.nsIWindowsRegKey.ACCESS_READ);
} catch (ex) {
return null;
}
}
let launchTarget = initWithPath(appRegKey.readStringValue("InstallLocation"));
launchTarget.append(appRegKey.readStringValue("AppFilename") + ".exe");
let appFilename, installLocation;
try {
appFilename = appRegKey.readStringValue("AppFilename");
installLocation = appRegKey.readStringValue("InstallLocation");
} catch (ex) {
return null;
} finally {
appRegKey.close();
}
let initWithPath = CC("@mozilla.org/file/local;1",
"nsILocalFile", "initWithPath");
let launchTarget = initWithPath(installLocation);
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;
}
} catch(ex) {}
return 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);
let origin = Services.io.newURI(aApp.origin, null, null);
let installDir = "." + origin.scheme + ";" +
origin.host +
(origin.port != -1 ? ";" + origin.port : "");
exeFile.append(installDir);
exeFile.append("webapprt-stub");
if (!exeFile.exists()) {
return null;
}
}
return exeFile;
#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);
if (!launchTarget) {
return false;
}
try {
let process = initProcess(launchTarget);
process.runwAsync([], 0);
} catch (e) {
return false;
} finally {
if (appRegKey) {
appRegKey.close();
}
}
return true;
#elifdef XP_MACOSX
let mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"]
.createInstance(Ci.nsIMacWebAppUtils);
let appPath;
try {
appPath = mwaUtils.pathForAppWithIdentifier(aData.origin);
} catch (e) {}
if (appPath) {
mwaUtils.launchAppWithIdentifier(aData.origin);
return true;
let launchIdentifier = this.getLaunchTarget(aApp);
if (!launchIdentifier) {
return false;
}
return false;
#elifdef XP_UNIX
let origin = Services.io.newURI(aData.origin, null, null);
let installDir = "." + origin.scheme + ";" +
origin.host +
(origin.port != -1 ? ";" + origin.port : "");
let exeFile = Services.dirsvc.get("Home", Ci.nsIFile);
exeFile.append(installDir);
exeFile.append("webapprt-stub");
let mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"]
.createInstance(Ci.nsIMacWebAppUtils);
try {
if (exeFile.exists()) {
let process = Cc["@mozilla.org/process/util;1"]
.createInstance(Ci.nsIProcess);
process.init(exeFile);
process.runAsync([], 0);
return true;
}
} catch (e) {}
mwaUtils.launchAppWithIdentifier(launchIdentifier);
} catch(e) {
return false;
}
return false;
return true;
#elifdef XP_UNIX
let exeFile = this.getLaunchTarget(aApp);
if (!exeFile) {
return false;
}
try {
let process = Cc["@mozilla.org/process/util;1"]
.createInstance(Ci.nsIProcess);
process.init(exeFile);
process.runAsync([], 0);
} catch (e) {
return false;
}
return true;
#endif
},
uninstall: function(aData) {
uninstall: function(aApp) {
let uniqueName = this.getUniqueName(aApp);
#ifdef XP_UNIX
#ifndef XP_MACOSX
let origin = Services.io.newURI(aData.origin, null, null);
let installDir = "." + origin.scheme + ";" +
origin.host +
(origin.port != -1 ? ";" + origin.port : "");
let exeFile = Services.dirsvc.get("Home", Ci.nsIFile);
exeFile.append(installDir);
exeFile.append("webapprt-stub");
let exeFile = this.getLaunchTarget(aApp);
if (!exeFile) {
return false;
}
try {
if (exeFile.exists()) {
var process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
process.init(exeFile);
process.runAsync(["-remove"], 1);
return true;
}
} catch(e) {}
let process = Cc["@mozilla.org/process/util;1"]
.createInstance(Ci.nsIProcess);
return false;
process.init(exeFile);
process.runAsync(["-remove"], 1);
} catch (e) {
return false;
}
return true;
#endif
#endif
},
/**
* Checks if the given app is locally installed.
*/
isLaunchable: function(aApp) {
let uniqueName = this.getUniqueName(aApp);
#ifdef XP_WIN
if (!this.getLaunchTarget(aApp)) {
return false;
}
return true;
#elifdef XP_MACOSX
if (!this.getLaunchTarget(aApp)) {
return false;
}
return true;
#elifdef XP_UNIX
let env = Cc["@mozilla.org/process/environment;1"]
.getService(Ci.nsIEnvironment);
let xdg_data_home_env;
try {
xdg_data_home_env = env.get("XDG_DATA_HOME");
} catch(ex) {}
let desktopINI;
if (xdg_data_home_env) {
desktopINI = new FileUtils.File(xdg_data_home_env);
} else {
desktopINI = FileUtils.getFile("Home", [".local", "share"]);
}
desktopINI.append("applications");
desktopINI.append("owa-" + uniqueName + ".desktop");
if (!desktopINI.exists()) {
if (xdg_data_home_env) {
desktopINI = new FileUtils.File(xdg_data_home_env);
} else {
desktopINI = FileUtils.getFile("Home", [".local", "share"]);
}
let origin = Services.io.newURI(aApp.origin, null, null);
let oldUniqueName = origin.scheme + ";" +
origin.host +
(origin.port != -1 ? ";" + origin.port : "");
desktopINI.append("owa-" + oldUniqueName + ".desktop");
return desktopINI.exists();
}
return true;
#endif
},
/**
* Sanitize the filename (accepts only a-z, 0-9, - and _)
*/
sanitizeStringForFilename: function(aPossiblyBadFilenameString) {
return aPossiblyBadFilenameString.replace(/[^a-z0-9_\-]/gi, "");
}
}

View File

@ -13,6 +13,8 @@ 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 = {
/**
@ -71,14 +73,9 @@ this.WebappsInstaller = {
function NativeApp(aData) {
let app = this.app = aData.app;
let origin = Services.io.newURI(app.origin, null, null);
this.uniqueName = WebappOSUtils.getUniqueName(app);
if (app.manifest.launch_path) {
this.launchURI = Services.io.newURI(origin.resolve(app.manifest.launch_path),
null, null);
} else {
this.launchURI = origin.clone();
}
let origin = Services.io.newURI(app.origin, null, null);
let biggestIcon = getBiggestIconURL(app.manifest.icons);
try {
@ -133,8 +130,7 @@ function NativeApp(aData) {
*
* The Windows installation process will generate the following files:
*
* ${FolderName} = protocol;app-origin[;port]
* e.g.: subdomain.example.com;http;85
* ${FolderName} = sanitized app name + "-" + manifest url hash
*
* %APPDATA%/${FolderName}
* - webapp.ini
@ -199,18 +195,9 @@ WinNativeApp.prototype = {
this.appNameAsFilename = "webapp";
}
// The ${InstallDir} format is as follows:
// protocol
// + ";" + host of the app origin
// + ";" + port (only if port is not default)
// The ${InstallDir} is: sanitized app name + "-" + manifest url hash
this.installDir = Services.dirsvc.get("AppData", Ci.nsIFile);
let installDirLeaf = this.launchURI.scheme
+ ";"
+ this.launchURI.host;
if (this.launchURI.port != -1) {
installDirLeaf += ";" + this.launchURI.port;
}
this.installDir.append(installDirLeaf);
this.installDir.append(this.uniqueName);
this.webapprt = this.installDir.clone();
this.webapprt.append(this.appNameAsFilename + ".exe");
@ -233,8 +220,7 @@ WinNativeApp.prototype = {
this.iconFile.append("default");
this.iconFile.append("default.ico");
this.uninstallSubkeyStr = this.launchURI.scheme + "://" +
this.launchURI.hostPort;
this.uninstallSubkeyStr = this.uniqueName;
},
/**
@ -477,14 +463,9 @@ MacNativeApp.prototype = {
this.appNameAsFilename = "Webapp";
}
// The ${ProfileDir} format is as follows:
// host of the app origin + ";" +
// protocol + ";" +
// port (-1 for default port)
// The ${ProfileDir} is: sanitized app name + "-" + manifest url hash
this.appProfileDir = this.appSupportDir.clone();
this.appProfileDir.append(this.launchURI.host + ";" +
this.launchURI.scheme + ";" +
this.launchURI.port);
this.appProfileDir.append(this.uniqueName);
this.installDir = Services.dirsvc.get("TmpD", Ci.nsILocalFile);
this.installDir.append(this.appNameAsFilename + ".app");
@ -585,7 +566,7 @@ MacNativeApp.prototype = {
<key>CFBundleIconFile</key>\n\
<string>appicon</string>\n\
<key>CFBundleIdentifier</key>\n\
<string>' + escapeXML(this.launchURI.prePath) + '</string>\n\
<string>' + escapeXML(this.uniqueName) + '</string>\n\
<key>CFBundleInfoDictionaryVersion</key>\n\
<string>6.0</string>\n\
<key>CFBundleName</key>\n\
@ -670,14 +651,8 @@ function LinuxNativeApp(aData) {
LinuxNativeApp.prototype = {
_init: function() {
// The ${InstallDir} and desktop entry filename format is as follows:
// host of the app origin + ";" +
// protocol
// + ";" + port (only if port is not default)
this.uniqueName = this.launchURI.scheme + ";" + this.launchURI.host;
if (this.launchURI.port != -1)
this.uniqueName += ";" + this.launchURI.port;
// 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);