diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/storage/ui.js | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/storage/ui.js')
-rw-r--r-- | devtools/client/storage/ui.js | 1073 |
1 files changed, 1073 insertions, 0 deletions
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: + * { + * <store_type1>: { + * <host1>: [<store_names1>, <store_name2>...], + * <host2>: [<store_names34>...], ... + * }, + * <store_type2>: { + * <host1>: [<store_names1>, <store_name2>...], + * <host2>: [<store_names34>...], ... + * }, ... + * } + * 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 [<store_namesX] is an array of the names + * of the changed store objects. This array is empty for deleted object + * if the host was completely removed. + */ + onUpdate: function ({ changed, added, deleted }) { + if (deleted) { + this.handleDeletedItems(deleted); + } + + if (added) { + this.handleAddedItems(added); + } + + if (changed) { + this.handleChangedItems(changed); + } + + if (added || deleted || changed) { + this.emit("store-objects-updated"); + } + }, + + /** + * Handle added items received by onUpdate + * + * @param {object} See onUpdate docs + */ + handleAddedItems: function (added) { + for (let type in added) { + for (let host in added[type]) { + this.tree.add([type, {id: host, type: "url"}]); + for (let name of added[type][host]) { + try { + name = JSON.parse(name); + if (name.length == 3) { + name.splice(2, 1); + } + this.tree.add([type, host, ...name]); + if (!this.tree.selectedItem) { + this.tree.selectedItem = [type, host, name[0], name[1]]; + this.fetchStorageObjects(type, host, [JSON.stringify(name)], + REASON.NEW_ROW); + } + } catch (ex) { + // Do nothing + } + } + + if (this.tree.isSelected([type, host])) { + this.fetchStorageObjects(type, host, added[type][host], + REASON.NEW_ROW); + } + } + } + }, + + /** + * Handle deleted items received by onUpdate + * + * @param {object} See onUpdate docs + */ + handleDeletedItems: function (deleted) { + for (let type in deleted) { + for (let host in deleted[type]) { + if (!deleted[type][host].length) { + // This means that the whole host is deleted, thus the item should + // be removed from the storage tree + if (this.tree.isSelected([type, host])) { + this.table.clear(); + this.hideSidebar(); + this.tree.selectPreviousItem(); + } + + this.tree.remove([type, host]); + } else { + for (let name of deleted[type][host]) { + try { + // trying to parse names in case of indexedDB or cache + let names = JSON.parse(name); + // Is a whole cache, database or objectstore deleted? + // Then remove it from the tree. + if (names.length < 3) { + if (this.tree.isSelected([type, host, ...names])) { + this.table.clear(); + this.hideSidebar(); + this.tree.selectPreviousItem(); + } + this.tree.remove([type, host, ...names]); + } + + // Remove the item from table if currently displayed. + if (names.length > 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 ])); + }, +}; |