toolkit/devtools/server/actors/storage.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

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.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 const {Cu, Cc, Ci} = require("chrome");
michael@0 8 const events = require("sdk/event/core");
michael@0 9 const protocol = require("devtools/server/protocol");
michael@0 10 const {async} = require("devtools/async-utils");
michael@0 11 const {Arg, Option, method, RetVal, types} = protocol;
michael@0 12 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
michael@0 13
michael@0 14 Cu.import("resource://gre/modules/Promise.jsm");
michael@0 15 Cu.import("resource://gre/modules/Services.jsm");
michael@0 16 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 17 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
michael@0 18
michael@0 19 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
michael@0 20 "resource://gre/modules/Sqlite.jsm");
michael@0 21
michael@0 22 XPCOMUtils.defineLazyModuleGetter(this, "OS",
michael@0 23 "resource://gre/modules/osfile.jsm");
michael@0 24
michael@0 25 exports.register = function(handle) {
michael@0 26 handle.addTabActor(StorageActor, "storageActor");
michael@0 27 };
michael@0 28
michael@0 29 exports.unregister = function(handle) {
michael@0 30 handle.removeTabActor(StorageActor);
michael@0 31 };
michael@0 32
michael@0 33 // Global required for window less Indexed DB instantiation.
michael@0 34 let global = this;
michael@0 35
michael@0 36 // Maximum number of cookies/local storage key-value-pairs that can be sent
michael@0 37 // over the wire to the client in one request.
michael@0 38 const MAX_STORE_OBJECT_COUNT = 30;
michael@0 39 // Interval for the batch job that sends the accumilated update packets to the
michael@0 40 // client.
michael@0 41 const UPDATE_INTERVAL = 500; // ms
michael@0 42
michael@0 43 // A RegExp for characters that cannot appear in a file/directory name. This is
michael@0 44 // used to sanitize the host name for indexed db to lookup whether the file is
michael@0 45 // present in <profileDir>/storage/persistent/ location
michael@0 46 let illegalFileNameCharacters = [
michael@0 47 "[",
michael@0 48 "\\x00-\\x25", // Control characters \001 to \037
michael@0 49 "/:*?\\\"<>|\\\\", // Special characters
michael@0 50 "]"
michael@0 51 ].join("");
michael@0 52 let ILLEGAL_CHAR_REGEX = new RegExp(illegalFileNameCharacters, "g");
michael@0 53
michael@0 54 // Holder for all the registered storage actors.
michael@0 55 let storageTypePool = new Map();
michael@0 56
michael@0 57 /**
michael@0 58 * Gets an accumulated list of all storage actors registered to be used to
michael@0 59 * create a RetVal to define the return type of StorageActor.listStores method.
michael@0 60 */
michael@0 61 function getRegisteredTypes() {
michael@0 62 let registeredTypes = {};
michael@0 63 for (let store of storageTypePool.keys()) {
michael@0 64 registeredTypes[store] = store;
michael@0 65 }
michael@0 66 return registeredTypes;
michael@0 67 }
michael@0 68
michael@0 69 /**
michael@0 70 * An async method equivalent to setTimeout but using Promises
michael@0 71 *
michael@0 72 * @param {number} time
michael@0 73 * The wait Ttme in milliseconds.
michael@0 74 */
michael@0 75 function sleep(time) {
michael@0 76 let wait = Promise.defer();
michael@0 77 let updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
michael@0 78 updateTimer.initWithCallback({
michael@0 79 notify: function() {
michael@0 80 updateTimer.cancel();
michael@0 81 updateTimer = null;
michael@0 82 wait.resolve(null);
michael@0 83 }
michael@0 84 } , time, Ci.nsITimer.TYPE_ONE_SHOT);
michael@0 85 return wait.promise;
michael@0 86 }
michael@0 87
michael@0 88 // Cookies store object
michael@0 89 types.addDictType("cookieobject", {
michael@0 90 name: "string",
michael@0 91 value: "longstring",
michael@0 92 path: "nullable:string",
michael@0 93 host: "string",
michael@0 94 isDomain: "boolean",
michael@0 95 isSecure: "boolean",
michael@0 96 isHttpOnly: "boolean",
michael@0 97 creationTime: "number",
michael@0 98 lastAccessed: "number",
michael@0 99 expires: "number"
michael@0 100 });
michael@0 101
michael@0 102 // Array of cookie store objects
michael@0 103 types.addDictType("cookiestoreobject", {
michael@0 104 total: "number",
michael@0 105 offset: "number",
michael@0 106 data: "array:nullable:cookieobject"
michael@0 107 });
michael@0 108
michael@0 109 // Local Storage / Session Storage store object
michael@0 110 types.addDictType("storageobject", {
michael@0 111 name: "string",
michael@0 112 value: "longstring"
michael@0 113 });
michael@0 114
michael@0 115 // Array of Local Storage / Session Storage store objects
michael@0 116 types.addDictType("storagestoreobject", {
michael@0 117 total: "number",
michael@0 118 offset: "number",
michael@0 119 data: "array:nullable:storageobject"
michael@0 120 });
michael@0 121
michael@0 122 // Indexed DB store object
michael@0 123 // This is a union on idb object, db metadata object and object store metadata
michael@0 124 // object
michael@0 125 types.addDictType("idbobject", {
michael@0 126 name: "nullable:string",
michael@0 127 db: "nullable:string",
michael@0 128 objectStore: "nullable:string",
michael@0 129 origin: "nullable:string",
michael@0 130 version: "nullable:number",
michael@0 131 objectStores: "nullable:number",
michael@0 132 keyPath: "nullable:string",
michael@0 133 autoIncrement: "nullable:boolean",
michael@0 134 indexes: "nullable:string",
michael@0 135 value: "nullable:longstring"
michael@0 136 });
michael@0 137
michael@0 138 // Array of Indexed DB store objects
michael@0 139 types.addDictType("idbstoreobject", {
michael@0 140 total: "number",
michael@0 141 offset: "number",
michael@0 142 data: "array:nullable:idbobject"
michael@0 143 });
michael@0 144
michael@0 145 // Update notification object
michael@0 146 types.addDictType("storeUpdateObject", {
michael@0 147 changed: "nullable:json",
michael@0 148 deleted: "nullable:json",
michael@0 149 added: "nullable:json"
michael@0 150 });
michael@0 151
michael@0 152 // Helper methods to create a storage actor.
michael@0 153 let StorageActors = {};
michael@0 154
michael@0 155 /**
michael@0 156 * Creates a default object with the common methods required by all storage
michael@0 157 * actors.
michael@0 158 *
michael@0 159 * This default object is missing a couple of required methods that should be
michael@0 160 * implemented seperately for each actor. They are namely:
michael@0 161 * - observe : Method which gets triggered on the notificaiton of the watched
michael@0 162 * topic.
michael@0 163 * - getNamesForHost : Given a host, get list of all known store names.
michael@0 164 * - getValuesForHost : Given a host (and optianally a name) get all known
michael@0 165 * store objects.
michael@0 166 * - toStoreObject : Given a store object, convert it to the required format
michael@0 167 * so that it can be transferred over wire.
michael@0 168 * - populateStoresForHost : Given a host, populate the map of all store
michael@0 169 * objects for it
michael@0 170 *
michael@0 171 * @param {string} typeName
michael@0 172 * The typeName of the actor.
michael@0 173 * @param {string} observationTopic
michael@0 174 * The topic which this actor listens to via Notification Observers.
michael@0 175 * @param {string} storeObjectType
michael@0 176 * The RetVal type of the store object of this actor.
michael@0 177 */
michael@0 178 StorageActors.defaults = function(typeName, observationTopic, storeObjectType) {
michael@0 179 return {
michael@0 180 typeName: typeName,
michael@0 181
michael@0 182 get conn() {
michael@0 183 return this.storageActor.conn;
michael@0 184 },
michael@0 185
michael@0 186 /**
michael@0 187 * Returns a list of currently knwon hosts for the target window. This list
michael@0 188 * contains unique hosts from the window + all inner windows.
michael@0 189 */
michael@0 190 get hosts() {
michael@0 191 let hosts = new Set();
michael@0 192 for (let {location} of this.storageActor.windows) {
michael@0 193 hosts.add(this.getHostName(location));
michael@0 194 }
michael@0 195 return hosts;
michael@0 196 },
michael@0 197
michael@0 198 /**
michael@0 199 * Returns all the windows present on the page. Includes main window + inner
michael@0 200 * iframe windows.
michael@0 201 */
michael@0 202 get windows() {
michael@0 203 return this.storageActor.windows;
michael@0 204 },
michael@0 205
michael@0 206 /**
michael@0 207 * Converts the window.location object into host.
michael@0 208 */
michael@0 209 getHostName: function(location) {
michael@0 210 return location.hostname || location.href;
michael@0 211 },
michael@0 212
michael@0 213 initialize: function(storageActor) {
michael@0 214 protocol.Actor.prototype.initialize.call(this, null);
michael@0 215
michael@0 216 this.storageActor = storageActor;
michael@0 217
michael@0 218 this.populateStoresForHosts();
michael@0 219 if (observationTopic) {
michael@0 220 Services.obs.addObserver(this, observationTopic, false);
michael@0 221 }
michael@0 222 this.onWindowReady = this.onWindowReady.bind(this);
michael@0 223 this.onWindowDestroyed = this.onWindowDestroyed.bind(this);
michael@0 224 events.on(this.storageActor, "window-ready", this.onWindowReady);
michael@0 225 events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed);
michael@0 226 },
michael@0 227
michael@0 228 destroy: function() {
michael@0 229 this.hostVsStores = null;
michael@0 230 if (observationTopic) {
michael@0 231 Services.obs.removeObserver(this, observationTopic, false);
michael@0 232 }
michael@0 233 events.off(this.storageActor, "window-ready", this.onWindowReady);
michael@0 234 events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed);
michael@0 235 },
michael@0 236
michael@0 237 getNamesForHost: function(host) {
michael@0 238 return [...this.hostVsStores.get(host).keys()];
michael@0 239 },
michael@0 240
michael@0 241 getValuesForHost: function(host, name) {
michael@0 242 if (name) {
michael@0 243 return [this.hostVsStores.get(host).get(name)];
michael@0 244 }
michael@0 245 return [...this.hostVsStores.get(host).values()];
michael@0 246 },
michael@0 247
michael@0 248 getObjectsSize: function(host, names) {
michael@0 249 return names.length;
michael@0 250 },
michael@0 251
michael@0 252 /**
michael@0 253 * When a new window is added to the page. This generally means that a new
michael@0 254 * iframe is created, or the current window is completely reloaded.
michael@0 255 *
michael@0 256 * @param {window} window
michael@0 257 * The window which was added.
michael@0 258 */
michael@0 259 onWindowReady: async(function*(window) {
michael@0 260 let host = this.getHostName(window.location);
michael@0 261 if (!this.hostVsStores.has(host)) {
michael@0 262 yield this.populateStoresForHost(host, window);
michael@0 263 let data = {};
michael@0 264 data[host] = this.getNamesForHost(host);
michael@0 265 this.storageActor.update("added", typeName, data);
michael@0 266 }
michael@0 267 }),
michael@0 268
michael@0 269 /**
michael@0 270 * When a window is removed from the page. This generally means that an
michael@0 271 * iframe was removed, or the current window reload is triggered.
michael@0 272 *
michael@0 273 * @param {window} window
michael@0 274 * The window which was removed.
michael@0 275 */
michael@0 276 onWindowDestroyed: function(window) {
michael@0 277 let host = this.getHostName(window.location);
michael@0 278 if (!this.hosts.has(host)) {
michael@0 279 this.hostVsStores.delete(host);
michael@0 280 let data = {};
michael@0 281 data[host] = [];
michael@0 282 this.storageActor.update("deleted", typeName, data);
michael@0 283 }
michael@0 284 },
michael@0 285
michael@0 286 form: function(form, detail) {
michael@0 287 if (detail === "actorid") {
michael@0 288 return this.actorID;
michael@0 289 }
michael@0 290
michael@0 291 let hosts = {};
michael@0 292 for (let host of this.hosts) {
michael@0 293 hosts[host] = [];
michael@0 294 }
michael@0 295
michael@0 296 return {
michael@0 297 actor: this.actorID,
michael@0 298 hosts: hosts
michael@0 299 };
michael@0 300 },
michael@0 301
michael@0 302 /**
michael@0 303 * Populates a map of known hosts vs a map of stores vs value.
michael@0 304 */
michael@0 305 populateStoresForHosts: function() {
michael@0 306 this.hostVsStores = new Map();
michael@0 307 for (let host of this.hosts) {
michael@0 308 this.populateStoresForHost(host);
michael@0 309 }
michael@0 310 },
michael@0 311
michael@0 312 /**
michael@0 313 * Returns a list of requested store objects. Maximum values returned are
michael@0 314 * MAX_STORE_OBJECT_COUNT. This method returns paginated values whose
michael@0 315 * starting index and total size can be controlled via the options object
michael@0 316 *
michael@0 317 * @param {string} host
michael@0 318 * The host name for which the store values are required.
michael@0 319 * @param {array:string} names
michael@0 320 * Array containing the names of required store objects. Empty if all
michael@0 321 * items are required.
michael@0 322 * @param {object} options
michael@0 323 * Additional options for the request containing following properties:
michael@0 324 * - offset {number} : The begin index of the returned array amongst
michael@0 325 * the total values
michael@0 326 * - size {number} : The number of values required.
michael@0 327 * - sortOn {string} : The values should be sorted on this property.
michael@0 328 * - index {string} : In case of indexed db, the IDBIndex to be used
michael@0 329 * for fetching the values.
michael@0 330 *
michael@0 331 * @return {object} An object containing following properties:
michael@0 332 * - offset - The actual offset of the returned array. This might
michael@0 333 * be different from the requested offset if that was
michael@0 334 * invalid
michael@0 335 * - total - The total number of entries possible.
michael@0 336 * - data - The requested values.
michael@0 337 */
michael@0 338 getStoreObjects: method(async(function*(host, names, options = {}) {
michael@0 339 let offset = options.offset || 0;
michael@0 340 let size = options.size || MAX_STORE_OBJECT_COUNT;
michael@0 341 if (size > MAX_STORE_OBJECT_COUNT) {
michael@0 342 size = MAX_STORE_OBJECT_COUNT;
michael@0 343 }
michael@0 344 let sortOn = options.sortOn || "name";
michael@0 345
michael@0 346 let toReturn = {
michael@0 347 offset: offset,
michael@0 348 total: 0,
michael@0 349 data: []
michael@0 350 };
michael@0 351
michael@0 352 if (names) {
michael@0 353 for (let name of names) {
michael@0 354 toReturn.data.push(
michael@0 355 // yield because getValuesForHost is async for Indexed DB
michael@0 356 ...(yield this.getValuesForHost(host, name, options))
michael@0 357 );
michael@0 358 }
michael@0 359 toReturn.total = this.getObjectsSize(host, names, options);
michael@0 360 if (offset > toReturn.total) {
michael@0 361 // In this case, toReturn.data is an empty array.
michael@0 362 toReturn.offset = toReturn.total;
michael@0 363 toReturn.data = [];
michael@0 364 }
michael@0 365 else {
michael@0 366 toReturn.data = toReturn.data.sort((a,b) => {
michael@0 367 return a[sortOn] - b[sortOn];
michael@0 368 }).slice(offset, offset + size).map(a => this.toStoreObject(a));
michael@0 369 }
michael@0 370 }
michael@0 371 else {
michael@0 372 let total = yield this.getValuesForHost(host);
michael@0 373 toReturn.total = total.length;
michael@0 374 if (offset > toReturn.total) {
michael@0 375 // In this case, toReturn.data is an empty array.
michael@0 376 toReturn.offset = offset = toReturn.total;
michael@0 377 toReturn.data = [];
michael@0 378 }
michael@0 379 else {
michael@0 380 toReturn.data = total.sort((a,b) => {
michael@0 381 return a[sortOn] - b[sortOn];
michael@0 382 }).slice(offset, offset + size)
michael@0 383 .map(object => this.toStoreObject(object));
michael@0 384 }
michael@0 385 }
michael@0 386
michael@0 387 return toReturn;
michael@0 388 }), {
michael@0 389 request: {
michael@0 390 host: Arg(0),
michael@0 391 names: Arg(1, "nullable:array:string"),
michael@0 392 options: Arg(2, "nullable:json")
michael@0 393 },
michael@0 394 response: RetVal(storeObjectType)
michael@0 395 })
michael@0 396 }
michael@0 397 };
michael@0 398
michael@0 399 /**
michael@0 400 * Creates an actor and its corresponding front and registers it to the Storage
michael@0 401 * Actor.
michael@0 402 *
michael@0 403 * @See StorageActors.defaults()
michael@0 404 *
michael@0 405 * @param {object} options
michael@0 406 * Options required by StorageActors.defaults method which are :
michael@0 407 * - typeName {string}
michael@0 408 * The typeName of the actor.
michael@0 409 * - observationTopic {string}
michael@0 410 * The topic which this actor listens to via
michael@0 411 * Notification Observers.
michael@0 412 * - storeObjectType {string}
michael@0 413 * The RetVal type of the store object of this actor.
michael@0 414 * @param {object} overrides
michael@0 415 * All the methods which you want to be differnt from the ones in
michael@0 416 * StorageActors.defaults method plus the required ones described there.
michael@0 417 */
michael@0 418 StorageActors.createActor = function(options = {}, overrides = {}) {
michael@0 419 let actorObject = StorageActors.defaults(
michael@0 420 options.typeName,
michael@0 421 options.observationTopic || null,
michael@0 422 options.storeObjectType
michael@0 423 );
michael@0 424 for (let key in overrides) {
michael@0 425 actorObject[key] = overrides[key];
michael@0 426 }
michael@0 427
michael@0 428 let actor = protocol.ActorClass(actorObject);
michael@0 429 let front = protocol.FrontClass(actor, {
michael@0 430 form: function(form, detail) {
michael@0 431 if (detail === "actorid") {
michael@0 432 this.actorID = form;
michael@0 433 return null;
michael@0 434 }
michael@0 435
michael@0 436 this.actorID = form.actor;
michael@0 437 this.hosts = form.hosts;
michael@0 438 return null;
michael@0 439 }
michael@0 440 });
michael@0 441 storageTypePool.set(actorObject.typeName, actor);
michael@0 442 }
michael@0 443
michael@0 444 /**
michael@0 445 * The Cookies actor and front.
michael@0 446 */
michael@0 447 StorageActors.createActor({
michael@0 448 typeName: "cookies",
michael@0 449 storeObjectType: "cookiestoreobject"
michael@0 450 }, {
michael@0 451 initialize: function(storageActor) {
michael@0 452 protocol.Actor.prototype.initialize.call(this, null);
michael@0 453
michael@0 454 this.storageActor = storageActor;
michael@0 455
michael@0 456 this.populateStoresForHosts();
michael@0 457 Services.obs.addObserver(this, "cookie-changed", false);
michael@0 458 Services.obs.addObserver(this, "http-on-response-set-cookie", false);
michael@0 459 this.onWindowReady = this.onWindowReady.bind(this);
michael@0 460 this.onWindowDestroyed = this.onWindowDestroyed.bind(this);
michael@0 461 events.on(this.storageActor, "window-ready", this.onWindowReady);
michael@0 462 events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed);
michael@0 463 },
michael@0 464
michael@0 465 destroy: function() {
michael@0 466 this.hostVsStores = null;
michael@0 467 Services.obs.removeObserver(this, "cookie-changed", false);
michael@0 468 Services.obs.removeObserver(this, "http-on-response-set-cookie", false);
michael@0 469 events.off(this.storageActor, "window-ready", this.onWindowReady);
michael@0 470 events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed);
michael@0 471 },
michael@0 472
michael@0 473 /**
michael@0 474 * Given a cookie object, figure out all the matching hosts from the page that
michael@0 475 * the cookie belong to.
michael@0 476 */
michael@0 477 getMatchingHosts: function(cookies) {
michael@0 478 if (!cookies.length) {
michael@0 479 cookies = [cookies];
michael@0 480 }
michael@0 481 let hosts = new Set();
michael@0 482 for (let host of this.hosts) {
michael@0 483 for (let cookie of cookies) {
michael@0 484 if (this.isCookieAtHost(cookie, host)) {
michael@0 485 hosts.add(host);
michael@0 486 }
michael@0 487 }
michael@0 488 }
michael@0 489 return [...hosts];
michael@0 490 },
michael@0 491
michael@0 492 /**
michael@0 493 * Given a cookie object and a host, figure out if the cookie is valid for
michael@0 494 * that host.
michael@0 495 */
michael@0 496 isCookieAtHost: function(cookie, host) {
michael@0 497 try {
michael@0 498 cookie = cookie.QueryInterface(Ci.nsICookie)
michael@0 499 .QueryInterface(Ci.nsICookie2);
michael@0 500 } catch(ex) {
michael@0 501 return false;
michael@0 502 }
michael@0 503 if (cookie.host == null) {
michael@0 504 return host == null;
michael@0 505 }
michael@0 506 if (cookie.host.startsWith(".")) {
michael@0 507 return host.endsWith(cookie.host);
michael@0 508 }
michael@0 509 else {
michael@0 510 return cookie.host == host;
michael@0 511 }
michael@0 512 },
michael@0 513
michael@0 514 toStoreObject: function(cookie) {
michael@0 515 if (!cookie) {
michael@0 516 return null;
michael@0 517 }
michael@0 518
michael@0 519 return {
michael@0 520 name: cookie.name,
michael@0 521 path: cookie.path || "",
michael@0 522 host: cookie.host || "",
michael@0 523 expires: (cookie.expires || 0) * 1000, // because expires is in seconds
michael@0 524 creationTime: cookie.creationTime / 1000, // because it is in micro seconds
michael@0 525 lastAccessed: cookie.lastAccessed / 1000, // - do -
michael@0 526 value: new LongStringActor(this.conn, cookie.value || ""),
michael@0 527 isDomain: cookie.isDomain,
michael@0 528 isSecure: cookie.isSecure,
michael@0 529 isHttpOnly: cookie.isHttpOnly
michael@0 530 }
michael@0 531 },
michael@0 532
michael@0 533 populateStoresForHost: function(host) {
michael@0 534 this.hostVsStores.set(host, new Map());
michael@0 535 let cookies = Services.cookies.getCookiesFromHost(host);
michael@0 536 while (cookies.hasMoreElements()) {
michael@0 537 let cookie = cookies.getNext().QueryInterface(Ci.nsICookie)
michael@0 538 .QueryInterface(Ci.nsICookie2);
michael@0 539 if (this.isCookieAtHost(cookie, host)) {
michael@0 540 this.hostVsStores.get(host).set(cookie.name, cookie);
michael@0 541 }
michael@0 542 }
michael@0 543 },
michael@0 544
michael@0 545 /**
michael@0 546 * Converts the raw cookie string returned in http request's response header
michael@0 547 * to a nsICookie compatible object.
michael@0 548 *
michael@0 549 * @param {string} cookieString
michael@0 550 * The raw cookie string coming from response header.
michael@0 551 * @param {string} domain
michael@0 552 * The domain of the url of the nsiChannel the cookie corresponds to.
michael@0 553 * This will be used when the cookie string does not have a domain.
michael@0 554 *
michael@0 555 * @returns {[object]}
michael@0 556 * An array of nsICookie like objects representing the cookies.
michael@0 557 */
michael@0 558 parseCookieString: function(cookieString, domain) {
michael@0 559 /**
michael@0 560 * Takes a date string present in raw cookie string coming from http
michael@0 561 * request's response headers and returns the number of milliseconds elapsed
michael@0 562 * since epoch. If the date string is undefined, its probably a session
michael@0 563 * cookie so return 0.
michael@0 564 */
michael@0 565 let parseDateString = dateString => {
michael@0 566 return dateString ? new Date(dateString.replace(/-/g, " ")).getTime(): 0;
michael@0 567 };
michael@0 568
michael@0 569 let cookies = [];
michael@0 570 for (let string of cookieString.split("\n")) {
michael@0 571 let keyVals = {}, name = null;
michael@0 572 for (let keyVal of string.split(/;\s*/)) {
michael@0 573 let tokens = keyVal.split(/\s*=\s*/);
michael@0 574 if (!name) {
michael@0 575 name = tokens[0];
michael@0 576 }
michael@0 577 else {
michael@0 578 tokens[0] = tokens[0].toLowerCase();
michael@0 579 }
michael@0 580 keyVals[tokens.splice(0, 1)[0]] = tokens.join("=");
michael@0 581 }
michael@0 582 let expiresTime = parseDateString(keyVals.expires);
michael@0 583 keyVals.domain = keyVals.domain || domain;
michael@0 584 cookies.push({
michael@0 585 name: name,
michael@0 586 value: keyVals[name] || "",
michael@0 587 path: keyVals.path,
michael@0 588 host: keyVals.domain,
michael@0 589 expires: expiresTime/1000, // seconds, to match with nsiCookie.expires
michael@0 590 lastAccessed: expiresTime * 1000,
michael@0 591 // microseconds, to match with nsiCookie.lastAccessed
michael@0 592 creationTime: expiresTime * 1000,
michael@0 593 // microseconds, to match with nsiCookie.creationTime
michael@0 594 isHttpOnly: true,
michael@0 595 isSecure: keyVals.secure != null,
michael@0 596 isDomain: keyVals.domain.startsWith("."),
michael@0 597 });
michael@0 598 }
michael@0 599 return cookies;
michael@0 600 },
michael@0 601
michael@0 602 /**
michael@0 603 * Notification observer for topics "http-on-response-set-cookie" and
michael@0 604 * "cookie-change".
michael@0 605 *
michael@0 606 * @param subject
michael@0 607 * {nsiChannel} The channel associated to the SET-COOKIE response
michael@0 608 * header in case of "http-on-response-set-cookie" topic.
michael@0 609 * {nsiCookie|[nsiCookie]} A single nsiCookie object or a list of it
michael@0 610 * depending on the action. Array is only in case of "batch-deleted"
michael@0 611 * action.
michael@0 612 * @param {string} topic
michael@0 613 * The topic of the notification.
michael@0 614 * @param {string} action
michael@0 615 * Additional data associated with the notification. Its the type of
michael@0 616 * cookie change in case of "cookie-change" topic and the cookie string
michael@0 617 * in case of "http-on-response-set-cookie" topic.
michael@0 618 */
michael@0 619 observe: function(subject, topic, action) {
michael@0 620 if (topic == "http-on-response-set-cookie") {
michael@0 621 // Some cookies got created as a result of http response header SET-COOKIE
michael@0 622 // subject here is an nsIChannel object referring to the http request.
michael@0 623 // We get the requestor of this channel and thus the content window.
michael@0 624 let channel = subject.QueryInterface(Ci.nsIChannel);
michael@0 625 let requestor = channel.notificationCallbacks ||
michael@0 626 channel.loadGroup.notificationCallbacks;
michael@0 627 // requester can be null sometimes.
michael@0 628 let window = requestor ? requestor.getInterface(Ci.nsIDOMWindow): null;
michael@0 629 // Proceed only if this window is present on the currently targetted tab
michael@0 630 if (window && this.storageActor.isIncludedInTopLevelWindow(window)) {
michael@0 631 let host = this.getHostName(window.location);
michael@0 632 if (this.hostVsStores.has(host)) {
michael@0 633 let cookies = this.parseCookieString(action, channel.URI.host);
michael@0 634 let data = {};
michael@0 635 data[host] = [];
michael@0 636 for (let cookie of cookies) {
michael@0 637 if (this.hostVsStores.get(host).has(cookie.name)) {
michael@0 638 continue;
michael@0 639 }
michael@0 640 this.hostVsStores.get(host).set(cookie.name, cookie);
michael@0 641 data[host].push(cookie.name);
michael@0 642 }
michael@0 643 if (data[host]) {
michael@0 644 this.storageActor.update("added", "cookies", data);
michael@0 645 }
michael@0 646 }
michael@0 647 }
michael@0 648 return null;
michael@0 649 }
michael@0 650
michael@0 651 if (topic != "cookie-changed") {
michael@0 652 return null;
michael@0 653 }
michael@0 654
michael@0 655 let hosts = this.getMatchingHosts(subject);
michael@0 656 let data = {};
michael@0 657
michael@0 658 switch(action) {
michael@0 659 case "added":
michael@0 660 case "changed":
michael@0 661 if (hosts.length) {
michael@0 662 subject = subject.QueryInterface(Ci.nsICookie)
michael@0 663 .QueryInterface(Ci.nsICookie2);
michael@0 664 for (let host of hosts) {
michael@0 665 this.hostVsStores.get(host).set(subject.name, subject);
michael@0 666 data[host] = [subject.name];
michael@0 667 }
michael@0 668 this.storageActor.update(action, "cookies", data);
michael@0 669 }
michael@0 670 break;
michael@0 671
michael@0 672 case "deleted":
michael@0 673 if (hosts.length) {
michael@0 674 subject = subject.QueryInterface(Ci.nsICookie)
michael@0 675 .QueryInterface(Ci.nsICookie2);
michael@0 676 for (let host of hosts) {
michael@0 677 this.hostVsStores.get(host).delete(subject.name);
michael@0 678 data[host] = [subject.name];
michael@0 679 }
michael@0 680 this.storageActor.update("deleted", "cookies", data);
michael@0 681 }
michael@0 682 break;
michael@0 683
michael@0 684 case "batch-deleted":
michael@0 685 if (hosts.length) {
michael@0 686 for (let host of hosts) {
michael@0 687 let stores = [];
michael@0 688 for (let cookie of subject) {
michael@0 689 cookie = cookie.QueryInterface(Ci.nsICookie)
michael@0 690 .QueryInterface(Ci.nsICookie2);
michael@0 691 this.hostVsStores.get(host).delete(cookie.name);
michael@0 692 stores.push(cookie.name);
michael@0 693 }
michael@0 694 data[host] = stores;
michael@0 695 }
michael@0 696 this.storageActor.update("deleted", "cookies", data);
michael@0 697 }
michael@0 698 break;
michael@0 699
michael@0 700 case "cleared":
michael@0 701 this.storageActor.update("cleared", "cookies", hosts);
michael@0 702 break;
michael@0 703
michael@0 704 case "reload":
michael@0 705 this.storageActor.update("reloaded", "cookies", hosts);
michael@0 706 break;
michael@0 707 }
michael@0 708 return null;
michael@0 709 },
michael@0 710 });
michael@0 711
michael@0 712
michael@0 713 /**
michael@0 714 * Helper method to create the overriden object required in
michael@0 715 * StorageActors.createActor for Local Storage and Session Storage.
michael@0 716 * This method exists as both Local Storage and Session Storage have almost
michael@0 717 * identical actors.
michael@0 718 */
michael@0 719 function getObjectForLocalOrSessionStorage(type) {
michael@0 720 return {
michael@0 721 getNamesForHost: function(host) {
michael@0 722 let storage = this.hostVsStores.get(host);
michael@0 723 return [key for (key in storage)];
michael@0 724 },
michael@0 725
michael@0 726 getValuesForHost: function(host, name) {
michael@0 727 let storage = this.hostVsStores.get(host);
michael@0 728 if (name) {
michael@0 729 return [{name: name, value: storage.getItem(name)}];
michael@0 730 }
michael@0 731 return [{name: name, value: storage.getItem(name)} for (name in storage)];
michael@0 732 },
michael@0 733
michael@0 734 getHostName: function(location) {
michael@0 735 if (!location.host) {
michael@0 736 return location.href;
michael@0 737 }
michael@0 738 return location.protocol + "//" + location.host;
michael@0 739 },
michael@0 740
michael@0 741 populateStoresForHost: function(host, window) {
michael@0 742 try {
michael@0 743 this.hostVsStores.set(host, window[type]);
michael@0 744 } catch(ex) {
michael@0 745 // Exceptions happen when local or session storage is inaccessible
michael@0 746 }
michael@0 747 return null;
michael@0 748 },
michael@0 749
michael@0 750 populateStoresForHosts: function() {
michael@0 751 this.hostVsStores = new Map();
michael@0 752 try {
michael@0 753 for (let window of this.windows) {
michael@0 754 this.hostVsStores.set(this.getHostName(window.location), window[type]);
michael@0 755 }
michael@0 756 } catch(ex) {
michael@0 757 // Exceptions happen when local or session storage is inaccessible
michael@0 758 }
michael@0 759 return null;
michael@0 760 },
michael@0 761
michael@0 762 observe: function(subject, topic, data) {
michael@0 763 if (topic != "dom-storage2-changed" || data != type) {
michael@0 764 return null;
michael@0 765 }
michael@0 766
michael@0 767 let host = this.getSchemaAndHost(subject.url);
michael@0 768
michael@0 769 if (!this.hostVsStores.has(host)) {
michael@0 770 return null;
michael@0 771 }
michael@0 772
michael@0 773 let action = "changed";
michael@0 774 if (subject.key == null) {
michael@0 775 return this.storageActor.update("cleared", type, [host]);
michael@0 776 }
michael@0 777 else if (subject.oldValue == null) {
michael@0 778 action = "added";
michael@0 779 }
michael@0 780 else if (subject.newValue == null) {
michael@0 781 action = "deleted";
michael@0 782 }
michael@0 783 let updateData = {};
michael@0 784 updateData[host] = [subject.key];
michael@0 785 return this.storageActor.update(action, type, updateData);
michael@0 786 },
michael@0 787
michael@0 788 /**
michael@0 789 * Given a url, correctly determine its protocol + hostname part.
michael@0 790 */
michael@0 791 getSchemaAndHost: function(url) {
michael@0 792 let uri = Services.io.newURI(url, null, null);
michael@0 793 return uri.scheme + "://" + uri.hostPort;
michael@0 794 },
michael@0 795
michael@0 796 toStoreObject: function(item) {
michael@0 797 if (!item) {
michael@0 798 return null;
michael@0 799 }
michael@0 800
michael@0 801 return {
michael@0 802 name: item.name,
michael@0 803 value: new LongStringActor(this.conn, item.value || "")
michael@0 804 };
michael@0 805 },
michael@0 806 }
michael@0 807 };
michael@0 808
michael@0 809 /**
michael@0 810 * The Local Storage actor and front.
michael@0 811 */
michael@0 812 StorageActors.createActor({
michael@0 813 typeName: "localStorage",
michael@0 814 observationTopic: "dom-storage2-changed",
michael@0 815 storeObjectType: "storagestoreobject"
michael@0 816 }, getObjectForLocalOrSessionStorage("localStorage"));
michael@0 817
michael@0 818 /**
michael@0 819 * The Session Storage actor and front.
michael@0 820 */
michael@0 821 StorageActors.createActor({
michael@0 822 typeName: "sessionStorage",
michael@0 823 observationTopic: "dom-storage2-changed",
michael@0 824 storeObjectType: "storagestoreobject"
michael@0 825 }, getObjectForLocalOrSessionStorage("sessionStorage"));
michael@0 826
michael@0 827
michael@0 828 /**
michael@0 829 * Code related to the Indexed DB actor and front
michael@0 830 */
michael@0 831
michael@0 832 // Metadata holder objects for various components of Indexed DB
michael@0 833
michael@0 834 /**
michael@0 835 * Meta data object for a particular index in an object store
michael@0 836 *
michael@0 837 * @param {IDBIndex} index
michael@0 838 * The particular index from the object store.
michael@0 839 */
michael@0 840 function IndexMetadata(index) {
michael@0 841 this._name = index.name;
michael@0 842 this._keyPath = index.keyPath;
michael@0 843 this._unique = index.unique;
michael@0 844 this._multiEntry = index.multiEntry;
michael@0 845 }
michael@0 846 IndexMetadata.prototype = {
michael@0 847 toObject: function() {
michael@0 848 return {
michael@0 849 name: this._name,
michael@0 850 keyPath: this._keyPath,
michael@0 851 unique: this._unique,
michael@0 852 multiEntry: this._multiEntry
michael@0 853 };
michael@0 854 }
michael@0 855 };
michael@0 856
michael@0 857 /**
michael@0 858 * Meta data object for a particular object store in a db
michael@0 859 *
michael@0 860 * @param {IDBObjectStore} objectStore
michael@0 861 * The particular object store from the db.
michael@0 862 */
michael@0 863 function ObjectStoreMetadata(objectStore) {
michael@0 864 this._name = objectStore.name;
michael@0 865 this._keyPath = objectStore.keyPath;
michael@0 866 this._autoIncrement = objectStore.autoIncrement;
michael@0 867 this._indexes = new Map();
michael@0 868
michael@0 869 for (let i = 0; i < objectStore.indexNames.length; i++) {
michael@0 870 let index = objectStore.index(objectStore.indexNames[i]);
michael@0 871 this._indexes.set(index, new IndexMetadata(index));
michael@0 872 }
michael@0 873 }
michael@0 874 ObjectStoreMetadata.prototype = {
michael@0 875 toObject: function() {
michael@0 876 return {
michael@0 877 name: this._name,
michael@0 878 keyPath: this._keyPath,
michael@0 879 autoIncrement: this._autoIncrement,
michael@0 880 indexes: JSON.stringify(
michael@0 881 [index.toObject() for (index of this._indexes.values())]
michael@0 882 )
michael@0 883 };
michael@0 884 }
michael@0 885 };
michael@0 886
michael@0 887 /**
michael@0 888 * Meta data object for a particular indexed db in a host.
michael@0 889 *
michael@0 890 * @param {string} origin
michael@0 891 * The host associated with this indexed db.
michael@0 892 * @param {IDBDatabase} db
michael@0 893 * The particular indexed db.
michael@0 894 */
michael@0 895 function DatabaseMetadata(origin, db) {
michael@0 896 this._origin = origin;
michael@0 897 this._name = db.name;
michael@0 898 this._version = db.version;
michael@0 899 this._objectStores = new Map();
michael@0 900
michael@0 901 if (db.objectStoreNames.length) {
michael@0 902 let transaction = db.transaction(db.objectStoreNames, "readonly");
michael@0 903
michael@0 904 for (let i = 0; i < transaction.objectStoreNames.length; i++) {
michael@0 905 let objectStore =
michael@0 906 transaction.objectStore(transaction.objectStoreNames[i]);
michael@0 907 this._objectStores.set(transaction.objectStoreNames[i],
michael@0 908 new ObjectStoreMetadata(objectStore));
michael@0 909 }
michael@0 910 }
michael@0 911 };
michael@0 912 DatabaseMetadata.prototype = {
michael@0 913 get objectStores() {
michael@0 914 return this._objectStores;
michael@0 915 },
michael@0 916
michael@0 917 toObject: function() {
michael@0 918 return {
michael@0 919 name: this._name,
michael@0 920 origin: this._origin,
michael@0 921 version: this._version,
michael@0 922 objectStores: this._objectStores.size
michael@0 923 };
michael@0 924 }
michael@0 925 };
michael@0 926
michael@0 927 StorageActors.createActor({
michael@0 928 typeName: "indexedDB",
michael@0 929 storeObjectType: "idbstoreobject"
michael@0 930 }, {
michael@0 931 initialize: function(storageActor) {
michael@0 932 protocol.Actor.prototype.initialize.call(this, null);
michael@0 933 if (!global.indexedDB) {
michael@0 934 let idbManager = Cc["@mozilla.org/dom/indexeddb/manager;1"]
michael@0 935 .getService(Ci.nsIIndexedDatabaseManager);
michael@0 936 idbManager.initWindowless(global);
michael@0 937 }
michael@0 938 this.objectsSize = {};
michael@0 939 this.storageActor = storageActor;
michael@0 940 this.onWindowReady = this.onWindowReady.bind(this);
michael@0 941 this.onWindowDestroyed = this.onWindowDestroyed.bind(this);
michael@0 942 events.on(this.storageActor, "window-ready", this.onWindowReady);
michael@0 943 events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed);
michael@0 944 },
michael@0 945
michael@0 946 destroy: function() {
michael@0 947 this.hostVsStores = null;
michael@0 948 this.objectsSize = null;
michael@0 949 events.off(this.storageActor, "window-ready", this.onWindowReady);
michael@0 950 events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed);
michael@0 951 },
michael@0 952
michael@0 953 getHostName: function(location) {
michael@0 954 if (!location.host) {
michael@0 955 return location.href;
michael@0 956 }
michael@0 957 return location.protocol + "//" + location.host;
michael@0 958 },
michael@0 959
michael@0 960 /**
michael@0 961 * This method is overriden and left blank as for indexedDB, this operation
michael@0 962 * cannot be performed synchronously. Thus, the preListStores method exists to
michael@0 963 * do the same task asynchronously.
michael@0 964 */
michael@0 965 populateStoresForHosts: function() {
michael@0 966 },
michael@0 967
michael@0 968 getNamesForHost: function(host) {
michael@0 969 let names = [];
michael@0 970 for (let [dbName, metaData] of this.hostVsStores.get(host)) {
michael@0 971 for (let objectStore of metaData.objectStores.keys()) {
michael@0 972 names.push(JSON.stringify([dbName, objectStore]));
michael@0 973 }
michael@0 974 }
michael@0 975 return names;
michael@0 976 },
michael@0 977
michael@0 978 /**
michael@0 979 * Returns all or requested entries from a particular objectStore from the db
michael@0 980 * in the given host.
michael@0 981 *
michael@0 982 * @param {string} host
michael@0 983 * The given host.
michael@0 984 * @param {string} dbName
michael@0 985 * The name of the indexed db from the above host.
michael@0 986 * @param {string} objectStore
michael@0 987 * The name of the object store from the above db.
michael@0 988 * @param {string} id
michael@0 989 * id of the requested entry from the above object store.
michael@0 990 * null if all entries from the above object store are requested.
michael@0 991 * @param {string} index
michael@0 992 * name of the IDBIndex to be iterated on while fetching entries.
michael@0 993 * null or "name" if no index is to be iterated.
michael@0 994 * @param {number} offset
michael@0 995 * ofsset of the entries to be fetched.
michael@0 996 * @param {number} size
michael@0 997 * The intended size of the entries to be fetched.
michael@0 998 */
michael@0 999 getObjectStoreData:
michael@0 1000 function(host, dbName, objectStore, id, index, offset, size) {
michael@0 1001 let request = this.openWithOrigin(host, dbName);
michael@0 1002 let success = Promise.defer();
michael@0 1003 let data = [];
michael@0 1004 if (!size || size > MAX_STORE_OBJECT_COUNT) {
michael@0 1005 size = MAX_STORE_OBJECT_COUNT;
michael@0 1006 }
michael@0 1007
michael@0 1008 request.onsuccess = event => {
michael@0 1009 let db = event.target.result;
michael@0 1010
michael@0 1011 let transaction = db.transaction(objectStore, "readonly");
michael@0 1012 let source = transaction.objectStore(objectStore);
michael@0 1013 if (index && index != "name") {
michael@0 1014 source = source.index(index);
michael@0 1015 }
michael@0 1016
michael@0 1017 source.count().onsuccess = event => {
michael@0 1018 let count = event.target.result;
michael@0 1019 this.objectsSize[host + dbName + objectStore + index] = count;
michael@0 1020
michael@0 1021 if (!offset) {
michael@0 1022 offset = 0;
michael@0 1023 }
michael@0 1024 else if (offset > count) {
michael@0 1025 db.close();
michael@0 1026 success.resolve([]);
michael@0 1027 return;
michael@0 1028 }
michael@0 1029
michael@0 1030 if (id) {
michael@0 1031 source.get(id).onsuccess = event => {
michael@0 1032 db.close();
michael@0 1033 success.resolve([{name: id, value: event.target.result}]);
michael@0 1034 };
michael@0 1035 }
michael@0 1036 else {
michael@0 1037 source.openCursor().onsuccess = event => {
michael@0 1038 let cursor = event.target.result;
michael@0 1039
michael@0 1040 if (!cursor || data.length >= size) {
michael@0 1041 db.close();
michael@0 1042 success.resolve(data);
michael@0 1043 return;
michael@0 1044 }
michael@0 1045 if (offset-- <= 0) {
michael@0 1046 data.push({name: cursor.key, value: cursor.value});
michael@0 1047 }
michael@0 1048 cursor.continue();
michael@0 1049 };
michael@0 1050 }
michael@0 1051 };
michael@0 1052 };
michael@0 1053 request.onerror = () => {
michael@0 1054 db.close();
michael@0 1055 success.resolve([]);
michael@0 1056 };
michael@0 1057 return success.promise;
michael@0 1058 },
michael@0 1059
michael@0 1060 /**
michael@0 1061 * Returns the total number of entries for various types of requests to
michael@0 1062 * getStoreObjects for Indexed DB actor.
michael@0 1063 *
michael@0 1064 * @param {string} host
michael@0 1065 * The host for the request.
michael@0 1066 * @param {array:string} names
michael@0 1067 * Array of stringified name objects for indexed db actor.
michael@0 1068 * The request type depends on the length of any parsed entry from this
michael@0 1069 * array. 0 length refers to request for the whole host. 1 length
michael@0 1070 * refers to request for a particular db in the host. 2 length refers
michael@0 1071 * to a particular object store in a db in a host. 3 length refers to
michael@0 1072 * particular items of an object store in a db in a host.
michael@0 1073 * @param {object} options
michael@0 1074 * An options object containing following properties:
michael@0 1075 * - index {string} The IDBIndex for the object store in the db.
michael@0 1076 */
michael@0 1077 getObjectsSize: function(host, names, options) {
michael@0 1078 // In Indexed DB, we are interested in only the first name, as the pattern
michael@0 1079 // should follow in all entries.
michael@0 1080 let name = names[0];
michael@0 1081 let parsedName = JSON.parse(name);
michael@0 1082
michael@0 1083 if (parsedName.length == 3) {
michael@0 1084 // This is the case where specific entries from an object store were
michael@0 1085 // requested
michael@0 1086 return names.length;
michael@0 1087 }
michael@0 1088 else if (parsedName.length == 2) {
michael@0 1089 // This is the case where all entries from an object store are requested.
michael@0 1090 let index = options.index;
michael@0 1091 let [db, objectStore] = parsedName;
michael@0 1092 if (this.objectsSize[host + db + objectStore + index]) {
michael@0 1093 return this.objectsSize[host + db + objectStore + index];
michael@0 1094 }
michael@0 1095 }
michael@0 1096 else if (parsedName.length == 1) {
michael@0 1097 // This is the case where details of all object stores in a db are
michael@0 1098 // requested.
michael@0 1099 if (this.hostVsStores.has(host) &&
michael@0 1100 this.hostVsStores.get(host).has(parsedName[0])) {
michael@0 1101 return this.hostVsStores.get(host).get(parsedName[0]).objectStores.size;
michael@0 1102 }
michael@0 1103 }
michael@0 1104 else if (!parsedName || !parsedName.length) {
michael@0 1105 // This is the case were details of all dbs in a host are requested.
michael@0 1106 if (this.hostVsStores.has(host)) {
michael@0 1107 return this.hostVsStores.get(host).size;
michael@0 1108 }
michael@0 1109 }
michael@0 1110 return 0;
michael@0 1111 },
michael@0 1112
michael@0 1113 getValuesForHost: async(function*(host, name = "null", options) {
michael@0 1114 name = JSON.parse(name);
michael@0 1115 if (!name || !name.length) {
michael@0 1116 // This means that details about the db in this particular host are
michael@0 1117 // requested.
michael@0 1118 let dbs = [];
michael@0 1119 if (this.hostVsStores.has(host)) {
michael@0 1120 for (let [dbName, db] of this.hostVsStores.get(host)) {
michael@0 1121 dbs.push(db.toObject());
michael@0 1122 }
michael@0 1123 }
michael@0 1124 return dbs;
michael@0 1125 }
michael@0 1126 let [db, objectStore, id] = name;
michael@0 1127 if (!objectStore) {
michael@0 1128 // This means that details about all the object stores in this db are
michael@0 1129 // requested.
michael@0 1130 let objectStores = [];
michael@0 1131 if (this.hostVsStores.has(host) && this.hostVsStores.get(host).has(db)) {
michael@0 1132 for (let objectStore of this.hostVsStores.get(host).get(db).objectStores) {
michael@0 1133 objectStores.push(objectStore[1].toObject());
michael@0 1134 }
michael@0 1135 }
michael@0 1136 return objectStores;
michael@0 1137 }
michael@0 1138 // Get either all entries from the object store, or a particular id
michael@0 1139 return yield this.getObjectStoreData(host, db, objectStore, id,
michael@0 1140 options.index, options.size);
michael@0 1141 }),
michael@0 1142
michael@0 1143 /**
michael@0 1144 * Purpose of this method is same as populateStoresForHosts but this is async.
michael@0 1145 * This exact same operation cannot be performed in populateStoresForHosts
michael@0 1146 * method, as that method is called in initialize method of the actor, which
michael@0 1147 * cannot be asynchronous.
michael@0 1148 */
michael@0 1149 preListStores: async(function*() {
michael@0 1150 this.hostVsStores = new Map();
michael@0 1151 for (let host of this.hosts) {
michael@0 1152 yield this.populateStoresForHost(host);
michael@0 1153 }
michael@0 1154 }),
michael@0 1155
michael@0 1156 populateStoresForHost: async(function*(host) {
michael@0 1157 let storeMap = new Map();
michael@0 1158 for (let name of (yield this.getDBNamesForHost(host))) {
michael@0 1159 storeMap.set(name, yield this.getDBMetaData(host, name));
michael@0 1160 }
michael@0 1161 this.hostVsStores.set(host, storeMap);
michael@0 1162 }),
michael@0 1163
michael@0 1164 /**
michael@0 1165 * Removes any illegal characters from the host name to make it a valid file
michael@0 1166 * name.
michael@0 1167 */
michael@0 1168 getSanitizedHost: function(host) {
michael@0 1169 return host.replace(ILLEGAL_CHAR_REGEX, "+");
michael@0 1170 },
michael@0 1171
michael@0 1172 /**
michael@0 1173 * Opens an indexed db connection for the given `host` and database `name`.
michael@0 1174 */
michael@0 1175 openWithOrigin: function(host, name) {
michael@0 1176 let principal;
michael@0 1177
michael@0 1178 if (/^(about:|chrome:)/.test(host)) {
michael@0 1179 principal = Services.scriptSecurityManager.getSystemPrincipal();
michael@0 1180 }
michael@0 1181 else {
michael@0 1182 let uri = Services.io.newURI(host, null, null);
michael@0 1183 principal = Services.scriptSecurityManager.getCodebasePrincipal(uri);
michael@0 1184 }
michael@0 1185
michael@0 1186 return indexedDB.openForPrincipal(principal, name);
michael@0 1187 },
michael@0 1188
michael@0 1189 /**
michael@0 1190 * Fetches and stores all the metadata information for the given database
michael@0 1191 * `name` for the given `host`. The stored metadata information is of
michael@0 1192 * `DatabaseMetadata` type.
michael@0 1193 */
michael@0 1194 getDBMetaData: function(host, name) {
michael@0 1195 let request = this.openWithOrigin(host, name);
michael@0 1196 let success = Promise.defer();
michael@0 1197 request.onsuccess = event => {
michael@0 1198 let db = event.target.result;
michael@0 1199
michael@0 1200 let dbData = new DatabaseMetadata(host, db);
michael@0 1201 db.close();
michael@0 1202 success.resolve(dbData);
michael@0 1203 };
michael@0 1204 request.onerror = event => {
michael@0 1205 console.error("Error opening indexeddb database " + name + " for host " +
michael@0 1206 host);
michael@0 1207 success.resolve(null);
michael@0 1208 };
michael@0 1209 return success.promise;
michael@0 1210 },
michael@0 1211
michael@0 1212 /**
michael@0 1213 * Retrives the proper indexed db database name from the provided .sqlite file
michael@0 1214 * location.
michael@0 1215 */
michael@0 1216 getNameFromDatabaseFile: async(function*(path) {
michael@0 1217 let connection = null;
michael@0 1218 let retryCount = 0;
michael@0 1219
michael@0 1220 // Content pages might be having an open transaction for the same indexed db
michael@0 1221 // which this sqlite file belongs to. In that case, sqlite.openConnection
michael@0 1222 // will throw. Thus we retey for some time to see if lock is removed.
michael@0 1223 while (!connection && retryCount++ < 25) {
michael@0 1224 try {
michael@0 1225 connection = yield Sqlite.openConnection({ path: path });
michael@0 1226 }
michael@0 1227 catch (ex) {
michael@0 1228 // Continuously retrying is overkill. Waiting for 100ms before next try
michael@0 1229 yield sleep(100);
michael@0 1230 }
michael@0 1231 }
michael@0 1232
michael@0 1233 if (!connection) {
michael@0 1234 return null;
michael@0 1235 }
michael@0 1236
michael@0 1237 let rows = yield connection.execute("SELECT name FROM database");
michael@0 1238 if (rows.length != 1) {
michael@0 1239 return null;
michael@0 1240 }
michael@0 1241
michael@0 1242 let name = rows[0].getResultByName("name");
michael@0 1243
michael@0 1244 yield connection.close();
michael@0 1245
michael@0 1246 return name;
michael@0 1247 }),
michael@0 1248
michael@0 1249 /**
michael@0 1250 * Fetches all the databases and their metadata for the given `host`.
michael@0 1251 */
michael@0 1252 getDBNamesForHost: async(function*(host) {
michael@0 1253 let sanitizedHost = this.getSanitizedHost(host);
michael@0 1254 let directory = OS.Path.join(OS.Constants.Path.profileDir, "storage",
michael@0 1255 "persistent", sanitizedHost, "idb");
michael@0 1256
michael@0 1257 let exists = yield OS.File.exists(directory);
michael@0 1258 if (!exists && host.startsWith("about:")) {
michael@0 1259 // try for moz-safe-about directory
michael@0 1260 sanitizedHost = this.getSanitizedHost("moz-safe-" + host);
michael@0 1261 directory = OS.Path.join(OS.Constants.Path.profileDir, "storage",
michael@0 1262 "persistent", sanitizedHost, "idb");
michael@0 1263 exists = yield OS.File.exists(directory);
michael@0 1264 }
michael@0 1265 if (!exists) {
michael@0 1266 return [];
michael@0 1267 }
michael@0 1268
michael@0 1269 let names = [];
michael@0 1270 let dirIterator = new OS.File.DirectoryIterator(directory);
michael@0 1271 try {
michael@0 1272 yield dirIterator.forEach(file => {
michael@0 1273 // Skip directories.
michael@0 1274 if (file.isDir) {
michael@0 1275 return null;
michael@0 1276 }
michael@0 1277
michael@0 1278 // Skip any non-sqlite files.
michael@0 1279 if (!file.name.endsWith(".sqlite")) {
michael@0 1280 return null;
michael@0 1281 }
michael@0 1282
michael@0 1283 return this.getNameFromDatabaseFile(file.path).then(name => {
michael@0 1284 if (name) {
michael@0 1285 names.push(name);
michael@0 1286 }
michael@0 1287 return null;
michael@0 1288 });
michael@0 1289 });
michael@0 1290 }
michael@0 1291 finally {
michael@0 1292 dirIterator.close();
michael@0 1293 }
michael@0 1294 return names;
michael@0 1295 }),
michael@0 1296
michael@0 1297 /**
michael@0 1298 * Returns the over-the-wire implementation of the indexed db entity.
michael@0 1299 */
michael@0 1300 toStoreObject: function(item) {
michael@0 1301 if (!item) {
michael@0 1302 return null;
michael@0 1303 }
michael@0 1304
michael@0 1305 if (item.indexes) {
michael@0 1306 // Object store meta data
michael@0 1307 return {
michael@0 1308 objectStore: item.name,
michael@0 1309 keyPath: item.keyPath,
michael@0 1310 autoIncrement: item.autoIncrement,
michael@0 1311 indexes: item.indexes
michael@0 1312 };
michael@0 1313 }
michael@0 1314 if (item.objectStores) {
michael@0 1315 // DB meta data
michael@0 1316 return {
michael@0 1317 db: item.name,
michael@0 1318 origin: item.origin,
michael@0 1319 version: item.version,
michael@0 1320 objectStores: item.objectStores
michael@0 1321 };
michael@0 1322 }
michael@0 1323 // Indexed db entry
michael@0 1324 return {
michael@0 1325 name: item.name,
michael@0 1326 value: new LongStringActor(this.conn, JSON.stringify(item.value))
michael@0 1327 };
michael@0 1328 },
michael@0 1329
michael@0 1330 form: function(form, detail) {
michael@0 1331 if (detail === "actorid") {
michael@0 1332 return this.actorID;
michael@0 1333 }
michael@0 1334
michael@0 1335 let hosts = {};
michael@0 1336 for (let host of this.hosts) {
michael@0 1337 hosts[host] = this.getNamesForHost(host);
michael@0 1338 }
michael@0 1339
michael@0 1340 return {
michael@0 1341 actor: this.actorID,
michael@0 1342 hosts: hosts
michael@0 1343 };
michael@0 1344 },
michael@0 1345 });
michael@0 1346
michael@0 1347 /**
michael@0 1348 * The main Storage Actor.
michael@0 1349 */
michael@0 1350 let StorageActor = exports.StorageActor = protocol.ActorClass({
michael@0 1351 typeName: "storage",
michael@0 1352
michael@0 1353 get window() {
michael@0 1354 return this.parentActor.window;
michael@0 1355 },
michael@0 1356
michael@0 1357 get document() {
michael@0 1358 return this.parentActor.window.document;
michael@0 1359 },
michael@0 1360
michael@0 1361 get windows() {
michael@0 1362 return this.childWindowPool;
michael@0 1363 },
michael@0 1364
michael@0 1365 /**
michael@0 1366 * List of event notifications that the server can send to the client.
michael@0 1367 *
michael@0 1368 * - stores-update : When any store object in any storage type changes.
michael@0 1369 * - stores-cleared : When all the store objects are removed.
michael@0 1370 * - stores-reloaded : When all stores are reloaded. This generally mean that
michael@0 1371 * we should refetch everything again.
michael@0 1372 */
michael@0 1373 events: {
michael@0 1374 "stores-update": {
michael@0 1375 type: "storesUpdate",
michael@0 1376 data: Arg(0, "storeUpdateObject")
michael@0 1377 },
michael@0 1378 "stores-cleared": {
michael@0 1379 type: "storesCleared",
michael@0 1380 data: Arg(0, "json")
michael@0 1381 },
michael@0 1382 "stores-reloaded": {
michael@0 1383 type: "storesRelaoded",
michael@0 1384 data: Arg(0, "json")
michael@0 1385 }
michael@0 1386 },
michael@0 1387
michael@0 1388 initialize: function (conn, tabActor) {
michael@0 1389 protocol.Actor.prototype.initialize.call(this, null);
michael@0 1390
michael@0 1391 this.conn = conn;
michael@0 1392 this.parentActor = tabActor;
michael@0 1393
michael@0 1394 this.childActorPool = new Map();
michael@0 1395 this.childWindowPool = new Set();
michael@0 1396
michael@0 1397 // Fetch all the inner iframe windows in this tab.
michael@0 1398 this.fetchChildWindows(this.parentActor.docShell);
michael@0 1399
michael@0 1400 // Initialize the registered store types
michael@0 1401 for (let [store, actor] of storageTypePool) {
michael@0 1402 this.childActorPool.set(store, new actor(this));
michael@0 1403 }
michael@0 1404
michael@0 1405 // Notifications that help us keep track of newly added windows and windows
michael@0 1406 // that got removed
michael@0 1407 Services.obs.addObserver(this, "content-document-global-created", false);
michael@0 1408 Services.obs.addObserver(this, "inner-window-destroyed", false);
michael@0 1409 this.onPageChange = this.onPageChange.bind(this);
michael@0 1410 tabActor.browser.addEventListener("pageshow", this.onPageChange, true);
michael@0 1411 tabActor.browser.addEventListener("pagehide", this.onPageChange, true);
michael@0 1412
michael@0 1413 this.destroyed = false;
michael@0 1414 this.boundUpdate = {};
michael@0 1415 // The time which periodically flushes and transfers the updated store
michael@0 1416 // objects.
michael@0 1417 this.updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
michael@0 1418 this.updateTimer.initWithCallback(this , UPDATE_INTERVAL,
michael@0 1419 Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP);
michael@0 1420
michael@0 1421 // Layout helper for window.parent and window.top helper methods that work
michael@0 1422 // accross devices.
michael@0 1423 this.layoutHelper = new LayoutHelpers(this.window);
michael@0 1424 },
michael@0 1425
michael@0 1426 destroy: function() {
michael@0 1427 this.updateTimer.cancel();
michael@0 1428 this.updateTimer = null;
michael@0 1429 this.layoutHelper = null;
michael@0 1430 // Remove observers
michael@0 1431 Services.obs.removeObserver(this, "content-document-global-created", false);
michael@0 1432 Services.obs.removeObserver(this, "inner-window-destroyed", false);
michael@0 1433 this.destroyed = true;
michael@0 1434 if (this.parentActor.browser) {
michael@0 1435 this.parentActor.browser.removeEventListener(
michael@0 1436 "pageshow", this.onPageChange, true);
michael@0 1437 this.parentActor.browser.removeEventListener(
michael@0 1438 "pagehide", this.onPageChange, true);
michael@0 1439 }
michael@0 1440 // Destroy the registered store types
michael@0 1441 for (let actor of this.childActorPool.values()) {
michael@0 1442 actor.destroy();
michael@0 1443 }
michael@0 1444 this.childActorPool.clear();
michael@0 1445 this.childWindowPool.clear();
michael@0 1446 this.childWindowPool = this.childActorPool = null;
michael@0 1447 },
michael@0 1448
michael@0 1449 /**
michael@0 1450 * Given a docshell, recursively find otu all the child windows from it.
michael@0 1451 *
michael@0 1452 * @param {nsIDocShell} item
michael@0 1453 * The docshell from which all inner windows need to be extracted.
michael@0 1454 */
michael@0 1455 fetchChildWindows: function(item) {
michael@0 1456 let docShell = item.QueryInterface(Ci.nsIDocShell)
michael@0 1457 .QueryInterface(Ci.nsIDocShellTreeItem);
michael@0 1458 if (!docShell.contentViewer) {
michael@0 1459 return null;
michael@0 1460 }
michael@0 1461 let window = docShell.contentViewer.DOMDocument.defaultView;
michael@0 1462 if (window.location.href == "about:blank") {
michael@0 1463 // Skip out about:blank windows as Gecko creates them multiple times while
michael@0 1464 // creating any global.
michael@0 1465 return null;
michael@0 1466 }
michael@0 1467 this.childWindowPool.add(window);
michael@0 1468 for (let i = 0; i < docShell.childCount; i++) {
michael@0 1469 let child = docShell.getChildAt(i);
michael@0 1470 this.fetchChildWindows(child);
michael@0 1471 }
michael@0 1472 return null;
michael@0 1473 },
michael@0 1474
michael@0 1475 isIncludedInTopLevelWindow: function(window) {
michael@0 1476 return this.layoutHelper.isIncludedInTopLevelWindow(window);
michael@0 1477 },
michael@0 1478
michael@0 1479 getWindowFromInnerWindowID: function(innerID) {
michael@0 1480 innerID = innerID.QueryInterface(Ci.nsISupportsPRUint64).data;
michael@0 1481 for (let win of this.childWindowPool.values()) {
michael@0 1482 let id = win.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1483 .getInterface(Ci.nsIDOMWindowUtils)
michael@0 1484 .currentInnerWindowID;
michael@0 1485 if (id == innerID) {
michael@0 1486 return win;
michael@0 1487 }
michael@0 1488 }
michael@0 1489 return null;
michael@0 1490 },
michael@0 1491
michael@0 1492 /**
michael@0 1493 * Event handler for any docshell update. This lets us figure out whenever
michael@0 1494 * any new window is added, or an existing window is removed.
michael@0 1495 */
michael@0 1496 observe: function(subject, topic, data) {
michael@0 1497 if (subject.location &&
michael@0 1498 (!subject.location.href || subject.location.href == "about:blank")) {
michael@0 1499 return null;
michael@0 1500 }
michael@0 1501 if (topic == "content-document-global-created" &&
michael@0 1502 this.isIncludedInTopLevelWindow(subject)) {
michael@0 1503 this.childWindowPool.add(subject);
michael@0 1504 events.emit(this, "window-ready", subject);
michael@0 1505 }
michael@0 1506 else if (topic == "inner-window-destroyed") {
michael@0 1507 let window = this.getWindowFromInnerWindowID(subject);
michael@0 1508 if (window) {
michael@0 1509 this.childWindowPool.delete(window);
michael@0 1510 events.emit(this, "window-destroyed", window);
michael@0 1511 }
michael@0 1512 }
michael@0 1513 return null;
michael@0 1514 },
michael@0 1515
michael@0 1516 /**
michael@0 1517 * Called on "pageshow" or "pagehide" event on the chromeEventHandler of
michael@0 1518 * current tab.
michael@0 1519 *
michael@0 1520 * @param {event} The event object passed to the handler. We are using these
michael@0 1521 * three properties from the event:
michael@0 1522 * - target {document} The document corresponding to the event.
michael@0 1523 * - type {string} Name of the event - "pageshow" or "pagehide".
michael@0 1524 * - persisted {boolean} true if there was no
michael@0 1525 * "content-document-global-created" notification along
michael@0 1526 * this event.
michael@0 1527 */
michael@0 1528 onPageChange: function({target, type, persisted}) {
michael@0 1529 if (this.destroyed) {
michael@0 1530 return;
michael@0 1531 }
michael@0 1532 let window = target.defaultView;
michael@0 1533 if (type == "pagehide" && this.childWindowPool.delete(window)) {
michael@0 1534 events.emit(this, "window-destroyed", window)
michael@0 1535 }
michael@0 1536 else if (type == "pageshow" && persisted && window.location.href &&
michael@0 1537 window.location.href != "about:blank" &&
michael@0 1538 this.isIncludedInTopLevelWindow(window)) {
michael@0 1539 this.childWindowPool.add(window);
michael@0 1540 events.emit(this, "window-ready", window);
michael@0 1541 }
michael@0 1542 },
michael@0 1543
michael@0 1544 /**
michael@0 1545 * Lists the available hosts for all the registered storage types.
michael@0 1546 *
michael@0 1547 * @returns {object} An object containing with the following structure:
michael@0 1548 * - <storageType> : [{
michael@0 1549 * actor: <actorId>,
michael@0 1550 * host: <hostname>
michael@0 1551 * }]
michael@0 1552 */
michael@0 1553 listStores: method(async(function*() {
michael@0 1554 let toReturn = {};
michael@0 1555 for (let [name, value] of this.childActorPool) {
michael@0 1556 if (value.preListStores) {
michael@0 1557 yield value.preListStores();
michael@0 1558 }
michael@0 1559 toReturn[name] = value;
michael@0 1560 }
michael@0 1561 return toReturn;
michael@0 1562 }), {
michael@0 1563 response: RetVal(types.addDictType("storelist", getRegisteredTypes()))
michael@0 1564 }),
michael@0 1565
michael@0 1566 /**
michael@0 1567 * Notifies the client front with the updates in stores at regular intervals.
michael@0 1568 */
michael@0 1569 notify: function() {
michael@0 1570 if (!this.updatePending || this.updatingUpdateObject) {
michael@0 1571 return null;
michael@0 1572 }
michael@0 1573 events.emit(this, "stores-update", this.boundUpdate);
michael@0 1574 this.boundUpdate = {};
michael@0 1575 this.updatePending = false;
michael@0 1576 return null;
michael@0 1577 },
michael@0 1578
michael@0 1579 /**
michael@0 1580 * This method is called by the registered storage types so as to tell the
michael@0 1581 * Storage Actor that there are some changes in the stores. Storage Actor then
michael@0 1582 * notifies the client front about these changes at regular (UPDATE_INTERVAL)
michael@0 1583 * interval.
michael@0 1584 *
michael@0 1585 * @param {string} action
michael@0 1586 * The type of change. One of "added", "changed" or "deleted"
michael@0 1587 * @param {string} storeType
michael@0 1588 * The storage actor in which this change has occurred.
michael@0 1589 * @param {object} data
michael@0 1590 * The update object. This object is of the following format:
michael@0 1591 * - {
michael@0 1592 * <host1>: [<store_names1>, <store_name2>...],
michael@0 1593 * <host2>: [<store_names34>...],
michael@0 1594 * }
michael@0 1595 * Where host1, host2 are the host in which this change happened and
michael@0 1596 * [<store_namesX] is an array of the names of the changed store
michael@0 1597 * objects. Leave it empty if the host was completely removed.
michael@0 1598 * When the action is "reloaded" or "cleared", `data` is an array of
michael@0 1599 * hosts for which the stores were cleared or reloaded.
michael@0 1600 */
michael@0 1601 update: function(action, storeType, data) {
michael@0 1602 if (action == "cleared" || action == "reloaded") {
michael@0 1603 let toSend = {};
michael@0 1604 toSend[storeType] = data
michael@0 1605 events.emit(this, "stores-" + action, toSend);
michael@0 1606 return null;
michael@0 1607 }
michael@0 1608
michael@0 1609 this.updatingUpdateObject = true;
michael@0 1610 if (!this.boundUpdate[action]) {
michael@0 1611 this.boundUpdate[action] = {};
michael@0 1612 }
michael@0 1613 if (!this.boundUpdate[action][storeType]) {
michael@0 1614 this.boundUpdate[action][storeType] = {};
michael@0 1615 }
michael@0 1616 this.updatePending = true;
michael@0 1617 for (let host in data) {
michael@0 1618 if (!this.boundUpdate[action][storeType][host] || action == "deleted") {
michael@0 1619 this.boundUpdate[action][storeType][host] = data[host];
michael@0 1620 }
michael@0 1621 else {
michael@0 1622 this.boundUpdate[action][storeType][host] =
michael@0 1623 this.boundUpdate[action][storeType][host].concat(data[host]);
michael@0 1624 }
michael@0 1625 }
michael@0 1626 if (action == "added") {
michael@0 1627 // If the same store name was previously deleted or changed, but now is
michael@0 1628 // added somehow, dont send the deleted or changed update.
michael@0 1629 this.removeNamesFromUpdateList("deleted", storeType, data);
michael@0 1630 this.removeNamesFromUpdateList("changed", storeType, data);
michael@0 1631 }
michael@0 1632 else if (action == "changed" && this.boundUpdate.added &&
michael@0 1633 this.boundUpdate.added[storeType]) {
michael@0 1634 // If something got added and changed at the same time, then remove those
michael@0 1635 // items from changed instead.
michael@0 1636 this.removeNamesFromUpdateList("changed", storeType,
michael@0 1637 this.boundUpdate.added[storeType]);
michael@0 1638 }
michael@0 1639 else if (action == "deleted") {
michael@0 1640 // If any item got delete, or a host got delete, no point in sending
michael@0 1641 // added or changed update
michael@0 1642 this.removeNamesFromUpdateList("added", storeType, data);
michael@0 1643 this.removeNamesFromUpdateList("changed", storeType, data);
michael@0 1644 for (let host in data) {
michael@0 1645 if (data[host].length == 0 && this.boundUpdate.added &&
michael@0 1646 this.boundUpdate.added[storeType] &&
michael@0 1647 this.boundUpdate.added[storeType][host]) {
michael@0 1648 delete this.boundUpdate.added[storeType][host];
michael@0 1649 }
michael@0 1650 if (data[host].length == 0 && this.boundUpdate.changed &&
michael@0 1651 this.boundUpdate.changed[storeType] &&
michael@0 1652 this.boundUpdate.changed[storeType][host]) {
michael@0 1653 delete this.boundUpdate.changed[storeType][host];
michael@0 1654 }
michael@0 1655 }
michael@0 1656 }
michael@0 1657 this.updatingUpdateObject = false;
michael@0 1658 return null;
michael@0 1659 },
michael@0 1660
michael@0 1661 /**
michael@0 1662 * This method removes data from the this.boundUpdate object in the same
michael@0 1663 * manner like this.update() adds data to it.
michael@0 1664 *
michael@0 1665 * @param {string} action
michael@0 1666 * The type of change. One of "added", "changed" or "deleted"
michael@0 1667 * @param {string} storeType
michael@0 1668 * The storage actor for which you want to remove the updates data.
michael@0 1669 * @param {object} data
michael@0 1670 * The update object. This object is of the following format:
michael@0 1671 * - {
michael@0 1672 * <host1>: [<store_names1>, <store_name2>...],
michael@0 1673 * <host2>: [<store_names34>...],
michael@0 1674 * }
michael@0 1675 * Where host1, host2 are the hosts which you want to remove and
michael@0 1676 * [<store_namesX] is an array of the names of the store objects.
michael@0 1677 */
michael@0 1678 removeNamesFromUpdateList: function(action, storeType, data) {
michael@0 1679 for (let host in data) {
michael@0 1680 if (this.boundUpdate[action] && this.boundUpdate[action][storeType] &&
michael@0 1681 this.boundUpdate[action][storeType][host]) {
michael@0 1682 for (let name in data[host]) {
michael@0 1683 let index = this.boundUpdate[action][storeType][host].indexOf(name);
michael@0 1684 if (index > -1) {
michael@0 1685 this.boundUpdate[action][storeType][host].splice(index, 1);
michael@0 1686 }
michael@0 1687 }
michael@0 1688 if (!this.boundUpdate[action][storeType][host].length) {
michael@0 1689 delete this.boundUpdate[action][storeType][host];
michael@0 1690 }
michael@0 1691 }
michael@0 1692 }
michael@0 1693 return null;
michael@0 1694 }
michael@0 1695 });
michael@0 1696
michael@0 1697 /**
michael@0 1698 * Front for the Storage Actor.
michael@0 1699 */
michael@0 1700 let StorageFront = exports.StorageFront = protocol.FrontClass(StorageActor, {
michael@0 1701 initialize: function(client, tabForm) {
michael@0 1702 protocol.Front.prototype.initialize.call(this, client);
michael@0 1703 this.actorID = tabForm.storageActor;
michael@0 1704
michael@0 1705 client.addActorPool(this);
michael@0 1706 this.manage(this);
michael@0 1707 }
michael@0 1708 });

mercurial