tor-browser

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

savedKeysDialog.js (8270B)


      1 // Copyright (c) 2020, The Tor Project, Inc.
      2 
      3 "use strict";
      4 
      5 ChromeUtils.defineESModuleGetters(this, {
      6  TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
      7 });
      8 
      9 var gOnionServicesSavedKeysDialog = {
     10  _tree: undefined,
     11  _busyCount: 0,
     12  get _isBusy() {
     13    // true when loading data, deleting a key, etc.
     14    return this._busyCount > 0;
     15  },
     16 
     17  /**
     18   * Whether the "remove selected" button is disabled.
     19   *
     20   * @type {boolean}
     21   */
     22  _removeSelectedDisabled: true,
     23 
     24  /**
     25   * Whether the "remove all" button is disabled.
     26   *
     27   * @type {boolean}
     28   */
     29  _removeAllDisabled: true,
     30 
     31  async _deleteSelectedKeys() {
     32    this._showError(null);
     33    this._withBusy(async () => {
     34      const indexesToDelete = [];
     35      const count = this._tree.view.selection.getRangeCount();
     36      for (let i = 0; i < count; ++i) {
     37        const minObj = {};
     38        const maxObj = {};
     39        this._tree.view.selection.getRangeAt(i, minObj, maxObj);
     40        for (let idx = minObj.value; idx <= maxObj.value; ++idx) {
     41          indexesToDelete.push(idx);
     42        }
     43      }
     44 
     45      if (indexesToDelete.length) {
     46        const provider = await TorProviderBuilder.build();
     47        try {
     48          // Remove in reverse index order to avoid issues caused by index
     49          // changes.
     50          for (let i = indexesToDelete.length - 1; i >= 0; --i) {
     51            await this._deleteOneKey(provider, indexesToDelete[i]);
     52          }
     53          // If successful and the user focus is still on the buttons move focus
     54          // to the table with the updated state. We do this before calling
     55          // _updateButtonState and potentially making the buttons disabled.
     56          if (
     57            this._removeButton.contains(document.activeElement) ||
     58            this._removeAllButton.contains(document.activeElement)
     59          ) {
     60            this._tree.focus();
     61          }
     62        } catch (e) {
     63          console.error("Removing a saved key failed", e);
     64          this._showError(
     65            "onion-site-saved-keys-dialog-remove-keys-error-message"
     66          );
     67        }
     68      }
     69    });
     70  },
     71 
     72  async _deleteAllKeys() {
     73    this._tree.view.selection.selectAll();
     74    await this._deleteSelectedKeys();
     75  },
     76 
     77  /**
     78   * Show the given button as being disabled or enabled.
     79   *
     80   * @param {Button} button - The button to change.
     81   * @param {boolean} disable - Whether to show the button as disabled or
     82   *   enabled.
     83   */
     84  _disableButton(button, disable) {
     85    // If we are disabled we show the button as disabled, and we also remove it
     86    // from the tab focus cycle using `tabIndex = -1`.
     87    // This is similar to using the `disabled` attribute, except that
     88    // `tabIndex = -1` still allows the button to be focusable. I.e. not part of
     89    // the focus cycle but can *keep* existing focus when the button becomes
     90    // disabled to avoid loosing focus to the top of the dialog.
     91    // TODO: Replace with moz-button when it handles this for us. See
     92    // tor-browser#43275.
     93    button.classList.toggle("spoof-button-disabled", disable);
     94    button.tabIndex = disable ? -1 : 0;
     95    if (disable) {
     96      this._removeButton.setAttribute("aria-disabled", "true");
     97    } else {
     98      this._removeButton.removeAttribute("aria-disabled");
     99    }
    100  },
    101 
    102  _updateButtonsState() {
    103    const haveSelection = this._tree.view.selection.getRangeCount() > 0;
    104    this._removeSelectedDisabled = this._isBusy || !haveSelection;
    105    this._removeAllDisabled = this._isBusy || this.rowCount === 0;
    106    this._disableButton(this._removeButton, this._removeSelectedDisabled);
    107    this._disableButton(this._removeAllButton, this._removeAllDisabled);
    108  },
    109 
    110  // Private functions.
    111  _onLoad() {
    112    document.mozSubdialogReady = this._init();
    113  },
    114 
    115  _init() {
    116    this._populateXUL();
    117    window.addEventListener("keypress", this._onWindowKeyPress.bind(this));
    118    this._loadSavedKeys();
    119  },
    120 
    121  _populateXUL() {
    122    this._errorMessageContainer = document.getElementById(
    123      "onionservices-savedkeys-errorContainer"
    124    );
    125    this._errorMessageEl = document.getElementById(
    126      "onionservices-savedkeys-errorMessage"
    127    );
    128    this._removeButton = document.getElementById(
    129      "onionservices-savedkeys-remove"
    130    );
    131    this._removeButton.addEventListener("click", () => {
    132      if (this._removeSelectedDisabled) {
    133        return;
    134      }
    135      this._deleteSelectedKeys();
    136    });
    137    this._removeAllButton = document.getElementById(
    138      "onionservices-savedkeys-removeall"
    139    );
    140    this._removeAllButton.addEventListener("click", () => {
    141      if (this._removeAllDisabled) {
    142        return;
    143      }
    144      this._deleteAllKeys();
    145    });
    146 
    147    this._tree = document.getElementById("onionservices-savedkeys-tree");
    148    this._tree.addEventListener("select", () => {
    149      this._updateButtonsState();
    150    });
    151  },
    152 
    153  async _loadSavedKeys() {
    154    this._showError(null);
    155    this._withBusy(async () => {
    156      try {
    157        this._tree.view = this;
    158 
    159        const provider = await TorProviderBuilder.build();
    160        const keyInfoList = await provider.onionAuthViewKeys();
    161        if (keyInfoList) {
    162          // Filter out temporary keys.
    163          this._keyInfoList = keyInfoList.filter(aKeyInfo =>
    164            aKeyInfo.flags?.includes("Permanent")
    165          );
    166          // Sort by the .onion address.
    167          this._keyInfoList.sort((aObj1, aObj2) => {
    168            const hsAddr1 = aObj1.address.toLowerCase();
    169            const hsAddr2 = aObj2.address.toLowerCase();
    170            if (hsAddr1 < hsAddr2) {
    171              return -1;
    172            }
    173            return hsAddr1 > hsAddr2 ? 1 : 0;
    174          });
    175        }
    176 
    177        // Render the tree content.
    178        this._tree.rowCountChanged(0, this.rowCount);
    179      } catch (e) {
    180        console.error("Failed to load keys", e);
    181        this._showError(
    182          "onion-site-saved-keys-dialog-fetch-keys-error-message"
    183        );
    184      }
    185    });
    186  },
    187 
    188  // This method may throw; callers should catch errors.
    189  async _deleteOneKey(provider, aIndex) {
    190    const keyInfoObj = this._keyInfoList[aIndex];
    191    await provider.onionAuthRemove(keyInfoObj.address);
    192    this._tree.view.selection.clearRange(aIndex, aIndex);
    193    this._keyInfoList.splice(aIndex, 1);
    194    this._tree.rowCountChanged(aIndex + 1, -1);
    195  },
    196 
    197  async _withBusy(func) {
    198    this._busyCount++;
    199    if (this._busyCount === 1) {
    200      this._updateButtonsState();
    201    }
    202    try {
    203      await func();
    204    } finally {
    205      this._busyCount--;
    206      if (this._busyCount === 0) {
    207        this._updateButtonsState();
    208      }
    209    }
    210  },
    211 
    212  _onWindowKeyPress(event) {
    213    if (this._isBusy) {
    214      return;
    215    }
    216    if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) {
    217      window.close();
    218    } else if (event.keyCode === KeyEvent.DOM_VK_DELETE) {
    219      this._deleteSelectedKeys();
    220    }
    221  },
    222 
    223  /**
    224   * Show an error, or clear it.
    225   *
    226   * @param {?string} messageId - The l10n ID of the message to show, or null to
    227   *   clear it.
    228   */
    229  _showError(messageId) {
    230    this._errorMessageContainer.classList.toggle("show-error", !!messageId);
    231    if (messageId) {
    232      document.l10n.setAttributes(this._errorMessageEl, messageId);
    233    } else {
    234      // Clean up.
    235      this._errorMessageEl.removeAttribute("data-l10n-id");
    236      this._errorMessageEl.textContent = "";
    237    }
    238  },
    239 
    240  // XUL tree widget view implementation.
    241  get rowCount() {
    242    return this._keyInfoList?.length ?? 0;
    243  },
    244 
    245  getCellText(aRow, aCol) {
    246    if (this._keyInfoList && aRow < this._keyInfoList.length) {
    247      const keyInfo = this._keyInfoList[aRow];
    248      if (aCol.id.endsWith("-siteCol")) {
    249        return keyInfo.address;
    250      } else if (aCol.id.endsWith("-keyCol")) {
    251        // keyType is always "x25519", so do not show it.
    252        return keyInfo.keyBlob;
    253      }
    254    }
    255    return "";
    256  },
    257 
    258  isSeparator(_index) {
    259    return false;
    260  },
    261 
    262  isSorted() {
    263    return false;
    264  },
    265 
    266  isContainer(_index) {
    267    return false;
    268  },
    269 
    270  setTree(_tree) {},
    271 
    272  getImageSrc(_row, _column) {},
    273 
    274  getCellValue(_row, _column) {},
    275 
    276  cycleHeader(_column) {},
    277 
    278  getRowProperties(_row) {
    279    return "";
    280  },
    281 
    282  getColumnProperties(_column) {
    283    return "";
    284  },
    285 
    286  getCellProperties(_row, _column) {
    287    return "";
    288  },
    289 };
    290 
    291 window.addEventListener("load", () => gOnionServicesSavedKeysDialog._onLoad());