tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ui.js (56936B)


      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/. */
      4 
      5 "use strict";
      6 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
      7 const { ELLIPSIS } = require("resource://devtools/shared/l10n.js");
      8 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
      9 const {
     10  parseItemValue,
     11 } = require("resource://devtools/shared/storage/utils.js");
     12 const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
     13 const {
     14  getUnicodeHostname,
     15 } = require("resource://devtools/client/shared/unicode-url.js");
     16 const getStorageTypeURL = require("resource://devtools/client/storage/utils/doc-utils.js");
     17 
     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}";
     23 
     24 loader.lazyRequireGetter(
     25  this,
     26  "TreeWidget",
     27  "resource://devtools/client/shared/widgets/TreeWidget.js",
     28  true
     29 );
     30 loader.lazyRequireGetter(
     31  this,
     32  "TableWidget",
     33  "resource://devtools/client/shared/widgets/TableWidget.js",
     34  true
     35 );
     36 loader.lazyRequireGetter(
     37  this,
     38  "debounce",
     39  "resource://devtools/shared/debounce.js",
     40  true
     41 );
     42 loader.lazyGetter(this, "standardSessionString", () => {
     43  const l10n = new Localization(["devtools/client/storage.ftl"], true);
     44  return l10n.formatValueSync("storage-expires-session");
     45 });
     46 
     47 const lazy = {};
     48 ChromeUtils.defineESModuleGetters(lazy, {
     49  VariablesView: "resource://devtools/client/storage/VariablesView.sys.mjs",
     50 });
     51 
     52 const REASON = {
     53  NEW_ROW: "new-row",
     54  NEXT_50_ITEMS: "next-50-items",
     55  POPULATE: "populate",
     56  UPDATE: "update",
     57 };
     58 
     59 // How long we wait to debounce resize events
     60 const LAZY_RESIZE_INTERVAL_MS = 200;
     61 
     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;
     65 
     66 const HEADERS_L10N_IDS = {
     67  Cache: {
     68    status: "storage-table-headers-cache-status",
     69  },
     70  cookies: {
     71    creationTime: "storage-table-headers-cookies-creation-time",
     72    updateTime: "storage-table-headers-cookies-update-time",
     73    expires: "storage-table-headers-cookies-expires",
     74    lastAccessed: "storage-table-headers-cookies-last-accessed",
     75    name: "storage-table-headers-cookies-name",
     76    size: "storage-table-headers-cookies-size",
     77    value: "storage-table-headers-cookies-value",
     78  },
     79  extensionStorage: {
     80    area: "storage-table-headers-extension-storage-area",
     81  },
     82 };
     83 
     84 // We only localize certain table headers. The headers that we do not localize
     85 // along with their label are stored in this dictionary for easy reference.
     86 const HEADERS_NON_L10N_STRINGS = {
     87  Cache: {
     88    url: "URL",
     89  },
     90  cookies: {
     91    host: "Domain",
     92    hostOnly: "HostOnly",
     93    isHttpOnly: "HttpOnly",
     94    isSecure: "Secure",
     95    partitionKey: "Partition Key",
     96    path: "Path",
     97    sameSite: "SameSite",
     98    uniqueKey: "Unique key",
     99  },
    100  extensionStorage: {
    101    name: "Key",
    102    value: "Value",
    103  },
    104  indexedDB: {
    105    autoIncrement: "Auto Increment",
    106    db: "Database Name",
    107    indexes: "Indexes",
    108    keyPath: "Key Path",
    109    name: "Key",
    110    objectStore: "Object Store Name",
    111    objectStores: "Object Stores",
    112    origin: "Origin",
    113    storage: "Storage",
    114    uniqueKey: "Unique key",
    115    value: "Value",
    116    version: "Version",
    117  },
    118  localStorage: {
    119    name: "Key",
    120    value: "Value",
    121  },
    122  sessionStorage: {
    123    name: "Key",
    124    value: "Value",
    125  },
    126 };
    127 
    128 /**
    129 * StorageUI is controls and builds the UI of the Storage Inspector.
    130 *
    131 * @param {Window} panelWin
    132 *        Window of the toolbox panel to populate UI in.
    133 * @param {object} commands
    134 *        The commands object with all interfaces defined from devtools/shared/commands/
    135 */
    136 class StorageUI {
    137  constructor(panelWin, toolbox, commands) {
    138    EventEmitter.decorate(this);
    139    this._window = panelWin;
    140    this._panelDoc = panelWin.document;
    141    this._toolbox = toolbox;
    142    this._commands = commands;
    143    this.sidebarToggledOpen = null;
    144    this.shouldLoadMoreItems = true;
    145 
    146    const treeNode = this._panelDoc.getElementById("storage-tree");
    147    this.tree = new TreeWidget(treeNode, {
    148      defaultType: "dir",
    149      contextMenuId: "storage-tree-popup",
    150    });
    151    this.onHostSelect = this.onHostSelect.bind(this);
    152    this.tree.on("select", this.onHostSelect);
    153 
    154    const tableNode = this._panelDoc.getElementById("storage-table");
    155    this.table = new TableWidget(tableNode, {
    156      emptyText: "storage-table-empty-text",
    157      highlightUpdated: true,
    158      cellContextMenuId: "storage-table-popup",
    159      l10n: this._panelDoc.l10n,
    160    });
    161 
    162    this.updateObjectSidebar = this.updateObjectSidebar.bind(this);
    163    this.table.on(TableWidget.EVENTS.ROW_SELECTED, this.updateObjectSidebar);
    164 
    165    this.handleScrollEnd = this.loadMoreItems.bind(this);
    166    this.table.on(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd);
    167 
    168    this.editItem = this.editItem.bind(this);
    169    this.table.on(TableWidget.EVENTS.CELL_EDIT, this.editItem);
    170 
    171    this.sidebar = this._panelDoc.getElementById("storage-sidebar");
    172 
    173    // Set suggested sizes for the xul:splitter's, so that the sidebar doesn't take too much space
    174    // in horizontal mode (width) and vertical (height).
    175    this.sidebar.style.width = "300px";
    176    this.sidebar.style.height = "300px";
    177 
    178    this.view = new lazy.VariablesView(this.sidebar.firstChild, {
    179      lazyEmpty: true,
    180      // ms
    181      lazyEmptyDelay: 10,
    182      searchEnabled: true,
    183      contextMenuId: "variable-view-popup",
    184    });
    185 
    186    this.filterItems = this.filterItems.bind(this);
    187    this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this);
    188    this.setupToolbar();
    189 
    190    this.handleKeypress = this.handleKeypress.bind(this);
    191    this._panelDoc.addEventListener("keypress", this.handleKeypress);
    192 
    193    this.onTreePopupShowing = this.onTreePopupShowing.bind(this);
    194    this._treePopup = this._panelDoc.getElementById("storage-tree-popup");
    195    this._treePopup.addEventListener("popupshowing", this.onTreePopupShowing);
    196 
    197    this.onTablePopupShowing = this.onTablePopupShowing.bind(this);
    198    this._tablePopup = this._panelDoc.getElementById("storage-table-popup");
    199    this._tablePopup.addEventListener("popupshowing", this.onTablePopupShowing);
    200 
    201    this.onVariableViewPopupShowing =
    202      this.onVariableViewPopupShowing.bind(this);
    203    this._variableViewPopup = this._panelDoc.getElementById(
    204      "variable-view-popup"
    205    );
    206    this._variableViewPopup.addEventListener(
    207      "popupshowing",
    208      this.onVariableViewPopupShowing
    209    );
    210 
    211    this.onRefreshTable = this.onRefreshTable.bind(this);
    212    this.onAddItem = this.onAddItem.bind(this);
    213    this.onCopyItem = this.onCopyItem.bind(this);
    214    this.onPanelWindowResize = debounce(
    215      this.#onLazyPanelResize,
    216      LAZY_RESIZE_INTERVAL_MS,
    217      this
    218    );
    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);
    224 
    225    this._refreshButton = this._panelDoc.getElementById("refresh-button");
    226    this._refreshButton.addEventListener("click", this.onRefreshTable);
    227 
    228    this._addButton = this._panelDoc.getElementById("add-button");
    229    this._addButton.addEventListener("click", this.onAddItem);
    230 
    231    this._window.addEventListener("resize", this.onPanelWindowResize, true);
    232 
    233    this._variableViewPopupCopy = this._panelDoc.getElementById(
    234      "variable-view-popup-copy"
    235    );
    236    this._variableViewPopupCopy.addEventListener("command", this.onCopyItem);
    237 
    238    this._tablePopupAddItem = this._panelDoc.getElementById(
    239      "storage-table-popup-add"
    240    );
    241    this._tablePopupAddItem.addEventListener("command", this.onAddItem);
    242 
    243    this._tablePopupDelete = this._panelDoc.getElementById(
    244      "storage-table-popup-delete"
    245    );
    246    this._tablePopupDelete.addEventListener("command", this.onRemoveItem);
    247 
    248    this._tablePopupDeleteAllFrom = this._panelDoc.getElementById(
    249      "storage-table-popup-delete-all-from"
    250    );
    251    this._tablePopupDeleteAllFrom.addEventListener(
    252      "command",
    253      this.onRemoveAllFrom
    254    );
    255 
    256    this._tablePopupDeleteAll = this._panelDoc.getElementById(
    257      "storage-table-popup-delete-all"
    258    );
    259    this._tablePopupDeleteAll.addEventListener("command", this.onRemoveAll);
    260 
    261    this._tablePopupDeleteAllSessionCookies = this._panelDoc.getElementById(
    262      "storage-table-popup-delete-all-session-cookies"
    263    );
    264    this._tablePopupDeleteAllSessionCookies.addEventListener(
    265      "command",
    266      this.onRemoveAllSessionCookies
    267    );
    268 
    269    this._treePopupDeleteAll = this._panelDoc.getElementById(
    270      "storage-tree-popup-delete-all"
    271    );
    272    this._treePopupDeleteAll.addEventListener("command", this.onRemoveAll);
    273 
    274    this._treePopupDeleteAllSessionCookies = this._panelDoc.getElementById(
    275      "storage-tree-popup-delete-all-session-cookies"
    276    );
    277    this._treePopupDeleteAllSessionCookies.addEventListener(
    278      "command",
    279      this.onRemoveAllSessionCookies
    280    );
    281 
    282    this._treePopupDelete = this._panelDoc.getElementById(
    283      "storage-tree-popup-delete"
    284    );
    285    this._treePopupDelete.addEventListener("command", this.onRemoveTreeItem);
    286  }
    287 
    288  get currentTarget() {
    289    return this._commands.targetCommand.targetFront;
    290  }
    291 
    292  async init() {
    293    // This is a distionary of arrays, keyed by storage key
    294    // - Keys are storage keys, available on each storage resource, via ${resource.resourceKey}
    295    //   and are typically "Cache", "cookies", "indexedDB", "localStorage", ...
    296    // - Values are arrays of storage fronts. This isn't the deprecated global storage front (target.getFront(storage), only used by legacy listener),
    297    //   but rather the storage specific front, i.e. a storage resource. Storage resources are fronts.
    298    this.storageResources = {};
    299 
    300    await this._initL10NStringsMap();
    301 
    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,
    305    });
    306    const key = this._l10nStrings.get("storage-filter-key");
    307    shortcuts.on(key, event => {
    308      event.preventDefault();
    309      this.searchBox.focus();
    310    });
    311 
    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,
    318    });
    319 
    320    this._onResourceListAvailable = this._onResourceListAvailable.bind(this);
    321 
    322    const { resourceCommand } = this._commands;
    323 
    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,
    332    ];
    333    // EXTENSION_STORAGE is only relevant when debugging web extensions
    334    if (this._commands.descriptorFront.isWebExtensionDescriptor) {
    335      this._listenedResourceTypes.push(resourceCommand.TYPES.EXTENSION_STORAGE);
    336    }
    337    await this._commands.resourceCommand.watchResources(
    338      this._listenedResourceTypes,
    339      {
    340        onAvailable: this._onResourceListAvailable,
    341      }
    342    );
    343  }
    344 
    345  async _initL10NStringsMap() {
    346    const ids = [
    347      "storage-filter-key",
    348      "storage-table-headers-cookies-name",
    349      "storage-table-headers-cookies-value",
    350      "storage-table-headers-cookies-expires",
    351      "storage-table-headers-cookies-size",
    352      "storage-table-headers-cookies-last-accessed",
    353      "storage-table-headers-cookies-creation-time",
    354      "storage-table-headers-cookies-update-time",
    355      "storage-table-headers-cache-status",
    356      "storage-table-headers-extension-storage-area",
    357      "storage-tree-labels-cookies",
    358      "storage-tree-labels-local-storage",
    359      "storage-tree-labels-session-storage",
    360      "storage-tree-labels-indexed-db",
    361      "storage-tree-labels-cache",
    362      "storage-tree-labels-extension-storage",
    363      "storage-expires-session",
    364    ];
    365    const results = await this._panelDoc.l10n.formatValues(
    366      ids.map(s => ({ id: s }))
    367    );
    368 
    369    this._l10nStrings = new Map(ids.map((id, i) => [id, results[i]]));
    370  }
    371 
    372  async _onResourceListAvailable(resources) {
    373    for (const resource of resources) {
    374      if (resource.isDestroyed()) {
    375        continue;
    376      }
    377      const { resourceKey } = resource;
    378 
    379      // NOTE: We might be getting more than 1 resource per storage type when
    380      //       we have remote frames in content process resources, so we need
    381      //       an array to store these.
    382      if (!this.storageResources[resourceKey]) {
    383        this.storageResources[resourceKey] = [];
    384      }
    385      this.storageResources[resourceKey].push(resource);
    386 
    387      resource.on(
    388        "single-store-update",
    389        this._onStoreUpdate.bind(this, resource)
    390      );
    391      resource.on(
    392        "single-store-cleared",
    393        this._onStoreCleared.bind(this, resource)
    394      );
    395    }
    396 
    397    try {
    398      await this.populateStorageTree();
    399    } catch (e) {
    400      if (!this._toolbox || this._toolbox._destroyer) {
    401        // The toolbox is in the process of being destroyed... in this case throwing here
    402        // is expected and normal so let's ignore the error.
    403        return;
    404      }
    405 
    406      // The toolbox is open so the error is unexpected and real so let's log it.
    407      console.error(e);
    408    }
    409  }
    410 
    411  // We only need to listen to target destruction, but TargetCommand.watchTarget
    412  // requires a target available function...
    413  async _onTargetAvailable() {}
    414 
    415  _onTargetDestroyed({ targetFront }) {
    416    // Remove all storages related to this target
    417    for (const type in this.storageResources) {
    418      this.storageResources[type] = this.storageResources[type].filter(
    419        storage => {
    420          // Note that the storage front may already be destroyed,
    421          // and have a null targetFront attribute. So also remove all already
    422          // destroyed fronts.
    423          return !storage.isDestroyed() && storage.targetFront != targetFront;
    424        }
    425      );
    426    }
    427 
    428    // Only support top level target and navigation to new processes.
    429    // i.e. ignore additional targets created for remote <iframes>
    430    if (!targetFront.isTopLevel) {
    431      return;
    432    }
    433 
    434    this.storageResources = {};
    435    this.table.clear();
    436    this.hideSidebar();
    437    this.tree.clear();
    438  }
    439 
    440  set animationsEnabled(value) {
    441    this._panelDoc.documentElement.classList.toggle("no-animate", !value);
    442  }
    443 
    444  destroy() {
    445    if (this._destroyed) {
    446      return;
    447    }
    448    this._destroyed = true;
    449 
    450    const { resourceCommand } = this._commands;
    451    resourceCommand.unwatchResources(this._listenedResourceTypes, {
    452      onAvailable: this._onResourceListAvailable,
    453    });
    454 
    455    this.table.off(TableWidget.EVENTS.ROW_SELECTED, this.updateObjectSidebar);
    456    this.table.off(TableWidget.EVENTS.SCROLL_END, this.loadMoreItems);
    457    this.table.off(TableWidget.EVENTS.CELL_EDIT, this.editItem);
    458    this.table.destroy();
    459 
    460    this._panelDoc.removeEventListener("keypress", this.handleKeypress);
    461    this.searchBox.removeEventListener("input", this.filterItems);
    462    this.searchBox = null;
    463 
    464    this.sidebarToggleBtn.removeEventListener(
    465      "click",
    466      this.onPaneToggleButtonClicked
    467    );
    468    this.sidebarToggleBtn = null;
    469 
    470    this._window.removeEventListener("resize", this.#onLazyPanelResize, true);
    471 
    472    this._treePopup.removeEventListener(
    473      "popupshowing",
    474      this.onTreePopupShowing
    475    );
    476    this._refreshButton.removeEventListener("click", this.onRefreshTable);
    477    this._addButton.removeEventListener("click", this.onAddItem);
    478    this._tablePopupAddItem.removeEventListener("command", this.onAddItem);
    479    this._treePopupDeleteAll.removeEventListener("command", this.onRemoveAll);
    480    this._treePopupDeleteAllSessionCookies.removeEventListener(
    481      "command",
    482      this.onRemoveAllSessionCookies
    483    );
    484    this._treePopupDelete.removeEventListener("command", this.onRemoveTreeItem);
    485 
    486    this._tablePopup.removeEventListener(
    487      "popupshowing",
    488      this.onTablePopupShowing
    489    );
    490    this._tablePopupDelete.removeEventListener("command", this.onRemoveItem);
    491    this._tablePopupDeleteAllFrom.removeEventListener(
    492      "command",
    493      this.onRemoveAllFrom
    494    );
    495    this._tablePopupDeleteAll.removeEventListener("command", this.onRemoveAll);
    496    this._tablePopupDeleteAllSessionCookies.removeEventListener(
    497      "command",
    498      this.onRemoveAllSessionCookies
    499    );
    500  }
    501 
    502  setupToolbar() {
    503    this.searchBox = this._panelDoc.getElementById("storage-searchbox");
    504    this.searchBox.addEventListener("input", this.filterItems);
    505 
    506    // Setup the sidebar toggle button.
    507    this.sidebarToggleBtn = this._panelDoc.querySelector(".sidebar-toggle");
    508    this.updateSidebarToggleButton();
    509 
    510    this.sidebarToggleBtn.addEventListener(
    511      "click",
    512      this.onPaneToggleButtonClicked
    513    );
    514  }
    515 
    516  onPaneToggleButtonClicked() {
    517    if (this.sidebar.hidden && this.table.selectedRow) {
    518      this.sidebar.hidden = false;
    519      this.sidebarToggledOpen = true;
    520      this.updateSidebarToggleButton();
    521    } else {
    522      this.sidebarToggledOpen = false;
    523      this.hideSidebar();
    524    }
    525  }
    526 
    527  updateSidebarToggleButton() {
    528    let dataL10nId;
    529    this.sidebarToggleBtn.hidden = !this.table.hasSelectedRow;
    530 
    531    if (this.sidebar.hidden) {
    532      this.sidebarToggleBtn.classList.add("pane-collapsed");
    533      dataL10nId = "storage-expand-pane";
    534    } else {
    535      this.sidebarToggleBtn.classList.remove("pane-collapsed");
    536      dataL10nId = "storage-collapse-pane";
    537    }
    538 
    539    this._panelDoc.l10n.setAttributes(this.sidebarToggleBtn, dataL10nId);
    540  }
    541 
    542  /**
    543   * Hide the object viewer sidebar
    544   */
    545  hideSidebar() {
    546    this.sidebar.hidden = true;
    547    this.updateSidebarToggleButton();
    548  }
    549 
    550  getCurrentFront() {
    551    const { datatype, host } = this.table;
    552    return this._getStorage(datatype, host);
    553  }
    554 
    555  _getStorage(type, host) {
    556    const storageType = this.storageResources[type];
    557    return storageType.find(x => host in x.hosts);
    558  }
    559 
    560  /**
    561   *  Make column fields editable
    562   *
    563   *  @param {Array} editableFields
    564   *         An array of keys of columns to be made editable
    565   */
    566  makeFieldsEditable(editableFields) {
    567    if (editableFields && editableFields.length) {
    568      this.table.makeFieldsEditable(editableFields);
    569    } else if (this.table._editableFieldsEngine) {
    570      this.table._editableFieldsEngine.destroy();
    571    }
    572  }
    573 
    574  async editItem(data, cellEditAbortController) {
    575    const selectedItem = this.tree.selectedItem;
    576    if (!selectedItem) {
    577      return;
    578    }
    579    const front = this.getCurrentFront();
    580 
    581    const result = await front.editItem(data);
    582    // At the moment, only editing cookies can return an error
    583    if (front.typeName === "cookies" && result?.errorString) {
    584      const notificationBox = this._toolbox.getNotificationBox();
    585      const message = await this._panelDoc.l10n.formatValue(
    586        "storage-cookie-edit-error",
    587        { errorString: result.errorString }
    588      );
    589 
    590      notificationBox.appendNotification(
    591        message,
    592        "storage-cookie-edit-error",
    593        null,
    594        notificationBox.PRIORITY_WARNING_LOW
    595      );
    596 
    597      // Revert value in table
    598      cellEditAbortController.abort();
    599    }
    600  }
    601 
    602  /**
    603   * Removes the given item from the storage table. Reselects the next item in
    604   * the table and repopulates the sidebar with that item's data if the item
    605   * being removed was selected.
    606   */
    607  async removeItemFromTable(name) {
    608    if (this.table.isSelected(name) && this.table.items.size > 1) {
    609      if (this.table.selectedIndex == 0) {
    610        this.table.selectNextRow();
    611      } else {
    612        this.table.selectPreviousRow();
    613      }
    614    }
    615 
    616    this.table.remove(name);
    617    await this.updateObjectSidebar();
    618  }
    619 
    620  /**
    621   * Event handler for "stores-cleared" event coming from the storage actor.
    622   *
    623   * @param {object}
    624   *        An object containing which hosts/paths are cleared from a
    625   *        storage
    626   */
    627  _onStoreCleared(resource, { clearedHostsOrPaths }) {
    628    const { resourceKey } = resource;
    629    function* enumPaths() {
    630      if (Array.isArray(clearedHostsOrPaths)) {
    631        // Handle the legacy response with array of hosts
    632        for (const host of clearedHostsOrPaths) {
    633          yield [host];
    634        }
    635      } else {
    636        // Handle the new format that supports clearing sub-stores in a host
    637        for (const host in clearedHostsOrPaths) {
    638          const paths = clearedHostsOrPaths[host];
    639 
    640          if (!paths.length) {
    641            yield [host];
    642          } else {
    643            for (let path of paths) {
    644              try {
    645                path = JSON.parse(path);
    646                yield [host, ...path];
    647              } catch (ex) {
    648                // ignore
    649              }
    650            }
    651          }
    652        }
    653      }
    654    }
    655 
    656    for (const path of enumPaths()) {
    657      // Find if the path is selected (there is max one) and clear it
    658      if (this.tree.isSelected([resourceKey, ...path])) {
    659        this.table.clear();
    660        this.hideSidebar();
    661 
    662        // Reset itemOffset to 0 so that items added after local storate is
    663        // cleared will be shown
    664        this.itemOffset = 0;
    665 
    666        this.emit("store-objects-cleared");
    667        break;
    668      }
    669    }
    670  }
    671 
    672  /**
    673   * Event handler for "stores-update" event coming from the storage actor.
    674   *
    675   * @param {object} argument0
    676   *        An object containing the details of the added, changed and deleted
    677   *        storage objects.
    678   *        Each of these 3 objects are of the following format:
    679   *        {
    680   *          <store_type1>: {
    681   *            <host1>: [<store_names1>, <store_name2>...],
    682   *            <host2>: [<store_names34>...], ...
    683   *          },
    684   *          <store_type2>: {
    685   *            <host1>: [<store_names1>, <store_name2>...],
    686   *            <host2>: [<store_names34>...], ...
    687   *          }, ...
    688   *        }
    689   *        Where store_type1 and store_type2 is one of cookies, indexedDB,
    690   *        sessionStorage and localStorage; host1, host2 are the host in which
    691   *        this change happened; and [<store_namesX] is an array of the names
    692   *        of the changed store objects. This array is empty for deleted object
    693   *        if the host was completely removed.
    694   */
    695  async _onStoreUpdate(resource, update) {
    696    const { changed, added, deleted } = update;
    697    if (added) {
    698      await this.handleAddedItems(added);
    699    }
    700 
    701    if (changed) {
    702      await this.handleChangedItems(changed);
    703    }
    704 
    705    // We are dealing with batches of changes here. Deleted **MUST** come last in case it
    706    // is in the same batch as added and changed events e.g.
    707    //   - An item is changed then deleted in the same batch: deleted then changed will
    708    //     display an item that has been deleted.
    709    //   - An item is added then deleted in the same batch: deleted then added will
    710    //     display an item that has been deleted.
    711    if (deleted) {
    712      await this.handleDeletedItems(deleted);
    713    }
    714 
    715    if (added || deleted || changed) {
    716      this.emit("store-objects-edit");
    717    }
    718  }
    719 
    720  /**
    721   * If the panel is resized we need to check if we should load the next batch of
    722   * storage items.
    723   */
    724  async #onLazyPanelResize() {
    725    // We can be called on a closed window or destroyed toolbox because of the
    726    // deferred task.
    727    if (this._window.closed || this._destroyed || this.table.hasScrollbar) {
    728      return;
    729    }
    730 
    731    await this.loadMoreItems();
    732    this.emit("storage-resize");
    733  }
    734 
    735  /**
    736   * Get a string for a column name automatically choosing whether or not the
    737   * string should be localized.
    738   *
    739   * @param {string} type
    740   *        The storage type.
    741   * @param {string} name
    742   *        The field name that may need to be localized.
    743   */
    744  _getColumnName(type, name) {
    745    // If the ID exists in HEADERS_NON_L10N_STRINGS then we do not translate it
    746    const columnName = HEADERS_NON_L10N_STRINGS[type]?.[name];
    747    if (columnName) {
    748      return columnName;
    749    }
    750 
    751    // otherwise we get it from the L10N Map (populated during init)
    752    const l10nId = HEADERS_L10N_IDS[type]?.[name];
    753    if (l10nId && this._l10nStrings.has(l10nId)) {
    754      return this._l10nStrings.get(l10nId);
    755    }
    756 
    757    // If the string isn't localized, we will just use the field name.
    758    return name;
    759  }
    760 
    761  /**
    762   * Handle added items received by onEdit
    763   *
    764   * @param {object} See onEdit docs
    765   */
    766  async handleAddedItems(added) {
    767    for (const type in added) {
    768      for (const host in added[type]) {
    769        const label = this.getReadableLabelFromHostname(host);
    770        this.tree.add([type, { id: host, label, type: "url" }]);
    771        for (let name of added[type][host]) {
    772          try {
    773            name = JSON.parse(name);
    774            if (name.length == 3) {
    775              name.splice(2, 1);
    776            }
    777            this.tree.add([type, host, ...name]);
    778            if (!this.tree.selectedItem) {
    779              this.tree.selectedItem = [type, host, name[0], name[1]];
    780              await this.fetchStorageObjects(
    781                type,
    782                host,
    783                [JSON.stringify(name)],
    784                REASON.NEW_ROW
    785              );
    786            }
    787          } catch (ex) {
    788            // Do nothing
    789          }
    790        }
    791 
    792        if (this.tree.isSelected([type, host])) {
    793          await this.fetchStorageObjects(
    794            type,
    795            host,
    796            added[type][host],
    797            REASON.NEW_ROW
    798          );
    799        }
    800      }
    801    }
    802  }
    803 
    804  /**
    805   * Handle deleted items received by onEdit
    806   *
    807   * @param {object} See onEdit docs
    808   */
    809  async handleDeletedItems(deleted) {
    810    for (const type in deleted) {
    811      for (const host in deleted[type]) {
    812        if (!deleted[type][host].length) {
    813          // This means that the whole host is deleted, thus the item should
    814          // be removed from the storage tree
    815          if (this.tree.isSelected([type, host])) {
    816            this.table.clear();
    817            this.hideSidebar();
    818            this.tree.selectPreviousItem();
    819          }
    820 
    821          this.tree.remove([type, host]);
    822        } else {
    823          for (const name of deleted[type][host]) {
    824            try {
    825              if (["indexedDB", "Cache"].includes(type)) {
    826                // For indexedDB and Cache, the key is being parsed because
    827                // these storages are represented as a tree and the key
    828                // used to notify their changes is not a simple string.
    829                const names = JSON.parse(name);
    830                // Is a whole cache, database or objectstore deleted?
    831                // Then remove it from the tree.
    832                if (names.length < 3) {
    833                  if (this.tree.isSelected([type, host, ...names])) {
    834                    this.table.clear();
    835                    this.hideSidebar();
    836                    this.tree.selectPreviousItem();
    837                  }
    838                  this.tree.remove([type, host, ...names]);
    839                }
    840 
    841                // Remove the item from table if currently displayed.
    842                if (names.length) {
    843                  const tableItemName = names.pop();
    844                  if (this.tree.isSelected([type, host, ...names])) {
    845                    await this.removeItemFromTable(tableItemName);
    846                  }
    847                }
    848              } else if (this.tree.isSelected([type, host])) {
    849                // For all the other storage types with a simple string key,
    850                // remove the item from the table by name without any parsing.
    851                await this.removeItemFromTable(name);
    852              }
    853            } catch (ex) {
    854              if (this.tree.isSelected([type, host])) {
    855                await this.removeItemFromTable(name);
    856              }
    857            }
    858          }
    859        }
    860      }
    861    }
    862  }
    863 
    864  /**
    865   * Handle changed items received by onEdit
    866   *
    867   * @param {object} See onEdit docs
    868   */
    869  async handleChangedItems(changed) {
    870    const selectedItem = this.tree.selectedItem;
    871    if (!selectedItem) {
    872      return;
    873    }
    874 
    875    const [type, host, db, objectStore] = selectedItem;
    876    if (!changed[type] || !changed[type][host] || !changed[type][host].length) {
    877      return;
    878    }
    879    try {
    880      const toUpdate = [];
    881      for (const name of changed[type][host]) {
    882        if (["indexedDB", "Cache"].includes(type)) {
    883          // For indexedDB and Cache, the key is being parsed because
    884          // these storage are represented as a tree and the key
    885          // used to notify their changes is not a simple string.
    886          const names = JSON.parse(name);
    887          if (names[0] == db && names[1] == objectStore && names[2]) {
    888            toUpdate.push(name);
    889          }
    890        } else {
    891          // For all the other storage types with a simple string key,
    892          // update the item from the table by name without any parsing.
    893          toUpdate.push(name);
    894        }
    895      }
    896      await this.fetchStorageObjects(type, host, toUpdate, REASON.UPDATE);
    897    } catch (ex) {
    898      await this.fetchStorageObjects(
    899        type,
    900        host,
    901        changed[type][host],
    902        REASON.UPDATE
    903      );
    904    }
    905  }
    906 
    907  /**
    908   * Fetches the storage objects from the storage actor and populates the
    909   * storage table with the returned data.
    910   *
    911   * @param {string} type
    912   *        The type of storage. Ex. "cookies"
    913   * @param {string} host
    914   *        Hostname
    915   * @param {Array} names
    916   *        Names of particular store objects. Empty if all are requested
    917   * @param {Constant} reason
    918   *        See REASON constant at top of file.
    919   */
    920  async fetchStorageObjects(type, host, names, reason) {
    921    const fetchOpts =
    922      reason === REASON.NEXT_50_ITEMS ? { offset: this.itemOffset } : {};
    923    fetchOpts.sessionString = standardSessionString;
    924    const storage = this._getStorage(type, host);
    925    this.sidebarToggledOpen = null;
    926 
    927    if (
    928      reason !== REASON.NEXT_50_ITEMS &&
    929      reason !== REASON.UPDATE &&
    930      reason !== REASON.NEW_ROW &&
    931      reason !== REASON.POPULATE
    932    ) {
    933      throw new Error("Invalid reason specified");
    934    }
    935 
    936    try {
    937      if (
    938        reason === REASON.POPULATE ||
    939        (reason === REASON.NEW_ROW && this.table.items.size === 0)
    940      ) {
    941        let subType = null;
    942        // The indexedDB type could have sub-type data to fetch.
    943        // If having names specified, then it means
    944        // we are fetching details of specific database or of object store.
    945        if (type === "indexedDB" && names) {
    946          const [dbName, objectStoreName] = JSON.parse(names[0]);
    947          if (dbName) {
    948            subType = "database";
    949          }
    950          if (objectStoreName) {
    951            subType = "object store";
    952          }
    953        }
    954 
    955        await this.resetColumns(type, host, subType);
    956      }
    957 
    958      const { data, total } = await storage.getStoreObjects(
    959        host,
    960        names,
    961        fetchOpts
    962      );
    963      if (data.length) {
    964        await this.populateTable(data, reason, total);
    965      } else if (reason === REASON.POPULATE) {
    966        await this.clearHeaders();
    967      }
    968      this.updateToolbar();
    969      this.emit("store-objects-updated");
    970    } catch (ex) {
    971      console.error(ex);
    972    }
    973  }
    974 
    975  supportsAddItem(type, host) {
    976    const storage = this._getStorage(type, host);
    977    return storage?.traits.supportsAddItem || false;
    978  }
    979 
    980  supportsRemoveItem(type, host) {
    981    const storage = this._getStorage(type, host);
    982    return storage?.traits.supportsRemoveItem || false;
    983  }
    984 
    985  supportsRemoveAll(type, host) {
    986    const storage = this._getStorage(type, host);
    987    return storage?.traits.supportsRemoveAll || false;
    988  }
    989 
    990  supportsRemoveAllSessionCookies(type, host) {
    991    const storage = this._getStorage(type, host);
    992    return storage?.traits.supportsRemoveAllSessionCookies || false;
    993  }
    994 
    995  /**
    996   * Updates the toolbar hiding and showing buttons as appropriate.
    997   */
    998  updateToolbar() {
    999    const item = this.tree.selectedItem;
   1000    if (!item) {
   1001      return;
   1002    }
   1003 
   1004    const [type, host] = item;
   1005 
   1006    // Add is only supported if the selected item has a host.
   1007    this._addButton.hidden = !host || !this.supportsAddItem(type, host);
   1008  }
   1009 
   1010  /**
   1011   * Populates the storage tree which displays the list of storages present for
   1012   * the page.
   1013   */
   1014  async populateStorageTree() {
   1015    const populateTreeFromResource = (type, resource) => {
   1016      for (const host in resource.hosts) {
   1017        const label = this.getReadableLabelFromHostname(host);
   1018        this.tree.add([type, { id: host, label, type: "url" }]);
   1019        for (const name of resource.hosts[host]) {
   1020          try {
   1021            const names = JSON.parse(name);
   1022            this.tree.add([type, host, ...names]);
   1023            if (!this.tree.selectedItem) {
   1024              this.tree.selectedItem = [type, host, names[0], names[1]];
   1025            }
   1026          } catch (ex) {
   1027            // Do Nothing
   1028          }
   1029        }
   1030        if (!this.tree.selectedItem) {
   1031          this.tree.selectedItem = [type, host];
   1032        }
   1033      }
   1034    };
   1035 
   1036    // When can we expect the "store-objects-updated" event?
   1037    //   -> TreeWidget setter `selectedItem` emits a "select" event
   1038    //   -> on tree "select" event, this module calls `onHostSelect`
   1039    //   -> finally `onHostSelect` calls `fetchStorageObjects`, which will emit
   1040    //      "store-objects-updated" at the end of the method.
   1041    // So if the selection changed, we can wait for "store-objects-updated",
   1042    // which is emitted at the end of `fetchStorageObjects`.
   1043    const onStoresObjectsUpdated = this.once("store-objects-updated");
   1044 
   1045    // Save the initially selected item to check if tree.selected was updated,
   1046    // see comment above.
   1047    const initialSelectedItem = this.tree.selectedItem;
   1048 
   1049    for (const [type, resources] of Object.entries(this.storageResources)) {
   1050      let typeLabel = type;
   1051      try {
   1052        typeLabel = this.getStorageTypeLabel(type);
   1053      } catch (e) {
   1054        console.error("Unable to localize tree label type:" + type);
   1055      }
   1056 
   1057      this.tree.add([{ id: type, label: typeLabel, type: "store" }]);
   1058 
   1059      // storageResources values are arrays, with storage resources.
   1060      // we may have many storage resources per type if we get remote iframes.
   1061      for (const resource of resources) {
   1062        populateTreeFromResource(type, resource);
   1063      }
   1064    }
   1065 
   1066    if (initialSelectedItem !== this.tree.selectedItem) {
   1067      await onStoresObjectsUpdated;
   1068    }
   1069  }
   1070 
   1071  getStorageTypeLabel(type) {
   1072    let dataL10nId;
   1073 
   1074    switch (type) {
   1075      case "cookies":
   1076        dataL10nId = "storage-tree-labels-cookies";
   1077        break;
   1078      case "localStorage":
   1079        dataL10nId = "storage-tree-labels-local-storage";
   1080        break;
   1081      case "sessionStorage":
   1082        dataL10nId = "storage-tree-labels-session-storage";
   1083        break;
   1084      case "indexedDB":
   1085        dataL10nId = "storage-tree-labels-indexed-db";
   1086        break;
   1087      case "Cache":
   1088        dataL10nId = "storage-tree-labels-cache";
   1089        break;
   1090      case "extensionStorage":
   1091        dataL10nId = "storage-tree-labels-extension-storage";
   1092        break;
   1093      default:
   1094        throw new Error("Unknown storage type");
   1095    }
   1096 
   1097    return this._l10nStrings.get(dataL10nId);
   1098  }
   1099 
   1100  /**
   1101   * Populates the selected entry from the table in the sidebar for a more
   1102   * detailed view.
   1103   */
   1104  /* eslint-disable-next-line */
   1105  async updateObjectSidebar() {
   1106    const item = this.table.selectedRow;
   1107    let value;
   1108 
   1109    // Get the string value (async action) and the update the UI synchronously.
   1110    if ((item?.name || item?.name === "") && item?.valueActor) {
   1111      value = await item.valueActor.string();
   1112    }
   1113 
   1114    // Bail if the selectedRow is no longer selected, the item doesn't exist or the state
   1115    // changed in another way during the above yield.
   1116    if (
   1117      this.table.items.size === 0 ||
   1118      !item ||
   1119      !this.table.selectedRow ||
   1120      item.uniqueKey !== this.table.selectedRow.uniqueKey
   1121    ) {
   1122      this.hideSidebar();
   1123      return;
   1124    }
   1125 
   1126    // Start updating the UI. Everything is sync beyond this point.
   1127    if (this.sidebarToggledOpen === null || this.sidebarToggledOpen === true) {
   1128      this.sidebar.hidden = false;
   1129    }
   1130 
   1131    this.updateSidebarToggleButton();
   1132    this.view.empty();
   1133    const mainScope = this.view.addScope("storage-data");
   1134    mainScope.expanded = true;
   1135 
   1136    if (value) {
   1137      const itemVar = mainScope.addItem(item.name + "", {}, { relaxed: true });
   1138 
   1139      // The main area where the value will be displayed
   1140      itemVar.setGrip(value);
   1141 
   1142      // May be the item value is a json or a key value pair itself
   1143      const obj = parseItemValue(value);
   1144      if (typeof obj === "object") {
   1145        this.populateSidebar(item.name, obj);
   1146      }
   1147 
   1148      // By default the item name and value are shown. If this is the only
   1149      // information available, then nothing else is to be displayed.
   1150      const itemProps = Object.keys(item);
   1151      if (itemProps.length > 3) {
   1152        // Display any other information other than the item name and value
   1153        // which may be available.
   1154        const rawObject = Object.create(null);
   1155        const otherProps = itemProps.filter(
   1156          e => !["name", "value", "valueActor"].includes(e)
   1157        );
   1158        for (const prop of otherProps) {
   1159          const column = this.table.columns.get(prop);
   1160          if (column?.private) {
   1161            continue;
   1162          }
   1163 
   1164          const fieldName = this._getColumnName(this.table.datatype, prop);
   1165          rawObject[fieldName] = item[prop];
   1166        }
   1167        itemVar.populate(rawObject, { sorted: true });
   1168        itemVar.twisty = true;
   1169        itemVar.expanded = true;
   1170      }
   1171    } else {
   1172      // Case when displaying IndexedDB db/object store properties.
   1173      for (const key in item) {
   1174        const column = this.table.columns.get(key);
   1175        if (column?.private) {
   1176          continue;
   1177        }
   1178 
   1179        mainScope.addItem(key, {}, true).setGrip(item[key]);
   1180        const obj = parseItemValue(item[key]);
   1181        if (typeof obj === "object") {
   1182          this.populateSidebar(item.name, obj);
   1183        }
   1184      }
   1185    }
   1186 
   1187    this.emit("sidebar-updated");
   1188  }
   1189 
   1190  /**
   1191   * Gets a readable label from the hostname. If the hostname is a Punycode
   1192   * domain(I.e. an ASCII domain name representing a Unicode domain name), then
   1193   * this function decodes it to the readable Unicode domain name, and label
   1194   * the Unicode domain name toggether with the original domian name, and then
   1195   * return the label; if the hostname isn't a Punycode domain(I.e. it isn't
   1196   * encoded and is readable on its own), then this function simply returns the
   1197   * original hostname.
   1198   *
   1199   * @param {string} host
   1200   *        The string representing a host, e.g, example.com, example.com:8000
   1201   */
   1202  getReadableLabelFromHostname(host) {
   1203    try {
   1204      const { hostname } = new URL(host);
   1205      const unicodeHostname = getUnicodeHostname(hostname);
   1206      if (hostname !== unicodeHostname) {
   1207        // If the hostname is a Punycode domain representing a Unicode domain,
   1208        // we decode it to the Unicode domain name, and then label the Unicode
   1209        // domain name together with the original domain name.
   1210        return host.replace(hostname, unicodeHostname) + " [ " + host + " ]";
   1211      }
   1212    } catch (_) {
   1213      // Skip decoding for a host which doesn't include a domain name, simply
   1214      // consider them to be readable.
   1215    }
   1216    return host;
   1217  }
   1218 
   1219  /**
   1220   * Populates the sidebar with a parsed object.
   1221   *
   1222   * @param {object} obj - Either a json or a key-value separated object or a
   1223   * key separated array
   1224   */
   1225  populateSidebar(name, obj) {
   1226    const jsonObject = Object.create(null);
   1227    const view = this.view;
   1228    jsonObject[name] = obj;
   1229    const valueScope =
   1230      view.getScopeAtIndex(1) || view.addScope("storage-parsed-value");
   1231    valueScope.expanded = true;
   1232    const jsonVar = valueScope.addItem("", Object.create(null), {
   1233      relaxed: true,
   1234    });
   1235    jsonVar.expanded = true;
   1236    jsonVar.twisty = true;
   1237    jsonVar.populate(jsonObject, { expanded: true });
   1238  }
   1239 
   1240  /**
   1241   * Select handler for the storage tree. Fetches details of the selected item
   1242   * from the storage details and populates the storage tree.
   1243   *
   1244   * @param {Array} item
   1245   *        An array of ids which represent the location of the selected item in
   1246   *        the storage tree
   1247   */
   1248  async onHostSelect(item) {
   1249    if (!item) {
   1250      return;
   1251    }
   1252 
   1253    this.table.clear();
   1254    this.hideSidebar();
   1255    this.searchBox.value = "";
   1256 
   1257    const [type, host] = item;
   1258    this.table.host = host;
   1259    this.table.datatype = type;
   1260 
   1261    this.updateToolbar();
   1262 
   1263    let names = null;
   1264    if (!host) {
   1265      let storageTypeHintL10nId = "";
   1266      switch (type) {
   1267        case "Cache":
   1268          storageTypeHintL10nId = "storage-table-type-cache-hint";
   1269          break;
   1270        case "cookies":
   1271          storageTypeHintL10nId = "storage-table-type-cookies-hint";
   1272          break;
   1273        case "extensionStorage":
   1274          storageTypeHintL10nId = "storage-table-type-extensionstorage-hint";
   1275          break;
   1276        case "localStorage":
   1277          storageTypeHintL10nId = "storage-table-type-localstorage-hint";
   1278          break;
   1279        case "indexedDB":
   1280          storageTypeHintL10nId = "storage-table-type-indexeddb-hint";
   1281          break;
   1282        case "sessionStorage":
   1283          storageTypeHintL10nId = "storage-table-type-sessionstorage-hint";
   1284          break;
   1285      }
   1286      this.table.setPlaceholder(
   1287        storageTypeHintL10nId,
   1288        getStorageTypeURL(this.table.datatype)
   1289      );
   1290 
   1291      // If selected item has no host then reset table headers
   1292      await this.clearHeaders();
   1293      return;
   1294    }
   1295    if (item.length > 2) {
   1296      names = [JSON.stringify(item.slice(2))];
   1297    }
   1298 
   1299    this.itemOffset = 0;
   1300    await this.fetchStorageObjects(type, host, names, REASON.POPULATE);
   1301  }
   1302 
   1303  /**
   1304   * Clear the column headers in the storage table
   1305   */
   1306  async clearHeaders() {
   1307    this.table.setColumns({}, null, {}, {});
   1308  }
   1309 
   1310  /**
   1311   * Resets the column headers in the storage table with the pased object `data`
   1312   *
   1313   * @param {string} type
   1314   *        The type of storage corresponding to the after-reset columns in the
   1315   *        table.
   1316   * @param {string} host
   1317   *        The host name corresponding to the table after reset.
   1318   *
   1319   * @param {string} [subType]
   1320   *        The sub type under the given type.
   1321   */
   1322  async resetColumns(type, host, subtype) {
   1323    this.table.host = host;
   1324    this.table.datatype = type;
   1325 
   1326    let uniqueKey = null;
   1327    const columns = {};
   1328    const editableFields = [];
   1329    const hiddenFields = [];
   1330    const privateFields = [];
   1331    const fields = await this.getCurrentFront().getFields(subtype);
   1332 
   1333    fields.forEach(f => {
   1334      if (!uniqueKey) {
   1335        this.table.uniqueId = uniqueKey = f.name;
   1336      }
   1337 
   1338      if (f.editable) {
   1339        editableFields.push(f.name);
   1340      }
   1341 
   1342      if (f.hidden) {
   1343        hiddenFields.push(f.name);
   1344      }
   1345 
   1346      if (f.private) {
   1347        privateFields.push(f.name);
   1348      }
   1349 
   1350      const columnName = this._getColumnName(type, f.name);
   1351      if (columnName) {
   1352        columns[f.name] = columnName;
   1353      } else if (!f.private) {
   1354        // Private fields are only displayed when running tests so there is no
   1355        // need to log an error if they are not localized.
   1356        columns[f.name] = f.name;
   1357        console.error(
   1358          `No string defined in HEADERS_NON_L10N_STRINGS for '${type}.${f.name}'`
   1359        );
   1360      }
   1361    });
   1362 
   1363    this.table.setColumns(columns, null, hiddenFields, privateFields);
   1364    this.hideSidebar();
   1365 
   1366    this.makeFieldsEditable(editableFields);
   1367  }
   1368 
   1369  /**
   1370   * Populates or updates the rows in the storage table.
   1371   *
   1372   * @param {Array[object]} data
   1373   *        Array of objects to be populated in the storage table
   1374   * @param {Constant} reason
   1375   *        See REASON constant at top of file.
   1376   * @param {number} totalAvailable
   1377   *        The total number of items available in the current storage type.
   1378   */
   1379  async populateTable(data, reason, totalAvailable) {
   1380    for (const item of data) {
   1381      if (item.value) {
   1382        item.valueActor = item.value;
   1383        item.value = item.value.initial || "";
   1384      }
   1385      if (item.expires != null) {
   1386        item.expires = item.expires
   1387          ? new Date(item.expires).toUTCString()
   1388          : this._l10nStrings.get("storage-expires-session");
   1389      }
   1390      if (item.creationTime != null) {
   1391        item.creationTime = new Date(item.creationTime).toUTCString();
   1392      }
   1393      if (item.updateTime != null) {
   1394        item.updateTime = new Date(item.updateTime).toUTCString();
   1395      }
   1396      if (item.lastAccessed != null) {
   1397        item.lastAccessed = new Date(item.lastAccessed).toUTCString();
   1398      }
   1399 
   1400      switch (reason) {
   1401        case REASON.POPULATE:
   1402        case REASON.NEXT_50_ITEMS:
   1403          // Update without flashing the row.
   1404          this.table.push(item, true);
   1405          break;
   1406        case REASON.NEW_ROW:
   1407          // Update and flash the row.
   1408          this.table.push(item, false);
   1409          break;
   1410        case REASON.UPDATE:
   1411          this.table.update(item);
   1412          if (item == this.table.selectedRow && !this.sidebar.hidden) {
   1413            await this.updateObjectSidebar();
   1414          }
   1415          break;
   1416      }
   1417 
   1418      this.shouldLoadMoreItems = true;
   1419    }
   1420 
   1421    if (
   1422      (reason === REASON.POPULATE || reason === REASON.NEXT_50_ITEMS) &&
   1423      this.table.items.size < totalAvailable &&
   1424      !this.table.hasScrollbar
   1425    ) {
   1426      await this.loadMoreItems();
   1427    }
   1428  }
   1429 
   1430  /**
   1431   * Handles keypress event on the body table to close the sidebar when open
   1432   *
   1433   * @param {DOMEvent} event
   1434   *        The event passed by the keypress event.
   1435   */
   1436  handleKeypress(event) {
   1437    if (event.keyCode == KeyCodes.DOM_VK_ESCAPE) {
   1438      if (!this.sidebar.hidden) {
   1439        this.hideSidebar();
   1440        this.sidebarToggledOpen = false;
   1441        // Stop Propagation to prevent opening up of split console
   1442        event.stopPropagation();
   1443        event.preventDefault();
   1444      }
   1445    } else if (
   1446      event.keyCode == KeyCodes.DOM_VK_BACK_SPACE ||
   1447      event.keyCode == KeyCodes.DOM_VK_DELETE
   1448    ) {
   1449      if (this.table.selectedRow && event.target.localName != "input") {
   1450        this.onRemoveItem(event);
   1451        event.stopPropagation();
   1452        event.preventDefault();
   1453      }
   1454    }
   1455  }
   1456 
   1457  /**
   1458   * Handles filtering the table
   1459   */
   1460  filterItems() {
   1461    const value = this.searchBox.value;
   1462    this.table.filterItems(value, ["valueActor"]);
   1463    this._panelDoc.documentElement.classList.toggle("filtering", !!value);
   1464  }
   1465 
   1466  /**
   1467   * Load the next batch of 50 items
   1468   */
   1469  async loadMoreItems() {
   1470    if (
   1471      !this.shouldLoadMoreItems ||
   1472      this._toolbox.currentToolId !== "storage" ||
   1473      !this.tree.selectedItem
   1474    ) {
   1475      return;
   1476    }
   1477    this.shouldLoadMoreItems = false;
   1478    this.itemOffset += 50;
   1479 
   1480    const item = this.tree.selectedItem;
   1481    const [type, host] = item;
   1482    let names = null;
   1483    if (item.length > 2) {
   1484      names = [JSON.stringify(item.slice(2))];
   1485    }
   1486    await this.fetchStorageObjects(type, host, names, REASON.NEXT_50_ITEMS);
   1487  }
   1488 
   1489  /**
   1490   * Fires before a cell context menu with the "Add" or "Delete" action is
   1491   * shown. If the currently selected storage object doesn't support adding or
   1492   * removing items, prevent showing the menu.
   1493   */
   1494  onTablePopupShowing(event) {
   1495    const selectedItem = this.tree.selectedItem;
   1496    const [type, host] = selectedItem;
   1497 
   1498    // IndexedDB only supports removing items from object stores (level 4 of the tree)
   1499    if (
   1500      (!this.supportsAddItem(type, host) &&
   1501        !this.supportsRemoveItem(type, host)) ||
   1502      (type === "indexedDB" && selectedItem.length !== 4)
   1503    ) {
   1504      event.preventDefault();
   1505      return;
   1506    }
   1507 
   1508    const rowId = this.table.contextMenuRowId;
   1509    const data = this.table.items.get(rowId);
   1510 
   1511    if (this.supportsRemoveItem(type, host)) {
   1512      const name = data[this.table.uniqueId];
   1513      const separatorRegex = new RegExp(SEPARATOR_GUID, "g");
   1514      const label = addEllipsis((name + "").replace(separatorRegex, "-"));
   1515 
   1516      this._panelDoc.l10n.setArgs(this._tablePopupDelete, { itemName: label });
   1517      this._tablePopupDelete.hidden = false;
   1518    } else {
   1519      this._tablePopupDelete.hidden = true;
   1520    }
   1521 
   1522    this._tablePopupAddItem.hidden = !this.supportsAddItem(type, host);
   1523 
   1524    let showDeleteAllSessionCookies = false;
   1525    if (this.supportsRemoveAllSessionCookies(type, host)) {
   1526      if (selectedItem.length === 2) {
   1527        showDeleteAllSessionCookies = true;
   1528      }
   1529    }
   1530 
   1531    this._tablePopupDeleteAllSessionCookies.hidden =
   1532      !showDeleteAllSessionCookies;
   1533 
   1534    if (type === "cookies") {
   1535      const hostString = addEllipsis(data.host);
   1536 
   1537      this._panelDoc.l10n.setArgs(this._tablePopupDeleteAllFrom, {
   1538        host: hostString,
   1539      });
   1540      this._tablePopupDeleteAllFrom.hidden = false;
   1541    } else {
   1542      this._tablePopupDeleteAllFrom.hidden = true;
   1543    }
   1544  }
   1545 
   1546  onTreePopupShowing(event) {
   1547    let showMenu = false;
   1548    const selectedItem = this.tree.selectedItem;
   1549 
   1550    if (selectedItem) {
   1551      const [type, host] = selectedItem;
   1552 
   1553      // The delete all (aka clear) action is displayed for IndexedDB object stores
   1554      // (level 4 of tree), for Cache objects (level 3) and for the whole host (level 2)
   1555      // for other storage types (cookies, localStorage, ...).
   1556      let showDeleteAll = false;
   1557      if (this.supportsRemoveAll(type, host)) {
   1558        let level;
   1559        if (type == "indexedDB") {
   1560          level = 4;
   1561        } else if (type == "Cache") {
   1562          level = 3;
   1563        } else {
   1564          level = 2;
   1565        }
   1566 
   1567        if (selectedItem.length == level) {
   1568          showDeleteAll = true;
   1569        }
   1570      }
   1571 
   1572      this._treePopupDeleteAll.hidden = !showDeleteAll;
   1573 
   1574      // The delete all session cookies action is displayed for cookie object stores
   1575      // (level 2 of tree)
   1576      let showDeleteAllSessionCookies = false;
   1577      if (this.supportsRemoveAllSessionCookies(type, host)) {
   1578        if (type === "cookies" && selectedItem.length === 2) {
   1579          showDeleteAllSessionCookies = true;
   1580        }
   1581      }
   1582 
   1583      this._treePopupDeleteAllSessionCookies.hidden =
   1584        !showDeleteAllSessionCookies;
   1585 
   1586      // The delete action is displayed for:
   1587      // - IndexedDB databases (level 3 of the tree)
   1588      // - Cache objects (level 3 of the tree)
   1589      const showDelete =
   1590        (type == "indexedDB" || type == "Cache") && selectedItem.length == 3;
   1591      this._treePopupDelete.hidden = !showDelete;
   1592      if (showDelete) {
   1593        const itemName = addEllipsis(selectedItem[selectedItem.length - 1]);
   1594        this._panelDoc.l10n.setArgs(this._treePopupDelete, { itemName });
   1595      }
   1596 
   1597      showMenu = showDeleteAll || showDelete;
   1598    }
   1599 
   1600    if (!showMenu) {
   1601      event.preventDefault();
   1602    }
   1603  }
   1604 
   1605  onVariableViewPopupShowing() {
   1606    const item = this.view.getFocusedItem();
   1607    this._variableViewPopupCopy.toggleAttribute("disabled", !item);
   1608  }
   1609 
   1610  /**
   1611   * Handles refreshing the selected storage
   1612   */
   1613  async onRefreshTable() {
   1614    await this.onHostSelect(this.tree.selectedItem);
   1615  }
   1616 
   1617  /**
   1618   * Handles adding an item from the storage
   1619   */
   1620  async onAddItem() {
   1621    const selectedItem = this.tree.selectedItem;
   1622    if (!selectedItem) {
   1623      return;
   1624    }
   1625 
   1626    const front = this.getCurrentFront();
   1627    const [, host] = selectedItem;
   1628 
   1629    // Prepare to scroll into view.
   1630    this.table.scrollIntoViewOnUpdate = true;
   1631    this.table.editBookmark = createGUID();
   1632    const result = await front.addItem(this.table.editBookmark, host);
   1633 
   1634    // At the moment, only adding cookies can (theorically) return an error (although in
   1635    // practice, since we set the default properties of the cookie ourselves, this shouldn't
   1636    // happen).
   1637    if (front.typeName === "cookies" && result?.errorString) {
   1638      const notificationBox = this._toolbox.getNotificationBox();
   1639      const message = await this._panelDoc.l10n.formatValue(
   1640        "storage-cookie-create-error",
   1641        { errorString: result.errorString }
   1642      );
   1643 
   1644      notificationBox.appendNotification(
   1645        message,
   1646        "storage-cookie-create-error",
   1647        null,
   1648        notificationBox.PRIORITY_WARNING_LOW
   1649      );
   1650    }
   1651  }
   1652 
   1653  /**
   1654   * Handles copy an item from the storage
   1655   */
   1656  onCopyItem() {
   1657    this.view._copyItem();
   1658  }
   1659 
   1660  /**
   1661   * Handles removing an item from the storage
   1662   *
   1663   * @param {DOMEvent} event
   1664   *        The event passed by the command or keypress event.
   1665   */
   1666  onRemoveItem(event) {
   1667    const [, host, ...path] = this.tree.selectedItem;
   1668    const front = this.getCurrentFront();
   1669    const uniqueId = this.table.uniqueId;
   1670    const rowId =
   1671      event.type == "command"
   1672        ? this.table.contextMenuRowId
   1673        : this.table.selectedRow[uniqueId];
   1674    const data = this.table.items.get(rowId);
   1675 
   1676    let name = data[uniqueId];
   1677    if (path.length) {
   1678      name = JSON.stringify([...path, name]);
   1679    }
   1680    front.removeItem(host, name);
   1681 
   1682    return false;
   1683  }
   1684 
   1685  /**
   1686   * Handles removing all items from the storage
   1687   */
   1688  onRemoveAll() {
   1689    // Cannot use this.currentActor() if the handler is called from the
   1690    // tree context menu: it returns correct value only after the table
   1691    // data from server are successfully fetched (and that's async).
   1692    const [, host, ...path] = this.tree.selectedItem;
   1693    const front = this.getCurrentFront();
   1694    const name = path.length ? JSON.stringify(path) : undefined;
   1695    front.removeAll(host, name);
   1696  }
   1697 
   1698  /**
   1699   * Handles removing all session cookies from the storage
   1700   */
   1701  onRemoveAllSessionCookies() {
   1702    // Cannot use this.currentActor() if the handler is called from the
   1703    // tree context menu: it returns the correct value only after the
   1704    // table data from server is successfully fetched (and that's async).
   1705    const [, host, ...path] = this.tree.selectedItem;
   1706    const front = this.getCurrentFront();
   1707    const name = path.length ? JSON.stringify(path) : undefined;
   1708    front.removeAllSessionCookies(host, name);
   1709  }
   1710 
   1711  /**
   1712   * Handles removing all cookies with exactly the same domain as the
   1713   * cookie in the selected row.
   1714   */
   1715  onRemoveAllFrom() {
   1716    const [, host] = this.tree.selectedItem;
   1717    const front = this.getCurrentFront();
   1718    const rowId = this.table.contextMenuRowId;
   1719    const data = this.table.items.get(rowId);
   1720 
   1721    front.removeAll(host, data.host);
   1722  }
   1723 
   1724  onRemoveTreeItem() {
   1725    const [type, host, ...path] = this.tree.selectedItem;
   1726 
   1727    if (type == "indexedDB" && path.length == 1) {
   1728      this.removeDatabase(host, path[0]);
   1729    } else if (type == "Cache" && path.length == 1) {
   1730      this.removeCache(host, path[0]);
   1731    }
   1732  }
   1733 
   1734  async removeDatabase(host, dbName) {
   1735    const front = this.getCurrentFront();
   1736 
   1737    try {
   1738      const result = await front.removeDatabase(host, dbName);
   1739      if (result.blocked) {
   1740        const notificationBox = this._toolbox.getNotificationBox();
   1741        const message = await this._panelDoc.l10n.formatValue(
   1742          "storage-idb-delete-blocked",
   1743          { dbName }
   1744        );
   1745 
   1746        notificationBox.appendNotification(
   1747          message,
   1748          "storage-idb-delete-blocked",
   1749          null,
   1750          notificationBox.PRIORITY_WARNING_LOW
   1751        );
   1752      }
   1753    } catch (error) {
   1754      const notificationBox = this._toolbox.getNotificationBox();
   1755      const message = await this._panelDoc.l10n.formatValue(
   1756        "storage-idb-delete-error",
   1757        { dbName }
   1758      );
   1759      notificationBox.appendNotification(
   1760        message,
   1761        "storage-idb-delete-error",
   1762        null,
   1763        notificationBox.PRIORITY_CRITICAL_LOW
   1764      );
   1765    }
   1766  }
   1767 
   1768  removeCache(host, cacheName) {
   1769    const front = this.getCurrentFront();
   1770 
   1771    front.removeItem(host, JSON.stringify([cacheName]));
   1772  }
   1773 }
   1774 
   1775 exports.StorageUI = StorageUI;
   1776 
   1777 // Helper Functions
   1778 
   1779 function createGUID() {
   1780  return "{cccccccc-cccc-4ccc-yccc-cccccccccccc}".replace(/[cy]/g, c => {
   1781    const r = (Math.random() * 16) | 0;
   1782    const v = c == "c" ? r : (r & 0x3) | 0x8;
   1783    return v.toString(16);
   1784  });
   1785 }
   1786 
   1787 function addEllipsis(name) {
   1788  if (name.length > ITEM_NAME_MAX_LENGTH) {
   1789    if (/^https?:/.test(name)) {
   1790      // For URLs, add ellipsis in the middle
   1791      const halfLen = ITEM_NAME_MAX_LENGTH / 2;
   1792      return name.slice(0, halfLen) + ELLIPSIS + name.slice(-halfLen);
   1793    }
   1794 
   1795    // For other strings, add ellipsis at the end
   1796    return name.substr(0, ITEM_NAME_MAX_LENGTH) + ELLIPSIS;
   1797  }
   1798 
   1799  return name;
   1800 }