/* 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"); 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); }, hasWidgetPage: function(aPageURL) { let uri = Services.io.newURI(aPageURL, null, null); let filepath = AppsUtils.getFilePath(uri.path); let eliminatedUri = Services.io.newURI(uri.prePath + filepath, null, null); let equalCriterion = aUrl => Services.io.newURI(aUrl, null, null) .equals(eliminatedUri); return this.widgetPages.find(equalCriterion) !== undefined; }, 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.redirects = aApp.redirects; aObj.widgetPages = aApp.widgetPages || []; 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; #ifdef MOZ_B2GDROID aObj.android_packagename = aApp.android_packagename; aObj.android_classname = aApp.android_classname; #endif } 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, originAttributes: { appId: aAppId, inBrowser: 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; }, // 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; }, 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 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 ""; 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.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"); } }, /** * 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); if ((mstone != savedmstone) || (buildID != savedBuildID)) { aPrefBranch.setBoolPref("dom.apps.reset-permissions", false); return true; } else { return false; } }, /** * 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 [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)); }, 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 this._manifest[aProp]; }, 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 widgetPages() { return this._localeProp("widgetPages"); }, 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 || ""; } }