diff --git a/devtools/client/locales/en-US/storage.dtd b/devtools/client/locales/en-US/storage.dtd
index 097b1b744e41..211c794369f4 100644
--- a/devtools/client/locales/en-US/storage.dtd
+++ b/devtools/client/locales/en-US/storage.dtd
@@ -6,3 +6,6 @@
+
+
+
diff --git a/devtools/client/locales/en-US/storage.properties b/devtools/client/locales/en-US/storage.properties
index efade402492d..1a22bead4f8c 100644
--- a/devtools/client/locales/en-US/storage.properties
+++ b/devtools/client/locales/en-US/storage.properties
@@ -119,3 +119,7 @@ storage.parsedValue.label=Parsed Value
# LOCALIZATION NOTE (storage.popupMenu.deleteLabel):
# Label of popup menu action to delete storage item.
storage.popupMenu.deleteLabel=Delete ā%Sā
+
+# LOCALIZATION NOTE (storage.popupMenu.deleteAllLabel):
+# Label of popup menu action to delete all storage items.
+storage.popupMenu.deleteAllFromLabel=Delete All From ā%Sā
diff --git a/devtools/client/shared/widgets/TableWidget.js b/devtools/client/shared/widgets/TableWidget.js
index b70803abbd56..0606ef9c1692 100644
--- a/devtools/client/shared/widgets/TableWidget.js
+++ b/devtools/client/shared/widgets/TableWidget.js
@@ -803,6 +803,9 @@ TableWidget.prototype = {
if (typeof item == "string") {
item = this.items.get(item);
}
+ if (!item) {
+ return;
+ }
let removed = this.items.delete(item[this.uniqueId]);
if (!removed) {
diff --git a/devtools/client/shared/widgets/TreeWidget.js b/devtools/client/shared/widgets/TreeWidget.js
index c270bf2c72bf..a92f13ca15fd 100644
--- a/devtools/client/shared/widgets/TreeWidget.js
+++ b/devtools/client/shared/widgets/TreeWidget.js
@@ -17,9 +17,11 @@ const EventEmitter = require("devtools/shared/event-emitter");
* @param {Object} options
* - emptyText {string}: text to display when no entries in the table.
* - defaultType {string}: The default type of the tree items. For ex.
- * 'js'
+ * 'js'
* - sorted {boolean}: Defaults to true. If true, tree items are kept in
- * lexical order. If false, items will be kept in insertion order.
+ * lexical order. If false, items will be kept in insertion order.
+ * - contextMenuId {string}: ID of context menu to be displayed on
+ * tree items.
*/
function TreeWidget(node, options = {}) {
EventEmitter.decorate(this);
@@ -31,6 +33,7 @@ function TreeWidget(node, options = {}) {
this.emptyText = options.emptyText || "";
this.defaultType = options.defaultType;
this.sorted = options.sorted !== false;
+ this.contextMenuId = options.contextMenuId;
this.setupRoot();
@@ -53,30 +56,31 @@ TreeWidget.prototype = {
/**
* Select any node in the tree.
*
- * @param {array} id
+ * @param {array} ids
* An array of ids leading upto the selected item
*/
- set selectedItem(id) {
+ set selectedItem(ids) {
if (this._selectedLabel) {
this._selectedLabel.classList.remove("theme-selected");
}
let currentSelected = this._selectedLabel;
- if (id == -1) {
+ if (ids == -1) {
this._selectedLabel = this._selectedItem = null;
return;
}
- if (!Array.isArray(id)) {
+ if (!Array.isArray(ids)) {
return;
}
- this._selectedLabel = this.root.setSelectedItem(id);
+ this._selectedLabel = this.root.setSelectedItem(ids);
if (!this._selectedLabel) {
this._selectedItem = null;
} else {
if (currentSelected != this._selectedLabel) {
this.ensureSelectedVisible();
}
- this._selectedItem =
- JSON.parse(this._selectedLabel.parentNode.getAttribute("data-id"));
+ this._selectedItem = ids;
+ this.emit("select", this._selectedItem,
+ this.attachments.get(JSON.stringify(ids)));
}
},
@@ -120,9 +124,16 @@ TreeWidget.prototype = {
*/
setupRoot: function() {
this.root = new TreeItem(this.document);
+ if (this.contextMenuId) {
+ this.root.children.addEventListener("contextmenu", (event) => {
+ let menu = this.document.getElementById(this.contextMenuId);
+ menu.openPopupAtScreen(event.screenX, event.screenY, true);
+ });
+ }
+
this._parent.appendChild(this.root.children);
- this.root.children.addEventListener("click", e => this.onClick(e));
+ this.root.children.addEventListener("mousedown", e => this.onClick(e));
this.root.children.addEventListener("keypress", e => this.onKeypress(e));
},
@@ -315,21 +326,17 @@ TreeWidget.prototype = {
if (!target) {
return;
}
+
if (target.hasAttribute("expanded")) {
target.removeAttribute("expanded");
} else {
target.setAttribute("expanded", "true");
}
- if (this._selectedLabel) {
- this._selectedLabel.classList.remove("theme-selected");
- }
+
if (this._selectedLabel != target) {
let ids = target.parentNode.getAttribute("data-id");
- this._selectedItem = JSON.parse(ids);
- this.emit("select", this._selectedItem, this.attachments.get(ids));
- this._selectedLabel = target;
+ this.selectedItem = JSON.parse(ids);
}
- target.classList.add("theme-selected");
},
/**
@@ -337,7 +344,6 @@ TreeWidget.prototype = {
* items, as well as collapsing and expanding any item.
*/
onKeypress: function(event) {
- let currentSelected = this._selectedLabel;
switch (event.keyCode) {
case event.DOM_VK_UP:
this.selectPreviousItem();
@@ -367,11 +373,6 @@ TreeWidget.prototype = {
default: return;
}
event.preventDefault();
- if (this._selectedLabel != currentSelected) {
- let ids = JSON.stringify(this._selectedItem);
- this.emit("select", this._selectedItem, this.attachments.get(ids));
- this.ensureSelectedVisible();
- }
},
/**
diff --git a/devtools/client/storage/storage.xul b/devtools/client/storage/storage.xul
index 9738c4ba4a05..702f3687ad55 100644
--- a/devtools/client/storage/storage.xul
+++ b/devtools/client/storage/storage.xul
@@ -23,8 +23,15 @@
diff --git a/devtools/client/storage/test/browser.ini b/devtools/client/storage/test/browser.ini
index 3ab650a2bcb2..a7773bd207ad 100644
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -15,12 +15,15 @@ support-files =
head.js
[browser_storage_basic.js]
+[browser_storage_cookies_delete_all.js]
[browser_storage_cookies_edit.js]
[browser_storage_cookies_edit_keyboard.js]
[browser_storage_cookies_tab_navigation.js]
[browser_storage_dynamic_updates.js]
[browser_storage_localstorage_edit.js]
[browser_storage_delete.js]
+[browser_storage_delete_all.js]
+[browser_storage_delete_tree.js]
[browser_storage_overflow.js]
[browser_storage_search.js]
skip-if = os == "linux" && e10s # Bug 1240804 - unhandled promise rejections
diff --git a/devtools/client/storage/test/browser_storage_cookies_delete_all.js b/devtools/client/storage/test/browser_storage_cookies_delete_all.js
new file mode 100644
index 000000000000..cc94fb7fe2a0
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_delete_all.js
@@ -0,0 +1,74 @@
+/* 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 head.js */
+
+"use strict";
+
+// Test deleting all cookies
+
+function* performDelete(store, rowName, deleteAll) {
+ let contextMenu = gPanelWindow.document.getElementById(
+ "storage-table-popup");
+ let menuDeleteAllItem = contextMenu.querySelector(
+ "#storage-table-popup-delete-all");
+ let menuDeleteAllFromItem = contextMenu.querySelector(
+ "#storage-table-popup-delete-all-from");
+
+ let storeName = store.join(" > ");
+
+ yield selectTreeItem(store);
+
+ let eventWait = gUI.once("store-objects-updated");
+
+ let cells = getRowCells(rowName);
+ yield waitForContextMenu(contextMenu, cells.name, () => {
+ info(`Opened context menu in ${storeName}, row '${rowName}'`);
+ if (deleteAll) {
+ menuDeleteAllItem.click();
+ } else {
+ menuDeleteAllFromItem.click();
+ let hostName = cells.host.value;
+ ok(menuDeleteAllFromItem.getAttribute("label").includes(hostName),
+ `Context menu item label contains '${hostName}'`);
+ }
+ });
+
+ yield eventWait;
+}
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ info("test state before delete");
+ yield checkState([
+ [["cookies", "test1.example.org"], ["c1", "c3", "cs2", "uc1"]],
+ [["cookies", "sectest1.example.org"], ["cs2", "sc1", "uc1"]],
+ ]);
+
+ info("delete all from domain");
+ // delete only cookies that match the host exactly
+ yield performDelete(["cookies", "test1.example.org"], "c1", false);
+
+ info("test state after delete all from domain");
+ yield checkState([
+ // Domain cookies (.example.org) must not be deleted.
+ [["cookies", "test1.example.org"], ["cs2", "uc1"]],
+ [["cookies", "sectest1.example.org"], ["cs2", "sc1", "uc1"]],
+ ]);
+
+ info("delete all");
+ // delete all cookies for host, including domain cookies
+ yield performDelete(["cookies", "sectest1.example.org"], "uc1", true);
+
+ info("test state after delete all");
+ yield checkState([
+ // Domain cookies (.example.org) are deleted too, so deleting in sectest1
+ // also removes stuff from test1.
+ [["cookies", "test1.example.org"], []],
+ [["cookies", "sectest1.example.org"], []],
+ ]);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_delete.js b/devtools/client/storage/test/browser_storage_delete.js
index 466377cc21a8..b38147968e72 100644
--- a/devtools/client/storage/test/browser_storage_delete.js
+++ b/devtools/client/storage/test/browser_storage_delete.js
@@ -28,10 +28,11 @@ add_task(function* () {
yield selectTreeItem([store, host]);
let row = getRowCells(rowName);
-
ok(gUI.table.items.has(rowName),
`There is a row '${rowName}' in ${store} > ${host}`);
+ let eventWait = gUI.once("store-objects-updated");
+
yield waitForContextMenu(contextMenu, row[cellToClick], () => {
info(`Opened context menu in ${store} > ${host}, row '${rowName}'`);
menuDeleteItem.click();
@@ -39,7 +40,7 @@ add_task(function* () {
`Context menu item label contains '${rowName}'`);
});
- yield gUI.once("store-objects-updated");
+ yield eventWait;
ok(!gUI.table.items.has(rowName),
`There is no row '${rowName}' in ${store} > ${host} after deletion`);
diff --git a/devtools/client/storage/test/browser_storage_delete_all.js b/devtools/client/storage/test/browser_storage_delete_all.js
new file mode 100644
index 000000000000..af4d38e8a8eb
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete_all.js
@@ -0,0 +1,79 @@
+/* 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 head.js */
+
+"use strict";
+
+// Test deleting all storage items
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ let contextMenu = gPanelWindow.document.getElementById("storage-table-popup");
+ let menuDeleteAllItem = contextMenu.querySelector(
+ "#storage-table-popup-delete-all");
+
+ info("test state before delete");
+ const beforeState = [
+ [["localStorage", "http://test1.example.org"],
+ ["ls1", "ls2"]],
+ [["localStorage", "http://sectest1.example.org"],
+ ["iframe-u-ls1"]],
+ [["localStorage", "https://sectest1.example.org"],
+ ["iframe-s-ls1"]],
+ [["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"]],
+ ];
+
+ 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"],
+ ];
+
+ for (let [store, rowName] of deleteHosts) {
+ let storeName = store.join(" > ");
+
+ yield selectTreeItem(store);
+
+ let eventWait = gUI.once("store-objects-cleared");
+
+ let cell = getRowCells(rowName).name;
+ yield waitForContextMenu(contextMenu, cell, () => {
+ info(`Opened context menu in ${storeName}, row '${rowName}'`);
+ menuDeleteAllItem.click();
+ });
+
+ yield eventWait;
+ }
+
+ info("test state after delete");
+ const afterState = [
+ // iframes from the same host, one secure, one unsecure, are independent
+ // from each other. Delete all in one doesn't touch the other one.
+ [["localStorage", "http://test1.example.org"],
+ ["ls1", "ls2"]],
+ [["localStorage", "http://sectest1.example.org"],
+ ["iframe-u-ls1"]],
+ [["localStorage", "https://sectest1.example.org"],
+ []],
+ [["sessionStorage", "http://test1.example.org"],
+ ["ss1"]],
+ [["sessionStorage", "http://sectest1.example.org"],
+ ["iframe-u-ss1", "iframe-u-ss2"]],
+ [["sessionStorage", "https://sectest1.example.org"],
+ []],
+ ];
+
+ yield checkState(afterState);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_delete_tree.js b/devtools/client/storage/test/browser_storage_delete_tree.js
new file mode 100644
index 000000000000..ae897b31724c
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete_tree.js
@@ -0,0 +1,60 @@
+/* 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 head.js */
+
+"use strict";
+
+// Test deleting all storage items from the tree.
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ let contextMenu = gPanelWindow.document.getElementById("storage-tree-popup");
+ let menuDeleteAllItem = contextMenu.querySelector(
+ "#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"]],
+ ]);
+
+ info("do the delete");
+ const deleteHosts = [
+ ["cookies", "test1.example.org"],
+ ["localStorage", "http://test1.example.org"],
+ ["sessionStorage", "http://test1.example.org"],
+ ];
+
+ for (let store of deleteHosts) {
+ let storeName = store.join(" > ");
+
+ yield selectTreeItem(store);
+
+ let eventName = "store-objects-" +
+ (store[0] == "cookies" ? "updated" : "cleared");
+ let eventWait = gUI.once(eventName);
+
+ let selector = `[data-id='${JSON.stringify(store)}'] > .tree-widget-item`;
+ let target = gPanelWindow.document.querySelector(selector);
+ ok(target, `tree item found in ${storeName}`);
+ yield waitForContextMenu(contextMenu, target, () => {
+ info(`Opened tree context menu in ${storeName}`);
+ menuDeleteAllItem.click();
+ });
+
+ yield eventWait;
+ }
+
+ info("test state after delete");
+ yield checkState([
+ [["cookies", "test1.example.org"], []],
+ [["localStorage", "http://test1.example.org"], []],
+ [["sessionStorage", "http://test1.example.org"], []],
+ ]);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/head.js b/devtools/client/storage/test/head.js
index f75625540e85..e1038a200d6b 100644
--- a/devtools/client/storage/test/head.js
+++ b/devtools/client/storage/test/head.js
@@ -509,17 +509,13 @@ function matchVariablesViewProperty(prop, rule) {
* The array id of the item in the tree
*/
function* selectTreeItem(ids) {
- // Expand tree as some/all items could be collapsed leading to click on an
- // incorrect tree item
- gUI.tree.expandAll();
-
- let selector = "[data-id='" + JSON.stringify(ids) + "'] > .tree-widget-item";
- let target = gPanelWindow.document.querySelector(selector);
- ok(target, "tree item found with ids " + JSON.stringify(ids));
+ /* If this item is already selected, return */
+ if (gUI.tree.isSelected(ids)) {
+ return;
+ }
let updated = gUI.once("store-objects-updated");
-
- yield click(target);
+ gUI.tree.selectedItem = ids;
yield updated;
}
@@ -845,8 +841,35 @@ function waitForContextMenu(popup, button, onShown, onHidden) {
popup.addEventListener("popupshown", onPopupShown);
info("wait for the context menu to open");
+ button.scrollIntoView();
let eventDetails = {type: "contextmenu", button: 2};
EventUtils.synthesizeMouse(button, 2, 2, eventDetails,
button.ownerDocument.defaultView);
return deferred.promise;
}
+
+/**
+ * Verify the storage inspector state: check that given type/host exists
+ * in the tree, and that the table contains rows with specified names.
+ *
+ * @param {Array} state Array of state specifications. For example,
+ * [["cookies", "example.com"], ["c1", "c2"]] means to select the
+ * "example.com" host in cookies and then verify there are "c1" and "c2"
+ * cookies (and no other ones).
+ */
+function* checkState(state) {
+ for (let [store, names] of state) {
+ let storeName = store.join(" > ");
+ info(`Selecting tree item ${storeName}`);
+ yield selectTreeItem(store);
+
+ let items = gUI.table.items;
+
+ is(items.size, names.length,
+ `There is correct number of rows in ${storeName}`);
+ for (let name of names) {
+ ok(items.has(name),
+ `There is item with name '${name}' in ${storeName}`);
+ }
+ }
+}
diff --git a/devtools/client/storage/ui.js b/devtools/client/storage/ui.js
index 5460f6eeca78..fefe6390e770 100644
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -70,7 +70,10 @@ var StorageUI = this.StorageUI = function StorageUI(front, target, panelWin) {
this.front = front;
let treeNode = this._panelDoc.getElementById("storage-tree");
- this.tree = new TreeWidget(treeNode, {defaultType: "dir"});
+ this.tree = new TreeWidget(treeNode, {
+ defaultType: "dir",
+ contextMenuId: "storage-tree-popup"
+ });
this.onHostSelect = this.onHostSelect.bind(this);
this.tree.on("select", this.onHostSelect);
@@ -111,14 +114,34 @@ var StorageUI = this.StorageUI = function StorageUI(front, target, panelWin) {
this.handleKeypress = this.handleKeypress.bind(this);
this._panelDoc.addEventListener("keypress", this.handleKeypress);
- this.onPopupShowing = this.onPopupShowing.bind(this);
+ this.onTreePopupShowing = this.onTreePopupShowing.bind(this);
+ this._treePopup = this._panelDoc.getElementById("storage-tree-popup");
+ this._treePopup.addEventListener("popupshowing", this.onTreePopupShowing);
+
+ this.onTablePopupShowing = this.onTablePopupShowing.bind(this);
this._tablePopup = this._panelDoc.getElementById("storage-table-popup");
- this._tablePopup.addEventListener("popupshowing", this.onPopupShowing, false);
+ 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._tablePopupDelete = this._panelDoc.getElementById(
"storage-table-popup-delete");
- this._tablePopupDelete.addEventListener("command", this.onRemoveItem, false);
+ this._tablePopupDelete.addEventListener("command", this.onRemoveItem);
+
+ this._tablePopupDeleteAllFrom = this._panelDoc.getElementById(
+ "storage-table-popup-delete-all-from");
+ this._tablePopupDeleteAllFrom.addEventListener("command",
+ this.onRemoveAllFrom);
+
+ 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);
};
exports.StorageUI = StorageUI;
@@ -126,7 +149,6 @@ exports.StorageUI = StorageUI;
StorageUI.prototype = {
storageTypes: null,
- shouldResetColumns: true,
shouldLoadMoreItems: true,
set animationsEnabled(value) {
@@ -145,8 +167,16 @@ StorageUI.prototype = {
this.searchBox.removeEventListener("input", this.filterItems);
this.searchBox = null;
- this._tablePopup.removeEventListener("popupshowing", this.onPopupShowing);
+ this._treePopup.removeEventListener("popupshowing",
+ this.onTreePopupShowing);
+ this._treePopupDeleteAll.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);
},
/**
@@ -391,10 +421,10 @@ StorageUI.prototype = {
this.emit("store-objects-updated");
return;
}
- if (this.shouldResetColumns) {
+ if (reason === REASON.POPULATE) {
this.resetColumns(data[0], type);
+ this.table.host = host;
}
- this.table.host = host;
this.populateTable(data, reason);
this.emit("store-objects-updated");
@@ -437,7 +467,6 @@ StorageUI.prototype = {
this.tree.add([type, host, ...names]);
if (!this.tree.selectedItem) {
this.tree.selectedItem = [type, host, names[0], names[1]];
- this.fetchStorageObjects(type, host, [name], REASON.POPULATE);
}
} catch (ex) {
// Do Nothing
@@ -445,7 +474,6 @@ StorageUI.prototype = {
}
if (!this.tree.selectedItem) {
this.tree.selectedItem = [type, host];
- this.fetchStorageObjects(type, host, null, REASON.POPULATE);
}
}
}
@@ -625,7 +653,6 @@ StorageUI.prototype = {
if (item.length > 2) {
names = [JSON.stringify(item.slice(2))];
}
- this.shouldResetColumns = true;
this.fetchStorageObjects(type, host, names, REASON.POPULATE);
this.itemOffset = 0;
},
@@ -657,7 +684,6 @@ StorageUI.prototype = {
}
this.table.setColumns(columns, null, HIDDEN_COLUMNS);
this.table.datatype = type;
- this.shouldResetColumns = false;
this.hideSidebar();
},
@@ -757,23 +783,54 @@ StorageUI.prototype = {
* If the current storage actor doesn't support removing items, prevent
* showing the menu.
*/
- onPopupShowing: function(event) {
+ onTablePopupShowing: function(event) {
if (!this.getCurrentActor().removeItem) {
event.preventDefault();
return;
}
+ const maxLen = ITEM_NAME_MAX_LENGTH;
+ let [type] = this.tree.selectedItem;
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));
+
+ if (type === "cookies") {
+ let host = data.host;
+ if (host.length > maxLen) {
+ host = host.substr(0, maxLen) + L10N.ellipsis;
+ }
+
+ this._tablePopupDeleteAllFrom.hidden = false;
+ this._tablePopupDeleteAllFrom.setAttribute("label",
+ L10N.getFormatStr("storage.popupMenu.deleteAllFromLabel", host));
+ } else {
+ this._tablePopupDeleteAllFrom.hidden = true;
+ }
+ },
+
+ onTreePopupShowing: function(event) {
+ let showMenu = false;
+ let selectedItem = this.tree.selectedItem;
+ // Never show menu on the 1st level item
+ if (selectedItem && selectedItem.length > 1) {
+ // this.currentActor() would return wrong value here
+ let actor = this.storageTypes[selectedItem[0]];
+ if (actor.removeAll) {
+ showMenu = true;
+ }
+ }
+
+ if (!showMenu) {
+ event.preventDefault();
+ }
},
/**
@@ -787,4 +844,30 @@ StorageUI.prototype = {
actor.removeItem(host, data[this.table.uniqueId]);
},
+
+ /**
+ * Handles removing all items from the storage
+ */
+ onRemoveAll: function() {
+ // Cannot use this.currentActor() if the handler is called from the
+ // tree context menu: it returns correct value only after the table
+ // data from server are successfully fetched (and that's async).
+ let [type, host] = this.tree.selectedItem;
+ let actor = this.storageTypes[type];
+
+ actor.removeAll(host);
+ },
+
+ /**
+ * Handles removing all cookies with exactly the same domain as the
+ * cookie in the selected row.
+ */
+ onRemoveAllFrom: function() {
+ 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);
+ },
};
diff --git a/devtools/server/actors/storage.js b/devtools/server/actors/storage.js
index dcd20140f738..38e71bbae2bd 100644
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -686,8 +686,18 @@ StorageActors.createActor({
this.removeCookie(host, name);
}), {
request: {
- host: Arg(0),
- name: Arg(1),
+ host: Arg(0, "string"),
+ name: Arg(1, "string"),
+ },
+ response: {}
+ }),
+
+ removeAll: method(Task.async(function*(host, domain) {
+ this.removeAllCookies(host, domain);
+ }), {
+ request: {
+ host: Arg(0, "string"),
+ domain: Arg(1, "nullable:string")
},
response: {}
}),
@@ -696,11 +706,18 @@ StorageActors.createActor({
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;
+ this.getCookiesFromHost =
+ cookieHelpers.getCookiesFromHost.bind(cookieHelpers);
+ this.addCookieObservers =
+ cookieHelpers.addCookieObservers.bind(cookieHelpers);
+ this.removeCookieObservers =
+ cookieHelpers.removeCookieObservers.bind(cookieHelpers);
+ this.editCookie =
+ cookieHelpers.editCookie.bind(cookieHelpers);
+ this.removeCookie =
+ cookieHelpers.removeCookie.bind(cookieHelpers);
+ this.removeAllCookies =
+ cookieHelpers.removeAllCookies.bind(cookieHelpers);
return;
}
@@ -722,6 +739,8 @@ StorageActors.createActor({
callParentProcess.bind(null, "editCookie");
this.removeCookie =
callParentProcess.bind(null, "removeCookie");
+ this.removeAllCookies =
+ callParentProcess.bind(null, "removeAllCookies");
addMessageListener("storage:storage-cookie-request-child",
cookieHelpers.handleParentRequest);
@@ -875,7 +894,7 @@ var cookieHelpers = {
);
},
- removeCookie: function(host, name) {
+ _removeCookies: function(host, opts = {}) {
function hostMatches(cookieHost, matchHost) {
if (cookieHost == null) {
return matchHost == null;
@@ -889,7 +908,9 @@ var cookieHelpers = {
let enumerator = Services.cookies.getCookiesFromHost(host);
while (enumerator.hasMoreElements()) {
let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
- if (hostMatches(cookie.host, host) && cookie.name === name) {
+ if (hostMatches(cookie.host, host) &&
+ (!opts.name || cookie.name === opts.name) &&
+ (!opts.domain || cookie.host === opts.domain)) {
Services.cookies.remove(
cookie.host,
cookie.name,
@@ -901,6 +922,16 @@ var cookieHelpers = {
}
},
+ removeCookie: function(host, name) {
+ if (name !== undefined) {
+ this._removeCookies(host, { name });
+ }
+ },
+
+ removeAllCookies: function(host, domain) {
+ this._removeCookies(host, { domain });
+ },
+
addCookieObservers: function() {
Services.obs.addObserver(cookieHelpers, "cookie-changed", false);
return null;
@@ -969,6 +1000,11 @@ var cookieHelpers = {
let name = msg.data.args[1];
return cookieHelpers.removeCookie(host, name);
}
+ case "removeAllCookies": {
+ let host = msg.data.args[0];
+ let domain = msg.data.args[1];
+ return cookieHelpers.removeAllCookies(host, domain);
+ }
default:
console.error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD", msg.json.method);
throw new Error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD");
@@ -1180,6 +1216,16 @@ function getObjectForLocalOrSessionStorage(type) {
},
response: {}
}),
+
+ removeAll: method(Task.async(function*(host) {
+ let storage = this.hostVsStores.get(host);
+ storage.clear();
+ }), {
+ request: {
+ host: Arg(0)
+ },
+ response: {}
+ }),
};
}
@@ -2078,7 +2124,7 @@ var StorageActor = exports.StorageActor = protocol.ActorClass({
data: Arg(0, "json")
},
"stores-reloaded": {
- type: "storesRelaoded",
+ type: "storesReloaded",
data: Arg(0, "json")
}
},
@@ -2310,11 +2356,13 @@ var StorageActor = exports.StorageActor = protocol.ActorClass({
this.boundUpdate[action][storeType] = {};
}
for (let host in data) {
- if (!this.boundUpdate[action][storeType][host] || action == "deleted") {
- this.boundUpdate[action][storeType][host] = data[host];
- } else {
- this.boundUpdate[action][storeType][host] =
- this.boundUpdate[action][storeType][host].concat(data[host]);
+ if (!this.boundUpdate[action][storeType][host]) {
+ this.boundUpdate[action][storeType][host] = [];
+ }
+ for (let name of data[host]) {
+ if (!this.boundUpdate[action][storeType][host].includes(name)) {
+ this.boundUpdate[action][storeType][host].push(name);
+ }
}
}
if (action == "added") {