|
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 }); |