/* -*- 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]);