summaryrefslogtreecommitdiffstats
path: root/dom/apps/AppsUtils.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'dom/apps/AppsUtils.jsm')
-rw-r--r--dom/apps/AppsUtils.jsm904
1 files changed, 904 insertions, 0 deletions
diff --git a/dom/apps/AppsUtils.jsm b/dom/apps/AppsUtils.jsm
new file mode 100644
index 000000000..3ca3f8552
--- /dev/null
+++ b/dom/apps/AppsUtils.jsm
@@ -0,0 +1,904 @@
+/* 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, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "appsService",
+ "@mozilla.org/AppsService;1",
+ "nsIAppsService");
+
+// Shared code for AppsServiceChild.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) {
+ // 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 perm = Services.perms.testExactPermissionFromPrincipal(this.principal,
+ aPermission);
+ return (perm === Ci.nsIPermissionManager.ALLOW_ACTION);
+ },
+
+ get principal() {
+ if (this._principal) {
+ return this._principal;
+ }
+
+ this._principal = null;
+
+ try {
+ this._principal = Services.scriptSecurityManager.createCodebasePrincipal(
+ Services.io.newURI(this.origin, null, null),
+ {appId: this.localId});
+ } catch(e) {
+ dump("Could not create app principal " + e + "\n");
+ }
+
+ return this._principal;
+ },
+
+ 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.kind = aApp.kind;
+ aObj.enabled = aApp.enabled !== undefined ? aApp.enabled : true;
+ aObj.sideloaded = aApp.sideloaded;
+ aObj.extensionVersion = aApp.extensionVersion;
+ aObj.blockedStatus =
+ aApp.blockedStatus !== undefined ? aApp.blockedStatus
+ : Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ aObj.blocklistId = aApp.blocklistId;
+}
+
+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 inIsolatedMozBrowser
+ // flag.
+ createLoadContext: function createLoadContext(aAppId, aInIsolatedMozBrowser) {
+ return {
+ associatedWindow: null,
+ topWindow : null,
+ appId: aAppId,
+ isInIsolatedMozBrowserElement: aInIsolatedMozBrowser,
+ originAttributes: {
+ appId: aAppId,
+ inIsolatedMozBrowser: aInIsolatedMozBrowser
+ },
+ usePrivateBrowsing: false,
+ isContent: false,
+
+ 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.asyncOpen2(listener);
+
+ return deferred.promise;
+ },
+
+ // Eliminate query and hash string.
+ getFilePath: function(aPagePath) {
+ let urlParser = Cc["@mozilla.org/network/url-parser;1?auth=no"]
+ .getService(Ci.nsIURLParser);
+ let uriData = [aPagePath, aPagePath.length, {}, {}, {}, {}, {}, {}];
+ urlParser.parsePath.apply(urlParser, uriData);
+ let [{value: pathPos}, {value: pathLen}] = uriData.slice(2, 4);
+ return aPagePath.substr(pathPos, pathLen);
+ },
+
+ 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;
+ },
+
+ 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 "";
+ },
+
+ areAnyAppsInstalled: function(aApps) {
+ return Object.getOwnPropertyNames(aApps).length > 0;
+ },
+
+ 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);
+
+ return { "path": app.basePath + "/" + app.id,
+ "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.parseResponseContentType(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) {
+ try {
+ // Anything is possible in developer mode.
+ if (Services.prefs.getBoolPref("dom.apps.developer_mode")) {
+ return true;
+ }
+ } catch(e) {}
+
+ 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;
+
+ let defaultShortName =
+ new ManifestHelper(aOldManifest, aApp.origin, aApp.manifestURL).short_name;
+ aNewManifest.short_name = defaultShortName;
+
+ // Nor through localized names
+ if ("locales" in aNewManifest) {
+ for (let locale in aNewManifest.locales) {
+ let newLocaleEntry = aNewManifest.locales[locale];
+
+ let oldLocaleEntry = aOldManifest && "locales" in aOldManifest &&
+ locale in aOldManifest.locales && aOldManifest.locales[locale];
+
+ if (newLocaleEntry.name) {
+ // In case previous manifest didn't had a name,
+ // we use the default app name
+ newLocaleEntry.name =
+ (oldLocaleEntry && oldLocaleEntry.name) || aApp.name;
+ }
+ if (newLocaleEntry.short_name) {
+ newLocaleEntry.short_name =
+ (oldLocaleEntry && oldLocaleEntry.short_name) || defaultShortName;
+ }
+ }
+ }
+ },
+
+ /**
+ * 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":
+ 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");
+ }
+ },
+
+ /**
+ * 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({
+ uri: NetUtil.newURI(file),
+ loadUsingSystemPrincipal: true});
+
+ 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 hash of a string, with MD5 as a default hashing function.
+ computeHash: function(aString, aAlgorithm = "MD5") {
+ 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.initWithString(aAlgorithm);
+ 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 Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join("");
+ },
+
+ // Returns the hash for a JS object.
+ computeObjectHash: function(aObject) {
+ return this.computeHash(JSON.stringify(aObject));
+ },
+
+ getAppManifestURLFromWindow: function(aWindow) {
+ let appId = aWindow.document.nodePrincipal.appId;
+ if (appId === Ci.nsIScriptSecurityManager.NO_APP_ID) {
+ return null;
+ }
+
+ return appsService.getManifestURLByLocalId(appId);
+ },
+}
+
+/**
+ * Helper object to access manifest information with locale support
+ */
+this.ManifestHelper = function(aManifest, aOrigin, aManifestURL, aLang) {
+ // 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 locale = aLang;
+ if (!locale) {
+ let chrome = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIXULChromeRegistry)
+ .QueryInterface(Ci.nsIToolkitChromeRegistry);
+ 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 (aProp in this._manifest) ? this._manifest[aProp] : undefined;
+ },
+
+ get name() {
+ return this._localeProp("name");
+ },
+
+ get short_name() {
+ return this._localeProp("short_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 size() {
+ return this._manifest["size"] || 0;
+ },
+
+ get permissions() {
+ if (this._manifest.permissions) {
+ return this._manifest.permissions;
+ }
+ return {};
+ },
+
+ biggestIconURL: function(predicate) {
+ let icons = this._localeProp("icons");
+ if (!icons) {
+ return null;
+ }
+
+ let iconSizes = Object.keys(icons).sort((a, b) => a - b)
+ .filter(predicate || (() => true));
+ if (iconSizes.length == 0) {
+ return null;
+ }
+ 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 || "") === "") {
+ // W3C start_url takes precedence over mozApps launch_path
+ if (this._localeProp("start_url")) {
+ return this._baseURI.resolve(this._localeProp("start_url") || "/");
+ }
+ 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 || "";
+ }
+}