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/. */
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");
11 } = require("resource://devtools/shared/storage/utils.js");
12 const { KeyCodes
} = require("resource://devtools/client/shared/keycodes.js");
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(
27 "resource://devtools/client/shared/widgets/TreeWidget.js",
30 loader
.lazyRequireGetter(
33 "resource://devtools/client/shared/widgets/TableWidget.js",
36 loader
.lazyRequireGetter(
39 "resource://devtools/shared/debounce.js",
42 loader
.lazyGetter(this, "standardSessionString", () => {
43 const l10n
= new Localization(["devtools/client/storage.ftl"], true);
44 return l10n
.formatValueSync("storage-expires-session");
48 ChromeUtils
.defineESModuleGetters(lazy
, {
49 VariablesView
: "resource://devtools/client/storage/VariablesView.sys.mjs",
54 NEXT_50_ITEMS
: "next-50-items",
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
= {
68 status
: "storage-table-headers-cache-status",
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",
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
= {
92 isHttpOnly
: "HttpOnly",
94 partitionKey
: "Partition Key",
97 uniqueKey
: "Unique key",
104 autoIncrement
: "Auto Increment",
109 objectStore
: "Object Store Name",
110 objectStores
: "Object Stores",
113 uniqueKey
: "Unique key",
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/
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
, {
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
, {
182 contextMenuId
: "variable-view-popup",
183 preventDescriptorModifiers
: true,
186 this.filterItems
= this.filterItems
.bind(this);
187 this.onPaneToggleButtonClicked
= this.onPaneToggleButtonClicked
.bind(this);
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(
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
,
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(
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(
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(
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
;
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() {
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()) {
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
);
387 "single-store-update",
388 this._onStoreUpdate
.bind(this, resource
)
391 "single-store-cleared",
392 this._onStoreCleared
.bind(this, resource
)
397 await
this.populateStorageTree();
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.
405 // The toolbox is open so the error is unexpected and real so let's log it.
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(
419 // Note that the storage front may already be destroyed,
420 // and have a null targetFront attribute. So also remove all already
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
) {
433 this.storageResources
= {};
439 set animationsEnabled(value
) {
440 this._panelDoc
.documentElement
.classList
.toggle("no-animate", !value
);
444 if (this._destroyed
) {
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(
465 this.onPaneToggleButtonClicked
467 this.sidebarToggleBtn
= null;
469 this._window
.removeEventListener("resize", this.#onLazyPanelResize
, true);
471 this._treePopup
.removeEventListener(
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(
481 this.onRemoveAllSessionCookies
483 this._treePopupDelete
.removeEventListener("command", this.onRemoveTreeItem
);
485 this._tablePopup
.removeEventListener(
487 this.onTablePopupShowing
489 this._tablePopupDelete
.removeEventListener("command", this.onRemoveItem
);
490 this._tablePopupDeleteAllFrom
.removeEventListener(
494 this._tablePopupDeleteAll
.removeEventListener("command", this.onRemoveAll
);
495 this._tablePopupDeleteAllSessionCookies
.removeEventListener(
497 this.onRemoveAllSessionCookies
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(
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();
521 this.sidebarToggledOpen
= false;
526 updateSidebarToggleButton() {
528 this.sidebarToggleBtn
.hidden
= !this.table
.hasSelectedRow
;
530 if (this.sidebar
.hidden
) {
531 this.sidebarToggleBtn
.classList
.add("pane-collapsed");
532 dataL10nId
= "storage-expand-pane";
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
545 this.sidebar
.hidden
= true;
546 this.updateSidebarToggleButton();
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();
574 const selectedItem
= this.tree
.selectedItem
;
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();
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.
605 * An object containing which hosts/paths are cleared from a
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
) {
617 // Handle the new format that supports clearing sub-stores in a host
618 for (const host
in clearedHostsOrPaths
) {
619 const paths
= clearedHostsOrPaths
[host
];
624 for (let path
of paths
) {
626 path
= JSON
.parse(path
);
627 yield [host
, ...path
];
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
])) {
643 // Reset itemOffset to 0 so that items added after local storate is
644 // cleared will be shown
647 this.emit("store-objects-cleared");
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
659 * Each of these 3 objects are of the following format:
662 * <host1>: [<store_names1>, <store_name2>...],
663 * <host2>: [<store_names34>...], ...
666 * <host1>: [<store_names1>, <store_name2>...],
667 * <host2>: [<store_names34>...], ...
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
;
679 await
this.handleAddedItems(added
);
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.
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
705 async
#onLazyPanelResize() {
706 // We can be called on a closed window or destroyed toolbox because of the
708 if (this._window
.closed
|| this._destroyed
|| this.table
.hasScrollbar
) {
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
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
];
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.
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
]) {
754 name
= JSON
.parse(name
);
755 if (name
.length
== 3) {
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(
764 [JSON
.stringify(name
)],
773 if (this.tree
.isSelected([type
, host
])) {
774 await
this.fetchStorageObjects(
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
])) {
799 this.tree
.selectPreviousItem();
802 this.tree
.remove([type
, host
]);
804 for (const name
of deleted
[type
][host
]) {
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
])) {
817 this.tree
.selectPreviousItem();
819 this.tree
.remove([type
, host
, ...names
]);
822 // Remove the item from table if currently displayed.
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
);
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
;
856 const [type
, host
, db
, objectStore
] = selectedItem
;
857 if (!changed
[type
] || !changed
[type
][host
] || !changed
[type
][host
].length
) {
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]) {
872 // For all the other storage types with a simple string key,
873 // update the item from the table by name without any parsing.
877 await
this.fetchStorageObjects(type
, host
, toUpdate
, REASON
.UPDATE
);
879 await
this.fetchStorageObjects(
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
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
) {
903 reason
=== REASON
.NEXT_50_ITEMS
? { offset
: this.itemOffset
} : {};
904 fetchOpts
.sessionString
= standardSessionString
;
905 const storage
= this._getStorage(type
, host
);
906 this.sidebarToggledOpen
= null;
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");
919 reason
=== REASON
.POPULATE
||
920 (reason
=== REASON
.NEW_ROW
&& this.table
.items
.size
=== 0)
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]);
929 subType
= "database";
931 if (objectStoreName
) {
932 subType
= "object store";
936 await
this.resetColumns(type
, host
, subType
);
939 const { data
, total
} = await storage
.getStoreObjects(
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");
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.
980 const item
= this.tree
.selectedItem
;
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
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
]) {
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]];
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
;
1033 typeLabel
= this.getStorageTypeLabel(type
);
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
) {
1057 dataL10nId
= "storage-tree-labels-cookies";
1059 case "localStorage":
1060 dataL10nId
= "storage-tree-labels-local-storage";
1062 case "sessionStorage":
1063 dataL10nId
= "storage-tree-labels-session-storage";
1066 dataL10nId
= "storage-tree-labels-indexed-db";
1069 dataL10nId
= "storage-tree-labels-cache";
1071 case "extensionStorage":
1072 dataL10nId
= "storage-tree-labels-extension-storage";
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
1085 /* eslint-disable-next-line */
1086 async
updateObjectSidebar() {
1087 const item
= this.table
.selectedRow
;
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.
1098 this.table
.items
.size
=== 0 ||
1100 !this.table
.selectedRow
||
1101 item
.uniqueKey
!== this.table
.selectedRow
.uniqueKey
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();
1114 const mainScope
= this.view
.addScope("storage-data");
1115 mainScope
.expanded
= true;
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) {
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;
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) {
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
) {
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
+ " ]";
1194 // Skip decoding for a host which doesn't include a domain name, simply
1195 // consider them to be readable.
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
;
1211 view
.getScopeAtIndex(1) || view
.addScope("storage-parsed-value");
1212 valueScope
.expanded
= true;
1213 const jsonVar
= valueScope
.addItem("", Object
.create(null), {
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
1229 async
onHostSelect(item
) {
1236 this.searchBox
.value
= "";
1238 const [type
, host
] = item
;
1239 this.table
.host
= host
;
1240 this.table
.datatype
= type
;
1242 this.updateToolbar();
1246 let storageTypeHintL10nId
= "";
1249 storageTypeHintL10nId
= "storage-table-type-cache-hint";
1252 storageTypeHintL10nId
= "storage-table-type-cookies-hint";
1254 case "extensionStorage":
1255 storageTypeHintL10nId
= "storage-table-type-extensionstorage-hint";
1257 case "localStorage":
1258 storageTypeHintL10nId
= "storage-table-type-localstorage-hint";
1261 storageTypeHintL10nId
= "storage-table-type-indexeddb-hint";
1263 case "sessionStorage":
1264 storageTypeHintL10nId
= "storage-table-type-sessionstorage-hint";
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();
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
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;
1309 const editableFields
= [];
1310 const hiddenFields
= [];
1311 const privateFields
= [];
1312 const fields
= await
this.getCurrentFront().getFields(subtype
);
1314 fields
.forEach(f
=> {
1316 this.table
.uniqueId
= uniqueKey
= f
.name
;
1320 editableFields
.push(f
.name
);
1324 hiddenFields
.push(f
.name
);
1328 privateFields
.push(f
.name
);
1331 const columnName
= this._getColumnName(type
, f
.name
);
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
;
1339 `No string defined in HEADERS_NON_L10N_STRINGS for '${type}.${f.name}'`
1344 this.table
.setColumns(columns
, null, hiddenFields
, privateFields
);
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
) {
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();
1379 case REASON
.POPULATE
:
1380 case REASON
.NEXT_50_ITEMS
:
1381 // Update without flashing the row.
1382 this.table
.push(item
, true);
1384 case REASON
.NEW_ROW
:
1385 // Update and flash the row.
1386 this.table
.push(item
, false);
1389 this.table
.update(item
);
1390 if (item
== this.table
.selectedRow
&& !this.sidebar
.hidden
) {
1391 await
this.updateObjectSidebar();
1396 this.shouldLoadMoreItems
= true;
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
) {
1418 this.sidebarToggledOpen
= false;
1419 // Stop Propagation to prevent opening up of split console
1420 event
.stopPropagation();
1421 event
.preventDefault();
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
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() {
1449 !this.shouldLoadMoreItems
||
1450 this._toolbox
.currentToolId
!== "storage" ||
1451 !this.tree
.selectedItem
1455 this.shouldLoadMoreItems
= false;
1456 this.itemOffset
+= 50;
1458 const item
= this.tree
.selectedItem
;
1459 const [type
, host
] = item
;
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)
1478 (!this.supportsAddItem(type
, host
) &&
1479 !this.supportsRemoveItem(type
, host
)) ||
1480 (type
=== "indexedDB" && selectedItem
.length
!== 4)
1482 event
.preventDefault();
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;
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
, {
1518 this._tablePopupDeleteAllFrom
.hidden
= false;
1520 this._tablePopupDeleteAllFrom
.hidden
= true;
1524 onTreePopupShowing(event
) {
1525 let showMenu
= false;
1526 const selectedItem
= this.tree
.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
)) {
1537 if (type
== "indexedDB") {
1539 } else if (type
== "Cache") {
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)
1568 (type
== "indexedDB" || type
== "Cache") && selectedItem
.length
== 3;
1569 this._treePopupDelete
.hidden
= !showDelete
;
1571 const itemName
= addEllipsis(selectedItem
[selectedItem
.length
- 1]);
1572 this._panelDoc
.l10n
.setArgs(this._treePopupDelete
, { itemName
});
1575 showMenu
= showDeleteAll
|| showDelete
;
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
1599 const selectedItem
= this.tree
.selectedItem
;
1600 if (!selectedItem
) {
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
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
;
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
];
1638 name
= JSON
.stringify([...path
, name
]);
1640 front
.removeItem(host
, name
);
1646 * Handles removing all items from the storage
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.
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();
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",
1706 notificationBox
.appendNotification(
1708 "storage-idb-delete-blocked",
1710 notificationBox
.PRIORITY_WARNING_LOW
1714 const notificationBox
= this._toolbox
.getNotificationBox();
1715 const message
= await
this._panelDoc
.l10n
.formatValue(
1716 "storage-idb-delete-error",
1719 notificationBox
.appendNotification(
1721 "storage-idb-delete-error",
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
;
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
;