From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- devtools/client/storage/moz.build | 12 + devtools/client/storage/panel.js | 87 ++ devtools/client/storage/storage.xul | 58 ++ devtools/client/storage/test/.eslintrc.js | 6 + devtools/client/storage/test/browser.ini | 44 + .../client/storage/test/browser_storage_basic.js | 118 +++ .../storage/test/browser_storage_cache_delete.js | 46 + .../storage/test/browser_storage_cache_error.js | 19 + .../test/browser_storage_cookies_delete_all.js | 74 ++ .../storage/test/browser_storage_cookies_domain.js | 21 + .../storage/test/browser_storage_cookies_edit.js | 22 + .../test/browser_storage_cookies_edit_keyboard.js | 23 + .../test/browser_storage_cookies_tab_navigation.js | 24 + .../client/storage/test/browser_storage_delete.js | 56 + .../storage/test/browser_storage_delete_all.js | 90 ++ .../storage/test/browser_storage_delete_tree.js | 67 ++ .../test/browser_storage_dynamic_updates.js | 213 ++++ .../test/browser_storage_empty_objectstores.js | 77 ++ .../test/browser_storage_indexeddb_delete.js | 47 + .../browser_storage_indexeddb_delete_blocked.js | 58 ++ .../test/browser_storage_localstorage_edit.js | 24 + .../test/browser_storage_localstorage_error.js | 24 + .../storage/test/browser_storage_overflow.js | 41 + .../client/storage/test/browser_storage_search.js | 87 ++ .../test/browser_storage_search_keyboard_trap.js | 15 + .../test/browser_storage_sessionstorage_edit.js | 24 + .../client/storage/test/browser_storage_sidebar.js | 125 +++ .../storage/test/browser_storage_sidebar_update.js | 41 + .../client/storage/test/browser_storage_values.js | 165 +++ devtools/client/storage/test/head.js | 840 +++++++++++++++ .../client/storage/test/storage-cache-error.html | 20 + .../storage/test/storage-complex-values.html | 123 +++ devtools/client/storage/test/storage-cookies.html | 24 + .../storage/test/storage-empty-objectstores.html | 62 ++ .../storage/test/storage-idb-delete-blocked.html | 52 + devtools/client/storage/test/storage-listings.html | 126 +++ .../client/storage/test/storage-localstorage.html | 23 + devtools/client/storage/test/storage-overflow.html | 19 + devtools/client/storage/test/storage-search.html | 23 + .../storage/test/storage-secured-iframe.html | 91 ++ .../storage/test/storage-sessionstorage.html | 23 + .../storage/test/storage-unsecured-iframe.html | 19 + devtools/client/storage/test/storage-updates.html | 64 ++ devtools/client/storage/ui.js | 1073 ++++++++++++++++++++ 44 files changed, 4290 insertions(+) create mode 100644 devtools/client/storage/moz.build create mode 100644 devtools/client/storage/panel.js create mode 100644 devtools/client/storage/storage.xul create mode 100644 devtools/client/storage/test/.eslintrc.js create mode 100644 devtools/client/storage/test/browser.ini create mode 100644 devtools/client/storage/test/browser_storage_basic.js create mode 100644 devtools/client/storage/test/browser_storage_cache_delete.js create mode 100644 devtools/client/storage/test/browser_storage_cache_error.js create mode 100644 devtools/client/storage/test/browser_storage_cookies_delete_all.js create mode 100644 devtools/client/storage/test/browser_storage_cookies_domain.js create mode 100644 devtools/client/storage/test/browser_storage_cookies_edit.js create mode 100644 devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js create mode 100644 devtools/client/storage/test/browser_storage_cookies_tab_navigation.js create mode 100644 devtools/client/storage/test/browser_storage_delete.js create mode 100644 devtools/client/storage/test/browser_storage_delete_all.js create mode 100644 devtools/client/storage/test/browser_storage_delete_tree.js create mode 100644 devtools/client/storage/test/browser_storage_dynamic_updates.js create mode 100644 devtools/client/storage/test/browser_storage_empty_objectstores.js create mode 100644 devtools/client/storage/test/browser_storage_indexeddb_delete.js create mode 100644 devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js create mode 100644 devtools/client/storage/test/browser_storage_localstorage_edit.js create mode 100644 devtools/client/storage/test/browser_storage_localstorage_error.js create mode 100644 devtools/client/storage/test/browser_storage_overflow.js create mode 100644 devtools/client/storage/test/browser_storage_search.js create mode 100644 devtools/client/storage/test/browser_storage_search_keyboard_trap.js create mode 100644 devtools/client/storage/test/browser_storage_sessionstorage_edit.js create mode 100644 devtools/client/storage/test/browser_storage_sidebar.js create mode 100644 devtools/client/storage/test/browser_storage_sidebar_update.js create mode 100644 devtools/client/storage/test/browser_storage_values.js create mode 100644 devtools/client/storage/test/head.js create mode 100644 devtools/client/storage/test/storage-cache-error.html create mode 100644 devtools/client/storage/test/storage-complex-values.html create mode 100644 devtools/client/storage/test/storage-cookies.html create mode 100644 devtools/client/storage/test/storage-empty-objectstores.html create mode 100644 devtools/client/storage/test/storage-idb-delete-blocked.html create mode 100644 devtools/client/storage/test/storage-listings.html create mode 100644 devtools/client/storage/test/storage-localstorage.html create mode 100644 devtools/client/storage/test/storage-overflow.html create mode 100644 devtools/client/storage/test/storage-search.html create mode 100644 devtools/client/storage/test/storage-secured-iframe.html create mode 100644 devtools/client/storage/test/storage-sessionstorage.html create mode 100644 devtools/client/storage/test/storage-unsecured-iframe.html create mode 100644 devtools/client/storage/test/storage-updates.html create mode 100644 devtools/client/storage/ui.js (limited to 'devtools/client/storage') diff --git a/devtools/client/storage/moz.build b/devtools/client/storage/moz.build new file mode 100644 index 000000000..0c7f2f46b --- /dev/null +++ b/devtools/client/storage/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] + +DevToolsModules( + 'panel.js', + 'ui.js' +) diff --git a/devtools/client/storage/panel.js b/devtools/client/storage/panel.js new file mode 100644 index 000000000..c24059d8e --- /dev/null +++ b/devtools/client/storage/panel.js @@ -0,0 +1,87 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EventEmitter = require("devtools/shared/event-emitter"); + +loader.lazyRequireGetter(this, "StorageFront", + "devtools/shared/fronts/storage", true); +loader.lazyRequireGetter(this, "StorageUI", + "devtools/client/storage/ui", true); + +var StoragePanel = this.StoragePanel = +function StoragePanel(panelWin, toolbox) { + EventEmitter.decorate(this); + + this._toolbox = toolbox; + this._target = toolbox.target; + this._panelWin = panelWin; + + this.destroy = this.destroy.bind(this); +}; + +exports.StoragePanel = StoragePanel; + +StoragePanel.prototype = { + get target() { + return this._toolbox.target; + }, + + get panelWindow() { + return this._panelWin; + }, + + /** + * open is effectively an asynchronous constructor + */ + open: function () { + let targetPromise; + // We always interact with the target as if it were remote + if (!this.target.isRemote) { + targetPromise = this.target.makeRemote(); + } else { + targetPromise = Promise.resolve(this.target); + } + + return targetPromise.then(() => { + this.target.on("close", this.destroy); + this._front = new StorageFront(this.target.client, this.target.form); + + this.UI = new StorageUI(this._front, this._target, + this._panelWin, this._toolbox); + this.isReady = true; + this.emit("ready"); + + return this; + }).catch(e => { + console.log("error while opening storage panel", e); + this.destroy(); + }); + }, + + /** + * Destroy the storage inspector. + */ + destroy: function () { + if (!this._destroyed) { + this.UI.destroy(); + this.UI = null; + + // Destroy front to ensure packet handler is removed from client + this._front.destroy(); + this._front = null; + this._destroyed = true; + + this._target.off("close", this.destroy); + this._target = null; + this._toolbox = null; + this._panelWin = null; + } + + return Promise.resolve(null); + }, +}; diff --git a/devtools/client/storage/storage.xul b/devtools/client/storage/storage.xul new file mode 100644 index 000000000..9fbef5199 --- /dev/null +++ b/devtools/client/storage/storage.xul @@ -0,0 +1,58 @@ + + + + + + + + + %storageDTD; +]> + + + + + + + + diff --git a/devtools/client/storage/test/storage-complex-values.html b/devtools/client/storage/test/storage-complex-values.html new file mode 100644 index 000000000..d96da1932 --- /dev/null +++ b/devtools/client/storage/test/storage-complex-values.html @@ -0,0 +1,123 @@ + + + + + + Storage inspector test for correct values in the sidebar + + + + + diff --git a/devtools/client/storage/test/storage-cookies.html b/devtools/client/storage/test/storage-cookies.html new file mode 100644 index 000000000..97c15abaa --- /dev/null +++ b/devtools/client/storage/test/storage-cookies.html @@ -0,0 +1,24 @@ + + + + + + Storage inspector cookie test + + + + + diff --git a/devtools/client/storage/test/storage-empty-objectstores.html b/devtools/client/storage/test/storage-empty-objectstores.html new file mode 100644 index 000000000..096e90a32 --- /dev/null +++ b/devtools/client/storage/test/storage-empty-objectstores.html @@ -0,0 +1,62 @@ + + + + + Test for proper listing indexedDB databases with no object stores + + + + + diff --git a/devtools/client/storage/test/storage-idb-delete-blocked.html b/devtools/client/storage/test/storage-idb-delete-blocked.html new file mode 100644 index 000000000..ef7017f08 --- /dev/null +++ b/devtools/client/storage/test/storage-idb-delete-blocked.html @@ -0,0 +1,52 @@ + + + + + Test for proper listing indexedDB databases with no object stores + + + + + diff --git a/devtools/client/storage/test/storage-listings.html b/devtools/client/storage/test/storage-listings.html new file mode 100644 index 000000000..de3054d3a --- /dev/null +++ b/devtools/client/storage/test/storage-listings.html @@ -0,0 +1,126 @@ + + + + + + Storage inspector test for listing hosts and storages + + + + + + + diff --git a/devtools/client/storage/test/storage-localstorage.html b/devtools/client/storage/test/storage-localstorage.html new file mode 100644 index 000000000..3a560b096 --- /dev/null +++ b/devtools/client/storage/test/storage-localstorage.html @@ -0,0 +1,23 @@ + + + + + + Storage inspector localStorage test + + + + + diff --git a/devtools/client/storage/test/storage-overflow.html b/devtools/client/storage/test/storage-overflow.html new file mode 100644 index 000000000..ee8db36e6 --- /dev/null +++ b/devtools/client/storage/test/storage-overflow.html @@ -0,0 +1,19 @@ + + + + + + Storage inspector endless scrolling test + + + + + diff --git a/devtools/client/storage/test/storage-search.html b/devtools/client/storage/test/storage-search.html new file mode 100644 index 000000000..1a84ff622 --- /dev/null +++ b/devtools/client/storage/test/storage-search.html @@ -0,0 +1,23 @@ + + + + + + Storage inspector table filtering test + + + + + diff --git a/devtools/client/storage/test/storage-secured-iframe.html b/devtools/client/storage/test/storage-secured-iframe.html new file mode 100644 index 000000000..8424fd4cd --- /dev/null +++ b/devtools/client/storage/test/storage-secured-iframe.html @@ -0,0 +1,91 @@ + + + + + + + + + + diff --git a/devtools/client/storage/test/storage-sessionstorage.html b/devtools/client/storage/test/storage-sessionstorage.html new file mode 100644 index 000000000..12de07e13 --- /dev/null +++ b/devtools/client/storage/test/storage-sessionstorage.html @@ -0,0 +1,23 @@ + + + + + + Storage inspector sessionStorage test + + + + + diff --git a/devtools/client/storage/test/storage-unsecured-iframe.html b/devtools/client/storage/test/storage-unsecured-iframe.html new file mode 100644 index 000000000..a69ffdfd1 --- /dev/null +++ b/devtools/client/storage/test/storage-unsecured-iframe.html @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/devtools/client/storage/test/storage-updates.html b/devtools/client/storage/test/storage-updates.html new file mode 100644 index 000000000..a009814b2 --- /dev/null +++ b/devtools/client/storage/test/storage-updates.html @@ -0,0 +1,64 @@ + + + + + + Storage inspector blank html for tests + + + + + diff --git a/devtools/client/storage/ui.js b/devtools/client/storage/ui.js new file mode 100644 index 000000000..6af493e44 --- /dev/null +++ b/devtools/client/storage/ui.js @@ -0,0 +1,1073 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {Task} = require("devtools/shared/task"); +const EventEmitter = require("devtools/shared/event-emitter"); +const {LocalizationHelper, ELLIPSIS} = require("devtools/shared/l10n"); +const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); +const JSOL = require("devtools/client/shared/vendor/jsol"); +const {KeyCodes} = require("devtools/client/shared/keycodes"); + +loader.lazyRequireGetter(this, "TreeWidget", + "devtools/client/shared/widgets/TreeWidget", true); +loader.lazyRequireGetter(this, "TableWidget", + "devtools/client/shared/widgets/TableWidget", true); +loader.lazyRequireGetter(this, "ViewHelpers", + "devtools/client/shared/widgets/view-helpers"); +loader.lazyImporter(this, "VariablesView", + "resource://devtools/client/shared/widgets/VariablesView.jsm"); + +/** + * Localization convenience methods. + */ +const STORAGE_STRINGS = "devtools/client/locales/storage.properties"; +const L10N = new LocalizationHelper(STORAGE_STRINGS); + +const GENERIC_VARIABLES_VIEW_SETTINGS = { + lazyEmpty: true, + // ms + lazyEmptyDelay: 10, + searchEnabled: true, + searchPlaceholder: L10N.getStr("storage.search.placeholder"), + preventDescriptorModifiers: true +}; + +// Columns which are hidden by default in the storage table +const HIDDEN_COLUMNS = [ + "creationTime", + "isDomain", + "isSecure" +]; + +const REASON = { + NEW_ROW: "new-row", + NEXT_50_ITEMS: "next-50-items", + POPULATE: "populate", + UPDATE: "update" +}; + +const COOKIE_KEY_MAP = { + path: "Path", + host: "Domain", + expires: "Expires", + isSecure: "Secure", + isHttpOnly: "HttpOnly", + isDomain: "HostOnly", + creationTime: "CreationTime", + lastAccessed: "LastAccessed" +}; + +// Maximum length of item name to show in context menu label - will be +// trimmed with ellipsis if it's longer. +const ITEM_NAME_MAX_LENGTH = 32; + +function addEllipsis(name) { + if (name.length > ITEM_NAME_MAX_LENGTH) { + if (/^https?:/.test(name)) { + // For URLs, add ellipsis in the middle + const halfLen = ITEM_NAME_MAX_LENGTH / 2; + return name.slice(0, halfLen) + ELLIPSIS + name.slice(-halfLen); + } + + // For other strings, add ellipsis at the end + return name.substr(0, ITEM_NAME_MAX_LENGTH) + ELLIPSIS; + } + + return name; +} + +/** + * StorageUI is controls and builds the UI of the Storage Inspector. + * + * @param {Front} front + * Front for the storage actor + * @param {Target} target + * Interface for the page we're debugging + * @param {Window} panelWin + * Window of the toolbox panel to populate UI in. + */ +function StorageUI(front, target, panelWin, toolbox) { + EventEmitter.decorate(this); + + this._target = target; + this._window = panelWin; + this._panelDoc = panelWin.document; + this._toolbox = toolbox; + this.front = front; + + let treeNode = this._panelDoc.getElementById("storage-tree"); + this.tree = new TreeWidget(treeNode, { + defaultType: "dir", + contextMenuId: "storage-tree-popup" + }); + this.onHostSelect = this.onHostSelect.bind(this); + this.tree.on("select", this.onHostSelect); + + let tableNode = this._panelDoc.getElementById("storage-table"); + this.table = new TableWidget(tableNode, { + emptyText: L10N.getStr("table.emptyText"), + highlightUpdated: true, + cellContextMenuId: "storage-table-popup" + }); + + this.displayObjectSidebar = this.displayObjectSidebar.bind(this); + this.table.on(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar); + + this.handleScrollEnd = this.handleScrollEnd.bind(this); + this.table.on(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd); + + this.editItem = this.editItem.bind(this); + this.table.on(TableWidget.EVENTS.CELL_EDIT, this.editItem); + + this.sidebar = this._panelDoc.getElementById("storage-sidebar"); + this.sidebar.setAttribute("width", "300"); + this.view = new VariablesView(this.sidebar.firstChild, + GENERIC_VARIABLES_VIEW_SETTINGS); + + this.searchBox = this._panelDoc.getElementById("storage-searchbox"); + this.filterItems = this.filterItems.bind(this); + this.searchBox.addEventListener("command", this.filterItems); + + let shortcuts = new KeyShortcuts({ + window: this._panelDoc.defaultView, + }); + let key = L10N.getStr("storage.filter.key"); + shortcuts.on(key, (name, event) => { + event.preventDefault(); + this.searchBox.focus(); + }); + + this.front.listStores().then(storageTypes => { + this.populateStorageTree(storageTypes); + }).then(null, console.error); + + this.onUpdate = this.onUpdate.bind(this); + this.front.on("stores-update", this.onUpdate); + this.onCleared = this.onCleared.bind(this); + this.front.on("stores-cleared", this.onCleared); + + this.handleKeypress = this.handleKeypress.bind(this); + this._panelDoc.addEventListener("keypress", this.handleKeypress); + + this.onTreePopupShowing = this.onTreePopupShowing.bind(this); + this._treePopup = this._panelDoc.getElementById("storage-tree-popup"); + this._treePopup.addEventListener("popupshowing", this.onTreePopupShowing); + + this.onTablePopupShowing = this.onTablePopupShowing.bind(this); + this._tablePopup = this._panelDoc.getElementById("storage-table-popup"); + this._tablePopup.addEventListener("popupshowing", this.onTablePopupShowing); + + this.onRemoveItem = this.onRemoveItem.bind(this); + this.onRemoveAllFrom = this.onRemoveAllFrom.bind(this); + this.onRemoveAll = this.onRemoveAll.bind(this); + this.onRemoveTreeItem = this.onRemoveTreeItem.bind(this); + + this._tablePopupDelete = this._panelDoc.getElementById( + "storage-table-popup-delete"); + this._tablePopupDelete.addEventListener("command", this.onRemoveItem); + + this._tablePopupDeleteAllFrom = this._panelDoc.getElementById( + "storage-table-popup-delete-all-from"); + this._tablePopupDeleteAllFrom.addEventListener("command", + this.onRemoveAllFrom); + + this._tablePopupDeleteAll = this._panelDoc.getElementById( + "storage-table-popup-delete-all"); + this._tablePopupDeleteAll.addEventListener("command", this.onRemoveAll); + + this._treePopupDeleteAll = this._panelDoc.getElementById( + "storage-tree-popup-delete-all"); + this._treePopupDeleteAll.addEventListener("command", this.onRemoveAll); + + this._treePopupDelete = this._panelDoc.getElementById("storage-tree-popup-delete"); + this._treePopupDelete.addEventListener("command", this.onRemoveTreeItem); +} + +exports.StorageUI = StorageUI; + +StorageUI.prototype = { + + storageTypes: null, + shouldLoadMoreItems: true, + + set animationsEnabled(value) { + this._panelDoc.documentElement.classList.toggle("no-animate", !value); + }, + + destroy: function () { + this.table.off(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar); + this.table.off(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd); + this.table.off(TableWidget.EVENTS.CELL_EDIT, this.editItem); + this.table.destroy(); + + this.front.off("stores-update", this.onUpdate); + this.front.off("stores-cleared", this.onCleared); + this._panelDoc.removeEventListener("keypress", this.handleKeypress); + this.searchBox.removeEventListener("input", this.filterItems); + this.searchBox = null; + + this._treePopup.removeEventListener("popupshowing", this.onTreePopupShowing); + this._treePopupDeleteAll.removeEventListener("command", this.onRemoveAll); + this._treePopupDelete.removeEventListener("command", this.onRemoveTreeItem); + + this._tablePopup.removeEventListener("popupshowing", this.onTablePopupShowing); + this._tablePopupDelete.removeEventListener("command", this.onRemoveItem); + this._tablePopupDeleteAllFrom.removeEventListener("command", this.onRemoveAllFrom); + this._tablePopupDeleteAll.removeEventListener("command", this.onRemoveAll); + }, + + /** + * Empties and hides the object viewer sidebar + */ + hideSidebar: function () { + this.view.empty(); + this.sidebar.hidden = true; + this.table.clearSelection(); + }, + + getCurrentActor: function () { + let type = this.table.datatype; + + return this.storageTypes[type]; + }, + + /** + * Make column fields editable + * + * @param {Array} editableFields + * An array of keys of columns to be made editable + */ + makeFieldsEditable: function* (editableFields) { + if (editableFields && editableFields.length > 0) { + this.table.makeFieldsEditable(editableFields); + } else if (this.table._editableFieldsEngine) { + this.table._editableFieldsEngine.destroy(); + } + }, + + editItem: function (eventType, data) { + let actor = this.getCurrentActor(); + + actor.editItem(data); + }, + + /** + * Removes the given item from the storage table. Reselects the next item in + * the table and repopulates the sidebar with that item's data if the item + * being removed was selected. + */ + removeItemFromTable: function (name) { + if (this.table.isSelected(name)) { + if (this.table.selectedIndex == 0) { + this.table.selectNextRow(); + } else { + this.table.selectPreviousRow(); + } + this.table.remove(name); + this.displayObjectSidebar(); + } else { + this.table.remove(name); + } + }, + + /** + * Event handler for "stores-cleared" event coming from the storage actor. + * + * @param {object} response + * An object containing which storage types were cleared + */ + onCleared: function (response) { + function* enumPaths() { + for (let type in response) { + if (Array.isArray(response[type])) { + // Handle the legacy response with array of hosts + for (let host of response[type]) { + yield [type, host]; + } + } else { + // Handle the new format that supports clearing sub-stores in a host + for (let host in response[type]) { + let paths = response[type][host]; + + if (!paths.length) { + yield [type, host]; + } else { + for (let path of paths) { + try { + path = JSON.parse(path); + yield [type, host, ...path]; + } catch (ex) { + // ignore + } + } + } + } + } + } + } + + for (let path of enumPaths()) { + // Find if the path is selected (there is max one) and clear it + if (this.tree.isSelected(path)) { + this.table.clear(); + this.hideSidebar(); + this.emit("store-objects-cleared"); + break; + } + } + }, + + /** + * Event handler for "stores-update" event coming from the storage actor. + * + * @param {object} argument0 + * An object containing the details of the added, changed and deleted + * storage objects. + * Each of these 3 objects are of the following format: + * { + * : { + * : [, ...], + * : [...], ... + * }, + * : { + * : [, ...], + * : [...], ... + * }, ... + * } + * Where store_type1 and store_type2 is one of cookies, indexedDB, + * sessionStorage and localStorage; host1, host2 are the host in which + * this change happened; and [ 0) { + let tableItemName = names.pop(); + if (this.tree.isSelected([type, host, ...names])) { + this.removeItemFromTable(tableItemName); + } + } + } catch (ex) { + if (this.tree.isSelected([type, host])) { + this.removeItemFromTable(name); + } + } + } + } + } + } + }, + + /** + * Handle changed items received by onUpdate + * + * @param {object} See onUpdate docs + */ + handleChangedItems: function (changed) { + let [type, host, db, objectStore] = this.tree.selectedItem; + if (!changed[type] || !changed[type][host] || + changed[type][host].length == 0) { + return; + } + try { + let toUpdate = []; + for (let name of changed[type][host]) { + let names = JSON.parse(name); + if (names[0] == db && names[1] == objectStore && names[2]) { + toUpdate.push(name); + } + } + this.fetchStorageObjects(type, host, toUpdate, REASON.UPDATE); + } catch (ex) { + this.fetchStorageObjects(type, host, changed[type][host], REASON.UPDATE); + } + }, + + /** + * Fetches the storage objects from the storage actor and populates the + * storage table with the returned data. + * + * @param {string} type + * The type of storage. Ex. "cookies" + * @param {string} host + * Hostname + * @param {array} names + * Names of particular store objects. Empty if all are requested + * @param {Constant} reason + * See REASON constant at top of file. + */ + fetchStorageObjects: Task.async(function* (type, host, names, reason) { + let fetchOpts = reason === REASON.NEXT_50_ITEMS ? {offset: this.itemOffset} + : {}; + let storageType = this.storageTypes[type]; + + if (reason !== REASON.NEXT_50_ITEMS && + reason !== REASON.UPDATE && + reason !== REASON.NEW_ROW && + reason !== REASON.POPULATE) { + throw new Error("Invalid reason specified"); + } + + try { + if (reason === REASON.POPULATE) { + let subType = null; + // The indexedDB type could have sub-type data to fetch. + // If having names specified, then it means + // we are fetching details of specific database or of object store. + if (type == "indexedDB" && names) { + let [ dbName, objectStoreName ] = JSON.parse(names[0]); + if (dbName) { + subType = "database"; + } + if (objectStoreName) { + subType = "object store"; + } + } + yield this.resetColumns(type, host, subType); + } + + let {data} = yield storageType.getStoreObjects(host, names, fetchOpts); + if (data.length) { + this.populateTable(data, reason); + } + this.emit("store-objects-updated"); + } catch (ex) { + console.error(ex); + } + }), + + /** + * Populates the storage tree which displays the list of storages present for + * the page. + * + * @param {object} storageTypes + * List of storages and their corresponding hosts returned by the + * StorageFront.listStores call. + */ + populateStorageTree: function (storageTypes) { + this.storageTypes = {}; + for (let type in storageTypes) { + // Ignore `from` field, which is just a protocol.js implementation + // artifact. + if (type === "from") { + continue; + } + let typeLabel = type; + try { + typeLabel = L10N.getStr("tree.labels." + type); + } catch (e) { + console.error("Unable to localize tree label type:" + type); + } + this.tree.add([{id: type, label: typeLabel, type: "store"}]); + if (!storageTypes[type].hosts) { + continue; + } + this.storageTypes[type] = storageTypes[type]; + for (let host in storageTypes[type].hosts) { + this.tree.add([type, {id: host, type: "url"}]); + for (let name of storageTypes[type].hosts[host]) { + try { + let names = JSON.parse(name); + this.tree.add([type, host, ...names]); + if (!this.tree.selectedItem) { + this.tree.selectedItem = [type, host, names[0], names[1]]; + } + } catch (ex) { + // Do Nothing + } + } + if (!this.tree.selectedItem) { + this.tree.selectedItem = [type, host]; + } + } + } + }, + + /** + * Populates the selected entry from teh table in the sidebar for a more + * detailed view. + */ + displayObjectSidebar: Task.async(function* () { + let item = this.table.selectedRow; + if (!item) { + // Make sure that sidebar is hidden and return + this.sidebar.hidden = true; + return; + } + + // Get the string value (async action) and the update the UI synchronously. + let value; + if (item.name && item.valueActor) { + value = yield item.valueActor.string(); + } + + // Start updating the UI. Everything is sync beyond this point. + this.sidebar.hidden = false; + this.view.empty(); + let mainScope = this.view.addScope(L10N.getStr("storage.data.label")); + mainScope.expanded = true; + + if (value) { + let itemVar = mainScope.addItem(item.name + "", {}, {relaxed: true}); + + // The main area where the value will be displayed + itemVar.setGrip(value); + + // May be the item value is a json or a key value pair itself + this.parseItemValue(item.name, value); + + // By default the item name and value are shown. If this is the only + // information available, then nothing else is to be displayed. + let itemProps = Object.keys(item); + if (itemProps.length > 3) { + // Display any other information other than the item name and value + // which may be available. + let rawObject = Object.create(null); + let otherProps = itemProps.filter( + e => !["name", "value", "valueActor"].includes(e)); + for (let prop of otherProps) { + let cookieProp = COOKIE_KEY_MAP[prop] || prop; + // The pseduo property of HostOnly refers to converse of isDomain property + rawObject[cookieProp] = (prop === "isDomain") ? !item[prop] : item[prop]; + } + itemVar.populate(rawObject, {sorted: true}); + itemVar.twisty = true; + itemVar.expanded = true; + } + } else { + // Case when displaying IndexedDB db/object store properties. + for (let key in item) { + mainScope.addItem(key, {}, true).setGrip(item[key]); + this.parseItemValue(key, item[key]); + } + } + + this.emit("sidebar-updated"); + }), + + /** + * Tries to parse a string value into either a json or a key-value separated + * object and populates the sidebar with the parsed value. The value can also + * be a key separated array. + * + * @param {string} name + * The key corresponding to the `value` string in the object + * @param {string} value + * The string to be parsed into an object + */ + parseItemValue: function (name, originalValue) { + // Find if value is URLEncoded ie + let decodedValue = ""; + try { + decodedValue = decodeURIComponent(originalValue); + } catch (e) { + // Unable to decode, nothing to do + } + let value = (decodedValue && decodedValue !== originalValue) + ? decodedValue : originalValue; + + let json = null; + try { + json = JSOL.parse(value); + } catch (ex) { + json = null; + } + + if (!json && value) { + json = this._extractKeyValPairs(value); + } + + // return if json is null, or same as value, or just a string. + if (!json || json == value || typeof json == "string") { + return; + } + + // One special case is a url which gets separated as key value pair on : + if ((json.length == 2 || Object.keys(json).length == 1) && + ((json[0] || Object.keys(json)[0]) + "").match(/^(http|file|ftp)/)) { + return; + } + + let jsonObject = Object.create(null); + let view = this.view; + jsonObject[name] = json; + let valueScope = view.getScopeAtIndex(1) || + view.addScope(L10N.getStr("storage.parsedValue.label")); + valueScope.expanded = true; + let jsonVar = valueScope.addItem("", Object.create(null), {relaxed: true}); + jsonVar.expanded = true; + jsonVar.twisty = true; + jsonVar.populate(jsonObject, {expanded: true}); + }, + + /** + * Tries to parse a string into an object on the basis of key-value pairs, + * separated by various separators. If failed, tries to parse for single + * separator separated values to form an array. + * + * @param {string} value + * The string to be parsed into an object or array + */ + _extractKeyValPairs: function (value) { + let makeObject = (keySep, pairSep) => { + let object = {}; + for (let pair of value.split(pairSep)) { + let [key, val] = pair.split(keySep); + object[key] = val; + } + return object; + }; + + // Possible separators. + const separators = ["=", ":", "~", "#", "&", "\\*", ",", "\\."]; + // Testing for object + for (let i = 0; i < separators.length; i++) { + let kv = separators[i]; + for (let j = 0; j < separators.length; j++) { + if (i == j) { + continue; + } + let p = separators[j]; + let word = `[^${kv}${p}]*`; + let keyValue = `${word}${kv}${word}`; + let keyValueList = `${keyValue}(${p}${keyValue})*`; + let regex = new RegExp(`^${keyValueList}$`); + if (value.match && value.match(regex) && value.includes(kv) && + (value.includes(p) || value.split(kv).length == 2)) { + return makeObject(kv, p); + } + } + } + // Testing for array + for (let p of separators) { + let word = `[^${p}]*`; + let wordList = `(${word}${p})+${word}`; + let regex = new RegExp(`^${wordList}$`); + if (value.match && value.match(regex)) { + return value.split(p.replace(/\\*/g, "")); + } + } + return null; + }, + + /** + * Select handler for the storage tree. Fetches details of the selected item + * from the storage details and populates the storage tree. + * + * @param {string} event + * The name of the event fired + * @param {array} item + * An array of ids which represent the location of the selected item in + * the storage tree + */ + onHostSelect: function (event, item) { + this.table.clear(); + this.hideSidebar(); + this.searchBox.value = ""; + + let [type, host] = item; + let names = null; + if (!host) { + return; + } + if (item.length > 2) { + names = [JSON.stringify(item.slice(2))]; + } + this.fetchStorageObjects(type, host, names, REASON.POPULATE); + this.itemOffset = 0; + }, + + /** + * Resets the column headers in the storage table with the pased object `data` + * + * @param {string} type + * The type of storage corresponding to the after-reset columns in the + * table. + * @param {string} host + * The host name corresponding to the table after reset. + * + * @param {string} [subType] + * The sub type under the given type. + */ + resetColumns: function* (type, host, subtype) { + this.table.host = host; + this.table.datatype = type; + + let uniqueKey = null; + let columns = {}; + let editableFields = []; + let fields = yield this.getCurrentActor().getFields(subtype); + + fields.forEach(f => { + if (!uniqueKey) { + this.table.uniqueId = uniqueKey = f.name; + } + + if (f.editable) { + editableFields.push(f.name); + } + + columns[f.name] = f.name; + let columnName; + try { + columnName = L10N.getStr("table.headers." + type + "." + f.name); + } catch (e) { + columnName = COOKIE_KEY_MAP[f.name]; + } + + if (!columnName) { + console.error("Unable to localize table header type:" + type + " key:" + f.name); + } else { + columns[f.name] = columnName; + } + }); + + this.table.setColumns(columns, null, HIDDEN_COLUMNS); + this.hideSidebar(); + + yield this.makeFieldsEditable(editableFields); + }, + + /** + * Populates or updates the rows in the storage table. + * + * @param {array[object]} data + * Array of objects to be populated in the storage table + * @param {Constant} reason + * See REASON constant at top of file. + */ + populateTable: function (data, reason) { + for (let item of data) { + if (item.value) { + item.valueActor = item.value; + item.value = item.value.initial || ""; + } + if (item.expires != null) { + item.expires = item.expires + ? new Date(item.expires).toUTCString() + : L10N.getStr("label.expires.session"); + } + if (item.creationTime != null) { + item.creationTime = new Date(item.creationTime).toUTCString(); + } + if (item.lastAccessed != null) { + item.lastAccessed = new Date(item.lastAccessed).toUTCString(); + } + + switch (reason) { + case REASON.POPULATE: + // Update without flashing the row. + this.table.push(item, true); + break; + case REASON.NEW_ROW: + case REASON.NEXT_50_ITEMS: + // Update and flash the row. + this.table.push(item, false); + break; + case REASON.UPDATE: + this.table.update(item); + if (item == this.table.selectedRow && !this.sidebar.hidden) { + this.displayObjectSidebar(); + } + break; + } + + this.shouldLoadMoreItems = true; + } + }, + + /** + * Handles keypress event on the body table to close the sidebar when open + * + * @param {DOMEvent} event + * The event passed by the keypress event. + */ + handleKeypress: function (event) { + if (event.keyCode == KeyCodes.DOM_VK_ESCAPE && !this.sidebar.hidden) { + // Stop Propagation to prevent opening up of split console + this.hideSidebar(); + event.stopPropagation(); + event.preventDefault(); + } + }, + + /** + * Handles filtering the table + */ + filterItems() { + let value = this.searchBox.value; + this.table.filterItems(value, ["valueActor"]); + this._panelDoc.documentElement.classList.toggle("filtering", !!value); + }, + + /** + * Handles endless scrolling for the table + */ + handleScrollEnd: function () { + if (!this.shouldLoadMoreItems) { + return; + } + this.shouldLoadMoreItems = false; + this.itemOffset += 50; + + let item = this.tree.selectedItem; + let [type, host] = item; + let names = null; + if (item.length > 2) { + names = [JSON.stringify(item.slice(2))]; + } + this.fetchStorageObjects(type, host, names, REASON.NEXT_50_ITEMS); + }, + + /** + * Fires before a cell context menu with the "Delete" action is shown. + * If the currently selected storage object doesn't support removing items, prevent + * showing the menu. + */ + onTablePopupShowing: function (event) { + let selectedItem = this.tree.selectedItem; + let type = selectedItem[0]; + let actor = this.getCurrentActor(); + + // IndexedDB only supports removing items from object stores (level 4 of the tree) + if (!actor.removeItem || (type === "indexedDB" && selectedItem.length !== 4)) { + event.preventDefault(); + return; + } + + let rowId = this.table.contextMenuRowId; + let data = this.table.items.get(rowId); + let name = addEllipsis(data[this.table.uniqueId]); + + this._tablePopupDelete.setAttribute("label", + L10N.getFormatStr("storage.popupMenu.deleteLabel", name)); + + if (type === "cookies") { + let host = addEllipsis(data.host); + + this._tablePopupDeleteAllFrom.hidden = false; + this._tablePopupDeleteAllFrom.setAttribute("label", + L10N.getFormatStr("storage.popupMenu.deleteAllFromLabel", host)); + } else { + this._tablePopupDeleteAllFrom.hidden = true; + } + }, + + onTreePopupShowing: function (event) { + let showMenu = false; + let selectedItem = this.tree.selectedItem; + + if (selectedItem) { + let type = selectedItem[0]; + let actor = this.storageTypes[type]; + + // The delete all (aka clear) action is displayed for IndexedDB object stores + // (level 4 of tree), for Cache objects (level 3) and for the whole host (level 2) + // for other storage types (cookies, localStorage, ...). + let showDeleteAll = false; + if (actor.removeAll) { + let level; + if (type == "indexedDB") { + level = 4; + } else if (type == "Cache") { + level = 3; + } else { + level = 2; + } + + if (selectedItem.length == level) { + showDeleteAll = true; + } + } + + this._treePopupDeleteAll.hidden = !showDeleteAll; + + // The delete action is displayed for: + // - IndexedDB databases (level 3 of the tree) + // - Cache objects (level 3 of the tree) + let showDelete = (type == "indexedDB" || type == "Cache") && + selectedItem.length == 3; + this._treePopupDelete.hidden = !showDelete; + if (showDelete) { + let itemName = addEllipsis(selectedItem[selectedItem.length - 1]); + this._treePopupDelete.setAttribute("label", + L10N.getFormatStr("storage.popupMenu.deleteLabel", itemName)); + } + + showMenu = showDeleteAll || showDelete; + } + + if (!showMenu) { + event.preventDefault(); + } + }, + + /** + * Handles removing an item from the storage + */ + onRemoveItem: function () { + let [, host, ...path] = this.tree.selectedItem; + let actor = this.getCurrentActor(); + let rowId = this.table.contextMenuRowId; + let data = this.table.items.get(rowId); + let name = data[this.table.uniqueId]; + if (path.length > 0) { + name = JSON.stringify([...path, name]); + } + actor.removeItem(host, name); + }, + + /** + * Handles removing all items from the storage + */ + onRemoveAll: function () { + // Cannot use this.currentActor() if the handler is called from the + // tree context menu: it returns correct value only after the table + // data from server are successfully fetched (and that's async). + let [type, host, ...path] = this.tree.selectedItem; + let actor = this.storageTypes[type]; + let name = path.length > 0 ? JSON.stringify(path) : undefined; + actor.removeAll(host, name); + }, + + /** + * Handles removing all cookies with exactly the same domain as the + * cookie in the selected row. + */ + onRemoveAllFrom: function () { + let [, host] = this.tree.selectedItem; + let actor = this.getCurrentActor(); + let rowId = this.table.contextMenuRowId; + let data = this.table.items.get(rowId); + + actor.removeAll(host, data.host); + }, + + onRemoveTreeItem: function () { + let [type, host, ...path] = this.tree.selectedItem; + + if (type == "indexedDB" && path.length == 1) { + this.removeDatabase(host, path[0]); + } else if (type == "Cache" && path.length == 1) { + this.removeCache(host, path[0]); + } + }, + + removeDatabase: function (host, dbName) { + let actor = this.storageTypes.indexedDB; + + actor.removeDatabase(host, dbName).then(result => { + if (result.blocked) { + let notificationBox = this._toolbox.getNotificationBox(); + notificationBox.appendNotification( + L10N.getFormatStr("storage.idb.deleteBlocked", dbName), + "storage-idb-delete-blocked", + null, + notificationBox.PRIORITY_WARNING_LOW); + } + }).catch(error => { + let notificationBox = this._toolbox.getNotificationBox(); + notificationBox.appendNotification( + L10N.getFormatStr("storage.idb.deleteError", dbName), + "storage-idb-delete-error", + null, + notificationBox.PRIORITY_CRITICAL_LOW); + }); + }, + + removeCache: function (host, cacheName) { + let actor = this.storageTypes.Cache; + + actor.removeItem(host, JSON.stringify([ cacheName ])); + }, +}; -- cgit v1.2.3