tor-commits
Threads by month
- ----- 2026 -----
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2025 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- 1 participants
- 215563 discussions
[Git][tpo/applications/tor-browser][tor-browser-153.0esr-16.0-1] fixup! BB 43864: Modify the urlbar for Base Browser.
by morgan (@morgan) 04 Aug '26
by morgan (@morgan) 04 Aug '26
04 Aug '26
morgan pushed to branch tor-browser-153.0esr-16.0-1 at The Tor Project / Applications / Tor Browser
Commits:
a502c2d2 by Henry Wilkes at 2026-08-04T13:48:31+00:00
fixup! BB 43864: Modify the urlbar for Base Browser.
BB 45168: Hide the history search option.
- - - - -
1 changed file:
- browser/components/urlbar/content/SearchModeSwitcher.mjs
Changes:
=====================================
browser/components/urlbar/content/SearchModeSwitcher.mjs
=====================================
@@ -456,10 +456,20 @@ export class SearchModeSwitcher {
// search modes. Hence when the settings redesign is enabled we show
// all local search modes regardless of the prefs.
this.#engines = searchEngines.concat(
- lazy.UrlbarUtils.LOCAL_SEARCH_MODES.filter(
- engine =>
+ lazy.UrlbarUtils.LOCAL_SEARCH_MODES.filter(engine => {
+ // Do not show the search history option in PBM. tor-browser#43864.
+ // Although, it can still be triggered with "^" restrict keyword or
+ // through an app menu item. See also mozilla bug 1980928.
+ if (
+ engine.source === lazy.UrlbarUtils.RESULT_SOURCE.HISTORY &&
+ lazy.PrivateBrowsingUtils.permanentPrivateBrowsing
+ ) {
+ return false;
+ }
+ return (
lazy.settingsRedesignEnabled || lazy.UrlbarPrefs.get(engine.pref)
- )
+ );
+ })
);
}
}
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/a502c2d…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/a502c2d…
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
1
0
[Git][tpo/applications/tor-browser][tor-browser-153.0esr-16.0-1] 6 commits: fixup! TB 31286: Implementation of bridge, proxy, and firewall settings in...
by morgan (@morgan) 04 Aug '26
by morgan (@morgan) 04 Aug '26
04 Aug '26
morgan pushed to branch tor-browser-153.0esr-16.0-1 at The Tor Project / Applications / Tor Browser
Commits:
0b37b3e9 by Henry Wilkes at 2026-08-04T13:23:04+00:00
fixup! TB 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
TB 45058: Copy code from content/connectionPane.js to
widgets/tor-bridges-display.mjs.
Copy markup within connectionPane.inc.xhtml.
- - - - -
550bff23 by Henry Wilkes at 2026-08-04T13:23:04+00:00
fixup! TB 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
TB 45058: Convert bridge settings to use config.
- - - - -
5088e918 by Henry Wilkes at 2026-08-04T13:23:04+00:00
fixup! TB 40597: Implement TorSettings module
TB 45058: Clean up TorSettings double init logic.
- - - - -
1ab514d3 by Henry Wilkes at 2026-08-04T13:23:04+00:00
fixup! Tor Browser strings
TB 45058: Combine bridge setting strings together.
- - - - -
d064e421 by Henry Wilkes at 2026-08-04T13:23:04+00:00
fixup! Tor Browser localization migration scripts.
TB 45058: Combine bridge setting strings together.
- - - - -
2a72140f by Henry Wilkes at 2026-08-04T13:23:04+00:00
TB 45143: Modify moz-fieldset.
- - - - -
13 changed files:
- browser/components/preferences/preferences.js
- browser/components/preferences/preferences.xhtml
- browser/components/torpreferences/config/connection.mjs
- + browser/components/torpreferences/config/helpers.mjs
- browser/components/torpreferences/content/connectionPane.inc.xhtml
- browser/components/torpreferences/content/torPreferences.css
- browser/components/torpreferences/jar.mn
- + browser/components/torpreferences/widgets/tor-bridges-display.mjs
- toolkit/content/widgets/moz-fieldset/moz-fieldset.css
- toolkit/content/widgets/moz-fieldset/moz-fieldset.mjs
- toolkit/locales/en-US/toolkit/global/tor-browser.ftl
- toolkit/modules/TorSettings.sys.mjs
- + tools/torbrowser/l10n/migrations/bug-45058-bridge-settings.py
Changes:
=====================================
browser/components/preferences/preferences.js
=====================================
@@ -271,7 +271,7 @@ const CONFIG_PANES = Object.freeze({
connection: {
l10nId: "tor-connection-settings-pane",
iconSrc: "chrome://browser/content/torconnect/tor-connect.svg",
- groupIds: ["connectionStatus"],
+ groupIds: ["connectionStatus", "torBridges"],
module: "chrome://browser/content/torpreferences/config/connection.mjs",
visible: () => {
return TorConnect.enabled;
=====================================
browser/components/preferences/preferences.xhtml
=====================================
@@ -108,6 +108,7 @@
<script type="module" src="chrome://browser/content/preferences/widgets/update-information.mjs"></script>
<script type="module" src="chrome://browser/content/preferences/widgets/update-state.mjs"></script>
<script type="module" src="chrome://browser/content/ipprotection/bandwidth-usage.mjs"></script>
+ <script type="module" src="chrome://browser/content/torpreferences/widgets/tor-bridges-display.mjs"></script>
<script type="module" src="chrome://browser/content/torpreferences/widgets/tor-connection-status.mjs"></script>
<script src="chrome://browser/content/torpreferences/bridgemoji/BridgeEmoji.js"/>
</head>
=====================================
browser/components/torpreferences/config/connection.mjs
=====================================
@@ -4,11 +4,33 @@ import { Preferences } from "chrome://global/content/preferences/Preferences.mjs
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
InternetStatus: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
+ openBridgeDialog:
+ "chrome://browser/content/torpreferences/config/helpers.mjs",
+ openUserProvideBridgeDialog:
+ "chrome://browser/content/torpreferences/config/helpers.mjs",
+ TorBridgeSource: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
TorConnect: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
TorConnectStage: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
TorConnectTopics: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
+ TorProviderBuilder:
+ "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
+ TorProviderState:
+ "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
+ TorProviderTopics:
+ "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
+ TorSettings: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
+ TorSettingsTopics: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
});
+// TODO: Change to GetLoxBridges if Lox enabled, and the account is set up.
+const TELEGRAM_USER_NAME = "GetBridgesBot";
+const TELEGRAM_HREF = `https://t.me/${TELEGRAM_USER_NAME}`;
+
+const TOR_BRIDGES_URL_NAME = "bridges.torproject.org";
+const TOR_BRIDGES_HREF = "https://bridges.torproject.org";
+
+const TOR_BRIDGES_EMAIL = "bridges(a)torproject.org";
+
SettingGroupManager.registerGroups({
connectionStatus: {
inProgress: true,
@@ -39,6 +61,123 @@ SettingGroupManager.registerGroups({
},
],
},
+ torBridges: {
+ inProgress: true,
+ l10nId: "tor-bridges-group",
+ supportPage: "tor-manual:bridges",
+ headingLevel: 2,
+ controlAttrs: { "focusable-heading": true },
+ items: [
+ {
+ id: "bridgesEnabled",
+ l10nId: "tor-bridges-use-bridges",
+ control: "moz-toggle",
+ },
+ {
+ id: "torBridgesDisplay",
+ control: "tor-bridges-display",
+ },
+ {
+ id: "newBridgesGroup",
+ control: "moz-fieldset",
+ controlAttrs: {
+ headinglevel: 3,
+ },
+ options: [
+ {
+ control: "moz-box-group",
+ items: [
+ {
+ id: "builtinBridges",
+ l10nId: "tor-bridges-choose-built-in-button",
+ control: "moz-box-button",
+ },
+ {
+ id: "userProvidedBridges",
+ l10nId: "tor-bridges-enter-bridges-button",
+ control: "moz-box-button",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: "findMoreBridgesGroup",
+ l10nId: "tor-bridges-find-more-group",
+ control: "moz-fieldset",
+ controlAttrs: {
+ headinglevel: 3,
+ },
+ options: [
+ {
+ control: "moz-box-item",
+ options: [
+ {
+ id: "torBridgesRequestBanner",
+ control: "article",
+ options: [
+ {
+ control: "img",
+ controlAttrs: {
+ alt: "",
+ src: "chrome://browser/content/torpreferences/bridge-bot.svg",
+ },
+ },
+ {
+ control: "p",
+ l10nId: "tor-bridges-request-from-browser2",
+ },
+ {
+ // NOTE: We use the wrapping `div` to simply switch from the
+ // `options` context to the `items` context, with the latter
+ // wrapping the elements in a setting-control.
+ control: "div",
+ items: [
+ {
+ id: "requestBridges",
+ l10nId: "tor-bridges-request-button2",
+ control: "moz-button",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ control: "moz-box-group",
+ options: [
+ {
+ l10nId: "tor-bridges-source-telegram-link",
+ l10nArgs: { telegramUserName: TELEGRAM_USER_NAME },
+ control: "moz-box-link",
+ iconSrc:
+ "chrome://browser/content/torpreferences/telegram-logo.svg",
+ controlAttrs: {
+ href: TELEGRAM_HREF,
+ },
+ },
+ {
+ l10nId: "tor-bridges-source-web-link",
+ l10nArgs: { url: TOR_BRIDGES_URL_NAME },
+ control: "moz-box-link",
+ iconSrc: "chrome://browser/content/torconnect/network.svg",
+ controlAttrs: {
+ href: TOR_BRIDGES_HREF,
+ },
+ },
+ {
+ l10nId: "tor-bridges-source-email-link",
+ l10nArgs: { address: TOR_BRIDGES_EMAIL },
+ control: "moz-box-item",
+ iconSrc: "chrome://browser/content/torpreferences/mail.svg",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
});
Preferences.addSetting({
@@ -113,3 +252,277 @@ Preferences.addSetting({
lazy.TorConnect.quickstart = val;
},
});
+
+Preferences.addSetting({
+ id: "torSettingsReady",
+ _ready: false,
+ setup(emitChange) {
+ if (!lazy.TorSettings.enabled) {
+ // Remain in `false`.
+ return;
+ }
+ // Most likely, TorSettings will already be initialised.
+ if (lazy.TorSettings.initialized) {
+ this._ready = true;
+ return;
+ }
+ // Else, wait for it to be initialised.
+ lazy.TorSettings.initializedPromise.then(
+ () => {
+ this._ready = true;
+ emitChange();
+ },
+ error => {
+ // No change in state.
+ console.error("TorSettings failed to initialize.", error);
+ }
+ );
+ },
+ get() {
+ return this._ready;
+ },
+});
+
+Preferences.addSetting({
+ id: "torBridgesRaw",
+ deps: ["torSettingsReady"],
+ _value: null,
+ setup(emitChange) {
+ const observer = subject => {
+ const { changes } = subject.wrappedJSObject;
+ // NOTE: We do not include "bridges.lox_id" in the changes. Instead, any
+ // widgets should wait for LoxTopics.UpdateActiveLoxId to ensure that the
+ // Lox module has responded to the change in ID strictly *before* we do.
+ // In particular, we want to make sure the invites and event data has been
+ // cleared.
+ if (
+ changes.includes("bridges.source") ||
+ changes.includes("bridges.bridge_strings") ||
+ changes.includes("bridges.builtin_type")
+ ) {
+ // Reset.
+ this._value = null;
+ emitChange();
+ }
+ };
+ Services.obs.addObserver(observer, lazy.TorSettingsTopics.SettingsChanged);
+ return () => {
+ Services.obs.removeObserver(
+ observer,
+ lazy.TorSettingsTopics.SettingsChanged
+ );
+ };
+ },
+ get(_pref, { torSettingsReady }) {
+ if (this._value === null) {
+ if (!torSettingsReady.value) {
+ // TorSettings getter will throw.
+ return null;
+ }
+ const source = lazy.TorSettings.bridges.source;
+ // Cache a value.
+ this._value = {
+ haveBridges: source !== lazy.TorBridgeSource.Invalid,
+ source,
+ builtinType: lazy.TorSettings.bridges.builtin_type,
+ bridgeStrings: lazy.TorSettings.bridges.bridge_strings,
+ };
+ }
+ return this._value;
+ },
+});
+
+Preferences.addSetting({
+ id: "connectedBridgeId",
+ _value: null,
+ setup(emitChange) {
+ const observer = async () => {
+ // NOTE: It should be safe for this method to be called concurrently.
+ let bridge = null;
+ try {
+ if (
+ lazy.TorProviderBuilder.currentState() ===
+ lazy.TorProviderState.Running
+ ) {
+ bridge = (await lazy.TorProviderBuilder.build()).currentBridge;
+ }
+ // Else, bridge is `null` whilst the provider is not running.
+ } catch (e) {
+ console.warn("Could not get current bridge", e);
+ }
+ const prevVal = this._value;
+ this._value = bridge?.fingerprint ?? null;
+ if (prevVal !== this._value) {
+ emitChange();
+ }
+ };
+ Services.obs.addObserver(observer, lazy.TorProviderTopics.BridgeChanged);
+ // NOTE: BridgeChanged is only fired directly by the provider instances,
+ // rather than by TorProviderBuilder. In particular, it will not fire when
+ // the previous provider had a bridge and the new one does not, because
+ // neither provider saw a change in their own bridge. But from the user's
+ // point of view, the overall change would mean that the current bridge has
+ // changed.
+ // Moreover, we want to show no connected bridge whilst we are missing a
+ // provider. Therefore, we also need to listen for a change in provider and
+ // its state.
+ // TODO: Maybe this logic should be moved to TorProviderBuilder itself if it
+ // is ever needed by other parts of the UI.
+ Services.obs.addObserver(
+ observer,
+ lazy.TorProviderTopics.ProviderStateChanged
+ );
+ // Get the initial value.
+ observer();
+
+ return () => {
+ Services.obs.removeObserver(
+ observer,
+ lazy.TorProviderTopics.ProviderStateChanged
+ );
+ Services.obs.removeObserver(
+ observer,
+ lazy.TorProviderTopics.BridgeChanged
+ );
+ };
+ },
+ get() {
+ return this._value;
+ },
+});
+
+Preferences.addSetting({
+ id: "bridgesEnabled",
+ deps: ["torSettingsReady", "torBridgesRaw"],
+ setup(emitChange) {
+ const observer = subject => {
+ const { changes } = subject.wrappedJSObject;
+ if (changes.includes("bridges.enabled")) {
+ emitChange();
+ }
+ };
+ Services.obs.addObserver(observer, lazy.TorSettingsTopics.SettingsChanged);
+ return () => {
+ Services.obs.removeObserver(
+ observer,
+ lazy.TorSettingsTopics.SettingsChanged
+ );
+ };
+ },
+ get(_prefVal, { torSettingsReady }) {
+ if (!torSettingsReady.value) {
+ // TorSettings.bridges will throw before TorSettings has finished
+ // initialisation.
+ return false;
+ }
+ return lazy.TorSettings.bridges.enabled;
+ },
+ set(val) {
+ lazy.TorSettings.changeSettings({
+ bridges: { enabled: val },
+ });
+ },
+ visible({ torSettingsReady }) {
+ return torSettingsReady.value;
+ },
+ disabled({ torBridgesRaw }) {
+ return !torBridgesRaw.value?.haveBridges;
+ },
+});
+
+Preferences.addSetting({
+ id: "torBridgesDisplay",
+ deps: ["torSettingsReady", "torBridgesRaw", "connectedBridgeId"],
+ getControlConfig(config, { torBridgesRaw, connectedBridgeId }) {
+ config.controlAttrs = {
+ ...config.controlAttrs,
+ // Set the `bridges` and `connnectedBridgeId` object *properties* (rather
+ // than attributes) by using the `.` prefix.
+ ".bridges": torBridgesRaw.value,
+ ".connectedBridgeId": connectedBridgeId.value,
+ };
+ return config;
+ },
+ visible({ torSettingsReady }) {
+ return torSettingsReady.value;
+ },
+});
+
+Preferences.addSetting({
+ id: "newBridgesGroup",
+ deps: ["torSettingsReady", "torBridgesRaw"],
+ getControlConfig(config, { torBridgesRaw }) {
+ config.l10nId = torBridgesRaw.value?.haveBridges
+ ? "tor-bridges-replace-bridges-group"
+ : "tor-bridges-add-bridges-group";
+ return config;
+ },
+ visible({ torSettingsReady }) {
+ return torSettingsReady.value;
+ },
+});
+
+Preferences.addSetting({
+ id: "builtinBridges",
+ onUserClick() {
+ lazy.openBridgeDialog(
+ window,
+ "chrome://browser/content/torpreferences/builtinBridgeDialog.xhtml",
+ null,
+ result => {
+ if (!result.type) {
+ return null;
+ }
+ return lazy.TorSettings.changeSettings({
+ bridges: {
+ enabled: true,
+ source: lazy.TorBridgeSource.BuiltIn,
+ builtin_type: result.type,
+ },
+ });
+ }
+ );
+ },
+});
+
+Preferences.addSetting({
+ id: "userProvidedBridges",
+ deps: ["torBridgesRaw"],
+ onUserClick(_event, { torBridgesRaw }) {
+ lazy.openUserProvideBridgeDialog(
+ window,
+ torBridgesRaw.value?.haveBridges ? "replace" : "add"
+ );
+ },
+});
+
+Preferences.addSetting({
+ id: "findMoreBridgesGroup",
+ deps: ["torSettingsReady"],
+ visible({ torSettingsReady }) {
+ return torSettingsReady.value;
+ },
+});
+
+Preferences.addSetting({
+ id: "requestBridges",
+ onUserClick() {
+ lazy.openBridgeDialog(
+ window,
+ "chrome://browser/content/torpreferences/requestBridgeDialog.xhtml",
+ null,
+ result => {
+ if (!result.bridges?.length) {
+ return null;
+ }
+ return lazy.TorSettings.changeSettings({
+ bridges: {
+ enabled: true,
+ source: lazy.TorBridgeSource.BridgeDB,
+ bridge_strings: result.bridges,
+ },
+ });
+ }
+ );
+ },
+});
=====================================
browser/components/torpreferences/config/helpers.mjs
=====================================
@@ -0,0 +1,125 @@
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ TorBridgeSource: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
+ TorConnectParent:
+ "moz-src:///browser/components/torconnect/TorConnectParent.sys.mjs",
+ TorSettings: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
+});
+
+/**
+ * Force the focus to move to the bridge heading.
+ *
+ * @param {Window} win - The preferences window.
+ * @param {boolean} [forceTopHeading=false] - Force the focus to move to the
+ * top "Bridges" setting heading.
+ */
+export function moveFocusToBridgeHeading(win, forceTopHeading = false) {
+ // Move focus to the start of the relevant section, which is a heading.
+ // They have tabindex="-1" so should be focusable, even though they are not
+ // part of the usual tab navigation.
+ // TODO: It might be better if we could use the # named anchor to
+ // re-orient the screen reader position instead of using tabIndex=-1, but
+ // about:preferences currently uses the anchor for showing categories
+ // only. See bugzilla bug 1799153.
+ if (
+ forceTopHeading ||
+ !win.document.getElementById("torBridgesDisplay").focusHeading()
+ ) {
+ win.document
+ .querySelector('setting-group[groupid="torBridges"] moz-fieldset')
+ .focusHeading();
+ }
+}
+
+/**
+ * Open a bridge dialog that will change the users bridges.
+ *
+ * @param {Window} win - The preferences window.
+ * @param {string} url - The url of the dialog to open.
+ * @param {object?} inputData - The input data to send to the dialog window.
+ * @param {Function} onAccept - The method to call if the bridge dialog was
+ * accepted by the user. This will be passed a "result" object containing
+ * data set by the dialog. This should return a promise that resolves once
+ * the bridge settings have been set, or null if the settings have not
+ * been applied.
+ */
+export function openBridgeDialog(win, url, inputData, onAccept) {
+ const result = { accepted: false, connect: false };
+ let savedSettings = null;
+ win.gSubDialog.open(
+ url,
+ {
+ features: "resizable=yes",
+ closingCallback: () => {
+ if (!result.accepted) {
+ return;
+ }
+ savedSettings = onAccept(result);
+ if (!savedSettings) {
+ // No change in settings.
+ return;
+ }
+ if (!result.connect) {
+ // Do not open about:torconnect.
+ return;
+ }
+
+ // Wait until the settings are applied before bootstrapping.
+ // NOTE: Saving the settings should also cancel any existing bootstrap
+ // attempt first. See tor-browser#41921.
+ savedSettings.then(() => {
+ // The bridge dialog button is "connect" when Tor is not
+ // bootstrapped, so do the connect.
+
+ // Start Bootstrapping, which should use the configured bridges.
+ // NOTE: We do this regardless of any previous TorConnect Error.
+ lazy.TorConnectParent.open({ beginBootstrapping: "hard" });
+ });
+ },
+ // closedCallback should be called after gSubDialog has already
+ // re-assigned focus back to the document.
+ closedCallback: () => {
+ if (!savedSettings) {
+ return;
+ }
+ // Wait until the settings have changed, so that the UI could
+ // respond, then move focus.
+ savedSettings.then(() => {
+ moveFocusToBridgeHeading(win);
+ });
+ },
+ },
+ result,
+ inputData
+ );
+}
+
+/**
+ * Open the user provide dialog.
+ *
+ * @param {Window} win - The preferences window.
+ * @param {string} mode - The mode to open the dialog in: "add", "replace" or
+ * "edit".
+ */
+export function openUserProvideBridgeDialog(win, mode) {
+ openBridgeDialog(
+ win,
+ "chrome://browser/content/torpreferences/provideBridgeDialog.xhtml",
+ { mode },
+ result => {
+ const loxId = result.loxId;
+ if (!loxId && !result.addresses?.length) {
+ return null;
+ }
+ const bridges = { enabled: true };
+ if (loxId) {
+ bridges.source = lazy.TorBridgeSource.Lox;
+ bridges.lox_id = loxId;
+ } else {
+ bridges.source = lazy.TorBridgeSource.UserProvided;
+ bridges.bridge_strings = result.addresses;
+ }
+ return lazy.TorSettings.changeSettings({ bridges });
+ }
+ );
+}
=====================================
browser/components/torpreferences/content/connectionPane.inc.xhtml
=====================================
@@ -1,3 +1,251 @@
+<html:template id="tor-bridges-display-template">
+ <html:div id="tor-bridges-none" hidden="hidden">
+ <html:img id="tor-bridges-none-icon" alt="" />
+ <html:p data-l10n-id="tor-bridges-none-added"></html:p>
+ </html:div>
+ <html:fieldset id="tor-bridges-current" hidden="hidden">
+ <html:legend>
+ <html:h4
+ id="tor-bridges-current-heading-non-search"
+ class="tor-bridges-current-heading tor-focusable-heading tor-small-heading"
+ tabindex="-1"
+ data-l10n-id="tor-bridges-your-bridges"
+ ></html:h4>
+ </html:legend>
+ <html:p
+ id="tor-bridges-user-label"
+ class="tor-bridges-source-label"
+ data-l10n-id="tor-bridges-source-user"
+ ></html:p>
+ <html:p
+ id="tor-bridges-built-in-label"
+ class="tor-bridges-source-label"
+ data-l10n-id="tor-bridges-source-built-in"
+ ></html:p>
+ <html:p
+ id="tor-bridges-requested-label"
+ class="tor-bridges-source-label"
+ data-l10n-id="tor-bridges-source-requested"
+ ></html:p>
+ <html:p id="tor-bridges-lox-label" class="tor-bridges-source-label">
+ <html:img id="tor-bridges-lox-label-icon" alt="" />
+ <html:span data-l10n-id="tor-bridges-source-lox"></html:span>
+ </html:p>
+ <html:button
+ id="tor-bridges-all-options-button"
+ class="tor-bridges-options-button"
+ aria-haspopup="menu"
+ aria-expanded="false"
+ aria-controls="tor-bridges-all-options-menu"
+ data-l10n-id="tor-bridges-options-button"
+ ></html:button>
+ <html:panel-list
+ id="tor-bridges-all-options-menu"
+ data-hidden-from-search="true"
+ >
+ <html:panel-item
+ id="tor-bridges-options-qr-all-menu-item"
+ data-l10n-attrs="accesskey"
+ data-l10n-id="tor-bridges-menu-item-qr-all-bridge-addresses"
+ ></html:panel-item>
+ <html:panel-item
+ id="tor-bridges-options-copy-all-menu-item"
+ data-l10n-attrs="accesskey"
+ data-l10n-id="tor-bridges-menu-item-copy-all-bridge-addresses"
+ ></html:panel-item>
+ <html:panel-item
+ id="tor-bridges-options-edit-all-menu-item"
+ data-l10n-attrs="accesskey"
+ data-l10n-id="tor-bridges-menu-item-edit-all-bridges"
+ ></html:panel-item>
+ <html:panel-item
+ id="tor-bridges-options-remove-all-menu-item"
+ data-l10n-attrs="accesskey"
+ data-l10n-id="tor-bridges-menu-item-remove-all-bridges"
+ ></html:panel-item>
+ </html:panel-list>
+ <html:div id="tor-bridges-built-in-display" hidden="hidden">
+ <html:p id="tor-bridges-built-in-type-name"></html:p>
+ <html:p id="tor-bridges-built-in-connected" class="bridge-status-badge">
+ <html:div class="bridge-status-icon"></html:div>
+ <html:span
+ data-l10n-id="tor-bridges-built-in-status-connected"
+ ></html:span>
+ </html:p>
+ <html:p id="tor-bridges-built-in-description"></html:p>
+ </html:div>
+ <html:div
+ id="tor-bridges-grid-display"
+ class="tor-bridges-grid"
+ role="grid"
+ aria-labelledby="tor-bridges-current-heading-non-search"
+ hidden="hidden"
+ ></html:div>
+ <html:template id="tor-bridges-grid-row-template">
+ <html:div class="tor-bridges-grid-row" role="row">
+ <!-- TODO: lox status cell for new bridges? -->
+ <html:span
+ class="tor-bridges-type-cell tor-bridges-grid-cell"
+ role="gridcell"
+ ></html:span>
+ <html:span class="tor-bridges-emojis-block" role="none"></html:span>
+ <html:span class="tor-bridges-grid-end-block" role="none">
+ <html:span
+ class="tor-bridges-address-cell tor-bridges-grid-cell"
+ role="gridcell"
+ >
+ <html:span class="tor-bridges-address-cell-text"></html:span>
+ </html:span>
+ <html:span
+ class="tor-bridges-status-cell tor-bridges-grid-cell"
+ role="gridcell"
+ >
+ <html:div class="bridge-status-badge">
+ <html:div class="bridge-status-icon"></html:div>
+ <html:span class="tor-bridges-status-cell-text"></html:span>
+ </html:div>
+ </html:span>
+ <html:span
+ class="tor-bridges-options-cell tor-bridges-grid-cell"
+ role="gridcell"
+ >
+ <html:button
+ class="tor-bridges-options-cell-button tor-bridges-options-button tor-bridges-grid-focus"
+ aria-haspopup="menu"
+ aria-expanded="false"
+ data-l10n-id="tor-bridges-individual-bridge-options-button"
+ ></html:button>
+ <html:panel-list
+ class="tor-bridges-individual-options-menu"
+ data-hidden-from-search="true"
+ >
+ <html:panel-item
+ class="tor-bridges-options-qr-one-menu-item"
+ data-l10n-attrs="accesskey"
+ data-l10n-id="tor-bridges-menu-item-qr-address"
+ ></html:panel-item>
+ <html:panel-item
+ class="tor-bridges-options-copy-one-menu-item"
+ data-l10n-attrs="accesskey"
+ data-l10n-id="tor-bridges-menu-item-copy-address"
+ ></html:panel-item>
+ <html:panel-item
+ class="tor-bridges-options-remove-one-menu-item"
+ data-l10n-attrs="accesskey"
+ data-l10n-id="tor-bridges-menu-item-remove-bridge"
+ ></html:panel-item>
+ </html:panel-list>
+ </html:span>
+ </html:span>
+ </html:div>
+ </html:template>
+ <html:div
+ id="tor-bridges-share"
+ class="tor-bridges-details-box"
+ hidden="hidden"
+ >
+ <html:h5
+ class="tor-bridges-share-heading tor-small-heading"
+ data-l10n-id="tor-bridges-share-heading"
+ ></html:h5>
+ <html:p
+ id="tor-bridges-share-description"
+ data-l10n-id="tor-bridges-share-description"
+ ></html:p>
+ <html:button
+ id="tor-bridges-copy-addresses-button"
+ data-l10n-id="tor-bridges-copy-addresses-button"
+ ></html:button>
+ <html:button
+ id="tor-bridges-qr-addresses-button"
+ data-l10n-id="tor-bridges-qr-addresses-button"
+ ></html:button>
+ </html:div>
+ <html:div id="tor-bridges-lox-status" hidden="hidden">
+ <html:div data-l10n-id="tor-bridges-lox-description"></html:div>
+ <html:div
+ id="tor-bridges-lox-details"
+ class="tor-bridges-details-box tor-bridges-lox-box"
+ hidden="hidden"
+ >
+ <html:img alt="" class="tor-bridges-lox-image-inner" />
+ <html:img alt="" class="tor-bridges-lox-image-outer" />
+ <html:h5
+ class="tor-bridges-lox-next-unlock-counter tor-small-heading tor-bridges-lox-intro tor-focusable-heading"
+ tabindex="-1"
+ ></html:h5>
+ <html:ul class="tor-bridges-lox-list">
+ <html:li
+ id="tor-bridges-lox-next-unlock-gain-bridges"
+ class="tor-bridges-lox-list-item tor-bridges-lox-list-item-bridge"
+ data-l10n-id="tor-bridges-lox-unlock-two-bridges"
+ hidden="hidden"
+ ></html:li>
+ <html:li
+ id="tor-bridges-lox-next-unlock-first-invites"
+ class="tor-bridges-lox-list-item tor-bridges-lox-list-item-invite"
+ data-l10n-id="tor-bridges-lox-unlock-first-invites"
+ hidden="hidden"
+ ></html:li>
+ <html:li
+ id="tor-bridges-lox-next-unlock-more-invites"
+ class="tor-bridges-lox-list-item tor-bridges-lox-list-item-invite"
+ data-l10n-id="tor-bridges-lox-unlock-more-invites"
+ hidden="hidden"
+ ></html:li>
+ </html:ul>
+ <html:div
+ id="tor-bridges-lox-remaining-invites"
+ hidden="hidden"
+ ></html:div>
+ <html:button
+ id="tor-bridges-lox-show-invites-button"
+ class="tor-bridges-lox-button"
+ data-l10n-id="tor-bridges-lox-show-invites-button"
+ hidden="hidden"
+ ></html:button>
+ </html:div>
+ <html:div
+ id="tor-bridges-lox-unlock-alert"
+ role="alert"
+ class="tor-bridges-details-box tor-bridges-lox-box"
+ hidden="hidden"
+ >
+ <html:img alt="" class="tor-bridges-lox-image-inner" />
+ <html:img alt="" class="tor-bridges-lox-image-outer" />
+ <html:div
+ id="tor-bridge-unlock-alert-title"
+ class="tor-small-heading tor-bridges-lox-intro"
+ ></html:div>
+ <html:ul class="tor-bridges-lox-list">
+ <html:li
+ id="tor-bridges-lox-unlock-alert-gain-bridges"
+ class="tor-bridges-lox-list-item tor-bridges-lox-list-item-bridge"
+ data-l10n-id="tor-bridges-lox-gained-two-bridges"
+ hidden="hidden"
+ ></html:li>
+ <html:li
+ id="tor-bridges-lox-unlock-alert-new-bridges"
+ class="tor-bridges-lox-list-item tor-bridges-lox-list-item-bridge"
+ data-l10n-id="tor-bridges-lox-new-bridges"
+ hidden="hidden"
+ ></html:li>
+ <html:li
+ id="tor-bridges-lox-unlock-alert-invites"
+ class="tor-bridges-lox-list-item tor-bridges-lox-list-item-invite"
+ hidden="hidden"
+ ></html:li>
+ </html:ul>
+ <html:button
+ id="tor-bridges-lox-unlock-alert-button"
+ class="tor-bridges-lox-button"
+ data-l10n-id="tor-bridges-lox-got-it-button"
+ ></html:button>
+ </html:div>
+ </html:div>
+ </html:fieldset>
+</html:template>
+
<!-- Tor panel -->
<script
=====================================
browser/components/torpreferences/content/torPreferences.css
=====================================
@@ -201,20 +201,27 @@ button.spoof-button-disabled {
#tor-bridges-none,
#tor-bridges-current {
- margin-inline: 0;
- margin-block: var(--space-xxlarge);
+ @media not -moz-pref("browser.settings-redesign.enabled") {
+ margin-inline: 0;
+ margin-block: var(--space-xxlarge);
+ }
}
#tor-bridges-none:not([hidden]) {
display: grid;
justify-items: center;
text-align: center;
- padding-block: 64px;
+ padding-block: 54px;
padding-inline: var(--space-xxlarge);
- gap: var(--space-large);
+ gap: var(--space-small);
border-radius: var(--border-radius-small);
color: var(--text-color-deemphasized);
border: 2px dashed var(--border-color-deemphasized);
+
+ @media not -moz-pref("browser.settings-redesign.enabled") {
+ padding-block: 64px;
+ gap: var(--space-large);
+ }
}
#tor-bridges-none-icon {
@@ -228,9 +235,13 @@ button.spoof-button-disabled {
.tor-bridges-box,
.tor-bridges-details-box {
padding: var(--space-large);
- border-radius: var(--border-radius-small);
- background: var(--background-color-box-info);
+ border-radius: var(--border-radius-medium);
border: var(--border-width) solid var(--border-color);
+
+ @media not -moz-pref("browser.settings-redesign.enabled") {
+ border-radius: var(--border-radius-small);
+ background: var(--background-color-box-info);
+ }
}
@media not forced-colors {
@@ -250,6 +261,56 @@ button.spoof-button-disabled {
white-space: nowrap;
}
+tor-bridges-display {
+ display: block;
+ margin-block: var(--space-small);
+
+ &.has-tor-bridges {
+ display: grid;
+ grid-template:
+ "heading source button" min-content
+ "bridges bridges bridges" auto
+ "extra extra extra" auto
+ / max-content 1fr max-content;
+ align-items: center;
+ border: var(--card-border);
+ border-radius: var(--border-radius-medium);
+ background-color: var(--card-background-color);
+ padding: var(--space-large);
+ }
+
+ & p {
+ margin: 0;
+ }
+
+ & #tor-bridges-current:not([hidden]) {
+ /* Avoid the special display logic for <fieldset> and <legend>. */
+ display: contents;
+ }
+
+ & legend {
+ grid-area: heading;
+ white-space: nowrap;
+ }
+
+ & .tor-bridges-source-label {
+ white-space: nowrap;
+ padding-inline-end: var(--space-small);
+ border-inline-end: var(--border-width) solid var(--border-color);
+ }
+
+ & :is(#tor-bridges-built-in-display, #tor-bridges-grid-display) {
+ grid-area: bridges;
+ margin-block-start: var(--space-large);
+ border-block-start: var(--border-width) solid var(--border-color);
+ padding-block-start: var(--space-large);
+ }
+
+ & :is(#tor-bridges-share, #tor-bridges-lox-status) {
+ grid-area: extra;
+ }
+}
+
.tor-bridges-current-heading {
grid-area: heading;
}
@@ -681,6 +742,24 @@ button.spoof-button-disabled {
grid-column: 2 / 3;
}
+#torBridgesRequestBanner {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ text-wrap-style: balance;
+ gap: var(--space-large);
+
+ & > * {
+ margin: 0;
+ flex: 0 0 auto;
+ }
+
+ & > p {
+ max-width: var(--size-layout-medium);
+ }
+}
+
#tor-bridges-request-box {
/* Take up the full height in the container. */
align-self: stretch;
=====================================
browser/components/torpreferences/jar.mn
=====================================
@@ -25,6 +25,8 @@ browser.jar:
content/browser/torpreferences/torLogDialog.js (content/torLogDialog.js)
content/browser/torpreferences/torLogDialog.xhtml (content/torLogDialog.xhtml)
content/browser/torpreferences/config/connection.mjs (config/connection.mjs)
+ content/browser/torpreferences/config/helpers.mjs (config/helpers.mjs)
+ content/browser/torpreferences/widgets/tor-bridges-display.mjs (widgets/tor-bridges-display.mjs)
content/browser/torpreferences/widgets/tor-connection-status.mjs (widgets/tor-connection-status.mjs)
content/browser/torpreferences/widgets/tor-connection-status.css (widgets/tor-connection-status.css)
content/browser/torpreferences/connectionPane.js (content/connectionPane.js)
=====================================
browser/components/torpreferences/widgets/tor-bridges-display.mjs
=====================================
@@ -0,0 +1,1955 @@
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ Lox: "moz-src:///toolkit/components/lox/Lox.sys.mjs",
+ LoxTopics: "moz-src:///toolkit/components/lox/Lox.sys.mjs",
+ moveFocusToBridgeHeading:
+ "chrome://browser/content/torpreferences/config/helpers.mjs",
+ openUserProvideBridgeDialog:
+ "chrome://browser/content/torpreferences/config/helpers.mjs",
+ TorBridgeSource: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
+ TorParsers: "moz-src:///toolkit/components/tor-launcher/TorParsers.sys.mjs",
+ TorSettings: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
+});
+
+/**
+ * Show the bridge QR to the user.
+ *
+ * @param {string} bridgeString - The string to use in the QR.
+ */
+function showBridgeQr(bridgeString) {
+ window.gSubDialog.open(
+ "chrome://browser/content/torpreferences/bridgeQrDialog.xhtml",
+ { features: "resizable=yes" },
+ bridgeString
+ );
+}
+
+/**
+ * Post a new notification, replacing any existing one.
+ *
+ * @param {string} type - The notification type.
+ */
+async function postBridgeNotification(type) {
+ let updateId;
+ switch (type) {
+ case "removed-one":
+ updateId = "tor-bridges-update-removed-one-bridge";
+ break;
+ case "removed-all":
+ updateId = "tor-bridges-update-removed-all-bridges";
+ break;
+ case "changed":
+ default:
+ // Generic message for when bridges change.
+ updateId = "tor-bridges-update-changed-bridges";
+ break;
+ }
+ const bridgeDisplay = document.querySelector("tor-bridges-display");
+ const settingGroup = bridgeDisplay?.closest("setting-group");
+ if (!settingGroup) {
+ console.error("Missing a setting-group for a notification.");
+ return;
+ }
+ if (!settingGroup.checkVisibility()) {
+ // Only ping the user if the bridge settings are visible.
+ // NOTE: Most operations to change the bridges will occur within the
+ // connection settings. However, in principle the user could have multiple
+ // setting tabs open, or they may have the settings open whilst Connection
+ // Assist is setting their bridges.
+ return;
+ }
+ const [message] = await Promise.all([
+ document.l10n.formatValue(updateId),
+ // Wait at least a small amount of time to actually trigger ariaNotify.
+ // Otherwise Orca will ignore the notification when it almost coincides with
+ // a change in focus, which is normally the case.
+ new Promise(resolve => setTimeout(resolve, 500)),
+ ]);
+ bridgeDisplay.ariaNotify(message);
+}
+
+/**
+ * Controls the bridge grid.
+ */
+const gBridgeGrid = {
+ /**
+ * The grid element.
+ *
+ * @type {Element?}
+ */
+ _grid: null,
+ /**
+ * The template for creating new rows.
+ *
+ * @type {HTMLTemplateElement?}
+ */
+ _rowTemplate: null,
+
+ /**
+ * @typedef {object} BridgeGridRow
+ *
+ * @property {Element} element - The row element.
+ * @property {Element} optionsButton - The options button.
+ * @property {Element} menu - The options menupopup.
+ * @property {Element} statusEl - The bridge status element.
+ * @property {Element} statusText - The status text.
+ * @property {string} bridgeLine - The identifying bridge string for this row.
+ * @property {string?} bridgeId - The ID/fingerprint for the bridge, or null
+ * if it doesn't have one.
+ * @property {integer} index - The index of the row in the grid.
+ * @property {boolean} connected - Whether we are connected to the bridge
+ * (recently in use for a Tor circuit).
+ * @property {BridgeGridCell[]} cells - The cells that belong to the row,
+ * ordered by their column.
+ */
+ /**
+ * @typedef {object} BridgeGridCell
+ *
+ * @property {Element} element - The cell element.
+ * @property {Element} focusEl - The element belonging to the cell that should
+ * receive focus. Should be the cell element itself, or an interactive
+ * focusable child.
+ * @property {integer} columnIndex - The index of the column this cell belongs
+ * to.
+ * @property {BridgeGridRow} row - The row this cell belongs to.
+ */
+ /**
+ * The current rows in the grid.
+ *
+ * @type {BridgeGridRow[]}
+ */
+ _rows: [],
+ /**
+ * The cell that should be the focus target when the user moves focus into the
+ * grid, or null if the grid itself should be the target.
+ *
+ * @type {BridgeGridCell?}
+ */
+ _focusCell: null,
+
+ /**
+ * Initialize the bridge grid.
+ */
+ init() {
+ this._grid = document.getElementById("tor-bridges-grid-display");
+ // Initially, make only the grid itself part of the keyboard tab cycle.
+ // matches _focusCell = null.
+ this._grid.tabIndex = 0;
+
+ this._rowTemplate = document.getElementById(
+ "tor-bridges-grid-row-template"
+ );
+
+ this._grid.addEventListener("keydown", this);
+ this._grid.addEventListener("mousedown", this);
+ this._grid.addEventListener("focusin", this);
+
+ this._supportedSources = [
+ lazy.TorBridgeSource.BridgeDB,
+ lazy.TorBridgeSource.UserProvided,
+ lazy.TorBridgeSource.Lox,
+ ];
+ },
+
+ /**
+ * Whether the grid is visible and responsive.
+ *
+ * @type {boolean}
+ */
+ _active: false,
+
+ /**
+ * Activate and show the bridge grid.
+ */
+ activate() {
+ if (this._active) {
+ return;
+ }
+
+ this._active = true;
+
+ this._grid.hidden = false;
+ },
+
+ /**
+ * Deactivate and hide the bridge grid.
+ */
+ deactivate() {
+ if (!this._active) {
+ return;
+ }
+
+ this._active = false;
+
+ this._forceCloseRowMenus();
+
+ this._grid.hidden = true;
+ },
+
+ handleEvent(event) {
+ if (event.type === "keydown") {
+ if (event.altKey || event.shiftKey || event.metaKey || event.ctrlKey) {
+ // Don't interfere with these events.
+ return;
+ }
+
+ if (this._rows.some(row => row.menu.open)) {
+ // Have an open menu, let the menu handle the event instead.
+ return;
+ }
+
+ let numRows = this._rows.length;
+ if (!numRows) {
+ // Nowhere for focus to go.
+ return;
+ }
+
+ let moveRow = 0;
+ let moveColumn = 0;
+ const isLTR = this._grid.matches(":dir(ltr)");
+ switch (event.key) {
+ case "ArrowDown":
+ moveRow = 1;
+ break;
+ case "ArrowUp":
+ moveRow = -1;
+ break;
+ case "ArrowRight":
+ moveColumn = isLTR ? 1 : -1;
+ break;
+ case "ArrowLeft":
+ moveColumn = isLTR ? -1 : 1;
+ break;
+ default:
+ return;
+ }
+
+ // Prevent scrolling the nearest scroll container.
+ event.preventDefault();
+
+ const curCell = this._focusCell;
+ let row = curCell ? curCell.row.index + moveRow : 0;
+ let column = curCell ? curCell.columnIndex + moveColumn : 0;
+
+ // Clamp in bounds.
+ if (row < 0) {
+ row = 0;
+ } else if (row >= numRows) {
+ row = numRows - 1;
+ }
+
+ const numCells = this._rows[row].cells.length;
+ if (column < 0) {
+ column = 0;
+ } else if (column >= numCells) {
+ column = numCells - 1;
+ }
+
+ const newCell = this._rows[row].cells[column];
+
+ if (newCell !== curCell) {
+ this._setFocus(newCell);
+ }
+ } else if (event.type === "mousedown") {
+ if (event.button !== 0) {
+ return;
+ }
+ // Move focus index to the clicked target.
+ // NOTE: Since the cells and the grid have "tabindex=-1", they are still
+ // click-focusable. Therefore, the default mousedown handler will try to
+ // move focus to it.
+ // Rather than block this default handler, we instead re-direct the focus
+ // to the correct cell in the "focusin" listener.
+ const newCell = this._getCellFromTarget(event.target);
+ // NOTE: If newCell is null, then we do nothing here, but instead wait for
+ // the focusin handler to trigger.
+ if (newCell && newCell !== this._focusCell) {
+ this._setFocus(newCell);
+ }
+ } else if (event.type === "focusin") {
+ const focusCell = this._getCellFromTarget(event.target);
+ if (focusCell !== this._focusCell) {
+ // Focus is not where it is expected.
+ // E.g. the user has clicked the edge of the grid.
+ // Restore focus immediately back to the cell we expect.
+ this._setFocus(this._focusCell);
+ }
+ }
+ },
+
+ /**
+ * Return the cell that was the target of an event.
+ *
+ * @param {Element} element - The target of an event.
+ *
+ * @returns {BridgeGridCell?} - The cell that the element belongs to, or null
+ * if it doesn't belong to any cell.
+ */
+ _getCellFromTarget(element) {
+ for (const row of this._rows) {
+ for (const cell of row.cells) {
+ if (cell.element.contains(element)) {
+ return cell;
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Determine whether the document's active element (focus) is within the grid
+ * or not.
+ *
+ * @returns {boolean} - Whether focus is within this grid or not.
+ */
+ _focusWithin() {
+ return this._grid.contains(document.activeElement);
+ },
+
+ /**
+ * Set the cell that should be the focus target of the grid, possibly moving
+ * the document's focus as well.
+ *
+ * @param {BridgeGridCell?} cell - The cell to make the focus target, or null
+ * if the grid itself should be the target.
+ * @param {boolean} [focusWithin] - Whether focus should be moved within the
+ * grid. If undefined, this will move focus if the grid currently contains
+ * the document's focus.
+ */
+ _setFocus(cell, focusWithin) {
+ if (focusWithin === undefined) {
+ focusWithin = this._focusWithin();
+ }
+ const prevFocusElement = this._focusCell
+ ? this._focusCell.focusEl
+ : this._grid;
+ const newFocusElement = cell ? cell.focusEl : this._grid;
+
+ if (prevFocusElement !== newFocusElement) {
+ prevFocusElement.tabIndex = -1;
+ newFocusElement.tabIndex = 0;
+ }
+ // Set _focusCell now, before we potentially call "focus", which can trigger
+ // the "focusin" handler.
+ this._focusCell = cell;
+
+ if (focusWithin) {
+ // Focus was within the grid, so we need to actively move it to the new
+ // element.
+ newFocusElement.focus({ preventScroll: true });
+ // Scroll to the whole cell into view, rather than just the focus element.
+ (cell?.element ?? newFocusElement).scrollIntoView({
+ block: "nearest",
+ inline: "nearest",
+ });
+ }
+ },
+
+ /**
+ * Reset the grids focus to be the first row's first cell, if any.
+ *
+ * @param {boolean} [focusWithin] - Whether focus should be moved within the
+ * grid. If undefined, this will move focus if the grid currently contains
+ * the document's focus.
+ */
+ _resetFocus(focusWithin) {
+ this._setFocus(
+ this._rows.length ? this._rows[0].cells[0] : null,
+ focusWithin
+ );
+ },
+
+ /**
+ * The bridge ID/fingerprint of the most recently used bridge (appearing in
+ * the latest Tor circuit). Roughly corresponds to the bridge we are currently
+ * connected to.
+ *
+ * null if there are no such bridges.
+ *
+ * @type {string?}
+ */
+ _connectedBridgeId: null,
+
+ set connectedBridgeId(bridgeId) {
+ if (bridgeId === this._connectedBridgeId) {
+ return;
+ }
+ this._connectedBridgeId = bridgeId;
+ for (const row of this._rows) {
+ this._updateRowStatus(row);
+ }
+ },
+
+ /**
+ * Update the status of a row.
+ *
+ * @param {BridgeGridRow} row - The row to update.
+ */
+ _updateRowStatus(row) {
+ const connected = row.bridgeId && this._connectedBridgeId === row.bridgeId;
+ // NOTE: row.connected is initially undefined, so won't match `connected`.
+ if (connected === row.connected) {
+ return;
+ }
+
+ row.connected = connected;
+
+ const noStatus = !connected;
+
+ row.element.classList.toggle("hide-status", noStatus);
+ row.statusEl.classList.toggle("bridge-status-none", noStatus);
+ row.statusEl.classList.toggle("bridge-status-connected", connected);
+
+ if (connected) {
+ document.l10n.setAttributes(
+ row.statusText,
+ "tor-bridges-status-connected"
+ );
+ } else {
+ document.l10n.setAttributes(row.statusText, "tor-bridges-status-none");
+ }
+ },
+
+ /**
+ * Create a new row for the grid.
+ *
+ * @param {string} bridgeLine - The bridge line for this row, which also acts
+ * as its ID.
+ *
+ * @returns {BridgeGridRow} - A new row, with then "index" unset and the
+ * "element" without a parent.
+ */
+ _createRow(bridgeLine) {
+ let details;
+ try {
+ details = lazy.TorParsers.parseBridgeLine(bridgeLine);
+ } catch (e) {
+ console.error(`Detected invalid bridge line: ${bridgeLine}`, e);
+ }
+ const row = {
+ element: this._rowTemplate.content.children[0].cloneNode(true),
+ bridgeLine,
+ bridgeId: details?.id ?? null,
+ cells: [],
+ };
+
+ const emojiBlock = row.element.querySelector(".tor-bridges-emojis-block");
+ const BridgeEmoji = customElements.get("tor-bridge-emoji");
+ for (const cell of BridgeEmoji.createForAddress(bridgeLine)) {
+ // Each emoji is its own cell, we rely on the fact that createForAddress
+ // always returns four elements.
+ cell.setAttribute("role", "gridcell");
+ cell.classList.add("tor-bridges-grid-cell", "tor-bridges-emoji-cell");
+ emojiBlock.append(cell);
+ }
+
+ for (const [columnIndex, element] of row.element
+ .querySelectorAll(".tor-bridges-grid-cell")
+ .entries()) {
+ const focusEl =
+ element.querySelector(".tor-bridges-grid-focus") ?? element;
+ // Set a negative tabIndex, this makes the element click-focusable but not
+ // part of the tab navigation sequence.
+ focusEl.tabIndex = -1;
+ row.cells.push({ element, focusEl, columnIndex, row });
+ }
+
+ const transport = details?.transport ?? "vanilla";
+ const typeCell = row.element.querySelector(".tor-bridges-type-cell");
+ if (transport === "vanilla") {
+ document.l10n.setAttributes(typeCell, "tor-bridges-type-prefix-generic");
+ } else {
+ document.l10n.setAttributes(typeCell, "tor-bridges-type-prefix", {
+ type: transport,
+ });
+ }
+
+ row.element.querySelector(".tor-bridges-address-cell-text").textContent =
+ bridgeLine;
+
+ row.statusEl = row.element.querySelector(
+ ".tor-bridges-status-cell .bridge-status-badge"
+ );
+ row.statusText = row.element.querySelector(".tor-bridges-status-cell-text");
+
+ this._initRowMenu(row);
+
+ this._updateRowStatus(row);
+ return row;
+ },
+
+ /**
+ * The row menu index used for generating new ids.
+ *
+ * @type {integer}
+ */
+ _rowMenuIndex: 0,
+ /**
+ * Generate a new id for the options menu.
+ *
+ * @returns {string} - The new id.
+ */
+ _generateRowMenuId() {
+ const id = `tor-bridges-individual-options-menu-${this._rowMenuIndex}`;
+ // Assume we won't run out of ids.
+ this._rowMenuIndex++;
+ return id;
+ },
+
+ /**
+ * Initialize the shared menu for a row.
+ *
+ * @param {BridgeGridRow} row - The row to initialize the menu of.
+ */
+ _initRowMenu(row) {
+ row.menu = row.element.querySelector(
+ ".tor-bridges-individual-options-menu"
+ );
+ row.optionsButton = row.element.querySelector(
+ ".tor-bridges-options-cell-button"
+ );
+
+ row.menu.id = this._generateRowMenuId();
+ row.optionsButton.setAttribute("aria-controls", row.menu.id);
+
+ row.optionsButton.addEventListener("click", event => {
+ row.menu.toggle(event);
+ });
+
+ row.menu.addEventListener("hidden", () => {
+ // Make sure the button receives focus again when the menu is hidden.
+ // Currently, panel-list.js only does this when the menu is opened with a
+ // keyboard, but this causes focus to be lost from the page if the user
+ // uses a mixture of keyboard and mouse.
+ row.optionsButton.focus();
+ });
+
+ const qrItem = row.menu.querySelector(
+ ".tor-bridges-options-qr-one-menu-item"
+ );
+ const removeItem = row.menu.querySelector(
+ ".tor-bridges-options-remove-one-menu-item"
+ );
+ row.menu.addEventListener("showing", () => {
+ const show =
+ this._bridgeSource === lazy.TorBridgeSource.UserProvided ||
+ this._bridgeSource === lazy.TorBridgeSource.BridgeDB;
+ qrItem.hidden = !show;
+ removeItem.hidden = !show;
+ });
+
+ qrItem.addEventListener("click", () => {
+ const bridgeLine = row.bridgeLine;
+ if (!bridgeLine) {
+ return;
+ }
+ showBridgeQr(bridgeLine);
+ });
+ row.menu
+ .querySelector(".tor-bridges-options-copy-one-menu-item")
+ .addEventListener("click", () => {
+ const clipboard = Cc[
+ "@mozilla.org/widget/clipboardhelper;1"
+ ].getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(row.bridgeLine);
+ });
+ removeItem.addEventListener("click", () => {
+ const bridgeLine = row.bridgeLine;
+ const source = lazy.TorSettings.bridges.source;
+ if (source !== this._bridgesVal?.source) {
+ // Our value is stale, abort.
+ return;
+ }
+ const strings = lazy.TorSettings.bridges.bridge_strings;
+ const index = strings.indexOf(bridgeLine);
+ if (index === -1) {
+ return;
+ }
+ strings.splice(index, 1);
+
+ if (strings.length) {
+ lazy.TorSettings.changeSettings({
+ bridges: { source, bridge_strings: strings },
+ });
+ } else {
+ // Remove all bridges and disable.
+ lazy.TorSettings.changeSettings({
+ bridges: { source: lazy.TorBridgeSource.Invalid },
+ });
+ }
+ });
+ },
+
+ /**
+ * Force the row menu to close.
+ */
+ _forceCloseRowMenus() {
+ for (const row of this._rows) {
+ row.menu.hide(null, { force: true });
+ }
+ },
+
+ /**
+ * The known bridge source.
+ *
+ * Initially null to indicate that it is unset.
+ *
+ * @type {integer?}
+ */
+ _bridgeSource: null,
+ /**
+ * The bridge sources this is shown for.
+ *
+ * @type {string[]}
+ */
+ _supportedSources: [],
+
+ /**
+ * The bridges value, set by the setting-control element.
+ *
+ * @type {object}
+ */
+ _bridgesVal: null,
+
+ set bridges(val) {
+ if (val === null) {
+ // Ignore and wait for the initial.
+ return;
+ }
+ const initial = this._bridgesVal === null;
+ this._bridgesVal = val;
+ this._updateRows(initial);
+ },
+
+ /**
+ * Update the grid to show the latest bridge strings.
+ *
+ * @param {boolean} initializing - Whether this is being called as part of
+ * initialization.
+ */
+ _updateRows(initializing) {
+ // Store whether we have focus within the grid, before removing or hiding
+ // DOM elements.
+ const focusWithin = this._focusWithin();
+
+ let lostAllBridges = false;
+ let newSource = false;
+ const bridgeSource = this._bridgesVal.source;
+ if (bridgeSource !== this._bridgeSource) {
+ newSource = true;
+
+ this._bridgeSource = bridgeSource;
+
+ if (this._supportedSources.includes(bridgeSource)) {
+ this.activate();
+ } else {
+ if (this._active && bridgeSource === lazy.TorBridgeSource.Invalid) {
+ lostAllBridges = true;
+ }
+ this.deactivate();
+ }
+ }
+
+ const ordered = this._active
+ ? this._bridgesVal.bridgeStrings.map(bridgeLine => {
+ const row = this._rows.find(r => r.bridgeLine === bridgeLine);
+ if (row) {
+ return row;
+ }
+ return this._createRow(bridgeLine);
+ })
+ : [];
+
+ // Whether we should reset the grid's focus.
+ // We always reset when we have a new bridge source.
+ // We reset the focus if no current Cell has focus. I.e. when adding a row
+ // to an empty grid, we want the focus to move to the first item.
+ // We also reset the focus if the current Cell is in a row that will be
+ // removed (including if all rows are removed).
+ // NOTE: In principle, if a row is removed, we could move the focus to the
+ // next or previous row (in the same cell column). However, most likely if
+ // the grid has the user focus, they are removing a single row using its
+ // options button. In this case, returning the user to some other row's
+ // options button might be more disorienting since it would not be simple
+ // for them to know *which* bridge they have landed on.
+ // NOTE: We do not reset the focus in other cases because we do not want the
+ // user to loose their place in the grid unnecessarily.
+ let resetFocus =
+ newSource || !this._focusCell || !ordered.includes(this._focusCell.row);
+
+ // Remove rows no longer needed from the DOM.
+ let numRowsRemoved = 0;
+ let rowAddedOrMoved = false;
+
+ for (const row of this._rows) {
+ if (!ordered.includes(row)) {
+ numRowsRemoved++;
+ // If the row menu was open, it will also be deleted.
+ // NOTE: Since the row menu is part of the row, focusWithin will be true
+ // if the menu had focus, so focus should be re-assigned.
+ row.element.remove();
+ }
+ }
+
+ // Go through all the rows to set their ".index" property and to ensure they
+ // are in the correct position in the DOM.
+ // NOTE: We could use replaceChildren to get the correct DOM structure, but
+ // we want to avoid rebuilding the entire tree when a single row is added or
+ // removed.
+ for (const [index, row] of ordered.entries()) {
+ row.index = index;
+ const element = row.element;
+ // Get the expected previous element, that should already be in the DOM
+ // from the previous loop.
+ const prevEl = index ? ordered[index - 1].element : null;
+
+ if (
+ element.parentElement === this._grid &&
+ prevEl === element.previousElementSibling
+ ) {
+ // Already in the correct position in the DOM.
+ continue;
+ }
+
+ rowAddedOrMoved = true;
+ // NOTE: Any elements already in the DOM, but not in the correct position
+ // will be removed and re-added by the below command.
+ // NOTE: if the row has document focus, then it should remain there.
+ if (prevEl) {
+ prevEl.after(element);
+ } else {
+ this._grid.prepend(element);
+ }
+ }
+ this._rows = ordered;
+
+ // Restore any lost focus.
+ if (resetFocus) {
+ // If we are not active (and therefore hidden), we will not try and move
+ // focus (activeElement), but may still change the *focusable* element for
+ // when we are shown again.
+ this._resetFocus(this._active && focusWithin);
+ }
+ // NOTE: In the case we were previously active and now inactive,
+ // tor-bridges-display will have already moved the focus out of this area.
+
+ // Notify the user if there was some change to the DOM.
+ // If we are initializing, we generate no notification since there has been
+ // no change in the setting.
+ if (!initializing) {
+ let notificationType;
+ if (lostAllBridges) {
+ // Just lost all bridges, and became de-active.
+ notificationType = "removed-all";
+ } else if (this._rows.length) {
+ // Otherwise, only generate a notification if we are still active, with
+ // at least one bridge.
+ // I.e. do not generate a message if the new source is "builtin".
+ if (newSource) {
+ // A change in source.
+ notificationType = "changed";
+ } else if (numRowsRemoved === 1 && !rowAddedOrMoved) {
+ // Only one bridge was removed. This is most likely in response to them
+ // manually removing a single bridge or using the bridge row's options
+ // menu.
+ notificationType = "removed-one";
+ } else if (numRowsRemoved || rowAddedOrMoved) {
+ // Some other change. This is most likely in response to a manual edit
+ // of the existing bridges.
+ notificationType = "changed";
+ }
+ // Else, there was no change.
+ }
+
+ if (notificationType) {
+ postBridgeNotification(notificationType);
+ }
+ }
+ },
+};
+
+/**
+ * Controls the built-in bridges area.
+ */
+const gBuiltinBridgesArea = {
+ /**
+ * The display area.
+ *
+ * @type {Element?}
+ */
+ _area: null,
+ /**
+ * The type name element.
+ *
+ * @type {Element?}
+ */
+ _nameEl: null,
+ /**
+ * The bridge type description element.
+ *
+ * @type {Element?}
+ */
+ _descriptionEl: null,
+ /**
+ * The connection status.
+ *
+ * @type {Element?}
+ */
+ _connectionStatusEl: null,
+
+ /**
+ * Initialize the built-in bridges area.
+ */
+ init() {
+ this._area = document.getElementById("tor-bridges-built-in-display");
+ this._nameEl = document.getElementById("tor-bridges-built-in-type-name");
+ this._descriptionEl = document.getElementById(
+ "tor-bridges-built-in-description"
+ );
+ this._connectionStatusEl = document.getElementById(
+ "tor-bridges-built-in-connected"
+ );
+ },
+
+ /**
+ * Whether the built-in area is visible and responsive.
+ *
+ * @type {boolean}
+ */
+ _active: false,
+
+ /**
+ * Activate and show the built-in bridge area.
+ */
+ activate() {
+ if (this._active) {
+ return;
+ }
+ this._active = true;
+
+ this._area.hidden = false;
+ },
+
+ /**
+ * Deactivate and hide built-in bridge area.
+ */
+ deactivate() {
+ if (!this._active) {
+ return;
+ }
+ this._active = false;
+
+ this._area.hidden = true;
+ },
+
+ /**
+ * Updates the shown connected state.
+ */
+ _updateConnectedState() {
+ this._connectionStatusEl.classList.toggle(
+ "bridge-status-connected",
+ this._bridgeType &&
+ this._connectedBridgeId &&
+ this._bridgeIds.includes(this._connectedBridgeId)
+ );
+ },
+
+ /**
+ * The bridges value, set by the setting-control element.
+ *
+ * @type {object}
+ */
+ _bridgesVal: null,
+ set bridges(val) {
+ if (val === null) {
+ // Ignore and wait for the initial.
+ return;
+ }
+ const initial = this._bridgesVal === null;
+ this._bridgesVal = val;
+ this._updateBridgeType(initial);
+ this._updateBridgeIds();
+ },
+
+ /**
+ * The currently shown bridge type. Empty if deactivated, and null if
+ * uninitialized.
+ *
+ * @type {string?}
+ */
+ _bridgeType: null,
+ /**
+ * The strings for each known bridge type.
+ *
+ * @type {{[key: string]: {[key: string]: string}}}
+ */
+ _bridgeTypeStrings: {
+ obfs4: {
+ name: "tor-bridges-built-in-obfs4-name",
+ description: "tor-bridges-built-in-obfs4-description",
+ },
+ snowflake: {
+ name: "tor-bridges-built-in-snowflake-name",
+ description: "tor-bridges-built-in-snowflake-description",
+ },
+ meek: {
+ name: "tor-bridges-built-in-meek-name",
+ description: "tor-bridges-built-in-meek-description",
+ },
+ },
+
+ /**
+ * The known bridge source.
+ *
+ * Initially null to indicate that it is unset.
+ *
+ * @type {integer?}
+ */
+ _bridgeSource: null,
+
+ /**
+ * Update the shown bridge type.
+ *
+ * @param {boolean} initializing - Whether this is being called as part of
+ * initialization.
+ */
+ async _updateBridgeType(initializing) {
+ let lostAllBridges = false;
+ let newSource = false;
+ const bridgeSource = this._bridgesVal.source;
+ if (bridgeSource !== this._bridgeSource) {
+ newSource = true;
+
+ this._bridgeSource = bridgeSource;
+
+ if (bridgeSource === lazy.TorBridgeSource.BuiltIn) {
+ this.activate();
+ } else {
+ if (this._active && bridgeSource === lazy.TorBridgeSource.Invalid) {
+ lostAllBridges = true;
+ }
+ this.deactivate();
+ // NOTE: In the case we were previously active, tor-bridges-display will
+ // have already moved the focus out of this area.
+ }
+ }
+
+ const bridgeType = this._active ? this._bridgesVal.builtinType : "";
+
+ let newType = false;
+ if (bridgeType !== this._bridgeType) {
+ newType = true;
+
+ this._bridgeType = bridgeType;
+
+ const bridgeStrings = this._bridgeTypeStrings[bridgeType];
+ if (bridgeStrings) {
+ document.l10n.setAttributes(this._nameEl, bridgeStrings.name);
+ document.l10n.setAttributes(
+ this._descriptionEl,
+ bridgeStrings.description
+ );
+ } else {
+ // Unknown type, or no type.
+ this._nameEl.removeAttribute("data-l10n-id");
+ this._nameEl.textContent = bridgeType;
+ this._descriptionEl.removeAttribute("data-l10n-id");
+ this._descriptionEl.textContent = "";
+ }
+
+ this._updateConnectedState();
+ }
+
+ // Notify the user if there was some change to the type.
+ // If we are initializing, we generate no notification since there has been
+ // no change in the setting.
+ if (!initializing) {
+ let notificationType;
+ if (lostAllBridges) {
+ // Just lost all bridges, and became de-active.
+ notificationType = "removed-all";
+ } else if (this._active && (newSource || newType)) {
+ // Otherwise, only generate a notification if we are still active, with
+ // a bridge type.
+ // I.e. do not generate a message if the new source is not "builtin".
+ notificationType = "changed";
+ }
+
+ if (notificationType) {
+ postBridgeNotification(notificationType);
+ }
+ }
+ },
+
+ /**
+ * The bridge IDs/fingerprints for the built-in bridges.
+ *
+ * @type {Array<string>}
+ */
+ _bridgeIds: [],
+ /**
+ * Update _bridgeIds
+ */
+ _updateBridgeIds() {
+ this._bridgeIds = [];
+ for (const bridgeLine of this._bridgesVal.bridgeStrings) {
+ try {
+ this._bridgeIds.push(lazy.TorParsers.parseBridgeLine(bridgeLine).id);
+ } catch (e) {
+ console.error(`Detected invalid bridge line: ${bridgeLine}`, e);
+ }
+ }
+
+ this._updateConnectedState();
+ },
+
+ /**
+ * The bridge ID/fingerprint of the most recently used bridge (appearing in
+ * the latest Tor circuit). Roughly corresponds to the bridge we are currently
+ * connected to.
+ *
+ * @type {string?}
+ */
+ _connectedBridgeId: null,
+
+ set connectedBridgeId(val) {
+ this._connectedBridgeId = val;
+ this._updateConnectedState();
+ },
+};
+
+/**
+ * Controls the bridge pass area.
+ */
+const gLoxStatus = {
+ /**
+ * The status area.
+ *
+ * @type {Element?}
+ */
+ _area: null,
+ /**
+ * The area for showing the next unlock and invites.
+ *
+ * @type {Element?}
+ */
+ _detailsArea: null,
+ /**
+ * The list items showing the next unlocks.
+ *
+ * @type {?{[key: string]: Element}}
+ */
+ _nextUnlockItems: null,
+ /**
+ * The day counter headings for the next unlock.
+ *
+ * One heading is shown during a search, the other is shown otherwise.
+ *
+ * @type {?Element[]}
+ */
+ _nextUnlockCounterEls: null,
+ /**
+ * Shows the number of remaining invites.
+ *
+ * @type {Element?}
+ */
+ _remainingInvitesEl: null,
+ /**
+ * The button to show the invites.
+ *
+ * @type {Element?}
+ */
+ _invitesButton: null,
+ /**
+ * The alert for new unlocks.
+ *
+ * @type {Element?}
+ */
+ _unlockAlert: null,
+ /**
+ * The list items showing the unlocks.
+ *
+ * @type {?{[key: string]: Element}}
+ */
+ _unlockItems: null,
+ /**
+ * The alert title.
+ *
+ * @type {Element?}
+ */
+ _unlockAlertTitle: null,
+ /**
+ * The alert invites item.
+ *
+ * @type {Element?}
+ */
+ _unlockAlertInvitesItem: null,
+ /**
+ * Button for the user to dismiss the alert.
+ *
+ * @type {Element?}
+ */
+ _unlockAlertButton: null,
+
+ _enabled: false,
+
+ /**
+ * Initialize the bridge pass area.
+ */
+ init() {
+ if (!lazy.Lox.enabled) {
+ // Area should remain inactive and hidden.
+ return;
+ }
+
+ this._enabled = true;
+ this._area = document.getElementById("tor-bridges-lox-status");
+ this._detailsArea = document.getElementById("tor-bridges-lox-details");
+ this._nextUnlockItems = {
+ gainBridges: document.getElementById(
+ "tor-bridges-lox-next-unlock-gain-bridges"
+ ),
+ firstInvites: document.getElementById(
+ "tor-bridges-lox-next-unlock-first-invites"
+ ),
+ moreInvites: document.getElementById(
+ "tor-bridges-lox-next-unlock-more-invites"
+ ),
+ };
+ this._nextUnlockCounterEls = Array.from(
+ document.querySelectorAll(".tor-bridges-lox-next-unlock-counter")
+ );
+ this._remainingInvitesEl = document.getElementById(
+ "tor-bridges-lox-remaining-invites"
+ );
+ this._invitesButton = document.getElementById(
+ "tor-bridges-lox-show-invites-button"
+ );
+ this._unlockAlert = document.getElementById("tor-bridges-lox-unlock-alert");
+ this._unlockItems = {
+ gainBridges: document.getElementById(
+ "tor-bridges-lox-unlock-alert-gain-bridges"
+ ),
+ newBridges: document.getElementById(
+ "tor-bridges-lox-unlock-alert-new-bridges"
+ ),
+ invites: document.getElementById("tor-bridges-lox-unlock-alert-invites"),
+ };
+ this._unlockAlertTitle = document.getElementById(
+ "tor-bridge-unlock-alert-title"
+ );
+ this._unlockAlertInviteItem = document.getElementById(
+ "tor-bridges-lox-unlock-alert-invites"
+ );
+ this._unlockAlertButton = document.getElementById(
+ "tor-bridges-lox-unlock-alert-button"
+ );
+
+ this._invitesButton.addEventListener("click", () => {
+ window.gSubDialog.open(
+ "chrome://browser/content/torpreferences/loxInviteDialog.xhtml",
+ { features: "resizable=yes" }
+ );
+ });
+ this._unlockAlertButton.addEventListener("click", () => {
+ lazy.Lox.clearEventData(this._loxId);
+ });
+
+ Services.obs.addObserver(this, lazy.LoxTopics.UpdateActiveLoxId);
+ Services.obs.addObserver(this, lazy.LoxTopics.UpdateEvents);
+ Services.obs.addObserver(this, lazy.LoxTopics.UpdateNextUnlock);
+ Services.obs.addObserver(this, lazy.LoxTopics.UpdateRemainingInvites);
+ Services.obs.addObserver(this, lazy.LoxTopics.NewInvite);
+
+ window.addEventListener(
+ "unload",
+ () => {
+ Services.obs.removeObserver(this, lazy.LoxTopics.UpdateActiveLoxId);
+ Services.obs.removeObserver(this, lazy.LoxTopics.UpdateEvents);
+ Services.obs.removeObserver(this, lazy.LoxTopics.UpdateNextUnlock);
+ Services.obs.removeObserver(
+ this,
+ lazy.LoxTopics.UpdateRemainingInvites
+ );
+ Services.obs.removeObserver(this, lazy.LoxTopics.NewInvite);
+ },
+ { once: true }
+ );
+ },
+
+ observe(subject, topic) {
+ switch (topic) {
+ case lazy.LoxTopics.UpdateActiveLoxId:
+ this._updateLoxId();
+ break;
+ case lazy.LoxTopics.UpdateNextUnlock:
+ this._updateNextUnlock();
+ break;
+ case lazy.LoxTopics.UpdateEvents:
+ this._updatePendingEvents();
+ break;
+ case lazy.LoxTopics.UpdateRemainingInvites:
+ this._updateRemainingInvites();
+ break;
+ case lazy.LoxTopics.NewInvite:
+ this._updateHaveExistingInvites();
+ break;
+ }
+ },
+
+ /**
+ * The bridges value, set by the setting-control element.
+ *
+ * @type {object}
+ */
+ _bridgesVal: null,
+ set bridges(val) {
+ if (val === null) {
+ // Ignore and wait for initial.
+ return;
+ }
+ if (!this._enabled) {
+ // Area should remain inactive and hidden.
+ return;
+ }
+ this._bridgesVal = val;
+ this._updateLoxId();
+ },
+
+ /**
+ * The Lox id currently shown. Empty if deactivated, and null if
+ * uninitialized.
+ *
+ * @type {string?}
+ */
+ _loxId: null,
+
+ /**
+ * Update the shown bridge pass.
+ */
+ async _updateLoxId() {
+ let loxId =
+ this._bridgesVal?.source === lazy.TorBridgeSource.Lox
+ ? lazy.Lox.activeLoxId
+ : "";
+ if (loxId === this._loxId) {
+ return;
+ }
+ this._loxId = loxId;
+ this._area.hidden = !loxId;
+ // We unset _nextUnlock to ensure the areas no longer use the old value for
+ // the new loxId.
+ this._updateNextUnlock(true);
+ this._updateRemainingInvites();
+ this._updateHaveExistingInvites();
+ this._updatePendingEvents();
+ },
+
+ /**
+ * The remaining invites shown, or null if uninitialized or no loxId.
+ *
+ * @type {integer?}
+ */
+ _remainingInvites: null,
+ /**
+ * Update the shown value.
+ */
+ _updateRemainingInvites() {
+ const numInvites = this._loxId
+ ? lazy.Lox.getRemainingInviteCount(this._loxId)
+ : null;
+ if (numInvites === this._remainingInvites) {
+ return;
+ }
+ this._remainingInvites = numInvites;
+ this._updateUnlockArea();
+ this._updateInvitesArea();
+ },
+ /**
+ * Whether we have existing invites, or null if uninitialized or no loxId.
+ *
+ * @type {boolean?}
+ */
+ _haveExistingInvites: null,
+ /**
+ * Update the shown value.
+ */
+ _updateHaveExistingInvites() {
+ const haveInvites = this._loxId ? !!lazy.Lox.getInvites().length : null;
+ if (haveInvites === this._haveExistingInvites) {
+ return;
+ }
+ this._haveExistingInvites = haveInvites;
+ this._updateInvitesArea();
+ },
+ /**
+ * Details about the next unlock, or null if uninitialized or no loxId.
+ *
+ * @type {UnlockData?}
+ */
+ _nextUnlock: null,
+ /**
+ * Tracker id to ensure that the results from later calls to _updateNextUnlock
+ * take priority over earlier calls.
+ *
+ * @type {integer}
+ */
+ _nextUnlockCallId: 0,
+ /**
+ * Update the shown value asynchronously.
+ *
+ * @param {boolean} [unset=false] - Whether to set the _nextUnlock value to
+ * null before waiting for the new value. I.e. ensure that the current value
+ * will not be used.
+ */
+ async _updateNextUnlock(unset = false) {
+ // NOTE: We do not expect the integer to exceed the maximum integer.
+ this._nextUnlockCallId++;
+ const callId = this._nextUnlockCallId;
+ if (unset) {
+ this._nextUnlock = null;
+ }
+ const nextUnlock = this._loxId
+ ? await lazy.Lox.getNextUnlock(this._loxId)
+ : null;
+ if (callId !== this._nextUnlockCallId) {
+ // Replaced by another update.
+ // E.g. if the _loxId changed. Or if getNextUnlock triggered
+ // LoxTopics.UpdateNextUnlock.
+ return;
+ }
+ // Should be safe to trigger the update, even when the value hasn't changed.
+ this._nextUnlock = nextUnlock;
+ this._updateUnlockArea();
+ },
+ /**
+ * The list of events the user has not yet cleared, or null if uninitialized
+ * or no loxId.
+ *
+ * @type {EventData[]?}
+ */
+ _pendingEvents: null,
+ /**
+ * Update the shown value.
+ */
+ _updatePendingEvents() {
+ // Should be safe to trigger the update, even when the value hasn't changed.
+ this._pendingEvents = this._loxId
+ ? lazy.Lox.getEventData(this._loxId)
+ : null;
+ this._updateUnlockArea();
+ },
+
+ /**
+ * Update the display of the current or next unlock.
+ */
+ _updateUnlockArea() {
+ if (
+ !this._loxId ||
+ this._pendingEvents === null ||
+ this._remainingInvites === null ||
+ this._nextUnlock === null
+ ) {
+ // Uninitialized or no Lox source.
+ // NOTE: This area may already be hidden by the change in Lox source,
+ // but we clean up for the next non-empty id.
+ this._unlockAlert.hidden = true;
+ this._detailsArea.hidden = true;
+ return;
+ }
+
+ // Grab focus state before changing visibility.
+ const alertHadFocus = this._unlockAlert.contains(document.activeElement);
+ const detailsHadFocus = this._detailsArea.contains(document.activeElement);
+
+ const pendingEvents = this._pendingEvents;
+ const showAlert = !!pendingEvents.length;
+ this._unlockAlert.hidden = !showAlert;
+ this._detailsArea.hidden = showAlert;
+
+ if (showAlert) {
+ // At level 0 and level 1, we do not have any invites.
+ // If the user starts and ends on level 0 or 1, then overall they would
+ // have had no change in their invites. So we do not want to show their
+ // latest updates.
+ // NOTE: If the user starts at level > 1 and ends with level 1 (levelling
+ // down to level 0 should not be possible), then we *do* want to show the
+ // user that they now have "0" invites.
+ // NOTE: pendingEvents are time-ordered, with the most recent event
+ // *last*.
+ const firstEvent = pendingEvents[0];
+ // NOTE: We cannot get a blockage event when the user starts at level 1 or
+ // 0.
+ const startingAtLowLevel =
+ firstEvent.type === "levelup" && firstEvent.newLevel <= 2;
+ const lastEvent = pendingEvents[pendingEvents.length - 1];
+ const endingAtLowLevel = lastEvent.newLevel <= 1;
+
+ const showInvites = !(startingAtLowLevel && endingAtLowLevel);
+
+ let blockage = false;
+ let levelUp = false;
+ let bridgeGain = false;
+ // Go through events, in the order that they occurred.
+ for (const loxEvent of pendingEvents) {
+ if (loxEvent.type === "levelup") {
+ levelUp = true;
+ if (loxEvent.newLevel === 1) {
+ // Gain 2 bridges from level 0 to 1.
+ bridgeGain = true;
+ }
+ } else {
+ blockage = true;
+ }
+ }
+
+ let alertTitleId;
+ if (levelUp && !blockage) {
+ alertTitleId = "tor-bridges-lox-upgrade";
+ } else {
+ // Show as blocked bridges replaced.
+ // Even if we have a mixture of level ups as well.
+ alertTitleId = "tor-bridges-lox-blocked";
+ }
+ document.l10n.setAttributes(this._unlockAlertTitle, alertTitleId);
+ document.l10n.setAttributes(
+ this._unlockAlertInviteItem,
+ "tor-bridges-lox-new-invites",
+ { numInvites: this._remainingInvites }
+ );
+ this._unlockAlert.classList.toggle(
+ "lox-unlock-upgrade",
+ levelUp && !blockage
+ );
+ this._unlockItems.gainBridges.hidden = !bridgeGain;
+ this._unlockItems.newBridges.hidden = !blockage;
+ this._unlockItems.invites.hidden = !showInvites;
+ } else {
+ // Show next unlock.
+ // Number of days until the next unlock, rounded up.
+ const numDays = Math.max(
+ 1,
+ Math.ceil(
+ (new Date(this._nextUnlock.date).getTime() - Date.now()) /
+ (24 * 60 * 60 * 1000)
+ )
+ );
+ for (const counterEl of this._nextUnlockCounterEls) {
+ document.l10n.setAttributes(
+ counterEl,
+ "tor-bridges-lox-days-until-unlock",
+ { numDays }
+ );
+ }
+
+ // Gain 2 bridges from level 0 to 1. After that gain invites.
+ this._nextUnlockItems.gainBridges.hidden =
+ this._nextUnlock.nextLevel !== 1;
+ this._nextUnlockItems.firstInvites.hidden =
+ this._nextUnlock.nextLevel !== 2;
+ this._nextUnlockItems.moreInvites.hidden =
+ this._nextUnlock.nextLevel <= 2;
+ }
+
+ if (alertHadFocus && !showAlert) {
+ // Alert has become hidden, move focus back up to the now revealed details
+ // area.
+ // NOTE: We have two headings: one shown during a search and one shown
+ // otherwise. We focus the heading that is currently visible.
+ // See tor-browser#43320.
+ // TODO: It might be better if we could use the # named anchor to
+ // re-orient the screen reader position instead of using tabIndex=-1, but
+ // about:preferences currently uses the anchor for showing categories
+ // only. See bugzilla bug 1799153.
+ if (
+ this._nextUnlockCounterEls[0].checkVisibility({
+ visibilityProperty: true,
+ })
+ ) {
+ this._nextUnlockCounterEls[0].focus();
+ } else {
+ this._nextUnlockCounterEls[1].focus();
+ }
+ } else if (detailsHadFocus && showAlert) {
+ this._unlockAlertButton.focus();
+ }
+ },
+
+ /**
+ * Update the invites area.
+ */
+ _updateInvitesArea() {
+ let hasInvites;
+ if (
+ !this._loxId ||
+ this._remainingInvites === null ||
+ this._haveExistingInvites === null
+ ) {
+ // Not initialized yet.
+ hasInvites = false;
+ } else {
+ hasInvites = this._haveExistingInvites || !!this._remainingInvites;
+ }
+
+ if (
+ !hasInvites &&
+ (this._remainingInvitesEl.contains(document.activeElement) ||
+ this._invitesButton.contains(document.activeElement))
+ ) {
+ // About to loose focus.
+ // Unexpected for the lox level to loose all invites.
+ // Move to the top of the details area, which should be visible if we
+ // just had focus.
+ this._nextUnlockCounterEl.focus();
+ }
+ // Hide the invite elements if we have no historic invites or a way of
+ // creating new ones.
+ this._remainingInvitesEl.hidden = !hasInvites;
+ this._invitesButton.hidden = !hasInvites;
+
+ if (hasInvites) {
+ document.l10n.setAttributes(
+ this._remainingInvitesEl,
+ "tor-bridges-lox-remaining-invites",
+ { numInvites: this._remainingInvites }
+ );
+ }
+ },
+};
+
+/**
+ * Controls the bridge settings.
+ */
+const gBridgeSettings = {
+ /**
+ * The display area.
+ *
+ * @type {Element?}
+ */
+ _displayEl: null,
+ /**
+ * The area for showing current bridges.
+ *
+ * @type {Element?}
+ */
+ _bridgesEl: null,
+ /**
+ * The area for sharing bridge addresses.
+ *
+ * @type {Element?}
+ */
+ _shareEl: null,
+ /**
+ * The area for showing no bridges.
+ *
+ * @type {Element?}
+ */
+ _noBridgesEl: null,
+ /**
+ * A map from the bridge source to its corresponding label.
+ *
+ * @type {?Map<number, Element>}
+ */
+ _sourceLabels: null,
+
+ /**
+ * Whether we have been initialized.
+ *
+ * @type {boolean}
+ */
+ _initialized: false,
+
+ /**
+ * Initialize the bridge settings.
+ *
+ * @param {Element} displayEl - The widget element we are controlling.
+ */
+ init(displayEl) {
+ if (this._initialized) {
+ return;
+ }
+
+ this._displayEl = displayEl;
+ this._bridgesEl = document.getElementById("tor-bridges-current");
+ this._noBridgesEl = document.getElementById("tor-bridges-none");
+
+ this._sourceLabels = new Map([
+ [
+ lazy.TorBridgeSource.BuiltIn,
+ document.getElementById("tor-bridges-built-in-label"),
+ ],
+ [
+ lazy.TorBridgeSource.UserProvided,
+ document.getElementById("tor-bridges-user-label"),
+ ],
+ [
+ lazy.TorBridgeSource.BridgeDB,
+ document.getElementById("tor-bridges-requested-label"),
+ ],
+ [
+ lazy.TorBridgeSource.Lox,
+ document.getElementById("tor-bridges-lox-label"),
+ ],
+ ]);
+ this._shareEl = document.getElementById("tor-bridges-share");
+
+ this._initBridgesMenu();
+ this._initShareArea();
+
+ gBridgeGrid.init();
+ gBuiltinBridgesArea.init();
+ gLoxStatus.init();
+ this._initialized = true;
+ // Re-trigger our current bridges value to pass on to any descendants.
+ this.bridges = this._bridgesVal;
+ this.connectedBridgeId = this._connectedBridgeId;
+ },
+
+ /**
+ * The bridges value, set by the setting-control element.
+ *
+ * @type {object}
+ */
+ _bridgesVal: null,
+
+ set bridges(val) {
+ if (val === null) {
+ // Corresponds to pending TorSettings initialization, wait for a non-null
+ // value.
+ return;
+ }
+ this._bridgesVal = val;
+ if (!this._initialized) {
+ return;
+ }
+ this._updateSource();
+ this._updateBridgeStrings();
+ // Pass on to descendants.
+ gBridgeGrid.bridges = val;
+ gBuiltinBridgesArea.bridges = val;
+ gLoxStatus.bridges = val;
+ },
+
+ /**
+ * The ID of the currently connected bridge, or `null` if there is none.
+ *
+ * @type {string?}
+ */
+ _connectedBridgeId: null,
+
+ set connectedBridgeId(val) {
+ this._connectedBridgeId = val;
+ if (!this._initialized) {
+ return;
+ }
+ // NOTE: This should be safe to call, even when _bridgesVal is still null.
+ gBridgeGrid.connectedBridgeId = val;
+ gBuiltinBridgesArea.connectedBridgeId = val;
+ },
+
+ /**
+ * The shown bridge source.
+ *
+ * Initially null to indicate that it is unset for the first call to
+ * _updateSource.
+ *
+ * @type {integer?}
+ */
+ _bridgeSource: null,
+ /**
+ * Whether the user is encouraged to share their bridge addresses.
+ *
+ * @type {boolean}
+ */
+ _canShare: false,
+
+ /**
+ * Update _bridgeSource.
+ */
+ _updateSource() {
+ // NOTE: This should only ever be called after TorSettings is already
+ // initialized.
+ const bridgeSource = this._bridgesVal.source;
+ if (bridgeSource === this._bridgeSource) {
+ // Avoid re-activating an area if the source has not changed.
+ return;
+ }
+
+ this._bridgeSource = bridgeSource;
+
+ // Before hiding elements, we determine whether our region contained the
+ // user focus.
+ const hadFocus =
+ this._bridgesEl.contains(document.activeElement) ||
+ this._noBridgesEl.contains(document.activeElement);
+
+ for (const [source, labelEl] of this._sourceLabels.entries()) {
+ labelEl.hidden = source !== bridgeSource;
+ }
+
+ this._canShare =
+ bridgeSource === lazy.TorBridgeSource.UserProvided ||
+ bridgeSource === lazy.TorBridgeSource.BridgeDB;
+
+ this._shareEl.hidden = !this._canShare;
+
+ // Force the menu to close whenever the source changes.
+ // NOTE: If the menu had focus then hadFocus will be true, and focus will be
+ // re-assigned.
+ this._forceCloseBridgesMenu();
+
+ // Update whether we have bridges.
+ this._updateHaveBridges();
+
+ if (hadFocus) {
+ // Always reset the focus to the start of the area whenever the source
+ // changes.
+ lazy.moveFocusToBridgeHeading(window);
+ }
+ },
+
+ /**
+ * Whether we have bridges or not, or null if it is unknown.
+ *
+ * @type {boolean?}
+ */
+ _haveBridges: null,
+
+ /**
+ * Update the _haveBridges value.
+ */
+ _updateHaveBridges() {
+ const haveBridges = this._bridgesVal.haveBridges;
+
+ if (haveBridges === this._haveBridges) {
+ return;
+ }
+
+ this._haveBridges = haveBridges;
+
+ // Add classes to show or hide the "no bridges" and "Your bridges" sections.
+ this._bridgesEl.hidden = !haveBridges;
+ this._noBridgesEl.hidden = haveBridges;
+
+ this._displayEl.classList.toggle("has-tor-bridges", haveBridges);
+ },
+
+ /**
+ * The bridge strings in a copy-able form.
+ *
+ * @type {string}
+ */
+ _bridgeStrings: "",
+ /**
+ * Whether the bridge strings should be shown as a QR code.
+ *
+ * @type {boolean}
+ */
+ _canQRBridges: false,
+
+ /**
+ * Update the stored bridge strings.
+ */
+ _updateBridgeStrings() {
+ const bridges = this._bridgesVal.bridgeStrings;
+
+ this._bridgeStrings = bridges.join("\n");
+ // TODO: Determine what logic we want.
+ this._canQRBridges = bridges.length <= 3;
+
+ this._qrButton.disabled = !this._canQRBridges;
+ },
+
+ /**
+ * Copy all the bridge addresses to the clipboard.
+ */
+ _copyBridges() {
+ const clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ clipboard.copyString(this._bridgeStrings);
+ },
+
+ /**
+ * Open the QR code dialog encoding all the bridge addresses.
+ */
+ _openQR() {
+ if (!this._canQRBridges) {
+ return;
+ }
+ showBridgeQr(this._bridgeStrings);
+ },
+
+ /**
+ * The QR button for copying all QR codes.
+ *
+ * @type {Element?}
+ */
+ _qrButton: null,
+
+ _initShareArea() {
+ document
+ .getElementById("tor-bridges-copy-addresses-button")
+ .addEventListener("click", () => {
+ this._copyBridges();
+ });
+
+ this._qrButton = document.getElementById("tor-bridges-qr-addresses-button");
+ this._qrButton.addEventListener("click", () => {
+ this._openQR();
+ });
+ },
+
+ /**
+ * The menu for all bridges.
+ *
+ * @type {Element?}
+ */
+ _bridgesMenu: null,
+
+ /**
+ * Initialize the menu for all bridges.
+ */
+ _initBridgesMenu() {
+ this._bridgesMenu = document.getElementById("tor-bridges-all-options-menu");
+
+ // NOTE: We generally assume that once the bridge menu is opened the
+ // this._bridgeStrings value will not change.
+ const qrItem = document.getElementById(
+ "tor-bridges-options-qr-all-menu-item"
+ );
+ qrItem.addEventListener("click", () => {
+ this._openQR();
+ });
+
+ const copyItem = document.getElementById(
+ "tor-bridges-options-copy-all-menu-item"
+ );
+ copyItem.addEventListener("click", () => {
+ this._copyBridges();
+ });
+
+ const editItem = document.getElementById(
+ "tor-bridges-options-edit-all-menu-item"
+ );
+ editItem.addEventListener("click", () => {
+ lazy.openUserProvideBridgeDialog(window, "edit");
+ });
+
+ // TODO: Do we want a different item for built-in bridges, rather than
+ // "Remove all bridges"?
+ document
+ .getElementById("tor-bridges-options-remove-all-menu-item")
+ .addEventListener("click", async () => {
+ // TODO: Should we only have a warning when not built-in?
+ const parentWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ const flags =
+ Services.prompt.BUTTON_POS_0 *
+ Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_0_DEFAULT +
+ Services.prompt.BUTTON_DEFAULT_IS_DESTRUCTIVE +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
+
+ const [titleString, bodyString, removeString] =
+ await document.l10n.formatValues([
+ { id: "remove-all-bridges-warning-title" },
+ { id: "remove-all-bridges-warning-description" },
+ { id: "remove-all-bridges-warning-remove-button" },
+ ]);
+
+ // TODO: Update the text, and remove old strings.
+ const buttonIndex = Services.prompt.confirmEx(
+ parentWindow,
+ titleString,
+ bodyString,
+ flags,
+ removeString,
+ null,
+ null,
+ null,
+ {}
+ );
+
+ if (buttonIndex !== 0) {
+ return;
+ }
+
+ lazy.TorSettings.changeSettings({
+ // This should always have the side effect of disabling bridges as
+ // well.
+ bridges: { source: lazy.TorBridgeSource.Invalid },
+ });
+ });
+
+ this._bridgesMenu.addEventListener("showing", () => {
+ qrItem.hidden = !this._canShare || !this._canQRBridges;
+ editItem.hidden =
+ this._bridgeSource !== lazy.TorBridgeSource.UserProvided;
+ });
+
+ const bridgesMenuButton = document.getElementById(
+ "tor-bridges-all-options-button"
+ );
+ bridgesMenuButton.addEventListener("click", event => {
+ this._bridgesMenu.toggle(event, bridgesMenuButton);
+ });
+
+ this._bridgesMenu.addEventListener("hidden", () => {
+ // Make sure the button receives focus again when the menu is hidden.
+ // Currently, panel-list.js only does this when the menu is opened with a
+ // keyboard, but this causes focus to be lost from the page if the user
+ // uses a mixture of keyboard and mouse.
+ bridgesMenuButton.focus();
+ });
+ },
+
+ /**
+ * Force the bridges menu to close.
+ */
+ _forceCloseBridgesMenu() {
+ this._bridgesMenu.hide(null, { force: true });
+ },
+};
+
+// TODO: Replace gBridgeSettings and #tor-bridges-display-template with proper
+// widgets (using MozLitElement).
+/**
+ * Show the current bridges.
+ */
+class TorBridgesDisplay extends HTMLElement {
+ connectedCallback() {
+ if (this.children.length) {
+ return;
+ }
+ // Take the template children since we only expect one instance of this.
+ this.replaceChildren(
+ ...document.getElementById("tor-bridges-display-template").content
+ .childNodes
+ );
+ gBridgeSettings.init(this);
+ }
+
+ set connectedBridgeId(val) {
+ gBridgeSettings.connectedBridgeId = val;
+ }
+
+ set bridges(val) {
+ gBridgeSettings.bridges = val;
+ }
+
+ /**
+ * Focus the "Your bridges" heading, if it is visible.
+ *
+ * @returns {boolean} - `true` if the heading was visible and focused.
+ */
+ focusHeading() {
+ if (!gBridgeSettings._haveBridges) {
+ // Heading is hidden.
+ return false;
+ }
+ document.getElementById("tor-bridges-current-heading-non-search").focus();
+ return true;
+ }
+}
+customElements.define("tor-bridges-display", TorBridgesDisplay);
=====================================
toolkit/content/widgets/moz-fieldset/moz-fieldset.css
=====================================
@@ -46,6 +46,10 @@ h4,
h5,
h6 {
margin: 0;
+
+ &[tabindex]:focus-visible {
+ outline-offset: var(--focus-outline-offset);
+ }
}
.description {
=====================================
toolkit/content/widgets/moz-fieldset/moz-fieldset.mjs
=====================================
@@ -11,12 +11,24 @@ import { MozLitElement } from "../lit-utils.mjs";
* @type {Record<number, (label: string) => ReturnType<typeof html>>}
*/
const HEADING_LEVEL_TEMPLATES = {
- 1: label => html`<h1 class="text-box-trim-start">${label}</h1>`,
- 2: label => html`<h2 class="text-box-trim-start">${label}</h2>`,
- 3: label => html`<h3 class="text-box-trim-start">${label}</h3>`,
- 4: label => html`<h4>${label}</h4>`,
- 5: label => html`<h5>${label}</h5>`,
- 6: label => html`<h6>${label}</h6>`,
+ 1: (label, tabindex) =>
+ html`<h1 class="text-box-trim-start" tabindex=${ifDefined(tabindex)}>
+ ${label}
+ </h1>`,
+ 2: (label, tabindex) =>
+ html`<h2 class="text-box-trim-start" tabindex=${ifDefined(tabindex)}>
+ ${label}
+ </h2>`,
+ 3: (label, tabindex) =>
+ html`<h3 class="text-box-trim-start" tabindex=${ifDefined(tabindex)}>
+ ${label}
+ </h3>`,
+ 4: (label, tabindex) =>
+ html`<h4 tabindex=${ifDefined(tabindex)}>${label}</h4>`,
+ 5: (label, tabindex) =>
+ html`<h5 tabindex=${ifDefined(tabindex)}>${label}</h5>`,
+ 6: (label, tabindex) =>
+ html`<h6 tabindex=${ifDefined(tabindex)}>${label}</h6>`,
};
/**
@@ -42,6 +54,9 @@ export default class MozFieldset extends MozLitElement {
disabled: { type: Boolean, reflect: true },
iconSrc: { type: String },
badge: { type: String },
+ // Allows the heading to be focusable, but not part of the Tab focus cycle.
+ // See tor-browser#45143.
+ focusableHeading: { type: Boolean, attribute: "focusable-heading" },
};
constructor() {
@@ -132,12 +147,25 @@ export default class MozFieldset extends MozLitElement {
legendTemplate() {
let label =
- HEADING_LEVEL_TEMPLATES[this.headingLevel]?.(this.label) || this.label;
+ HEADING_LEVEL_TEMPLATES[this.headingLevel]?.(
+ this.label,
+ this.focusableHeading ? "-1" : undefined
+ ) || this.label;
return html`<legend part="label">
${this.iconTemplate()}${label}${this.badgeTemplate()}
</legend>`;
}
+ /**
+ * Move the user's focus to the heading, if it is focusable.
+ */
+ focusHeading() {
+ if (!this.focusableHeading) {
+ return;
+ }
+ this.shadowRoot.querySelector("h1,h2,h3,h4,h5,h6")?.focus();
+ }
+
iconTemplate() {
if (!this.iconSrc) {
return "";
=====================================
toolkit/locales/en-US/toolkit/global/tor-browser.ftl
=====================================
@@ -152,6 +152,9 @@ tor-connection-status-connect-button =
tor-bridges-heading = Bridges
tor-bridges-overview = Bridges help you securely access the Tor network in places where Tor is blocked. Depending on where you are, one bridge may work better than another.
tor-bridges-learn-more-link = Learn more
+tor-bridges-group =
+ .label = Bridges
+ .description = Bridges help you securely access the Tor network in places where Tor is blocked. Depending on where you are, one bridge may work better than another.
# Toggle button for enabling and disabling the use of bridges.
tor-bridges-use-bridges =
@@ -310,12 +313,23 @@ tor-bridges-lox-got-it-button = Got it
tor-bridges-add-bridges-heading = Add bridges
# Shown as a heading when the user has existing bridges that can be replaced.
tor-bridges-replace-bridges-heading = Replace your bridges
+# Shown as a heading when the user has no current bridges.
+tor-bridges-add-bridges-group =
+ .label = Add bridges
+# Shown as a heading when the user has existing bridges that can be replaced.
+tor-bridges-replace-bridges-group =
+ .label = Replace your bridges
# -brand-short-name refers to 'Tor Browser', localized.
tor-bridges-select-built-in-description = Choose from one of { -brand-short-name }’s built-in bridges
+# -brand-short-name refers to 'Tor Browser', localized.
+tor-bridges-choose-built-in-button =
+ .label = Choose from one of { -brand-short-name }’s built-in bridges
tor-bridges-select-built-in-button = Select a built-in bridge…
tor-bridges-add-addresses-description = Enter bridge addresses you already know
+tor-bridges-enter-bridges-button =
+ .label = Enter bridge addresses you already know
# Shown when the user has no current bridges.
# Opens a dialog where the user can provide a new bridge address or share code.
tor-bridges-add-new-button = Add new bridges…
@@ -326,6 +340,10 @@ tor-bridges-replace-button = Replace bridges…
tor-bridges-find-more-heading = Find more bridges
# "Tor Project" is the organisation name.
tor-bridges-find-more-description = Since many bridge addresses aren’t public, you may need to request some from the Tor Project.
+# "Tor Project" is the organisation name.
+tor-bridges-find-more-group =
+ .label = Find more bridges
+ .description = Since many bridge addresses aren’t public, you may need to request some from the Tor Project.
# "Telegram" is the common brand name of the Telegram Messenger application
tor-bridges-provider-telegram-name = Telegram
@@ -333,6 +351,12 @@ tor-bridges-provider-telegram-name = Telegram
# $telegramUserName (String) - The Telegram Messenger user name that should receive messages. Should be wrapped in '<a data-l10n-name="user">' and '</a>'.
# E.g. in English, "Message GetBridgesBot".
tor-bridges-provider-telegram-instruction = Message <a data-l10n-name="user">{ $telegramUserName }</a>
+# "Telegram" is the common brand name of the Telegram Messenger application.
+# Here "Message" is a verb, short for "Send a message to". This is an instruction to send a message to the given Telegram Messenger user to receive a new bridge.
+# $telegramUserName (String) - The Telegram Messenger user name that should receive messages.
+tor-bridges-source-telegram-link =
+ .label = Telegram
+ .description = Message { $telegramUserName }
# "Web" is the proper noun for the "World Wide Web".
tor-bridges-provider-web-name = Web
@@ -340,15 +364,31 @@ tor-bridges-provider-web-name = Web
# $url (String) - The URL for Tor Project bridges. Should be wrapped in '<a data-l10n-name"url">' and '</a>'.
tor-bridges-provider-web-instruction = Visit <a data-l10n-name="url">{ $url }</a>
+# "Web" is the proper noun for the "World Wide Web".
+# $url (String) - The URL for Tor Project bridges.
+tor-bridges-source-web-link =
+ .label = Web
+ .description = Visit { $url }
+
# "Gmail" is the Google brand name. "Riseup" refers to the Riseup organisation at riseup.net.
tor-bridges-provider-email-name = Gmail or Riseup
# Here "Email" is a verb, short for "Send an email to". This is an instruction to send an email to the given address to receive a new bridge.
# $address (String) - The email address that should receive the email.
# E.g. in English, "Email bridges(a)torproject.org".
tor-bridges-provider-email-instruction = Email { $address }
+# "Gmail" is the Google brand name. "Riseup" refers to the Riseup organisation at riseup.net.
+# Here "Email" is a verb, short for "Send an email to". This is an instruction to send an email to the given address to receive a new bridge.
+# $address (String) - The email address that should receive the email.
+# E.g. in English, "Email bridges(a)torproject.org".
+tor-bridges-source-email-link =
+ .label = Gmail or Riseup
+ .description = Email { $address }
tor-bridges-request-from-browser = You can also get bridges from the bridge bot without leaving { -brand-short-name }.
tor-bridges-request-button = Request bridges…
+tor-bridges-request-from-browser2 = Get bridges from the bridge bot without leaving { -brand-short-name }.
+tor-bridges-request-button2 =
+ .label = Request bridges…
## Warning dialog when removing all bridges.
=====================================
toolkit/modules/TorSettings.sys.mjs
=====================================
@@ -358,6 +358,12 @@ class TorSettingsImpl {
*/
#initialized = false;
+ /**
+ * Whether init has been called.
+ *
+ * @type {boolean}
+ */
+ #initCalled = false;
/**
* Whether uninit cleanup has been called.
*
@@ -510,11 +516,12 @@ class TorSettingsImpl {
* Load or init our settings.
*/
async init() {
- if (this.#initialized) {
+ if (this.#initCalled) {
lazy.logger.warn("Called init twice.");
await this.#initializedPromise;
return;
}
+ this.#initCalled = true;
try {
await this.#initInternal();
this.#initialized = true;
=====================================
tools/torbrowser/l10n/migrations/bug-45058-bridge-settings.py
=====================================
@@ -0,0 +1,102 @@
+import re
+
+import fluent.syntax.ast as FTL
+from fluent.migrate.helpers import transforms_from
+from fluent.migrate.transforms import COPY_PATTERN, FluentSource
+from fluent.syntax.visitor import Visitor
+
+
+class RemoveAnchorVisitor(Visitor):
+ """Class to remove <a> and </a> wrappers from a Fluent TextElement."""
+
+ def __init__(self):
+ # Good enough regex for our needs that will match starting and ending
+ # tags.
+ self._anchor_regex = re.compile(r"<\/?[aA](| [^>]*)>")
+ super().__init__()
+
+ def visit_TextElement(self, node):
+ node.value = self._anchor_regex.sub("", node.value)
+
+
+class RemoveAnchorTransform(FluentSource):
+ """Class to remove <a> and </a> wrappers from a Fluent source."""
+
+ def __call__(self, ctx):
+ pattern = ctx.get_fluent_source_pattern(self.path, self.key).clone()
+ # Visit every node in the pattern, replacing each TextElement's content.
+ RemoveAnchorVisitor().visit(pattern)
+ return pattern
+
+
+def migrate(ctx):
+ ctx.add_transforms(
+ "tor-browser.ftl",
+ "tor-browser.ftl",
+ transforms_from(
+ """
+tor-bridges-group =
+ .label = { COPY_PATTERN(path, "tor-bridges-heading") }
+ .description = { COPY_PATTERN(path, "tor-bridges-overview") }
+tor-bridges-add-bridges-group =
+ .label = { COPY_PATTERN(path, "tor-bridges-add-bridges-heading") }
+tor-bridges-replace-bridges-group =
+ .label = { COPY_PATTERN(path, "tor-bridges-replace-bridges-heading") }
+tor-bridges-choose-built-in-button =
+ .label = { COPY_PATTERN(path, "tor-bridges-select-built-in-description") }
+tor-bridges-enter-bridges-button =
+ .label = { COPY_PATTERN(path, "tor-bridges-add-addresses-description") }
+tor-bridges-find-more-group =
+ .label = { COPY_PATTERN(path, "tor-bridges-find-more-heading") }
+ .description = { COPY_PATTERN(path, "tor-bridges-find-more-description") }
+tor-bridges-request-button2 =
+ .label = { COPY_PATTERN(path, "tor-bridges-request-button") }
+tor-bridges-source-email-link =
+ .label = { COPY_PATTERN(path, "tor-bridges-provider-email-name") }
+ .description = { COPY_PATTERN(path, "tor-bridges-provider-email-instruction") }
+""",
+ path="tor-browser.ftl",
+ )
+ + [
+ FTL.Message(
+ id=FTL.Identifier("tor-bridges-source-telegram-link"),
+ value=None,
+ attributes=[
+ FTL.Attribute(
+ id=FTL.Identifier("label"),
+ value=COPY_PATTERN(
+ "tor-browser.ftl",
+ "tor-bridges-provider-telegram-name",
+ ),
+ ),
+ FTL.Attribute(
+ id=FTL.Identifier("description"),
+ value=RemoveAnchorTransform(
+ "tor-browser.ftl",
+ "tor-bridges-provider-telegram-instruction",
+ ),
+ ),
+ ],
+ ),
+ FTL.Message(
+ id=FTL.Identifier("tor-bridges-source-web-link"),
+ value=None,
+ attributes=[
+ FTL.Attribute(
+ id=FTL.Identifier("label"),
+ value=COPY_PATTERN(
+ "tor-browser.ftl",
+ "tor-bridges-provider-web-name",
+ ),
+ ),
+ FTL.Attribute(
+ id=FTL.Identifier("description"),
+ value=RemoveAnchorTransform(
+ "tor-browser.ftl",
+ "tor-bridges-provider-web-instruction",
+ ),
+ ),
+ ],
+ ),
+ ],
+ )
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/7a41b6…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/7a41b6…
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
1
0
[Git][tpo/applications/tor-browser][tor-browser-153.0esr-16.0-1] 2 commits: fixup! [android] Modify build system
by Pier Angelo Vendrame (@pierov) 04 Aug '26
by Pier Angelo Vendrame (@pierov) 04 Aug '26
04 Aug '26
Pier Angelo Vendrame pushed to branch tor-browser-153.0esr-16.0-1 at The Tor Project / Applications / Tor Browser
Commits:
e76d7d68 by Pier Angelo Vendrame at 2026-08-04T13:06:48+00:00
fixup! [android] Modify build system
Revert "fixup! [android] Modify build system"
This reverts commit b827102ad26c2e1b10cdee1ba4cb568e034f4020.
- - - - -
7a41b6fc by Pier Angelo Vendrame at 2026-08-04T13:06:48+00:00
fixup! TB 42669: [android] Use custom no-op app-services
Revert "fixup! TB 42669: [android] Use custom no-op app-services"
This reverts commit becb4a94d8c9185a02c157f395cb0de2e079957c.
- - - - -
4 changed files:
- mobile/android/fenix/app/build.gradle
- mobile/android/gradle/plugins/nimbus-gradle-plugin/src/main/kotlin/org/mozilla/appservices/tooling/nimbus/NimbusAssembleToolsTask.kt
- mobile/android/gradle/plugins/nimbus-gradle-plugin/src/main/kotlin/org/mozilla/appservices/tooling/nimbus/NimbusGradlePlugin.kt
- mobile/android/moz.configure
Changes:
=====================================
mobile/android/fenix/app/build.gradle
=====================================
@@ -291,12 +291,18 @@ androidComponents {
onVariants(selector().all()) { variant ->
def buildType = variant.buildType
+ // When this is set, a-s doesn't attempt to download NIMBUS_FML.
+ if (gradle.mozconfig.substs.NIMBUS_FML) {
+ System.setProperty("nimbusFml", gradle.mozconfig.substs.NIMBUS_FML)
+ }
+
def disableTor = providers.gradleProperty("disableTor").getOrElse(false)
project.logger.debug("----------------------------------------------")
project.logger.debug("Variant name: " + variant.name)
project.logger.debug("Build type: " + buildType)
project.logger.debug("Flavor: " + variant.flavorName)
+ project.logger.debug("nimbusFml: " + providers.gradleProperty("nimbusFml").getOrNull())
project.logger.debug("Tor is disabled: " + disableTor)
variant.buildConfigFields.put("DISABLE_TOR", new BuildConfigField("boolean", "$disableTor", null))
=====================================
mobile/android/gradle/plugins/nimbus-gradle-plugin/src/main/kotlin/org/mozilla/appservices/tooling/nimbus/NimbusAssembleToolsTask.kt
=====================================
@@ -28,6 +28,11 @@ import java.net.URI
import java.security.MessageDigest
import javax.inject.Inject
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.nio.file.StandardCopyOption
+
/**
* A task that fetches a prebuilt `nimbus-fml` binary for the current platform.
*
@@ -113,6 +118,17 @@ abstract class NimbusAssembleToolsTask : DefaultTask() {
@TaskAction
fun assembleTools() {
+ var nimbusFml = System.getenv("NIMBUS_FML") ?: ""
+ if (nimbusFml == "") {
+ nimbusFml = System.getProperty("nimbusFml", "")
+ }
+ if (nimbusFml != "") {
+ val source = File(nimbusFml).toPath()
+ val dest = fmlBinary.get().asFile.toPath()
+ Files.copy(source, dest, StandardCopyOption.REPLACE_EXISTING)
+ return
+ }
+
val binaryFile = fmlBinary.get().asFile
val archiveFileObj = archiveFile.get().asFile
val hashFileObj = hashFile.get().asFile
=====================================
mobile/android/gradle/plugins/nimbus-gradle-plugin/src/main/kotlin/org/mozilla/appservices/tooling/nimbus/NimbusGradlePlugin.kt
=====================================
@@ -118,20 +118,20 @@ class NimbusPlugin : Plugin<Project> {
@Suppress("UNCHECKED_CAST")
val mozconfigSubsts = mozconfig?.get("substs") as? Map<String, Any>
- // This is subtle. We capture `NIMBUS_FML` in the configuration cache as a `String`.
- // If we access `project.gradle...` in the `provider` `Callable` below, we capture the
- // `Project` in the configuration cache, which is not desirable.
- //
- // We can't produce a `File` immediately, because in some configurations, namely
- // `android-gradle-dependencies` tasks, `NIMBUS_FML` is legitimately unset (`null`). So
- // we pass strings around and map to `File` types lazily "by hand".
- //
- // Finally: if this process fails, including with an exception, the framework swallows
- // the details and says something like `MissingValueException`, which can be hard to
- // interpret. Hence, this explanation of the details.
- val nimbusFmlPath = mozconfigSubsts?.get("NIMBUS_FML") as? String
-
- if (nimbusFmlPath != null) {
+ if (mozconfigSubsts?.get("MOZ_APPSERVICES_IN_TREE").isTruthy()) {
+ // This is subtle. We capture `NIMBUS_FML` in the configuration cache as a `String`.
+ // If we access `project.gradle...` in the `provider` `Callable` below, we capture the
+ // `Project` in the configuration cache, which is not desirable.
+ //
+ // We can't produce a `File` immediately, because in some configurations, namely
+ // `android-gradle-dependencies` tasks, `NIMBUS_FML` is legitimately unset (`null`). So
+ // we pass strings around and map to `File` types lazily "by hand".
+ //
+ // Finally: if this process fails, including with an exception, the framework swallows
+ // the details and says something like `MissingValueException`, which can be hard to
+ // interpret. Hence, this explanation of the details.
+ val nimbusFmlPath = mozconfigSubsts?.get("NIMBUS_FML") as? String
+
val fmlBinaryString = project.providers.provider {
nimbusFmlPath
}
=====================================
mobile/android/moz.configure
=====================================
@@ -216,8 +216,9 @@ def check_host_bin_prog(var, prog, toolchain=None):
)
-check_host_bin_prog("EMBEDDED_UNIFFI_BINDGEN", "embedded-uniffi-bindgen")
-check_host_bin_prog("NIMBUS_FML", "nimbus-fml")
+# tb-44669 - this was added in rebase 148 to make android compile but tracking potentially better fix
+#check_host_bin_prog("EMBEDDED_UNIFFI_BINDGEN", "embedded-uniffi-bindgen")
+#check_host_bin_prog("NIMBUS_FML", "nimbus-fml")
project_flag(
"MOZ_ANDROID_NETWORK_STATE",
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/fd46fa…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/fd46fa…
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
1
0
[Git][tpo/applications/mullvad-browser][mullvad-browser-153.0esr-16.0-1] fixup! BB 43072: Add aria label and description to moz-message-bar.
by morgan (@morgan) 04 Aug '26
by morgan (@morgan) 04 Aug '26
04 Aug '26
morgan pushed to branch mullvad-browser-153.0esr-16.0-1 at The Tor Project / Applications / Mullvad Browser
Commits:
14873cf2 by Henry Wilkes at 2026-08-04T13:01:38+00:00
fixup! BB 43072: Add aria label and description to moz-message-bar.
BB 45186: Remove duplicate alert roles.
We also restrict the `aria-labelledby` and `aria-describedby` attributes
to only be used with the "alert" role.
- - - - -
2 changed files:
- toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs
- toolkit/content/widgets/notificationbox.js
Changes:
=====================================
toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs
=====================================
@@ -65,8 +65,9 @@ export default class MozMessageBar extends MozLitElement {
supportPage: { type: String },
messageL10nId: { type: String },
messageL10nArgs: { type: String },
- role: { type: String, reflect: true },
- useAlertRole: { type: Boolean },
+ // Move the role from the widget to its shadow root, where we can apply
+ // aria-labelledby and aria-describedby. tor-browser#45186.
+ role: { type: String, mapped: true },
};
constructor() {
@@ -127,8 +128,6 @@ export default class MozMessageBar extends MozLitElement {
* @type {string}
*/
this.role = "alert";
-
- this.useAlertRole = true;
}
onActionSlotchange() {
@@ -170,17 +169,6 @@ export default class MozMessageBar extends MozLitElement {
></slot>`;
}
- setAlertRole() {
- // Wait a little for this to render before setting the role for more
- // consistent alerts to screen readers.
- this.useAlertRole = false;
- window.requestAnimationFrame(() => {
- window.requestAnimationFrame(() => {
- this.useAlertRole = true;
- });
- });
- }
-
iconTemplate() {
let iconData = messageTypeToIconData[this.type];
if (iconData) {
@@ -224,6 +212,16 @@ export default class MozMessageBar extends MozLitElement {
}
render() {
+ let ariaLabelledBy;
+ let ariaDescribedBy;
+ if (this.role === "alert") {
+ if (this.heading) {
+ ariaLabelledBy = "heading";
+ ariaDescribedBy = "content";
+ } else {
+ ariaLabelledBy = "content";
+ }
+ }
return html`
<link
rel="stylesheet"
@@ -231,9 +229,9 @@ export default class MozMessageBar extends MozLitElement {
/>
<div
class="container"
- role=${ifDefined(this.useAlertRole ? "alert" : undefined)}
- aria-labelledby=${this.heading ? "heading" : "content"}
- aria-describedby=${ifDefined(this.heading ? "content" : undefined)}
+ role=${ifDefined(this.role || undefined)}
+ aria-labelledby=${ifDefined(ariaLabelledBy)}
+ aria-describedby=${ifDefined(ariaDescribedBy)}
>
${this.iconTemplate()}
<div class="content">
=====================================
toolkit/content/widgets/notificationbox.js
=====================================
@@ -492,6 +492,20 @@
this.control.removeNotification(this);
}
+ setAlertRole() {
+ // Wait a little for this to render before setting the role for more
+ // consistent alerts to screen readers.
+ // tor-browser#45186: "role" is a mapped attribute, so `removeAttribute`
+ // will go undetected by the moz-message-bar widget. Instead we set the
+ // role property directly.
+ this.role = undefined;
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ this.role = "alert";
+ });
+ });
+ }
+
handleEvent(e) {
// If clickjacking delay is active, prevent any "click"/"command" from
// going through. Also restart the delay if the user tries to click too early.
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/commit/148…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/commit/148…
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
1
0
[Git][tpo/applications/tor-browser][tor-browser-153.0esr-16.0-1] fixup! BB 43072: Add aria label and description to moz-message-bar.
by morgan (@morgan) 04 Aug '26
by morgan (@morgan) 04 Aug '26
04 Aug '26
morgan pushed to branch tor-browser-153.0esr-16.0-1 at The Tor Project / Applications / Tor Browser
Commits:
fd46faa3 by Henry Wilkes at 2026-08-04T12:43:06+00:00
fixup! BB 43072: Add aria label and description to moz-message-bar.
BB 45186: Remove duplicate alert roles.
We also restrict the `aria-labelledby` and `aria-describedby` attributes
to only be used with the "alert" role.
- - - - -
2 changed files:
- toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs
- toolkit/content/widgets/notificationbox.js
Changes:
=====================================
toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs
=====================================
@@ -65,8 +65,9 @@ export default class MozMessageBar extends MozLitElement {
supportPage: { type: String },
messageL10nId: { type: String },
messageL10nArgs: { type: String },
- role: { type: String, reflect: true },
- useAlertRole: { type: Boolean },
+ // Move the role from the widget to its shadow root, where we can apply
+ // aria-labelledby and aria-describedby. tor-browser#45186.
+ role: { type: String, mapped: true },
};
constructor() {
@@ -127,8 +128,6 @@ export default class MozMessageBar extends MozLitElement {
* @type {string}
*/
this.role = "alert";
-
- this.useAlertRole = true;
}
onActionSlotchange() {
@@ -170,17 +169,6 @@ export default class MozMessageBar extends MozLitElement {
></slot>`;
}
- setAlertRole() {
- // Wait a little for this to render before setting the role for more
- // consistent alerts to screen readers.
- this.useAlertRole = false;
- window.requestAnimationFrame(() => {
- window.requestAnimationFrame(() => {
- this.useAlertRole = true;
- });
- });
- }
-
iconTemplate() {
let iconData = messageTypeToIconData[this.type];
if (iconData) {
@@ -224,6 +212,16 @@ export default class MozMessageBar extends MozLitElement {
}
render() {
+ let ariaLabelledBy;
+ let ariaDescribedBy;
+ if (this.role === "alert") {
+ if (this.heading) {
+ ariaLabelledBy = "heading";
+ ariaDescribedBy = "content";
+ } else {
+ ariaLabelledBy = "content";
+ }
+ }
return html`
<link
rel="stylesheet"
@@ -231,9 +229,9 @@ export default class MozMessageBar extends MozLitElement {
/>
<div
class="container"
- role=${ifDefined(this.useAlertRole ? "alert" : undefined)}
- aria-labelledby=${this.heading ? "heading" : "content"}
- aria-describedby=${ifDefined(this.heading ? "content" : undefined)}
+ role=${ifDefined(this.role || undefined)}
+ aria-labelledby=${ifDefined(ariaLabelledBy)}
+ aria-describedby=${ifDefined(ariaDescribedBy)}
>
${this.iconTemplate()}
<div class="content">
=====================================
toolkit/content/widgets/notificationbox.js
=====================================
@@ -492,6 +492,20 @@
this.control.removeNotification(this);
}
+ setAlertRole() {
+ // Wait a little for this to render before setting the role for more
+ // consistent alerts to screen readers.
+ // tor-browser#45186: "role" is a mapped attribute, so `removeAttribute`
+ // will go undetected by the moz-message-bar widget. Instead we set the
+ // role property directly.
+ this.role = undefined;
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ this.role = "alert";
+ });
+ });
+ }
+
handleEvent(e) {
// If clickjacking delay is active, prevent any "click"/"command" from
// going through. Also restart the delay if the user tries to click too early.
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/fd46faa…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/fd46faa…
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
1
0
[Git][tpo/applications/mullvad-browser][mullvad-browser-153.0esr-16.0-1] fixup! BB 42730: Patch RemoteSettings to use only local dumps as a data source
by morgan (@morgan) 04 Aug '26
by morgan (@morgan) 04 Aug '26
04 Aug '26
morgan pushed to branch mullvad-browser-153.0esr-16.0-1 at The Tor Project / Applications / Mullvad Browser
Commits:
5bff9b07 by Pier Angelo Vendrame at 2026-08-04T12:41:31+00:00
fixup! BB 42730: Patch RemoteSettings to use only local dumps as a data source
BB 45185: Make sure the Rust remote settings client stays offline.
- - - - -
2 changed files:
- services/settings/Utils.sys.mjs
- third_party/application-services/components/viaduct/src/lib.rs
Changes:
=====================================
services/settings/Utils.sys.mjs
=====================================
@@ -113,6 +113,9 @@ export var Utils = {
log,
get shouldSkipRemoteActivity() {
+ if (AppConstants.BASE_BROWSER_VERSION) {
+ return true;
+ }
if (
(lazy.isRunningTests || Cu.isInAutomation) &&
this.SERVER_URL == "data:,#remote-settings-dummy/v1"
=====================================
third_party/application-services/components/viaduct/src/lib.rs
=====================================
@@ -96,7 +96,8 @@ impl Request {
}
pub fn send(self) -> Result<Response, ViaductError> {
- crate::backend::send(self)
+ // tor-browser#44576: Force an error as a defense-in-depth.
+ Err(ViaductError::NetworkError("Viaduct requests are disabled.".into()))
}
/// Alias for `Request::new(Method::Get, url)`, for convenience.
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/commit/5bf…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/commit/5bf…
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
1
0
[Git][tpo/applications/tor-browser][tor-browser-153.0esr-16.0-1] fixup! BB 42730: Patch RemoteSettings to use only local dumps as a data source
by Pier Angelo Vendrame (@pierov) 04 Aug '26
by Pier Angelo Vendrame (@pierov) 04 Aug '26
04 Aug '26
Pier Angelo Vendrame pushed to branch tor-browser-153.0esr-16.0-1 at The Tor Project / Applications / Tor Browser
Commits:
4cb78731 by Pier Angelo Vendrame at 2026-08-04T12:37:03+00:00
fixup! BB 42730: Patch RemoteSettings to use only local dumps as a data source
BB 45185: Make sure the Rust remote settings client stays offline.
- - - - -
2 changed files:
- services/settings/Utils.sys.mjs
- third_party/application-services/components/viaduct/src/lib.rs
Changes:
=====================================
services/settings/Utils.sys.mjs
=====================================
@@ -113,6 +113,9 @@ export var Utils = {
log,
get shouldSkipRemoteActivity() {
+ if (AppConstants.BASE_BROWSER_VERSION) {
+ return true;
+ }
if (
(lazy.isRunningTests || Cu.isInAutomation) &&
this.SERVER_URL == "data:,#remote-settings-dummy/v1"
=====================================
third_party/application-services/components/viaduct/src/lib.rs
=====================================
@@ -96,7 +96,8 @@ impl Request {
}
pub fn send(self) -> Result<Response, ViaductError> {
- crate::backend::send(self)
+ // tor-browser#44576: Force an error as a defense-in-depth.
+ Err(ViaductError::NetworkError("Viaduct requests are disabled.".into()))
}
/// Alias for `Request::new(Method::Get, url)`, for convenience.
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/4cb7873…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/4cb7873…
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
1
0
[Git][tpo/applications/mullvad-browser][mullvad-browser-153.0esr-16.0-1] 2 commits: BB 44629: Update the search settings for Base Browser.
by morgan (@morgan) 04 Aug '26
by morgan (@morgan) 04 Aug '26
04 Aug '26
morgan pushed to branch mullvad-browser-153.0esr-16.0-1 at The Tor Project / Applications / Mullvad Browser
Commits:
a806677c by Henry Wilkes at 2026-08-04T11:53:41+00:00
BB 44629: Update the search settings for Base Browser.
- - - - -
26523f6e by Henry Wilkes at 2026-08-04T11:53:42+00:00
fixup! Base Browser strings
BB 44629: Add a search suggestions banner.
- - - - -
2 changed files:
- browser/components/preferences/config/search.mjs
- toolkit/locales/en-US/toolkit/global/base-browser.ftl
Changes:
=====================================
browser/components/preferences/config/search.mjs
=====================================
@@ -271,6 +271,11 @@ Preferences.addSetting({
pref: "browser.urlbar.trending.featureGate",
});
+// Add a warning banner. tor-browser#44629.
+Preferences.addSetting({
+ id: "searchSuggestionsWarningBanner",
+});
+
// The show search suggestion box behaves differently depending on whether the
// separate search bar is shown. When the separate search bar is shown, it
// controls just the search suggestion preference, and the
@@ -1147,6 +1152,16 @@ SettingGroupManager.registerGroups({
l10nId: "search-suggestions-header-2",
headingLevel: 2,
items: [
+ // Add a warning banner. tor-browser#44629.
+ {
+ id: "searchSuggestionsWarningBanner",
+ l10nId: "search-suggestions-warning-banner",
+ control: "moz-message-bar",
+ controlAttrs: {
+ role: "status",
+ type: "warning",
+ },
+ },
{
id: "suggestionsInSearchFieldsCheckbox",
l10nId: "search-show-suggestions-option",
=====================================
toolkit/locales/en-US/toolkit/global/base-browser.ftl
=====================================
@@ -110,6 +110,11 @@ browser-layout-show-sidebar-limited =
.label = Show sidebar
.description = Quickly access bookmarks and more without leaving your main view.
+## Preferences - Search suggestions.
+
+search-suggestions-warning-banner =
+ .message = Turning this on reduces your privacy by sharing your queries with the search engine as you type.
+
## Preferences - Contrast Control.
preferences-contrast-control-fixed-color2 =
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/compare/e8…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/compare/e8…
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
1
0
[Git][tpo/applications/tor-browser][tor-browser-153.0esr-16.0-1] 2 commits: BB 44629: Update the search settings for Base Browser.
by morgan (@morgan) 04 Aug '26
by morgan (@morgan) 04 Aug '26
04 Aug '26
morgan pushed to branch tor-browser-153.0esr-16.0-1 at The Tor Project / Applications / Tor Browser
Commits:
09b63226 by Henry Wilkes at 2026-08-04T11:52:11+00:00
BB 44629: Update the search settings for Base Browser.
- - - - -
f1546785 by Henry Wilkes at 2026-08-04T11:52:11+00:00
fixup! Base Browser strings
BB 44629: Add a search suggestions banner.
- - - - -
2 changed files:
- browser/components/preferences/config/search.mjs
- toolkit/locales/en-US/toolkit/global/base-browser.ftl
Changes:
=====================================
browser/components/preferences/config/search.mjs
=====================================
@@ -271,6 +271,11 @@ Preferences.addSetting({
pref: "browser.urlbar.trending.featureGate",
});
+// Add a warning banner. tor-browser#44629.
+Preferences.addSetting({
+ id: "searchSuggestionsWarningBanner",
+});
+
// The show search suggestion box behaves differently depending on whether the
// separate search bar is shown. When the separate search bar is shown, it
// controls just the search suggestion preference, and the
@@ -1147,6 +1152,16 @@ SettingGroupManager.registerGroups({
l10nId: "search-suggestions-header-2",
headingLevel: 2,
items: [
+ // Add a warning banner. tor-browser#44629.
+ {
+ id: "searchSuggestionsWarningBanner",
+ l10nId: "search-suggestions-warning-banner",
+ control: "moz-message-bar",
+ controlAttrs: {
+ role: "status",
+ type: "warning",
+ },
+ },
{
id: "suggestionsInSearchFieldsCheckbox",
l10nId: "search-show-suggestions-option",
=====================================
toolkit/locales/en-US/toolkit/global/base-browser.ftl
=====================================
@@ -110,6 +110,11 @@ browser-layout-show-sidebar-limited =
.label = Show sidebar
.description = Quickly access bookmarks and more without leaving your main view.
+## Preferences - Search suggestions.
+
+search-suggestions-warning-banner =
+ .message = Turning this on reduces your privacy by sharing your queries with the search engine as you type.
+
## Preferences - Contrast Control.
preferences-contrast-control-fixed-color2 =
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/5504dc…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/5504dc…
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
1
0
[Git][tpo/applications/mullvad-browser][mullvad-browser-153.0esr-16.0-1] fixup! Firefox preference overrides.
by morgan (@morgan) 04 Aug '26
by morgan (@morgan) 04 Aug '26
04 Aug '26
morgan pushed to branch mullvad-browser-153.0esr-16.0-1 at The Tor Project / Applications / Mullvad Browser
Commits:
e84efe02 by Henry Wilkes at 2026-08-04T11:50:12+00:00
fixup! Firefox preference overrides.
BB 45123: Disable profile backup service.
- - - - -
1 changed file:
- browser/app/profile/001-base-profile.js
Changes:
=====================================
browser/app/profile/001-base-profile.js
=====================================
@@ -130,6 +130,9 @@ pref("browser.pagethumbnails.capturing_disabled", true);
// Users can still opt-in if they wish.
pref("browser.backup.enabled", false);
pref("browser.backup.scheduled.enabled", false);
+// tor-browser#45123: Disable the profile automated backup and restore service.
+pref("browser.backup.archive.enabled", false);
+pref("browser.backup.restore.enabled", false);
// Empty clipboard content from private windows on exit (tor-browser#42154)
pref("browser.privatebrowsing.preserveClipboard", false);
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/commit/e84…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/commit/e84…
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
1
0