michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {Cu, Cc, Ci} = require("chrome"); michael@0: const events = require("sdk/event/core"); michael@0: const protocol = require("devtools/server/protocol"); michael@0: const {async} = require("devtools/async-utils"); michael@0: const {Arg, Option, method, RetVal, types} = protocol; michael@0: const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); michael@0: michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Sqlite", michael@0: "resource://gre/modules/Sqlite.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm"); michael@0: michael@0: exports.register = function(handle) { michael@0: handle.addTabActor(StorageActor, "storageActor"); michael@0: }; michael@0: michael@0: exports.unregister = function(handle) { michael@0: handle.removeTabActor(StorageActor); michael@0: }; michael@0: michael@0: // Global required for window less Indexed DB instantiation. michael@0: let global = this; michael@0: michael@0: // Maximum number of cookies/local storage key-value-pairs that can be sent michael@0: // over the wire to the client in one request. michael@0: const MAX_STORE_OBJECT_COUNT = 30; michael@0: // Interval for the batch job that sends the accumilated update packets to the michael@0: // client. michael@0: const UPDATE_INTERVAL = 500; // ms michael@0: michael@0: // A RegExp for characters that cannot appear in a file/directory name. This is michael@0: // used to sanitize the host name for indexed db to lookup whether the file is michael@0: // present in /storage/persistent/ location michael@0: let illegalFileNameCharacters = [ michael@0: "[", michael@0: "\\x00-\\x25", // Control characters \001 to \037 michael@0: "/:*?\\\"<>|\\\\", // Special characters michael@0: "]" michael@0: ].join(""); michael@0: let ILLEGAL_CHAR_REGEX = new RegExp(illegalFileNameCharacters, "g"); michael@0: michael@0: // Holder for all the registered storage actors. michael@0: let storageTypePool = new Map(); michael@0: michael@0: /** michael@0: * Gets an accumulated list of all storage actors registered to be used to michael@0: * create a RetVal to define the return type of StorageActor.listStores method. michael@0: */ michael@0: function getRegisteredTypes() { michael@0: let registeredTypes = {}; michael@0: for (let store of storageTypePool.keys()) { michael@0: registeredTypes[store] = store; michael@0: } michael@0: return registeredTypes; michael@0: } michael@0: michael@0: /** michael@0: * An async method equivalent to setTimeout but using Promises michael@0: * michael@0: * @param {number} time michael@0: * The wait Ttme in milliseconds. michael@0: */ michael@0: function sleep(time) { michael@0: let wait = Promise.defer(); michael@0: let updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: updateTimer.initWithCallback({ michael@0: notify: function() { michael@0: updateTimer.cancel(); michael@0: updateTimer = null; michael@0: wait.resolve(null); michael@0: } michael@0: } , time, Ci.nsITimer.TYPE_ONE_SHOT); michael@0: return wait.promise; michael@0: } michael@0: michael@0: // Cookies store object michael@0: types.addDictType("cookieobject", { michael@0: name: "string", michael@0: value: "longstring", michael@0: path: "nullable:string", michael@0: host: "string", michael@0: isDomain: "boolean", michael@0: isSecure: "boolean", michael@0: isHttpOnly: "boolean", michael@0: creationTime: "number", michael@0: lastAccessed: "number", michael@0: expires: "number" michael@0: }); michael@0: michael@0: // Array of cookie store objects michael@0: types.addDictType("cookiestoreobject", { michael@0: total: "number", michael@0: offset: "number", michael@0: data: "array:nullable:cookieobject" michael@0: }); michael@0: michael@0: // Local Storage / Session Storage store object michael@0: types.addDictType("storageobject", { michael@0: name: "string", michael@0: value: "longstring" michael@0: }); michael@0: michael@0: // Array of Local Storage / Session Storage store objects michael@0: types.addDictType("storagestoreobject", { michael@0: total: "number", michael@0: offset: "number", michael@0: data: "array:nullable:storageobject" michael@0: }); michael@0: michael@0: // Indexed DB store object michael@0: // This is a union on idb object, db metadata object and object store metadata michael@0: // object michael@0: types.addDictType("idbobject", { michael@0: name: "nullable:string", michael@0: db: "nullable:string", michael@0: objectStore: "nullable:string", michael@0: origin: "nullable:string", michael@0: version: "nullable:number", michael@0: objectStores: "nullable:number", michael@0: keyPath: "nullable:string", michael@0: autoIncrement: "nullable:boolean", michael@0: indexes: "nullable:string", michael@0: value: "nullable:longstring" michael@0: }); michael@0: michael@0: // Array of Indexed DB store objects michael@0: types.addDictType("idbstoreobject", { michael@0: total: "number", michael@0: offset: "number", michael@0: data: "array:nullable:idbobject" michael@0: }); michael@0: michael@0: // Update notification object michael@0: types.addDictType("storeUpdateObject", { michael@0: changed: "nullable:json", michael@0: deleted: "nullable:json", michael@0: added: "nullable:json" michael@0: }); michael@0: michael@0: // Helper methods to create a storage actor. michael@0: let StorageActors = {}; michael@0: michael@0: /** michael@0: * Creates a default object with the common methods required by all storage michael@0: * actors. michael@0: * michael@0: * This default object is missing a couple of required methods that should be michael@0: * implemented seperately for each actor. They are namely: michael@0: * - observe : Method which gets triggered on the notificaiton of the watched michael@0: * topic. michael@0: * - getNamesForHost : Given a host, get list of all known store names. michael@0: * - getValuesForHost : Given a host (and optianally a name) get all known michael@0: * store objects. michael@0: * - toStoreObject : Given a store object, convert it to the required format michael@0: * so that it can be transferred over wire. michael@0: * - populateStoresForHost : Given a host, populate the map of all store michael@0: * objects for it michael@0: * michael@0: * @param {string} typeName michael@0: * The typeName of the actor. michael@0: * @param {string} observationTopic michael@0: * The topic which this actor listens to via Notification Observers. michael@0: * @param {string} storeObjectType michael@0: * The RetVal type of the store object of this actor. michael@0: */ michael@0: StorageActors.defaults = function(typeName, observationTopic, storeObjectType) { michael@0: return { michael@0: typeName: typeName, michael@0: michael@0: get conn() { michael@0: return this.storageActor.conn; michael@0: }, michael@0: michael@0: /** michael@0: * Returns a list of currently knwon hosts for the target window. This list michael@0: * contains unique hosts from the window + all inner windows. michael@0: */ michael@0: get hosts() { michael@0: let hosts = new Set(); michael@0: for (let {location} of this.storageActor.windows) { michael@0: hosts.add(this.getHostName(location)); michael@0: } michael@0: return hosts; michael@0: }, michael@0: michael@0: /** michael@0: * Returns all the windows present on the page. Includes main window + inner michael@0: * iframe windows. michael@0: */ michael@0: get windows() { michael@0: return this.storageActor.windows; michael@0: }, michael@0: michael@0: /** michael@0: * Converts the window.location object into host. michael@0: */ michael@0: getHostName: function(location) { michael@0: return location.hostname || location.href; michael@0: }, michael@0: michael@0: initialize: function(storageActor) { michael@0: protocol.Actor.prototype.initialize.call(this, null); michael@0: michael@0: this.storageActor = storageActor; michael@0: michael@0: this.populateStoresForHosts(); michael@0: if (observationTopic) { michael@0: Services.obs.addObserver(this, observationTopic, false); michael@0: } michael@0: this.onWindowReady = this.onWindowReady.bind(this); michael@0: this.onWindowDestroyed = this.onWindowDestroyed.bind(this); michael@0: events.on(this.storageActor, "window-ready", this.onWindowReady); michael@0: events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed); michael@0: }, michael@0: michael@0: destroy: function() { michael@0: this.hostVsStores = null; michael@0: if (observationTopic) { michael@0: Services.obs.removeObserver(this, observationTopic, false); michael@0: } michael@0: events.off(this.storageActor, "window-ready", this.onWindowReady); michael@0: events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed); michael@0: }, michael@0: michael@0: getNamesForHost: function(host) { michael@0: return [...this.hostVsStores.get(host).keys()]; michael@0: }, michael@0: michael@0: getValuesForHost: function(host, name) { michael@0: if (name) { michael@0: return [this.hostVsStores.get(host).get(name)]; michael@0: } michael@0: return [...this.hostVsStores.get(host).values()]; michael@0: }, michael@0: michael@0: getObjectsSize: function(host, names) { michael@0: return names.length; michael@0: }, michael@0: michael@0: /** michael@0: * When a new window is added to the page. This generally means that a new michael@0: * iframe is created, or the current window is completely reloaded. michael@0: * michael@0: * @param {window} window michael@0: * The window which was added. michael@0: */ michael@0: onWindowReady: async(function*(window) { michael@0: let host = this.getHostName(window.location); michael@0: if (!this.hostVsStores.has(host)) { michael@0: yield this.populateStoresForHost(host, window); michael@0: let data = {}; michael@0: data[host] = this.getNamesForHost(host); michael@0: this.storageActor.update("added", typeName, data); michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * When a window is removed from the page. This generally means that an michael@0: * iframe was removed, or the current window reload is triggered. michael@0: * michael@0: * @param {window} window michael@0: * The window which was removed. michael@0: */ michael@0: onWindowDestroyed: function(window) { michael@0: let host = this.getHostName(window.location); michael@0: if (!this.hosts.has(host)) { michael@0: this.hostVsStores.delete(host); michael@0: let data = {}; michael@0: data[host] = []; michael@0: this.storageActor.update("deleted", typeName, data); michael@0: } michael@0: }, michael@0: michael@0: form: function(form, detail) { michael@0: if (detail === "actorid") { michael@0: return this.actorID; michael@0: } michael@0: michael@0: let hosts = {}; michael@0: for (let host of this.hosts) { michael@0: hosts[host] = []; michael@0: } michael@0: michael@0: return { michael@0: actor: this.actorID, michael@0: hosts: hosts michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Populates a map of known hosts vs a map of stores vs value. michael@0: */ michael@0: populateStoresForHosts: function() { michael@0: this.hostVsStores = new Map(); michael@0: for (let host of this.hosts) { michael@0: this.populateStoresForHost(host); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns a list of requested store objects. Maximum values returned are michael@0: * MAX_STORE_OBJECT_COUNT. This method returns paginated values whose michael@0: * starting index and total size can be controlled via the options object michael@0: * michael@0: * @param {string} host michael@0: * The host name for which the store values are required. michael@0: * @param {array:string} names michael@0: * Array containing the names of required store objects. Empty if all michael@0: * items are required. michael@0: * @param {object} options michael@0: * Additional options for the request containing following properties: michael@0: * - offset {number} : The begin index of the returned array amongst michael@0: * the total values michael@0: * - size {number} : The number of values required. michael@0: * - sortOn {string} : The values should be sorted on this property. michael@0: * - index {string} : In case of indexed db, the IDBIndex to be used michael@0: * for fetching the values. michael@0: * michael@0: * @return {object} An object containing following properties: michael@0: * - offset - The actual offset of the returned array. This might michael@0: * be different from the requested offset if that was michael@0: * invalid michael@0: * - total - The total number of entries possible. michael@0: * - data - The requested values. michael@0: */ michael@0: getStoreObjects: method(async(function*(host, names, options = {}) { michael@0: let offset = options.offset || 0; michael@0: let size = options.size || MAX_STORE_OBJECT_COUNT; michael@0: if (size > MAX_STORE_OBJECT_COUNT) { michael@0: size = MAX_STORE_OBJECT_COUNT; michael@0: } michael@0: let sortOn = options.sortOn || "name"; michael@0: michael@0: let toReturn = { michael@0: offset: offset, michael@0: total: 0, michael@0: data: [] michael@0: }; michael@0: michael@0: if (names) { michael@0: for (let name of names) { michael@0: toReturn.data.push( michael@0: // yield because getValuesForHost is async for Indexed DB michael@0: ...(yield this.getValuesForHost(host, name, options)) michael@0: ); michael@0: } michael@0: toReturn.total = this.getObjectsSize(host, names, options); michael@0: if (offset > toReturn.total) { michael@0: // In this case, toReturn.data is an empty array. michael@0: toReturn.offset = toReturn.total; michael@0: toReturn.data = []; michael@0: } michael@0: else { michael@0: toReturn.data = toReturn.data.sort((a,b) => { michael@0: return a[sortOn] - b[sortOn]; michael@0: }).slice(offset, offset + size).map(a => this.toStoreObject(a)); michael@0: } michael@0: } michael@0: else { michael@0: let total = yield this.getValuesForHost(host); michael@0: toReturn.total = total.length; michael@0: if (offset > toReturn.total) { michael@0: // In this case, toReturn.data is an empty array. michael@0: toReturn.offset = offset = toReturn.total; michael@0: toReturn.data = []; michael@0: } michael@0: else { michael@0: toReturn.data = total.sort((a,b) => { michael@0: return a[sortOn] - b[sortOn]; michael@0: }).slice(offset, offset + size) michael@0: .map(object => this.toStoreObject(object)); michael@0: } michael@0: } michael@0: michael@0: return toReturn; michael@0: }), { michael@0: request: { michael@0: host: Arg(0), michael@0: names: Arg(1, "nullable:array:string"), michael@0: options: Arg(2, "nullable:json") michael@0: }, michael@0: response: RetVal(storeObjectType) michael@0: }) michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Creates an actor and its corresponding front and registers it to the Storage michael@0: * Actor. michael@0: * michael@0: * @See StorageActors.defaults() michael@0: * michael@0: * @param {object} options michael@0: * Options required by StorageActors.defaults method which are : michael@0: * - typeName {string} michael@0: * The typeName of the actor. michael@0: * - observationTopic {string} michael@0: * The topic which this actor listens to via michael@0: * Notification Observers. michael@0: * - storeObjectType {string} michael@0: * The RetVal type of the store object of this actor. michael@0: * @param {object} overrides michael@0: * All the methods which you want to be differnt from the ones in michael@0: * StorageActors.defaults method plus the required ones described there. michael@0: */ michael@0: StorageActors.createActor = function(options = {}, overrides = {}) { michael@0: let actorObject = StorageActors.defaults( michael@0: options.typeName, michael@0: options.observationTopic || null, michael@0: options.storeObjectType michael@0: ); michael@0: for (let key in overrides) { michael@0: actorObject[key] = overrides[key]; michael@0: } michael@0: michael@0: let actor = protocol.ActorClass(actorObject); michael@0: let front = protocol.FrontClass(actor, { michael@0: form: function(form, detail) { michael@0: if (detail === "actorid") { michael@0: this.actorID = form; michael@0: return null; michael@0: } michael@0: michael@0: this.actorID = form.actor; michael@0: this.hosts = form.hosts; michael@0: return null; michael@0: } michael@0: }); michael@0: storageTypePool.set(actorObject.typeName, actor); michael@0: } michael@0: michael@0: /** michael@0: * The Cookies actor and front. michael@0: */ michael@0: StorageActors.createActor({ michael@0: typeName: "cookies", michael@0: storeObjectType: "cookiestoreobject" michael@0: }, { michael@0: initialize: function(storageActor) { michael@0: protocol.Actor.prototype.initialize.call(this, null); michael@0: michael@0: this.storageActor = storageActor; michael@0: michael@0: this.populateStoresForHosts(); michael@0: Services.obs.addObserver(this, "cookie-changed", false); michael@0: Services.obs.addObserver(this, "http-on-response-set-cookie", false); michael@0: this.onWindowReady = this.onWindowReady.bind(this); michael@0: this.onWindowDestroyed = this.onWindowDestroyed.bind(this); michael@0: events.on(this.storageActor, "window-ready", this.onWindowReady); michael@0: events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed); michael@0: }, michael@0: michael@0: destroy: function() { michael@0: this.hostVsStores = null; michael@0: Services.obs.removeObserver(this, "cookie-changed", false); michael@0: Services.obs.removeObserver(this, "http-on-response-set-cookie", false); michael@0: events.off(this.storageActor, "window-ready", this.onWindowReady); michael@0: events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed); michael@0: }, michael@0: michael@0: /** michael@0: * Given a cookie object, figure out all the matching hosts from the page that michael@0: * the cookie belong to. michael@0: */ michael@0: getMatchingHosts: function(cookies) { michael@0: if (!cookies.length) { michael@0: cookies = [cookies]; michael@0: } michael@0: let hosts = new Set(); michael@0: for (let host of this.hosts) { michael@0: for (let cookie of cookies) { michael@0: if (this.isCookieAtHost(cookie, host)) { michael@0: hosts.add(host); michael@0: } michael@0: } michael@0: } michael@0: return [...hosts]; michael@0: }, michael@0: michael@0: /** michael@0: * Given a cookie object and a host, figure out if the cookie is valid for michael@0: * that host. michael@0: */ michael@0: isCookieAtHost: function(cookie, host) { michael@0: try { michael@0: cookie = cookie.QueryInterface(Ci.nsICookie) michael@0: .QueryInterface(Ci.nsICookie2); michael@0: } catch(ex) { michael@0: return false; michael@0: } michael@0: if (cookie.host == null) { michael@0: return host == null; michael@0: } michael@0: if (cookie.host.startsWith(".")) { michael@0: return host.endsWith(cookie.host); michael@0: } michael@0: else { michael@0: return cookie.host == host; michael@0: } michael@0: }, michael@0: michael@0: toStoreObject: function(cookie) { michael@0: if (!cookie) { michael@0: return null; michael@0: } michael@0: michael@0: return { michael@0: name: cookie.name, michael@0: path: cookie.path || "", michael@0: host: cookie.host || "", michael@0: expires: (cookie.expires || 0) * 1000, // because expires is in seconds michael@0: creationTime: cookie.creationTime / 1000, // because it is in micro seconds michael@0: lastAccessed: cookie.lastAccessed / 1000, // - do - michael@0: value: new LongStringActor(this.conn, cookie.value || ""), michael@0: isDomain: cookie.isDomain, michael@0: isSecure: cookie.isSecure, michael@0: isHttpOnly: cookie.isHttpOnly michael@0: } michael@0: }, michael@0: michael@0: populateStoresForHost: function(host) { michael@0: this.hostVsStores.set(host, new Map()); michael@0: let cookies = Services.cookies.getCookiesFromHost(host); michael@0: while (cookies.hasMoreElements()) { michael@0: let cookie = cookies.getNext().QueryInterface(Ci.nsICookie) michael@0: .QueryInterface(Ci.nsICookie2); michael@0: if (this.isCookieAtHost(cookie, host)) { michael@0: this.hostVsStores.get(host).set(cookie.name, cookie); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Converts the raw cookie string returned in http request's response header michael@0: * to a nsICookie compatible object. michael@0: * michael@0: * @param {string} cookieString michael@0: * The raw cookie string coming from response header. michael@0: * @param {string} domain michael@0: * The domain of the url of the nsiChannel the cookie corresponds to. michael@0: * This will be used when the cookie string does not have a domain. michael@0: * michael@0: * @returns {[object]} michael@0: * An array of nsICookie like objects representing the cookies. michael@0: */ michael@0: parseCookieString: function(cookieString, domain) { michael@0: /** michael@0: * Takes a date string present in raw cookie string coming from http michael@0: * request's response headers and returns the number of milliseconds elapsed michael@0: * since epoch. If the date string is undefined, its probably a session michael@0: * cookie so return 0. michael@0: */ michael@0: let parseDateString = dateString => { michael@0: return dateString ? new Date(dateString.replace(/-/g, " ")).getTime(): 0; michael@0: }; michael@0: michael@0: let cookies = []; michael@0: for (let string of cookieString.split("\n")) { michael@0: let keyVals = {}, name = null; michael@0: for (let keyVal of string.split(/;\s*/)) { michael@0: let tokens = keyVal.split(/\s*=\s*/); michael@0: if (!name) { michael@0: name = tokens[0]; michael@0: } michael@0: else { michael@0: tokens[0] = tokens[0].toLowerCase(); michael@0: } michael@0: keyVals[tokens.splice(0, 1)[0]] = tokens.join("="); michael@0: } michael@0: let expiresTime = parseDateString(keyVals.expires); michael@0: keyVals.domain = keyVals.domain || domain; michael@0: cookies.push({ michael@0: name: name, michael@0: value: keyVals[name] || "", michael@0: path: keyVals.path, michael@0: host: keyVals.domain, michael@0: expires: expiresTime/1000, // seconds, to match with nsiCookie.expires michael@0: lastAccessed: expiresTime * 1000, michael@0: // microseconds, to match with nsiCookie.lastAccessed michael@0: creationTime: expiresTime * 1000, michael@0: // microseconds, to match with nsiCookie.creationTime michael@0: isHttpOnly: true, michael@0: isSecure: keyVals.secure != null, michael@0: isDomain: keyVals.domain.startsWith("."), michael@0: }); michael@0: } michael@0: return cookies; michael@0: }, michael@0: michael@0: /** michael@0: * Notification observer for topics "http-on-response-set-cookie" and michael@0: * "cookie-change". michael@0: * michael@0: * @param subject michael@0: * {nsiChannel} The channel associated to the SET-COOKIE response michael@0: * header in case of "http-on-response-set-cookie" topic. michael@0: * {nsiCookie|[nsiCookie]} A single nsiCookie object or a list of it michael@0: * depending on the action. Array is only in case of "batch-deleted" michael@0: * action. michael@0: * @param {string} topic michael@0: * The topic of the notification. michael@0: * @param {string} action michael@0: * Additional data associated with the notification. Its the type of michael@0: * cookie change in case of "cookie-change" topic and the cookie string michael@0: * in case of "http-on-response-set-cookie" topic. michael@0: */ michael@0: observe: function(subject, topic, action) { michael@0: if (topic == "http-on-response-set-cookie") { michael@0: // Some cookies got created as a result of http response header SET-COOKIE michael@0: // subject here is an nsIChannel object referring to the http request. michael@0: // We get the requestor of this channel and thus the content window. michael@0: let channel = subject.QueryInterface(Ci.nsIChannel); michael@0: let requestor = channel.notificationCallbacks || michael@0: channel.loadGroup.notificationCallbacks; michael@0: // requester can be null sometimes. michael@0: let window = requestor ? requestor.getInterface(Ci.nsIDOMWindow): null; michael@0: // Proceed only if this window is present on the currently targetted tab michael@0: if (window && this.storageActor.isIncludedInTopLevelWindow(window)) { michael@0: let host = this.getHostName(window.location); michael@0: if (this.hostVsStores.has(host)) { michael@0: let cookies = this.parseCookieString(action, channel.URI.host); michael@0: let data = {}; michael@0: data[host] = []; michael@0: for (let cookie of cookies) { michael@0: if (this.hostVsStores.get(host).has(cookie.name)) { michael@0: continue; michael@0: } michael@0: this.hostVsStores.get(host).set(cookie.name, cookie); michael@0: data[host].push(cookie.name); michael@0: } michael@0: if (data[host]) { michael@0: this.storageActor.update("added", "cookies", data); michael@0: } michael@0: } michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: if (topic != "cookie-changed") { michael@0: return null; michael@0: } michael@0: michael@0: let hosts = this.getMatchingHosts(subject); michael@0: let data = {}; michael@0: michael@0: switch(action) { michael@0: case "added": michael@0: case "changed": michael@0: if (hosts.length) { michael@0: subject = subject.QueryInterface(Ci.nsICookie) michael@0: .QueryInterface(Ci.nsICookie2); michael@0: for (let host of hosts) { michael@0: this.hostVsStores.get(host).set(subject.name, subject); michael@0: data[host] = [subject.name]; michael@0: } michael@0: this.storageActor.update(action, "cookies", data); michael@0: } michael@0: break; michael@0: michael@0: case "deleted": michael@0: if (hosts.length) { michael@0: subject = subject.QueryInterface(Ci.nsICookie) michael@0: .QueryInterface(Ci.nsICookie2); michael@0: for (let host of hosts) { michael@0: this.hostVsStores.get(host).delete(subject.name); michael@0: data[host] = [subject.name]; michael@0: } michael@0: this.storageActor.update("deleted", "cookies", data); michael@0: } michael@0: break; michael@0: michael@0: case "batch-deleted": michael@0: if (hosts.length) { michael@0: for (let host of hosts) { michael@0: let stores = []; michael@0: for (let cookie of subject) { michael@0: cookie = cookie.QueryInterface(Ci.nsICookie) michael@0: .QueryInterface(Ci.nsICookie2); michael@0: this.hostVsStores.get(host).delete(cookie.name); michael@0: stores.push(cookie.name); michael@0: } michael@0: data[host] = stores; michael@0: } michael@0: this.storageActor.update("deleted", "cookies", data); michael@0: } michael@0: break; michael@0: michael@0: case "cleared": michael@0: this.storageActor.update("cleared", "cookies", hosts); michael@0: break; michael@0: michael@0: case "reload": michael@0: this.storageActor.update("reloaded", "cookies", hosts); michael@0: break; michael@0: } michael@0: return null; michael@0: }, michael@0: }); michael@0: michael@0: michael@0: /** michael@0: * Helper method to create the overriden object required in michael@0: * StorageActors.createActor for Local Storage and Session Storage. michael@0: * This method exists as both Local Storage and Session Storage have almost michael@0: * identical actors. michael@0: */ michael@0: function getObjectForLocalOrSessionStorage(type) { michael@0: return { michael@0: getNamesForHost: function(host) { michael@0: let storage = this.hostVsStores.get(host); michael@0: return [key for (key in storage)]; michael@0: }, michael@0: michael@0: getValuesForHost: function(host, name) { michael@0: let storage = this.hostVsStores.get(host); michael@0: if (name) { michael@0: return [{name: name, value: storage.getItem(name)}]; michael@0: } michael@0: return [{name: name, value: storage.getItem(name)} for (name in storage)]; michael@0: }, michael@0: michael@0: getHostName: function(location) { michael@0: if (!location.host) { michael@0: return location.href; michael@0: } michael@0: return location.protocol + "//" + location.host; michael@0: }, michael@0: michael@0: populateStoresForHost: function(host, window) { michael@0: try { michael@0: this.hostVsStores.set(host, window[type]); michael@0: } catch(ex) { michael@0: // Exceptions happen when local or session storage is inaccessible michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: populateStoresForHosts: function() { michael@0: this.hostVsStores = new Map(); michael@0: try { michael@0: for (let window of this.windows) { michael@0: this.hostVsStores.set(this.getHostName(window.location), window[type]); michael@0: } michael@0: } catch(ex) { michael@0: // Exceptions happen when local or session storage is inaccessible michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: observe: function(subject, topic, data) { michael@0: if (topic != "dom-storage2-changed" || data != type) { michael@0: return null; michael@0: } michael@0: michael@0: let host = this.getSchemaAndHost(subject.url); michael@0: michael@0: if (!this.hostVsStores.has(host)) { michael@0: return null; michael@0: } michael@0: michael@0: let action = "changed"; michael@0: if (subject.key == null) { michael@0: return this.storageActor.update("cleared", type, [host]); michael@0: } michael@0: else if (subject.oldValue == null) { michael@0: action = "added"; michael@0: } michael@0: else if (subject.newValue == null) { michael@0: action = "deleted"; michael@0: } michael@0: let updateData = {}; michael@0: updateData[host] = [subject.key]; michael@0: return this.storageActor.update(action, type, updateData); michael@0: }, michael@0: michael@0: /** michael@0: * Given a url, correctly determine its protocol + hostname part. michael@0: */ michael@0: getSchemaAndHost: function(url) { michael@0: let uri = Services.io.newURI(url, null, null); michael@0: return uri.scheme + "://" + uri.hostPort; michael@0: }, michael@0: michael@0: toStoreObject: function(item) { michael@0: if (!item) { michael@0: return null; michael@0: } michael@0: michael@0: return { michael@0: name: item.name, michael@0: value: new LongStringActor(this.conn, item.value || "") michael@0: }; michael@0: }, michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * The Local Storage actor and front. michael@0: */ michael@0: StorageActors.createActor({ michael@0: typeName: "localStorage", michael@0: observationTopic: "dom-storage2-changed", michael@0: storeObjectType: "storagestoreobject" michael@0: }, getObjectForLocalOrSessionStorage("localStorage")); michael@0: michael@0: /** michael@0: * The Session Storage actor and front. michael@0: */ michael@0: StorageActors.createActor({ michael@0: typeName: "sessionStorage", michael@0: observationTopic: "dom-storage2-changed", michael@0: storeObjectType: "storagestoreobject" michael@0: }, getObjectForLocalOrSessionStorage("sessionStorage")); michael@0: michael@0: michael@0: /** michael@0: * Code related to the Indexed DB actor and front michael@0: */ michael@0: michael@0: // Metadata holder objects for various components of Indexed DB michael@0: michael@0: /** michael@0: * Meta data object for a particular index in an object store michael@0: * michael@0: * @param {IDBIndex} index michael@0: * The particular index from the object store. michael@0: */ michael@0: function IndexMetadata(index) { michael@0: this._name = index.name; michael@0: this._keyPath = index.keyPath; michael@0: this._unique = index.unique; michael@0: this._multiEntry = index.multiEntry; michael@0: } michael@0: IndexMetadata.prototype = { michael@0: toObject: function() { michael@0: return { michael@0: name: this._name, michael@0: keyPath: this._keyPath, michael@0: unique: this._unique, michael@0: multiEntry: this._multiEntry michael@0: }; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Meta data object for a particular object store in a db michael@0: * michael@0: * @param {IDBObjectStore} objectStore michael@0: * The particular object store from the db. michael@0: */ michael@0: function ObjectStoreMetadata(objectStore) { michael@0: this._name = objectStore.name; michael@0: this._keyPath = objectStore.keyPath; michael@0: this._autoIncrement = objectStore.autoIncrement; michael@0: this._indexes = new Map(); michael@0: michael@0: for (let i = 0; i < objectStore.indexNames.length; i++) { michael@0: let index = objectStore.index(objectStore.indexNames[i]); michael@0: this._indexes.set(index, new IndexMetadata(index)); michael@0: } michael@0: } michael@0: ObjectStoreMetadata.prototype = { michael@0: toObject: function() { michael@0: return { michael@0: name: this._name, michael@0: keyPath: this._keyPath, michael@0: autoIncrement: this._autoIncrement, michael@0: indexes: JSON.stringify( michael@0: [index.toObject() for (index of this._indexes.values())] michael@0: ) michael@0: }; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Meta data object for a particular indexed db in a host. michael@0: * michael@0: * @param {string} origin michael@0: * The host associated with this indexed db. michael@0: * @param {IDBDatabase} db michael@0: * The particular indexed db. michael@0: */ michael@0: function DatabaseMetadata(origin, db) { michael@0: this._origin = origin; michael@0: this._name = db.name; michael@0: this._version = db.version; michael@0: this._objectStores = new Map(); michael@0: michael@0: if (db.objectStoreNames.length) { michael@0: let transaction = db.transaction(db.objectStoreNames, "readonly"); michael@0: michael@0: for (let i = 0; i < transaction.objectStoreNames.length; i++) { michael@0: let objectStore = michael@0: transaction.objectStore(transaction.objectStoreNames[i]); michael@0: this._objectStores.set(transaction.objectStoreNames[i], michael@0: new ObjectStoreMetadata(objectStore)); michael@0: } michael@0: } michael@0: }; michael@0: DatabaseMetadata.prototype = { michael@0: get objectStores() { michael@0: return this._objectStores; michael@0: }, michael@0: michael@0: toObject: function() { michael@0: return { michael@0: name: this._name, michael@0: origin: this._origin, michael@0: version: this._version, michael@0: objectStores: this._objectStores.size michael@0: }; michael@0: } michael@0: }; michael@0: michael@0: StorageActors.createActor({ michael@0: typeName: "indexedDB", michael@0: storeObjectType: "idbstoreobject" michael@0: }, { michael@0: initialize: function(storageActor) { michael@0: protocol.Actor.prototype.initialize.call(this, null); michael@0: if (!global.indexedDB) { michael@0: let idbManager = Cc["@mozilla.org/dom/indexeddb/manager;1"] michael@0: .getService(Ci.nsIIndexedDatabaseManager); michael@0: idbManager.initWindowless(global); michael@0: } michael@0: this.objectsSize = {}; michael@0: this.storageActor = storageActor; michael@0: this.onWindowReady = this.onWindowReady.bind(this); michael@0: this.onWindowDestroyed = this.onWindowDestroyed.bind(this); michael@0: events.on(this.storageActor, "window-ready", this.onWindowReady); michael@0: events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed); michael@0: }, michael@0: michael@0: destroy: function() { michael@0: this.hostVsStores = null; michael@0: this.objectsSize = null; michael@0: events.off(this.storageActor, "window-ready", this.onWindowReady); michael@0: events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed); michael@0: }, michael@0: michael@0: getHostName: function(location) { michael@0: if (!location.host) { michael@0: return location.href; michael@0: } michael@0: return location.protocol + "//" + location.host; michael@0: }, michael@0: michael@0: /** michael@0: * This method is overriden and left blank as for indexedDB, this operation michael@0: * cannot be performed synchronously. Thus, the preListStores method exists to michael@0: * do the same task asynchronously. michael@0: */ michael@0: populateStoresForHosts: function() { michael@0: }, michael@0: michael@0: getNamesForHost: function(host) { michael@0: let names = []; michael@0: for (let [dbName, metaData] of this.hostVsStores.get(host)) { michael@0: for (let objectStore of metaData.objectStores.keys()) { michael@0: names.push(JSON.stringify([dbName, objectStore])); michael@0: } michael@0: } michael@0: return names; michael@0: }, michael@0: michael@0: /** michael@0: * Returns all or requested entries from a particular objectStore from the db michael@0: * in the given host. michael@0: * michael@0: * @param {string} host michael@0: * The given host. michael@0: * @param {string} dbName michael@0: * The name of the indexed db from the above host. michael@0: * @param {string} objectStore michael@0: * The name of the object store from the above db. michael@0: * @param {string} id michael@0: * id of the requested entry from the above object store. michael@0: * null if all entries from the above object store are requested. michael@0: * @param {string} index michael@0: * name of the IDBIndex to be iterated on while fetching entries. michael@0: * null or "name" if no index is to be iterated. michael@0: * @param {number} offset michael@0: * ofsset of the entries to be fetched. michael@0: * @param {number} size michael@0: * The intended size of the entries to be fetched. michael@0: */ michael@0: getObjectStoreData: michael@0: function(host, dbName, objectStore, id, index, offset, size) { michael@0: let request = this.openWithOrigin(host, dbName); michael@0: let success = Promise.defer(); michael@0: let data = []; michael@0: if (!size || size > MAX_STORE_OBJECT_COUNT) { michael@0: size = MAX_STORE_OBJECT_COUNT; michael@0: } michael@0: michael@0: request.onsuccess = event => { michael@0: let db = event.target.result; michael@0: michael@0: let transaction = db.transaction(objectStore, "readonly"); michael@0: let source = transaction.objectStore(objectStore); michael@0: if (index && index != "name") { michael@0: source = source.index(index); michael@0: } michael@0: michael@0: source.count().onsuccess = event => { michael@0: let count = event.target.result; michael@0: this.objectsSize[host + dbName + objectStore + index] = count; michael@0: michael@0: if (!offset) { michael@0: offset = 0; michael@0: } michael@0: else if (offset > count) { michael@0: db.close(); michael@0: success.resolve([]); michael@0: return; michael@0: } michael@0: michael@0: if (id) { michael@0: source.get(id).onsuccess = event => { michael@0: db.close(); michael@0: success.resolve([{name: id, value: event.target.result}]); michael@0: }; michael@0: } michael@0: else { michael@0: source.openCursor().onsuccess = event => { michael@0: let cursor = event.target.result; michael@0: michael@0: if (!cursor || data.length >= size) { michael@0: db.close(); michael@0: success.resolve(data); michael@0: return; michael@0: } michael@0: if (offset-- <= 0) { michael@0: data.push({name: cursor.key, value: cursor.value}); michael@0: } michael@0: cursor.continue(); michael@0: }; michael@0: } michael@0: }; michael@0: }; michael@0: request.onerror = () => { michael@0: db.close(); michael@0: success.resolve([]); michael@0: }; michael@0: return success.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the total number of entries for various types of requests to michael@0: * getStoreObjects for Indexed DB actor. michael@0: * michael@0: * @param {string} host michael@0: * The host for the request. michael@0: * @param {array:string} names michael@0: * Array of stringified name objects for indexed db actor. michael@0: * The request type depends on the length of any parsed entry from this michael@0: * array. 0 length refers to request for the whole host. 1 length michael@0: * refers to request for a particular db in the host. 2 length refers michael@0: * to a particular object store in a db in a host. 3 length refers to michael@0: * particular items of an object store in a db in a host. michael@0: * @param {object} options michael@0: * An options object containing following properties: michael@0: * - index {string} The IDBIndex for the object store in the db. michael@0: */ michael@0: getObjectsSize: function(host, names, options) { michael@0: // In Indexed DB, we are interested in only the first name, as the pattern michael@0: // should follow in all entries. michael@0: let name = names[0]; michael@0: let parsedName = JSON.parse(name); michael@0: michael@0: if (parsedName.length == 3) { michael@0: // This is the case where specific entries from an object store were michael@0: // requested michael@0: return names.length; michael@0: } michael@0: else if (parsedName.length == 2) { michael@0: // This is the case where all entries from an object store are requested. michael@0: let index = options.index; michael@0: let [db, objectStore] = parsedName; michael@0: if (this.objectsSize[host + db + objectStore + index]) { michael@0: return this.objectsSize[host + db + objectStore + index]; michael@0: } michael@0: } michael@0: else if (parsedName.length == 1) { michael@0: // This is the case where details of all object stores in a db are michael@0: // requested. michael@0: if (this.hostVsStores.has(host) && michael@0: this.hostVsStores.get(host).has(parsedName[0])) { michael@0: return this.hostVsStores.get(host).get(parsedName[0]).objectStores.size; michael@0: } michael@0: } michael@0: else if (!parsedName || !parsedName.length) { michael@0: // This is the case were details of all dbs in a host are requested. michael@0: if (this.hostVsStores.has(host)) { michael@0: return this.hostVsStores.get(host).size; michael@0: } michael@0: } michael@0: return 0; michael@0: }, michael@0: michael@0: getValuesForHost: async(function*(host, name = "null", options) { michael@0: name = JSON.parse(name); michael@0: if (!name || !name.length) { michael@0: // This means that details about the db in this particular host are michael@0: // requested. michael@0: let dbs = []; michael@0: if (this.hostVsStores.has(host)) { michael@0: for (let [dbName, db] of this.hostVsStores.get(host)) { michael@0: dbs.push(db.toObject()); michael@0: } michael@0: } michael@0: return dbs; michael@0: } michael@0: let [db, objectStore, id] = name; michael@0: if (!objectStore) { michael@0: // This means that details about all the object stores in this db are michael@0: // requested. michael@0: let objectStores = []; michael@0: if (this.hostVsStores.has(host) && this.hostVsStores.get(host).has(db)) { michael@0: for (let objectStore of this.hostVsStores.get(host).get(db).objectStores) { michael@0: objectStores.push(objectStore[1].toObject()); michael@0: } michael@0: } michael@0: return objectStores; michael@0: } michael@0: // Get either all entries from the object store, or a particular id michael@0: return yield this.getObjectStoreData(host, db, objectStore, id, michael@0: options.index, options.size); michael@0: }), michael@0: michael@0: /** michael@0: * Purpose of this method is same as populateStoresForHosts but this is async. michael@0: * This exact same operation cannot be performed in populateStoresForHosts michael@0: * method, as that method is called in initialize method of the actor, which michael@0: * cannot be asynchronous. michael@0: */ michael@0: preListStores: async(function*() { michael@0: this.hostVsStores = new Map(); michael@0: for (let host of this.hosts) { michael@0: yield this.populateStoresForHost(host); michael@0: } michael@0: }), michael@0: michael@0: populateStoresForHost: async(function*(host) { michael@0: let storeMap = new Map(); michael@0: for (let name of (yield this.getDBNamesForHost(host))) { michael@0: storeMap.set(name, yield this.getDBMetaData(host, name)); michael@0: } michael@0: this.hostVsStores.set(host, storeMap); michael@0: }), michael@0: michael@0: /** michael@0: * Removes any illegal characters from the host name to make it a valid file michael@0: * name. michael@0: */ michael@0: getSanitizedHost: function(host) { michael@0: return host.replace(ILLEGAL_CHAR_REGEX, "+"); michael@0: }, michael@0: michael@0: /** michael@0: * Opens an indexed db connection for the given `host` and database `name`. michael@0: */ michael@0: openWithOrigin: function(host, name) { michael@0: let principal; michael@0: michael@0: if (/^(about:|chrome:)/.test(host)) { michael@0: principal = Services.scriptSecurityManager.getSystemPrincipal(); michael@0: } michael@0: else { michael@0: let uri = Services.io.newURI(host, null, null); michael@0: principal = Services.scriptSecurityManager.getCodebasePrincipal(uri); michael@0: } michael@0: michael@0: return indexedDB.openForPrincipal(principal, name); michael@0: }, michael@0: michael@0: /** michael@0: * Fetches and stores all the metadata information for the given database michael@0: * `name` for the given `host`. The stored metadata information is of michael@0: * `DatabaseMetadata` type. michael@0: */ michael@0: getDBMetaData: function(host, name) { michael@0: let request = this.openWithOrigin(host, name); michael@0: let success = Promise.defer(); michael@0: request.onsuccess = event => { michael@0: let db = event.target.result; michael@0: michael@0: let dbData = new DatabaseMetadata(host, db); michael@0: db.close(); michael@0: success.resolve(dbData); michael@0: }; michael@0: request.onerror = event => { michael@0: console.error("Error opening indexeddb database " + name + " for host " + michael@0: host); michael@0: success.resolve(null); michael@0: }; michael@0: return success.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Retrives the proper indexed db database name from the provided .sqlite file michael@0: * location. michael@0: */ michael@0: getNameFromDatabaseFile: async(function*(path) { michael@0: let connection = null; michael@0: let retryCount = 0; michael@0: michael@0: // Content pages might be having an open transaction for the same indexed db michael@0: // which this sqlite file belongs to. In that case, sqlite.openConnection michael@0: // will throw. Thus we retey for some time to see if lock is removed. michael@0: while (!connection && retryCount++ < 25) { michael@0: try { michael@0: connection = yield Sqlite.openConnection({ path: path }); michael@0: } michael@0: catch (ex) { michael@0: // Continuously retrying is overkill. Waiting for 100ms before next try michael@0: yield sleep(100); michael@0: } michael@0: } michael@0: michael@0: if (!connection) { michael@0: return null; michael@0: } michael@0: michael@0: let rows = yield connection.execute("SELECT name FROM database"); michael@0: if (rows.length != 1) { michael@0: return null; michael@0: } michael@0: michael@0: let name = rows[0].getResultByName("name"); michael@0: michael@0: yield connection.close(); michael@0: michael@0: return name; michael@0: }), michael@0: michael@0: /** michael@0: * Fetches all the databases and their metadata for the given `host`. michael@0: */ michael@0: getDBNamesForHost: async(function*(host) { michael@0: let sanitizedHost = this.getSanitizedHost(host); michael@0: let directory = OS.Path.join(OS.Constants.Path.profileDir, "storage", michael@0: "persistent", sanitizedHost, "idb"); michael@0: michael@0: let exists = yield OS.File.exists(directory); michael@0: if (!exists && host.startsWith("about:")) { michael@0: // try for moz-safe-about directory michael@0: sanitizedHost = this.getSanitizedHost("moz-safe-" + host); michael@0: directory = OS.Path.join(OS.Constants.Path.profileDir, "storage", michael@0: "persistent", sanitizedHost, "idb"); michael@0: exists = yield OS.File.exists(directory); michael@0: } michael@0: if (!exists) { michael@0: return []; michael@0: } michael@0: michael@0: let names = []; michael@0: let dirIterator = new OS.File.DirectoryIterator(directory); michael@0: try { michael@0: yield dirIterator.forEach(file => { michael@0: // Skip directories. michael@0: if (file.isDir) { michael@0: return null; michael@0: } michael@0: michael@0: // Skip any non-sqlite files. michael@0: if (!file.name.endsWith(".sqlite")) { michael@0: return null; michael@0: } michael@0: michael@0: return this.getNameFromDatabaseFile(file.path).then(name => { michael@0: if (name) { michael@0: names.push(name); michael@0: } michael@0: return null; michael@0: }); michael@0: }); michael@0: } michael@0: finally { michael@0: dirIterator.close(); michael@0: } michael@0: return names; michael@0: }), michael@0: michael@0: /** michael@0: * Returns the over-the-wire implementation of the indexed db entity. michael@0: */ michael@0: toStoreObject: function(item) { michael@0: if (!item) { michael@0: return null; michael@0: } michael@0: michael@0: if (item.indexes) { michael@0: // Object store meta data michael@0: return { michael@0: objectStore: item.name, michael@0: keyPath: item.keyPath, michael@0: autoIncrement: item.autoIncrement, michael@0: indexes: item.indexes michael@0: }; michael@0: } michael@0: if (item.objectStores) { michael@0: // DB meta data michael@0: return { michael@0: db: item.name, michael@0: origin: item.origin, michael@0: version: item.version, michael@0: objectStores: item.objectStores michael@0: }; michael@0: } michael@0: // Indexed db entry michael@0: return { michael@0: name: item.name, michael@0: value: new LongStringActor(this.conn, JSON.stringify(item.value)) michael@0: }; michael@0: }, michael@0: michael@0: form: function(form, detail) { michael@0: if (detail === "actorid") { michael@0: return this.actorID; michael@0: } michael@0: michael@0: let hosts = {}; michael@0: for (let host of this.hosts) { michael@0: hosts[host] = this.getNamesForHost(host); michael@0: } michael@0: michael@0: return { michael@0: actor: this.actorID, michael@0: hosts: hosts michael@0: }; michael@0: }, michael@0: }); michael@0: michael@0: /** michael@0: * The main Storage Actor. michael@0: */ michael@0: let StorageActor = exports.StorageActor = protocol.ActorClass({ michael@0: typeName: "storage", michael@0: michael@0: get window() { michael@0: return this.parentActor.window; michael@0: }, michael@0: michael@0: get document() { michael@0: return this.parentActor.window.document; michael@0: }, michael@0: michael@0: get windows() { michael@0: return this.childWindowPool; michael@0: }, michael@0: michael@0: /** michael@0: * List of event notifications that the server can send to the client. michael@0: * michael@0: * - stores-update : When any store object in any storage type changes. michael@0: * - stores-cleared : When all the store objects are removed. michael@0: * - stores-reloaded : When all stores are reloaded. This generally mean that michael@0: * we should refetch everything again. michael@0: */ michael@0: events: { michael@0: "stores-update": { michael@0: type: "storesUpdate", michael@0: data: Arg(0, "storeUpdateObject") michael@0: }, michael@0: "stores-cleared": { michael@0: type: "storesCleared", michael@0: data: Arg(0, "json") michael@0: }, michael@0: "stores-reloaded": { michael@0: type: "storesRelaoded", michael@0: data: Arg(0, "json") michael@0: } michael@0: }, michael@0: michael@0: initialize: function (conn, tabActor) { michael@0: protocol.Actor.prototype.initialize.call(this, null); michael@0: michael@0: this.conn = conn; michael@0: this.parentActor = tabActor; michael@0: michael@0: this.childActorPool = new Map(); michael@0: this.childWindowPool = new Set(); michael@0: michael@0: // Fetch all the inner iframe windows in this tab. michael@0: this.fetchChildWindows(this.parentActor.docShell); michael@0: michael@0: // Initialize the registered store types michael@0: for (let [store, actor] of storageTypePool) { michael@0: this.childActorPool.set(store, new actor(this)); michael@0: } michael@0: michael@0: // Notifications that help us keep track of newly added windows and windows michael@0: // that got removed michael@0: Services.obs.addObserver(this, "content-document-global-created", false); michael@0: Services.obs.addObserver(this, "inner-window-destroyed", false); michael@0: this.onPageChange = this.onPageChange.bind(this); michael@0: tabActor.browser.addEventListener("pageshow", this.onPageChange, true); michael@0: tabActor.browser.addEventListener("pagehide", this.onPageChange, true); michael@0: michael@0: this.destroyed = false; michael@0: this.boundUpdate = {}; michael@0: // The time which periodically flushes and transfers the updated store michael@0: // objects. michael@0: this.updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: this.updateTimer.initWithCallback(this , UPDATE_INTERVAL, michael@0: Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP); michael@0: michael@0: // Layout helper for window.parent and window.top helper methods that work michael@0: // accross devices. michael@0: this.layoutHelper = new LayoutHelpers(this.window); michael@0: }, michael@0: michael@0: destroy: function() { michael@0: this.updateTimer.cancel(); michael@0: this.updateTimer = null; michael@0: this.layoutHelper = null; michael@0: // Remove observers michael@0: Services.obs.removeObserver(this, "content-document-global-created", false); michael@0: Services.obs.removeObserver(this, "inner-window-destroyed", false); michael@0: this.destroyed = true; michael@0: if (this.parentActor.browser) { michael@0: this.parentActor.browser.removeEventListener( michael@0: "pageshow", this.onPageChange, true); michael@0: this.parentActor.browser.removeEventListener( michael@0: "pagehide", this.onPageChange, true); michael@0: } michael@0: // Destroy the registered store types michael@0: for (let actor of this.childActorPool.values()) { michael@0: actor.destroy(); michael@0: } michael@0: this.childActorPool.clear(); michael@0: this.childWindowPool.clear(); michael@0: this.childWindowPool = this.childActorPool = null; michael@0: }, michael@0: michael@0: /** michael@0: * Given a docshell, recursively find otu all the child windows from it. michael@0: * michael@0: * @param {nsIDocShell} item michael@0: * The docshell from which all inner windows need to be extracted. michael@0: */ michael@0: fetchChildWindows: function(item) { michael@0: let docShell = item.QueryInterface(Ci.nsIDocShell) michael@0: .QueryInterface(Ci.nsIDocShellTreeItem); michael@0: if (!docShell.contentViewer) { michael@0: return null; michael@0: } michael@0: let window = docShell.contentViewer.DOMDocument.defaultView; michael@0: if (window.location.href == "about:blank") { michael@0: // Skip out about:blank windows as Gecko creates them multiple times while michael@0: // creating any global. michael@0: return null; michael@0: } michael@0: this.childWindowPool.add(window); michael@0: for (let i = 0; i < docShell.childCount; i++) { michael@0: let child = docShell.getChildAt(i); michael@0: this.fetchChildWindows(child); michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: isIncludedInTopLevelWindow: function(window) { michael@0: return this.layoutHelper.isIncludedInTopLevelWindow(window); michael@0: }, michael@0: michael@0: getWindowFromInnerWindowID: function(innerID) { michael@0: innerID = innerID.QueryInterface(Ci.nsISupportsPRUint64).data; michael@0: for (let win of this.childWindowPool.values()) { michael@0: let id = win.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils) michael@0: .currentInnerWindowID; michael@0: if (id == innerID) { michael@0: return win; michael@0: } michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Event handler for any docshell update. This lets us figure out whenever michael@0: * any new window is added, or an existing window is removed. michael@0: */ michael@0: observe: function(subject, topic, data) { michael@0: if (subject.location && michael@0: (!subject.location.href || subject.location.href == "about:blank")) { michael@0: return null; michael@0: } michael@0: if (topic == "content-document-global-created" && michael@0: this.isIncludedInTopLevelWindow(subject)) { michael@0: this.childWindowPool.add(subject); michael@0: events.emit(this, "window-ready", subject); michael@0: } michael@0: else if (topic == "inner-window-destroyed") { michael@0: let window = this.getWindowFromInnerWindowID(subject); michael@0: if (window) { michael@0: this.childWindowPool.delete(window); michael@0: events.emit(this, "window-destroyed", window); michael@0: } michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Called on "pageshow" or "pagehide" event on the chromeEventHandler of michael@0: * current tab. michael@0: * michael@0: * @param {event} The event object passed to the handler. We are using these michael@0: * three properties from the event: michael@0: * - target {document} The document corresponding to the event. michael@0: * - type {string} Name of the event - "pageshow" or "pagehide". michael@0: * - persisted {boolean} true if there was no michael@0: * "content-document-global-created" notification along michael@0: * this event. michael@0: */ michael@0: onPageChange: function({target, type, persisted}) { michael@0: if (this.destroyed) { michael@0: return; michael@0: } michael@0: let window = target.defaultView; michael@0: if (type == "pagehide" && this.childWindowPool.delete(window)) { michael@0: events.emit(this, "window-destroyed", window) michael@0: } michael@0: else if (type == "pageshow" && persisted && window.location.href && michael@0: window.location.href != "about:blank" && michael@0: this.isIncludedInTopLevelWindow(window)) { michael@0: this.childWindowPool.add(window); michael@0: events.emit(this, "window-ready", window); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Lists the available hosts for all the registered storage types. michael@0: * michael@0: * @returns {object} An object containing with the following structure: michael@0: * - : [{ michael@0: * actor: , michael@0: * host: michael@0: * }] michael@0: */ michael@0: listStores: method(async(function*() { michael@0: let toReturn = {}; michael@0: for (let [name, value] of this.childActorPool) { michael@0: if (value.preListStores) { michael@0: yield value.preListStores(); michael@0: } michael@0: toReturn[name] = value; michael@0: } michael@0: return toReturn; michael@0: }), { michael@0: response: RetVal(types.addDictType("storelist", getRegisteredTypes())) michael@0: }), michael@0: michael@0: /** michael@0: * Notifies the client front with the updates in stores at regular intervals. michael@0: */ michael@0: notify: function() { michael@0: if (!this.updatePending || this.updatingUpdateObject) { michael@0: return null; michael@0: } michael@0: events.emit(this, "stores-update", this.boundUpdate); michael@0: this.boundUpdate = {}; michael@0: this.updatePending = false; michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * This method is called by the registered storage types so as to tell the michael@0: * Storage Actor that there are some changes in the stores. Storage Actor then michael@0: * notifies the client front about these changes at regular (UPDATE_INTERVAL) michael@0: * interval. michael@0: * michael@0: * @param {string} action michael@0: * The type of change. One of "added", "changed" or "deleted" michael@0: * @param {string} storeType michael@0: * The storage actor in which this change has occurred. michael@0: * @param {object} data michael@0: * The update object. This object is of the following format: michael@0: * - { michael@0: * : [, ...], michael@0: * : [...], michael@0: * } michael@0: * Where host1, host2 are the host in which this change happened and michael@0: * [: [, ...], michael@0: * : [...], michael@0: * } michael@0: * Where host1, host2 are the hosts which you want to remove and michael@0: * [ -1) { michael@0: this.boundUpdate[action][storeType][host].splice(index, 1); michael@0: } michael@0: } michael@0: if (!this.boundUpdate[action][storeType][host].length) { michael@0: delete this.boundUpdate[action][storeType][host]; michael@0: } michael@0: } michael@0: } michael@0: return null; michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Front for the Storage Actor. michael@0: */ michael@0: let StorageFront = exports.StorageFront = protocol.FrontClass(StorageActor, { michael@0: initialize: function(client, tabForm) { michael@0: protocol.Front.prototype.initialize.call(this, client); michael@0: this.actorID = tabForm.storageActor; michael@0: michael@0: client.addActorPool(this); michael@0: this.manage(this); michael@0: } michael@0: });