toolkit/devtools/server/actors/storage.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/devtools/server/actors/storage.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1708 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +const {Cu, Cc, Ci} = require("chrome");
    1.11 +const events = require("sdk/event/core");
    1.12 +const protocol = require("devtools/server/protocol");
    1.13 +const {async} = require("devtools/async-utils");
    1.14 +const {Arg, Option, method, RetVal, types} = protocol;
    1.15 +const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
    1.16 +
    1.17 +Cu.import("resource://gre/modules/Promise.jsm");
    1.18 +Cu.import("resource://gre/modules/Services.jsm");
    1.19 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.20 +Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
    1.21 +
    1.22 +XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
    1.23 +  "resource://gre/modules/Sqlite.jsm");
    1.24 +
    1.25 +XPCOMUtils.defineLazyModuleGetter(this, "OS",
    1.26 +  "resource://gre/modules/osfile.jsm");
    1.27 +
    1.28 +exports.register = function(handle) {
    1.29 +  handle.addTabActor(StorageActor, "storageActor");
    1.30 +};
    1.31 +
    1.32 +exports.unregister = function(handle) {
    1.33 +  handle.removeTabActor(StorageActor);
    1.34 +};
    1.35 +
    1.36 +// Global required for window less Indexed DB instantiation.
    1.37 +let global = this;
    1.38 +
    1.39 +// Maximum number of cookies/local storage key-value-pairs that can be sent
    1.40 +// over the wire to the client in one request.
    1.41 +const MAX_STORE_OBJECT_COUNT = 30;
    1.42 +// Interval for the batch job that sends the accumilated update packets to the
    1.43 +// client.
    1.44 +const UPDATE_INTERVAL = 500; // ms
    1.45 +
    1.46 +// A RegExp for characters that cannot appear in a file/directory name. This is
    1.47 +// used to sanitize the host name for indexed db to lookup whether the file is
    1.48 +// present in <profileDir>/storage/persistent/ location
    1.49 +let illegalFileNameCharacters = [
    1.50 +  "[",
    1.51 +  "\\x00-\\x25",     // Control characters \001 to \037
    1.52 +  "/:*?\\\"<>|\\\\", // Special characters
    1.53 +  "]"
    1.54 +].join("");
    1.55 +let ILLEGAL_CHAR_REGEX = new RegExp(illegalFileNameCharacters, "g");
    1.56 +
    1.57 +// Holder for all the registered storage actors.
    1.58 +let storageTypePool = new Map();
    1.59 +
    1.60 +/**
    1.61 + * Gets an accumulated list of all storage actors registered to be used to
    1.62 + * create a RetVal to define the return type of StorageActor.listStores method.
    1.63 + */
    1.64 +function getRegisteredTypes() {
    1.65 +  let registeredTypes = {};
    1.66 +  for (let store of storageTypePool.keys()) {
    1.67 +    registeredTypes[store] = store;
    1.68 +  }
    1.69 +  return registeredTypes;
    1.70 +}
    1.71 +
    1.72 +/**
    1.73 + * An async method equivalent to setTimeout but using Promises
    1.74 + *
    1.75 + * @param {number} time
    1.76 + *        The wait Ttme in milliseconds.
    1.77 + */
    1.78 +function sleep(time) {
    1.79 +  let wait = Promise.defer();
    1.80 +  let updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    1.81 +  updateTimer.initWithCallback({
    1.82 +    notify: function() {
    1.83 +      updateTimer.cancel();
    1.84 +      updateTimer = null;
    1.85 +      wait.resolve(null);
    1.86 +    }
    1.87 +  } , time, Ci.nsITimer.TYPE_ONE_SHOT);
    1.88 +  return wait.promise;
    1.89 +}
    1.90 +
    1.91 +// Cookies store object
    1.92 +types.addDictType("cookieobject", {
    1.93 +  name: "string",
    1.94 +  value: "longstring",
    1.95 +  path: "nullable:string",
    1.96 +  host: "string",
    1.97 +  isDomain: "boolean",
    1.98 +  isSecure: "boolean",
    1.99 +  isHttpOnly: "boolean",
   1.100 +  creationTime: "number",
   1.101 +  lastAccessed: "number",
   1.102 +  expires: "number"
   1.103 +});
   1.104 +
   1.105 +// Array of cookie store objects
   1.106 +types.addDictType("cookiestoreobject", {
   1.107 +  total: "number",
   1.108 +  offset: "number",
   1.109 +  data: "array:nullable:cookieobject"
   1.110 +});
   1.111 +
   1.112 +// Local Storage / Session Storage store object
   1.113 +types.addDictType("storageobject", {
   1.114 +  name: "string",
   1.115 +  value: "longstring"
   1.116 +});
   1.117 +
   1.118 +// Array of Local Storage / Session Storage store objects
   1.119 +types.addDictType("storagestoreobject", {
   1.120 +  total: "number",
   1.121 +  offset: "number",
   1.122 +  data: "array:nullable:storageobject"
   1.123 +});
   1.124 +
   1.125 +// Indexed DB store object
   1.126 +// This is a union on idb object, db metadata object and object store metadata
   1.127 +// object
   1.128 +types.addDictType("idbobject", {
   1.129 +  name: "nullable:string",
   1.130 +  db: "nullable:string",
   1.131 +  objectStore: "nullable:string",
   1.132 +  origin: "nullable:string",
   1.133 +  version: "nullable:number",
   1.134 +  objectStores: "nullable:number",
   1.135 +  keyPath: "nullable:string",
   1.136 +  autoIncrement: "nullable:boolean",
   1.137 +  indexes: "nullable:string",
   1.138 +  value: "nullable:longstring"
   1.139 +});
   1.140 +
   1.141 +// Array of Indexed DB store objects
   1.142 +types.addDictType("idbstoreobject", {
   1.143 +  total: "number",
   1.144 +  offset: "number",
   1.145 +  data: "array:nullable:idbobject"
   1.146 +});
   1.147 +
   1.148 +// Update notification object
   1.149 +types.addDictType("storeUpdateObject", {
   1.150 +  changed: "nullable:json",
   1.151 +  deleted: "nullable:json",
   1.152 +  added: "nullable:json"
   1.153 +});
   1.154 +
   1.155 +// Helper methods to create a storage actor.
   1.156 +let StorageActors = {};
   1.157 +
   1.158 +/**
   1.159 + * Creates a default object with the common methods required by all storage
   1.160 + * actors.
   1.161 + *
   1.162 + * This default object is missing a couple of required methods that should be
   1.163 + * implemented seperately for each actor. They are namely:
   1.164 + *   - observe : Method which gets triggered on the notificaiton of the watched
   1.165 + *               topic.
   1.166 + *   - getNamesForHost : Given a host, get list of all known store names.
   1.167 + *   - getValuesForHost : Given a host (and optianally a name) get all known
   1.168 + *                        store objects.
   1.169 + *   - toStoreObject : Given a store object, convert it to the required format
   1.170 + *                     so that it can be transferred over wire.
   1.171 + *   - populateStoresForHost : Given a host, populate the map of all store
   1.172 + *                             objects for it
   1.173 + *
   1.174 + * @param {string} typeName
   1.175 + *        The typeName of the actor.
   1.176 + * @param {string} observationTopic
   1.177 + *        The topic which this actor listens to via Notification Observers.
   1.178 + * @param {string} storeObjectType
   1.179 + *        The RetVal type of the store object of this actor.
   1.180 + */
   1.181 +StorageActors.defaults = function(typeName, observationTopic, storeObjectType) {
   1.182 +  return {
   1.183 +    typeName: typeName,
   1.184 +
   1.185 +    get conn() {
   1.186 +      return this.storageActor.conn;
   1.187 +    },
   1.188 +
   1.189 +    /**
   1.190 +     * Returns a list of currently knwon hosts for the target window. This list
   1.191 +     * contains unique hosts from the window + all inner windows.
   1.192 +     */
   1.193 +    get hosts() {
   1.194 +      let hosts = new Set();
   1.195 +      for (let {location} of this.storageActor.windows) {
   1.196 +        hosts.add(this.getHostName(location));
   1.197 +      }
   1.198 +      return hosts;
   1.199 +    },
   1.200 +
   1.201 +    /**
   1.202 +     * Returns all the windows present on the page. Includes main window + inner
   1.203 +     * iframe windows.
   1.204 +     */
   1.205 +    get windows() {
   1.206 +      return this.storageActor.windows;
   1.207 +    },
   1.208 +
   1.209 +    /**
   1.210 +     * Converts the window.location object into host.
   1.211 +     */
   1.212 +    getHostName: function(location) {
   1.213 +      return location.hostname || location.href;
   1.214 +    },
   1.215 +
   1.216 +    initialize: function(storageActor) {
   1.217 +      protocol.Actor.prototype.initialize.call(this, null);
   1.218 +
   1.219 +      this.storageActor = storageActor;
   1.220 +
   1.221 +      this.populateStoresForHosts();
   1.222 +      if (observationTopic) {
   1.223 +        Services.obs.addObserver(this, observationTopic, false);
   1.224 +      }
   1.225 +      this.onWindowReady = this.onWindowReady.bind(this);
   1.226 +      this.onWindowDestroyed = this.onWindowDestroyed.bind(this);
   1.227 +      events.on(this.storageActor, "window-ready", this.onWindowReady);
   1.228 +      events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed);
   1.229 +    },
   1.230 +
   1.231 +    destroy: function() {
   1.232 +      this.hostVsStores = null;
   1.233 +      if (observationTopic) {
   1.234 +        Services.obs.removeObserver(this, observationTopic, false);
   1.235 +      }
   1.236 +      events.off(this.storageActor, "window-ready", this.onWindowReady);
   1.237 +      events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed);
   1.238 +    },
   1.239 +
   1.240 +    getNamesForHost: function(host) {
   1.241 +      return [...this.hostVsStores.get(host).keys()];
   1.242 +    },
   1.243 +
   1.244 +    getValuesForHost: function(host, name) {
   1.245 +      if (name) {
   1.246 +        return [this.hostVsStores.get(host).get(name)];
   1.247 +      }
   1.248 +      return [...this.hostVsStores.get(host).values()];
   1.249 +    },
   1.250 +
   1.251 +    getObjectsSize: function(host, names) {
   1.252 +      return names.length;
   1.253 +    },
   1.254 +
   1.255 +    /**
   1.256 +     * When a new window is added to the page. This generally means that a new
   1.257 +     * iframe is created, or the current window is completely reloaded.
   1.258 +     *
   1.259 +     * @param {window} window
   1.260 +     *        The window which was added.
   1.261 +     */
   1.262 +    onWindowReady: async(function*(window) {
   1.263 +      let host = this.getHostName(window.location);
   1.264 +      if (!this.hostVsStores.has(host)) {
   1.265 +        yield this.populateStoresForHost(host, window);
   1.266 +        let data = {};
   1.267 +        data[host] = this.getNamesForHost(host);
   1.268 +        this.storageActor.update("added", typeName, data);
   1.269 +      }
   1.270 +    }),
   1.271 +
   1.272 +    /**
   1.273 +     * When a window is removed from the page. This generally means that an
   1.274 +     * iframe was removed, or the current window reload is triggered.
   1.275 +     *
   1.276 +     * @param {window} window
   1.277 +     *        The window which was removed.
   1.278 +     */
   1.279 +    onWindowDestroyed: function(window) {
   1.280 +      let host = this.getHostName(window.location);
   1.281 +      if (!this.hosts.has(host)) {
   1.282 +        this.hostVsStores.delete(host);
   1.283 +        let data = {};
   1.284 +        data[host] = [];
   1.285 +        this.storageActor.update("deleted", typeName, data);
   1.286 +      }
   1.287 +    },
   1.288 +
   1.289 +    form: function(form, detail) {
   1.290 +      if (detail === "actorid") {
   1.291 +        return this.actorID;
   1.292 +      }
   1.293 +
   1.294 +      let hosts = {};
   1.295 +      for (let host of this.hosts) {
   1.296 +        hosts[host] = [];
   1.297 +      }
   1.298 +
   1.299 +      return {
   1.300 +        actor: this.actorID,
   1.301 +        hosts: hosts
   1.302 +      };
   1.303 +    },
   1.304 +
   1.305 +    /**
   1.306 +     * Populates a map of known hosts vs a map of stores vs value.
   1.307 +     */
   1.308 +    populateStoresForHosts: function() {
   1.309 +      this.hostVsStores = new Map();
   1.310 +      for (let host of this.hosts) {
   1.311 +        this.populateStoresForHost(host);
   1.312 +      }
   1.313 +    },
   1.314 +
   1.315 +    /**
   1.316 +     * Returns a list of requested store objects. Maximum values returned are
   1.317 +     * MAX_STORE_OBJECT_COUNT. This method returns paginated values whose
   1.318 +     * starting index and total size can be controlled via the options object
   1.319 +     *
   1.320 +     * @param {string} host
   1.321 +     *        The host name for which the store values are required.
   1.322 +     * @param {array:string} names
   1.323 +     *        Array containing the names of required store objects. Empty if all
   1.324 +     *        items are required.
   1.325 +     * @param {object} options
   1.326 +     *        Additional options for the request containing following properties:
   1.327 +     *         - offset {number} : The begin index of the returned array amongst
   1.328 +     *                  the total values
   1.329 +     *         - size {number} : The number of values required.
   1.330 +     *         - sortOn {string} : The values should be sorted on this property.
   1.331 +     *         - index {string} : In case of indexed db, the IDBIndex to be used
   1.332 +     *                 for fetching the values.
   1.333 +     *
   1.334 +     * @return {object} An object containing following properties:
   1.335 +     *          - offset - The actual offset of the returned array. This might
   1.336 +     *                     be different from the requested offset if that was
   1.337 +     *                     invalid
   1.338 +     *          - total - The total number of entries possible.
   1.339 +     *          - data - The requested values.
   1.340 +     */
   1.341 +    getStoreObjects: method(async(function*(host, names, options = {}) {
   1.342 +      let offset = options.offset || 0;
   1.343 +      let size = options.size || MAX_STORE_OBJECT_COUNT;
   1.344 +      if (size > MAX_STORE_OBJECT_COUNT) {
   1.345 +        size = MAX_STORE_OBJECT_COUNT;
   1.346 +      }
   1.347 +      let sortOn = options.sortOn || "name";
   1.348 +
   1.349 +      let toReturn = {
   1.350 +        offset: offset,
   1.351 +        total: 0,
   1.352 +        data: []
   1.353 +      };
   1.354 +
   1.355 +      if (names) {
   1.356 +        for (let name of names) {
   1.357 +          toReturn.data.push(
   1.358 +            // yield because getValuesForHost is async for Indexed DB
   1.359 +            ...(yield this.getValuesForHost(host, name, options))
   1.360 +          );
   1.361 +        }
   1.362 +        toReturn.total = this.getObjectsSize(host, names, options);
   1.363 +        if (offset > toReturn.total) {
   1.364 +          // In this case, toReturn.data is an empty array.
   1.365 +          toReturn.offset = toReturn.total;
   1.366 +          toReturn.data = [];
   1.367 +        }
   1.368 +        else {
   1.369 +          toReturn.data = toReturn.data.sort((a,b) => {
   1.370 +            return a[sortOn] - b[sortOn];
   1.371 +          }).slice(offset, offset + size).map(a => this.toStoreObject(a));
   1.372 +        }
   1.373 +      }
   1.374 +      else {
   1.375 +        let total = yield this.getValuesForHost(host);
   1.376 +        toReturn.total = total.length;
   1.377 +        if (offset > toReturn.total) {
   1.378 +          // In this case, toReturn.data is an empty array.
   1.379 +          toReturn.offset = offset = toReturn.total;
   1.380 +          toReturn.data = [];
   1.381 +        }
   1.382 +        else {
   1.383 +          toReturn.data = total.sort((a,b) => {
   1.384 +            return a[sortOn] - b[sortOn];
   1.385 +          }).slice(offset, offset + size)
   1.386 +            .map(object => this.toStoreObject(object));
   1.387 +        }
   1.388 +      }
   1.389 +
   1.390 +      return toReturn;
   1.391 +    }), {
   1.392 +      request: {
   1.393 +        host: Arg(0),
   1.394 +        names: Arg(1, "nullable:array:string"),
   1.395 +        options: Arg(2, "nullable:json")
   1.396 +      },
   1.397 +      response: RetVal(storeObjectType)
   1.398 +    })
   1.399 +  }
   1.400 +};
   1.401 +
   1.402 +/**
   1.403 + * Creates an actor and its corresponding front and registers it to the Storage
   1.404 + * Actor.
   1.405 + *
   1.406 + * @See StorageActors.defaults()
   1.407 + *
   1.408 + * @param {object} options
   1.409 + *        Options required by StorageActors.defaults method which are :
   1.410 + *         - typeName {string}
   1.411 + *                    The typeName of the actor.
   1.412 + *         - observationTopic {string}
   1.413 + *                            The topic which this actor listens to via
   1.414 + *                            Notification Observers.
   1.415 + *         - storeObjectType {string}
   1.416 + *                           The RetVal type of the store object of this actor.
   1.417 + * @param {object} overrides
   1.418 + *        All the methods which you want to be differnt from the ones in
   1.419 + *        StorageActors.defaults method plus the required ones described there.
   1.420 + */
   1.421 +StorageActors.createActor = function(options = {}, overrides = {}) {
   1.422 +  let actorObject = StorageActors.defaults(
   1.423 +    options.typeName,
   1.424 +    options.observationTopic || null,
   1.425 +    options.storeObjectType
   1.426 +  );
   1.427 +  for (let key in overrides) {
   1.428 +    actorObject[key] = overrides[key];
   1.429 +  }
   1.430 +
   1.431 +  let actor = protocol.ActorClass(actorObject);
   1.432 +  let front = protocol.FrontClass(actor, {
   1.433 +    form: function(form, detail) {
   1.434 +      if (detail === "actorid") {
   1.435 +        this.actorID = form;
   1.436 +        return null;
   1.437 +      }
   1.438 +
   1.439 +      this.actorID = form.actor;
   1.440 +      this.hosts = form.hosts;
   1.441 +      return null;
   1.442 +    }
   1.443 +  });
   1.444 +  storageTypePool.set(actorObject.typeName, actor);
   1.445 +}
   1.446 +
   1.447 +/**
   1.448 + * The Cookies actor and front.
   1.449 + */
   1.450 +StorageActors.createActor({
   1.451 +  typeName: "cookies",
   1.452 +  storeObjectType: "cookiestoreobject"
   1.453 +}, {
   1.454 +  initialize: function(storageActor) {
   1.455 +    protocol.Actor.prototype.initialize.call(this, null);
   1.456 +
   1.457 +    this.storageActor = storageActor;
   1.458 +
   1.459 +    this.populateStoresForHosts();
   1.460 +    Services.obs.addObserver(this, "cookie-changed", false);
   1.461 +    Services.obs.addObserver(this, "http-on-response-set-cookie", false);
   1.462 +    this.onWindowReady = this.onWindowReady.bind(this);
   1.463 +    this.onWindowDestroyed = this.onWindowDestroyed.bind(this);
   1.464 +    events.on(this.storageActor, "window-ready", this.onWindowReady);
   1.465 +    events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed);
   1.466 +  },
   1.467 +
   1.468 +  destroy: function() {
   1.469 +    this.hostVsStores = null;
   1.470 +    Services.obs.removeObserver(this, "cookie-changed", false);
   1.471 +    Services.obs.removeObserver(this, "http-on-response-set-cookie", false);
   1.472 +    events.off(this.storageActor, "window-ready", this.onWindowReady);
   1.473 +    events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed);
   1.474 +  },
   1.475 +
   1.476 +  /**
   1.477 +   * Given a cookie object, figure out all the matching hosts from the page that
   1.478 +   * the cookie belong to.
   1.479 +   */
   1.480 +  getMatchingHosts: function(cookies) {
   1.481 +    if (!cookies.length) {
   1.482 +      cookies = [cookies];
   1.483 +    }
   1.484 +    let hosts = new Set();
   1.485 +    for (let host of this.hosts) {
   1.486 +      for (let cookie of cookies) {
   1.487 +        if (this.isCookieAtHost(cookie, host)) {
   1.488 +          hosts.add(host);
   1.489 +        }
   1.490 +      }
   1.491 +    }
   1.492 +    return [...hosts];
   1.493 +  },
   1.494 +
   1.495 +  /**
   1.496 +   * Given a cookie object and a host, figure out if the cookie is valid for
   1.497 +   * that host.
   1.498 +   */
   1.499 +  isCookieAtHost: function(cookie, host) {
   1.500 +    try {
   1.501 +      cookie = cookie.QueryInterface(Ci.nsICookie)
   1.502 +                     .QueryInterface(Ci.nsICookie2);
   1.503 +    } catch(ex) {
   1.504 +      return false;
   1.505 +    }
   1.506 +    if (cookie.host == null) {
   1.507 +      return host == null;
   1.508 +    }
   1.509 +    if (cookie.host.startsWith(".")) {
   1.510 +      return host.endsWith(cookie.host);
   1.511 +    }
   1.512 +    else {
   1.513 +      return cookie.host == host;
   1.514 +    }
   1.515 +  },
   1.516 +
   1.517 +  toStoreObject: function(cookie) {
   1.518 +    if (!cookie) {
   1.519 +      return null;
   1.520 +    }
   1.521 +
   1.522 +    return {
   1.523 +      name: cookie.name,
   1.524 +      path: cookie.path || "",
   1.525 +      host: cookie.host || "",
   1.526 +      expires: (cookie.expires || 0) * 1000, // because expires is in seconds
   1.527 +      creationTime: cookie.creationTime / 1000, // because it is in micro seconds
   1.528 +      lastAccessed: cookie.lastAccessed / 1000, // - do -
   1.529 +      value: new LongStringActor(this.conn, cookie.value || ""),
   1.530 +      isDomain: cookie.isDomain,
   1.531 +      isSecure: cookie.isSecure,
   1.532 +      isHttpOnly: cookie.isHttpOnly
   1.533 +    }
   1.534 +  },
   1.535 +
   1.536 +  populateStoresForHost: function(host) {
   1.537 +    this.hostVsStores.set(host, new Map());
   1.538 +    let cookies = Services.cookies.getCookiesFromHost(host);
   1.539 +    while (cookies.hasMoreElements()) {
   1.540 +      let cookie = cookies.getNext().QueryInterface(Ci.nsICookie)
   1.541 +                          .QueryInterface(Ci.nsICookie2);
   1.542 +      if (this.isCookieAtHost(cookie, host)) {
   1.543 +        this.hostVsStores.get(host).set(cookie.name, cookie);
   1.544 +      }
   1.545 +    }
   1.546 +  },
   1.547 +
   1.548 +  /**
   1.549 +   * Converts the raw cookie string returned in http request's response header
   1.550 +   * to a nsICookie compatible object.
   1.551 +   *
   1.552 +   * @param {string} cookieString
   1.553 +   *        The raw cookie string coming from response header.
   1.554 +   * @param {string} domain
   1.555 +   *        The domain of the url of the nsiChannel the cookie corresponds to.
   1.556 +   *        This will be used when the cookie string does not have a domain.
   1.557 +   *
   1.558 +   * @returns {[object]}
   1.559 +   *          An array of nsICookie like objects representing the cookies.
   1.560 +   */
   1.561 +  parseCookieString: function(cookieString, domain) {
   1.562 +    /**
   1.563 +     * Takes a date string present in raw cookie string coming from http
   1.564 +     * request's response headers and returns the number of milliseconds elapsed
   1.565 +     * since epoch. If the date string is undefined, its probably a session
   1.566 +     * cookie so return 0.
   1.567 +     */
   1.568 +    let parseDateString = dateString => {
   1.569 +      return dateString ? new Date(dateString.replace(/-/g, " ")).getTime(): 0;
   1.570 +    };
   1.571 +
   1.572 +    let cookies = [];
   1.573 +    for (let string of cookieString.split("\n")) {
   1.574 +      let keyVals = {}, name = null;
   1.575 +      for (let keyVal of string.split(/;\s*/)) {
   1.576 +        let tokens = keyVal.split(/\s*=\s*/);
   1.577 +        if (!name) {
   1.578 +          name = tokens[0];
   1.579 +        }
   1.580 +        else {
   1.581 +          tokens[0] = tokens[0].toLowerCase();
   1.582 +        }
   1.583 +        keyVals[tokens.splice(0, 1)[0]] = tokens.join("=");
   1.584 +      }
   1.585 +      let expiresTime = parseDateString(keyVals.expires);
   1.586 +      keyVals.domain = keyVals.domain || domain;
   1.587 +      cookies.push({
   1.588 +        name: name,
   1.589 +        value: keyVals[name] || "",
   1.590 +        path: keyVals.path,
   1.591 +        host: keyVals.domain,
   1.592 +        expires: expiresTime/1000, // seconds, to match with nsiCookie.expires
   1.593 +        lastAccessed: expiresTime * 1000,
   1.594 +        // microseconds, to match with nsiCookie.lastAccessed
   1.595 +        creationTime: expiresTime * 1000,
   1.596 +        // microseconds, to match with nsiCookie.creationTime
   1.597 +        isHttpOnly: true,
   1.598 +        isSecure: keyVals.secure != null,
   1.599 +        isDomain: keyVals.domain.startsWith("."),
   1.600 +      });
   1.601 +    }
   1.602 +    return cookies;
   1.603 +  },
   1.604 +
   1.605 +  /**
   1.606 +   * Notification observer for topics "http-on-response-set-cookie" and
   1.607 +   * "cookie-change".
   1.608 +   *
   1.609 +   * @param subject
   1.610 +   *        {nsiChannel} The channel associated to the SET-COOKIE response
   1.611 +   *        header in case of "http-on-response-set-cookie" topic.
   1.612 +   *        {nsiCookie|[nsiCookie]} A single nsiCookie object or a list of it
   1.613 +   *        depending on the action. Array is only in case of "batch-deleted"
   1.614 +   *        action.
   1.615 +   * @param {string} topic
   1.616 +   *        The topic of the notification.
   1.617 +   * @param {string} action
   1.618 +   *        Additional data associated with the notification. Its the type of
   1.619 +   *        cookie change in case of "cookie-change" topic and the cookie string
   1.620 +   *        in case of "http-on-response-set-cookie" topic.
   1.621 +   */
   1.622 +  observe: function(subject, topic, action) {
   1.623 +    if (topic == "http-on-response-set-cookie") {
   1.624 +      // Some cookies got created as a result of http response header SET-COOKIE
   1.625 +      // subject here is an nsIChannel object referring to the http request.
   1.626 +      // We get the requestor of this channel and thus the content window.
   1.627 +      let channel = subject.QueryInterface(Ci.nsIChannel);
   1.628 +      let requestor = channel.notificationCallbacks ||
   1.629 +                      channel.loadGroup.notificationCallbacks;
   1.630 +      // requester can be null sometimes.
   1.631 +      let window = requestor ? requestor.getInterface(Ci.nsIDOMWindow): null;
   1.632 +      // Proceed only if this window is present on the currently targetted tab
   1.633 +      if (window && this.storageActor.isIncludedInTopLevelWindow(window)) {
   1.634 +        let host = this.getHostName(window.location);
   1.635 +        if (this.hostVsStores.has(host)) {
   1.636 +          let cookies = this.parseCookieString(action, channel.URI.host);
   1.637 +          let data = {};
   1.638 +          data[host] =  [];
   1.639 +          for (let cookie of cookies) {
   1.640 +            if (this.hostVsStores.get(host).has(cookie.name)) {
   1.641 +              continue;
   1.642 +            }
   1.643 +            this.hostVsStores.get(host).set(cookie.name, cookie);
   1.644 +            data[host].push(cookie.name);
   1.645 +          }
   1.646 +          if (data[host]) {
   1.647 +            this.storageActor.update("added", "cookies", data);
   1.648 +          }
   1.649 +        }
   1.650 +      }
   1.651 +      return null;
   1.652 +    }
   1.653 +
   1.654 +    if (topic != "cookie-changed") {
   1.655 +      return null;
   1.656 +    }
   1.657 +
   1.658 +    let hosts = this.getMatchingHosts(subject);
   1.659 +    let data = {};
   1.660 +
   1.661 +    switch(action) {
   1.662 +      case "added":
   1.663 +      case "changed":
   1.664 +        if (hosts.length) {
   1.665 +          subject = subject.QueryInterface(Ci.nsICookie)
   1.666 +                           .QueryInterface(Ci.nsICookie2);
   1.667 +          for (let host of hosts) {
   1.668 +            this.hostVsStores.get(host).set(subject.name, subject);
   1.669 +            data[host] = [subject.name];
   1.670 +          }
   1.671 +          this.storageActor.update(action, "cookies", data);
   1.672 +        }
   1.673 +        break;
   1.674 +
   1.675 +      case "deleted":
   1.676 +        if (hosts.length) {
   1.677 +          subject = subject.QueryInterface(Ci.nsICookie)
   1.678 +                           .QueryInterface(Ci.nsICookie2);
   1.679 +          for (let host of hosts) {
   1.680 +            this.hostVsStores.get(host).delete(subject.name);
   1.681 +            data[host] = [subject.name];
   1.682 +          }
   1.683 +          this.storageActor.update("deleted", "cookies", data);
   1.684 +        }
   1.685 +        break;
   1.686 +
   1.687 +      case "batch-deleted":
   1.688 +        if (hosts.length) {
   1.689 +          for (let host of hosts) {
   1.690 +            let stores = [];
   1.691 +            for (let cookie of subject) {
   1.692 +              cookie = cookie.QueryInterface(Ci.nsICookie)
   1.693 +                             .QueryInterface(Ci.nsICookie2);
   1.694 +              this.hostVsStores.get(host).delete(cookie.name);
   1.695 +              stores.push(cookie.name);
   1.696 +            }
   1.697 +            data[host] = stores;
   1.698 +          }
   1.699 +          this.storageActor.update("deleted", "cookies", data);
   1.700 +        }
   1.701 +        break;
   1.702 +
   1.703 +      case "cleared":
   1.704 +        this.storageActor.update("cleared", "cookies", hosts);
   1.705 +        break;
   1.706 +
   1.707 +      case "reload":
   1.708 +        this.storageActor.update("reloaded", "cookies", hosts);
   1.709 +        break;
   1.710 +    }
   1.711 +    return null;
   1.712 +  },
   1.713 +});
   1.714 +
   1.715 +
   1.716 +/**
   1.717 + * Helper method to create the overriden object required in
   1.718 + * StorageActors.createActor for Local Storage and Session Storage.
   1.719 + * This method exists as both Local Storage and Session Storage have almost
   1.720 + * identical actors.
   1.721 + */
   1.722 +function getObjectForLocalOrSessionStorage(type) {
   1.723 +  return {
   1.724 +    getNamesForHost: function(host) {
   1.725 +      let storage = this.hostVsStores.get(host);
   1.726 +      return [key for (key in storage)];
   1.727 +    },
   1.728 +
   1.729 +    getValuesForHost: function(host, name) {
   1.730 +      let storage = this.hostVsStores.get(host);
   1.731 +      if (name) {
   1.732 +        return [{name: name, value: storage.getItem(name)}];
   1.733 +      }
   1.734 +      return [{name: name, value: storage.getItem(name)} for (name in storage)];
   1.735 +    },
   1.736 +
   1.737 +    getHostName: function(location) {
   1.738 +      if (!location.host) {
   1.739 +        return location.href;
   1.740 +      }
   1.741 +      return location.protocol + "//" + location.host;
   1.742 +    },
   1.743 +
   1.744 +    populateStoresForHost: function(host, window) {
   1.745 +      try {
   1.746 +        this.hostVsStores.set(host, window[type]);
   1.747 +      } catch(ex) {
   1.748 +        // Exceptions happen when local or session storage is inaccessible
   1.749 +      }
   1.750 +      return null;
   1.751 +    },
   1.752 +
   1.753 +    populateStoresForHosts: function() {
   1.754 +      this.hostVsStores = new Map();
   1.755 +      try {
   1.756 +        for (let window of this.windows) {
   1.757 +          this.hostVsStores.set(this.getHostName(window.location), window[type]);
   1.758 +        }
   1.759 +      } catch(ex) {
   1.760 +        // Exceptions happen when local or session storage is inaccessible
   1.761 +      }
   1.762 +      return null;
   1.763 +    },
   1.764 +
   1.765 +    observe: function(subject, topic, data) {
   1.766 +      if (topic != "dom-storage2-changed" || data != type) {
   1.767 +        return null;
   1.768 +      }
   1.769 +
   1.770 +      let host = this.getSchemaAndHost(subject.url);
   1.771 +
   1.772 +      if (!this.hostVsStores.has(host)) {
   1.773 +        return null;
   1.774 +      }
   1.775 +
   1.776 +      let action = "changed";
   1.777 +      if (subject.key == null) {
   1.778 +        return this.storageActor.update("cleared", type, [host]);
   1.779 +      }
   1.780 +      else if (subject.oldValue == null) {
   1.781 +        action = "added";
   1.782 +      }
   1.783 +      else if (subject.newValue == null) {
   1.784 +        action = "deleted";
   1.785 +      }
   1.786 +      let updateData = {};
   1.787 +      updateData[host] = [subject.key];
   1.788 +      return this.storageActor.update(action, type, updateData);
   1.789 +    },
   1.790 +
   1.791 +    /**
   1.792 +     * Given a url, correctly determine its protocol + hostname part.
   1.793 +     */
   1.794 +    getSchemaAndHost: function(url) {
   1.795 +      let uri = Services.io.newURI(url, null, null);
   1.796 +      return uri.scheme + "://" + uri.hostPort;
   1.797 +    },
   1.798 +
   1.799 +    toStoreObject: function(item) {
   1.800 +      if (!item) {
   1.801 +        return null;
   1.802 +      }
   1.803 +
   1.804 +      return {
   1.805 +        name: item.name,
   1.806 +        value: new LongStringActor(this.conn, item.value || "")
   1.807 +      };
   1.808 +    },
   1.809 +  }
   1.810 +};
   1.811 +
   1.812 +/**
   1.813 + * The Local Storage actor and front.
   1.814 + */
   1.815 +StorageActors.createActor({
   1.816 +  typeName: "localStorage",
   1.817 +  observationTopic: "dom-storage2-changed",
   1.818 +  storeObjectType: "storagestoreobject"
   1.819 +}, getObjectForLocalOrSessionStorage("localStorage"));
   1.820 +
   1.821 +/**
   1.822 + * The Session Storage actor and front.
   1.823 + */
   1.824 +StorageActors.createActor({
   1.825 +  typeName: "sessionStorage",
   1.826 +  observationTopic: "dom-storage2-changed",
   1.827 +  storeObjectType: "storagestoreobject"
   1.828 +}, getObjectForLocalOrSessionStorage("sessionStorage"));
   1.829 +
   1.830 +
   1.831 +/**
   1.832 + * Code related to the Indexed DB actor and front
   1.833 + */
   1.834 +
   1.835 +// Metadata holder objects for various components of Indexed DB
   1.836 +
   1.837 +/**
   1.838 + * Meta data object for a particular index in an object store
   1.839 + *
   1.840 + * @param {IDBIndex} index
   1.841 + *        The particular index from the object store.
   1.842 + */
   1.843 +function IndexMetadata(index) {
   1.844 +  this._name = index.name;
   1.845 +  this._keyPath = index.keyPath;
   1.846 +  this._unique = index.unique;
   1.847 +  this._multiEntry = index.multiEntry;
   1.848 +}
   1.849 +IndexMetadata.prototype = {
   1.850 +  toObject: function() {
   1.851 +    return {
   1.852 +      name: this._name,
   1.853 +      keyPath: this._keyPath,
   1.854 +      unique: this._unique,
   1.855 +      multiEntry: this._multiEntry
   1.856 +    };
   1.857 +  }
   1.858 +};
   1.859 +
   1.860 +/**
   1.861 + * Meta data object for a particular object store in a db
   1.862 + *
   1.863 + * @param {IDBObjectStore} objectStore
   1.864 + *        The particular object store from the db.
   1.865 + */
   1.866 +function ObjectStoreMetadata(objectStore) {
   1.867 +  this._name = objectStore.name;
   1.868 +  this._keyPath = objectStore.keyPath;
   1.869 +  this._autoIncrement = objectStore.autoIncrement;
   1.870 +  this._indexes = new Map();
   1.871 +
   1.872 +  for (let i = 0; i < objectStore.indexNames.length; i++) {
   1.873 +    let index = objectStore.index(objectStore.indexNames[i]);
   1.874 +    this._indexes.set(index, new IndexMetadata(index));
   1.875 +  }
   1.876 +}
   1.877 +ObjectStoreMetadata.prototype = {
   1.878 +  toObject: function() {
   1.879 +    return {
   1.880 +      name: this._name,
   1.881 +      keyPath: this._keyPath,
   1.882 +      autoIncrement: this._autoIncrement,
   1.883 +      indexes: JSON.stringify(
   1.884 +        [index.toObject() for (index of this._indexes.values())]
   1.885 +      )
   1.886 +    };
   1.887 +  }
   1.888 +};
   1.889 +
   1.890 +/**
   1.891 + * Meta data object for a particular indexed db in a host.
   1.892 + *
   1.893 + * @param {string} origin
   1.894 + *        The host associated with this indexed db.
   1.895 + * @param {IDBDatabase} db
   1.896 + *        The particular indexed db.
   1.897 + */
   1.898 +function DatabaseMetadata(origin, db) {
   1.899 +  this._origin = origin;
   1.900 +  this._name = db.name;
   1.901 +  this._version = db.version;
   1.902 +  this._objectStores = new Map();
   1.903 +
   1.904 +  if (db.objectStoreNames.length) {
   1.905 +    let transaction = db.transaction(db.objectStoreNames, "readonly");
   1.906 +
   1.907 +    for (let i = 0; i < transaction.objectStoreNames.length; i++) {
   1.908 +      let objectStore =
   1.909 +        transaction.objectStore(transaction.objectStoreNames[i]);
   1.910 +      this._objectStores.set(transaction.objectStoreNames[i],
   1.911 +                             new ObjectStoreMetadata(objectStore));
   1.912 +    }
   1.913 +  }
   1.914 +};
   1.915 +DatabaseMetadata.prototype = {
   1.916 +  get objectStores() {
   1.917 +    return this._objectStores;
   1.918 +  },
   1.919 +
   1.920 +  toObject: function() {
   1.921 +    return {
   1.922 +      name: this._name,
   1.923 +      origin: this._origin,
   1.924 +      version: this._version,
   1.925 +      objectStores: this._objectStores.size
   1.926 +    };
   1.927 +  }
   1.928 +};
   1.929 +
   1.930 +StorageActors.createActor({
   1.931 +  typeName: "indexedDB",
   1.932 +  storeObjectType: "idbstoreobject"
   1.933 +}, {
   1.934 +  initialize: function(storageActor) {
   1.935 +    protocol.Actor.prototype.initialize.call(this, null);
   1.936 +    if (!global.indexedDB) {
   1.937 +      let idbManager = Cc["@mozilla.org/dom/indexeddb/manager;1"]
   1.938 +                         .getService(Ci.nsIIndexedDatabaseManager);
   1.939 +      idbManager.initWindowless(global);
   1.940 +    }
   1.941 +    this.objectsSize = {};
   1.942 +    this.storageActor = storageActor;
   1.943 +    this.onWindowReady = this.onWindowReady.bind(this);
   1.944 +    this.onWindowDestroyed = this.onWindowDestroyed.bind(this);
   1.945 +    events.on(this.storageActor, "window-ready", this.onWindowReady);
   1.946 +    events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed);
   1.947 +  },
   1.948 +
   1.949 +  destroy: function() {
   1.950 +    this.hostVsStores = null;
   1.951 +    this.objectsSize = null;
   1.952 +    events.off(this.storageActor, "window-ready", this.onWindowReady);
   1.953 +    events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed);
   1.954 +  },
   1.955 +
   1.956 +  getHostName: function(location) {
   1.957 +    if (!location.host) {
   1.958 +      return location.href;
   1.959 +    }
   1.960 +    return location.protocol + "//" + location.host;
   1.961 +  },
   1.962 +
   1.963 +  /**
   1.964 +   * This method is overriden and left blank as for indexedDB, this operation
   1.965 +   * cannot be performed synchronously. Thus, the preListStores method exists to
   1.966 +   * do the same task asynchronously.
   1.967 +   */
   1.968 +  populateStoresForHosts: function() {
   1.969 +  },
   1.970 +
   1.971 +  getNamesForHost: function(host) {
   1.972 +    let names = [];
   1.973 +    for (let [dbName, metaData] of this.hostVsStores.get(host)) {
   1.974 +      for (let objectStore of metaData.objectStores.keys()) {
   1.975 +        names.push(JSON.stringify([dbName, objectStore]));
   1.976 +      }
   1.977 +    }
   1.978 +    return names;
   1.979 +  },
   1.980 +
   1.981 +  /**
   1.982 +   * Returns all or requested entries from a particular objectStore from the db
   1.983 +   * in the given host.
   1.984 +   *
   1.985 +   * @param {string} host
   1.986 +   *        The given host.
   1.987 +   * @param {string} dbName
   1.988 +   *        The name of the indexed db from the above host.
   1.989 +   * @param {string} objectStore
   1.990 +   *        The name of the object store from the above db.
   1.991 +   * @param {string} id
   1.992 +   *        id of the requested entry from the above object store.
   1.993 +   *        null if all entries from the above object store are requested.
   1.994 +   * @param {string} index
   1.995 +   *        name of the IDBIndex to be iterated on while fetching entries.
   1.996 +   *        null or "name" if no index is to be iterated.
   1.997 +   * @param {number} offset
   1.998 +   *        ofsset of the entries to be fetched.
   1.999 +   * @param {number} size
  1.1000 +   *        The intended size of the entries to be fetched.
  1.1001 +   */
  1.1002 +  getObjectStoreData:
  1.1003 +  function(host, dbName, objectStore, id, index, offset, size) {
  1.1004 +    let request = this.openWithOrigin(host, dbName);
  1.1005 +    let success = Promise.defer();
  1.1006 +    let data = [];
  1.1007 +    if (!size || size > MAX_STORE_OBJECT_COUNT) {
  1.1008 +      size = MAX_STORE_OBJECT_COUNT;
  1.1009 +    }
  1.1010 +
  1.1011 +    request.onsuccess = event => {
  1.1012 +      let db = event.target.result;
  1.1013 +
  1.1014 +      let transaction = db.transaction(objectStore, "readonly");
  1.1015 +      let source = transaction.objectStore(objectStore);
  1.1016 +      if (index && index != "name") {
  1.1017 +        source = source.index(index);
  1.1018 +      }
  1.1019 +
  1.1020 +      source.count().onsuccess = event => {
  1.1021 +        let count = event.target.result;
  1.1022 +        this.objectsSize[host + dbName + objectStore + index] = count;
  1.1023 +
  1.1024 +        if (!offset) {
  1.1025 +          offset = 0;
  1.1026 +        }
  1.1027 +        else if (offset > count) {
  1.1028 +          db.close();
  1.1029 +          success.resolve([]);
  1.1030 +          return;
  1.1031 +        }
  1.1032 +
  1.1033 +        if (id) {
  1.1034 +          source.get(id).onsuccess = event => {
  1.1035 +            db.close();
  1.1036 +            success.resolve([{name: id, value: event.target.result}]);
  1.1037 +          };
  1.1038 +        }
  1.1039 +        else {
  1.1040 +          source.openCursor().onsuccess = event => {
  1.1041 +            let cursor = event.target.result;
  1.1042 +
  1.1043 +            if (!cursor || data.length >= size) {
  1.1044 +              db.close();
  1.1045 +              success.resolve(data);
  1.1046 +              return;
  1.1047 +            }
  1.1048 +            if (offset-- <= 0) {
  1.1049 +              data.push({name: cursor.key, value: cursor.value});
  1.1050 +            }
  1.1051 +            cursor.continue();
  1.1052 +          };
  1.1053 +        }
  1.1054 +      };
  1.1055 +    };
  1.1056 +    request.onerror = () => {
  1.1057 +      db.close();
  1.1058 +      success.resolve([]);
  1.1059 +    };
  1.1060 +    return success.promise;
  1.1061 +  },
  1.1062 +
  1.1063 +  /**
  1.1064 +   * Returns the total number of entries for various types of requests to
  1.1065 +   * getStoreObjects for Indexed DB actor.
  1.1066 +   *
  1.1067 +   * @param {string} host
  1.1068 +   *        The host for the request.
  1.1069 +   * @param {array:string} names
  1.1070 +   *        Array of stringified name objects for indexed db actor.
  1.1071 +   *        The request type depends on the length of any parsed entry from this
  1.1072 +   *        array. 0 length refers to request for the whole host. 1 length
  1.1073 +   *        refers to request for a particular db in the host. 2 length refers
  1.1074 +   *        to a particular object store in a db in a host. 3 length refers to
  1.1075 +   *        particular items of an object store in a db in a host.
  1.1076 +   * @param {object} options
  1.1077 +   *        An options object containing following properties:
  1.1078 +   *         - index {string} The IDBIndex for the object store in the db.
  1.1079 +   */
  1.1080 +  getObjectsSize: function(host, names, options) {
  1.1081 +    // In Indexed DB, we are interested in only the first name, as the pattern
  1.1082 +    // should follow in all entries.
  1.1083 +    let name = names[0];
  1.1084 +    let parsedName = JSON.parse(name);
  1.1085 +
  1.1086 +    if (parsedName.length == 3) {
  1.1087 +      // This is the case where specific entries from an object store were
  1.1088 +      // requested
  1.1089 +      return names.length;
  1.1090 +    }
  1.1091 +    else if (parsedName.length == 2) {
  1.1092 +      // This is the case where all entries from an object store are requested.
  1.1093 +      let index = options.index;
  1.1094 +      let [db, objectStore] = parsedName;
  1.1095 +      if (this.objectsSize[host + db + objectStore + index]) {
  1.1096 +        return this.objectsSize[host + db + objectStore + index];
  1.1097 +      }
  1.1098 +    }
  1.1099 +    else if (parsedName.length == 1) {
  1.1100 +      // This is the case where details of all object stores in a db are
  1.1101 +      // requested.
  1.1102 +      if (this.hostVsStores.has(host) &&
  1.1103 +          this.hostVsStores.get(host).has(parsedName[0])) {
  1.1104 +        return this.hostVsStores.get(host).get(parsedName[0]).objectStores.size;
  1.1105 +      }
  1.1106 +    }
  1.1107 +    else if (!parsedName || !parsedName.length) {
  1.1108 +      // This is the case were details of all dbs in a host are requested.
  1.1109 +      if (this.hostVsStores.has(host)) {
  1.1110 +        return this.hostVsStores.get(host).size;
  1.1111 +      }
  1.1112 +    }
  1.1113 +    return 0;
  1.1114 +  },
  1.1115 +
  1.1116 +  getValuesForHost: async(function*(host, name = "null", options) {
  1.1117 +    name = JSON.parse(name);
  1.1118 +    if (!name || !name.length) {
  1.1119 +      // This means that details about the db in this particular host are
  1.1120 +      // requested.
  1.1121 +      let dbs = [];
  1.1122 +      if (this.hostVsStores.has(host)) {
  1.1123 +        for (let [dbName, db] of this.hostVsStores.get(host)) {
  1.1124 +          dbs.push(db.toObject());
  1.1125 +        }
  1.1126 +      }
  1.1127 +      return dbs;
  1.1128 +    }
  1.1129 +    let [db, objectStore, id] = name;
  1.1130 +    if (!objectStore) {
  1.1131 +      // This means that details about all the object stores in this db are
  1.1132 +      // requested.
  1.1133 +      let objectStores = [];
  1.1134 +      if (this.hostVsStores.has(host) && this.hostVsStores.get(host).has(db)) {
  1.1135 +        for (let objectStore of this.hostVsStores.get(host).get(db).objectStores) {
  1.1136 +          objectStores.push(objectStore[1].toObject());
  1.1137 +        }
  1.1138 +      }
  1.1139 +      return objectStores;
  1.1140 +    }
  1.1141 +    // Get either all entries from the object store, or a particular id
  1.1142 +    return yield this.getObjectStoreData(host, db, objectStore, id,
  1.1143 +                                         options.index, options.size);
  1.1144 +  }),
  1.1145 +
  1.1146 +  /**
  1.1147 +   * Purpose of this method is same as populateStoresForHosts but this is async.
  1.1148 +   * This exact same operation cannot be performed in populateStoresForHosts
  1.1149 +   * method, as that method is called in initialize method of the actor, which
  1.1150 +   * cannot be asynchronous.
  1.1151 +   */
  1.1152 +  preListStores: async(function*() {
  1.1153 +    this.hostVsStores = new Map();
  1.1154 +    for (let host of this.hosts) {
  1.1155 +      yield this.populateStoresForHost(host);
  1.1156 +    }
  1.1157 +  }),
  1.1158 +
  1.1159 +  populateStoresForHost: async(function*(host) {
  1.1160 +    let storeMap = new Map();
  1.1161 +    for (let name of (yield this.getDBNamesForHost(host))) {
  1.1162 +      storeMap.set(name, yield this.getDBMetaData(host, name));
  1.1163 +    }
  1.1164 +    this.hostVsStores.set(host, storeMap);
  1.1165 +  }),
  1.1166 +
  1.1167 +  /**
  1.1168 +   * Removes any illegal characters from the host name to make it a valid file
  1.1169 +   * name.
  1.1170 +   */
  1.1171 +  getSanitizedHost: function(host) {
  1.1172 +    return host.replace(ILLEGAL_CHAR_REGEX, "+");
  1.1173 +  },
  1.1174 +
  1.1175 +  /**
  1.1176 +   * Opens an indexed db connection for the given `host` and database `name`.
  1.1177 +   */
  1.1178 +  openWithOrigin: function(host, name) {
  1.1179 +    let principal;
  1.1180 +
  1.1181 +    if (/^(about:|chrome:)/.test(host)) {
  1.1182 +      principal = Services.scriptSecurityManager.getSystemPrincipal();
  1.1183 +    }
  1.1184 +    else {
  1.1185 +      let uri = Services.io.newURI(host, null, null);
  1.1186 +      principal = Services.scriptSecurityManager.getCodebasePrincipal(uri);
  1.1187 +    }
  1.1188 +
  1.1189 +    return indexedDB.openForPrincipal(principal, name);
  1.1190 +  },
  1.1191 +
  1.1192 +  /**
  1.1193 +   * Fetches and stores all the metadata information for the given database
  1.1194 +   * `name` for the given `host`. The stored metadata information is of
  1.1195 +   * `DatabaseMetadata` type.
  1.1196 +   */
  1.1197 +  getDBMetaData: function(host, name) {
  1.1198 +    let request = this.openWithOrigin(host, name);
  1.1199 +    let success = Promise.defer();
  1.1200 +    request.onsuccess = event => {
  1.1201 +      let db = event.target.result;
  1.1202 +
  1.1203 +      let dbData = new DatabaseMetadata(host, db);
  1.1204 +      db.close();
  1.1205 +      success.resolve(dbData);
  1.1206 +    };
  1.1207 +    request.onerror = event => {
  1.1208 +      console.error("Error opening indexeddb database " + name + " for host " +
  1.1209 +                    host);
  1.1210 +      success.resolve(null);
  1.1211 +    };
  1.1212 +    return success.promise;
  1.1213 +  },
  1.1214 +
  1.1215 +  /**
  1.1216 +   * Retrives the proper indexed db database name from the provided .sqlite file
  1.1217 +   * location.
  1.1218 +   */
  1.1219 +  getNameFromDatabaseFile: async(function*(path) {
  1.1220 +    let connection = null;
  1.1221 +    let retryCount = 0;
  1.1222 +
  1.1223 +    // Content pages might be having an open transaction for the same indexed db
  1.1224 +    // which this sqlite file belongs to. In that case, sqlite.openConnection
  1.1225 +    // will throw. Thus we retey for some time to see if lock is removed.
  1.1226 +    while (!connection && retryCount++ < 25) {
  1.1227 +      try {
  1.1228 +        connection = yield Sqlite.openConnection({ path: path });
  1.1229 +      }
  1.1230 +      catch (ex) {
  1.1231 +        // Continuously retrying is overkill. Waiting for 100ms before next try
  1.1232 +        yield sleep(100);
  1.1233 +      }
  1.1234 +    }
  1.1235 +
  1.1236 +    if (!connection) {
  1.1237 +      return null;
  1.1238 +    }
  1.1239 +
  1.1240 +    let rows = yield connection.execute("SELECT name FROM database");
  1.1241 +    if (rows.length != 1) {
  1.1242 +      return null;
  1.1243 +    }
  1.1244 +
  1.1245 +    let name = rows[0].getResultByName("name");
  1.1246 +
  1.1247 +    yield connection.close();
  1.1248 +
  1.1249 +    return name;
  1.1250 +  }),
  1.1251 +
  1.1252 +  /**
  1.1253 +   * Fetches all the databases and their metadata for the given `host`.
  1.1254 +   */
  1.1255 +  getDBNamesForHost: async(function*(host) {
  1.1256 +    let sanitizedHost = this.getSanitizedHost(host);
  1.1257 +    let directory = OS.Path.join(OS.Constants.Path.profileDir, "storage",
  1.1258 +                                 "persistent", sanitizedHost, "idb");
  1.1259 +
  1.1260 +    let exists = yield OS.File.exists(directory);
  1.1261 +    if (!exists && host.startsWith("about:")) {
  1.1262 +      // try for moz-safe-about directory
  1.1263 +      sanitizedHost = this.getSanitizedHost("moz-safe-" + host);
  1.1264 +      directory = OS.Path.join(OS.Constants.Path.profileDir, "storage",
  1.1265 +                               "persistent", sanitizedHost, "idb");
  1.1266 +      exists = yield OS.File.exists(directory);
  1.1267 +    }
  1.1268 +    if (!exists) {
  1.1269 +      return [];
  1.1270 +    }
  1.1271 +
  1.1272 +    let names = [];
  1.1273 +    let dirIterator = new OS.File.DirectoryIterator(directory);
  1.1274 +    try {
  1.1275 +      yield dirIterator.forEach(file => {
  1.1276 +        // Skip directories.
  1.1277 +        if (file.isDir) {
  1.1278 +          return null;
  1.1279 +        }
  1.1280 +
  1.1281 +        // Skip any non-sqlite files.
  1.1282 +        if (!file.name.endsWith(".sqlite")) {
  1.1283 +          return null;
  1.1284 +        }
  1.1285 +
  1.1286 +        return this.getNameFromDatabaseFile(file.path).then(name => {
  1.1287 +          if (name) {
  1.1288 +            names.push(name);
  1.1289 +          }
  1.1290 +          return null;
  1.1291 +        });
  1.1292 +      });
  1.1293 +    }
  1.1294 +    finally {
  1.1295 +      dirIterator.close();
  1.1296 +    }
  1.1297 +    return names;
  1.1298 +  }),
  1.1299 +
  1.1300 +  /**
  1.1301 +   * Returns the over-the-wire implementation of the indexed db entity.
  1.1302 +   */
  1.1303 +  toStoreObject: function(item) {
  1.1304 +    if (!item) {
  1.1305 +      return null;
  1.1306 +    }
  1.1307 +
  1.1308 +    if (item.indexes) {
  1.1309 +      // Object store meta data
  1.1310 +      return {
  1.1311 +        objectStore: item.name,
  1.1312 +        keyPath: item.keyPath,
  1.1313 +        autoIncrement: item.autoIncrement,
  1.1314 +        indexes: item.indexes
  1.1315 +      };
  1.1316 +    }
  1.1317 +    if (item.objectStores) {
  1.1318 +      // DB meta data
  1.1319 +      return {
  1.1320 +        db: item.name,
  1.1321 +        origin: item.origin,
  1.1322 +        version: item.version,
  1.1323 +        objectStores: item.objectStores
  1.1324 +      };
  1.1325 +    }
  1.1326 +    // Indexed db entry
  1.1327 +    return {
  1.1328 +      name: item.name,
  1.1329 +      value: new LongStringActor(this.conn, JSON.stringify(item.value))
  1.1330 +    };
  1.1331 +  },
  1.1332 +
  1.1333 +  form: function(form, detail) {
  1.1334 +    if (detail === "actorid") {
  1.1335 +      return this.actorID;
  1.1336 +    }
  1.1337 +
  1.1338 +    let hosts = {};
  1.1339 +    for (let host of this.hosts) {
  1.1340 +      hosts[host] = this.getNamesForHost(host);
  1.1341 +    }
  1.1342 +
  1.1343 +    return {
  1.1344 +      actor: this.actorID,
  1.1345 +      hosts: hosts
  1.1346 +    };
  1.1347 +  },
  1.1348 +});
  1.1349 +
  1.1350 +/**
  1.1351 + * The main Storage Actor.
  1.1352 + */
  1.1353 +let StorageActor = exports.StorageActor = protocol.ActorClass({
  1.1354 +  typeName: "storage",
  1.1355 +
  1.1356 +  get window() {
  1.1357 +    return this.parentActor.window;
  1.1358 +  },
  1.1359 +
  1.1360 +  get document() {
  1.1361 +    return this.parentActor.window.document;
  1.1362 +  },
  1.1363 +
  1.1364 +  get windows() {
  1.1365 +    return this.childWindowPool;
  1.1366 +  },
  1.1367 +
  1.1368 +  /**
  1.1369 +   * List of event notifications that the server can send to the client.
  1.1370 +   *
  1.1371 +   *  - stores-update : When any store object in any storage type changes.
  1.1372 +   *  - stores-cleared : When all the store objects are removed.
  1.1373 +   *  - stores-reloaded : When all stores are reloaded. This generally mean that
  1.1374 +   *                      we should refetch everything again.
  1.1375 +   */
  1.1376 +  events: {
  1.1377 +    "stores-update": {
  1.1378 +      type: "storesUpdate",
  1.1379 +      data: Arg(0, "storeUpdateObject")
  1.1380 +    },
  1.1381 +    "stores-cleared": {
  1.1382 +      type: "storesCleared",
  1.1383 +      data: Arg(0, "json")
  1.1384 +    },
  1.1385 +    "stores-reloaded": {
  1.1386 +      type: "storesRelaoded",
  1.1387 +      data: Arg(0, "json")
  1.1388 +    }
  1.1389 +  },
  1.1390 +
  1.1391 +  initialize: function (conn, tabActor) {
  1.1392 +    protocol.Actor.prototype.initialize.call(this, null);
  1.1393 +
  1.1394 +    this.conn = conn;
  1.1395 +    this.parentActor = tabActor;
  1.1396 +
  1.1397 +    this.childActorPool = new Map();
  1.1398 +    this.childWindowPool = new Set();
  1.1399 +
  1.1400 +    // Fetch all the inner iframe windows in this tab.
  1.1401 +    this.fetchChildWindows(this.parentActor.docShell);
  1.1402 +
  1.1403 +    // Initialize the registered store types
  1.1404 +    for (let [store, actor] of storageTypePool) {
  1.1405 +      this.childActorPool.set(store, new actor(this));
  1.1406 +    }
  1.1407 +
  1.1408 +    // Notifications that help us keep track of newly added windows and windows
  1.1409 +    // that got removed
  1.1410 +    Services.obs.addObserver(this, "content-document-global-created", false);
  1.1411 +    Services.obs.addObserver(this, "inner-window-destroyed", false);
  1.1412 +    this.onPageChange = this.onPageChange.bind(this);
  1.1413 +    tabActor.browser.addEventListener("pageshow", this.onPageChange, true);
  1.1414 +    tabActor.browser.addEventListener("pagehide", this.onPageChange, true);
  1.1415 +
  1.1416 +    this.destroyed = false;
  1.1417 +    this.boundUpdate = {};
  1.1418 +    // The time which periodically flushes and transfers the updated store
  1.1419 +    // objects.
  1.1420 +    this.updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  1.1421 +    this.updateTimer.initWithCallback(this , UPDATE_INTERVAL,
  1.1422 +      Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP);
  1.1423 +
  1.1424 +    // Layout helper for window.parent and window.top helper methods that work
  1.1425 +    // accross devices.
  1.1426 +    this.layoutHelper = new LayoutHelpers(this.window);
  1.1427 +  },
  1.1428 +
  1.1429 +  destroy: function() {
  1.1430 +    this.updateTimer.cancel();
  1.1431 +    this.updateTimer = null;
  1.1432 +    this.layoutHelper = null;
  1.1433 +    // Remove observers
  1.1434 +    Services.obs.removeObserver(this, "content-document-global-created", false);
  1.1435 +    Services.obs.removeObserver(this, "inner-window-destroyed", false);
  1.1436 +    this.destroyed = true;
  1.1437 +    if (this.parentActor.browser) {
  1.1438 +      this.parentActor.browser.removeEventListener(
  1.1439 +        "pageshow", this.onPageChange, true);
  1.1440 +      this.parentActor.browser.removeEventListener(
  1.1441 +        "pagehide", this.onPageChange, true);
  1.1442 +    }
  1.1443 +    // Destroy the registered store types
  1.1444 +    for (let actor of this.childActorPool.values()) {
  1.1445 +      actor.destroy();
  1.1446 +    }
  1.1447 +    this.childActorPool.clear();
  1.1448 +    this.childWindowPool.clear();
  1.1449 +    this.childWindowPool = this.childActorPool = null;
  1.1450 +  },
  1.1451 +
  1.1452 +  /**
  1.1453 +   * Given a docshell, recursively find otu all the child windows from it.
  1.1454 +   *
  1.1455 +   * @param {nsIDocShell} item
  1.1456 +   *        The docshell from which all inner windows need to be extracted.
  1.1457 +   */
  1.1458 +  fetchChildWindows: function(item) {
  1.1459 +    let docShell = item.QueryInterface(Ci.nsIDocShell)
  1.1460 +                       .QueryInterface(Ci.nsIDocShellTreeItem);
  1.1461 +    if (!docShell.contentViewer) {
  1.1462 +      return null;
  1.1463 +    }
  1.1464 +    let window = docShell.contentViewer.DOMDocument.defaultView;
  1.1465 +    if (window.location.href == "about:blank") {
  1.1466 +      // Skip out about:blank windows as Gecko creates them multiple times while
  1.1467 +      // creating any global.
  1.1468 +      return null;
  1.1469 +    }
  1.1470 +    this.childWindowPool.add(window);
  1.1471 +    for (let i = 0; i < docShell.childCount; i++) {
  1.1472 +      let child = docShell.getChildAt(i);
  1.1473 +      this.fetchChildWindows(child);
  1.1474 +    }
  1.1475 +    return null;
  1.1476 +  },
  1.1477 +
  1.1478 +  isIncludedInTopLevelWindow: function(window) {
  1.1479 +    return this.layoutHelper.isIncludedInTopLevelWindow(window);
  1.1480 +  },
  1.1481 +
  1.1482 +  getWindowFromInnerWindowID: function(innerID) {
  1.1483 +    innerID = innerID.QueryInterface(Ci.nsISupportsPRUint64).data;
  1.1484 +    for (let win of this.childWindowPool.values()) {
  1.1485 +      let id = win.QueryInterface(Ci.nsIInterfaceRequestor)
  1.1486 +                   .getInterface(Ci.nsIDOMWindowUtils)
  1.1487 +                   .currentInnerWindowID;
  1.1488 +      if (id == innerID) {
  1.1489 +        return win;
  1.1490 +      }
  1.1491 +    }
  1.1492 +    return null;
  1.1493 +  },
  1.1494 +
  1.1495 +  /**
  1.1496 +   * Event handler for any docshell update. This lets us figure out whenever
  1.1497 +   * any new window is added, or an existing window is removed.
  1.1498 +   */
  1.1499 +  observe: function(subject, topic, data) {
  1.1500 +    if (subject.location &&
  1.1501 +        (!subject.location.href || subject.location.href == "about:blank")) {
  1.1502 +      return null;
  1.1503 +    }
  1.1504 +    if (topic == "content-document-global-created" &&
  1.1505 +        this.isIncludedInTopLevelWindow(subject)) {
  1.1506 +      this.childWindowPool.add(subject);
  1.1507 +      events.emit(this, "window-ready", subject);
  1.1508 +    }
  1.1509 +    else if (topic == "inner-window-destroyed") {
  1.1510 +      let window = this.getWindowFromInnerWindowID(subject);
  1.1511 +      if (window) {
  1.1512 +        this.childWindowPool.delete(window);
  1.1513 +        events.emit(this, "window-destroyed", window);
  1.1514 +      }
  1.1515 +    }
  1.1516 +    return null;
  1.1517 +  },
  1.1518 +
  1.1519 +  /**
  1.1520 +   * Called on "pageshow" or "pagehide" event on the chromeEventHandler of
  1.1521 +   * current tab.
  1.1522 +   *
  1.1523 +   * @param {event} The event object passed to the handler. We are using these
  1.1524 +   *        three properties from the event:
  1.1525 +   *         - target {document} The document corresponding to the event.
  1.1526 +   *         - type {string} Name of the event - "pageshow" or "pagehide".
  1.1527 +   *         - persisted {boolean} true if there was no
  1.1528 +   *                     "content-document-global-created" notification along
  1.1529 +   *                     this event.
  1.1530 +   */
  1.1531 +  onPageChange: function({target, type, persisted}) {
  1.1532 +    if (this.destroyed) {
  1.1533 +      return;
  1.1534 +    }
  1.1535 +    let window = target.defaultView;
  1.1536 +    if (type == "pagehide" && this.childWindowPool.delete(window)) {
  1.1537 +      events.emit(this, "window-destroyed", window)
  1.1538 +    }
  1.1539 +    else if (type == "pageshow" && persisted  && window.location.href &&
  1.1540 +             window.location.href != "about:blank" &&
  1.1541 +             this.isIncludedInTopLevelWindow(window)) {
  1.1542 +      this.childWindowPool.add(window);
  1.1543 +      events.emit(this, "window-ready", window);
  1.1544 +    }
  1.1545 +  },
  1.1546 +
  1.1547 +  /**
  1.1548 +   * Lists the available hosts for all the registered storage types.
  1.1549 +   *
  1.1550 +   * @returns {object} An object containing with the following structure:
  1.1551 +   *  - <storageType> : [{
  1.1552 +   *      actor: <actorId>,
  1.1553 +   *      host: <hostname>
  1.1554 +   *    }]
  1.1555 +   */
  1.1556 +  listStores: method(async(function*() {
  1.1557 +    let toReturn = {};
  1.1558 +    for (let [name, value] of this.childActorPool) {
  1.1559 +      if (value.preListStores) {
  1.1560 +        yield value.preListStores();
  1.1561 +      }
  1.1562 +      toReturn[name] = value;
  1.1563 +    }
  1.1564 +    return toReturn;
  1.1565 +  }), {
  1.1566 +    response: RetVal(types.addDictType("storelist", getRegisteredTypes()))
  1.1567 +  }),
  1.1568 +
  1.1569 +  /**
  1.1570 +   * Notifies the client front with the updates in stores at regular intervals.
  1.1571 +   */
  1.1572 +  notify: function() {
  1.1573 +    if (!this.updatePending || this.updatingUpdateObject) {
  1.1574 +      return null;
  1.1575 +    }
  1.1576 +    events.emit(this, "stores-update", this.boundUpdate);
  1.1577 +    this.boundUpdate = {};
  1.1578 +    this.updatePending = false;
  1.1579 +    return null;
  1.1580 +  },
  1.1581 +
  1.1582 +  /**
  1.1583 +   * This method is called by the registered storage types so as to tell the
  1.1584 +   * Storage Actor that there are some changes in the stores. Storage Actor then
  1.1585 +   * notifies the client front about these changes at regular (UPDATE_INTERVAL)
  1.1586 +   * interval.
  1.1587 +   *
  1.1588 +   * @param {string} action
  1.1589 +   *        The type of change. One of "added", "changed" or "deleted"
  1.1590 +   * @param {string} storeType
  1.1591 +   *        The storage actor in which this change has occurred.
  1.1592 +   * @param {object} data
  1.1593 +   *        The update object. This object is of the following format:
  1.1594 +   *         - {
  1.1595 +   *             <host1>: [<store_names1>, <store_name2>...],
  1.1596 +   *             <host2>: [<store_names34>...],
  1.1597 +   *           }
  1.1598 +   *           Where host1, host2 are the host in which this change happened and
  1.1599 +   *           [<store_namesX] is an array of the names of the changed store
  1.1600 +   *           objects. Leave it empty if the host was completely removed.
  1.1601 +   *        When the action is "reloaded" or "cleared", `data` is an array of
  1.1602 +   *        hosts for which the stores were cleared or reloaded.
  1.1603 +   */
  1.1604 +  update: function(action, storeType, data) {
  1.1605 +    if (action == "cleared" || action == "reloaded") {
  1.1606 +      let toSend = {};
  1.1607 +      toSend[storeType] = data
  1.1608 +      events.emit(this, "stores-" + action, toSend);
  1.1609 +      return null;
  1.1610 +    }
  1.1611 +
  1.1612 +    this.updatingUpdateObject = true;
  1.1613 +    if (!this.boundUpdate[action]) {
  1.1614 +      this.boundUpdate[action] = {};
  1.1615 +    }
  1.1616 +    if (!this.boundUpdate[action][storeType]) {
  1.1617 +      this.boundUpdate[action][storeType] = {};
  1.1618 +    }
  1.1619 +    this.updatePending = true;
  1.1620 +    for (let host in data) {
  1.1621 +      if (!this.boundUpdate[action][storeType][host] || action == "deleted") {
  1.1622 +        this.boundUpdate[action][storeType][host] = data[host];
  1.1623 +      }
  1.1624 +      else {
  1.1625 +        this.boundUpdate[action][storeType][host] =
  1.1626 +        this.boundUpdate[action][storeType][host].concat(data[host]);
  1.1627 +      }
  1.1628 +    }
  1.1629 +    if (action == "added") {
  1.1630 +      // If the same store name was previously deleted or changed, but now is
  1.1631 +      // added somehow, dont send the deleted or changed update.
  1.1632 +      this.removeNamesFromUpdateList("deleted", storeType, data);
  1.1633 +      this.removeNamesFromUpdateList("changed", storeType, data);
  1.1634 +    }
  1.1635 +    else if (action == "changed" && this.boundUpdate.added &&
  1.1636 +             this.boundUpdate.added[storeType]) {
  1.1637 +      // If something got added and changed at the same time, then remove those
  1.1638 +      // items from changed instead.
  1.1639 +      this.removeNamesFromUpdateList("changed", storeType,
  1.1640 +                                     this.boundUpdate.added[storeType]);
  1.1641 +    }
  1.1642 +    else if (action == "deleted") {
  1.1643 +      // If any item got delete, or a host got delete, no point in sending
  1.1644 +      // added or changed update
  1.1645 +      this.removeNamesFromUpdateList("added", storeType, data);
  1.1646 +      this.removeNamesFromUpdateList("changed", storeType, data);
  1.1647 +      for (let host in data) {
  1.1648 +        if (data[host].length == 0 && this.boundUpdate.added &&
  1.1649 +            this.boundUpdate.added[storeType] &&
  1.1650 +            this.boundUpdate.added[storeType][host]) {
  1.1651 +          delete this.boundUpdate.added[storeType][host];
  1.1652 +        }
  1.1653 +        if (data[host].length == 0 && this.boundUpdate.changed &&
  1.1654 +            this.boundUpdate.changed[storeType] &&
  1.1655 +            this.boundUpdate.changed[storeType][host]) {
  1.1656 +          delete this.boundUpdate.changed[storeType][host];
  1.1657 +        }
  1.1658 +      }
  1.1659 +    }
  1.1660 +    this.updatingUpdateObject = false;
  1.1661 +    return null;
  1.1662 +  },
  1.1663 +
  1.1664 +  /**
  1.1665 +   * This method removes data from the this.boundUpdate object in the same
  1.1666 +   * manner like this.update() adds data to it.
  1.1667 +   *
  1.1668 +   * @param {string} action
  1.1669 +   *        The type of change. One of "added", "changed" or "deleted"
  1.1670 +   * @param {string} storeType
  1.1671 +   *        The storage actor for which you want to remove the updates data.
  1.1672 +   * @param {object} data
  1.1673 +   *        The update object. This object is of the following format:
  1.1674 +   *         - {
  1.1675 +   *             <host1>: [<store_names1>, <store_name2>...],
  1.1676 +   *             <host2>: [<store_names34>...],
  1.1677 +   *           }
  1.1678 +   *           Where host1, host2 are the hosts which you want to remove and
  1.1679 +   *           [<store_namesX] is an array of the names of the store objects.
  1.1680 +   */
  1.1681 +  removeNamesFromUpdateList: function(action, storeType, data) {
  1.1682 +    for (let host in data) {
  1.1683 +      if (this.boundUpdate[action] && this.boundUpdate[action][storeType] &&
  1.1684 +          this.boundUpdate[action][storeType][host]) {
  1.1685 +        for (let name in data[host]) {
  1.1686 +          let index = this.boundUpdate[action][storeType][host].indexOf(name);
  1.1687 +          if (index > -1) {
  1.1688 +            this.boundUpdate[action][storeType][host].splice(index, 1);
  1.1689 +          }
  1.1690 +        }
  1.1691 +        if (!this.boundUpdate[action][storeType][host].length) {
  1.1692 +          delete this.boundUpdate[action][storeType][host];
  1.1693 +        }
  1.1694 +      }
  1.1695 +    }
  1.1696 +    return null;
  1.1697 +  }
  1.1698 +});
  1.1699 +
  1.1700 +/**
  1.1701 + * Front for the Storage Actor.
  1.1702 + */
  1.1703 +let StorageFront = exports.StorageFront = protocol.FrontClass(StorageActor, {
  1.1704 +  initialize: function(client, tabForm) {
  1.1705 +    protocol.Front.prototype.initialize.call(this, client);
  1.1706 +    this.actorID = tabForm.storageActor;
  1.1707 +
  1.1708 +    client.addActorPool(this);
  1.1709 +    this.manage(this);
  1.1710 +  }
  1.1711 +});

mercurial