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 */