diff options
Diffstat (limited to 'dom/apps')
-rw-r--r-- | dom/apps/AppsService.js | 114 | ||||
-rw-r--r-- | dom/apps/AppsService.manifest | 2 | ||||
-rw-r--r-- | dom/apps/AppsServiceChild.jsm | 408 | ||||
-rw-r--r-- | dom/apps/AppsUtils.jsm | 904 | ||||
-rw-r--r-- | dom/apps/PermissionsInstaller.jsm | 210 | ||||
-rw-r--r-- | dom/apps/PermissionsTable.jsm | 593 | ||||
-rw-r--r-- | dom/apps/moz.build | 22 | ||||
-rwxr-xr-x | dom/apps/tests/create_test_receipts.py | 163 | ||||
-rw-r--r-- | dom/apps/tests/head.js | 118 | ||||
-rw-r--r-- | dom/apps/tests/unit/test_manifestHelper.js | 23 | ||||
-rw-r--r-- | dom/apps/tests/unit/test_manifestSanitizer.js | 63 | ||||
-rw-r--r-- | dom/apps/tests/unit/test_moziapplication.js | 64 | ||||
-rw-r--r-- | dom/apps/tests/unit/xpcshell.ini | 5 |
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] |