indexed-db.js (29251B)
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 { 8 BaseStorageActor, 9 MAX_STORE_OBJECT_COUNT, 10 SEPARATOR_GUID, 11 } = require("resource://devtools/server/actors/resources/storage/index.js"); 12 const { 13 LongStringActor, 14 } = require("resource://devtools/server/actors/string.js"); 15 // We give this a funny name to avoid confusion with the global 16 // indexedDB. 17 loader.lazyGetter(this, "indexedDBForStorage", () => { 18 // On xpcshell, we can't instantiate indexedDB without crashing 19 try { 20 const sandbox = Cu.Sandbox( 21 Components.Constructor( 22 "@mozilla.org/systemprincipal;1", 23 "nsIPrincipal" 24 )(), 25 { wantGlobalProperties: ["indexedDB"] } 26 ); 27 return sandbox.indexedDB; 28 } catch (e) { 29 return {}; 30 } 31 }); 32 const lazy = {}; 33 ChromeUtils.defineESModuleGetters( 34 lazy, 35 { 36 Sqlite: "resource://gre/modules/Sqlite.sys.mjs", 37 }, 38 { global: "contextual" } 39 ); 40 41 /** 42 * An async method equivalent to setTimeout but using Promises 43 * 44 * @param {number} time 45 * The wait time in milliseconds. 46 */ 47 function sleep(time) { 48 return new Promise(resolve => { 49 setTimeout(() => { 50 resolve(null); 51 }, time); 52 }); 53 } 54 55 const SAFE_HOSTS_PREFIXES_REGEX = /^(about\+|https?\+|file\+|moz-extension\+)/; 56 57 // A RegExp for characters that cannot appear in a file/directory name. This is 58 // used to sanitize the host name for indexed db to lookup whether the file is 59 // present in <profileDir>/storage/default/ location 60 const illegalFileNameCharacters = [ 61 "[", 62 // Control characters \001 to \036 63 "\\x00-\\x24", 64 // Special characters 65 '/:*?\\"<>|\\\\', 66 "]", 67 ].join(""); 68 const ILLEGAL_CHAR_REGEX = new RegExp(illegalFileNameCharacters, "g"); 69 70 /** 71 * Code related to the Indexed DB actor and front 72 */ 73 74 // Metadata holder objects for various components of Indexed DB 75 76 /** 77 * Meta data object for a particular index in an object store 78 */ 79 class IndexMetadata { 80 /** 81 * @param {IDBIndex} index 82 * The particular index from the object store. 83 */ 84 constructor(index) { 85 this._name = index.name; 86 this._keyPath = index.keyPath; 87 this._unique = index.unique; 88 this._multiEntry = index.multiEntry; 89 } 90 toObject() { 91 return { 92 name: this._name, 93 keyPath: this._keyPath, 94 unique: this._unique, 95 multiEntry: this._multiEntry, 96 }; 97 } 98 } 99 100 /** 101 * Meta data object for a particular object store in a db 102 */ 103 class ObjectStoreMetadata { 104 /** 105 * @param {IDBObjectStore} objectStore 106 * The particular object store from the db. 107 */ 108 constructor(objectStore) { 109 this._name = objectStore.name; 110 this._keyPath = objectStore.keyPath; 111 this._autoIncrement = objectStore.autoIncrement; 112 this._indexes = []; 113 114 for (let i = 0; i < objectStore.indexNames.length; i++) { 115 const index = objectStore.index(objectStore.indexNames[i]); 116 117 const newIndex = { 118 keypath: index.keyPath, 119 multiEntry: index.multiEntry, 120 name: index.name, 121 objectStore: { 122 autoIncrement: index.objectStore.autoIncrement, 123 indexNames: [...index.objectStore.indexNames], 124 keyPath: index.objectStore.keyPath, 125 name: index.objectStore.name, 126 }, 127 }; 128 129 this._indexes.push([newIndex, new IndexMetadata(index)]); 130 } 131 } 132 toObject() { 133 return { 134 name: this._name, 135 keyPath: this._keyPath, 136 autoIncrement: this._autoIncrement, 137 indexes: JSON.stringify( 138 [...this._indexes.values()].map(index => index.toObject()) 139 ), 140 }; 141 } 142 } 143 144 /** 145 * Meta data object for a particular indexed db in a host. 146 */ 147 class DatabaseMetadata { 148 /** 149 * @param {string} origin 150 * The host associated with this indexed db. 151 * @param {IDBDatabase} db 152 * The particular indexed db. 153 * @param {string} storage 154 * Storage type, either "temporary", "default" or "persistent". 155 */ 156 constructor(origin, db, storage) { 157 this._origin = origin; 158 this._name = db.name; 159 this._version = db.version; 160 this._objectStores = []; 161 this.storage = storage; 162 163 if (db.objectStoreNames.length) { 164 const transaction = db.transaction(db.objectStoreNames, "readonly"); 165 166 for (let i = 0; i < transaction.objectStoreNames.length; i++) { 167 const objectStore = transaction.objectStore( 168 transaction.objectStoreNames[i] 169 ); 170 this._objectStores.push([ 171 transaction.objectStoreNames[i], 172 new ObjectStoreMetadata(objectStore), 173 ]); 174 } 175 } 176 } 177 get objectStores() { 178 return this._objectStores; 179 } 180 181 toObject() { 182 return { 183 uniqueKey: `${this._name}${SEPARATOR_GUID}${this.storage}`, 184 name: this._name, 185 storage: this.storage, 186 origin: this._origin, 187 version: this._version, 188 objectStores: this._objectStores.size, 189 }; 190 } 191 } 192 193 class IndexedDBStorageActor extends BaseStorageActor { 194 constructor(storageActor) { 195 super(storageActor, "indexedDB"); 196 197 this.objectsSize = {}; 198 this.storageActor = storageActor; 199 } 200 201 destroy() { 202 this.objectsSize = null; 203 204 super.destroy(); 205 } 206 207 // We need to override this method because of custom, async getHosts method 208 async populateStoresForHosts() { 209 for (const host of await this.getHosts()) { 210 await this.populateStoresForHost(host); 211 } 212 } 213 214 async populateStoresForHost(host) { 215 const storeMap = new Map(); 216 217 const win = this.storageActor.getWindowFromHost(host); 218 const principal = this.getPrincipal(win); 219 220 const { names } = await this.getDBNamesForHost(host, principal); 221 222 for (const { name, storage } of names) { 223 let metadata = await this.getDBMetaData(host, principal, name, storage); 224 225 metadata = this.patchMetadataMapsAndProtos(metadata); 226 227 storeMap.set(`${name} (${storage})`, metadata); 228 } 229 230 this.hostVsStores.set(host, storeMap); 231 } 232 233 /** 234 * Returns a list of currently known hosts for the target window. This list 235 * contains unique hosts from the window, all inner windows and all permanent 236 * indexedDB hosts defined inside the browser. 237 */ 238 async getHosts() { 239 // Add internal hosts to this._internalHosts, which will be picked up by 240 // the this.hosts getter. Because this.hosts is a property on the default 241 // storage actor and inherited by all storage actors we have to do it this 242 // way. 243 // Only look up internal hosts if we are in the browser toolbox 244 const isBrowserToolbox = this.storageActor.parentActor.isRootActor; 245 246 this._internalHosts = isBrowserToolbox ? await this.getInternalHosts() : []; 247 248 return this.hosts; 249 } 250 251 /** 252 * Remove an indexedDB database from given host with a given name. 253 */ 254 async removeDatabase(host, name) { 255 const win = this.storageActor.getWindowFromHost(host); 256 if (!win) { 257 return { error: `Window for host ${host} not found` }; 258 } 259 260 const principal = win.document.effectiveStoragePrincipal; 261 return this.removeDB(host, principal, name); 262 } 263 264 async removeAll(host, name) { 265 const [db, store] = JSON.parse(name); 266 267 const win = this.storageActor.getWindowFromHost(host); 268 if (!win) { 269 return; 270 } 271 272 const principal = win.document.effectiveStoragePrincipal; 273 this.clearDBStore(host, principal, db, store); 274 } 275 276 async removeItem(host, name) { 277 const [db, store, id] = JSON.parse(name); 278 279 const win = this.storageActor.getWindowFromHost(host); 280 if (!win) { 281 return; 282 } 283 284 const principal = win.document.effectiveStoragePrincipal; 285 this.removeDBRecord(host, principal, db, store, id); 286 } 287 288 getNamesForHost(host) { 289 const storesForHost = this.hostVsStores.get(host); 290 if (!storesForHost) { 291 return []; 292 } 293 294 const names = []; 295 296 for (const [dbName, { objectStores }] of storesForHost) { 297 if (objectStores.size) { 298 for (const objectStore of objectStores.keys()) { 299 names.push(JSON.stringify([dbName, objectStore])); 300 } 301 } else { 302 names.push(JSON.stringify([dbName])); 303 } 304 } 305 306 return names; 307 } 308 309 /** 310 * Returns the total number of entries for various types of requests to 311 * getStoreObjects for Indexed DB actor. 312 * 313 * @param {string} host 314 * The host for the request. 315 * @param {array:string} names 316 * Array of stringified name objects for indexed db actor. 317 * The request type depends on the length of any parsed entry from this 318 * array. 0 length refers to request for the whole host. 1 length 319 * refers to request for a particular db in the host. 2 length refers 320 * to a particular object store in a db in a host. 3 length refers to 321 * particular items of an object store in a db in a host. 322 * @param {object} options 323 * An options object containing following properties: 324 * - index {string} The IDBIndex for the object store in the db. 325 */ 326 getObjectsSize(host, names, options) { 327 // In Indexed DB, we are interested in only the first name, as the pattern 328 // should follow in all entries. 329 const name = names[0]; 330 const parsedName = JSON.parse(name); 331 332 if (parsedName.length == 3) { 333 // This is the case where specific entries from an object store were 334 // requested 335 return names.length; 336 } else if (parsedName.length == 2) { 337 // This is the case where all entries from an object store are requested. 338 const index = options.index; 339 const [db, objectStore] = parsedName; 340 if (this.objectsSize[host + db + objectStore + index]) { 341 return this.objectsSize[host + db + objectStore + index]; 342 } 343 } else if (parsedName.length == 1) { 344 // This is the case where details of all object stores in a db are 345 // requested. 346 if ( 347 this.hostVsStores.has(host) && 348 this.hostVsStores.get(host).has(parsedName[0]) 349 ) { 350 return this.hostVsStores.get(host).get(parsedName[0]).objectStores.size; 351 } 352 } else if (!parsedName || !parsedName.length) { 353 // This is the case were details of all dbs in a host are requested. 354 if (this.hostVsStores.has(host)) { 355 return this.hostVsStores.get(host).size; 356 } 357 } 358 return 0; 359 } 360 361 /** 362 * Returns the over-the-wire implementation of the indexed db entity. 363 */ 364 toStoreObject(item) { 365 if (!item) { 366 return null; 367 } 368 369 if ("indexes" in item) { 370 // Object store meta data 371 return { 372 objectStore: item.name, 373 keyPath: item.keyPath, 374 autoIncrement: item.autoIncrement, 375 indexes: item.indexes, 376 }; 377 } 378 if ("objectStores" in item) { 379 // DB meta data 380 return { 381 uniqueKey: `${item.name} (${item.storage})`, 382 db: item.name, 383 storage: item.storage, 384 origin: item.origin, 385 version: item.version, 386 objectStores: item.objectStores, 387 }; 388 } 389 390 const value = JSON.stringify(item.value); 391 392 // Indexed db entry 393 return { 394 name: item.name, 395 value: new LongStringActor(this.conn, value), 396 }; 397 } 398 399 form() { 400 const hosts = {}; 401 for (const host of this.hosts) { 402 hosts[host] = this.getNamesForHost(host); 403 } 404 405 return { 406 actor: this.actorID, 407 hosts, 408 traits: this._getTraits(), 409 }; 410 } 411 412 onItemUpdated(action, host, path) { 413 dump(" IDX.onItemUpdated(" + action + " - " + host + " - " + path + "\n"); 414 // Database was removed, remove it from stores map 415 if (action === "deleted" && path.length === 1) { 416 if (this.hostVsStores.has(host)) { 417 this.hostVsStores.get(host).delete(path[0]); 418 } 419 } 420 421 this.storageActor.update(action, "indexedDB", { 422 [host]: [JSON.stringify(path)], 423 }); 424 } 425 426 async getFields(subType) { 427 switch (subType) { 428 // Detail of database 429 case "database": 430 return [ 431 { name: "objectStore", editable: false }, 432 { name: "keyPath", editable: false }, 433 { name: "autoIncrement", editable: false }, 434 { name: "indexes", editable: false }, 435 ]; 436 437 // Detail of object store 438 case "object store": 439 return [ 440 { name: "name", editable: false }, 441 { name: "value", editable: false }, 442 ]; 443 444 // Detail of indexedDB for one origin 445 default: 446 return [ 447 { name: "uniqueKey", editable: false, private: true }, 448 { name: "db", editable: false }, 449 { name: "storage", editable: false }, 450 { name: "origin", editable: false }, 451 { name: "version", editable: false }, 452 { name: "objectStores", editable: false }, 453 ]; 454 } 455 } 456 457 /** 458 * Fetches and stores all the metadata information for the given database 459 * `name` for the given `host` with its `principal`. The stored metadata 460 * information is of `DatabaseMetadata` type. 461 */ 462 async getDBMetaData(host, principal, name, storage) { 463 const request = this.openWithPrincipal(principal, name, storage); 464 return new Promise(resolve => { 465 request.onsuccess = event => { 466 const db = event.target.result; 467 const dbData = new DatabaseMetadata(host, db, storage); 468 db.close(); 469 470 resolve(dbData); 471 }; 472 request.onerror = ({ target }) => { 473 console.error( 474 `Error opening indexeddb database ${name} for host ${host}`, 475 target.error 476 ); 477 resolve(null); 478 }; 479 }); 480 } 481 482 splitNameAndStorage(name) { 483 const lastOpenBracketIndex = name.lastIndexOf("("); 484 const lastCloseBracketIndex = name.lastIndexOf(")"); 485 const delta = lastCloseBracketIndex - lastOpenBracketIndex - 1; 486 487 const storage = name.substr(lastOpenBracketIndex + 1, delta); 488 489 name = name.substr(0, lastOpenBracketIndex - 1); 490 491 return { storage, name }; 492 } 493 494 /** 495 * Get all "internal" hosts. Internal hosts are database namespaces used by 496 * the browser. 497 */ 498 async getInternalHosts() { 499 const profileDir = PathUtils.profileDir; 500 const storagePath = PathUtils.join(profileDir, "storage", "permanent"); 501 const children = await IOUtils.getChildren(storagePath); 502 const hosts = []; 503 504 for (const path of children) { 505 const exists = await IOUtils.exists(path); 506 if (!exists) { 507 continue; 508 } 509 510 const stats = await IOUtils.stat(path); 511 if ( 512 stats.type === "directory" && 513 !SAFE_HOSTS_PREFIXES_REGEX.test(stats.path) 514 ) { 515 const basename = PathUtils.filename(path); 516 hosts.push(basename); 517 } 518 } 519 520 return hosts; 521 } 522 523 /** 524 * Opens an indexed db connection for the given `principal` and 525 * database `name`. 526 */ 527 openWithPrincipal(principal, name, storage) { 528 return indexedDBForStorage.openForPrincipal(principal, name, { 529 storage, 530 }); 531 } 532 533 async removeDB(host, principal, dbName) { 534 const result = new Promise(resolve => { 535 const { name, storage } = this.splitNameAndStorage(dbName); 536 const request = indexedDBForStorage.deleteForPrincipal(principal, name, { 537 storage, 538 }); 539 540 request.onsuccess = () => { 541 resolve({}); 542 this.onItemUpdated("deleted", host, [dbName]); 543 }; 544 545 request.onblocked = () => { 546 console.warn( 547 `Deleting indexedDB database ${name} for host ${host} is blocked` 548 ); 549 resolve({ blocked: true }); 550 }; 551 552 request.onerror = () => { 553 const { error } = request; 554 console.warn( 555 `Error deleting indexedDB database ${name} for host ${host}: ${error}` 556 ); 557 resolve({ error: error.message }); 558 }; 559 560 // If the database is blocked repeatedly, the onblocked event will not 561 // be fired again. To avoid waiting forever, report as blocked if nothing 562 // else happens after 3 seconds. 563 setTimeout(() => resolve({ blocked: true }), 3000); 564 }); 565 566 return result; 567 } 568 569 async removeDBRecord(host, principal, dbName, storeName, id) { 570 let db; 571 const { name, storage } = this.splitNameAndStorage(dbName); 572 573 try { 574 db = await new Promise((resolve, reject) => { 575 const request = this.openWithPrincipal(principal, name, storage); 576 request.onsuccess = ev => resolve(ev.target.result); 577 request.onerror = ev => reject(ev.target.error); 578 }); 579 580 const transaction = db.transaction(storeName, "readwrite"); 581 const store = transaction.objectStore(storeName); 582 583 await new Promise((resolve, reject) => { 584 const request = store.delete(id); 585 request.onsuccess = () => resolve(); 586 request.onerror = ev => reject(ev.target.error); 587 }); 588 589 this.onItemUpdated("deleted", host, [dbName, storeName, id]); 590 } catch (error) { 591 const recordPath = [dbName, storeName, id].join("/"); 592 console.error( 593 `Failed to delete indexedDB record: ${recordPath}: ${error}` 594 ); 595 } 596 597 if (db) { 598 db.close(); 599 } 600 601 return null; 602 } 603 604 async clearDBStore(host, principal, dbName, storeName) { 605 let db; 606 const { name, storage } = this.splitNameAndStorage(dbName); 607 608 try { 609 db = await new Promise((resolve, reject) => { 610 const request = this.openWithPrincipal(principal, name, storage); 611 request.onsuccess = ev => resolve(ev.target.result); 612 request.onerror = ev => reject(ev.target.error); 613 }); 614 615 const transaction = db.transaction(storeName, "readwrite"); 616 const store = transaction.objectStore(storeName); 617 618 await new Promise((resolve, reject) => { 619 const request = store.clear(); 620 request.onsuccess = () => resolve(); 621 request.onerror = ev => reject(ev.target.error); 622 }); 623 624 this.onItemUpdated("cleared", host, [dbName, storeName]); 625 } catch (error) { 626 const storePath = [dbName, storeName].join("/"); 627 console.error(`Failed to clear indexedDB store: ${storePath}: ${error}`); 628 } 629 630 if (db) { 631 db.close(); 632 } 633 634 return null; 635 } 636 637 /** 638 * Fetches all the databases and their metadata for the given `host`. 639 */ 640 async getDBNamesForHost(host, principal) { 641 const sanitizedHost = this.getSanitizedHost(host) + principal.originSuffix; 642 const profileDir = PathUtils.profileDir; 643 const storagePath = PathUtils.join(profileDir, "storage"); 644 const files = []; 645 const names = []; 646 647 // We expect sqlite DB paths to look something like this: 648 // - PathToProfileDir/storage/default/http+++www.example.com/ 649 // idb/1556056096MeysDaabta.sqlite 650 // - PathToProfileDir/storage/permanent/http+++www.example.com/ 651 // idb/1556056096MeysDaabta.sqlite 652 // - PathToProfileDir/storage/temporary/http+++www.example.com/ 653 // idb/1556056096MeysDaabta.sqlite 654 // The subdirectory inside the storage folder is determined by the storage 655 // type: 656 // - default: { storage: "default" } or not specified. 657 // - permanent: { storage: "persistent" }. 658 // - temporary: { storage: "temporary" }. 659 const sqliteFiles = await this.findSqlitePathsForHost( 660 storagePath, 661 sanitizedHost 662 ); 663 664 for (const file of sqliteFiles) { 665 const splitPath = PathUtils.split(file); 666 const idbIndex = splitPath.indexOf("idb"); 667 const storage = splitPath[idbIndex - 2]; 668 const relative = file.substr(profileDir.length + 1); 669 670 files.push({ 671 file: relative, 672 storage: storage === "permanent" ? "persistent" : storage, 673 }); 674 } 675 676 if (files.length) { 677 for (const { file, storage } of files) { 678 const name = await this.getNameFromDatabaseFile(file); 679 if (name) { 680 names.push({ 681 name, 682 storage, 683 }); 684 } 685 } 686 } 687 688 return { names }; 689 } 690 691 /** 692 * Find all SQLite files that hold IndexedDB data for a host, such as: 693 * storage/temporary/http+++www.example.com/idb/1556056096MeysDaabta.sqlite 694 */ 695 async findSqlitePathsForHost(storagePath, sanitizedHost) { 696 const sqlitePaths = []; 697 const idbPaths = await this.findIDBPathsForHost(storagePath, sanitizedHost); 698 for (const idbPath of idbPaths) { 699 const children = await IOUtils.getChildren(idbPath); 700 701 for (const path of children) { 702 const exists = await IOUtils.exists(path); 703 if (!exists) { 704 continue; 705 } 706 707 const stats = await IOUtils.stat(path); 708 if (stats.type !== "directory" && stats.path.endsWith(".sqlite")) { 709 sqlitePaths.push(path); 710 } 711 } 712 } 713 return sqlitePaths; 714 } 715 716 /** 717 * Find all paths that hold IndexedDB data for a host, such as: 718 * storage/temporary/http+++www.example.com/idb 719 */ 720 async findIDBPathsForHost(storagePath, sanitizedHost) { 721 const idbPaths = []; 722 const typePaths = await this.findStorageTypePaths(storagePath); 723 for (const typePath of typePaths) { 724 const idbPath = PathUtils.join(typePath, sanitizedHost, "idb"); 725 if (await IOUtils.exists(idbPath)) { 726 idbPaths.push(idbPath); 727 } 728 } 729 return idbPaths; 730 } 731 732 /** 733 * Find all the storage types, such as "default", "permanent", or "temporary". 734 * These names have changed over time, so it seems simpler to look through all 735 * types that currently exist in the profile. 736 */ 737 async findStorageTypePaths(storagePath) { 738 const children = await IOUtils.getChildren(storagePath); 739 const typePaths = []; 740 741 for (const path of children) { 742 const exists = await IOUtils.exists(path); 743 if (!exists) { 744 continue; 745 } 746 747 const stats = await IOUtils.stat(path); 748 if (stats.type === "directory") { 749 typePaths.push(path); 750 } 751 } 752 753 return typePaths; 754 } 755 756 /** 757 * Removes any illegal characters from the host name to make it a valid file 758 * name. 759 */ 760 getSanitizedHost(host) { 761 if (host.startsWith("about:")) { 762 host = "moz-safe-" + host; 763 } 764 return host.replace(ILLEGAL_CHAR_REGEX, "+"); 765 } 766 767 /** 768 * Retrieves the proper indexed db database name from the provided .sqlite 769 * file location. 770 */ 771 async getNameFromDatabaseFile(path) { 772 let connection = null; 773 774 // First establish a connection 775 // Content pages might be having an open transaction for the same indexed db 776 // which this sqlite file belongs to. In that case, sqlite.openConnection 777 // will throw. Thus we retry for some time to see if lock is removed. 778 for (let retries = 0; !connection && retries < 25; retries++) { 779 try { 780 connection = await lazy.Sqlite.openConnection({ path }); 781 } catch (ex) { 782 // Continuously retrying is overkill. Waiting for 100ms before next try 783 await sleep(100); 784 } 785 } 786 787 // If the connection could not be established, bail out. 788 if (connection === null) { 789 console.error("Unable to open Sqlite connection to path: " + path); 790 return null; 791 } 792 793 // Then try to read the name. 794 let name = undefined; 795 for (let retries = 0; name === undefined && retries < 25; retries++) { 796 try { 797 const rows = await connection.execute("SELECT name FROM database"); 798 name = rows[0].getResultByName("name"); 799 } catch { 800 // Database might not be ready immediately, keep polling. 801 await sleep(100); 802 } 803 } 804 805 if (!name) { 806 console.error("Unable to get the database name for path: " + path); 807 } 808 809 // Always close the connection, even if a name could not be retrieved. 810 await connection.close(); 811 812 return name; 813 } 814 815 async getValuesForHost( 816 host, 817 name = "null", 818 options, 819 hostVsStores, 820 principal 821 ) { 822 name = JSON.parse(name); 823 if (!name || !name.length) { 824 // This means that details about the db in this particular host are 825 // requested. 826 const dbs = []; 827 if (hostVsStores.has(host)) { 828 for (let [, db] of hostVsStores.get(host)) { 829 db = this.patchMetadataMapsAndProtos(db); 830 dbs.push(db.toObject()); 831 } 832 } 833 return { dbs }; 834 } 835 836 const [db2, objectStore, id] = name; 837 if (!objectStore) { 838 // This means that details about all the object stores in this db are 839 // requested. 840 const objectStores = []; 841 if (hostVsStores.has(host) && hostVsStores.get(host).has(db2)) { 842 let db = hostVsStores.get(host).get(db2); 843 844 db = this.patchMetadataMapsAndProtos(db); 845 846 const objectStores2 = db.objectStores; 847 848 for (const objectStore2 of objectStores2) { 849 objectStores.push(objectStore2[1].toObject()); 850 } 851 } 852 return { 853 objectStores, 854 }; 855 } 856 // Get either all entries from the object store, or a particular id 857 const storage = hostVsStores.get(host).get(db2).storage; 858 const result = await this.getObjectStoreData( 859 host, 860 principal, 861 db2, 862 storage, 863 { 864 objectStore, 865 id, 866 index: options.index, 867 offset: options.offset, 868 size: options.size, 869 } 870 ); 871 return { result }; 872 } 873 874 /** 875 * Returns requested entries (or at most MAX_STORE_OBJECT_COUNT) from a particular 876 * objectStore from the db in the given host. 877 * 878 * @param {string} host 879 * The given host. 880 * @param {nsIPrincipal} principal 881 * The principal of the given document. 882 * @param {string} dbName 883 * The name of the indexed db from the above host. 884 * @param {string} storage 885 * Storage type, either "temporary", "default" or "persistent". 886 * @param {object} requestOptions 887 * An object in the following format: 888 * { 889 * objectStore: The name of the object store from the above db, 890 * id: Id of the requested entry from the above object 891 * store. null if all entries from the above object 892 * store are requested, 893 * index: Name of the IDBIndex to be iterated on while fetching 894 * entries. null or "name" if no index is to be 895 * iterated, 896 * offset: offset of the entries to be fetched, 897 * size: The intended size of the entries to be fetched 898 * } 899 */ 900 getObjectStoreData(host, principal, dbName, storage, requestOptions) { 901 const { name } = this.splitNameAndStorage(dbName); 902 const request = this.openWithPrincipal(principal, name, storage); 903 904 return new Promise(resolve => { 905 let { objectStore, id, index, offset, size } = requestOptions; 906 const data = []; 907 let db; 908 909 if (!size || size > MAX_STORE_OBJECT_COUNT) { 910 size = MAX_STORE_OBJECT_COUNT; 911 } 912 913 request.onsuccess = event => { 914 db = event.target.result; 915 916 const transaction = db.transaction(objectStore, "readonly"); 917 let source = transaction.objectStore(objectStore); 918 if (index && index != "name") { 919 source = source.index(index); 920 } 921 922 source.count().onsuccess = event2 => { 923 const objectsSize = []; 924 const count = event2.target.result; 925 objectsSize.push({ 926 key: host + dbName + objectStore + index, 927 count, 928 }); 929 930 if (!offset) { 931 offset = 0; 932 } else if (offset > count) { 933 db.close(); 934 resolve([]); 935 return; 936 } 937 938 if (id) { 939 source.get(id).onsuccess = event3 => { 940 db.close(); 941 resolve([{ name: id, value: event3.target.result }]); 942 }; 943 } else { 944 source.openCursor().onsuccess = event4 => { 945 const cursor = event4.target.result; 946 947 if (!cursor || data.length >= size) { 948 db.close(); 949 resolve({ 950 data, 951 objectsSize, 952 }); 953 return; 954 } 955 if (offset-- <= 0) { 956 data.push({ name: cursor.key, value: cursor.value }); 957 } 958 cursor.continue(); 959 }; 960 } 961 }; 962 }; 963 964 request.onerror = () => { 965 db.close(); 966 resolve([]); 967 }; 968 }); 969 } 970 971 /** 972 * When indexedDB metadata is parsed to and from JSON then the object's 973 * prototype is dropped and any Maps are changed to arrays of arrays. This 974 * method is used to repair the prototypes and fix any broken Maps. 975 */ 976 patchMetadataMapsAndProtos(metadata) { 977 const md = Object.create(DatabaseMetadata.prototype); 978 Object.assign(md, metadata); 979 980 md._objectStores = new Map(metadata._objectStores); 981 982 for (const [name, store] of md._objectStores) { 983 const obj = Object.create(ObjectStoreMetadata.prototype); 984 Object.assign(obj, store); 985 986 md._objectStores.set(name, obj); 987 988 if (typeof store._indexes.length !== "undefined") { 989 obj._indexes = new Map(store._indexes); 990 } 991 992 for (const [name2, value] of obj._indexes) { 993 const obj2 = Object.create(IndexMetadata.prototype); 994 Object.assign(obj2, value); 995 996 obj._indexes.set(name2, obj2); 997 } 998 } 999 1000 return md; 1001 } 1002 } 1003 exports.IndexedDBStorageActor = IndexedDBStorageActor;