index.js (13341B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { Actor } = require("resource://devtools/shared/protocol.js"); 8 const specs = require("resource://devtools/shared/specs/storage.js"); 9 10 loader.lazyRequireGetter( 11 this, 12 "naturalSortCaseInsensitive", 13 "resource://devtools/shared/natural-sort.js", 14 true 15 ); 16 17 // Maximum number of cookies/local storage key-value-pairs that can be sent 18 // over the wire to the client in one request. 19 const MAX_STORE_OBJECT_COUNT = 50; 20 exports.MAX_STORE_OBJECT_COUNT = MAX_STORE_OBJECT_COUNT; 21 22 const DEFAULT_VALUE = "value"; 23 exports.DEFAULT_VALUE = DEFAULT_VALUE; 24 25 // GUID to be used as a separator in compound keys. This must match the same 26 // constant in devtools/client/storage/ui.js, 27 // devtools/client/storage/test/head.js and 28 // devtools/server/tests/browser/head.js 29 const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}"; 30 exports.SEPARATOR_GUID = SEPARATOR_GUID; 31 32 class BaseStorageActor extends Actor { 33 /** 34 * Base class with the common methods required by all storage actors. 35 * 36 * This base class is missing a couple of required methods that should be 37 * implemented seperately for each actor. They are namely: 38 * - observe : Method which gets triggered on the notification of the watched 39 * topic. 40 * - getNamesForHost : Given a host, get list of all known store names. 41 * - getValuesForHost : Given a host (and optionally a name) get all known 42 * store objects. 43 * - toStoreObject : Given a store object, convert it to the required format 44 * so that it can be transferred over wire. 45 * - populateStoresForHost : Given a host, populate the map of all store 46 * objects for it 47 * - getFields: Given a subType(optional), get an array of objects containing 48 * column field info. The info includes, 49 * "name" is name of colume key. 50 * "editable" is 1 means editable field; 0 means uneditable. 51 * 52 * @param {string} typeName 53 * The typeName of the actor. 54 */ 55 constructor(storageActor, typeName) { 56 super(storageActor.conn, specs.childSpecs[typeName]); 57 58 this.storageActor = storageActor; 59 60 // Map keyed by host name whose values are nested Maps. 61 // Nested maps are keyed by store names and values are store values. 62 // Store values are specific to each sub class. 63 // Map(host name => stores <Map(name => values )>) 64 // Map(string => stores <Map(string => any )>) 65 this.hostVsStores = new Map(); 66 67 this.onWindowReady = this.onWindowReady.bind(this); 68 this.onWindowDestroyed = this.onWindowDestroyed.bind(this); 69 this.storageActor.on("window-ready", this.onWindowReady); 70 this.storageActor.on("window-destroyed", this.onWindowDestroyed); 71 } 72 73 destroy() { 74 if (!this.storageActor) { 75 return; 76 } 77 78 this.storageActor.off("window-ready", this.onWindowReady); 79 this.storageActor.off("window-destroyed", this.onWindowDestroyed); 80 81 this.hostVsStores.clear(); 82 83 super.destroy(); 84 85 this.storageActor = null; 86 } 87 88 /** 89 * Returns a list of currently known hosts for the target window. This list 90 * contains unique hosts from the window + all inner windows. If 91 * this._internalHosts is defined then these will also be added to the list. 92 */ 93 get hosts() { 94 const hosts = new Set(); 95 for (const { location } of this.storageActor.windows) { 96 const host = this.getHostName(location); 97 98 if (host) { 99 hosts.add(host); 100 } 101 } 102 if (this._internalHosts) { 103 for (const host of this._internalHosts) { 104 hosts.add(host); 105 } 106 } 107 return hosts; 108 } 109 110 /** 111 * Returns all the windows present on the page. Includes main window + inner 112 * iframe windows. 113 */ 114 get windows() { 115 return this.storageActor.windows; 116 } 117 118 /** 119 * Converts the window.location object into a URL (e.g. http://domain.com). 120 */ 121 getHostName(location) { 122 if (!location) { 123 // Debugging a legacy Firefox extension... no hostname available and no 124 // storage possible. 125 return null; 126 } 127 128 if (this.storageActor.getHostName) { 129 return this.storageActor.getHostName(location); 130 } 131 132 switch (location.protocol) { 133 case "about:": 134 return `${location.protocol}${location.pathname}`; 135 case "chrome:": 136 // chrome: URLs do not support storage of any type. 137 return null; 138 case "data:": 139 // data: URLs do not support storage of any type. 140 return null; 141 case "file:": 142 return `${location.protocol}//${location.pathname}`; 143 case "javascript:": 144 return location.href; 145 case "moz-extension:": 146 return location.origin; 147 case "resource:": 148 return `${location.origin}${location.pathname}`; 149 default: 150 // http: or unknown protocol. 151 return `${location.protocol}//${location.host}`; 152 } 153 } 154 155 /** 156 * Populates a map of known hosts vs a map of stores vs value. 157 */ 158 async populateStoresForHosts() { 159 for (const host of this.hosts) { 160 await this.populateStoresForHost(host); 161 } 162 } 163 164 getNamesForHost(host) { 165 return [...this.hostVsStores.get(host).keys()]; 166 } 167 168 getValuesForHost(host, name) { 169 if (name) { 170 return [this.hostVsStores.get(host).get(name)]; 171 } 172 return [...this.hostVsStores.get(host).values()]; 173 } 174 175 getObjectsSize(host, names) { 176 return names.length; 177 } 178 179 /** 180 * When a new window is added to the page. This generally means that a new 181 * iframe is created, or the current window is completely reloaded. 182 * 183 * @param {window} window 184 * The window which was added. 185 */ 186 async onWindowReady(window) { 187 if (!this.hostVsStores) { 188 return; 189 } 190 const host = this.getHostName(window.location); 191 if (host && !this.hostVsStores.has(host)) { 192 await this.populateStoresForHost(host, window); 193 if (!this.storageActor) { 194 // The actor might be destroyed during populateStoresForHost. 195 return; 196 } 197 198 const data = {}; 199 data[host] = this.getNamesForHost(host); 200 this.storageActor.update("added", this.typeName, data); 201 } 202 } 203 204 /** 205 * When a window is removed from the page. This generally means that an 206 * iframe was removed, or the current window reload is triggered. 207 * 208 * @param {window} window 209 * The window which was removed. 210 * @param {object} options 211 * @param {boolean} options.dontCheckHost 212 * If set to true, the function won't check if the host still is in this.hosts. 213 * This is helpful in the case of the StorageActorMock, as the `hosts` getter 214 * uses its `windows` getter, and at this point in time the window which is 215 * going to be destroyed still exists. 216 */ 217 onWindowDestroyed(window, { dontCheckHost } = {}) { 218 if (!this.hostVsStores) { 219 return; 220 } 221 if (!window.location) { 222 // Nothing can be done if location object is null 223 return; 224 } 225 const host = this.getHostName(window.location); 226 if (host && (!this.hosts.has(host) || dontCheckHost)) { 227 this.hostVsStores.delete(host); 228 const data = {}; 229 data[host] = []; 230 this.storageActor.update("deleted", this.typeName, data); 231 } 232 } 233 234 form() { 235 const hosts = {}; 236 for (const host of this.hosts) { 237 hosts[host] = []; 238 } 239 240 return { 241 actor: this.actorID, 242 hosts, 243 traits: this._getTraits(), 244 }; 245 } 246 247 // Share getTraits for child classes overriding form() 248 _getTraits() { 249 return { 250 // The supportsXXX traits are not related to backward compatibility 251 // Different storage actor types implement different APIs, the traits 252 // help the client to know what is supported or not. 253 supportsAddItem: typeof this.addItem === "function", 254 // Note: supportsRemoveItem and supportsRemoveAll are always defined 255 // for all actors. See Bug 1655001. 256 supportsRemoveItem: typeof this.removeItem === "function", 257 supportsRemoveAll: typeof this.removeAll === "function", 258 supportsRemoveAllSessionCookies: 259 typeof this.removeAllSessionCookies === "function", 260 }; 261 } 262 263 /** 264 * Returns a list of requested store objects. Maximum values returned are 265 * MAX_STORE_OBJECT_COUNT. This method returns paginated values whose 266 * starting index and total size can be controlled via the options object 267 * 268 * @param {string} host 269 * The host name for which the store values are required. 270 * @param {array:string} names 271 * Array containing the names of required store objects. Empty if all 272 * items are required. 273 * @param {object} options 274 * Additional options for the request containing following 275 * properties: 276 * - offset {number} : The begin index of the returned array amongst 277 * the total values 278 * - size {number} : The number of values required. 279 * - sortOn {string} : The values should be sorted on this property. 280 * - index {string} : In case of indexed db, the IDBIndex to be used 281 * for fetching the values. 282 * - sessionString {string} : Client-side value of storage-expires-session 283 * l10n string. Since this function can be called from both 284 * the client and the server, and given that client and 285 * server might have different locales, we can't compute 286 * the localized string directly from here. 287 * @return {object} An object containing following properties: 288 * - offset - The actual offset of the returned array. This might 289 * be different from the requested offset if that was 290 * invalid 291 * - total - The total number of entries possible. 292 * - data - The requested values. 293 */ 294 async getStoreObjects(host, names, options = {}) { 295 const offset = options.offset || 0; 296 let size = options.size || MAX_STORE_OBJECT_COUNT; 297 if (size > MAX_STORE_OBJECT_COUNT) { 298 size = MAX_STORE_OBJECT_COUNT; 299 } 300 const sortOn = options.sortOn || "name"; 301 302 const toReturn = { 303 offset, 304 total: 0, 305 data: [], 306 }; 307 308 let principal = null; 309 if (this.typeName === "indexedDB") { 310 // We only acquire principal when the type of the storage is indexedDB 311 // because the principal only matters the indexedDB. 312 const win = this.storageActor.getWindowFromHost(host); 313 principal = this.getPrincipal(win); 314 } 315 316 if (names) { 317 for (const name of names) { 318 const values = await this.getValuesForHost( 319 host, 320 name, 321 options, 322 this.hostVsStores, 323 principal 324 ); 325 326 const { result, objectStores } = values; 327 328 if (result && typeof result.objectsSize !== "undefined") { 329 for (const { key, count } of result.objectsSize) { 330 this.objectsSize[key] = count; 331 } 332 } 333 334 if (result) { 335 toReturn.data.push(...result.data); 336 } else if (objectStores) { 337 toReturn.data.push(...objectStores); 338 } else { 339 toReturn.data.push(...values); 340 } 341 } 342 343 if (this.typeName === "Cache") { 344 // Cache storage contains several items per name but misses a custom 345 // `getObjectsSize` implementation, as implemented for IndexedDB. 346 // See Bug 1745242. 347 toReturn.total = toReturn.data.length; 348 } else { 349 toReturn.total = this.getObjectsSize(host, names, options); 350 } 351 } else { 352 let obj = await this.getValuesForHost( 353 host, 354 undefined, 355 undefined, 356 this.hostVsStores, 357 principal 358 ); 359 if (obj.dbs) { 360 obj = obj.dbs; 361 } 362 363 toReturn.total = obj.length; 364 toReturn.data = obj; 365 } 366 367 if (offset > toReturn.total) { 368 // In this case, toReturn.data is an empty array. 369 toReturn.offset = toReturn.total; 370 toReturn.data = []; 371 } else { 372 // We need to use natural sort before slicing. 373 const sorted = toReturn.data.sort((a, b) => { 374 return naturalSortCaseInsensitive( 375 a[sortOn], 376 b[sortOn], 377 options.sessionString 378 ); 379 }); 380 let sliced; 381 if (this.typeName === "indexedDB") { 382 // indexedDB's getValuesForHost never returns *all* values available but only 383 // a slice, starting at the expected offset. Therefore the result is already 384 // sliced as expected. 385 sliced = sorted; 386 } else { 387 sliced = sorted.slice(offset, offset + size); 388 } 389 toReturn.data = sliced.map(a => this.toStoreObject(a)); 390 } 391 392 return toReturn; 393 } 394 395 getPrincipal(win) { 396 if (win) { 397 return win.document.effectiveStoragePrincipal; 398 } 399 // We are running in the browser toolbox and viewing system DBs so we 400 // need to use system principal. 401 return Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal); 402 } 403 } 404 exports.BaseStorageActor = BaseStorageActor;