Bug 1445160 - Enable inspect indexedDBs inside JSMs via Storage Inspector in browser toolbox r?nchevobbe draft
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Wed, 21 Mar 2018 16:09:57 +0000
changeset 779728 4f77446122677ee4d87f7c14ed8de70aa7c7016d
parent 779502 83de58ddda2057f1cb949537f6b111e3b115ea3d
child 779729 00d27e2fc6fa3e35ae7f7eb699af0b770a6b4e6d
push id105839
push userbmo:mratcliffe@mozilla.com
push dateTue, 10 Apr 2018 12:22:43 +0000
reviewersnchevobbe
bugs1445160
milestone61.0a1
Bug 1445160 - Enable inspect indexedDBs inside JSMs via Storage Inspector in browser toolbox r?nchevobbe Fixed added file: and moz-extension: to prefix regex. MozReview-Commit-ID: JtCgzquybA4
devtools/client/storage/test/browser_storage_basic.js
devtools/client/storage/test/browser_storage_basic_usercontextid_1.js
devtools/client/storage/test/browser_storage_basic_usercontextid_2.js
devtools/client/storage/test/browser_storage_basic_with_fragment.js
devtools/client/storage/test/browser_storage_delete_usercontextid.js
devtools/client/storage/ui.js
devtools/server/actors/storage.js
--- a/devtools/client/storage/test/browser_storage_basic.js
+++ b/devtools/client/storage/test/browser_storage_basic.js
@@ -90,17 +90,17 @@ const testCases = [
 
 /**
  * Test that the desired number of tree items are present
  */
 function testTree() {
   let doc = gPanelWindow.document;
   for (let [item] of testCases) {
     ok(doc.querySelector("[data-id='" + JSON.stringify(item) + "']"),
-       "Tree item " + item[0] + " should be present in the storage tree");
+      `Tree item ${item.toSource()} should be present in the storage tree`);
   }
 }
 
 /**
  * Test that correct table entries are shown for each of the tree item
  */
 async function testTables() {
   let doc = gPanelWindow.document;
--- a/devtools/client/storage/test/browser_storage_basic_usercontextid_1.js
+++ b/devtools/client/storage/test/browser_storage_basic_usercontextid_1.js
@@ -75,17 +75,17 @@ const testCases = [
 
 /**
  * Test that the desired number of tree items are present
  */
 function testTree(tests) {
   let doc = gPanelWindow.document;
   for (let [item] of tests) {
     ok(doc.querySelector("[data-id='" + JSON.stringify(item) + "']"),
-       "Tree item " + item[0] + " should be present in the storage tree");
+      `Tree item ${item.toSource()} should be present in the storage tree`);
   }
 }
 
 /**
  * Test that correct table entries are shown for each of the tree item
  */
 async function testTables(tests) {
   let doc = gPanelWindow.document;
--- a/devtools/client/storage/test/browser_storage_basic_usercontextid_2.js
+++ b/devtools/client/storage/test/browser_storage_basic_usercontextid_2.js
@@ -69,17 +69,17 @@ const testCasesUserContextId = [
 
 /**
  * Test that the desired number of tree items are present
  */
 function testTree(tests) {
   let doc = gPanelWindow.document;
   for (let [item] of tests) {
     ok(doc.querySelector("[data-id='" + JSON.stringify(item) + "']"),
-       "Tree item " + item[0] + " should be present in the storage tree");
+      `Tree item ${item.toSource()} should be present in the storage tree`);
   }
 }
 
 /**
  * Test that correct table entries are shown for each of the tree item
  */
 async function testTables(tests) {
   let doc = gPanelWindow.document;
--- a/devtools/client/storage/test/browser_storage_basic_with_fragment.js
+++ b/devtools/client/storage/test/browser_storage_basic_with_fragment.js
@@ -93,17 +93,17 @@ const testCases = [
 
 /**
  * Test that the desired number of tree items are present
  */
 function testTree() {
   let doc = gPanelWindow.document;
   for (let [item] of testCases) {
     ok(doc.querySelector("[data-id='" + JSON.stringify(item) + "']"),
-       "Tree item " + item[0] + " should be present in the storage tree");
+      `Tree item ${item.toSource()} should be present in the storage tree`);
   }
 }
 
 /**
  * Test that correct table entries are shown for each of the tree item
  */
 async function testTables() {
   let doc = gPanelWindow.document;
--- a/devtools/client/storage/test/browser_storage_delete_usercontextid.js
+++ b/devtools/client/storage/test/browser_storage_delete_usercontextid.js
@@ -93,17 +93,17 @@ const storageItemsForDefault = [
 
 /**
  * Test that the desired number of tree items are present
  */
 function testTree(tests) {
   let doc = gPanelWindow.document;
   for (let [item] of tests) {
     ok(doc.querySelector("[data-id='" + JSON.stringify(item) + "']"),
-       "Tree item " + item[0] + " should be present in the storage tree");
+      `Tree item ${item.toSource()} should be present in the storage tree`);
   }
 }
 
 /**
  * Test that correct table entries are shown for each of the tree item
  */
 async function testTables(tests) {
   let doc = gPanelWindow.document;
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -54,16 +54,18 @@ const COOKIE_KEY_MAP = {
   expires: "Expires",
   isSecure: "Secure",
   isHttpOnly: "HttpOnly",
   isDomain: "HostOnly",
   creationTime: "CreationTime",
   lastAccessed: "LastAccessed"
 };
 
+const SAFE_HOSTS_PREFIXES_REGEX = /^(about:|https?:|file:|moz-extension:)/;
+
 // Maximum length of item name to show in context menu label - will be
 // trimmed with ellipsis if it's longer.
 const ITEM_NAME_MAX_LENGTH = 32;
 
 /**
  * StorageUI is controls and builds the UI of the Storage Inspector.
  *
  * @param {Front} front
@@ -124,16 +126,36 @@ class StorageUI {
     });
     let key = L10N.getStr("storage.filter.key");
     shortcuts.on(key, event => {
       event.preventDefault();
       this.searchBox.focus();
     });
 
     this.front.listStores().then(storageTypes => {
+      // When we are in the browser console we list indexedDBs internal to
+      // Firefox e.g. defined inside a .jsm. Because there is no way before this
+      // point to know whether or not we are inside the browser toolbox we have
+      // already fetched the hostnames of these databases.
+      //
+      // If we are not inside the browser toolbox we need to delete these
+      // hostnames.
+      if (!this._target.chrome && storageTypes.indexedDB) {
+        let hosts = storageTypes.indexedDB.hosts;
+        let newHosts = {};
+
+        for (let [host, dbs] of Object.entries(hosts)) {
+          if (SAFE_HOSTS_PREFIXES_REGEX.test(host)) {
+            newHosts[host] = dbs;
+          }
+        }
+
+        storageTypes.indexedDB.hosts = newHosts;
+      }
+
       this.populateStorageTree(storageTypes);
     }).catch(e => {
       if (!this._toolbox || this._toolbox._destroyer) {
         // The toolbox is in the process of being destroyed... in this case throwing here
         // is expected and normal so let's ignore the error.
         return;
       }
 
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -1,36 +1,42 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const {Ci, Cu, CC} = require("chrome");
+const {Cc, Ci, Cu, CC} = require("chrome");
 const protocol = require("devtools/shared/protocol");
 const {LongStringActor} = require("devtools/server/actors/string");
 const {DebuggerServer} = require("devtools/server/main");
 const Services = require("Services");
 const defer = require("devtools/shared/defer");
 const {isWindowIncluded} = require("devtools/shared/layout/utils");
 const specs = require("devtools/shared/specs/storage");
 
+const CHROME_ENABLED_PREF = "devtools.chrome.enabled";
+const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled";
+
 const DEFAULT_VALUE = "value";
 
 loader.lazyRequireGetter(this, "naturalSortCaseInsensitive",
   "devtools/client/shared/natural-sort", true);
 
 // "Lax", "Strict" and "Unset" are special values of the sameSite property
 // that should not be translated.
 const COOKIE_SAMESITE = {
   LAX: "Lax",
   STRICT: "Strict",
   UNSET: "Unset"
 };
 
+const SAFE_HOSTS_PREFIXES_REGEX =
+  /^(about\+|https?\+|file\+|moz-extension\+)/;
+
 // GUID to be used as a separator in compound keys. This must match the same
 // constant in devtools/client/storage/ui.js,
 // devtools/client/storage/test/head.js and
 // devtools/server/tests/browser/head.js
 const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
 
 loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
 loader.lazyImporter(this, "Sqlite", "resource://gre/modules/Sqlite.jsm");
@@ -124,28 +130,34 @@ StorageActors.defaults = function(typeNa
   return {
     typeName: typeName,
 
     get conn() {
       return this.storageActor.conn;
     },
 
     /**
-     * Returns a list of currently knwon hosts for the target window. This list
-     * contains unique hosts from the window + all inner windows.
+     * Returns a list of currently known hosts for the target window. This list
+     * contains unique hosts from the window + all inner windows. If
+     * this._internalHosts is defined then these will also be added to the list.
      */
     get hosts() {
       let hosts = new Set();
       for (let {location} of this.storageActor.windows) {
         let host = this.getHostName(location);
 
         if (host) {
           hosts.add(host);
         }
       }
+      if (this._internalHosts) {
+        for (let host of this._internalHosts) {
+          hosts.add(host);
+        }
+      }
       return hosts;
     },
 
     /**
      * Returns all the windows present on the page. Includes main window + inner
      * iframe windows.
      */
     get windows() {
@@ -338,19 +350,17 @@ StorageActors.defaults = function(typeNa
         data: []
       };
 
       let principal = null;
       if (this.typeName === "indexedDB") {
         // We only acquire principal when the type of the storage is indexedDB
         // because the principal only matters the indexedDB.
         let win = this.storageActor.getWindowFromHost(host);
-        if (win) {
-          principal = win.document.nodePrincipal;
-        }
+        principal = this.getPrincipal(win);
       }
 
       if (names) {
         for (let name of names) {
           let values = await this.getValuesForHost(host, name, options,
             this.hostVsStores, principal);
 
           let {result, objectStores} = values;
@@ -403,16 +413,26 @@ StorageActors.defaults = function(typeNa
             return naturalSortCaseInsensitive(a[sortOn], b[sortOn]);
           });
           let sliced = sorted.slice(offset, offset + size);
           toReturn.data = sliced.map(object => this.toStoreObject(object));
         }
       }
 
       return toReturn;
+    },
+
+    getPrincipal(win) {
+      if (win) {
+        return win.document.nodePrincipal;
+      }
+      // We are running in the browser toolbox and viewing system DBs so we
+      // need to use system principal.
+      return Cc["@mozilla.org/systemprincipal;1"]
+                .createInstance(Ci.nsIPrincipal);
     }
   };
 };
 
 /**
  * Creates an actor and its corresponding front and registers it to the Storage
  * Actor.
  *
@@ -1613,16 +1633,31 @@ StorageActors.createActor({
     this.storageActor.off("window-destroyed", this.onWindowDestroyed);
 
     protocol.Actor.prototype.destroy.call(this);
 
     this.storageActor = null;
   },
 
   /**
+   * Returns a list of currently known hosts for the target window. This list
+   * contains unique hosts from the window, all inner windows and all permanent
+   * indexedDB hosts defined inside the browser.
+   */
+  async getHosts() {
+    // Add internal hosts to this._internalHosts, which will be picked up by
+    // the this.hosts getter. Because this.hosts is a property on the default
+    // storage actor and inherited by all storage actors we have to do it this
+    // way.
+    this._internalHosts = await this.getInternalHosts();
+
+    return this.hosts;
+  },
+
+  /**
    * Remove an indexedDB database from given host with a given name.
    */
   async removeDatabase(host, name) {
     let win = this.storageActor.getWindowFromHost(host);
     if (!win) {
       return { error: `Window for host ${host} not found` };
     }
 
@@ -1730,36 +1765,35 @@ StorageActors.createActor({
    * Purpose of this method is same as populateStoresForHosts but this is async.
    * This exact same operation cannot be performed in populateStoresForHosts
    * method, as that method is called in initialize method of the actor, which
    * cannot be asynchronous.
    */
   async preListStores() {
     this.hostVsStores = new Map();
 
-    for (let host of this.hosts) {
+    for (let host of await this.getHosts()) {
       await this.populateStoresForHost(host);
     }
   },
 
   async populateStoresForHost(host) {
     let storeMap = new Map();
 
     let win = this.storageActor.getWindowFromHost(host);
-    if (win) {
-      let principal = win.document.nodePrincipal;
-      let {names} = await this.getDBNamesForHost(host, principal);
-
-      for (let {name, storage} of names) {
-        let metadata = await this.getDBMetaData(host, principal, name, storage);
-
-        metadata = indexedDBHelpers.patchMetadataMapsAndProtos(metadata);
-
-        storeMap.set(`${name} (${storage})`, metadata);
-      }
+    let principal = this.getPrincipal(win);
+
+    let {names} = await this.getDBNamesForHost(host, principal);
+
+    for (let {name, storage} of names) {
+      let metadata = await this.getDBMetaData(host, principal, name, storage);
+
+      metadata = indexedDBHelpers.patchMetadataMapsAndProtos(metadata);
+
+      storeMap.set(`${name} (${storage})`, metadata);
     }
 
     this.hostVsStores.set(host, storeMap);
   },
 
   /**
    * Returns the over-the-wire implementation of the indexed db entity.
    */
@@ -1848,29 +1882,31 @@ StorageActors.createActor({
       this.getNameFromDatabaseFile = indexedDBHelpers.getNameFromDatabaseFile;
       this.getObjectStoreData = indexedDBHelpers.getObjectStoreData;
       this.getSanitizedHost = indexedDBHelpers.getSanitizedHost;
       this.getValuesForHost = indexedDBHelpers.getValuesForHost;
       this.openWithPrincipal = indexedDBHelpers.openWithPrincipal;
       this.removeDB = indexedDBHelpers.removeDB;
       this.removeDBRecord = indexedDBHelpers.removeDBRecord;
       this.splitNameAndStorage = indexedDBHelpers.splitNameAndStorage;
+      this.getInternalHosts = indexedDBHelpers.getInternalHosts;
       return;
     }
 
     const { sendAsyncMessage, addMessageListener } =
       this.conn.parentMessageManager;
 
     this.conn.setupInParent({
       module: "devtools/server/actors/storage",
       setupParent: "setupParentProcessForIndexedDB"
     });
 
     this.getDBMetaData = callParentProcessAsync.bind(null, "getDBMetaData");
     this.splitNameAndStorage = callParentProcessAsync.bind(null, "splitNameAndStorage");
+    this.getInternalHosts = callParentProcessAsync.bind(null, "getInternalHosts");
     this.getDBNamesForHost = callParentProcessAsync.bind(null, "getDBNamesForHost");
     this.getValuesForHost = callParentProcessAsync.bind(null, "getValuesForHost");
     this.removeDB = callParentProcessAsync.bind(null, "removeDB");
     this.removeDBRecord = callParentProcessAsync.bind(null, "removeDBRecord");
     this.clearDBStore = callParentProcessAsync.bind(null, "clearDBStore");
 
     addMessageListener("debug:storage-indexedDB-request-child", msg => {
       switch (msg.json.method) {
@@ -1984,16 +2020,42 @@ var indexedDBHelpers = {
     let storage = name.substr(lastOpenBracketIndex + 1, delta);
 
     name = name.substr(0, lastOpenBracketIndex - 1);
 
     return { storage, name };
   },
 
   /**
+   * Get all "internal" hosts. Internal hosts are database namespaces used by
+   * the browser.
+   */
+  async getInternalHosts() {
+    // Return an empty array if the browser toolbox is not enabled.
+    if (!Services.prefs.getBoolPref(CHROME_ENABLED_PREF) ||
+        !Services.prefs.getBoolPref(REMOTE_ENABLED_PREF)) {
+      return this.backToChild("getInternalHosts", []);
+    }
+
+    let profileDir = OS.Constants.Path.profileDir;
+    let storagePath = OS.Path.join(profileDir, "storage", "permanent");
+    let iterator = new OS.File.DirectoryIterator(storagePath);
+    let hosts = [];
+
+    await iterator.forEach(entry => {
+      if (entry.isDir && !SAFE_HOSTS_PREFIXES_REGEX.test(entry.name)) {
+        hosts.push(entry.name);
+      }
+    });
+    iterator.close();
+
+    return this.backToChild("getInternalHosts", hosts);
+  },
+
+  /**
    * Opens an indexed db connection for the given `principal` and
    * database `name`.
    */
   openWithPrincipal: function(principal, name, storage) {
     return indexedDBForStorage.openForPrincipal(principal, name,
                                                 { storage: storage });
   },
 
@@ -2422,16 +2484,19 @@ var indexedDBHelpers = {
   handleChildRequest(msg) {
     let args = msg.data.args;
 
     switch (msg.json.method) {
       case "getDBMetaData": {
         let [host, principal, name, storage] = args;
         return indexedDBHelpers.getDBMetaData(host, principal, name, storage);
       }
+      case "getInternalHosts": {
+        return indexedDBHelpers.getInternalHosts();
+      }
       case "splitNameAndStorage": {
         let [name] = args;
         return indexedDBHelpers.splitNameAndStorage(name);
       }
       case "getDBNamesForHost": {
         let [host, principal] = args;
         return indexedDBHelpers.getDBNamesForHost(host, principal);
       }