toolkit/devtools/server/actors/storage.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial