/* vim:set ts=2 sw=2 sts=2 et: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {Task} = require("devtools/shared/task"); const EventEmitter = require("devtools/shared/event-emitter"); const {LocalizationHelper, ELLIPSIS} = require("devtools/shared/l10n"); const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); const JSOL = require("devtools/client/shared/vendor/jsol"); const {KeyCodes} = require("devtools/client/shared/keycodes"); // GUID to be used as a separator in compound keys. This must match the same // constant in devtools/server/actors/storage.js, // devtools/client/storage/test/head.js and // devtools/server/tests/browser/head.js const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}"; loader.lazyRequireGetter(this, "TreeWidget", "devtools/client/shared/widgets/TreeWidget", true); loader.lazyRequireGetter(this, "TableWidget", "devtools/client/shared/widgets/TableWidget", true); loader.lazyRequireGetter(this, "ViewHelpers", "devtools/client/shared/widgets/view-helpers"); loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm"); /** * Localization convenience methods. */ const STORAGE_STRINGS = "devtools/client/locales/storage.properties"; const L10N = new LocalizationHelper(STORAGE_STRINGS); const GENERIC_VARIABLES_VIEW_SETTINGS = { lazyEmpty: true, // ms lazyEmptyDelay: 10, searchEnabled: true, searchPlaceholder: L10N.getStr("storage.search.placeholder"), preventDescriptorModifiers: true }; const REASON = { NEW_ROW: "new-row", NEXT_50_ITEMS: "next-50-items", POPULATE: "populate", UPDATE: "update" }; const COOKIE_KEY_MAP = { path: "Path", host: "Domain", expires: "Expires", isSecure: "Secure", isHttpOnly: "HttpOnly", isDomain: "HostOnly", creationTime: "CreationTime", lastAccessed: "LastAccessed" }; // 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. * * @param {Front} front * Front for the storage actor * @param {Target} target * Interface for the page we're debugging * @param {Window} panelWin * Window of the toolbox panel to populate UI in. */ function StorageUI(front, target, panelWin, toolbox) { EventEmitter.decorate(this); this._target = target; this._window = panelWin; this._panelDoc = panelWin.document; this._toolbox = toolbox; this.front = front; let treeNode = this._panelDoc.getElementById("storage-tree"); this.tree = new TreeWidget(treeNode, { defaultType: "dir", contextMenuId: "storage-tree-popup" }); 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); this.editItem = this.editItem.bind(this); this.table.on(TableWidget.EVENTS.CELL_EDIT, this.editItem); this.sidebar = this._panelDoc.getElementById("storage-sidebar"); this.sidebar.setAttribute("width", "300"); this.view = new VariablesView(this.sidebar.firstChild, GENERIC_VARIABLES_VIEW_SETTINGS); this.searchBox = this._panelDoc.getElementById("storage-searchbox"); this.filterItems = this.filterItems.bind(this); this.searchBox.addEventListener("command", this.filterItems); let shortcuts = new KeyShortcuts({ window: this._panelDoc.defaultView, }); let key = L10N.getStr("storage.filter.key"); shortcuts.on(key, (name, event) => { event.preventDefault(); this.searchBox.focus(); }); this.front.listStores().then(storageTypes => { this.populateStorageTree(storageTypes); }).then(null, console.error); 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.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.onTablePopupShowing); this.onRemoveItem = this.onRemoveItem.bind(this); this.onRemoveAllFrom = this.onRemoveAllFrom.bind(this); this.onRemoveAll = this.onRemoveAll.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", 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); this._treePopupDelete = this._panelDoc.getElementById("storage-tree-popup-delete"); this._treePopupDelete.addEventListener("command", this.onRemoveTreeItem); } exports.StorageUI = StorageUI; StorageUI.prototype = { storageTypes: null, shouldLoadMoreItems: true, set animationsEnabled(value) { this._panelDoc.documentElement.classList.toggle("no-animate", !value); }, destroy: function () { this.table.off(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar); this.table.off(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd); 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._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); }, /** * Empties and hides the object viewer sidebar */ hideSidebar: function () { this.view.empty(); this.sidebar.hidden = true; this.table.clearSelection(); }, getCurrentActor: function () { let type = this.table.datatype; return this.storageTypes[type]; }, /** * Make column fields editable * * @param {Array} editableFields * An array of keys of columns to be made editable */ makeFieldsEditable: function* (editableFields) { if (editableFields && editableFields.length > 0) { this.table.makeFieldsEditable(editableFields); } else if (this.table._editableFieldsEngine) { this.table._editableFieldsEngine.destroy(); } }, editItem: function (eventType, data) { let actor = this.getCurrentActor(); actor.editItem(data); }, /** * Removes the given item from the storage table. Reselects the next item in * the table and repopulates the sidebar with that item's data if the item * being removed was selected. */ removeItemFromTable: function (name) { if (this.table.isSelected(name)) { if (this.table.selectedIndex == 0) { this.table.selectNextRow(); } else { this.table.selectPreviousRow(); } this.table.remove(name); this.displayObjectSidebar(); } else { this.table.remove(name); } }, /** * Event handler for "stores-cleared" event coming from the storage actor. * * @param {object} response * An object containing which storage types were cleared */ onCleared: function (response) { function* enumPaths() { for (let type in response) { if (Array.isArray(response[type])) { // Handle the legacy response with array of hosts for (let host of response[type]) { yield [type, host]; } } else { // Handle the new format that supports clearing sub-stores in a host for (let host in response[type]) { let paths = response[type][host]; if (!paths.length) { yield [type, host]; } else { for (let path of paths) { try { path = JSON.parse(path); yield [type, host, ...path]; } catch (ex) { // ignore } } } } } } } for (let path of enumPaths()) { // Find if the path is selected (there is max one) and clear it if (this.tree.isSelected(path)) { this.table.clear(); this.hideSidebar(); this.emit("store-objects-cleared"); break; } } }, /** * Event handler for "stores-update" event coming from the storage actor. * * @param {object} argument0 * An object containing the details of the added, changed and deleted * storage objects. * Each of these 3 objects are of the following format: * { * : { * : [, ...], * : [...], ... * }, * : { * : [, ...], * : [...], ... * }, ... * } * Where store_type1 and store_type2 is one of cookies, indexedDB, * sessionStorage and localStorage; host1, host2 are the host in which * this change happened; and [ 0) { let tableItemName = names.pop(); if (this.tree.isSelected([type, host, ...names])) { this.removeItemFromTable(tableItemName); } } } catch (ex) { if (this.tree.isSelected([type, host])) { this.removeItemFromTable(name); } } } } } } }, /** * Handle changed items received by onUpdate * * @param {object} See onUpdate docs */ handleChangedItems: function (changed) { let [type, host, db, objectStore] = this.tree.selectedItem; if (!changed[type] || !changed[type][host] || changed[type][host].length == 0) { return; } try { let toUpdate = []; for (let name of changed[type][host]) { let names = JSON.parse(name); if (names[0] == db && names[1] == objectStore && names[2]) { toUpdate.push(name); } } this.fetchStorageObjects(type, host, toUpdate, REASON.UPDATE); } catch (ex) { this.fetchStorageObjects(type, host, changed[type][host], REASON.UPDATE); } }, /** * Fetches the storage objects from the storage actor and populates the * storage table with the returned data. * * @param {string} type * The type of storage. Ex. "cookies" * @param {string} host * Hostname * @param {array} names * Names of particular store objects. Empty if all are requested * @param {Constant} reason * See REASON constant at top of file. */ fetchStorageObjects: Task.async(function* (type, host, names, reason) { let fetchOpts = reason === REASON.NEXT_50_ITEMS ? {offset: this.itemOffset} : {}; let storageType = this.storageTypes[type]; if (reason !== REASON.NEXT_50_ITEMS && reason !== REASON.UPDATE && reason !== REASON.NEW_ROW && reason !== REASON.POPULATE) { throw new Error("Invalid reason specified"); } try { if (reason === REASON.POPULATE) { let subType = null; // The indexedDB type could have sub-type data to fetch. // If having names specified, then it means // we are fetching details of specific database or of object store. if (type == "indexedDB" && names) { let [ dbName, objectStoreName ] = JSON.parse(names[0]); if (dbName) { subType = "database"; } if (objectStoreName) { subType = "object store"; } } yield this.resetColumns(type, host, subType); } let {data} = yield storageType.getStoreObjects(host, names, fetchOpts); if (data.length) { this.populateTable(data, reason); } this.emit("store-objects-updated"); } catch (ex) { console.error(ex); } }), /** * Populates the storage tree which displays the list of storages present for * the page. * * @param {object} storageTypes * List of storages and their corresponding hosts returned by the * StorageFront.listStores call. */ populateStorageTree: function (storageTypes) { this.storageTypes = {}; for (let type in storageTypes) { // Ignore `from` field, which is just a protocol.js implementation // artifact. if (type === "from") { continue; } let typeLabel = type; try { typeLabel = L10N.getStr("tree.labels." + type); } catch (e) { console.error("Unable to localize tree label type:" + type); } this.tree.add([{id: type, label: typeLabel, type: "store"}]); if (!storageTypes[type].hosts) { continue; } this.storageTypes[type] = storageTypes[type]; for (let host in storageTypes[type].hosts) { this.tree.add([type, {id: host, type: "url"}]); for (let name of storageTypes[type].hosts[host]) { try { let names = JSON.parse(name); this.tree.add([type, host, ...names]); if (!this.tree.selectedItem) { this.tree.selectedItem = [type, host, names[0], names[1]]; } } catch (ex) { // Do Nothing } } if (!this.tree.selectedItem) { this.tree.selectedItem = [type, host]; } } } }, /** * Populates the selected entry from teh table in the sidebar for a more * detailed view. */ displayObjectSidebar: Task.async(function* () { let item = this.table.selectedRow; if (!item) { // Make sure that sidebar is hidden and return this.sidebar.hidden = true; return; } // Get the string value (async action) and the update the UI synchronously. let value; if (item.name && item.valueActor) { value = yield item.valueActor.string(); } // Start updating the UI. Everything is sync beyond this point. this.sidebar.hidden = false; this.view.empty(); let mainScope = this.view.addScope(L10N.getStr("storage.data.label")); mainScope.expanded = true; if (value) { let itemVar = mainScope.addItem(item.name + "", {}, {relaxed: true}); // The main area where the value will be displayed itemVar.setGrip(value); // May be the item value is a json or a key value pair itself this.parseItemValue(item.name, value); // By default the item name and value are shown. If this is the only // information available, then nothing else is to be displayed. let itemProps = Object.keys(item); if (itemProps.length > 3) { // Display any other information other than the item name and value // which may be available. let rawObject = Object.create(null); let otherProps = itemProps.filter( e => !["name", "value", "valueActor"].includes(e)); for (let prop of otherProps) { let cookieProp = COOKIE_KEY_MAP[prop] || prop; // The pseduo property of HostOnly refers to converse of isDomain property rawObject[cookieProp] = (prop === "isDomain") ? !item[prop] : item[prop]; } itemVar.populate(rawObject, {sorted: true}); itemVar.twisty = true; itemVar.expanded = true; } } else { // Case when displaying IndexedDB db/object store properties. for (let key in item) { mainScope.addItem(key, {}, true).setGrip(item[key]); this.parseItemValue(key, item[key]); } } this.emit("sidebar-updated"); }), /** * Tries to parse a string value into either a json or a key-value separated * object and populates the sidebar with the parsed value. The value can also * be a key separated array. * * @param {string} name * The key corresponding to the `value` string in the object * @param {string} value * The string to be parsed into an object */ parseItemValue: function (name, originalValue) { // Find if value is URLEncoded ie let decodedValue = ""; try { decodedValue = decodeURIComponent(originalValue); } catch (e) { // Unable to decode, nothing to do } let value = (decodedValue && decodedValue !== originalValue) ? decodedValue : originalValue; let json = null; try { json = JSOL.parse(value); } catch (ex) { json = null; } if (!json && value) { json = this._extractKeyValPairs(value); } // return if json is null, or same as value, or just a string. if (!json || json == value || typeof json == "string") { return; } // One special case is a url which gets separated as key value pair on : if ((json.length == 2 || Object.keys(json).length == 1) && ((json[0] || Object.keys(json)[0]) + "").match(/^(http|file|ftp)/)) { return; } let jsonObject = Object.create(null); let view = this.view; jsonObject[name] = json; let valueScope = view.getScopeAtIndex(1) || view.addScope(L10N.getStr("storage.parsedValue.label")); valueScope.expanded = true; let jsonVar = valueScope.addItem("", Object.create(null), {relaxed: true}); jsonVar.expanded = true; jsonVar.twisty = true; jsonVar.populate(jsonObject, {expanded: true}); }, /** * Tries to parse a string into an object on the basis of key-value pairs, * separated by various separators. If failed, tries to parse for single * separator separated values to form an array. * * @param {string} value * The string to be parsed into an object or array */ _extractKeyValPairs: function (value) { let makeObject = (keySep, pairSep) => { let object = {}; for (let pair of value.split(pairSep)) { let [key, val] = pair.split(keySep); object[key] = val; } return object; }; // Possible separators. const separators = ["=", ":", "~", "#", "&", "\\*", ",", "\\."]; // Testing for object for (let i = 0; i < separators.length; i++) { let kv = separators[i]; for (let j = 0; j < separators.length; j++) { if (i == j) { continue; } let p = separators[j]; let word = `[^${kv}${p}]*`; let keyValue = `${word}${kv}${word}`; let keyValueList = `${keyValue}(${p}${keyValue})*`; let regex = new RegExp(`^${keyValueList}$`); if (value.match && value.match(regex) && value.includes(kv) && (value.includes(p) || value.split(kv).length == 2)) { return makeObject(kv, p); } } } // Testing for array for (let p of separators) { let word = `[^${p}]*`; let wordList = `(${word}${p})+${word}`; let regex = new RegExp(`^${wordList}$`); if (value.match && value.match(regex)) { return value.split(p.replace(/\\*/g, "")); } } return null; }, /** * Select handler for the storage tree. Fetches details of the selected item * from the storage details and populates the storage tree. * * @param {string} event * The name of the event fired * @param {array} item * An array of ids which represent the location of the selected item in * the storage tree */ onHostSelect: function (event, item) { this.table.clear(); this.hideSidebar(); this.searchBox.value = ""; let [type, host] = item; let names = null; if (!host) { return; } if (item.length > 2) { names = [JSON.stringify(item.slice(2))]; } this.fetchStorageObjects(type, host, names, REASON.POPULATE); this.itemOffset = 0; }, /** * Resets the column headers in the storage table with the pased object `data` * * @param {string} type * The type of storage corresponding to the after-reset columns in the * table. * @param {string} host * The host name corresponding to the table after reset. * * @param {string} [subType] * The sub type under the given type. */ resetColumns: function* (type, host, subtype) { this.table.host = host; this.table.datatype = type; let uniqueKey = null; let columns = {}; let editableFields = []; let hiddenFields = []; let privateFields = []; let fields = yield this.getCurrentActor().getFields(subtype); fields.forEach(f => { if (!uniqueKey) { this.table.uniqueId = uniqueKey = f.name; } if (f.editable) { editableFields.push(f.name); } if (f.hidden) { hiddenFields.push(f.name); } if (f.private) { privateFields.push(f.name); } columns[f.name] = f.name; let columnName; try { columnName = L10N.getStr("table.headers." + type + "." + f.name); } catch (e) { columnName = COOKIE_KEY_MAP[f.name]; } if (!columnName) { console.error("Unable to localize table header type:" + type + " key:" + f.name); } else { columns[f.name] = columnName; } }); this.table.setColumns(columns, null, hiddenFields, privateFields); this.hideSidebar(); yield this.makeFieldsEditable(editableFields); }, /** * Populates or updates the rows in the storage table. * * @param {array[object]} data * Array of objects to be populated in the storage table * @param {Constant} reason * See REASON constant at top of file. */ populateTable: function (data, reason) { for (let item of data) { if (item.value) { item.valueActor = item.value; item.value = item.value.initial || ""; } if (item.expires != null) { item.expires = item.expires ? new Date(item.expires).toUTCString() : L10N.getStr("label.expires.session"); } if (item.creationTime != null) { item.creationTime = new Date(item.creationTime).toUTCString(); } if (item.lastAccessed != null) { item.lastAccessed = new Date(item.lastAccessed).toUTCString(); } switch (reason) { case REASON.POPULATE: // Update without flashing the row. this.table.push(item, true); break; case REASON.NEW_ROW: case REASON.NEXT_50_ITEMS: // Update and flash the row. this.table.push(item, false); break; case REASON.UPDATE: this.table.update(item); if (item == this.table.selectedRow && !this.sidebar.hidden) { this.displayObjectSidebar(); } break; } this.shouldLoadMoreItems = true; } }, /** * Handles keypress event on the body table to close the sidebar when open * * @param {DOMEvent} event * The event passed by the keypress event. */ handleKeypress: function (event) { if (event.keyCode == KeyCodes.DOM_VK_ESCAPE && !this.sidebar.hidden) { // Stop Propagation to prevent opening up of split console this.hideSidebar(); event.stopPropagation(); event.preventDefault(); } }, /** * Handles filtering the table */ filterItems() { let value = this.searchBox.value; this.table.filterItems(value, ["valueActor"]); this._panelDoc.documentElement.classList.toggle("filtering", !!value); }, /** * Handles endless scrolling for the table */ handleScrollEnd: function () { if (!this.shouldLoadMoreItems) { return; } this.shouldLoadMoreItems = false; this.itemOffset += 50; 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 currently selected storage object doesn't support removing items, prevent * showing the menu. */ onTablePopupShowing: function (event) { let selectedItem = this.tree.selectedItem; let type = selectedItem[0]; let actor = this.getCurrentActor(); // IndexedDB only supports removing items from object stores (level 4 of the tree) if (!actor.removeItem || (type === "indexedDB" && selectedItem.length !== 4)) { event.preventDefault(); return; } let rowId = this.table.contextMenuRowId; let data = this.table.items.get(rowId); let name = data[this.table.uniqueId]; let separatorRegex = new RegExp(SEPARATOR_GUID, "g"); let label = addEllipsis((name + "").replace(separatorRegex, "-")); this._tablePopupDelete.setAttribute("label", L10N.getFormatStr("storage.popupMenu.deleteLabel", label)); if (type === "cookies") { let host = addEllipsis(data.host); 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; 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), 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 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 || showDelete; } if (!showMenu) { event.preventDefault(); } }, /** * Handles removing an item from the storage */ onRemoveItem: function () { let [, host, ...path] = this.tree.selectedItem; let actor = this.getCurrentActor(); let rowId = this.table.contextMenuRowId; let data = this.table.items.get(rowId); let name = data[this.table.uniqueId]; if (path.length > 0) { name = JSON.stringify([...path, name]); } actor.removeItem(host, name); }, /** * 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, ...path] = this.tree.selectedItem; let actor = this.storageTypes[type]; let name = path.length > 0 ? JSON.stringify(path) : undefined; actor.removeAll(host, name); }, /** * 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); }, onRemoveTreeItem: function () { let [type, host, ...path] = this.tree.selectedItem; 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", dbName), "storage-idb-delete-blocked", null, notificationBox.PRIORITY_WARNING_LOW); } }).catch(error => { let notificationBox = this._toolbox.getNotificationBox(); notificationBox.appendNotification( 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 ])); }, };