michael@0: /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: 'use strict' michael@0: michael@0: /* static functions */ michael@0: michael@0: function debug(s) { michael@0: //dump('DEBUG DataStoreService: ' + s + '\n'); michael@0: } michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; michael@0: michael@0: Cu.import('resource://gre/modules/XPCOMUtils.jsm'); michael@0: Cu.import('resource://gre/modules/Services.jsm'); michael@0: Cu.import('resource://gre/modules/DataStoreImpl.jsm'); michael@0: Cu.import("resource://gre/modules/DataStoreDB.jsm"); michael@0: Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "cpmm", michael@0: "@mozilla.org/childprocessmessagemanager;1", michael@0: "nsIMessageSender"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "ppmm", michael@0: "@mozilla.org/parentprocessmessagemanager;1", michael@0: "nsIMessageBroadcaster"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "permissionManager", michael@0: "@mozilla.org/permissionmanager;1", michael@0: "nsIPermissionManager"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "secMan", michael@0: "@mozilla.org/scriptsecuritymanager;1", michael@0: "nsIScriptSecurityManager"); michael@0: michael@0: /* DataStoreService */ michael@0: michael@0: const DATASTORESERVICE_CID = Components.ID('{d193d0e2-c677-4a7b-bb0a-19155b470f2e}'); michael@0: const REVISION_VOID = "void"; michael@0: michael@0: function DataStoreService() { michael@0: debug('DataStoreService Constructor'); michael@0: michael@0: this.inParent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) michael@0: .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; michael@0: michael@0: if (this.inParent) { michael@0: let obs = Services.obs; michael@0: if (!obs) { michael@0: debug("DataStore Error: observer-service is null!"); michael@0: return; michael@0: } michael@0: michael@0: obs.addObserver(this, 'webapps-clear-data', false); michael@0: } michael@0: michael@0: let self = this; michael@0: cpmm.addMessageListener("datastore-first-revision-created", michael@0: function(aMsg) { self.receiveMessage(aMsg); }); michael@0: } michael@0: michael@0: DataStoreService.prototype = { michael@0: inParent: false, michael@0: michael@0: // Hash of DataStores michael@0: stores: {}, michael@0: accessStores: {}, michael@0: pendingRequests: {}, michael@0: michael@0: installDataStore: function(aAppId, aName, aOrigin, aOwner, aReadOnly) { michael@0: debug('installDataStore - appId: ' + aAppId + ', aName: ' + michael@0: aName + ', aOrigin: ' + aOrigin + ', aOwner:' + aOwner + michael@0: ', aReadOnly: ' + aReadOnly); michael@0: michael@0: this.checkIfInParent(); michael@0: michael@0: if (aName in this.stores && aAppId in this.stores[aName]) { michael@0: debug('This should not happen'); michael@0: return; michael@0: } michael@0: michael@0: if (!(aName in this.stores)) { michael@0: this.stores[aName] = {}; michael@0: } michael@0: michael@0: // A DataStore is enabled when it has a first valid revision. michael@0: this.stores[aName][aAppId] = { origin: aOrigin, owner: aOwner, michael@0: readOnly: aReadOnly, enabled: false }; michael@0: michael@0: this.addPermissions(aAppId, aName, aOrigin, aOwner, aReadOnly); michael@0: michael@0: this.createFirstRevisionId(aAppId, aName, aOwner); michael@0: }, michael@0: michael@0: installAccessDataStore: function(aAppId, aName, aOrigin, aOwner, aReadOnly) { michael@0: debug('installAccessDataStore - appId: ' + aAppId + ', aName: ' + michael@0: aName + ', aOrigin: ' + aOrigin + ', aOwner:' + aOwner + michael@0: ', aReadOnly: ' + aReadOnly); michael@0: michael@0: this.checkIfInParent(); michael@0: michael@0: if (aName in this.accessStores && aAppId in this.accessStores[aName]) { michael@0: debug('This should not happen'); michael@0: return; michael@0: } michael@0: michael@0: if (!(aName in this.accessStores)) { michael@0: this.accessStores[aName] = {}; michael@0: } michael@0: michael@0: this.accessStores[aName][aAppId] = { origin: aOrigin, owner: aOwner, michael@0: readOnly: aReadOnly }; michael@0: this.addAccessPermissions(aAppId, aName, aOrigin, aOwner, aReadOnly); michael@0: }, michael@0: michael@0: checkIfInParent: function() { michael@0: if (!this.inParent) { michael@0: throw "DataStore can execute this operation just in the parent process"; michael@0: } michael@0: }, michael@0: michael@0: createFirstRevisionId: function(aAppId, aName, aOwner) { michael@0: debug("createFirstRevisionId database: " + aName); michael@0: michael@0: let self = this; michael@0: let db = new DataStoreDB(); michael@0: db.init(aOwner, aName); michael@0: db.revisionTxn( michael@0: 'readwrite', michael@0: function(aTxn, aRevisionStore) { michael@0: debug("createFirstRevisionId - transaction success"); michael@0: michael@0: let request = aRevisionStore.openCursor(null, 'prev'); michael@0: request.onsuccess = function(aEvent) { michael@0: let cursor = aEvent.target.result; michael@0: if (cursor) { michael@0: debug("First revision already created."); michael@0: self.enableDataStore(aAppId, aName, aOwner); michael@0: } else { michael@0: // If the revision doesn't exist, let's create the first one. michael@0: db.addRevision(aRevisionStore, 0, REVISION_VOID, function() { michael@0: debug("First revision created."); michael@0: self.enableDataStore(aAppId, aName, aOwner); michael@0: }); michael@0: } michael@0: }; michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: enableDataStore: function(aAppId, aName, aOwner) { michael@0: if (aName in this.stores && aAppId in this.stores[aName]) { michael@0: this.stores[aName][aAppId].enabled = true; michael@0: ppmm.broadcastAsyncMessage('datastore-first-revision-created', michael@0: { name: aName, owner: aOwner }); michael@0: } michael@0: }, michael@0: michael@0: addPermissions: function(aAppId, aName, aOrigin, aOwner, aReadOnly) { michael@0: // When a new DataStore is installed, the permissions must be set for the michael@0: // owner app. michael@0: let permission = "indexedDB-chrome-" + aName + '|' + aOwner; michael@0: this.resetPermissions(aAppId, aOrigin, aOwner, permission, aReadOnly); michael@0: michael@0: // For any app that wants to have access to this DataStore we add the michael@0: // permissions. michael@0: if (aName in this.accessStores) { michael@0: for (let appId in this.accessStores[aName]) { michael@0: // ReadOnly is decided by the owner first. michael@0: let readOnly = aReadOnly || this.accessStores[aName][appId].readOnly; michael@0: this.resetPermissions(appId, this.accessStores[aName][appId].origin, michael@0: this.accessStores[aName][appId].owner, michael@0: permission, readOnly); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: addAccessPermissions: function(aAppId, aName, aOrigin, aOwner, aReadOnly) { michael@0: // When an app wants to have access to a DataStore, the permissions must be michael@0: // set. michael@0: if (!(aName in this.stores)) { michael@0: return; michael@0: } michael@0: michael@0: for (let appId in this.stores[aName]) { michael@0: let permission = "indexedDB-chrome-" + aName + '|' + this.stores[aName][appId].owner; michael@0: // The ReadOnly is decied by the owenr first. michael@0: let readOnly = this.stores[aName][appId].readOnly || aReadOnly; michael@0: this.resetPermissions(aAppId, aOrigin, aOwner, permission, readOnly); michael@0: } michael@0: }, michael@0: michael@0: resetPermissions: function(aAppId, aOrigin, aOwner, aPermission, aReadOnly) { michael@0: debug("ResetPermissions - appId: " + aAppId + " - origin: " + aOrigin + michael@0: " - owner: " + aOwner + " - permissions: " + aPermission + michael@0: " - readOnly: " + aReadOnly); michael@0: michael@0: let uri = Services.io.newURI(aOrigin, null, null); michael@0: let principal = secMan.getAppCodebasePrincipal(uri, aAppId, false); michael@0: michael@0: let result = permissionManager.testExactPermissionFromPrincipal(principal, michael@0: aPermission + '-write'); michael@0: michael@0: if (aReadOnly && result == Ci.nsIPermissionManager.ALLOW_ACTION) { michael@0: debug("Write permission removed"); michael@0: permissionManager.removeFromPrincipal(principal, aPermission + '-write'); michael@0: } else if (!aReadOnly && result != Ci.nsIPermissionManager.ALLOW_ACTION) { michael@0: debug("Write permission added"); michael@0: permissionManager.addFromPrincipal(principal, aPermission + '-write', michael@0: Ci.nsIPermissionManager.ALLOW_ACTION); michael@0: } michael@0: michael@0: result = permissionManager.testExactPermissionFromPrincipal(principal, michael@0: aPermission + '-read'); michael@0: if (result != Ci.nsIPermissionManager.ALLOW_ACTION) { michael@0: debug("Read permission added"); michael@0: permissionManager.addFromPrincipal(principal, aPermission + '-read', michael@0: Ci.nsIPermissionManager.ALLOW_ACTION); michael@0: } michael@0: michael@0: result = permissionManager.testExactPermissionFromPrincipal(principal, aPermission); michael@0: if (result != Ci.nsIPermissionManager.ALLOW_ACTION) { michael@0: debug("Generic permission added"); michael@0: permissionManager.addFromPrincipal(principal, aPermission, michael@0: Ci.nsIPermissionManager.ALLOW_ACTION); michael@0: } michael@0: }, michael@0: michael@0: getDataStores: function(aWindow, aName) { michael@0: debug('getDataStores - aName: ' + aName); michael@0: michael@0: let self = this; michael@0: return new aWindow.Promise(function(resolve, reject) { michael@0: // If this request comes from the main process, we have access to the michael@0: // window, so we can skip the ipc communication. michael@0: if (self.inParent) { michael@0: let stores = self.getDataStoresInfo(aName, aWindow.document.nodePrincipal.appId); michael@0: if (stores === null) { michael@0: reject(new aWindow.DOMError("SecurityError", "Access denied")); michael@0: return; michael@0: } michael@0: self.getDataStoreCreate(aWindow, resolve, stores); michael@0: } else { michael@0: // This method can be called in the child so we need to send a request michael@0: // to the parent and create DataStore object here. michael@0: new DataStoreServiceChild(aWindow, aName, function(aStores) { michael@0: debug("DataStoreServiceChild success callback!"); michael@0: self.getDataStoreCreate(aWindow, resolve, aStores); michael@0: }, function() { michael@0: debug("DataStoreServiceChild error callback!"); michael@0: reject(new aWindow.DOMError("SecurityError", "Access denied")); michael@0: }); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: getDataStoresInfo: function(aName, aAppId) { michael@0: debug('GetDataStoresInfo'); michael@0: michael@0: let appsService = Cc["@mozilla.org/AppsService;1"] michael@0: .getService(Ci.nsIAppsService); michael@0: let app = appsService.getAppByLocalId(aAppId); michael@0: if (!app) { michael@0: return null; michael@0: } michael@0: michael@0: let prefName = "dom.testing.datastore_enabled_for_hosted_apps"; michael@0: if (app.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED && michael@0: (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_INVALID || michael@0: !Services.prefs.getBoolPref(prefName))) { michael@0: return null; michael@0: } michael@0: michael@0: let results = []; michael@0: michael@0: if (aName in this.stores) { michael@0: if (aAppId in this.stores[aName]) { michael@0: results.push({ name: aName, michael@0: owner: this.stores[aName][aAppId].owner, michael@0: readOnly: false, michael@0: enabled: this.stores[aName][aAppId].enabled }); michael@0: } michael@0: michael@0: for (var i in this.stores[aName]) { michael@0: if (i == aAppId) { michael@0: continue; michael@0: } michael@0: michael@0: let access = this.getDataStoreAccess(aName, aAppId); michael@0: if (!access) { michael@0: continue; michael@0: } michael@0: michael@0: let readOnly = this.stores[aName][i].readOnly || access.readOnly; michael@0: results.push({ name: aName, michael@0: owner: this.stores[aName][i].owner, michael@0: readOnly: readOnly, michael@0: enabled: this.stores[aName][i].enabled }); michael@0: } michael@0: } michael@0: michael@0: return results; michael@0: }, michael@0: michael@0: getDataStoreCreate: function(aWindow, aResolve, aStores) { michael@0: debug("GetDataStoreCreate"); michael@0: michael@0: let results = []; michael@0: michael@0: if (!aStores.length) { michael@0: aResolve(results); michael@0: return; michael@0: } michael@0: michael@0: let pendingDataStores = []; michael@0: michael@0: for (let i = 0; i < aStores.length; ++i) { michael@0: if (!aStores[i].enabled) { michael@0: pendingDataStores.push(aStores[i].owner); michael@0: } michael@0: } michael@0: michael@0: if (!pendingDataStores.length) { michael@0: this.getDataStoreResolve(aWindow, aResolve, aStores); michael@0: return; michael@0: } michael@0: michael@0: if (!(aStores[0].name in this.pendingRequests)) { michael@0: this.pendingRequests[aStores[0].name] = []; michael@0: } michael@0: michael@0: this.pendingRequests[aStores[0].name].push({ window: aWindow, michael@0: resolve: aResolve, michael@0: stores: aStores, michael@0: pendingDataStores: pendingDataStores }); michael@0: }, michael@0: michael@0: getDataStoreResolve: function(aWindow, aResolve, aStores) { michael@0: debug("GetDataStoreResolve"); michael@0: michael@0: let callbackPending = aStores.length; michael@0: let results = []; michael@0: michael@0: if (!callbackPending) { michael@0: aResolve(results); michael@0: return; michael@0: } michael@0: michael@0: for (let i = 0; i < aStores.length; ++i) { michael@0: let obj = new DataStore(aWindow, aStores[i].name, michael@0: aStores[i].owner, aStores[i].readOnly); michael@0: michael@0: let storeImpl = aWindow.DataStoreImpl._create(aWindow, obj); michael@0: michael@0: let exposedStore = new aWindow.DataStore(); michael@0: exposedStore.setDataStoreImpl(storeImpl); michael@0: michael@0: obj.exposedObject = exposedStore; michael@0: michael@0: results.push(exposedStore); michael@0: michael@0: obj.retrieveRevisionId( michael@0: function() { michael@0: --callbackPending; michael@0: if (!callbackPending) { michael@0: aResolve(results); michael@0: } michael@0: } michael@0: ); michael@0: } michael@0: }, michael@0: michael@0: getDataStoreAccess: function(aName, aAppId) { michael@0: if (!(aName in this.accessStores) || michael@0: !(aAppId in this.accessStores[aName])) { michael@0: return null; michael@0: } michael@0: michael@0: return this.accessStores[aName][aAppId]; michael@0: }, michael@0: michael@0: observe: function observe(aSubject, aTopic, aData) { michael@0: debug('observe - aTopic: ' + aTopic); michael@0: if (aTopic != 'webapps-clear-data') { michael@0: return; michael@0: } michael@0: michael@0: let params = michael@0: aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams); michael@0: michael@0: // DataStore is explosed to apps, not browser content. michael@0: if (params.browserOnly) { michael@0: return; michael@0: } michael@0: michael@0: function isEmpty(aMap) { michael@0: for (var key in aMap) { michael@0: if (aMap.hasOwnProperty(key)) { michael@0: return false; michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: for (let key in this.stores) { michael@0: if (params.appId in this.stores[key]) { michael@0: this.deleteDatabase(key, this.stores[key][params.appId].owner); michael@0: delete this.stores[key][params.appId]; michael@0: } michael@0: michael@0: if (isEmpty(this.stores[key])) { michael@0: delete this.stores[key]; michael@0: } michael@0: } michael@0: michael@0: for (let key in this.accessStores) { michael@0: if (params.appId in this.accessStores[key]) { michael@0: delete this.accessStores[key][params.appId]; michael@0: } michael@0: michael@0: if (isEmpty(this.accessStores[key])) { michael@0: delete this.accessStores[key]; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: deleteDatabase: function(aName, aOwner) { michael@0: debug("delete database: " + aName); michael@0: michael@0: let db = new DataStoreDB(); michael@0: db.init(aOwner, aName); michael@0: db.delete(); michael@0: }, michael@0: michael@0: receiveMessage: function(aMsg) { michael@0: debug("receiveMessage"); michael@0: let data = aMsg.json; michael@0: michael@0: if (!(data.name in this.pendingRequests)) { michael@0: return; michael@0: } michael@0: michael@0: for (let i = 0; i < this.pendingRequests[data.name].length;) { michael@0: let pos = this.pendingRequests[data.name][i].pendingDataStores.indexOf(data.owner); michael@0: if (pos != -1) { michael@0: this.pendingRequests[data.name][i].pendingDataStores.splice(pos, 1); michael@0: if (!this.pendingRequests[data.name][i].pendingDataStores.length) { michael@0: this.getDataStoreResolve(this.pendingRequests[data.name][i].window, michael@0: this.pendingRequests[data.name][i].resolve, michael@0: this.pendingRequests[data.name][i].stores); michael@0: this.pendingRequests[data.name].splice(i, 1); michael@0: continue; michael@0: } michael@0: } michael@0: michael@0: ++i; michael@0: } michael@0: michael@0: if (!this.pendingRequests[data.name].length) { michael@0: delete this.pendingRequests[data.name]; michael@0: } michael@0: }, michael@0: michael@0: classID : DATASTORESERVICE_CID, michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataStoreService, michael@0: Ci.nsIObserver]), michael@0: classInfo: XPCOMUtils.generateCI({ michael@0: classID: DATASTORESERVICE_CID, michael@0: contractID: '@mozilla.org/datastore-service;1', michael@0: interfaces: [Ci.nsIDataStoreService, Ci.nsIObserver], michael@0: flags: Ci.nsIClassInfo.SINGLETON michael@0: }) michael@0: }; michael@0: michael@0: /* DataStoreServiceChild */ michael@0: michael@0: function DataStoreServiceChild(aWindow, aName, aSuccessCb, aErrorCb) { michael@0: debug("DataStoreServiceChild created"); michael@0: this.init(aWindow, aName, aSuccessCb, aErrorCb); michael@0: } michael@0: michael@0: DataStoreServiceChild.prototype = { michael@0: __proto__: DOMRequestIpcHelper.prototype, michael@0: michael@0: init: function(aWindow, aName, aSuccessCb, aErrorCb) { michael@0: debug("DataStoreServiceChild init"); michael@0: this._successCb = aSuccessCb; michael@0: this._errorCb = aErrorCb; michael@0: michael@0: this.initDOMRequestHelper(aWindow, [ "DataStore:Get:Return:OK", michael@0: "DataStore:Get:Return:KO" ]); michael@0: michael@0: cpmm.sendAsyncMessage("DataStore:Get", michael@0: { name: aName }, null, aWindow.document.nodePrincipal ); michael@0: }, michael@0: michael@0: receiveMessage: function(aMessage) { michael@0: debug("DataStoreServiceChild receiveMessage"); michael@0: michael@0: switch (aMessage.name) { michael@0: case 'DataStore:Get:Return:OK': michael@0: this.destroyDOMRequestHelper(); michael@0: this._successCb(aMessage.data.stores); michael@0: break; michael@0: michael@0: case 'DataStore:Get:Return:KO': michael@0: this.destroyDOMRequestHelper(); michael@0: this._errorCb(); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataStoreService]);