tor-browser

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

connectionPane.js (78613B)


      1 // Copyright (c) 2022, The Tor Project, Inc.
      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 /* import-globals-from /browser/components/preferences/preferences.js */
      9 /* import-globals-from /browser/components/preferences/search.js */
     10 
     11 const { setTimeout, clearTimeout } = ChromeUtils.importESModule(
     12  "resource://gre/modules/Timer.sys.mjs"
     13 );
     14 
     15 const { TorSettings, TorSettingsTopics, TorBridgeSource } =
     16  ChromeUtils.importESModule("resource://gre/modules/TorSettings.sys.mjs");
     17 
     18 const { TorParsers } = ChromeUtils.importESModule(
     19  "resource://gre/modules/TorParsers.sys.mjs"
     20 );
     21 const { TorProviderBuilder, TorProviderTopics } = ChromeUtils.importESModule(
     22  "resource://gre/modules/TorProviderBuilder.sys.mjs"
     23 );
     24 
     25 const { InternetStatus, TorConnect, TorConnectTopics, TorConnectStage } =
     26  ChromeUtils.importESModule("resource://gre/modules/TorConnect.sys.mjs");
     27 
     28 const { TorConnectParent } = ChromeUtils.importESModule(
     29  "resource://gre/actors/TorConnectParent.sys.mjs"
     30 );
     31 
     32 const { QRCode } = ChromeUtils.importESModule(
     33  "resource://gre/modules/QRCode.sys.mjs"
     34 );
     35 
     36 const { TorStrings } = ChromeUtils.importESModule(
     37  "resource://gre/modules/TorStrings.sys.mjs"
     38 );
     39 
     40 const { Lox, LoxTopics } = ChromeUtils.importESModule(
     41  "resource://gre/modules/Lox.sys.mjs"
     42 );
     43 
     44 const log = console.createInstance({
     45  maxLogLevel: "Warn",
     46  prefix: "connectionPane",
     47 });
     48 
     49 /*
     50 * Fake Lox module:
     51 
     52 const Lox = {
     53  levelHistory: [0, 1],
     54  // levelHistory: [1, 2],
     55  // levelHistory: [2, 3],
     56  // levelHistory: [3, 4],
     57  // levelHistory: [0, 1, 2],
     58  // levelHistory: [1, 2, 3],
     59  // levelHistory: [4, 3],
     60  // levelHistory: [4, 1],
     61  // levelHistory: [2, 1],
     62  //levelHistory: [2, 3, 4, 1, 2],
     63  // Gain some invites and then loose them all. Shouldn't show any change.
     64  // levelHistory: [0, 1, 2, 1],
     65  // levelHistory: [1, 2, 3, 1],
     66  getEventData() {
     67    let prevLevel = this.levelHistory[0];
     68    const events = [];
     69    for (let i = 1; i < this.levelHistory.length; i++) {
     70      const level = this.levelHistory[i];
     71      events.push({ type: level > prevLevel ? "levelup" : "blockage", newLevel: level });
     72      prevLevel = level;
     73    }
     74    return events;
     75  },
     76  clearEventData() {
     77    this.levelHistory = [];
     78  },
     79  nextUnlock: { date: "2024-01-31T00:00:00Z", nextLevel: 1 },
     80  //nextUnlock: { date: "2024-01-31T00:00:00Z", nextLevel: 2 },
     81  //nextUnlock: { date: "2024-01-31T00:00:00Z", nextLevel: 3 },
     82  //nextUnlock: { date: "2024-01-31T00:00:00Z", nextLevel: 4 },
     83  getNextUnlock() {
     84    return this.nextUnlock;
     85  },
     86  remainingInvites: 3,
     87  // remainingInvites: 0,
     88  getRemainingInviteCount() {
     89    return this.remainingInvites;
     90  },
     91  invites: [],
     92  // invites: ["a", "b"],
     93  getInvites() {
     94    return this.invites;
     95  },
     96 };
     97 */
     98 
     99 /**
    100 * Get the ID/fingerprint of the bridge used in the most recent Tor circuit.
    101 *
    102 * @returns {string?} - The bridge ID or null if a bridge with an id was not
    103 *   used in the last circuit.
    104 */
    105 async function getConnectedBridgeId() {
    106  // TODO: PieroV: We could make sure TorSettings is in sync by monitoring also
    107  // changes of settings. At that point, we could query it, instead of doing a
    108  // query over the control port.
    109  let bridge = null;
    110  try {
    111    const provider = await TorProviderBuilder.build();
    112    bridge = provider.currentBridge;
    113  } catch (e) {
    114    console.warn("Could not get current bridge", e);
    115  }
    116  return bridge?.fingerprint ?? null;
    117 }
    118 
    119 /**
    120 * Show the bridge QR to the user.
    121 *
    122 * @param {string} bridgeString - The string to use in the QR.
    123 */
    124 function showBridgeQr(bridgeString) {
    125  gSubDialog.open(
    126    "chrome://browser/content/torpreferences/bridgeQrDialog.xhtml",
    127    { features: "resizable=yes" },
    128    bridgeString
    129  );
    130 }
    131 
    132 // TODO: Instead of aria-live in the DOM, use the proposed ariaNotify
    133 // API if it gets accepted into firefox and works with screen readers.
    134 // See https://github.com/WICG/proposals/issues/112
    135 /**
    136 * Notification for screen reader users.
    137 */
    138 const gBridgesNotification = {
    139  /**
    140   * The screen reader area that shows updates.
    141   *
    142   * @type {Element?}
    143   */
    144  _updateArea: null,
    145  /**
    146   * The text for the screen reader update.
    147   *
    148   * @type {Element?}
    149   */
    150  _textEl: null,
    151  /**
    152   * A timeout for hiding the update.
    153   *
    154   * @type {integer?}
    155   */
    156  _hideUpdateTimeout: null,
    157 
    158  /**
    159   * Initialize the area for notifications.
    160   */
    161  init() {
    162    this._updateArea = document.getElementById("tor-bridges-update-area");
    163    this._textEl = document.getElementById("tor-bridges-update-area-text");
    164  },
    165 
    166  /**
    167   * Post a new notification, replacing any existing one.
    168   *
    169   * @param {string} type - The notification type.
    170   */
    171  post(type) {
    172    this._updateArea.hidden = false;
    173    // First we clear the update area to reset the text to be empty.
    174    this._textEl.removeAttribute("data-l10n-id");
    175    this._textEl.textContent = "";
    176    if (this._hideUpdateTimeout !== null) {
    177      clearTimeout(this._hideUpdateTimeout);
    178      this._hideUpdateTimeout = null;
    179    }
    180 
    181    let updateId;
    182    switch (type) {
    183      case "removed-one":
    184        updateId = "tor-bridges-update-removed-one-bridge";
    185        break;
    186      case "removed-all":
    187        updateId = "tor-bridges-update-removed-all-bridges";
    188        break;
    189      case "changed":
    190      default:
    191        // Generic message for when bridges change.
    192        updateId = "tor-bridges-update-changed-bridges";
    193        break;
    194    }
    195 
    196    // Hide the area after 5 minutes, when the update is not "recent" any
    197    // more.
    198    this._hideUpdateTimeout = setTimeout(() => {
    199      this._updateArea.hidden = true;
    200    }, 300000);
    201 
    202    // Wait a small amount of time to actually set the textContent. Otherwise
    203    // the screen reader (tested with Orca) may not pick up on the change in
    204    // text.
    205    setTimeout(() => {
    206      document.l10n.setAttributes(this._textEl, updateId);
    207    }, 500);
    208  },
    209 };
    210 
    211 /**
    212 * Controls the bridge grid.
    213 */
    214 const gBridgeGrid = {
    215  /**
    216   * The grid element.
    217   *
    218   * @type {Element?}
    219   */
    220  _grid: null,
    221  /**
    222   * The template for creating new rows.
    223   *
    224   * @type {HTMLTemplateElement?}
    225   */
    226  _rowTemplate: null,
    227 
    228  /**
    229   * @typedef {object} BridgeGridRow
    230   *
    231   * @property {Element} element - The row element.
    232   * @property {Element} optionsButton - The options button.
    233   * @property {Element} menu - The options menupopup.
    234   * @property {Element} statusEl - The bridge status element.
    235   * @property {Element} statusText - The status text.
    236   * @property {string} bridgeLine - The identifying bridge string for this row.
    237   * @property {string?} bridgeId - The ID/fingerprint for the bridge, or null
    238   *   if it doesn't have one.
    239   * @property {integer} index - The index of the row in the grid.
    240   * @property {boolean} connected - Whether we are connected to the bridge
    241   *   (recently in use for a Tor circuit).
    242   * @property {BridgeGridCell[]} cells - The cells that belong to the row,
    243   *   ordered by their column.
    244   */
    245  /**
    246   * @typedef {object} BridgeGridCell
    247   *
    248   * @property {Element} element - The cell element.
    249   * @property {Element} focusEl - The element belonging to the cell that should
    250   *   receive focus. Should be the cell element itself, or an interactive
    251   *   focusable child.
    252   * @property {integer} columnIndex - The index of the column this cell belongs
    253   *   to.
    254   * @property {BridgeGridRow} row - The row this cell belongs to.
    255   */
    256  /**
    257   * The current rows in the grid.
    258   *
    259   * @type {BridgeGridRow[]}
    260   */
    261  _rows: [],
    262  /**
    263   * The cell that should be the focus target when the user moves focus into the
    264   * grid, or null if the grid itself should be the target.
    265   *
    266   * @type {BridgeGridCell?}
    267   */
    268  _focusCell: null,
    269 
    270  /**
    271   * Initialize the bridge grid.
    272   */
    273  init() {
    274    this._grid = document.getElementById("tor-bridges-grid-display");
    275    // Initially, make only the grid itself part of the keyboard tab cycle.
    276    // matches _focusCell = null.
    277    this._grid.tabIndex = 0;
    278 
    279    this._rowTemplate = document.getElementById(
    280      "tor-bridges-grid-row-template"
    281    );
    282 
    283    this._grid.addEventListener("keydown", this);
    284    this._grid.addEventListener("mousedown", this);
    285    this._grid.addEventListener("focusin", this);
    286 
    287    Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
    288 
    289    // NOTE: Before initializedPromise completes, this area is hidden.
    290    TorSettings.initializedPromise.then(() => {
    291      this._updateRows(true);
    292    });
    293  },
    294 
    295  /**
    296   * Uninitialize the bridge grid.
    297   */
    298  uninit() {
    299    Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
    300    this.deactivate();
    301  },
    302 
    303  /**
    304   * Whether the grid is visible and responsive.
    305   *
    306   * @type {boolean}
    307   */
    308  _active: false,
    309 
    310  /**
    311   * Activate and show the bridge grid.
    312   */
    313  activate() {
    314    if (this._active) {
    315      return;
    316    }
    317 
    318    this._active = true;
    319 
    320    Services.obs.addObserver(this, TorProviderTopics.BridgeChanged);
    321 
    322    this._grid.hidden = false;
    323 
    324    this._updateConnectedBridge();
    325  },
    326 
    327  /**
    328   * Deactivate and hide the bridge grid.
    329   */
    330  deactivate() {
    331    if (!this._active) {
    332      return;
    333    }
    334 
    335    this._active = false;
    336 
    337    this._forceCloseRowMenus();
    338 
    339    this._grid.hidden = true;
    340 
    341    Services.obs.removeObserver(this, TorProviderTopics.BridgeChanged);
    342  },
    343 
    344  observe(subject, topic) {
    345    switch (topic) {
    346      case TorSettingsTopics.SettingsChanged: {
    347        const { changes } = subject.wrappedJSObject;
    348        if (
    349          changes.includes("bridges.source") ||
    350          changes.includes("bridges.bridge_strings")
    351        ) {
    352          this._updateRows();
    353        }
    354        break;
    355      }
    356      case TorProviderTopics.BridgeChanged:
    357        this._updateConnectedBridge();
    358        break;
    359    }
    360  },
    361 
    362  handleEvent(event) {
    363    if (event.type === "keydown") {
    364      if (event.altKey || event.shiftKey || event.metaKey || event.ctrlKey) {
    365        // Don't interfere with these events.
    366        return;
    367      }
    368 
    369      if (this._rows.some(row => row.menu.open)) {
    370        // Have an open menu, let the menu handle the event instead.
    371        return;
    372      }
    373 
    374      let numRows = this._rows.length;
    375      if (!numRows) {
    376        // Nowhere for focus to go.
    377        return;
    378      }
    379 
    380      let moveRow = 0;
    381      let moveColumn = 0;
    382      const isLTR = this._grid.matches(":dir(ltr)");
    383      switch (event.key) {
    384        case "ArrowDown":
    385          moveRow = 1;
    386          break;
    387        case "ArrowUp":
    388          moveRow = -1;
    389          break;
    390        case "ArrowRight":
    391          moveColumn = isLTR ? 1 : -1;
    392          break;
    393        case "ArrowLeft":
    394          moveColumn = isLTR ? -1 : 1;
    395          break;
    396        default:
    397          return;
    398      }
    399 
    400      // Prevent scrolling the nearest scroll container.
    401      event.preventDefault();
    402 
    403      const curCell = this._focusCell;
    404      let row = curCell ? curCell.row.index + moveRow : 0;
    405      let column = curCell ? curCell.columnIndex + moveColumn : 0;
    406 
    407      // Clamp in bounds.
    408      if (row < 0) {
    409        row = 0;
    410      } else if (row >= numRows) {
    411        row = numRows - 1;
    412      }
    413 
    414      const numCells = this._rows[row].cells.length;
    415      if (column < 0) {
    416        column = 0;
    417      } else if (column >= numCells) {
    418        column = numCells - 1;
    419      }
    420 
    421      const newCell = this._rows[row].cells[column];
    422 
    423      if (newCell !== curCell) {
    424        this._setFocus(newCell);
    425      }
    426    } else if (event.type === "mousedown") {
    427      if (event.button !== 0) {
    428        return;
    429      }
    430      // Move focus index to the clicked target.
    431      // NOTE: Since the cells and the grid have "tabindex=-1", they are still
    432      // click-focusable. Therefore, the default mousedown handler will try to
    433      // move focus to it.
    434      // Rather than block this default handler, we instead re-direct the focus
    435      // to the correct cell in the "focusin" listener.
    436      const newCell = this._getCellFromTarget(event.target);
    437      // NOTE: If newCell is null, then we do nothing here, but instead wait for
    438      // the focusin handler to trigger.
    439      if (newCell && newCell !== this._focusCell) {
    440        this._setFocus(newCell);
    441      }
    442    } else if (event.type === "focusin") {
    443      const focusCell = this._getCellFromTarget(event.target);
    444      if (focusCell !== this._focusCell) {
    445        // Focus is not where it is expected.
    446        // E.g. the user has clicked the edge of the grid.
    447        // Restore focus immediately back to the cell we expect.
    448        this._setFocus(this._focusCell);
    449      }
    450    }
    451  },
    452 
    453  /**
    454   * Return the cell that was the target of an event.
    455   *
    456   * @param {Element} element - The target of an event.
    457   *
    458   * @returns {BridgeGridCell?} - The cell that the element belongs to, or null
    459   *   if it doesn't belong to any cell.
    460   */
    461  _getCellFromTarget(element) {
    462    for (const row of this._rows) {
    463      for (const cell of row.cells) {
    464        if (cell.element.contains(element)) {
    465          return cell;
    466        }
    467      }
    468    }
    469    return null;
    470  },
    471 
    472  /**
    473   * Determine whether the document's active element (focus) is within the grid
    474   * or not.
    475   *
    476   * @returns {boolean} - Whether focus is within this grid or not.
    477   */
    478  _focusWithin() {
    479    return this._grid.contains(document.activeElement);
    480  },
    481 
    482  /**
    483   * Set the cell that should be the focus target of the grid, possibly moving
    484   * the document's focus as well.
    485   *
    486   * @param {BridgeGridCell?} cell - The cell to make the focus target, or null
    487   *   if the grid itself should be the target.
    488   * @param {boolean} [focusWithin] - Whether focus should be moved within the
    489   *   grid. If undefined, this will move focus if the grid currently contains
    490   *   the document's focus.
    491   */
    492  _setFocus(cell, focusWithin) {
    493    if (focusWithin === undefined) {
    494      focusWithin = this._focusWithin();
    495    }
    496    const prevFocusElement = this._focusCell
    497      ? this._focusCell.focusEl
    498      : this._grid;
    499    const newFocusElement = cell ? cell.focusEl : this._grid;
    500 
    501    if (prevFocusElement !== newFocusElement) {
    502      prevFocusElement.tabIndex = -1;
    503      newFocusElement.tabIndex = 0;
    504    }
    505    // Set _focusCell now, before we potentially call "focus", which can trigger
    506    // the "focusin" handler.
    507    this._focusCell = cell;
    508 
    509    if (focusWithin) {
    510      // Focus was within the grid, so we need to actively move it to the new
    511      // element.
    512      newFocusElement.focus({ preventScroll: true });
    513      // Scroll to the whole cell into view, rather than just the focus element.
    514      (cell?.element ?? newFocusElement).scrollIntoView({
    515        block: "nearest",
    516        inline: "nearest",
    517      });
    518    }
    519  },
    520 
    521  /**
    522   * Reset the grids focus to be the first row's first cell, if any.
    523   *
    524   * @param {boolean} [focusWithin] - Whether focus should be moved within the
    525   *   grid. If undefined, this will move focus if the grid currently contains
    526   *   the document's focus.
    527   */
    528  _resetFocus(focusWithin) {
    529    this._setFocus(
    530      this._rows.length ? this._rows[0].cells[0] : null,
    531      focusWithin
    532    );
    533  },
    534 
    535  /**
    536   * The bridge ID/fingerprint of the most recently used bridge (appearing in
    537   * the latest Tor circuit). Roughly corresponds to the bridge we are currently
    538   * connected to.
    539   *
    540   * null if there are no such bridges.
    541   *
    542   * @type {string?}
    543   */
    544  _connectedBridgeId: null,
    545  /**
    546   * Update _connectedBridgeId.
    547   */
    548  async _updateConnectedBridge() {
    549    const bridgeId = await getConnectedBridgeId();
    550    if (bridgeId === this._connectedBridgeId) {
    551      return;
    552    }
    553    this._connectedBridgeId = bridgeId;
    554    for (const row of this._rows) {
    555      this._updateRowStatus(row);
    556    }
    557  },
    558 
    559  /**
    560   * Update the status of a row.
    561   *
    562   * @param {BridgeGridRow} row - The row to update.
    563   */
    564  _updateRowStatus(row) {
    565    const connected = row.bridgeId && this._connectedBridgeId === row.bridgeId;
    566    // NOTE: row.connected is initially undefined, so won't match `connected`.
    567    if (connected === row.connected) {
    568      return;
    569    }
    570 
    571    row.connected = connected;
    572 
    573    const noStatus = !connected;
    574 
    575    row.element.classList.toggle("hide-status", noStatus);
    576    row.statusEl.classList.toggle("bridge-status-none", noStatus);
    577    row.statusEl.classList.toggle("bridge-status-connected", connected);
    578 
    579    if (connected) {
    580      document.l10n.setAttributes(
    581        row.statusText,
    582        "tor-bridges-status-connected"
    583      );
    584    } else {
    585      document.l10n.setAttributes(row.statusText, "tor-bridges-status-none");
    586    }
    587  },
    588 
    589  /**
    590   * Create a new row for the grid.
    591   *
    592   * @param {string} bridgeLine - The bridge line for this row, which also acts
    593   *   as its ID.
    594   *
    595   * @returns {BridgeGridRow} - A new row, with then "index" unset and the
    596   *   "element" without a parent.
    597   */
    598  _createRow(bridgeLine) {
    599    let details;
    600    try {
    601      details = TorParsers.parseBridgeLine(bridgeLine);
    602    } catch (e) {
    603      console.error(`Detected invalid bridge line: ${bridgeLine}`, e);
    604    }
    605    const row = {
    606      element: this._rowTemplate.content.children[0].cloneNode(true),
    607      bridgeLine,
    608      bridgeId: details?.id ?? null,
    609      cells: [],
    610    };
    611 
    612    const emojiBlock = row.element.querySelector(".tor-bridges-emojis-block");
    613    const BridgeEmoji = customElements.get("tor-bridge-emoji");
    614    for (const cell of BridgeEmoji.createForAddress(bridgeLine)) {
    615      // Each emoji is its own cell, we rely on the fact that createForAddress
    616      // always returns four elements.
    617      cell.setAttribute("role", "gridcell");
    618      cell.classList.add("tor-bridges-grid-cell", "tor-bridges-emoji-cell");
    619      emojiBlock.append(cell);
    620    }
    621 
    622    for (const [columnIndex, element] of row.element
    623      .querySelectorAll(".tor-bridges-grid-cell")
    624      .entries()) {
    625      const focusEl =
    626        element.querySelector(".tor-bridges-grid-focus") ?? element;
    627      // Set a negative tabIndex, this makes the element click-focusable but not
    628      // part of the tab navigation sequence.
    629      focusEl.tabIndex = -1;
    630      row.cells.push({ element, focusEl, columnIndex, row });
    631    }
    632 
    633    const transport = details?.transport ?? "vanilla";
    634    const typeCell = row.element.querySelector(".tor-bridges-type-cell");
    635    if (transport === "vanilla") {
    636      document.l10n.setAttributes(typeCell, "tor-bridges-type-prefix-generic");
    637    } else {
    638      document.l10n.setAttributes(typeCell, "tor-bridges-type-prefix", {
    639        type: transport,
    640      });
    641    }
    642 
    643    row.element.querySelector(".tor-bridges-address-cell-text").textContent =
    644      bridgeLine;
    645 
    646    row.statusEl = row.element.querySelector(
    647      ".tor-bridges-status-cell .bridge-status-badge"
    648    );
    649    row.statusText = row.element.querySelector(".tor-bridges-status-cell-text");
    650 
    651    this._initRowMenu(row);
    652 
    653    this._updateRowStatus(row);
    654    return row;
    655  },
    656 
    657  /**
    658   * The row menu index used for generating new ids.
    659   *
    660   * @type {integer}
    661   */
    662  _rowMenuIndex: 0,
    663  /**
    664   * Generate a new id for the options menu.
    665   *
    666   * @returns {string} - The new id.
    667   */
    668  _generateRowMenuId() {
    669    const id = `tor-bridges-individual-options-menu-${this._rowMenuIndex}`;
    670    // Assume we won't run out of ids.
    671    this._rowMenuIndex++;
    672    return id;
    673  },
    674 
    675  /**
    676   * Initialize the shared menu for a row.
    677   *
    678   * @param {BridgeGridRow} row - The row to initialize the menu of.
    679   */
    680  _initRowMenu(row) {
    681    row.menu = row.element.querySelector(
    682      ".tor-bridges-individual-options-menu"
    683    );
    684    row.optionsButton = row.element.querySelector(
    685      ".tor-bridges-options-cell-button"
    686    );
    687 
    688    row.menu.id = this._generateRowMenuId();
    689    row.optionsButton.setAttribute("aria-controls", row.menu.id);
    690 
    691    row.optionsButton.addEventListener("click", event => {
    692      row.menu.toggle(event);
    693    });
    694 
    695    row.menu.addEventListener("hidden", () => {
    696      // Make sure the button receives focus again when the menu is hidden.
    697      // Currently, panel-list.js only does this when the menu is opened with a
    698      // keyboard, but this causes focus to be lost from the page if the user
    699      // uses a mixture of keyboard and mouse.
    700      row.optionsButton.focus();
    701    });
    702 
    703    const qrItem = row.menu.querySelector(
    704      ".tor-bridges-options-qr-one-menu-item"
    705    );
    706    const removeItem = row.menu.querySelector(
    707      ".tor-bridges-options-remove-one-menu-item"
    708    );
    709    row.menu.addEventListener("showing", () => {
    710      const show =
    711        this._bridgeSource === TorBridgeSource.UserProvided ||
    712        this._bridgeSource === TorBridgeSource.BridgeDB;
    713      qrItem.hidden = !show;
    714      removeItem.hidden = !show;
    715    });
    716 
    717    qrItem.addEventListener("click", () => {
    718      const bridgeLine = row.bridgeLine;
    719      if (!bridgeLine) {
    720        return;
    721      }
    722      showBridgeQr(bridgeLine);
    723    });
    724    row.menu
    725      .querySelector(".tor-bridges-options-copy-one-menu-item")
    726      .addEventListener("click", () => {
    727        const clipboard = Cc[
    728          "@mozilla.org/widget/clipboardhelper;1"
    729        ].getService(Ci.nsIClipboardHelper);
    730        clipboard.copyString(row.bridgeLine);
    731      });
    732    removeItem.addEventListener("click", () => {
    733      const bridgeLine = row.bridgeLine;
    734      const source = TorSettings.bridges.source;
    735      const strings = TorSettings.bridges.bridge_strings;
    736      const index = strings.indexOf(bridgeLine);
    737      if (index === -1) {
    738        return;
    739      }
    740      strings.splice(index, 1);
    741 
    742      if (strings.length) {
    743        TorSettings.changeSettings({
    744          bridges: { source, bridge_strings: strings },
    745        });
    746      } else {
    747        // Remove all bridges and disable.
    748        TorSettings.changeSettings({
    749          bridges: { source: TorBridgeSource.Invalid },
    750        });
    751      }
    752    });
    753  },
    754 
    755  /**
    756   * Force the row menu to close.
    757   */
    758  _forceCloseRowMenus() {
    759    for (const row of this._rows) {
    760      row.menu.hide(null, { force: true });
    761    }
    762  },
    763 
    764  /**
    765   * The known bridge source.
    766   *
    767   * Initially null to indicate that it is unset.
    768   *
    769   * @type {integer?}
    770   */
    771  _bridgeSource: null,
    772  /**
    773   * The bridge sources this is shown for.
    774   *
    775   * @type {string[]}
    776   */
    777  _supportedSources: [
    778    TorBridgeSource.BridgeDB,
    779    TorBridgeSource.UserProvided,
    780    TorBridgeSource.Lox,
    781  ],
    782 
    783  /**
    784   * Update the grid to show the latest bridge strings.
    785   *
    786   * @param {boolean} [initializing=false] - Whether this is being called as
    787   *   part of initialization.
    788   */
    789  _updateRows(initializing = false) {
    790    // Store whether we have focus within the grid, before removing or hiding
    791    // DOM elements.
    792    const focusWithin = this._focusWithin();
    793 
    794    let lostAllBridges = false;
    795    let newSource = false;
    796    const bridgeSource = TorSettings.bridges.source;
    797    if (bridgeSource !== this._bridgeSource) {
    798      newSource = true;
    799 
    800      this._bridgeSource = bridgeSource;
    801 
    802      if (this._supportedSources.includes(bridgeSource)) {
    803        this.activate();
    804      } else {
    805        if (this._active && bridgeSource === TorBridgeSource.Invalid) {
    806          lostAllBridges = true;
    807        }
    808        this.deactivate();
    809      }
    810    }
    811 
    812    const ordered = this._active
    813      ? TorSettings.bridges.bridge_strings.map(bridgeLine => {
    814          const row = this._rows.find(r => r.bridgeLine === bridgeLine);
    815          if (row) {
    816            return row;
    817          }
    818          return this._createRow(bridgeLine);
    819        })
    820      : [];
    821 
    822    // Whether we should reset the grid's focus.
    823    // We always reset when we have a new bridge source.
    824    // We reset the focus if no current Cell has focus. I.e. when adding a row
    825    // to an empty grid, we want the focus to move to the first item.
    826    // We also reset the focus if the current Cell is in a row that will be
    827    // removed (including if all rows are removed).
    828    // NOTE: In principle, if a row is removed, we could move the focus to the
    829    // next or previous row (in the same cell column). However, most likely if
    830    // the grid has the user focus, they are removing a single row using its
    831    // options button. In this case, returning the user to some other row's
    832    // options button might be more disorienting since it would not be simple
    833    // for them to know *which* bridge they have landed on.
    834    // NOTE: We do not reset the focus in other cases because we do not want the
    835    // user to loose their place in the grid unnecessarily.
    836    let resetFocus =
    837      newSource || !this._focusCell || !ordered.includes(this._focusCell.row);
    838 
    839    // Remove rows no longer needed from the DOM.
    840    let numRowsRemoved = 0;
    841    let rowAddedOrMoved = false;
    842 
    843    for (const row of this._rows) {
    844      if (!ordered.includes(row)) {
    845        numRowsRemoved++;
    846        // If the row menu was open, it will also be deleted.
    847        // NOTE: Since the row menu is part of the row, focusWithin will be true
    848        // if the menu had focus, so focus should be re-assigned.
    849        row.element.remove();
    850      }
    851    }
    852 
    853    // Go through all the rows to set their ".index" property and to ensure they
    854    // are in the correct position in the DOM.
    855    // NOTE: We could use replaceChildren to get the correct DOM structure, but
    856    // we want to avoid rebuilding the entire tree when a single row is added or
    857    // removed.
    858    for (const [index, row] of ordered.entries()) {
    859      row.index = index;
    860      const element = row.element;
    861      // Get the expected previous element, that should already be in the DOM
    862      // from the previous loop.
    863      const prevEl = index ? ordered[index - 1].element : null;
    864 
    865      if (
    866        element.parentElement === this._grid &&
    867        prevEl === element.previousElementSibling
    868      ) {
    869        // Already in the correct position in the DOM.
    870        continue;
    871      }
    872 
    873      rowAddedOrMoved = true;
    874      // NOTE: Any elements already in the DOM, but not in the correct position
    875      // will be removed and re-added by the below command.
    876      // NOTE: if the row has document focus, then it should remain there.
    877      if (prevEl) {
    878        prevEl.after(element);
    879      } else {
    880        this._grid.prepend(element);
    881      }
    882    }
    883    this._rows = ordered;
    884 
    885    // Restore any lost focus.
    886    if (resetFocus) {
    887      // If we are not active (and therefore hidden), we will not try and move
    888      // focus (activeElement), but may still change the *focusable* element for
    889      // when we are shown again.
    890      this._resetFocus(this._active && focusWithin);
    891    }
    892    if (!this._active && focusWithin) {
    893      // Move focus out of this element, which has been hidden.
    894      gBridgeSettings.takeFocus();
    895    }
    896 
    897    // Notify the user if there was some change to the DOM.
    898    // If we are initializing, we generate no notification since there has been
    899    // no change in the setting.
    900    if (!initializing) {
    901      let notificationType;
    902      if (lostAllBridges) {
    903        // Just lost all bridges, and became de-active.
    904        notificationType = "removed-all";
    905      } else if (this._rows.length) {
    906        // Otherwise, only generate a notification if we are still active, with
    907        // at least one bridge.
    908        // I.e. do not generate a message if the new source is "builtin".
    909        if (newSource) {
    910          // A change in source.
    911          notificationType = "changed";
    912        } else if (numRowsRemoved === 1 && !rowAddedOrMoved) {
    913          // Only one bridge was removed. This is most likely in response to them
    914          // manually removing a single bridge or using the bridge row's options
    915          // menu.
    916          notificationType = "removed-one";
    917        } else if (numRowsRemoved || rowAddedOrMoved) {
    918          // Some other change. This is most likely in response to a manual edit
    919          // of the existing bridges.
    920          notificationType = "changed";
    921        }
    922        // Else, there was no change.
    923      }
    924 
    925      if (notificationType) {
    926        gBridgesNotification.post(notificationType);
    927      }
    928    }
    929  },
    930 };
    931 
    932 /**
    933 * Controls the built-in bridges area.
    934 */
    935 const gBuiltinBridgesArea = {
    936  /**
    937   * The display area.
    938   *
    939   * @type {Element?}
    940   */
    941  _area: null,
    942  /**
    943   * The type name element.
    944   *
    945   * @type {Element?}
    946   */
    947  _nameEl: null,
    948  /**
    949   * The bridge type description element.
    950   *
    951   * @type {Element?}
    952   */
    953  _descriptionEl: null,
    954  /**
    955   * The connection status.
    956   *
    957   * @type {Element?}
    958   */
    959  _connectionStatusEl: null,
    960 
    961  /**
    962   * Initialize the built-in bridges area.
    963   */
    964  init() {
    965    this._area = document.getElementById("tor-bridges-built-in-display");
    966    this._nameEl = document.getElementById("tor-bridges-built-in-type-name");
    967    this._descriptionEl = document.getElementById(
    968      "tor-bridges-built-in-description"
    969    );
    970    this._connectionStatusEl = document.getElementById(
    971      "tor-bridges-built-in-connected"
    972    );
    973 
    974    Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
    975 
    976    // NOTE: Before initializedPromise completes, this area is hidden.
    977    TorSettings.initializedPromise.then(() => {
    978      this._updateBridgeType(true);
    979    });
    980  },
    981 
    982  /**
    983   * Uninitialize the built-in bridges area.
    984   */
    985  uninit() {
    986    Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
    987    this.deactivate();
    988  },
    989 
    990  /**
    991   * Whether the built-in area is visible and responsive.
    992   *
    993   * @type {boolean}
    994   */
    995  _active: false,
    996 
    997  /**
    998   * Activate and show the built-in bridge area.
    999   */
   1000  activate() {
   1001    if (this._active) {
   1002      return;
   1003    }
   1004    this._active = true;
   1005 
   1006    Services.obs.addObserver(this, TorProviderTopics.BridgeChanged);
   1007 
   1008    this._area.hidden = false;
   1009 
   1010    this._updateBridgeIds();
   1011    this._updateConnectedBridge();
   1012  },
   1013 
   1014  /**
   1015   * Deactivate and hide built-in bridge area.
   1016   */
   1017  deactivate() {
   1018    if (!this._active) {
   1019      return;
   1020    }
   1021    this._active = false;
   1022 
   1023    this._area.hidden = true;
   1024 
   1025    Services.obs.removeObserver(this, TorProviderTopics.BridgeChanged);
   1026  },
   1027 
   1028  observe(subject, topic) {
   1029    switch (topic) {
   1030      case TorSettingsTopics.SettingsChanged: {
   1031        const { changes } = subject.wrappedJSObject;
   1032        if (
   1033          changes.includes("bridges.source") ||
   1034          changes.includes("bridges.builtin_type")
   1035        ) {
   1036          this._updateBridgeType();
   1037        }
   1038        if (changes.includes("bridges.bridge_strings")) {
   1039          this._updateBridgeIds();
   1040        }
   1041        break;
   1042      }
   1043      case TorProviderTopics.BridgeChanged:
   1044        this._updateConnectedBridge();
   1045        break;
   1046    }
   1047  },
   1048 
   1049  /**
   1050   * Updates the shown connected state.
   1051   */
   1052  _updateConnectedState() {
   1053    this._connectionStatusEl.classList.toggle(
   1054      "bridge-status-connected",
   1055      this._bridgeType &&
   1056        this._connectedBridgeId &&
   1057        this._bridgeIds.includes(this._connectedBridgeId)
   1058    );
   1059  },
   1060 
   1061  /**
   1062   * The currently shown bridge type. Empty if deactivated, and null if
   1063   * uninitialized.
   1064   *
   1065   * @type {string?}
   1066   */
   1067  _bridgeType: null,
   1068  /**
   1069   * The strings for each known bridge type.
   1070   *
   1071   * @type {{[key: string]: {[key: string]: string}}}
   1072   */
   1073  _bridgeTypeStrings: {
   1074    obfs4: {
   1075      name: "tor-bridges-built-in-obfs4-name",
   1076      description: "tor-bridges-built-in-obfs4-description",
   1077    },
   1078    snowflake: {
   1079      name: "tor-bridges-built-in-snowflake-name",
   1080      description: "tor-bridges-built-in-snowflake-description",
   1081    },
   1082    meek: {
   1083      name: "tor-bridges-built-in-meek-name",
   1084      description: "tor-bridges-built-in-meek-description",
   1085    },
   1086  },
   1087 
   1088  /**
   1089   * The known bridge source.
   1090   *
   1091   * Initially null to indicate that it is unset.
   1092   *
   1093   * @type {integer?}
   1094   */
   1095  _bridgeSource: null,
   1096 
   1097  /**
   1098   * Update the shown bridge type.
   1099   *
   1100   * @param {boolean} [initializing=false] - Whether this is being called as
   1101   *   part of initialization.
   1102   */
   1103  async _updateBridgeType(initializing = false) {
   1104    let lostAllBridges = false;
   1105    let newSource = false;
   1106    const bridgeSource = TorSettings.bridges.source;
   1107    if (bridgeSource !== this._bridgeSource) {
   1108      newSource = true;
   1109 
   1110      this._bridgeSource = bridgeSource;
   1111 
   1112      if (bridgeSource === TorBridgeSource.BuiltIn) {
   1113        this.activate();
   1114      } else {
   1115        if (this._active && bridgeSource === TorBridgeSource.Invalid) {
   1116          lostAllBridges = true;
   1117        }
   1118        const hadFocus = this._area.contains(document.activeElement);
   1119        this.deactivate();
   1120        if (hadFocus) {
   1121          gBridgeSettings.takeFocus();
   1122        }
   1123      }
   1124    }
   1125 
   1126    const bridgeType = this._active ? TorSettings.bridges.builtin_type : "";
   1127 
   1128    let newType = false;
   1129    if (bridgeType !== this._bridgeType) {
   1130      newType = true;
   1131 
   1132      this._bridgeType = bridgeType;
   1133 
   1134      const bridgeStrings = this._bridgeTypeStrings[bridgeType];
   1135      if (bridgeStrings) {
   1136        document.l10n.setAttributes(this._nameEl, bridgeStrings.name);
   1137        document.l10n.setAttributes(
   1138          this._descriptionEl,
   1139          bridgeStrings.description
   1140        );
   1141      } else {
   1142        // Unknown type, or no type.
   1143        this._nameEl.removeAttribute("data-l10n-id");
   1144        this._nameEl.textContent = bridgeType;
   1145        this._descriptionEl.removeAttribute("data-l10n-id");
   1146        this._descriptionEl.textContent = "";
   1147      }
   1148 
   1149      this._updateConnectedState();
   1150    }
   1151 
   1152    // Notify the user if there was some change to the type.
   1153    // If we are initializing, we generate no notification since there has been
   1154    // no change in the setting.
   1155    if (!initializing) {
   1156      let notificationType;
   1157      if (lostAllBridges) {
   1158        // Just lost all bridges, and became de-active.
   1159        notificationType = "removed-all";
   1160      } else if (this._active && (newSource || newType)) {
   1161        // Otherwise, only generate a notification if we are still active, with
   1162        // a bridge type.
   1163        // I.e. do not generate a message if the new source is not "builtin".
   1164        notificationType = "changed";
   1165      }
   1166 
   1167      if (notificationType) {
   1168        gBridgesNotification.post(notificationType);
   1169      }
   1170    }
   1171  },
   1172 
   1173  /**
   1174   * The bridge IDs/fingerprints for the built-in bridges.
   1175   *
   1176   * @type {Array<string>}
   1177   */
   1178  _bridgeIds: [],
   1179  /**
   1180   * Update _bridgeIds
   1181   */
   1182  _updateBridgeIds() {
   1183    this._bridgeIds = [];
   1184    for (const bridgeLine of TorSettings.bridges.bridge_strings) {
   1185      try {
   1186        this._bridgeIds.push(TorParsers.parseBridgeLine(bridgeLine).id);
   1187      } catch (e) {
   1188        console.error(`Detected invalid bridge line: ${bridgeLine}`, e);
   1189      }
   1190    }
   1191 
   1192    this._updateConnectedState();
   1193  },
   1194 
   1195  /**
   1196   * The bridge ID/fingerprint of the most recently used bridge (appearing in
   1197   * the latest Tor circuit). Roughly corresponds to the bridge we are currently
   1198   * connected to.
   1199   *
   1200   * @type {string?}
   1201   */
   1202  _connectedBridgeId: null,
   1203  /**
   1204   * Update _connectedBridgeId.
   1205   */
   1206  async _updateConnectedBridge() {
   1207    this._connectedBridgeId = await getConnectedBridgeId();
   1208    this._updateConnectedState();
   1209  },
   1210 };
   1211 
   1212 /**
   1213 * Controls the bridge pass area.
   1214 */
   1215 const gLoxStatus = {
   1216  /**
   1217   * The status area.
   1218   *
   1219   * @type {Element?}
   1220   */
   1221  _area: null,
   1222  /**
   1223   * The area for showing the next unlock and invites.
   1224   *
   1225   * @type {Element?}
   1226   */
   1227  _detailsArea: null,
   1228  /**
   1229   * The list items showing the next unlocks.
   1230   *
   1231   * @type {?{[key: string]: Element}}
   1232   */
   1233  _nextUnlockItems: null,
   1234  /**
   1235   * The day counter headings for the next unlock.
   1236   *
   1237   * One heading is shown during a search, the other is shown otherwise.
   1238   *
   1239   * @type {?Element[]}
   1240   */
   1241  _nextUnlockCounterEls: null,
   1242  /**
   1243   * Shows the number of remaining invites.
   1244   *
   1245   * @type {Element?}
   1246   */
   1247  _remainingInvitesEl: null,
   1248  /**
   1249   * The button to show the invites.
   1250   *
   1251   * @type {Element?}
   1252   */
   1253  _invitesButton: null,
   1254  /**
   1255   * The alert for new unlocks.
   1256   *
   1257   * @type {Element?}
   1258   */
   1259  _unlockAlert: null,
   1260  /**
   1261   * The list items showing the unlocks.
   1262   *
   1263   * @type {?{[key: string]: Element}}
   1264   */
   1265  _unlockItems: null,
   1266  /**
   1267   * The alert title.
   1268   *
   1269   * @type {Element?}
   1270   */
   1271  _unlockAlertTitle: null,
   1272  /**
   1273   * The alert invites item.
   1274   *
   1275   * @type {Element?}
   1276   */
   1277  _unlockAlertInvitesItem: null,
   1278  /**
   1279   * Button for the user to dismiss the alert.
   1280   *
   1281   * @type {Element?}
   1282   */
   1283  _unlockAlertButton: null,
   1284 
   1285  /**
   1286   * Initialize the bridge pass area.
   1287   */
   1288  init() {
   1289    if (!Lox.enabled) {
   1290      // Area should remain inactive and hidden.
   1291      return;
   1292    }
   1293 
   1294    this._area = document.getElementById("tor-bridges-lox-status");
   1295    this._detailsArea = document.getElementById("tor-bridges-lox-details");
   1296    this._nextUnlockItems = {
   1297      gainBridges: document.getElementById(
   1298        "tor-bridges-lox-next-unlock-gain-bridges"
   1299      ),
   1300      firstInvites: document.getElementById(
   1301        "tor-bridges-lox-next-unlock-first-invites"
   1302      ),
   1303      moreInvites: document.getElementById(
   1304        "tor-bridges-lox-next-unlock-more-invites"
   1305      ),
   1306    };
   1307    this._nextUnlockCounterEls = Array.from(
   1308      document.querySelectorAll(".tor-bridges-lox-next-unlock-counter")
   1309    );
   1310    this._remainingInvitesEl = document.getElementById(
   1311      "tor-bridges-lox-remaining-invites"
   1312    );
   1313    this._invitesButton = document.getElementById(
   1314      "tor-bridges-lox-show-invites-button"
   1315    );
   1316    this._unlockAlert = document.getElementById("tor-bridges-lox-unlock-alert");
   1317    this._unlockItems = {
   1318      gainBridges: document.getElementById(
   1319        "tor-bridges-lox-unlock-alert-gain-bridges"
   1320      ),
   1321      newBridges: document.getElementById(
   1322        "tor-bridges-lox-unlock-alert-new-bridges"
   1323      ),
   1324      invites: document.getElementById("tor-bridges-lox-unlock-alert-invites"),
   1325    };
   1326    this._unlockAlertTitle = document.getElementById(
   1327      "tor-bridge-unlock-alert-title"
   1328    );
   1329    this._unlockAlertInviteItem = document.getElementById(
   1330      "tor-bridges-lox-unlock-alert-invites"
   1331    );
   1332    this._unlockAlertButton = document.getElementById(
   1333      "tor-bridges-lox-unlock-alert-button"
   1334    );
   1335 
   1336    this._invitesButton.addEventListener("click", () => {
   1337      gSubDialog.open(
   1338        "chrome://browser/content/torpreferences/loxInviteDialog.xhtml",
   1339        { features: "resizable=yes" }
   1340      );
   1341    });
   1342    this._unlockAlertButton.addEventListener("click", () => {
   1343      Lox.clearEventData(this._loxId);
   1344    });
   1345 
   1346    Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
   1347    Services.obs.addObserver(this, LoxTopics.UpdateActiveLoxId);
   1348    Services.obs.addObserver(this, LoxTopics.UpdateEvents);
   1349    Services.obs.addObserver(this, LoxTopics.UpdateNextUnlock);
   1350    Services.obs.addObserver(this, LoxTopics.UpdateRemainingInvites);
   1351    Services.obs.addObserver(this, LoxTopics.NewInvite);
   1352 
   1353    // NOTE: Before initializedPromise completes, this area is hidden.
   1354    TorSettings.initializedPromise.then(() => {
   1355      this._updateLoxId();
   1356    });
   1357  },
   1358 
   1359  /**
   1360   * Uninitialize the built-in bridges area.
   1361   */
   1362  uninit() {
   1363    if (!Lox.enabled) {
   1364      return;
   1365    }
   1366 
   1367    Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
   1368    Services.obs.removeObserver(this, LoxTopics.UpdateActiveLoxId);
   1369    Services.obs.removeObserver(this, LoxTopics.UpdateEvents);
   1370    Services.obs.removeObserver(this, LoxTopics.UpdateNextUnlock);
   1371    Services.obs.removeObserver(this, LoxTopics.UpdateRemainingInvites);
   1372    Services.obs.removeObserver(this, LoxTopics.NewInvite);
   1373  },
   1374 
   1375  observe(subject, topic) {
   1376    switch (topic) {
   1377      case TorSettingsTopics.SettingsChanged: {
   1378        const { changes } = subject.wrappedJSObject;
   1379        if (changes.includes("bridges.source")) {
   1380          this._updateLoxId();
   1381        }
   1382        // NOTE: We do not call _updateLoxId when "bridges.lox_id" is in the
   1383        // changes. Instead we wait until LoxTopics.UpdateActiveLoxId to ensure
   1384        // that the Lox module has responded to the change in ID strictly
   1385        // *before* we do. In particular, we want to make sure the invites and
   1386        // event data has been cleared.
   1387        break;
   1388      }
   1389      case LoxTopics.UpdateActiveLoxId:
   1390        this._updateLoxId();
   1391        break;
   1392      case LoxTopics.UpdateNextUnlock:
   1393        this._updateNextUnlock();
   1394        break;
   1395      case LoxTopics.UpdateEvents:
   1396        this._updatePendingEvents();
   1397        break;
   1398      case LoxTopics.UpdateRemainingInvites:
   1399        this._updateRemainingInvites();
   1400        break;
   1401      case LoxTopics.NewInvite:
   1402        this._updateHaveExistingInvites();
   1403        break;
   1404    }
   1405  },
   1406 
   1407  /**
   1408   * The Lox id currently shown. Empty if deactivated, and null if
   1409   * uninitialized.
   1410   *
   1411   * @type {string?}
   1412   */
   1413  _loxId: null,
   1414 
   1415  /**
   1416   * Update the shown bridge pass.
   1417   */
   1418  async _updateLoxId() {
   1419    let loxId =
   1420      TorSettings.bridges.source === TorBridgeSource.Lox ? Lox.activeLoxId : "";
   1421    if (loxId === this._loxId) {
   1422      return;
   1423    }
   1424    this._loxId = loxId;
   1425    this._area.hidden = !loxId;
   1426    // We unset _nextUnlock to ensure the areas no longer use the old value for
   1427    // the new loxId.
   1428    this._updateNextUnlock(true);
   1429    this._updateRemainingInvites();
   1430    this._updateHaveExistingInvites();
   1431    this._updatePendingEvents();
   1432  },
   1433 
   1434  /**
   1435   * The remaining invites shown, or null if uninitialized or no loxId.
   1436   *
   1437   * @type {integer?}
   1438   */
   1439  _remainingInvites: null,
   1440  /**
   1441   * Update the shown value.
   1442   */
   1443  _updateRemainingInvites() {
   1444    const numInvites = this._loxId
   1445      ? Lox.getRemainingInviteCount(this._loxId)
   1446      : null;
   1447    if (numInvites === this._remainingInvites) {
   1448      return;
   1449    }
   1450    this._remainingInvites = numInvites;
   1451    this._updateUnlockArea();
   1452    this._updateInvitesArea();
   1453  },
   1454  /**
   1455   * Whether we have existing invites, or null if uninitialized or no loxId.
   1456   *
   1457   * @type {boolean?}
   1458   */
   1459  _haveExistingInvites: null,
   1460  /**
   1461   * Update the shown value.
   1462   */
   1463  _updateHaveExistingInvites() {
   1464    const haveInvites = this._loxId ? !!Lox.getInvites().length : null;
   1465    if (haveInvites === this._haveExistingInvites) {
   1466      return;
   1467    }
   1468    this._haveExistingInvites = haveInvites;
   1469    this._updateInvitesArea();
   1470  },
   1471  /**
   1472   * Details about the next unlock, or null if uninitialized or no loxId.
   1473   *
   1474   * @type {UnlockData?}
   1475   */
   1476  _nextUnlock: null,
   1477  /**
   1478   * Tracker id to ensure that the results from later calls to _updateNextUnlock
   1479   * take priority over earlier calls.
   1480   *
   1481   * @type {integer}
   1482   */
   1483  _nextUnlockCallId: 0,
   1484  /**
   1485   * Update the shown value asynchronously.
   1486   *
   1487   * @param {boolean} [unset=false] - Whether to set the _nextUnlock value to
   1488   *   null before waiting for the new value. I.e. ensure that the current value
   1489   *   will not be used.
   1490   */
   1491  async _updateNextUnlock(unset = false) {
   1492    // NOTE: We do not expect the integer to exceed the maximum integer.
   1493    this._nextUnlockCallId++;
   1494    const callId = this._nextUnlockCallId;
   1495    if (unset) {
   1496      this._nextUnlock = null;
   1497    }
   1498    const nextUnlock = this._loxId
   1499      ? await Lox.getNextUnlock(this._loxId)
   1500      : null;
   1501    if (callId !== this._nextUnlockCallId) {
   1502      // Replaced by another update.
   1503      // E.g. if the _loxId changed. Or if getNextUnlock triggered
   1504      // LoxTopics.UpdateNextUnlock.
   1505      return;
   1506    }
   1507    // Should be safe to trigger the update, even when the value hasn't changed.
   1508    this._nextUnlock = nextUnlock;
   1509    this._updateUnlockArea();
   1510  },
   1511  /**
   1512   * The list of events the user has not yet cleared, or null if uninitialized
   1513   * or no loxId.
   1514   *
   1515   * @type {EventData[]?}
   1516   */
   1517  _pendingEvents: null,
   1518  /**
   1519   * Update the shown value.
   1520   */
   1521  _updatePendingEvents() {
   1522    // Should be safe to trigger the update, even when the value hasn't changed.
   1523    this._pendingEvents = this._loxId ? Lox.getEventData(this._loxId) : null;
   1524    this._updateUnlockArea();
   1525  },
   1526 
   1527  /**
   1528   * Update the display of the current or next unlock.
   1529   */
   1530  _updateUnlockArea() {
   1531    if (
   1532      !this._loxId ||
   1533      this._pendingEvents === null ||
   1534      this._remainingInvites === null ||
   1535      this._nextUnlock === null
   1536    ) {
   1537      // Uninitialized or no Lox source.
   1538      // NOTE: This area may already be hidden by the change in Lox source,
   1539      // but we clean up for the next non-empty id.
   1540      this._unlockAlert.hidden = true;
   1541      this._detailsArea.hidden = true;
   1542      return;
   1543    }
   1544 
   1545    // Grab focus state before changing visibility.
   1546    const alertHadFocus = this._unlockAlert.contains(document.activeElement);
   1547    const detailsHadFocus = this._detailsArea.contains(document.activeElement);
   1548 
   1549    const pendingEvents = this._pendingEvents;
   1550    const showAlert = !!pendingEvents.length;
   1551    this._unlockAlert.hidden = !showAlert;
   1552    this._detailsArea.hidden = showAlert;
   1553 
   1554    if (showAlert) {
   1555      // At level 0 and level 1, we do not have any invites.
   1556      // If the user starts and ends on level 0 or 1, then overall they would
   1557      // have had no change in their invites. So we do not want to show their
   1558      // latest updates.
   1559      // NOTE: If the user starts at level > 1 and ends with level 1 (levelling
   1560      // down to level 0 should not be possible), then we *do* want to show the
   1561      // user that they now have "0" invites.
   1562      // NOTE: pendingEvents are time-ordered, with the most recent event
   1563      // *last*.
   1564      const firstEvent = pendingEvents[0];
   1565      // NOTE: We cannot get a blockage event when the user starts at level 1 or
   1566      // 0.
   1567      const startingAtLowLevel =
   1568        firstEvent.type === "levelup" && firstEvent.newLevel <= 2;
   1569      const lastEvent = pendingEvents[pendingEvents.length - 1];
   1570      const endingAtLowLevel = lastEvent.newLevel <= 1;
   1571 
   1572      const showInvites = !(startingAtLowLevel && endingAtLowLevel);
   1573 
   1574      let blockage = false;
   1575      let levelUp = false;
   1576      let bridgeGain = false;
   1577      // Go through events, in the order that they occurred.
   1578      for (const loxEvent of pendingEvents) {
   1579        if (loxEvent.type === "levelup") {
   1580          levelUp = true;
   1581          if (loxEvent.newLevel === 1) {
   1582            // Gain 2 bridges from level 0 to 1.
   1583            bridgeGain = true;
   1584          }
   1585        } else {
   1586          blockage = true;
   1587        }
   1588      }
   1589 
   1590      let alertTitleId;
   1591      if (levelUp && !blockage) {
   1592        alertTitleId = "tor-bridges-lox-upgrade";
   1593      } else {
   1594        // Show as blocked bridges replaced.
   1595        // Even if we have a mixture of level ups as well.
   1596        alertTitleId = "tor-bridges-lox-blocked";
   1597      }
   1598      document.l10n.setAttributes(this._unlockAlertTitle, alertTitleId);
   1599      document.l10n.setAttributes(
   1600        this._unlockAlertInviteItem,
   1601        "tor-bridges-lox-new-invites",
   1602        { numInvites: this._remainingInvites }
   1603      );
   1604      this._unlockAlert.classList.toggle(
   1605        "lox-unlock-upgrade",
   1606        levelUp && !blockage
   1607      );
   1608      this._unlockItems.gainBridges.hidden = !bridgeGain;
   1609      this._unlockItems.newBridges.hidden = !blockage;
   1610      this._unlockItems.invites.hidden = !showInvites;
   1611    } else {
   1612      // Show next unlock.
   1613      // Number of days until the next unlock, rounded up.
   1614      const numDays = Math.max(
   1615        1,
   1616        Math.ceil(
   1617          (new Date(this._nextUnlock.date).getTime() - Date.now()) /
   1618            (24 * 60 * 60 * 1000)
   1619        )
   1620      );
   1621      for (const counterEl of this._nextUnlockCounterEls) {
   1622        document.l10n.setAttributes(
   1623          counterEl,
   1624          "tor-bridges-lox-days-until-unlock",
   1625          { numDays }
   1626        );
   1627      }
   1628 
   1629      // Gain 2 bridges from level 0 to 1. After that gain invites.
   1630      this._nextUnlockItems.gainBridges.hidden =
   1631        this._nextUnlock.nextLevel !== 1;
   1632      this._nextUnlockItems.firstInvites.hidden =
   1633        this._nextUnlock.nextLevel !== 2;
   1634      this._nextUnlockItems.moreInvites.hidden =
   1635        this._nextUnlock.nextLevel <= 2;
   1636    }
   1637 
   1638    if (alertHadFocus && !showAlert) {
   1639      // Alert has become hidden, move focus back up to the now revealed details
   1640      // area.
   1641      // NOTE: We have two headings: one shown during a search and one shown
   1642      // otherwise. We focus the heading that is currently visible.
   1643      // See tor-browser#43320.
   1644      // TODO: It might be better if we could use the # named anchor to
   1645      // re-orient the screen reader position instead of using tabIndex=-1, but
   1646      // about:preferences currently uses the anchor for showing categories
   1647      // only. See bugzilla bug 1799153.
   1648      if (
   1649        this._nextUnlockCounterEls[0].checkVisibility({
   1650          visibilityProperty: true,
   1651        })
   1652      ) {
   1653        this._nextUnlockCounterEls[0].focus();
   1654      } else {
   1655        this._nextUnlockCounterEls[1].focus();
   1656      }
   1657    } else if (detailsHadFocus && showAlert) {
   1658      this._unlockAlertButton.focus();
   1659    }
   1660  },
   1661 
   1662  /**
   1663   * Update the invites area.
   1664   */
   1665  _updateInvitesArea() {
   1666    let hasInvites;
   1667    if (
   1668      !this._loxId ||
   1669      this._remainingInvites === null ||
   1670      this._haveExistingInvites === null
   1671    ) {
   1672      // Not initialized yet.
   1673      hasInvites = false;
   1674    } else {
   1675      hasInvites = this._haveExistingInvites || !!this._remainingInvites;
   1676    }
   1677 
   1678    if (
   1679      !hasInvites &&
   1680      (this._remainingInvitesEl.contains(document.activeElement) ||
   1681        this._invitesButton.contains(document.activeElement))
   1682    ) {
   1683      // About to loose focus.
   1684      // Unexpected for the lox level to loose all invites.
   1685      // Move to the top of the details area, which should be visible if we
   1686      // just had focus.
   1687      this._nextUnlockCounterEl.focus();
   1688    }
   1689    // Hide the invite elements if we have no historic invites or a way of
   1690    // creating new ones.
   1691    this._remainingInvitesEl.hidden = !hasInvites;
   1692    this._invitesButton.hidden = !hasInvites;
   1693 
   1694    if (hasInvites) {
   1695      document.l10n.setAttributes(
   1696        this._remainingInvitesEl,
   1697        "tor-bridges-lox-remaining-invites",
   1698        { numInvites: this._remainingInvites }
   1699      );
   1700    }
   1701  },
   1702 };
   1703 
   1704 /**
   1705 * Controls the bridge settings.
   1706 */
   1707 const gBridgeSettings = {
   1708  /**
   1709   * The preferences <groupbox> for bridges
   1710   *
   1711   * @type {Element?}
   1712   */
   1713  _groupEl: null,
   1714  /**
   1715   * The button for controlling whether bridges are enabled.
   1716   *
   1717   * @type {Element?}
   1718   */
   1719  _toggleButton: null,
   1720  /**
   1721   * The area for showing current bridges.
   1722   *
   1723   * @type {Element?}
   1724   */
   1725  _bridgesEl: null,
   1726  /**
   1727   * The area for sharing bridge addresses.
   1728   *
   1729   * @type {Element?}
   1730   */
   1731  _shareEl: null,
   1732  /**
   1733   * The two headings for the bridge settings.
   1734   *
   1735   * One heading is shown during a search, the other is shown otherwise.
   1736   *
   1737   * @type {?Element[]}
   1738   */
   1739  _bridgesSettingsHeadings: null,
   1740  /**
   1741   * The two headings for the current bridges, at the start of the area.
   1742   *
   1743   * One heading is shown during a search, the other is shown otherwise.
   1744   *
   1745   * @type {Element?}
   1746   */
   1747  _currentBridgesHeadings: null,
   1748  /**
   1749   * The area for showing no bridges.
   1750   *
   1751   * @type {Element?}
   1752   */
   1753  _noBridgesEl: null,
   1754  /**
   1755   * The heading elements for changing bridges.
   1756   *
   1757   * One heading is shown during a search, the other is shown otherwise.
   1758   *
   1759   * @type {?Element[]}
   1760   */
   1761  _changeHeadingEls: null,
   1762  /**
   1763   * The button for user to provide a bridge address or share code.
   1764   *
   1765   * @type {Element?}
   1766   */
   1767  _userProvideButton: null,
   1768  /**
   1769   * A map from the bridge source to its corresponding label.
   1770   *
   1771   * @type {?Map<number, Element>}
   1772   */
   1773  _sourceLabels: null,
   1774 
   1775  /**
   1776   * Initialize the bridge settings.
   1777   */
   1778  init() {
   1779    gBridgesNotification.init();
   1780 
   1781    this._bridgesSettingsHeadings = Array.from(
   1782      document.querySelectorAll(".tor-bridges-subcategory-heading")
   1783    );
   1784    this._currentBridgesHeadings = Array.from(
   1785      document.querySelectorAll(".tor-bridges-current-heading")
   1786    );
   1787    this._bridgesEl = document.getElementById("tor-bridges-current");
   1788    this._noBridgesEl = document.getElementById("tor-bridges-none");
   1789    this._groupEl = document.getElementById("torPreferences-bridges-group");
   1790 
   1791    this._sourceLabels = new Map([
   1792      [
   1793        TorBridgeSource.BuiltIn,
   1794        document.getElementById("tor-bridges-built-in-label"),
   1795      ],
   1796      [
   1797        TorBridgeSource.UserProvided,
   1798        document.getElementById("tor-bridges-user-label"),
   1799      ],
   1800      [
   1801        TorBridgeSource.BridgeDB,
   1802        document.getElementById("tor-bridges-requested-label"),
   1803      ],
   1804      [TorBridgeSource.Lox, document.getElementById("tor-bridges-lox-label")],
   1805    ]);
   1806    this._shareEl = document.getElementById("tor-bridges-share");
   1807 
   1808    this._toggleButton = document.getElementById("tor-bridges-enabled-toggle");
   1809    // Initially disabled whilst TorSettings may not be initialized.
   1810    this._toggleButton.disabled = true;
   1811 
   1812    this._toggleButton.addEventListener("toggle", () => {
   1813      if (!this._haveBridges) {
   1814        return;
   1815      }
   1816      TorSettings.changeSettings({
   1817        bridges: { enabled: this._toggleButton.pressed },
   1818      });
   1819    });
   1820 
   1821    this._changeHeadingEls = Array.from(
   1822      document.querySelectorAll(".tor-bridges-change-heading")
   1823    );
   1824    this._userProvideButton = document.getElementById(
   1825      "tor-bridges-open-user-provide-dialog-button"
   1826    );
   1827 
   1828    document.l10n.setAttributes(
   1829      document.getElementById("tor-bridges-user-provide-description"),
   1830      // TODO: Set a different string if we have Lox enabled.
   1831      "tor-bridges-add-addresses-description"
   1832    );
   1833 
   1834    // TODO: Change to GetLoxBridges if Lox enabled, and the account is set up.
   1835    const telegramUserName = "GetBridgesBot";
   1836    const telegramInstruction = document.getElementById(
   1837      "tor-bridges-provider-instruction-telegram"
   1838    );
   1839    telegramInstruction.querySelector("a").href =
   1840      `https://t.me/${telegramUserName}`;
   1841    document.l10n.setAttributes(
   1842      telegramInstruction,
   1843      "tor-bridges-provider-telegram-instruction",
   1844      { telegramUserName }
   1845    );
   1846 
   1847    document
   1848      .getElementById("tor-bridges-open-built-in-dialog-button")
   1849      .addEventListener("click", () => {
   1850        this._openBuiltinDialog();
   1851      });
   1852    this._userProvideButton.addEventListener("click", () => {
   1853      this._openUserProvideDialog(this._haveBridges ? "replace" : "add");
   1854    });
   1855    document
   1856      .getElementById("tor-bridges-open-request-dialog-button")
   1857      .addEventListener("click", () => {
   1858        this._openRequestDialog();
   1859      });
   1860 
   1861    Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
   1862 
   1863    gBridgeGrid.init();
   1864    gBuiltinBridgesArea.init();
   1865    gLoxStatus.init();
   1866 
   1867    this._initBridgesMenu();
   1868    this._initShareArea();
   1869 
   1870    // NOTE: Before initializedPromise completes, the current bridges sections
   1871    // should be hidden.
   1872    // And gBridgeGrid and gBuiltinBridgesArea are not active.
   1873    TorSettings.initializedPromise.then(() => {
   1874      this._updateEnabled();
   1875      this._updateBridgeStrings();
   1876      this._updateSource();
   1877    });
   1878  },
   1879 
   1880  /**
   1881   * Un-initialize the bridge settings.
   1882   */
   1883  uninit() {
   1884    gBridgeGrid.uninit();
   1885    gBuiltinBridgesArea.uninit();
   1886    gLoxStatus.uninit();
   1887 
   1888    Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
   1889  },
   1890 
   1891  observe(subject, topic) {
   1892    switch (topic) {
   1893      case TorSettingsTopics.SettingsChanged: {
   1894        const { changes } = subject.wrappedJSObject;
   1895        if (changes.includes("bridges.enabled")) {
   1896          this._updateEnabled();
   1897        }
   1898        if (changes.includes("bridges.source")) {
   1899          this._updateSource();
   1900        }
   1901        if (changes.includes("bridges.bridge_strings")) {
   1902          this._updateBridgeStrings();
   1903        }
   1904        break;
   1905      }
   1906    }
   1907  },
   1908 
   1909  /**
   1910   * Update whether the bridges should be shown as enabled.
   1911   */
   1912  _updateEnabled() {
   1913    // Changing the pressed property on moz-toggle should not trigger its
   1914    // "toggle" event.
   1915    this._toggleButton.pressed = TorSettings.bridges.enabled;
   1916  },
   1917 
   1918  /**
   1919   * The shown bridge source.
   1920   *
   1921   * Initially null to indicate that it is unset for the first call to
   1922   * _updateSource.
   1923   *
   1924   * @type {integer?}
   1925   */
   1926  _bridgeSource: null,
   1927  /**
   1928   * Whether the user is encouraged to share their bridge addresses.
   1929   *
   1930   * @type {boolean}
   1931   */
   1932  _canShare: false,
   1933 
   1934  /**
   1935   * Update _bridgeSource.
   1936   */
   1937  _updateSource() {
   1938    // NOTE: This should only ever be called after TorSettings is already
   1939    // initialized.
   1940    const bridgeSource = TorSettings.bridges.source;
   1941    if (bridgeSource === this._bridgeSource) {
   1942      // Avoid re-activating an area if the source has not changed.
   1943      return;
   1944    }
   1945 
   1946    this._bridgeSource = bridgeSource;
   1947 
   1948    // Before hiding elements, we determine whether our region contained the
   1949    // user focus.
   1950    const hadFocus =
   1951      this._bridgesEl.contains(document.activeElement) ||
   1952      this._noBridgesEl.contains(document.activeElement);
   1953 
   1954    for (const [source, labelEl] of this._sourceLabels.entries()) {
   1955      labelEl.hidden = source !== bridgeSource;
   1956    }
   1957 
   1958    this._canShare =
   1959      bridgeSource === TorBridgeSource.UserProvided ||
   1960      bridgeSource === TorBridgeSource.BridgeDB;
   1961 
   1962    this._shareEl.hidden = !this._canShare;
   1963 
   1964    // Force the menu to close whenever the source changes.
   1965    // NOTE: If the menu had focus then hadFocus will be true, and focus will be
   1966    // re-assigned.
   1967    this._forceCloseBridgesMenu();
   1968 
   1969    // Update whether we have bridges.
   1970    this._updateHaveBridges();
   1971 
   1972    if (hadFocus) {
   1973      // Always reset the focus to the start of the area whenever the source
   1974      // changes.
   1975      // NOTE: gBuiltinBridges._updateBridgeType and gBridgeGrid._updateRows
   1976      // may have already called takeFocus in response to them being
   1977      // de-activated. The re-call should be safe.
   1978      this.takeFocus();
   1979    }
   1980  },
   1981 
   1982  /**
   1983   * Whether we have bridges or not, or null if it is unknown.
   1984   *
   1985   * @type {boolean?}
   1986   */
   1987  _haveBridges: null,
   1988 
   1989  /**
   1990   * Update the _haveBridges value.
   1991   */
   1992  _updateHaveBridges() {
   1993    // NOTE: We use the TorSettings.bridges.source value, rather than
   1994    // this._bridgeSource because _updateHaveBridges can be called just before
   1995    // _updateSource (via takeFocus).
   1996    const haveBridges = TorSettings.bridges.source !== TorBridgeSource.Invalid;
   1997 
   1998    if (haveBridges === this._haveBridges) {
   1999      return;
   2000    }
   2001 
   2002    this._haveBridges = haveBridges;
   2003 
   2004    this._toggleButton.disabled = !haveBridges;
   2005    // Add classes to show or hide the "no bridges" and "Your bridges" sections.
   2006    // NOTE: Before haveBridges is set, neither class is added, so both sections
   2007    // and hidden.
   2008    this._groupEl.classList.add("bridges-initialized");
   2009    this._bridgesEl.hidden = !haveBridges;
   2010    this._noBridgesEl.hidden = haveBridges;
   2011 
   2012    for (const headingEl of this._changeHeadingEls) {
   2013      document.l10n.setAttributes(
   2014        headingEl,
   2015        haveBridges
   2016          ? "tor-bridges-replace-bridges-heading"
   2017          : "tor-bridges-add-bridges-heading"
   2018      );
   2019    }
   2020    document.l10n.setAttributes(
   2021      this._userProvideButton,
   2022      haveBridges ? "tor-bridges-replace-button" : "tor-bridges-add-new-button"
   2023    );
   2024  },
   2025 
   2026  /**
   2027   * Force the focus to move to the bridge area.
   2028   */
   2029  takeFocus() {
   2030    if (this._haveBridges === null) {
   2031      // The bridges area has not been initialized yet, which means that
   2032      // TorSettings may not be initialized.
   2033      // Unexpected to receive a call before then, so just return early.
   2034      return;
   2035    }
   2036 
   2037    // Make sure we have the latest value for _haveBridges.
   2038    // We also ensure that the _currentBridgesHeadings element is visible before
   2039    // we focus it.
   2040    this._updateHaveBridges();
   2041 
   2042    // Move focus to the start of the relevant section, which is a heading.
   2043    // They have tabindex="-1" so should be focusable, even though they are not
   2044    // part of the usual tab navigation.
   2045    // NOTE: We have two headings: one shown during a search and one shown
   2046    // otherwise. We focus the heading that is currently visible.
   2047    // See tor-browser#43320.
   2048    // TODO: It might be better if we could use the # named anchor to
   2049    // re-orient the screen reader position instead of using tabIndex=-1, but
   2050    // about:preferences currently uses the anchor for showing categories
   2051    // only. See bugzilla bug 1799153.
   2052    const focusHeadings = this._haveBridges
   2053      ? this._currentBridgesHeadings // The heading above the new bridges.
   2054      : this._bridgesSettingsHeadings; // The top of the bridge settings.
   2055    if (focusHeadings[0].checkVisibility({ visibilityProperty: true })) {
   2056      focusHeadings[0].focus();
   2057    } else {
   2058      focusHeadings[1].focus();
   2059    }
   2060  },
   2061 
   2062  /**
   2063   * The bridge strings in a copy-able form.
   2064   *
   2065   * @type {string}
   2066   */
   2067  _bridgeStrings: "",
   2068  /**
   2069   * Whether the bridge strings should be shown as a QR code.
   2070   *
   2071   * @type {boolean}
   2072   */
   2073  _canQRBridges: false,
   2074 
   2075  /**
   2076   * Update the stored bridge strings.
   2077   */
   2078  _updateBridgeStrings() {
   2079    const bridges = TorSettings.bridges.bridge_strings;
   2080 
   2081    this._bridgeStrings = bridges.join("\n");
   2082    // TODO: Determine what logic we want.
   2083    this._canQRBridges = bridges.length <= 3;
   2084 
   2085    this._qrButton.disabled = !this._canQRBridges;
   2086  },
   2087 
   2088  /**
   2089   * Copy all the bridge addresses to the clipboard.
   2090   */
   2091  _copyBridges() {
   2092    const clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
   2093      Ci.nsIClipboardHelper
   2094    );
   2095    clipboard.copyString(this._bridgeStrings);
   2096  },
   2097 
   2098  /**
   2099   * Open the QR code dialog encoding all the bridge addresses.
   2100   */
   2101  _openQR() {
   2102    if (!this._canQRBridges) {
   2103      return;
   2104    }
   2105    showBridgeQr(this._bridgeStrings);
   2106  },
   2107 
   2108  /**
   2109   * The QR button for copying all QR codes.
   2110   *
   2111   * @type {Element?}
   2112   */
   2113  _qrButton: null,
   2114 
   2115  _initShareArea() {
   2116    document
   2117      .getElementById("tor-bridges-copy-addresses-button")
   2118      .addEventListener("click", () => {
   2119        this._copyBridges();
   2120      });
   2121 
   2122    this._qrButton = document.getElementById("tor-bridges-qr-addresses-button");
   2123    this._qrButton.addEventListener("click", () => {
   2124      this._openQR();
   2125    });
   2126  },
   2127 
   2128  /**
   2129   * The menu for all bridges.
   2130   *
   2131   * @type {Element?}
   2132   */
   2133  _bridgesMenu: null,
   2134 
   2135  /**
   2136   * Initialize the menu for all bridges.
   2137   */
   2138  _initBridgesMenu() {
   2139    this._bridgesMenu = document.getElementById("tor-bridges-all-options-menu");
   2140 
   2141    // NOTE: We generally assume that once the bridge menu is opened the
   2142    // this._bridgeStrings value will not change.
   2143    const qrItem = document.getElementById(
   2144      "tor-bridges-options-qr-all-menu-item"
   2145    );
   2146    qrItem.addEventListener("click", () => {
   2147      this._openQR();
   2148    });
   2149 
   2150    const copyItem = document.getElementById(
   2151      "tor-bridges-options-copy-all-menu-item"
   2152    );
   2153    copyItem.addEventListener("click", () => {
   2154      this._copyBridges();
   2155    });
   2156 
   2157    const editItem = document.getElementById(
   2158      "tor-bridges-options-edit-all-menu-item"
   2159    );
   2160    editItem.addEventListener("click", () => {
   2161      this._openUserProvideDialog("edit");
   2162    });
   2163 
   2164    // TODO: Do we want a different item for built-in bridges, rather than
   2165    // "Remove all bridges"?
   2166    document
   2167      .getElementById("tor-bridges-options-remove-all-menu-item")
   2168      .addEventListener("click", async () => {
   2169        // TODO: Should we only have a warning when not built-in?
   2170        const parentWindow =
   2171          Services.wm.getMostRecentWindow("navigator:browser");
   2172        const flags =
   2173          Services.prompt.BUTTON_POS_0 *
   2174            Services.prompt.BUTTON_TITLE_IS_STRING +
   2175          Services.prompt.BUTTON_POS_0_DEFAULT +
   2176          Services.prompt.BUTTON_DEFAULT_IS_DESTRUCTIVE +
   2177          Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
   2178 
   2179        const [titleString, bodyString, removeString] =
   2180          await document.l10n.formatValues([
   2181            { id: "remove-all-bridges-warning-title" },
   2182            { id: "remove-all-bridges-warning-description" },
   2183            { id: "remove-all-bridges-warning-remove-button" },
   2184          ]);
   2185 
   2186        // TODO: Update the text, and remove old strings.
   2187        const buttonIndex = Services.prompt.confirmEx(
   2188          parentWindow,
   2189          titleString,
   2190          bodyString,
   2191          flags,
   2192          removeString,
   2193          null,
   2194          null,
   2195          null,
   2196          {}
   2197        );
   2198 
   2199        if (buttonIndex !== 0) {
   2200          return;
   2201        }
   2202 
   2203        TorSettings.changeSettings({
   2204          // This should always have the side effect of disabling bridges as
   2205          // well.
   2206          bridges: { source: TorBridgeSource.Invalid },
   2207        });
   2208      });
   2209 
   2210    this._bridgesMenu.addEventListener("showing", () => {
   2211      qrItem.hidden = !this._canShare || !this._canQRBridges;
   2212      editItem.hidden = this._bridgeSource !== TorBridgeSource.UserProvided;
   2213    });
   2214 
   2215    const bridgesMenuButton = document.getElementById(
   2216      "tor-bridges-all-options-button"
   2217    );
   2218    bridgesMenuButton.addEventListener("click", event => {
   2219      this._bridgesMenu.toggle(event, bridgesMenuButton);
   2220    });
   2221 
   2222    this._bridgesMenu.addEventListener("hidden", () => {
   2223      // Make sure the button receives focus again when the menu is hidden.
   2224      // Currently, panel-list.js only does this when the menu is opened with a
   2225      // keyboard, but this causes focus to be lost from the page if the user
   2226      // uses a mixture of keyboard and mouse.
   2227      bridgesMenuButton.focus();
   2228    });
   2229  },
   2230 
   2231  /**
   2232   * Force the bridges menu to close.
   2233   */
   2234  _forceCloseBridgesMenu() {
   2235    this._bridgesMenu.hide(null, { force: true });
   2236  },
   2237 
   2238  /**
   2239   * Open a bridge dialog that will change the users bridges.
   2240   *
   2241   * @param {string} url - The url of the dialog to open.
   2242   * @param {object?} inputData - The input data to send to the dialog window.
   2243   * @param {Function} onAccept - The method to call if the bridge dialog was
   2244   *   accepted by the user. This will be passed a "result" object containing
   2245   *   data set by the dialog. This should return a promise that resolves once
   2246   *   the bridge settings have been set, or null if the settings have not
   2247   *   been applied.
   2248   */
   2249  _openDialog(url, inputData, onAccept) {
   2250    const result = { accepted: false, connect: false };
   2251    let savedSettings = null;
   2252    gSubDialog.open(
   2253      url,
   2254      {
   2255        features: "resizable=yes",
   2256        closingCallback: () => {
   2257          if (!result.accepted) {
   2258            return;
   2259          }
   2260          savedSettings = onAccept(result);
   2261          if (!savedSettings) {
   2262            // No change in settings.
   2263            return;
   2264          }
   2265          if (!result.connect) {
   2266            // Do not open about:torconnect.
   2267            return;
   2268          }
   2269 
   2270          // Wait until the settings are applied before bootstrapping.
   2271          // NOTE: Saving the settings should also cancel any existing bootstrap
   2272          // attempt first. See tor-browser#41921.
   2273          savedSettings.then(() => {
   2274            // The bridge dialog button is "connect" when Tor is not
   2275            // bootstrapped, so do the connect.
   2276 
   2277            // Start Bootstrapping, which should use the configured bridges.
   2278            // NOTE: We do this regardless of any previous TorConnect Error.
   2279            TorConnectParent.open({ beginBootstrapping: "hard" });
   2280          });
   2281        },
   2282        // closedCallback should be called after gSubDialog has already
   2283        // re-assigned focus back to the document.
   2284        closedCallback: () => {
   2285          if (!savedSettings) {
   2286            return;
   2287          }
   2288          // Wait until the settings have changed, so that the UI could
   2289          // respond, then move focus.
   2290          savedSettings.then(() => gBridgeSettings.takeFocus());
   2291        },
   2292      },
   2293      result,
   2294      inputData
   2295    );
   2296  },
   2297 
   2298  /**
   2299   * Open the built-in bridge dialog.
   2300   */
   2301  _openBuiltinDialog() {
   2302    this._openDialog(
   2303      "chrome://browser/content/torpreferences/builtinBridgeDialog.xhtml",
   2304      null,
   2305      result => {
   2306        if (!result.type) {
   2307          return null;
   2308        }
   2309        return TorSettings.changeSettings({
   2310          bridges: {
   2311            enabled: true,
   2312            source: TorBridgeSource.BuiltIn,
   2313            builtin_type: result.type,
   2314          },
   2315        });
   2316      }
   2317    );
   2318  },
   2319 
   2320  /*
   2321   * Open the request bridge dialog.
   2322   */
   2323  _openRequestDialog() {
   2324    this._openDialog(
   2325      "chrome://browser/content/torpreferences/requestBridgeDialog.xhtml",
   2326      null,
   2327      result => {
   2328        if (!result.bridges?.length) {
   2329          return null;
   2330        }
   2331        return TorSettings.changeSettings({
   2332          bridges: {
   2333            enabled: true,
   2334            source: TorBridgeSource.BridgeDB,
   2335            bridge_strings: result.bridges,
   2336          },
   2337        });
   2338      }
   2339    );
   2340  },
   2341 
   2342  /**
   2343   * Open the user provide dialog.
   2344   *
   2345   * @param {string} mode - The mode to open the dialog in: "add", "replace" or
   2346   *   "edit".
   2347   */
   2348  _openUserProvideDialog(mode) {
   2349    this._openDialog(
   2350      "chrome://browser/content/torpreferences/provideBridgeDialog.xhtml",
   2351      { mode },
   2352      result => {
   2353        const loxId = result.loxId;
   2354        if (!loxId && !result.addresses?.length) {
   2355          return null;
   2356        }
   2357        const bridges = { enabled: true };
   2358        if (loxId) {
   2359          bridges.source = TorBridgeSource.Lox;
   2360          bridges.lox_id = loxId;
   2361        } else {
   2362          bridges.source = TorBridgeSource.UserProvided;
   2363          bridges.bridge_strings = result.addresses;
   2364        }
   2365        return TorSettings.changeSettings({ bridges });
   2366      }
   2367    );
   2368  },
   2369 };
   2370 
   2371 /**
   2372 * Area to show the internet and tor network connection status.
   2373 */
   2374 const gNetworkStatus = {
   2375  /**
   2376   * Initialize the area.
   2377   */
   2378  init() {
   2379    this._internetAreaEl = document.getElementById(
   2380      "network-status-internet-area"
   2381    );
   2382    this._internetResultEl = this._internetAreaEl.querySelector(
   2383      ".network-status-result"
   2384    );
   2385 
   2386    this._torAreaEl = document.getElementById("network-status-tor-area");
   2387    this._torResultEl = this._torAreaEl.querySelector(".network-status-result");
   2388    this._torConnectButton = document.getElementById(
   2389      "network-status-tor-connect-button"
   2390    );
   2391    this._torConnectButton.addEventListener("click", () => {
   2392      TorConnectParent.open({ beginBootstrapping: "soft" });
   2393    });
   2394 
   2395    this._updateInternetStatus();
   2396    this._updateTorConnectionStatus();
   2397 
   2398    Services.obs.addObserver(this, TorConnectTopics.StageChange);
   2399    Services.obs.addObserver(this, TorConnectTopics.InternetStatusChange);
   2400  },
   2401 
   2402  /**
   2403   * Un-initialize the area.
   2404   */
   2405  uninit() {
   2406    Services.obs.removeObserver(this, TorConnectTopics.StageChange);
   2407    Services.obs.removeObserver(this, TorConnectTopics.InternetStatusChange);
   2408  },
   2409 
   2410  observe(subject, topic) {
   2411    switch (topic) {
   2412      // triggered when tor connect state changes and we may
   2413      // need to update the messagebox
   2414      case TorConnectTopics.StageChange:
   2415        this._updateTorConnectionStatus();
   2416        break;
   2417      case TorConnectTopics.InternetStatusChange:
   2418        this._updateInternetStatus();
   2419        break;
   2420    }
   2421  },
   2422 
   2423  /**
   2424   * Update the shown internet status.
   2425   */
   2426  _updateInternetStatus() {
   2427    let l10nId;
   2428    let isOffline = false;
   2429    switch (TorConnect.internetStatus) {
   2430      case InternetStatus.Offline:
   2431        l10nId = "tor-connection-internet-status-offline";
   2432        isOffline = true;
   2433        break;
   2434      case InternetStatus.Online:
   2435        l10nId = "tor-connection-internet-status-online";
   2436        break;
   2437      default:
   2438        l10nId = "tor-connection-internet-status-unknown";
   2439        break;
   2440    }
   2441    this._internetResultEl.setAttribute("data-l10n-id", l10nId);
   2442    this._internetAreaEl.classList.toggle("status-offline", isOffline);
   2443  },
   2444 
   2445  /**
   2446   * Update the shown Tor connection status.
   2447   */
   2448  _updateTorConnectionStatus() {
   2449    const buttonHadFocus = this._torConnectButton.contains(
   2450      document.activeElement
   2451    );
   2452    const isBootstrapped =
   2453      TorConnect.stageName === TorConnectStage.Bootstrapped;
   2454    const isBlocked = !isBootstrapped && TorConnect.potentiallyBlocked;
   2455    let l10nId;
   2456    if (isBootstrapped) {
   2457      l10nId = "tor-connection-network-status-connected";
   2458    } else if (isBlocked) {
   2459      l10nId = "tor-connection-network-status-blocked";
   2460    } else {
   2461      l10nId = "tor-connection-network-status-not-connected";
   2462    }
   2463 
   2464    document.l10n.setAttributes(this._torResultEl, l10nId);
   2465    this._torAreaEl.classList.toggle("status-connected", isBootstrapped);
   2466    this._torAreaEl.classList.toggle("status-blocked", isBlocked);
   2467    if (isBootstrapped && buttonHadFocus) {
   2468      // Button has become hidden and will loose focus. Most likely this has
   2469      // happened because the user clicked the button to open about:torconnect.
   2470      // Since this is near the top of the page, we move focus to the search
   2471      // input (for when the user returns).
   2472      gSearchResultsPane.searchInput.focus();
   2473    }
   2474  },
   2475 };
   2476 
   2477 /*
   2478  Connection Pane
   2479 
   2480  Code for populating the XUL in about:preferences#connection, handling input events, interfacing with tor-launcher
   2481 */
   2482 const gConnectionPane = (function () {
   2483  /* CSS selectors for all of the Tor Network DOM elements we need to access */
   2484  const selectors = {
   2485    bridges: {
   2486      locationGroup: "#torPreferences-bridges-locationGroup",
   2487      locationLabel: "#torPreferences-bridges-locationLabel",
   2488      location: "#torPreferences-bridges-location",
   2489      locationEntries: "#torPreferences-bridges-locationEntries",
   2490      chooseForMe: "#torPreferences-bridges-buttonChooseBridgeForMe",
   2491    },
   2492  }; /* selectors */
   2493 
   2494  const retval = {
   2495    // cached frequently accessed DOM elements
   2496    _enableQuickstartToggle: null,
   2497 
   2498    // populate xul with strings and cache the relevant elements
   2499    _populateXUL() {
   2500      // Quickstart
   2501      this._enableQuickstartToggle = document.getElementById(
   2502        "tor-connection-quickstart-toggle"
   2503      );
   2504      this._enableQuickstartToggle.addEventListener("toggle", () => {
   2505        TorConnect.quickstart = this._enableQuickstartToggle.pressed;
   2506      });
   2507      this._enableQuickstartToggle.pressed = TorConnect.quickstart;
   2508      Services.obs.addObserver(this, TorConnectTopics.QuickstartChange);
   2509 
   2510      // Location
   2511      {
   2512        const prefpane = document.getElementById("mainPrefPane");
   2513 
   2514        const locationGroup = prefpane.querySelector(
   2515          selectors.bridges.locationGroup
   2516        );
   2517        prefpane.querySelector(selectors.bridges.locationLabel).textContent =
   2518          TorStrings.settings.bridgeLocation;
   2519        const location = prefpane.querySelector(selectors.bridges.location);
   2520        const locationEntries = prefpane.querySelector(
   2521          selectors.bridges.locationEntries
   2522        );
   2523        const chooseForMe = prefpane.querySelector(
   2524          selectors.bridges.chooseForMe
   2525        );
   2526        chooseForMe.setAttribute(
   2527          "label",
   2528          TorStrings.settings.bridgeChooseForMe
   2529        );
   2530        chooseForMe.addEventListener("command", () => {
   2531          if (!location.value) {
   2532            return;
   2533          }
   2534          TorConnectParent.open({
   2535            beginBootstrapping: "hard",
   2536            regionCode: location.value,
   2537          });
   2538        });
   2539        const createItem = (value, label, disabled) => {
   2540          const item = document.createXULElement("menuitem");
   2541          item.setAttribute("value", value);
   2542          item.setAttribute("label", label);
   2543          if (disabled) {
   2544            item.setAttribute("disabled", "true");
   2545          }
   2546          return item;
   2547        };
   2548 
   2549        // TODO: Re-fetch when intl:app-locales-changed is fired, if we keep
   2550        // this after tor-browser#42477.
   2551        const regionNames = TorConnect.getRegionNames();
   2552        const addLocations = codes => {
   2553          const items = [];
   2554          for (const code of codes) {
   2555            items.push(createItem(code, regionNames[code] || code));
   2556          }
   2557          items.sort((left, right) => left.label.localeCompare(right.label));
   2558          locationEntries.append(...items);
   2559        };
   2560        // Add automatic before waiting for getFrequentRegions.
   2561        locationEntries.append(
   2562          createItem("automatic", TorStrings.settings.bridgeLocationAutomatic)
   2563        );
   2564        location.value = "automatic";
   2565        TorConnect.getFrequentRegions().then(frequentCodes => {
   2566          locationEntries.append(
   2567            createItem("", TorStrings.settings.bridgeLocationFrequent, true)
   2568          );
   2569          addLocations(frequentCodes);
   2570          locationEntries.append(
   2571            createItem("", TorStrings.settings.bridgeLocationOther, true)
   2572          );
   2573          addLocations(Object.keys(regionNames));
   2574        });
   2575        this._showAutoconfiguration = () => {
   2576          locationGroup.hidden =
   2577            !TorConnect.canBeginAutoBootstrap || !TorConnect.potentiallyBlocked;
   2578        };
   2579        this._showAutoconfiguration();
   2580      }
   2581 
   2582      // Advanced setup
   2583      document
   2584        .getElementById("torPreferences-advanced-button")
   2585        .addEventListener("click", () => {
   2586          this.onAdvancedSettings();
   2587        });
   2588 
   2589      // Tor logs
   2590      document
   2591        .getElementById("torPreferences-buttonTorLogs")
   2592        .addEventListener("click", () => {
   2593          this.onViewTorLogs();
   2594        });
   2595 
   2596      Services.obs.addObserver(this, TorConnectTopics.StageChange);
   2597    },
   2598 
   2599    init() {
   2600      gBridgeSettings.init();
   2601      gNetworkStatus.init();
   2602 
   2603      this._populateXUL();
   2604 
   2605      const onUnload = () => {
   2606        window.removeEventListener("unload", onUnload);
   2607        gConnectionPane.uninit();
   2608      };
   2609      window.addEventListener("unload", onUnload);
   2610    },
   2611 
   2612    uninit() {
   2613      gBridgeSettings.uninit();
   2614      gNetworkStatus.uninit();
   2615 
   2616      // unregister our observer topics
   2617      Services.obs.removeObserver(this, TorConnectTopics.QuickstartChange);
   2618      Services.obs.removeObserver(this, TorConnectTopics.StageChange);
   2619    },
   2620 
   2621    // whether the page should be present in about:preferences
   2622    get enabled() {
   2623      return TorConnect.enabled;
   2624    },
   2625 
   2626    //
   2627    // Callbacks
   2628    //
   2629 
   2630    observe(subject, topic) {
   2631      switch (topic) {
   2632        case TorConnectTopics.QuickstartChange: {
   2633          this._enableQuickstartToggle.pressed = TorConnect.quickstart;
   2634          break;
   2635        }
   2636        // triggered when tor connect state changes and we may
   2637        // need to update the messagebox
   2638        case TorConnectTopics.StageChange: {
   2639          this._showAutoconfiguration();
   2640          break;
   2641        }
   2642      }
   2643    },
   2644 
   2645    async onAdvancedSettings() {
   2646      // Ensure TorSettings is complete before loading the dialog, which reads
   2647      // from TorSettings.
   2648      await TorSettings.initializedPromise;
   2649      gSubDialog.open(
   2650        "chrome://browser/content/torpreferences/connectionSettingsDialog.xhtml",
   2651        { features: "resizable=yes" }
   2652      );
   2653    },
   2654 
   2655    onViewTorLogs() {
   2656      gSubDialog.open(
   2657        "chrome://browser/content/torpreferences/torLogDialog.xhtml",
   2658        { features: "resizable=yes" }
   2659      );
   2660    },
   2661  };
   2662  return retval;
   2663 })(); /* gConnectionPane */