|
1 /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ |
|
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 'use strict' |
|
8 |
|
9 /* static functions */ |
|
10 |
|
11 function debug(s) { |
|
12 //dump('DEBUG DataStoreService: ' + s + '\n'); |
|
13 } |
|
14 |
|
15 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; |
|
16 |
|
17 Cu.import('resource://gre/modules/XPCOMUtils.jsm'); |
|
18 Cu.import('resource://gre/modules/Services.jsm'); |
|
19 Cu.import('resource://gre/modules/DataStoreImpl.jsm'); |
|
20 Cu.import("resource://gre/modules/DataStoreDB.jsm"); |
|
21 Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); |
|
22 |
|
23 XPCOMUtils.defineLazyServiceGetter(this, "cpmm", |
|
24 "@mozilla.org/childprocessmessagemanager;1", |
|
25 "nsIMessageSender"); |
|
26 |
|
27 XPCOMUtils.defineLazyServiceGetter(this, "ppmm", |
|
28 "@mozilla.org/parentprocessmessagemanager;1", |
|
29 "nsIMessageBroadcaster"); |
|
30 |
|
31 XPCOMUtils.defineLazyServiceGetter(this, "permissionManager", |
|
32 "@mozilla.org/permissionmanager;1", |
|
33 "nsIPermissionManager"); |
|
34 |
|
35 XPCOMUtils.defineLazyServiceGetter(this, "secMan", |
|
36 "@mozilla.org/scriptsecuritymanager;1", |
|
37 "nsIScriptSecurityManager"); |
|
38 |
|
39 /* DataStoreService */ |
|
40 |
|
41 const DATASTORESERVICE_CID = Components.ID('{d193d0e2-c677-4a7b-bb0a-19155b470f2e}'); |
|
42 const REVISION_VOID = "void"; |
|
43 |
|
44 function DataStoreService() { |
|
45 debug('DataStoreService Constructor'); |
|
46 |
|
47 this.inParent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) |
|
48 .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; |
|
49 |
|
50 if (this.inParent) { |
|
51 let obs = Services.obs; |
|
52 if (!obs) { |
|
53 debug("DataStore Error: observer-service is null!"); |
|
54 return; |
|
55 } |
|
56 |
|
57 obs.addObserver(this, 'webapps-clear-data', false); |
|
58 } |
|
59 |
|
60 let self = this; |
|
61 cpmm.addMessageListener("datastore-first-revision-created", |
|
62 function(aMsg) { self.receiveMessage(aMsg); }); |
|
63 } |
|
64 |
|
65 DataStoreService.prototype = { |
|
66 inParent: false, |
|
67 |
|
68 // Hash of DataStores |
|
69 stores: {}, |
|
70 accessStores: {}, |
|
71 pendingRequests: {}, |
|
72 |
|
73 installDataStore: function(aAppId, aName, aOrigin, aOwner, aReadOnly) { |
|
74 debug('installDataStore - appId: ' + aAppId + ', aName: ' + |
|
75 aName + ', aOrigin: ' + aOrigin + ', aOwner:' + aOwner + |
|
76 ', aReadOnly: ' + aReadOnly); |
|
77 |
|
78 this.checkIfInParent(); |
|
79 |
|
80 if (aName in this.stores && aAppId in this.stores[aName]) { |
|
81 debug('This should not happen'); |
|
82 return; |
|
83 } |
|
84 |
|
85 if (!(aName in this.stores)) { |
|
86 this.stores[aName] = {}; |
|
87 } |
|
88 |
|
89 // A DataStore is enabled when it has a first valid revision. |
|
90 this.stores[aName][aAppId] = { origin: aOrigin, owner: aOwner, |
|
91 readOnly: aReadOnly, enabled: false }; |
|
92 |
|
93 this.addPermissions(aAppId, aName, aOrigin, aOwner, aReadOnly); |
|
94 |
|
95 this.createFirstRevisionId(aAppId, aName, aOwner); |
|
96 }, |
|
97 |
|
98 installAccessDataStore: function(aAppId, aName, aOrigin, aOwner, aReadOnly) { |
|
99 debug('installAccessDataStore - appId: ' + aAppId + ', aName: ' + |
|
100 aName + ', aOrigin: ' + aOrigin + ', aOwner:' + aOwner + |
|
101 ', aReadOnly: ' + aReadOnly); |
|
102 |
|
103 this.checkIfInParent(); |
|
104 |
|
105 if (aName in this.accessStores && aAppId in this.accessStores[aName]) { |
|
106 debug('This should not happen'); |
|
107 return; |
|
108 } |
|
109 |
|
110 if (!(aName in this.accessStores)) { |
|
111 this.accessStores[aName] = {}; |
|
112 } |
|
113 |
|
114 this.accessStores[aName][aAppId] = { origin: aOrigin, owner: aOwner, |
|
115 readOnly: aReadOnly }; |
|
116 this.addAccessPermissions(aAppId, aName, aOrigin, aOwner, aReadOnly); |
|
117 }, |
|
118 |
|
119 checkIfInParent: function() { |
|
120 if (!this.inParent) { |
|
121 throw "DataStore can execute this operation just in the parent process"; |
|
122 } |
|
123 }, |
|
124 |
|
125 createFirstRevisionId: function(aAppId, aName, aOwner) { |
|
126 debug("createFirstRevisionId database: " + aName); |
|
127 |
|
128 let self = this; |
|
129 let db = new DataStoreDB(); |
|
130 db.init(aOwner, aName); |
|
131 db.revisionTxn( |
|
132 'readwrite', |
|
133 function(aTxn, aRevisionStore) { |
|
134 debug("createFirstRevisionId - transaction success"); |
|
135 |
|
136 let request = aRevisionStore.openCursor(null, 'prev'); |
|
137 request.onsuccess = function(aEvent) { |
|
138 let cursor = aEvent.target.result; |
|
139 if (cursor) { |
|
140 debug("First revision already created."); |
|
141 self.enableDataStore(aAppId, aName, aOwner); |
|
142 } else { |
|
143 // If the revision doesn't exist, let's create the first one. |
|
144 db.addRevision(aRevisionStore, 0, REVISION_VOID, function() { |
|
145 debug("First revision created."); |
|
146 self.enableDataStore(aAppId, aName, aOwner); |
|
147 }); |
|
148 } |
|
149 }; |
|
150 } |
|
151 ); |
|
152 }, |
|
153 |
|
154 enableDataStore: function(aAppId, aName, aOwner) { |
|
155 if (aName in this.stores && aAppId in this.stores[aName]) { |
|
156 this.stores[aName][aAppId].enabled = true; |
|
157 ppmm.broadcastAsyncMessage('datastore-first-revision-created', |
|
158 { name: aName, owner: aOwner }); |
|
159 } |
|
160 }, |
|
161 |
|
162 addPermissions: function(aAppId, aName, aOrigin, aOwner, aReadOnly) { |
|
163 // When a new DataStore is installed, the permissions must be set for the |
|
164 // owner app. |
|
165 let permission = "indexedDB-chrome-" + aName + '|' + aOwner; |
|
166 this.resetPermissions(aAppId, aOrigin, aOwner, permission, aReadOnly); |
|
167 |
|
168 // For any app that wants to have access to this DataStore we add the |
|
169 // permissions. |
|
170 if (aName in this.accessStores) { |
|
171 for (let appId in this.accessStores[aName]) { |
|
172 // ReadOnly is decided by the owner first. |
|
173 let readOnly = aReadOnly || this.accessStores[aName][appId].readOnly; |
|
174 this.resetPermissions(appId, this.accessStores[aName][appId].origin, |
|
175 this.accessStores[aName][appId].owner, |
|
176 permission, readOnly); |
|
177 } |
|
178 } |
|
179 }, |
|
180 |
|
181 addAccessPermissions: function(aAppId, aName, aOrigin, aOwner, aReadOnly) { |
|
182 // When an app wants to have access to a DataStore, the permissions must be |
|
183 // set. |
|
184 if (!(aName in this.stores)) { |
|
185 return; |
|
186 } |
|
187 |
|
188 for (let appId in this.stores[aName]) { |
|
189 let permission = "indexedDB-chrome-" + aName + '|' + this.stores[aName][appId].owner; |
|
190 // The ReadOnly is decied by the owenr first. |
|
191 let readOnly = this.stores[aName][appId].readOnly || aReadOnly; |
|
192 this.resetPermissions(aAppId, aOrigin, aOwner, permission, readOnly); |
|
193 } |
|
194 }, |
|
195 |
|
196 resetPermissions: function(aAppId, aOrigin, aOwner, aPermission, aReadOnly) { |
|
197 debug("ResetPermissions - appId: " + aAppId + " - origin: " + aOrigin + |
|
198 " - owner: " + aOwner + " - permissions: " + aPermission + |
|
199 " - readOnly: " + aReadOnly); |
|
200 |
|
201 let uri = Services.io.newURI(aOrigin, null, null); |
|
202 let principal = secMan.getAppCodebasePrincipal(uri, aAppId, false); |
|
203 |
|
204 let result = permissionManager.testExactPermissionFromPrincipal(principal, |
|
205 aPermission + '-write'); |
|
206 |
|
207 if (aReadOnly && result == Ci.nsIPermissionManager.ALLOW_ACTION) { |
|
208 debug("Write permission removed"); |
|
209 permissionManager.removeFromPrincipal(principal, aPermission + '-write'); |
|
210 } else if (!aReadOnly && result != Ci.nsIPermissionManager.ALLOW_ACTION) { |
|
211 debug("Write permission added"); |
|
212 permissionManager.addFromPrincipal(principal, aPermission + '-write', |
|
213 Ci.nsIPermissionManager.ALLOW_ACTION); |
|
214 } |
|
215 |
|
216 result = permissionManager.testExactPermissionFromPrincipal(principal, |
|
217 aPermission + '-read'); |
|
218 if (result != Ci.nsIPermissionManager.ALLOW_ACTION) { |
|
219 debug("Read permission added"); |
|
220 permissionManager.addFromPrincipal(principal, aPermission + '-read', |
|
221 Ci.nsIPermissionManager.ALLOW_ACTION); |
|
222 } |
|
223 |
|
224 result = permissionManager.testExactPermissionFromPrincipal(principal, aPermission); |
|
225 if (result != Ci.nsIPermissionManager.ALLOW_ACTION) { |
|
226 debug("Generic permission added"); |
|
227 permissionManager.addFromPrincipal(principal, aPermission, |
|
228 Ci.nsIPermissionManager.ALLOW_ACTION); |
|
229 } |
|
230 }, |
|
231 |
|
232 getDataStores: function(aWindow, aName) { |
|
233 debug('getDataStores - aName: ' + aName); |
|
234 |
|
235 let self = this; |
|
236 return new aWindow.Promise(function(resolve, reject) { |
|
237 // If this request comes from the main process, we have access to the |
|
238 // window, so we can skip the ipc communication. |
|
239 if (self.inParent) { |
|
240 let stores = self.getDataStoresInfo(aName, aWindow.document.nodePrincipal.appId); |
|
241 if (stores === null) { |
|
242 reject(new aWindow.DOMError("SecurityError", "Access denied")); |
|
243 return; |
|
244 } |
|
245 self.getDataStoreCreate(aWindow, resolve, stores); |
|
246 } else { |
|
247 // This method can be called in the child so we need to send a request |
|
248 // to the parent and create DataStore object here. |
|
249 new DataStoreServiceChild(aWindow, aName, function(aStores) { |
|
250 debug("DataStoreServiceChild success callback!"); |
|
251 self.getDataStoreCreate(aWindow, resolve, aStores); |
|
252 }, function() { |
|
253 debug("DataStoreServiceChild error callback!"); |
|
254 reject(new aWindow.DOMError("SecurityError", "Access denied")); |
|
255 }); |
|
256 } |
|
257 }); |
|
258 }, |
|
259 |
|
260 getDataStoresInfo: function(aName, aAppId) { |
|
261 debug('GetDataStoresInfo'); |
|
262 |
|
263 let appsService = Cc["@mozilla.org/AppsService;1"] |
|
264 .getService(Ci.nsIAppsService); |
|
265 let app = appsService.getAppByLocalId(aAppId); |
|
266 if (!app) { |
|
267 return null; |
|
268 } |
|
269 |
|
270 let prefName = "dom.testing.datastore_enabled_for_hosted_apps"; |
|
271 if (app.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED && |
|
272 (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_INVALID || |
|
273 !Services.prefs.getBoolPref(prefName))) { |
|
274 return null; |
|
275 } |
|
276 |
|
277 let results = []; |
|
278 |
|
279 if (aName in this.stores) { |
|
280 if (aAppId in this.stores[aName]) { |
|
281 results.push({ name: aName, |
|
282 owner: this.stores[aName][aAppId].owner, |
|
283 readOnly: false, |
|
284 enabled: this.stores[aName][aAppId].enabled }); |
|
285 } |
|
286 |
|
287 for (var i in this.stores[aName]) { |
|
288 if (i == aAppId) { |
|
289 continue; |
|
290 } |
|
291 |
|
292 let access = this.getDataStoreAccess(aName, aAppId); |
|
293 if (!access) { |
|
294 continue; |
|
295 } |
|
296 |
|
297 let readOnly = this.stores[aName][i].readOnly || access.readOnly; |
|
298 results.push({ name: aName, |
|
299 owner: this.stores[aName][i].owner, |
|
300 readOnly: readOnly, |
|
301 enabled: this.stores[aName][i].enabled }); |
|
302 } |
|
303 } |
|
304 |
|
305 return results; |
|
306 }, |
|
307 |
|
308 getDataStoreCreate: function(aWindow, aResolve, aStores) { |
|
309 debug("GetDataStoreCreate"); |
|
310 |
|
311 let results = []; |
|
312 |
|
313 if (!aStores.length) { |
|
314 aResolve(results); |
|
315 return; |
|
316 } |
|
317 |
|
318 let pendingDataStores = []; |
|
319 |
|
320 for (let i = 0; i < aStores.length; ++i) { |
|
321 if (!aStores[i].enabled) { |
|
322 pendingDataStores.push(aStores[i].owner); |
|
323 } |
|
324 } |
|
325 |
|
326 if (!pendingDataStores.length) { |
|
327 this.getDataStoreResolve(aWindow, aResolve, aStores); |
|
328 return; |
|
329 } |
|
330 |
|
331 if (!(aStores[0].name in this.pendingRequests)) { |
|
332 this.pendingRequests[aStores[0].name] = []; |
|
333 } |
|
334 |
|
335 this.pendingRequests[aStores[0].name].push({ window: aWindow, |
|
336 resolve: aResolve, |
|
337 stores: aStores, |
|
338 pendingDataStores: pendingDataStores }); |
|
339 }, |
|
340 |
|
341 getDataStoreResolve: function(aWindow, aResolve, aStores) { |
|
342 debug("GetDataStoreResolve"); |
|
343 |
|
344 let callbackPending = aStores.length; |
|
345 let results = []; |
|
346 |
|
347 if (!callbackPending) { |
|
348 aResolve(results); |
|
349 return; |
|
350 } |
|
351 |
|
352 for (let i = 0; i < aStores.length; ++i) { |
|
353 let obj = new DataStore(aWindow, aStores[i].name, |
|
354 aStores[i].owner, aStores[i].readOnly); |
|
355 |
|
356 let storeImpl = aWindow.DataStoreImpl._create(aWindow, obj); |
|
357 |
|
358 let exposedStore = new aWindow.DataStore(); |
|
359 exposedStore.setDataStoreImpl(storeImpl); |
|
360 |
|
361 obj.exposedObject = exposedStore; |
|
362 |
|
363 results.push(exposedStore); |
|
364 |
|
365 obj.retrieveRevisionId( |
|
366 function() { |
|
367 --callbackPending; |
|
368 if (!callbackPending) { |
|
369 aResolve(results); |
|
370 } |
|
371 } |
|
372 ); |
|
373 } |
|
374 }, |
|
375 |
|
376 getDataStoreAccess: function(aName, aAppId) { |
|
377 if (!(aName in this.accessStores) || |
|
378 !(aAppId in this.accessStores[aName])) { |
|
379 return null; |
|
380 } |
|
381 |
|
382 return this.accessStores[aName][aAppId]; |
|
383 }, |
|
384 |
|
385 observe: function observe(aSubject, aTopic, aData) { |
|
386 debug('observe - aTopic: ' + aTopic); |
|
387 if (aTopic != 'webapps-clear-data') { |
|
388 return; |
|
389 } |
|
390 |
|
391 let params = |
|
392 aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams); |
|
393 |
|
394 // DataStore is explosed to apps, not browser content. |
|
395 if (params.browserOnly) { |
|
396 return; |
|
397 } |
|
398 |
|
399 function isEmpty(aMap) { |
|
400 for (var key in aMap) { |
|
401 if (aMap.hasOwnProperty(key)) { |
|
402 return false; |
|
403 } |
|
404 } |
|
405 return true; |
|
406 } |
|
407 |
|
408 for (let key in this.stores) { |
|
409 if (params.appId in this.stores[key]) { |
|
410 this.deleteDatabase(key, this.stores[key][params.appId].owner); |
|
411 delete this.stores[key][params.appId]; |
|
412 } |
|
413 |
|
414 if (isEmpty(this.stores[key])) { |
|
415 delete this.stores[key]; |
|
416 } |
|
417 } |
|
418 |
|
419 for (let key in this.accessStores) { |
|
420 if (params.appId in this.accessStores[key]) { |
|
421 delete this.accessStores[key][params.appId]; |
|
422 } |
|
423 |
|
424 if (isEmpty(this.accessStores[key])) { |
|
425 delete this.accessStores[key]; |
|
426 } |
|
427 } |
|
428 }, |
|
429 |
|
430 deleteDatabase: function(aName, aOwner) { |
|
431 debug("delete database: " + aName); |
|
432 |
|
433 let db = new DataStoreDB(); |
|
434 db.init(aOwner, aName); |
|
435 db.delete(); |
|
436 }, |
|
437 |
|
438 receiveMessage: function(aMsg) { |
|
439 debug("receiveMessage"); |
|
440 let data = aMsg.json; |
|
441 |
|
442 if (!(data.name in this.pendingRequests)) { |
|
443 return; |
|
444 } |
|
445 |
|
446 for (let i = 0; i < this.pendingRequests[data.name].length;) { |
|
447 let pos = this.pendingRequests[data.name][i].pendingDataStores.indexOf(data.owner); |
|
448 if (pos != -1) { |
|
449 this.pendingRequests[data.name][i].pendingDataStores.splice(pos, 1); |
|
450 if (!this.pendingRequests[data.name][i].pendingDataStores.length) { |
|
451 this.getDataStoreResolve(this.pendingRequests[data.name][i].window, |
|
452 this.pendingRequests[data.name][i].resolve, |
|
453 this.pendingRequests[data.name][i].stores); |
|
454 this.pendingRequests[data.name].splice(i, 1); |
|
455 continue; |
|
456 } |
|
457 } |
|
458 |
|
459 ++i; |
|
460 } |
|
461 |
|
462 if (!this.pendingRequests[data.name].length) { |
|
463 delete this.pendingRequests[data.name]; |
|
464 } |
|
465 }, |
|
466 |
|
467 classID : DATASTORESERVICE_CID, |
|
468 QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataStoreService, |
|
469 Ci.nsIObserver]), |
|
470 classInfo: XPCOMUtils.generateCI({ |
|
471 classID: DATASTORESERVICE_CID, |
|
472 contractID: '@mozilla.org/datastore-service;1', |
|
473 interfaces: [Ci.nsIDataStoreService, Ci.nsIObserver], |
|
474 flags: Ci.nsIClassInfo.SINGLETON |
|
475 }) |
|
476 }; |
|
477 |
|
478 /* DataStoreServiceChild */ |
|
479 |
|
480 function DataStoreServiceChild(aWindow, aName, aSuccessCb, aErrorCb) { |
|
481 debug("DataStoreServiceChild created"); |
|
482 this.init(aWindow, aName, aSuccessCb, aErrorCb); |
|
483 } |
|
484 |
|
485 DataStoreServiceChild.prototype = { |
|
486 __proto__: DOMRequestIpcHelper.prototype, |
|
487 |
|
488 init: function(aWindow, aName, aSuccessCb, aErrorCb) { |
|
489 debug("DataStoreServiceChild init"); |
|
490 this._successCb = aSuccessCb; |
|
491 this._errorCb = aErrorCb; |
|
492 |
|
493 this.initDOMRequestHelper(aWindow, [ "DataStore:Get:Return:OK", |
|
494 "DataStore:Get:Return:KO" ]); |
|
495 |
|
496 cpmm.sendAsyncMessage("DataStore:Get", |
|
497 { name: aName }, null, aWindow.document.nodePrincipal ); |
|
498 }, |
|
499 |
|
500 receiveMessage: function(aMessage) { |
|
501 debug("DataStoreServiceChild receiveMessage"); |
|
502 |
|
503 switch (aMessage.name) { |
|
504 case 'DataStore:Get:Return:OK': |
|
505 this.destroyDOMRequestHelper(); |
|
506 this._successCb(aMessage.data.stores); |
|
507 break; |
|
508 |
|
509 case 'DataStore:Get:Return:KO': |
|
510 this.destroyDOMRequestHelper(); |
|
511 this._errorCb(); |
|
512 break; |
|
513 } |
|
514 } |
|
515 } |
|
516 |
|
517 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataStoreService]); |