dom/datastore/DataStoreImpl.jsm

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

     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 this.EXPORTED_SYMBOLS = ["DataStore"];
    11 function debug(s) {
    12   //dump('DEBUG DataStore: ' + s + '\n');
    13 }
    15 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
    17 const REVISION_ADDED = "added";
    18 const REVISION_UPDATED = "updated";
    19 const REVISION_REMOVED = "removed";
    20 const REVISION_VOID = "void";
    22 // This value has to be tuned a bit. Currently it's just a guess
    23 // and yet we don't know if it's too low or too high.
    24 const MAX_REQUESTS = 25;
    26 Cu.import("resource://gre/modules/DataStoreCursorImpl.jsm");
    27 Cu.import("resource://gre/modules/DataStoreDB.jsm");
    28 Cu.import('resource://gre/modules/Services.jsm');
    29 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
    30 Cu.importGlobalProperties(["indexedDB"]);
    32 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
    33                                    "@mozilla.org/childprocessmessagemanager;1",
    34                                    "nsIMessageSender");
    36 /* Helper functions */
    37 function createDOMError(aWindow, aEvent) {
    38   return new aWindow.DOMError(aEvent);
    39 }
    41 function throwInvalidArg(aWindow) {
    42   return aWindow.Promise.reject(
    43     new aWindow.DOMError("SyntaxError", "Non-numeric or invalid id"));
    44 }
    46 function throwReadOnly(aWindow) {
    47   return aWindow.Promise.reject(
    48     new aWindow.DOMError("ReadOnlyError", "DataStore in readonly mode"));
    49 }
    51 function validateId(aId) {
    52   // If string, it cannot be empty.
    53   if (typeof(aId) == 'string') {
    54     return aId.length;
    55   }
    57   aId = parseInt(aId);
    58   return (!isNaN(aId) && aId > 0);
    59 }
    61 /* DataStore object */
    62 this.DataStore = function(aWindow, aName, aOwner, aReadOnly) {
    63   debug("DataStore created");
    64   this.init(aWindow, aName, aOwner, aReadOnly);
    65 }
    67 this.DataStore.prototype = {
    68   classDescription: "DataStore XPCOM Component",
    69   classID: Components.ID("{db5c9602-030f-4bff-a3de-881a8de370f2}"),
    70   contractID: "@mozilla.org/dom/datastore-impl;1",
    71   QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports,
    72                                          Components.interfaces.nsIObserver]),
    74   callbacks: [],
    76   _window: null,
    77   _name: null,
    78   _owner: null,
    79   _readOnly: null,
    80   _revisionId: null,
    81   _exposedObject: null,
    82   _cursor: null,
    83   _shuttingdown: false,
    84   _eventTarget: null,
    86   init: function(aWindow, aName, aOwner, aReadOnly) {
    87     debug("DataStore init");
    89     this._window = aWindow;
    90     this._name = aName;
    91     this._owner = aOwner;
    92     this._readOnly = aReadOnly;
    94     this._db = new DataStoreDB();
    95     this._db.init(aOwner, aName);
    97     Services.obs.addObserver(this, "inner-window-destroyed", false);
    99     let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   100                       .getInterface(Ci.nsIDOMWindowUtils);
   101     this._innerWindowID = util.currentInnerWindowID;
   103     cpmm.addMessageListener("DataStore:Changed:Return:OK", this);
   104     cpmm.sendAsyncMessage("DataStore:RegisterForMessages",
   105                           { store: this._name, owner: this._owner });
   106   },
   108   observe: function(aSubject, aTopic, aData) {
   109     let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
   110     if (wId == this._innerWindowID) {
   111       Services.obs.removeObserver(this, "inner-window-destroyed");
   113       cpmm.removeMessageListener("DataStore:Changed:Return:OK", this);
   114       cpmm.sendAsyncMessage("DataStore:UnregisterForMessages");
   115       this._shuttingdown = true;
   116       this._db.close();
   117     }
   118   },
   120   setEventTarget: function(aEventTarget) {
   121     this._eventTarget = aEventTarget;
   122   },
   124   newDBPromise: function(aTxnType, aFunction) {
   125     let self = this;
   126     return new this._window.Promise(function(aResolve, aReject) {
   127       debug("DBPromise started");
   128       self._db.txn(
   129         aTxnType,
   130         function(aTxn, aStore, aRevisionStore) {
   131           debug("DBPromise success");
   132           aFunction(aResolve, aReject, aTxn, aStore, aRevisionStore);
   133         },
   134         function(aEvent) {
   135           debug("DBPromise error");
   136           aReject(createDOMError(self._window, aEvent));
   137         }
   138       );
   139     });
   140   },
   142   checkRevision: function(aReject, aRevisionStore, aRevisionId, aCallback) {
   143     if (!aRevisionId) {
   144       aCallback();
   145       return;
   146     }
   148     let self = this;
   150     let request = aRevisionStore.openCursor(null, 'prev');
   151     request.onsuccess = function(aEvent) {
   152       let cursor = aEvent.target.result;
   153       if (!cursor) {
   154         dump("This cannot really happen.");
   155         return;
   156       }
   158       if (cursor.value.revisionId != aRevisionId) {
   159         aReject(new self._window.DOMError("ConstraintError",
   160                                           "RevisionId is not up-to-date"));
   161         return;
   162       }
   164       aCallback();
   165     }
   166   },
   168   getInternal: function(aStore, aIds, aCallback) {
   169     debug("GetInternal: " + aIds.toSource());
   171     // Creation of the results array.
   172     let results = new Array(aIds.length);
   174     // We're going to create this amount of requests.
   175     let pendingIds = aIds.length;
   176     let indexPos = 0;
   178     let self = this;
   180     function getInternalSuccess(aEvent, aPos) {
   181       debug("GetInternal success. Record: " + aEvent.target.result);
   182       results[aPos] = Cu.cloneInto(aEvent.target.result, self._window);
   183       if (!--pendingIds) {
   184         aCallback(results);
   185         return;
   186       }
   188       if (indexPos < aIds.length) {
   189         // Just MAX_REQUESTS requests at the same time.
   190         let count = 0;
   191         while (indexPos < aIds.length && ++count < MAX_REQUESTS) {
   192           getInternalRequest();
   193         }
   194       }
   195     }
   197     function getInternalRequest() {
   198       let currentPos = indexPos++;
   199       let request = aStore.get(aIds[currentPos]);
   200       request.onsuccess = function(aEvent) {
   201         getInternalSuccess(aEvent, currentPos);
   202       }
   203     }
   205     getInternalRequest();
   206   },
   208   putInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) {
   209     debug("putInternal " + aId);
   211     let self = this;
   212     let request = aStore.put(aObj, aId);
   213     request.onsuccess = function(aEvent) {
   214       debug("putInternal success");
   216       self.addRevision(aRevisionStore, aId, REVISION_UPDATED,
   217         function() {
   218           debug("putInternal - revisionId increased");
   219           // No wrap here because the result is always a int.
   220           aResolve(aEvent.target.result);
   221         }
   222       );
   223     };
   224   },
   226   addInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) {
   227     debug("AddInternal");
   229     let self = this;
   230     let request = aStore.add(aObj, aId);
   231     request.onsuccess = function(aEvent) {
   232       debug("Request successful. Id: " + aEvent.target.result);
   233       self.addRevision(aRevisionStore, aEvent.target.result, REVISION_ADDED,
   234         function() {
   235           debug("AddInternal - revisionId increased");
   236           // No wrap here because the result is always a int.
   237           aResolve(aEvent.target.result);
   238         }
   239       );
   240     };
   241   },
   243   removeInternal: function(aResolve, aStore, aRevisionStore, aId) {
   244     debug("RemoveInternal");
   246     let self = this;
   247     let request = aStore.get(aId);
   248     request.onsuccess = function(aEvent) {
   249       debug("RemoveInternal success. Record: " + aEvent.target.result);
   250       if (aEvent.target.result === undefined) {
   251         aResolve(false);
   252         return;
   253       }
   255       let deleteRequest = aStore.delete(aId);
   256       deleteRequest.onsuccess = function() {
   257         debug("RemoveInternal success");
   258         self.addRevision(aRevisionStore, aId, REVISION_REMOVED,
   259           function() {
   260             aResolve(true);
   261           }
   262         );
   263       };
   264     };
   265   },
   267   clearInternal: function(aResolve, aStore, aRevisionStore) {
   268     debug("ClearInternal");
   270     let self = this;
   271     let request = aStore.clear();
   272     request.onsuccess = function() {
   273       debug("ClearInternal success");
   274       self._db.clearRevisions(aRevisionStore,
   275         function() {
   276           debug("Revisions cleared");
   278           self.addRevision(aRevisionStore, 0, REVISION_VOID,
   279             function() {
   280               debug("ClearInternal - revisionId increased");
   281               aResolve();
   282             }
   283           );
   284         }
   285       );
   286     };
   287   },
   289   getLengthInternal: function(aResolve, aStore) {
   290     debug("GetLengthInternal");
   292     let request = aStore.count();
   293     request.onsuccess = function(aEvent) {
   294       debug("GetLengthInternal success: " + aEvent.target.result);
   295       // No wrap here because the result is always a int.
   296       aResolve(aEvent.target.result);
   297     };
   298   },
   300   addRevision: function(aRevisionStore, aId, aType, aSuccessCb) {
   301     let self = this;
   302     this._db.addRevision(aRevisionStore, aId, aType,
   303       function(aRevisionId) {
   304         self._revisionId = aRevisionId;
   305         self.sendNotification(aId, aType, aRevisionId);
   306         aSuccessCb();
   307       }
   308     );
   309   },
   311   retrieveRevisionId: function(aSuccessCb) {
   312     let self = this;
   313     this._db.revisionTxn(
   314       'readonly',
   315       function(aTxn, aRevisionStore) {
   316         debug("RetrieveRevisionId transaction success");
   318         let request = aRevisionStore.openCursor(null, 'prev');
   319         request.onsuccess = function(aEvent) {
   320           let cursor = aEvent.target.result;
   321           if (cursor) {
   322             self._revisionId = cursor.value.revisionId;
   323           }
   325           aSuccessCb(self._revisionId);
   326         };
   327       }
   328     );
   329   },
   331   sendNotification: function(aId, aOperation, aRevisionId) {
   332     debug("SendNotification");
   333     if (aOperation == REVISION_VOID) {
   334       aOperation = "cleared";
   335     }
   337     cpmm.sendAsyncMessage("DataStore:Changed",
   338                           { store: this.name, owner: this._owner,
   339                             message: { revisionId: aRevisionId, id: aId,
   340                                        operation: aOperation, owner: this._owner } } );
   341   },
   343   receiveMessage: function(aMessage) {
   344     debug("receiveMessage");
   346     if (aMessage.name != "DataStore:Changed:Return:OK") {
   347       debug("Wrong message: " + aMessage.name);
   348       return;
   349     }
   351     // If this message is not for this DataStore, let's ignore it.
   352     if (aMessage.data.owner != this._owner ||
   353         aMessage.data.store != this._name) {
   354       return;
   355     }
   357     let self = this;
   359     this.retrieveRevisionId(
   360       function() {
   361         // If the window has been destroyed we don't emit the events.
   362         if (self._shuttingdown) {
   363           return;
   364         }
   366         // If we have an active cursor we don't emit events.
   367         if (self._cursor) {
   368           return;
   369         }
   371         let event = new self._window.DataStoreChangeEvent('change',
   372                                                           aMessage.data.message);
   373         self._eventTarget.dispatchEvent(event);
   374       }
   375     );
   376   },
   378   get exposedObject() {
   379     debug("get exposedObject");
   380     return this._exposedObject;
   381   },
   383   set exposedObject(aObject) {
   384     debug("set exposedObject");
   385     this._exposedObject = aObject;
   386   },
   388   syncTerminated: function(aCursor) {
   389     // This checks is to avoid that an invalid cursor stops a sync.
   390     if (this._cursor == aCursor) {
   391       this._cursor = null;
   392     }
   393   },
   395   // Public interface :
   397   get name() {
   398     return this._name;
   399   },
   401   get owner() {
   402     return this._owner;
   403   },
   405   get readOnly() {
   406     return this._readOnly;
   407   },
   409   get: function() {
   410     let ids = Array.prototype.slice.call(arguments);
   411     for (let i = 0; i < ids.length; ++i) {
   412       if (!validateId(ids[i])) {
   413         return throwInvalidArg(this._window);
   414       }
   415     }
   417     let self = this;
   419     // Promise<Object>
   420     return this.newDBPromise("readonly",
   421       function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
   422                self.getInternal(aStore, ids,
   423                                 function(aResults) {
   424           aResolve(ids.length > 1 ? aResults : aResults[0]);
   425         });
   426       }
   427     );
   428   },
   430   put: function(aObj, aId, aRevisionId) {
   431     if (!validateId(aId)) {
   432       return throwInvalidArg(this._window);
   433     }
   435     if (this._readOnly) {
   436       return throwReadOnly(this._window);
   437     }
   439     let self = this;
   441     // Promise<void>
   442     return this.newDBPromise("readwrite",
   443       function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
   444         self.checkRevision(aReject, aRevisionStore, aRevisionId, function() {
   445           self.putInternal(aResolve, aStore, aRevisionStore, aObj, aId);
   446         });
   447       }
   448     );
   449   },
   451   add: function(aObj, aId, aRevisionId) {
   452     if (aId) {
   453       if (!validateId(aId)) {
   454         return throwInvalidArg(this._window);
   455       }
   456     }
   458     if (this._readOnly) {
   459       return throwReadOnly(this._window);
   460     }
   462     let self = this;
   464     // Promise<int>
   465     return this.newDBPromise("readwrite",
   466       function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
   467         self.checkRevision(aReject, aRevisionStore, aRevisionId, function() {
   468           self.addInternal(aResolve, aStore, aRevisionStore, aObj, aId);
   469         });
   470       }
   471     );
   472   },
   474   remove: function(aId, aRevisionId) {
   475     if (!validateId(aId)) {
   476       return throwInvalidArg(this._window);
   477     }
   479     if (this._readOnly) {
   480       return throwReadOnly(this._window);
   481     }
   483     let self = this;
   485     // Promise<void>
   486     return this.newDBPromise("readwrite",
   487       function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
   488         self.checkRevision(aReject, aRevisionStore, aRevisionId, function() {
   489           self.removeInternal(aResolve, aStore, aRevisionStore, aId);
   490         });
   491       }
   492     );
   493   },
   495   clear: function(aRevisionId) {
   496     if (this._readOnly) {
   497       return throwReadOnly(this._window);
   498     }
   500     let self = this;
   502     // Promise<void>
   503     return this.newDBPromise("readwrite",
   504       function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
   505         self.checkRevision(aReject, aRevisionStore, aRevisionId, function() {
   506           self.clearInternal(aResolve, aStore, aRevisionStore);
   507         });
   508       }
   509     );
   510   },
   512   get revisionId() {
   513     return this._revisionId;
   514   },
   516   getLength: function() {
   517     let self = this;
   519     // Promise<int>
   520     return this.newDBPromise("readonly",
   521       function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
   522         self.getLengthInternal(aResolve, aStore);
   523       }
   524     );
   525   },
   527   sync: function(aRevisionId) {
   528     debug("Sync");
   529     this._cursor = new DataStoreCursor(this._window, this, aRevisionId);
   531     let cursorImpl = this._window.DataStoreCursorImpl.
   532                                   _create(this._window, this._cursor);
   534     let exposedCursor = new this._window.DataStoreCursor();
   535     exposedCursor.setDataStoreCursorImpl(cursorImpl);
   536     return exposedCursor;
   537   }
   538 };

mercurial