Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / devtools / client / storage / ui.js
blob7a81ed80c599d1afdca1a0312be0441e1df6c19b
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
6 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
7 const { ELLIPSIS } = require("resource://devtools/shared/l10n.js");
8 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
9 const {
10 parseItemValue,
11 } = require("resource://devtools/shared/storage/utils.js");
12 const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
13 const {
14 getUnicodeHostname,
15 } = require("resource://devtools/client/shared/unicode-url.js");
16 const getStorageTypeURL = require("resource://devtools/client/storage/utils/doc-utils.js");
18 // GUID to be used as a separator in compound keys. This must match the same
19 // constant in devtools/server/actors/resources/storage/index.js,
20 // devtools/client/storage/test/head.js and
21 // devtools/server/tests/browser/head.js
22 const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
24 loader.lazyRequireGetter(
25 this,
26 "TreeWidget",
27 "resource://devtools/client/shared/widgets/TreeWidget.js",
28 true
30 loader.lazyRequireGetter(
31 this,
32 "TableWidget",
33 "resource://devtools/client/shared/widgets/TableWidget.js",
34 true
36 loader.lazyRequireGetter(
37 this,
38 "debounce",
39 "resource://devtools/shared/debounce.js",
40 true
42 loader.lazyGetter(this, "standardSessionString", () => {
43 const l10n = new Localization(["devtools/client/storage.ftl"], true);
44 return l10n.formatValueSync("storage-expires-session");
45 });
47 const lazy = {};
48 ChromeUtils.defineESModuleGetters(lazy, {
49 VariablesView: "resource://devtools/client/storage/VariablesView.sys.mjs",
50 });
52 const REASON = {
53 NEW_ROW: "new-row",
54 NEXT_50_ITEMS: "next-50-items",
55 POPULATE: "populate",
56 UPDATE: "update",
59 // How long we wait to debounce resize events
60 const LAZY_RESIZE_INTERVAL_MS = 200;
62 // Maximum length of item name to show in context menu label - will be
63 // trimmed with ellipsis if it's longer.
64 const ITEM_NAME_MAX_LENGTH = 32;
66 const HEADERS_L10N_IDS = {
67 Cache: {
68 status: "storage-table-headers-cache-status",
70 cookies: {
71 creationTime: "storage-table-headers-cookies-creation-time",
72 expires: "storage-table-headers-cookies-expires",
73 lastAccessed: "storage-table-headers-cookies-last-accessed",
74 name: "storage-table-headers-cookies-name",
75 size: "storage-table-headers-cookies-size",
76 value: "storage-table-headers-cookies-value",
78 extensionStorage: {
79 area: "storage-table-headers-extension-storage-area",
83 // We only localize certain table headers. The headers that we do not localize
84 // along with their label are stored in this dictionary for easy reference.
85 const HEADERS_NON_L10N_STRINGS = {
86 Cache: {
87 url: "URL",
89 cookies: {
90 host: "Domain",
91 hostOnly: "HostOnly",
92 isHttpOnly: "HttpOnly",
93 isSecure: "Secure",
94 partitionKey: "Partition Key",
95 path: "Path",
96 sameSite: "SameSite",
97 uniqueKey: "Unique key",
99 extensionStorage: {
100 name: "Key",
101 value: "Value",
103 indexedDB: {
104 autoIncrement: "Auto Increment",
105 db: "Database Name",
106 indexes: "Indexes",
107 keyPath: "Key Path",
108 name: "Key",
109 objectStore: "Object Store Name",
110 objectStores: "Object Stores",
111 origin: "Origin",
112 storage: "Storage",
113 uniqueKey: "Unique key",
114 value: "Value",
115 version: "Version",
117 localStorage: {
118 name: "Key",
119 value: "Value",
121 sessionStorage: {
122 name: "Key",
123 value: "Value",
128 * StorageUI is controls and builds the UI of the Storage Inspector.
130 * @param {Window} panelWin
131 * Window of the toolbox panel to populate UI in.
132 * @param {Object} commands
133 * The commands object with all interfaces defined from devtools/shared/commands/
135 class StorageUI {
136 constructor(panelWin, toolbox, commands) {
137 EventEmitter.decorate(this);
138 this._window = panelWin;
139 this._panelDoc = panelWin.document;
140 this._toolbox = toolbox;
141 this._commands = commands;
142 this.sidebarToggledOpen = null;
143 this.shouldLoadMoreItems = true;
145 const treeNode = this._panelDoc.getElementById("storage-tree");
146 this.tree = new TreeWidget(treeNode, {
147 defaultType: "dir",
148 contextMenuId: "storage-tree-popup",
150 this.onHostSelect = this.onHostSelect.bind(this);
151 this.tree.on("select", this.onHostSelect);
153 const tableNode = this._panelDoc.getElementById("storage-table");
154 this.table = new TableWidget(tableNode, {
155 emptyText: "storage-table-empty-text",
156 highlightUpdated: true,
157 cellContextMenuId: "storage-table-popup",
158 l10n: this._panelDoc.l10n,
161 this.updateObjectSidebar = this.updateObjectSidebar.bind(this);
162 this.table.on(TableWidget.EVENTS.ROW_SELECTED, this.updateObjectSidebar);
164 this.handleScrollEnd = this.loadMoreItems.bind(this);
165 this.table.on(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd);
167 this.editItem = this.editItem.bind(this);
168 this.table.on(TableWidget.EVENTS.CELL_EDIT, this.editItem);
170 this.sidebar = this._panelDoc.getElementById("storage-sidebar");
172 // Set suggested sizes for the xul:splitter's, so that the sidebar doesn't take too much space
173 // in horizontal mode (width) and vertical (height).
174 this.sidebar.style.width = "300px";
175 this.sidebar.style.height = "300px";
177 this.view = new lazy.VariablesView(this.sidebar.firstChild, {
178 lazyEmpty: true,
179 // ms
180 lazyEmptyDelay: 10,
181 searchEnabled: true,
182 contextMenuId: "variable-view-popup",
183 preventDescriptorModifiers: true,
186 this.filterItems = this.filterItems.bind(this);
187 this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this);
188 this.setupToolbar();
190 this.handleKeypress = this.handleKeypress.bind(this);
191 this._panelDoc.addEventListener("keypress", this.handleKeypress);
193 this.onTreePopupShowing = this.onTreePopupShowing.bind(this);
194 this._treePopup = this._panelDoc.getElementById("storage-tree-popup");
195 this._treePopup.addEventListener("popupshowing", this.onTreePopupShowing);
197 this.onTablePopupShowing = this.onTablePopupShowing.bind(this);
198 this._tablePopup = this._panelDoc.getElementById("storage-table-popup");
199 this._tablePopup.addEventListener("popupshowing", this.onTablePopupShowing);
201 this.onVariableViewPopupShowing =
202 this.onVariableViewPopupShowing.bind(this);
203 this._variableViewPopup = this._panelDoc.getElementById(
204 "variable-view-popup"
206 this._variableViewPopup.addEventListener(
207 "popupshowing",
208 this.onVariableViewPopupShowing
211 this.onRefreshTable = this.onRefreshTable.bind(this);
212 this.onAddItem = this.onAddItem.bind(this);
213 this.onCopyItem = this.onCopyItem.bind(this);
214 this.onPanelWindowResize = debounce(
215 this.#onLazyPanelResize,
216 LAZY_RESIZE_INTERVAL_MS,
217 this
219 this.onRemoveItem = this.onRemoveItem.bind(this);
220 this.onRemoveAllFrom = this.onRemoveAllFrom.bind(this);
221 this.onRemoveAll = this.onRemoveAll.bind(this);
222 this.onRemoveAllSessionCookies = this.onRemoveAllSessionCookies.bind(this);
223 this.onRemoveTreeItem = this.onRemoveTreeItem.bind(this);
225 this._refreshButton = this._panelDoc.getElementById("refresh-button");
226 this._refreshButton.addEventListener("click", this.onRefreshTable);
228 this._addButton = this._panelDoc.getElementById("add-button");
229 this._addButton.addEventListener("click", this.onAddItem);
231 this._window.addEventListener("resize", this.onPanelWindowResize, true);
233 this._variableViewPopupCopy = this._panelDoc.getElementById(
234 "variable-view-popup-copy"
236 this._variableViewPopupCopy.addEventListener("command", this.onCopyItem);
238 this._tablePopupAddItem = this._panelDoc.getElementById(
239 "storage-table-popup-add"
241 this._tablePopupAddItem.addEventListener("command", this.onAddItem);
243 this._tablePopupDelete = this._panelDoc.getElementById(
244 "storage-table-popup-delete"
246 this._tablePopupDelete.addEventListener("command", this.onRemoveItem);
248 this._tablePopupDeleteAllFrom = this._panelDoc.getElementById(
249 "storage-table-popup-delete-all-from"
251 this._tablePopupDeleteAllFrom.addEventListener(
252 "command",
253 this.onRemoveAllFrom
256 this._tablePopupDeleteAll = this._panelDoc.getElementById(
257 "storage-table-popup-delete-all"
259 this._tablePopupDeleteAll.addEventListener("command", this.onRemoveAll);
261 this._tablePopupDeleteAllSessionCookies = this._panelDoc.getElementById(
262 "storage-table-popup-delete-all-session-cookies"
264 this._tablePopupDeleteAllSessionCookies.addEventListener(
265 "command",
266 this.onRemoveAllSessionCookies
269 this._treePopupDeleteAll = this._panelDoc.getElementById(
270 "storage-tree-popup-delete-all"
272 this._treePopupDeleteAll.addEventListener("command", this.onRemoveAll);
274 this._treePopupDeleteAllSessionCookies = this._panelDoc.getElementById(
275 "storage-tree-popup-delete-all-session-cookies"
277 this._treePopupDeleteAllSessionCookies.addEventListener(
278 "command",
279 this.onRemoveAllSessionCookies
282 this._treePopupDelete = this._panelDoc.getElementById(
283 "storage-tree-popup-delete"
285 this._treePopupDelete.addEventListener("command", this.onRemoveTreeItem);
288 get currentTarget() {
289 return this._commands.targetCommand.targetFront;
292 async init() {
293 // This is a distionary of arrays, keyed by storage key
294 // - Keys are storage keys, available on each storage resource, via ${resource.resourceKey}
295 // and are typically "Cache", "cookies", "indexedDB", "localStorage", ...
296 // - Values are arrays of storage fronts. This isn't the deprecated global storage front (target.getFront(storage), only used by legacy listener),
297 // but rather the storage specific front, i.e. a storage resource. Storage resources are fronts.
298 this.storageResources = {};
300 await this._initL10NStringsMap();
302 // This can only be done after l10n strings were retrieved as we're using "storage-filter-key"
303 const shortcuts = new KeyShortcuts({
304 window: this._panelDoc.defaultView,
306 const key = this._l10nStrings.get("storage-filter-key");
307 shortcuts.on(key, event => {
308 event.preventDefault();
309 this.searchBox.focus();
312 this._onTargetAvailable = this._onTargetAvailable.bind(this);
313 this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
314 await this._commands.targetCommand.watchTargets({
315 types: [this._commands.targetCommand.TYPES.FRAME],
316 onAvailable: this._onTargetAvailable,
317 onDestroyed: this._onTargetDestroyed,
320 this._onResourceListAvailable = this._onResourceListAvailable.bind(this);
322 const { resourceCommand } = this._toolbox;
324 this._listenedResourceTypes = [
325 // The first item in this list will be the first selected storage item
326 // Tests assume Cookie -- moving cookie will break tests
327 resourceCommand.TYPES.COOKIE,
328 resourceCommand.TYPES.CACHE_STORAGE,
329 resourceCommand.TYPES.INDEXED_DB,
330 resourceCommand.TYPES.LOCAL_STORAGE,
331 resourceCommand.TYPES.SESSION_STORAGE,
333 // EXTENSION_STORAGE is only relevant when debugging web extensions
334 if (this._commands.descriptorFront.isWebExtensionDescriptor) {
335 this._listenedResourceTypes.push(resourceCommand.TYPES.EXTENSION_STORAGE);
337 await this._toolbox.resourceCommand.watchResources(
338 this._listenedResourceTypes,
340 onAvailable: this._onResourceListAvailable,
345 async _initL10NStringsMap() {
346 const ids = [
347 "storage-filter-key",
348 "storage-table-headers-cookies-name",
349 "storage-table-headers-cookies-value",
350 "storage-table-headers-cookies-expires",
351 "storage-table-headers-cookies-size",
352 "storage-table-headers-cookies-last-accessed",
353 "storage-table-headers-cookies-creation-time",
354 "storage-table-headers-cache-status",
355 "storage-table-headers-extension-storage-area",
356 "storage-tree-labels-cookies",
357 "storage-tree-labels-local-storage",
358 "storage-tree-labels-session-storage",
359 "storage-tree-labels-indexed-db",
360 "storage-tree-labels-cache",
361 "storage-tree-labels-extension-storage",
362 "storage-expires-session",
364 const results = await this._panelDoc.l10n.formatValues(
365 ids.map(s => ({ id: s }))
368 this._l10nStrings = new Map(ids.map((id, i) => [id, results[i]]));
371 async _onResourceListAvailable(resources) {
372 for (const resource of resources) {
373 if (resource.isDestroyed()) {
374 continue;
376 const { resourceKey } = resource;
378 // NOTE: We might be getting more than 1 resource per storage type when
379 // we have remote frames in content process resources, so we need
380 // an array to store these.
381 if (!this.storageResources[resourceKey]) {
382 this.storageResources[resourceKey] = [];
384 this.storageResources[resourceKey].push(resource);
386 resource.on(
387 "single-store-update",
388 this._onStoreUpdate.bind(this, resource)
390 resource.on(
391 "single-store-cleared",
392 this._onStoreCleared.bind(this, resource)
396 try {
397 await this.populateStorageTree();
398 } catch (e) {
399 if (!this._toolbox || this._toolbox._destroyer) {
400 // The toolbox is in the process of being destroyed... in this case throwing here
401 // is expected and normal so let's ignore the error.
402 return;
405 // The toolbox is open so the error is unexpected and real so let's log it.
406 console.error(e);
410 // We only need to listen to target destruction, but TargetCommand.watchTarget
411 // requires a target available function...
412 async _onTargetAvailable() {}
414 _onTargetDestroyed({ targetFront }) {
415 // Remove all storages related to this target
416 for (const type in this.storageResources) {
417 this.storageResources[type] = this.storageResources[type].filter(
418 storage => {
419 // Note that the storage front may already be destroyed,
420 // and have a null targetFront attribute. So also remove all already
421 // destroyed fronts.
422 return !storage.isDestroyed() && storage.targetFront != targetFront;
427 // Only support top level target and navigation to new processes.
428 // i.e. ignore additional targets created for remote <iframes>
429 if (!targetFront.isTopLevel) {
430 return;
433 this.storageResources = {};
434 this.table.clear();
435 this.hideSidebar();
436 this.tree.clear();
439 set animationsEnabled(value) {
440 this._panelDoc.documentElement.classList.toggle("no-animate", !value);
443 destroy() {
444 if (this._destroyed) {
445 return;
447 this._destroyed = true;
449 const { resourceCommand } = this._toolbox;
450 resourceCommand.unwatchResources(this._listenedResourceTypes, {
451 onAvailable: this._onResourceListAvailable,
454 this.table.off(TableWidget.EVENTS.ROW_SELECTED, this.updateObjectSidebar);
455 this.table.off(TableWidget.EVENTS.SCROLL_END, this.loadMoreItems);
456 this.table.off(TableWidget.EVENTS.CELL_EDIT, this.editItem);
457 this.table.destroy();
459 this._panelDoc.removeEventListener("keypress", this.handleKeypress);
460 this.searchBox.removeEventListener("input", this.filterItems);
461 this.searchBox = null;
463 this.sidebarToggleBtn.removeEventListener(
464 "click",
465 this.onPaneToggleButtonClicked
467 this.sidebarToggleBtn = null;
469 this._window.removeEventListener("resize", this.#onLazyPanelResize, true);
471 this._treePopup.removeEventListener(
472 "popupshowing",
473 this.onTreePopupShowing
475 this._refreshButton.removeEventListener("click", this.onRefreshTable);
476 this._addButton.removeEventListener("click", this.onAddItem);
477 this._tablePopupAddItem.removeEventListener("command", this.onAddItem);
478 this._treePopupDeleteAll.removeEventListener("command", this.onRemoveAll);
479 this._treePopupDeleteAllSessionCookies.removeEventListener(
480 "command",
481 this.onRemoveAllSessionCookies
483 this._treePopupDelete.removeEventListener("command", this.onRemoveTreeItem);
485 this._tablePopup.removeEventListener(
486 "popupshowing",
487 this.onTablePopupShowing
489 this._tablePopupDelete.removeEventListener("command", this.onRemoveItem);
490 this._tablePopupDeleteAllFrom.removeEventListener(
491 "command",
492 this.onRemoveAllFrom
494 this._tablePopupDeleteAll.removeEventListener("command", this.onRemoveAll);
495 this._tablePopupDeleteAllSessionCookies.removeEventListener(
496 "command",
497 this.onRemoveAllSessionCookies
501 setupToolbar() {
502 this.searchBox = this._panelDoc.getElementById("storage-searchbox");
503 this.searchBox.addEventListener("input", this.filterItems);
505 // Setup the sidebar toggle button.
506 this.sidebarToggleBtn = this._panelDoc.querySelector(".sidebar-toggle");
507 this.updateSidebarToggleButton();
509 this.sidebarToggleBtn.addEventListener(
510 "click",
511 this.onPaneToggleButtonClicked
515 onPaneToggleButtonClicked() {
516 if (this.sidebar.hidden && this.table.selectedRow) {
517 this.sidebar.hidden = false;
518 this.sidebarToggledOpen = true;
519 this.updateSidebarToggleButton();
520 } else {
521 this.sidebarToggledOpen = false;
522 this.hideSidebar();
526 updateSidebarToggleButton() {
527 let dataL10nId;
528 this.sidebarToggleBtn.hidden = !this.table.hasSelectedRow;
530 if (this.sidebar.hidden) {
531 this.sidebarToggleBtn.classList.add("pane-collapsed");
532 dataL10nId = "storage-expand-pane";
533 } else {
534 this.sidebarToggleBtn.classList.remove("pane-collapsed");
535 dataL10nId = "storage-collapse-pane";
538 this._panelDoc.l10n.setAttributes(this.sidebarToggleBtn, dataL10nId);
542 * Hide the object viewer sidebar
544 hideSidebar() {
545 this.sidebar.hidden = true;
546 this.updateSidebarToggleButton();
549 getCurrentFront() {
550 const { datatype, host } = this.table;
551 return this._getStorage(datatype, host);
554 _getStorage(type, host) {
555 const storageType = this.storageResources[type];
556 return storageType.find(x => host in x.hosts);
560 * Make column fields editable
562 * @param {Array} editableFields
563 * An array of keys of columns to be made editable
565 makeFieldsEditable(editableFields) {
566 if (editableFields && editableFields.length) {
567 this.table.makeFieldsEditable(editableFields);
568 } else if (this.table._editableFieldsEngine) {
569 this.table._editableFieldsEngine.destroy();
573 editItem(data) {
574 const selectedItem = this.tree.selectedItem;
575 if (!selectedItem) {
576 return;
578 const front = this.getCurrentFront();
580 front.editItem(data);
584 * Removes the given item from the storage table. Reselects the next item in
585 * the table and repopulates the sidebar with that item's data if the item
586 * being removed was selected.
588 async removeItemFromTable(name) {
589 if (this.table.isSelected(name) && this.table.items.size > 1) {
590 if (this.table.selectedIndex == 0) {
591 this.table.selectNextRow();
592 } else {
593 this.table.selectPreviousRow();
597 this.table.remove(name);
598 await this.updateObjectSidebar();
602 * Event handler for "stores-cleared" event coming from the storage actor.
604 * @param {object}
605 * An object containing which hosts/paths are cleared from a
606 * storage
608 _onStoreCleared(resource, { clearedHostsOrPaths }) {
609 const { resourceKey } = resource;
610 function* enumPaths() {
611 if (Array.isArray(clearedHostsOrPaths)) {
612 // Handle the legacy response with array of hosts
613 for (const host of clearedHostsOrPaths) {
614 yield [host];
616 } else {
617 // Handle the new format that supports clearing sub-stores in a host
618 for (const host in clearedHostsOrPaths) {
619 const paths = clearedHostsOrPaths[host];
621 if (!paths.length) {
622 yield [host];
623 } else {
624 for (let path of paths) {
625 try {
626 path = JSON.parse(path);
627 yield [host, ...path];
628 } catch (ex) {
629 // ignore
637 for (const path of enumPaths()) {
638 // Find if the path is selected (there is max one) and clear it
639 if (this.tree.isSelected([resourceKey, ...path])) {
640 this.table.clear();
641 this.hideSidebar();
643 // Reset itemOffset to 0 so that items added after local storate is
644 // cleared will be shown
645 this.itemOffset = 0;
647 this.emit("store-objects-cleared");
648 break;
654 * Event handler for "stores-update" event coming from the storage actor.
656 * @param {object} argument0
657 * An object containing the details of the added, changed and deleted
658 * storage objects.
659 * Each of these 3 objects are of the following format:
661 * <store_type1>: {
662 * <host1>: [<store_names1>, <store_name2>...],
663 * <host2>: [<store_names34>...], ...
664 * },
665 * <store_type2>: {
666 * <host1>: [<store_names1>, <store_name2>...],
667 * <host2>: [<store_names34>...], ...
668 * }, ...
670 * Where store_type1 and store_type2 is one of cookies, indexedDB,
671 * sessionStorage and localStorage; host1, host2 are the host in which
672 * this change happened; and [<store_namesX] is an array of the names
673 * of the changed store objects. This array is empty for deleted object
674 * if the host was completely removed.
676 async _onStoreUpdate(resource, update) {
677 const { changed, added, deleted } = update;
678 if (added) {
679 await this.handleAddedItems(added);
682 if (changed) {
683 await this.handleChangedItems(changed);
686 // We are dealing with batches of changes here. Deleted **MUST** come last in case it
687 // is in the same batch as added and changed events e.g.
688 // - An item is changed then deleted in the same batch: deleted then changed will
689 // display an item that has been deleted.
690 // - An item is added then deleted in the same batch: deleted then added will
691 // display an item that has been deleted.
692 if (deleted) {
693 await this.handleDeletedItems(deleted);
696 if (added || deleted || changed) {
697 this.emit("store-objects-edit");
702 * If the panel is resized we need to check if we should load the next batch of
703 * storage items.
705 async #onLazyPanelResize() {
706 // We can be called on a closed window or destroyed toolbox because of the
707 // deferred task.
708 if (this._window.closed || this._destroyed || this.table.hasScrollbar) {
709 return;
712 await this.loadMoreItems();
713 this.emit("storage-resize");
717 * Get a string for a column name automatically choosing whether or not the
718 * string should be localized.
720 * @param {String} type
721 * The storage type.
722 * @param {String} name
723 * The field name that may need to be localized.
725 _getColumnName(type, name) {
726 // If the ID exists in HEADERS_NON_L10N_STRINGS then we do not translate it
727 const columnName = HEADERS_NON_L10N_STRINGS[type]?.[name];
728 if (columnName) {
729 return columnName;
732 // otherwise we get it from the L10N Map (populated during init)
733 const l10nId = HEADERS_L10N_IDS[type]?.[name];
734 if (l10nId && this._l10nStrings.has(l10nId)) {
735 return this._l10nStrings.get(l10nId);
738 // If the string isn't localized, we will just use the field name.
739 return name;
743 * Handle added items received by onEdit
745 * @param {object} See onEdit docs
747 async handleAddedItems(added) {
748 for (const type in added) {
749 for (const host in added[type]) {
750 const label = this.getReadableLabelFromHostname(host);
751 this.tree.add([type, { id: host, label, type: "url" }]);
752 for (let name of added[type][host]) {
753 try {
754 name = JSON.parse(name);
755 if (name.length == 3) {
756 name.splice(2, 1);
758 this.tree.add([type, host, ...name]);
759 if (!this.tree.selectedItem) {
760 this.tree.selectedItem = [type, host, name[0], name[1]];
761 await this.fetchStorageObjects(
762 type,
763 host,
764 [JSON.stringify(name)],
765 REASON.NEW_ROW
768 } catch (ex) {
769 // Do nothing
773 if (this.tree.isSelected([type, host])) {
774 await this.fetchStorageObjects(
775 type,
776 host,
777 added[type][host],
778 REASON.NEW_ROW
786 * Handle deleted items received by onEdit
788 * @param {object} See onEdit docs
790 async handleDeletedItems(deleted) {
791 for (const type in deleted) {
792 for (const host in deleted[type]) {
793 if (!deleted[type][host].length) {
794 // This means that the whole host is deleted, thus the item should
795 // be removed from the storage tree
796 if (this.tree.isSelected([type, host])) {
797 this.table.clear();
798 this.hideSidebar();
799 this.tree.selectPreviousItem();
802 this.tree.remove([type, host]);
803 } else {
804 for (const name of deleted[type][host]) {
805 try {
806 if (["indexedDB", "Cache"].includes(type)) {
807 // For indexedDB and Cache, the key is being parsed because
808 // these storages are represented as a tree and the key
809 // used to notify their changes is not a simple string.
810 const names = JSON.parse(name);
811 // Is a whole cache, database or objectstore deleted?
812 // Then remove it from the tree.
813 if (names.length < 3) {
814 if (this.tree.isSelected([type, host, ...names])) {
815 this.table.clear();
816 this.hideSidebar();
817 this.tree.selectPreviousItem();
819 this.tree.remove([type, host, ...names]);
822 // Remove the item from table if currently displayed.
823 if (names.length) {
824 const tableItemName = names.pop();
825 if (this.tree.isSelected([type, host, ...names])) {
826 await this.removeItemFromTable(tableItemName);
829 } else if (this.tree.isSelected([type, host])) {
830 // For all the other storage types with a simple string key,
831 // remove the item from the table by name without any parsing.
832 await this.removeItemFromTable(name);
834 } catch (ex) {
835 if (this.tree.isSelected([type, host])) {
836 await this.removeItemFromTable(name);
846 * Handle changed items received by onEdit
848 * @param {object} See onEdit docs
850 async handleChangedItems(changed) {
851 const selectedItem = this.tree.selectedItem;
852 if (!selectedItem) {
853 return;
856 const [type, host, db, objectStore] = selectedItem;
857 if (!changed[type] || !changed[type][host] || !changed[type][host].length) {
858 return;
860 try {
861 const toUpdate = [];
862 for (const name of changed[type][host]) {
863 if (["indexedDB", "Cache"].includes(type)) {
864 // For indexedDB and Cache, the key is being parsed because
865 // these storage are represented as a tree and the key
866 // used to notify their changes is not a simple string.
867 const names = JSON.parse(name);
868 if (names[0] == db && names[1] == objectStore && names[2]) {
869 toUpdate.push(name);
871 } else {
872 // For all the other storage types with a simple string key,
873 // update the item from the table by name without any parsing.
874 toUpdate.push(name);
877 await this.fetchStorageObjects(type, host, toUpdate, REASON.UPDATE);
878 } catch (ex) {
879 await this.fetchStorageObjects(
880 type,
881 host,
882 changed[type][host],
883 REASON.UPDATE
889 * Fetches the storage objects from the storage actor and populates the
890 * storage table with the returned data.
892 * @param {string} type
893 * The type of storage. Ex. "cookies"
894 * @param {string} host
895 * Hostname
896 * @param {array} names
897 * Names of particular store objects. Empty if all are requested
898 * @param {Constant} reason
899 * See REASON constant at top of file.
901 async fetchStorageObjects(type, host, names, reason) {
902 const fetchOpts =
903 reason === REASON.NEXT_50_ITEMS ? { offset: this.itemOffset } : {};
904 fetchOpts.sessionString = standardSessionString;
905 const storage = this._getStorage(type, host);
906 this.sidebarToggledOpen = null;
908 if (
909 reason !== REASON.NEXT_50_ITEMS &&
910 reason !== REASON.UPDATE &&
911 reason !== REASON.NEW_ROW &&
912 reason !== REASON.POPULATE
914 throw new Error("Invalid reason specified");
917 try {
918 if (
919 reason === REASON.POPULATE ||
920 (reason === REASON.NEW_ROW && this.table.items.size === 0)
922 let subType = null;
923 // The indexedDB type could have sub-type data to fetch.
924 // If having names specified, then it means
925 // we are fetching details of specific database or of object store.
926 if (type === "indexedDB" && names) {
927 const [dbName, objectStoreName] = JSON.parse(names[0]);
928 if (dbName) {
929 subType = "database";
931 if (objectStoreName) {
932 subType = "object store";
936 await this.resetColumns(type, host, subType);
939 const { data, total } = await storage.getStoreObjects(
940 host,
941 names,
942 fetchOpts
944 if (data.length) {
945 await this.populateTable(data, reason, total);
946 } else if (reason === REASON.POPULATE) {
947 await this.clearHeaders();
949 this.updateToolbar();
950 this.emit("store-objects-updated");
951 } catch (ex) {
952 console.error(ex);
956 supportsAddItem(type, host) {
957 const storage = this._getStorage(type, host);
958 return storage?.traits.supportsAddItem || false;
961 supportsRemoveItem(type, host) {
962 const storage = this._getStorage(type, host);
963 return storage?.traits.supportsRemoveItem || false;
966 supportsRemoveAll(type, host) {
967 const storage = this._getStorage(type, host);
968 return storage?.traits.supportsRemoveAll || false;
971 supportsRemoveAllSessionCookies(type, host) {
972 const storage = this._getStorage(type, host);
973 return storage?.traits.supportsRemoveAllSessionCookies || false;
977 * Updates the toolbar hiding and showing buttons as appropriate.
979 updateToolbar() {
980 const item = this.tree.selectedItem;
981 if (!item) {
982 return;
985 const [type, host] = item;
987 // Add is only supported if the selected item has a host.
988 this._addButton.hidden = !host || !this.supportsAddItem(type, host);
992 * Populates the storage tree which displays the list of storages present for
993 * the page.
995 async populateStorageTree() {
996 const populateTreeFromResource = (type, resource) => {
997 for (const host in resource.hosts) {
998 const label = this.getReadableLabelFromHostname(host);
999 this.tree.add([type, { id: host, label, type: "url" }]);
1000 for (const name of resource.hosts[host]) {
1001 try {
1002 const names = JSON.parse(name);
1003 this.tree.add([type, host, ...names]);
1004 if (!this.tree.selectedItem) {
1005 this.tree.selectedItem = [type, host, names[0], names[1]];
1007 } catch (ex) {
1008 // Do Nothing
1011 if (!this.tree.selectedItem) {
1012 this.tree.selectedItem = [type, host];
1017 // When can we expect the "store-objects-updated" event?
1018 // -> TreeWidget setter `selectedItem` emits a "select" event
1019 // -> on tree "select" event, this module calls `onHostSelect`
1020 // -> finally `onHostSelect` calls `fetchStorageObjects`, which will emit
1021 // "store-objects-updated" at the end of the method.
1022 // So if the selection changed, we can wait for "store-objects-updated",
1023 // which is emitted at the end of `fetchStorageObjects`.
1024 const onStoresObjectsUpdated = this.once("store-objects-updated");
1026 // Save the initially selected item to check if tree.selected was updated,
1027 // see comment above.
1028 const initialSelectedItem = this.tree.selectedItem;
1030 for (const [type, resources] of Object.entries(this.storageResources)) {
1031 let typeLabel = type;
1032 try {
1033 typeLabel = this.getStorageTypeLabel(type);
1034 } catch (e) {
1035 console.error("Unable to localize tree label type:" + type);
1038 this.tree.add([{ id: type, label: typeLabel, type: "store" }]);
1040 // storageResources values are arrays, with storage resources.
1041 // we may have many storage resources per type if we get remote iframes.
1042 for (const resource of resources) {
1043 populateTreeFromResource(type, resource);
1047 if (initialSelectedItem !== this.tree.selectedItem) {
1048 await onStoresObjectsUpdated;
1052 getStorageTypeLabel(type) {
1053 let dataL10nId;
1055 switch (type) {
1056 case "cookies":
1057 dataL10nId = "storage-tree-labels-cookies";
1058 break;
1059 case "localStorage":
1060 dataL10nId = "storage-tree-labels-local-storage";
1061 break;
1062 case "sessionStorage":
1063 dataL10nId = "storage-tree-labels-session-storage";
1064 break;
1065 case "indexedDB":
1066 dataL10nId = "storage-tree-labels-indexed-db";
1067 break;
1068 case "Cache":
1069 dataL10nId = "storage-tree-labels-cache";
1070 break;
1071 case "extensionStorage":
1072 dataL10nId = "storage-tree-labels-extension-storage";
1073 break;
1074 default:
1075 throw new Error("Unknown storage type");
1078 return this._l10nStrings.get(dataL10nId);
1082 * Populates the selected entry from the table in the sidebar for a more
1083 * detailed view.
1085 /* eslint-disable-next-line */
1086 async updateObjectSidebar() {
1087 const item = this.table.selectedRow;
1088 let value;
1090 // Get the string value (async action) and the update the UI synchronously.
1091 if ((item?.name || item?.name === "") && item?.valueActor) {
1092 value = await item.valueActor.string();
1095 // Bail if the selectedRow is no longer selected, the item doesn't exist or the state
1096 // changed in another way during the above yield.
1097 if (
1098 this.table.items.size === 0 ||
1099 !item ||
1100 !this.table.selectedRow ||
1101 item.uniqueKey !== this.table.selectedRow.uniqueKey
1103 this.hideSidebar();
1104 return;
1107 // Start updating the UI. Everything is sync beyond this point.
1108 if (this.sidebarToggledOpen === null || this.sidebarToggledOpen === true) {
1109 this.sidebar.hidden = false;
1112 this.updateSidebarToggleButton();
1113 this.view.empty();
1114 const mainScope = this.view.addScope("storage-data");
1115 mainScope.expanded = true;
1117 if (value) {
1118 const itemVar = mainScope.addItem(item.name + "", {}, { relaxed: true });
1120 // The main area where the value will be displayed
1121 itemVar.setGrip(value);
1123 // May be the item value is a json or a key value pair itself
1124 const obj = parseItemValue(value);
1125 if (typeof obj === "object") {
1126 this.populateSidebar(item.name, obj);
1129 // By default the item name and value are shown. If this is the only
1130 // information available, then nothing else is to be displayed.
1131 const itemProps = Object.keys(item);
1132 if (itemProps.length > 3) {
1133 // Display any other information other than the item name and value
1134 // which may be available.
1135 const rawObject = Object.create(null);
1136 const otherProps = itemProps.filter(
1137 e => !["name", "value", "valueActor"].includes(e)
1139 for (const prop of otherProps) {
1140 const column = this.table.columns.get(prop);
1141 if (column?.private) {
1142 continue;
1145 const fieldName = this._getColumnName(this.table.datatype, prop);
1146 rawObject[fieldName] = item[prop];
1148 itemVar.populate(rawObject, { sorted: true });
1149 itemVar.twisty = true;
1150 itemVar.expanded = true;
1152 } else {
1153 // Case when displaying IndexedDB db/object store properties.
1154 for (const key in item) {
1155 const column = this.table.columns.get(key);
1156 if (column?.private) {
1157 continue;
1160 mainScope.addItem(key, {}, true).setGrip(item[key]);
1161 const obj = parseItemValue(item[key]);
1162 if (typeof obj === "object") {
1163 this.populateSidebar(item.name, obj);
1168 this.emit("sidebar-updated");
1172 * Gets a readable label from the hostname. If the hostname is a Punycode
1173 * domain(I.e. an ASCII domain name representing a Unicode domain name), then
1174 * this function decodes it to the readable Unicode domain name, and label
1175 * the Unicode domain name toggether with the original domian name, and then
1176 * return the label; if the hostname isn't a Punycode domain(I.e. it isn't
1177 * encoded and is readable on its own), then this function simply returns the
1178 * original hostname.
1180 * @param {string} host
1181 * The string representing a host, e.g, example.com, example.com:8000
1183 getReadableLabelFromHostname(host) {
1184 try {
1185 const { hostname } = new URL(host);
1186 const unicodeHostname = getUnicodeHostname(hostname);
1187 if (hostname !== unicodeHostname) {
1188 // If the hostname is a Punycode domain representing a Unicode domain,
1189 // we decode it to the Unicode domain name, and then label the Unicode
1190 // domain name together with the original domain name.
1191 return host.replace(hostname, unicodeHostname) + " [ " + host + " ]";
1193 } catch (_) {
1194 // Skip decoding for a host which doesn't include a domain name, simply
1195 // consider them to be readable.
1197 return host;
1201 * Populates the sidebar with a parsed object.
1203 * @param {object} obj - Either a json or a key-value separated object or a
1204 * key separated array
1206 populateSidebar(name, obj) {
1207 const jsonObject = Object.create(null);
1208 const view = this.view;
1209 jsonObject[name] = obj;
1210 const valueScope =
1211 view.getScopeAtIndex(1) || view.addScope("storage-parsed-value");
1212 valueScope.expanded = true;
1213 const jsonVar = valueScope.addItem("", Object.create(null), {
1214 relaxed: true,
1216 jsonVar.expanded = true;
1217 jsonVar.twisty = true;
1218 jsonVar.populate(jsonObject, { expanded: true });
1222 * Select handler for the storage tree. Fetches details of the selected item
1223 * from the storage details and populates the storage tree.
1225 * @param {array} item
1226 * An array of ids which represent the location of the selected item in
1227 * the storage tree
1229 async onHostSelect(item) {
1230 if (!item) {
1231 return;
1234 this.table.clear();
1235 this.hideSidebar();
1236 this.searchBox.value = "";
1238 const [type, host] = item;
1239 this.table.host = host;
1240 this.table.datatype = type;
1242 this.updateToolbar();
1244 let names = null;
1245 if (!host) {
1246 let storageTypeHintL10nId = "";
1247 switch (type) {
1248 case "Cache":
1249 storageTypeHintL10nId = "storage-table-type-cache-hint";
1250 break;
1251 case "cookies":
1252 storageTypeHintL10nId = "storage-table-type-cookies-hint";
1253 break;
1254 case "extensionStorage":
1255 storageTypeHintL10nId = "storage-table-type-extensionstorage-hint";
1256 break;
1257 case "localStorage":
1258 storageTypeHintL10nId = "storage-table-type-localstorage-hint";
1259 break;
1260 case "indexedDB":
1261 storageTypeHintL10nId = "storage-table-type-indexeddb-hint";
1262 break;
1263 case "sessionStorage":
1264 storageTypeHintL10nId = "storage-table-type-sessionstorage-hint";
1265 break;
1267 this.table.setPlaceholder(
1268 storageTypeHintL10nId,
1269 getStorageTypeURL(this.table.datatype)
1272 // If selected item has no host then reset table headers
1273 await this.clearHeaders();
1274 return;
1276 if (item.length > 2) {
1277 names = [JSON.stringify(item.slice(2))];
1280 this.itemOffset = 0;
1281 await this.fetchStorageObjects(type, host, names, REASON.POPULATE);
1285 * Clear the column headers in the storage table
1287 async clearHeaders() {
1288 this.table.setColumns({}, null, {}, {});
1292 * Resets the column headers in the storage table with the pased object `data`
1294 * @param {string} type
1295 * The type of storage corresponding to the after-reset columns in the
1296 * table.
1297 * @param {string} host
1298 * The host name corresponding to the table after reset.
1300 * @param {string} [subType]
1301 * The sub type under the given type.
1303 async resetColumns(type, host, subtype) {
1304 this.table.host = host;
1305 this.table.datatype = type;
1307 let uniqueKey = null;
1308 const columns = {};
1309 const editableFields = [];
1310 const hiddenFields = [];
1311 const privateFields = [];
1312 const fields = await this.getCurrentFront().getFields(subtype);
1314 fields.forEach(f => {
1315 if (!uniqueKey) {
1316 this.table.uniqueId = uniqueKey = f.name;
1319 if (f.editable) {
1320 editableFields.push(f.name);
1323 if (f.hidden) {
1324 hiddenFields.push(f.name);
1327 if (f.private) {
1328 privateFields.push(f.name);
1331 const columnName = this._getColumnName(type, f.name);
1332 if (columnName) {
1333 columns[f.name] = columnName;
1334 } else if (!f.private) {
1335 // Private fields are only displayed when running tests so there is no
1336 // need to log an error if they are not localized.
1337 columns[f.name] = f.name;
1338 console.error(
1339 `No string defined in HEADERS_NON_L10N_STRINGS for '${type}.${f.name}'`
1344 this.table.setColumns(columns, null, hiddenFields, privateFields);
1345 this.hideSidebar();
1347 this.makeFieldsEditable(editableFields);
1351 * Populates or updates the rows in the storage table.
1353 * @param {array[object]} data
1354 * Array of objects to be populated in the storage table
1355 * @param {Constant} reason
1356 * See REASON constant at top of file.
1357 * @param {number} totalAvailable
1358 * The total number of items available in the current storage type.
1360 async populateTable(data, reason, totalAvailable) {
1361 for (const item of data) {
1362 if (item.value) {
1363 item.valueActor = item.value;
1364 item.value = item.value.initial || "";
1366 if (item.expires != null) {
1367 item.expires = item.expires
1368 ? new Date(item.expires).toUTCString()
1369 : this._l10nStrings.get("storage-expires-session");
1371 if (item.creationTime != null) {
1372 item.creationTime = new Date(item.creationTime).toUTCString();
1374 if (item.lastAccessed != null) {
1375 item.lastAccessed = new Date(item.lastAccessed).toUTCString();
1378 switch (reason) {
1379 case REASON.POPULATE:
1380 case REASON.NEXT_50_ITEMS:
1381 // Update without flashing the row.
1382 this.table.push(item, true);
1383 break;
1384 case REASON.NEW_ROW:
1385 // Update and flash the row.
1386 this.table.push(item, false);
1387 break;
1388 case REASON.UPDATE:
1389 this.table.update(item);
1390 if (item == this.table.selectedRow && !this.sidebar.hidden) {
1391 await this.updateObjectSidebar();
1393 break;
1396 this.shouldLoadMoreItems = true;
1399 if (
1400 (reason === REASON.POPULATE || reason === REASON.NEXT_50_ITEMS) &&
1401 this.table.items.size < totalAvailable &&
1402 !this.table.hasScrollbar
1404 await this.loadMoreItems();
1409 * Handles keypress event on the body table to close the sidebar when open
1411 * @param {DOMEvent} event
1412 * The event passed by the keypress event.
1414 handleKeypress(event) {
1415 if (event.keyCode == KeyCodes.DOM_VK_ESCAPE) {
1416 if (!this.sidebar.hidden) {
1417 this.hideSidebar();
1418 this.sidebarToggledOpen = false;
1419 // Stop Propagation to prevent opening up of split console
1420 event.stopPropagation();
1421 event.preventDefault();
1423 } else if (
1424 event.keyCode == KeyCodes.DOM_VK_BACK_SPACE ||
1425 event.keyCode == KeyCodes.DOM_VK_DELETE
1427 if (this.table.selectedRow && event.target.localName != "input") {
1428 this.onRemoveItem(event);
1429 event.stopPropagation();
1430 event.preventDefault();
1436 * Handles filtering the table
1438 filterItems() {
1439 const value = this.searchBox.value;
1440 this.table.filterItems(value, ["valueActor"]);
1441 this._panelDoc.documentElement.classList.toggle("filtering", !!value);
1445 * Load the next batch of 50 items
1447 async loadMoreItems() {
1448 if (
1449 !this.shouldLoadMoreItems ||
1450 this._toolbox.currentToolId !== "storage" ||
1451 !this.tree.selectedItem
1453 return;
1455 this.shouldLoadMoreItems = false;
1456 this.itemOffset += 50;
1458 const item = this.tree.selectedItem;
1459 const [type, host] = item;
1460 let names = null;
1461 if (item.length > 2) {
1462 names = [JSON.stringify(item.slice(2))];
1464 await this.fetchStorageObjects(type, host, names, REASON.NEXT_50_ITEMS);
1468 * Fires before a cell context menu with the "Add" or "Delete" action is
1469 * shown. If the currently selected storage object doesn't support adding or
1470 * removing items, prevent showing the menu.
1472 onTablePopupShowing(event) {
1473 const selectedItem = this.tree.selectedItem;
1474 const [type, host] = selectedItem;
1476 // IndexedDB only supports removing items from object stores (level 4 of the tree)
1477 if (
1478 (!this.supportsAddItem(type, host) &&
1479 !this.supportsRemoveItem(type, host)) ||
1480 (type === "indexedDB" && selectedItem.length !== 4)
1482 event.preventDefault();
1483 return;
1486 const rowId = this.table.contextMenuRowId;
1487 const data = this.table.items.get(rowId);
1489 if (this.supportsRemoveItem(type, host)) {
1490 const name = data[this.table.uniqueId];
1491 const separatorRegex = new RegExp(SEPARATOR_GUID, "g");
1492 const label = addEllipsis((name + "").replace(separatorRegex, "-"));
1494 this._panelDoc.l10n.setArgs(this._tablePopupDelete, { itemName: label });
1495 this._tablePopupDelete.hidden = false;
1496 } else {
1497 this._tablePopupDelete.hidden = true;
1500 this._tablePopupAddItem.hidden = !this.supportsAddItem(type, host);
1502 let showDeleteAllSessionCookies = false;
1503 if (this.supportsRemoveAllSessionCookies(type, host)) {
1504 if (selectedItem.length === 2) {
1505 showDeleteAllSessionCookies = true;
1509 this._tablePopupDeleteAllSessionCookies.hidden =
1510 !showDeleteAllSessionCookies;
1512 if (type === "cookies") {
1513 const hostString = addEllipsis(data.host);
1515 this._panelDoc.l10n.setArgs(this._tablePopupDeleteAllFrom, {
1516 host: hostString,
1518 this._tablePopupDeleteAllFrom.hidden = false;
1519 } else {
1520 this._tablePopupDeleteAllFrom.hidden = true;
1524 onTreePopupShowing(event) {
1525 let showMenu = false;
1526 const selectedItem = this.tree.selectedItem;
1528 if (selectedItem) {
1529 const [type, host] = selectedItem;
1531 // The delete all (aka clear) action is displayed for IndexedDB object stores
1532 // (level 4 of tree), for Cache objects (level 3) and for the whole host (level 2)
1533 // for other storage types (cookies, localStorage, ...).
1534 let showDeleteAll = false;
1535 if (this.supportsRemoveAll(type, host)) {
1536 let level;
1537 if (type == "indexedDB") {
1538 level = 4;
1539 } else if (type == "Cache") {
1540 level = 3;
1541 } else {
1542 level = 2;
1545 if (selectedItem.length == level) {
1546 showDeleteAll = true;
1550 this._treePopupDeleteAll.hidden = !showDeleteAll;
1552 // The delete all session cookies action is displayed for cookie object stores
1553 // (level 2 of tree)
1554 let showDeleteAllSessionCookies = false;
1555 if (this.supportsRemoveAllSessionCookies(type, host)) {
1556 if (type === "cookies" && selectedItem.length === 2) {
1557 showDeleteAllSessionCookies = true;
1561 this._treePopupDeleteAllSessionCookies.hidden =
1562 !showDeleteAllSessionCookies;
1564 // The delete action is displayed for:
1565 // - IndexedDB databases (level 3 of the tree)
1566 // - Cache objects (level 3 of the tree)
1567 const showDelete =
1568 (type == "indexedDB" || type == "Cache") && selectedItem.length == 3;
1569 this._treePopupDelete.hidden = !showDelete;
1570 if (showDelete) {
1571 const itemName = addEllipsis(selectedItem[selectedItem.length - 1]);
1572 this._panelDoc.l10n.setArgs(this._treePopupDelete, { itemName });
1575 showMenu = showDeleteAll || showDelete;
1578 if (!showMenu) {
1579 event.preventDefault();
1583 onVariableViewPopupShowing() {
1584 const item = this.view.getFocusedItem();
1585 this._variableViewPopupCopy.setAttribute("disabled", !item);
1589 * Handles refreshing the selected storage
1591 async onRefreshTable() {
1592 await this.onHostSelect(this.tree.selectedItem);
1596 * Handles adding an item from the storage
1598 onAddItem() {
1599 const selectedItem = this.tree.selectedItem;
1600 if (!selectedItem) {
1601 return;
1604 const front = this.getCurrentFront();
1605 const [, host] = selectedItem;
1607 // Prepare to scroll into view.
1608 this.table.scrollIntoViewOnUpdate = true;
1609 this.table.editBookmark = createGUID();
1610 front.addItem(this.table.editBookmark, host);
1614 * Handles copy an item from the storage
1616 onCopyItem() {
1617 this.view._copyItem();
1621 * Handles removing an item from the storage
1623 * @param {DOMEvent} event
1624 * The event passed by the command or keypress event.
1626 onRemoveItem(event) {
1627 const [, host, ...path] = this.tree.selectedItem;
1628 const front = this.getCurrentFront();
1629 const uniqueId = this.table.uniqueId;
1630 const rowId =
1631 event.type == "command"
1632 ? this.table.contextMenuRowId
1633 : this.table.selectedRow[uniqueId];
1634 const data = this.table.items.get(rowId);
1636 let name = data[uniqueId];
1637 if (path.length) {
1638 name = JSON.stringify([...path, name]);
1640 front.removeItem(host, name);
1642 return false;
1646 * Handles removing all items from the storage
1648 onRemoveAll() {
1649 // Cannot use this.currentActor() if the handler is called from the
1650 // tree context menu: it returns correct value only after the table
1651 // data from server are successfully fetched (and that's async).
1652 const [, host, ...path] = this.tree.selectedItem;
1653 const front = this.getCurrentFront();
1654 const name = path.length ? JSON.stringify(path) : undefined;
1655 front.removeAll(host, name);
1659 * Handles removing all session cookies from the storage
1661 onRemoveAllSessionCookies() {
1662 // Cannot use this.currentActor() if the handler is called from the
1663 // tree context menu: it returns the correct value only after the
1664 // table data from server is successfully fetched (and that's async).
1665 const [, host, ...path] = this.tree.selectedItem;
1666 const front = this.getCurrentFront();
1667 const name = path.length ? JSON.stringify(path) : undefined;
1668 front.removeAllSessionCookies(host, name);
1672 * Handles removing all cookies with exactly the same domain as the
1673 * cookie in the selected row.
1675 onRemoveAllFrom() {
1676 const [, host] = this.tree.selectedItem;
1677 const front = this.getCurrentFront();
1678 const rowId = this.table.contextMenuRowId;
1679 const data = this.table.items.get(rowId);
1681 front.removeAll(host, data.host);
1684 onRemoveTreeItem() {
1685 const [type, host, ...path] = this.tree.selectedItem;
1687 if (type == "indexedDB" && path.length == 1) {
1688 this.removeDatabase(host, path[0]);
1689 } else if (type == "Cache" && path.length == 1) {
1690 this.removeCache(host, path[0]);
1694 async removeDatabase(host, dbName) {
1695 const front = this.getCurrentFront();
1697 try {
1698 const result = await front.removeDatabase(host, dbName);
1699 if (result.blocked) {
1700 const notificationBox = this._toolbox.getNotificationBox();
1701 const message = await this._panelDoc.l10n.formatValue(
1702 "storage-idb-delete-blocked",
1703 { dbName }
1706 notificationBox.appendNotification(
1707 message,
1708 "storage-idb-delete-blocked",
1709 null,
1710 notificationBox.PRIORITY_WARNING_LOW
1713 } catch (error) {
1714 const notificationBox = this._toolbox.getNotificationBox();
1715 const message = await this._panelDoc.l10n.formatValue(
1716 "storage-idb-delete-error",
1717 { dbName }
1719 notificationBox.appendNotification(
1720 message,
1721 "storage-idb-delete-error",
1722 null,
1723 notificationBox.PRIORITY_CRITICAL_LOW
1728 removeCache(host, cacheName) {
1729 const front = this.getCurrentFront();
1731 front.removeItem(host, JSON.stringify([cacheName]));
1735 exports.StorageUI = StorageUI;
1737 // Helper Functions
1739 function createGUID() {
1740 return "{cccccccc-cccc-4ccc-yccc-cccccccccccc}".replace(/[cy]/g, c => {
1741 const r = (Math.random() * 16) | 0;
1742 const v = c == "c" ? r : (r & 0x3) | 0x8;
1743 return v.toString(16);
1747 function addEllipsis(name) {
1748 if (name.length > ITEM_NAME_MAX_LENGTH) {
1749 if (/^https?:/.test(name)) {
1750 // For URLs, add ellipsis in the middle
1751 const halfLen = ITEM_NAME_MAX_LENGTH / 2;
1752 return name.slice(0, halfLen) + ELLIPSIS + name.slice(-halfLen);
1755 // For other strings, add ellipsis at the end
1756 return name.substr(0, ITEM_NAME_MAX_LENGTH) + ELLIPSIS;
1759 return name;