summaryrefslogtreecommitdiffstats
path: root/dom/apps
diff options
context:
space:
mode:
Diffstat (limited to 'dom/apps')
-rw-r--r--dom/apps/AppsService.js114
-rw-r--r--dom/apps/AppsService.manifest2
-rw-r--r--dom/apps/AppsServiceChild.jsm408
-rw-r--r--dom/apps/AppsUtils.jsm904
-rw-r--r--dom/apps/PermissionsInstaller.jsm210
-rw-r--r--dom/apps/PermissionsTable.jsm593
-rw-r--r--dom/apps/moz.build22
-rwxr-xr-xdom/apps/tests/create_test_receipts.py163
-rw-r--r--dom/apps/tests/head.js118
-rw-r--r--dom/apps/tests/unit/test_manifestHelper.js23
-rw-r--r--dom/apps/tests/unit/test_manifestSanitizer.js63
-rw-r--r--dom/apps/tests/unit/test_moziapplication.js64
-rw-r--r--dom/apps/tests/unit/xpcshell.ini5
13 files changed, 2689 insertions, 0 deletions
diff --git a/dom/apps/AppsService.js b/dom/apps/AppsService.js
new file mode 100644
index 000000000..e0a7ea041
--- /dev/null
+++ b/dom/apps/AppsService.js
@@ -0,0 +1,114 @@
+/* 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"
+
+function debug(s) {
+ //dump("-*- AppsService.js: " + s + "\n");
+}
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+
+const APPS_SERVICE_CID = Components.ID("{05072afa-92fe-45bf-ae22-39b69c117058}");
+
+function AppsService()
+{
+ debug("AppsService Constructor");
+ this.inParent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+ debug("inParent: " + this.inParent);
+ if (!this.inParent) {
+ Cu.import("resource://gre/modules/AppsServiceChild.jsm");
+ }
+}
+
+AppsService.prototype = {
+
+ isInvalidId: function(localId) {
+ return (localId == Ci.nsIScriptSecurityManager.NO_APP_ID ||
+ localId == Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID);
+ },
+
+ getAppByManifestURL: function getAppByManifestURL(aManifestURL) {
+ debug("GetAppByManifestURL( " + aManifestURL + " )");
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ getManifestFor: function getManifestFor(aManifestURL) {
+ debug("getManifestFor(" + aManifestURL + ")");
+ if (this.inParent) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ } else {
+ return Promise.reject(
+ new Error("Calling getManifestFor() from child is not supported"));
+ }
+ },
+
+ getAppLocalIdByManifestURL: function getAppLocalIdByManifestURL(aManifestURL) {
+ debug("getAppLocalIdByManifestURL( " + aManifestURL + " )");
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ getAppLocalIdByStoreId: function getAppLocalIdByStoreId(aStoreId) {
+ debug("getAppLocalIdByStoreId( " + aStoreId + " )");
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ getAppByLocalId: function getAppByLocalId(aLocalId) {
+ debug("getAppByLocalId( " + aLocalId + " )");
+ if (this.isInvalidId(aLocalId)) {
+ return null;
+ }
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ getManifestURLByLocalId: function getManifestURLByLocalId(aLocalId) {
+ debug("getManifestURLByLocalId( " + aLocalId + " )");
+ if (this.isInvalidId(aLocalId)) {
+ return null;
+ }
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ getCoreAppsBasePath: function getCoreAppsBasePath() {
+ debug("getCoreAppsBasePath()");
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ getWebAppsBasePath: function getWebAppsBasePath() {
+ debug("getWebAppsBasePath()");
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ areAnyAppsInstalled: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ getAppInfo: function getAppInfo(aAppId) {
+ debug("getAppInfo()");
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ getScopeByLocalId: function(aLocalId) {
+ debug("getScopeByLocalId( " + aLocalId + " )");
+ if (this.isInvalidId(aLocalId)) {
+ return null;
+ }
+ // TODO : implement properly!
+ // We just return null for now to not break PushService.jsm
+ return null;
+ },
+
+ classID : APPS_SERVICE_CID,
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIAppsService])
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AppsService])
diff --git a/dom/apps/AppsService.manifest b/dom/apps/AppsService.manifest
new file mode 100644
index 000000000..2ed92d4e8
--- /dev/null
+++ b/dom/apps/AppsService.manifest
@@ -0,0 +1,2 @@
+component {05072afa-92fe-45bf-ae22-39b69c117058} AppsService.js
+contract @mozilla.org/AppsService;1 {05072afa-92fe-45bf-ae22-39b69c117058}
diff --git a/dom/apps/AppsServiceChild.jsm b/dom/apps/AppsServiceChild.jsm
new file mode 100644
index 000000000..4aca938af
--- /dev/null
+++ b/dom/apps/AppsServiceChild.jsm
@@ -0,0 +1,408 @@
+/* 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;
+
+// This module exposes a subset of the functionalities of the parent DOM
+// Registry to content processes, to be used from the AppsService component.
+
+this.EXPORTED_SYMBOLS = ["DOMApplicationRegistry", "WrappedManifestCache"];
+
+Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function debug(s) {
+ //dump("-*- AppsServiceChild.jsm: " + s + "\n");
+}
+
+const APPS_IPC_MSG_NAMES = [
+ "Webapps:AddApp",
+ "Webapps:RemoveApp",
+ "Webapps:UpdateApp",
+ "Webapps:CheckForUpdate:Return:KO",
+ "Webapps:FireEvent",
+ "Webapps:UpdateState"
+];
+
+// A simple cache for the wrapped manifests.
+this.WrappedManifestCache = {
+ _cache: { },
+
+ // Gets an entry from the cache, and populates the cache if needed.
+ get: function mcache_get(aManifestURL, aManifest, aWindow, aInnerWindowID) {
+ if (!aManifest) {
+ return;
+ }
+
+ if (!(aManifestURL in this._cache)) {
+ this._cache[aManifestURL] = { };
+ }
+
+ let winObjs = this._cache[aManifestURL];
+ if (!(aInnerWindowID in winObjs)) {
+ winObjs[aInnerWindowID] = Cu.cloneInto(aManifest, aWindow);
+ }
+
+ return winObjs[aInnerWindowID];
+ },
+
+ // Invalidates an entry in the cache.
+ evict: function mcache_evict(aManifestURL, aInnerWindowID) {
+ debug("Evicting manifest " + aManifestURL + " window ID " +
+ aInnerWindowID);
+ if (aManifestURL in this._cache) {
+ let winObjs = this._cache[aManifestURL];
+ if (aInnerWindowID in winObjs) {
+ delete winObjs[aInnerWindowID];
+ }
+
+ if (Object.keys(winObjs).length == 0) {
+ delete this._cache[aManifestURL];
+ }
+ }
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ // Clear the cache on memory pressure.
+ this._cache = { };
+ Cu.forceGC();
+ },
+
+ init: function() {
+ Services.obs.addObserver(this, "memory-pressure", false);
+ }
+};
+
+this.WrappedManifestCache.init();
+
+
+// DOMApplicationRegistry keeps a cache containing a list of apps in the device.
+// This information is updated with the data received from the main process and
+// it is queried by the DOM objects to set their state.
+// This module handle all the messages broadcasted from the parent process,
+// including DOM events, which are dispatched to the corresponding DOM objects.
+
+this.DOMApplicationRegistry = {
+ // DOMApps will hold a list of arrays of weak references to
+ // mozIDOMApplication objects indexed by manifest URL.
+ DOMApps: {},
+
+ ready: false,
+ webapps: null,
+
+ init: function init() {
+ this.cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+
+ APPS_IPC_MSG_NAMES.forEach((function(aMsgName) {
+ this.cpmm.addMessageListener(aMsgName, this);
+ }).bind(this));
+
+ this.webapps = { };
+ // We need a fast mapping from localId -> app, so we add an index.
+ // We also add the manifest to the app object.
+ this.localIdIndex = { };
+ for (let id in this.webapps) {
+ let app = this.webapps[id];
+ this.localIdIndex[app.localId] = app;
+ app.manifest = list.manifests[id];
+ }
+
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ // cpmm.addMessageListener causes the DOMApplicationRegistry object to
+ // live forever if we don't clean up properly.
+ this.webapps = null;
+ this.DOMApps = null;
+
+ APPS_IPC_MSG_NAMES.forEach((aMsgName) => {
+ this.cpmm.removeMessageListener(aMsgName, this);
+ });
+ },
+
+ receiveMessage: function receiveMessage(aMessage) {
+ debug("Received " + aMessage.name + " message.");
+ let msg = aMessage.data;
+ switch (aMessage.name) {
+ case "Webapps:AddApp":
+ this.webapps[msg.id] = msg.app;
+ this.localIdIndex[msg.app.localId] = msg.app;
+ if (msg.manifest) {
+ this.webapps[msg.id].manifest = msg.manifest;
+ }
+ break;
+ case "Webapps:RemoveApp":
+ delete this.DOMApps[this.webapps[msg.id].manifestURL];
+ delete this.localIdIndex[this.webapps[msg.id].localId];
+ delete this.webapps[msg.id];
+ break;
+ case "Webapps:UpdateApp":
+ let app = this.webapps[msg.oldId];
+ if (!app) {
+ return;
+ }
+
+ if (msg.app) {
+ for (let prop in msg.app) {
+ app[prop] = msg.app[prop];
+ }
+ }
+
+ this.webapps[msg.newId] = app;
+ this.localIdIndex[app.localId] = app;
+ delete this.webapps[msg.oldId];
+
+ let apps = this.DOMApps[msg.app.manifestURL];
+ if (!apps) {
+ return;
+ }
+ for (let i = 0; i < apps.length; i++) {
+ let domApp = apps[i].get();
+ if (!domApp || domApp._window === null) {
+ apps.splice(i, 1);
+ continue;
+ }
+ domApp._proxy = new Proxy(domApp, {
+ get: function(target, prop) {
+ if (!DOMApplicationRegistry.webapps[msg.newId]) {
+ return;
+ }
+ return DOMApplicationRegistry.webapps[msg.newId][prop];
+ },
+ set: function(target, prop, val) {
+ if (!DOMApplicationRegistry.webapps[msg.newId]) {
+ return;
+ }
+ DOMApplicationRegistry.webapps[msg.newId][prop] = val;
+ return;
+ },
+ });
+ }
+ break;
+ case "Webapps:FireEvent":
+ this._fireEvent(aMessage);
+ break;
+ case "Webapps:UpdateState":
+ this._updateState(msg);
+ break;
+ case "Webapps:CheckForUpdate:Return:KO":
+ let DOMApps = this.DOMApps[msg.manifestURL];
+ if (!DOMApps || !msg.requestID) {
+ return;
+ }
+ DOMApps.forEach((DOMApp) => {
+ let domApp = DOMApp.get();
+ if (domApp && msg.requestID) {
+ domApp._fireRequestResult(aMessage, true /* aIsError */);
+ }
+ });
+ break;
+ }
+ },
+
+ /**
+ * mozIDOMApplication management
+ */
+
+ // Every time a DOM app is created, we save a weak reference to it that will
+ // be used to dispatch events and fire request results.
+ addDOMApp: function(aApp, aManifestURL, aId) {
+ let weakRef = Cu.getWeakReference(aApp);
+
+ if (!this.DOMApps[aManifestURL]) {
+ this.DOMApps[aManifestURL] = [];
+ }
+
+ let apps = this.DOMApps[aManifestURL];
+
+ // Get rid of dead weak references.
+ for (let i = 0; i < apps.length; i++) {
+ let app = apps[i].get();
+ if (!app || app._window === null) {
+ apps.splice(i, 1);
+ }
+ }
+
+ apps.push(weakRef);
+
+ // Each DOM app contains a proxy object used to build their state. We
+ // return the handler for this proxy object with traps to get and set
+ // app properties kept in the DOMApplicationRegistry app cache.
+ return {
+ get: function(target, prop) {
+ if (!DOMApplicationRegistry.webapps[aId]) {
+ return;
+ }
+
+ if (prop in DOMApplicationRegistry.webapps[aId]) {
+ return DOMApplicationRegistry.webapps[aId][prop];
+ }
+ return null;
+ },
+ set: function(target, prop, val) {
+ if (!DOMApplicationRegistry.webapps[aId]) {
+ return;
+ }
+ DOMApplicationRegistry.webapps[aId][prop] = val;
+ return;
+ },
+ };
+ },
+
+ _fireEvent: function(aMessage) {
+ let msg = aMessage.data;
+ debug("_fireEvent " + JSON.stringify(msg));
+ if (!this.DOMApps || !msg.manifestURL || !msg.eventType) {
+ return;
+ }
+
+ let DOMApps = this.DOMApps[msg.manifestURL];
+ if (!DOMApps) {
+ return;
+ }
+
+ // The parent might ask childs to trigger more than one event in one
+ // shot, so in order to avoid needless IPC we allow an array for the
+ // 'eventType' IPC message field.
+ if (!Array.isArray(msg.eventType)) {
+ msg.eventType = [msg.eventType];
+ }
+
+ DOMApps.forEach((DOMApp) => {
+ let domApp = DOMApp.get();
+ if (!domApp) {
+ return;
+ }
+ msg.eventType.forEach((aEventType) => {
+ if ('on' + aEventType in domApp) {
+ domApp._fireEvent(aEventType);
+ }
+ });
+
+ if (msg.requestID) {
+ aMessage.data.result = msg.manifestURL;
+ domApp._fireRequestResult(aMessage);
+ }
+ });
+ },
+
+ _updateState: function(aMessage) {
+ if (!this.DOMApps || !aMessage.id) {
+ return;
+ }
+
+ let app = this.webapps[aMessage.id];
+ if (!app) {
+ return;
+ }
+
+ if (aMessage.app) {
+ for (let prop in aMessage.app) {
+ app[prop] = aMessage.app[prop];
+ }
+ }
+
+ if ("error" in aMessage) {
+ app.downloadError = aMessage.error;
+ }
+
+ if (aMessage.manifest) {
+ app.manifest = aMessage.manifest;
+ // Evict the wrapped manifest cache for all the affected DOM objects.
+ let DOMApps = this.DOMApps[app.manifestURL];
+ if (!DOMApps) {
+ return;
+ }
+ DOMApps.forEach((DOMApp) => {
+ let domApp = DOMApp.get();
+ if (!domApp) {
+ return;
+ }
+ WrappedManifestCache.evict(app.manifestURL, domApp.innerWindowID);
+ });
+ }
+ },
+
+ getAll: function(aCallback) {
+ debug("getAll()\n");
+ if (!aCallback || typeof aCallback !== "function") {
+ return;
+ }
+
+ let res = [];
+ for (let id in this.webapps) {
+ res.push(this.webapps[id]);
+ }
+ aCallback(res);
+ },
+
+ getAdditionalLanguages: function(aManifestURL) {
+ for (let id in this.webapps) {
+ if (this.webapps[id].manifestURL == aManifestURL) {
+ return this.webapps[id].additionalLanguages || {};
+ }
+ }
+ return {};
+ },
+
+ /**
+ * nsIAppsService API
+ */
+ getAppByManifestURL: function getAppByManifestURL(aManifestURL) {
+ debug("getAppByManifestURL " + aManifestURL);
+ return AppsUtils.getAppByManifestURL(this.webapps, aManifestURL);
+ },
+
+ getAppLocalIdByManifestURL: function getAppLocalIdByManifestURL(aManifestURL) {
+ debug("getAppLocalIdByManifestURL " + aManifestURL);
+ return AppsUtils.getAppLocalIdByManifestURL(this.webapps, aManifestURL);
+ },
+
+ getAppLocalIdByStoreId: function(aStoreId) {
+ debug("getAppLocalIdByStoreId:" + aStoreId);
+ return AppsUtils.getAppLocalIdByStoreId(this.webapps, aStoreId);
+ },
+
+ getAppByLocalId: function getAppByLocalId(aLocalId) {
+ debug("getAppByLocalId " + aLocalId + " - ready: " + this.ready);
+ let app = this.localIdIndex[aLocalId];
+ if (!app) {
+ debug("Ouch, No app!");
+ return null;
+ }
+
+ return new mozIApplication(app);
+ },
+
+ getManifestURLByLocalId: function getManifestURLByLocalId(aLocalId) {
+ debug("getManifestURLByLocalId " + aLocalId);
+ return AppsUtils.getManifestURLByLocalId(this.webapps, aLocalId);
+ },
+
+ getCoreAppsBasePath: function getCoreAppsBasePath() {
+ debug("getCoreAppsBasePath() not yet supported on child!");
+ return null;
+ },
+
+ getWebAppsBasePath: function getWebAppsBasePath() {
+ debug("getWebAppsBasePath() not yet supported on child!");
+ return null;
+ },
+
+ areAnyAppsInstalled: function() {
+ return AppsUtils.areAnyAppsInstalled(this.webapps);
+ },
+
+ getAppInfo: function getAppInfo(aAppId) {
+ return AppsUtils.getAppInfo(this.webapps, aAppId);
+ }
+}
+
+DOMApplicationRegistry.init();
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 || "";
+ }
+}
diff --git a/dom/apps/PermissionsInstaller.jsm b/dom/apps/PermissionsInstaller.jsm
new file mode 100644
index 000000000..e3ed3aca8
--- /dev/null
+++ b/dom/apps/PermissionsInstaller.jsm
@@ -0,0 +1,210 @@
+/* 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 Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/PermissionSettings.jsm");
+Cu.import("resource://gre/modules/PermissionsTable.jsm");
+
+this.EXPORTED_SYMBOLS = ["PermissionsInstaller"];
+const UNKNOWN_ACTION = Ci.nsIPermissionManager.UNKNOWN_ACTION;
+const ALLOW_ACTION = Ci.nsIPermissionManager.ALLOW_ACTION;
+const DENY_ACTION = Ci.nsIPermissionManager.DENY_ACTION;
+const PROMPT_ACTION = Ci.nsIPermissionManager.PROMPT_ACTION;
+
+// Permission access flags
+const READONLY = "readonly";
+const CREATEONLY = "createonly";
+const READCREATE = "readcreate";
+const READWRITE = "readwrite";
+
+const PERM_TO_STRING = ["unknown", "allow", "deny", "prompt"];
+
+function debug(aMsg) {
+ //dump("-*-*- PermissionsInstaller.jsm : " + aMsg + "\n");
+}
+
+this.PermissionsInstaller = {
+ /**
+ * Install permissisions or remove deprecated permissions upon re-install.
+ * @param object aApp
+ * The just-installed app configuration.
+ * The properties used are manifestURL, origin and manifest.
+ * @param boolean aIsReinstall
+ * Indicates the app was just re-installed
+ * @param function aOnError
+ * A function called if an error occurs
+ * @returns void
+ **/
+ installPermissions: function installPermissions(aApp, aIsReinstall,
+ aOnError) {
+ try {
+ let newManifest =
+ new ManifestHelper(aApp.manifest, aApp.origin, aApp.manifestURL);
+ if (!newManifest.permissions && !aIsReinstall) {
+ return;
+ }
+
+ if (aIsReinstall) {
+ // Compare the original permissions against the new permissions
+ // Remove any deprecated Permissions
+
+ if (newManifest.permissions) {
+ // Expand permission names.
+ let newPermNames = [];
+ for (let permName in newManifest.permissions) {
+ let expandedPermNames =
+ expandPermissions(permName,
+ newManifest.permissions[permName].access);
+ newPermNames = newPermNames.concat(expandedPermNames);
+ }
+
+ newPermNames.push("indexedDB");
+
+ // Add the appcache related permissions.
+ if (newManifest.appcache_path) {
+ newPermNames = newPermNames.concat(["offline-app", "pin-app"]);
+ }
+
+ for (let idx in AllPossiblePermissions) {
+ let permName = AllPossiblePermissions[idx];
+ let index = newPermNames.indexOf(permName);
+ if (index == -1) {
+ // See if the permission was installed previously.
+ let permValue =
+ PermissionSettingsModule.getPermission(permName,
+ aApp.manifestURL,
+ aApp.origin,
+ false);
+ if (permValue == "unknown" || permValue == "deny") {
+ // All 'deny' permissions should be preserved
+ continue;
+ }
+ // Remove the deprecated permission
+ PermissionSettingsModule.removePermission(permName,
+ aApp.manifestURL,
+ aApp.origin,
+ false);
+ }
+ }
+ }
+ }
+
+ // Check to see if the 'webapp' is app/privileged/certified.
+ let appStatus;
+ switch (AppsUtils.getAppManifestStatus(aApp.manifest)) {
+ case Ci.nsIPrincipal.APP_STATUS_CERTIFIED:
+ appStatus = "certified";
+ break;
+ case Ci.nsIPrincipal.APP_STATUS_PRIVILEGED:
+ appStatus = "privileged";
+ break;
+ case Ci.nsIPrincipal.APP_STATUS_INSTALLED:
+ appStatus = "app";
+ break;
+ default:
+ // Cannot determine app type, abort install by throwing an error.
+ throw new Error("PermissionsInstaller.jsm: " +
+ "Cannot determine the app's status. Install cancelled.");
+ break;
+ }
+
+ this._setPermission("indexedDB", "allow", aApp);
+
+ // Add the appcache related permissions. We allow it for all kinds of
+ // apps.
+ if (newManifest.appcache_path) {
+ this._setPermission("offline-app", "allow", aApp);
+ this._setPermission("pin-app", "allow", aApp);
+ }
+
+ for (let permName in newManifest.permissions) {
+ if (!PermissionsTable[permName]) {
+ Cu.reportError("PermissionsInstaller.jsm: '" + permName + "'" +
+ " is not a valid Webapps permission name.");
+ dump("PermissionsInstaller.jsm: '" + permName + "'" +
+ " is not a valid Webapps permission name.");
+ continue;
+ }
+
+ let expandedPermNames =
+ expandPermissions(permName,
+ newManifest.permissions[permName].access);
+ for (let idx in expandedPermNames) {
+
+ let isPromptPermission =
+ PermissionsTable[permName][appStatus] === PROMPT_ACTION;
+
+ // We silently upgrade the permission to whatever the permission
+ // is for certified apps (ALLOW or PROMPT) only if the
+ // following holds true:
+ // * The app is preinstalled
+ // * The permission that would be granted is PROMPT
+ // * The app is privileged
+ let permission =
+ aApp.isPreinstalled && isPromptPermission &&
+ appStatus === "privileged"
+ ? PermissionsTable[permName]["certified"]
+ : PermissionsTable[permName][appStatus];
+
+ let permValue = PERM_TO_STRING[permission];
+ if (isPromptPermission) {
+ // If the permission is prompt, keep the current value. This will
+ // work even on a system update, with the caveat that if a
+ // ALLOW/DENY permission is changed to PROMPT then the system should
+ // inform the user that he can now change a permission that he could
+ // not change before.
+ permValue =
+ PermissionSettingsModule.getPermission(expandedPermNames[idx],
+ aApp.manifestURL,
+ aApp.origin,
+ false,
+ aApp.isCachedPackage);
+ if (permValue === "unknown") {
+ permValue = PERM_TO_STRING[permission];
+ }
+ }
+
+ this._setPermission(expandedPermNames[idx], permValue, aApp);
+ }
+ }
+ }
+ catch (ex) {
+ dump("Caught webapps install permissions error for " + aApp.origin +
+ " : " + ex + "\n");
+ Cu.reportError(ex);
+ if (aOnError) {
+ aOnError();
+ }
+ }
+ },
+
+ /**
+ * Set a permission value.
+ * @param string aPermName
+ * The permission name.
+ * @param string aPermValue
+ * The permission value.
+ * @param object aApp
+ * The just-installed app configuration.
+ * The properties used are manifestURL, origin, appId, isCachedPackage.
+ * @returns void
+ **/
+ _setPermission: function setPermission(aPermName, aPermValue, aApp) {
+ PermissionSettingsModule.addPermission({
+ type: aPermName,
+ origin: aApp.origin,
+ manifestURL: aApp.manifestURL,
+ value: aPermValue,
+ browserFlag: false,
+ localId: aApp.localId,
+ isCachedPackage: aApp.isCachedPackage,
+ });
+ }
+};
diff --git a/dom/apps/PermissionsTable.jsm b/dom/apps/PermissionsTable.jsm
new file mode 100644
index 000000000..7c6a8490f
--- /dev/null
+++ b/dom/apps/PermissionsTable.jsm
@@ -0,0 +1,593 @@
+/* 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 Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [
+ "PermissionsTable",
+ "PermissionsReverseTable",
+ "expandPermissions",
+ "appendAccessToPermName",
+ "isExplicitInPermissionsTable",
+ "AllPossiblePermissions"
+];
+
+// Permission access flags
+const READONLY = "readonly";
+const CREATEONLY = "createonly";
+const READCREATE = "readcreate";
+const READWRITE = "readwrite";
+
+const UNKNOWN_ACTION = Ci.nsIPermissionManager.UNKNOWN_ACTION;
+const ALLOW_ACTION = Ci.nsIPermissionManager.ALLOW_ACTION;
+const DENY_ACTION = Ci.nsIPermissionManager.DENY_ACTION;
+const PROMPT_ACTION = Ci.nsIPermissionManager.PROMPT_ACTION;
+
+// Permissions Matrix: https://docs.google.com/spreadsheet/ccc?key=0Akyz_Bqjgf5pdENVekxYRjBTX0dCXzItMnRyUU1RQ0E#gid=0
+
+// Permissions that are implicit:
+// battery-status, network-information, vibration,
+// device-capabilities
+
+this.PermissionsTable = { geolocation: {
+ app: PROMPT_ACTION,
+ privileged: PROMPT_ACTION,
+ certified: PROMPT_ACTION
+ },
+ "geolocation-noprompt": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION,
+ substitute: ["geolocation"]
+ },
+ camera: {
+ app: DENY_ACTION,
+ privileged: PROMPT_ACTION,
+ certified: ALLOW_ACTION
+ },
+ alarms: {
+ app: ALLOW_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "tcp-socket": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "udp-socket": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "network-events": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ contacts: {
+ app: DENY_ACTION,
+ privileged: PROMPT_ACTION,
+ certified: ALLOW_ACTION,
+ access: ["read", "write", "create"]
+ },
+ "device-storage:apps": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION,
+ access: ["read"]
+ },
+ "device-storage:crashes": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION,
+ access: ["read"]
+ },
+ "device-storage:pictures": {
+ app: DENY_ACTION,
+ privileged: PROMPT_ACTION,
+ certified: ALLOW_ACTION,
+ access: ["read", "write", "create"]
+ },
+ "device-storage:videos": {
+ app: DENY_ACTION,
+ privileged: PROMPT_ACTION,
+ certified: ALLOW_ACTION,
+ access: ["read", "write", "create"]
+ },
+ "device-storage:music": {
+ app: DENY_ACTION,
+ privileged: PROMPT_ACTION,
+ certified: ALLOW_ACTION,
+ access: ["read", "write", "create"]
+ },
+ "device-storage:sdcard": {
+ app: DENY_ACTION,
+ privileged: PROMPT_ACTION,
+ certified: ALLOW_ACTION,
+ access: ["read", "write", "create"]
+ },
+ sms: {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "speech-recognition": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ browser: {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "browser:universalxss": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "browser:embedded-system-app": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ mobilenetwork: {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ power: {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ push: {
+ app: ALLOW_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ settings: {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION,
+ access: ["read", "write"],
+ additional: ["indexedDB-chrome-settings", "settings-api"]
+ },
+ // This exists purely for tests, no app
+ // should ever use it. It can only be
+ // handed out by SpecialPowers.
+ "settings-clear": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: DENY_ACTION,
+ additional: ["indexedDB-chrome-settings", "settings-api"]
+ },
+ permissions: {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ attention: {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "global-clickthrough-overlay": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "moz-attention": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION,
+ substitute: ["attention"]
+ },
+ "webapps-manage": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "homescreen-webapps-manage": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "backgroundservice": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "desktop-notification": {
+ app: ALLOW_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "networkstats-manage": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "wifi-manage": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "systemXHR": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "idle": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "time": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "embed-apps": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "background-sensors": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "audio-channel-normal": {
+ app: ALLOW_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "audio-channel-content": {
+ app: ALLOW_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "audio-channel-notification": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "audio-channel-alarm": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "audio-channel-system": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "audio-channel-telephony": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "moz-audio-channel-telephony": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION,
+ substitute: ["audio-channel-telephony"]
+ },
+ "audio-channel-ringer": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "moz-audio-channel-ringer": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION,
+ substitute: ["audio-channel-ringer"]
+ },
+ "audio-channel-publicnotification": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "open-remote-window": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "input": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "input-manage": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "audio-capture": {
+ app: PROMPT_ACTION,
+ privileged: PROMPT_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "audio-capture:3gpp": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "audio-capture:3gpp2": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "speaker-control": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "downloads": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "video-capture": {
+ app: PROMPT_ACTION,
+ privileged: PROMPT_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "feature-detection": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ // This permission doesn't actually grant access to
+ // anything. It exists only to check the correctness
+ // of web prompt composed permissions in tests.
+ "test-permission": {
+ app: PROMPT_ACTION,
+ privileged: PROMPT_ACTION,
+ certified: ALLOW_ACTION,
+ access: ["read", "write", "create"]
+ },
+ "firefox-accounts": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "moz-firefox-accounts": {
+ app: DENY_ACTION,
+ privileged: PROMPT_ACTION,
+ certified: ALLOW_ACTION,
+ substitute: ["firefox-accounts"]
+ },
+ "themeable": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "settings:wallpaper.image": {
+ app: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION,
+ access: ["read", "write"],
+ additional: ["settings-api"]
+ },
+ "tv": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "before-after-keyboard-event": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "presentation-device-manage": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "secureelement-manage": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "inputport": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "system-update": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "open-hidden-window": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "moz-extremely-unstable-and-will-change-webcomponents": {
+ app: DENY_ACTION,
+ trusted: DENY_ACTION,
+ privileged: ALLOW_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "system-app-only-audio-channels-in-app": {
+ app: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ },
+ "previously-certified-app": {
+ app: DENY_ACTION,
+ trusted: DENY_ACTION,
+ privileged: DENY_ACTION,
+ certified: ALLOW_ACTION
+ }
+ };
+
+/**
+ * Append access modes to the permission name as suffixes.
+ * e.g. permission name 'contacts' with ['read', 'write'] =
+ * ['contacts-read', contacts-write']
+ * @param string aPermName
+ * @param array aAccess
+ * @returns array containing access-appended permission names.
+ **/
+this.appendAccessToPermName = function appendAccessToPermName(aPermName, aAccess) {
+ if (aAccess.length == 0) {
+ return [aPermName];
+ }
+ return aAccess.map(function(aMode) {
+ return aPermName + "-" + aMode;
+ });
+};
+
+/**
+ * Expand an access string into multiple permission names,
+ * e.g: permission name 'contacts' with 'readwrite' =
+ * ['contacts-read', 'contacts-create', 'contacts-write']
+ * @param string aPermName
+ * @param string aAccess (optional)
+ * @returns array containing expanded permission names.
+ **/
+this.expandPermissions = function expandPermissions(aPermName, aAccess) {
+ if (!PermissionsTable[aPermName]) {
+ let errorMsg =
+ "PermissionsTable.jsm: expandPermissions: Unknown Permission: " + aPermName;
+ Cu.reportError(errorMsg);
+ dump(errorMsg);
+ return [];
+ }
+
+ const tableEntry = PermissionsTable[aPermName];
+
+ if (tableEntry.substitute && tableEntry.additional) {
+ let errorMsg =
+ "PermissionsTable.jsm: expandPermissions: Can't handle both 'substitute' " +
+ "and 'additional' entries for permission: " + aPermName;
+ Cu.reportError(errorMsg);
+ dump(errorMsg);
+ return [];
+ }
+
+ if (!aAccess && tableEntry.access ||
+ aAccess && !tableEntry.access) {
+ let errorMsg =
+ "PermissionsTable.jsm: expandPermissions: Invalid access for permission " +
+ aPermName + ": " + aAccess + "\n";
+ Cu.reportError(errorMsg);
+ dump(errorMsg);
+ return [];
+ }
+
+ let expandedPermNames = [];
+
+ if (tableEntry.access && aAccess) {
+ let requestedSuffixes = [];
+ switch (aAccess) {
+ case READONLY:
+ requestedSuffixes.push("read");
+ break;
+ case CREATEONLY:
+ requestedSuffixes.push("create");
+ break;
+ case READCREATE:
+ requestedSuffixes.push("read", "create");
+ break;
+ case READWRITE:
+ requestedSuffixes.push("read", "create", "write");
+ break;
+ default:
+ return [];
+ }
+
+ let permArr = appendAccessToPermName(aPermName, requestedSuffixes);
+
+ // Add the same suffix to each of the additions.
+ if (tableEntry.additional) {
+ for (let additional of tableEntry.additional) {
+ permArr = permArr.concat(appendAccessToPermName(additional, requestedSuffixes));
+ }
+ }
+
+ // Only add the suffixed version if the suffix exists in the table.
+ for (let idx in permArr) {
+ let suffix = requestedSuffixes[idx % requestedSuffixes.length];
+ if (tableEntry.access.indexOf(suffix) != -1) {
+ expandedPermNames.push(permArr[idx]);
+ }
+ }
+ } else if (tableEntry.substitute) {
+ expandedPermNames = expandedPermNames.concat(tableEntry.substitute);
+ } else {
+ expandedPermNames.push(aPermName);
+ // Include each of the additions exactly as they appear in the table.
+ if (tableEntry.additional) {
+ expandedPermNames = expandedPermNames.concat(tableEntry.additional);
+ }
+ }
+
+ return expandedPermNames;
+};
+
+this.PermissionsReverseTable = {};
+this.AllPossiblePermissions = [];
+
+(function () {
+ // PermissionsTable as it is works well for direct searches, but not
+ // so well for reverse ones (that is, if I get something like
+ // device-storage:music-read or indexedDB-chrome-settings-read how
+ // do I know which permission it really is? Hence this table is
+ // born. The idea is that
+ // reverseTable[device-storage:music-read] should return
+ // device-storage:music
+ //
+ // We also need a list of all the possible permissions for things like the
+ // settingsmanager, so construct that while we're at it.
+ for (let permName in PermissionsTable) {
+ let permAliases = [];
+ if (PermissionsTable[permName].access) {
+ permAliases = expandPermissions(permName, "readwrite");
+ } else if (!PermissionsTable[permName].substitute) {
+ permAliases = expandPermissions(permName);
+ }
+ for (let i = 0; i < permAliases.length; i++) {
+ PermissionsReverseTable[permAliases[i]] = permName;
+ AllPossiblePermissions.push(permAliases[i]);
+ }
+ }
+ AllPossiblePermissions =
+ AllPossiblePermissions.concat(["indexedDB", "offline-app", "pin-app"]);
+})();
+
+this.isExplicitInPermissionsTable = function(aPermName, aIntStatus) {
+
+ // Check to see if the 'webapp' is app/privileged/certified.
+ let appStatus;
+ switch (aIntStatus) {
+ case Ci.nsIPrincipal.APP_STATUS_CERTIFIED:
+ appStatus = "certified";
+ break;
+ case Ci.nsIPrincipal.APP_STATUS_PRIVILEGED:
+ appStatus = "privileged";
+ break;
+ default: // If it isn't certified or privileged, it's app
+ appStatus = "app";
+ break;
+ }
+
+ let realPerm = PermissionsReverseTable[aPermName];
+
+ if (realPerm) {
+ return (PermissionsTable[realPerm][appStatus] ==
+ Ci.nsIPermissionManager.PROMPT_ACTION);
+ } else {
+ return false;
+ }
+}
diff --git a/dom/apps/moz.build b/dom/apps/moz.build
new file mode 100644
index 000000000..c8dc90335
--- /dev/null
+++ b/dom/apps/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
+
+EXTRA_COMPONENTS += [
+ 'AppsService.js',
+ 'AppsService.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'AppsServiceChild.jsm',
+ 'PermissionsInstaller.jsm',
+ 'PermissionsTable.jsm',
+]
+
+EXTRA_PP_JS_MODULES += [
+ 'AppsUtils.jsm',
+]
diff --git a/dom/apps/tests/create_test_receipts.py b/dom/apps/tests/create_test_receipts.py
new file mode 100755
index 000000000..e8774edec
--- /dev/null
+++ b/dom/apps/tests/create_test_receipts.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python
+
+import jwt
+
+receipt1 = {
+ "typ": "purchase-receipt",
+ "product": {
+ "url": "https://www.mozilla.org",
+ "storedata": "5169314356"
+ },
+ "user": {
+ "type": "directed-identifier",
+ "value": "4fb35151-2b9b-4ba2-8283-c49d381640bd"
+ },
+ "iss": "http://mochi.test:8888",
+ "nbf": 131360185,
+ "iat": 131360188,
+ "detail": "http://mochi.test:8888/receipt/5169314356",
+ "verify": "http://mochi.test:8888/verify/5169314356",
+ "reissue": "http://mochi.test:8888/reissue/5169314356"
+}
+
+receipt2 = {
+ "typ": "purchase-receipt",
+ "product": {
+ "url": "https://www.mozilla.org",
+ "storedata": "5169314357"
+ },
+ "user": {
+ "type": "directed-identifier",
+ "value": "4fb35151-2b9b-4ba2-8283-c49d381640bd"
+ },
+ "iss": "http://mochi.test:8888",
+ "nbf": 131360185,
+ "iat": 131360188,
+ "detail": "http://mochi.test:8888/receipt/5169314356",
+ "verify": "http://mochi.test:8888/verify/5169314356",
+ "reissue": "http://mochi.test:8888/reissue/5169314356"
+}
+
+receipt_without_typ = {
+ "product": {
+ "url": "https://www.mozilla.org",
+ "storedata": "5169314358"
+ },
+ "user": {
+ "type": "directed-identifier",
+ "value": "4fb35151-2b9b-4ba2-8283-c49d381640bd"
+ },
+ "iss": "http://mochi.test:8888",
+ "nbf": 131360185,
+ "iat": 131360188,
+ "detail": "http://mochi.test:8888/receipt/5169314356",
+ "verify": "http://mochi.test:8888/verify/5169314356",
+ "reissue": "http://mochi.test:8888/reissue/5169314356"
+}
+
+receipt_without_product = {
+ "typ": "purchase-receipt",
+ "user": {
+ "type": "directed-identifier",
+ "value": "4fb35151-2b9b-4ba2-8283-c49d381640bd"
+ },
+ "iss": "http://mochi.test:8888",
+ "nbf": 131360185,
+ "iat": 131360188,
+ "detail": "http://mochi.test:8888/receipt/5169314356",
+ "verify": "http://mochi.test:8888/verify/5169314356",
+ "reissue": "http://mochi.test:8888/reissue/5169314356"
+}
+
+receipt_without_user = {
+ "typ": "purchase-receipt",
+ "product": {
+ "url": "https://www.mozilla.org",
+ "storedata": "5169314358"
+ },
+ "iss": "http://mochi.test:8888",
+ "nbf": 131360185,
+ "iat": 131360188,
+ "detail": "http://mochi.test:8888/receipt/5169314356",
+ "verify": "http://mochi.test:8888/verify/5169314356",
+ "reissue": "http://mochi.test:8888/reissue/5169314356"
+}
+
+receipt_without_iss = {
+ "typ": "purchase-receipt",
+ "product": {
+ "url": "https://www.mozilla.org",
+ "storedata": "5169314358"
+ },
+ "user": {
+ "type": "directed-identifier",
+ "value": "4fb35151-2b9b-4ba2-8283-c49d381640bd"
+ },
+ "nbf": 131360185,
+ "iat": 131360188,
+ "detail": "http://mochi.test:8888/receipt/5169314356",
+ "verify": "http://mochi.test:8888/verify/5169314356",
+ "reissue": "http://mochi.test:8888/reissue/5169314356"
+}
+
+receipt_without_nbf = {
+ "typ": "purchase-receipt",
+ "product": {
+ "url": "https://www.mozilla.org",
+ "storedata": "5169314358"
+ },
+ "user": {
+ "type": "directed-identifier",
+ "value": "4fb35151-2b9b-4ba2-8283-c49d381640bd"
+ },
+ "iss": "http://mochi.test:8888",
+ "iat": 131360188,
+ "detail": "http://mochi.test:8888/receipt/5169314356",
+ "verify": "http://mochi.test:8888/verify/5169314356",
+ "reissue": "http://mochi.test:8888/reissue/5169314356"
+}
+
+receipt_without_iat = {
+ "typ": "purchase-receipt",
+ "product": {
+ "url": "https://www.mozilla.org",
+ "storedata": "5169314358"
+ },
+ "user": {
+ "type": "directed-identifier",
+ "value": "4fb35151-2b9b-4ba2-8283-c49d381640bd"
+ },
+ "iss": "http://mochi.test:8888",
+ "nbf": 131360185,
+ "detail": "http://mochi.test:8888/receipt/5169314356",
+ "verify": "http://mochi.test:8888/verify/5169314356",
+ "reissue": "http://mochi.test:8888/reissue/5169314356"
+}
+
+receipt_with_wrong_typ = {
+ "typ": "fake",
+ "product": {
+ "url": "https://www.mozilla.org",
+ "storedata": "5169314358"
+ },
+ "user": {
+ "type": "directed-identifier",
+ "value": "4fb35151-2b9b-4ba2-8283-c49d381640bd"
+ },
+ "iss": "http://mochi.test:8888",
+ "nbf": 131360185,
+ "iat": 131360188,
+ "detail": "http://mochi.test:8888/receipt/5169314356",
+ "verify": "http://mochi.test:8888/verify/5169314356",
+ "reissue": "http://mochi.test:8888/reissue/5169314356"
+}
+
+print("let valid_receipt1 = \"" + jwt.encode(receipt1, "") + "\";\n")
+print("let valid_receipt2 = \"" + jwt.encode(receipt2, "") + "\";\n")
+print("let receipt_without_typ = \"" + jwt.encode(receipt_without_typ, "") + "\";\n")
+print("let receipt_without_product = \"" + jwt.encode(receipt_without_product, "") + "\";\n")
+print("let receipt_without_user = \"" + jwt.encode(receipt_without_user, "") + "\";\n")
+print("let receipt_without_iss = \"" + jwt.encode(receipt_without_iss, "") + "\";\n")
+print("let receipt_without_nbf = \"" + jwt.encode(receipt_without_nbf, "") + "\";\n")
+print("let receipt_without_iat = \"" + jwt.encode(receipt_without_iat, "") + "\";\n")
+print("let receipt_with_wrong_typ = \"" + jwt.encode(receipt_with_wrong_typ, "") + "\";\n")
diff --git a/dom/apps/tests/head.js b/dom/apps/tests/head.js
new file mode 100644
index 000000000..662d119a0
--- /dev/null
+++ b/dom/apps/tests/head.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function runAll(steps) {
+ SimpleTest.waitForExplicitFinish();
+
+ // Clone the array so we don't modify the original.
+ steps = steps.concat();
+ function next() {
+ if (steps.length) {
+ steps.shift()(next);
+ }
+ else {
+ SimpleTest.finish();
+ }
+ }
+ next();
+}
+
+function confirmNextPopup() {
+ var Ci = SpecialPowers.Ci;
+
+ var popupNotifications = SpecialPowers.wrap(window).top.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIDocShell).
+ chromeEventHandler.ownerDocument.defaultView.
+ PopupNotifications;
+
+ var popupPanel = popupNotifications.panel;
+
+ function onPopupShown() {
+ popupPanel.removeEventListener("popupshown", onPopupShown, false);
+ SpecialPowers.wrap(this).childNodes[0].button.doCommand();
+ popupNotifications._dismiss();
+ }
+ popupPanel.addEventListener("popupshown", onPopupShown, false);
+}
+
+function promiseNoPopup() {
+ var Ci = SpecialPowers.Ci;
+
+ var popupNotifications = SpecialPowers.wrap(window).top.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIDocShell).
+ chromeEventHandler.ownerDocument.defaultView.
+ PopupNotifications;
+
+ return new Promise((resolve) => {
+ var tries = 0;
+ var interval = setInterval(function() {
+ if (tries >= 30) {
+ ok(true, "The webapps-install notification didn't appear");
+ moveOn();
+ }
+
+ if (popupNotifications.getNotification("webapps-install")) {
+ ok(false, "Found the webapps-install notification");
+ moveOn();
+ }
+ tries++;
+ }, 100);
+
+ var moveOn = () => {
+ clearInterval(interval);
+ resolve();
+ };
+ });
+}
+
+// We need to mock the Alerts service, otherwise the alert that is shown
+// at the end of an installation makes the test leak the app's icon.
+
+const CID = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID();
+const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1";
+const ALERTS_SERVICE_CID = Components.ID(Cc[ALERTS_SERVICE_CONTRACT_ID].number);
+
+var AlertsService = {
+ classID: Components.ID(CID),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory,
+ Ci.nsIAlertsService]),
+
+ createInstance: function(aOuter, aIID) {
+ if (aOuter) {
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+
+ return this.QueryInterface(aIID);
+ },
+
+ init: function() {
+ Components.manager.nsIComponentRegistrar.registerFactory(this.classID,
+ "", ALERTS_SERVICE_CONTRACT_ID, this);
+ },
+
+ restore: function() {
+ Components.manager.nsIComponentRegistrar.registerFactory(ALERTS_SERVICE_CID,
+ "", ALERTS_SERVICE_CONTRACT_ID, null);
+ },
+
+ showAlert: function() {
+ },
+
+ showAlertNotification: function() {
+ },
+};
+
+AlertsService.init();
+
+SimpleTest.registerCleanupFunction(() => {
+ AlertsService.restore();
+});
diff --git a/dom/apps/tests/unit/test_manifestHelper.js b/dom/apps/tests/unit/test_manifestHelper.js
new file mode 100644
index 000000000..092287a7f
--- /dev/null
+++ b/dom/apps/tests/unit/test_manifestHelper.js
@@ -0,0 +1,23 @@
+// Test that W3C start_url takes precedence over mozApps launch_path
+function run_test() {
+ Components.utils.import("resource:///modules/AppsUtils.jsm");
+
+ let manifest1 = {
+ launch_path: "other.html"
+ };
+
+ let manifest2 = {
+ start_url: "start.html",
+ launch_path: "other.html"
+ };
+
+ var helper = new ManifestHelper(manifest1, "http://foo.com",
+ "http://foo.com/manifest.json");
+ var path = helper.fullLaunchPath();
+ do_check_true(path == "http://foo.com/other.html");
+
+ helper = new ManifestHelper(manifest2, "http://foo.com",
+ "http://foo.com/manifest.json");
+ path = helper.fullLaunchPath();
+ do_check_true(path == "http://foo.com/start.html");
+}
diff --git a/dom/apps/tests/unit/test_manifestSanitizer.js b/dom/apps/tests/unit/test_manifestSanitizer.js
new file mode 100644
index 000000000..4b0c999a7
--- /dev/null
+++ b/dom/apps/tests/unit/test_manifestSanitizer.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function testEntryPoint(aRoot) {
+ do_check_true(aRoot.name == "hello world");
+ do_check_true(aRoot.description == "A bold name");
+ do_check_true(aRoot.developer.name == "Blink Inc.");
+
+ let permissions = aRoot.permissions;
+ do_check_true(permissions.contacts.description == "Required for autocompletion in the share screen");
+ do_check_true(permissions.alarms.description == "Required to schedule notifications");
+}
+
+function run_test() {
+ Components.utils.import("resource:///modules/AppsUtils.jsm");
+
+ do_check_true(!!AppsUtils);
+
+ // Test manifest, with one entry point.
+ let manifest = {
+ name: "hello <b>world</b>",
+ description: "A bold name",
+ developer: {
+ name: "<blink>Blink</blink> Inc.",
+ url: "http://blink.org"
+ },
+ permissions : {
+ "contacts": {
+ "description": "Required for autocompletion in the <a href='http://shareme.com'>share</a> screen",
+ "access": "readcreate"
+ },
+ "alarms": {
+ "description": "Required to schedule notifications"
+ }
+ },
+
+ entry_points: {
+ "subapp": {
+ name: "hello <b>world</b>",
+ description: "A bold name",
+ developer: {
+ name: "<blink>Blink</blink> Inc.",
+ url: "http://blink.org"
+ },
+ permissions : {
+ "contacts": {
+ "description": "Required for autocompletion in the <a href='http://shareme.com'>share</a> screen",
+ "access": "readcreate"
+ },
+ "alarms": {
+ "description": "Required to schedule notifications"
+ }
+ }
+ }
+ }
+ }
+
+ AppsUtils.sanitizeManifest(manifest);
+
+ // Check the main section and the subapp entry point.
+ testEntryPoint(manifest);
+ testEntryPoint(manifest.entry_points.subapp);
+}
diff --git a/dom/apps/tests/unit/test_moziapplication.js b/dom/apps/tests/unit/test_moziapplication.js
new file mode 100644
index 000000000..dbcf2372a
--- /dev/null
+++ b/dom/apps/tests/unit/test_moziapplication.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource:///modules/AppsUtils.jsm");
+
+add_test(() => {
+ let app = {
+ name: "TestApp",
+ csp: "aCsp",
+ installOrigin: "http://installorigin.com",
+ origin: "http://www.example.com",
+ installTime: Date.now(),
+ manifestURL: "http://www.example.com/manifest.webapp",
+ appStatus: Ci.nsIPrincipal.APP_STATUS_NOT_INSTALLED,
+ removable: false,
+ id: 123,
+ localId: 123,
+ basePath: "/",
+ progress: 1.0,
+ installState: "installed",
+ downloadAvailable: false,
+ downloading: false,
+ lastUpdateCheck: Date.now(),
+ updateTime: Date.now(),
+ etag: "aEtag",
+ packageEtag: "aPackageEtag",
+ manifestHash: "aManifestHash",
+ packageHash: "aPackageHash",
+ staged: false,
+ installerAppId: 345,
+ installerIsBrowser: false,
+ storeId: "aStoreId",
+ storeVersion: 1,
+ role: "aRole",
+ kind: "aKind",
+ enabled: true,
+ sideloaded: false
+ };
+
+ let mozapp = new mozIApplication(app);
+
+ Object.keys(app).forEach((key) => {
+ if (key == "principal") {
+ return;
+ }
+ Assert.equal(app[key], mozapp[key],
+ "app[" + key + "] should be equal to mozapp[" + key + "]");
+ });
+
+ Assert.ok(mozapp.principal, "app principal should exist");
+ let expectedPrincipalOrigin = app.origin + "^appId=" + app.localId;
+ Assert.equal(mozapp.principal.origin, expectedPrincipalOrigin,
+ "app principal origin ok");
+ Assert.equal(mozapp.principal.appId, app.localId, "app principal appId ok");
+ Assert.equal(mozapp.principal.isInIsolatedMozBrowserElement, false,
+ "app principal isInIsolatedMozBrowserElement ok");
+ run_next_test();
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/dom/apps/tests/unit/xpcshell.ini b/dom/apps/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..9745b198f
--- /dev/null
+++ b/dom/apps/tests/unit/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+
+[test_manifestSanitizer.js]
+[test_manifestHelper.js]
+[test_moziapplication.js]