morgan pushed to branch tor-browser-153.1.0esr-16.0-1 at The Tor Project / Applications / Tor Browser Commits: 6a6cb5e8 by Henry Wilkes at 2026-08-26T12:08:36+00:00 fixup! TB 27476: Implement about:torconnect captive portal within Tor Browser TB 45236: Make the breadcrumbs focusable and activatable with a keyboard. We also update the breadcrumb states to be more consistent between stages, and we no longer disable prior breadcrumbs during a bootstrap since it is safe for the user to switch back to a prior stage. - - - - - 9250f769 by Henry Wilkes at 2026-08-26T12:08:36+00:00 fixup! TB 27476: Implement about:torconnect captive portal within Tor Browser TB 45236: Have the "configure connection" link point to about:preferences to give users a hint of where they are going. - - - - - fe4fe5a2 by Henry Wilkes at 2026-08-26T12:08:36+00:00 fixup! TB 27476: Implement about:torconnect captive portal within Tor Browser TB 45236: Drop the wrapping <form> element. Also fix the `for` attribute to point to the `<select>` element. - - - - - 747ca252 by Henry Wilkes at 2026-08-26T12:08:36+00:00 fixup! TB 27476: Implement about:torconnect captive portal within Tor Browser TB 45236: Expose the progress bar to accessibility tools. - - - - - 3cb7efed by Henry Wilkes at 2026-08-26T12:08:36+00:00 fixup! TB 40597: Implement TorSettings module TB 45236: Return early when Moat returns no bridges. - - - - - f54fe85b by Henry Wilkes at 2026-08-26T12:08:36+00:00 fixup! TB 40597: Implement TorSettings module TB 45236: Add an option to simulate progress in simulated bootstraps. This is useful for debugging the about:torconnect page. We also lazy-load the `setTimeout` and related methods since these are only used for simulated scenarios. - - - - - 4 changed files: - browser/components/torconnect/content/aboutTorConnect.css - browser/components/torconnect/content/aboutTorConnect.html - browser/components/torconnect/content/aboutTorConnect.js - toolkit/modules/TorConnect.sys.mjs Changes: ===================================== browser/components/torconnect/content/aboutTorConnect.css ===================================== @@ -25,42 +25,59 @@ body { #breadcrumbs { grid-area: breadcrumbs; display: flex; - align-items: center; + gap: var(--space-large); margin: 0 0 var(--space-xlarge) 0; -} -#breadcrumbs.hidden { - visibility: hidden; + &.hide-breadcrumbs { + visibility: hidden; + } + + & :is(ol, li:not([hidden])) { + display: contents; + } } .breadcrumb-item, .breadcrumb-separator { - display: flex; - margin: 0; - margin-inline-start: var(--space-large); + flex: 0 0 auto; padding: var(--space-small); } .breadcrumb-item { + display: flex; + gap: var(--space-xsmall); align-items: center; cursor: pointer; color: var(--text-color); border-radius: var(--border-radius-small); -} -.breadcrumb-item:hover { - color: var(--color-accent-primary); - background-color: var(--button-background-color-hover); -} + & > * { + flex: 0 0 auto; + } + + &:not(.breadcrumb-disabled):hover { + color: var(--color-accent-primary); + background-color: var(--button-background-color-hover); + } + + &:not(.breadcrumb-disabled):active { + color: var(--color-accent-primary-active); + background-color: var(--button-background-color-active); + } + + &.breadcrumb-active { + color: var(--color-accent-primary); + } -.breadcrumb-item:active { - color: var(--color-accent-primary-active); - background-color: var(--button-background-color-active); + &.breadcrumb-disabled { + color: var(--text-color); + opacity: 0.4; + cursor: unset; + } } .breadcrumb-separator { - width: var(--icon-size); - list-style-image: url("chrome://browser/content/torconnect/arrow-right.svg"); + content: url("chrome://browser/content/torconnect/arrow-right.svg"); } .breadcrumb-separator:dir(rtl) { @@ -68,61 +85,35 @@ body { } .breadcrumb-icon { - display: inline list-item; + width: var(--icon-size); height: var(--icon-size); - list-style-position: inside; -moz-context-properties: fill, stroke; fill: currentColor; stroke: currentColor; } -.breadcrumb-item.active { - color: var(--color-accent-primary); -} - -.breadcrumb-item.disabled, -.breadcrumb-item.disabled:hover, -.breadcrumb-item.disabled:active { - color: var(--text-color); - opacity: 0.4; - cursor: default; -} - -.breadcrumb-item.hidden, -.breadcrumb-separator.hidden { - display: none; -} - #connect-to-tor { margin-inline-start: 0; } #connect-to-tor-icon { - list-style-image: url("chrome://browser/content/torconnect/tor-connect.svg"); + content: url("chrome://browser/content/torconnect/tor-connect.svg"); } #connection-assist-icon { - list-style-image: url("chrome://browser/content/torconnect/tor-connect-broken.svg"); -} - -#try-bridge { - cursor: default; + content: url("chrome://browser/content/torconnect/tor-connect-broken.svg"); } #try-bridge-icon { - list-style-image: url("chrome://browser/content/torconnect/bridge.svg"); + content: url("chrome://browser/content/torconnect/bridge.svg"); } #locationDropdownLabel.error { color: var(--text-color-error); } -form#locationDropdown { +#regions-select { width: 240px; -} - -form#locationDropdown select { - width: 100%; margin: 0; font-weight: var(--font-weight-bold); } ===================================== browser/components/torconnect/content/aboutTorConnect.html ===================================== @@ -33,33 +33,43 @@ ></script> </head> <body class="onion-pattern-background"> - <div id="progressBar" hidden="hidden"> + <div + id="progressBar" + role="progressbar" + aria-valuenow="0" + aria-valuemin="0" + aria-valuemax="100" + aria-labelledby="tor-connect-heading" + hidden="hidden" + > <div id="progressSolid"></div> <div id="progressBackground"></div> <div id="progressBarBackground"></div> </div> - <div id="breadcrumbs" class="hidden"> - <span id="connect-to-tor" class="breadcrumb-item"> - <span id="connect-to-tor-icon" class="breadcrumb-icon"></span> - <span class="breadcrumb-label"></span> - </span> - <span - id="connection-assist-separator" - class="breadcrumb-separator breadcrumb-icon" - ></span> - <span id="connection-assist" class="breadcrumb-item"> - <span id="connection-assist-icon" class="breadcrumb-icon"></span> - <span class="breadcrumb-label"></span> - </span> - <span - id="try-bridge-separator" - class="breadcrumb-separator breadcrumb-icon" - ></span> - <span id="try-bridge" class="breadcrumb-item"> - <span id="try-bridge-icon" class="breadcrumb-icon"></span> - <span class="breadcrumb-label"></span> - </span> - </div> + <nav id="breadcrumbs"> + <ol> + <li id="connect-to-tor"> + <div class="breadcrumb-item" role="link" tabindex="0"> + <span id="connect-to-tor-icon" class="breadcrumb-icon"></span> + <span class="breadcrumb-label"></span> + </div> + </li> + <li id="connection-assist"> + <div class="breadcrumb-separator breadcrumb-icon"></div> + <div class="breadcrumb-item" role="link" tabindex="0"> + <span id="connection-assist-icon" class="breadcrumb-icon"></span> + <span class="breadcrumb-label"></span> + </div> + </li> + <li id="try-bridge"> + <div class="breadcrumb-separator breadcrumb-icon"></div> + <div class="breadcrumb-item" role="link" tabindex="0"> + <span id="try-bridge-icon" class="breadcrumb-icon"></span> + <span class="breadcrumb-label"></span> + </div> + </li> + </ol> + </nav> <div class="torconnect-stage-content" data-stage-name="ProviderStopped"> <img class="torconnect-icon assist-icon" alt="" /> <h1 @@ -166,14 +176,12 @@ <button id="configureButton" hidden="true"></button> <button id="cancelButton" hidden="true"></button> <button id="connectButton" hidden="true" class="tor-button"></button> - <label id="locationDropdownLabel" for="countries"></label> - <form id="locationDropdown" hidden="true"> - <select id="regions-select"> - <option id="first-region-option"></option> - <optgroup id="frequent-regions-option-group"></optgroup> - <optgroup id="full-regions-option-group"></optgroup> - </select> - </form> + <label id="locationDropdownLabel" for="regions-select"></label> + <select id="regions-select" hidden="hidden"> + <option id="first-region-option"></option> + <optgroup id="frequent-regions-option-group"></optgroup> + <optgroup id="full-regions-option-group"></optgroup> + </select> <button id="tryBridgeButton" hidden="true" class="tor-button"></button> </div> </div> ===================================== browser/components/torconnect/content/aboutTorConnect.js ===================================== @@ -42,12 +42,10 @@ class AboutTorConnect { label: "#connect-to-tor .breadcrumb-label", }, connectionAssist: { - separator: "#connection-assist-separator", link: "#connection-assist", label: "#connection-assist .breadcrumb-label", }, tryBridge: { - separator: "#try-bridge-separator", link: "#try-bridge", label: "#try-bridge .breadcrumb-label", }, @@ -66,7 +64,6 @@ class AboutTorConnect { connect: "button#connectButton", tryBridge: "button#tryBridgeButton", locationDropdownLabel: "#locationDropdownLabel", - locationDropdown: "form#locationDropdown", locationDropdownSelect: "#regions-select", }, }); @@ -90,18 +87,12 @@ class AboutTorConnect { connectToTorLabel: document.querySelector( this.selectors.breadcrumbs.connectToTor.label ), - connectionAssistSeparator: document.querySelector( - this.selectors.breadcrumbs.connectionAssist.separator - ), connectionAssistLink: document.querySelector( this.selectors.breadcrumbs.connectionAssist.link ), connectionAssistLabel: document.querySelector( this.selectors.breadcrumbs.connectionAssist.label ), - tryBridgeSeparator: document.querySelector( - this.selectors.breadcrumbs.tryBridge.separator - ), tryBridgeLink: document.querySelector( this.selectors.breadcrumbs.tryBridge.link ), @@ -120,9 +111,6 @@ class AboutTorConnect { locationDropdownLabel: document.querySelector( this.selectors.buttons.locationDropdownLabel ), - locationDropdown: document.querySelector( - this.selectors.buttons.locationDropdown - ), locationDropdownSelect: document.querySelector( this.selectors.buttons.locationDropdownSelect ), @@ -221,7 +209,7 @@ class AboutTorConnect { this.hide(this.elements.cancelButton); this.hide(this.elements.connectButton); this.hide(this.elements.locationDropdownLabel); - this.hide(this.elements.locationDropdown); + this.hide(this.elements.locationDropdownSelect); this.hide(this.elements.tryBridgeButton); } @@ -239,33 +227,49 @@ class AboutTorConnect { } setBreadcrumbsStatus(connectToTor, connectionAssist, tryBridge) { - this.elements.breadcrumbContainer.classList.remove("hidden"); - const elems = [ - [this.elements.connectToTorLink, connectToTor, null], - [ - this.elements.connectionAssistLink, - connectionAssist, - this.elements.connectionAssistSeparator, - ], - [ - this.elements.tryBridgeLink, - tryBridge, - this.elements.tryBridgeSeparator, - ], - ]; - elems.forEach(([elem, status, separator]) => { - elem.classList.remove(BreadcrumbStatus.Hidden); - elem.classList.remove(BreadcrumbStatus.Disabled); - elem.classList.remove(BreadcrumbStatus.Active); - if (status !== "") { - elem.classList.add(status); + this.elements.breadcrumbContainer.classList.remove("hide-breadcrumbs"); + for (const { listEl, status } of [ + { listEl: this.elements.connectToTorLink, status: connectToTor }, + { listEl: this.elements.connectionAssistLink, status: connectionAssist }, + { listEl: this.elements.tryBridgeLink, status: tryBridge }, + ]) { + let hidden = false; + let disabled = false; + let active = false; + switch (status) { + case BreadcrumbStatus.Hidden: + hidden = true; + break; + case BreadcrumbStatus.Disabled: + disabled = true; + break; + case BreadcrumbStatus.Active: + active = true; + break; } - separator?.classList.toggle("hidden", status === BreadcrumbStatus.Hidden); - }); + // NOTE: Hiding or disabling elements can cause them to loose focus. + // However, with each stage change the focus should already be moved. + listEl.hidden = hidden; + const linkEl = listEl.querySelector(".breadcrumb-item"); + linkEl.classList.toggle("breadcrumb-disabled", disabled); + linkEl.classList.toggle("breadcrumb-active", active); + if (disabled) { + linkEl.removeAttribute("tabindex"); + linkEl.setAttribute("aria-disabled", "true"); + } else { + linkEl.setAttribute("tabindex", "0"); + linkEl.removeAttribute("aria-disabled", "true"); + } + if (active) { + linkEl.setAttribute("aria-current", "step"); + } else { + linkEl.removeAttribute("aria-current"); + } + } } hideBreadcrumbs() { - this.elements.breadcrumbContainer.classList.add("hidden"); + this.elements.breadcrumbContainer.classList.add("hide-breadcrumbs"); } getLocalizedStatus(status) { @@ -432,7 +436,11 @@ class AboutTorConnect { break; case "Bootstrapping": showProgress = true; - this.showBootstrapping(stage.bootstrapTrigger, stage.tryAgain); + this.showBootstrapping( + stage.bootstrapTrigger, + stage.tryAgain, + stage.potentiallyBlocked + ); // Always focus the cancel button. moveFocus = this.elements.cancelButton; break; @@ -637,10 +645,13 @@ class AboutTorConnect { } updateBootstrappingStatus(data) { + let { progress } = data; + progress = Math.max(0, Math.min(progress, 100)); this.elements.progressMeter.style.setProperty( "--progress-percent", - `${data.progress}%` + `${progress}%` ); + this.elements.progressMeter.setAttribute("aria-valuenow", String(progress)); if (this.shownStage === "Bootstrapping" && data.hasWarning) { // When bootstrapping starts, we hide the log button, but we re-show it if // we get a warning. @@ -676,16 +687,18 @@ class AboutTorConnect { BreadcrumbStatus.Default, BreadcrumbStatus.Disabled ); + } else { + this.hideBreadcrumbs(); } } - showBootstrapping(trigger, tryAgain) { + showBootstrapping(trigger, tryAgain, potentiallyBlocked) { let title = ""; let description = ""; const breadcrumbs = [ - BreadcrumbStatus.Disabled, - BreadcrumbStatus.Disabled, - BreadcrumbStatus.Disabled, + BreadcrumbStatus.Default, + potentiallyBlocked ? BreadcrumbStatus.Default : BreadcrumbStatus.Hidden, + potentiallyBlocked ? BreadcrumbStatus.Disabled : BreadcrumbStatus.Hidden, ]; switch (trigger) { case "Start": @@ -718,7 +731,7 @@ class AboutTorConnect { this.setTitle(title, ""); this.showConfigureConnectionLink(description); this.elements.progressDescription.textContent = ""; - if (tryAgain) { + if (tryAgain || potentiallyBlocked) { this.setBreadcrumbsStatus(...breadcrumbs); } else { this.hideBreadcrumbs(); @@ -780,8 +793,8 @@ class AboutTorConnect { this.getMaybeLocalizedError(error); this.setBreadcrumbsStatus( BreadcrumbStatus.Default, - BreadcrumbStatus.Default, - BreadcrumbStatus.Active + BreadcrumbStatus.Active, + BreadcrumbStatus.Disabled ); this.showLocationForm(false, TorStrings.torConnect.tryAgain); } @@ -805,7 +818,8 @@ class AboutTorConnect { const pieces = text.split("%S"); const link = document.createElement("a"); link.textContent = TorStrings.torConnect.configureConnection; - link.setAttribute("href", "#"); + link.setAttribute("href", "about:preferences#connection"); + link.setAttribute("target", "_blank"); link.addEventListener("click", e => { e.preventDefault(); RPMSendAsyncMessage("torconnect:open-tor-preferences"); @@ -918,7 +932,7 @@ class AboutTorConnect { this.populateDelayedRegionOptions(); this.show(this.elements.locationDropdownLabel); - this.show(this.elements.locationDropdown); + this.show(this.elements.locationDropdownSelect); this.elements.locationDropdownLabel.classList.toggle("error", !isChoose); this.show(this.elements.tryBridgeButton, true); if (buttonLabel !== undefined) { @@ -929,24 +943,38 @@ class AboutTorConnect { initElements(direction) { document.documentElement.setAttribute("dir", direction); - this.elements.connectToTorLink.addEventListener("click", () => { - RPMSendAsyncMessage("torconnect:start-again"); - }); + for (const [linkEl, command] of [ + [ + this.elements.connectToTorLink, + () => { + RPMSendAsyncMessage("torconnect:start-again"); + }, + ], + [ + this.elements.connectionAssistLink, + () => { + RPMSendAsyncMessage("torconnect:choose-region"); + }, + ], + // Final link does nothing. + ]) { + // NOTE: These link should never be both visible and disabled. + // NOTE: We allow re-selecting this link when it is already the + // "aria-current" item, to allow the user to reset to the beginning of + // Connection Assist. + linkEl.addEventListener("click", () => { + command(); + }); + linkEl.addEventListener("keydown", event => { + if (event.key !== "Enter") { + return; + } + command(); + }); + } + this.elements.connectToTorLabel.textContent = TorStrings.torConnect.torConnect; - this.elements.connectionAssistLink.addEventListener("click", () => { - if ( - this.elements.connectionAssistLink.classList.contains( - BreadcrumbStatus.Active - ) || - this.elements.connectionAssistLink.classList.contains( - BreadcrumbStatus.Disabled - ) - ) { - return; - } - RPMSendAsyncMessage("torconnect:choose-region"); - }); this.elements.connectionAssistLabel.textContent = TorStrings.torConnect.breadcrumbAssist; this.elements.tryBridgeLabel.textContent = @@ -1073,7 +1101,7 @@ class AboutTorConnect { RPMAddMessageListener("torconnect:region-names-change", () => { // Reset the regions list. this.regions = null; - if (!this.elements.locationDropdown.hidden) { + if (!this.elements.locationDropdownSelect.hidden) { // Re-populate immediately. this.populateDelayedRegionOptions(); } ===================================== toolkit/modules/TorConnect.sys.mjs ===================================== @@ -2,8 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs"; - const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -24,6 +22,10 @@ ChromeUtils.defineESModuleGetters(lazy, { "moz-src:///toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs", TorSettings: "moz-src:///toolkit/modules/TorSettings.sys.mjs", TorSettingsTopics: "moz-src:///toolkit/modules/TorSettings.sys.mjs", + clearInterval: "resource://gre/modules/Timer.sys.mjs", + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + setInterval: "resource://gre/modules/Timer.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "NetworkLinkService", () => { @@ -122,12 +124,17 @@ export const TorConnectTopics = Object.freeze({ * bootstrap. * @property {integer} [simulateDelay] - The delay in microseconds to apply to * simulated bootstraps. + * @property {number} [simulateProgress] - The progress percent to reach before + * the simulated bootstrap fails. * @property {MoatSettings} [simulateMoatResponse] - Simulate a Moat response * for circumvention settings. Should include a "bridgesList" property, and * optionally a "country" property. The "bridgesList" property should be an * Array of MoatBridges objects that match the bridge settings accepted by - * TorSettings.bridges, plus you may add a "simulateCensorship" property to - * make only their bootstrap attempts fail. + * TorSettings.bridges. For each MoatBridges object, you may also add a + * "simulateCensorship" property, with an optional "simulateProgress" + * property, to make the bootstrap attempt fail for this set of bridges + * (whilst still allowing it to pass for other sets of bridges in the + * simulated Moat response). * @property {string} [regionCode] - The region code to use to fetch * auto-bootstrap settings, or "automatic" to automatically choose the region. */ @@ -232,7 +239,7 @@ class BootstrapAttempt { this.#bootstrap = { _timeout: 0, bootstrap() { - this._timeout = setTimeout(() => { + this._timeout = lazy.setTimeout(() => { const err = new lazy.TorBootstrapError({ summary: "Censorship simulation", phase: "conn", @@ -240,9 +247,32 @@ class BootstrapAttempt { }); this.onbootstraperror(err); }, options.simulateDelay || 0); + this.onbootstrapstatus(0); + if (options.simulateDelay && options.simulateProgress) { + let progress = 0; + const step = 5; + const delay = Math.floor( + // We want the last step to last for 10 delay periods to emulate a + // short "hang" at this stage. + (step * options.simulateDelay) / + (options.simulateProgress + 10 * step) + ); + this._progress = lazy.setInterval(() => { + progress += step; + if (progress >= options.simulateProgress) { + progress = options.simulateProgress; + lazy.clearInterval(this._progress); + delete this._progress; + } + this.onbootstrapstatus(progress); + }, delay); + } }, cancel() { - clearTimeout(this._timeout); + lazy.clearTimeout(this._timeout); + if (this._progress) { + lazy.clearInterval(this._progress); + } }, }; } else { @@ -458,6 +488,7 @@ class AutoBootstrapAttempt { : TorConnectError.NoSettingsForCountry ), }); + return; } // Apply each of our settings and try to bootstrap with each. @@ -489,7 +520,7 @@ class AutoBootstrapAttempt { async #fetchBridges(options) { if (options.simulateMoatResponse) { await Promise.race([ - new Promise(res => setTimeout(res, options.simulateDelay || 0)), + new Promise(res => lazy.setTimeout(res, options.simulateDelay || 0)), this.#cancelledPromise, ]); @@ -588,8 +619,13 @@ class AutoBootstrapAttempt { // Move the simulateCensorship option to the options for the next // BootstrapAttempt. bridges = structuredClone(bridges); + const simulateProgress = bridges.simulateProgress; delete bridges.simulateCensorship; + delete bridges.simulateProgress; options = { ...options, simulateCensorship: true }; + if (simulateProgress) { + options.simulateProgress = simulateProgress; + } } // Send the new settings directly to the provider. We will save them only @@ -1271,6 +1307,10 @@ export const TorConnect = { bootstrapOptions.simulateDelay = this.simulateBootstrapOptions.simulateDelay; } + if (this.simulateBootstrapOptions.simulateProgress) { + bootstrapOptions.simulateProgress = + this.simulateBootstrapOptions.simulateProgress; + } if (this.simulateBootstrapOptions.simulateMoatResponse) { bootstrapOptions.simulateMoatResponse = this.simulateBootstrapOptions.simulateMoatResponse; @@ -1280,8 +1320,9 @@ export const TorConnect = { TorConnectPrefs.censorship_level, 0 ); - if (censorshipLevel > 0 && !bootstrapOptions.simulateDelay) { - bootstrapOptions.simulateDelay = 1500; + if (censorshipLevel > 0) { + bootstrapOptions.simulateDelay ||= 1500; + bootstrapOptions.simulateProgress ||= 20; } if (censorshipLevel === 1) { // Bootstrap fails, but auto-bootstrap does not. View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/17053ff... -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/17053ff... You're receiving this email because of your account on gitlab.torproject.org. Manage all notifications: https://gitlab.torproject.org/-/profile/notifications | Help: https://gitlab.torproject.org/help
participants (1)
-
morgan (@morgan)