tor-browser

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

siteDataSettings.js (10261B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 "use strict";
      7 
      8 var { AppConstants } = ChromeUtils.importESModule(
      9  "resource://gre/modules/AppConstants.sys.mjs"
     10 );
     11 
     12 ChromeUtils.defineESModuleGetters(this, {
     13  DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
     14  SiteDataManager: "resource:///modules/SiteDataManager.sys.mjs",
     15 });
     16 
     17 let gSiteDataSettings = {
     18  // Array of metadata of sites. Each array element is object holding:
     19  // - uri: uri of site; instance of nsIURI
     20  // - baseDomain: base domain of the site
     21  // - cookies: array of cookies of that site
     22  // - usage: disk usage which site uses
     23  // - userAction: "remove" or "update-permission"; the action user wants to take.
     24  _sites: null,
     25 
     26  _list: null,
     27  _searchBox: null,
     28 
     29  _createSiteListItem(site) {
     30    let item = document.createXULElement("richlistitem");
     31    item.setAttribute("host", site.baseDomain);
     32    let container = document.createXULElement("hbox");
     33 
     34    // Creates a new column item with the specified relative width.
     35    function addColumnItem(l10n, flexWidth, tooltipText) {
     36      let box = document.createXULElement("hbox");
     37      box.className = "item-box";
     38      box.setAttribute("style", `flex: ${flexWidth} ${flexWidth};`);
     39      let label = document.createXULElement("label");
     40      label.setAttribute("crop", "end");
     41      if (l10n) {
     42        if (l10n.hasOwnProperty("raw")) {
     43          box.setAttribute("tooltiptext", l10n.raw);
     44          label.setAttribute("value", l10n.raw);
     45        } else {
     46          document.l10n.setAttributes(label, l10n.id, l10n.args);
     47        }
     48      }
     49      if (tooltipText) {
     50        box.setAttribute("tooltiptext", tooltipText);
     51      }
     52      box.appendChild(label);
     53      container.appendChild(box);
     54    }
     55 
     56    // Add "Host" column.
     57    let hostData = site.baseDomain
     58      ? { raw: site.baseDomain }
     59      : { id: "site-data-local-file-host" };
     60    addColumnItem(hostData, "4");
     61 
     62    // Add "Cookies" column.
     63    addColumnItem({ raw: site.cookies.length }, "1");
     64 
     65    // Add "Storage" column
     66    if (site.usage > 0 || site.persisted) {
     67      let [value, unit] = DownloadUtils.convertByteUnits(site.usage);
     68      let strName = site.persisted
     69        ? "site-storage-persistent"
     70        : "site-storage-usage";
     71      addColumnItem(
     72        {
     73          id: strName,
     74          args: { value, unit },
     75        },
     76        "2"
     77      );
     78    } else {
     79      // Pass null to avoid showing "0KB" when there is no site data stored.
     80      addColumnItem(null, "2");
     81    }
     82 
     83    // Add "Last Used" column.
     84    let formattedLastAccessed =
     85      site.lastAccessed > 0
     86        ? this._relativeTimeFormat.formatBestUnit(site.lastAccessed)
     87        : null;
     88    let formattedFullDate =
     89      site.lastAccessed > 0
     90        ? this._absoluteTimeFormat.format(site.lastAccessed)
     91        : null;
     92    addColumnItem(
     93      site.lastAccessed > 0 ? { raw: formattedLastAccessed } : null,
     94      "2",
     95      formattedFullDate
     96    );
     97 
     98    item.appendChild(container);
     99    return item;
    100  },
    101 
    102  init() {
    103    function setEventListener(id, eventType, callback) {
    104      document
    105        .getElementById(id)
    106        .addEventListener(eventType, callback.bind(gSiteDataSettings));
    107    }
    108 
    109    this._absoluteTimeFormat = new Services.intl.DateTimeFormat(undefined, {
    110      dateStyle: "short",
    111      timeStyle: "short",
    112    });
    113 
    114    this._relativeTimeFormat = new Services.intl.RelativeTimeFormat(
    115      undefined,
    116      {}
    117    );
    118 
    119    this._list = document.getElementById("sitesList");
    120    this._searchBox = document.getElementById("searchBox");
    121    SiteDataManager.getSites().then(sites => {
    122      this._sites = sites;
    123      let sortCol = document.querySelector(
    124        "treecol[data-isCurrentSortCol=true]"
    125      );
    126      this._sortSites(this._sites, sortCol);
    127      this._buildSitesList(this._sites);
    128      Services.obs.notifyObservers(null, "sitedata-settings-init");
    129    });
    130 
    131    setEventListener("sitesList", "select", this.onSelect);
    132    setEventListener("hostCol", "click", this.onClickTreeCol);
    133    setEventListener("usageCol", "click", this.onClickTreeCol);
    134    setEventListener("lastAccessedCol", "click", this.onClickTreeCol);
    135    setEventListener("cookiesCol", "click", this.onClickTreeCol);
    136    setEventListener("searchBox", "MozInputSearch:search", this.onInputSearch);
    137    setEventListener("removeAll", "command", this.onClickRemoveAll);
    138    setEventListener("removeSelected", "command", this.removeSelected);
    139 
    140    document.addEventListener("dialogaccept", e => this.saveChanges(e));
    141    window.addEventListener("keypress", e => this.onKeyPress(e));
    142  },
    143 
    144  _updateButtonsState() {
    145    let items = this._list.getElementsByTagName("richlistitem");
    146    let removeSelectedBtn = document.getElementById("removeSelected");
    147    let removeAllBtn = document.getElementById("removeAll");
    148    removeSelectedBtn.disabled = !this._list.selectedItems.length;
    149    removeAllBtn.disabled = !items.length;
    150 
    151    let l10nId = this._searchBox.value
    152      ? "site-data-remove-shown"
    153      : "site-data-remove-all";
    154    document.l10n.setAttributes(removeAllBtn, l10nId);
    155  },
    156 
    157  /**
    158   * @param sites {Array}
    159   * @param col {XULElement} the <treecol> being sorted on
    160   */
    161  _sortSites(sites, col) {
    162    let isCurrentSortCol = col.getAttribute("data-isCurrentSortCol");
    163    let sortDirection =
    164      col.getAttribute("data-last-sortDirection") || "ascending";
    165    if (isCurrentSortCol) {
    166      // Sort on the current column, flip the sorting direction
    167      sortDirection =
    168        sortDirection === "ascending" ? "descending" : "ascending";
    169    }
    170 
    171    let sortFunc = null;
    172    switch (col.id) {
    173      case "hostCol":
    174        sortFunc = (a, b) => {
    175          let aHost = a.baseDomain.toLowerCase();
    176          let bHost = b.baseDomain.toLowerCase();
    177          return aHost.localeCompare(bHost);
    178        };
    179        break;
    180 
    181      case "cookiesCol":
    182        sortFunc = (a, b) => a.cookies.length - b.cookies.length;
    183        break;
    184 
    185      case "usageCol":
    186        sortFunc = (a, b) => a.usage - b.usage;
    187        break;
    188 
    189      case "lastAccessedCol":
    190        sortFunc = (a, b) => a.lastAccessed - b.lastAccessed;
    191        break;
    192    }
    193    if (sortDirection === "descending") {
    194      sites.sort((a, b) => sortFunc(b, a));
    195    } else {
    196      sites.sort(sortFunc);
    197    }
    198 
    199    let cols = this._list.previousElementSibling.querySelectorAll("treecol");
    200    cols.forEach(c => {
    201      c.removeAttribute("sortDirection");
    202      c.removeAttribute("data-isCurrentSortCol");
    203    });
    204    col.setAttribute("data-isCurrentSortCol", true);
    205    col.setAttribute("sortDirection", sortDirection);
    206    col.setAttribute("data-last-sortDirection", sortDirection);
    207  },
    208 
    209  /**
    210   * @param sites {Array} array of metadata of sites
    211   */
    212  _buildSitesList(sites) {
    213    // Clear old entries.
    214    let oldItems = this._list.querySelectorAll("richlistitem");
    215    for (let item of oldItems) {
    216      item.remove();
    217    }
    218 
    219    let keyword = this._searchBox.value.toLowerCase().trim();
    220    let fragment = document.createDocumentFragment();
    221    for (let site of sites) {
    222      if (keyword && !site.baseDomain.includes(keyword)) {
    223        continue;
    224      }
    225 
    226      if (site.userAction === "remove") {
    227        continue;
    228      }
    229 
    230      let item = this._createSiteListItem(site);
    231      fragment.appendChild(item);
    232    }
    233    this._list.appendChild(fragment);
    234    this._updateButtonsState();
    235  },
    236 
    237  _removeSiteItems(items) {
    238    for (let i = items.length - 1; i >= 0; --i) {
    239      let item = items[i];
    240      let baseDomain = item.getAttribute("host");
    241      let siteForBaseDomain = this._sites.find(
    242        site => site.baseDomain == baseDomain
    243      );
    244      if (siteForBaseDomain) {
    245        siteForBaseDomain.userAction = "remove";
    246      }
    247      item.remove();
    248    }
    249    this._updateButtonsState();
    250  },
    251 
    252  async saveChanges(event) {
    253    let removals = this._sites
    254      .filter(site => site.userAction == "remove")
    255      .map(site => site.baseDomain);
    256 
    257    if (removals.length) {
    258      let removeAll = removals.length == this._sites.length;
    259      let promptArg = removeAll ? undefined : removals;
    260      if (!SiteDataManager.promptSiteDataRemoval(window, promptArg)) {
    261        // If the user cancelled the confirm dialog keep the site data window open,
    262        // they can still press cancel again to exit.
    263        event.preventDefault();
    264        return;
    265      }
    266      try {
    267        if (removeAll) {
    268          await SiteDataManager.removeAll();
    269        } else {
    270          await SiteDataManager.remove(removals);
    271        }
    272      } catch (e) {
    273        console.error(e);
    274      }
    275    }
    276  },
    277 
    278  removeSelected() {
    279    let lastIndex = this._list.selectedItems.length - 1;
    280    let lastSelectedItem = this._list.selectedItems[lastIndex];
    281    let lastSelectedItemPosition = this._list.getIndexOfItem(lastSelectedItem);
    282    let nextSelectedItem = this._list.getItemAtIndex(
    283      lastSelectedItemPosition + 1
    284    );
    285 
    286    this._removeSiteItems(this._list.selectedItems);
    287    this._list.clearSelection();
    288 
    289    if (nextSelectedItem) {
    290      this._list.selectedItem = nextSelectedItem;
    291    } else {
    292      this._list.selectedIndex = this._list.itemCount - 1;
    293    }
    294  },
    295 
    296  onClickTreeCol(e) {
    297    this._sortSites(this._sites, e.target);
    298    this._buildSitesList(this._sites);
    299    this._list.clearSelection();
    300  },
    301 
    302  onInputSearch() {
    303    this._buildSitesList(this._sites);
    304    this._list.clearSelection();
    305  },
    306 
    307  onClickRemoveAll() {
    308    let siteItems = this._list.getElementsByTagName("richlistitem");
    309    if (siteItems.length) {
    310      this._removeSiteItems(siteItems);
    311    }
    312  },
    313 
    314  onKeyPress(e) {
    315    if (
    316      e.keyCode == KeyEvent.DOM_VK_DELETE ||
    317      (AppConstants.platform == "macosx" &&
    318        e.keyCode == KeyEvent.DOM_VK_BACK_SPACE)
    319    ) {
    320      if (!e.target.closest("#sitesList")) {
    321        // The user is typing or has not selected an item from the list to remove
    322        return;
    323      }
    324      // The users intention is to delete site data
    325      this.removeSelected();
    326    }
    327  },
    328 
    329  onSelect() {
    330    this._updateButtonsState();
    331  },
    332 };
    333 
    334 window.addEventListener("load", () => gSiteDataSettings.init());