Bug 1304297 - Support for deleting Cache and Response objects in Storage Inspector r?mikeratcliffe draft
authorJarda Snajdr <jsnajdr@gmail.com>
Thu, 22 Sep 2016 12:29:48 +0200
changeset 416569 34236afb4c74c4536179d7c3e896bc6741b21f54
parent 416562 f0e6cc6360213ba21fd98c887b55fce5c680df68
child 531876 92c476a7ad9c995c03b2c6770829b925b0efb5e8
push id30168
push userbmo:jsnajdr@gmail.com
push dateThu, 22 Sep 2016 10:38:36 +0000
reviewersmikeratcliffe
bugs1304297
milestone52.0a1
Bug 1304297 - Support for deleting Cache and Response objects in Storage Inspector r?mikeratcliffe MozReview-Commit-ID: BdK4rKhmzTo
devtools/client/storage/storage.xul
devtools/client/storage/test/browser.ini
devtools/client/storage/test/browser_storage_cache_delete.js
devtools/client/storage/test/browser_storage_delete.js
devtools/client/storage/test/browser_storage_delete_all.js
devtools/client/storage/test/browser_storage_delete_tree.js
devtools/client/storage/test/browser_storage_indexeddb_delete.js
devtools/client/storage/ui.js
devtools/server/actors/storage.js
devtools/shared/specs/storage.js
--- a/devtools/client/storage/storage.xul
+++ b/devtools/client/storage/storage.xul
@@ -21,17 +21,17 @@
   <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
 
   <commandset id="editMenuCommands"/>
 
   <popupset id="storagePopupSet">
     <menupopup id="storage-tree-popup">
       <menuitem id="storage-tree-popup-delete-all"
                 label="&storage.popupMenu.deleteAllLabel;"/>
-      <menuitem id="storage-tree-popup-delete-database"/>
+      <menuitem id="storage-tree-popup-delete"/>
     </menupopup>
     <menupopup id="storage-table-popup">
       <menuitem id="storage-table-popup-delete"/>
       <menuitem id="storage-table-popup-delete-all-from"/>
       <menuitem id="storage-table-popup-delete-all"
                 label="&storage.popupMenu.deleteAllLabel;"/>
     </menupopup>
   </popupset>
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -14,16 +14,17 @@ support-files =
   storage-secured-iframe.html
   storage-sessionstorage.html
   storage-unsecured-iframe.html
   storage-updates.html
   head.js
   !/devtools/client/framework/test/shared-head.js
 
 [browser_storage_basic.js]
+[browser_storage_cache_delete.js]
 [browser_storage_cache_error.js]
 [browser_storage_cookies_delete_all.js]
 [browser_storage_cookies_domain.js]
 [browser_storage_cookies_edit.js]
 [browser_storage_cookies_edit_keyboard.js]
 [browser_storage_cookies_tab_navigation.js]
 [browser_storage_delete.js]
 [browser_storage_delete_all.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cache_delete.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// Test deleting a Cache object from the tree using context menu
+
+add_task(function* () {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+  let contextMenu = gPanelWindow.document.getElementById("storage-tree-popup");
+  let menuDeleteItem = contextMenu.querySelector("#storage-tree-popup-delete");
+
+  let cacheToDelete = ["Cache", "http://test1.example.org", "plop"];
+
+  info("test state before delete");
+  yield selectTreeItem(cacheToDelete);
+  ok(gUI.tree.isSelected(cacheToDelete), "Cache item is present in the tree");
+
+  info("do the delete");
+  let eventWait = gUI.once("store-objects-updated");
+
+  let selector = `[data-id='${JSON.stringify(cacheToDelete)}'] > .tree-widget-item`;
+  let target = gPanelWindow.document.querySelector(selector);
+  ok(target, "Cache item's tree element is present");
+
+  yield waitForContextMenu(contextMenu, target, () => {
+    info("Opened tree context menu");
+    menuDeleteItem.click();
+
+    let cacheName = cacheToDelete[2];
+    ok(menuDeleteItem.getAttribute("label").includes(cacheName),
+      `Context menu item label contains '${cacheName}')`);
+  });
+
+  yield eventWait;
+
+  info("test state after delete");
+  yield selectTreeItem(cacheToDelete);
+  ok(!gUI.tree.isSelected(cacheToDelete), "Cache item is no longer present in the tree");
+
+  yield finishTests();
+});
--- a/devtools/client/storage/test/browser_storage_delete.js
+++ b/devtools/client/storage/test/browser_storage_delete.js
@@ -11,17 +11,19 @@
 const TEST_CASES = [
   [["localStorage", "http://test1.example.org"],
     "ls1", "name"],
   [["sessionStorage", "http://test1.example.org"],
     "ss1", "name"],
   [["cookies", "test1.example.org"],
     "c1", "name"],
   [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
-    1, "name"]
+    1, "name"],
+  [["Cache", "http://test1.example.org", "plop"],
+    MAIN_DOMAIN + "404_cached_file.js", "url"],
 ];
 
 add_task(function* () {
   yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
 
   let contextMenu = gPanelWindow.document.getElementById("storage-table-popup");
   let menuDeleteItem = contextMenu.querySelector("#storage-table-popup-delete");
 
@@ -34,18 +36,19 @@ add_task(function* () {
     let row = getRowCells(rowName);
     ok(gUI.table.items.has(rowName), `There is a row '${rowName}' in ${treeItemName}`);
 
     let eventWait = gUI.once("store-objects-updated");
 
     yield waitForContextMenu(contextMenu, row[cellToClick], () => {
       info(`Opened context menu in ${treeItemName}, row '${rowName}'`);
       menuDeleteItem.click();
-      ok(menuDeleteItem.getAttribute("label").includes(rowName),
-        `Context menu item label contains '${rowName}'`);
+      let truncatedRowName = String(rowName).substr(0, 16);
+      ok(menuDeleteItem.getAttribute("label").includes(truncatedRowName),
+        `Context menu item label contains '${rowName}' (maybe truncated)`);
     });
 
     yield eventWait;
 
     ok(!gUI.table.items.has(rowName),
       `There is no row '${rowName}' in ${treeItemName} after deletion`);
   }
 
--- a/devtools/client/storage/test/browser_storage_delete_all.js
+++ b/devtools/client/storage/test/browser_storage_delete_all.js
@@ -26,35 +26,39 @@ add_task(function* () {
     [["sessionStorage", "http://test1.example.org"],
       ["ss1"]],
     [["sessionStorage", "http://sectest1.example.org"],
       ["iframe-u-ss1", "iframe-u-ss2"]],
     [["sessionStorage", "https://sectest1.example.org"],
       ["iframe-s-ss1"]],
     [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
       [1, 2, 3]],
+    [["Cache", "http://test1.example.org", "plop"],
+      [MAIN_DOMAIN + "404_cached_file.js", MAIN_DOMAIN + "browser_storage_basic.js"]],
   ];
 
   yield checkState(beforeState);
 
   info("do the delete");
   const deleteHosts = [
-    [["localStorage", "https://sectest1.example.org"], "iframe-s-ls1"],
-    [["sessionStorage", "https://sectest1.example.org"], "iframe-s-ss1"],
-    [["indexedDB", "http://test1.example.org", "idb1", "obj1"], 1],
+    [["localStorage", "https://sectest1.example.org"], "iframe-s-ls1", "name"],
+    [["sessionStorage", "https://sectest1.example.org"], "iframe-s-ss1", "name"],
+    [["indexedDB", "http://test1.example.org", "idb1", "obj1"], 1, "name"],
+    [["Cache", "http://test1.example.org", "plop"],
+      MAIN_DOMAIN + "404_cached_file.js", "url"],
   ];
 
-  for (let [store, rowName] of deleteHosts) {
+  for (let [store, rowName, cellToClick] of deleteHosts) {
     let storeName = store.join(" > ");
 
     yield selectTreeItem(store);
 
     let eventWait = gUI.once("store-objects-cleared");
 
-    let cell = getRowCells(rowName).name;
+    let cell = getRowCells(rowName)[cellToClick];
     yield waitForContextMenu(contextMenu, cell, () => {
       info(`Opened context menu in ${storeName}, row '${rowName}'`);
       menuDeleteAllItem.click();
     });
 
     yield eventWait;
   }
 
@@ -71,14 +75,16 @@ add_task(function* () {
     [["sessionStorage", "http://test1.example.org"],
       ["ss1"]],
     [["sessionStorage", "http://sectest1.example.org"],
       ["iframe-u-ss1", "iframe-u-ss2"]],
     [["sessionStorage", "https://sectest1.example.org"],
       []],
     [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
       []],
+    [["Cache", "http://test1.example.org", "plop"],
+      []],
   ];
 
   yield checkState(afterState);
 
   yield finishTests();
 });
--- a/devtools/client/storage/test/browser_storage_delete_tree.js
+++ b/devtools/client/storage/test/browser_storage_delete_tree.js
@@ -16,24 +16,27 @@ add_task(function* () {
     "#storage-tree-popup-delete-all");
 
   info("test state before delete");
   yield checkState([
     [["cookies", "test1.example.org"], ["c1", "c3", "cs2", "uc1"]],
     [["localStorage", "http://test1.example.org"], ["ls1", "ls2"]],
     [["sessionStorage", "http://test1.example.org"], ["ss1"]],
     [["indexedDB", "http://test1.example.org", "idb1", "obj1"], [1, 2, 3]],
+    [["Cache", "http://test1.example.org", "plop"],
+      [MAIN_DOMAIN + "404_cached_file.js", MAIN_DOMAIN + "browser_storage_basic.js"]],
   ]);
 
   info("do the delete");
   const deleteHosts = [
     ["cookies", "test1.example.org"],
     ["localStorage", "http://test1.example.org"],
     ["sessionStorage", "http://test1.example.org"],
     ["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+    ["Cache", "http://test1.example.org", "plop"],
   ];
 
   for (let store of deleteHosts) {
     let storeName = store.join(" > ");
 
     yield selectTreeItem(store);
 
     let eventName = "store-objects-" +
@@ -52,12 +55,13 @@ add_task(function* () {
   }
 
   info("test state after delete");
   yield checkState([
     [["cookies", "test1.example.org"], []],
     [["localStorage", "http://test1.example.org"], []],
     [["sessionStorage", "http://test1.example.org"], []],
     [["indexedDB", "http://test1.example.org", "idb1", "obj1"], []],
+    [["Cache", "http://test1.example.org", "plop"], []],
   ]);
 
   yield finishTests();
 });
--- a/devtools/client/storage/test/browser_storage_indexeddb_delete.js
+++ b/devtools/client/storage/test/browser_storage_indexeddb_delete.js
@@ -7,18 +7,17 @@
 "use strict";
 
 // Test deleting indexedDB database from the tree using context menu
 
 add_task(function* () {
   yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-empty-objectstores.html");
 
   let contextMenu = gPanelWindow.document.getElementById("storage-tree-popup");
-  let menuDeleteDb = contextMenu.querySelector(
-    "#storage-tree-popup-delete-database");
+  let menuDeleteDb = contextMenu.querySelector("#storage-tree-popup-delete");
 
   info("test state before delete");
   yield checkState([
     [["indexedDB", "http://test1.example.org"], ["idb1", "idb2"]],
   ]);
 
   info("do the delete");
   const deletedDb = ["indexedDB", "http://test1.example.org", "idb1"];
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -62,16 +62,23 @@ const COOKIE_KEY_MAP = {
 };
 
 // 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;
 
 function addEllipsis(name) {
   if (name.length > ITEM_NAME_MAX_LENGTH) {
+    if (/^https?:/.test(name)) {
+      // For URLs, add ellipsis in the middle
+      const halfLen = ITEM_NAME_MAX_LENGTH / 2;
+      return name.slice(0, halfLen) + ELLIPSIS + name.slice(-halfLen);
+    }
+
+    // For other strings, add ellipsis at the end
     return name.substr(0, ITEM_NAME_MAX_LENGTH) + ELLIPSIS;
   }
 
   return name;
 }
 
 /**
  * StorageUI is controls and builds the UI of the Storage Inspector.
@@ -152,17 +159,17 @@ function StorageUI(front, target, panelW
 
   this.onTablePopupShowing = this.onTablePopupShowing.bind(this);
   this._tablePopup = this._panelDoc.getElementById("storage-table-popup");
   this._tablePopup.addEventListener("popupshowing", this.onTablePopupShowing);
 
   this.onRemoveItem = this.onRemoveItem.bind(this);
   this.onRemoveAllFrom = this.onRemoveAllFrom.bind(this);
   this.onRemoveAll = this.onRemoveAll.bind(this);
-  this.onRemoveDatabase = this.onRemoveDatabase.bind(this);
+  this.onRemoveTreeItem = this.onRemoveTreeItem.bind(this);
 
   this._tablePopupDelete = this._panelDoc.getElementById(
     "storage-table-popup-delete");
   this._tablePopupDelete.addEventListener("command", this.onRemoveItem);
 
   this._tablePopupDeleteAllFrom = this._panelDoc.getElementById(
     "storage-table-popup-delete-all-from");
   this._tablePopupDeleteAllFrom.addEventListener("command",
@@ -171,20 +178,18 @@ function StorageUI(front, target, panelW
   this._tablePopupDeleteAll = this._panelDoc.getElementById(
     "storage-table-popup-delete-all");
   this._tablePopupDeleteAll.addEventListener("command", this.onRemoveAll);
 
   this._treePopupDeleteAll = this._panelDoc.getElementById(
     "storage-tree-popup-delete-all");
   this._treePopupDeleteAll.addEventListener("command", this.onRemoveAll);
 
-  this._treePopupDeleteDatabase = this._panelDoc.getElementById(
-    "storage-tree-popup-delete-database");
-  this._treePopupDeleteDatabase.addEventListener("command",
-    this.onRemoveDatabase);
+  this._treePopupDelete = this._panelDoc.getElementById("storage-tree-popup-delete");
+  this._treePopupDelete.addEventListener("command", this.onRemoveTreeItem);
 }
 
 exports.StorageUI = StorageUI;
 
 StorageUI.prototype = {
 
   storageTypes: null,
   shouldLoadMoreItems: true,
@@ -200,31 +205,24 @@ StorageUI.prototype = {
     this.table.destroy();
 
     this.front.off("stores-update", this.onUpdate);
     this.front.off("stores-cleared", this.onCleared);
     this._panelDoc.removeEventListener("keypress", this.handleKeypress);
     this.searchBox.removeEventListener("input", this.filterItems);
     this.searchBox = null;
 
-    this._treePopup.removeEventListener("popupshowing",
-      this.onTreePopupShowing);
-    this._treePopupDeleteAll.removeEventListener("command",
-      this.onRemoveAll);
-    this._treePopupDeleteDatabase.removeEventListener("command",
-      this.onRemoveDatabase);
+    this._treePopup.removeEventListener("popupshowing", this.onTreePopupShowing);
+    this._treePopupDeleteAll.removeEventListener("command", this.onRemoveAll);
+    this._treePopupDelete.removeEventListener("command", this.onRemoveTreeItem);
 
-    this._tablePopup.removeEventListener("popupshowing",
-      this.onTablePopupShowing);
-    this._tablePopupDelete.removeEventListener("command",
-      this.onRemoveItem);
-    this._tablePopupDeleteAllFrom.removeEventListener("command",
-      this.onRemoveAllFrom);
-    this._tablePopupDeleteAll.removeEventListener("command",
-      this.onRemoveAll);
+    this._tablePopup.removeEventListener("popupshowing", this.onTablePopupShowing);
+    this._tablePopupDelete.removeEventListener("command", this.onRemoveItem);
+    this._tablePopupDeleteAllFrom.removeEventListener("command", this.onRemoveAllFrom);
+    this._tablePopupDeleteAll.removeEventListener("command", this.onRemoveAll);
   },
 
   /**
    * Empties and hides the object viewer sidebar
    */
   hideSidebar: function () {
     this.view.empty();
     this.sidebar.hidden = true;
@@ -939,34 +937,49 @@ StorageUI.prototype = {
     let showMenu = false;
     let selectedItem = this.tree.selectedItem;
 
     if (selectedItem) {
       let type = selectedItem[0];
       let actor = this.storageTypes[type];
 
       // The delete all (aka clear) action is displayed for IndexedDB object stores
-      // (level 4 of tree) and for the whole host (level 2 of tree) of other storage
-      // types (cookies, localStorage, ...).
-      let showDeleteAll = actor.removeAll &&
-        (selectedItem.length === (type === "indexedDB" ? 4 : 2));
+      // (level 4 of tree), for Cache objects (level 3) and for the whole host (level 2)
+      // for other storage types (cookies, localStorage, ...).
+      let showDeleteAll = false;
+      if (actor.removeAll) {
+        let level;
+        if (type == "indexedDB") {
+          level = 4;
+        } else if (type == "Cache") {
+          level = 3;
+        } else {
+          level = 2;
+        }
+
+        if (selectedItem.length == level) {
+          showDeleteAll = true;
+        }
+      }
 
       this._treePopupDeleteAll.hidden = !showDeleteAll;
 
-      // The action to delete database is available for IndexedDB databases, i.e.,
-      // at level 3 of the tree.
-      let showDeleteDb = actor.removeDatabase && selectedItem.length === 3;
-      this._treePopupDeleteDatabase.hidden = !showDeleteDb;
-      if (showDeleteDb) {
-        let dbName = addEllipsis(selectedItem[2]);
-        this._treePopupDeleteDatabase.setAttribute("label",
-          L10N.getFormatStr("storage.popupMenu.deleteLabel", dbName));
+      // The delete action is displayed for:
+      // - IndexedDB databases (level 3 of the tree)
+      // - Cache objects (level 3 of the tree)
+      let showDelete = (type == "indexedDB" || type == "Cache") &&
+                       selectedItem.length == 3;
+      this._treePopupDelete.hidden = !showDelete;
+      if (showDelete) {
+        let itemName = addEllipsis(selectedItem[selectedItem.length - 1]);
+        this._treePopupDelete.setAttribute("label",
+          L10N.getFormatStr("storage.popupMenu.deleteLabel", itemName));
       }
 
-      showMenu = showDeleteAll || showDeleteDb;
+      showMenu = showDeleteAll || showDelete;
     }
 
     if (!showMenu) {
       event.preventDefault();
     }
   },
 
   /**
@@ -1005,31 +1018,46 @@ StorageUI.prototype = {
     let [, host] = this.tree.selectedItem;
     let actor = this.getCurrentActor();
     let rowId = this.table.contextMenuRowId;
     let data = this.table.items.get(rowId);
 
     actor.removeAll(host, data.host);
   },
 
-  onRemoveDatabase: function () {
-    let [type, host, name] = this.tree.selectedItem;
-    let actor = this.storageTypes[type];
+  onRemoveTreeItem: function () {
+    let [type, host, ...path] = this.tree.selectedItem;
 
-    actor.removeDatabase(host, name).then(result => {
+    if (type == "indexedDB" && path.length == 1) {
+      this.removeDatabase(host, path[0]);
+    } else if (type == "Cache" && path.length == 1) {
+      this.removeCache(host, path[0]);
+    }
+  },
+
+  removeDatabase: function (host, dbName) {
+    let actor = this.storageTypes.indexedDB;
+
+    actor.removeDatabase(host, dbName).then(result => {
       if (result.blocked) {
         let notificationBox = this._toolbox.getNotificationBox();
         notificationBox.appendNotification(
-          L10N.getFormatStr("storage.idb.deleteBlocked", name),
+          L10N.getFormatStr("storage.idb.deleteBlocked", dbName),
           "storage-idb-delete-blocked",
           null,
           notificationBox.PRIORITY_WARNING_LOW);
       }
     }).catch(error => {
       let notificationBox = this._toolbox.getNotificationBox();
       notificationBox.appendNotification(
-        L10N.getFormatStr("storage.idb.deleteError", name),
+        L10N.getFormatStr("storage.idb.deleteError", dbName),
         "storage-idb-delete-error",
         null,
         notificationBox.PRIORITY_CRITICAL_LOW);
     });
-  }
+  },
+
+  removeCache: function (host, cacheName) {
+    let actor = this.storageTypes.Cache;
+
+    actor.removeItem(host, JSON.stringify([ cacheName ]));
+  },
 };
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -1245,16 +1245,71 @@ StorageActors.createActor({
   getSchemaAndHost(url) {
     let uri = Services.io.newURI(url, null, null);
     return uri.scheme + "://" + uri.hostPort;
   },
 
   toStoreObject(item) {
     return item;
   },
+
+  removeItem: Task.async(function* (host, name) {
+    const cacheMap = this.hostVsStores.get(host);
+    if (!cacheMap) {
+      return;
+    }
+
+    const parsedName = JSON.parse(name);
+
+    if (parsedName.length == 1) {
+      // Delete the whole Cache object
+      const [ cacheName ] = parsedName;
+      cacheMap.delete(cacheName);
+      const cacheStorage = yield this.getCachesForHost(host);
+      yield cacheStorage.delete(cacheName);
+      this.onItemUpdated("deleted", host, [ cacheName ]);
+    } else if (parsedName.length == 2) {
+      // Delete one cached request
+      const [ cacheName, url ] = parsedName;
+      const cache = cacheMap.get(cacheName);
+      if (cache) {
+        yield cache.delete(url);
+        this.onItemUpdated("deleted", host, [ cacheName, url ]);
+      }
+    }
+  }),
+
+  removeAll: Task.async(function* (host, name) {
+    const cacheMap = this.hostVsStores.get(host);
+    if (!cacheMap) {
+      return;
+    }
+
+    const parsedName = JSON.parse(name);
+
+    // Only a Cache object is a valid object to clear
+    if (parsedName.length == 1) {
+      const [ cacheName ] = parsedName;
+      const cache = cacheMap.get(cacheName);
+      if (cache) {
+        let keys = yield cache.keys();
+        yield promise.all(keys.map(key => cache.delete(key)));
+        this.onItemUpdated("cleared", host, [ cacheName ]);
+      }
+    }
+  }),
+
+  /**
+   * CacheStorage API doesn't support any notifications, we must fake them
+   */
+  onItemUpdated(action, host, path) {
+    this.storageActor.update(action, "Cache", {
+      [host]: [ JSON.stringify(path) ]
+    });
+  },
 });
 
 /**
  * Code related to the Indexed DB actor and front
  */
 
 // Metadata holder objects for various components of Indexed DB
 
--- a/devtools/shared/specs/storage.js
+++ b/devtools/shared/specs/storage.js
@@ -148,17 +148,33 @@ types.addDictType("cachestoreobject", {
   total: "number",
   offset: "number",
   data: "array:nullable:cacheobject"
 });
 
 // Cache storage spec
 createStorageSpec({
   typeName: "Cache",
-  storeObjectType: "cachestoreobject"
+  storeObjectType: "cachestoreobject",
+  methods: {
+    removeAll: {
+      request: {
+        host: Arg(0, "string"),
+        name: Arg(1, "string"),
+      },
+      response: {}
+    },
+    removeItem: {
+      request: {
+        host: Arg(0, "string"),
+        name: Arg(1, "string"),
+      },
+      response: {}
+    },
+  }
 });
 
 // Indexed DB store object
 // This is a union on idb object, db metadata object and object store metadata
 // object
 types.addDictType("idbobject", {
   name: "nullable:string",
   db: "nullable:string",