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