mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-31 06:05:44 +00:00
919 lines
27 KiB
JavaScript
919 lines
27 KiB
JavaScript
/* 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/. */
|
|
|
|
"use strict";
|
|
|
|
const Cu = Components.utils;
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cr = Components.results;
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/Promise.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
|
"resource://gre/modules/FileUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils",
|
|
"resource://gre/modules/WebappOSUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
|
"resource://gre/modules/NetUtil.jsm");
|
|
|
|
// Shared code for AppsServiceChild.jsm, TrustedHostedAppsUtils.jsm,
|
|
// Webapps.jsm and Webapps.js
|
|
|
|
this.EXPORTED_SYMBOLS =
|
|
["AppsUtils", "ManifestHelper", "isAbsoluteURI", "mozIApplication"];
|
|
|
|
function debug(s) {
|
|
//dump("-*- AppsUtils.jsm: " + s + "\n");
|
|
}
|
|
|
|
this.isAbsoluteURI = function(aURI) {
|
|
let foo = Services.io.newURI("http://foo", null, null);
|
|
let bar = Services.io.newURI("http://bar", null, null);
|
|
return Services.io.newURI(aURI, null, foo).prePath != foo.prePath ||
|
|
Services.io.newURI(aURI, null, bar).prePath != bar.prePath;
|
|
}
|
|
|
|
this.mozIApplication = function(aApp) {
|
|
_setAppProperties(this, aApp);
|
|
}
|
|
|
|
mozIApplication.prototype = {
|
|
hasPermission: function(aPermission) {
|
|
let uri = Services.io.newURI(this.origin, null, null);
|
|
let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]
|
|
.getService(Ci.nsIScriptSecurityManager);
|
|
// This helper checks an URI inside |aApp|'s origin and part of |aApp| has a
|
|
// specific permission. It is not checking if browsers inside |aApp| have such
|
|
// permission.
|
|
let principal = secMan.getAppCodebasePrincipal(uri, this.localId,
|
|
/*mozbrowser*/false);
|
|
let perm = Services.perms.testExactPermissionFromPrincipal(principal,
|
|
aPermission);
|
|
return (perm === Ci.nsIPermissionManager.ALLOW_ACTION);
|
|
},
|
|
|
|
hasWidgetPage: function(aPageURL) {
|
|
return this.widgetPages.indexOf(aPageURL) != -1;
|
|
},
|
|
|
|
QueryInterface: function(aIID) {
|
|
if (aIID.equals(Ci.mozIApplication) ||
|
|
aIID.equals(Ci.nsISupports))
|
|
return this;
|
|
throw Cr.NS_ERROR_NO_INTERFACE;
|
|
}
|
|
}
|
|
|
|
function _setAppProperties(aObj, aApp) {
|
|
aObj.name = aApp.name;
|
|
aObj.csp = aApp.csp;
|
|
aObj.installOrigin = aApp.installOrigin;
|
|
aObj.origin = aApp.origin;
|
|
#ifdef MOZ_WIDGET_ANDROID
|
|
aObj.apkPackageName = aApp.apkPackageName;
|
|
#endif
|
|
aObj.receipts = aApp.receipts ? JSON.parse(JSON.stringify(aApp.receipts)) : null;
|
|
aObj.installTime = aApp.installTime;
|
|
aObj.manifestURL = aApp.manifestURL;
|
|
aObj.appStatus = aApp.appStatus;
|
|
aObj.removable = aApp.removable;
|
|
aObj.id = aApp.id;
|
|
aObj.localId = aApp.localId;
|
|
aObj.basePath = aApp.basePath;
|
|
aObj.progress = aApp.progress || 0.0;
|
|
aObj.installState = aApp.installState || "installed";
|
|
aObj.downloadAvailable = aApp.downloadAvailable;
|
|
aObj.downloading = aApp.downloading;
|
|
aObj.readyToApplyDownload = aApp.readyToApplyDownload;
|
|
aObj.downloadSize = aApp.downloadSize || 0;
|
|
aObj.lastUpdateCheck = aApp.lastUpdateCheck;
|
|
aObj.updateTime = aApp.updateTime;
|
|
aObj.etag = aApp.etag;
|
|
aObj.packageEtag = aApp.packageEtag;
|
|
aObj.manifestHash = aApp.manifestHash;
|
|
aObj.packageHash = aApp.packageHash;
|
|
aObj.staged = aApp.staged;
|
|
aObj.installerAppId = aApp.installerAppId || Ci.nsIScriptSecurityManager.NO_APP_ID;
|
|
aObj.installerIsBrowser = !!aApp.installerIsBrowser;
|
|
aObj.storeId = aApp.storeId || "";
|
|
aObj.storeVersion = aApp.storeVersion || 0;
|
|
aObj.role = aApp.role || "";
|
|
aObj.redirects = aApp.redirects;
|
|
aObj.widgetPages = aApp.widgetPages || [];
|
|
aObj.kind = aApp.kind;
|
|
aObj.enabled = aApp.enabled !== undefined ? aApp.enabled : true;
|
|
aObj.sideloaded = aApp.sideloaded;
|
|
}
|
|
|
|
this.AppsUtils = {
|
|
// Clones a app, without the manifest.
|
|
cloneAppObject: function(aApp) {
|
|
let obj = {};
|
|
_setAppProperties(obj, aApp);
|
|
return obj;
|
|
},
|
|
|
|
// Creates a nsILoadContext object with a given appId and isBrowser flag.
|
|
createLoadContext: function createLoadContext(aAppId, aIsBrowser) {
|
|
return {
|
|
associatedWindow: null,
|
|
topWindow : null,
|
|
appId: aAppId,
|
|
isInBrowserElement: aIsBrowser,
|
|
usePrivateBrowsing: false,
|
|
isContent: false,
|
|
|
|
isAppOfType: function(appType) {
|
|
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
|
},
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsILoadContext,
|
|
Ci.nsIInterfaceRequestor,
|
|
Ci.nsISupports]),
|
|
getInterface: function(iid) {
|
|
if (iid.equals(Ci.nsILoadContext))
|
|
return this;
|
|
throw Cr.NS_ERROR_NO_INTERFACE;
|
|
}
|
|
}
|
|
},
|
|
|
|
// Sends data downloaded from aRequestChannel to a file
|
|
// identified by aId and aFileName.
|
|
getFile: function(aRequestChannel, aId, aFileName) {
|
|
let deferred = Promise.defer();
|
|
|
|
// Staging the file in TmpD until all the checks are done.
|
|
let file = FileUtils.getFile("TmpD", ["webapps", aId, aFileName], true);
|
|
|
|
// We need an output stream to write the channel content to the out file.
|
|
let outputStream = Cc["@mozilla.org/network/file-output-stream;1"]
|
|
.createInstance(Ci.nsIFileOutputStream);
|
|
// write, create, truncate
|
|
outputStream.init(file, 0x02 | 0x08 | 0x20, parseInt("0664", 8), 0);
|
|
let bufferedOutputStream =
|
|
Cc['@mozilla.org/network/buffered-output-stream;1']
|
|
.createInstance(Ci.nsIBufferedOutputStream);
|
|
bufferedOutputStream.init(outputStream, 1024);
|
|
|
|
// Create a listener that will give data to the file output stream.
|
|
let listener = Cc["@mozilla.org/network/simple-stream-listener;1"]
|
|
.createInstance(Ci.nsISimpleStreamListener);
|
|
|
|
listener.init(bufferedOutputStream, {
|
|
onStartRequest: function(aRequest, aContext) {
|
|
// Nothing to do there anymore.
|
|
},
|
|
|
|
onStopRequest: function(aRequest, aContext, aStatusCode) {
|
|
bufferedOutputStream.close();
|
|
outputStream.close();
|
|
|
|
if (!Components.isSuccessCode(aStatusCode)) {
|
|
deferred.reject({ msg: "NETWORK_ERROR", downloadAvailable: true});
|
|
return;
|
|
}
|
|
|
|
// If we get a 4XX or a 5XX http status, bail out like if we had a
|
|
// network error.
|
|
let responseStatus = aRequestChannel.responseStatus;
|
|
if (responseStatus >= 400 && responseStatus <= 599) {
|
|
// unrecoverable error, don't bug the user
|
|
deferred.reject({ msg: "NETWORK_ERROR", downloadAvailable: false});
|
|
return;
|
|
}
|
|
|
|
deferred.resolve(file);
|
|
}
|
|
});
|
|
aRequestChannel.asyncOpen(listener, null);
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
getAppByManifestURL: function getAppByManifestURL(aApps, aManifestURL) {
|
|
debug("getAppByManifestURL " + aManifestURL);
|
|
// This could be O(1) if |webapps| was a dictionary indexed on manifestURL
|
|
// which should be the unique app identifier.
|
|
// It's currently O(n).
|
|
for (let id in aApps) {
|
|
let app = aApps[id];
|
|
if (app.manifestURL == aManifestURL) {
|
|
return new mozIApplication(app);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
getManifestFor: function getManifestFor(aManifestURL) {
|
|
debug("getManifestFor(" + aManifestURL + ")");
|
|
return DOMApplicationRegistry.getManifestFor(aManifestURL);
|
|
},
|
|
|
|
getAppLocalIdByManifestURL: function getAppLocalIdByManifestURL(aApps, aManifestURL) {
|
|
debug("getAppLocalIdByManifestURL " + aManifestURL);
|
|
for (let id in aApps) {
|
|
if (aApps[id].manifestURL == aManifestURL) {
|
|
return aApps[id].localId;
|
|
}
|
|
}
|
|
|
|
return Ci.nsIScriptSecurityManager.NO_APP_ID;
|
|
},
|
|
|
|
getAppLocalIdByStoreId: function(aApps, aStoreId) {
|
|
debug("getAppLocalIdByStoreId:" + aStoreId);
|
|
for (let id in aApps) {
|
|
if (aApps[id].storeId == aStoreId) {
|
|
return aApps[id].localId;
|
|
}
|
|
}
|
|
|
|
return Ci.nsIScriptSecurityManager.NO_APP_ID;
|
|
},
|
|
|
|
getManifestCSPByLocalId: function getManifestCSPByLocalId(aApps, aLocalId) {
|
|
debug("getManifestCSPByLocalId " + aLocalId);
|
|
for (let id in aApps) {
|
|
let app = aApps[id];
|
|
if (app.localId == aLocalId) {
|
|
return ( app.csp || "" );
|
|
}
|
|
}
|
|
|
|
return "";
|
|
},
|
|
|
|
getDefaultCSPByLocalId: function(aApps, aLocalId) {
|
|
debug("getDefaultCSPByLocalId " + aLocalId);
|
|
for (let id in aApps) {
|
|
let app = aApps[id];
|
|
if (app.localId == aLocalId) {
|
|
// Use the app kind and the app status to choose the right default CSP.
|
|
try {
|
|
switch (app.appStatus) {
|
|
case Ci.nsIPrincipal.APP_STATUS_CERTIFIED:
|
|
return Services.prefs.getCharPref("security.apps.certified.CSP.default");
|
|
break;
|
|
case Ci.nsIPrincipal.APP_STATUS_PRIVILEGED:
|
|
return Services.prefs.getCharPref("security.apps.privileged.CSP.default");
|
|
break;
|
|
case Ci.nsIPrincipal.APP_STATUS_INSTALLED:
|
|
return app.kind == "hosted-trusted"
|
|
? Services.prefs.getCharPref("security.apps.trusted.CSP.default")
|
|
: "";
|
|
break;
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
}
|
|
|
|
return "default-src 'self'; object-src 'none'";
|
|
},
|
|
|
|
getAppByLocalId: function getAppByLocalId(aApps, aLocalId) {
|
|
debug("getAppByLocalId " + aLocalId);
|
|
for (let id in aApps) {
|
|
let app = aApps[id];
|
|
if (app.localId == aLocalId) {
|
|
return new mozIApplication(app);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
getManifestURLByLocalId: function getManifestURLByLocalId(aApps, aLocalId) {
|
|
debug("getManifestURLByLocalId " + aLocalId);
|
|
for (let id in aApps) {
|
|
let app = aApps[id];
|
|
if (app.localId == aLocalId) {
|
|
return app.manifestURL;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
},
|
|
|
|
getCoreAppsBasePath: function getCoreAppsBasePath() {
|
|
debug("getCoreAppsBasePath()");
|
|
try {
|
|
return FileUtils.getDir("coreAppsDir", ["webapps"], false).path;
|
|
} catch(e) {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
getAppInfo: function getAppInfo(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;
|
|
|
|
#ifdef MOZ_WIDGET_GONK
|
|
isCoreApp = app.basePath == this.getCoreAppsBasePath();
|
|
#endif
|
|
debug(app.basePath + " isCoreApp: " + isCoreApp);
|
|
|
|
// Before bug 910473, this is a temporary workaround to get correct path
|
|
// from child process in mochitest.
|
|
let prefName = "dom.mozApps.auto_confirm_install";
|
|
if (Services.prefs.prefHasUserValue(prefName) &&
|
|
Services.prefs.getBoolPref(prefName)) {
|
|
return { "path": app.basePath + "/" + app.id,
|
|
"isCoreApp": isCoreApp };
|
|
}
|
|
|
|
return { "path": WebappOSUtils.getPackagePath(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) {
|
|
let sanitizer = Cc["@mozilla.org/parserutils;1"]
|
|
.getService(Ci.nsIParserUtils);
|
|
if (!sanitizer) {
|
|
return;
|
|
}
|
|
|
|
function sanitize(aStr) {
|
|
return sanitizer.convertToPlainText(aStr,
|
|
Ci.nsIDocumentEncoder.OutputRaw, 0);
|
|
}
|
|
|
|
function sanitizeEntryPoint(aRoot) {
|
|
aRoot.name = sanitize(aRoot.name);
|
|
|
|
if (aRoot.description) {
|
|
aRoot.description = sanitize(aRoot.description);
|
|
}
|
|
|
|
if (aRoot.developer && aRoot.developer.name) {
|
|
aRoot.developer.name = sanitize(aRoot.developer.name);
|
|
}
|
|
|
|
if (aRoot.permissions) {
|
|
for (let permission in aRoot.permissions) {
|
|
if (aRoot.permissions[permission].description) {
|
|
aRoot.permissions[permission].description =
|
|
sanitize(aRoot.permissions[permission].description);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// First process the main section, then the entry points.
|
|
sanitizeEntryPoint(aManifest);
|
|
|
|
if (aManifest.entry_points) {
|
|
for (let entry in aManifest.entry_points) {
|
|
sanitizeEntryPoint(aManifest.entry_points[entry]);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* From https://developer.mozilla.org/en/OpenWebApps/The_Manifest
|
|
* Only the name property is mandatory.
|
|
*/
|
|
checkManifest: function(aManifest, app) {
|
|
if (aManifest.name == undefined)
|
|
return false;
|
|
|
|
this.sanitizeManifest(aManifest);
|
|
|
|
// launch_path, entry_points launch paths, message hrefs, and activity hrefs can't be absolute
|
|
if (aManifest.launch_path && isAbsoluteURI(aManifest.launch_path))
|
|
return false;
|
|
|
|
function checkAbsoluteEntryPoints(entryPoints) {
|
|
for (let name in entryPoints) {
|
|
if (entryPoints[name].launch_path && isAbsoluteURI(entryPoints[name].launch_path)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (checkAbsoluteEntryPoints(aManifest.entry_points))
|
|
return false;
|
|
|
|
for (let localeName in aManifest.locales) {
|
|
if (checkAbsoluteEntryPoints(aManifest.locales[localeName].entry_points)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (aManifest.activities) {
|
|
for (let activityName in aManifest.activities) {
|
|
let activity = aManifest.activities[activityName];
|
|
if (activity.href && isAbsoluteURI(activity.href)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// |messages| is an array of items, where each item is either a string or
|
|
// a {name: href} object.
|
|
let messages = aManifest.messages;
|
|
if (messages) {
|
|
if (!Array.isArray(messages)) {
|
|
return false;
|
|
}
|
|
for (let item of aManifest.messages) {
|
|
if (typeof item == "object") {
|
|
let keys = Object.keys(item);
|
|
if (keys.length != 1) {
|
|
return false;
|
|
}
|
|
if (isAbsoluteURI(item[keys[0]])) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The 'size' field must be a positive integer.
|
|
if (aManifest.size) {
|
|
aManifest.size = parseInt(aManifest.size);
|
|
if (Number.isNaN(aManifest.size) || aManifest.size < 0) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// The 'role' field must be a string.
|
|
if (aManifest.role && (typeof aManifest.role !== "string")) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
checkManifestContentType: function
|
|
checkManifestContentType(aInstallOrigin, aWebappOrigin, aContentType) {
|
|
let hadCharset = { };
|
|
let charset = { };
|
|
let netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil);
|
|
let contentType = netutil.parseContentType(aContentType, charset, hadCharset);
|
|
if (aInstallOrigin != aWebappOrigin &&
|
|
!(contentType == "application/x-web-app-manifest+json" ||
|
|
contentType == "application/manifest+json")) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
allowUnsignedAddons: false, // for testing purposes.
|
|
|
|
/**
|
|
* Checks if the app role is allowed:
|
|
* Only certified apps can be themes.
|
|
* Only privileged or certified apps can be addons.
|
|
* @param aRole : the role assigned to this app.
|
|
* @param aStatus : the APP_STATUS_* for this app.
|
|
*/
|
|
checkAppRole: function(aRole, aStatus) {
|
|
if (aRole == "theme" && aStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
|
|
return false;
|
|
}
|
|
if (!this.allowUnsignedAddons &&
|
|
(aRole == "addon" &&
|
|
aStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED &&
|
|
aStatus !== Ci.nsIPrincipal.APP_STATUS_PRIVILEGED)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Method to apply modifications to webapp manifests file saved internally.
|
|
* For now, only ensure app can't rename itself.
|
|
*/
|
|
ensureSameAppName: function ensureSameAppName(aOldManifest, aNewManifest, aApp) {
|
|
// Ensure that app name can't be updated
|
|
aNewManifest.name = aApp.name;
|
|
|
|
// Nor through localized names
|
|
if ('locales' in aNewManifest) {
|
|
let defaultName =
|
|
new ManifestHelper(aOldManifest, aApp.origin, aApp.manifestURL).name;
|
|
for (let locale in aNewManifest.locales) {
|
|
let entry = aNewManifest.locales[locale];
|
|
if (!entry.name) {
|
|
continue;
|
|
}
|
|
// In case previous manifest didn't had a name,
|
|
// we use the default app name
|
|
let localizedName = defaultName;
|
|
if (aOldManifest && 'locales' in aOldManifest &&
|
|
locale in aOldManifest.locales) {
|
|
localizedName = aOldManifest.locales[locale].name;
|
|
}
|
|
entry.name = localizedName;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Determines whether the manifest allows installs for the given origin.
|
|
* @param object aManifest
|
|
* @param string aInstallOrigin
|
|
* @return boolean
|
|
**/
|
|
checkInstallAllowed: function checkInstallAllowed(aManifest, aInstallOrigin) {
|
|
if (!aManifest.installs_allowed_from) {
|
|
return true;
|
|
}
|
|
|
|
function cbCheckAllowedOrigin(aOrigin) {
|
|
return aOrigin == "*" || aOrigin == aInstallOrigin;
|
|
}
|
|
|
|
return aManifest.installs_allowed_from.some(cbCheckAllowedOrigin);
|
|
},
|
|
|
|
/**
|
|
* Determine the type of app (app, privileged, certified)
|
|
* that is installed by the manifest
|
|
* @param object aManifest
|
|
* @returns integer
|
|
**/
|
|
getAppManifestStatus: function getAppManifestStatus(aManifest) {
|
|
let type = aManifest.type || "web";
|
|
|
|
switch(type) {
|
|
case "web":
|
|
case "trusted":
|
|
return Ci.nsIPrincipal.APP_STATUS_INSTALLED;
|
|
case "privileged":
|
|
return Ci.nsIPrincipal.APP_STATUS_PRIVILEGED;
|
|
case "certified":
|
|
return Ci.nsIPrincipal.APP_STATUS_CERTIFIED;
|
|
default:
|
|
throw new Error("Webapps.jsm: Undetermined app manifest type");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Determines if an update or a factory reset occured.
|
|
*/
|
|
isFirstRun: function isFirstRun(aPrefBranch) {
|
|
let savedmstone = null;
|
|
try {
|
|
savedmstone = aPrefBranch.getCharPref("gecko.mstone");
|
|
} catch (e) {}
|
|
|
|
let mstone = Services.appinfo.platformVersion;
|
|
|
|
let savedBuildID = null;
|
|
try {
|
|
savedBuildID = aPrefBranch.getCharPref("gecko.buildID");
|
|
} catch (e) {}
|
|
|
|
let buildID = Services.appinfo.platformBuildID;
|
|
|
|
aPrefBranch.setCharPref("gecko.mstone", mstone);
|
|
aPrefBranch.setCharPref("gecko.buildID", buildID);
|
|
|
|
return ((mstone != savedmstone) || (buildID != savedBuildID));
|
|
},
|
|
|
|
/**
|
|
* Check if two manifests have the same set of properties and that the
|
|
* values of these properties are the same, in each locale.
|
|
* Manifests here are raw json ones.
|
|
*/
|
|
compareManifests: function compareManifests(aManifest1, aManifest2) {
|
|
// 1. check if we have the same locales in both manifests.
|
|
let locales1 = [];
|
|
let locales2 = [];
|
|
if (aManifest1.locales) {
|
|
for (let locale in aManifest1.locales) {
|
|
locales1.push(locale);
|
|
}
|
|
}
|
|
if (aManifest2.locales) {
|
|
for (let locale in aManifest2.locales) {
|
|
locales2.push(locale);
|
|
}
|
|
}
|
|
if (locales1.sort().join() !== locales2.sort().join()) {
|
|
return false;
|
|
}
|
|
|
|
// Helper function to check the app name and developer information for
|
|
// two given roots.
|
|
let checkNameAndDev = function(aRoot1, aRoot2) {
|
|
let name1 = aRoot1.name;
|
|
let name2 = aRoot2.name;
|
|
if (name1 !== name2) {
|
|
return false;
|
|
}
|
|
|
|
let dev1 = aRoot1.developer;
|
|
let dev2 = aRoot2.developer;
|
|
if ((dev1 && !dev2) || (dev2 && !dev1)) {
|
|
return false;
|
|
}
|
|
|
|
return (!dev1 && !dev2) ||
|
|
(dev1.name === dev2.name && dev1.url === dev2.url);
|
|
}
|
|
|
|
// 2. For each locale, check if the name and dev info are the same.
|
|
if (!checkNameAndDev(aManifest1, aManifest2)) {
|
|
return false;
|
|
}
|
|
|
|
for (let locale in aManifest1.locales) {
|
|
if (!checkNameAndDev(aManifest1.locales[locale],
|
|
aManifest2.locales[locale])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Nothing failed.
|
|
return true;
|
|
},
|
|
|
|
// Asynchronously loads a JSON file. aPath is a string representing the path
|
|
// of the file to be read.
|
|
loadJSONAsync: function(aPath) {
|
|
let deferred = Promise.defer();
|
|
|
|
try {
|
|
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
|
|
file.initWithPath(aPath);
|
|
|
|
let channel = NetUtil.newChannel(file);
|
|
channel.contentType = "application/json";
|
|
|
|
NetUtil.asyncFetch(channel, function(aStream, aResult) {
|
|
if (!Components.isSuccessCode(aResult)) {
|
|
deferred.resolve(null);
|
|
|
|
if (aResult == Cr.NS_ERROR_FILE_NOT_FOUND) {
|
|
// We expect this under certain circumstances, like for webapps.json
|
|
// on firstrun, so we return early without reporting an error.
|
|
return;
|
|
}
|
|
|
|
Cu.reportError("AppsUtils: Could not read from json file " + aPath);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 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";
|
|
|
|
// Read json file into a string
|
|
let data = JSON.parse(converter.ConvertToUnicode(NetUtil.readInputStreamToString(aStream,
|
|
aStream.available()) || ""));
|
|
aStream.close();
|
|
|
|
deferred.resolve(data);
|
|
} catch (ex) {
|
|
Cu.reportError("AppsUtils: Could not parse JSON: " +
|
|
aPath + " " + ex + "\n" + ex.stack);
|
|
deferred.resolve(null);
|
|
}
|
|
});
|
|
} catch (ex) {
|
|
Cu.reportError("AppsUtils: Could not read from " +
|
|
aPath + " : " + ex + "\n" + ex.stack);
|
|
deferred.resolve(null);
|
|
}
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
// 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("");
|
|
},
|
|
|
|
// Returns the hash for a JS object.
|
|
computeObjectHash: function(aObject) {
|
|
return this.computeHash(JSON.stringify(aObject));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper object to access manifest information with locale support
|
|
*/
|
|
this.ManifestHelper = function(aManifest, aOrigin, aManifestURL) {
|
|
// If the app is packaged, we resolve uris against the origin.
|
|
// If it's not, against the manifest url.
|
|
|
|
if (!aOrigin || !aManifestURL) {
|
|
throw Error("ManifestHelper needs both origin and manifestURL");
|
|
}
|
|
|
|
this._baseURI = Services.io.newURI(
|
|
aOrigin.startsWith("app://") ? aOrigin : aManifestURL, null, null);
|
|
|
|
// We keep the manifest url in all cases since we need it to
|
|
// resolve the package path for packaged apps.
|
|
this._manifestURL = Services.io.newURI(aManifestURL, null, null);
|
|
|
|
this._manifest = aManifest;
|
|
let chrome = Cc["@mozilla.org/chrome/chrome-registry;1"]
|
|
.getService(Ci.nsIXULChromeRegistry)
|
|
.QueryInterface(Ci.nsIToolkitChromeRegistry);
|
|
let locale = chrome.getSelectedLocale("global").toLowerCase();
|
|
this._localeRoot = this._manifest;
|
|
|
|
if (this._manifest.locales && this._manifest.locales[locale]) {
|
|
this._localeRoot = this._manifest.locales[locale];
|
|
}
|
|
else if (this._manifest.locales) {
|
|
// try with the language part of the locale ("en" for en-GB) only
|
|
let lang = locale.split('-')[0];
|
|
if (lang != locale && this._manifest.locales[lang])
|
|
this._localeRoot = this._manifest.locales[lang];
|
|
}
|
|
};
|
|
|
|
ManifestHelper.prototype = {
|
|
_localeProp: function(aProp) {
|
|
if (this._localeRoot[aProp] != undefined)
|
|
return this._localeRoot[aProp];
|
|
return this._manifest[aProp];
|
|
},
|
|
|
|
get name() {
|
|
return this._localeProp("name");
|
|
},
|
|
|
|
get description() {
|
|
return this._localeProp("description");
|
|
},
|
|
|
|
get type() {
|
|
return this._localeProp("type");
|
|
},
|
|
|
|
get version() {
|
|
return this._localeProp("version");
|
|
},
|
|
|
|
get launch_path() {
|
|
return this._localeProp("launch_path");
|
|
},
|
|
|
|
get developer() {
|
|
// Default to {} in order to avoid exception in code
|
|
// that doesn't check for null `developer`
|
|
return this._localeProp("developer") || {};
|
|
},
|
|
|
|
get icons() {
|
|
return this._localeProp("icons");
|
|
},
|
|
|
|
get appcache_path() {
|
|
return this._localeProp("appcache_path");
|
|
},
|
|
|
|
get orientation() {
|
|
return this._localeProp("orientation");
|
|
},
|
|
|
|
get package_path() {
|
|
return this._localeProp("package_path");
|
|
},
|
|
|
|
get widgetPages() {
|
|
return this._localeProp("widgetPages");
|
|
},
|
|
|
|
get size() {
|
|
return this._manifest["size"] || 0;
|
|
},
|
|
|
|
get permissions() {
|
|
if (this._manifest.permissions) {
|
|
return this._manifest.permissions;
|
|
}
|
|
return {};
|
|
},
|
|
|
|
get biggestIconURL() {
|
|
let icons = this._localeProp("icons");
|
|
if (!icons) {
|
|
return null;
|
|
}
|
|
|
|
let iconSizes = Object.keys(icons);
|
|
if (iconSizes.length == 0) {
|
|
return null;
|
|
}
|
|
|
|
iconSizes.sort((a, b) => a - b);
|
|
let biggestIconSize = iconSizes.pop();
|
|
let biggestIcon = icons[biggestIconSize];
|
|
let biggestIconURL = this._baseURI.resolve(biggestIcon);
|
|
|
|
return biggestIconURL;
|
|
},
|
|
|
|
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);
|
|
if (Math.abs(iSize - aSize) < dist) {
|
|
icon = this._baseURI.resolve(icons[size]);
|
|
dist = Math.abs(iSize - aSize);
|
|
}
|
|
}
|
|
return icon;
|
|
},
|
|
|
|
fullLaunchPath: function(aStartPoint) {
|
|
// If no start point is specified, we use the root launch path.
|
|
// In all error cases, we just return null.
|
|
if ((aStartPoint || "") === "") {
|
|
return this._baseURI.resolve(this._localeProp("launch_path") || "/");
|
|
}
|
|
|
|
// Search for the l10n entry_points property.
|
|
let entryPoints = this._localeProp("entry_points");
|
|
if (!entryPoints) {
|
|
return null;
|
|
}
|
|
|
|
if (entryPoints[aStartPoint]) {
|
|
return this._baseURI.resolve(entryPoints[aStartPoint].launch_path || "/");
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
resolveURL: function(aURI) {
|
|
// This should be enforced higher up, but check it here just in case.
|
|
if (isAbsoluteURI(aURI)) {
|
|
throw new Error("Webapps.jsm: non-relative URI passed to resolve");
|
|
}
|
|
return this._baseURI.resolve(aURI);
|
|
},
|
|
|
|
fullAppcachePath: function() {
|
|
let appcachePath = this._localeProp("appcache_path");
|
|
return this._baseURI.resolve(appcachePath ? appcachePath : "/");
|
|
},
|
|
|
|
fullPackagePath: function() {
|
|
let packagePath = this._localeProp("package_path");
|
|
return this._manifestURL.resolve(packagePath ? packagePath : "/");
|
|
},
|
|
|
|
get role() {
|
|
return this._manifest.role || "";
|
|
},
|
|
|
|
get csp() {
|
|
return this._manifest.csp || "";
|
|
}
|
|
}
|