Bug 1231437 - Storage Inspector: context menu to remove cookie/storage item r=mratcliffe a=kwierso
☠☠ backed out by b5300f1eae93 ☠ ☠
authorJarda Snajdr <jsnajdr@gmail.com>
Thu, 31 Mar 2016 16:14:15 -0700
changeset 291267 66f61a6565710232618266a9c8099cdb85287701
parent 291266 6ecf26c604a3f6a80e6757b9107a2deb1a689937
child 291268 27104628616c5550fc56b060e727248c5f3a1c2c
push id74545
push userkwierso@gmail.com
push dateFri, 01 Apr 2016 23:05:42 +0000
treeherdermozilla-inbound@c410d4e20586 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmratcliffe, kwierso
bugs1231437
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1231437 - Storage Inspector: context menu to remove cookie/storage item r=mratcliffe a=kwierso MozReview-Commit-ID: ETmjp8jjQ6z
devtools/client/framework/test/shared-head.js
devtools/client/locales/en-US/storage.properties
devtools/client/shared/widgets/TableWidget.js
devtools/client/storage/storage.xul
devtools/client/storage/test/browser.ini
devtools/client/storage/test/browser_storage_delete.js
devtools/client/storage/test/head.js
devtools/client/storage/ui.js
devtools/client/webconsole/test/head.js
devtools/server/actors/storage.js
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -301,8 +301,55 @@ function evalInDebuggee (mm, script) {
       }
 
       info(`Successfully evaled in debuggee: ${script}`);
       mm.removeMessageListener("devtools:test:eval:response", handler);
       resolve(data.value);
     }
   });
 }
+
+/**
+ * Wait for a context menu popup to open.
+ *
+ * @param nsIDOMElement popup
+ *        The XUL popup you expect to open.
+ * @param nsIDOMElement button
+ *        The button/element that receives the contextmenu event. This is
+ *        expected to open the popup.
+ * @param function onShown
+ *        Function to invoke on popupshown event.
+ * @param function onHidden
+ *        Function to invoke on popuphidden event.
+ * @return object
+ *         A Promise object that is resolved after the popuphidden event
+ *         callback is invoked.
+ */
+function waitForContextMenu(popup, button, onShown, onHidden) {
+  let deferred = promise.defer();
+
+  function onPopupShown() {
+    info("onPopupShown");
+    popup.removeEventListener("popupshown", onPopupShown);
+
+    onShown && onShown();
+
+    // Use executeSoon() to get out of the popupshown event.
+    popup.addEventListener("popuphidden", onPopupHidden);
+    executeSoon(() => popup.hidePopup());
+  }
+  function onPopupHidden() {
+    info("onPopupHidden");
+    popup.removeEventListener("popuphidden", onPopupHidden);
+
+    onHidden && onHidden();
+
+    deferred.resolve(popup);
+  }
+
+  popup.addEventListener("popupshown", onPopupShown);
+
+  info("wait for the context menu to open");
+  let eventDetails = {type: "contextmenu", button: 2};
+  EventUtils.synthesizeMouse(button, 2, 2, eventDetails,
+                             button.ownerDocument.defaultView);
+  return deferred.promise;
+}
--- a/devtools/client/locales/en-US/storage.properties
+++ b/devtools/client/locales/en-US/storage.properties
@@ -110,8 +110,12 @@ storage.search.placeholder=Filter values
 
 # LOCALIZATION NOTE (storage.data.label):
 # This is the heading displayed over the item value in the sidebar
 storage.data.label=Data
 
 # LOCALIZATION NOTE (storage.parsedValue.label):
 # This is the heading displayed over the item parsed value in the sidebar
 storage.parsedValue.label=Parsed Value
+
+# LOCALIZATION NOTE (storage.popupMenu.deleteLabel):
+# Label of popup menu action to delete storage item.
+storage.popupMenu.deleteLabel=Delete "%S"
--- a/devtools/client/shared/widgets/TableWidget.js
+++ b/devtools/client/shared/widgets/TableWidget.js
@@ -52,31 +52,34 @@ const MAX_VISIBLE_STRING_SIZE = 100;
  *                          the table. See @setupColumns for more info.
  *        - uniqueId: the column which will be the unique identifier of each
  *                    entry in the table. Default: name.
  *        - emptyText: text to display when no entries in the table to display.
  *        - highlightUpdated: true to highlight the changed/added row.
  *        - removableColumns: Whether columns are removeable. If set to false,
  *                            the context menu in the headers will not appear.
  *        - firstColumn: key of the first column that should appear.
+ *        - cellContextMenuId: ID of a <menupopup> element to be set as a
+ *                             context menu of every cell.
  */
 function TableWidget(node, options = {}) {
   EventEmitter.decorate(this);
 
   this.document = node.ownerDocument;
   this.window = this.document.defaultView;
   this._parent = node;
 
   let {initialColumns, emptyText, uniqueId, highlightUpdated, removableColumns,
-       firstColumn} = options;
+       firstColumn, cellContextMenuId} = options;
   this.emptyText = emptyText || "";
   this.uniqueId = uniqueId || "name";
   this.firstColumn = firstColumn || "";
   this.highlightUpdated = highlightUpdated || false;
   this.removableColumns = removableColumns !== false;
+  this.cellContextMenuId = cellContextMenuId;
 
   this.tbody = this.document.createElementNS(XUL_NS, "hbox");
   this.tbody.className = "table-widget-body theme-body";
   this.tbody.setAttribute("flex", "1");
   this.tbody.setAttribute("tabindex", "0");
   this._parent.appendChild(this.tbody);
   this.afterScroll = this.afterScroll.bind(this);
   this.tbody.addEventListener("scroll", this.onScroll.bind(this));
@@ -801,16 +804,17 @@ TableWidget.prototype = {
     }
     let removed = this.items.delete(item[this.uniqueId]);
 
     if (!removed) {
       return;
     }
     for (let column of this.columns.values()) {
       column.remove(item);
+      column.updateZebra();
     }
     if (this.items.size == 0) {
       this.tbody.setAttribute("empty", "empty");
     }
 
     this.emit(EVENTS.ROW_REMOVED, item);
   },
 
@@ -1443,16 +1447,25 @@ function Cell(column, item, nextCell) {
   this.label.className = "plain table-widget-cell";
 
   if (nextCell) {
     column.column.insertBefore(this.label, nextCell.label);
   } else {
     column.column.appendChild(this.label);
   }
 
+  if (column.table.cellContextMenuId) {
+    this.label.setAttribute("context", column.table.cellContextMenuId);
+    this.label.addEventListener("contextmenu", (event) => {
+      // Make the ID of the clicked cell available as a property on the table.
+      // It's then available for the popupshowing or command handler.
+      column.table.contextMenuRowId = this.id;
+    }, false);
+  }
+
   this.value = item[column.id];
   this.id = item[column.uniqueId];
 }
 
 Cell.prototype = {
 
   set id(value) {
     this._id = value;
--- a/devtools/client/storage/storage.xul
+++ b/devtools/client/storage/storage.xul
@@ -17,16 +17,22 @@
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <script type="application/javascript;version=1.8"
           src="chrome://devtools/content/shared/theme-switching.js"/>
   <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
 
   <commandset id="editMenuCommands"/>
 
+  <popupset id="storagePopupSet">
+    <menupopup id="storage-table-popup">
+      <menuitem id="storage-table-popup-delete"/>
+    </menupopup>
+  </popupset>
+
   <box flex="1" class="devtools-responsive-container theme-body">
     <vbox id="storage-tree"/>
     <splitter class="devtools-side-splitter"/>
     <vbox flex="1">
       <hbox id="storage-toolbar" class="devtools-toolbar">
         <textbox id="storage-searchbox"
                  class="devtools-searchinput"
                  type="search"
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -12,12 +12,13 @@ support-files =
   storage-updates.html
   head.js
 
 [browser_storage_basic.js]
 [browser_storage_dynamic_updates.js]
 [browser_storage_cookies_edit.js]
 [browser_storage_cookies_edit_keyboard.js]
 [browser_storage_cookies_tab_navigation.js]
+[browser_storage_delete.js]
 [browser_storage_overflow.js]
 [browser_storage_search.js]
 [browser_storage_sidebar.js]
 [browser_storage_values.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete.js
@@ -0,0 +1,48 @@
+/* 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 storage items
+
+const TEST_CASES = [
+  [["localStorage", "http://test1.example.org"],
+    "ls1", "name"],
+  [["sessionStorage", "http://test1.example.org"],
+    "ss1", "name"],
+  [["cookies", "test1.example.org"],
+    "c1", "name"]
+];
+
+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");
+
+  for (let [ [store, host], rowName, cellToClick] of TEST_CASES) {
+    info(`Selecting tree item ${store} > ${host}`);
+    yield selectTreeItem([store, host]);
+
+    let row = getRowCells(rowName);
+
+    ok(gUI.table.items.has(rowName),
+      `There is a row '${rowName}' in ${store} > ${host}`);
+
+    yield waitForContextMenu(contextMenu, row[cellToClick], () => {
+      info(`Opened context menu in ${store} > ${host}, row '${rowName}'`);
+      menuDeleteItem.click();
+      ok(menuDeleteItem.getAttribute("label").includes(rowName),
+        `Context menu item label contains '${rowName}'`);
+    });
+
+    yield gUI.once("store-objects-updated");
+
+    ok(!gUI.table.items.has(rowName),
+      `There is no row '${rowName}' in ${store} > ${host} after deletion`);
+  }
+
+  yield finishTests();
+});
--- a/devtools/client/storage/test/head.js
+++ b/devtools/client/storage/test/head.js
@@ -1,35 +1,32 @@
 /* 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";
 
 /* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../framework/test/shared-head.js */
 
-var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
-var { TargetFactory } = require("devtools/client/framework/target");
-var promise = require("promise");
-var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
 
 const {TableWidget} = require("devtools/client/shared/widgets/TableWidget");
 const SPLIT_CONSOLE_PREF = "devtools.toolbox.splitconsoleEnabled";
 const STORAGE_PREF = "devtools.storage.enabled";
 const DUMPEMIT_PREF = "devtools.dump.emit";
 const DEBUGGERLOG_PREF = "devtools.debugger.log";
 // Allows Cache API to be working on usage `http` test page
 const CACHES_ON_HTTP_PREF = "dom.caches.testing.enabled";
 const PATH = "browser/devtools/client/storage/test/";
 const MAIN_DOMAIN = "http://test1.example.org/" + PATH;
 const ALT_DOMAIN = "http://sectest1.example.org/" + PATH;
 const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH;
 
-waitForExplicitFinish();
-
 var gToolbox, gPanelWindow, gWindow, gUI;
 
 // Services.prefs.setBoolPref(DUMPEMIT_PREF, true);
 // Services.prefs.setBoolPref(DEBUGGERLOG_PREF, true);
 
 Services.prefs.setBoolPref(STORAGE_PREF, true);
 Services.prefs.setBoolPref(CACHES_ON_HTTP_PREF, true);
 DevToolsUtils.testing = true;
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -42,16 +42,20 @@ const HIDDEN_COLUMNS = [
 
 const REASON = {
   NEW_ROW: "new-row",
   NEXT_50_ITEMS: "next-50-items",
   POPULATE: "populate",
   UPDATE: "update"
 };
 
+// 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
  *        Front for the storage actor
  * @param {Target} target
  *        Interface for the page we're debugging
  * @param {Window} panelWin
@@ -69,16 +73,17 @@ var StorageUI = this.StorageUI = functio
   this.tree = new TreeWidget(treeNode, {defaultType: "dir"});
   this.onHostSelect = this.onHostSelect.bind(this);
   this.tree.on("select", this.onHostSelect);
 
   let tableNode = this._panelDoc.getElementById("storage-table");
   this.table = new TableWidget(tableNode, {
     emptyText: L10N.getStr("table.emptyText"),
     highlightUpdated: true,
+    cellContextMenuId: "storage-table-popup"
   });
 
   this.displayObjectSidebar = this.displayObjectSidebar.bind(this);
   this.table.on(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar);
 
   this.handleScrollEnd = this.handleScrollEnd.bind(this);
   this.table.on(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd);
 
@@ -100,16 +105,25 @@ var StorageUI = this.StorageUI = functio
 
   this.onUpdate = this.onUpdate.bind(this);
   this.front.on("stores-update", this.onUpdate);
   this.onCleared = this.onCleared.bind(this);
   this.front.on("stores-cleared", this.onCleared);
 
   this.handleKeypress = this.handleKeypress.bind(this);
   this._panelDoc.addEventListener("keypress", this.handleKeypress);
+
+  this.onPopupShowing = this.onPopupShowing.bind(this);
+  this._tablePopup = this._panelDoc.getElementById("storage-table-popup");
+  this._tablePopup.addEventListener("popupshowing", this.onPopupShowing, false);
+
+  this.onRemoveItem = this.onRemoveItem.bind(this);
+  this._tablePopupDelete = this._panelDoc.getElementById(
+    "storage-table-popup-delete");
+  this._tablePopupDelete.addEventListener("command", this.onRemoveItem, false);
 };
 
 exports.StorageUI = StorageUI;
 
 StorageUI.prototype = {
 
   storageTypes: null,
   shouldResetColumns: true,
@@ -125,16 +139,19 @@ StorageUI.prototype = {
     this.table.off(TableWidget.EVENTS.CELL_EDIT, this.editItem);
     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._tablePopup.removeEventListener("popupshowing", this.onPopupShowing);
+    this._tablePopupDelete.removeEventListener("command", this.onRemoveItem);
   },
 
   /**
    * Empties and hides the object viewer sidebar
    */
   hideSidebar: function() {
     this.view.empty();
     this.sidebar.hidden = true;
@@ -725,10 +742,46 @@ StorageUI.prototype = {
 
     let item = this.tree.selectedItem;
     let [type, host] = item;
     let names = null;
     if (item.length > 2) {
       names = [JSON.stringify(item.slice(2))];
     }
     this.fetchStorageObjects(type, host, names, REASON.NEXT_50_ITEMS);
-  }
+  },
+
+  /**
+   * Fires before a cell context menu with the "Delete" action is shown.
+   * If the current storage actor doesn't support removing items, prevent
+   * showing the menu.
+   */
+  onPopupShowing: function(event) {
+    if (!this.getCurrentActor().removeItem) {
+      event.preventDefault();
+      return;
+    }
+
+    let rowId = this.table.contextMenuRowId;
+    let data = this.table.items.get(rowId);
+    let name = data[this.table.uniqueId];
+
+    const maxLen = ITEM_NAME_MAX_LENGTH;
+    if (name.length > maxLen) {
+      name = name.substr(0, maxLen) + L10N.ellipsis;
+    }
+
+    this._tablePopupDelete.setAttribute("label",
+      L10N.getFormatStr("storage.popupMenu.deleteLabel", name));
+  },
+
+  /**
+   * Handles removing an item from the storage
+   */
+  onRemoveItem: function() {
+    let [, host] = this.tree.selectedItem;
+    let actor = this.getCurrentActor();
+    let rowId = this.table.contextMenuRowId;
+    let data = this.table.items.get(rowId);
+
+    actor.removeItem(host, data[this.table.uniqueId]);
+  },
 };
--- a/devtools/client/webconsole/test/head.js
+++ b/devtools/client/webconsole/test/head.js
@@ -237,63 +237,16 @@ var closeConsole = Task.async(function* 
   let target = TargetFactory.forTab(tab || gBrowser.selectedTab);
   let toolbox = gDevTools.getToolbox(target);
   if (toolbox) {
     yield toolbox.destroy();
   }
 });
 
 /**
- * Wait for a context menu popup to open.
- *
- * @param nsIDOMElement popup
- *        The XUL popup you expect to open.
- * @param nsIDOMElement button
- *        The button/element that receives the contextmenu event. This is
- *        expected to open the popup.
- * @param function onShown
- *        Function to invoke on popupshown event.
- * @param function onHidden
- *        Function to invoke on popuphidden event.
- * @return object
- *         A Promise object that is resolved after the popuphidden event
- *         callback is invoked.
- */
-function waitForContextMenu(popup, button, onShown, onHidden) {
-  let deferred = promise.defer();
-
-  function onPopupShown() {
-    info("onPopupShown");
-    popup.removeEventListener("popupshown", onPopupShown);
-
-    onShown && onShown();
-
-    // Use executeSoon() to get out of the popupshown event.
-    popup.addEventListener("popuphidden", onPopupHidden);
-    executeSoon(() => popup.hidePopup());
-  }
-  function onPopupHidden() {
-    info("onPopupHidden");
-    popup.removeEventListener("popuphidden", onPopupHidden);
-
-    onHidden && onHidden();
-
-    deferred.resolve(popup);
-  }
-
-  popup.addEventListener("popupshown", onPopupShown);
-
-  info("wait for the context menu to open");
-  let eventDetails = {type: "contextmenu", button: 2};
-  EventUtils.synthesizeMouse(button, 2, 2, eventDetails,
-                             button.ownerDocument.defaultView);
-  return deferred.promise;
-}
-
-/**
  * Listen for a new tab to open and return a promise that resolves when one
  * does and completes the load event.
  * @return a promise that resolves to the tab object
  */
 var waitForTab = Task.async(function*() {
   info("Waiting for a tab to open");
   yield once(gBrowser.tabContainer, "TabOpen");
   let tab = gBrowser.selectedTab;
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -421,17 +421,17 @@ StorageActors.defaults = function(typeNa
  *         - typeName {string}
  *                    The typeName of the actor.
  *         - observationTopic {string}
  *                            The topic which this actor listens to via
  *                            Notification Observers.
  *         - storeObjectType {string}
  *                           The RetVal type of the store object of this actor.
  * @param {object} overrides
- *        All the methods which you want to be differnt from the ones in
+ *        All the methods which you want to be different from the ones in
  *        StorageActors.defaults method plus the required ones described there.
  */
 StorageActors.createActor = function(options = {}, overrides = {}) {
   let actorObject = StorageActors.defaults(
     options.typeName,
     options.observationTopic || null,
     options.storeObjectType
   );
@@ -669,24 +669,35 @@ StorageActors.createActor({
     this.editCookie(data);
   }), {
     request: {
       data: Arg(0, "json"),
     },
     response: {}
   }),
 
+  removeItem: method(Task.async(function*(host, name) {
+    this.removeCookie(host, name);
+  }), {
+    request: {
+      host: Arg(0),
+      name: Arg(1),
+    },
+    response: {}
+  }),
+
   maybeSetupChildProcess: function() {
     cookieHelpers.onCookieChanged = this.onCookieChanged.bind(this);
 
     if (!DebuggerServer.isInChildProcess) {
       this.getCookiesFromHost = cookieHelpers.getCookiesFromHost;
       this.addCookieObservers = cookieHelpers.addCookieObservers;
       this.removeCookieObservers = cookieHelpers.removeCookieObservers;
       this.editCookie = cookieHelpers.editCookie;
+      this.removeCookie = cookieHelpers.removeCookie;
       return;
     }
 
     const { sendSyncMessage, addMessageListener } =
       this.conn.parentMessageManager;
 
     this.conn.setupInParent({
       module: "devtools/server/actors/storage",
@@ -696,16 +707,18 @@ StorageActors.createActor({
     this.getCookiesFromHost =
       callParentProcess.bind(null, "getCookiesFromHost");
     this.addCookieObservers =
       callParentProcess.bind(null, "addCookieObservers");
     this.removeCookieObservers =
       callParentProcess.bind(null, "removeCookieObservers");
     this.editCookie =
       callParentProcess.bind(null, "editCookie");
+    this.removeCookie =
+      callParentProcess.bind(null, "removeCookie");
 
     addMessageListener("storage:storage-cookie-request-child",
                        cookieHelpers.handleParentRequest);
 
     function callParentProcess(methodName, ...args) {
       let reply = sendSyncMessage("storage:storage-cookie-request-parent", {
         method: methodName,
         args: args
@@ -848,16 +861,42 @@ var cookieHelpers = {
       cookie.value,
       cookie.isSecure,
       cookie.isHttpOnly,
       cookie.isSession,
       cookie.isSession ? MAX_COOKIE_EXPIRY : cookie.expires
     );
   },
 
+  removeCookie: function(host, name) {
+    function hostMatches(cookieHost, matchHost) {
+      if (cookieHost == null) {
+        return matchHost == null;
+      }
+      if (cookieHost.startsWith(".")) {
+        return matchHost.endsWith(cookieHost);
+      }
+      return cookieHost == host;
+    }
+
+    let enumerator = Services.cookies.getCookiesFromHost(host);
+    while (enumerator.hasMoreElements()) {
+      let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
+      if (hostMatches(cookie.host, host) && cookie.name === name) {
+        Services.cookies.remove(
+          cookie.host,
+          cookie.name,
+          cookie.path,
+          cookie.originAttributes,
+          false
+        );
+      }
+    }
+  },
+
   addCookieObservers: function() {
     Services.obs.addObserver(cookieHelpers, "cookie-changed", false);
     return null;
   },
 
   removeCookieObservers: function() {
     Services.obs.removeObserver(cookieHelpers, "cookie-changed", false);
     return null;
@@ -896,27 +935,36 @@ var cookieHelpers = {
         cookie = JSON.parse(cookie);
         cookieHelpers.onCookieChanged(cookie, topic, data);
         break;
     }
   },
 
   handleChildRequest: function(msg) {
     switch (msg.json.method) {
-      case "getCookiesFromHost":
+      case "getCookiesFromHost": {
         let host = msg.data.args[0];
         let cookies = cookieHelpers.getCookiesFromHost(host);
         return JSON.stringify(cookies);
-      case "addCookieObservers":
+      }
+      case "addCookieObservers": {
         return cookieHelpers.addCookieObservers();
-      case "removeCookieObservers":
+      }
+      case "removeCookieObservers": {
         return cookieHelpers.removeCookieObservers();
-      case "editCookie":
+      }
+      case "editCookie": {
         let rowdata = msg.data.args[0];
         return cookieHelpers.editCookie(rowdata);
+      }
+      case "removeCookie": {
+        let host = msg.data.args[0];
+        let name = msg.data.args[1];
+        return cookieHelpers.removeCookie(host, name);
+      }
       default:
         console.error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD", msg.json.method);
         throw new Error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD");
     }
   },
 };
 
 /**
@@ -1068,16 +1116,27 @@ function getObjectForLocalOrSessionStora
         return null;
       }
 
       return {
         name: item.name,
         value: new LongStringActor(this.conn, item.value || "")
       };
     },
+
+    removeItem: method(Task.async(function*(host, name) {
+      let storage = this.hostVsStores.get(host);
+      storage.removeItem(name);
+    }), {
+      request: {
+        host: Arg(0),
+        name: Arg(1),
+      },
+      response: {}
+    }),
   };
 }
 
 /**
  * The Local Storage actor and front.
  */
 StorageActors.createActor({
   typeName: "localStorage",