tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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;