Pier Angelo Vendrame pushed to branch tor-browser-128.4.0esr-14.5-1 at The Tor Project / Applications / Tor Browser

Commits:

18 changed files:

Changes:

  • browser/base/content/browser.js
    ... ... @@ -85,7 +85,7 @@ ChromeUtils.defineESModuleGetters(this, {
    85 85
       TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
    
    86 86
       TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs",
    
    87 87
       TorConnect: "resource://gre/modules/TorConnect.sys.mjs",
    
    88
    -  TorConnectState: "resource://gre/modules/TorConnect.sys.mjs",
    
    88
    +  TorConnectStage: "resource://gre/modules/TorConnect.sys.mjs",
    
    89 89
       TorConnectTopics: "resource://gre/modules/TorConnect.sys.mjs",
    
    90 90
       TorUIUtils: "resource:///modules/TorUIUtils.sys.mjs",
    
    91 91
       TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
    

  • browser/base/content/browser.js.globals
    ... ... @@ -276,7 +276,7 @@
    276 276
       "TorDomainIsolator",
    
    277 277
       "gTorCircuitPanel",
    
    278 278
       "TorConnect",
    
    279
    -  "TorConnectState",
    
    279
    +  "TorConnectStage",
    
    280 280
       "TorConnectTopics",
    
    281 281
       "gTorConnectUrlbarButton",
    
    282 282
       "gTorConnectTitlebarStatus",
    

  • browser/components/torpreferences/content/builtinBridgeDialog.js
    ... ... @@ -79,14 +79,14 @@ const gBuiltinBridgeDialog = {
    79 79
     
    
    80 80
         this._acceptButton = dialog.getButton("accept");
    
    81 81
     
    
    82
    -    Services.obs.addObserver(this, TorConnectTopics.StateChange);
    
    82
    +    Services.obs.addObserver(this, TorConnectTopics.StageChange);
    
    83 83
     
    
    84 84
         this.onSelectChange();
    
    85 85
         this.onAcceptStateChange();
    
    86 86
       },
    
    87 87
     
    
    88 88
       uninit() {
    
    89
    -    Services.obs.removeObserver(this, TorConnectTopics.StateChange);
    
    89
    +    Services.obs.removeObserver(this, TorConnectTopics.StageChange);
    
    90 90
       },
    
    91 91
     
    
    92 92
       onSelectChange() {
    
    ... ... @@ -107,7 +107,7 @@ const gBuiltinBridgeDialog = {
    107 107
     
    
    108 108
       observe(subject, topic) {
    
    109 109
         switch (topic) {
    
    110
    -      case TorConnectTopics.StateChange:
    
    110
    +      case TorConnectTopics.StageChange:
    
    111 111
             this.onAcceptStateChange();
    
    112 112
             break;
    
    113 113
         }
    

  • browser/components/torpreferences/content/connectionPane.js
    ... ... @@ -22,7 +22,7 @@ const { TorProviderBuilder, TorProviderTopics } = ChromeUtils.importESModule(
    22 22
       "resource://gre/modules/TorProviderBuilder.sys.mjs"
    
    23 23
     );
    
    24 24
     
    
    25
    -const { TorConnect, TorConnectTopics, TorConnectState, TorCensorshipLevel } =
    
    25
    +const { TorConnect, TorConnectTopics, TorConnectStage, TorCensorshipLevel } =
    
    26 26
       ChromeUtils.importESModule("resource://gre/modules/TorConnect.sys.mjs");
    
    27 27
     
    
    28 28
     const { MoatRPC } = ChromeUtils.importESModule(
    
    ... ... @@ -2195,18 +2195,7 @@ const gBridgeSettings = {
    2195 2195
     
    
    2196 2196
                 // Start Bootstrapping, which should use the configured bridges.
    
    2197 2197
                 // NOTE: We do this regardless of any previous TorConnect Error.
    
    2198
    -            if (TorConnect.canBeginBootstrap) {
    
    2199
    -              TorConnect.beginBootstrap();
    
    2200
    -            }
    
    2201
    -            // Open "about:torconnect".
    
    2202
    -            // FIXME: If there has been a previous bootstrapping error then
    
    2203
    -            // "about:torconnect" will be trying to get the user to use
    
    2204
    -            // AutoBootstrapping. It is not set up to handle a forced direct
    
    2205
    -            // entry to plain Bootstrapping from this dialog so the UI will
    
    2206
    -            // not be aligned. In particular the
    
    2207
    -            // AboutTorConnect.uiState.bootstrapCause will be aligned to
    
    2208
    -            // whatever was shown previously in "about:torconnect" instead.
    
    2209
    -            TorConnect.openTorConnect();
    
    2198
    +            TorConnect.openTorConnect({ beginBootstrapping: "hard" });
    
    2210 2199
               });
    
    2211 2200
             },
    
    2212 2201
             // closedCallback should be called after gSubDialog has already
    
    ... ... @@ -2322,27 +2311,27 @@ const gNetworkStatus = {
    2322 2311
           "network-status-tor-connect-button"
    
    2323 2312
         );
    
    2324 2313
         this._torConnectButton.addEventListener("click", () => {
    
    2325
    -      TorConnect.openTorConnect({ beginBootstrap: true });
    
    2314
    +      TorConnect.openTorConnect({ beginBootstrapping: "soft" });
    
    2326 2315
         });
    
    2327 2316
     
    
    2328 2317
         this._updateInternetStatus("unknown");
    
    2329 2318
         this._updateTorConnectionStatus();
    
    2330 2319
     
    
    2331
    -    Services.obs.addObserver(this, TorConnectTopics.StateChange);
    
    2320
    +    Services.obs.addObserver(this, TorConnectTopics.StageChange);
    
    2332 2321
       },
    
    2333 2322
     
    
    2334 2323
       /**
    
    2335 2324
        * Un-initialize the area.
    
    2336 2325
        */
    
    2337 2326
       uninit() {
    
    2338
    -    Services.obs.removeObserver(this, TorConnectTopics.StateChange);
    
    2327
    +    Services.obs.removeObserver(this, TorConnectTopics.StageChange);
    
    2339 2328
       },
    
    2340 2329
     
    
    2341 2330
       observe(subject, topic) {
    
    2342 2331
         switch (topic) {
    
    2343 2332
           // triggered when tor connect state changes and we may
    
    2344 2333
           // need to update the messagebox
    
    2345
    -      case TorConnectTopics.StateChange: {
    
    2334
    +      case TorConnectTopics.StageChange: {
    
    2346 2335
             this._updateTorConnectionStatus();
    
    2347 2336
             break;
    
    2348 2337
           }
    
    ... ... @@ -2433,7 +2422,8 @@ const gNetworkStatus = {
    2433 2422
         const buttonHadFocus = this._torConnectButton.contains(
    
    2434 2423
           document.activeElement
    
    2435 2424
         );
    
    2436
    -    const isBootstrapped = TorConnect.state === TorConnectState.Bootstrapped;
    
    2425
    +    const isBootstrapped =
    
    2426
    +      TorConnect.stageName === TorConnectStage.Bootstrapped;
    
    2437 2427
         const isBlocked = !isBootstrapped && TorConnect.potentiallyBlocked;
    
    2438 2428
         let l10nId;
    
    2439 2429
         if (isBootstrapped) {
    
    ... ... @@ -2527,7 +2517,8 @@ const gConnectionPane = (function () {
    2527 2517
             );
    
    2528 2518
             chooseForMe.addEventListener("command", () => {
    
    2529 2519
               TorConnect.openTorConnect({
    
    2530
    -            beginAutoBootstrap: location.value,
    
    2520
    +            beginBootstrapping: "hard",
    
    2521
    +            regionCode: location.value,
    
    2531 2522
               });
    
    2532 2523
             });
    
    2533 2524
             this._populateLocations = () => {
    
    ... ... @@ -2558,7 +2549,7 @@ const gConnectionPane = (function () {
    2558 2549
                 locationEntries.append(...items);
    
    2559 2550
               };
    
    2560 2551
               locationEntries.append(
    
    2561
    -            createItem("", TorStrings.settings.bridgeLocationAutomatic)
    
    2552
    +            createItem("automatic", TorStrings.settings.bridgeLocationAutomatic)
    
    2562 2553
               );
    
    2563 2554
               if (TorConnect.countryCodes.length) {
    
    2564 2555
                 locationEntries.append(
    
    ... ... @@ -2607,7 +2598,7 @@ const gConnectionPane = (function () {
    2607 2598
               this.onViewTorLogs();
    
    2608 2599
             });
    
    2609 2600
     
    
    2610
    -      Services.obs.addObserver(this, TorConnectTopics.StateChange);
    
    2601
    +      Services.obs.addObserver(this, TorConnectTopics.StageChange);
    
    2611 2602
         },
    
    2612 2603
     
    
    2613 2604
         init() {
    
    ... ... @@ -2629,7 +2620,7 @@ const gConnectionPane = (function () {
    2629 2620
     
    
    2630 2621
           // unregister our observer topics
    
    2631 2622
           Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
    
    2632
    -      Services.obs.removeObserver(this, TorConnectTopics.StateChange);
    
    2623
    +      Services.obs.removeObserver(this, TorConnectTopics.StageChange);
    
    2633 2624
         },
    
    2634 2625
     
    
    2635 2626
         // whether the page should be present in about:preferences
    
    ... ... @@ -2653,7 +2644,7 @@ const gConnectionPane = (function () {
    2653 2644
             }
    
    2654 2645
             // triggered when tor connect state changes and we may
    
    2655 2646
             // need to update the messagebox
    
    2656
    -        case TorConnectTopics.StateChange: {
    
    2647
    +        case TorConnectTopics.StageChange: {
    
    2657 2648
               this._showAutoconfiguration();
    
    2658 2649
               break;
    
    2659 2650
             }
    

  • browser/components/torpreferences/content/provideBridgeDialog.js
    ... ... @@ -128,14 +128,14 @@ const gProvideBridgeDialog = {
    128 128
           this.onDialogAccept(event)
    
    129 129
         );
    
    130 130
     
    
    131
    -    Services.obs.addObserver(this, TorConnectTopics.StateChange);
    
    131
    +    Services.obs.addObserver(this, TorConnectTopics.StageChange);
    
    132 132
     
    
    133 133
         this.setPage("entry");
    
    134 134
         this.checkValue();
    
    135 135
       },
    
    136 136
     
    
    137 137
       uninit() {
    
    138
    -    Services.obs.removeObserver(this, TorConnectTopics.StateChange);
    
    138
    +    Services.obs.removeObserver(this, TorConnectTopics.StageChange);
    
    139 139
       },
    
    140 140
     
    
    141 141
       /**
    
    ... ... @@ -512,7 +512,7 @@ const gProvideBridgeDialog = {
    512 512
     
    
    513 513
       observe(subject, topic) {
    
    514 514
         switch (topic) {
    
    515
    -      case TorConnectTopics.StateChange:
    
    515
    +      case TorConnectTopics.StageChange:
    
    516 516
             this.onAcceptStateChange();
    
    517 517
             break;
    
    518 518
         }
    

  • browser/components/torpreferences/content/requestBridgeDialog.js
    ... ... @@ -91,14 +91,14 @@ const gRequestBridgeDialog = {
    91 91
           selectors.incorrectCaptchaHbox
    
    92 92
         );
    
    93 93
     
    
    94
    -    Services.obs.addObserver(this, TorConnectTopics.StateChange);
    
    94
    +    Services.obs.addObserver(this, TorConnectTopics.StageChange);
    
    95 95
         this.onAcceptStateChange();
    
    96 96
       },
    
    97 97
     
    
    98 98
       uninit() {
    
    99 99
         BridgeDB.close();
    
    100 100
         // Unregister our observer topics.
    
    101
    -    Services.obs.removeObserver(this, TorConnectTopics.StateChange);
    
    101
    +    Services.obs.removeObserver(this, TorConnectTopics.StageChange);
    
    102 102
       },
    
    103 103
     
    
    104 104
       onAcceptStateChange() {
    
    ... ... @@ -113,7 +113,7 @@ const gRequestBridgeDialog = {
    113 113
     
    
    114 114
       observe(subject, topic) {
    
    115 115
         switch (topic) {
    
    116
    -      case TorConnectTopics.StateChange:
    
    116
    +      case TorConnectTopics.StageChange:
    
    117 117
             this.onAcceptStateChange();
    
    118 118
             break;
    
    119 119
         }
    

  • mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
    ... ... @@ -1438,7 +1438,4 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity, TorIn
    1438 1438
             navHost.navController.navigate(NavGraphDirections.actionStartupHome())
    
    1439 1439
         }
    
    1440 1440
         override fun onBootstrapError(code: String?, message: String?, phase: String?, reason: String?) = Unit
    
    1441
    -    override fun onSettingsRequested() {
    
    1442
    -        navHost.navController.navigate(NavGraphDirections.actionGlobalSettingsFragment())
    
    1443
    -    }
    
    1444 1441
     }

  • mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorControllerGV.kt
    ... ... @@ -332,20 +332,28 @@ class TorControllerGV(
    332 332
         // TorEventsBootstrapStateChangeListener
    
    333 333
         override fun onBootstrapProgress(progress: Double, hasWarnings: Boolean) {
    
    334 334
             Log.d(TAG, "onBootstrapProgress($progress, $hasWarnings)")
    
    335
    +	// TODO: onBootstrapProgress should only be used to change the shown
    
    336
    +	// bootstrap percentage or a Tor log option during a "Bootstrapping"
    
    337
    +	// stage.
    
    338
    +	// The progress value should not be used to change the `lastKnownStatus`
    
    339
    +	// value or determine if a bootstrap has started or completed. The
    
    340
    +	// TorConnectStage should be used instead.
    
    335 341
             if (progress == 100.0) {
    
    336 342
                 lastKnownStatus = TorConnectState.Bootstrapped
    
    337 343
                 wasTorBootstrapped = true
    
    338 344
                 onTorConnected()
    
    339
    -        } else {
    
    340
    -            lastKnownStatus = TorConnectState.Bootstrapping
    
    345
    +        } else if (lastKnownStatus == TorConnectState.Bootstrapping) {
    
    341 346
                 onTorConnecting()
    
    342
    -
    
    343 347
             }
    
    344 348
             onTorStatusUpdate("", lastKnownStatus.toTorStatus().status, progress)
    
    345 349
         }
    
    346 350
     
    
    347 351
         // TorEventsBootstrapStateChangeListener
    
    348 352
         override fun onBootstrapComplete() {
    
    353
    +	// TODO: There should be no need to respond to the BootstrapComplete
    
    354
    +	// event if we are already handling TorConnectStage.Bootstrapped.
    
    355
    +	// In particular, `lastKnownStatus` and onTorConnected should be set in
    
    356
    +	// response to a change in TorConnectStage instead.
    
    349 357
             lastKnownStatus = TorConnectState.Bootstrapped
    
    350 358
             this.onTorConnected()
    
    351 359
         }
    
    ... ... @@ -354,9 +362,4 @@ class TorControllerGV(
    354 362
         override fun onBootstrapError(code: String?, message: String?, phase: String?, reason: String?) {
    
    355 363
             lastKnownError = TorError(code ?: "", message ?: "", phase ?: "", reason ?: "")
    
    356 364
         }
    
    357
    -
    
    358
    -    // TorEventsBootstrapStateChangeListener
    
    359
    -    override fun onSettingsRequested() {
    
    360
    -        // noop
    
    361
    -    }
    
    362 365
     }

  • mobile/android/geckoview/src/main/java/org/mozilla/geckoview/TorIntegrationAndroid.java
    ... ... @@ -44,7 +44,6 @@ public class TorIntegrationAndroid implements BundleEventListener {
    44 44
       private static final String EVENT_TOR_LOGS = "GeckoView:Tor:Logs";
    
    45 45
       private static final String EVENT_SETTINGS_READY = "GeckoView:Tor:SettingsReady";
    
    46 46
       private static final String EVENT_SETTINGS_CHANGED = "GeckoView:Tor:SettingsChanged";
    
    47
    -  private static final String EVENT_SETTINGS_OPEN = "GeckoView:Tor:OpenSettings";
    
    48 47
     
    
    49 48
       // Events we emit
    
    50 49
       private static final String EVENT_SETTINGS_GET = "GeckoView:Tor:SettingsGet";
    
    ... ... @@ -118,8 +117,7 @@ public class TorIntegrationAndroid implements BundleEventListener {
    118 117
                 EVENT_CONNECT_ERROR,
    
    119 118
                 EVENT_BOOTSTRAP_PROGRESS,
    
    120 119
                 EVENT_BOOTSTRAP_COMPLETE,
    
    121
    -            EVENT_TOR_LOGS,
    
    122
    -            EVENT_SETTINGS_OPEN);
    
    120
    +            EVENT_TOR_LOGS);
    
    123 121
       }
    
    124 122
     
    
    125 123
       @Override // BundleEventListener
    
    ... ... @@ -176,10 +174,6 @@ public class TorIntegrationAndroid implements BundleEventListener {
    176 174
           for (TorLogListener listener : mLogListeners) {
    
    177 175
             listener.onLog(type, msg);
    
    178 176
           }
    
    179
    -    } else if (EVENT_SETTINGS_OPEN.equals(event)) {
    
    180
    -      for (BootstrapStateChangeListener listener : mBootstrapStateListeners) {
    
    181
    -        listener.onSettingsRequested();
    
    182
    -      }
    
    183 177
         }
    
    184 178
       }
    
    185 179
     
    
    ... ... @@ -641,8 +635,6 @@ public class TorIntegrationAndroid implements BundleEventListener {
    641 635
         void onBootstrapComplete();
    
    642 636
     
    
    643 637
         void onBootstrapError(String code, String message, String phase, String reason);
    
    644
    -
    
    645
    -    void onSettingsRequested();
    
    646 638
       }
    
    647 639
     
    
    648 640
       public interface TorLogListener {
    

  • toolkit/components/lox/Lox.sys.mjs
    ... ... @@ -23,7 +23,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
    23 23
       DomainFrontRequestResponseError:
    
    24 24
         "resource://gre/modules/DomainFrontedRequests.sys.mjs",
    
    25 25
       TorConnect: "resource://gre/modules/TorConnect.sys.mjs",
    
    26
    -  TorConnectState: "resource://gre/modules/TorConnect.sys.mjs",
    
    26
    +  TorConnectStage: "resource://gre/modules/TorConnect.sys.mjs",
    
    27 27
       TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
    
    28 28
       TorSettingsTopics: "resource://gre/modules/TorSettings.sys.mjs",
    
    29 29
       TorBridgeSource: "resource://gre/modules/TorSettings.sys.mjs",
    
    ... ... @@ -1049,7 +1049,7 @@ class LoxImpl {
    1049 1049
         const method = "POST";
    
    1050 1050
         const contentType = "application/vnd.api+json";
    
    1051 1051
     
    
    1052
    -    if (lazy.TorConnect.state === lazy.TorConnectState.Bootstrapped) {
    
    1052
    +    if (lazy.TorConnect.stageName === lazy.TorConnectStage.Bootstrapped) {
    
    1053 1053
           let request;
    
    1054 1054
           try {
    
    1055 1055
             request = await fetch(url, {
    

  • toolkit/components/torconnect/TorConnectChild.sys.mjs
    ... ... @@ -77,7 +77,7 @@ export class TorConnectChild extends RemotePageChild {
    77 77
       receiveMessage(message) {
    
    78 78
         super.receiveMessage(message);
    
    79 79
     
    
    80
    -    if (message.name === "torconnect:state-change") {
    
    80
    +    if (message.name === "torconnect:stage-change") {
    
    81 81
           this.#maybeRedirect();
    
    82 82
         }
    
    83 83
       }
    

  • toolkit/components/torconnect/TorConnectParent.sys.mjs
    ... ... @@ -2,29 +2,20 @@
    2 2
     
    
    3 3
     import { TorStrings } from "resource://gre/modules/TorStrings.sys.mjs";
    
    4 4
     import {
    
    5
    -  InternetStatus,
    
    6 5
       TorConnect,
    
    7 6
       TorConnectTopics,
    
    8
    -  TorConnectState,
    
    9 7
     } from "resource://gre/modules/TorConnect.sys.mjs";
    
    10 8
     import {
    
    11 9
       TorSettings,
    
    12 10
       TorSettingsTopics,
    
    13 11
     } from "resource://gre/modules/TorSettings.sys.mjs";
    
    14 12
     
    
    15
    -const BroadcastTopic = "about-torconnect:broadcast";
    
    16
    -
    
    17 13
     const lazy = {};
    
    18 14
     
    
    19 15
     ChromeUtils.defineESModuleGetters(lazy, {
    
    20 16
       HomePage: "resource:///modules/HomePage.sys.jsm",
    
    21 17
     });
    
    22 18
     
    
    23
    -const log = console.createInstance({
    
    24
    -  maxLogLevel: "Warn",
    
    25
    -  prefix: "TorConnectParent",
    
    26
    -});
    
    27
    -
    
    28 19
     /*
    
    29 20
     This object is basically a marshalling interface between the TorConnect module
    
    30 21
     and a particular about:torconnect page
    
    ... ... @@ -40,31 +31,6 @@ export class TorConnectParent extends JSWindowActorParent {
    40 31
     
    
    41 32
         const self = this;
    
    42 33
     
    
    43
    -    this.state = {
    
    44
    -      State: TorConnect.state,
    
    45
    -      StateChanged: false,
    
    46
    -      PreviousState: TorConnectState.Initial,
    
    47
    -      ErrorCode: TorConnect.errorCode,
    
    48
    -      ErrorDetails: TorConnect.errorDetails,
    
    49
    -      BootstrapProgress: TorConnect.bootstrapProgress,
    
    50
    -      InternetStatus: TorConnect.internetStatus,
    
    51
    -      DetectedLocation: TorConnect.detectedLocation,
    
    52
    -      ShowViewLog: TorConnect.logHasWarningOrError,
    
    53
    -      HasEverFailed: TorConnect.hasEverFailed,
    
    54
    -      UIState: TorConnect.uiState,
    
    55
    -    };
    
    56
    -
    
    57
    -    // Workaround for a race condition, but we should fix it asap.
    
    58
    -    // about:torconnect is loaded before TorSettings is actually initialized.
    
    59
    -    // The getter might throw and the page not loaded correctly as a result.
    
    60
    -    // Silence any warning for now, but we should really fix it.
    
    61
    -    // See also tor-browser#41921.
    
    62
    -    try {
    
    63
    -      this.state.QuickStartEnabled = TorSettings.quickstart.enabled;
    
    64
    -    } catch (e) {
    
    65
    -      this.state.QuickStartEnabled = false;
    
    66
    -    }
    
    67
    -
    
    68 34
         // JSWindowActiveParent derived objects cannot observe directly, so create a
    
    69 35
         // member object to do our observing for us.
    
    70 36
         //
    
    ... ... @@ -72,103 +38,54 @@ export class TorConnectParent extends JSWindowActorParent {
    72 38
         // module, and maintains a state object which we pass down to our
    
    73 39
         // about:torconnect page, which uses the state object to update its UI.
    
    74 40
         this.torConnectObserver = {
    
    75
    -      observe(aSubject, aTopic) {
    
    76
    -        let obj = aSubject?.wrappedJSObject;
    
    77
    -
    
    78
    -        // Update our state struct based on received torconnect topics and
    
    79
    -        // forward on to aboutTorConnect.js.
    
    80
    -        self.state.StateChanged = false;
    
    81
    -        switch (aTopic) {
    
    82
    -          case TorConnectTopics.StateChange: {
    
    83
    -            self.state.PreviousState = self.state.State;
    
    84
    -            self.state.State = obj.state;
    
    85
    -            self.state.StateChanged = true;
    
    86
    -            // Clear any previous error information if we are bootstrapping.
    
    87
    -            if (self.state.State === TorConnectState.Bootstrapping) {
    
    88
    -              self.state.ErrorCode = null;
    
    89
    -              self.state.ErrorDetails = null;
    
    90
    -            }
    
    91
    -            self.state.BootstrapProgress = TorConnect.bootstrapProgress;
    
    92
    -            self.state.ShowViewLog = TorConnect.logHasWarningOrError;
    
    93
    -            self.state.HasEverFailed = TorConnect.hasEverFailed;
    
    94
    -            break;
    
    95
    -          }
    
    96
    -          case TorConnectTopics.BootstrapProgress: {
    
    97
    -            self.state.BootstrapProgress = obj.progress;
    
    98
    -            self.state.ShowViewLog = obj.hasWarnings;
    
    99
    -            break;
    
    100
    -          }
    
    101
    -          case TorConnectTopics.BootstrapComplete: {
    
    102
    -            // noop
    
    41
    +      observe(subject, topic) {
    
    42
    +        const obj = subject?.wrappedJSObject;
    
    43
    +        switch (topic) {
    
    44
    +          case TorConnectTopics.StageChange:
    
    45
    +            self.sendAsyncMessage("torconnect:stage-change", obj);
    
    103 46
                 break;
    
    104
    -          }
    
    105
    -          case TorConnectTopics.Error: {
    
    106
    -            self.state.ErrorCode = obj.code;
    
    107
    -            self.state.ErrorDetails = obj;
    
    108
    -            self.state.InternetStatus = TorConnect.internetStatus;
    
    109
    -            self.state.DetectedLocation = TorConnect.detectedLocation;
    
    110
    -            self.state.ShowViewLog = true;
    
    47
    +          case TorConnectTopics.BootstrapProgress:
    
    48
    +            self.sendAsyncMessage("torconnect:bootstrap-progress", obj);
    
    111 49
                 break;
    
    112
    -          }
    
    113
    -          case TorSettingsTopics.Ready: {
    
    114
    -            if (
    
    115
    -              self.state.QuickStartEnabled !== TorSettings.quickstart.enabled
    
    116
    -            ) {
    
    117
    -              self.state.QuickStartEnabled = TorSettings.quickstart.enabled;
    
    118
    -            } else {
    
    119
    -              return;
    
    50
    +          case TorSettingsTopics.SettingsChanged:
    
    51
    +            if (!obj.changes.includes("quickstart.enabled")) {
    
    52
    +              break;
    
    120 53
                 }
    
    54
    +          // eslint-disable-next-lined no-fallthrough
    
    55
    +          case TorSettingsTopics.Ready:
    
    56
    +            self.sendAsyncMessage(
    
    57
    +              "torconnect:quickstart-changed",
    
    58
    +              TorSettings.quickstart.enabled
    
    59
    +            );
    
    121 60
                 break;
    
    122
    -          }
    
    123
    -          case TorSettingsTopics.SettingsChanged: {
    
    124
    -            if (
    
    125
    -              aSubject.wrappedJSObject.changes.includes("quickstart.enabled")
    
    126
    -            ) {
    
    127
    -              self.state.QuickStartEnabled = TorSettings.quickstart.enabled;
    
    128
    -            } else {
    
    129
    -              // this isn't a setting torconnect cares about
    
    130
    -              return;
    
    131
    -            }
    
    132
    -            break;
    
    133
    -          }
    
    134
    -          default: {
    
    135
    -            log.warn(`TorConnect: unhandled observe topic '${aTopic}'`);
    
    136
    -          }
    
    137 61
             }
    
    138
    -
    
    139
    -        self.sendAsyncMessage("torconnect:state-change", self.state);
    
    140 62
           },
    
    141 63
         };
    
    142 64
     
    
    143
    -    // Observe all of the torconnect:.* topics.
    
    144
    -    for (const key in TorConnectTopics) {
    
    145
    -      const topic = TorConnectTopics[key];
    
    146
    -      Services.obs.addObserver(this.torConnectObserver, topic);
    
    147
    -    }
    
    65
    +    Services.obs.addObserver(
    
    66
    +      this.torConnectObserver,
    
    67
    +      TorConnectTopics.StageChange
    
    68
    +    );
    
    69
    +    Services.obs.addObserver(
    
    70
    +      this.torConnectObserver,
    
    71
    +      TorConnectTopics.BootstrapProgress
    
    72
    +    );
    
    148 73
         Services.obs.addObserver(this.torConnectObserver, TorSettingsTopics.Ready);
    
    149 74
         Services.obs.addObserver(
    
    150 75
           this.torConnectObserver,
    
    151 76
           TorSettingsTopics.SettingsChanged
    
    152 77
         );
    
    153
    -
    
    154
    -    this.userActionObserver = {
    
    155
    -      observe(aSubject) {
    
    156
    -        let obj = aSubject?.wrappedJSObject;
    
    157
    -        if (obj) {
    
    158
    -          obj.connState = self.state;
    
    159
    -          self.sendAsyncMessage("torconnect:user-action", obj);
    
    160
    -        }
    
    161
    -      },
    
    162
    -    };
    
    163
    -    Services.obs.addObserver(this.userActionObserver, BroadcastTopic);
    
    164 78
       }
    
    165 79
     
    
    166 80
       willDestroy() {
    
    167
    -    // Stop observing all of our torconnect:.* topics.
    
    168
    -    for (const key in TorConnectTopics) {
    
    169
    -      const topic = TorConnectTopics[key];
    
    170
    -      Services.obs.removeObserver(this.torConnectObserver, topic);
    
    171
    -    }
    
    81
    +    Services.obs.removeObserver(
    
    82
    +      this.torConnectObserver,
    
    83
    +      TorConnectTopics.StageChange
    
    84
    +    );
    
    85
    +    Services.obs.removeObserver(
    
    86
    +      this.torConnectObserver,
    
    87
    +      TorConnectTopics.BootstrapProgress
    
    88
    +    );
    
    172 89
         Services.obs.removeObserver(
    
    173 90
           this.torConnectObserver,
    
    174 91
           TorSettingsTopics.Ready
    
    ... ... @@ -177,7 +94,6 @@ export class TorConnectParent extends JSWindowActorParent {
    177 94
           this.torConnectObserver,
    
    178 95
           TorSettingsTopics.SettingsChanged
    
    179 96
         );
    
    180
    -    Services.obs.removeObserver(this.userActionObserver, BroadcastTopic);
    
    181 97
       }
    
    182 98
     
    
    183 99
       async receiveMessage(message) {
    
    ... ... @@ -192,48 +108,57 @@ export class TorConnectParent extends JSWindowActorParent {
    192 108
             TorSettings.saveToPrefs().applySettings();
    
    193 109
             break;
    
    194 110
           case "torconnect:open-tor-preferences":
    
    195
    -        TorConnect.openTorPreferences();
    
    196
    -        break;
    
    197
    -      case "torconnect:cancel-bootstrap":
    
    198
    -        TorConnect.cancelBootstrap();
    
    199
    -        break;
    
    200
    -      case "torconnect:begin-bootstrap":
    
    201
    -        TorConnect.beginBootstrap();
    
    202
    -        break;
    
    203
    -      case "torconnect:begin-autobootstrap":
    
    204
    -        TorConnect.beginAutoBootstrap(message.data);
    
    111
    +        this.browsingContext.top.embedderElement.ownerGlobal.openPreferences(
    
    112
    +          "connection"
    
    113
    +        );
    
    205 114
             break;
    
    206 115
           case "torconnect:view-tor-logs":
    
    207
    -        TorConnect.viewTorLogs();
    
    116
    +        this.browsingContext.top.embedderElement.ownerGlobal.openPreferences(
    
    117
    +          "connection-viewlogs"
    
    118
    +        );
    
    208 119
             break;
    
    209 120
           case "torconnect:restart":
    
    210 121
             Services.startup.quit(
    
    211 122
               Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit
    
    212 123
             );
    
    213 124
             break;
    
    214
    -      case "torconnect:set-ui-state":
    
    215
    -        TorConnect.uiState = message.data;
    
    216
    -        this.state.UIState = TorConnect.uiState;
    
    125
    +      case "torconnect:start-again":
    
    126
    +        TorConnect.startAgain();
    
    127
    +        break;
    
    128
    +      case "torconnect:choose-region":
    
    129
    +        TorConnect.chooseRegion();
    
    130
    +        break;
    
    131
    +      case "torconnect:begin-bootstrapping":
    
    132
    +        TorConnect.beginBootstrapping(message.data.regionCode);
    
    217 133
             break;
    
    218
    -      case "torconnect:broadcast-user-action":
    
    219
    -        Services.obs.notifyObservers(message.data, BroadcastTopic);
    
    134
    +      case "torconnect:cancel-bootstrapping":
    
    135
    +        TorConnect.cancelBootstrapping();
    
    220 136
             break;
    
    221
    -      case "torconnect:get-init-args":
    
    137
    +      case "torconnect:get-init-args": {
    
    222 138
             // Called on AboutTorConnect.init(), pass down all state data it needs
    
    223 139
             // to init.
    
    224 140
     
    
    225
    -        // pretend this is a state transition on init
    
    226
    -        // so we always get fresh UI
    
    227
    -        this.state.StateChanged = true;
    
    228
    -        this.state.UIState = TorConnect.uiState;
    
    141
    +        let quickstartEnabled = false;
    
    142
    +
    
    143
    +        // Workaround for a race condition, but we should fix it asap.
    
    144
    +        // about:torconnect is loaded before TorSettings is actually initialized.
    
    145
    +        // The getter might throw and the page not loaded correctly as a result.
    
    146
    +        // Silence any warning for now, but we should really fix it.
    
    147
    +        // See also tor-browser#41921.
    
    148
    +        try {
    
    149
    +          quickstartEnabled = TorSettings.quickstart.enabled;
    
    150
    +        } catch (e) {
    
    151
    +          // Do not throw.
    
    152
    +        }
    
    153
    +
    
    229 154
             return {
    
    230 155
               TorStrings,
    
    231
    -          TorConnectState,
    
    232
    -          InternetStatus,
    
    233 156
               Direction: Services.locale.isAppLocaleRTL ? "rtl" : "ltr",
    
    234
    -          State: this.state,
    
    235 157
               CountryNames: TorConnect.countryNames,
    
    158
    +          stage: TorConnect.stage,
    
    159
    +          quickstartEnabled,
    
    236 160
             };
    
    161
    +      }
    
    237 162
           case "torconnect:get-country-codes":
    
    238 163
             return TorConnect.getCountryCodes();
    
    239 164
         }
    

  • toolkit/components/torconnect/content/aboutTorConnect.js
    ... ... @@ -7,8 +7,6 @@
    7 7
     
    
    8 8
     // populated in AboutTorConnect.init()
    
    9 9
     let TorStrings = {};
    
    10
    -let TorConnectState = {};
    
    11
    -let InternetStatus = {};
    
    12 10
     
    
    13 11
     const UIStates = Object.freeze({
    
    14 12
       ConnectToTor: "ConnectToTor",
    
    ... ... @@ -135,53 +133,23 @@ class AboutTorConnect {
    135 133
         tryBridgeButton: document.querySelector(this.selectors.buttons.tryBridge),
    
    136 134
       });
    
    137 135
     
    
    138
    -  uiState = {
    
    139
    -    currentState: UIStates.ConnectToTor,
    
    140
    -    allowAutomaticLocation: true,
    
    141
    -    selectedLocation: "automatic",
    
    142
    -    bootstrapCause: UIStates.ConnectToTor,
    
    143
    -  };
    
    136
    +  selectedLocation;
    
    137
    +  shownStage = null;
    
    144 138
     
    
    145 139
       locations = {};
    
    146 140
     
    
    147
    -  constructor() {
    
    148
    -    this.uiStates = Object.freeze(
    
    149
    -      Object.fromEntries([
    
    150
    -        [UIStates.ConnectToTor, this.showConnectToTor.bind(this)],
    
    151
    -        [UIStates.Offline, this.showOffline.bind(this)],
    
    152
    -        [UIStates.ConnectionAssist, this.showConnectionAssistant.bind(this)],
    
    153
    -        [UIStates.CouldNotLocate, this.showCouldNotLocate.bind(this)],
    
    154
    -        [UIStates.LocationConfirm, this.showLocationConfirmation.bind(this)],
    
    155
    -        [UIStates.FinalError, this.showFinalError.bind(this)],
    
    156
    -      ])
    
    157
    -    );
    
    158
    -  }
    
    159
    -
    
    160
    -  beginBootstrap() {
    
    161
    -    RPMSendAsyncMessage("torconnect:begin-bootstrap");
    
    162
    -  }
    
    163
    -
    
    164
    -  beginAutoBootstrap(countryCode) {
    
    165
    -    if (countryCode === "automatic") {
    
    166
    -      countryCode = "";
    
    167
    -    }
    
    168
    -    RPMSendAsyncMessage("torconnect:begin-autobootstrap", countryCode);
    
    141
    +  beginBootstrapping() {
    
    142
    +    RPMSendAsyncMessage("torconnect:begin-bootstrapping", {});
    
    169 143
       }
    
    170 144
     
    
    171
    -  cancelBootstrap() {
    
    172
    -    RPMSendAsyncMessage("torconnect:cancel-bootstrap");
    
    173
    -  }
    
    174
    -
    
    175
    -  transitionUIState(nextState, connState) {
    
    176
    -    if (nextState !== this.uiState.currentState) {
    
    177
    -      this.uiState.currentState = nextState;
    
    178
    -      this.saveUIState();
    
    179
    -    }
    
    180
    -    this.uiStates[nextState](connState);
    
    145
    +  beginAutoBootstrapping(regionCode) {
    
    146
    +    RPMSendAsyncMessage("torconnect:begin-bootstrapping", {
    
    147
    +      regionCode,
    
    148
    +    });
    
    181 149
       }
    
    182 150
     
    
    183
    -  saveUIState() {
    
    184
    -    RPMSendAsyncMessage("torconnect:set-ui-state", this.uiState);
    
    151
    +  cancelBootstrapping() {
    
    152
    +    RPMSendAsyncMessage("torconnect:cancel-bootstrapping");
    
    185 153
       }
    
    186 154
     
    
    187 155
       /*
    
    ... ... @@ -305,19 +273,6 @@ class AboutTorConnect {
    305 273
         this.elements.longContentText.append(...args);
    
    306 274
       }
    
    307 275
     
    
    308
    -  setProgress(description, visible, percent) {
    
    309
    -    this.elements.progressDescription.textContent = description;
    
    310
    -    if (visible) {
    
    311
    -      this.show(this.elements.progressMeter);
    
    312
    -      this.elements.progressMeter.style.setProperty(
    
    313
    -        "--progress-percent",
    
    314
    -        `${percent}%`
    
    315
    -      );
    
    316
    -    } else {
    
    317
    -      this.hide(this.elements.progressMeter);
    
    318
    -    }
    
    319
    -  }
    
    320
    -
    
    321 276
       setBreadcrumbsStatus(connectToTor, connectionAssist, tryBridge) {
    
    322 277
         this.elements.breadcrumbContainer.classList.remove("hidden");
    
    323 278
         const elems = [
    
    ... ... @@ -362,22 +317,17 @@ class AboutTorConnect {
    362 317
         return TorStrings.torConnect.bootstrapStatus[status] ?? status;
    
    363 318
       }
    
    364 319
     
    
    365
    -  getMaybeLocalizedError(state) {
    
    366
    -    if (!state?.ErrorCode) {
    
    367
    -      return "";
    
    368
    -    }
    
    369
    -    switch (state.ErrorCode) {
    
    320
    +  getMaybeLocalizedError(error) {
    
    321
    +    switch (error.code) {
    
    370 322
           case "Offline":
    
    371 323
             return TorStrings.torConnect.offline;
    
    372 324
           case "BootstrapError": {
    
    373
    -        const details = state.ErrorDetails?.cause;
    
    374
    -        if (!details?.phase || !details?.reason) {
    
    325
    +        if (!error.phase || !error.reason) {
    
    375 326
               return TorStrings.torConnect.torBootstrapFailed;
    
    376 327
             }
    
    377
    -        let status = this.getLocalizedStatus(details.phase);
    
    328
    +        let status = this.getLocalizedStatus(error.phase);
    
    378 329
             const reason =
    
    379
    -          TorStrings.torConnect.bootstrapWarning[details.reason] ??
    
    380
    -          details.reason;
    
    330
    +          TorStrings.torConnect.bootstrapWarning[error.reason] ?? error.reason;
    
    381 331
             return TorStrings.torConnect.bootstrapFailedDetails
    
    382 332
               .replace("%1$S", status)
    
    383 333
               .replace("%2$S", reason);
    
    ... ... @@ -392,13 +342,10 @@ class AboutTorConnect {
    392 342
             // A standard JS error, or something for which we do probably do not
    
    393 343
             // have a translation. Returning the original message is the best we can
    
    394 344
             // do.
    
    395
    -        return state.ErrorDetails.message;
    
    345
    +        return error.message;
    
    396 346
           default:
    
    397
    -        console.warn(
    
    398
    -          `Unknown error code: ${state.ErrorCode}`,
    
    399
    -          state.ErrorDetails
    
    400
    -        );
    
    401
    -        return state.ErrorDetails?.message ?? state.ErrorCode;
    
    347
    +        console.warn(`Unknown error code: ${error.code}`, error);
    
    348
    +        return error.message || error.code;
    
    402 349
         }
    
    403 350
       }
    
    404 351
     
    
    ... ... @@ -406,109 +353,119 @@ class AboutTorConnect {
    406 353
       These methods update the UI based on the current TorConnect state
    
    407 354
       */
    
    408 355
     
    
    409
    -  updateUI(state) {
    
    410
    -    // calls update_$state()
    
    411
    -    this[`update_${state.State}`](state);
    
    412
    -    this.elements.quickstartToggle.pressed = state.QuickStartEnabled;
    
    413
    -  }
    
    356
    +  updateStage(stage) {
    
    357
    +    if (stage.name === this.shownStage) {
    
    358
    +      return;
    
    359
    +    }
    
    414 360
     
    
    415
    -  /* Per-state updates */
    
    361
    +    this.shownStage = stage.name;
    
    362
    +    this.selectedLocation = stage.defaultRegion;
    
    416 363
     
    
    417
    -  update_Initial(state) {
    
    418
    -    this.showConnectToTor(state);
    
    419
    -  }
    
    364
    +    let showProgress = false;
    
    365
    +    let showLog = false;
    
    366
    +    switch (stage.name) {
    
    367
    +      case "Disabled":
    
    368
    +        console.error("Should not be open when TorConnect is disabled");
    
    369
    +        break;
    
    370
    +      case "Loading":
    
    371
    +      case "Start":
    
    372
    +        // Loading is not currnetly handled, treat the same as "Start", but UI
    
    373
    +        // will be unresponsive.
    
    374
    +        this.showStart(stage.tryAgain, stage.potentiallyBlocked);
    
    375
    +        break;
    
    376
    +      case "Bootstrapping":
    
    377
    +        showProgress = true;
    
    378
    +        this.showBootstrapping(stage.bootstrapTrigger, stage.tryAgain);
    
    379
    +        break;
    
    380
    +      case "Offline":
    
    381
    +        showLog = true;
    
    382
    +        this.showOffline();
    
    383
    +        break;
    
    384
    +      case "ChooseRegion":
    
    385
    +        showLog = true;
    
    386
    +        this.showChooseRegion(stage.error);
    
    387
    +        break;
    
    388
    +      case "RegionNotFound":
    
    389
    +        showLog = true;
    
    390
    +        this.showRegionNotFound();
    
    391
    +        break;
    
    392
    +      case "ConfirmRegion":
    
    393
    +        showLog = true;
    
    394
    +        this.showConfirmRegion(stage.error);
    
    395
    +        break;
    
    396
    +      case "FinalError":
    
    397
    +        showLog = true;
    
    398
    +        this.showFinalError(stage.error);
    
    399
    +        break;
    
    400
    +      case "Bootstrapped":
    
    401
    +        showProgress = true;
    
    402
    +        this.showBootstrapped();
    
    403
    +        break;
    
    404
    +      default:
    
    405
    +        console.error(`Unknown stage ${stage.name}`);
    
    406
    +        break;
    
    407
    +    }
    
    420 408
     
    
    421
    -  update_Configuring(state) {
    
    422
    -    if (
    
    423
    -      state.StateChanged &&
    
    424
    -      (state.PreviousState === TorConnectState.Bootstrapping ||
    
    425
    -        state.PreviousState === TorConnectState.AutoBootstrapping)
    
    426
    -    ) {
    
    427
    -      // The bootstrap has been cancelled
    
    428
    -      this.transitionUIState(this.uiState.bootstrapCause, state);
    
    409
    +    if (showProgress) {
    
    410
    +      this.show(this.elements.progressMeter);
    
    411
    +    } else {
    
    412
    +      this.hide(this.elements.progressMeter);
    
    429 413
         }
    
    430
    -  }
    
    431 414
     
    
    432
    -  update_AutoBootstrapping(state) {
    
    433
    -    this.showBootstrapping(state);
    
    434
    -  }
    
    415
    +    this.updateBootstrappingStatus(stage.bootstrappingStatus);
    
    435 416
     
    
    436
    -  update_Bootstrapping(state) {
    
    437
    -    this.showBootstrapping(state);
    
    417
    +    if (showLog) {
    
    418
    +      this.show(this.elements.viewLogButton);
    
    419
    +    } else {
    
    420
    +      this.hide(this.elements.viewLogButton);
    
    421
    +    }
    
    438 422
       }
    
    439 423
     
    
    440
    -  update_Error(state) {
    
    441
    -    if (!state.StateChanged) {
    
    442
    -      return;
    
    443
    -    }
    
    444
    -    if (state.InternetStatus === InternetStatus.Offline) {
    
    445
    -      this.transitionUIState(UIStates.Offline, state);
    
    446
    -    } else if (state.PreviousState === TorConnectState.Bootstrapping) {
    
    447
    -      this.transitionUIState(UIStates.ConnectionAssist, state);
    
    448
    -    } else if (state.PreviousState === TorConnectState.AutoBootstrapping) {
    
    449
    -      if (this.uiState.bootstrapCause === UIStates.ConnectionAssist) {
    
    450
    -        if (this.getLocation() === "automatic") {
    
    451
    -          this.uiState.allowAutomaticLocation = false;
    
    452
    -          if (!state.DetectedLocation) {
    
    453
    -            this.transitionUIState(UIStates.CouldNotLocate, state);
    
    454
    -            return;
    
    455
    -          }
    
    456
    -          // Change the location only here, to avoid overriding any user change/
    
    457
    -          // insisting with the detected location
    
    458
    -          this.setLocation(state.DetectedLocation);
    
    459
    -        }
    
    460
    -        this.transitionUIState(UIStates.LocationConfirm, state);
    
    461
    -      } else {
    
    462
    -        this.transitionUIState(UIStates.FinalError, state);
    
    463
    -      }
    
    464
    -    } else {
    
    465
    -      console.error(
    
    466
    -        "We received an error starting from an unexpected state",
    
    467
    -        state
    
    468
    -      );
    
    424
    +  updateBootstrappingStatus(data) {
    
    425
    +    this.elements.progressMeter.style.setProperty(
    
    426
    +      "--progress-percent",
    
    427
    +      `${data.progress}%`
    
    428
    +    );
    
    429
    +    if (this.shownStage === "Bootstrapping" && data.hasWarning) {
    
    430
    +      // When bootstrapping starts, we hide the log button, but we re-show it if
    
    431
    +      // we get a warning.
    
    432
    +      this.show(this.elements.viewLogButton);
    
    469 433
         }
    
    470 434
       }
    
    471 435
     
    
    472
    -  update_Bootstrapped(_state) {
    
    473
    -    const showProgressbar = true;
    
    436
    +  updateQuickstart(enabled) {
    
    437
    +    this.elements.quickstartToggle.pressed = enabled;
    
    438
    +  }
    
    474 439
     
    
    440
    +  showBootstrapped() {
    
    475 441
         this.setTitle(TorStrings.torConnect.torConnected, "");
    
    476 442
         this.setLongText(TorStrings.settings.torPreferencesDescription);
    
    477
    -    this.setProgress("", showProgressbar, 100);
    
    443
    +    this.elements.progressDescription.textContent = "";
    
    478 444
         this.hideButtons();
    
    479 445
       }
    
    480 446
     
    
    481
    -  update_Disabled(_state) {
    
    482
    -    // TODO: we should probably have some UX here if a user goes to about:torconnect when
    
    483
    -    // it isn't in use (eg using tor-launcher or system tor)
    
    484
    -  }
    
    485
    -
    
    486
    -  showConnectToTor(state) {
    
    447
    +  showStart(tryAgain, potentiallyBlocked) {
    
    487 448
         this.setTitle(TorStrings.torConnect.torConnect, "");
    
    488 449
         this.setLongText(TorStrings.settings.torPreferencesDescription);
    
    489
    -    this.setProgress("", false);
    
    490
    -    this.hide(this.elements.viewLogButton);
    
    450
    +    this.elements.progressDescription.textContent = "";
    
    491 451
         this.hideButtons();
    
    492 452
         this.show(this.elements.quickstartContainer);
    
    493 453
         this.show(this.elements.configureButton);
    
    494 454
         this.show(this.elements.connectButton, true);
    
    495
    -    if (state?.StateChanged) {
    
    496
    -      this.elements.connectButton.focus();
    
    455
    +    this.elements.connectButton.focus();
    
    456
    +    if (tryAgain) {
    
    457
    +      this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain;
    
    497 458
         }
    
    498
    -    if (state?.HasEverFailed) {
    
    459
    +    if (potentiallyBlocked) {
    
    499 460
           this.setBreadcrumbsStatus(
    
    500 461
             BreadcrumbStatus.Active,
    
    501 462
             BreadcrumbStatus.Default,
    
    502 463
             BreadcrumbStatus.Disabled
    
    503 464
           );
    
    504
    -      this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain;
    
    505 465
         }
    
    506
    -    this.uiState.bootstrapCause = UIStates.ConnectToTor;
    
    507
    -    this.saveUIState();
    
    508 466
       }
    
    509 467
     
    
    510
    -  showBootstrapping(state) {
    
    511
    -    const showProgressbar = true;
    
    468
    +  showBootstrapping(trigger, tryAgain) {
    
    512 469
         let title = "";
    
    513 470
         let description = "";
    
    514 471
         const breadcrumbs = [
    
    ... ... @@ -516,128 +473,114 @@ class AboutTorConnect {
    516 473
           BreadcrumbStatus.Disabled,
    
    517 474
           BreadcrumbStatus.Disabled,
    
    518 475
         ];
    
    519
    -    switch (this.uiState.bootstrapCause) {
    
    520
    -      case UIStates.ConnectToTor:
    
    476
    +    switch (trigger) {
    
    477
    +      case "Start":
    
    478
    +      case "Offline":
    
    521 479
             breadcrumbs[0] = BreadcrumbStatus.Active;
    
    522
    -        title = state.HasEverFailed
    
    480
    +        title = tryAgain
    
    523 481
               ? TorStrings.torConnect.tryAgain
    
    524 482
               : TorStrings.torConnect.torConnecting;
    
    525 483
             description = TorStrings.settings.torPreferencesDescription;
    
    526 484
             break;
    
    527
    -      case UIStates.ConnectionAssist:
    
    485
    +      case "ChooseRegion":
    
    528 486
             breadcrumbs[2] = BreadcrumbStatus.Active;
    
    529 487
             title = TorStrings.torConnect.tryingBridge;
    
    530 488
             description = TorStrings.torConnect.assistDescription;
    
    531 489
             break;
    
    532
    -      case UIStates.CouldNotLocate:
    
    490
    +      case "RegionNotFound":
    
    533 491
             breadcrumbs[2] = BreadcrumbStatus.Active;
    
    534 492
             title = TorStrings.torConnect.tryingBridgeAgain;
    
    535 493
             description = TorStrings.torConnect.errorLocationDescription;
    
    536 494
             break;
    
    537
    -      case UIStates.LocationConfirm:
    
    495
    +      case "ConfirmRegion":
    
    538 496
             breadcrumbs[2] = BreadcrumbStatus.Active;
    
    539 497
             title = TorStrings.torConnect.tryingBridgeAgain;
    
    540 498
             description = TorStrings.torConnect.isLocationCorrectDescription;
    
    541 499
             break;
    
    500
    +      default:
    
    501
    +        console.warn("Unrecognized bootstrap trigger", trigger);
    
    502
    +        break;
    
    542 503
         }
    
    543 504
         this.setTitle(title, "");
    
    544 505
         this.showConfigureConnectionLink(description);
    
    545
    -    this.setProgress("", showProgressbar, state.BootstrapProgress);
    
    546
    -    if (state.HasEverFailed) {
    
    506
    +    this.elements.progressDescription.textContent = "";
    
    507
    +    if (tryAgain) {
    
    547 508
           this.setBreadcrumbsStatus(...breadcrumbs);
    
    548 509
         } else {
    
    549 510
           this.hideBreadcrumbs();
    
    550 511
         }
    
    551 512
         this.hideButtons();
    
    552
    -    if (state.ShowViewLog) {
    
    553
    -      this.show(this.elements.viewLogButton);
    
    554
    -    } else {
    
    555
    -      this.hide(this.elements.viewLogButton);
    
    556
    -    }
    
    557 513
         this.show(this.elements.cancelButton);
    
    558
    -    if (state.StateChanged) {
    
    559
    -      this.elements.cancelButton.focus();
    
    560
    -    }
    
    514
    +    this.elements.cancelButton.focus();
    
    561 515
       }
    
    562 516
     
    
    563
    -  showOffline(state) {
    
    517
    +  showOffline() {
    
    564 518
         this.setTitle(TorStrings.torConnect.noInternet, "offline");
    
    565 519
         this.setLongText(TorStrings.torConnect.noInternetDescription);
    
    566
    -    this.setProgress(this.getMaybeLocalizedError(state), false);
    
    520
    +    this.elements.progressDescription.textContent =
    
    521
    +      TorStrings.torConnect.offline;
    
    567 522
         this.setBreadcrumbsStatus(
    
    568 523
           BreadcrumbStatus.Default,
    
    569 524
           BreadcrumbStatus.Active,
    
    570 525
           BreadcrumbStatus.Hidden
    
    571 526
         );
    
    572
    -    this.show(this.elements.viewLogButton);
    
    573 527
         this.hideButtons();
    
    574 528
         this.show(this.elements.configureButton);
    
    575 529
         this.show(this.elements.connectButton, true);
    
    576 530
         this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain;
    
    577 531
       }
    
    578 532
     
    
    579
    -  showConnectionAssistant(state) {
    
    533
    +  showChooseRegion(error) {
    
    580 534
         this.setTitle(TorStrings.torConnect.couldNotConnect, "assist");
    
    581 535
         this.showConfigureConnectionLink(TorStrings.torConnect.assistDescription);
    
    582
    -    this.setProgress(this.getMaybeLocalizedError(state), false);
    
    536
    +    this.elements.progressDescription.textContent =
    
    537
    +      this.getMaybeLocalizedError(error);
    
    583 538
         this.setBreadcrumbsStatus(
    
    584 539
           BreadcrumbStatus.Default,
    
    585 540
           BreadcrumbStatus.Active,
    
    586 541
           BreadcrumbStatus.Disabled
    
    587 542
         );
    
    588
    -    this.showLocationForm(false, TorStrings.torConnect.tryBridge);
    
    589
    -    if (state?.StateChanged) {
    
    590
    -      this.elements.tryBridgeButton.focus();
    
    591
    -    }
    
    592
    -    this.uiState.bootstrapCause = UIStates.ConnectionAssist;
    
    593
    -    this.saveUIState();
    
    543
    +    this.showLocationForm(true, TorStrings.torConnect.tryBridge);
    
    544
    +    this.elements.tryBridgeButton.focus();
    
    594 545
       }
    
    595 546
     
    
    596
    -  showCouldNotLocate(state) {
    
    597
    -    this.uiState.allowAutomaticLocation = false;
    
    547
    +  showRegionNotFound() {
    
    598 548
         this.setTitle(TorStrings.torConnect.errorLocation, "location");
    
    599 549
         this.showConfigureConnectionLink(
    
    600 550
           TorStrings.torConnect.errorLocationDescription
    
    601 551
         );
    
    602
    -    this.setProgress(TorStrings.torConnect.cannotDetermineCountry, false);
    
    552
    +    this.elements.progressDescription.textContent =
    
    553
    +      TorStrings.torConnect.cannotDetermineCountry;
    
    603 554
         this.setBreadcrumbsStatus(
    
    604 555
           BreadcrumbStatus.Default,
    
    605 556
           BreadcrumbStatus.Active,
    
    606 557
           BreadcrumbStatus.Disabled
    
    607 558
         );
    
    608
    -    this.show(this.elements.viewLogButton);
    
    609
    -    this.showLocationForm(true, TorStrings.torConnect.tryBridge);
    
    610
    -    if (state.StateChanged) {
    
    611
    -      this.elements.tryBridgeButton.focus();
    
    612
    -    }
    
    613
    -    this.uiState.bootstrapCause = UIStates.CouldNotLocate;
    
    614
    -    this.saveUIState();
    
    559
    +    this.showLocationForm(false, TorStrings.torConnect.tryBridge);
    
    560
    +    this.elements.tryBridgeButton.focus();
    
    615 561
       }
    
    616 562
     
    
    617
    -  showLocationConfirmation(state) {
    
    563
    +  showConfirmRegion(error) {
    
    618 564
         this.setTitle(TorStrings.torConnect.isLocationCorrect, "location");
    
    619 565
         this.showConfigureConnectionLink(
    
    620 566
           TorStrings.torConnect.isLocationCorrectDescription
    
    621 567
         );
    
    622
    -    this.setProgress(this.getMaybeLocalizedError(state), false);
    
    568
    +    this.elements.progressDescription.textContent =
    
    569
    +      this.getMaybeLocalizedError(error);
    
    623 570
         this.setBreadcrumbsStatus(
    
    624 571
           BreadcrumbStatus.Default,
    
    625 572
           BreadcrumbStatus.Default,
    
    626 573
           BreadcrumbStatus.Active
    
    627 574
         );
    
    628
    -    this.show(this.elements.viewLogButton);
    
    629
    -    this.showLocationForm(true, TorStrings.torConnect.tryAgain);
    
    630
    -    if (state.StateChanged) {
    
    631
    -      this.elements.tryBridgeButton.focus();
    
    632
    -    }
    
    633
    -    this.uiState.bootstrapCause = UIStates.LocationConfirm;
    
    634
    -    this.saveUIState();
    
    575
    +    this.showLocationForm(false, TorStrings.torConnect.tryAgain);
    
    576
    +    this.elements.tryBridgeButton.focus();
    
    635 577
       }
    
    636 578
     
    
    637
    -  showFinalError(state) {
    
    579
    +  showFinalError(error) {
    
    638 580
         this.setTitle(TorStrings.torConnect.finalError, "final");
    
    639 581
         this.setLongText(TorStrings.torConnect.finalErrorDescription);
    
    640
    -    this.setProgress(this.getMaybeLocalizedError(state), false);
    
    582
    +    this.elements.progressDescription.textContent =
    
    583
    +      this.getMaybeLocalizedError(error);
    
    641 584
         this.setBreadcrumbsStatus(
    
    642 585
           BreadcrumbStatus.Default,
    
    643 586
           BreadcrumbStatus.Default,
    
    ... ... @@ -665,7 +608,7 @@ class AboutTorConnect {
    665 608
         }
    
    666 609
       }
    
    667 610
     
    
    668
    -  showLocationForm(isError, buttonLabel) {
    
    611
    +  showLocationForm(isChoose, buttonLabel) {
    
    669 612
         this.hideButtons();
    
    670 613
         RPMSendQuery("torconnect:get-country-codes").then(codes => {
    
    671 614
           if (codes && codes.length) {
    
    ... ... @@ -674,7 +617,7 @@ class AboutTorConnect {
    674 617
           }
    
    675 618
         });
    
    676 619
         let firstOpt = this.elements.locationDropdownSelect.options[0];
    
    677
    -    if (this.uiState.allowAutomaticLocation) {
    
    620
    +    if (isChoose) {
    
    678 621
           firstOpt.value = "automatic";
    
    679 622
           firstOpt.textContent = TorStrings.torConnect.automatic;
    
    680 623
         } else {
    
    ... ... @@ -685,7 +628,7 @@ class AboutTorConnect {
    685 628
         this.validateLocation();
    
    686 629
         this.show(this.elements.locationDropdownLabel);
    
    687 630
         this.show(this.elements.locationDropdown);
    
    688
    -    this.elements.locationDropdownLabel.classList.toggle("error", isError);
    
    631
    +    this.elements.locationDropdownLabel.classList.toggle("error", !isChoose);
    
    689 632
         this.show(this.elements.tryBridgeButton, true);
    
    690 633
         if (buttonLabel !== undefined) {
    
    691 634
           this.elements.tryBridgeButton.textContent = buttonLabel;
    
    ... ... @@ -697,12 +640,8 @@ class AboutTorConnect {
    697 640
         return this.elements.locationDropdownSelect.options[selectedIndex].value;
    
    698 641
       }
    
    699 642
     
    
    700
    -  setLocation(code) {
    
    701
    -    if (!code) {
    
    702
    -      code = this.uiState.selectedLocation;
    
    703
    -    } else {
    
    704
    -      this.uiState.selectedLocation = code;
    
    705
    -    }
    
    643
    +  setLocation() {
    
    644
    +    const code = this.selectedLocation;
    
    706 645
         if (this.getLocation() === code) {
    
    707 646
           return;
    
    708 647
         }
    
    ... ... @@ -726,13 +665,7 @@ class AboutTorConnect {
    726 665
         document.documentElement.setAttribute("dir", direction);
    
    727 666
     
    
    728 667
         this.elements.connectToTorLink.addEventListener("click", () => {
    
    729
    -      if (this.uiState.currentState === UIStates.ConnectToTor) {
    
    730
    -        return;
    
    731
    -      }
    
    732
    -      this.transitionUIState(UIStates.ConnectToTor, null);
    
    733
    -      RPMSendAsyncMessage("torconnect:broadcast-user-action", {
    
    734
    -        uiState: UIStates.ConnectToTor,
    
    735
    -      });
    
    668
    +      RPMSendAsyncMessage("torconnect:start-again");
    
    736 669
         });
    
    737 670
         this.elements.connectToTorLabel.textContent =
    
    738 671
           TorStrings.torConnect.torConnect;
    
    ... ... @@ -747,10 +680,7 @@ class AboutTorConnect {
    747 680
           ) {
    
    748 681
             return;
    
    749 682
           }
    
    750
    -      this.transitionUIState(UIStates.ConnectionAssist, null);
    
    751
    -      RPMSendAsyncMessage("torconnect:broadcast-user-action", {
    
    752
    -        uiState: UIStates.ConnectionAssist,
    
    753
    -      });
    
    683
    +      RPMSendAsyncMessage("torconnect:choose-region");
    
    754 684
         });
    
    755 685
         this.elements.connectionAssistLabel.textContent =
    
    756 686
           TorStrings.torConnect.breadcrumbAssist;
    
    ... ... @@ -786,23 +716,18 @@ class AboutTorConnect {
    786 716
     
    
    787 717
         this.elements.cancelButton.textContent = TorStrings.torConnect.cancel;
    
    788 718
         this.elements.cancelButton.addEventListener("click", () => {
    
    789
    -      this.cancelBootstrap();
    
    719
    +      this.cancelBootstrapping();
    
    790 720
         });
    
    791 721
     
    
    792 722
         this.elements.connectButton.textContent =
    
    793 723
           TorStrings.torConnect.torConnectButton;
    
    794 724
         this.elements.connectButton.addEventListener("click", () => {
    
    795
    -      this.beginBootstrap();
    
    725
    +      this.beginBootstrapping();
    
    796 726
         });
    
    797 727
     
    
    798 728
         this.populateLocations();
    
    799 729
         this.elements.locationDropdownSelect.addEventListener("change", () => {
    
    800
    -      this.uiState.selectedLocation = this.getLocation();
    
    801
    -      this.saveUIState();
    
    802 730
           this.validateLocation();
    
    803
    -      RPMSendAsyncMessage("torconnect:broadcast-user-action", {
    
    804
    -        location: this.uiState.selectedLocation,
    
    805
    -      });
    
    806 731
         });
    
    807 732
     
    
    808 733
         this.elements.locationDropdownLabel.textContent =
    
    ... ... @@ -811,10 +736,8 @@ class AboutTorConnect {
    811 736
         this.elements.tryBridgeButton.textContent = TorStrings.torConnect.tryBridge;
    
    812 737
         this.elements.tryBridgeButton.addEventListener("click", () => {
    
    813 738
           const value = this.getLocation();
    
    814
    -      if (value === "automatic") {
    
    815
    -        this.beginAutoBootstrap();
    
    816
    -      } else {
    
    817
    -        this.beginAutoBootstrap(value);
    
    739
    +      if (value) {
    
    740
    +        this.beginAutoBootstrapping(value);
    
    818 741
           }
    
    819 742
         });
    
    820 743
     
    
    ... ... @@ -846,17 +769,14 @@ class AboutTorConnect {
    846 769
     
    
    847 770
       initObservers() {
    
    848 771
         // TorConnectParent feeds us state blobs to we use to update our UI
    
    849
    -    RPMAddMessageListener("torconnect:state-change", ({ data }) => {
    
    850
    -      this.updateUI(data);
    
    772
    +    RPMAddMessageListener("torconnect:stage-change", ({ data }) => {
    
    773
    +      this.updateStage(data);
    
    851 774
         });
    
    852
    -    RPMAddMessageListener("torconnect:user-action", ({ data }) => {
    
    853
    -      if (data.location) {
    
    854
    -        this.uiState.selectedLocation = data.location;
    
    855
    -        this.setLocation();
    
    856
    -      }
    
    857
    -      if (data.uiState !== undefined) {
    
    858
    -        this.transitionUIState(data.uiState, data.connState);
    
    859
    -      }
    
    775
    +    RPMAddMessageListener("torconnect:bootstrap-progress", ({ data }) => {
    
    776
    +      this.updateBootstrappingStatus(data);
    
    777
    +    });
    
    778
    +    RPMAddMessageListener("torconnect:quickstart-change", ({ data }) => {
    
    779
    +      this.updateQuickstart(data);
    
    860 780
         });
    
    861 781
       }
    
    862 782
     
    
    ... ... @@ -866,7 +786,7 @@ class AboutTorConnect {
    866 786
           // integers, so we must resort to a string compare here :(
    
    867 787
           // see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for relevant documentation
    
    868 788
           if (evt.code === "Escape") {
    
    869
    -        this.cancelBootstrap();
    
    789
    +        this.cancelBootstrapping();
    
    870 790
           }
    
    871 791
         };
    
    872 792
       }
    
    ... ... @@ -876,23 +796,14 @@ class AboutTorConnect {
    876 796
     
    
    877 797
         // various constants
    
    878 798
         TorStrings = Object.freeze(args.TorStrings);
    
    879
    -    TorConnectState = Object.freeze(args.TorConnectState);
    
    880
    -    InternetStatus = Object.freeze(args.InternetStatus);
    
    881 799
         this.locations = args.CountryNames;
    
    882 800
     
    
    883 801
         this.initElements(args.Direction);
    
    884 802
         this.initObservers();
    
    885 803
         this.initKeyboardShortcuts();
    
    886 804
     
    
    887
    -    if (Object.keys(args.State.UIState).length) {
    
    888
    -      this.uiState = args.State.UIState;
    
    889
    -    } else {
    
    890
    -      args.State.UIState = this.uiState;
    
    891
    -      this.saveUIState();
    
    892
    -    }
    
    893
    -    this.uiStates[this.uiState.currentState](args.State);
    
    894
    -    // populate UI based on current state
    
    895
    -    this.updateUI(args.State);
    
    805
    +    this.updateStage(args.stage);
    
    806
    +    this.updateQuickstart(args.quickstartEnabled);
    
    896 807
       }
    
    897 808
     }
    
    898 809
     
    

  • toolkit/components/torconnect/content/torConnectTitlebarStatus.js
    ... ... @@ -38,7 +38,7 @@ var gTorConnectTitlebarStatus = {
    38 38
         // The title also acts as an accessible name for the role="status".
    
    39 39
         this.node.setAttribute("title", this._strings.titlebarStatusName);
    
    40 40
     
    
    41
    -    this._observeTopic = TorConnectTopics.StateChange;
    
    41
    +    this._observeTopic = TorConnectTopics.StageChange;
    
    42 42
         this._stateListener = {
    
    43 43
           observe: (subject, topic) => {
    
    44 44
             if (topic !== this._observeTopic) {
    
    ... ... @@ -66,17 +66,16 @@ var gTorConnectTitlebarStatus = {
    66 66
         let textId;
    
    67 67
         let connected = false;
    
    68 68
         let potentiallyBlocked = false;
    
    69
    -    switch (TorConnect.state) {
    
    70
    -      case TorConnectState.Disabled:
    
    69
    +    switch (TorConnect.stageName) {
    
    70
    +      case TorConnectStage.Disabled:
    
    71 71
             // Hide immediately.
    
    72 72
             this.node.hidden = true;
    
    73 73
             return;
    
    74
    -      case TorConnectState.Bootstrapped:
    
    74
    +      case TorConnectStage.Bootstrapped:
    
    75 75
             textId = "titlebarStatusConnected";
    
    76 76
             connected = true;
    
    77 77
             break;
    
    78
    -      case TorConnectState.Bootstrapping:
    
    79
    -      case TorConnectState.AutoBootstrapping:
    
    78
    +      case TorConnectStage.Bootstrapping:
    
    80 79
             textId = "titlebarStatusConnecting";
    
    81 80
             break;
    
    82 81
           default:
    

  • toolkit/components/torconnect/content/torConnectUrlbarButton.js
    ... ... @@ -55,13 +55,13 @@ var gTorConnectUrlbarButton = {
    55 55
           this.connect();
    
    56 56
         });
    
    57 57
     
    
    58
    -    this._observeTopic = TorConnectTopics.StateChange;
    
    58
    +    this._observeTopic = TorConnectTopics.StageChange;
    
    59 59
         this._stateListener = {
    
    60 60
           observe: (subject, topic) => {
    
    61 61
             if (topic !== this._observeTopic) {
    
    62 62
               return;
    
    63 63
             }
    
    64
    -        this._torConnectStateChanged();
    
    64
    +        this._torConnectStageChanged();
    
    65 65
           },
    
    66 66
         };
    
    67 67
         Services.obs.addObserver(this._stateListener, this._observeTopic);
    
    ... ... @@ -84,7 +84,7 @@ var gTorConnectUrlbarButton = {
    84 84
         // switching selected browser.
    
    85 85
         gBrowser.addProgressListener(this._locationListener);
    
    86 86
     
    
    87
    -    this._torConnectStateChanged();
    
    87
    +    this._torConnectStageChanged();
    
    88 88
       },
    
    89 89
     
    
    90 90
       /**
    
    ... ... @@ -105,17 +105,17 @@ var gTorConnectUrlbarButton = {
    105 105
        * Begin the tor connection bootstrapping process.
    
    106 106
        */
    
    107 107
       connect() {
    
    108
    -    TorConnect.openTorConnect({ beginBootstrap: true });
    
    108
    +    TorConnect.openTorConnect({ beginBootstrapping: "soft" });
    
    109 109
       },
    
    110 110
     
    
    111 111
       /**
    
    112
    -   * Callback for when the TorConnect state changes.
    
    112
    +   * Callback for when the TorConnect stage changes.
    
    113 113
        */
    
    114
    -  _torConnectStateChanged() {
    
    115
    -    if (TorConnect.state === TorConnectState.Disabled) {
    
    114
    +  _torConnectStageChanged() {
    
    115
    +    if (TorConnect.stageName === TorConnectStage.Disabled) {
    
    116 116
           // NOTE: We do not uninit early when we reach the
    
    117
    -      // TorConnectState.Bootstrapped state because we can still leave the
    
    118
    -      // Bootstrapped state if the tor process exists early and needs a restart.
    
    117
    +      // TorConnectStage.Bootstrapped stage because we can still leave the
    
    118
    +      // Bootstrapped stage if the tor process exists early and needs a restart.
    
    119 119
           this.uninit();
    
    120 120
           return;
    
    121 121
         }
    

  • toolkit/modules/RemotePageAccessManager.sys.mjs
    ... ... @@ -239,19 +239,19 @@ export let RemotePageAccessManager = {
    239 239
         },
    
    240 240
         "about:torconnect": {
    
    241 241
           RPMAddMessageListener: [
    
    242
    -        "torconnect:state-change",
    
    243
    -        "torconnect:user-action",
    
    242
    +        "torconnect:stage-change",
    
    243
    +        "torconnect:bootstrap-progress",
    
    244
    +        "torconnect:quickstart-change",
    
    244 245
           ],
    
    245 246
           RPMSendAsyncMessage: [
    
    246 247
             "torconnect:open-tor-preferences",
    
    247
    -        "torconnect:begin-bootstrap",
    
    248
    -        "torconnect:begin-autobootstrap",
    
    249
    -        "torconnect:cancel-bootstrap",
    
    248
    +        "torconnect:begin-bootstrapping",
    
    249
    +        "torconnect:cancel-bootstrapping",
    
    250 250
             "torconnect:set-quickstart",
    
    251 251
             "torconnect:view-tor-logs",
    
    252 252
             "torconnect:restart",
    
    253
    -        "torconnect:set-ui-state",
    
    254
    -        "torconnect:broadcast-user-action",
    
    253
    +        "torconnect:start-again",
    
    254
    +        "torconnect:choose-region",
    
    255 255
           ],
    
    256 256
           RPMSendQuery: [
    
    257 257
             "torconnect:get-init-args",
    

  • toolkit/modules/TorAndroidIntegration.sys.mjs
    ... ... @@ -83,6 +83,7 @@ class TorAndroidIntegrationImpl {
    83 83
     
    
    84 84
       observe(subj, topic) {
    
    85 85
         switch (topic) {
    
    86
    +      // TODO: Replace with StageChange.
    
    86 87
           case lazy.TorConnectTopics.StateChange:
    
    87 88
             lazy.EventDispatcher.instance.sendRequest({
    
    88 89
               type: EmittedEvents.connectStateChanged,
    
    ... ... @@ -101,6 +102,7 @@ class TorAndroidIntegrationImpl {
    101 102
               type: EmittedEvents.bootstrapComplete,
    
    102 103
             });
    
    103 104
             break;
    
    105
    +      // TODO: Replace with StageChange stage.error.
    
    104 106
           case lazy.TorConnectTopics.Error:
    
    105 107
             lazy.EventDispatcher.instance.sendRequest({
    
    106 108
               type: EmittedEvents.connectError,
    
    ... ... @@ -159,17 +161,23 @@ class TorAndroidIntegrationImpl {
    159 161
               await lazy.TorSettings.saveToPrefs();
    
    160 162
               break;
    
    161 163
             case ListenedEvents.bootstrapBegin:
    
    162
    -          lazy.TorConnect.beginBootstrap();
    
    164
    +          lazy.TorConnect.beginBootstrapping();
    
    163 165
               break;
    
    164 166
             case ListenedEvents.bootstrapBeginAuto:
    
    165
    -          lazy.TorConnect.beginAutoBootstrap(data.countryCode);
    
    167
    +          // TODO: The countryCode should be set to "automatic" by the caller
    
    168
    +          // rather than `null`, so we can just pass in `data.countryCode`
    
    169
    +          // directly.
    
    170
    +          lazy.TorConnect.beginBootstrapping(data.countryCode || "automatic");
    
    166 171
               break;
    
    167 172
             case ListenedEvents.bootstrapCancel:
    
    168
    -          lazy.TorConnect.cancelBootstrap();
    
    173
    +          lazy.TorConnect.cancelBootstrapping();
    
    169 174
               break;
    
    175
    +        // TODO: Replace with TorConnect.stage.
    
    170 176
             case ListenedEvents.bootstrapGetState:
    
    171 177
               callback?.onSuccess(lazy.TorConnect.state);
    
    172 178
               return;
    
    179
    +        // TODO: Expose TorConnect.startAgain() to allow users to begin
    
    180
    +        // from the start again.
    
    173 181
           }
    
    174 182
           callback?.onSuccess();
    
    175 183
         } catch (e) {
    

  • toolkit/modules/TorConnect.sys.mjs
    ... ... @@ -8,7 +8,6 @@ const lazy = {};
    8 8
     
    
    9 9
     ChromeUtils.defineESModuleGetters(lazy, {
    
    10 10
       BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
    
    11
    -  EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
    
    12 11
       MoatRPC: "resource://gre/modules/Moat.sys.mjs",
    
    13 12
       TorBootstrapRequest: "resource://gre/modules/TorBootstrapRequest.sys.mjs",
    
    14 13
       TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
    
    ... ... @@ -79,240 +78,181 @@ ChromeUtils.defineLazyGetter(lazy, "logger", () =>
    79 78
       })
    
    80 79
     );
    
    81 80
     
    
    82
    -/*
    
    83
    -                             TorConnect State Transitions
    
    84
    -
    
    85
    -    ┌─────────┐                                                       ┌────────┐
    
    86
    -    │         ▼                                                       ▼        │
    
    87
    -    │       ┌──────────────────────────────────────────────────────────┐       │
    
    88
    -  ┌─┼────── │                           Error                          │ ◀───┐ │
    
    89
    -  │ │       └──────────────────────────────────────────────────────────┘     │ │
    
    90
    -  │ │         ▲                                                              │ │
    
    91
    -  │ │         │                                                              │ │
    
    92
    -  │ │         │                                                              │ │
    
    93
    -  │ │       ┌───────────────────────┐                       ┌──────────┐     │ │
    
    94
    -  │ │ ┌──── │        Initial        │ ────────────────────▶ │ Disabled │     │ │
    
    95
    -  │ │ │     └───────────────────────┘                       └──────────┘     │ │
    
    96
    -  │ │ │       │                                                              │ │
    
    97
    -  │ │ │       │ beginBootstrap()                                             │ │
    
    98
    -  │ │ │       ▼                                                              │ │
    
    99
    -  │ │ │     ┌──────────────────────────────────────────────────────────┐     │ │
    
    100
    -  │ │ │     │                      Bootstrapping                       │ ────┘ │
    
    101
    -  │ │ │     └──────────────────────────────────────────────────────────┘       │
    
    102
    -  │ │ │       │                        ▲                             │         │
    
    103
    -  │ │ │       │ cancelBootstrap()      │ beginBootstrap()            └────┐    │
    
    104
    -  │ │ │       ▼                        │                                  │    │
    
    105
    -  │ │ │     ┌──────────────────────────────────────────────────────────┐  │    │
    
    106
    -  │ │ └───▶ │                                                          │ ─┼────┘
    
    107
    -  │ │       │                                                          │  │
    
    108
    -  │ │       │                                                          │  │
    
    109
    -  │ │       │                       Configuring                        │  │
    
    110
    -  │ │       │                                                          │  │
    
    111
    -  │ │       │                                                          │  │
    
    112
    -  └─┼─────▶ │                                                          │  │
    
    113
    -    │       └──────────────────────────────────────────────────────────┘  │
    
    114
    -    │         │                        ▲                       ▲          │
    
    115
    -    │         │ beginAutoBootstrap()   │ cancelBootstrap()     │          │
    
    116
    -    │         ▼                        │                       │          │
    
    117
    -    │       ┌───────────────────────┐  │                       │          │
    
    118
    -    └────── │   AutoBootstrapping   │ ─┘                       │          │
    
    119
    -            └───────────────────────┘                          │          │
    
    120
    -              │                                                │          │
    
    121
    -              │               ┌────────────────────────────────┘          │
    
    122
    -              ▼               │                                           │
    
    123
    -            ┌───────────────────────┐                                     │
    
    124
    -            │     Bootstrapped      │ ◀───────────────────────────────────┘
    
    125
    -            └───────────────────────┘
    
    126
    -*/
    
    127
    -
    
    128 81
     /* Topics Notified by the TorConnect module */
    
    129 82
     export const TorConnectTopics = Object.freeze({
    
    83
    +  StageChange: "torconnect:stage-change",
    
    84
    +  // TODO: Remove torconnect:state-change when pages have switched to stage.
    
    130 85
       StateChange: "torconnect:state-change",
    
    131 86
       BootstrapProgress: "torconnect:bootstrap-progress",
    
    132 87
       BootstrapComplete: "torconnect:bootstrap-complete",
    
    88
    +  // TODO: Remove torconnect:error when pages have switched to stage.
    
    133 89
       Error: "torconnect:error",
    
    134 90
     });
    
    135 91
     
    
    136
    -// The StateCallback is the base class to implement the various states.
    
    137
    -// All states should extend it and implement a `run` function, which can
    
    138
    -// optionally be async, and define an array of valid transitions.
    
    139
    -// The parent class will handle everything else, including the transition to
    
    140
    -// other states when the run function is complete etc...
    
    141
    -// A system is also provided to allow this function to early-out. The runner
    
    142
    -// should check the transitioning getter when appropriate and return.
    
    143
    -// In addition to that, a state can implement a transitionRequested callback,
    
    144
    -// which can be used in conjunction with a mechanism like Promise.race.
    
    145
    -// This allows to handle, for example, users' requests to cancel a bootstrap
    
    146
    -// attempt.
    
    147
    -// A state can optionally define a cleanup function, that will be run in all
    
    148
    -// cases before transitioning to the next state.
    
    149
    -class StateCallback {
    
    150
    -  #state;
    
    151
    -  #promise;
    
    152
    -  #transitioning = false;
    
    153
    -
    
    154
    -  constructor(stateName) {
    
    155
    -    this.#state = stateName;
    
    156
    -  }
    
    157
    -
    
    158
    -  async begin(...args) {
    
    159
    -    lazy.logger.trace(`Entering ${this.#state} state`);
    
    160
    -    // Make sure we always have an actual promise.
    
    161
    -    try {
    
    162
    -      this.#promise = Promise.resolve(this.run(...args));
    
    163
    -    } catch (err) {
    
    164
    -      this.#promise = Promise.reject(err);
    
    165
    -    }
    
    166
    -    try {
    
    167
    -      // If the callback throws, transition to error as soon as possible.
    
    168
    -      await this.#promise;
    
    169
    -      lazy.logger.info(`${this.#state}'s run is done`);
    
    170
    -    } catch (err) {
    
    171
    -      if (this.transitioning) {
    
    172
    -        lazy.logger.error(
    
    173
    -          `A transition from ${
    
    174
    -            this.#state
    
    175
    -          } is already happening, silencing this exception.`,
    
    176
    -          err
    
    177
    -        );
    
    178
    -        return;
    
    179
    -      }
    
    180
    -      lazy.logger.error(
    
    181
    -        `${this.#state}'s run threw, transitioning to the Error state.`,
    
    182
    -        err
    
    183
    -      );
    
    184
    -      this.changeState(TorConnectState.Error, err);
    
    185
    -    }
    
    186
    -  }
    
    187
    -
    
    188
    -  async end(nextState) {
    
    189
    -    lazy.logger.trace(
    
    190
    -      `Ending state ${this.#state} (to transition to ${nextState})`
    
    191
    -    );
    
    192
    -
    
    193
    -    if (this.#transitioning) {
    
    194
    -      // Should we check turn this into an error?
    
    195
    -      // It will make dealing with the error state harder.
    
    196
    -      lazy.logger.warn("this.#transitioning is already true.");
    
    197
    -    }
    
    198
    -
    
    199
    -    // Signal we should bail out ASAP.
    
    200
    -    this.#transitioning = true;
    
    201
    -    if (this.transitionRequested) {
    
    202
    -      this.transitionRequested();
    
    203
    -    }
    
    204
    -
    
    205
    -    lazy.logger.debug(
    
    206
    -      `Waiting for the ${
    
    207
    -        this.#state
    
    208
    -      }'s callback to return before the transition.`
    
    209
    -    );
    
    210
    -    try {
    
    211
    -      await this.#promise;
    
    212
    -    } finally {
    
    213
    -      lazy.logger.debug(`Calling ${this.#state}'s cleanup, if implemented.`);
    
    214
    -      if (this.cleanup) {
    
    215
    -        try {
    
    216
    -          await this.cleanup(nextState);
    
    217
    -          lazy.logger.debug(`${this.#state}'s cleanup function done.`);
    
    218
    -        } catch (e) {
    
    219
    -          lazy.logger.warn(`${this.#state}'s cleanup function threw.`, e);
    
    220
    -        }
    
    221
    -      }
    
    222
    -    }
    
    223
    -  }
    
    224
    -
    
    225
    -  changeState(stateName, ...args) {
    
    226
    -    TorConnect._changeState(stateName, ...args);
    
    227
    -  }
    
    228
    -
    
    229
    -  get transitioning() {
    
    230
    -    return this.#transitioning;
    
    231
    -  }
    
    232
    -
    
    233
    -  get state() {
    
    234
    -    return this.#state;
    
    235
    -  }
    
    236
    -}
    
    237
    -
    
    238
    -// async method to sleep for a given amount of time
    
    239
    -const debugSleep = async ms => {
    
    240
    -  return new Promise(resolve => {
    
    241
    -    setTimeout(resolve, ms);
    
    242
    -  });
    
    243
    -};
    
    244
    -
    
    245
    -class InitialState extends StateCallback {
    
    246
    -  allowedTransitions = Object.freeze([
    
    247
    -    TorConnectState.Disabled,
    
    248
    -    TorConnectState.Bootstrapping,
    
    249
    -    TorConnectState.Configuring,
    
    250
    -    TorConnectState.Error,
    
    251
    -  ]);
    
    252
    -
    
    253
    -  constructor() {
    
    254
    -    super(TorConnectState.Initial);
    
    255
    -  }
    
    256
    -
    
    257
    -  run() {
    
    258
    -    // TODO: Block this transition until we successfully build a TorProvider.
    
    259
    -  }
    
    260
    -}
    
    261
    -
    
    262
    -class ConfiguringState extends StateCallback {
    
    263
    -  allowedTransitions = Object.freeze([
    
    264
    -    TorConnectState.AutoBootstrapping,
    
    265
    -    TorConnectState.Bootstrapping,
    
    266
    -    TorConnectState.Error,
    
    267
    -  ]);
    
    268
    -
    
    269
    -  constructor() {
    
    270
    -    super(TorConnectState.Configuring);
    
    271
    -  }
    
    272
    -
    
    273
    -  run() {
    
    274
    -    TorConnect._bootstrapProgress = 0;
    
    275
    -  }
    
    276
    -}
    
    277
    -
    
    278
    -class BootstrappingState extends StateCallback {
    
    92
    +/**
    
    93
    + * @callback ProgressCallback
    
    94
    + *
    
    95
    + * @param {integer} progress - The progress percent.
    
    96
    + */
    
    97
    +/**
    
    98
    + * @typedef {object} BootstrapOptions
    
    99
    + *
    
    100
    + * Options for a bootstrap attempt.
    
    101
    + *
    
    102
    + * @property {boolean} [options.simulateCensorship] - Whether to simulate a
    
    103
    + *   failing bootstrap.
    
    104
    + * @property {integer} [options.simulateDelay] - The delay in microseconds to
    
    105
    + *   apply to simulated bootstraps.
    
    106
    + * @property {object} [options.simulateMoatResponse] - Simulate a Moat response
    
    107
    + *   for circumvention settings. Should include a "settings" property, and
    
    108
    + *   optionally a "country" property. You may add a "simulateCensorship"
    
    109
    + *   property to some of the settings to make only their bootstrap attempts
    
    110
    + *   fail.
    
    111
    + * @property {boolean} [options.testInternet] - Whether to also test the
    
    112
    + *   internet connection.
    
    113
    + * @property {boolean} [options.simulateOffline] - Whether to simulate an
    
    114
    + *   offline test result. This will not cause the bootstrap to fail.
    
    115
    + * @property {string} [options.regionCode] - The region code to use to fetch
    
    116
    + *   auto-bootstrap settings, or "automatic" to automatically choose the region.
    
    117
    + */
    
    118
    +/**
    
    119
    + * @typedef {object} BootstrapResult
    
    120
    + *
    
    121
    + * The result of a bootstrap attempt.
    
    122
    + *
    
    123
    + * @property {string} [result] - The bootstrap result.
    
    124
    + * @property {Error} [error] - An error from the attempt.
    
    125
    + */
    
    126
    +/**
    
    127
    + * @callback ResolveBootstrap
    
    128
    + *
    
    129
    + * Resolve a bootstrap attempt.
    
    130
    + *
    
    131
    + * @param {BootstrapResult} - The result, or error.
    
    132
    + */
    
    133
    +
    
    134
    +/**
    
    135
    + * Each instance can be used to attempt one bootstrapping.
    
    136
    + */
    
    137
    +class BootstrapAttempt {
    
    138
    +  /**
    
    139
    +   * The ongoing bootstrap request.
    
    140
    +   *
    
    141
    +   * @type {?TorBootstrapRequest}
    
    142
    +   */
    
    279 143
       #bootstrap = null;
    
    144
    +  /**
    
    145
    +   * The error returned by the bootstrap request, if any.
    
    146
    +   *
    
    147
    +   * @type {?Error}
    
    148
    +   */
    
    280 149
       #bootstrapError = null;
    
    150
    +  /**
    
    151
    +   * The ongoing internet test, if any.
    
    152
    +   *
    
    153
    +   * @type {?InternetTest}
    
    154
    +   */
    
    281 155
       #internetTest = null;
    
    156
    +  /**
    
    157
    +   * The method to call to complete the `run` promise.
    
    158
    +   *
    
    159
    +   * @type {?ResolveBootstrap}
    
    160
    +   */
    
    161
    +  #resolveRun = null;
    
    162
    +  /**
    
    163
    +   * Whether the `run` promise has been, or is about to be, resolved.
    
    164
    +   *
    
    165
    +   * @type {boolean}
    
    166
    +   */
    
    167
    +  #resolved = false;
    
    168
    +  /**
    
    169
    +   * Whether a cancel request has been started.
    
    170
    +   *
    
    171
    +   * @type {boolean}
    
    172
    +   */
    
    282 173
       #cancelled = false;
    
    283 174
     
    
    284
    -  allowedTransitions = Object.freeze([
    
    285
    -    TorConnectState.Configuring,
    
    286
    -    TorConnectState.Bootstrapped,
    
    287
    -    TorConnectState.Error,
    
    288
    -  ]);
    
    175
    +  /**
    
    176
    +   * Run a bootstrap attempt.
    
    177
    +   *
    
    178
    +   * @param {ProgressCallback} progressCallback - The callback to invoke with
    
    179
    +   *   the bootstrap progress.
    
    180
    +   * @param {BootstrapOptions} options - Options to apply to the bootstrap.
    
    181
    +   *
    
    182
    +   * @return {Promise<string, Error>} - The result of the bootstrap.
    
    183
    +   */
    
    184
    +  run(progressCallback, options) {
    
    185
    +    const { promise, resolve, reject } = Promise.withResolvers();
    
    186
    +    this.#resolveRun = arg => {
    
    187
    +      if (this.#resolved) {
    
    188
    +        // Already been called once.
    
    189
    +        if (arg.error) {
    
    190
    +          lazy.logger.error("Delayed bootstrap error", arg.error);
    
    191
    +        }
    
    192
    +        return;
    
    193
    +      }
    
    194
    +      this.#resolved = true;
    
    195
    +      try {
    
    196
    +        // Should be ok to call this twice in the case where we "cancel" the
    
    197
    +        // bootstrap.
    
    198
    +        this.#internetTest?.cancel();
    
    199
    +      } catch (error) {
    
    200
    +        lazy.logger.error("Unexpected error in bootstrap cleanup", error);
    
    201
    +      }
    
    202
    +      if (arg.error) {
    
    203
    +        reject(arg.error);
    
    204
    +      } else {
    
    205
    +        resolve(arg.result);
    
    206
    +      }
    
    207
    +    };
    
    208
    +    try {
    
    209
    +      this.#runInternal(progressCallback, options);
    
    210
    +    } catch (error) {
    
    211
    +      this.#resolveRun({ error });
    
    212
    +    }
    
    289 213
     
    
    290
    -  constructor() {
    
    291
    -    super(TorConnectState.Bootstrapping);
    
    214
    +    return promise;
    
    292 215
       }
    
    293 216
     
    
    294
    -  async run() {
    
    295
    -    if (await this.#simulateCensorship()) {
    
    296
    -      return;
    
    217
    +  /**
    
    218
    +   * Run the attempt.
    
    219
    +   *
    
    220
    +   * @param {ProgressCallback} progressCallback - The callback to invoke with
    
    221
    +   *   the bootstrap progress.
    
    222
    +   * @param {BootstrapOptions} options - Options to apply to the bootstrap.
    
    223
    +   */
    
    224
    +  #runInternal(progressCallback, options) {
    
    225
    +    if (options.simulateCensorship) {
    
    226
    +      // Create a fake request.
    
    227
    +      this.#bootstrap = {
    
    228
    +        _timeout: 0,
    
    229
    +        bootstrap() {
    
    230
    +          this._timeout = setTimeout(() => {
    
    231
    +            const err = new Error("Censorship simulation");
    
    232
    +            err.phase = "conn";
    
    233
    +            err.reason = "noroute";
    
    234
    +            this.onbootstraperror(err);
    
    235
    +          }, options.simulateDelay || 0);
    
    236
    +        },
    
    237
    +        cancel() {
    
    238
    +          clearTimeout(this._timeout);
    
    239
    +        },
    
    240
    +      };
    
    241
    +    } else {
    
    242
    +      this.#bootstrap = new lazy.TorBootstrapRequest();
    
    297 243
         }
    
    298 244
     
    
    299
    -    this.#bootstrap = new lazy.TorBootstrapRequest();
    
    300
    -    this.#bootstrap.onbootstrapstatus = (progress, status) => {
    
    301
    -      TorConnect._updateBootstrapProgress(progress, status);
    
    245
    +    this.#bootstrap.onbootstrapstatus = (progress, _status) => {
    
    246
    +      if (!this.#resolved) {
    
    247
    +        progressCallback(progress);
    
    248
    +      }
    
    302 249
         };
    
    303 250
         this.#bootstrap.onbootstrapcomplete = () => {
    
    304
    -      this.#internetTest.cancel();
    
    305
    -      this.changeState(TorConnectState.Bootstrapped);
    
    251
    +      this.#resolveRun({ result: "complete" });
    
    306 252
         };
    
    307 253
         this.#bootstrap.onbootstraperror = error => {
    
    308
    -      if (this.#cancelled) {
    
    309
    -        // We ignore this error since it occurred after cancelling (by the
    
    310
    -        // user). We assume the error is just a side effect of the cancelling.
    
    311
    -        // E.g. If the cancelling is triggered late in the process, we get
    
    312
    -        // "Building circuits: Establishing a Tor circuit failed".
    
    313
    -        // TODO: Maybe move this logic deeper in the process to know when to
    
    314
    -        // filter out such errors triggered by cancelling.
    
    315
    -        lazy.logger.warn("Post-cancel error.", error);
    
    254
    +      if (this.#bootstrapError) {
    
    255
    +        lazy.logger.warn("Another bootstrap error", error);
    
    316 256
             return;
    
    317 257
           }
    
    318 258
           // We have to wait for the Internet test to finish before sending the
    
    ... ... @@ -320,30 +260,40 @@ class BootstrappingState extends StateCallback {
    320 260
           this.#bootstrapError = error;
    
    321 261
           this.#maybeTransitionToError();
    
    322 262
         };
    
    323
    -
    
    324
    -    this.#internetTest = new InternetTest();
    
    325
    -    this.#internetTest.onResult = status => {
    
    326
    -      TorConnect._internetStatus = status;
    
    327
    -      this.#maybeTransitionToError();
    
    328
    -    };
    
    329
    -    this.#internetTest.onError = () => {
    
    330
    -      this.#maybeTransitionToError();
    
    331
    -    };
    
    263
    +    if (options.testInternet) {
    
    264
    +      this.#internetTest = new InternetTest(options.simulateOffline);
    
    265
    +      this.#internetTest.onResult = () => {
    
    266
    +        this.#maybeTransitionToError();
    
    267
    +      };
    
    268
    +      this.#internetTest.onError = () => {
    
    269
    +        this.#maybeTransitionToError();
    
    270
    +      };
    
    271
    +    }
    
    332 272
     
    
    333 273
         this.#bootstrap.bootstrap();
    
    334 274
       }
    
    335 275
     
    
    336
    -  async cleanup(nextState) {
    
    337
    -    if (nextState === TorConnectState.Configuring) {
    
    338
    -      // stop bootstrap process if user cancelled
    
    339
    -      this.#cancelled = true;
    
    340
    -      this.#internetTest?.cancel();
    
    341
    -      await this.#bootstrap?.cancel();
    
    276
    +  /**
    
    277
    +   * Callback for when we get a new bootstrap error or a change in the internet
    
    278
    +   * status.
    
    279
    +   */
    
    280
    +  #maybeTransitionToError() {
    
    281
    +    if (this.#resolved || this.#cancelled) {
    
    282
    +      if (this.#bootstrapError) {
    
    283
    +        // We ignore this error since it occurred after cancelling (by the
    
    284
    +        // user), or we have already resolved. We assume the error is just a
    
    285
    +        // side effect of the cancelling.
    
    286
    +        // E.g. If the cancelling is triggered late in the process, we get
    
    287
    +        // "Building circuits: Establishing a Tor circuit failed".
    
    288
    +        // TODO: Maybe move this logic deeper in the process to know when to
    
    289
    +        // filter out such errors triggered by cancelling.
    
    290
    +        lazy.logger.warn("Post-complete error.", this.#bootstrapError);
    
    291
    +      }
    
    292
    +      return;
    
    342 293
         }
    
    343
    -  }
    
    344 294
     
    
    345
    -  #maybeTransitionToError() {
    
    346 295
         if (
    
    296
    +      this.#internetTest &&
    
    347 297
           this.#internetTest.status === InternetStatus.Unknown &&
    
    348 298
           this.#internetTest.error === null &&
    
    349 299
           this.#internetTest.enabled
    
    ... ... @@ -355,356 +305,394 @@ class BootstrappingState extends StateCallback {
    355 305
           // us again.
    
    356 306
           return;
    
    357 307
         }
    
    358
    -    // Do not transition to the offline error until we are sure that also the
    
    359
    -    // bootstrap failed, in case Moat is down but the bootstrap can proceed
    
    360
    -    // anyway.
    
    308
    +    // Do not transition to "offline" until we are sure that also the bootstrap
    
    309
    +    // failed, in case Moat is down but the bootstrap can proceed anyway.
    
    361 310
         if (!this.#bootstrapError) {
    
    362 311
           return;
    
    363 312
         }
    
    364
    -    if (this.#internetTest.status === InternetStatus.Offline) {
    
    365
    -      this.changeState(
    
    366
    -        TorConnectState.Error,
    
    367
    -        new TorConnectError(TorConnectError.Offline)
    
    368
    -      );
    
    369
    -    } else {
    
    370
    -      // Give priority to the bootstrap error, in case the Internet test fails
    
    371
    -      TorConnect._hasBootstrapEverFailed = true;
    
    372
    -      this.changeState(
    
    373
    -        TorConnectState.Error,
    
    374
    -        new TorConnectError(
    
    375
    -          TorConnectError.BootstrapError,
    
    313
    +    if (this.#internetTest?.status === InternetStatus.Offline) {
    
    314
    +      if (this.#bootstrapError) {
    
    315
    +        lazy.logger.info(
    
    316
    +          "Ignoring bootstrap error since offline.",
    
    376 317
               this.#bootstrapError
    
    377
    -        )
    
    378
    -      );
    
    379
    -    }
    
    380
    -  }
    
    381
    -
    
    382
    -  async #simulateCensorship() {
    
    383
    -    // debug hook to simulate censorship preventing bootstrapping
    
    384
    -    const censorshipLevel = Services.prefs.getIntPref(
    
    385
    -      TorConnectPrefs.censorship_level,
    
    386
    -      0
    
    387
    -    );
    
    388
    -    if (censorshipLevel <= 0) {
    
    389
    -      return false;
    
    390
    -    }
    
    391
    -
    
    392
    -    await debugSleep(1500);
    
    393
    -    if (this.transitioning) {
    
    394
    -      // Already left this state.
    
    395
    -      return true;
    
    318
    +        );
    
    319
    +      }
    
    320
    +      this.#resolveRun({ result: "offline" });
    
    321
    +      return;
    
    396 322
         }
    
    397
    -    TorConnect._hasBootstrapEverFailed = true;
    
    398
    -    if (censorshipLevel === 2) {
    
    399
    -      const codes = Object.keys(TorConnect._countryNames);
    
    400
    -      TorConnect._detectedLocation =
    
    401
    -        codes[Math.floor(Math.random() * codes.length)];
    
    402
    -    }
    
    403
    -    const err = new Error("Censorship simulation");
    
    404
    -    err.phase = "conn";
    
    405
    -    err.reason = "noroute";
    
    406
    -    this.changeState(
    
    407
    -      TorConnectState.Error,
    
    408
    -      new TorConnectError(TorConnectError.BootstrapError, err)
    
    409
    -    );
    
    410
    -    return true;
    
    411
    -  }
    
    412
    -}
    
    413
    -
    
    414
    -class AutoBootstrappingState extends StateCallback {
    
    415
    -  #moat;
    
    416
    -  #settings;
    
    417
    -  #changedSettings = false;
    
    418
    -  #transitionPromise;
    
    419
    -  #transitionResolve;
    
    420
    -
    
    421
    -  allowedTransitions = Object.freeze([
    
    422
    -    TorConnectState.Configuring,
    
    423
    -    TorConnectState.Bootstrapped,
    
    424
    -    TorConnectState.Error,
    
    425
    -  ]);
    
    426
    -
    
    427
    -  constructor() {
    
    428
    -    super(TorConnectState.AutoBootstrapping);
    
    429
    -    this.#transitionPromise = new Promise(resolve => {
    
    430
    -      this.#transitionResolve = resolve;
    
    323
    +    this.#resolveRun({
    
    324
    +      error: new TorConnectError(
    
    325
    +        TorConnectError.BootstrapError,
    
    326
    +        this.#bootstrapError
    
    327
    +      ),
    
    431 328
         });
    
    432 329
       }
    
    433 330
     
    
    434
    -  async run(countryCode) {
    
    435
    -    if (await this.#simulateCensorship(countryCode)) {
    
    436
    -      return;
    
    437
    -    }
    
    438
    -    await this.#initMoat();
    
    439
    -    if (this.transitioning) {
    
    331
    +  /**
    
    332
    +   * Cancel the bootstrap attempt.
    
    333
    +   */
    
    334
    +  async cancel() {
    
    335
    +    if (this.#cancelled) {
    
    336
    +      lazy.logger.warn(
    
    337
    +        "Cancelled bootstrap after it has already been cancelled"
    
    338
    +      );
    
    440 339
           return;
    
    441 340
         }
    
    442
    -    await this.#fetchSettings(countryCode);
    
    443
    -    if (this.transitioning) {
    
    341
    +    this.#cancelled = true;
    
    342
    +    if (this.#resolved) {
    
    343
    +      lazy.logger.warn("Cancelled bootstrap after it has already resolved");
    
    444 344
           return;
    
    445 345
         }
    
    446
    -    await this.#trySettings();
    
    346
    +    // Wait until after bootstrap.cancel returns before we resolve with
    
    347
    +    // cancelled. In particular, there is a small chance that the bootstrap
    
    348
    +    // completes, in which case we want to be able to resolve with a success
    
    349
    +    // instead.
    
    350
    +    this.#internetTest?.cancel();
    
    351
    +    await this.#bootstrap?.cancel();
    
    352
    +    this.#resolveRun({ result: "cancelled" });
    
    447 353
       }
    
    354
    +}
    
    448 355
     
    
    356
    +/**
    
    357
    + * Each instance can be used to attempt one auto-bootstrapping sequence.
    
    358
    + */
    
    359
    +class AutoBootstrapAttempt {
    
    449 360
       /**
    
    450
    -   * Simulate a censorship event, if needed.
    
    361
    +   * The current bootstrap attempt, if any.
    
    451 362
        *
    
    452
    -   * @param {string} countryCode The country code passed to the state
    
    453
    -   * @returns {Promise<boolean>} true if we are simulating the censorship and
    
    454
    -   * the bootstrap should stop immediately, or false if the bootstrap should
    
    455
    -   * continue normally.
    
    363
    +   * @type {?BootstrapAttempt}
    
    456 364
        */
    
    457
    -  async #simulateCensorship(countryCode) {
    
    458
    -    const censorshipLevel = Services.prefs.getIntPref(
    
    459
    -      TorConnectPrefs.censorship_level,
    
    460
    -      0
    
    461
    -    );
    
    462
    -    if (censorshipLevel <= 0) {
    
    463
    -      return false;
    
    464
    -    }
    
    365
    +  #bootstrapAttempt = null;
    
    366
    +  /**
    
    367
    +   * The method to call to complete the `run` promise.
    
    368
    +   *
    
    369
    +   * @type {?ResolveBootstrap}
    
    370
    +   */
    
    371
    +  #resolveRun = null;
    
    372
    +  /**
    
    373
    +   * Whether the `run` promise has been, or is about to be, resolved.
    
    374
    +   *
    
    375
    +   * @type {boolean}
    
    376
    +   */
    
    377
    +  #resolved = false;
    
    378
    +  /**
    
    379
    +   * Whether a cancel request has been started.
    
    380
    +   *
    
    381
    +   * @type {boolean}
    
    382
    +   */
    
    383
    +  #cancelled = false;
    
    384
    +  /**
    
    385
    +   * The method to call when the cancelled value is set to true.
    
    386
    +   *
    
    387
    +   * @type {?Function}
    
    388
    +   */
    
    389
    +  #resolveCancelled = null;
    
    390
    +  /**
    
    391
    +   * A promise that resolves when the cancelled value is set to true. We can use
    
    392
    +   * this with Promise.race to end early when the user cancels.
    
    393
    +   *
    
    394
    +   * @type {?Promise}
    
    395
    +   */
    
    396
    +  #cancelledPromise = null;
    
    397
    +  /**
    
    398
    +   * The found settings from Moat.
    
    399
    +   *
    
    400
    +   * @type {?object[]}
    
    401
    +   */
    
    402
    +  #settings = null;
    
    403
    +  /**
    
    404
    +   * The last settings that have been applied to the TorProvider, if any.
    
    405
    +   *
    
    406
    +   * @type {?object}
    
    407
    +   */
    
    408
    +  #changedSetting = null;
    
    409
    +  /**
    
    410
    +   * The detected region code returned by Moat, if any.
    
    411
    +   *
    
    412
    +   * @type {?string}
    
    413
    +   */
    
    414
    +  detectedRegion = null;
    
    465 415
     
    
    466
    -    // Very severe censorship: always fail even after manually selecting
    
    467
    -    // location specific settings.
    
    468
    -    if (censorshipLevel === 3) {
    
    469
    -      await debugSleep(2500);
    
    470
    -      if (!this.transitioning) {
    
    471
    -        this.changeState(
    
    472
    -          TorConnectState.Error,
    
    473
    -          new TorConnectError(TorConnectError.AllSettingsFailed)
    
    474
    -        );
    
    416
    +  /**
    
    417
    +   * Run an auto-bootstrap attempt.
    
    418
    +   *
    
    419
    +   * @param {ProgressCallback} progressCallback - The callback to invoke with
    
    420
    +   *   the bootstrap progress.
    
    421
    +   * @param {BootstrapOptions} options - Options to apply to the bootstrap.
    
    422
    +   *
    
    423
    +   * @return {Promise<string, Error>} - The result of the bootstrap.
    
    424
    +   */
    
    425
    +  run(progressCallback, options) {
    
    426
    +    const { promise, resolve, reject } = Promise.withResolvers();
    
    427
    +
    
    428
    +    this.#resolveRun = async arg => {
    
    429
    +      if (this.#resolved) {
    
    430
    +        // Already been called once.
    
    431
    +        if (arg.error) {
    
    432
    +          lazy.logger.error("Delayed auto-bootstrap error", arg.error);
    
    433
    +        }
    
    434
    +        return;
    
    475 435
           }
    
    476
    -      return true;
    
    477
    -    }
    
    478
    -
    
    479
    -    // Severe censorship: only fail after auto selecting, but succeed after
    
    480
    -    // manually selecting a country.
    
    481
    -    if (censorshipLevel === 2 && !countryCode) {
    
    482
    -      await debugSleep(2500);
    
    483
    -      if (!this.transitioning) {
    
    484
    -        this.changeState(
    
    485
    -          TorConnectState.Error,
    
    486
    -          new TorConnectError(TorConnectError.CannotDetermineCountry)
    
    487
    -        );
    
    436
    +      this.#resolved = true;
    
    437
    +      try {
    
    438
    +        // Run cleanup before we resolve the promise to ensure two instances
    
    439
    +        // of AutoBootstrapAttempt are not trying to change the settings at
    
    440
    +        // the same time.
    
    441
    +        if (this.#changedSetting) {
    
    442
    +          if (arg.result === "complete") {
    
    443
    +            // Persist the current settings to preferences.
    
    444
    +            lazy.TorSettings.setSettings(this.#changedSetting);
    
    445
    +            lazy.TorSettings.saveToPrefs();
    
    446
    +          } // else, applySettings will restore the current settings.
    
    447
    +          await lazy.TorSettings.applySettings();
    
    448
    +        }
    
    449
    +      } catch (error) {
    
    450
    +        lazy.logger.error("Unexpected error in auto-bootstrap cleanup", error);
    
    488 451
           }
    
    489
    -      return true;
    
    490
    -    }
    
    452
    +      if (arg.error) {
    
    453
    +        reject(arg.error);
    
    454
    +      } else {
    
    455
    +        resolve(arg.result);
    
    456
    +      }
    
    457
    +    };
    
    491 458
     
    
    492
    -    return false;
    
    493
    -  }
    
    459
    +    ({ promise: this.#cancelledPromise, resolve: this.#resolveCancelled } =
    
    460
    +      Promise.withResolvers());
    
    494 461
     
    
    495
    -  /**
    
    496
    -   * Initialize the MoatRPC to communicate with the backend.
    
    497
    -   */
    
    498
    -  async #initMoat() {
    
    499
    -    this.#moat = new lazy.MoatRPC();
    
    500
    -    // We need to wait Moat's initialization even when we are requested to
    
    501
    -    // transition to another state to be sure its uninit will have its intended
    
    502
    -    // effect. So, do not use Promise.race here.
    
    503
    -    await this.#moat.init();
    
    462
    +    this.#runInternal(progressCallback, options).catch(error => {
    
    463
    +      this.#resolveRun({ error });
    
    464
    +    });
    
    465
    +
    
    466
    +    return promise;
    
    504 467
       }
    
    505 468
     
    
    506 469
       /**
    
    507
    -   * Lookup user's potential censorship circumvention settings from Moat
    
    508
    -   * service.
    
    470
    +   * Run the attempt.
    
    471
    +   *
    
    472
    +   * Note, this is an async method, but should *not* be awaited by the `run`
    
    473
    +   * method.
    
    474
    +   *
    
    475
    +   * @param {ProgressCallback} progressCallback - The callback to invoke with
    
    476
    +   *   the bootstrap progress.
    
    477
    +   * @param {BootstrapOptions} options - Options to apply to the bootstrap.
    
    509 478
        */
    
    510
    -  async #fetchSettings(countryCode) {
    
    511
    -    // For now, throw any errors we receive from the backend, except when it was
    
    512
    -    // unable to detect user's country/region.
    
    513
    -    // If we use specialized error objects, we could pass the original errors to
    
    514
    -    // them.
    
    515
    -    const maybeSettings = await Promise.race([
    
    516
    -      this.#moat.circumvention_settings(
    
    517
    -        [...lazy.TorSettings.builtinBridgeTypes, "vanilla"],
    
    518
    -        countryCode
    
    519
    -      ),
    
    520
    -      // This might set maybeSettings to undefined.
    
    521
    -      this.#transitionPromise,
    
    522
    -    ]);
    
    523
    -    if (maybeSettings?.country) {
    
    524
    -      TorConnect._detectedLocation = maybeSettings.country;
    
    525
    -    }
    
    526
    -
    
    527
    -    if (maybeSettings?.settings && maybeSettings.settings.length) {
    
    528
    -      this.#settings = maybeSettings.settings;
    
    529
    -    } else if (!this.transitioning) {
    
    530
    -      // Keep consistency with the other call.
    
    531
    -      this.#settings = await Promise.race([
    
    532
    -        this.#moat.circumvention_defaults([
    
    533
    -          ...lazy.TorSettings.builtinBridgeTypes,
    
    534
    -          "vanilla",
    
    535
    -        ]),
    
    536
    -        // This might set this.#settings to undefined.
    
    537
    -        this.#transitionPromise,
    
    538
    -      ]);
    
    479
    +  async #runInternal(progressCallback, options) {
    
    480
    +    await this.#fetchSettings(options);
    
    481
    +    if (this.#cancelled || this.#resolved) {
    
    482
    +      return;
    
    539 483
         }
    
    540 484
     
    
    541
    -    if (!this.#settings?.length && !this.transitioning) {
    
    542
    -      if (!TorConnect._detectedLocation) {
    
    543
    -        // unable to determine country
    
    544
    -        throw new TorConnectError(TorConnectError.CannotDetermineCountry);
    
    545
    -      } else {
    
    546
    -        // no settings available for country
    
    547
    -        throw new TorConnectError(TorConnectError.NoSettingsForCountry);
    
    548
    -      }
    
    485
    +    if (!this.#settings?.length) {
    
    486
    +      this.#resolveRun({
    
    487
    +        error: new TorConnectError(
    
    488
    +          options.regionCode === "automatic" && !this.detectedRegion
    
    489
    +            ? TorConnectError.CannotDetermineCountry
    
    490
    +            : TorConnectError.NoSettingsForCountry
    
    491
    +        ),
    
    492
    +      });
    
    549 493
         }
    
    550
    -  }
    
    551 494
     
    
    552
    -  /**
    
    553
    -   * Try to apply the settings we fetched.
    
    554
    -   */
    
    555
    -  async #trySettings() {
    
    556
    -    // Otherwise, apply each of our settings and try to bootstrap with each.
    
    495
    +    // Apply each of our settings and try to bootstrap with each.
    
    557 496
         for (const [index, currentSetting] of this.#settings.entries()) {
    
    558
    -      if (this.transitioning) {
    
    559
    -        break;
    
    560
    -      }
    
    561
    -
    
    562 497
           lazy.logger.info(
    
    563 498
             `Attempting Bootstrap with configuration ${index + 1}/${
    
    564 499
               this.#settings.length
    
    565 500
             }`
    
    566 501
           );
    
    567 502
     
    
    568
    -      // Send the new settings directly to the provider. We will save them only
    
    569
    -      // if the bootstrap succeeds.
    
    570
    -      // FIXME: We should somehow signal TorSettings users that we have set
    
    571
    -      // custom settings, and they should not apply theirs until we are done
    
    572
    -      // with trying ours.
    
    573
    -      // Otherwise, the new settings provided by the user while we were
    
    574
    -      // bootstrapping could be the ones that cause the bootstrap to succeed,
    
    575
    -      // but we overwrite them (unless we backup the original settings, and then
    
    576
    -      // save our new settings only if they have not changed).
    
    577
    -      // Another idea (maybe easier to implement) is to disable the settings
    
    578
    -      // UI while *any* bootstrap is going on.
    
    579
    -      // This is also documented in tor-browser#41921.
    
    580
    -      const provider = await lazy.TorProviderBuilder.build();
    
    581
    -      this.#changedSettings = true;
    
    582
    -      // We need to merge with old settings, in case the user is using a proxy
    
    583
    -      // or is behind a firewall.
    
    584
    -      await provider.writeSettings({
    
    585
    -        ...lazy.TorSettings.getSettings(),
    
    586
    -        ...currentSetting,
    
    587
    -      });
    
    588
    -
    
    589
    -      // Build out our bootstrap request.
    
    590
    -      const bootstrap = new lazy.TorBootstrapRequest();
    
    591
    -      bootstrap.onbootstrapstatus = (progress, status) => {
    
    592
    -        TorConnect._updateBootstrapProgress(progress, status);
    
    593
    -      };
    
    594
    -      bootstrap.onbootstraperror = error => {
    
    595
    -        lazy.logger.error("Auto-Bootstrap error", error);
    
    596
    -      };
    
    503
    +      await this.#trySetting(currentSetting, progressCallback, options);
    
    597 504
     
    
    598
    -      // Begin the bootstrap.
    
    599
    -      const success = await Promise.race([
    
    600
    -        bootstrap.bootstrap(),
    
    601
    -        this.#transitionPromise,
    
    602
    -      ]);
    
    603
    -      // Either the bootstrap request has finished, or a transition (caused by
    
    604
    -      // an error or by user's cancelation) started.
    
    605
    -      // However, we cannot be already transitioning in case of success, so if
    
    606
    -      // we are we should cancel the current bootstrap.
    
    607
    -      // With the current TorProvider, this will set DisableNetwork=1 again,
    
    608
    -      // which is what the user wanted if they canceled.
    
    609
    -      if (this.transitioning) {
    
    610
    -        if (success) {
    
    611
    -          lazy.logger.warn(
    
    612
    -            "We were already transitioning after a success, we were not expecting this."
    
    613
    -          );
    
    614
    -        }
    
    615
    -        bootstrap.cancel();
    
    616
    -        return;
    
    617
    -      }
    
    618
    -      if (success) {
    
    619
    -        // Persist the current settings to preferences.
    
    620
    -        lazy.TorSettings.setSettings(currentSetting);
    
    621
    -        lazy.TorSettings.saveToPrefs();
    
    622
    -        // Do not await `applySettings`. Otherwise this opens up a window of
    
    623
    -        // time where the user can still "Cancel" the bootstrap.
    
    624
    -        // We are calling `applySettings` just to be on the safe side, but the
    
    625
    -        // settings we are passing now should be exactly the same we already
    
    626
    -        // passed earlier.
    
    627
    -        lazy.TorSettings.applySettings().catch(e =>
    
    628
    -          lazy.logger.error("TorSettings.applySettings threw unexpectedly.", e)
    
    629
    -        );
    
    630
    -        this.changeState(TorConnectState.Bootstrapped);
    
    505
    +      if (this.#cancelled || this.#resolved) {
    
    631 506
             return;
    
    632 507
           }
    
    633 508
         }
    
    634 509
     
    
    635
    -    // Only explicitly change state here if something else has not transitioned
    
    636
    -    // us.
    
    637
    -    if (!this.transitioning) {
    
    638
    -      throw new TorConnectError(TorConnectError.AllSettingsFailed);
    
    639
    -    }
    
    640
    -  }
    
    641
    -
    
    642
    -  transitionRequested() {
    
    643
    -    this.#transitionResolve();
    
    510
    +    this.#resolveRun({
    
    511
    +      error: new TorConnectError(TorConnectError.AllSettingsFailed),
    
    512
    +    });
    
    644 513
       }
    
    645 514
     
    
    646
    -  async cleanup(nextState) {
    
    647
    -    // No need to await.
    
    648
    -    this.#moat?.uninit();
    
    649
    -    this.#moat = null;
    
    515
    +  /**
    
    516
    +   * Lookup user's potential censorship circumvention settings from Moat
    
    517
    +   * service.
    
    518
    +   *
    
    519
    +   * @param {BootstrapOptions} options - Options to apply to the bootstrap.
    
    520
    +   */
    
    521
    +  async #fetchSettings(options) {
    
    522
    +    if (options.simulateMoatResponse) {
    
    523
    +      await Promise.race([
    
    524
    +        new Promise(res => setTimeout(res, options.simulateDelay || 0)),
    
    525
    +        this.#cancelledPromise,
    
    526
    +      ]);
    
    650 527
     
    
    651
    -    if (this.#changedSettings && nextState !== TorConnectState.Bootstrapped) {
    
    652
    -      try {
    
    653
    -        await lazy.TorSettings.applySettings();
    
    654
    -      } catch (e) {
    
    655
    -        // We cannot do much if the original settings were bad or
    
    656
    -        // if the connection closed, so just report it in the
    
    657
    -        // console.
    
    658
    -        lazy.logger.warn("Failed to restore original settings.", e);
    
    528
    +      if (this.#cancelled || this.#resolved) {
    
    529
    +        return;
    
    659 530
           }
    
    531
    +
    
    532
    +      this.detectedRegion = options.simulateMoatResponse.country || null;
    
    533
    +      this.#settings = options.simulateMoatResponse.settings ?? null;
    
    534
    +
    
    535
    +      return;
    
    660 536
         }
    
    661
    -  }
    
    662
    -}
    
    663 537
     
    
    664
    -class BootstrappedState extends StateCallback {
    
    665
    -  // We may need to leave the bootstrapped state if the tor daemon
    
    666
    -  // exits (if it is restarted, we will have to bootstrap again).
    
    667
    -  allowedTransitions = Object.freeze([TorConnectState.Configuring]);
    
    538
    +    const moat = new lazy.MoatRPC();
    
    539
    +    try {
    
    540
    +      // We need to wait Moat's initialization even when we are requested to
    
    541
    +      // transition to another state to be sure its uninit will have its
    
    542
    +      // intended effect. So, do not use Promise.race here.
    
    543
    +      await moat.init();
    
    668 544
     
    
    669
    -  constructor() {
    
    670
    -    super(TorConnectState.Bootstrapped);
    
    671
    -  }
    
    545
    +      if (this.#cancelled || this.#resolved) {
    
    546
    +        return;
    
    547
    +      }
    
    672 548
     
    
    673
    -  run() {
    
    674
    -    // Notify observers of bootstrap completion.
    
    675
    -    Services.obs.notifyObservers(null, TorConnectTopics.BootstrapComplete);
    
    549
    +      // For now, throw any errors we receive from the backend, except when it
    
    550
    +      // was unable to detect user's country/region.
    
    551
    +      // If we use specialized error objects, we could pass the original errors
    
    552
    +      // to them.
    
    553
    +      const maybeSettings = await Promise.race([
    
    554
    +        moat.circumvention_settings(
    
    555
    +          [...lazy.TorSettings.builtinBridgeTypes, "vanilla"],
    
    556
    +          options.regionCode === "automatic" ? null : options.regionCode
    
    557
    +        ),
    
    558
    +        // This might set maybeSettings to undefined.
    
    559
    +        this.#cancelledPromise,
    
    560
    +      ]);
    
    561
    +      if (this.#cancelled || this.#resolved) {
    
    562
    +        return;
    
    563
    +      }
    
    564
    +
    
    565
    +      this.detectedRegion = maybeSettings?.country || null;
    
    566
    +
    
    567
    +      if (maybeSettings?.settings?.length) {
    
    568
    +        this.#settings = maybeSettings.settings;
    
    569
    +      } else {
    
    570
    +        // Keep consistency with the other call.
    
    571
    +        this.#settings = await Promise.race([
    
    572
    +          moat.circumvention_defaults([
    
    573
    +            ...lazy.TorSettings.builtinBridgeTypes,
    
    574
    +            "vanilla",
    
    575
    +          ]),
    
    576
    +          // This might set this.#settings to undefined.
    
    577
    +          this.#cancelledPromise,
    
    578
    +        ]);
    
    579
    +      }
    
    580
    +    } finally {
    
    581
    +      // Do not await the uninit.
    
    582
    +      moat.uninit();
    
    583
    +    }
    
    676 584
       }
    
    677
    -}
    
    678 585
     
    
    679
    -class ErrorState extends StateCallback {
    
    680
    -  allowedTransitions = Object.freeze([TorConnectState.Configuring]);
    
    586
    +  /**
    
    587
    +   * Try to apply the settings we fetched.
    
    588
    +   *
    
    589
    +   * @param {object} setting - The setting to try.
    
    590
    +   * @param {ProgressCallback} progressCallback - The callback to invoke with
    
    591
    +   *   the bootstrap progress.
    
    592
    +   * @param {BootstrapOptions} options - Options to apply to the bootstrap.
    
    593
    +   */
    
    594
    +  async #trySetting(setting, progressCallback, options) {
    
    595
    +    if (this.#cancelled || this.#resolved) {
    
    596
    +      return;
    
    597
    +    }
    
    681 598
     
    
    682
    -  static #hasEverHappened = false;
    
    599
    +    if (options.simulateMoatResponse && setting.simulateCensorship) {
    
    600
    +      // Move the simulateCensorship option to the options for the next
    
    601
    +      // BootstrapAttempt.
    
    602
    +      setting = structuredClone(setting);
    
    603
    +      delete setting.simulateCensorship;
    
    604
    +      options = { ...options, simulateCensorship: true };
    
    605
    +    }
    
    683 606
     
    
    684
    -  constructor() {
    
    685
    -    super(TorConnectState.Error);
    
    686
    -    ErrorState.#hasEverHappened = true;
    
    687
    -  }
    
    607
    +    // Send the new settings directly to the provider. We will save them only
    
    608
    +    // if the bootstrap succeeds.
    
    609
    +    // FIXME: We should somehow signal TorSettings users that we have set
    
    610
    +    // custom settings, and they should not apply theirs until we are done
    
    611
    +    // with trying ours.
    
    612
    +    // Otherwise, the new settings provided by the user while we were
    
    613
    +    // bootstrapping could be the ones that cause the bootstrap to succeed,
    
    614
    +    // but we overwrite them (unless we backup the original settings, and then
    
    615
    +    // save our new settings only if they have not changed).
    
    616
    +    // Another idea (maybe easier to implement) is to disable the settings
    
    617
    +    // UI while *any* bootstrap is going on.
    
    618
    +    // This is also documented in tor-browser#41921.
    
    619
    +    const provider = await lazy.TorProviderBuilder.build();
    
    620
    +    this.#changedSetting = setting;
    
    621
    +    // We need to merge with old settings, in case the user is using a proxy
    
    622
    +    // or is behind a firewall.
    
    623
    +    await provider.writeSettings({
    
    624
    +      ...lazy.TorSettings.getSettings(),
    
    625
    +      ...setting,
    
    626
    +    });
    
    688 627
     
    
    689
    -  run(_error) {
    
    690
    -    this.changeState(TorConnectState.Configuring);
    
    691
    -  }
    
    628
    +    if (this.#cancelled || this.#resolved) {
    
    629
    +      return;
    
    630
    +    }
    
    692 631
     
    
    693
    -  static get hasEverHappened() {
    
    694
    -    return ErrorState.#hasEverHappened;
    
    695
    -  }
    
    696
    -}
    
    632
    +    let result;
    
    633
    +    try {
    
    634
    +      this.#bootstrapAttempt = new BootstrapAttempt();
    
    635
    +      // At this stage, cancelling AutoBootstrap will also cancel this
    
    636
    +      // bootstrapAttempt.
    
    637
    +      result = await this.#bootstrapAttempt.run(progressCallback, options);
    
    638
    +    } catch (error) {
    
    639
    +      // Only re-try with the next settings *if* we have a BootstrapError.
    
    640
    +      // Other errors will end this auto-bootstrap attempt entirely.
    
    641
    +      if (
    
    642
    +        error instanceof TorConnectError &&
    
    643
    +        error.code === TorConnectError.BootstrapError
    
    644
    +      ) {
    
    645
    +        lazy.logger.info("TorConnect setting failed", setting, error);
    
    646
    +        // Try with the next settings.
    
    647
    +        // NOTE: We do not restore the user settings in between these runs.
    
    648
    +        // Instead we wait for #resolveRun callback to do so.
    
    649
    +        // This means there is a window of time where the setting is applied, but
    
    650
    +        // no bootstrap is running.
    
    651
    +        return;
    
    652
    +      }
    
    653
    +      // Pass error up.
    
    654
    +      throw error;
    
    655
    +    } finally {
    
    656
    +      this.#bootstrapAttempt = null;
    
    657
    +    }
    
    697 658
     
    
    698
    -class DisabledState extends StateCallback {
    
    699
    -  // Trap state: no way to leave the Disabled state.
    
    700
    -  allowedTransitions = Object.freeze([]);
    
    659
    +    if (this.#cancelled || this.#resolved) {
    
    660
    +      return;
    
    661
    +    }
    
    701 662
     
    
    702
    -  constructor() {
    
    703
    -    super(TorConnectState.Disabled);
    
    663
    +    // Pass the BootstrapAttempt result up.
    
    664
    +    this.#resolveRun({ result });
    
    704 665
       }
    
    705 666
     
    
    706
    -  async run() {
    
    707
    -    lazy.logger.debug("Entered the disabled state.");
    
    667
    +  /**
    
    668
    +   * Cancel the bootstrap attempt.
    
    669
    +   */
    
    670
    +  async cancel() {
    
    671
    +    if (this.#cancelled) {
    
    672
    +      lazy.logger.warn(
    
    673
    +        "Cancelled auto-bootstrap after it has already been cancelled"
    
    674
    +      );
    
    675
    +      return;
    
    676
    +    }
    
    677
    +    this.#cancelled = true;
    
    678
    +    this.#resolveCancelled();
    
    679
    +    if (this.#resolved) {
    
    680
    +      lazy.logger.warn(
    
    681
    +        "Cancelled auto-bootstrap after it has already resolved"
    
    682
    +      );
    
    683
    +      return;
    
    684
    +    }
    
    685
    +
    
    686
    +    // Wait until after bootstrap.cancel returns before we resolve with
    
    687
    +    // cancelled. In particular, there is a small chance that the bootstrap
    
    688
    +    // completes, in which case we want to be able to resolve with a success
    
    689
    +    // instead.
    
    690
    +    if (this.#bootstrapAttempt) {
    
    691
    +      this.#bootstrapAttempt.cancel();
    
    692
    +      await this.#bootstrapAttempt;
    
    693
    +    }
    
    694
    +    // In case no bootstrap is running, we resolve with "cancelled".
    
    695
    +    this.#resolveRun({ result: "cancelled" });
    
    708 696
       }
    
    709 697
     }
    
    710 698
     
    
    ... ... @@ -721,8 +709,11 @@ class InternetTest {
    721 709
       #pending = false;
    
    722 710
       #canceled = false;
    
    723 711
       #timeout = 0;
    
    712
    +  #simulateOffline = false;
    
    713
    +
    
    714
    +  constructor(simulateOffline) {
    
    715
    +    this.#simulateOffline = simulateOffline;
    
    724 716
     
    
    725
    -  constructor() {
    
    726 717
         this.#enabled = Services.prefs.getBoolPref(
    
    727 718
           TorConnectPrefs.allow_internet_test,
    
    728 719
           true
    
    ... ... @@ -752,6 +743,19 @@ class InternetTest {
    752 743
         this.#canceled = false;
    
    753 744
     
    
    754 745
         lazy.logger.info("Starting the Internet test");
    
    746
    +
    
    747
    +    if (this.#simulateOffline) {
    
    748
    +      await new Promise(res => setTimeout(res, 500));
    
    749
    +
    
    750
    +      this.#status = InternetStatus.Offline;
    
    751
    +
    
    752
    +      if (this.#canceled) {
    
    753
    +        return;
    
    754
    +      }
    
    755
    +      this.onResult(this.#status);
    
    756
    +      return;
    
    757
    +    }
    
    758
    +
    
    755 759
         const mrpc = new lazy.MoatRPC();
    
    756 760
         try {
    
    757 761
           await mrpc.init();
    
    ... ... @@ -792,27 +796,173 @@ class InternetTest {
    792 796
         return this.#status;
    
    793 797
       }
    
    794 798
     
    
    795
    -  get error() {
    
    796
    -    return this.#error;
    
    797
    -  }
    
    799
    +  get error() {
    
    800
    +    return this.#error;
    
    801
    +  }
    
    802
    +
    
    803
    +  get enabled() {
    
    804
    +    return this.#enabled;
    
    805
    +  }
    
    806
    +
    
    807
    +  // We randomize the Internet test timeout to make fingerprinting it harder, at
    
    808
    +  // least a little bit...
    
    809
    +  #timeoutRand() {
    
    810
    +    const offset = 30000;
    
    811
    +    const randRange = 5000;
    
    812
    +    return offset + randRange * (Math.random() * 2 - 1);
    
    813
    +  }
    
    814
    +}
    
    815
    +
    
    816
    +export const TorConnectStage = Object.freeze({
    
    817
    +  Disabled: "Disabled",
    
    818
    +  Loading: "Loading",
    
    819
    +  Start: "Start",
    
    820
    +  Bootstrapping: "Bootstrapping",
    
    821
    +  Offline: "Offline",
    
    822
    +  ChooseRegion: "ChooseRegion",
    
    823
    +  RegionNotFound: "RegionNotFound",
    
    824
    +  ConfirmRegion: "ConfirmRegion",
    
    825
    +  FinalError: "FinalError",
    
    826
    +  Bootstrapped: "Bootstrapped",
    
    827
    +});
    
    828
    +
    
    829
    +/**
    
    830
    + * @typedef {object} ConnectStage
    
    831
    + *
    
    832
    + * A summary of the user stage.
    
    833
    + *
    
    834
    + * @property {string} name - The name of the stage.
    
    835
    + * @property {string} defaultRegion - The default region to show in the UI.
    
    836
    + * @property {?string} bootstrapTrigger - The TorConnectStage prior to this
    
    837
    + *   bootstrap attempt. Only set during the "Bootstrapping" stage.
    
    838
    + * @property {?BootstrapError} error - The last bootstrapping error.
    
    839
    + * @property {boolean} tryAgain - Whether a bootstrap attempt has failed, so
    
    840
    + *   that a normal bootstrap should be shown as "Try Again" instead of
    
    841
    + *   "Connect". NOTE: to be removed when about:torconnect no longer uses
    
    842
    + *   breadcrumbs.
    
    843
    + * @property {boolean} potentiallyBlocked - Whether bootstrapping has ever
    
    844
    + *   failed, not including being cancelled or being offline. I.e. whether we
    
    845
    + *   have reached an error stage at some point before being bootstrapped.
    
    846
    + * @property {BootstrappingStatus} bootstrappingStatus - The current
    
    847
    + *   bootstrapping status.
    
    848
    + */
    
    849
    +
    
    850
    +/**
    
    851
    + * @typedef {object} BootstrappingStatus
    
    852
    + *
    
    853
    + * The status of a bootstrap.
    
    854
    + *
    
    855
    + * @property {number} progress - The percent progress.
    
    856
    + * @property {boolean} hasWarning - Whether this bootstrap has a warning in the
    
    857
    + *   Tor log.
    
    858
    + */
    
    859
    +
    
    860
    +/**
    
    861
    + * @typedef {object} BootstrapError
    
    862
    + *
    
    863
    + * Details about the error that caused bootstrapping to fail.
    
    864
    + *
    
    865
    + * @property {string} code - The error code type.
    
    866
    + * @property {string} message - The error message.
    
    867
    + * @property {?string} phase - The bootstrapping phase that failed.
    
    868
    + * @property {?string} reason - The bootstrapping failure reason.
    
    869
    + */
    
    870
    +
    
    871
    +export const TorConnect = {
    
    872
    +  /**
    
    873
    +   * Default bootstrap options for simulation.
    
    874
    +   *
    
    875
    +   * @type {BootstrapOptions}
    
    876
    +   */
    
    877
    +  simulateBootstrapOptions: {},
    
    878
    +
    
    879
    +  /**
    
    880
    +   * The name of the current stage the user is in.
    
    881
    +   *
    
    882
    +   * @type {string}
    
    883
    +   */
    
    884
    +  _stageName: TorConnectStage.Loading,
    
    885
    +
    
    886
    +  get stageName() {
    
    887
    +    return this._stageName;
    
    888
    +  },
    
    889
    +
    
    890
    +  /**
    
    891
    +   * The stage that triggered bootstrapping.
    
    892
    +   *
    
    893
    +   * @type {?string}
    
    894
    +   */
    
    895
    +  _bootstrapTrigger: null,
    
    896
    +
    
    897
    +  /**
    
    898
    +   * The alternative stage that we should move to after bootstrapping completes.
    
    899
    +   *
    
    900
    +   * @type {?string}
    
    901
    +   */
    
    902
    +  _requestedStage: null,
    
    903
    +
    
    904
    +  /**
    
    905
    +   * The default region to show in the UI for auto-bootstrapping.
    
    906
    +   *
    
    907
    +   * @type {string}
    
    908
    +   */
    
    909
    +  _defaultRegion: "automatic",
    
    910
    +
    
    911
    +  /**
    
    912
    +   * The current bootstrap attempt, if any.
    
    913
    +   *
    
    914
    +   * @type {?(BootstrapAttempt|AutoBootstrapAttempt)}
    
    915
    +   */
    
    916
    +  _bootstrapAttempt: null,
    
    917
    +
    
    918
    +  /**
    
    919
    +   * The bootstrap error that was last generated.
    
    920
    +   *
    
    921
    +   * @type {?TorConnectError}
    
    922
    +   */
    
    923
    +  _errorDetails: null,
    
    924
    +
    
    925
    +  /**
    
    926
    +   * Whether a bootstrap attempt has failed, so that a normal bootstrap should
    
    927
    +   * be shown as "Try Again" instead of "Connect".
    
    928
    +   *
    
    929
    +   * @type {boolean}
    
    930
    +   */
    
    931
    +  // TODO: Drop tryAgain when we remove breadcrumbs and use "Start again"
    
    932
    +  // instead.
    
    933
    +  _tryAgain: false,
    
    798 934
     
    
    799
    -  get enabled() {
    
    800
    -    return this.#enabled;
    
    801
    -  }
    
    935
    +  /**
    
    936
    +   * Whether bootstrapping has ever returned an error.
    
    937
    +   *
    
    938
    +   * @type {boolean}
    
    939
    +   */
    
    940
    +  _potentiallyBlocked: false,
    
    802 941
     
    
    803
    -  // We randomize the Internet test timeout to make fingerprinting it harder, at
    
    804
    -  // least a little bit...
    
    805
    -  #timeoutRand() {
    
    806
    -    const offset = 30000;
    
    807
    -    const randRange = 5000;
    
    808
    -    return offset + randRange * (Math.random() * 2 - 1);
    
    809
    -  }
    
    810
    -}
    
    942
    +  /**
    
    943
    +   * Get a summary of the current user stage.
    
    944
    +   *
    
    945
    +   * @type {ConnectStage}
    
    946
    +   */
    
    947
    +  get stage() {
    
    948
    +    return {
    
    949
    +      name: this._stageName,
    
    950
    +      defaultRegion: this._defaultRegion,
    
    951
    +      bootstrapTrigger: this._bootstrapTrigger,
    
    952
    +      error: this._errorDetails
    
    953
    +        ? {
    
    954
    +            code: this._errorDetails.code,
    
    955
    +            message: String(this._errorDetails.message ?? ""),
    
    956
    +            phase: this._errorDetails.cause?.phase ?? null,
    
    957
    +            reason: this._errorDetails.cause?.reason ?? null,
    
    958
    +          }
    
    959
    +        : null,
    
    960
    +      tryAgain: this._tryAgain,
    
    961
    +      potentiallyBlocked: this._potentiallyBlocked,
    
    962
    +      bootstrappingStatus: structuredClone(this._bootstrappingStatus),
    
    963
    +    };
    
    964
    +  },
    
    811 965
     
    
    812
    -export const TorConnect = {
    
    813
    -  _stateHandler: new InitialState(),
    
    814
    -  _bootstrapProgress: 0,
    
    815
    -  _internetStatus: InternetStatus.Unknown,
    
    816 966
       // list of country codes Moat has settings for
    
    817 967
       _countryCodes: [],
    
    818 968
       _countryNames: Object.freeze(
    
    ... ... @@ -826,109 +976,28 @@ export const TorConnect = {
    826 976
           return codesNames;
    
    827 977
         })()
    
    828 978
       ),
    
    829
    -  _detectedLocation: "",
    
    830
    -  _errorCode: null,
    
    831
    -  _errorDetails: null,
    
    832
    -  _logHasWarningOrError: false,
    
    833
    -  _hasBootstrapEverFailed: false,
    
    834
    -  _transitionPromise: null,
    
    835 979
     
    
    836 980
       // This is used as a helper to make the state of about:torconnect persistent
    
    837 981
       // during a session, but TorConnect does not use this data at all.
    
    838 982
       _uiState: {},
    
    839 983
     
    
    840
    -  _stateCallbacks: Object.freeze(
    
    841
    -    new Map([
    
    842
    -      // Initial is never transitioned to
    
    843
    -      [TorConnectState.Initial, InitialState],
    
    844
    -      [TorConnectState.Configuring, ConfiguringState],
    
    845
    -      [TorConnectState.Bootstrapping, BootstrappingState],
    
    846
    -      [TorConnectState.AutoBootstrapping, AutoBootstrappingState],
    
    847
    -      [TorConnectState.Bootstrapped, BootstrappedState],
    
    848
    -      [TorConnectState.Error, ErrorState],
    
    849
    -      [TorConnectState.Disabled, DisabledState],
    
    850
    -    ])
    
    851
    -  ),
    
    852
    -
    
    853
    -  _makeState(state) {
    
    854
    -    const klass = this._stateCallbacks.get(state);
    
    855
    -    if (!klass) {
    
    856
    -      throw new Error(`${state} is not a valid state.`);
    
    857
    -    }
    
    858
    -    return new klass();
    
    859
    -  },
    
    860
    -
    
    861
    -  async _changeState(newState, ...args) {
    
    862
    -    if (this._stateHandler.transitioning) {
    
    863
    -      // Avoid an exception to prevent it to be propagated to the original
    
    864
    -      // begin call.
    
    865
    -      lazy.logger.warn("Already transitioning");
    
    866
    -      return;
    
    867
    -    }
    
    868
    -    const prevState = this._stateHandler;
    
    869
    -
    
    870
    -    // ensure this is a valid state transition
    
    871
    -    if (!prevState.allowedTransitions.includes(newState)) {
    
    872
    -      throw Error(
    
    873
    -        `TorConnect: Attempted invalid state transition from ${prevState.state} to ${newState}`
    
    874
    -      );
    
    875
    -    }
    
    876
    -
    
    877
    -    lazy.logger.trace(
    
    878
    -      `Try transitioning from ${prevState.state} to ${newState}`,
    
    879
    -      args
    
    880
    -    );
    
    881
    -    try {
    
    882
    -      await prevState.end(newState);
    
    883
    -    } catch (e) {
    
    884
    -      // We take for granted that the begin of this state will call us again,
    
    885
    -      // to request the transition to the error state.
    
    886
    -      if (newState !== TorConnectState.Error) {
    
    887
    -        lazy.logger.debug(
    
    888
    -          `Refusing the transition from ${prevState.state} to ${newState} because the previous state threw.`
    
    889
    -        );
    
    890
    -        return;
    
    891
    -      }
    
    892
    -    }
    
    893
    -
    
    894
    -    // Set our new state first so that state transitions can themselves
    
    895
    -    // trigger a state transition.
    
    896
    -    this._stateHandler = this._makeState(newState);
    
    897
    -
    
    898
    -    // Error signal needs to be sent out before we enter the Error state.
    
    899
    -    // Expected on android `onBootstrapError` to set lastKnownError.
    
    900
    -    // Expected in about:torconnect to set the error codes and internet status
    
    901
    -    // *before* the StateChange signal.
    
    902
    -    if (newState === TorConnectState.Error) {
    
    903
    -      let error = args[0];
    
    904
    -      if (!(error instanceof TorConnectError)) {
    
    905
    -        error = new TorConnectError(TorConnectError.ExternalError, error);
    
    906
    -      }
    
    907
    -      TorConnect._errorCode = error.code;
    
    908
    -      TorConnect._errorDetails = error;
    
    909
    -      lazy.logger.error(`Entering error state (${error.code})`, error);
    
    910
    -
    
    911
    -      Services.obs.notifyObservers(error, TorConnectTopics.Error);
    
    912
    -    }
    
    913
    -
    
    914
    -    Services.obs.notifyObservers(
    
    915
    -      { state: newState },
    
    916
    -      TorConnectTopics.StateChange
    
    917
    -    );
    
    918
    -    this._stateHandler.begin(...args);
    
    984
    +  /**
    
    985
    +   * The status of the most recent bootstrap attempt.
    
    986
    +   *
    
    987
    +   * @type {BootstrappingStatus}
    
    988
    +   */
    
    989
    +  _bootstrappingStatus: {
    
    990
    +    progress: 0,
    
    991
    +    hasWarning: false,
    
    919 992
       },
    
    920 993
     
    
    921
    -  _updateBootstrapProgress(progress, status) {
    
    922
    -    this._bootstrapProgress = progress;
    
    923
    -
    
    924
    -    lazy.logger.info(
    
    925
    -      `Bootstrapping ${this._bootstrapProgress}% complete (${status})`
    
    926
    -    );
    
    994
    +  /**
    
    995
    +   * Notify the bootstrap progress.
    
    996
    +   */
    
    997
    +  _notifyBootstrapProgress() {
    
    998
    +    lazy.logger.debug("BootstrappingStatus", this._bootstrappingStatus);
    
    927 999
         Services.obs.notifyObservers(
    
    928
    -      {
    
    929
    -        progress: TorConnect._bootstrapProgress,
    
    930
    -        hasWarnings: TorConnect._logHasWarningOrError,
    
    931
    -      },
    
    1000
    +      this._bootstrappingStatus,
    
    932 1001
           TorConnectTopics.BootstrapProgress
    
    933 1002
         );
    
    934 1003
       },
    
    ... ... @@ -936,62 +1005,54 @@ export const TorConnect = {
    936 1005
       // init should be called by TorStartupService
    
    937 1006
       init() {
    
    938 1007
         lazy.logger.debug("TorConnect.init()");
    
    939
    -    this._stateHandler.begin();
    
    940 1008
     
    
    941 1009
         if (!this.enabled) {
    
    942 1010
           // Disabled
    
    943
    -      this._changeState(TorConnectState.Disabled);
    
    944
    -    } else {
    
    945
    -      let observeTopic = addTopic => {
    
    946
    -        Services.obs.addObserver(this, addTopic);
    
    947
    -        lazy.logger.debug(`Observing topic '${addTopic}'`);
    
    948
    -      };
    
    1011
    +      this._setStage(TorConnectStage.Disabled);
    
    1012
    +      return;
    
    1013
    +    }
    
    949 1014
     
    
    950
    -      // Wait for TorSettings, as we will need it.
    
    951
    -      // We will wait for a TorProvider only after TorSettings is ready,
    
    952
    -      // because the TorProviderBuilder initialization might not have finished
    
    953
    -      // at this point, and TorSettings initialization is a prerequisite for
    
    954
    -      // having a provider.
    
    955
    -      // So, we prefer initializing TorConnect as soon as possible, so that
    
    956
    -      // the UI will be able to detect it is in the Initializing state and act
    
    957
    -      // consequently.
    
    958
    -      lazy.TorSettings.initializedPromise.then(() =>
    
    959
    -        this._settingsInitialized()
    
    960
    -      );
    
    1015
    +    let observeTopic = addTopic => {
    
    1016
    +      Services.obs.addObserver(this, addTopic);
    
    1017
    +      lazy.logger.debug(`Observing topic '${addTopic}'`);
    
    1018
    +    };
    
    961 1019
     
    
    962
    -      // register the Tor topics we always care about
    
    963
    -      observeTopic(lazy.TorProviderTopics.ProcessExited);
    
    964
    -      observeTopic(lazy.TorProviderTopics.HasWarnOrErr);
    
    965
    -    }
    
    1020
    +    // Wait for TorSettings, as we will need it.
    
    1021
    +    // We will wait for a TorProvider only after TorSettings is ready,
    
    1022
    +    // because the TorProviderBuilder initialization might not have finished
    
    1023
    +    // at this point, and TorSettings initialization is a prerequisite for
    
    1024
    +    // having a provider.
    
    1025
    +    // So, we prefer initializing TorConnect as soon as possible, so that
    
    1026
    +    // the UI will be able to detect it is in the Initializing state and act
    
    1027
    +    // consequently.
    
    1028
    +    lazy.TorSettings.initializedPromise.then(() => this._settingsInitialized());
    
    1029
    +
    
    1030
    +    // register the Tor topics we always care about
    
    1031
    +    observeTopic(lazy.TorProviderTopics.ProcessExited);
    
    1032
    +    observeTopic(lazy.TorProviderTopics.HasWarnOrErr);
    
    966 1033
       },
    
    967 1034
     
    
    968 1035
       async observe(subject, topic) {
    
    969 1036
         lazy.logger.debug(`Observed ${topic}`);
    
    970 1037
     
    
    971 1038
         switch (topic) {
    
    972
    -      case lazy.TorProviderTopics.HasWarnOrErr: {
    
    973
    -        this._logHasWarningOrError = true;
    
    1039
    +      case lazy.TorProviderTopics.HasWarnOrErr:
    
    1040
    +        if (this._bootstrappingStatus.hasWarning) {
    
    1041
    +          // No change.
    
    1042
    +          return;
    
    1043
    +        }
    
    1044
    +        if (this._stageName === "Bootstrapping") {
    
    1045
    +          this._bootstrappingStatus.hasWarning = true;
    
    1046
    +          this._notifyBootstrapProgress();
    
    1047
    +        }
    
    974 1048
             break;
    
    975
    -      }
    
    976
    -      case lazy.TorProviderTopics.ProcessExited: {
    
    1049
    +      case lazy.TorProviderTopics.ProcessExited:
    
    1050
    +        lazy.logger.info("Starting again since the tor process exited");
    
    977 1051
             // Treat a failure as a possibly broken configuration.
    
    978 1052
             // So, prevent quickstart at the next start.
    
    979 1053
             Services.prefs.setBoolPref(TorLauncherPrefs.prompt_at_startup, true);
    
    980
    -        switch (this.state) {
    
    981
    -          case TorConnectState.Bootstrapping:
    
    982
    -          case TorConnectState.AutoBootstrapping:
    
    983
    -          case TorConnectState.Bootstrapped:
    
    984
    -            // If we are in the bootstrap or auto bootstrap, we could go
    
    985
    -            // through the error phase (and eventually we might do it, if some
    
    986
    -            // transition calls fail). However, this would start the
    
    987
    -            // connection assist, so we go directly to configuring.
    
    988
    -            // FIXME: Find a better way to handle this.
    
    989
    -            this._changeState(TorConnectState.Configuring);
    
    990
    -            break;
    
    991
    -          // Other states naturally resolve in configuration.
    
    992
    -        }
    
    1054
    +        this._makeStageRequest(TorConnectStage.Start, true);
    
    993 1055
             break;
    
    994
    -      }
    
    995 1056
           default:
    
    996 1057
             // ignore
    
    997 1058
             break;
    
    ... ... @@ -1003,29 +1064,47 @@ export const TorConnect = {
    1003 1064
         // daemon when it exits (tor-browser#21053, tor-browser#41921).
    
    1004 1065
         await lazy.TorProviderBuilder.build();
    
    1005 1066
     
    
    1006
    -    // tor-browser#41907: This is only a workaround to avoid users being
    
    1007
    -    // bounced back to the initial panel without any explanation.
    
    1008
    -    // Longer term we should disable the clickable elements, or find a UX
    
    1009
    -    // to prevent this from happening (e.g., allow buttons to be clicked,
    
    1010
    -    // but show an intermediate starting state, or a message that tor is
    
    1011
    -    // starting while the butons are disabled, etc...).
    
    1012
    -    // Notice that currently the initial state does not do anything.
    
    1013
    -    // Instead of just waiting, we could move this code in its callback.
    
    1014
    -    // See also tor-browser#41921.
    
    1015
    -    if (this.state !== TorConnectState.Initial) {
    
    1016
    -      lazy.logger.warn(
    
    1017
    -        "The TorProvider was built after the state had already changed."
    
    1018
    -      );
    
    1019
    -      return;
    
    1020
    -    }
    
    1021 1067
         lazy.logger.debug("The TorProvider is ready, changing state.");
    
    1068
    +    // NOTE: If the tor process exits before this point, then
    
    1069
    +    // shouldQuickStart would be `false`.
    
    1070
    +    // NOTE: At this point, _requestedStage should still be `null`.
    
    1071
    +    this._setStage(TorConnectStage.Start);
    
    1022 1072
         if (this.shouldQuickStart) {
    
    1023 1073
           // Quickstart
    
    1024
    -      this._changeState(TorConnectState.Bootstrapping);
    
    1025
    -    } else {
    
    1026
    -      // Configuring
    
    1027
    -      this._changeState(TorConnectState.Configuring);
    
    1074
    +      this.beginBootstrapping();
    
    1075
    +    }
    
    1076
    +  },
    
    1077
    +
    
    1078
    +  /**
    
    1079
    +   * Set the user stage.
    
    1080
    +   *
    
    1081
    +   * @param {string} name - The name of the stage to move to.
    
    1082
    +   */
    
    1083
    +  _setStage(name) {
    
    1084
    +    if (this._bootstrapAttempt) {
    
    1085
    +      throw new Error(`Trying to set the stage to ${name} during a bootstrap`);
    
    1086
    +    }
    
    1087
    +
    
    1088
    +    lazy.logger.info(`Entering stage ${name}`);
    
    1089
    +    const prevState = this.state;
    
    1090
    +    this._stageName = name;
    
    1091
    +    this._bootstrappingStatus.hasWarning = false;
    
    1092
    +    this._bootstrappingStatus.progress =
    
    1093
    +      name === TorConnectStage.Bootstrapped ? 100 : 0;
    
    1094
    +
    
    1095
    +    Services.obs.notifyObservers(this.stage, TorConnectTopics.StageChange);
    
    1096
    +
    
    1097
    +    // TODO: Remove when all pages have switched to stage.
    
    1098
    +    const newState = this.state;
    
    1099
    +    if (prevState !== newState) {
    
    1100
    +      Services.obs.notifyObservers(
    
    1101
    +        { state: newState },
    
    1102
    +        TorConnectTopics.StateChange
    
    1103
    +      );
    
    1028 1104
         }
    
    1105
    +
    
    1106
    +    // Update the progress after the stage has changed.
    
    1107
    +    this._notifyBootstrapProgress();
    
    1029 1108
       },
    
    1030 1109
     
    
    1031 1110
       /*
    
    ... ... @@ -1049,33 +1128,41 @@ export const TorConnect = {
    1049 1128
         return (
    
    1050 1129
           this.enabled &&
    
    1051 1130
           // if we have succesfully bootstraped, then no need to show TorConnect
    
    1052
    -      this.state !== TorConnectState.Bootstrapped
    
    1131
    +      this._stageName !== TorConnectStage.Bootstrapped
    
    1053 1132
         );
    
    1054 1133
       },
    
    1055 1134
     
    
    1056 1135
       /**
    
    1057
    -   * Whether bootstrapping can currently begin.
    
    1136
    +   * Whether we are in a stage that can lead into the Bootstrapping stage. I.e.
    
    1137
    +   * whether we can make a "normal" or "auto" bootstrapping request.
    
    1058 1138
        *
    
    1059
    -   * The value may change with TorConnectTopics.StateChanged.
    
    1139
    +   * The value may change with TorConnectTopics.StageChanged.
    
    1060 1140
        *
    
    1061 1141
        * @param {boolean}
    
    1062 1142
        */
    
    1063 1143
       get canBeginBootstrap() {
    
    1064
    -    return this._stateHandler.allowedTransitions.includes(
    
    1065
    -      TorConnectState.Bootstrapping
    
    1144
    +    return (
    
    1145
    +      this._stageName === TorConnectStage.Start ||
    
    1146
    +      this._stageName === TorConnectStage.Offline ||
    
    1147
    +      this._stageName === TorConnectStage.ChooseRegion ||
    
    1148
    +      this._stageName === TorConnectStage.RegionNotFound ||
    
    1149
    +      this._stageName === TorConnectStage.ConfirmRegion
    
    1066 1150
         );
    
    1067 1151
       },
    
    1068 1152
     
    
    1069 1153
       /**
    
    1070
    -   * Whether auto-bootstrapping can currently begin.
    
    1154
    +   * Whether we are in an error stage that can lead into the Bootstrapping
    
    1155
    +   * stage. I.e. whether we can make an "auto" bootstrapping request.
    
    1071 1156
        *
    
    1072
    -   * The value may change with TorConnectTopics.StateChanged.
    
    1157
    +   * The value may change with TorConnectTopics.StageChanged.
    
    1073 1158
        *
    
    1074 1159
        * @param {boolean}
    
    1075 1160
        */
    
    1076 1161
       get canBeginAutoBootstrap() {
    
    1077
    -    return this._stateHandler.allowedTransitions.includes(
    
    1078
    -      TorConnectState.AutoBootstrapping
    
    1162
    +    return (
    
    1163
    +      this._stageName === TorConnectStage.ChooseRegion ||
    
    1164
    +      this._stageName === TorConnectStage.RegionNotFound ||
    
    1165
    +      this._stageName === TorConnectStage.ConfirmRegion
    
    1079 1166
         );
    
    1080 1167
       },
    
    1081 1168
     
    
    ... ... @@ -1088,16 +1175,39 @@ export const TorConnect = {
    1088 1175
         );
    
    1089 1176
       },
    
    1090 1177
     
    
    1178
    +  // TODO: Remove when all pages have switched to "stage".
    
    1091 1179
       get state() {
    
    1092
    -    return this._stateHandler.state;
    
    1093
    -  },
    
    1094
    -
    
    1095
    -  get bootstrapProgress() {
    
    1096
    -    return this._bootstrapProgress;
    
    1097
    -  },
    
    1098
    -
    
    1099
    -  get internetStatus() {
    
    1100
    -    return this._internetStatus;
    
    1180
    +    // There is no "Error" stage, but about:torconnect relies on receiving the
    
    1181
    +    // Error state to update its display. So we temporarily set the stage for a
    
    1182
    +    // StateChange signal.
    
    1183
    +    if (this._isErrorState) {
    
    1184
    +      return TorConnectState.Error;
    
    1185
    +    }
    
    1186
    +    switch (this._stageName) {
    
    1187
    +      case TorConnectStage.Disabled:
    
    1188
    +        return TorConnectState.Disabled;
    
    1189
    +      case TorConnectStage.Loading:
    
    1190
    +        return TorConnectState.Initial;
    
    1191
    +      case TorConnectStage.Start:
    
    1192
    +      case TorConnectStage.Offline:
    
    1193
    +      case TorConnectStage.ChooseRegion:
    
    1194
    +      case TorConnectStage.RegionNotFound:
    
    1195
    +      case TorConnectStage.ConfirmRegion:
    
    1196
    +      case TorConnectStage.FinalError:
    
    1197
    +        return TorConnectState.Configuring;
    
    1198
    +      case TorConnectStage.Bootstrapping:
    
    1199
    +        if (
    
    1200
    +          this._bootstrapTrigger === TorConnectStage.Start ||
    
    1201
    +          this._bootstrapTrigger === TorConnectStage.Offline
    
    1202
    +        ) {
    
    1203
    +          return TorConnectState.Bootstrapping;
    
    1204
    +        }
    
    1205
    +        return TorConnectState.AutoBootstrapping;
    
    1206
    +      case TorConnectStage.Bootstrapped:
    
    1207
    +        return TorConnectState.Bootstrapped;
    
    1208
    +    }
    
    1209
    +    lazy.logger.error(`Unknown state at stage ${this._stageName}`);
    
    1210
    +    return null;
    
    1101 1211
       },
    
    1102 1212
     
    
    1103 1213
       get countryCodes() {
    
    ... ... @@ -1108,92 +1218,414 @@ export const TorConnect = {
    1108 1218
         return this._countryNames;
    
    1109 1219
       },
    
    1110 1220
     
    
    1111
    -  get detectedLocation() {
    
    1112
    -    return this._detectedLocation;
    
    1221
    +  /**
    
    1222
    +   * Whether the Bootstrapping process has ever failed, not including being
    
    1223
    +   * cancelled or being offline.
    
    1224
    +   *
    
    1225
    +   * The value may change with TorConnectTopics.StageChanged.
    
    1226
    +   *
    
    1227
    +   * @type {boolean}
    
    1228
    +   */
    
    1229
    +  get potentiallyBlocked() {
    
    1230
    +    return this._potentiallyBlocked;
    
    1113 1231
       },
    
    1114 1232
     
    
    1115
    -  get errorCode() {
    
    1116
    -    return this._errorCode;
    
    1233
    +  /**
    
    1234
    +   * Ensure that we are not disabled.
    
    1235
    +   */
    
    1236
    +  _ensureEnabled() {
    
    1237
    +    if (!this.enabled || this._stageName === TorConnectStage.Disabled) {
    
    1238
    +      throw new Error("Unexpected Disabled stage for user method");
    
    1239
    +    }
    
    1117 1240
       },
    
    1118 1241
     
    
    1119
    -  get errorDetails() {
    
    1120
    -    return this._errorDetails;
    
    1121
    -  },
    
    1242
    +  /**
    
    1243
    +   * Signal an error to listeners.
    
    1244
    +   *
    
    1245
    +   * @param {Error} error - The error.
    
    1246
    +   */
    
    1247
    +  _signalError(error) {
    
    1248
    +    // TODO: Replace this method with _setError without any signalling when
    
    1249
    +    // pages have switched to stage.
    
    1250
    +    // Currently it simulates the old behaviour for about:torconnect.
    
    1251
    +    lazy.logger.debug("Signalling error", error);
    
    1252
    +
    
    1253
    +    if (!(error instanceof TorConnectError)) {
    
    1254
    +      error = new TorConnectError(TorConnectError.ExternalError, error);
    
    1255
    +    }
    
    1256
    +    this._errorDetails = error;
    
    1122 1257
     
    
    1123
    -  get logHasWarningOrError() {
    
    1124
    -    return this._logHasWarningOrError;
    
    1258
    +    // Temporarily set an error state for listeners.
    
    1259
    +    // We send the Error signal before the "StateChange" signal.
    
    1260
    +    // Expected on android `onBootstrapError` to set lastKnownError.
    
    1261
    +    // Expected in about:torconnect to set the error codes and internet status
    
    1262
    +    // *before* the StateChange signal.
    
    1263
    +    this._isErrorState = true;
    
    1264
    +    Services.obs.notifyObservers(error, TorConnectTopics.Error);
    
    1265
    +    Services.obs.notifyObservers(
    
    1266
    +      { state: this.state },
    
    1267
    +      TorConnectTopics.StateChange
    
    1268
    +    );
    
    1269
    +    this._isErrorState = false;
    
    1125 1270
       },
    
    1126 1271
     
    
    1127 1272
       /**
    
    1128
    -   * Whether we have ever entered the Error state.
    
    1273
    +   * Add simulation options to the bootstrap request.
    
    1129 1274
        *
    
    1130
    -   * @type {boolean}
    
    1275
    +   * @param {BootstrapOptions} bootstrapOptions - The options to add to.
    
    1276
    +   * @param {string} [regionCode] - The region code being used.
    
    1131 1277
        */
    
    1132
    -  get hasEverFailed() {
    
    1133
    -    return ErrorState.hasEverHappened;
    
    1278
    +  _addSimulateOptions(bootstrapOptions, regionCode) {
    
    1279
    +    if (this.simulateBootstrapOptions.simulateCensorship) {
    
    1280
    +      bootstrapOptions.simulateCensorship = true;
    
    1281
    +    }
    
    1282
    +    if (this.simulateBootstrapOptions.simulateDelay) {
    
    1283
    +      bootstrapOptions.simulateDelay =
    
    1284
    +        this.simulateBootstrapOptions.simulateDelay;
    
    1285
    +    }
    
    1286
    +    if (this.simulateBootstrapOptions.simulateOffline) {
    
    1287
    +      bootstrapOptions.simulateOffline = true;
    
    1288
    +    }
    
    1289
    +    if (this.simulateBootstrapOptions.simulateMoatResponse) {
    
    1290
    +      bootstrapOptions.simulateMoatResponse =
    
    1291
    +        this.simulateBootstrapOptions.simulateMoatResponse;
    
    1292
    +    }
    
    1293
    +
    
    1294
    +    const censorshipLevel = Services.prefs.getIntPref(
    
    1295
    +      TorConnectPrefs.censorship_level,
    
    1296
    +      0
    
    1297
    +    );
    
    1298
    +    if (censorshipLevel > 0 && !bootstrapOptions.simulateDelay) {
    
    1299
    +      bootstrapOptions.simulateDelay = 1500;
    
    1300
    +    }
    
    1301
    +    if (censorshipLevel === 1) {
    
    1302
    +      // Bootstrap fails, but auto-bootstrap does not.
    
    1303
    +      if (!regionCode) {
    
    1304
    +        bootstrapOptions.simulateCensorship = true;
    
    1305
    +      }
    
    1306
    +    } else if (censorshipLevel === 2) {
    
    1307
    +      // Bootstrap fails. Auto-bootstrap fails with ConfirmRegion when using
    
    1308
    +      // auto-detect region, but succeeds otherwise.
    
    1309
    +      if (!regionCode) {
    
    1310
    +        bootstrapOptions.simulateCensorship = true;
    
    1311
    +      }
    
    1312
    +      if (regionCode === "automatic") {
    
    1313
    +        bootstrapOptions.simulateCensorship = true;
    
    1314
    +        bootstrapOptions.simulateMoatResponse = {
    
    1315
    +          country: "fi",
    
    1316
    +          settings: [{}, {}],
    
    1317
    +        };
    
    1318
    +      }
    
    1319
    +    } else if (censorshipLevel === 3) {
    
    1320
    +      // Bootstrap and auto-bootstrap fail.
    
    1321
    +      bootstrapOptions.simulateCensorship = true;
    
    1322
    +      bootstrapOptions.simulateMoatResponse = {
    
    1323
    +        country: null,
    
    1324
    +        settings: [],
    
    1325
    +      };
    
    1326
    +    }
    
    1134 1327
       },
    
    1135 1328
     
    
    1136 1329
       /**
    
    1137
    -   * Whether the Bootstrapping process has ever failed, not including when it
    
    1138
    -   * failed due to not being connected to the internet.
    
    1330
    +   * Confirm that a bootstrapping can take place, and whether the given values
    
    1331
    +   * are valid.
    
    1139 1332
        *
    
    1140
    -   * This does not include a failure in AutoBootstrapping.
    
    1333
    +   * @param {string} [regionCode] - The region code passed in.
    
    1141 1334
        *
    
    1142
    -   * @type {boolean}
    
    1335
    +   * @return {boolean} whether bootstrapping can proceed.
    
    1143 1336
        */
    
    1144
    -  get potentiallyBlocked() {
    
    1145
    -    return this._hasBootstrapEverFailed;
    
    1146
    -  },
    
    1337
    +  _confirmBootstrapping(regionCode) {
    
    1338
    +    this._ensureEnabled();
    
    1339
    +
    
    1340
    +    if (this._bootstrapAttempt) {
    
    1341
    +      lazy.logger.warn(
    
    1342
    +        "Already have an ongoing bootstrap attempt." +
    
    1343
    +          ` Ignoring request with ${regionCode}.`
    
    1344
    +      );
    
    1345
    +      return false;
    
    1346
    +    }
    
    1347
    +
    
    1348
    +    const currentStage = this._stageName;
    
    1349
    +
    
    1350
    +    if (regionCode) {
    
    1351
    +      if (!this.canBeginAutoBootstrap) {
    
    1352
    +        lazy.logger.warn(
    
    1353
    +          `Cannot begin auto bootstrap in stage ${currentStage}`
    
    1354
    +        );
    
    1355
    +        return false;
    
    1356
    +      }
    
    1357
    +      if (
    
    1358
    +        regionCode === "automatic" &&
    
    1359
    +        currentStage !== TorConnectStage.ChooseRegion
    
    1360
    +      ) {
    
    1361
    +        lazy.logger.warn("Auto bootstrap is missing an explicit regionCode");
    
    1362
    +        return false;
    
    1363
    +      }
    
    1364
    +      return true;
    
    1365
    +    }
    
    1366
    +
    
    1367
    +    if (!this.canBeginBootstrap) {
    
    1368
    +      lazy.logger.warn(`Cannot begin bootstrap in stage ${currentStage}`);
    
    1369
    +      return false;
    
    1370
    +    }
    
    1371
    +    if (this.canBeginAutoBootstrap) {
    
    1372
    +      // Only expect "auto" bootstraps to be triggered when in an error stage.
    
    1373
    +      lazy.logger.warn(
    
    1374
    +        `Expected a regionCode to bootstrap in stage ${currentStage}`
    
    1375
    +      );
    
    1376
    +      return false;
    
    1377
    +    }
    
    1147 1378
     
    
    1148
    -  get uiState() {
    
    1149
    -    return this._uiState;
    
    1379
    +    return true;
    
    1150 1380
       },
    
    1151
    -  set uiState(newState) {
    
    1152
    -    this._uiState = newState;
    
    1381
    +
    
    1382
    +  /**
    
    1383
    +   * Begin a bootstrap attempt.
    
    1384
    +   *
    
    1385
    +   * @param {string} [regionCode] - An optional region code string to use, or
    
    1386
    +   *   "automatic" to automatically determine the region. If given, will start
    
    1387
    +   *   an auto-bootstrap attempt.
    
    1388
    +   */
    
    1389
    +  async beginBootstrapping(regionCode) {
    
    1390
    +    lazy.logger.debug("TorConnect.beginBootstrapping()");
    
    1391
    +
    
    1392
    +    if (!this._confirmBootstrapping(regionCode)) {
    
    1393
    +      return;
    
    1394
    +    }
    
    1395
    +
    
    1396
    +    const beginStage = this._stageName;
    
    1397
    +    const bootstrapOptions = { regionCode };
    
    1398
    +    const bootstrapAttempt = regionCode
    
    1399
    +      ? new AutoBootstrapAttempt()
    
    1400
    +      : new BootstrapAttempt();
    
    1401
    +
    
    1402
    +    if (!regionCode) {
    
    1403
    +      // Only test internet for the first bootstrap attempt.
    
    1404
    +      // TODO: Remove this since we do not have user consent. tor-browser#42605.
    
    1405
    +      bootstrapOptions.testInternet = true;
    
    1406
    +    }
    
    1407
    +
    
    1408
    +    this._addSimulateOptions(bootstrapOptions, regionCode);
    
    1409
    +
    
    1410
    +    // NOTE: The only `await` in this method is for `bootstrapAttempt.run`.
    
    1411
    +    // Moreover, we returned early if `_bootstrapAttempt` was non-`null`.
    
    1412
    +    // Therefore, the method is effectively "locked" by `_bootstrapAttempt`, so
    
    1413
    +    // there should only ever be one caller at a time.
    
    1414
    +
    
    1415
    +    if (regionCode) {
    
    1416
    +      // Set the default to what the user chose.
    
    1417
    +      this._defaultRegion = regionCode;
    
    1418
    +    } else {
    
    1419
    +      // Reset the default region to show in the UI.
    
    1420
    +      this._defaultRegion = "automatic";
    
    1421
    +    }
    
    1422
    +    this._requestedStage = null;
    
    1423
    +    this._bootstrapTrigger = beginStage;
    
    1424
    +    this._setStage(TorConnectStage.Bootstrapping);
    
    1425
    +    this._bootstrapAttempt = bootstrapAttempt;
    
    1426
    +
    
    1427
    +    let error = null;
    
    1428
    +    let result = null;
    
    1429
    +    try {
    
    1430
    +      result = await bootstrapAttempt.run(progress => {
    
    1431
    +        this._bootstrappingStatus.progress = progress;
    
    1432
    +        lazy.logger.info(`Bootstrapping ${progress}% complete`);
    
    1433
    +        this._notifyBootstrapProgress();
    
    1434
    +      }, bootstrapOptions);
    
    1435
    +    } catch (err) {
    
    1436
    +      error = err;
    
    1437
    +    }
    
    1438
    +
    
    1439
    +    const requestedStage = this._requestedStage;
    
    1440
    +    this._requestedStage = null;
    
    1441
    +    this._bootstrapTrigger = null;
    
    1442
    +    this._bootstrapAttempt = null;
    
    1443
    +
    
    1444
    +    if (bootstrapAttempt.detectedRegion) {
    
    1445
    +      this._defaultRegion = bootstrapAttempt.detectedRegion;
    
    1446
    +    }
    
    1447
    +
    
    1448
    +    if (result === "complete") {
    
    1449
    +      // Reset tryAgain, potentiallyBlocked and errorDetails in case the tor
    
    1450
    +      // process exists later on.
    
    1451
    +      this._tryAgain = false;
    
    1452
    +      this._potentiallyBlocked = false;
    
    1453
    +      this._errorDetails = null;
    
    1454
    +
    
    1455
    +      if (requestedStage) {
    
    1456
    +        lazy.logger.warn(
    
    1457
    +          `Ignoring ${requestedStage} request since we are bootstrapped`
    
    1458
    +        );
    
    1459
    +      }
    
    1460
    +      this._setStage(TorConnectStage.Bootstrapped);
    
    1461
    +      Services.obs.notifyObservers(null, TorConnectTopics.BootstrapComplete);
    
    1462
    +      return;
    
    1463
    +    }
    
    1464
    +
    
    1465
    +    if (requestedStage) {
    
    1466
    +      lazy.logger.debug("Ignoring bootstrap result", result, error);
    
    1467
    +      this._setStage(requestedStage);
    
    1468
    +      return;
    
    1469
    +    }
    
    1470
    +
    
    1471
    +    if (
    
    1472
    +      result === "offline" &&
    
    1473
    +      (beginStage === TorConnectStage.Start ||
    
    1474
    +        beginStage === TorConnectStage.Offline)
    
    1475
    +    ) {
    
    1476
    +      this._tryAgain = true;
    
    1477
    +      this._signalError(new TorConnectError(TorConnectError.Offline));
    
    1478
    +
    
    1479
    +      this._setStage(TorConnectStage.Offline);
    
    1480
    +      return;
    
    1481
    +    }
    
    1482
    +
    
    1483
    +    if (error) {
    
    1484
    +      lazy.logger.info("Bootstrap attempt error", error);
    
    1485
    +
    
    1486
    +      this._tryAgain = true;
    
    1487
    +      this._potentiallyBlocked = true;
    
    1488
    +
    
    1489
    +      this._signalError(error);
    
    1490
    +
    
    1491
    +      switch (beginStage) {
    
    1492
    +        case TorConnectStage.Start:
    
    1493
    +        case TorConnectStage.Offline:
    
    1494
    +          this._setStage(TorConnectStage.ChooseRegion);
    
    1495
    +          return;
    
    1496
    +        case TorConnectStage.ChooseRegion:
    
    1497
    +          // TODO: Uncomment for behaviour in tor-browser#42550.
    
    1498
    +          /*
    
    1499
    +          if (regionCode !== "automatic") {
    
    1500
    +            // Not automatic. Go straight to the final error.
    
    1501
    +            this._setStage(TorConnectStage.FinalError);
    
    1502
    +            return;
    
    1503
    +          }
    
    1504
    +          */
    
    1505
    +          if (regionCode !== "automatic" || bootstrapAttempt.detectedRegion) {
    
    1506
    +            this._setStage(TorConnectStage.ConfirmRegion);
    
    1507
    +            return;
    
    1508
    +          }
    
    1509
    +          this._setStage(TorConnectStage.RegionNotFound);
    
    1510
    +          return;
    
    1511
    +      }
    
    1512
    +      this._setStage(TorConnectStage.FinalError);
    
    1513
    +      return;
    
    1514
    +    }
    
    1515
    +
    
    1516
    +    // Bootstrap was cancelled.
    
    1517
    +    if (result !== "cancelled") {
    
    1518
    +      lazy.logger.error(`Unexpected bootstrap result`, result);
    
    1519
    +    }
    
    1520
    +
    
    1521
    +    // TODO: Remove this Offline hack when pages use "stage".
    
    1522
    +    if (beginStage === TorConnectStage.Offline) {
    
    1523
    +      // Re-send the "Offline" error to push the pages back to "Offline".
    
    1524
    +      this._signalError(new TorConnectError(TorConnectError.Offline));
    
    1525
    +    }
    
    1526
    +
    
    1527
    +    // Return to the previous stage.
    
    1528
    +    this._setStage(beginStage);
    
    1153 1529
       },
    
    1154 1530
     
    
    1155
    -  /*
    
    1156
    -    These functions allow external consumers to tell TorConnect to transition states
    
    1531
    +  /**
    
    1532
    +   * Cancel an ongoing bootstrap attempt.
    
    1157 1533
        */
    
    1534
    +  cancelBootstrapping() {
    
    1535
    +    lazy.logger.debug("TorConnect.cancelBootstrapping()");
    
    1536
    +
    
    1537
    +    this._ensureEnabled();
    
    1538
    +
    
    1539
    +    if (!this._bootstrapAttempt) {
    
    1540
    +      lazy.logger.warn("No bootstrap attempt to cancel");
    
    1541
    +      return;
    
    1542
    +    }
    
    1158 1543
     
    
    1159
    -  beginBootstrap() {
    
    1160
    -    lazy.logger.debug("TorConnect.beginBootstrap()");
    
    1161
    -    this._changeState(TorConnectState.Bootstrapping);
    
    1544
    +    this._bootstrapAttempt.cancel();
    
    1162 1545
       },
    
    1163 1546
     
    
    1164
    -  cancelBootstrap() {
    
    1165
    -    lazy.logger.debug("TorConnect.cancelBootstrap()");
    
    1547
    +  /**
    
    1548
    +   * Request the transition to the given stage.
    
    1549
    +   *
    
    1550
    +   * If we are bootstrapping, it will be cancelled and the stage will be
    
    1551
    +   * transitioned to when it resolves. Otherwise, we will switch to the stage
    
    1552
    +   * immediately.
    
    1553
    +   *
    
    1554
    +   * @param {string} stage - The stage to request.
    
    1555
    +   * @param {boolean} [overideBootstrapped=false] - Whether the request can
    
    1556
    +   *   override the "Bootstrapped" stage.
    
    1557
    +   */
    
    1558
    +  _makeStageRequest(stage, overrideBootstrapped = false) {
    
    1559
    +    lazy.logger.debug(`Request for stage ${stage}`);
    
    1560
    +
    
    1561
    +    this._ensureEnabled();
    
    1562
    +
    
    1563
    +    if (stage === this._stageName) {
    
    1564
    +      lazy.logger.info(`Ignoring request for current stage ${stage}`);
    
    1565
    +      return;
    
    1566
    +    }
    
    1166 1567
         if (
    
    1167
    -      this.state !== TorConnectState.AutoBootstrapping &&
    
    1168
    -      this.state !== TorConnectState.Bootstrapping
    
    1568
    +      !overrideBootstrapped &&
    
    1569
    +      this._stageName === TorConnectStage.Bootstrapped
    
    1169 1570
         ) {
    
    1571
    +      lazy.logger.warn(`Cannot move to ${stage} when bootstrapped`);
    
    1572
    +      return;
    
    1573
    +    }
    
    1574
    +    if (this._stageName === TorConnectStage.Loading) {
    
    1575
    +      if (stage === TorConnectStage.Start) {
    
    1576
    +        // Will transition to "Start" stage when loading completes.
    
    1577
    +        lazy.logger.info("Still in the Loading stage");
    
    1578
    +      } else {
    
    1579
    +        lazy.logger.warn(`Cannot move to ${stage} when Loading`);
    
    1580
    +      }
    
    1581
    +      return;
    
    1582
    +    }
    
    1583
    +
    
    1584
    +    if (!this._bootstrapAttempt) {
    
    1585
    +      // Transition immediately.
    
    1586
    +      this._setStage(stage);
    
    1587
    +      return;
    
    1588
    +    }
    
    1589
    +
    
    1590
    +    if (this._requestedStage === stage) {
    
    1591
    +      lazy.logger.info(`Already requesting stage ${stage}`);
    
    1592
    +      return;
    
    1593
    +    }
    
    1594
    +    if (this._requestedStage) {
    
    1170 1595
           lazy.logger.warn(
    
    1171
    -        `Cannot cancel bootstrapping in the ${this.state} state`
    
    1596
    +        `Overriding request for ${this._requestedStage} with ${stage}`
    
    1172 1597
           );
    
    1173
    -      return;
    
    1174 1598
         }
    
    1175
    -    this._changeState(TorConnectState.Configuring);
    
    1599
    +    // Move to stage *after* bootstrap completes.
    
    1600
    +    this._requestedStage = stage;
    
    1601
    +    this._bootstrapAttempt?.cancel();
    
    1176 1602
       },
    
    1177 1603
     
    
    1178
    -  beginAutoBootstrap(countryCode) {
    
    1179
    -    lazy.logger.debug("TorConnect.beginAutoBootstrap()");
    
    1180
    -    this._changeState(TorConnectState.AutoBootstrapping, countryCode);
    
    1604
    +  /**
    
    1605
    +   * Restart the TorConnect stage to the start.
    
    1606
    +   */
    
    1607
    +  startAgain() {
    
    1608
    +    this._makeStageRequest(TorConnectStage.Start);
    
    1181 1609
       },
    
    1182 1610
     
    
    1183
    -  /*
    
    1184
    -    Further external commands and helper methods
    
    1611
    +  /**
    
    1612
    +   * Set the stage to be "ChooseRegion".
    
    1185 1613
        */
    
    1186
    -  openTorPreferences() {
    
    1187
    -    if (lazy.TorLauncherUtil.isAndroid) {
    
    1188
    -      lazy.EventDispatcher.instance.sendRequest({
    
    1189
    -        type: "GeckoView:Tor:OpenSettings",
    
    1190
    -      });
    
    1614
    +  chooseRegion() {
    
    1615
    +    if (!this._potentiallyBlocked) {
    
    1616
    +      lazy.logger.error("chooseRegion request before getting an error");
    
    1191 1617
           return;
    
    1192 1618
         }
    
    1193
    -    const win = lazy.BrowserWindowTracker.getTopWindow();
    
    1194
    -    win.switchToTabHavingURI("about:preferences#connection", true);
    
    1619
    +    // NOTE: The ChooseRegion stage needs _errorDetails to be displayed in
    
    1620
    +    // about:torconnect. The _potentiallyBlocked condition should be
    
    1621
    +    // sufficient to ensure this.
    
    1622
    +    this._makeStageRequest(TorConnectStage.ChooseRegion);
    
    1195 1623
       },
    
    1196 1624
     
    
    1625
    +  /*
    
    1626
    +    Further external commands and helper methods
    
    1627
    +   */
    
    1628
    +
    
    1197 1629
       /**
    
    1198 1630
        * Open the "about:torconnect" tab.
    
    1199 1631
        *
    
    ... ... @@ -1204,10 +1636,11 @@ export const TorConnect = {
    1204 1636
        * potentially blocked.
    
    1205 1637
        *
    
    1206 1638
        * @param {object} [options] - extra options.
    
    1207
    -   * @property {boolean} [options.beginBootstrap=false] - Whether to try and
    
    1208
    -   *   begin Bootstrapping.
    
    1209
    -   * @property {string} [options.beginAutoBootstrap] - The location to use to
    
    1210
    -   *   begin AutoBootstrapping, if possible.
    
    1639
    +   * @property {"soft"|"hard"} [options.beginBootstrapping] - Whether to try and
    
    1640
    +   *   begin bootstrapping. "soft" will only trigger the bootstrap if we are not
    
    1641
    +   *   `potentiallyBlocked`. "hard" will try begin the bootstrap regardless.
    
    1642
    +   * @property {string} [options.regionCode] - A region to pass in for
    
    1643
    +   *   auto-bootstrapping.
    
    1211 1644
        */
    
    1212 1645
       openTorConnect(options) {
    
    1213 1646
         // FIXME: Should we move this to the about:torconnect actor?
    
    ... ... @@ -1215,25 +1648,23 @@ export const TorConnect = {
    1215 1648
         win.switchToTabHavingURI("about:torconnect", true, {
    
    1216 1649
           ignoreQueryString: true,
    
    1217 1650
         });
    
    1218
    -    if (
    
    1219
    -      options?.beginBootstrap &&
    
    1220
    -      this.canBeginBootstrap &&
    
    1221
    -      !this.potentiallyBlocked
    
    1222
    -    ) {
    
    1223
    -      this.beginBootstrap();
    
    1651
    +
    
    1652
    +    if (!options?.beginBootstrapping || !this.canBeginBootstrap) {
    
    1653
    +      return;
    
    1224 1654
         }
    
    1225
    -    // options.beginAutoBootstrap can be an empty string.
    
    1226
    -    if (
    
    1227
    -      options?.beginAutoBootstrap !== undefined &&
    
    1228
    -      this.canBeginAutoBootstrap
    
    1229
    -    ) {
    
    1230
    -      this.beginAutoBootstrap(options.beginAutoBootstrap);
    
    1655
    +
    
    1656
    +    if (options.beginBootstrapping === "hard") {
    
    1657
    +      if (this.canBeginAutoBootstrap && !options.regionCode) {
    
    1658
    +        // Treat as an addition startAgain request to first move back to the
    
    1659
    +        // "Start" stage before bootstrapping.
    
    1660
    +        this.startAgain();
    
    1661
    +      }
    
    1662
    +    } else if (this.potentiallyBlocked) {
    
    1663
    +      // Do not trigger the bootstrap if we have ever had an error.
    
    1664
    +      return;
    
    1231 1665
         }
    
    1232
    -  },
    
    1233 1666
     
    
    1234
    -  viewTorLogs() {
    
    1235
    -    const win = lazy.BrowserWindowTracker.getTopWindow();
    
    1236
    -    win.switchToTabHavingURI("about:preferences#connection-viewlogs", true);
    
    1667
    +    this.beginBootstrapping(options.regionCode);
    
    1237 1668
       },
    
    1238 1669
     
    
    1239 1670
       async getCountryCodes() {