Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
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/. */
7 'use strict'
9 /* static functions */
11 function debug(s) {
12 //dump('DEBUG DataStoreService: ' + s + '\n');
13 }
15 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
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");
23 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
24 "@mozilla.org/childprocessmessagemanager;1",
25 "nsIMessageSender");
27 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
28 "@mozilla.org/parentprocessmessagemanager;1",
29 "nsIMessageBroadcaster");
31 XPCOMUtils.defineLazyServiceGetter(this, "permissionManager",
32 "@mozilla.org/permissionmanager;1",
33 "nsIPermissionManager");
35 XPCOMUtils.defineLazyServiceGetter(this, "secMan",
36 "@mozilla.org/scriptsecuritymanager;1",
37 "nsIScriptSecurityManager");
39 /* DataStoreService */
41 const DATASTORESERVICE_CID = Components.ID('{d193d0e2-c677-4a7b-bb0a-19155b470f2e}');
42 const REVISION_VOID = "void";
44 function DataStoreService() {
45 debug('DataStoreService Constructor');
47 this.inParent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
48 .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
50 if (this.inParent) {
51 let obs = Services.obs;
52 if (!obs) {
53 debug("DataStore Error: observer-service is null!");
54 return;
55 }
57 obs.addObserver(this, 'webapps-clear-data', false);
58 }
60 let self = this;
61 cpmm.addMessageListener("datastore-first-revision-created",
62 function(aMsg) { self.receiveMessage(aMsg); });
63 }
65 DataStoreService.prototype = {
66 inParent: false,
68 // Hash of DataStores
69 stores: {},
70 accessStores: {},
71 pendingRequests: {},
73 installDataStore: function(aAppId, aName, aOrigin, aOwner, aReadOnly) {
74 debug('installDataStore - appId: ' + aAppId + ', aName: ' +
75 aName + ', aOrigin: ' + aOrigin + ', aOwner:' + aOwner +
76 ', aReadOnly: ' + aReadOnly);
78 this.checkIfInParent();
80 if (aName in this.stores && aAppId in this.stores[aName]) {
81 debug('This should not happen');
82 return;
83 }
85 if (!(aName in this.stores)) {
86 this.stores[aName] = {};
87 }
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 };
93 this.addPermissions(aAppId, aName, aOrigin, aOwner, aReadOnly);
95 this.createFirstRevisionId(aAppId, aName, aOwner);
96 },
98 installAccessDataStore: function(aAppId, aName, aOrigin, aOwner, aReadOnly) {
99 debug('installAccessDataStore - appId: ' + aAppId + ', aName: ' +
100 aName + ', aOrigin: ' + aOrigin + ', aOwner:' + aOwner +
101 ', aReadOnly: ' + aReadOnly);
103 this.checkIfInParent();
105 if (aName in this.accessStores && aAppId in this.accessStores[aName]) {
106 debug('This should not happen');
107 return;
108 }
110 if (!(aName in this.accessStores)) {
111 this.accessStores[aName] = {};
112 }
114 this.accessStores[aName][aAppId] = { origin: aOrigin, owner: aOwner,
115 readOnly: aReadOnly };
116 this.addAccessPermissions(aAppId, aName, aOrigin, aOwner, aReadOnly);
117 },
119 checkIfInParent: function() {
120 if (!this.inParent) {
121 throw "DataStore can execute this operation just in the parent process";
122 }
123 },
125 createFirstRevisionId: function(aAppId, aName, aOwner) {
126 debug("createFirstRevisionId database: " + aName);
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");
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 },
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 },
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);
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 },
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 }
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 },
196 resetPermissions: function(aAppId, aOrigin, aOwner, aPermission, aReadOnly) {
197 debug("ResetPermissions - appId: " + aAppId + " - origin: " + aOrigin +
198 " - owner: " + aOwner + " - permissions: " + aPermission +
199 " - readOnly: " + aReadOnly);
201 let uri = Services.io.newURI(aOrigin, null, null);
202 let principal = secMan.getAppCodebasePrincipal(uri, aAppId, false);
204 let result = permissionManager.testExactPermissionFromPrincipal(principal,
205 aPermission + '-write');
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 }
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 }
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 },
232 getDataStores: function(aWindow, aName) {
233 debug('getDataStores - aName: ' + aName);
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 },
260 getDataStoresInfo: function(aName, aAppId) {
261 debug('GetDataStoresInfo');
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 }
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 }
277 let results = [];
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 }
287 for (var i in this.stores[aName]) {
288 if (i == aAppId) {
289 continue;
290 }
292 let access = this.getDataStoreAccess(aName, aAppId);
293 if (!access) {
294 continue;
295 }
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 }
305 return results;
306 },
308 getDataStoreCreate: function(aWindow, aResolve, aStores) {
309 debug("GetDataStoreCreate");
311 let results = [];
313 if (!aStores.length) {
314 aResolve(results);
315 return;
316 }
318 let pendingDataStores = [];
320 for (let i = 0; i < aStores.length; ++i) {
321 if (!aStores[i].enabled) {
322 pendingDataStores.push(aStores[i].owner);
323 }
324 }
326 if (!pendingDataStores.length) {
327 this.getDataStoreResolve(aWindow, aResolve, aStores);
328 return;
329 }
331 if (!(aStores[0].name in this.pendingRequests)) {
332 this.pendingRequests[aStores[0].name] = [];
333 }
335 this.pendingRequests[aStores[0].name].push({ window: aWindow,
336 resolve: aResolve,
337 stores: aStores,
338 pendingDataStores: pendingDataStores });
339 },
341 getDataStoreResolve: function(aWindow, aResolve, aStores) {
342 debug("GetDataStoreResolve");
344 let callbackPending = aStores.length;
345 let results = [];
347 if (!callbackPending) {
348 aResolve(results);
349 return;
350 }
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);
356 let storeImpl = aWindow.DataStoreImpl._create(aWindow, obj);
358 let exposedStore = new aWindow.DataStore();
359 exposedStore.setDataStoreImpl(storeImpl);
361 obj.exposedObject = exposedStore;
363 results.push(exposedStore);
365 obj.retrieveRevisionId(
366 function() {
367 --callbackPending;
368 if (!callbackPending) {
369 aResolve(results);
370 }
371 }
372 );
373 }
374 },
376 getDataStoreAccess: function(aName, aAppId) {
377 if (!(aName in this.accessStores) ||
378 !(aAppId in this.accessStores[aName])) {
379 return null;
380 }
382 return this.accessStores[aName][aAppId];
383 },
385 observe: function observe(aSubject, aTopic, aData) {
386 debug('observe - aTopic: ' + aTopic);
387 if (aTopic != 'webapps-clear-data') {
388 return;
389 }
391 let params =
392 aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
394 // DataStore is explosed to apps, not browser content.
395 if (params.browserOnly) {
396 return;
397 }
399 function isEmpty(aMap) {
400 for (var key in aMap) {
401 if (aMap.hasOwnProperty(key)) {
402 return false;
403 }
404 }
405 return true;
406 }
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 }
414 if (isEmpty(this.stores[key])) {
415 delete this.stores[key];
416 }
417 }
419 for (let key in this.accessStores) {
420 if (params.appId in this.accessStores[key]) {
421 delete this.accessStores[key][params.appId];
422 }
424 if (isEmpty(this.accessStores[key])) {
425 delete this.accessStores[key];
426 }
427 }
428 },
430 deleteDatabase: function(aName, aOwner) {
431 debug("delete database: " + aName);
433 let db = new DataStoreDB();
434 db.init(aOwner, aName);
435 db.delete();
436 },
438 receiveMessage: function(aMsg) {
439 debug("receiveMessage");
440 let data = aMsg.json;
442 if (!(data.name in this.pendingRequests)) {
443 return;
444 }
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 }
459 ++i;
460 }
462 if (!this.pendingRequests[data.name].length) {
463 delete this.pendingRequests[data.name];
464 }
465 },
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 };
478 /* DataStoreServiceChild */
480 function DataStoreServiceChild(aWindow, aName, aSuccessCb, aErrorCb) {
481 debug("DataStoreServiceChild created");
482 this.init(aWindow, aName, aSuccessCb, aErrorCb);
483 }
485 DataStoreServiceChild.prototype = {
486 __proto__: DOMRequestIpcHelper.prototype,
488 init: function(aWindow, aName, aSuccessCb, aErrorCb) {
489 debug("DataStoreServiceChild init");
490 this._successCb = aSuccessCb;
491 this._errorCb = aErrorCb;
493 this.initDOMRequestHelper(aWindow, [ "DataStore:Get:Return:OK",
494 "DataStore:Get:Return:KO" ]);
496 cpmm.sendAsyncMessage("DataStore:Get",
497 { name: aName }, null, aWindow.document.nodePrincipal );
498 },
500 receiveMessage: function(aMessage) {
501 debug("DataStoreServiceChild receiveMessage");
503 switch (aMessage.name) {
504 case 'DataStore:Get:Return:OK':
505 this.destroyDOMRequestHelper();
506 this._successCb(aMessage.data.stores);
507 break;
509 case 'DataStore:Get:Return:KO':
510 this.destroyDOMRequestHelper();
511 this._errorCb();
512 break;
513 }
514 }
515 }
517 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataStoreService]);