toolkit/devtools/server/actors/storage.js

branch
TOR_BUG_9701
changeset 15
b8a032363ba2
equal deleted inserted replaced
-1:000000000000 0:cbef5ab61327
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/. */
4
5 "use strict";
6
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");
13
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");
18
19 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
20 "resource://gre/modules/Sqlite.jsm");
21
22 XPCOMUtils.defineLazyModuleGetter(this, "OS",
23 "resource://gre/modules/osfile.jsm");
24
25 exports.register = function(handle) {
26 handle.addTabActor(StorageActor, "storageActor");
27 };
28
29 exports.unregister = function(handle) {
30 handle.removeTabActor(StorageActor);
31 };
32
33 // Global required for window less Indexed DB instantiation.
34 let global = this;
35
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
42
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");
53
54 // Holder for all the registered storage actors.
55 let storageTypePool = new Map();
56
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 }
68
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 }
87
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 });
101
102 // Array of cookie store objects
103 types.addDictType("cookiestoreobject", {
104 total: "number",
105 offset: "number",
106 data: "array:nullable:cookieobject"
107 });
108
109 // Local Storage / Session Storage store object
110 types.addDictType("storageobject", {
111 name: "string",
112 value: "longstring"
113 });
114
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 });
121
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 });
137
138 // Array of Indexed DB store objects
139 types.addDictType("idbstoreobject", {
140 total: "number",
141 offset: "number",
142 data: "array:nullable:idbobject"
143 });
144
145 // Update notification object
146 types.addDictType("storeUpdateObject", {
147 changed: "nullable:json",
148 deleted: "nullable:json",
149 added: "nullable:json"
150 });
151
152 // Helper methods to create a storage actor.
153 let StorageActors = {};
154
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,
181
182 get conn() {
183 return this.storageActor.conn;
184 },
185
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 },
197
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 },
205
206 /**
207 * Converts the window.location object into host.
208 */
209 getHostName: function(location) {
210 return location.hostname || location.href;
211 },
212
213 initialize: function(storageActor) {
214 protocol.Actor.prototype.initialize.call(this, null);
215
216 this.storageActor = storageActor;
217
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 },
227
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 },
236
237 getNamesForHost: function(host) {
238 return [...this.hostVsStores.get(host).keys()];
239 },
240
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 },
247
248 getObjectsSize: function(host, names) {
249 return names.length;
250 },
251
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 }),
268
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 },
285
286 form: function(form, detail) {
287 if (detail === "actorid") {
288 return this.actorID;
289 }
290
291 let hosts = {};
292 for (let host of this.hosts) {
293 hosts[host] = [];
294 }
295
296 return {
297 actor: this.actorID,
298 hosts: hosts
299 };
300 },
301
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 },
311
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";
345
346 let toReturn = {
347 offset: offset,
348 total: 0,
349 data: []
350 };
351
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 }
386
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 };
398
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 }
427
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 }
435
436 this.actorID = form.actor;
437 this.hosts = form.hosts;
438 return null;
439 }
440 });
441 storageTypePool.set(actorObject.typeName, actor);
442 }
443
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);
453
454 this.storageActor = storageActor;
455
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 },
464
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 },
472
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 },
491
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 },
513
514 toStoreObject: function(cookie) {
515 if (!cookie) {
516 return null;
517 }
518
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 },
532
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 },
544
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 };
568
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 },
601
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 }
650
651 if (topic != "cookie-changed") {
652 return null;
653 }
654
655 let hosts = this.getMatchingHosts(subject);
656 let data = {};
657
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;
671
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;
683
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;
699
700 case "cleared":
701 this.storageActor.update("cleared", "cookies", hosts);
702 break;
703
704 case "reload":
705 this.storageActor.update("reloaded", "cookies", hosts);
706 break;
707 }
708 return null;
709 },
710 });
711
712
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 },
725
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 },
733
734 getHostName: function(location) {
735 if (!location.host) {
736 return location.href;
737 }
738 return location.protocol + "//" + location.host;
739 },
740
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 },
749
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 },
761
762 observe: function(subject, topic, data) {
763 if (topic != "dom-storage2-changed" || data != type) {
764 return null;
765 }
766
767 let host = this.getSchemaAndHost(subject.url);
768
769 if (!this.hostVsStores.has(host)) {
770 return null;
771 }
772
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 },
787
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 },
795
796 toStoreObject: function(item) {
797 if (!item) {
798 return null;
799 }
800
801 return {
802 name: item.name,
803 value: new LongStringActor(this.conn, item.value || "")
804 };
805 },
806 }
807 };
808
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"));
817
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"));
826
827
828 /**
829 * Code related to the Indexed DB actor and front
830 */
831
832 // Metadata holder objects for various components of Indexed DB
833
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 };
856
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();
868
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 };
886
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();
900
901 if (db.objectStoreNames.length) {
902 let transaction = db.transaction(db.objectStoreNames, "readonly");
903
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 },
916
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 };
926
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 },
945
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 },
952
953 getHostName: function(location) {
954 if (!location.host) {
955 return location.href;
956 }
957 return location.protocol + "//" + location.host;
958 },
959
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 },
967
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 },
977
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;
1006 }
1007
1008 request.onsuccess = event => {
1009 let db = event.target.result;
1010
1011 let transaction = db.transaction(objectStore, "readonly");
1012 let source = transaction.objectStore(objectStore);
1013 if (index && index != "name") {
1014 source = source.index(index);
1015 }
1016
1017 source.count().onsuccess = event => {
1018 let count = event.target.result;
1019 this.objectsSize[host + dbName + objectStore + index] = count;
1020
1021 if (!offset) {
1022 offset = 0;
1023 }
1024 else if (offset > count) {
1025 db.close();
1026 success.resolve([]);
1027 return;
1028 }
1029
1030 if (id) {
1031 source.get(id).onsuccess = event => {
1032 db.close();
1033 success.resolve([{name: id, value: event.target.result}]);
1034 };
1035 }
1036 else {
1037 source.openCursor().onsuccess = event => {
1038 let cursor = event.target.result;
1039
1040 if (!cursor || data.length >= size) {
1041 db.close();
1042 success.resolve(data);
1043 return;
1044 }
1045 if (offset-- <= 0) {
1046 data.push({name: cursor.key, value: cursor.value});
1047 }
1048 cursor.continue();
1049 };
1050 }
1051 };
1052 };
1053 request.onerror = () => {
1054 db.close();
1055 success.resolve([]);
1056 };
1057 return success.promise;
1058 },
1059
1060 /**
1061 * Returns the total number of entries for various types of requests to
1062 * getStoreObjects for Indexed DB actor.
1063 *
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);
1082
1083 if (parsedName.length == 3) {
1084 // This is the case where specific entries from an object store were
1085 // requested
1086 return names.length;
1087 }
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];
1094 }
1095 }
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;
1102 }
1103 }
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;
1108 }
1109 }
1110 return 0;
1111 },
1112
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());
1122 }
1123 }
1124 return dbs;
1125 }
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());
1134 }
1135 }
1136 return objectStores;
1137 }
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 }),
1142
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);
1153 }
1154 }),
1155
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));
1160 }
1161 this.hostVsStores.set(host, storeMap);
1162 }),
1163
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 },
1171
1172 /**
1173 * Opens an indexed db connection for the given `host` and database `name`.
1174 */
1175 openWithOrigin: function(host, name) {
1176 let principal;
1177
1178 if (/^(about:|chrome:)/.test(host)) {
1179 principal = Services.scriptSecurityManager.getSystemPrincipal();
1180 }
1181 else {
1182 let uri = Services.io.newURI(host, null, null);
1183 principal = Services.scriptSecurityManager.getCodebasePrincipal(uri);
1184 }
1185
1186 return indexedDB.openForPrincipal(principal, name);
1187 },
1188
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;
1199
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 },
1211
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;
1219
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 });
1226 }
1227 catch (ex) {
1228 // Continuously retrying is overkill. Waiting for 100ms before next try
1229 yield sleep(100);
1230 }
1231 }
1232
1233 if (!connection) {
1234 return null;
1235 }
1236
1237 let rows = yield connection.execute("SELECT name FROM database");
1238 if (rows.length != 1) {
1239 return null;
1240 }
1241
1242 let name = rows[0].getResultByName("name");
1243
1244 yield connection.close();
1245
1246 return name;
1247 }),
1248
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");
1256
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);
1264 }
1265 if (!exists) {
1266 return [];
1267 }
1268
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;
1276 }
1277
1278 // Skip any non-sqlite files.
1279 if (!file.name.endsWith(".sqlite")) {
1280 return null;
1281 }
1282
1283 return this.getNameFromDatabaseFile(file.path).then(name => {
1284 if (name) {
1285 names.push(name);
1286 }
1287 return null;
1288 });
1289 });
1290 }
1291 finally {
1292 dirIterator.close();
1293 }
1294 return names;
1295 }),
1296
1297 /**
1298 * Returns the over-the-wire implementation of the indexed db entity.
1299 */
1300 toStoreObject: function(item) {
1301 if (!item) {
1302 return null;
1303 }
1304
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 };
1313 }
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 };
1322 }
1323 // Indexed db entry
1324 return {
1325 name: item.name,
1326 value: new LongStringActor(this.conn, JSON.stringify(item.value))
1327 };
1328 },
1329
1330 form: function(form, detail) {
1331 if (detail === "actorid") {
1332 return this.actorID;
1333 }
1334
1335 let hosts = {};
1336 for (let host of this.hosts) {
1337 hosts[host] = this.getNamesForHost(host);
1338 }
1339
1340 return {
1341 actor: this.actorID,
1342 hosts: hosts
1343 };
1344 },
1345 });
1346
1347 /**
1348 * The main Storage Actor.
1349 */
1350 let StorageActor = exports.StorageActor = protocol.ActorClass({
1351 typeName: "storage",
1352
1353 get window() {
1354 return this.parentActor.window;
1355 },
1356
1357 get document() {
1358 return this.parentActor.window.document;
1359 },
1360
1361 get windows() {
1362 return this.childWindowPool;
1363 },
1364
1365 /**
1366 * List of event notifications that the server can send to the client.
1367 *
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")
1385 }
1386 },
1387
1388 initialize: function (conn, tabActor) {
1389 protocol.Actor.prototype.initialize.call(this, null);
1390
1391 this.conn = conn;
1392 this.parentActor = tabActor;
1393
1394 this.childActorPool = new Map();
1395 this.childWindowPool = new Set();
1396
1397 // Fetch all the inner iframe windows in this tab.
1398 this.fetchChildWindows(this.parentActor.docShell);
1399
1400 // Initialize the registered store types
1401 for (let [store, actor] of storageTypePool) {
1402 this.childActorPool.set(store, new actor(this));
1403 }
1404
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);
1412
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);
1420
1421 // Layout helper for window.parent and window.top helper methods that work
1422 // accross devices.
1423 this.layoutHelper = new LayoutHelpers(this.window);
1424 },
1425
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);
1439 }
1440 // Destroy the registered store types
1441 for (let actor of this.childActorPool.values()) {
1442 actor.destroy();
1443 }
1444 this.childActorPool.clear();
1445 this.childWindowPool.clear();
1446 this.childWindowPool = this.childActorPool = null;
1447 },
1448
1449 /**
1450 * Given a docshell, recursively find otu all the child windows from it.
1451 *
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;
1460 }
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;
1466 }
1467 this.childWindowPool.add(window);
1468 for (let i = 0; i < docShell.childCount; i++) {
1469 let child = docShell.getChildAt(i);
1470 this.fetchChildWindows(child);
1471 }
1472 return null;
1473 },
1474
1475 isIncludedInTopLevelWindow: function(window) {
1476 return this.layoutHelper.isIncludedInTopLevelWindow(window);
1477 },
1478
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;
1487 }
1488 }
1489 return null;
1490 },
1491
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;
1500 }
1501 if (topic == "content-document-global-created" &&
1502 this.isIncludedInTopLevelWindow(subject)) {
1503 this.childWindowPool.add(subject);
1504 events.emit(this, "window-ready", subject);
1505 }
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);
1511 }
1512 }
1513 return null;
1514 },
1515
1516 /**
1517 * Called on "pageshow" or "pagehide" event on the chromeEventHandler of
1518 * current tab.
1519 *
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;
1531 }
1532 let window = target.defaultView;
1533 if (type == "pagehide" && this.childWindowPool.delete(window)) {
1534 events.emit(this, "window-destroyed", window)
1535 }
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);
1541 }
1542 },
1543
1544 /**
1545 * Lists the available hosts for all the registered storage types.
1546 *
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();
1558 }
1559 toReturn[name] = value;
1560 }
1561 return toReturn;
1562 }), {
1563 response: RetVal(types.addDictType("storelist", getRegisteredTypes()))
1564 }),
1565
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;
1572 }
1573 events.emit(this, "stores-update", this.boundUpdate);
1574 this.boundUpdate = {};
1575 this.updatePending = false;
1576 return null;
1577 },
1578
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.
1584 *
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;
1607 }
1608
1609 this.updatingUpdateObject = true;
1610 if (!this.boundUpdate[action]) {
1611 this.boundUpdate[action] = {};
1612 }
1613 if (!this.boundUpdate[action][storeType]) {
1614 this.boundUpdate[action][storeType] = {};
1615 }
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];
1620 }
1621 else {
1622 this.boundUpdate[action][storeType][host] =
1623 this.boundUpdate[action][storeType][host].concat(data[host]);
1624 }
1625 }
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);
1631 }
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]);
1638 }
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];
1649 }
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];
1654 }
1655 }
1656 }
1657 this.updatingUpdateObject = false;
1658 return null;
1659 },
1660
1661 /**
1662 * This method removes data from the this.boundUpdate object in the same
1663 * manner like this.update() adds data to it.
1664 *
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);
1686 }
1687 }
1688 if (!this.boundUpdate[action][storeType][host].length) {
1689 delete this.boundUpdate[action][storeType][host];
1690 }
1691 }
1692 }
1693 return null;
1694 }
1695 });
1696
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;
1704
1705 client.addActorPool(this);
1706 this.manage(this);
1707 }
1708 });

mercurial