Bug 1231154 - Make cookies table fields editable via double-click in storage inspector r+pbro
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Tue, 01 Mar 2016 23:36:39 +0000
changeset 343878 1f40e55de92a0e9398a4cde225b1bc27be51ef85
parent 343877 881863474a3493f5b74fa7bd62f1a8a7e71588d8
child 343989 189d3e93d615c6d79cb12fb5a05bdd5139a14e52
push id13696
push usermratcliffe@mozilla.com
push dateWed, 23 Mar 2016 12:12:52 +0000
bugs1231154
milestone48.0a1
Bug 1231154 - Make cookies table fields editable via double-click in storage inspector r+pbro MozReview-Commit-ID: 9RthfEhCev1
devtools/client/storage/test/browser.ini
devtools/client/storage/test/browser_storage_cookies_edit.js
devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js
devtools/client/storage/test/browser_storage_cookies_tab_navigation.js
devtools/client/storage/test/head.js
devtools/client/storage/test/storage-complex-values.html
devtools/client/storage/test/storage-cookies.html
devtools/client/storage/test/storage-listings.html
devtools/client/storage/test/storage-overflow.html
devtools/client/storage/test/storage-search.html
devtools/client/storage/test/storage-secured-iframe.html
devtools/client/storage/test/storage-unsecured-iframe.html
devtools/client/storage/test/storage-updates.html
devtools/server/actors/storage.js
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -1,19 +1,23 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
+  storage-cookies.html
   storage-complex-values.html
   storage-listings.html
   storage-overflow.html
   storage-search.html
   storage-secured-iframe.html
   storage-unsecured-iframe.html
   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_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_cookies_edit.js
@@ -0,0 +1,24 @@
+/* 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/. */
+
+// Basic test to check the editing of cookies.
+
+"use strict";
+
+add_task(function*() {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
+  yield gUI.table.once(TableWidget.EVENTS.FIELDS_EDITABLE);
+
+  showAllColumns(true);
+
+  yield editCell("test3", "name", "newTest3");
+  yield editCell("newTest3", "path", "/");
+  yield editCell("newTest3", "host", "test1.example.org");
+  yield editCell("newTest3", "expires", "Tue, 14 Feb 2040 17:41:14 GMT");
+  yield editCell("newTest3", "value", "newValue3");
+  yield editCell("newTest3", "isSecure", "true");
+  yield editCell("newTest3", "isHttpOnly", "true");
+
+  yield finishTests();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js
@@ -0,0 +1,25 @@
+/* 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/. */
+
+// Basic test to check the editing of cookies with the keyboard.
+
+"use strict";
+
+add_task(function*() {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
+  yield gUI.table.once(TableWidget.EVENTS.FIELDS_EDITABLE);
+
+  showAllColumns(true);
+
+  yield startCellEdit("test4", "name");
+  yield typeWithTerminator("test6", "VK_TAB");
+  yield typeWithTerminator("/", "VK_TAB");
+  yield typeWithTerminator(".example.org", "VK_TAB");
+  yield typeWithTerminator("Tue, 25 Dec 2040 12:00:00 GMT", "VK_TAB");
+  yield typeWithTerminator("test6value", "VK_TAB");
+  yield typeWithTerminator("false", "VK_TAB");
+  yield typeWithTerminator("false", "VK_TAB");
+
+  yield finishTests();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js
@@ -0,0 +1,26 @@
+/* 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/. */
+
+// Basic test to check cookie table tab navigation.
+
+"use strict";
+
+add_task(function*() {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
+  yield gUI.table.once(TableWidget.EVENTS.FIELDS_EDITABLE);
+
+  showAllColumns(true);
+
+  yield startCellEdit("test1", "name");
+
+  PressKeyXTimes("VK_TAB", 18);
+  is(getCurrentEditorValue(), "value3",
+     "We have tabbed to the correct cell.");
+
+  PressKeyXTimes("VK_TAB", 18, {shiftKey: true});
+  is(getCurrentEditorValue(), "test1",
+     "We have shift-tabbed to the correct cell.");
+
+  yield finishTests();
+});
--- a/devtools/client/storage/test/head.js
+++ b/devtools/client/storage/test/head.js
@@ -6,16 +6,17 @@
 
 /* eslint no-unused-vars: [2, {"vars": "local"}] */
 
 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");
 
+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;
@@ -244,22 +245,26 @@ function* finishTests() {
       };
       _getAllWindows(baseWindow);
 
       return windows;
     }
 
     let windows = getAllWindows(content);
     for (let win of windows) {
+      win.localStorage.clear();
+      win.sessionStorage.clear();
+
       if (win.clear) {
         yield win.clear();
       }
     }
   });
 
+  Services.cookies.removeAll();
   forceCollections();
   finish();
 }
 
 // Sends a click event on the passed DOM node in an async manner
 function* click(node) {
   let def = promise.defer();
 
@@ -557,8 +562,244 @@ function once(target, eventName, useCapt
         deferred.resolve.apply(deferred, aArgs);
       }, useCapture);
       break;
     }
   }
 
   return deferred.promise;
 }
+
+/**
+ * Get values for a row.
+ *
+ * @param  {String}  id
+ *         The uniqueId of the given row.
+ * @param  {Boolean} includeHidden
+ *         Include hidden columns.
+ *
+ * @return {Object}
+ *         An object of column names to values for the given row.
+ */
+function getRowValues(id, includeHidden = false) {
+  let cells = getRowCells(id, includeHidden);
+  let values = {};
+
+  for (let name in cells) {
+    let cell = cells[name];
+
+    values[name] = cell.value;
+  }
+
+  return values;
+}
+
+/**
+ * Get cells for a row.
+ *
+ * @param  {String}  id
+ *         The uniqueId of the given row.
+ * @param  {Boolean} includeHidden
+ *         Include hidden columns.
+ *
+ * @return {Object}
+ *         An object of column names to cells for the given row.
+ */
+function getRowCells(id, includeHidden = false) {
+  let doc = gPanelWindow.document;
+  let table = gUI.table;
+  let item = doc.querySelector(".table-widget-column#" + table.uniqueId +
+                               " .table-widget-cell[value='" + id + "']");
+
+  if (!item) {
+    ok(false, "Row id '" + id + "' exists");
+  }
+
+  let index = table.columns.get(table.uniqueId).visibleCellNodes.indexOf(item);
+  let cells = {};
+
+  for (let [name, column] of [...table.columns]) {
+    if (!includeHidden && column.column.parentNode.hidden) {
+      continue;
+    }
+    cells[name] = column.visibleCellNodes[index];
+  }
+
+  return cells;
+}
+
+/**
+ * Get a cell value.
+ *
+ * @param {String} id
+ *        The uniqueId of the row.
+ * @param {String} column
+ *        The id of the column
+ *
+ * @yield {String}
+ *        The cell value.
+ */
+function getCellValue(id, column) {
+  let row = getRowValues(id, true);
+
+  return row[column];
+}
+
+/**
+ * Edit a cell value. The cell is assumed to be in edit mode, see startCellEdit.
+ *
+ * @param {String} id
+ *        The uniqueId of the row.
+ * @param {String} column
+ *        The id of the column
+ * @param {String} newValue
+ *        Replacement value.
+ * @param {Boolean} validate
+ *        Validate result? Default true.
+ *
+ * @yield {String}
+ *        The uniqueId of the changed row.
+ */
+function* editCell(id, column, newValue, validate = true) {
+  let row = getRowCells(id, true);
+  let editableFieldsEngine = gUI.table._editableFieldsEngine;
+
+  editableFieldsEngine.edit(row[column]);
+
+  return yield typeWithTerminator(newValue, "VK_RETURN", validate);
+}
+
+/**
+ * Begin edit mode for a cell.
+ *
+ * @param {String} id
+ *        The uniqueId of the row.
+ * @param {String} column
+ *        The id of the column
+ * @param {Boolean} selectText
+ *        Select text? Default true.
+ */
+function* startCellEdit(id, column, selectText = true) {
+  let row = getRowCells(id, true);
+  let editableFieldsEngine = gUI.table._editableFieldsEngine;
+  let cell = row[column];
+
+  info("Selecting row " + id);
+  gUI.table.selectedRow = id;
+
+  info("Starting cell edit (" + id + ", " + column + ")");
+  editableFieldsEngine.edit(cell);
+
+  if (!selectText) {
+    let textbox = gUI.table._editableFieldsEngine.textbox;
+    textbox.selectionEnd = textbox.selectionStart;
+  }
+}
+
+/**
+ * Check a cell value.
+ *
+ * @param {String} id
+ *        The uniqueId of the row.
+ * @param {String} column
+ *        The id of the column
+ * @param {String} expected
+ *        Expected value.
+ */
+function checkCell(id, column, expected) {
+  is(getCellValue(id, column), expected,
+     column + " column has the right value for " + id);
+}
+
+/**
+ * Show or hide a column.
+ *
+ * @param  {String} id
+ *         The uniqueId of the given column.
+ * @param  {Boolean} state
+ *         true = show, false = hide
+ */
+function showColumn(id, state) {
+  let columns = gUI.table.columns;
+  let column = columns.get(id);
+
+  if (state) {
+    column.wrapper.removeAttribute("hidden");
+  } else {
+    column.wrapper.setAttribute("hidden", true);
+  }
+}
+
+/**
+ * Show or hide all columns.
+ *
+ * @param  {Boolean} state
+ *         true = show, false = hide
+ */
+function showAllColumns(state) {
+  let columns = gUI.table.columns;
+
+  for (let [id] of columns) {
+    showColumn(id, state);
+  }
+}
+
+/**
+ * Type a string in the currently selected editor and then wait for the row to
+ * be updated.
+ *
+ * @param  {String} str
+ *         The string to type.
+ * @param  {String} terminator
+ *         The terminating key e.g. VK_RETURN or VK_TAB
+ * @param  {Boolean} validate
+ *         Validate result? Default true.
+ */
+function* typeWithTerminator(str, terminator, validate = true) {
+  let editableFieldsEngine = gUI.table._editableFieldsEngine;
+  let textbox = editableFieldsEngine.textbox;
+  let colName = textbox.closest(".table-widget-column").id;
+
+  let changeExpected = str !== textbox.value;
+
+  if (!changeExpected) {
+    return editableFieldsEngine.currentTarget.getAttribute("data-id");
+  }
+
+  info("Typing " + str);
+  EventUtils.sendString(str);
+
+  info("Pressing " + terminator);
+  EventUtils.synthesizeKey(terminator, {});
+
+  if (validate) {
+    info("Validating results... waiting for ROW_EDIT event.");
+    let uniqueId = yield gUI.table.once(TableWidget.EVENTS.ROW_EDIT);
+
+    checkCell(uniqueId, colName, str);
+    return uniqueId;
+  }
+
+  return yield gUI.table.once(TableWidget.EVENTS.ROW_EDIT);
+}
+
+function getCurrentEditorValue() {
+  let editableFieldsEngine = gUI.table._editableFieldsEngine;
+  let textbox = editableFieldsEngine.textbox;
+
+  return textbox.value;
+}
+
+/**
+ * Press a key x times.
+ *
+ * @param  {String} key
+ *         The key to press e.g. VK_RETURN or VK_TAB
+ * @param {Number} x
+ *         The number of times to press the key.
+ * @param {Object} modifiers
+ *         The event modifier e.g. {shiftKey: true}
+ */
+function PressKeyXTimes(key, x, modifiers = {}) {
+  for (let i = 0; i < x; i++) {
+    EventUtils.synthesizeKey(key, modifiers);
+  }
+}
--- a/devtools/client/storage/test/storage-complex-values.html
+++ b/devtools/client/storage/test/storage-complex-values.html
@@ -100,23 +100,16 @@ function deleteDB(dbName) {
   });
 }
 
 window.setup = function*() {
   yield idbGenerator();
 };
 
 window.clear = function*() {
-  document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/browser";
-  document.cookie = "cs2=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
-
-  localStorage.clear();
-  sessionStorage.clear();
-
   yield deleteDB("idb1");
   yield deleteDB("idb2");
 
-  dump("removed cookies, localStorage, sessionStorage and indexedDB data " +
-       "from " + document.location + "\n");
+  dump("removed indexedDB data from " + document.location + "\n");
 };
 </script>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/storage-cookies.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+  <!--
+  Bug 970517 - Storage inspector front end - tests
+  -->
+  <head>
+    <meta charset="utf-8">
+    <title>Storage inspector cookie test</title>
+  </head>
+  <body>
+    <script type="application/javascript;version=1.7">
+    "use strict";
+    let partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
+    let expiresIn24Hours = new Date(Date.now() + 60 * 60 * 24 * 1000).toUTCString();
+    for (let i = 1; i <= 5; i++) {
+    let cookieString = "test" + i + "=value" + i +
+    ";expires=" + expiresIn24Hours + ";path=/browser";
+    if (i % 2) {
+    cookieString += ";domain=.example.org";
+    }
+    document.cookie = cookieString;
+    }
+    </script>
+  </body>
+</html>
--- a/devtools/client/storage/test/storage-listings.html
+++ b/devtools/client/storage/test/storage-listings.html
@@ -109,29 +109,18 @@ let cacheGenerator = function*() {
 };
 
 window.setup = function*() {
   yield idbGenerator();
   yield cacheGenerator();
 };
 
 window.clear = function*() {
-  document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/browser";
-  document.cookie =
-    "c3=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure=true";
-  document.cookie =
-    "cs2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=" +
-    partialHostname;
-
-  localStorage.clear();
-  sessionStorage.clear();
-
   yield deleteDB("idb1");
   yield deleteDB("idb2");
 
   yield caches.delete("plop");
 
-  dump("removed cookies, localStorage, sessionStorage and indexedDB data " +
-       "from " + document.location + "\n");
+  dump("removed indexedDB and cache data from " + document.location + "\n");
 };
 </script>
 </body>
 </html>
--- a/devtools/client/storage/test/storage-overflow.html
+++ b/devtools/client/storage/test/storage-overflow.html
@@ -5,18 +5,15 @@ Bug 1171903 - Storage Inspector endless 
 -->
 <head>
   <meta charset="utf-8">
   <title>Storage inspector endless scrolling test</title>
 </head>
 <body>
 <script type="text/javascript;version=1.8">
 "use strict";
-window.clear = () => {
-  localStorage.clear();
-};
 
 for (let i = 0; i < 160; i++) {
   localStorage.setItem(`item-${i}`, `value-${i}`);
 }
 </script>
 </body>
 </html>
--- a/devtools/client/storage/test/storage-search.html
+++ b/devtools/client/storage/test/storage-search.html
@@ -5,19 +5,16 @@ Bug 1224115 - Storage Inspector table fi
 -->
 <head>
   <meta charset="utf-8">
   <title>Storage inspector table filtering test</title>
 </head>
 <body>
 <script type="text/javascript;version=1.8">
 "use strict";
-window.clear = () => {
-  localStorage.clear();
-};
 
 localStorage.setItem("01234", "56789");
 localStorage.setItem("ANIMAL", "hOrSe");
 localStorage.setItem("FOO", "bArBaz");
 localStorage.setItem("food", "energy bar");
 localStorage.setItem("money", "##$$$**");
 localStorage.setItem("sport", "football");
 localStorage.setItem("year", "2016");
--- a/devtools/client/storage/test/storage-secured-iframe.html
+++ b/devtools/client/storage/test/storage-secured-iframe.html
@@ -75,22 +75,16 @@ function deleteDB(dbName) {
   });
 }
 
 window.setup = function*() {
   yield idbGenerator();
 };
 
 window.clear = function*() {
-  document.cookie = "sc1=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
-
-  localStorage.clear();
-  sessionStorage.clear();
-
   yield deleteDB("idb-s1");
   yield deleteDB("idb-s2");
 
-  dump("removed cookies, localStorage, sessionStorage and indexedDB data " +
-       "from " + document.location + "\n");
+  dump("removed indexedDB data from " + document.location + "\n");
 };
 </script>
 </body>
 </html>
--- a/devtools/client/storage/test/storage-unsecured-iframe.html
+++ b/devtools/client/storage/test/storage-unsecured-iframe.html
@@ -9,22 +9,11 @@ Iframe for testing multiple host detetio
 <body>
 <script>
 "use strict";
 document.cookie = "uc1=foobar; domain=.example.org; path=/; secure=true";
 localStorage.setItem("iframe-u-ls1", "foobar");
 sessionStorage.setItem("iframe-u-ss1", "foobar1");
 sessionStorage.setItem("iframe-u-ss2", "foobar2");
 console.log("added cookies and stuff from unsecured iframe");
-
-window.clear = function*() {
-  document.cookie = "uc1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; " +
-                    "domain=.example.org; secure=true";
-
-  localStorage.clear();
-  sessionStorage.clear();
-
-  dump("removed cookies, localStorage and sessionStorage from " +
-       document.location + "\n");
-};
 </script>
 </body>
 </html>
--- a/devtools/client/storage/test/storage-updates.html
+++ b/devtools/client/storage/test/storage-updates.html
@@ -27,28 +27,25 @@ window.addCookie = function(name, value,
   document.cookie = cookieString;
 };
 
 window.removeCookie = function(name, path) {
   document.cookie =
     name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=" + path;
 };
 
+/**
+ * We keep this method here even though these items are automatically cleared
+ * after the test is complete. this is so that the store-objects-cleared event
+ * can be tested.
+ */
 window.clear = function*() {
-  let cookies = document.cookie;
-  for (let cookie of cookies.split(";")) {
-    removeCookie(cookie.split("=")[0]);
-    removeCookie(cookie.split("=")[0], "/browser");
-  }
-
-  localStorage.clear();
   sessionStorage.clear();
 
-  dump("removed cookies, localStorage and sessionStorage from " +
-       document.location + "\n");
+  dump("removed sessionStorage from " + document.location + "\n");
 };
 
 window.onload = function() {
   addCookie("c1", "1.2.3.4.5.6.7", "/browser");
   addCookie("c2", "foobar", "/browser");
 
   localStorage.setItem("ls1", "testing");
   localStorage.setItem("ls2", "testing");
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -23,16 +23,20 @@ var gTrackedMessageManager = new Map();
 
 // Maximum number of cookies/local storage key-value-pairs that can be sent
 // over the wire to the client in one request.
 const MAX_STORE_OBJECT_COUNT = 50;
 // Delay for the batch job that sends the accumulated update packets to the
 // client (ms).
 const BATCH_DELAY = 200;
 
+// MAX_COOKIE_EXPIRY should be 2^63-1, but JavaScript can't handle that
+// precision.
+const MAX_COOKIE_EXPIRY = Math.pow(2, 62);
+
 // A RegExp for characters that cannot appear in a file/directory name. This is
 // used to sanitize the host name for indexed db to lookup whether the file is
 // present in <profileDir>/storage/default/ location
 var illegalFileNameCharacters = [
   "[",
   // Control characters \001 to \036
   "\\x00-\\x24",
   // Special characters
@@ -561,19 +565,19 @@ StorageActors.createActor({
       }
     }
   },
 
   /**
    * Notification observer for "cookie-change".
    *
    * @param subject
-   *        {nsiCookie|[nsiCookie]} A single nsiCookie object or a list of it
-   *        depending on the action. Array is only in case of "batch-deleted"
-   *        action.
+   *        {Cookie|[Array]} A JSON parsed object containing either a single
+   *        cookie representation or an array. Array is only in case of
+   *        a "batch-deleted" action.
    * @param {string} topic
    *        The topic of the notification.
    * @param {string} action
    *        Additional data associated with the notification. Its the type of
    *        cookie change in the "cookie-change" topic.
    */
   onCookieChanged: function(subject, topic, action) {
     if (topic !== "cookie-changed" ||
@@ -627,23 +631,62 @@ StorageActors.createActor({
 
       case "reload":
         this.storageActor.update("reloaded", "cookies", hosts);
         break;
     }
     return null;
   },
 
+  /**
+   * This method marks the table as editable.
+   *
+   * @return {Array}
+   *         An array of column header ids.
+   */
+  getEditableFields: method(Task.async(function*() {
+    return [
+      "name",
+      "path",
+      "host",
+      "expires",
+      "value",
+      "isSecure",
+      "isHttpOnly"
+    ];
+  }), {
+    request: {},
+    response: {
+      value: RetVal("json")
+    }
+  }),
+
+  /**
+   * Pass the editItem command from the content to the chrome process.
+   *
+   * @param {Object} data
+   *        See editCookie() for format details.
+   */
+  editItem: method(Task.async(function*(data) {
+    this.editCookie(data);
+  }), {
+    request: {
+      data: Arg(0, "json"),
+    },
+    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;
       return;
     }
 
     const { sendSyncMessage, addMessageListener } =
       this.conn.parentMessageManager;
 
     this.conn.setupInParent({
       module: "devtools/server/actors/storage",
@@ -651,16 +694,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");
 
     addMessageListener("storage:storage-cookie-request-child",
                        cookieHelpers.handleParentRequest);
 
     function callParentProcess(methodName, ...args) {
       let reply = sendSyncMessage("storage:storage-cookie-request-parent", {
         method: methodName,
         args: args
@@ -690,35 +735,159 @@ var cookieHelpers = {
       host = "";
     }
 
     let cookies = Services.cookies.getCookiesFromHost(host);
     let store = [];
 
     while (cookies.hasMoreElements()) {
       let cookie = cookies.getNext().QueryInterface(Ci.nsICookie2);
+
       store.push(cookie);
     }
 
     return store;
   },
 
+  /**
+   * Apply the results of a cookie edit.
+   *
+   * @param {Object} data
+   *        An object in the following format:
+   *        {
+   *          field: "value",
+   *          key: "name",
+   *          oldValue: "%7BHello%7D",
+   *          newValue: "%7BHelloo%7D",
+   *          items: {
+   *            name: "optimizelyBuckets",
+   *            path: "/",
+   *            host: ".mozilla.org",
+   *            expires: "Mon, 02 Jun 2025 12:37:37 GMT",
+   *            creationTime: "Tue, 18 Nov 2014 16:21:18 GMT",
+   *            lastAccessed: "Wed, 17 Feb 2016 10:06:23 GMT",
+   *            value: "%7BHelloo%7D",
+   *            isDomain: "true",
+   *            isSecure: "false",
+   *            isHttpOnly: "false"
+   *          }
+   *        }
+   */
+  editCookie: function(data) {
+    let {field, oldValue, newValue} = data;
+    let origName = field === "name" ? oldValue : data.items.name;
+    let origHost = field === "host" ? oldValue : data.items.host;
+    let origPath = field === "path" ? oldValue : data.items.path;
+    let cookie = null;
+
+    let enumerator = Services.cookies.getCookiesFromHost(origHost);
+    while (enumerator.hasMoreElements()) {
+      let nsiCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
+      if (nsiCookie.name === origName && nsiCookie.host === origHost) {
+        cookie = {
+          host: nsiCookie.host,
+          path: nsiCookie.path,
+          name: nsiCookie.name,
+          value: nsiCookie.value,
+          isSecure: nsiCookie.isSecure,
+          isHttpOnly: nsiCookie.isHttpOnly,
+          isSession: nsiCookie.isSession,
+          expires: nsiCookie.expires,
+          originAttributes: nsiCookie.originAttributes
+        };
+        break;
+      }
+    }
+
+    if (!cookie) {
+      return;
+    }
+
+    // If the date is expired set it for 1 minute in the future.
+    let now = new Date();
+    if (!cookie.isSession && (cookie.expires * 1000) <= now) {
+      let tenSecondsFromNow = (now.getTime() + 10 * 1000) / 1000;
+
+      cookie.expires = tenSecondsFromNow;
+    }
+
+    switch (field) {
+      case "isSecure":
+      case "isHttpOnly":
+      case "isSession":
+        newValue = newValue === "true";
+        break;
+
+      case "expires":
+        newValue = Date.parse(newValue) / 1000;
+
+        if (isNaN(newValue)) {
+          newValue = MAX_COOKIE_EXPIRY;
+        }
+        break;
+
+      case "host":
+      case "name":
+      case "path":
+        // Remove the edited cookie.
+        Services.cookies.remove(origHost, origName, origPath,
+                                cookie.originAttributes, false);
+        break;
+    }
+
+    // Apply changes.
+    cookie[field] = newValue;
+
+    // cookie.isSession is not always set correctly on session cookies so we
+    // need to trust cookie.expires instead.
+    cookie.isSession = !cookie.expires;
+
+    // Add the edited cookie.
+    Services.cookies.add(
+      cookie.host,
+      cookie.path,
+      cookie.name,
+      cookie.value,
+      cookie.isSecure,
+      cookie.isHttpOnly,
+      cookie.isSession,
+      cookie.isSession ? MAX_COOKIE_EXPIRY : cookie.expires
+    );
+  },
+
   addCookieObservers: function() {
     Services.obs.addObserver(cookieHelpers, "cookie-changed", false);
     return null;
   },
 
   removeCookieObservers: function() {
     Services.obs.removeObserver(cookieHelpers, "cookie-changed", false);
     return null;
   },
 
   observe: function(subject, topic, data) {
+    if (!subject) {
+      return;
+    }
+
     switch (topic) {
       case "cookie-changed":
+        if (data === "batch-deleted") {
+          let cookiesNoInterface = subject.QueryInterface(Ci.nsIArray);
+          let cookies = [];
+
+          for (let i = 0; i < cookiesNoInterface.length; i++) {
+            let cookie = cookiesNoInterface.queryElementAt(i, Ci.nsICookie2);
+            cookies.push(cookie);
+          }
+          cookieHelpers.onCookieChanged(cookies, topic, data);
+
+          return;
+        }
+
         let cookie = subject.QueryInterface(Ci.nsICookie2);
         cookieHelpers.onCookieChanged(cookie, topic, data);
         break;
     }
   },
 
   handleParentRequest: function(msg) {
     switch (msg.json.method) {
@@ -735,16 +904,19 @@ var cookieHelpers = {
       case "getCookiesFromHost":
         let host = msg.data.args[0];
         let cookies = cookieHelpers.getCookiesFromHost(host);
         return JSON.stringify(cookies);
       case "addCookieObservers":
         return cookieHelpers.addCookieObservers();
       case "removeCookieObservers":
         return cookieHelpers.removeCookieObservers();
+      case "editCookie":
+        let rowdata = msg.data.args[0];
+        return cookieHelpers.editCookie(rowdata);
       default:
         console.error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD", msg.json.method);
         throw new Error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD");
     }
   },
 };
 
 /**