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