1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/datastore/DataStoreService.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,517 @@ 1.4 +/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +'use strict' 1.11 + 1.12 +/* static functions */ 1.13 + 1.14 +function debug(s) { 1.15 + //dump('DEBUG DataStoreService: ' + s + '\n'); 1.16 +} 1.17 + 1.18 +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; 1.19 + 1.20 +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); 1.21 +Cu.import('resource://gre/modules/Services.jsm'); 1.22 +Cu.import('resource://gre/modules/DataStoreImpl.jsm'); 1.23 +Cu.import("resource://gre/modules/DataStoreDB.jsm"); 1.24 +Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); 1.25 + 1.26 +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", 1.27 + "@mozilla.org/childprocessmessagemanager;1", 1.28 + "nsIMessageSender"); 1.29 + 1.30 +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", 1.31 + "@mozilla.org/parentprocessmessagemanager;1", 1.32 + "nsIMessageBroadcaster"); 1.33 + 1.34 +XPCOMUtils.defineLazyServiceGetter(this, "permissionManager", 1.35 + "@mozilla.org/permissionmanager;1", 1.36 + "nsIPermissionManager"); 1.37 + 1.38 +XPCOMUtils.defineLazyServiceGetter(this, "secMan", 1.39 + "@mozilla.org/scriptsecuritymanager;1", 1.40 + "nsIScriptSecurityManager"); 1.41 + 1.42 +/* DataStoreService */ 1.43 + 1.44 +const DATASTORESERVICE_CID = Components.ID('{d193d0e2-c677-4a7b-bb0a-19155b470f2e}'); 1.45 +const REVISION_VOID = "void"; 1.46 + 1.47 +function DataStoreService() { 1.48 + debug('DataStoreService Constructor'); 1.49 + 1.50 + this.inParent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) 1.51 + .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; 1.52 + 1.53 + if (this.inParent) { 1.54 + let obs = Services.obs; 1.55 + if (!obs) { 1.56 + debug("DataStore Error: observer-service is null!"); 1.57 + return; 1.58 + } 1.59 + 1.60 + obs.addObserver(this, 'webapps-clear-data', false); 1.61 + } 1.62 + 1.63 + let self = this; 1.64 + cpmm.addMessageListener("datastore-first-revision-created", 1.65 + function(aMsg) { self.receiveMessage(aMsg); }); 1.66 +} 1.67 + 1.68 +DataStoreService.prototype = { 1.69 + inParent: false, 1.70 + 1.71 + // Hash of DataStores 1.72 + stores: {}, 1.73 + accessStores: {}, 1.74 + pendingRequests: {}, 1.75 + 1.76 + installDataStore: function(aAppId, aName, aOrigin, aOwner, aReadOnly) { 1.77 + debug('installDataStore - appId: ' + aAppId + ', aName: ' + 1.78 + aName + ', aOrigin: ' + aOrigin + ', aOwner:' + aOwner + 1.79 + ', aReadOnly: ' + aReadOnly); 1.80 + 1.81 + this.checkIfInParent(); 1.82 + 1.83 + if (aName in this.stores && aAppId in this.stores[aName]) { 1.84 + debug('This should not happen'); 1.85 + return; 1.86 + } 1.87 + 1.88 + if (!(aName in this.stores)) { 1.89 + this.stores[aName] = {}; 1.90 + } 1.91 + 1.92 + // A DataStore is enabled when it has a first valid revision. 1.93 + this.stores[aName][aAppId] = { origin: aOrigin, owner: aOwner, 1.94 + readOnly: aReadOnly, enabled: false }; 1.95 + 1.96 + this.addPermissions(aAppId, aName, aOrigin, aOwner, aReadOnly); 1.97 + 1.98 + this.createFirstRevisionId(aAppId, aName, aOwner); 1.99 + }, 1.100 + 1.101 + installAccessDataStore: function(aAppId, aName, aOrigin, aOwner, aReadOnly) { 1.102 + debug('installAccessDataStore - appId: ' + aAppId + ', aName: ' + 1.103 + aName + ', aOrigin: ' + aOrigin + ', aOwner:' + aOwner + 1.104 + ', aReadOnly: ' + aReadOnly); 1.105 + 1.106 + this.checkIfInParent(); 1.107 + 1.108 + if (aName in this.accessStores && aAppId in this.accessStores[aName]) { 1.109 + debug('This should not happen'); 1.110 + return; 1.111 + } 1.112 + 1.113 + if (!(aName in this.accessStores)) { 1.114 + this.accessStores[aName] = {}; 1.115 + } 1.116 + 1.117 + this.accessStores[aName][aAppId] = { origin: aOrigin, owner: aOwner, 1.118 + readOnly: aReadOnly }; 1.119 + this.addAccessPermissions(aAppId, aName, aOrigin, aOwner, aReadOnly); 1.120 + }, 1.121 + 1.122 + checkIfInParent: function() { 1.123 + if (!this.inParent) { 1.124 + throw "DataStore can execute this operation just in the parent process"; 1.125 + } 1.126 + }, 1.127 + 1.128 + createFirstRevisionId: function(aAppId, aName, aOwner) { 1.129 + debug("createFirstRevisionId database: " + aName); 1.130 + 1.131 + let self = this; 1.132 + let db = new DataStoreDB(); 1.133 + db.init(aOwner, aName); 1.134 + db.revisionTxn( 1.135 + 'readwrite', 1.136 + function(aTxn, aRevisionStore) { 1.137 + debug("createFirstRevisionId - transaction success"); 1.138 + 1.139 + let request = aRevisionStore.openCursor(null, 'prev'); 1.140 + request.onsuccess = function(aEvent) { 1.141 + let cursor = aEvent.target.result; 1.142 + if (cursor) { 1.143 + debug("First revision already created."); 1.144 + self.enableDataStore(aAppId, aName, aOwner); 1.145 + } else { 1.146 + // If the revision doesn't exist, let's create the first one. 1.147 + db.addRevision(aRevisionStore, 0, REVISION_VOID, function() { 1.148 + debug("First revision created."); 1.149 + self.enableDataStore(aAppId, aName, aOwner); 1.150 + }); 1.151 + } 1.152 + }; 1.153 + } 1.154 + ); 1.155 + }, 1.156 + 1.157 + enableDataStore: function(aAppId, aName, aOwner) { 1.158 + if (aName in this.stores && aAppId in this.stores[aName]) { 1.159 + this.stores[aName][aAppId].enabled = true; 1.160 + ppmm.broadcastAsyncMessage('datastore-first-revision-created', 1.161 + { name: aName, owner: aOwner }); 1.162 + } 1.163 + }, 1.164 + 1.165 + addPermissions: function(aAppId, aName, aOrigin, aOwner, aReadOnly) { 1.166 + // When a new DataStore is installed, the permissions must be set for the 1.167 + // owner app. 1.168 + let permission = "indexedDB-chrome-" + aName + '|' + aOwner; 1.169 + this.resetPermissions(aAppId, aOrigin, aOwner, permission, aReadOnly); 1.170 + 1.171 + // For any app that wants to have access to this DataStore we add the 1.172 + // permissions. 1.173 + if (aName in this.accessStores) { 1.174 + for (let appId in this.accessStores[aName]) { 1.175 + // ReadOnly is decided by the owner first. 1.176 + let readOnly = aReadOnly || this.accessStores[aName][appId].readOnly; 1.177 + this.resetPermissions(appId, this.accessStores[aName][appId].origin, 1.178 + this.accessStores[aName][appId].owner, 1.179 + permission, readOnly); 1.180 + } 1.181 + } 1.182 + }, 1.183 + 1.184 + addAccessPermissions: function(aAppId, aName, aOrigin, aOwner, aReadOnly) { 1.185 + // When an app wants to have access to a DataStore, the permissions must be 1.186 + // set. 1.187 + if (!(aName in this.stores)) { 1.188 + return; 1.189 + } 1.190 + 1.191 + for (let appId in this.stores[aName]) { 1.192 + let permission = "indexedDB-chrome-" + aName + '|' + this.stores[aName][appId].owner; 1.193 + // The ReadOnly is decied by the owenr first. 1.194 + let readOnly = this.stores[aName][appId].readOnly || aReadOnly; 1.195 + this.resetPermissions(aAppId, aOrigin, aOwner, permission, readOnly); 1.196 + } 1.197 + }, 1.198 + 1.199 + resetPermissions: function(aAppId, aOrigin, aOwner, aPermission, aReadOnly) { 1.200 + debug("ResetPermissions - appId: " + aAppId + " - origin: " + aOrigin + 1.201 + " - owner: " + aOwner + " - permissions: " + aPermission + 1.202 + " - readOnly: " + aReadOnly); 1.203 + 1.204 + let uri = Services.io.newURI(aOrigin, null, null); 1.205 + let principal = secMan.getAppCodebasePrincipal(uri, aAppId, false); 1.206 + 1.207 + let result = permissionManager.testExactPermissionFromPrincipal(principal, 1.208 + aPermission + '-write'); 1.209 + 1.210 + if (aReadOnly && result == Ci.nsIPermissionManager.ALLOW_ACTION) { 1.211 + debug("Write permission removed"); 1.212 + permissionManager.removeFromPrincipal(principal, aPermission + '-write'); 1.213 + } else if (!aReadOnly && result != Ci.nsIPermissionManager.ALLOW_ACTION) { 1.214 + debug("Write permission added"); 1.215 + permissionManager.addFromPrincipal(principal, aPermission + '-write', 1.216 + Ci.nsIPermissionManager.ALLOW_ACTION); 1.217 + } 1.218 + 1.219 + result = permissionManager.testExactPermissionFromPrincipal(principal, 1.220 + aPermission + '-read'); 1.221 + if (result != Ci.nsIPermissionManager.ALLOW_ACTION) { 1.222 + debug("Read permission added"); 1.223 + permissionManager.addFromPrincipal(principal, aPermission + '-read', 1.224 + Ci.nsIPermissionManager.ALLOW_ACTION); 1.225 + } 1.226 + 1.227 + result = permissionManager.testExactPermissionFromPrincipal(principal, aPermission); 1.228 + if (result != Ci.nsIPermissionManager.ALLOW_ACTION) { 1.229 + debug("Generic permission added"); 1.230 + permissionManager.addFromPrincipal(principal, aPermission, 1.231 + Ci.nsIPermissionManager.ALLOW_ACTION); 1.232 + } 1.233 + }, 1.234 + 1.235 + getDataStores: function(aWindow, aName) { 1.236 + debug('getDataStores - aName: ' + aName); 1.237 + 1.238 + let self = this; 1.239 + return new aWindow.Promise(function(resolve, reject) { 1.240 + // If this request comes from the main process, we have access to the 1.241 + // window, so we can skip the ipc communication. 1.242 + if (self.inParent) { 1.243 + let stores = self.getDataStoresInfo(aName, aWindow.document.nodePrincipal.appId); 1.244 + if (stores === null) { 1.245 + reject(new aWindow.DOMError("SecurityError", "Access denied")); 1.246 + return; 1.247 + } 1.248 + self.getDataStoreCreate(aWindow, resolve, stores); 1.249 + } else { 1.250 + // This method can be called in the child so we need to send a request 1.251 + // to the parent and create DataStore object here. 1.252 + new DataStoreServiceChild(aWindow, aName, function(aStores) { 1.253 + debug("DataStoreServiceChild success callback!"); 1.254 + self.getDataStoreCreate(aWindow, resolve, aStores); 1.255 + }, function() { 1.256 + debug("DataStoreServiceChild error callback!"); 1.257 + reject(new aWindow.DOMError("SecurityError", "Access denied")); 1.258 + }); 1.259 + } 1.260 + }); 1.261 + }, 1.262 + 1.263 + getDataStoresInfo: function(aName, aAppId) { 1.264 + debug('GetDataStoresInfo'); 1.265 + 1.266 + let appsService = Cc["@mozilla.org/AppsService;1"] 1.267 + .getService(Ci.nsIAppsService); 1.268 + let app = appsService.getAppByLocalId(aAppId); 1.269 + if (!app) { 1.270 + return null; 1.271 + } 1.272 + 1.273 + let prefName = "dom.testing.datastore_enabled_for_hosted_apps"; 1.274 + if (app.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED && 1.275 + (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_INVALID || 1.276 + !Services.prefs.getBoolPref(prefName))) { 1.277 + return null; 1.278 + } 1.279 + 1.280 + let results = []; 1.281 + 1.282 + if (aName in this.stores) { 1.283 + if (aAppId in this.stores[aName]) { 1.284 + results.push({ name: aName, 1.285 + owner: this.stores[aName][aAppId].owner, 1.286 + readOnly: false, 1.287 + enabled: this.stores[aName][aAppId].enabled }); 1.288 + } 1.289 + 1.290 + for (var i in this.stores[aName]) { 1.291 + if (i == aAppId) { 1.292 + continue; 1.293 + } 1.294 + 1.295 + let access = this.getDataStoreAccess(aName, aAppId); 1.296 + if (!access) { 1.297 + continue; 1.298 + } 1.299 + 1.300 + let readOnly = this.stores[aName][i].readOnly || access.readOnly; 1.301 + results.push({ name: aName, 1.302 + owner: this.stores[aName][i].owner, 1.303 + readOnly: readOnly, 1.304 + enabled: this.stores[aName][i].enabled }); 1.305 + } 1.306 + } 1.307 + 1.308 + return results; 1.309 + }, 1.310 + 1.311 + getDataStoreCreate: function(aWindow, aResolve, aStores) { 1.312 + debug("GetDataStoreCreate"); 1.313 + 1.314 + let results = []; 1.315 + 1.316 + if (!aStores.length) { 1.317 + aResolve(results); 1.318 + return; 1.319 + } 1.320 + 1.321 + let pendingDataStores = []; 1.322 + 1.323 + for (let i = 0; i < aStores.length; ++i) { 1.324 + if (!aStores[i].enabled) { 1.325 + pendingDataStores.push(aStores[i].owner); 1.326 + } 1.327 + } 1.328 + 1.329 + if (!pendingDataStores.length) { 1.330 + this.getDataStoreResolve(aWindow, aResolve, aStores); 1.331 + return; 1.332 + } 1.333 + 1.334 + if (!(aStores[0].name in this.pendingRequests)) { 1.335 + this.pendingRequests[aStores[0].name] = []; 1.336 + } 1.337 + 1.338 + this.pendingRequests[aStores[0].name].push({ window: aWindow, 1.339 + resolve: aResolve, 1.340 + stores: aStores, 1.341 + pendingDataStores: pendingDataStores }); 1.342 + }, 1.343 + 1.344 + getDataStoreResolve: function(aWindow, aResolve, aStores) { 1.345 + debug("GetDataStoreResolve"); 1.346 + 1.347 + let callbackPending = aStores.length; 1.348 + let results = []; 1.349 + 1.350 + if (!callbackPending) { 1.351 + aResolve(results); 1.352 + return; 1.353 + } 1.354 + 1.355 + for (let i = 0; i < aStores.length; ++i) { 1.356 + let obj = new DataStore(aWindow, aStores[i].name, 1.357 + aStores[i].owner, aStores[i].readOnly); 1.358 + 1.359 + let storeImpl = aWindow.DataStoreImpl._create(aWindow, obj); 1.360 + 1.361 + let exposedStore = new aWindow.DataStore(); 1.362 + exposedStore.setDataStoreImpl(storeImpl); 1.363 + 1.364 + obj.exposedObject = exposedStore; 1.365 + 1.366 + results.push(exposedStore); 1.367 + 1.368 + obj.retrieveRevisionId( 1.369 + function() { 1.370 + --callbackPending; 1.371 + if (!callbackPending) { 1.372 + aResolve(results); 1.373 + } 1.374 + } 1.375 + ); 1.376 + } 1.377 + }, 1.378 + 1.379 + getDataStoreAccess: function(aName, aAppId) { 1.380 + if (!(aName in this.accessStores) || 1.381 + !(aAppId in this.accessStores[aName])) { 1.382 + return null; 1.383 + } 1.384 + 1.385 + return this.accessStores[aName][aAppId]; 1.386 + }, 1.387 + 1.388 + observe: function observe(aSubject, aTopic, aData) { 1.389 + debug('observe - aTopic: ' + aTopic); 1.390 + if (aTopic != 'webapps-clear-data') { 1.391 + return; 1.392 + } 1.393 + 1.394 + let params = 1.395 + aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams); 1.396 + 1.397 + // DataStore is explosed to apps, not browser content. 1.398 + if (params.browserOnly) { 1.399 + return; 1.400 + } 1.401 + 1.402 + function isEmpty(aMap) { 1.403 + for (var key in aMap) { 1.404 + if (aMap.hasOwnProperty(key)) { 1.405 + return false; 1.406 + } 1.407 + } 1.408 + return true; 1.409 + } 1.410 + 1.411 + for (let key in this.stores) { 1.412 + if (params.appId in this.stores[key]) { 1.413 + this.deleteDatabase(key, this.stores[key][params.appId].owner); 1.414 + delete this.stores[key][params.appId]; 1.415 + } 1.416 + 1.417 + if (isEmpty(this.stores[key])) { 1.418 + delete this.stores[key]; 1.419 + } 1.420 + } 1.421 + 1.422 + for (let key in this.accessStores) { 1.423 + if (params.appId in this.accessStores[key]) { 1.424 + delete this.accessStores[key][params.appId]; 1.425 + } 1.426 + 1.427 + if (isEmpty(this.accessStores[key])) { 1.428 + delete this.accessStores[key]; 1.429 + } 1.430 + } 1.431 + }, 1.432 + 1.433 + deleteDatabase: function(aName, aOwner) { 1.434 + debug("delete database: " + aName); 1.435 + 1.436 + let db = new DataStoreDB(); 1.437 + db.init(aOwner, aName); 1.438 + db.delete(); 1.439 + }, 1.440 + 1.441 + receiveMessage: function(aMsg) { 1.442 + debug("receiveMessage"); 1.443 + let data = aMsg.json; 1.444 + 1.445 + if (!(data.name in this.pendingRequests)) { 1.446 + return; 1.447 + } 1.448 + 1.449 + for (let i = 0; i < this.pendingRequests[data.name].length;) { 1.450 + let pos = this.pendingRequests[data.name][i].pendingDataStores.indexOf(data.owner); 1.451 + if (pos != -1) { 1.452 + this.pendingRequests[data.name][i].pendingDataStores.splice(pos, 1); 1.453 + if (!this.pendingRequests[data.name][i].pendingDataStores.length) { 1.454 + this.getDataStoreResolve(this.pendingRequests[data.name][i].window, 1.455 + this.pendingRequests[data.name][i].resolve, 1.456 + this.pendingRequests[data.name][i].stores); 1.457 + this.pendingRequests[data.name].splice(i, 1); 1.458 + continue; 1.459 + } 1.460 + } 1.461 + 1.462 + ++i; 1.463 + } 1.464 + 1.465 + if (!this.pendingRequests[data.name].length) { 1.466 + delete this.pendingRequests[data.name]; 1.467 + } 1.468 + }, 1.469 + 1.470 + classID : DATASTORESERVICE_CID, 1.471 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataStoreService, 1.472 + Ci.nsIObserver]), 1.473 + classInfo: XPCOMUtils.generateCI({ 1.474 + classID: DATASTORESERVICE_CID, 1.475 + contractID: '@mozilla.org/datastore-service;1', 1.476 + interfaces: [Ci.nsIDataStoreService, Ci.nsIObserver], 1.477 + flags: Ci.nsIClassInfo.SINGLETON 1.478 + }) 1.479 +}; 1.480 + 1.481 +/* DataStoreServiceChild */ 1.482 + 1.483 +function DataStoreServiceChild(aWindow, aName, aSuccessCb, aErrorCb) { 1.484 + debug("DataStoreServiceChild created"); 1.485 + this.init(aWindow, aName, aSuccessCb, aErrorCb); 1.486 +} 1.487 + 1.488 +DataStoreServiceChild.prototype = { 1.489 + __proto__: DOMRequestIpcHelper.prototype, 1.490 + 1.491 + init: function(aWindow, aName, aSuccessCb, aErrorCb) { 1.492 + debug("DataStoreServiceChild init"); 1.493 + this._successCb = aSuccessCb; 1.494 + this._errorCb = aErrorCb; 1.495 + 1.496 + this.initDOMRequestHelper(aWindow, [ "DataStore:Get:Return:OK", 1.497 + "DataStore:Get:Return:KO" ]); 1.498 + 1.499 + cpmm.sendAsyncMessage("DataStore:Get", 1.500 + { name: aName }, null, aWindow.document.nodePrincipal ); 1.501 + }, 1.502 + 1.503 + receiveMessage: function(aMessage) { 1.504 + debug("DataStoreServiceChild receiveMessage"); 1.505 + 1.506 + switch (aMessage.name) { 1.507 + case 'DataStore:Get:Return:OK': 1.508 + this.destroyDOMRequestHelper(); 1.509 + this._successCb(aMessage.data.stores); 1.510 + break; 1.511 + 1.512 + case 'DataStore:Get:Return:KO': 1.513 + this.destroyDOMRequestHelper(); 1.514 + this._errorCb(); 1.515 + break; 1.516 + } 1.517 + } 1.518 +} 1.519 + 1.520 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataStoreService]);