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
-
550bff23
by Henry Wilkes at 2026-08-04T13:23:04+00:00
-
5088e918
by Henry Wilkes at 2026-08-04T13:23:04+00:00
-
1ab514d3
by Henry Wilkes at 2026-08-04T13:23:04+00:00
-
d064e421
by Henry Wilkes at 2026-08-04T13:23:04+00:00
-
2a72140f
by Henry Wilkes at 2026-08-04T13:23:04+00:00
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:
| ... | ... | @@ -271,7 +271,7 @@ const CONFIG_PANES = Object.freeze({ |
| 271 | 271 | connection: {
|
| 272 | 272 | l10nId: "tor-connection-settings-pane",
|
| 273 | 273 | iconSrc: "chrome://browser/content/torconnect/tor-connect.svg",
|
| 274 | - groupIds: ["connectionStatus"],
|
|
| 274 | + groupIds: ["connectionStatus", "torBridges"],
|
|
| 275 | 275 | module: "chrome://browser/content/torpreferences/config/connection.mjs",
|
| 276 | 276 | visible: () => {
|
| 277 | 277 | return TorConnect.enabled;
|
| ... | ... | @@ -108,6 +108,7 @@ |
| 108 | 108 | <script type="module" src="chrome://browser/content/preferences/widgets/update-information.mjs"></script>
|
| 109 | 109 | <script type="module" src="chrome://browser/content/preferences/widgets/update-state.mjs"></script>
|
| 110 | 110 | <script type="module" src="chrome://browser/content/ipprotection/bandwidth-usage.mjs"></script>
|
| 111 | + <script type="module" src="chrome://browser/content/torpreferences/widgets/tor-bridges-display.mjs"></script>
|
|
| 111 | 112 | <script type="module" src="chrome://browser/content/torpreferences/widgets/tor-connection-status.mjs"></script>
|
| 112 | 113 | <script src="chrome://browser/content/torpreferences/bridgemoji/BridgeEmoji.js"/>
|
| 113 | 114 | </head>
|
| ... | ... | @@ -4,11 +4,33 @@ import { Preferences } from "chrome://global/content/preferences/Preferences.mjs |
| 4 | 4 | const lazy = {};
|
| 5 | 5 | ChromeUtils.defineESModuleGetters(lazy, {
|
| 6 | 6 | InternetStatus: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
|
| 7 | + openBridgeDialog:
|
|
| 8 | + "chrome://browser/content/torpreferences/config/helpers.mjs",
|
|
| 9 | + openUserProvideBridgeDialog:
|
|
| 10 | + "chrome://browser/content/torpreferences/config/helpers.mjs",
|
|
| 11 | + TorBridgeSource: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
|
|
| 7 | 12 | TorConnect: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
|
| 8 | 13 | TorConnectStage: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
|
| 9 | 14 | TorConnectTopics: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
|
| 15 | + TorProviderBuilder:
|
|
| 16 | + "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
|
|
| 17 | + TorProviderState:
|
|
| 18 | + "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
|
|
| 19 | + TorProviderTopics:
|
|
| 20 | + "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
|
|
| 21 | + TorSettings: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
|
|
| 22 | + TorSettingsTopics: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
|
|
| 10 | 23 | });
|
| 11 | 24 | |
| 25 | +// TODO: Change to GetLoxBridges if Lox enabled, and the account is set up.
|
|
| 26 | +const TELEGRAM_USER_NAME = "GetBridgesBot";
|
|
| 27 | +const TELEGRAM_HREF = `https://t.me/${TELEGRAM_USER_NAME}`;
|
|
| 28 | + |
|
| 29 | +const TOR_BRIDGES_URL_NAME = "bridges.torproject.org";
|
|
| 30 | +const TOR_BRIDGES_HREF = "https://bridges.torproject.org";
|
|
| 31 | + |
|
| 32 | +const TOR_BRIDGES_EMAIL = "bridges@torproject.org";
|
|
| 33 | + |
|
| 12 | 34 | SettingGroupManager.registerGroups({
|
| 13 | 35 | connectionStatus: {
|
| 14 | 36 | inProgress: true,
|
| ... | ... | @@ -39,6 +61,123 @@ SettingGroupManager.registerGroups({ |
| 39 | 61 | },
|
| 40 | 62 | ],
|
| 41 | 63 | },
|
| 64 | + torBridges: {
|
|
| 65 | + inProgress: true,
|
|
| 66 | + l10nId: "tor-bridges-group",
|
|
| 67 | + supportPage: "tor-manual:bridges",
|
|
| 68 | + headingLevel: 2,
|
|
| 69 | + controlAttrs: { "focusable-heading": true },
|
|
| 70 | + items: [
|
|
| 71 | + {
|
|
| 72 | + id: "bridgesEnabled",
|
|
| 73 | + l10nId: "tor-bridges-use-bridges",
|
|
| 74 | + control: "moz-toggle",
|
|
| 75 | + },
|
|
| 76 | + {
|
|
| 77 | + id: "torBridgesDisplay",
|
|
| 78 | + control: "tor-bridges-display",
|
|
| 79 | + },
|
|
| 80 | + {
|
|
| 81 | + id: "newBridgesGroup",
|
|
| 82 | + control: "moz-fieldset",
|
|
| 83 | + controlAttrs: {
|
|
| 84 | + headinglevel: 3,
|
|
| 85 | + },
|
|
| 86 | + options: [
|
|
| 87 | + {
|
|
| 88 | + control: "moz-box-group",
|
|
| 89 | + items: [
|
|
| 90 | + {
|
|
| 91 | + id: "builtinBridges",
|
|
| 92 | + l10nId: "tor-bridges-choose-built-in-button",
|
|
| 93 | + control: "moz-box-button",
|
|
| 94 | + },
|
|
| 95 | + {
|
|
| 96 | + id: "userProvidedBridges",
|
|
| 97 | + l10nId: "tor-bridges-enter-bridges-button",
|
|
| 98 | + control: "moz-box-button",
|
|
| 99 | + },
|
|
| 100 | + ],
|
|
| 101 | + },
|
|
| 102 | + ],
|
|
| 103 | + },
|
|
| 104 | + {
|
|
| 105 | + id: "findMoreBridgesGroup",
|
|
| 106 | + l10nId: "tor-bridges-find-more-group",
|
|
| 107 | + control: "moz-fieldset",
|
|
| 108 | + controlAttrs: {
|
|
| 109 | + headinglevel: 3,
|
|
| 110 | + },
|
|
| 111 | + options: [
|
|
| 112 | + {
|
|
| 113 | + control: "moz-box-item",
|
|
| 114 | + options: [
|
|
| 115 | + {
|
|
| 116 | + id: "torBridgesRequestBanner",
|
|
| 117 | + control: "article",
|
|
| 118 | + options: [
|
|
| 119 | + {
|
|
| 120 | + control: "img",
|
|
| 121 | + controlAttrs: {
|
|
| 122 | + alt: "",
|
|
| 123 | + src: "chrome://browser/content/torpreferences/bridge-bot.svg",
|
|
| 124 | + },
|
|
| 125 | + },
|
|
| 126 | + {
|
|
| 127 | + control: "p",
|
|
| 128 | + l10nId: "tor-bridges-request-from-browser2",
|
|
| 129 | + },
|
|
| 130 | + {
|
|
| 131 | + // NOTE: We use the wrapping `div` to simply switch from the
|
|
| 132 | + // `options` context to the `items` context, with the latter
|
|
| 133 | + // wrapping the elements in a setting-control.
|
|
| 134 | + control: "div",
|
|
| 135 | + items: [
|
|
| 136 | + {
|
|
| 137 | + id: "requestBridges",
|
|
| 138 | + l10nId: "tor-bridges-request-button2",
|
|
| 139 | + control: "moz-button",
|
|
| 140 | + },
|
|
| 141 | + ],
|
|
| 142 | + },
|
|
| 143 | + ],
|
|
| 144 | + },
|
|
| 145 | + ],
|
|
| 146 | + },
|
|
| 147 | + {
|
|
| 148 | + control: "moz-box-group",
|
|
| 149 | + options: [
|
|
| 150 | + {
|
|
| 151 | + l10nId: "tor-bridges-source-telegram-link",
|
|
| 152 | + l10nArgs: { telegramUserName: TELEGRAM_USER_NAME },
|
|
| 153 | + control: "moz-box-link",
|
|
| 154 | + iconSrc:
|
|
| 155 | + "chrome://browser/content/torpreferences/telegram-logo.svg",
|
|
| 156 | + controlAttrs: {
|
|
| 157 | + href: TELEGRAM_HREF,
|
|
| 158 | + },
|
|
| 159 | + },
|
|
| 160 | + {
|
|
| 161 | + l10nId: "tor-bridges-source-web-link",
|
|
| 162 | + l10nArgs: { url: TOR_BRIDGES_URL_NAME },
|
|
| 163 | + control: "moz-box-link",
|
|
| 164 | + iconSrc: "chrome://browser/content/torconnect/network.svg",
|
|
| 165 | + controlAttrs: {
|
|
| 166 | + href: TOR_BRIDGES_HREF,
|
|
| 167 | + },
|
|
| 168 | + },
|
|
| 169 | + {
|
|
| 170 | + l10nId: "tor-bridges-source-email-link",
|
|
| 171 | + l10nArgs: { address: TOR_BRIDGES_EMAIL },
|
|
| 172 | + control: "moz-box-item",
|
|
| 173 | + iconSrc: "chrome://browser/content/torpreferences/mail.svg",
|
|
| 174 | + },
|
|
| 175 | + ],
|
|
| 176 | + },
|
|
| 177 | + ],
|
|
| 178 | + },
|
|
| 179 | + ],
|
|
| 180 | + },
|
|
| 42 | 181 | });
|
| 43 | 182 | |
| 44 | 183 | Preferences.addSetting({
|
| ... | ... | @@ -113,3 +252,277 @@ Preferences.addSetting({ |
| 113 | 252 | lazy.TorConnect.quickstart = val;
|
| 114 | 253 | },
|
| 115 | 254 | });
|
| 255 | + |
|
| 256 | +Preferences.addSetting({
|
|
| 257 | + id: "torSettingsReady",
|
|
| 258 | + _ready: false,
|
|
| 259 | + setup(emitChange) {
|
|
| 260 | + if (!lazy.TorSettings.enabled) {
|
|
| 261 | + // Remain in `false`.
|
|
| 262 | + return;
|
|
| 263 | + }
|
|
| 264 | + // Most likely, TorSettings will already be initialised.
|
|
| 265 | + if (lazy.TorSettings.initialized) {
|
|
| 266 | + this._ready = true;
|
|
| 267 | + return;
|
|
| 268 | + }
|
|
| 269 | + // Else, wait for it to be initialised.
|
|
| 270 | + lazy.TorSettings.initializedPromise.then(
|
|
| 271 | + () => {
|
|
| 272 | + this._ready = true;
|
|
| 273 | + emitChange();
|
|
| 274 | + },
|
|
| 275 | + error => {
|
|
| 276 | + // No change in state.
|
|
| 277 | + console.error("TorSettings failed to initialize.", error);
|
|
| 278 | + }
|
|
| 279 | + );
|
|
| 280 | + },
|
|
| 281 | + get() {
|
|
| 282 | + return this._ready;
|
|
| 283 | + },
|
|
| 284 | +});
|
|
| 285 | + |
|
| 286 | +Preferences.addSetting({
|
|
| 287 | + id: "torBridgesRaw",
|
|
| 288 | + deps: ["torSettingsReady"],
|
|
| 289 | + _value: null,
|
|
| 290 | + setup(emitChange) {
|
|
| 291 | + const observer = subject => {
|
|
| 292 | + const { changes } = subject.wrappedJSObject;
|
|
| 293 | + // NOTE: We do not include "bridges.lox_id" in the changes. Instead, any
|
|
| 294 | + // widgets should wait for LoxTopics.UpdateActiveLoxId to ensure that the
|
|
| 295 | + // Lox module has responded to the change in ID strictly *before* we do.
|
|
| 296 | + // In particular, we want to make sure the invites and event data has been
|
|
| 297 | + // cleared.
|
|
| 298 | + if (
|
|
| 299 | + changes.includes("bridges.source") ||
|
|
| 300 | + changes.includes("bridges.bridge_strings") ||
|
|
| 301 | + changes.includes("bridges.builtin_type")
|
|
| 302 | + ) {
|
|
| 303 | + // Reset.
|
|
| 304 | + this._value = null;
|
|
| 305 | + emitChange();
|
|
| 306 | + }
|
|
| 307 | + };
|
|
| 308 | + Services.obs.addObserver(observer, lazy.TorSettingsTopics.SettingsChanged);
|
|
| 309 | + return () => {
|
|
| 310 | + Services.obs.removeObserver(
|
|
| 311 | + observer,
|
|
| 312 | + lazy.TorSettingsTopics.SettingsChanged
|
|
| 313 | + );
|
|
| 314 | + };
|
|
| 315 | + },
|
|
| 316 | + get(_pref, { torSettingsReady }) {
|
|
| 317 | + if (this._value === null) {
|
|
| 318 | + if (!torSettingsReady.value) {
|
|
| 319 | + // TorSettings getter will throw.
|
|
| 320 | + return null;
|
|
| 321 | + }
|
|
| 322 | + const source = lazy.TorSettings.bridges.source;
|
|
| 323 | + // Cache a value.
|
|
| 324 | + this._value = {
|
|
| 325 | + haveBridges: source !== lazy.TorBridgeSource.Invalid,
|
|
| 326 | + source,
|
|
| 327 | + builtinType: lazy.TorSettings.bridges.builtin_type,
|
|
| 328 | + bridgeStrings: lazy.TorSettings.bridges.bridge_strings,
|
|
| 329 | + };
|
|
| 330 | + }
|
|
| 331 | + return this._value;
|
|
| 332 | + },
|
|
| 333 | +});
|
|
| 334 | + |
|
| 335 | +Preferences.addSetting({
|
|
| 336 | + id: "connectedBridgeId",
|
|
| 337 | + _value: null,
|
|
| 338 | + setup(emitChange) {
|
|
| 339 | + const observer = async () => {
|
|
| 340 | + // NOTE: It should be safe for this method to be called concurrently.
|
|
| 341 | + let bridge = null;
|
|
| 342 | + try {
|
|
| 343 | + if (
|
|
| 344 | + lazy.TorProviderBuilder.currentState() ===
|
|
| 345 | + lazy.TorProviderState.Running
|
|
| 346 | + ) {
|
|
| 347 | + bridge = (await lazy.TorProviderBuilder.build()).currentBridge;
|
|
| 348 | + }
|
|
| 349 | + // Else, bridge is `null` whilst the provider is not running.
|
|
| 350 | + } catch (e) {
|
|
| 351 | + console.warn("Could not get current bridge", e);
|
|
| 352 | + }
|
|
| 353 | + const prevVal = this._value;
|
|
| 354 | + this._value = bridge?.fingerprint ?? null;
|
|
| 355 | + if (prevVal !== this._value) {
|
|
| 356 | + emitChange();
|
|
| 357 | + }
|
|
| 358 | + };
|
|
| 359 | + Services.obs.addObserver(observer, lazy.TorProviderTopics.BridgeChanged);
|
|
| 360 | + // NOTE: BridgeChanged is only fired directly by the provider instances,
|
|
| 361 | + // rather than by TorProviderBuilder. In particular, it will not fire when
|
|
| 362 | + // the previous provider had a bridge and the new one does not, because
|
|
| 363 | + // neither provider saw a change in their own bridge. But from the user's
|
|
| 364 | + // point of view, the overall change would mean that the current bridge has
|
|
| 365 | + // changed.
|
|
| 366 | + // Moreover, we want to show no connected bridge whilst we are missing a
|
|
| 367 | + // provider. Therefore, we also need to listen for a change in provider and
|
|
| 368 | + // its state.
|
|
| 369 | + // TODO: Maybe this logic should be moved to TorProviderBuilder itself if it
|
|
| 370 | + // is ever needed by other parts of the UI.
|
|
| 371 | + Services.obs.addObserver(
|
|
| 372 | + observer,
|
|
| 373 | + lazy.TorProviderTopics.ProviderStateChanged
|
|
| 374 | + );
|
|
| 375 | + // Get the initial value.
|
|
| 376 | + observer();
|
|
| 377 | + |
|
| 378 | + return () => {
|
|
| 379 | + Services.obs.removeObserver(
|
|
| 380 | + observer,
|
|
| 381 | + lazy.TorProviderTopics.ProviderStateChanged
|
|
| 382 | + );
|
|
| 383 | + Services.obs.removeObserver(
|
|
| 384 | + observer,
|
|
| 385 | + lazy.TorProviderTopics.BridgeChanged
|
|
| 386 | + );
|
|
| 387 | + };
|
|
| 388 | + },
|
|
| 389 | + get() {
|
|
| 390 | + return this._value;
|
|
| 391 | + },
|
|
| 392 | +});
|
|
| 393 | + |
|
| 394 | +Preferences.addSetting({
|
|
| 395 | + id: "bridgesEnabled",
|
|
| 396 | + deps: ["torSettingsReady", "torBridgesRaw"],
|
|
| 397 | + setup(emitChange) {
|
|
| 398 | + const observer = subject => {
|
|
| 399 | + const { changes } = subject.wrappedJSObject;
|
|
| 400 | + if (changes.includes("bridges.enabled")) {
|
|
| 401 | + emitChange();
|
|
| 402 | + }
|
|
| 403 | + };
|
|
| 404 | + Services.obs.addObserver(observer, lazy.TorSettingsTopics.SettingsChanged);
|
|
| 405 | + return () => {
|
|
| 406 | + Services.obs.removeObserver(
|
|
| 407 | + observer,
|
|
| 408 | + lazy.TorSettingsTopics.SettingsChanged
|
|
| 409 | + );
|
|
| 410 | + };
|
|
| 411 | + },
|
|
| 412 | + get(_prefVal, { torSettingsReady }) {
|
|
| 413 | + if (!torSettingsReady.value) {
|
|
| 414 | + // TorSettings.bridges will throw before TorSettings has finished
|
|
| 415 | + // initialisation.
|
|
| 416 | + return false;
|
|
| 417 | + }
|
|
| 418 | + return lazy.TorSettings.bridges.enabled;
|
|
| 419 | + },
|
|
| 420 | + set(val) {
|
|
| 421 | + lazy.TorSettings.changeSettings({
|
|
| 422 | + bridges: { enabled: val },
|
|
| 423 | + });
|
|
| 424 | + },
|
|
| 425 | + visible({ torSettingsReady }) {
|
|
| 426 | + return torSettingsReady.value;
|
|
| 427 | + },
|
|
| 428 | + disabled({ torBridgesRaw }) {
|
|
| 429 | + return !torBridgesRaw.value?.haveBridges;
|
|
| 430 | + },
|
|
| 431 | +});
|
|
| 432 | + |
|
| 433 | +Preferences.addSetting({
|
|
| 434 | + id: "torBridgesDisplay",
|
|
| 435 | + deps: ["torSettingsReady", "torBridgesRaw", "connectedBridgeId"],
|
|
| 436 | + getControlConfig(config, { torBridgesRaw, connectedBridgeId }) {
|
|
| 437 | + config.controlAttrs = {
|
|
| 438 | + ...config.controlAttrs,
|
|
| 439 | + // Set the `bridges` and `connnectedBridgeId` object *properties* (rather
|
|
| 440 | + // than attributes) by using the `.` prefix.
|
|
| 441 | + ".bridges": torBridgesRaw.value,
|
|
| 442 | + ".connectedBridgeId": connectedBridgeId.value,
|
|
| 443 | + };
|
|
| 444 | + return config;
|
|
| 445 | + },
|
|
| 446 | + visible({ torSettingsReady }) {
|
|
| 447 | + return torSettingsReady.value;
|
|
| 448 | + },
|
|
| 449 | +});
|
|
| 450 | + |
|
| 451 | +Preferences.addSetting({
|
|
| 452 | + id: "newBridgesGroup",
|
|
| 453 | + deps: ["torSettingsReady", "torBridgesRaw"],
|
|
| 454 | + getControlConfig(config, { torBridgesRaw }) {
|
|
| 455 | + config.l10nId = torBridgesRaw.value?.haveBridges
|
|
| 456 | + ? "tor-bridges-replace-bridges-group"
|
|
| 457 | + : "tor-bridges-add-bridges-group";
|
|
| 458 | + return config;
|
|
| 459 | + },
|
|
| 460 | + visible({ torSettingsReady }) {
|
|
| 461 | + return torSettingsReady.value;
|
|
| 462 | + },
|
|
| 463 | +});
|
|
| 464 | + |
|
| 465 | +Preferences.addSetting({
|
|
| 466 | + id: "builtinBridges",
|
|
| 467 | + onUserClick() {
|
|
| 468 | + lazy.openBridgeDialog(
|
|
| 469 | + window,
|
|
| 470 | + "chrome://browser/content/torpreferences/builtinBridgeDialog.xhtml",
|
|
| 471 | + null,
|
|
| 472 | + result => {
|
|
| 473 | + if (!result.type) {
|
|
| 474 | + return null;
|
|
| 475 | + }
|
|
| 476 | + return lazy.TorSettings.changeSettings({
|
|
| 477 | + bridges: {
|
|
| 478 | + enabled: true,
|
|
| 479 | + source: lazy.TorBridgeSource.BuiltIn,
|
|
| 480 | + builtin_type: result.type,
|
|
| 481 | + },
|
|
| 482 | + });
|
|
| 483 | + }
|
|
| 484 | + );
|
|
| 485 | + },
|
|
| 486 | +});
|
|
| 487 | + |
|
| 488 | +Preferences.addSetting({
|
|
| 489 | + id: "userProvidedBridges",
|
|
| 490 | + deps: ["torBridgesRaw"],
|
|
| 491 | + onUserClick(_event, { torBridgesRaw }) {
|
|
| 492 | + lazy.openUserProvideBridgeDialog(
|
|
| 493 | + window,
|
|
| 494 | + torBridgesRaw.value?.haveBridges ? "replace" : "add"
|
|
| 495 | + );
|
|
| 496 | + },
|
|
| 497 | +});
|
|
| 498 | + |
|
| 499 | +Preferences.addSetting({
|
|
| 500 | + id: "findMoreBridgesGroup",
|
|
| 501 | + deps: ["torSettingsReady"],
|
|
| 502 | + visible({ torSettingsReady }) {
|
|
| 503 | + return torSettingsReady.value;
|
|
| 504 | + },
|
|
| 505 | +});
|
|
| 506 | + |
|
| 507 | +Preferences.addSetting({
|
|
| 508 | + id: "requestBridges",
|
|
| 509 | + onUserClick() {
|
|
| 510 | + lazy.openBridgeDialog(
|
|
| 511 | + window,
|
|
| 512 | + "chrome://browser/content/torpreferences/requestBridgeDialog.xhtml",
|
|
| 513 | + null,
|
|
| 514 | + result => {
|
|
| 515 | + if (!result.bridges?.length) {
|
|
| 516 | + return null;
|
|
| 517 | + }
|
|
| 518 | + return lazy.TorSettings.changeSettings({
|
|
| 519 | + bridges: {
|
|
| 520 | + enabled: true,
|
|
| 521 | + source: lazy.TorBridgeSource.BridgeDB,
|
|
| 522 | + bridge_strings: result.bridges,
|
|
| 523 | + },
|
|
| 524 | + });
|
|
| 525 | + }
|
|
| 526 | + );
|
|
| 527 | + },
|
|
| 528 | +}); |
| 1 | +const lazy = {};
|
|
| 2 | +ChromeUtils.defineESModuleGetters(lazy, {
|
|
| 3 | + TorBridgeSource: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
|
|
| 4 | + TorConnectParent:
|
|
| 5 | + "moz-src:///browser/components/torconnect/TorConnectParent.sys.mjs",
|
|
| 6 | + TorSettings: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
|
|
| 7 | +});
|
|
| 8 | + |
|
| 9 | +/**
|
|
| 10 | + * Force the focus to move to the bridge heading.
|
|
| 11 | + *
|
|
| 12 | + * @param {Window} win - The preferences window.
|
|
| 13 | + * @param {boolean} [forceTopHeading=false] - Force the focus to move to the
|
|
| 14 | + * top "Bridges" setting heading.
|
|
| 15 | + */
|
|
| 16 | +export function moveFocusToBridgeHeading(win, forceTopHeading = false) {
|
|
| 17 | + // Move focus to the start of the relevant section, which is a heading.
|
|
| 18 | + // They have tabindex="-1" so should be focusable, even though they are not
|
|
| 19 | + // part of the usual tab navigation.
|
|
| 20 | + // TODO: It might be better if we could use the # named anchor to
|
|
| 21 | + // re-orient the screen reader position instead of using tabIndex=-1, but
|
|
| 22 | + // about:preferences currently uses the anchor for showing categories
|
|
| 23 | + // only. See bugzilla bug 1799153.
|
|
| 24 | + if (
|
|
| 25 | + forceTopHeading ||
|
|
| 26 | + !win.document.getElementById("torBridgesDisplay").focusHeading()
|
|
| 27 | + ) {
|
|
| 28 | + win.document
|
|
| 29 | + .querySelector('setting-group[groupid="torBridges"] moz-fieldset')
|
|
| 30 | + .focusHeading();
|
|
| 31 | + }
|
|
| 32 | +}
|
|
| 33 | + |
|
| 34 | +/**
|
|
| 35 | + * Open a bridge dialog that will change the users bridges.
|
|
| 36 | + *
|
|
| 37 | + * @param {Window} win - The preferences window.
|
|
| 38 | + * @param {string} url - The url of the dialog to open.
|
|
| 39 | + * @param {object?} inputData - The input data to send to the dialog window.
|
|
| 40 | + * @param {Function} onAccept - The method to call if the bridge dialog was
|
|
| 41 | + * accepted by the user. This will be passed a "result" object containing
|
|
| 42 | + * data set by the dialog. This should return a promise that resolves once
|
|
| 43 | + * the bridge settings have been set, or null if the settings have not
|
|
| 44 | + * been applied.
|
|
| 45 | + */
|
|
| 46 | +export function openBridgeDialog(win, url, inputData, onAccept) {
|
|
| 47 | + const result = { accepted: false, connect: false };
|
|
| 48 | + let savedSettings = null;
|
|
| 49 | + win.gSubDialog.open(
|
|
| 50 | + url,
|
|
| 51 | + {
|
|
| 52 | + features: "resizable=yes",
|
|
| 53 | + closingCallback: () => {
|
|
| 54 | + if (!result.accepted) {
|
|
| 55 | + return;
|
|
| 56 | + }
|
|
| 57 | + savedSettings = onAccept(result);
|
|
| 58 | + if (!savedSettings) {
|
|
| 59 | + // No change in settings.
|
|
| 60 | + return;
|
|
| 61 | + }
|
|
| 62 | + if (!result.connect) {
|
|
| 63 | + // Do not open about:torconnect.
|
|
| 64 | + return;
|
|
| 65 | + }
|
|
| 66 | + |
|
| 67 | + // Wait until the settings are applied before bootstrapping.
|
|
| 68 | + // NOTE: Saving the settings should also cancel any existing bootstrap
|
|
| 69 | + // attempt first. See tor-browser#41921.
|
|
| 70 | + savedSettings.then(() => {
|
|
| 71 | + // The bridge dialog button is "connect" when Tor is not
|
|
| 72 | + // bootstrapped, so do the connect.
|
|
| 73 | + |
|
| 74 | + // Start Bootstrapping, which should use the configured bridges.
|
|
| 75 | + // NOTE: We do this regardless of any previous TorConnect Error.
|
|
| 76 | + lazy.TorConnectParent.open({ beginBootstrapping: "hard" });
|
|
| 77 | + });
|
|
| 78 | + },
|
|
| 79 | + // closedCallback should be called after gSubDialog has already
|
|
| 80 | + // re-assigned focus back to the document.
|
|
| 81 | + closedCallback: () => {
|
|
| 82 | + if (!savedSettings) {
|
|
| 83 | + return;
|
|
| 84 | + }
|
|
| 85 | + // Wait until the settings have changed, so that the UI could
|
|
| 86 | + // respond, then move focus.
|
|
| 87 | + savedSettings.then(() => {
|
|
| 88 | + moveFocusToBridgeHeading(win);
|
|
| 89 | + });
|
|
| 90 | + },
|
|
| 91 | + },
|
|
| 92 | + result,
|
|
| 93 | + inputData
|
|
| 94 | + );
|
|
| 95 | +}
|
|
| 96 | + |
|
| 97 | +/**
|
|
| 98 | + * Open the user provide dialog.
|
|
| 99 | + *
|
|
| 100 | + * @param {Window} win - The preferences window.
|
|
| 101 | + * @param {string} mode - The mode to open the dialog in: "add", "replace" or
|
|
| 102 | + * "edit".
|
|
| 103 | + */
|
|
| 104 | +export function openUserProvideBridgeDialog(win, mode) {
|
|
| 105 | + openBridgeDialog(
|
|
| 106 | + win,
|
|
| 107 | + "chrome://browser/content/torpreferences/provideBridgeDialog.xhtml",
|
|
| 108 | + { mode },
|
|
| 109 | + result => {
|
|
| 110 | + const loxId = result.loxId;
|
|
| 111 | + if (!loxId && !result.addresses?.length) {
|
|
| 112 | + return null;
|
|
| 113 | + }
|
|
| 114 | + const bridges = { enabled: true };
|
|
| 115 | + if (loxId) {
|
|
| 116 | + bridges.source = lazy.TorBridgeSource.Lox;
|
|
| 117 | + bridges.lox_id = loxId;
|
|
| 118 | + } else {
|
|
| 119 | + bridges.source = lazy.TorBridgeSource.UserProvided;
|
|
| 120 | + bridges.bridge_strings = result.addresses;
|
|
| 121 | + }
|
|
| 122 | + return lazy.TorSettings.changeSettings({ bridges });
|
|
| 123 | + }
|
|
| 124 | + );
|
|
| 125 | +} |
| 1 | +<html:template id="tor-bridges-display-template">
|
|
| 2 | + <html:div id="tor-bridges-none" hidden="hidden">
|
|
| 3 | + <html:img id="tor-bridges-none-icon" alt="" />
|
|
| 4 | + <html:p data-l10n-id="tor-bridges-none-added"></html:p>
|
|
| 5 | + </html:div>
|
|
| 6 | + <html:fieldset id="tor-bridges-current" hidden="hidden">
|
|
| 7 | + <html:legend>
|
|
| 8 | + <html:h4
|
|
| 9 | + id="tor-bridges-current-heading-non-search"
|
|
| 10 | + class="tor-bridges-current-heading tor-focusable-heading tor-small-heading"
|
|
| 11 | + tabindex="-1"
|
|
| 12 | + data-l10n-id="tor-bridges-your-bridges"
|
|
| 13 | + ></html:h4>
|
|
| 14 | + </html:legend>
|
|
| 15 | + <html:p
|
|
| 16 | + id="tor-bridges-user-label"
|
|
| 17 | + class="tor-bridges-source-label"
|
|
| 18 | + data-l10n-id="tor-bridges-source-user"
|
|
| 19 | + ></html:p>
|
|
| 20 | + <html:p
|
|
| 21 | + id="tor-bridges-built-in-label"
|
|
| 22 | + class="tor-bridges-source-label"
|
|
| 23 | + data-l10n-id="tor-bridges-source-built-in"
|
|
| 24 | + ></html:p>
|
|
| 25 | + <html:p
|
|
| 26 | + id="tor-bridges-requested-label"
|
|
| 27 | + class="tor-bridges-source-label"
|
|
| 28 | + data-l10n-id="tor-bridges-source-requested"
|
|
| 29 | + ></html:p>
|
|
| 30 | + <html:p id="tor-bridges-lox-label" class="tor-bridges-source-label">
|
|
| 31 | + <html:img id="tor-bridges-lox-label-icon" alt="" />
|
|
| 32 | + <html:span data-l10n-id="tor-bridges-source-lox"></html:span>
|
|
| 33 | + </html:p>
|
|
| 34 | + <html:button
|
|
| 35 | + id="tor-bridges-all-options-button"
|
|
| 36 | + class="tor-bridges-options-button"
|
|
| 37 | + aria-haspopup="menu"
|
|
| 38 | + aria-expanded="false"
|
|
| 39 | + aria-controls="tor-bridges-all-options-menu"
|
|
| 40 | + data-l10n-id="tor-bridges-options-button"
|
|
| 41 | + ></html:button>
|
|
| 42 | + <html:panel-list
|
|
| 43 | + id="tor-bridges-all-options-menu"
|
|
| 44 | + data-hidden-from-search="true"
|
|
| 45 | + >
|
|
| 46 | + <html:panel-item
|
|
| 47 | + id="tor-bridges-options-qr-all-menu-item"
|
|
| 48 | + data-l10n-attrs="accesskey"
|
|
| 49 | + data-l10n-id="tor-bridges-menu-item-qr-all-bridge-addresses"
|
|
| 50 | + ></html:panel-item>
|
|
| 51 | + <html:panel-item
|
|
| 52 | + id="tor-bridges-options-copy-all-menu-item"
|
|
| 53 | + data-l10n-attrs="accesskey"
|
|
| 54 | + data-l10n-id="tor-bridges-menu-item-copy-all-bridge-addresses"
|
|
| 55 | + ></html:panel-item>
|
|
| 56 | + <html:panel-item
|
|
| 57 | + id="tor-bridges-options-edit-all-menu-item"
|
|
| 58 | + data-l10n-attrs="accesskey"
|
|
| 59 | + data-l10n-id="tor-bridges-menu-item-edit-all-bridges"
|
|
| 60 | + ></html:panel-item>
|
|
| 61 | + <html:panel-item
|
|
| 62 | + id="tor-bridges-options-remove-all-menu-item"
|
|
| 63 | + data-l10n-attrs="accesskey"
|
|
| 64 | + data-l10n-id="tor-bridges-menu-item-remove-all-bridges"
|
|
| 65 | + ></html:panel-item>
|
|
| 66 | + </html:panel-list>
|
|
| 67 | + <html:div id="tor-bridges-built-in-display" hidden="hidden">
|
|
| 68 | + <html:p id="tor-bridges-built-in-type-name"></html:p>
|
|
| 69 | + <html:p id="tor-bridges-built-in-connected" class="bridge-status-badge">
|
|
| 70 | + <html:div class="bridge-status-icon"></html:div>
|
|
| 71 | + <html:span
|
|
| 72 | + data-l10n-id="tor-bridges-built-in-status-connected"
|
|
| 73 | + ></html:span>
|
|
| 74 | + </html:p>
|
|
| 75 | + <html:p id="tor-bridges-built-in-description"></html:p>
|
|
| 76 | + </html:div>
|
|
| 77 | + <html:div
|
|
| 78 | + id="tor-bridges-grid-display"
|
|
| 79 | + class="tor-bridges-grid"
|
|
| 80 | + role="grid"
|
|
| 81 | + aria-labelledby="tor-bridges-current-heading-non-search"
|
|
| 82 | + hidden="hidden"
|
|
| 83 | + ></html:div>
|
|
| 84 | + <html:template id="tor-bridges-grid-row-template">
|
|
| 85 | + <html:div class="tor-bridges-grid-row" role="row">
|
|
| 86 | + <!-- TODO: lox status cell for new bridges? -->
|
|
| 87 | + <html:span
|
|
| 88 | + class="tor-bridges-type-cell tor-bridges-grid-cell"
|
|
| 89 | + role="gridcell"
|
|
| 90 | + ></html:span>
|
|
| 91 | + <html:span class="tor-bridges-emojis-block" role="none"></html:span>
|
|
| 92 | + <html:span class="tor-bridges-grid-end-block" role="none">
|
|
| 93 | + <html:span
|
|
| 94 | + class="tor-bridges-address-cell tor-bridges-grid-cell"
|
|
| 95 | + role="gridcell"
|
|
| 96 | + >
|
|
| 97 | + <html:span class="tor-bridges-address-cell-text"></html:span>
|
|
| 98 | + </html:span>
|
|
| 99 | + <html:span
|
|
| 100 | + class="tor-bridges-status-cell tor-bridges-grid-cell"
|
|
| 101 | + role="gridcell"
|
|
| 102 | + >
|
|
| 103 | + <html:div class="bridge-status-badge">
|
|
| 104 | + <html:div class="bridge-status-icon"></html:div>
|
|
| 105 | + <html:span class="tor-bridges-status-cell-text"></html:span>
|
|
| 106 | + </html:div>
|
|
| 107 | + </html:span>
|
|
| 108 | + <html:span
|
|
| 109 | + class="tor-bridges-options-cell tor-bridges-grid-cell"
|
|
| 110 | + role="gridcell"
|
|
| 111 | + >
|
|
| 112 | + <html:button
|
|
| 113 | + class="tor-bridges-options-cell-button tor-bridges-options-button tor-bridges-grid-focus"
|
|
| 114 | + aria-haspopup="menu"
|
|
| 115 | + aria-expanded="false"
|
|
| 116 | + data-l10n-id="tor-bridges-individual-bridge-options-button"
|
|
| 117 | + ></html:button>
|
|
| 118 | + <html:panel-list
|
|
| 119 | + class="tor-bridges-individual-options-menu"
|
|
| 120 | + data-hidden-from-search="true"
|
|
| 121 | + >
|
|
| 122 | + <html:panel-item
|
|
| 123 | + class="tor-bridges-options-qr-one-menu-item"
|
|
| 124 | + data-l10n-attrs="accesskey"
|
|
| 125 | + data-l10n-id="tor-bridges-menu-item-qr-address"
|
|
| 126 | + ></html:panel-item>
|
|
| 127 | + <html:panel-item
|
|
| 128 | + class="tor-bridges-options-copy-one-menu-item"
|
|
| 129 | + data-l10n-attrs="accesskey"
|
|
| 130 | + data-l10n-id="tor-bridges-menu-item-copy-address"
|
|
| 131 | + ></html:panel-item>
|
|
| 132 | + <html:panel-item
|
|
| 133 | + class="tor-bridges-options-remove-one-menu-item"
|
|
| 134 | + data-l10n-attrs="accesskey"
|
|
| 135 | + data-l10n-id="tor-bridges-menu-item-remove-bridge"
|
|
| 136 | + ></html:panel-item>
|
|
| 137 | + </html:panel-list>
|
|
| 138 | + </html:span>
|
|
| 139 | + </html:span>
|
|
| 140 | + </html:div>
|
|
| 141 | + </html:template>
|
|
| 142 | + <html:div
|
|
| 143 | + id="tor-bridges-share"
|
|
| 144 | + class="tor-bridges-details-box"
|
|
| 145 | + hidden="hidden"
|
|
| 146 | + >
|
|
| 147 | + <html:h5
|
|
| 148 | + class="tor-bridges-share-heading tor-small-heading"
|
|
| 149 | + data-l10n-id="tor-bridges-share-heading"
|
|
| 150 | + ></html:h5>
|
|
| 151 | + <html:p
|
|
| 152 | + id="tor-bridges-share-description"
|
|
| 153 | + data-l10n-id="tor-bridges-share-description"
|
|
| 154 | + ></html:p>
|
|
| 155 | + <html:button
|
|
| 156 | + id="tor-bridges-copy-addresses-button"
|
|
| 157 | + data-l10n-id="tor-bridges-copy-addresses-button"
|
|
| 158 | + ></html:button>
|
|
| 159 | + <html:button
|
|
| 160 | + id="tor-bridges-qr-addresses-button"
|
|
| 161 | + data-l10n-id="tor-bridges-qr-addresses-button"
|
|
| 162 | + ></html:button>
|
|
| 163 | + </html:div>
|
|
| 164 | + <html:div id="tor-bridges-lox-status" hidden="hidden">
|
|
| 165 | + <html:div data-l10n-id="tor-bridges-lox-description"></html:div>
|
|
| 166 | + <html:div
|
|
| 167 | + id="tor-bridges-lox-details"
|
|
| 168 | + class="tor-bridges-details-box tor-bridges-lox-box"
|
|
| 169 | + hidden="hidden"
|
|
| 170 | + >
|
|
| 171 | + <html:img alt="" class="tor-bridges-lox-image-inner" />
|
|
| 172 | + <html:img alt="" class="tor-bridges-lox-image-outer" />
|
|
| 173 | + <html:h5
|
|
| 174 | + class="tor-bridges-lox-next-unlock-counter tor-small-heading tor-bridges-lox-intro tor-focusable-heading"
|
|
| 175 | + tabindex="-1"
|
|
| 176 | + ></html:h5>
|
|
| 177 | + <html:ul class="tor-bridges-lox-list">
|
|
| 178 | + <html:li
|
|
| 179 | + id="tor-bridges-lox-next-unlock-gain-bridges"
|
|
| 180 | + class="tor-bridges-lox-list-item tor-bridges-lox-list-item-bridge"
|
|
| 181 | + data-l10n-id="tor-bridges-lox-unlock-two-bridges"
|
|
| 182 | + hidden="hidden"
|
|
| 183 | + ></html:li>
|
|
| 184 | + <html:li
|
|
| 185 | + id="tor-bridges-lox-next-unlock-first-invites"
|
|
| 186 | + class="tor-bridges-lox-list-item tor-bridges-lox-list-item-invite"
|
|
| 187 | + data-l10n-id="tor-bridges-lox-unlock-first-invites"
|
|
| 188 | + hidden="hidden"
|
|
| 189 | + ></html:li>
|
|
| 190 | + <html:li
|
|
| 191 | + id="tor-bridges-lox-next-unlock-more-invites"
|
|
| 192 | + class="tor-bridges-lox-list-item tor-bridges-lox-list-item-invite"
|
|
| 193 | + data-l10n-id="tor-bridges-lox-unlock-more-invites"
|
|
| 194 | + hidden="hidden"
|
|
| 195 | + ></html:li>
|
|
| 196 | + </html:ul>
|
|
| 197 | + <html:div
|
|
| 198 | + id="tor-bridges-lox-remaining-invites"
|
|
| 199 | + hidden="hidden"
|
|
| 200 | + ></html:div>
|
|
| 201 | + <html:button
|
|
| 202 | + id="tor-bridges-lox-show-invites-button"
|
|
| 203 | + class="tor-bridges-lox-button"
|
|
| 204 | + data-l10n-id="tor-bridges-lox-show-invites-button"
|
|
| 205 | + hidden="hidden"
|
|
| 206 | + ></html:button>
|
|
| 207 | + </html:div>
|
|
| 208 | + <html:div
|
|
| 209 | + id="tor-bridges-lox-unlock-alert"
|
|
| 210 | + role="alert"
|
|
| 211 | + class="tor-bridges-details-box tor-bridges-lox-box"
|
|
| 212 | + hidden="hidden"
|
|
| 213 | + >
|
|
| 214 | + <html:img alt="" class="tor-bridges-lox-image-inner" />
|
|
| 215 | + <html:img alt="" class="tor-bridges-lox-image-outer" />
|
|
| 216 | + <html:div
|
|
| 217 | + id="tor-bridge-unlock-alert-title"
|
|
| 218 | + class="tor-small-heading tor-bridges-lox-intro"
|
|
| 219 | + ></html:div>
|
|
| 220 | + <html:ul class="tor-bridges-lox-list">
|
|
| 221 | + <html:li
|
|
| 222 | + id="tor-bridges-lox-unlock-alert-gain-bridges"
|
|
| 223 | + class="tor-bridges-lox-list-item tor-bridges-lox-list-item-bridge"
|
|
| 224 | + data-l10n-id="tor-bridges-lox-gained-two-bridges"
|
|
| 225 | + hidden="hidden"
|
|
| 226 | + ></html:li>
|
|
| 227 | + <html:li
|
|
| 228 | + id="tor-bridges-lox-unlock-alert-new-bridges"
|
|
| 229 | + class="tor-bridges-lox-list-item tor-bridges-lox-list-item-bridge"
|
|
| 230 | + data-l10n-id="tor-bridges-lox-new-bridges"
|
|
| 231 | + hidden="hidden"
|
|
| 232 | + ></html:li>
|
|
| 233 | + <html:li
|
|
| 234 | + id="tor-bridges-lox-unlock-alert-invites"
|
|
| 235 | + class="tor-bridges-lox-list-item tor-bridges-lox-list-item-invite"
|
|
| 236 | + hidden="hidden"
|
|
| 237 | + ></html:li>
|
|
| 238 | + </html:ul>
|
|
| 239 | + <html:button
|
|
| 240 | + id="tor-bridges-lox-unlock-alert-button"
|
|
| 241 | + class="tor-bridges-lox-button"
|
|
| 242 | + data-l10n-id="tor-bridges-lox-got-it-button"
|
|
| 243 | + ></html:button>
|
|
| 244 | + </html:div>
|
|
| 245 | + </html:div>
|
|
| 246 | + </html:fieldset>
|
|
| 247 | +</html:template>
|
|
| 248 | + |
|
| 1 | 249 | <!-- Tor panel -->
|
| 2 | 250 | |
| 3 | 251 | <script
|
| ... | ... | @@ -201,20 +201,27 @@ button.spoof-button-disabled { |
| 201 | 201 | |
| 202 | 202 | #tor-bridges-none,
|
| 203 | 203 | #tor-bridges-current {
|
| 204 | - margin-inline: 0;
|
|
| 205 | - margin-block: var(--space-xxlarge);
|
|
| 204 | + @media not -moz-pref("browser.settings-redesign.enabled") {
|
|
| 205 | + margin-inline: 0;
|
|
| 206 | + margin-block: var(--space-xxlarge);
|
|
| 207 | + }
|
|
| 206 | 208 | }
|
| 207 | 209 | |
| 208 | 210 | #tor-bridges-none:not([hidden]) {
|
| 209 | 211 | display: grid;
|
| 210 | 212 | justify-items: center;
|
| 211 | 213 | text-align: center;
|
| 212 | - padding-block: 64px;
|
|
| 214 | + padding-block: 54px;
|
|
| 213 | 215 | padding-inline: var(--space-xxlarge);
|
| 214 | - gap: var(--space-large);
|
|
| 216 | + gap: var(--space-small);
|
|
| 215 | 217 | border-radius: var(--border-radius-small);
|
| 216 | 218 | color: var(--text-color-deemphasized);
|
| 217 | 219 | border: 2px dashed var(--border-color-deemphasized);
|
| 220 | + |
|
| 221 | + @media not -moz-pref("browser.settings-redesign.enabled") {
|
|
| 222 | + padding-block: 64px;
|
|
| 223 | + gap: var(--space-large);
|
|
| 224 | + }
|
|
| 218 | 225 | }
|
| 219 | 226 | |
| 220 | 227 | #tor-bridges-none-icon {
|
| ... | ... | @@ -228,9 +235,13 @@ button.spoof-button-disabled { |
| 228 | 235 | .tor-bridges-box,
|
| 229 | 236 | .tor-bridges-details-box {
|
| 230 | 237 | padding: var(--space-large);
|
| 231 | - border-radius: var(--border-radius-small);
|
|
| 232 | - background: var(--background-color-box-info);
|
|
| 238 | + border-radius: var(--border-radius-medium);
|
|
| 233 | 239 | border: var(--border-width) solid var(--border-color);
|
| 240 | + |
|
| 241 | + @media not -moz-pref("browser.settings-redesign.enabled") {
|
|
| 242 | + border-radius: var(--border-radius-small);
|
|
| 243 | + background: var(--background-color-box-info);
|
|
| 244 | + }
|
|
| 234 | 245 | }
|
| 235 | 246 | |
| 236 | 247 | @media not forced-colors {
|
| ... | ... | @@ -250,6 +261,56 @@ button.spoof-button-disabled { |
| 250 | 261 | white-space: nowrap;
|
| 251 | 262 | }
|
| 252 | 263 | |
| 264 | +tor-bridges-display {
|
|
| 265 | + display: block;
|
|
| 266 | + margin-block: var(--space-small);
|
|
| 267 | + |
|
| 268 | + &.has-tor-bridges {
|
|
| 269 | + display: grid;
|
|
| 270 | + grid-template:
|
|
| 271 | + "heading source button" min-content
|
|
| 272 | + "bridges bridges bridges" auto
|
|
| 273 | + "extra extra extra" auto
|
|
| 274 | + / max-content 1fr max-content;
|
|
| 275 | + align-items: center;
|
|
| 276 | + border: var(--card-border);
|
|
| 277 | + border-radius: var(--border-radius-medium);
|
|
| 278 | + background-color: var(--card-background-color);
|
|
| 279 | + padding: var(--space-large);
|
|
| 280 | + }
|
|
| 281 | + |
|
| 282 | + & p {
|
|
| 283 | + margin: 0;
|
|
| 284 | + }
|
|
| 285 | + |
|
| 286 | + & #tor-bridges-current:not([hidden]) {
|
|
| 287 | + /* Avoid the special display logic for <fieldset> and <legend>. */
|
|
| 288 | + display: contents;
|
|
| 289 | + }
|
|
| 290 | + |
|
| 291 | + & legend {
|
|
| 292 | + grid-area: heading;
|
|
| 293 | + white-space: nowrap;
|
|
| 294 | + }
|
|
| 295 | + |
|
| 296 | + & .tor-bridges-source-label {
|
|
| 297 | + white-space: nowrap;
|
|
| 298 | + padding-inline-end: var(--space-small);
|
|
| 299 | + border-inline-end: var(--border-width) solid var(--border-color);
|
|
| 300 | + }
|
|
| 301 | + |
|
| 302 | + & :is(#tor-bridges-built-in-display, #tor-bridges-grid-display) {
|
|
| 303 | + grid-area: bridges;
|
|
| 304 | + margin-block-start: var(--space-large);
|
|
| 305 | + border-block-start: var(--border-width) solid var(--border-color);
|
|
| 306 | + padding-block-start: var(--space-large);
|
|
| 307 | + }
|
|
| 308 | + |
|
| 309 | + & :is(#tor-bridges-share, #tor-bridges-lox-status) {
|
|
| 310 | + grid-area: extra;
|
|
| 311 | + }
|
|
| 312 | +}
|
|
| 313 | + |
|
| 253 | 314 | .tor-bridges-current-heading {
|
| 254 | 315 | grid-area: heading;
|
| 255 | 316 | }
|
| ... | ... | @@ -681,6 +742,24 @@ button.spoof-button-disabled { |
| 681 | 742 | grid-column: 2 / 3;
|
| 682 | 743 | }
|
| 683 | 744 | |
| 745 | +#torBridgesRequestBanner {
|
|
| 746 | + display: flex;
|
|
| 747 | + flex-direction: column;
|
|
| 748 | + align-items: center;
|
|
| 749 | + text-align: center;
|
|
| 750 | + text-wrap-style: balance;
|
|
| 751 | + gap: var(--space-large);
|
|
| 752 | + |
|
| 753 | + & > * {
|
|
| 754 | + margin: 0;
|
|
| 755 | + flex: 0 0 auto;
|
|
| 756 | + }
|
|
| 757 | + |
|
| 758 | + & > p {
|
|
| 759 | + max-width: var(--size-layout-medium);
|
|
| 760 | + }
|
|
| 761 | +}
|
|
| 762 | + |
|
| 684 | 763 | #tor-bridges-request-box {
|
| 685 | 764 | /* Take up the full height in the container. */
|
| 686 | 765 | align-self: stretch;
|
| ... | ... | @@ -25,6 +25,8 @@ browser.jar: |
| 25 | 25 | content/browser/torpreferences/torLogDialog.js (content/torLogDialog.js)
|
| 26 | 26 | content/browser/torpreferences/torLogDialog.xhtml (content/torLogDialog.xhtml)
|
| 27 | 27 | content/browser/torpreferences/config/connection.mjs (config/connection.mjs)
|
| 28 | + content/browser/torpreferences/config/helpers.mjs (config/helpers.mjs)
|
|
| 29 | + content/browser/torpreferences/widgets/tor-bridges-display.mjs (widgets/tor-bridges-display.mjs)
|
|
| 28 | 30 | content/browser/torpreferences/widgets/tor-connection-status.mjs (widgets/tor-connection-status.mjs)
|
| 29 | 31 | content/browser/torpreferences/widgets/tor-connection-status.css (widgets/tor-connection-status.css)
|
| 30 | 32 | content/browser/torpreferences/connectionPane.js (content/connectionPane.js)
|
| 1 | +const lazy = {};
|
|
| 2 | +ChromeUtils.defineESModuleGetters(lazy, {
|
|
| 3 | + Lox: "moz-src:///toolkit/components/lox/Lox.sys.mjs",
|
|
| 4 | + LoxTopics: "moz-src:///toolkit/components/lox/Lox.sys.mjs",
|
|
| 5 | + moveFocusToBridgeHeading:
|
|
| 6 | + "chrome://browser/content/torpreferences/config/helpers.mjs",
|
|
| 7 | + openUserProvideBridgeDialog:
|
|
| 8 | + "chrome://browser/content/torpreferences/config/helpers.mjs",
|
|
| 9 | + TorBridgeSource: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
|
|
| 10 | + TorParsers: "moz-src:///toolkit/components/tor-launcher/TorParsers.sys.mjs",
|
|
| 11 | + TorSettings: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
|
|
| 12 | +});
|
|
| 13 | + |
|
| 14 | +/**
|
|
| 15 | + * Show the bridge QR to the user.
|
|
| 16 | + *
|
|
| 17 | + * @param {string} bridgeString - The string to use in the QR.
|
|
| 18 | + */
|
|
| 19 | +function showBridgeQr(bridgeString) {
|
|
| 20 | + window.gSubDialog.open(
|
|
| 21 | + "chrome://browser/content/torpreferences/bridgeQrDialog.xhtml",
|
|
| 22 | + { features: "resizable=yes" },
|
|
| 23 | + bridgeString
|
|
| 24 | + );
|
|
| 25 | +}
|
|
| 26 | + |
|
| 27 | +/**
|
|
| 28 | + * Post a new notification, replacing any existing one.
|
|
| 29 | + *
|
|
| 30 | + * @param {string} type - The notification type.
|
|
| 31 | + */
|
|
| 32 | +async function postBridgeNotification(type) {
|
|
| 33 | + let updateId;
|
|
| 34 | + switch (type) {
|
|
| 35 | + case "removed-one":
|
|
| 36 | + updateId = "tor-bridges-update-removed-one-bridge";
|
|
| 37 | + break;
|
|
| 38 | + case "removed-all":
|
|
| 39 | + updateId = "tor-bridges-update-removed-all-bridges";
|
|
| 40 | + break;
|
|
| 41 | + case "changed":
|
|
| 42 | + default:
|
|
| 43 | + // Generic message for when bridges change.
|
|
| 44 | + updateId = "tor-bridges-update-changed-bridges";
|
|
| 45 | + break;
|
|
| 46 | + }
|
|
| 47 | + const bridgeDisplay = document.querySelector("tor-bridges-display");
|
|
| 48 | + const settingGroup = bridgeDisplay?.closest("setting-group");
|
|
| 49 | + if (!settingGroup) {
|
|
| 50 | + console.error("Missing a setting-group for a notification.");
|
|
| 51 | + return;
|
|
| 52 | + }
|
|
| 53 | + if (!settingGroup.checkVisibility()) {
|
|
| 54 | + // Only ping the user if the bridge settings are visible.
|
|
| 55 | + // NOTE: Most operations to change the bridges will occur within the
|
|
| 56 | + // connection settings. However, in principle the user could have multiple
|
|
| 57 | + // setting tabs open, or they may have the settings open whilst Connection
|
|
| 58 | + // Assist is setting their bridges.
|
|
| 59 | + return;
|
|
| 60 | + }
|
|
| 61 | + const [message] = await Promise.all([
|
|
| 62 | + document.l10n.formatValue(updateId),
|
|
| 63 | + // Wait at least a small amount of time to actually trigger ariaNotify.
|
|
| 64 | + // Otherwise Orca will ignore the notification when it almost coincides with
|
|
| 65 | + // a change in focus, which is normally the case.
|
|
| 66 | + new Promise(resolve => setTimeout(resolve, 500)),
|
|
| 67 | + ]);
|
|
| 68 | + bridgeDisplay.ariaNotify(message);
|
|
| 69 | +}
|
|
| 70 | + |
|
| 71 | +/**
|
|
| 72 | + * Controls the bridge grid.
|
|
| 73 | + */
|
|
| 74 | +const gBridgeGrid = {
|
|
| 75 | + /**
|
|
| 76 | + * The grid element.
|
|
| 77 | + *
|
|
| 78 | + * @type {Element?}
|
|
| 79 | + */
|
|
| 80 | + _grid: null,
|
|
| 81 | + /**
|
|
| 82 | + * The template for creating new rows.
|
|
| 83 | + *
|
|
| 84 | + * @type {HTMLTemplateElement?}
|
|
| 85 | + */
|
|
| 86 | + _rowTemplate: null,
|
|
| 87 | + |
|
| 88 | + /**
|
|
| 89 | + * @typedef {object} BridgeGridRow
|
|
| 90 | + *
|
|
| 91 | + * @property {Element} element - The row element.
|
|
| 92 | + * @property {Element} optionsButton - The options button.
|
|
| 93 | + * @property {Element} menu - The options menupopup.
|
|
| 94 | + * @property {Element} statusEl - The bridge status element.
|
|
| 95 | + * @property {Element} statusText - The status text.
|
|
| 96 | + * @property {string} bridgeLine - The identifying bridge string for this row.
|
|
| 97 | + * @property {string?} bridgeId - The ID/fingerprint for the bridge, or null
|
|
| 98 | + * if it doesn't have one.
|
|
| 99 | + * @property {integer} index - The index of the row in the grid.
|
|
| 100 | + * @property {boolean} connected - Whether we are connected to the bridge
|
|
| 101 | + * (recently in use for a Tor circuit).
|
|
| 102 | + * @property {BridgeGridCell[]} cells - The cells that belong to the row,
|
|
| 103 | + * ordered by their column.
|
|
| 104 | + */
|
|
| 105 | + /**
|
|
| 106 | + * @typedef {object} BridgeGridCell
|
|
| 107 | + *
|
|
| 108 | + * @property {Element} element - The cell element.
|
|
| 109 | + * @property {Element} focusEl - The element belonging to the cell that should
|
|
| 110 | + * receive focus. Should be the cell element itself, or an interactive
|
|
| 111 | + * focusable child.
|
|
| 112 | + * @property {integer} columnIndex - The index of the column this cell belongs
|
|
| 113 | + * to.
|
|
| 114 | + * @property {BridgeGridRow} row - The row this cell belongs to.
|
|
| 115 | + */
|
|
| 116 | + /**
|
|
| 117 | + * The current rows in the grid.
|
|
| 118 | + *
|
|
| 119 | + * @type {BridgeGridRow[]}
|
|
| 120 | + */
|
|
| 121 | + _rows: [],
|
|
| 122 | + /**
|
|
| 123 | + * The cell that should be the focus target when the user moves focus into the
|
|
| 124 | + * grid, or null if the grid itself should be the target.
|
|
| 125 | + *
|
|
| 126 | + * @type {BridgeGridCell?}
|
|
| 127 | + */
|
|
| 128 | + _focusCell: null,
|
|
| 129 | + |
|
| 130 | + /**
|
|
| 131 | + * Initialize the bridge grid.
|
|
| 132 | + */
|
|
| 133 | + init() {
|
|
| 134 | + this._grid = document.getElementById("tor-bridges-grid-display");
|
|
| 135 | + // Initially, make only the grid itself part of the keyboard tab cycle.
|
|
| 136 | + // matches _focusCell = null.
|
|
| 137 | + this._grid.tabIndex = 0;
|
|
| 138 | + |
|
| 139 | + this._rowTemplate = document.getElementById(
|
|
| 140 | + "tor-bridges-grid-row-template"
|
|
| 141 | + );
|
|
| 142 | + |
|
| 143 | + this._grid.addEventListener("keydown", this);
|
|
| 144 | + this._grid.addEventListener("mousedown", this);
|
|
| 145 | + this._grid.addEventListener("focusin", this);
|
|
| 146 | + |
|
| 147 | + this._supportedSources = [
|
|
| 148 | + lazy.TorBridgeSource.BridgeDB,
|
|
| 149 | + lazy.TorBridgeSource.UserProvided,
|
|
| 150 | + lazy.TorBridgeSource.Lox,
|
|
| 151 | + ];
|
|
| 152 | + },
|
|
| 153 | + |
|
| 154 | + /**
|
|
| 155 | + * Whether the grid is visible and responsive.
|
|
| 156 | + *
|
|
| 157 | + * @type {boolean}
|
|
| 158 | + */
|
|
| 159 | + _active: false,
|
|
| 160 | + |
|
| 161 | + /**
|
|
| 162 | + * Activate and show the bridge grid.
|
|
| 163 | + */
|
|
| 164 | + activate() {
|
|
| 165 | + if (this._active) {
|
|
| 166 | + return;
|
|
| 167 | + }
|
|
| 168 | + |
|
| 169 | + this._active = true;
|
|
| 170 | + |
|
| 171 | + this._grid.hidden = false;
|
|
| 172 | + },
|
|
| 173 | + |
|
| 174 | + /**
|
|
| 175 | + * Deactivate and hide the bridge grid.
|
|
| 176 | + */
|
|
| 177 | + deactivate() {
|
|
| 178 | + if (!this._active) {
|
|
| 179 | + return;
|
|
| 180 | + }
|
|
| 181 | + |
|
| 182 | + this._active = false;
|
|
| 183 | + |
|
| 184 | + this._forceCloseRowMenus();
|
|
| 185 | + |
|
| 186 | + this._grid.hidden = true;
|
|
| 187 | + },
|
|
| 188 | + |
|
| 189 | + handleEvent(event) {
|
|
| 190 | + if (event.type === "keydown") {
|
|
| 191 | + if (event.altKey || event.shiftKey || event.metaKey || event.ctrlKey) {
|
|
| 192 | + // Don't interfere with these events.
|
|
| 193 | + return;
|
|
| 194 | + }
|
|
| 195 | + |
|
| 196 | + if (this._rows.some(row => row.menu.open)) {
|
|
| 197 | + // Have an open menu, let the menu handle the event instead.
|
|
| 198 | + return;
|
|
| 199 | + }
|
|
| 200 | + |
|
| 201 | + let numRows = this._rows.length;
|
|
| 202 | + if (!numRows) {
|
|
| 203 | + // Nowhere for focus to go.
|
|
| 204 | + return;
|
|
| 205 | + }
|
|
| 206 | + |
|
| 207 | + let moveRow = 0;
|
|
| 208 | + let moveColumn = 0;
|
|
| 209 | + const isLTR = this._grid.matches(":dir(ltr)");
|
|
| 210 | + switch (event.key) {
|
|
| 211 | + case "ArrowDown":
|
|
| 212 | + moveRow = 1;
|
|
| 213 | + break;
|
|
| 214 | + case "ArrowUp":
|
|
| 215 | + moveRow = -1;
|
|
| 216 | + break;
|
|
| 217 | + case "ArrowRight":
|
|
| 218 | + moveColumn = isLTR ? 1 : -1;
|
|
| 219 | + break;
|
|
| 220 | + case "ArrowLeft":
|
|
| 221 | + moveColumn = isLTR ? -1 : 1;
|
|
| 222 | + break;
|
|
| 223 | + default:
|
|
| 224 | + return;
|
|
| 225 | + }
|
|
| 226 | + |
|
| 227 | + // Prevent scrolling the nearest scroll container.
|
|
| 228 | + event.preventDefault();
|
|
| 229 | + |
|
| 230 | + const curCell = this._focusCell;
|
|
| 231 | + let row = curCell ? curCell.row.index + moveRow : 0;
|
|
| 232 | + let column = curCell ? curCell.columnIndex + moveColumn : 0;
|
|
| 233 | + |
|
| 234 | + // Clamp in bounds.
|
|
| 235 | + if (row < 0) {
|
|
| 236 | + row = 0;
|
|
| 237 | + } else if (row >= numRows) {
|
|
| 238 | + row = numRows - 1;
|
|
| 239 | + }
|
|
| 240 | + |
|
| 241 | + const numCells = this._rows[row].cells.length;
|
|
| 242 | + if (column < 0) {
|
|
| 243 | + column = 0;
|
|
| 244 | + } else if (column >= numCells) {
|
|
| 245 | + column = numCells - 1;
|
|
| 246 | + }
|
|
| 247 | + |
|
| 248 | + const newCell = this._rows[row].cells[column];
|
|
| 249 | + |
|
| 250 | + if (newCell !== curCell) {
|
|
| 251 | + this._setFocus(newCell);
|
|
| 252 | + }
|
|
| 253 | + } else if (event.type === "mousedown") {
|
|
| 254 | + if (event.button !== 0) {
|
|
| 255 | + return;
|
|
| 256 | + }
|
|
| 257 | + // Move focus index to the clicked target.
|
|
| 258 | + // NOTE: Since the cells and the grid have "tabindex=-1", they are still
|
|
| 259 | + // click-focusable. Therefore, the default mousedown handler will try to
|
|
| 260 | + // move focus to it.
|
|
| 261 | + // Rather than block this default handler, we instead re-direct the focus
|
|
| 262 | + // to the correct cell in the "focusin" listener.
|
|
| 263 | + const newCell = this._getCellFromTarget(event.target);
|
|
| 264 | + // NOTE: If newCell is null, then we do nothing here, but instead wait for
|
|
| 265 | + // the focusin handler to trigger.
|
|
| 266 | + if (newCell && newCell !== this._focusCell) {
|
|
| 267 | + this._setFocus(newCell);
|
|
| 268 | + }
|
|
| 269 | + } else if (event.type === "focusin") {
|
|
| 270 | + const focusCell = this._getCellFromTarget(event.target);
|
|
| 271 | + if (focusCell !== this._focusCell) {
|
|
| 272 | + // Focus is not where it is expected.
|
|
| 273 | + // E.g. the user has clicked the edge of the grid.
|
|
| 274 | + // Restore focus immediately back to the cell we expect.
|
|
| 275 | + this._setFocus(this._focusCell);
|
|
| 276 | + }
|
|
| 277 | + }
|
|
| 278 | + },
|
|
| 279 | + |
|
| 280 | + /**
|
|
| 281 | + * Return the cell that was the target of an event.
|
|
| 282 | + *
|
|
| 283 | + * @param {Element} element - The target of an event.
|
|
| 284 | + *
|
|
| 285 | + * @returns {BridgeGridCell?} - The cell that the element belongs to, or null
|
|
| 286 | + * if it doesn't belong to any cell.
|
|
| 287 | + */
|
|
| 288 | + _getCellFromTarget(element) {
|
|
| 289 | + for (const row of this._rows) {
|
|
| 290 | + for (const cell of row.cells) {
|
|
| 291 | + if (cell.element.contains(element)) {
|
|
| 292 | + return cell;
|
|
| 293 | + }
|
|
| 294 | + }
|
|
| 295 | + }
|
|
| 296 | + return null;
|
|
| 297 | + },
|
|
| 298 | + |
|
| 299 | + /**
|
|
| 300 | + * Determine whether the document's active element (focus) is within the grid
|
|
| 301 | + * or not.
|
|
| 302 | + *
|
|
| 303 | + * @returns {boolean} - Whether focus is within this grid or not.
|
|
| 304 | + */
|
|
| 305 | + _focusWithin() {
|
|
| 306 | + return this._grid.contains(document.activeElement);
|
|
| 307 | + },
|
|
| 308 | + |
|
| 309 | + /**
|
|
| 310 | + * Set the cell that should be the focus target of the grid, possibly moving
|
|
| 311 | + * the document's focus as well.
|
|
| 312 | + *
|
|
| 313 | + * @param {BridgeGridCell?} cell - The cell to make the focus target, or null
|
|
| 314 | + * if the grid itself should be the target.
|
|
| 315 | + * @param {boolean} [focusWithin] - Whether focus should be moved within the
|
|
| 316 | + * grid. If undefined, this will move focus if the grid currently contains
|
|
| 317 | + * the document's focus.
|
|
| 318 | + */
|
|
| 319 | + _setFocus(cell, focusWithin) {
|
|
| 320 | + if (focusWithin === undefined) {
|
|
| 321 | + focusWithin = this._focusWithin();
|
|
| 322 | + }
|
|
| 323 | + const prevFocusElement = this._focusCell
|
|
| 324 | + ? this._focusCell.focusEl
|
|
| 325 | + : this._grid;
|
|
| 326 | + const newFocusElement = cell ? cell.focusEl : this._grid;
|
|
| 327 | + |
|
| 328 | + if (prevFocusElement !== newFocusElement) {
|
|
| 329 | + prevFocusElement.tabIndex = -1;
|
|
| 330 | + newFocusElement.tabIndex = 0;
|
|
| 331 | + }
|
|
| 332 | + // Set _focusCell now, before we potentially call "focus", which can trigger
|
|
| 333 | + // the "focusin" handler.
|
|
| 334 | + this._focusCell = cell;
|
|
| 335 | + |
|
| 336 | + if (focusWithin) {
|
|
| 337 | + // Focus was within the grid, so we need to actively move it to the new
|
|
| 338 | + // element.
|
|
| 339 | + newFocusElement.focus({ preventScroll: true });
|
|
| 340 | + // Scroll to the whole cell into view, rather than just the focus element.
|
|
| 341 | + (cell?.element ?? newFocusElement).scrollIntoView({
|
|
| 342 | + block: "nearest",
|
|
| 343 | + inline: "nearest",
|
|
| 344 | + });
|
|
| 345 | + }
|
|
| 346 | + },
|
|
| 347 | + |
|
| 348 | + /**
|
|
| 349 | + * Reset the grids focus to be the first row's first cell, if any.
|
|
| 350 | + *
|
|
| 351 | + * @param {boolean} [focusWithin] - Whether focus should be moved within the
|
|
| 352 | + * grid. If undefined, this will move focus if the grid currently contains
|
|
| 353 | + * the document's focus.
|
|
| 354 | + */
|
|
| 355 | + _resetFocus(focusWithin) {
|
|
| 356 | + this._setFocus(
|
|
| 357 | + this._rows.length ? this._rows[0].cells[0] : null,
|
|
| 358 | + focusWithin
|
|
| 359 | + );
|
|
| 360 | + },
|
|
| 361 | + |
|
| 362 | + /**
|
|
| 363 | + * The bridge ID/fingerprint of the most recently used bridge (appearing in
|
|
| 364 | + * the latest Tor circuit). Roughly corresponds to the bridge we are currently
|
|
| 365 | + * connected to.
|
|
| 366 | + *
|
|
| 367 | + * null if there are no such bridges.
|
|
| 368 | + *
|
|
| 369 | + * @type {string?}
|
|
| 370 | + */
|
|
| 371 | + _connectedBridgeId: null,
|
|
| 372 | + |
|
| 373 | + set connectedBridgeId(bridgeId) {
|
|
| 374 | + if (bridgeId === this._connectedBridgeId) {
|
|
| 375 | + return;
|
|
| 376 | + }
|
|
| 377 | + this._connectedBridgeId = bridgeId;
|
|
| 378 | + for (const row of this._rows) {
|
|
| 379 | + this._updateRowStatus(row);
|
|
| 380 | + }
|
|
| 381 | + },
|
|
| 382 | + |
|
| 383 | + /**
|
|
| 384 | + * Update the status of a row.
|
|
| 385 | + *
|
|
| 386 | + * @param {BridgeGridRow} row - The row to update.
|
|
| 387 | + */
|
|
| 388 | + _updateRowStatus(row) {
|
|
| 389 | + const connected = row.bridgeId && this._connectedBridgeId === row.bridgeId;
|
|
| 390 | + // NOTE: row.connected is initially undefined, so won't match `connected`.
|
|
| 391 | + if (connected === row.connected) {
|
|
| 392 | + return;
|
|
| 393 | + }
|
|
| 394 | + |
|
| 395 | + row.connected = connected;
|
|
| 396 | + |
|
| 397 | + const noStatus = !connected;
|
|
| 398 | + |
|
| 399 | + row.element.classList.toggle("hide-status", noStatus);
|
|
| 400 | + row.statusEl.classList.toggle("bridge-status-none", noStatus);
|
|
| 401 | + row.statusEl.classList.toggle("bridge-status-connected", connected);
|
|
| 402 | + |
|
| 403 | + if (connected) {
|
|
| 404 | + document.l10n.setAttributes(
|
|
| 405 | + row.statusText,
|
|
| 406 | + "tor-bridges-status-connected"
|
|
| 407 | + );
|
|
| 408 | + } else {
|
|
| 409 | + document.l10n.setAttributes(row.statusText, "tor-bridges-status-none");
|
|
| 410 | + }
|
|
| 411 | + },
|
|
| 412 | + |
|
| 413 | + /**
|
|
| 414 | + * Create a new row for the grid.
|
|
| 415 | + *
|
|
| 416 | + * @param {string} bridgeLine - The bridge line for this row, which also acts
|
|
| 417 | + * as its ID.
|
|
| 418 | + *
|
|
| 419 | + * @returns {BridgeGridRow} - A new row, with then "index" unset and the
|
|
| 420 | + * "element" without a parent.
|
|
| 421 | + */
|
|
| 422 | + _createRow(bridgeLine) {
|
|
| 423 | + let details;
|
|
| 424 | + try {
|
|
| 425 | + details = lazy.TorParsers.parseBridgeLine(bridgeLine);
|
|
| 426 | + } catch (e) {
|
|
| 427 | + console.error(`Detected invalid bridge line: ${bridgeLine}`, e);
|
|
| 428 | + }
|
|
| 429 | + const row = {
|
|
| 430 | + element: this._rowTemplate.content.children[0].cloneNode(true),
|
|
| 431 | + bridgeLine,
|
|
| 432 | + bridgeId: details?.id ?? null,
|
|
| 433 | + cells: [],
|
|
| 434 | + };
|
|
| 435 | + |
|
| 436 | + const emojiBlock = row.element.querySelector(".tor-bridges-emojis-block");
|
|
| 437 | + const BridgeEmoji = customElements.get("tor-bridge-emoji");
|
|
| 438 | + for (const cell of BridgeEmoji.createForAddress(bridgeLine)) {
|
|
| 439 | + // Each emoji is its own cell, we rely on the fact that createForAddress
|
|
| 440 | + // always returns four elements.
|
|
| 441 | + cell.setAttribute("role", "gridcell");
|
|
| 442 | + cell.classList.add("tor-bridges-grid-cell", "tor-bridges-emoji-cell");
|
|
| 443 | + emojiBlock.append(cell);
|
|
| 444 | + }
|
|
| 445 | + |
|
| 446 | + for (const [columnIndex, element] of row.element
|
|
| 447 | + .querySelectorAll(".tor-bridges-grid-cell")
|
|
| 448 | + .entries()) {
|
|
| 449 | + const focusEl =
|
|
| 450 | + element.querySelector(".tor-bridges-grid-focus") ?? element;
|
|
| 451 | + // Set a negative tabIndex, this makes the element click-focusable but not
|
|
| 452 | + // part of the tab navigation sequence.
|
|
| 453 | + focusEl.tabIndex = -1;
|
|
| 454 | + row.cells.push({ element, focusEl, columnIndex, row });
|
|
| 455 | + }
|
|
| 456 | + |
|
| 457 | + const transport = details?.transport ?? "vanilla";
|
|
| 458 | + const typeCell = row.element.querySelector(".tor-bridges-type-cell");
|
|
| 459 | + if (transport === "vanilla") {
|
|
| 460 | + document.l10n.setAttributes(typeCell, "tor-bridges-type-prefix-generic");
|
|
| 461 | + } else {
|
|
| 462 | + document.l10n.setAttributes(typeCell, "tor-bridges-type-prefix", {
|
|
| 463 | + type: transport,
|
|
| 464 | + });
|
|
| 465 | + }
|
|
| 466 | + |
|
| 467 | + row.element.querySelector(".tor-bridges-address-cell-text").textContent =
|
|
| 468 | + bridgeLine;
|
|
| 469 | + |
|
| 470 | + row.statusEl = row.element.querySelector(
|
|
| 471 | + ".tor-bridges-status-cell .bridge-status-badge"
|
|
| 472 | + );
|
|
| 473 | + row.statusText = row.element.querySelector(".tor-bridges-status-cell-text");
|
|
| 474 | + |
|
| 475 | + this._initRowMenu(row);
|
|
| 476 | + |
|
| 477 | + this._updateRowStatus(row);
|
|
| 478 | + return row;
|
|
| 479 | + },
|
|
| 480 | + |
|
| 481 | + /**
|
|
| 482 | + * The row menu index used for generating new ids.
|
|
| 483 | + *
|
|
| 484 | + * @type {integer}
|
|
| 485 | + */
|
|
| 486 | + _rowMenuIndex: 0,
|
|
| 487 | + /**
|
|
| 488 | + * Generate a new id for the options menu.
|
|
| 489 | + *
|
|
| 490 | + * @returns {string} - The new id.
|
|
| 491 | + */
|
|
| 492 | + _generateRowMenuId() {
|
|
| 493 | + const id = `tor-bridges-individual-options-menu-${this._rowMenuIndex}`;
|
|
| 494 | + // Assume we won't run out of ids.
|
|
| 495 | + this._rowMenuIndex++;
|
|
| 496 | + return id;
|
|
| 497 | + },
|
|
| 498 | + |
|
| 499 | + /**
|
|
| 500 | + * Initialize the shared menu for a row.
|
|
| 501 | + *
|
|
| 502 | + * @param {BridgeGridRow} row - The row to initialize the menu of.
|
|
| 503 | + */
|
|
| 504 | + _initRowMenu(row) {
|
|
| 505 | + row.menu = row.element.querySelector(
|
|
| 506 | + ".tor-bridges-individual-options-menu"
|
|
| 507 | + );
|
|
| 508 | + row.optionsButton = row.element.querySelector(
|
|
| 509 | + ".tor-bridges-options-cell-button"
|
|
| 510 | + );
|
|
| 511 | + |
|
| 512 | + row.menu.id = this._generateRowMenuId();
|
|
| 513 | + row.optionsButton.setAttribute("aria-controls", row.menu.id);
|
|
| 514 | + |
|
| 515 | + row.optionsButton.addEventListener("click", event => {
|
|
| 516 | + row.menu.toggle(event);
|
|
| 517 | + });
|
|
| 518 | + |
|
| 519 | + row.menu.addEventListener("hidden", () => {
|
|
| 520 | + // Make sure the button receives focus again when the menu is hidden.
|
|
| 521 | + // Currently, panel-list.js only does this when the menu is opened with a
|
|
| 522 | + // keyboard, but this causes focus to be lost from the page if the user
|
|
| 523 | + // uses a mixture of keyboard and mouse.
|
|
| 524 | + row.optionsButton.focus();
|
|
| 525 | + });
|
|
| 526 | + |
|
| 527 | + const qrItem = row.menu.querySelector(
|
|
| 528 | + ".tor-bridges-options-qr-one-menu-item"
|
|
| 529 | + );
|
|
| 530 | + const removeItem = row.menu.querySelector(
|
|
| 531 | + ".tor-bridges-options-remove-one-menu-item"
|
|
| 532 | + );
|
|
| 533 | + row.menu.addEventListener("showing", () => {
|
|
| 534 | + const show =
|
|
| 535 | + this._bridgeSource === lazy.TorBridgeSource.UserProvided ||
|
|
| 536 | + this._bridgeSource === lazy.TorBridgeSource.BridgeDB;
|
|
| 537 | + qrItem.hidden = !show;
|
|
| 538 | + removeItem.hidden = !show;
|
|
| 539 | + });
|
|
| 540 | + |
|
| 541 | + qrItem.addEventListener("click", () => {
|
|
| 542 | + const bridgeLine = row.bridgeLine;
|
|
| 543 | + if (!bridgeLine) {
|
|
| 544 | + return;
|
|
| 545 | + }
|
|
| 546 | + showBridgeQr(bridgeLine);
|
|
| 547 | + });
|
|
| 548 | + row.menu
|
|
| 549 | + .querySelector(".tor-bridges-options-copy-one-menu-item")
|
|
| 550 | + .addEventListener("click", () => {
|
|
| 551 | + const clipboard = Cc[
|
|
| 552 | + "@mozilla.org/widget/clipboardhelper;1"
|
|
| 553 | + ].getService(Ci.nsIClipboardHelper);
|
|
| 554 | + clipboard.copyString(row.bridgeLine);
|
|
| 555 | + });
|
|
| 556 | + removeItem.addEventListener("click", () => {
|
|
| 557 | + const bridgeLine = row.bridgeLine;
|
|
| 558 | + const source = lazy.TorSettings.bridges.source;
|
|
| 559 | + if (source !== this._bridgesVal?.source) {
|
|
| 560 | + // Our value is stale, abort.
|
|
| 561 | + return;
|
|
| 562 | + }
|
|
| 563 | + const strings = lazy.TorSettings.bridges.bridge_strings;
|
|
| 564 | + const index = strings.indexOf(bridgeLine);
|
|
| 565 | + if (index === -1) {
|
|
| 566 | + return;
|
|
| 567 | + }
|
|
| 568 | + strings.splice(index, 1);
|
|
| 569 | + |
|
| 570 | + if (strings.length) {
|
|
| 571 | + lazy.TorSettings.changeSettings({
|
|
| 572 | + bridges: { source, bridge_strings: strings },
|
|
| 573 | + });
|
|
| 574 | + } else {
|
|
| 575 | + // Remove all bridges and disable.
|
|
| 576 | + lazy.TorSettings.changeSettings({
|
|
| 577 | + bridges: { source: lazy.TorBridgeSource.Invalid },
|
|
| 578 | + });
|
|
| 579 | + }
|
|
| 580 | + });
|
|
| 581 | + },
|
|
| 582 | + |
|
| 583 | + /**
|
|
| 584 | + * Force the row menu to close.
|
|
| 585 | + */
|
|
| 586 | + _forceCloseRowMenus() {
|
|
| 587 | + for (const row of this._rows) {
|
|
| 588 | + row.menu.hide(null, { force: true });
|
|
| 589 | + }
|
|
| 590 | + },
|
|
| 591 | + |
|
| 592 | + /**
|
|
| 593 | + * The known bridge source.
|
|
| 594 | + *
|
|
| 595 | + * Initially null to indicate that it is unset.
|
|
| 596 | + *
|
|
| 597 | + * @type {integer?}
|
|
| 598 | + */
|
|
| 599 | + _bridgeSource: null,
|
|
| 600 | + /**
|
|
| 601 | + * The bridge sources this is shown for.
|
|
| 602 | + *
|
|
| 603 | + * @type {string[]}
|
|
| 604 | + */
|
|
| 605 | + _supportedSources: [],
|
|
| 606 | + |
|
| 607 | + /**
|
|
| 608 | + * The bridges value, set by the setting-control element.
|
|
| 609 | + *
|
|
| 610 | + * @type {object}
|
|
| 611 | + */
|
|
| 612 | + _bridgesVal: null,
|
|
| 613 | + |
|
| 614 | + set bridges(val) {
|
|
| 615 | + if (val === null) {
|
|
| 616 | + // Ignore and wait for the initial.
|
|
| 617 | + return;
|
|
| 618 | + }
|
|
| 619 | + const initial = this._bridgesVal === null;
|
|
| 620 | + this._bridgesVal = val;
|
|
| 621 | + this._updateRows(initial);
|
|
| 622 | + },
|
|
| 623 | + |
|
| 624 | + /**
|
|
| 625 | + * Update the grid to show the latest bridge strings.
|
|
| 626 | + *
|
|
| 627 | + * @param {boolean} initializing - Whether this is being called as part of
|
|
| 628 | + * initialization.
|
|
| 629 | + */
|
|
| 630 | + _updateRows(initializing) {
|
|
| 631 | + // Store whether we have focus within the grid, before removing or hiding
|
|
| 632 | + // DOM elements.
|
|
| 633 | + const focusWithin = this._focusWithin();
|
|
| 634 | + |
|
| 635 | + let lostAllBridges = false;
|
|
| 636 | + let newSource = false;
|
|
| 637 | + const bridgeSource = this._bridgesVal.source;
|
|
| 638 | + if (bridgeSource !== this._bridgeSource) {
|
|
| 639 | + newSource = true;
|
|
| 640 | + |
|
| 641 | + this._bridgeSource = bridgeSource;
|
|
| 642 | + |
|
| 643 | + if (this._supportedSources.includes(bridgeSource)) {
|
|
| 644 | + this.activate();
|
|
| 645 | + } else {
|
|
| 646 | + if (this._active && bridgeSource === lazy.TorBridgeSource.Invalid) {
|
|
| 647 | + lostAllBridges = true;
|
|
| 648 | + }
|
|
| 649 | + this.deactivate();
|
|
| 650 | + }
|
|
| 651 | + }
|
|
| 652 | + |
|
| 653 | + const ordered = this._active
|
|
| 654 | + ? this._bridgesVal.bridgeStrings.map(bridgeLine => {
|
|
| 655 | + const row = this._rows.find(r => r.bridgeLine === bridgeLine);
|
|
| 656 | + if (row) {
|
|
| 657 | + return row;
|
|
| 658 | + }
|
|
| 659 | + return this._createRow(bridgeLine);
|
|
| 660 | + })
|
|
| 661 | + : [];
|
|
| 662 | + |
|
| 663 | + // Whether we should reset the grid's focus.
|
|
| 664 | + // We always reset when we have a new bridge source.
|
|
| 665 | + // We reset the focus if no current Cell has focus. I.e. when adding a row
|
|
| 666 | + // to an empty grid, we want the focus to move to the first item.
|
|
| 667 | + // We also reset the focus if the current Cell is in a row that will be
|
|
| 668 | + // removed (including if all rows are removed).
|
|
| 669 | + // NOTE: In principle, if a row is removed, we could move the focus to the
|
|
| 670 | + // next or previous row (in the same cell column). However, most likely if
|
|
| 671 | + // the grid has the user focus, they are removing a single row using its
|
|
| 672 | + // options button. In this case, returning the user to some other row's
|
|
| 673 | + // options button might be more disorienting since it would not be simple
|
|
| 674 | + // for them to know *which* bridge they have landed on.
|
|
| 675 | + // NOTE: We do not reset the focus in other cases because we do not want the
|
|
| 676 | + // user to loose their place in the grid unnecessarily.
|
|
| 677 | + let resetFocus =
|
|
| 678 | + newSource || !this._focusCell || !ordered.includes(this._focusCell.row);
|
|
| 679 | + |
|
| 680 | + // Remove rows no longer needed from the DOM.
|
|
| 681 | + let numRowsRemoved = 0;
|
|
| 682 | + let rowAddedOrMoved = false;
|
|
| 683 | + |
|
| 684 | + for (const row of this._rows) {
|
|
| 685 | + if (!ordered.includes(row)) {
|
|
| 686 | + numRowsRemoved++;
|
|
| 687 | + // If the row menu was open, it will also be deleted.
|
|
| 688 | + // NOTE: Since the row menu is part of the row, focusWithin will be true
|
|
| 689 | + // if the menu had focus, so focus should be re-assigned.
|
|
| 690 | + row.element.remove();
|
|
| 691 | + }
|
|
| 692 | + }
|
|
| 693 | + |
|
| 694 | + // Go through all the rows to set their ".index" property and to ensure they
|
|
| 695 | + // are in the correct position in the DOM.
|
|
| 696 | + // NOTE: We could use replaceChildren to get the correct DOM structure, but
|
|
| 697 | + // we want to avoid rebuilding the entire tree when a single row is added or
|
|
| 698 | + // removed.
|
|
| 699 | + for (const [index, row] of ordered.entries()) {
|
|
| 700 | + row.index = index;
|
|
| 701 | + const element = row.element;
|
|
| 702 | + // Get the expected previous element, that should already be in the DOM
|
|
| 703 | + // from the previous loop.
|
|
| 704 | + const prevEl = index ? ordered[index - 1].element : null;
|
|
| 705 | + |
|
| 706 | + if (
|
|
| 707 | + element.parentElement === this._grid &&
|
|
| 708 | + prevEl === element.previousElementSibling
|
|
| 709 | + ) {
|
|
| 710 | + // Already in the correct position in the DOM.
|
|
| 711 | + continue;
|
|
| 712 | + }
|
|
| 713 | + |
|
| 714 | + rowAddedOrMoved = true;
|
|
| 715 | + // NOTE: Any elements already in the DOM, but not in the correct position
|
|
| 716 | + // will be removed and re-added by the below command.
|
|
| 717 | + // NOTE: if the row has document focus, then it should remain there.
|
|
| 718 | + if (prevEl) {
|
|
| 719 | + prevEl.after(element);
|
|
| 720 | + } else {
|
|
| 721 | + this._grid.prepend(element);
|
|
| 722 | + }
|
|
| 723 | + }
|
|
| 724 | + this._rows = ordered;
|
|
| 725 | + |
|
| 726 | + // Restore any lost focus.
|
|
| 727 | + if (resetFocus) {
|
|
| 728 | + // If we are not active (and therefore hidden), we will not try and move
|
|
| 729 | + // focus (activeElement), but may still change the *focusable* element for
|
|
| 730 | + // when we are shown again.
|
|
| 731 | + this._resetFocus(this._active && focusWithin);
|
|
| 732 | + }
|
|
| 733 | + // NOTE: In the case we were previously active and now inactive,
|
|
| 734 | + // tor-bridges-display will have already moved the focus out of this area.
|
|
| 735 | + |
|
| 736 | + // Notify the user if there was some change to the DOM.
|
|
| 737 | + // If we are initializing, we generate no notification since there has been
|
|
| 738 | + // no change in the setting.
|
|
| 739 | + if (!initializing) {
|
|
| 740 | + let notificationType;
|
|
| 741 | + if (lostAllBridges) {
|
|
| 742 | + // Just lost all bridges, and became de-active.
|
|
| 743 | + notificationType = "removed-all";
|
|
| 744 | + } else if (this._rows.length) {
|
|
| 745 | + // Otherwise, only generate a notification if we are still active, with
|
|
| 746 | + // at least one bridge.
|
|
| 747 | + // I.e. do not generate a message if the new source is "builtin".
|
|
| 748 | + if (newSource) {
|
|
| 749 | + // A change in source.
|
|
| 750 | + notificationType = "changed";
|
|
| 751 | + } else if (numRowsRemoved === 1 && !rowAddedOrMoved) {
|
|
| 752 | + // Only one bridge was removed. This is most likely in response to them
|
|
| 753 | + // manually removing a single bridge or using the bridge row's options
|
|
| 754 | + // menu.
|
|
| 755 | + notificationType = "removed-one";
|
|
| 756 | + } else if (numRowsRemoved || rowAddedOrMoved) {
|
|
| 757 | + // Some other change. This is most likely in response to a manual edit
|
|
| 758 | + // of the existing bridges.
|
|
| 759 | + notificationType = "changed";
|
|
| 760 | + }
|
|
| 761 | + // Else, there was no change.
|
|
| 762 | + }
|
|
| 763 | + |
|
| 764 | + if (notificationType) {
|
|
| 765 | + postBridgeNotification(notificationType);
|
|
| 766 | + }
|
|
| 767 | + }
|
|
| 768 | + },
|
|
| 769 | +};
|
|
| 770 | + |
|
| 771 | +/**
|
|
| 772 | + * Controls the built-in bridges area.
|
|
| 773 | + */
|
|
| 774 | +const gBuiltinBridgesArea = {
|
|
| 775 | + /**
|
|
| 776 | + * The display area.
|
|
| 777 | + *
|
|
| 778 | + * @type {Element?}
|
|
| 779 | + */
|
|
| 780 | + _area: null,
|
|
| 781 | + /**
|
|
| 782 | + * The type name element.
|
|
| 783 | + *
|
|
| 784 | + * @type {Element?}
|
|
| 785 | + */
|
|
| 786 | + _nameEl: null,
|
|
| 787 | + /**
|
|
| 788 | + * The bridge type description element.
|
|
| 789 | + *
|
|
| 790 | + * @type {Element?}
|
|
| 791 | + */
|
|
| 792 | + _descriptionEl: null,
|
|
| 793 | + /**
|
|
| 794 | + * The connection status.
|
|
| 795 | + *
|
|
| 796 | + * @type {Element?}
|
|
| 797 | + */
|
|
| 798 | + _connectionStatusEl: null,
|
|
| 799 | + |
|
| 800 | + /**
|
|
| 801 | + * Initialize the built-in bridges area.
|
|
| 802 | + */
|
|
| 803 | + init() {
|
|
| 804 | + this._area = document.getElementById("tor-bridges-built-in-display");
|
|
| 805 | + this._nameEl = document.getElementById("tor-bridges-built-in-type-name");
|
|
| 806 | + this._descriptionEl = document.getElementById(
|
|
| 807 | + "tor-bridges-built-in-description"
|
|
| 808 | + );
|
|
| 809 | + this._connectionStatusEl = document.getElementById(
|
|
| 810 | + "tor-bridges-built-in-connected"
|
|
| 811 | + );
|
|
| 812 | + },
|
|
| 813 | + |
|
| 814 | + /**
|
|
| 815 | + * Whether the built-in area is visible and responsive.
|
|
| 816 | + *
|
|
| 817 | + * @type {boolean}
|
|
| 818 | + */
|
|
| 819 | + _active: false,
|
|
| 820 | + |
|
| 821 | + /**
|
|
| 822 | + * Activate and show the built-in bridge area.
|
|
| 823 | + */
|
|
| 824 | + activate() {
|
|
| 825 | + if (this._active) {
|
|
| 826 | + return;
|
|
| 827 | + }
|
|
| 828 | + this._active = true;
|
|
| 829 | + |
|
| 830 | + this._area.hidden = false;
|
|
| 831 | + },
|
|
| 832 | + |
|
| 833 | + /**
|
|
| 834 | + * Deactivate and hide built-in bridge area.
|
|
| 835 | + */
|
|
| 836 | + deactivate() {
|
|
| 837 | + if (!this._active) {
|
|
| 838 | + return;
|
|
| 839 | + }
|
|
| 840 | + this._active = false;
|
|
| 841 | + |
|
| 842 | + this._area.hidden = true;
|
|
| 843 | + },
|
|
| 844 | + |
|
| 845 | + /**
|
|
| 846 | + * Updates the shown connected state.
|
|
| 847 | + */
|
|
| 848 | + _updateConnectedState() {
|
|
| 849 | + this._connectionStatusEl.classList.toggle(
|
|
| 850 | + "bridge-status-connected",
|
|
| 851 | + this._bridgeType &&
|
|
| 852 | + this._connectedBridgeId &&
|
|
| 853 | + this._bridgeIds.includes(this._connectedBridgeId)
|
|
| 854 | + );
|
|
| 855 | + },
|
|
| 856 | + |
|
| 857 | + /**
|
|
| 858 | + * The bridges value, set by the setting-control element.
|
|
| 859 | + *
|
|
| 860 | + * @type {object}
|
|
| 861 | + */
|
|
| 862 | + _bridgesVal: null,
|
|
| 863 | + set bridges(val) {
|
|
| 864 | + if (val === null) {
|
|
| 865 | + // Ignore and wait for the initial.
|
|
| 866 | + return;
|
|
| 867 | + }
|
|
| 868 | + const initial = this._bridgesVal === null;
|
|
| 869 | + this._bridgesVal = val;
|
|
| 870 | + this._updateBridgeType(initial);
|
|
| 871 | + this._updateBridgeIds();
|
|
| 872 | + },
|
|
| 873 | + |
|
| 874 | + /**
|
|
| 875 | + * The currently shown bridge type. Empty if deactivated, and null if
|
|
| 876 | + * uninitialized.
|
|
| 877 | + *
|
|
| 878 | + * @type {string?}
|
|
| 879 | + */
|
|
| 880 | + _bridgeType: null,
|
|
| 881 | + /**
|
|
| 882 | + * The strings for each known bridge type.
|
|
| 883 | + *
|
|
| 884 | + * @type {{[key: string]: {[key: string]: string}}}
|
|
| 885 | + */
|
|
| 886 | + _bridgeTypeStrings: {
|
|
| 887 | + obfs4: {
|
|
| 888 | + name: "tor-bridges-built-in-obfs4-name",
|
|
| 889 | + description: "tor-bridges-built-in-obfs4-description",
|
|
| 890 | + },
|
|
| 891 | + snowflake: {
|
|
| 892 | + name: "tor-bridges-built-in-snowflake-name",
|
|
| 893 | + description: "tor-bridges-built-in-snowflake-description",
|
|
| 894 | + },
|
|
| 895 | + meek: {
|
|
| 896 | + name: "tor-bridges-built-in-meek-name",
|
|
| 897 | + description: "tor-bridges-built-in-meek-description",
|
|
| 898 | + },
|
|
| 899 | + },
|
|
| 900 | + |
|
| 901 | + /**
|
|
| 902 | + * The known bridge source.
|
|
| 903 | + *
|
|
| 904 | + * Initially null to indicate that it is unset.
|
|
| 905 | + *
|
|
| 906 | + * @type {integer?}
|
|
| 907 | + */
|
|
| 908 | + _bridgeSource: null,
|
|
| 909 | + |
|
| 910 | + /**
|
|
| 911 | + * Update the shown bridge type.
|
|
| 912 | + *
|
|
| 913 | + * @param {boolean} initializing - Whether this is being called as part of
|
|
| 914 | + * initialization.
|
|
| 915 | + */
|
|
| 916 | + async _updateBridgeType(initializing) {
|
|
| 917 | + let lostAllBridges = false;
|
|
| 918 | + let newSource = false;
|
|
| 919 | + const bridgeSource = this._bridgesVal.source;
|
|
| 920 | + if (bridgeSource !== this._bridgeSource) {
|
|
| 921 | + newSource = true;
|
|
| 922 | + |
|
| 923 | + this._bridgeSource = bridgeSource;
|
|
| 924 | + |
|
| 925 | + if (bridgeSource === lazy.TorBridgeSource.BuiltIn) {
|
|
| 926 | + this.activate();
|
|
| 927 | + } else {
|
|
| 928 | + if (this._active && bridgeSource === lazy.TorBridgeSource.Invalid) {
|
|
| 929 | + lostAllBridges = true;
|
|
| 930 | + }
|
|
| 931 | + this.deactivate();
|
|
| 932 | + // NOTE: In the case we were previously active, tor-bridges-display will
|
|
| 933 | + // have already moved the focus out of this area.
|
|
| 934 | + }
|
|
| 935 | + }
|
|
| 936 | + |
|
| 937 | + const bridgeType = this._active ? this._bridgesVal.builtinType : "";
|
|
| 938 | + |
|
| 939 | + let newType = false;
|
|
| 940 | + if (bridgeType !== this._bridgeType) {
|
|
| 941 | + newType = true;
|
|
| 942 | + |
|
| 943 | + this._bridgeType = bridgeType;
|
|
| 944 | + |
|
| 945 | + const bridgeStrings = this._bridgeTypeStrings[bridgeType];
|
|
| 946 | + if (bridgeStrings) {
|
|
| 947 | + document.l10n.setAttributes(this._nameEl, bridgeStrings.name);
|
|
| 948 | + document.l10n.setAttributes(
|
|
| 949 | + this._descriptionEl,
|
|
| 950 | + bridgeStrings.description
|
|
| 951 | + );
|
|
| 952 | + } else {
|
|
| 953 | + // Unknown type, or no type.
|
|
| 954 | + this._nameEl.removeAttribute("data-l10n-id");
|
|
| 955 | + this._nameEl.textContent = bridgeType;
|
|
| 956 | + this._descriptionEl.removeAttribute("data-l10n-id");
|
|
| 957 | + this._descriptionEl.textContent = "";
|
|
| 958 | + }
|
|
| 959 | + |
|
| 960 | + this._updateConnectedState();
|
|
| 961 | + }
|
|
| 962 | + |
|
| 963 | + // Notify the user if there was some change to the type.
|
|
| 964 | + // If we are initializing, we generate no notification since there has been
|
|
| 965 | + // no change in the setting.
|
|
| 966 | + if (!initializing) {
|
|
| 967 | + let notificationType;
|
|
| 968 | + if (lostAllBridges) {
|
|
| 969 | + // Just lost all bridges, and became de-active.
|
|
| 970 | + notificationType = "removed-all";
|
|
| 971 | + } else if (this._active && (newSource || newType)) {
|
|
| 972 | + // Otherwise, only generate a notification if we are still active, with
|
|
| 973 | + // a bridge type.
|
|
| 974 | + // I.e. do not generate a message if the new source is not "builtin".
|
|
| 975 | + notificationType = "changed";
|
|
| 976 | + }
|
|
| 977 | + |
|
| 978 | + if (notificationType) {
|
|
| 979 | + postBridgeNotification(notificationType);
|
|
| 980 | + }
|
|
| 981 | + }
|
|
| 982 | + },
|
|
| 983 | + |
|
| 984 | + /**
|
|
| 985 | + * The bridge IDs/fingerprints for the built-in bridges.
|
|
| 986 | + *
|
|
| 987 | + * @type {Array<string>}
|
|
| 988 | + */
|
|
| 989 | + _bridgeIds: [],
|
|
| 990 | + /**
|
|
| 991 | + * Update _bridgeIds
|
|
| 992 | + */
|
|
| 993 | + _updateBridgeIds() {
|
|
| 994 | + this._bridgeIds = [];
|
|
| 995 | + for (const bridgeLine of this._bridgesVal.bridgeStrings) {
|
|
| 996 | + try {
|
|
| 997 | + this._bridgeIds.push(lazy.TorParsers.parseBridgeLine(bridgeLine).id);
|
|
| 998 | + } catch (e) {
|
|
| 999 | + console.error(`Detected invalid bridge line: ${bridgeLine}`, e);
|
|
| 1000 | + }
|
|
| 1001 | + }
|
|
| 1002 | + |
|
| 1003 | + this._updateConnectedState();
|
|
| 1004 | + },
|
|
| 1005 | + |
|
| 1006 | + /**
|
|
| 1007 | + * The bridge ID/fingerprint of the most recently used bridge (appearing in
|
|
| 1008 | + * the latest Tor circuit). Roughly corresponds to the bridge we are currently
|
|
| 1009 | + * connected to.
|
|
| 1010 | + *
|
|
| 1011 | + * @type {string?}
|
|
| 1012 | + */
|
|
| 1013 | + _connectedBridgeId: null,
|
|
| 1014 | + |
|
| 1015 | + set connectedBridgeId(val) {
|
|
| 1016 | + this._connectedBridgeId = val;
|
|
| 1017 | + this._updateConnectedState();
|
|
| 1018 | + },
|
|
| 1019 | +};
|
|
| 1020 | + |
|
| 1021 | +/**
|
|
| 1022 | + * Controls the bridge pass area.
|
|
| 1023 | + */
|
|
| 1024 | +const gLoxStatus = {
|
|
| 1025 | + /**
|
|
| 1026 | + * The status area.
|
|
| 1027 | + *
|
|
| 1028 | + * @type {Element?}
|
|
| 1029 | + */
|
|
| 1030 | + _area: null,
|
|
| 1031 | + /**
|
|
| 1032 | + * The area for showing the next unlock and invites.
|
|
| 1033 | + *
|
|
| 1034 | + * @type {Element?}
|
|
| 1035 | + */
|
|
| 1036 | + _detailsArea: null,
|
|
| 1037 | + /**
|
|
| 1038 | + * The list items showing the next unlocks.
|
|
| 1039 | + *
|
|
| 1040 | + * @type {?{[key: string]: Element}}
|
|
| 1041 | + */
|
|
| 1042 | + _nextUnlockItems: null,
|
|
| 1043 | + /**
|
|
| 1044 | + * The day counter headings for the next unlock.
|
|
| 1045 | + *
|
|
| 1046 | + * One heading is shown during a search, the other is shown otherwise.
|
|
| 1047 | + *
|
|
| 1048 | + * @type {?Element[]}
|
|
| 1049 | + */
|
|
| 1050 | + _nextUnlockCounterEls: null,
|
|
| 1051 | + /**
|
|
| 1052 | + * Shows the number of remaining invites.
|
|
| 1053 | + *
|
|
| 1054 | + * @type {Element?}
|
|
| 1055 | + */
|
|
| 1056 | + _remainingInvitesEl: null,
|
|
| 1057 | + /**
|
|
| 1058 | + * The button to show the invites.
|
|
| 1059 | + *
|
|
| 1060 | + * @type {Element?}
|
|
| 1061 | + */
|
|
| 1062 | + _invitesButton: null,
|
|
| 1063 | + /**
|
|
| 1064 | + * The alert for new unlocks.
|
|
| 1065 | + *
|
|
| 1066 | + * @type {Element?}
|
|
| 1067 | + */
|
|
| 1068 | + _unlockAlert: null,
|
|
| 1069 | + /**
|
|
| 1070 | + * The list items showing the unlocks.
|
|
| 1071 | + *
|
|
| 1072 | + * @type {?{[key: string]: Element}}
|
|
| 1073 | + */
|
|
| 1074 | + _unlockItems: null,
|
|
| 1075 | + /**
|
|
| 1076 | + * The alert title.
|
|
| 1077 | + *
|
|
| 1078 | + * @type {Element?}
|
|
| 1079 | + */
|
|
| 1080 | + _unlockAlertTitle: null,
|
|
| 1081 | + /**
|
|
| 1082 | + * The alert invites item.
|
|
| 1083 | + *
|
|
| 1084 | + * @type {Element?}
|
|
| 1085 | + */
|
|
| 1086 | + _unlockAlertInvitesItem: null,
|
|
| 1087 | + /**
|
|
| 1088 | + * Button for the user to dismiss the alert.
|
|
| 1089 | + *
|
|
| 1090 | + * @type {Element?}
|
|
| 1091 | + */
|
|
| 1092 | + _unlockAlertButton: null,
|
|
| 1093 | + |
|
| 1094 | + _enabled: false,
|
|
| 1095 | + |
|
| 1096 | + /**
|
|
| 1097 | + * Initialize the bridge pass area.
|
|
| 1098 | + */
|
|
| 1099 | + init() {
|
|
| 1100 | + if (!lazy.Lox.enabled) {
|
|
| 1101 | + // Area should remain inactive and hidden.
|
|
| 1102 | + return;
|
|
| 1103 | + }
|
|
| 1104 | + |
|
| 1105 | + this._enabled = true;
|
|
| 1106 | + this._area = document.getElementById("tor-bridges-lox-status");
|
|
| 1107 | + this._detailsArea = document.getElementById("tor-bridges-lox-details");
|
|
| 1108 | + this._nextUnlockItems = {
|
|
| 1109 | + gainBridges: document.getElementById(
|
|
| 1110 | + "tor-bridges-lox-next-unlock-gain-bridges"
|
|
| 1111 | + ),
|
|
| 1112 | + firstInvites: document.getElementById(
|
|
| 1113 | + "tor-bridges-lox-next-unlock-first-invites"
|
|
| 1114 | + ),
|
|
| 1115 | + moreInvites: document.getElementById(
|
|
| 1116 | + "tor-bridges-lox-next-unlock-more-invites"
|
|
| 1117 | + ),
|
|
| 1118 | + };
|
|
| 1119 | + this._nextUnlockCounterEls = Array.from(
|
|
| 1120 | + document.querySelectorAll(".tor-bridges-lox-next-unlock-counter")
|
|
| 1121 | + );
|
|
| 1122 | + this._remainingInvitesEl = document.getElementById(
|
|
| 1123 | + "tor-bridges-lox-remaining-invites"
|
|
| 1124 | + );
|
|
| 1125 | + this._invitesButton = document.getElementById(
|
|
| 1126 | + "tor-bridges-lox-show-invites-button"
|
|
| 1127 | + );
|
|
| 1128 | + this._unlockAlert = document.getElementById("tor-bridges-lox-unlock-alert");
|
|
| 1129 | + this._unlockItems = {
|
|
| 1130 | + gainBridges: document.getElementById(
|
|
| 1131 | + "tor-bridges-lox-unlock-alert-gain-bridges"
|
|
| 1132 | + ),
|
|
| 1133 | + newBridges: document.getElementById(
|
|
| 1134 | + "tor-bridges-lox-unlock-alert-new-bridges"
|
|
| 1135 | + ),
|
|
| 1136 | + invites: document.getElementById("tor-bridges-lox-unlock-alert-invites"),
|
|
| 1137 | + };
|
|
| 1138 | + this._unlockAlertTitle = document.getElementById(
|
|
| 1139 | + "tor-bridge-unlock-alert-title"
|
|
| 1140 | + );
|
|
| 1141 | + this._unlockAlertInviteItem = document.getElementById(
|
|
| 1142 | + "tor-bridges-lox-unlock-alert-invites"
|
|
| 1143 | + );
|
|
| 1144 | + this._unlockAlertButton = document.getElementById(
|
|
| 1145 | + "tor-bridges-lox-unlock-alert-button"
|
|
| 1146 | + );
|
|
| 1147 | + |
|
| 1148 | + this._invitesButton.addEventListener("click", () => {
|
|
| 1149 | + window.gSubDialog.open(
|
|
| 1150 | + "chrome://browser/content/torpreferences/loxInviteDialog.xhtml",
|
|
| 1151 | + { features: "resizable=yes" }
|
|
| 1152 | + );
|
|
| 1153 | + });
|
|
| 1154 | + this._unlockAlertButton.addEventListener("click", () => {
|
|
| 1155 | + lazy.Lox.clearEventData(this._loxId);
|
|
| 1156 | + });
|
|
| 1157 | + |
|
| 1158 | + Services.obs.addObserver(this, lazy.LoxTopics.UpdateActiveLoxId);
|
|
| 1159 | + Services.obs.addObserver(this, lazy.LoxTopics.UpdateEvents);
|
|
| 1160 | + Services.obs.addObserver(this, lazy.LoxTopics.UpdateNextUnlock);
|
|
| 1161 | + Services.obs.addObserver(this, lazy.LoxTopics.UpdateRemainingInvites);
|
|
| 1162 | + Services.obs.addObserver(this, lazy.LoxTopics.NewInvite);
|
|
| 1163 | + |
|
| 1164 | + window.addEventListener(
|
|
| 1165 | + "unload",
|
|
| 1166 | + () => {
|
|
| 1167 | + Services.obs.removeObserver(this, lazy.LoxTopics.UpdateActiveLoxId);
|
|
| 1168 | + Services.obs.removeObserver(this, lazy.LoxTopics.UpdateEvents);
|
|
| 1169 | + Services.obs.removeObserver(this, lazy.LoxTopics.UpdateNextUnlock);
|
|
| 1170 | + Services.obs.removeObserver(
|
|
| 1171 | + this,
|
|
| 1172 | + lazy.LoxTopics.UpdateRemainingInvites
|
|
| 1173 | + );
|
|
| 1174 | + Services.obs.removeObserver(this, lazy.LoxTopics.NewInvite);
|
|
| 1175 | + },
|
|
| 1176 | + { once: true }
|
|
| 1177 | + );
|
|
| 1178 | + },
|
|
| 1179 | + |
|
| 1180 | + observe(subject, topic) {
|
|
| 1181 | + switch (topic) {
|
|
| 1182 | + case lazy.LoxTopics.UpdateActiveLoxId:
|
|
| 1183 | + this._updateLoxId();
|
|
| 1184 | + break;
|
|
| 1185 | + case lazy.LoxTopics.UpdateNextUnlock:
|
|
| 1186 | + this._updateNextUnlock();
|
|
| 1187 | + break;
|
|
| 1188 | + case lazy.LoxTopics.UpdateEvents:
|
|
| 1189 | + this._updatePendingEvents();
|
|
| 1190 | + break;
|
|
| 1191 | + case lazy.LoxTopics.UpdateRemainingInvites:
|
|
| 1192 | + this._updateRemainingInvites();
|
|
| 1193 | + break;
|
|
| 1194 | + case lazy.LoxTopics.NewInvite:
|
|
| 1195 | + this._updateHaveExistingInvites();
|
|
| 1196 | + break;
|
|
| 1197 | + }
|
|
| 1198 | + },
|
|
| 1199 | + |
|
| 1200 | + /**
|
|
| 1201 | + * The bridges value, set by the setting-control element.
|
|
| 1202 | + *
|
|
| 1203 | + * @type {object}
|
|
| 1204 | + */
|
|
| 1205 | + _bridgesVal: null,
|
|
| 1206 | + set bridges(val) {
|
|
| 1207 | + if (val === null) {
|
|
| 1208 | + // Ignore and wait for initial.
|
|
| 1209 | + return;
|
|
| 1210 | + }
|
|
| 1211 | + if (!this._enabled) {
|
|
| 1212 | + // Area should remain inactive and hidden.
|
|
| 1213 | + return;
|
|
| 1214 | + }
|
|
| 1215 | + this._bridgesVal = val;
|
|
| 1216 | + this._updateLoxId();
|
|
| 1217 | + },
|
|
| 1218 | + |
|
| 1219 | + /**
|
|
| 1220 | + * The Lox id currently shown. Empty if deactivated, and null if
|
|
| 1221 | + * uninitialized.
|
|
| 1222 | + *
|
|
| 1223 | + * @type {string?}
|
|
| 1224 | + */
|
|
| 1225 | + _loxId: null,
|
|
| 1226 | + |
|
| 1227 | + /**
|
|
| 1228 | + * Update the shown bridge pass.
|
|
| 1229 | + */
|
|
| 1230 | + async _updateLoxId() {
|
|
| 1231 | + let loxId =
|
|
| 1232 | + this._bridgesVal?.source === lazy.TorBridgeSource.Lox
|
|
| 1233 | + ? lazy.Lox.activeLoxId
|
|
| 1234 | + : "";
|
|
| 1235 | + if (loxId === this._loxId) {
|
|
| 1236 | + return;
|
|
| 1237 | + }
|
|
| 1238 | + this._loxId = loxId;
|
|
| 1239 | + this._area.hidden = !loxId;
|
|
| 1240 | + // We unset _nextUnlock to ensure the areas no longer use the old value for
|
|
| 1241 | + // the new loxId.
|
|
| 1242 | + this._updateNextUnlock(true);
|
|
| 1243 | + this._updateRemainingInvites();
|
|
| 1244 | + this._updateHaveExistingInvites();
|
|
| 1245 | + this._updatePendingEvents();
|
|
| 1246 | + },
|
|
| 1247 | + |
|
| 1248 | + /**
|
|
| 1249 | + * The remaining invites shown, or null if uninitialized or no loxId.
|
|
| 1250 | + *
|
|
| 1251 | + * @type {integer?}
|
|
| 1252 | + */
|
|
| 1253 | + _remainingInvites: null,
|
|
| 1254 | + /**
|
|
| 1255 | + * Update the shown value.
|
|
| 1256 | + */
|
|
| 1257 | + _updateRemainingInvites() {
|
|
| 1258 | + const numInvites = this._loxId
|
|
| 1259 | + ? lazy.Lox.getRemainingInviteCount(this._loxId)
|
|
| 1260 | + : null;
|
|
| 1261 | + if (numInvites === this._remainingInvites) {
|
|
| 1262 | + return;
|
|
| 1263 | + }
|
|
| 1264 | + this._remainingInvites = numInvites;
|
|
| 1265 | + this._updateUnlockArea();
|
|
| 1266 | + this._updateInvitesArea();
|
|
| 1267 | + },
|
|
| 1268 | + /**
|
|
| 1269 | + * Whether we have existing invites, or null if uninitialized or no loxId.
|
|
| 1270 | + *
|
|
| 1271 | + * @type {boolean?}
|
|
| 1272 | + */
|
|
| 1273 | + _haveExistingInvites: null,
|
|
| 1274 | + /**
|
|
| 1275 | + * Update the shown value.
|
|
| 1276 | + */
|
|
| 1277 | + _updateHaveExistingInvites() {
|
|
| 1278 | + const haveInvites = this._loxId ? !!lazy.Lox.getInvites().length : null;
|
|
| 1279 | + if (haveInvites === this._haveExistingInvites) {
|
|
| 1280 | + return;
|
|
| 1281 | + }
|
|
| 1282 | + this._haveExistingInvites = haveInvites;
|
|
| 1283 | + this._updateInvitesArea();
|
|
| 1284 | + },
|
|
| 1285 | + /**
|
|
| 1286 | + * Details about the next unlock, or null if uninitialized or no loxId.
|
|
| 1287 | + *
|
|
| 1288 | + * @type {UnlockData?}
|
|
| 1289 | + */
|
|
| 1290 | + _nextUnlock: null,
|
|
| 1291 | + /**
|
|
| 1292 | + * Tracker id to ensure that the results from later calls to _updateNextUnlock
|
|
| 1293 | + * take priority over earlier calls.
|
|
| 1294 | + *
|
|
| 1295 | + * @type {integer}
|
|
| 1296 | + */
|
|
| 1297 | + _nextUnlockCallId: 0,
|
|
| 1298 | + /**
|
|
| 1299 | + * Update the shown value asynchronously.
|
|
| 1300 | + *
|
|
| 1301 | + * @param {boolean} [unset=false] - Whether to set the _nextUnlock value to
|
|
| 1302 | + * null before waiting for the new value. I.e. ensure that the current value
|
|
| 1303 | + * will not be used.
|
|
| 1304 | + */
|
|
| 1305 | + async _updateNextUnlock(unset = false) {
|
|
| 1306 | + // NOTE: We do not expect the integer to exceed the maximum integer.
|
|
| 1307 | + this._nextUnlockCallId++;
|
|
| 1308 | + const callId = this._nextUnlockCallId;
|
|
| 1309 | + if (unset) {
|
|
| 1310 | + this._nextUnlock = null;
|
|
| 1311 | + }
|
|
| 1312 | + const nextUnlock = this._loxId
|
|
| 1313 | + ? await lazy.Lox.getNextUnlock(this._loxId)
|
|
| 1314 | + : null;
|
|
| 1315 | + if (callId !== this._nextUnlockCallId) {
|
|
| 1316 | + // Replaced by another update.
|
|
| 1317 | + // E.g. if the _loxId changed. Or if getNextUnlock triggered
|
|
| 1318 | + // LoxTopics.UpdateNextUnlock.
|
|
| 1319 | + return;
|
|
| 1320 | + }
|
|
| 1321 | + // Should be safe to trigger the update, even when the value hasn't changed.
|
|
| 1322 | + this._nextUnlock = nextUnlock;
|
|
| 1323 | + this._updateUnlockArea();
|
|
| 1324 | + },
|
|
| 1325 | + /**
|
|
| 1326 | + * The list of events the user has not yet cleared, or null if uninitialized
|
|
| 1327 | + * or no loxId.
|
|
| 1328 | + *
|
|
| 1329 | + * @type {EventData[]?}
|
|
| 1330 | + */
|
|
| 1331 | + _pendingEvents: null,
|
|
| 1332 | + /**
|
|
| 1333 | + * Update the shown value.
|
|
| 1334 | + */
|
|
| 1335 | + _updatePendingEvents() {
|
|
| 1336 | + // Should be safe to trigger the update, even when the value hasn't changed.
|
|
| 1337 | + this._pendingEvents = this._loxId
|
|
| 1338 | + ? lazy.Lox.getEventData(this._loxId)
|
|
| 1339 | + : null;
|
|
| 1340 | + this._updateUnlockArea();
|
|
| 1341 | + },
|
|
| 1342 | + |
|
| 1343 | + /**
|
|
| 1344 | + * Update the display of the current or next unlock.
|
|
| 1345 | + */
|
|
| 1346 | + _updateUnlockArea() {
|
|
| 1347 | + if (
|
|
| 1348 | + !this._loxId ||
|
|
| 1349 | + this._pendingEvents === null ||
|
|
| 1350 | + this._remainingInvites === null ||
|
|
| 1351 | + this._nextUnlock === null
|
|
| 1352 | + ) {
|
|
| 1353 | + // Uninitialized or no Lox source.
|
|
| 1354 | + // NOTE: This area may already be hidden by the change in Lox source,
|
|
| 1355 | + // but we clean up for the next non-empty id.
|
|
| 1356 | + this._unlockAlert.hidden = true;
|
|
| 1357 | + this._detailsArea.hidden = true;
|
|
| 1358 | + return;
|
|
| 1359 | + }
|
|
| 1360 | + |
|
| 1361 | + // Grab focus state before changing visibility.
|
|
| 1362 | + const alertHadFocus = this._unlockAlert.contains(document.activeElement);
|
|
| 1363 | + const detailsHadFocus = this._detailsArea.contains(document.activeElement);
|
|
| 1364 | + |
|
| 1365 | + const pendingEvents = this._pendingEvents;
|
|
| 1366 | + const showAlert = !!pendingEvents.length;
|
|
| 1367 | + this._unlockAlert.hidden = !showAlert;
|
|
| 1368 | + this._detailsArea.hidden = showAlert;
|
|
| 1369 | + |
|
| 1370 | + if (showAlert) {
|
|
| 1371 | + // At level 0 and level 1, we do not have any invites.
|
|
| 1372 | + // If the user starts and ends on level 0 or 1, then overall they would
|
|
| 1373 | + // have had no change in their invites. So we do not want to show their
|
|
| 1374 | + // latest updates.
|
|
| 1375 | + // NOTE: If the user starts at level > 1 and ends with level 1 (levelling
|
|
| 1376 | + // down to level 0 should not be possible), then we *do* want to show the
|
|
| 1377 | + // user that they now have "0" invites.
|
|
| 1378 | + // NOTE: pendingEvents are time-ordered, with the most recent event
|
|
| 1379 | + // *last*.
|
|
| 1380 | + const firstEvent = pendingEvents[0];
|
|
| 1381 | + // NOTE: We cannot get a blockage event when the user starts at level 1 or
|
|
| 1382 | + // 0.
|
|
| 1383 | + const startingAtLowLevel =
|
|
| 1384 | + firstEvent.type === "levelup" && firstEvent.newLevel <= 2;
|
|
| 1385 | + const lastEvent = pendingEvents[pendingEvents.length - 1];
|
|
| 1386 | + const endingAtLowLevel = lastEvent.newLevel <= 1;
|
|
| 1387 | + |
|
| 1388 | + const showInvites = !(startingAtLowLevel && endingAtLowLevel);
|
|
| 1389 | + |
|
| 1390 | + let blockage = false;
|
|
| 1391 | + let levelUp = false;
|
|
| 1392 | + let bridgeGain = false;
|
|
| 1393 | + // Go through events, in the order that they occurred.
|
|
| 1394 | + for (const loxEvent of pendingEvents) {
|
|
| 1395 | + if (loxEvent.type === "levelup") {
|
|
| 1396 | + levelUp = true;
|
|
| 1397 | + if (loxEvent.newLevel === 1) {
|
|
| 1398 | + // Gain 2 bridges from level 0 to 1.
|
|
| 1399 | + bridgeGain = true;
|
|
| 1400 | + }
|
|
| 1401 | + } else {
|
|
| 1402 | + blockage = true;
|
|
| 1403 | + }
|
|
| 1404 | + }
|
|
| 1405 | + |
|
| 1406 | + let alertTitleId;
|
|
| 1407 | + if (levelUp && !blockage) {
|
|
| 1408 | + alertTitleId = "tor-bridges-lox-upgrade";
|
|
| 1409 | + } else {
|
|
| 1410 | + // Show as blocked bridges replaced.
|
|
| 1411 | + // Even if we have a mixture of level ups as well.
|
|
| 1412 | + alertTitleId = "tor-bridges-lox-blocked";
|
|
| 1413 | + }
|
|
| 1414 | + document.l10n.setAttributes(this._unlockAlertTitle, alertTitleId);
|
|
| 1415 | + document.l10n.setAttributes(
|
|
| 1416 | + this._unlockAlertInviteItem,
|
|
| 1417 | + "tor-bridges-lox-new-invites",
|
|
| 1418 | + { numInvites: this._remainingInvites }
|
|
| 1419 | + );
|
|
| 1420 | + this._unlockAlert.classList.toggle(
|
|
| 1421 | + "lox-unlock-upgrade",
|
|
| 1422 | + levelUp && !blockage
|
|
| 1423 | + );
|
|
| 1424 | + this._unlockItems.gainBridges.hidden = !bridgeGain;
|
|
| 1425 | + this._unlockItems.newBridges.hidden = !blockage;
|
|
| 1426 | + this._unlockItems.invites.hidden = !showInvites;
|
|
| 1427 | + } else {
|
|
| 1428 | + // Show next unlock.
|
|
| 1429 | + // Number of days until the next unlock, rounded up.
|
|
| 1430 | + const numDays = Math.max(
|
|
| 1431 | + 1,
|
|
| 1432 | + Math.ceil(
|
|
| 1433 | + (new Date(this._nextUnlock.date).getTime() - Date.now()) /
|
|
| 1434 | + (24 * 60 * 60 * 1000)
|
|
| 1435 | + )
|
|
| 1436 | + );
|
|
| 1437 | + for (const counterEl of this._nextUnlockCounterEls) {
|
|
| 1438 | + document.l10n.setAttributes(
|
|
| 1439 | + counterEl,
|
|
| 1440 | + "tor-bridges-lox-days-until-unlock",
|
|
| 1441 | + { numDays }
|
|
| 1442 | + );
|
|
| 1443 | + }
|
|
| 1444 | + |
|
| 1445 | + // Gain 2 bridges from level 0 to 1. After that gain invites.
|
|
| 1446 | + this._nextUnlockItems.gainBridges.hidden =
|
|
| 1447 | + this._nextUnlock.nextLevel !== 1;
|
|
| 1448 | + this._nextUnlockItems.firstInvites.hidden =
|
|
| 1449 | + this._nextUnlock.nextLevel !== 2;
|
|
| 1450 | + this._nextUnlockItems.moreInvites.hidden =
|
|
| 1451 | + this._nextUnlock.nextLevel <= 2;
|
|
| 1452 | + }
|
|
| 1453 | + |
|
| 1454 | + if (alertHadFocus && !showAlert) {
|
|
| 1455 | + // Alert has become hidden, move focus back up to the now revealed details
|
|
| 1456 | + // area.
|
|
| 1457 | + // NOTE: We have two headings: one shown during a search and one shown
|
|
| 1458 | + // otherwise. We focus the heading that is currently visible.
|
|
| 1459 | + // See tor-browser#43320.
|
|
| 1460 | + // TODO: It might be better if we could use the # named anchor to
|
|
| 1461 | + // re-orient the screen reader position instead of using tabIndex=-1, but
|
|
| 1462 | + // about:preferences currently uses the anchor for showing categories
|
|
| 1463 | + // only. See bugzilla bug 1799153.
|
|
| 1464 | + if (
|
|
| 1465 | + this._nextUnlockCounterEls[0].checkVisibility({
|
|
| 1466 | + visibilityProperty: true,
|
|
| 1467 | + })
|
|
| 1468 | + ) {
|
|
| 1469 | + this._nextUnlockCounterEls[0].focus();
|
|
| 1470 | + } else {
|
|
| 1471 | + this._nextUnlockCounterEls[1].focus();
|
|
| 1472 | + }
|
|
| 1473 | + } else if (detailsHadFocus && showAlert) {
|
|
| 1474 | + this._unlockAlertButton.focus();
|
|
| 1475 | + }
|
|
| 1476 | + },
|
|
| 1477 | + |
|
| 1478 | + /**
|
|
| 1479 | + * Update the invites area.
|
|
| 1480 | + */
|
|
| 1481 | + _updateInvitesArea() {
|
|
| 1482 | + let hasInvites;
|
|
| 1483 | + if (
|
|
| 1484 | + !this._loxId ||
|
|
| 1485 | + this._remainingInvites === null ||
|
|
| 1486 | + this._haveExistingInvites === null
|
|
| 1487 | + ) {
|
|
| 1488 | + // Not initialized yet.
|
|
| 1489 | + hasInvites = false;
|
|
| 1490 | + } else {
|
|
| 1491 | + hasInvites = this._haveExistingInvites || !!this._remainingInvites;
|
|
| 1492 | + }
|
|
| 1493 | + |
|
| 1494 | + if (
|
|
| 1495 | + !hasInvites &&
|
|
| 1496 | + (this._remainingInvitesEl.contains(document.activeElement) ||
|
|
| 1497 | + this._invitesButton.contains(document.activeElement))
|
|
| 1498 | + ) {
|
|
| 1499 | + // About to loose focus.
|
|
| 1500 | + // Unexpected for the lox level to loose all invites.
|
|
| 1501 | + // Move to the top of the details area, which should be visible if we
|
|
| 1502 | + // just had focus.
|
|
| 1503 | + this._nextUnlockCounterEl.focus();
|
|
| 1504 | + }
|
|
| 1505 | + // Hide the invite elements if we have no historic invites or a way of
|
|
| 1506 | + // creating new ones.
|
|
| 1507 | + this._remainingInvitesEl.hidden = !hasInvites;
|
|
| 1508 | + this._invitesButton.hidden = !hasInvites;
|
|
| 1509 | + |
|
| 1510 | + if (hasInvites) {
|
|
| 1511 | + document.l10n.setAttributes(
|
|
| 1512 | + this._remainingInvitesEl,
|
|
| 1513 | + "tor-bridges-lox-remaining-invites",
|
|
| 1514 | + { numInvites: this._remainingInvites }
|
|
| 1515 | + );
|
|
| 1516 | + }
|
|
| 1517 | + },
|
|
| 1518 | +};
|
|
| 1519 | + |
|
| 1520 | +/**
|
|
| 1521 | + * Controls the bridge settings.
|
|
| 1522 | + */
|
|
| 1523 | +const gBridgeSettings = {
|
|
| 1524 | + /**
|
|
| 1525 | + * The display area.
|
|
| 1526 | + *
|
|
| 1527 | + * @type {Element?}
|
|
| 1528 | + */
|
|
| 1529 | + _displayEl: null,
|
|
| 1530 | + /**
|
|
| 1531 | + * The area for showing current bridges.
|
|
| 1532 | + *
|
|
| 1533 | + * @type {Element?}
|
|
| 1534 | + */
|
|
| 1535 | + _bridgesEl: null,
|
|
| 1536 | + /**
|
|
| 1537 | + * The area for sharing bridge addresses.
|
|
| 1538 | + *
|
|
| 1539 | + * @type {Element?}
|
|
| 1540 | + */
|
|
| 1541 | + _shareEl: null,
|
|
| 1542 | + /**
|
|
| 1543 | + * The area for showing no bridges.
|
|
| 1544 | + *
|
|
| 1545 | + * @type {Element?}
|
|
| 1546 | + */
|
|
| 1547 | + _noBridgesEl: null,
|
|
| 1548 | + /**
|
|
| 1549 | + * A map from the bridge source to its corresponding label.
|
|
| 1550 | + *
|
|
| 1551 | + * @type {?Map<number, Element>}
|
|
| 1552 | + */
|
|
| 1553 | + _sourceLabels: null,
|
|
| 1554 | + |
|
| 1555 | + /**
|
|
| 1556 | + * Whether we have been initialized.
|
|
| 1557 | + *
|
|
| 1558 | + * @type {boolean}
|
|
| 1559 | + */
|
|
| 1560 | + _initialized: false,
|
|
| 1561 | + |
|
| 1562 | + /**
|
|
| 1563 | + * Initialize the bridge settings.
|
|
| 1564 | + *
|
|
| 1565 | + * @param {Element} displayEl - The widget element we are controlling.
|
|
| 1566 | + */
|
|
| 1567 | + init(displayEl) {
|
|
| 1568 | + if (this._initialized) {
|
|
| 1569 | + return;
|
|
| 1570 | + }
|
|
| 1571 | + |
|
| 1572 | + this._displayEl = displayEl;
|
|
| 1573 | + this._bridgesEl = document.getElementById("tor-bridges-current");
|
|
| 1574 | + this._noBridgesEl = document.getElementById("tor-bridges-none");
|
|
| 1575 | + |
|
| 1576 | + this._sourceLabels = new Map([
|
|
| 1577 | + [
|
|
| 1578 | + lazy.TorBridgeSource.BuiltIn,
|
|
| 1579 | + document.getElementById("tor-bridges-built-in-label"),
|
|
| 1580 | + ],
|
|
| 1581 | + [
|
|
| 1582 | + lazy.TorBridgeSource.UserProvided,
|
|
| 1583 | + document.getElementById("tor-bridges-user-label"),
|
|
| 1584 | + ],
|
|
| 1585 | + [
|
|
| 1586 | + lazy.TorBridgeSource.BridgeDB,
|
|
| 1587 | + document.getElementById("tor-bridges-requested-label"),
|
|
| 1588 | + ],
|
|
| 1589 | + [
|
|
| 1590 | + lazy.TorBridgeSource.Lox,
|
|
| 1591 | + document.getElementById("tor-bridges-lox-label"),
|
|
| 1592 | + ],
|
|
| 1593 | + ]);
|
|
| 1594 | + this._shareEl = document.getElementById("tor-bridges-share");
|
|
| 1595 | + |
|
| 1596 | + this._initBridgesMenu();
|
|
| 1597 | + this._initShareArea();
|
|
| 1598 | + |
|
| 1599 | + gBridgeGrid.init();
|
|
| 1600 | + gBuiltinBridgesArea.init();
|
|
| 1601 | + gLoxStatus.init();
|
|
| 1602 | + this._initialized = true;
|
|
| 1603 | + // Re-trigger our current bridges value to pass on to any descendants.
|
|
| 1604 | + this.bridges = this._bridgesVal;
|
|
| 1605 | + this.connectedBridgeId = this._connectedBridgeId;
|
|
| 1606 | + },
|
|
| 1607 | + |
|
| 1608 | + /**
|
|
| 1609 | + * The bridges value, set by the setting-control element.
|
|
| 1610 | + *
|
|
| 1611 | + * @type {object}
|
|
| 1612 | + */
|
|
| 1613 | + _bridgesVal: null,
|
|
| 1614 | + |
|
| 1615 | + set bridges(val) {
|
|
| 1616 | + if (val === null) {
|
|
| 1617 | + // Corresponds to pending TorSettings initialization, wait for a non-null
|
|
| 1618 | + // value.
|
|
| 1619 | + return;
|
|
| 1620 | + }
|
|
| 1621 | + this._bridgesVal = val;
|
|
| 1622 | + if (!this._initialized) {
|
|
| 1623 | + return;
|
|
| 1624 | + }
|
|
| 1625 | + this._updateSource();
|
|
| 1626 | + this._updateBridgeStrings();
|
|
| 1627 | + // Pass on to descendants.
|
|
| 1628 | + gBridgeGrid.bridges = val;
|
|
| 1629 | + gBuiltinBridgesArea.bridges = val;
|
|
| 1630 | + gLoxStatus.bridges = val;
|
|
| 1631 | + },
|
|
| 1632 | + |
|
| 1633 | + /**
|
|
| 1634 | + * The ID of the currently connected bridge, or `null` if there is none.
|
|
| 1635 | + *
|
|
| 1636 | + * @type {string?}
|
|
| 1637 | + */
|
|
| 1638 | + _connectedBridgeId: null,
|
|
| 1639 | + |
|
| 1640 | + set connectedBridgeId(val) {
|
|
| 1641 | + this._connectedBridgeId = val;
|
|
| 1642 | + if (!this._initialized) {
|
|
| 1643 | + return;
|
|
| 1644 | + }
|
|
| 1645 | + // NOTE: This should be safe to call, even when _bridgesVal is still null.
|
|
| 1646 | + gBridgeGrid.connectedBridgeId = val;
|
|
| 1647 | + gBuiltinBridgesArea.connectedBridgeId = val;
|
|
| 1648 | + },
|
|
| 1649 | + |
|
| 1650 | + /**
|
|
| 1651 | + * The shown bridge source.
|
|
| 1652 | + *
|
|
| 1653 | + * Initially null to indicate that it is unset for the first call to
|
|
| 1654 | + * _updateSource.
|
|
| 1655 | + *
|
|
| 1656 | + * @type {integer?}
|
|
| 1657 | + */
|
|
| 1658 | + _bridgeSource: null,
|
|
| 1659 | + /**
|
|
| 1660 | + * Whether the user is encouraged to share their bridge addresses.
|
|
| 1661 | + *
|
|
| 1662 | + * @type {boolean}
|
|
| 1663 | + */
|
|
| 1664 | + _canShare: false,
|
|
| 1665 | + |
|
| 1666 | + /**
|
|
| 1667 | + * Update _bridgeSource.
|
|
| 1668 | + */
|
|
| 1669 | + _updateSource() {
|
|
| 1670 | + // NOTE: This should only ever be called after TorSettings is already
|
|
| 1671 | + // initialized.
|
|
| 1672 | + const bridgeSource = this._bridgesVal.source;
|
|
| 1673 | + if (bridgeSource === this._bridgeSource) {
|
|
| 1674 | + // Avoid re-activating an area if the source has not changed.
|
|
| 1675 | + return;
|
|
| 1676 | + }
|
|
| 1677 | + |
|
| 1678 | + this._bridgeSource = bridgeSource;
|
|
| 1679 | + |
|
| 1680 | + // Before hiding elements, we determine whether our region contained the
|
|
| 1681 | + // user focus.
|
|
| 1682 | + const hadFocus =
|
|
| 1683 | + this._bridgesEl.contains(document.activeElement) ||
|
|
| 1684 | + this._noBridgesEl.contains(document.activeElement);
|
|
| 1685 | + |
|
| 1686 | + for (const [source, labelEl] of this._sourceLabels.entries()) {
|
|
| 1687 | + labelEl.hidden = source !== bridgeSource;
|
|
| 1688 | + }
|
|
| 1689 | + |
|
| 1690 | + this._canShare =
|
|
| 1691 | + bridgeSource === lazy.TorBridgeSource.UserProvided ||
|
|
| 1692 | + bridgeSource === lazy.TorBridgeSource.BridgeDB;
|
|
| 1693 | + |
|
| 1694 | + this._shareEl.hidden = !this._canShare;
|
|
| 1695 | + |
|
| 1696 | + // Force the menu to close whenever the source changes.
|
|
| 1697 | + // NOTE: If the menu had focus then hadFocus will be true, and focus will be
|
|
| 1698 | + // re-assigned.
|
|
| 1699 | + this._forceCloseBridgesMenu();
|
|
| 1700 | + |
|
| 1701 | + // Update whether we have bridges.
|
|
| 1702 | + this._updateHaveBridges();
|
|
| 1703 | + |
|
| 1704 | + if (hadFocus) {
|
|
| 1705 | + // Always reset the focus to the start of the area whenever the source
|
|
| 1706 | + // changes.
|
|
| 1707 | + lazy.moveFocusToBridgeHeading(window);
|
|
| 1708 | + }
|
|
| 1709 | + },
|
|
| 1710 | + |
|
| 1711 | + /**
|
|
| 1712 | + * Whether we have bridges or not, or null if it is unknown.
|
|
| 1713 | + *
|
|
| 1714 | + * @type {boolean?}
|
|
| 1715 | + */
|
|
| 1716 | + _haveBridges: null,
|
|
| 1717 | + |
|
| 1718 | + /**
|
|
| 1719 | + * Update the _haveBridges value.
|
|
| 1720 | + */
|
|
| 1721 | + _updateHaveBridges() {
|
|
| 1722 | + const haveBridges = this._bridgesVal.haveBridges;
|
|
| 1723 | + |
|
| 1724 | + if (haveBridges === this._haveBridges) {
|
|
| 1725 | + return;
|
|
| 1726 | + }
|
|
| 1727 | + |
|
| 1728 | + this._haveBridges = haveBridges;
|
|
| 1729 | + |
|
| 1730 | + // Add classes to show or hide the "no bridges" and "Your bridges" sections.
|
|
| 1731 | + this._bridgesEl.hidden = !haveBridges;
|
|
| 1732 | + this._noBridgesEl.hidden = haveBridges;
|
|
| 1733 | + |
|
| 1734 | + this._displayEl.classList.toggle("has-tor-bridges", haveBridges);
|
|
| 1735 | + },
|
|
| 1736 | + |
|
| 1737 | + /**
|
|
| 1738 | + * The bridge strings in a copy-able form.
|
|
| 1739 | + *
|
|
| 1740 | + * @type {string}
|
|
| 1741 | + */
|
|
| 1742 | + _bridgeStrings: "",
|
|
| 1743 | + /**
|
|
| 1744 | + * Whether the bridge strings should be shown as a QR code.
|
|
| 1745 | + *
|
|
| 1746 | + * @type {boolean}
|
|
| 1747 | + */
|
|
| 1748 | + _canQRBridges: false,
|
|
| 1749 | + |
|
| 1750 | + /**
|
|
| 1751 | + * Update the stored bridge strings.
|
|
| 1752 | + */
|
|
| 1753 | + _updateBridgeStrings() {
|
|
| 1754 | + const bridges = this._bridgesVal.bridgeStrings;
|
|
| 1755 | + |
|
| 1756 | + this._bridgeStrings = bridges.join("\n");
|
|
| 1757 | + // TODO: Determine what logic we want.
|
|
| 1758 | + this._canQRBridges = bridges.length <= 3;
|
|
| 1759 | + |
|
| 1760 | + this._qrButton.disabled = !this._canQRBridges;
|
|
| 1761 | + },
|
|
| 1762 | + |
|
| 1763 | + /**
|
|
| 1764 | + * Copy all the bridge addresses to the clipboard.
|
|
| 1765 | + */
|
|
| 1766 | + _copyBridges() {
|
|
| 1767 | + const clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
|
|
| 1768 | + Ci.nsIClipboardHelper
|
|
| 1769 | + );
|
|
| 1770 | + clipboard.copyString(this._bridgeStrings);
|
|
| 1771 | + },
|
|
| 1772 | + |
|
| 1773 | + /**
|
|
| 1774 | + * Open the QR code dialog encoding all the bridge addresses.
|
|
| 1775 | + */
|
|
| 1776 | + _openQR() {
|
|
| 1777 | + if (!this._canQRBridges) {
|
|
| 1778 | + return;
|
|
| 1779 | + }
|
|
| 1780 | + showBridgeQr(this._bridgeStrings);
|
|
| 1781 | + },
|
|
| 1782 | + |
|
| 1783 | + /**
|
|
| 1784 | + * The QR button for copying all QR codes.
|
|
| 1785 | + *
|
|
| 1786 | + * @type {Element?}
|
|
| 1787 | + */
|
|
| 1788 | + _qrButton: null,
|
|
| 1789 | + |
|
| 1790 | + _initShareArea() {
|
|
| 1791 | + document
|
|
| 1792 | + .getElementById("tor-bridges-copy-addresses-button")
|
|
| 1793 | + .addEventListener("click", () => {
|
|
| 1794 | + this._copyBridges();
|
|
| 1795 | + });
|
|
| 1796 | + |
|
| 1797 | + this._qrButton = document.getElementById("tor-bridges-qr-addresses-button");
|
|
| 1798 | + this._qrButton.addEventListener("click", () => {
|
|
| 1799 | + this._openQR();
|
|
| 1800 | + });
|
|
| 1801 | + },
|
|
| 1802 | + |
|
| 1803 | + /**
|
|
| 1804 | + * The menu for all bridges.
|
|
| 1805 | + *
|
|
| 1806 | + * @type {Element?}
|
|
| 1807 | + */
|
|
| 1808 | + _bridgesMenu: null,
|
|
| 1809 | + |
|
| 1810 | + /**
|
|
| 1811 | + * Initialize the menu for all bridges.
|
|
| 1812 | + */
|
|
| 1813 | + _initBridgesMenu() {
|
|
| 1814 | + this._bridgesMenu = document.getElementById("tor-bridges-all-options-menu");
|
|
| 1815 | + |
|
| 1816 | + // NOTE: We generally assume that once the bridge menu is opened the
|
|
| 1817 | + // this._bridgeStrings value will not change.
|
|
| 1818 | + const qrItem = document.getElementById(
|
|
| 1819 | + "tor-bridges-options-qr-all-menu-item"
|
|
| 1820 | + );
|
|
| 1821 | + qrItem.addEventListener("click", () => {
|
|
| 1822 | + this._openQR();
|
|
| 1823 | + });
|
|
| 1824 | + |
|
| 1825 | + const copyItem = document.getElementById(
|
|
| 1826 | + "tor-bridges-options-copy-all-menu-item"
|
|
| 1827 | + );
|
|
| 1828 | + copyItem.addEventListener("click", () => {
|
|
| 1829 | + this._copyBridges();
|
|
| 1830 | + });
|
|
| 1831 | + |
|
| 1832 | + const editItem = document.getElementById(
|
|
| 1833 | + "tor-bridges-options-edit-all-menu-item"
|
|
| 1834 | + );
|
|
| 1835 | + editItem.addEventListener("click", () => {
|
|
| 1836 | + lazy.openUserProvideBridgeDialog(window, "edit");
|
|
| 1837 | + });
|
|
| 1838 | + |
|
| 1839 | + // TODO: Do we want a different item for built-in bridges, rather than
|
|
| 1840 | + // "Remove all bridges"?
|
|
| 1841 | + document
|
|
| 1842 | + .getElementById("tor-bridges-options-remove-all-menu-item")
|
|
| 1843 | + .addEventListener("click", async () => {
|
|
| 1844 | + // TODO: Should we only have a warning when not built-in?
|
|
| 1845 | + const parentWindow =
|
|
| 1846 | + Services.wm.getMostRecentWindow("navigator:browser");
|
|
| 1847 | + const flags =
|
|
| 1848 | + Services.prompt.BUTTON_POS_0 *
|
|
| 1849 | + Services.prompt.BUTTON_TITLE_IS_STRING +
|
|
| 1850 | + Services.prompt.BUTTON_POS_0_DEFAULT +
|
|
| 1851 | + Services.prompt.BUTTON_DEFAULT_IS_DESTRUCTIVE +
|
|
| 1852 | + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
|
|
| 1853 | + |
|
| 1854 | + const [titleString, bodyString, removeString] =
|
|
| 1855 | + await document.l10n.formatValues([
|
|
| 1856 | + { id: "remove-all-bridges-warning-title" },
|
|
| 1857 | + { id: "remove-all-bridges-warning-description" },
|
|
| 1858 | + { id: "remove-all-bridges-warning-remove-button" },
|
|
| 1859 | + ]);
|
|
| 1860 | + |
|
| 1861 | + // TODO: Update the text, and remove old strings.
|
|
| 1862 | + const buttonIndex = Services.prompt.confirmEx(
|
|
| 1863 | + parentWindow,
|
|
| 1864 | + titleString,
|
|
| 1865 | + bodyString,
|
|
| 1866 | + flags,
|
|
| 1867 | + removeString,
|
|
| 1868 | + null,
|
|
| 1869 | + null,
|
|
| 1870 | + null,
|
|
| 1871 | + {}
|
|
| 1872 | + );
|
|
| 1873 | + |
|
| 1874 | + if (buttonIndex !== 0) {
|
|
| 1875 | + return;
|
|
| 1876 | + }
|
|
| 1877 | + |
|
| 1878 | + lazy.TorSettings.changeSettings({
|
|
| 1879 | + // This should always have the side effect of disabling bridges as
|
|
| 1880 | + // well.
|
|
| 1881 | + bridges: { source: lazy.TorBridgeSource.Invalid },
|
|
| 1882 | + });
|
|
| 1883 | + });
|
|
| 1884 | + |
|
| 1885 | + this._bridgesMenu.addEventListener("showing", () => {
|
|
| 1886 | + qrItem.hidden = !this._canShare || !this._canQRBridges;
|
|
| 1887 | + editItem.hidden =
|
|
| 1888 | + this._bridgeSource !== lazy.TorBridgeSource.UserProvided;
|
|
| 1889 | + });
|
|
| 1890 | + |
|
| 1891 | + const bridgesMenuButton = document.getElementById(
|
|
| 1892 | + "tor-bridges-all-options-button"
|
|
| 1893 | + );
|
|
| 1894 | + bridgesMenuButton.addEventListener("click", event => {
|
|
| 1895 | + this._bridgesMenu.toggle(event, bridgesMenuButton);
|
|
| 1896 | + });
|
|
| 1897 | + |
|
| 1898 | + this._bridgesMenu.addEventListener("hidden", () => {
|
|
| 1899 | + // Make sure the button receives focus again when the menu is hidden.
|
|
| 1900 | + // Currently, panel-list.js only does this when the menu is opened with a
|
|
| 1901 | + // keyboard, but this causes focus to be lost from the page if the user
|
|
| 1902 | + // uses a mixture of keyboard and mouse.
|
|
| 1903 | + bridgesMenuButton.focus();
|
|
| 1904 | + });
|
|
| 1905 | + },
|
|
| 1906 | + |
|
| 1907 | + /**
|
|
| 1908 | + * Force the bridges menu to close.
|
|
| 1909 | + */
|
|
| 1910 | + _forceCloseBridgesMenu() {
|
|
| 1911 | + this._bridgesMenu.hide(null, { force: true });
|
|
| 1912 | + },
|
|
| 1913 | +};
|
|
| 1914 | + |
|
| 1915 | +// TODO: Replace gBridgeSettings and #tor-bridges-display-template with proper
|
|
| 1916 | +// widgets (using MozLitElement).
|
|
| 1917 | +/**
|
|
| 1918 | + * Show the current bridges.
|
|
| 1919 | + */
|
|
| 1920 | +class TorBridgesDisplay extends HTMLElement {
|
|
| 1921 | + connectedCallback() {
|
|
| 1922 | + if (this.children.length) {
|
|
| 1923 | + return;
|
|
| 1924 | + }
|
|
| 1925 | + // Take the template children since we only expect one instance of this.
|
|
| 1926 | + this.replaceChildren(
|
|
| 1927 | + ...document.getElementById("tor-bridges-display-template").content
|
|
| 1928 | + .childNodes
|
|
| 1929 | + );
|
|
| 1930 | + gBridgeSettings.init(this);
|
|
| 1931 | + }
|
|
| 1932 | + |
|
| 1933 | + set connectedBridgeId(val) {
|
|
| 1934 | + gBridgeSettings.connectedBridgeId = val;
|
|
| 1935 | + }
|
|
| 1936 | + |
|
| 1937 | + set bridges(val) {
|
|
| 1938 | + gBridgeSettings.bridges = val;
|
|
| 1939 | + }
|
|
| 1940 | + |
|
| 1941 | + /**
|
|
| 1942 | + * Focus the "Your bridges" heading, if it is visible.
|
|
| 1943 | + *
|
|
| 1944 | + * @returns {boolean} - `true` if the heading was visible and focused.
|
|
| 1945 | + */
|
|
| 1946 | + focusHeading() {
|
|
| 1947 | + if (!gBridgeSettings._haveBridges) {
|
|
| 1948 | + // Heading is hidden.
|
|
| 1949 | + return false;
|
|
| 1950 | + }
|
|
| 1951 | + document.getElementById("tor-bridges-current-heading-non-search").focus();
|
|
| 1952 | + return true;
|
|
| 1953 | + }
|
|
| 1954 | +}
|
|
| 1955 | +customElements.define("tor-bridges-display", TorBridgesDisplay); |
| ... | ... | @@ -46,6 +46,10 @@ h4, |
| 46 | 46 | h5,
|
| 47 | 47 | h6 {
|
| 48 | 48 | margin: 0;
|
| 49 | + |
|
| 50 | + &[tabindex]:focus-visible {
|
|
| 51 | + outline-offset: var(--focus-outline-offset);
|
|
| 52 | + }
|
|
| 49 | 53 | }
|
| 50 | 54 | |
| 51 | 55 | .description {
|
| ... | ... | @@ -11,12 +11,24 @@ import { MozLitElement } from "../lit-utils.mjs"; |
| 11 | 11 | * @type {Record<number, (label: string) => ReturnType<typeof html>>}
|
| 12 | 12 | */
|
| 13 | 13 | const HEADING_LEVEL_TEMPLATES = {
|
| 14 | - 1: label => html`<h1 class="text-box-trim-start">${label}</h1>`,
|
|
| 15 | - 2: label => html`<h2 class="text-box-trim-start">${label}</h2>`,
|
|
| 16 | - 3: label => html`<h3 class="text-box-trim-start">${label}</h3>`,
|
|
| 17 | - 4: label => html`<h4>${label}</h4>`,
|
|
| 18 | - 5: label => html`<h5>${label}</h5>`,
|
|
| 19 | - 6: label => html`<h6>${label}</h6>`,
|
|
| 14 | + 1: (label, tabindex) =>
|
|
| 15 | + html`<h1 class="text-box-trim-start" tabindex=${ifDefined(tabindex)}>
|
|
| 16 | + ${label}
|
|
| 17 | + </h1>`,
|
|
| 18 | + 2: (label, tabindex) =>
|
|
| 19 | + html`<h2 class="text-box-trim-start" tabindex=${ifDefined(tabindex)}>
|
|
| 20 | + ${label}
|
|
| 21 | + </h2>`,
|
|
| 22 | + 3: (label, tabindex) =>
|
|
| 23 | + html`<h3 class="text-box-trim-start" tabindex=${ifDefined(tabindex)}>
|
|
| 24 | + ${label}
|
|
| 25 | + </h3>`,
|
|
| 26 | + 4: (label, tabindex) =>
|
|
| 27 | + html`<h4 tabindex=${ifDefined(tabindex)}>${label}</h4>`,
|
|
| 28 | + 5: (label, tabindex) =>
|
|
| 29 | + html`<h5 tabindex=${ifDefined(tabindex)}>${label}</h5>`,
|
|
| 30 | + 6: (label, tabindex) =>
|
|
| 31 | + html`<h6 tabindex=${ifDefined(tabindex)}>${label}</h6>`,
|
|
| 20 | 32 | };
|
| 21 | 33 | |
| 22 | 34 | /**
|
| ... | ... | @@ -42,6 +54,9 @@ export default class MozFieldset extends MozLitElement { |
| 42 | 54 | disabled: { type: Boolean, reflect: true },
|
| 43 | 55 | iconSrc: { type: String },
|
| 44 | 56 | badge: { type: String },
|
| 57 | + // Allows the heading to be focusable, but not part of the Tab focus cycle.
|
|
| 58 | + // See tor-browser#45143.
|
|
| 59 | + focusableHeading: { type: Boolean, attribute: "focusable-heading" },
|
|
| 45 | 60 | };
|
| 46 | 61 | |
| 47 | 62 | constructor() {
|
| ... | ... | @@ -132,12 +147,25 @@ export default class MozFieldset extends MozLitElement { |
| 132 | 147 | |
| 133 | 148 | legendTemplate() {
|
| 134 | 149 | let label =
|
| 135 | - HEADING_LEVEL_TEMPLATES[this.headingLevel]?.(this.label) || this.label;
|
|
| 150 | + HEADING_LEVEL_TEMPLATES[this.headingLevel]?.(
|
|
| 151 | + this.label,
|
|
| 152 | + this.focusableHeading ? "-1" : undefined
|
|
| 153 | + ) || this.label;
|
|
| 136 | 154 | return html`<legend part="label">
|
| 137 | 155 | ${this.iconTemplate()}${label}${this.badgeTemplate()}
|
| 138 | 156 | </legend>`;
|
| 139 | 157 | }
|
| 140 | 158 | |
| 159 | + /**
|
|
| 160 | + * Move the user's focus to the heading, if it is focusable.
|
|
| 161 | + */
|
|
| 162 | + focusHeading() {
|
|
| 163 | + if (!this.focusableHeading) {
|
|
| 164 | + return;
|
|
| 165 | + }
|
|
| 166 | + this.shadowRoot.querySelector("h1,h2,h3,h4,h5,h6")?.focus();
|
|
| 167 | + }
|
|
| 168 | + |
|
| 141 | 169 | iconTemplate() {
|
| 142 | 170 | if (!this.iconSrc) {
|
| 143 | 171 | return "";
|
| ... | ... | @@ -152,6 +152,9 @@ tor-connection-status-connect-button = |
| 152 | 152 | tor-bridges-heading = Bridges
|
| 153 | 153 | 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.
|
| 154 | 154 | tor-bridges-learn-more-link = Learn more
|
| 155 | +tor-bridges-group =
|
|
| 156 | + .label = Bridges
|
|
| 157 | + .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.
|
|
| 155 | 158 | |
| 156 | 159 | # Toggle button for enabling and disabling the use of bridges.
|
| 157 | 160 | tor-bridges-use-bridges =
|
| ... | ... | @@ -310,12 +313,23 @@ tor-bridges-lox-got-it-button = Got it |
| 310 | 313 | tor-bridges-add-bridges-heading = Add bridges
|
| 311 | 314 | # Shown as a heading when the user has existing bridges that can be replaced.
|
| 312 | 315 | tor-bridges-replace-bridges-heading = Replace your bridges
|
| 316 | +# Shown as a heading when the user has no current bridges.
|
|
| 317 | +tor-bridges-add-bridges-group =
|
|
| 318 | + .label = Add bridges
|
|
| 319 | +# Shown as a heading when the user has existing bridges that can be replaced.
|
|
| 320 | +tor-bridges-replace-bridges-group =
|
|
| 321 | + .label = Replace your bridges
|
|
| 313 | 322 | |
| 314 | 323 | # -brand-short-name refers to 'Tor Browser', localized.
|
| 315 | 324 | tor-bridges-select-built-in-description = Choose from one of { -brand-short-name }’s built-in bridges
|
| 325 | +# -brand-short-name refers to 'Tor Browser', localized.
|
|
| 326 | +tor-bridges-choose-built-in-button =
|
|
| 327 | + .label = Choose from one of { -brand-short-name }’s built-in bridges
|
|
| 316 | 328 | tor-bridges-select-built-in-button = Select a built-in bridge…
|
| 317 | 329 | |
| 318 | 330 | tor-bridges-add-addresses-description = Enter bridge addresses you already know
|
| 331 | +tor-bridges-enter-bridges-button =
|
|
| 332 | + .label = Enter bridge addresses you already know
|
|
| 319 | 333 | # Shown when the user has no current bridges.
|
| 320 | 334 | # Opens a dialog where the user can provide a new bridge address or share code.
|
| 321 | 335 | tor-bridges-add-new-button = Add new bridges…
|
| ... | ... | @@ -326,6 +340,10 @@ tor-bridges-replace-button = Replace bridges… |
| 326 | 340 | tor-bridges-find-more-heading = Find more bridges
|
| 327 | 341 | # "Tor Project" is the organisation name.
|
| 328 | 342 | tor-bridges-find-more-description = Since many bridge addresses aren’t public, you may need to request some from the Tor Project.
|
| 343 | +# "Tor Project" is the organisation name.
|
|
| 344 | +tor-bridges-find-more-group =
|
|
| 345 | + .label = Find more bridges
|
|
| 346 | + .description = Since many bridge addresses aren’t public, you may need to request some from the Tor Project.
|
|
| 329 | 347 | |
| 330 | 348 | # "Telegram" is the common brand name of the Telegram Messenger application
|
| 331 | 349 | tor-bridges-provider-telegram-name = Telegram
|
| ... | ... | @@ -333,6 +351,12 @@ tor-bridges-provider-telegram-name = Telegram |
| 333 | 351 | # $telegramUserName (String) - The Telegram Messenger user name that should receive messages. Should be wrapped in '<a data-l10n-name="user">' and '</a>'.
|
| 334 | 352 | # E.g. in English, "Message GetBridgesBot".
|
| 335 | 353 | tor-bridges-provider-telegram-instruction = Message <a data-l10n-name="user">{ $telegramUserName }</a>
|
| 354 | +# "Telegram" is the common brand name of the Telegram Messenger application.
|
|
| 355 | +# 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.
|
|
| 356 | +# $telegramUserName (String) - The Telegram Messenger user name that should receive messages.
|
|
| 357 | +tor-bridges-source-telegram-link =
|
|
| 358 | + .label = Telegram
|
|
| 359 | + .description = Message { $telegramUserName }
|
|
| 336 | 360 | |
| 337 | 361 | # "Web" is the proper noun for the "World Wide Web".
|
| 338 | 362 | tor-bridges-provider-web-name = Web
|
| ... | ... | @@ -340,15 +364,31 @@ tor-bridges-provider-web-name = Web |
| 340 | 364 | # $url (String) - The URL for Tor Project bridges. Should be wrapped in '<a data-l10n-name"url">' and '</a>'.
|
| 341 | 365 | tor-bridges-provider-web-instruction = Visit <a data-l10n-name="url">{ $url }</a>
|
| 342 | 366 | |
| 367 | +# "Web" is the proper noun for the "World Wide Web".
|
|
| 368 | +# $url (String) - The URL for Tor Project bridges.
|
|
| 369 | +tor-bridges-source-web-link =
|
|
| 370 | + .label = Web
|
|
| 371 | + .description = Visit { $url }
|
|
| 372 | + |
|
| 343 | 373 | # "Gmail" is the Google brand name. "Riseup" refers to the Riseup organisation at riseup.net.
|
| 344 | 374 | tor-bridges-provider-email-name = Gmail or Riseup
|
| 345 | 375 | # 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.
|
| 346 | 376 | # $address (String) - The email address that should receive the email.
|
| 347 | 377 | # E.g. in English, "Email bridges@torproject.org".
|
| 348 | 378 | tor-bridges-provider-email-instruction = Email { $address }
|
| 379 | +# "Gmail" is the Google brand name. "Riseup" refers to the Riseup organisation at riseup.net.
|
|
| 380 | +# 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.
|
|
| 381 | +# $address (String) - The email address that should receive the email.
|
|
| 382 | +# E.g. in English, "Email bridges@torproject.org".
|
|
| 383 | +tor-bridges-source-email-link =
|
|
| 384 | + .label = Gmail or Riseup
|
|
| 385 | + .description = Email { $address }
|
|
| 349 | 386 | |
| 350 | 387 | tor-bridges-request-from-browser = You can also get bridges from the bridge bot without leaving { -brand-short-name }.
|
| 351 | 388 | tor-bridges-request-button = Request bridges…
|
| 389 | +tor-bridges-request-from-browser2 = Get bridges from the bridge bot without leaving { -brand-short-name }.
|
|
| 390 | +tor-bridges-request-button2 =
|
|
| 391 | + .label = Request bridges…
|
|
| 352 | 392 | |
| 353 | 393 | ## Warning dialog when removing all bridges.
|
| 354 | 394 |
| ... | ... | @@ -358,6 +358,12 @@ class TorSettingsImpl { |
| 358 | 358 | */
|
| 359 | 359 | #initialized = false;
|
| 360 | 360 | |
| 361 | + /**
|
|
| 362 | + * Whether init has been called.
|
|
| 363 | + *
|
|
| 364 | + * @type {boolean}
|
|
| 365 | + */
|
|
| 366 | + #initCalled = false;
|
|
| 361 | 367 | /**
|
| 362 | 368 | * Whether uninit cleanup has been called.
|
| 363 | 369 | *
|
| ... | ... | @@ -510,11 +516,12 @@ class TorSettingsImpl { |
| 510 | 516 | * Load or init our settings.
|
| 511 | 517 | */
|
| 512 | 518 | async init() {
|
| 513 | - if (this.#initialized) {
|
|
| 519 | + if (this.#initCalled) {
|
|
| 514 | 520 | lazy.logger.warn("Called init twice.");
|
| 515 | 521 | await this.#initializedPromise;
|
| 516 | 522 | return;
|
| 517 | 523 | }
|
| 524 | + this.#initCalled = true;
|
|
| 518 | 525 | try {
|
| 519 | 526 | await this.#initInternal();
|
| 520 | 527 | this.#initialized = true;
|
| 1 | +import re
|
|
| 2 | + |
|
| 3 | +import fluent.syntax.ast as FTL
|
|
| 4 | +from fluent.migrate.helpers import transforms_from
|
|
| 5 | +from fluent.migrate.transforms import COPY_PATTERN, FluentSource
|
|
| 6 | +from fluent.syntax.visitor import Visitor
|
|
| 7 | + |
|
| 8 | + |
|
| 9 | +class RemoveAnchorVisitor(Visitor):
|
|
| 10 | + """Class to remove <a> and </a> wrappers from a Fluent TextElement."""
|
|
| 11 | + |
|
| 12 | + def __init__(self):
|
|
| 13 | + # Good enough regex for our needs that will match starting and ending
|
|
| 14 | + # tags.
|
|
| 15 | + self._anchor_regex = re.compile(r"<\/?[aA](| [^>]*)>")
|
|
| 16 | + super().__init__()
|
|
| 17 | + |
|
| 18 | + def visit_TextElement(self, node):
|
|
| 19 | + node.value = self._anchor_regex.sub("", node.value)
|
|
| 20 | + |
|
| 21 | + |
|
| 22 | +class RemoveAnchorTransform(FluentSource):
|
|
| 23 | + """Class to remove <a> and </a> wrappers from a Fluent source."""
|
|
| 24 | + |
|
| 25 | + def __call__(self, ctx):
|
|
| 26 | + pattern = ctx.get_fluent_source_pattern(self.path, self.key).clone()
|
|
| 27 | + # Visit every node in the pattern, replacing each TextElement's content.
|
|
| 28 | + RemoveAnchorVisitor().visit(pattern)
|
|
| 29 | + return pattern
|
|
| 30 | + |
|
| 31 | + |
|
| 32 | +def migrate(ctx):
|
|
| 33 | + ctx.add_transforms(
|
|
| 34 | + "tor-browser.ftl",
|
|
| 35 | + "tor-browser.ftl",
|
|
| 36 | + transforms_from(
|
|
| 37 | + """
|
|
| 38 | +tor-bridges-group =
|
|
| 39 | + .label = { COPY_PATTERN(path, "tor-bridges-heading") }
|
|
| 40 | + .description = { COPY_PATTERN(path, "tor-bridges-overview") }
|
|
| 41 | +tor-bridges-add-bridges-group =
|
|
| 42 | + .label = { COPY_PATTERN(path, "tor-bridges-add-bridges-heading") }
|
|
| 43 | +tor-bridges-replace-bridges-group =
|
|
| 44 | + .label = { COPY_PATTERN(path, "tor-bridges-replace-bridges-heading") }
|
|
| 45 | +tor-bridges-choose-built-in-button =
|
|
| 46 | + .label = { COPY_PATTERN(path, "tor-bridges-select-built-in-description") }
|
|
| 47 | +tor-bridges-enter-bridges-button =
|
|
| 48 | + .label = { COPY_PATTERN(path, "tor-bridges-add-addresses-description") }
|
|
| 49 | +tor-bridges-find-more-group =
|
|
| 50 | + .label = { COPY_PATTERN(path, "tor-bridges-find-more-heading") }
|
|
| 51 | + .description = { COPY_PATTERN(path, "tor-bridges-find-more-description") }
|
|
| 52 | +tor-bridges-request-button2 =
|
|
| 53 | + .label = { COPY_PATTERN(path, "tor-bridges-request-button") }
|
|
| 54 | +tor-bridges-source-email-link =
|
|
| 55 | + .label = { COPY_PATTERN(path, "tor-bridges-provider-email-name") }
|
|
| 56 | + .description = { COPY_PATTERN(path, "tor-bridges-provider-email-instruction") }
|
|
| 57 | +""",
|
|
| 58 | + path="tor-browser.ftl",
|
|
| 59 | + )
|
|
| 60 | + + [
|
|
| 61 | + FTL.Message(
|
|
| 62 | + id=FTL.Identifier("tor-bridges-source-telegram-link"),
|
|
| 63 | + value=None,
|
|
| 64 | + attributes=[
|
|
| 65 | + FTL.Attribute(
|
|
| 66 | + id=FTL.Identifier("label"),
|
|
| 67 | + value=COPY_PATTERN(
|
|
| 68 | + "tor-browser.ftl",
|
|
| 69 | + "tor-bridges-provider-telegram-name",
|
|
| 70 | + ),
|
|
| 71 | + ),
|
|
| 72 | + FTL.Attribute(
|
|
| 73 | + id=FTL.Identifier("description"),
|
|
| 74 | + value=RemoveAnchorTransform(
|
|
| 75 | + "tor-browser.ftl",
|
|
| 76 | + "tor-bridges-provider-telegram-instruction",
|
|
| 77 | + ),
|
|
| 78 | + ),
|
|
| 79 | + ],
|
|
| 80 | + ),
|
|
| 81 | + FTL.Message(
|
|
| 82 | + id=FTL.Identifier("tor-bridges-source-web-link"),
|
|
| 83 | + value=None,
|
|
| 84 | + attributes=[
|
|
| 85 | + FTL.Attribute(
|
|
| 86 | + id=FTL.Identifier("label"),
|
|
| 87 | + value=COPY_PATTERN(
|
|
| 88 | + "tor-browser.ftl",
|
|
| 89 | + "tor-bridges-provider-web-name",
|
|
| 90 | + ),
|
|
| 91 | + ),
|
|
| 92 | + FTL.Attribute(
|
|
| 93 | + id=FTL.Identifier("description"),
|
|
| 94 | + value=RemoveAnchorTransform(
|
|
| 95 | + "tor-browser.ftl",
|
|
| 96 | + "tor-bridges-provider-web-instruction",
|
|
| 97 | + ),
|
|
| 98 | + ),
|
|
| 99 | + ],
|
|
| 100 | + ),
|
|
| 101 | + ],
|
|
| 102 | + ) |