1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/server/actors/storage.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1708 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const {Cu, Cc, Ci} = require("chrome"); 1.11 +const events = require("sdk/event/core"); 1.12 +const protocol = require("devtools/server/protocol"); 1.13 +const {async} = require("devtools/async-utils"); 1.14 +const {Arg, Option, method, RetVal, types} = protocol; 1.15 +const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); 1.16 + 1.17 +Cu.import("resource://gre/modules/Promise.jsm"); 1.18 +Cu.import("resource://gre/modules/Services.jsm"); 1.19 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.20 +Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm"); 1.21 + 1.22 +XPCOMUtils.defineLazyModuleGetter(this, "Sqlite", 1.23 + "resource://gre/modules/Sqlite.jsm"); 1.24 + 1.25 +XPCOMUtils.defineLazyModuleGetter(this, "OS", 1.26 + "resource://gre/modules/osfile.jsm"); 1.27 + 1.28 +exports.register = function(handle) { 1.29 + handle.addTabActor(StorageActor, "storageActor"); 1.30 +}; 1.31 + 1.32 +exports.unregister = function(handle) { 1.33 + handle.removeTabActor(StorageActor); 1.34 +}; 1.35 + 1.36 +// Global required for window less Indexed DB instantiation. 1.37 +let global = this; 1.38 + 1.39 +// Maximum number of cookies/local storage key-value-pairs that can be sent 1.40 +// over the wire to the client in one request. 1.41 +const MAX_STORE_OBJECT_COUNT = 30; 1.42 +// Interval for the batch job that sends the accumilated update packets to the 1.43 +// client. 1.44 +const UPDATE_INTERVAL = 500; // ms 1.45 + 1.46 +// A RegExp for characters that cannot appear in a file/directory name. This is 1.47 +// used to sanitize the host name for indexed db to lookup whether the file is 1.48 +// present in <profileDir>/storage/persistent/ location 1.49 +let illegalFileNameCharacters = [ 1.50 + "[", 1.51 + "\\x00-\\x25", // Control characters \001 to \037 1.52 + "/:*?\\\"<>|\\\\", // Special characters 1.53 + "]" 1.54 +].join(""); 1.55 +let ILLEGAL_CHAR_REGEX = new RegExp(illegalFileNameCharacters, "g"); 1.56 + 1.57 +// Holder for all the registered storage actors. 1.58 +let storageTypePool = new Map(); 1.59 + 1.60 +/** 1.61 + * Gets an accumulated list of all storage actors registered to be used to 1.62 + * create a RetVal to define the return type of StorageActor.listStores method. 1.63 + */ 1.64 +function getRegisteredTypes() { 1.65 + let registeredTypes = {}; 1.66 + for (let store of storageTypePool.keys()) { 1.67 + registeredTypes[store] = store; 1.68 + } 1.69 + return registeredTypes; 1.70 +} 1.71 + 1.72 +/** 1.73 + * An async method equivalent to setTimeout but using Promises 1.74 + * 1.75 + * @param {number} time 1.76 + * The wait Ttme in milliseconds. 1.77 + */ 1.78 +function sleep(time) { 1.79 + let wait = Promise.defer(); 1.80 + let updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 1.81 + updateTimer.initWithCallback({ 1.82 + notify: function() { 1.83 + updateTimer.cancel(); 1.84 + updateTimer = null; 1.85 + wait.resolve(null); 1.86 + } 1.87 + } , time, Ci.nsITimer.TYPE_ONE_SHOT); 1.88 + return wait.promise; 1.89 +} 1.90 + 1.91 +// Cookies store object 1.92 +types.addDictType("cookieobject", { 1.93 + name: "string", 1.94 + value: "longstring", 1.95 + path: "nullable:string", 1.96 + host: "string", 1.97 + isDomain: "boolean", 1.98 + isSecure: "boolean", 1.99 + isHttpOnly: "boolean", 1.100 + creationTime: "number", 1.101 + lastAccessed: "number", 1.102 + expires: "number" 1.103 +}); 1.104 + 1.105 +// Array of cookie store objects 1.106 +types.addDictType("cookiestoreobject", { 1.107 + total: "number", 1.108 + offset: "number", 1.109 + data: "array:nullable:cookieobject" 1.110 +}); 1.111 + 1.112 +// Local Storage / Session Storage store object 1.113 +types.addDictType("storageobject", { 1.114 + name: "string", 1.115 + value: "longstring" 1.116 +}); 1.117 + 1.118 +// Array of Local Storage / Session Storage store objects 1.119 +types.addDictType("storagestoreobject", { 1.120 + total: "number", 1.121 + offset: "number", 1.122 + data: "array:nullable:storageobject" 1.123 +}); 1.124 + 1.125 +// Indexed DB store object 1.126 +// This is a union on idb object, db metadata object and object store metadata 1.127 +// object 1.128 +types.addDictType("idbobject", { 1.129 + name: "nullable:string", 1.130 + db: "nullable:string", 1.131 + objectStore: "nullable:string", 1.132 + origin: "nullable:string", 1.133 + version: "nullable:number", 1.134 + objectStores: "nullable:number", 1.135 + keyPath: "nullable:string", 1.136 + autoIncrement: "nullable:boolean", 1.137 + indexes: "nullable:string", 1.138 + value: "nullable:longstring" 1.139 +}); 1.140 + 1.141 +// Array of Indexed DB store objects 1.142 +types.addDictType("idbstoreobject", { 1.143 + total: "number", 1.144 + offset: "number", 1.145 + data: "array:nullable:idbobject" 1.146 +}); 1.147 + 1.148 +// Update notification object 1.149 +types.addDictType("storeUpdateObject", { 1.150 + changed: "nullable:json", 1.151 + deleted: "nullable:json", 1.152 + added: "nullable:json" 1.153 +}); 1.154 + 1.155 +// Helper methods to create a storage actor. 1.156 +let StorageActors = {}; 1.157 + 1.158 +/** 1.159 + * Creates a default object with the common methods required by all storage 1.160 + * actors. 1.161 + * 1.162 + * This default object is missing a couple of required methods that should be 1.163 + * implemented seperately for each actor. They are namely: 1.164 + * - observe : Method which gets triggered on the notificaiton of the watched 1.165 + * topic. 1.166 + * - getNamesForHost : Given a host, get list of all known store names. 1.167 + * - getValuesForHost : Given a host (and optianally a name) get all known 1.168 + * store objects. 1.169 + * - toStoreObject : Given a store object, convert it to the required format 1.170 + * so that it can be transferred over wire. 1.171 + * - populateStoresForHost : Given a host, populate the map of all store 1.172 + * objects for it 1.173 + * 1.174 + * @param {string} typeName 1.175 + * The typeName of the actor. 1.176 + * @param {string} observationTopic 1.177 + * The topic which this actor listens to via Notification Observers. 1.178 + * @param {string} storeObjectType 1.179 + * The RetVal type of the store object of this actor. 1.180 + */ 1.181 +StorageActors.defaults = function(typeName, observationTopic, storeObjectType) { 1.182 + return { 1.183 + typeName: typeName, 1.184 + 1.185 + get conn() { 1.186 + return this.storageActor.conn; 1.187 + }, 1.188 + 1.189 + /** 1.190 + * Returns a list of currently knwon hosts for the target window. This list 1.191 + * contains unique hosts from the window + all inner windows. 1.192 + */ 1.193 + get hosts() { 1.194 + let hosts = new Set(); 1.195 + for (let {location} of this.storageActor.windows) { 1.196 + hosts.add(this.getHostName(location)); 1.197 + } 1.198 + return hosts; 1.199 + }, 1.200 + 1.201 + /** 1.202 + * Returns all the windows present on the page. Includes main window + inner 1.203 + * iframe windows. 1.204 + */ 1.205 + get windows() { 1.206 + return this.storageActor.windows; 1.207 + }, 1.208 + 1.209 + /** 1.210 + * Converts the window.location object into host. 1.211 + */ 1.212 + getHostName: function(location) { 1.213 + return location.hostname || location.href; 1.214 + }, 1.215 + 1.216 + initialize: function(storageActor) { 1.217 + protocol.Actor.prototype.initialize.call(this, null); 1.218 + 1.219 + this.storageActor = storageActor; 1.220 + 1.221 + this.populateStoresForHosts(); 1.222 + if (observationTopic) { 1.223 + Services.obs.addObserver(this, observationTopic, false); 1.224 + } 1.225 + this.onWindowReady = this.onWindowReady.bind(this); 1.226 + this.onWindowDestroyed = this.onWindowDestroyed.bind(this); 1.227 + events.on(this.storageActor, "window-ready", this.onWindowReady); 1.228 + events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed); 1.229 + }, 1.230 + 1.231 + destroy: function() { 1.232 + this.hostVsStores = null; 1.233 + if (observationTopic) { 1.234 + Services.obs.removeObserver(this, observationTopic, false); 1.235 + } 1.236 + events.off(this.storageActor, "window-ready", this.onWindowReady); 1.237 + events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed); 1.238 + }, 1.239 + 1.240 + getNamesForHost: function(host) { 1.241 + return [...this.hostVsStores.get(host).keys()]; 1.242 + }, 1.243 + 1.244 + getValuesForHost: function(host, name) { 1.245 + if (name) { 1.246 + return [this.hostVsStores.get(host).get(name)]; 1.247 + } 1.248 + return [...this.hostVsStores.get(host).values()]; 1.249 + }, 1.250 + 1.251 + getObjectsSize: function(host, names) { 1.252 + return names.length; 1.253 + }, 1.254 + 1.255 + /** 1.256 + * When a new window is added to the page. This generally means that a new 1.257 + * iframe is created, or the current window is completely reloaded. 1.258 + * 1.259 + * @param {window} window 1.260 + * The window which was added. 1.261 + */ 1.262 + onWindowReady: async(function*(window) { 1.263 + let host = this.getHostName(window.location); 1.264 + if (!this.hostVsStores.has(host)) { 1.265 + yield this.populateStoresForHost(host, window); 1.266 + let data = {}; 1.267 + data[host] = this.getNamesForHost(host); 1.268 + this.storageActor.update("added", typeName, data); 1.269 + } 1.270 + }), 1.271 + 1.272 + /** 1.273 + * When a window is removed from the page. This generally means that an 1.274 + * iframe was removed, or the current window reload is triggered. 1.275 + * 1.276 + * @param {window} window 1.277 + * The window which was removed. 1.278 + */ 1.279 + onWindowDestroyed: function(window) { 1.280 + let host = this.getHostName(window.location); 1.281 + if (!this.hosts.has(host)) { 1.282 + this.hostVsStores.delete(host); 1.283 + let data = {}; 1.284 + data[host] = []; 1.285 + this.storageActor.update("deleted", typeName, data); 1.286 + } 1.287 + }, 1.288 + 1.289 + form: function(form, detail) { 1.290 + if (detail === "actorid") { 1.291 + return this.actorID; 1.292 + } 1.293 + 1.294 + let hosts = {}; 1.295 + for (let host of this.hosts) { 1.296 + hosts[host] = []; 1.297 + } 1.298 + 1.299 + return { 1.300 + actor: this.actorID, 1.301 + hosts: hosts 1.302 + }; 1.303 + }, 1.304 + 1.305 + /** 1.306 + * Populates a map of known hosts vs a map of stores vs value. 1.307 + */ 1.308 + populateStoresForHosts: function() { 1.309 + this.hostVsStores = new Map(); 1.310 + for (let host of this.hosts) { 1.311 + this.populateStoresForHost(host); 1.312 + } 1.313 + }, 1.314 + 1.315 + /** 1.316 + * Returns a list of requested store objects. Maximum values returned are 1.317 + * MAX_STORE_OBJECT_COUNT. This method returns paginated values whose 1.318 + * starting index and total size can be controlled via the options object 1.319 + * 1.320 + * @param {string} host 1.321 + * The host name for which the store values are required. 1.322 + * @param {array:string} names 1.323 + * Array containing the names of required store objects. Empty if all 1.324 + * items are required. 1.325 + * @param {object} options 1.326 + * Additional options for the request containing following properties: 1.327 + * - offset {number} : The begin index of the returned array amongst 1.328 + * the total values 1.329 + * - size {number} : The number of values required. 1.330 + * - sortOn {string} : The values should be sorted on this property. 1.331 + * - index {string} : In case of indexed db, the IDBIndex to be used 1.332 + * for fetching the values. 1.333 + * 1.334 + * @return {object} An object containing following properties: 1.335 + * - offset - The actual offset of the returned array. This might 1.336 + * be different from the requested offset if that was 1.337 + * invalid 1.338 + * - total - The total number of entries possible. 1.339 + * - data - The requested values. 1.340 + */ 1.341 + getStoreObjects: method(async(function*(host, names, options = {}) { 1.342 + let offset = options.offset || 0; 1.343 + let size = options.size || MAX_STORE_OBJECT_COUNT; 1.344 + if (size > MAX_STORE_OBJECT_COUNT) { 1.345 + size = MAX_STORE_OBJECT_COUNT; 1.346 + } 1.347 + let sortOn = options.sortOn || "name"; 1.348 + 1.349 + let toReturn = { 1.350 + offset: offset, 1.351 + total: 0, 1.352 + data: [] 1.353 + }; 1.354 + 1.355 + if (names) { 1.356 + for (let name of names) { 1.357 + toReturn.data.push( 1.358 + // yield because getValuesForHost is async for Indexed DB 1.359 + ...(yield this.getValuesForHost(host, name, options)) 1.360 + ); 1.361 + } 1.362 + toReturn.total = this.getObjectsSize(host, names, options); 1.363 + if (offset > toReturn.total) { 1.364 + // In this case, toReturn.data is an empty array. 1.365 + toReturn.offset = toReturn.total; 1.366 + toReturn.data = []; 1.367 + } 1.368 + else { 1.369 + toReturn.data = toReturn.data.sort((a,b) => { 1.370 + return a[sortOn] - b[sortOn]; 1.371 + }).slice(offset, offset + size).map(a => this.toStoreObject(a)); 1.372 + } 1.373 + } 1.374 + else { 1.375 + let total = yield this.getValuesForHost(host); 1.376 + toReturn.total = total.length; 1.377 + if (offset > toReturn.total) { 1.378 + // In this case, toReturn.data is an empty array. 1.379 + toReturn.offset = offset = toReturn.total; 1.380 + toReturn.data = []; 1.381 + } 1.382 + else { 1.383 + toReturn.data = total.sort((a,b) => { 1.384 + return a[sortOn] - b[sortOn]; 1.385 + }).slice(offset, offset + size) 1.386 + .map(object => this.toStoreObject(object)); 1.387 + } 1.388 + } 1.389 + 1.390 + return toReturn; 1.391 + }), { 1.392 + request: { 1.393 + host: Arg(0), 1.394 + names: Arg(1, "nullable:array:string"), 1.395 + options: Arg(2, "nullable:json") 1.396 + }, 1.397 + response: RetVal(storeObjectType) 1.398 + }) 1.399 + } 1.400 +}; 1.401 + 1.402 +/** 1.403 + * Creates an actor and its corresponding front and registers it to the Storage 1.404 + * Actor. 1.405 + * 1.406 + * @See StorageActors.defaults() 1.407 + * 1.408 + * @param {object} options 1.409 + * Options required by StorageActors.defaults method which are : 1.410 + * - typeName {string} 1.411 + * The typeName of the actor. 1.412 + * - observationTopic {string} 1.413 + * The topic which this actor listens to via 1.414 + * Notification Observers. 1.415 + * - storeObjectType {string} 1.416 + * The RetVal type of the store object of this actor. 1.417 + * @param {object} overrides 1.418 + * All the methods which you want to be differnt from the ones in 1.419 + * StorageActors.defaults method plus the required ones described there. 1.420 + */ 1.421 +StorageActors.createActor = function(options = {}, overrides = {}) { 1.422 + let actorObject = StorageActors.defaults( 1.423 + options.typeName, 1.424 + options.observationTopic || null, 1.425 + options.storeObjectType 1.426 + ); 1.427 + for (let key in overrides) { 1.428 + actorObject[key] = overrides[key]; 1.429 + } 1.430 + 1.431 + let actor = protocol.ActorClass(actorObject); 1.432 + let front = protocol.FrontClass(actor, { 1.433 + form: function(form, detail) { 1.434 + if (detail === "actorid") { 1.435 + this.actorID = form; 1.436 + return null; 1.437 + } 1.438 + 1.439 + this.actorID = form.actor; 1.440 + this.hosts = form.hosts; 1.441 + return null; 1.442 + } 1.443 + }); 1.444 + storageTypePool.set(actorObject.typeName, actor); 1.445 +} 1.446 + 1.447 +/** 1.448 + * The Cookies actor and front. 1.449 + */ 1.450 +StorageActors.createActor({ 1.451 + typeName: "cookies", 1.452 + storeObjectType: "cookiestoreobject" 1.453 +}, { 1.454 + initialize: function(storageActor) { 1.455 + protocol.Actor.prototype.initialize.call(this, null); 1.456 + 1.457 + this.storageActor = storageActor; 1.458 + 1.459 + this.populateStoresForHosts(); 1.460 + Services.obs.addObserver(this, "cookie-changed", false); 1.461 + Services.obs.addObserver(this, "http-on-response-set-cookie", false); 1.462 + this.onWindowReady = this.onWindowReady.bind(this); 1.463 + this.onWindowDestroyed = this.onWindowDestroyed.bind(this); 1.464 + events.on(this.storageActor, "window-ready", this.onWindowReady); 1.465 + events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed); 1.466 + }, 1.467 + 1.468 + destroy: function() { 1.469 + this.hostVsStores = null; 1.470 + Services.obs.removeObserver(this, "cookie-changed", false); 1.471 + Services.obs.removeObserver(this, "http-on-response-set-cookie", false); 1.472 + events.off(this.storageActor, "window-ready", this.onWindowReady); 1.473 + events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed); 1.474 + }, 1.475 + 1.476 + /** 1.477 + * Given a cookie object, figure out all the matching hosts from the page that 1.478 + * the cookie belong to. 1.479 + */ 1.480 + getMatchingHosts: function(cookies) { 1.481 + if (!cookies.length) { 1.482 + cookies = [cookies]; 1.483 + } 1.484 + let hosts = new Set(); 1.485 + for (let host of this.hosts) { 1.486 + for (let cookie of cookies) { 1.487 + if (this.isCookieAtHost(cookie, host)) { 1.488 + hosts.add(host); 1.489 + } 1.490 + } 1.491 + } 1.492 + return [...hosts]; 1.493 + }, 1.494 + 1.495 + /** 1.496 + * Given a cookie object and a host, figure out if the cookie is valid for 1.497 + * that host. 1.498 + */ 1.499 + isCookieAtHost: function(cookie, host) { 1.500 + try { 1.501 + cookie = cookie.QueryInterface(Ci.nsICookie) 1.502 + .QueryInterface(Ci.nsICookie2); 1.503 + } catch(ex) { 1.504 + return false; 1.505 + } 1.506 + if (cookie.host == null) { 1.507 + return host == null; 1.508 + } 1.509 + if (cookie.host.startsWith(".")) { 1.510 + return host.endsWith(cookie.host); 1.511 + } 1.512 + else { 1.513 + return cookie.host == host; 1.514 + } 1.515 + }, 1.516 + 1.517 + toStoreObject: function(cookie) { 1.518 + if (!cookie) { 1.519 + return null; 1.520 + } 1.521 + 1.522 + return { 1.523 + name: cookie.name, 1.524 + path: cookie.path || "", 1.525 + host: cookie.host || "", 1.526 + expires: (cookie.expires || 0) * 1000, // because expires is in seconds 1.527 + creationTime: cookie.creationTime / 1000, // because it is in micro seconds 1.528 + lastAccessed: cookie.lastAccessed / 1000, // - do - 1.529 + value: new LongStringActor(this.conn, cookie.value || ""), 1.530 + isDomain: cookie.isDomain, 1.531 + isSecure: cookie.isSecure, 1.532 + isHttpOnly: cookie.isHttpOnly 1.533 + } 1.534 + }, 1.535 + 1.536 + populateStoresForHost: function(host) { 1.537 + this.hostVsStores.set(host, new Map()); 1.538 + let cookies = Services.cookies.getCookiesFromHost(host); 1.539 + while (cookies.hasMoreElements()) { 1.540 + let cookie = cookies.getNext().QueryInterface(Ci.nsICookie) 1.541 + .QueryInterface(Ci.nsICookie2); 1.542 + if (this.isCookieAtHost(cookie, host)) { 1.543 + this.hostVsStores.get(host).set(cookie.name, cookie); 1.544 + } 1.545 + } 1.546 + }, 1.547 + 1.548 + /** 1.549 + * Converts the raw cookie string returned in http request's response header 1.550 + * to a nsICookie compatible object. 1.551 + * 1.552 + * @param {string} cookieString 1.553 + * The raw cookie string coming from response header. 1.554 + * @param {string} domain 1.555 + * The domain of the url of the nsiChannel the cookie corresponds to. 1.556 + * This will be used when the cookie string does not have a domain. 1.557 + * 1.558 + * @returns {[object]} 1.559 + * An array of nsICookie like objects representing the cookies. 1.560 + */ 1.561 + parseCookieString: function(cookieString, domain) { 1.562 + /** 1.563 + * Takes a date string present in raw cookie string coming from http 1.564 + * request's response headers and returns the number of milliseconds elapsed 1.565 + * since epoch. If the date string is undefined, its probably a session 1.566 + * cookie so return 0. 1.567 + */ 1.568 + let parseDateString = dateString => { 1.569 + return dateString ? new Date(dateString.replace(/-/g, " ")).getTime(): 0; 1.570 + }; 1.571 + 1.572 + let cookies = []; 1.573 + for (let string of cookieString.split("\n")) { 1.574 + let keyVals = {}, name = null; 1.575 + for (let keyVal of string.split(/;\s*/)) { 1.576 + let tokens = keyVal.split(/\s*=\s*/); 1.577 + if (!name) { 1.578 + name = tokens[0]; 1.579 + } 1.580 + else { 1.581 + tokens[0] = tokens[0].toLowerCase(); 1.582 + } 1.583 + keyVals[tokens.splice(0, 1)[0]] = tokens.join("="); 1.584 + } 1.585 + let expiresTime = parseDateString(keyVals.expires); 1.586 + keyVals.domain = keyVals.domain || domain; 1.587 + cookies.push({ 1.588 + name: name, 1.589 + value: keyVals[name] || "", 1.590 + path: keyVals.path, 1.591 + host: keyVals.domain, 1.592 + expires: expiresTime/1000, // seconds, to match with nsiCookie.expires 1.593 + lastAccessed: expiresTime * 1000, 1.594 + // microseconds, to match with nsiCookie.lastAccessed 1.595 + creationTime: expiresTime * 1000, 1.596 + // microseconds, to match with nsiCookie.creationTime 1.597 + isHttpOnly: true, 1.598 + isSecure: keyVals.secure != null, 1.599 + isDomain: keyVals.domain.startsWith("."), 1.600 + }); 1.601 + } 1.602 + return cookies; 1.603 + }, 1.604 + 1.605 + /** 1.606 + * Notification observer for topics "http-on-response-set-cookie" and 1.607 + * "cookie-change". 1.608 + * 1.609 + * @param subject 1.610 + * {nsiChannel} The channel associated to the SET-COOKIE response 1.611 + * header in case of "http-on-response-set-cookie" topic. 1.612 + * {nsiCookie|[nsiCookie]} A single nsiCookie object or a list of it 1.613 + * depending on the action. Array is only in case of "batch-deleted" 1.614 + * action. 1.615 + * @param {string} topic 1.616 + * The topic of the notification. 1.617 + * @param {string} action 1.618 + * Additional data associated with the notification. Its the type of 1.619 + * cookie change in case of "cookie-change" topic and the cookie string 1.620 + * in case of "http-on-response-set-cookie" topic. 1.621 + */ 1.622 + observe: function(subject, topic, action) { 1.623 + if (topic == "http-on-response-set-cookie") { 1.624 + // Some cookies got created as a result of http response header SET-COOKIE 1.625 + // subject here is an nsIChannel object referring to the http request. 1.626 + // We get the requestor of this channel and thus the content window. 1.627 + let channel = subject.QueryInterface(Ci.nsIChannel); 1.628 + let requestor = channel.notificationCallbacks || 1.629 + channel.loadGroup.notificationCallbacks; 1.630 + // requester can be null sometimes. 1.631 + let window = requestor ? requestor.getInterface(Ci.nsIDOMWindow): null; 1.632 + // Proceed only if this window is present on the currently targetted tab 1.633 + if (window && this.storageActor.isIncludedInTopLevelWindow(window)) { 1.634 + let host = this.getHostName(window.location); 1.635 + if (this.hostVsStores.has(host)) { 1.636 + let cookies = this.parseCookieString(action, channel.URI.host); 1.637 + let data = {}; 1.638 + data[host] = []; 1.639 + for (let cookie of cookies) { 1.640 + if (this.hostVsStores.get(host).has(cookie.name)) { 1.641 + continue; 1.642 + } 1.643 + this.hostVsStores.get(host).set(cookie.name, cookie); 1.644 + data[host].push(cookie.name); 1.645 + } 1.646 + if (data[host]) { 1.647 + this.storageActor.update("added", "cookies", data); 1.648 + } 1.649 + } 1.650 + } 1.651 + return null; 1.652 + } 1.653 + 1.654 + if (topic != "cookie-changed") { 1.655 + return null; 1.656 + } 1.657 + 1.658 + let hosts = this.getMatchingHosts(subject); 1.659 + let data = {}; 1.660 + 1.661 + switch(action) { 1.662 + case "added": 1.663 + case "changed": 1.664 + if (hosts.length) { 1.665 + subject = subject.QueryInterface(Ci.nsICookie) 1.666 + .QueryInterface(Ci.nsICookie2); 1.667 + for (let host of hosts) { 1.668 + this.hostVsStores.get(host).set(subject.name, subject); 1.669 + data[host] = [subject.name]; 1.670 + } 1.671 + this.storageActor.update(action, "cookies", data); 1.672 + } 1.673 + break; 1.674 + 1.675 + case "deleted": 1.676 + if (hosts.length) { 1.677 + subject = subject.QueryInterface(Ci.nsICookie) 1.678 + .QueryInterface(Ci.nsICookie2); 1.679 + for (let host of hosts) { 1.680 + this.hostVsStores.get(host).delete(subject.name); 1.681 + data[host] = [subject.name]; 1.682 + } 1.683 + this.storageActor.update("deleted", "cookies", data); 1.684 + } 1.685 + break; 1.686 + 1.687 + case "batch-deleted": 1.688 + if (hosts.length) { 1.689 + for (let host of hosts) { 1.690 + let stores = []; 1.691 + for (let cookie of subject) { 1.692 + cookie = cookie.QueryInterface(Ci.nsICookie) 1.693 + .QueryInterface(Ci.nsICookie2); 1.694 + this.hostVsStores.get(host).delete(cookie.name); 1.695 + stores.push(cookie.name); 1.696 + } 1.697 + data[host] = stores; 1.698 + } 1.699 + this.storageActor.update("deleted", "cookies", data); 1.700 + } 1.701 + break; 1.702 + 1.703 + case "cleared": 1.704 + this.storageActor.update("cleared", "cookies", hosts); 1.705 + break; 1.706 + 1.707 + case "reload": 1.708 + this.storageActor.update("reloaded", "cookies", hosts); 1.709 + break; 1.710 + } 1.711 + return null; 1.712 + }, 1.713 +}); 1.714 + 1.715 + 1.716 +/** 1.717 + * Helper method to create the overriden object required in 1.718 + * StorageActors.createActor for Local Storage and Session Storage. 1.719 + * This method exists as both Local Storage and Session Storage have almost 1.720 + * identical actors. 1.721 + */ 1.722 +function getObjectForLocalOrSessionStorage(type) { 1.723 + return { 1.724 + getNamesForHost: function(host) { 1.725 + let storage = this.hostVsStores.get(host); 1.726 + return [key for (key in storage)]; 1.727 + }, 1.728 + 1.729 + getValuesForHost: function(host, name) { 1.730 + let storage = this.hostVsStores.get(host); 1.731 + if (name) { 1.732 + return [{name: name, value: storage.getItem(name)}]; 1.733 + } 1.734 + return [{name: name, value: storage.getItem(name)} for (name in storage)]; 1.735 + }, 1.736 + 1.737 + getHostName: function(location) { 1.738 + if (!location.host) { 1.739 + return location.href; 1.740 + } 1.741 + return location.protocol + "//" + location.host; 1.742 + }, 1.743 + 1.744 + populateStoresForHost: function(host, window) { 1.745 + try { 1.746 + this.hostVsStores.set(host, window[type]); 1.747 + } catch(ex) { 1.748 + // Exceptions happen when local or session storage is inaccessible 1.749 + } 1.750 + return null; 1.751 + }, 1.752 + 1.753 + populateStoresForHosts: function() { 1.754 + this.hostVsStores = new Map(); 1.755 + try { 1.756 + for (let window of this.windows) { 1.757 + this.hostVsStores.set(this.getHostName(window.location), window[type]); 1.758 + } 1.759 + } catch(ex) { 1.760 + // Exceptions happen when local or session storage is inaccessible 1.761 + } 1.762 + return null; 1.763 + }, 1.764 + 1.765 + observe: function(subject, topic, data) { 1.766 + if (topic != "dom-storage2-changed" || data != type) { 1.767 + return null; 1.768 + } 1.769 + 1.770 + let host = this.getSchemaAndHost(subject.url); 1.771 + 1.772 + if (!this.hostVsStores.has(host)) { 1.773 + return null; 1.774 + } 1.775 + 1.776 + let action = "changed"; 1.777 + if (subject.key == null) { 1.778 + return this.storageActor.update("cleared", type, [host]); 1.779 + } 1.780 + else if (subject.oldValue == null) { 1.781 + action = "added"; 1.782 + } 1.783 + else if (subject.newValue == null) { 1.784 + action = "deleted"; 1.785 + } 1.786 + let updateData = {}; 1.787 + updateData[host] = [subject.key]; 1.788 + return this.storageActor.update(action, type, updateData); 1.789 + }, 1.790 + 1.791 + /** 1.792 + * Given a url, correctly determine its protocol + hostname part. 1.793 + */ 1.794 + getSchemaAndHost: function(url) { 1.795 + let uri = Services.io.newURI(url, null, null); 1.796 + return uri.scheme + "://" + uri.hostPort; 1.797 + }, 1.798 + 1.799 + toStoreObject: function(item) { 1.800 + if (!item) { 1.801 + return null; 1.802 + } 1.803 + 1.804 + return { 1.805 + name: item.name, 1.806 + value: new LongStringActor(this.conn, item.value || "") 1.807 + }; 1.808 + }, 1.809 + } 1.810 +}; 1.811 + 1.812 +/** 1.813 + * The Local Storage actor and front. 1.814 + */ 1.815 +StorageActors.createActor({ 1.816 + typeName: "localStorage", 1.817 + observationTopic: "dom-storage2-changed", 1.818 + storeObjectType: "storagestoreobject" 1.819 +}, getObjectForLocalOrSessionStorage("localStorage")); 1.820 + 1.821 +/** 1.822 + * The Session Storage actor and front. 1.823 + */ 1.824 +StorageActors.createActor({ 1.825 + typeName: "sessionStorage", 1.826 + observationTopic: "dom-storage2-changed", 1.827 + storeObjectType: "storagestoreobject" 1.828 +}, getObjectForLocalOrSessionStorage("sessionStorage")); 1.829 + 1.830 + 1.831 +/** 1.832 + * Code related to the Indexed DB actor and front 1.833 + */ 1.834 + 1.835 +// Metadata holder objects for various components of Indexed DB 1.836 + 1.837 +/** 1.838 + * Meta data object for a particular index in an object store 1.839 + * 1.840 + * @param {IDBIndex} index 1.841 + * The particular index from the object store. 1.842 + */ 1.843 +function IndexMetadata(index) { 1.844 + this._name = index.name; 1.845 + this._keyPath = index.keyPath; 1.846 + this._unique = index.unique; 1.847 + this._multiEntry = index.multiEntry; 1.848 +} 1.849 +IndexMetadata.prototype = { 1.850 + toObject: function() { 1.851 + return { 1.852 + name: this._name, 1.853 + keyPath: this._keyPath, 1.854 + unique: this._unique, 1.855 + multiEntry: this._multiEntry 1.856 + }; 1.857 + } 1.858 +}; 1.859 + 1.860 +/** 1.861 + * Meta data object for a particular object store in a db 1.862 + * 1.863 + * @param {IDBObjectStore} objectStore 1.864 + * The particular object store from the db. 1.865 + */ 1.866 +function ObjectStoreMetadata(objectStore) { 1.867 + this._name = objectStore.name; 1.868 + this._keyPath = objectStore.keyPath; 1.869 + this._autoIncrement = objectStore.autoIncrement; 1.870 + this._indexes = new Map(); 1.871 + 1.872 + for (let i = 0; i < objectStore.indexNames.length; i++) { 1.873 + let index = objectStore.index(objectStore.indexNames[i]); 1.874 + this._indexes.set(index, new IndexMetadata(index)); 1.875 + } 1.876 +} 1.877 +ObjectStoreMetadata.prototype = { 1.878 + toObject: function() { 1.879 + return { 1.880 + name: this._name, 1.881 + keyPath: this._keyPath, 1.882 + autoIncrement: this._autoIncrement, 1.883 + indexes: JSON.stringify( 1.884 + [index.toObject() for (index of this._indexes.values())] 1.885 + ) 1.886 + }; 1.887 + } 1.888 +}; 1.889 + 1.890 +/** 1.891 + * Meta data object for a particular indexed db in a host. 1.892 + * 1.893 + * @param {string} origin 1.894 + * The host associated with this indexed db. 1.895 + * @param {IDBDatabase} db 1.896 + * The particular indexed db. 1.897 + */ 1.898 +function DatabaseMetadata(origin, db) { 1.899 + this._origin = origin; 1.900 + this._name = db.name; 1.901 + this._version = db.version; 1.902 + this._objectStores = new Map(); 1.903 + 1.904 + if (db.objectStoreNames.length) { 1.905 + let transaction = db.transaction(db.objectStoreNames, "readonly"); 1.906 + 1.907 + for (let i = 0; i < transaction.objectStoreNames.length; i++) { 1.908 + let objectStore = 1.909 + transaction.objectStore(transaction.objectStoreNames[i]); 1.910 + this._objectStores.set(transaction.objectStoreNames[i], 1.911 + new ObjectStoreMetadata(objectStore)); 1.912 + } 1.913 + } 1.914 +}; 1.915 +DatabaseMetadata.prototype = { 1.916 + get objectStores() { 1.917 + return this._objectStores; 1.918 + }, 1.919 + 1.920 + toObject: function() { 1.921 + return { 1.922 + name: this._name, 1.923 + origin: this._origin, 1.924 + version: this._version, 1.925 + objectStores: this._objectStores.size 1.926 + }; 1.927 + } 1.928 +}; 1.929 + 1.930 +StorageActors.createActor({ 1.931 + typeName: "indexedDB", 1.932 + storeObjectType: "idbstoreobject" 1.933 +}, { 1.934 + initialize: function(storageActor) { 1.935 + protocol.Actor.prototype.initialize.call(this, null); 1.936 + if (!global.indexedDB) { 1.937 + let idbManager = Cc["@mozilla.org/dom/indexeddb/manager;1"] 1.938 + .getService(Ci.nsIIndexedDatabaseManager); 1.939 + idbManager.initWindowless(global); 1.940 + } 1.941 + this.objectsSize = {}; 1.942 + this.storageActor = storageActor; 1.943 + this.onWindowReady = this.onWindowReady.bind(this); 1.944 + this.onWindowDestroyed = this.onWindowDestroyed.bind(this); 1.945 + events.on(this.storageActor, "window-ready", this.onWindowReady); 1.946 + events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed); 1.947 + }, 1.948 + 1.949 + destroy: function() { 1.950 + this.hostVsStores = null; 1.951 + this.objectsSize = null; 1.952 + events.off(this.storageActor, "window-ready", this.onWindowReady); 1.953 + events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed); 1.954 + }, 1.955 + 1.956 + getHostName: function(location) { 1.957 + if (!location.host) { 1.958 + return location.href; 1.959 + } 1.960 + return location.protocol + "//" + location.host; 1.961 + }, 1.962 + 1.963 + /** 1.964 + * This method is overriden and left blank as for indexedDB, this operation 1.965 + * cannot be performed synchronously. Thus, the preListStores method exists to 1.966 + * do the same task asynchronously. 1.967 + */ 1.968 + populateStoresForHosts: function() { 1.969 + }, 1.970 + 1.971 + getNamesForHost: function(host) { 1.972 + let names = []; 1.973 + for (let [dbName, metaData] of this.hostVsStores.get(host)) { 1.974 + for (let objectStore of metaData.objectStores.keys()) { 1.975 + names.push(JSON.stringify([dbName, objectStore])); 1.976 + } 1.977 + } 1.978 + return names; 1.979 + }, 1.980 + 1.981 + /** 1.982 + * Returns all or requested entries from a particular objectStore from the db 1.983 + * in the given host. 1.984 + * 1.985 + * @param {string} host 1.986 + * The given host. 1.987 + * @param {string} dbName 1.988 + * The name of the indexed db from the above host. 1.989 + * @param {string} objectStore 1.990 + * The name of the object store from the above db. 1.991 + * @param {string} id 1.992 + * id of the requested entry from the above object store. 1.993 + * null if all entries from the above object store are requested. 1.994 + * @param {string} index 1.995 + * name of the IDBIndex to be iterated on while fetching entries. 1.996 + * null or "name" if no index is to be iterated. 1.997 + * @param {number} offset 1.998 + * ofsset of the entries to be fetched. 1.999 + * @param {number} size 1.1000 + * The intended size of the entries to be fetched. 1.1001 + */ 1.1002 + getObjectStoreData: 1.1003 + function(host, dbName, objectStore, id, index, offset, size) { 1.1004 + let request = this.openWithOrigin(host, dbName); 1.1005 + let success = Promise.defer(); 1.1006 + let data = []; 1.1007 + if (!size || size > MAX_STORE_OBJECT_COUNT) { 1.1008 + size = MAX_STORE_OBJECT_COUNT; 1.1009 + } 1.1010 + 1.1011 + request.onsuccess = event => { 1.1012 + let db = event.target.result; 1.1013 + 1.1014 + let transaction = db.transaction(objectStore, "readonly"); 1.1015 + let source = transaction.objectStore(objectStore); 1.1016 + if (index && index != "name") { 1.1017 + source = source.index(index); 1.1018 + } 1.1019 + 1.1020 + source.count().onsuccess = event => { 1.1021 + let count = event.target.result; 1.1022 + this.objectsSize[host + dbName + objectStore + index] = count; 1.1023 + 1.1024 + if (!offset) { 1.1025 + offset = 0; 1.1026 + } 1.1027 + else if (offset > count) { 1.1028 + db.close(); 1.1029 + success.resolve([]); 1.1030 + return; 1.1031 + } 1.1032 + 1.1033 + if (id) { 1.1034 + source.get(id).onsuccess = event => { 1.1035 + db.close(); 1.1036 + success.resolve([{name: id, value: event.target.result}]); 1.1037 + }; 1.1038 + } 1.1039 + else { 1.1040 + source.openCursor().onsuccess = event => { 1.1041 + let cursor = event.target.result; 1.1042 + 1.1043 + if (!cursor || data.length >= size) { 1.1044 + db.close(); 1.1045 + success.resolve(data); 1.1046 + return; 1.1047 + } 1.1048 + if (offset-- <= 0) { 1.1049 + data.push({name: cursor.key, value: cursor.value}); 1.1050 + } 1.1051 + cursor.continue(); 1.1052 + }; 1.1053 + } 1.1054 + }; 1.1055 + }; 1.1056 + request.onerror = () => { 1.1057 + db.close(); 1.1058 + success.resolve([]); 1.1059 + }; 1.1060 + return success.promise; 1.1061 + }, 1.1062 + 1.1063 + /** 1.1064 + * Returns the total number of entries for various types of requests to 1.1065 + * getStoreObjects for Indexed DB actor. 1.1066 + * 1.1067 + * @param {string} host 1.1068 + * The host for the request. 1.1069 + * @param {array:string} names 1.1070 + * Array of stringified name objects for indexed db actor. 1.1071 + * The request type depends on the length of any parsed entry from this 1.1072 + * array. 0 length refers to request for the whole host. 1 length 1.1073 + * refers to request for a particular db in the host. 2 length refers 1.1074 + * to a particular object store in a db in a host. 3 length refers to 1.1075 + * particular items of an object store in a db in a host. 1.1076 + * @param {object} options 1.1077 + * An options object containing following properties: 1.1078 + * - index {string} The IDBIndex for the object store in the db. 1.1079 + */ 1.1080 + getObjectsSize: function(host, names, options) { 1.1081 + // In Indexed DB, we are interested in only the first name, as the pattern 1.1082 + // should follow in all entries. 1.1083 + let name = names[0]; 1.1084 + let parsedName = JSON.parse(name); 1.1085 + 1.1086 + if (parsedName.length == 3) { 1.1087 + // This is the case where specific entries from an object store were 1.1088 + // requested 1.1089 + return names.length; 1.1090 + } 1.1091 + else if (parsedName.length == 2) { 1.1092 + // This is the case where all entries from an object store are requested. 1.1093 + let index = options.index; 1.1094 + let [db, objectStore] = parsedName; 1.1095 + if (this.objectsSize[host + db + objectStore + index]) { 1.1096 + return this.objectsSize[host + db + objectStore + index]; 1.1097 + } 1.1098 + } 1.1099 + else if (parsedName.length == 1) { 1.1100 + // This is the case where details of all object stores in a db are 1.1101 + // requested. 1.1102 + if (this.hostVsStores.has(host) && 1.1103 + this.hostVsStores.get(host).has(parsedName[0])) { 1.1104 + return this.hostVsStores.get(host).get(parsedName[0]).objectStores.size; 1.1105 + } 1.1106 + } 1.1107 + else if (!parsedName || !parsedName.length) { 1.1108 + // This is the case were details of all dbs in a host are requested. 1.1109 + if (this.hostVsStores.has(host)) { 1.1110 + return this.hostVsStores.get(host).size; 1.1111 + } 1.1112 + } 1.1113 + return 0; 1.1114 + }, 1.1115 + 1.1116 + getValuesForHost: async(function*(host, name = "null", options) { 1.1117 + name = JSON.parse(name); 1.1118 + if (!name || !name.length) { 1.1119 + // This means that details about the db in this particular host are 1.1120 + // requested. 1.1121 + let dbs = []; 1.1122 + if (this.hostVsStores.has(host)) { 1.1123 + for (let [dbName, db] of this.hostVsStores.get(host)) { 1.1124 + dbs.push(db.toObject()); 1.1125 + } 1.1126 + } 1.1127 + return dbs; 1.1128 + } 1.1129 + let [db, objectStore, id] = name; 1.1130 + if (!objectStore) { 1.1131 + // This means that details about all the object stores in this db are 1.1132 + // requested. 1.1133 + let objectStores = []; 1.1134 + if (this.hostVsStores.has(host) && this.hostVsStores.get(host).has(db)) { 1.1135 + for (let objectStore of this.hostVsStores.get(host).get(db).objectStores) { 1.1136 + objectStores.push(objectStore[1].toObject()); 1.1137 + } 1.1138 + } 1.1139 + return objectStores; 1.1140 + } 1.1141 + // Get either all entries from the object store, or a particular id 1.1142 + return yield this.getObjectStoreData(host, db, objectStore, id, 1.1143 + options.index, options.size); 1.1144 + }), 1.1145 + 1.1146 + /** 1.1147 + * Purpose of this method is same as populateStoresForHosts but this is async. 1.1148 + * This exact same operation cannot be performed in populateStoresForHosts 1.1149 + * method, as that method is called in initialize method of the actor, which 1.1150 + * cannot be asynchronous. 1.1151 + */ 1.1152 + preListStores: async(function*() { 1.1153 + this.hostVsStores = new Map(); 1.1154 + for (let host of this.hosts) { 1.1155 + yield this.populateStoresForHost(host); 1.1156 + } 1.1157 + }), 1.1158 + 1.1159 + populateStoresForHost: async(function*(host) { 1.1160 + let storeMap = new Map(); 1.1161 + for (let name of (yield this.getDBNamesForHost(host))) { 1.1162 + storeMap.set(name, yield this.getDBMetaData(host, name)); 1.1163 + } 1.1164 + this.hostVsStores.set(host, storeMap); 1.1165 + }), 1.1166 + 1.1167 + /** 1.1168 + * Removes any illegal characters from the host name to make it a valid file 1.1169 + * name. 1.1170 + */ 1.1171 + getSanitizedHost: function(host) { 1.1172 + return host.replace(ILLEGAL_CHAR_REGEX, "+"); 1.1173 + }, 1.1174 + 1.1175 + /** 1.1176 + * Opens an indexed db connection for the given `host` and database `name`. 1.1177 + */ 1.1178 + openWithOrigin: function(host, name) { 1.1179 + let principal; 1.1180 + 1.1181 + if (/^(about:|chrome:)/.test(host)) { 1.1182 + principal = Services.scriptSecurityManager.getSystemPrincipal(); 1.1183 + } 1.1184 + else { 1.1185 + let uri = Services.io.newURI(host, null, null); 1.1186 + principal = Services.scriptSecurityManager.getCodebasePrincipal(uri); 1.1187 + } 1.1188 + 1.1189 + return indexedDB.openForPrincipal(principal, name); 1.1190 + }, 1.1191 + 1.1192 + /** 1.1193 + * Fetches and stores all the metadata information for the given database 1.1194 + * `name` for the given `host`. The stored metadata information is of 1.1195 + * `DatabaseMetadata` type. 1.1196 + */ 1.1197 + getDBMetaData: function(host, name) { 1.1198 + let request = this.openWithOrigin(host, name); 1.1199 + let success = Promise.defer(); 1.1200 + request.onsuccess = event => { 1.1201 + let db = event.target.result; 1.1202 + 1.1203 + let dbData = new DatabaseMetadata(host, db); 1.1204 + db.close(); 1.1205 + success.resolve(dbData); 1.1206 + }; 1.1207 + request.onerror = event => { 1.1208 + console.error("Error opening indexeddb database " + name + " for host " + 1.1209 + host); 1.1210 + success.resolve(null); 1.1211 + }; 1.1212 + return success.promise; 1.1213 + }, 1.1214 + 1.1215 + /** 1.1216 + * Retrives the proper indexed db database name from the provided .sqlite file 1.1217 + * location. 1.1218 + */ 1.1219 + getNameFromDatabaseFile: async(function*(path) { 1.1220 + let connection = null; 1.1221 + let retryCount = 0; 1.1222 + 1.1223 + // Content pages might be having an open transaction for the same indexed db 1.1224 + // which this sqlite file belongs to. In that case, sqlite.openConnection 1.1225 + // will throw. Thus we retey for some time to see if lock is removed. 1.1226 + while (!connection && retryCount++ < 25) { 1.1227 + try { 1.1228 + connection = yield Sqlite.openConnection({ path: path }); 1.1229 + } 1.1230 + catch (ex) { 1.1231 + // Continuously retrying is overkill. Waiting for 100ms before next try 1.1232 + yield sleep(100); 1.1233 + } 1.1234 + } 1.1235 + 1.1236 + if (!connection) { 1.1237 + return null; 1.1238 + } 1.1239 + 1.1240 + let rows = yield connection.execute("SELECT name FROM database"); 1.1241 + if (rows.length != 1) { 1.1242 + return null; 1.1243 + } 1.1244 + 1.1245 + let name = rows[0].getResultByName("name"); 1.1246 + 1.1247 + yield connection.close(); 1.1248 + 1.1249 + return name; 1.1250 + }), 1.1251 + 1.1252 + /** 1.1253 + * Fetches all the databases and their metadata for the given `host`. 1.1254 + */ 1.1255 + getDBNamesForHost: async(function*(host) { 1.1256 + let sanitizedHost = this.getSanitizedHost(host); 1.1257 + let directory = OS.Path.join(OS.Constants.Path.profileDir, "storage", 1.1258 + "persistent", sanitizedHost, "idb"); 1.1259 + 1.1260 + let exists = yield OS.File.exists(directory); 1.1261 + if (!exists && host.startsWith("about:")) { 1.1262 + // try for moz-safe-about directory 1.1263 + sanitizedHost = this.getSanitizedHost("moz-safe-" + host); 1.1264 + directory = OS.Path.join(OS.Constants.Path.profileDir, "storage", 1.1265 + "persistent", sanitizedHost, "idb"); 1.1266 + exists = yield OS.File.exists(directory); 1.1267 + } 1.1268 + if (!exists) { 1.1269 + return []; 1.1270 + } 1.1271 + 1.1272 + let names = []; 1.1273 + let dirIterator = new OS.File.DirectoryIterator(directory); 1.1274 + try { 1.1275 + yield dirIterator.forEach(file => { 1.1276 + // Skip directories. 1.1277 + if (file.isDir) { 1.1278 + return null; 1.1279 + } 1.1280 + 1.1281 + // Skip any non-sqlite files. 1.1282 + if (!file.name.endsWith(".sqlite")) { 1.1283 + return null; 1.1284 + } 1.1285 + 1.1286 + return this.getNameFromDatabaseFile(file.path).then(name => { 1.1287 + if (name) { 1.1288 + names.push(name); 1.1289 + } 1.1290 + return null; 1.1291 + }); 1.1292 + }); 1.1293 + } 1.1294 + finally { 1.1295 + dirIterator.close(); 1.1296 + } 1.1297 + return names; 1.1298 + }), 1.1299 + 1.1300 + /** 1.1301 + * Returns the over-the-wire implementation of the indexed db entity. 1.1302 + */ 1.1303 + toStoreObject: function(item) { 1.1304 + if (!item) { 1.1305 + return null; 1.1306 + } 1.1307 + 1.1308 + if (item.indexes) { 1.1309 + // Object store meta data 1.1310 + return { 1.1311 + objectStore: item.name, 1.1312 + keyPath: item.keyPath, 1.1313 + autoIncrement: item.autoIncrement, 1.1314 + indexes: item.indexes 1.1315 + }; 1.1316 + } 1.1317 + if (item.objectStores) { 1.1318 + // DB meta data 1.1319 + return { 1.1320 + db: item.name, 1.1321 + origin: item.origin, 1.1322 + version: item.version, 1.1323 + objectStores: item.objectStores 1.1324 + }; 1.1325 + } 1.1326 + // Indexed db entry 1.1327 + return { 1.1328 + name: item.name, 1.1329 + value: new LongStringActor(this.conn, JSON.stringify(item.value)) 1.1330 + }; 1.1331 + }, 1.1332 + 1.1333 + form: function(form, detail) { 1.1334 + if (detail === "actorid") { 1.1335 + return this.actorID; 1.1336 + } 1.1337 + 1.1338 + let hosts = {}; 1.1339 + for (let host of this.hosts) { 1.1340 + hosts[host] = this.getNamesForHost(host); 1.1341 + } 1.1342 + 1.1343 + return { 1.1344 + actor: this.actorID, 1.1345 + hosts: hosts 1.1346 + }; 1.1347 + }, 1.1348 +}); 1.1349 + 1.1350 +/** 1.1351 + * The main Storage Actor. 1.1352 + */ 1.1353 +let StorageActor = exports.StorageActor = protocol.ActorClass({ 1.1354 + typeName: "storage", 1.1355 + 1.1356 + get window() { 1.1357 + return this.parentActor.window; 1.1358 + }, 1.1359 + 1.1360 + get document() { 1.1361 + return this.parentActor.window.document; 1.1362 + }, 1.1363 + 1.1364 + get windows() { 1.1365 + return this.childWindowPool; 1.1366 + }, 1.1367 + 1.1368 + /** 1.1369 + * List of event notifications that the server can send to the client. 1.1370 + * 1.1371 + * - stores-update : When any store object in any storage type changes. 1.1372 + * - stores-cleared : When all the store objects are removed. 1.1373 + * - stores-reloaded : When all stores are reloaded. This generally mean that 1.1374 + * we should refetch everything again. 1.1375 + */ 1.1376 + events: { 1.1377 + "stores-update": { 1.1378 + type: "storesUpdate", 1.1379 + data: Arg(0, "storeUpdateObject") 1.1380 + }, 1.1381 + "stores-cleared": { 1.1382 + type: "storesCleared", 1.1383 + data: Arg(0, "json") 1.1384 + }, 1.1385 + "stores-reloaded": { 1.1386 + type: "storesRelaoded", 1.1387 + data: Arg(0, "json") 1.1388 + } 1.1389 + }, 1.1390 + 1.1391 + initialize: function (conn, tabActor) { 1.1392 + protocol.Actor.prototype.initialize.call(this, null); 1.1393 + 1.1394 + this.conn = conn; 1.1395 + this.parentActor = tabActor; 1.1396 + 1.1397 + this.childActorPool = new Map(); 1.1398 + this.childWindowPool = new Set(); 1.1399 + 1.1400 + // Fetch all the inner iframe windows in this tab. 1.1401 + this.fetchChildWindows(this.parentActor.docShell); 1.1402 + 1.1403 + // Initialize the registered store types 1.1404 + for (let [store, actor] of storageTypePool) { 1.1405 + this.childActorPool.set(store, new actor(this)); 1.1406 + } 1.1407 + 1.1408 + // Notifications that help us keep track of newly added windows and windows 1.1409 + // that got removed 1.1410 + Services.obs.addObserver(this, "content-document-global-created", false); 1.1411 + Services.obs.addObserver(this, "inner-window-destroyed", false); 1.1412 + this.onPageChange = this.onPageChange.bind(this); 1.1413 + tabActor.browser.addEventListener("pageshow", this.onPageChange, true); 1.1414 + tabActor.browser.addEventListener("pagehide", this.onPageChange, true); 1.1415 + 1.1416 + this.destroyed = false; 1.1417 + this.boundUpdate = {}; 1.1418 + // The time which periodically flushes and transfers the updated store 1.1419 + // objects. 1.1420 + this.updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 1.1421 + this.updateTimer.initWithCallback(this , UPDATE_INTERVAL, 1.1422 + Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP); 1.1423 + 1.1424 + // Layout helper for window.parent and window.top helper methods that work 1.1425 + // accross devices. 1.1426 + this.layoutHelper = new LayoutHelpers(this.window); 1.1427 + }, 1.1428 + 1.1429 + destroy: function() { 1.1430 + this.updateTimer.cancel(); 1.1431 + this.updateTimer = null; 1.1432 + this.layoutHelper = null; 1.1433 + // Remove observers 1.1434 + Services.obs.removeObserver(this, "content-document-global-created", false); 1.1435 + Services.obs.removeObserver(this, "inner-window-destroyed", false); 1.1436 + this.destroyed = true; 1.1437 + if (this.parentActor.browser) { 1.1438 + this.parentActor.browser.removeEventListener( 1.1439 + "pageshow", this.onPageChange, true); 1.1440 + this.parentActor.browser.removeEventListener( 1.1441 + "pagehide", this.onPageChange, true); 1.1442 + } 1.1443 + // Destroy the registered store types 1.1444 + for (let actor of this.childActorPool.values()) { 1.1445 + actor.destroy(); 1.1446 + } 1.1447 + this.childActorPool.clear(); 1.1448 + this.childWindowPool.clear(); 1.1449 + this.childWindowPool = this.childActorPool = null; 1.1450 + }, 1.1451 + 1.1452 + /** 1.1453 + * Given a docshell, recursively find otu all the child windows from it. 1.1454 + * 1.1455 + * @param {nsIDocShell} item 1.1456 + * The docshell from which all inner windows need to be extracted. 1.1457 + */ 1.1458 + fetchChildWindows: function(item) { 1.1459 + let docShell = item.QueryInterface(Ci.nsIDocShell) 1.1460 + .QueryInterface(Ci.nsIDocShellTreeItem); 1.1461 + if (!docShell.contentViewer) { 1.1462 + return null; 1.1463 + } 1.1464 + let window = docShell.contentViewer.DOMDocument.defaultView; 1.1465 + if (window.location.href == "about:blank") { 1.1466 + // Skip out about:blank windows as Gecko creates them multiple times while 1.1467 + // creating any global. 1.1468 + return null; 1.1469 + } 1.1470 + this.childWindowPool.add(window); 1.1471 + for (let i = 0; i < docShell.childCount; i++) { 1.1472 + let child = docShell.getChildAt(i); 1.1473 + this.fetchChildWindows(child); 1.1474 + } 1.1475 + return null; 1.1476 + }, 1.1477 + 1.1478 + isIncludedInTopLevelWindow: function(window) { 1.1479 + return this.layoutHelper.isIncludedInTopLevelWindow(window); 1.1480 + }, 1.1481 + 1.1482 + getWindowFromInnerWindowID: function(innerID) { 1.1483 + innerID = innerID.QueryInterface(Ci.nsISupportsPRUint64).data; 1.1484 + for (let win of this.childWindowPool.values()) { 1.1485 + let id = win.QueryInterface(Ci.nsIInterfaceRequestor) 1.1486 + .getInterface(Ci.nsIDOMWindowUtils) 1.1487 + .currentInnerWindowID; 1.1488 + if (id == innerID) { 1.1489 + return win; 1.1490 + } 1.1491 + } 1.1492 + return null; 1.1493 + }, 1.1494 + 1.1495 + /** 1.1496 + * Event handler for any docshell update. This lets us figure out whenever 1.1497 + * any new window is added, or an existing window is removed. 1.1498 + */ 1.1499 + observe: function(subject, topic, data) { 1.1500 + if (subject.location && 1.1501 + (!subject.location.href || subject.location.href == "about:blank")) { 1.1502 + return null; 1.1503 + } 1.1504 + if (topic == "content-document-global-created" && 1.1505 + this.isIncludedInTopLevelWindow(subject)) { 1.1506 + this.childWindowPool.add(subject); 1.1507 + events.emit(this, "window-ready", subject); 1.1508 + } 1.1509 + else if (topic == "inner-window-destroyed") { 1.1510 + let window = this.getWindowFromInnerWindowID(subject); 1.1511 + if (window) { 1.1512 + this.childWindowPool.delete(window); 1.1513 + events.emit(this, "window-destroyed", window); 1.1514 + } 1.1515 + } 1.1516 + return null; 1.1517 + }, 1.1518 + 1.1519 + /** 1.1520 + * Called on "pageshow" or "pagehide" event on the chromeEventHandler of 1.1521 + * current tab. 1.1522 + * 1.1523 + * @param {event} The event object passed to the handler. We are using these 1.1524 + * three properties from the event: 1.1525 + * - target {document} The document corresponding to the event. 1.1526 + * - type {string} Name of the event - "pageshow" or "pagehide". 1.1527 + * - persisted {boolean} true if there was no 1.1528 + * "content-document-global-created" notification along 1.1529 + * this event. 1.1530 + */ 1.1531 + onPageChange: function({target, type, persisted}) { 1.1532 + if (this.destroyed) { 1.1533 + return; 1.1534 + } 1.1535 + let window = target.defaultView; 1.1536 + if (type == "pagehide" && this.childWindowPool.delete(window)) { 1.1537 + events.emit(this, "window-destroyed", window) 1.1538 + } 1.1539 + else if (type == "pageshow" && persisted && window.location.href && 1.1540 + window.location.href != "about:blank" && 1.1541 + this.isIncludedInTopLevelWindow(window)) { 1.1542 + this.childWindowPool.add(window); 1.1543 + events.emit(this, "window-ready", window); 1.1544 + } 1.1545 + }, 1.1546 + 1.1547 + /** 1.1548 + * Lists the available hosts for all the registered storage types. 1.1549 + * 1.1550 + * @returns {object} An object containing with the following structure: 1.1551 + * - <storageType> : [{ 1.1552 + * actor: <actorId>, 1.1553 + * host: <hostname> 1.1554 + * }] 1.1555 + */ 1.1556 + listStores: method(async(function*() { 1.1557 + let toReturn = {}; 1.1558 + for (let [name, value] of this.childActorPool) { 1.1559 + if (value.preListStores) { 1.1560 + yield value.preListStores(); 1.1561 + } 1.1562 + toReturn[name] = value; 1.1563 + } 1.1564 + return toReturn; 1.1565 + }), { 1.1566 + response: RetVal(types.addDictType("storelist", getRegisteredTypes())) 1.1567 + }), 1.1568 + 1.1569 + /** 1.1570 + * Notifies the client front with the updates in stores at regular intervals. 1.1571 + */ 1.1572 + notify: function() { 1.1573 + if (!this.updatePending || this.updatingUpdateObject) { 1.1574 + return null; 1.1575 + } 1.1576 + events.emit(this, "stores-update", this.boundUpdate); 1.1577 + this.boundUpdate = {}; 1.1578 + this.updatePending = false; 1.1579 + return null; 1.1580 + }, 1.1581 + 1.1582 + /** 1.1583 + * This method is called by the registered storage types so as to tell the 1.1584 + * Storage Actor that there are some changes in the stores. Storage Actor then 1.1585 + * notifies the client front about these changes at regular (UPDATE_INTERVAL) 1.1586 + * interval. 1.1587 + * 1.1588 + * @param {string} action 1.1589 + * The type of change. One of "added", "changed" or "deleted" 1.1590 + * @param {string} storeType 1.1591 + * The storage actor in which this change has occurred. 1.1592 + * @param {object} data 1.1593 + * The update object. This object is of the following format: 1.1594 + * - { 1.1595 + * <host1>: [<store_names1>, <store_name2>...], 1.1596 + * <host2>: [<store_names34>...], 1.1597 + * } 1.1598 + * Where host1, host2 are the host in which this change happened and 1.1599 + * [<store_namesX] is an array of the names of the changed store 1.1600 + * objects. Leave it empty if the host was completely removed. 1.1601 + * When the action is "reloaded" or "cleared", `data` is an array of 1.1602 + * hosts for which the stores were cleared or reloaded. 1.1603 + */ 1.1604 + update: function(action, storeType, data) { 1.1605 + if (action == "cleared" || action == "reloaded") { 1.1606 + let toSend = {}; 1.1607 + toSend[storeType] = data 1.1608 + events.emit(this, "stores-" + action, toSend); 1.1609 + return null; 1.1610 + } 1.1611 + 1.1612 + this.updatingUpdateObject = true; 1.1613 + if (!this.boundUpdate[action]) { 1.1614 + this.boundUpdate[action] = {}; 1.1615 + } 1.1616 + if (!this.boundUpdate[action][storeType]) { 1.1617 + this.boundUpdate[action][storeType] = {}; 1.1618 + } 1.1619 + this.updatePending = true; 1.1620 + for (let host in data) { 1.1621 + if (!this.boundUpdate[action][storeType][host] || action == "deleted") { 1.1622 + this.boundUpdate[action][storeType][host] = data[host]; 1.1623 + } 1.1624 + else { 1.1625 + this.boundUpdate[action][storeType][host] = 1.1626 + this.boundUpdate[action][storeType][host].concat(data[host]); 1.1627 + } 1.1628 + } 1.1629 + if (action == "added") { 1.1630 + // If the same store name was previously deleted or changed, but now is 1.1631 + // added somehow, dont send the deleted or changed update. 1.1632 + this.removeNamesFromUpdateList("deleted", storeType, data); 1.1633 + this.removeNamesFromUpdateList("changed", storeType, data); 1.1634 + } 1.1635 + else if (action == "changed" && this.boundUpdate.added && 1.1636 + this.boundUpdate.added[storeType]) { 1.1637 + // If something got added and changed at the same time, then remove those 1.1638 + // items from changed instead. 1.1639 + this.removeNamesFromUpdateList("changed", storeType, 1.1640 + this.boundUpdate.added[storeType]); 1.1641 + } 1.1642 + else if (action == "deleted") { 1.1643 + // If any item got delete, or a host got delete, no point in sending 1.1644 + // added or changed update 1.1645 + this.removeNamesFromUpdateList("added", storeType, data); 1.1646 + this.removeNamesFromUpdateList("changed", storeType, data); 1.1647 + for (let host in data) { 1.1648 + if (data[host].length == 0 && this.boundUpdate.added && 1.1649 + this.boundUpdate.added[storeType] && 1.1650 + this.boundUpdate.added[storeType][host]) { 1.1651 + delete this.boundUpdate.added[storeType][host]; 1.1652 + } 1.1653 + if (data[host].length == 0 && this.boundUpdate.changed && 1.1654 + this.boundUpdate.changed[storeType] && 1.1655 + this.boundUpdate.changed[storeType][host]) { 1.1656 + delete this.boundUpdate.changed[storeType][host]; 1.1657 + } 1.1658 + } 1.1659 + } 1.1660 + this.updatingUpdateObject = false; 1.1661 + return null; 1.1662 + }, 1.1663 + 1.1664 + /** 1.1665 + * This method removes data from the this.boundUpdate object in the same 1.1666 + * manner like this.update() adds data to it. 1.1667 + * 1.1668 + * @param {string} action 1.1669 + * The type of change. One of "added", "changed" or "deleted" 1.1670 + * @param {string} storeType 1.1671 + * The storage actor for which you want to remove the updates data. 1.1672 + * @param {object} data 1.1673 + * The update object. This object is of the following format: 1.1674 + * - { 1.1675 + * <host1>: [<store_names1>, <store_name2>...], 1.1676 + * <host2>: [<store_names34>...], 1.1677 + * } 1.1678 + * Where host1, host2 are the hosts which you want to remove and 1.1679 + * [<store_namesX] is an array of the names of the store objects. 1.1680 + */ 1.1681 + removeNamesFromUpdateList: function(action, storeType, data) { 1.1682 + for (let host in data) { 1.1683 + if (this.boundUpdate[action] && this.boundUpdate[action][storeType] && 1.1684 + this.boundUpdate[action][storeType][host]) { 1.1685 + for (let name in data[host]) { 1.1686 + let index = this.boundUpdate[action][storeType][host].indexOf(name); 1.1687 + if (index > -1) { 1.1688 + this.boundUpdate[action][storeType][host].splice(index, 1); 1.1689 + } 1.1690 + } 1.1691 + if (!this.boundUpdate[action][storeType][host].length) { 1.1692 + delete this.boundUpdate[action][storeType][host]; 1.1693 + } 1.1694 + } 1.1695 + } 1.1696 + return null; 1.1697 + } 1.1698 +}); 1.1699 + 1.1700 +/** 1.1701 + * Front for the Storage Actor. 1.1702 + */ 1.1703 +let StorageFront = exports.StorageFront = protocol.FrontClass(StorageActor, { 1.1704 + initialize: function(client, tabForm) { 1.1705 + protocol.Front.prototype.initialize.call(this, client); 1.1706 + this.actorID = tabForm.storageActor; 1.1707 + 1.1708 + client.addActorPool(this); 1.1709 + this.manage(this); 1.1710 + } 1.1711 +});