[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[tor-commits] [Git][tpo/applications/tor-browser][tor-browser-128.4.0esr-14.5-1] 17 commits: fixup! Bug 40597: Implement TorSettings module



Title: GitLab

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

Commits:

  • 8fee92cd
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Bug 40597: Implement TorSettings module
    
    Bug 41710: Move bootstrapping attempts into a new class.
    
    We copy the logic from "BootstrappingState" and "AutoBootstrappingState"
    into "BootstrappingAttempt" and "AutoBootstrappingAttempt". The main
    difference is that we can do the following:
    
    ```
    bootstrapAttempt = new BootstrapAttempt();
    bootstrapResult = await bootstrapAttempt.run();
    // ...
    bootstrapAttempt.cancel();
    ```
    
    rather than using the "StateCallback" class, which requires some
    complicated state management.
    
    Moreover, "AutoBootstrappingAttempt" will use "BootstrappingAttempt" for
    each of its attempts. So the logic for bootstrapping can be kept in one
    place.
    
    Some other changes:
    
    1. Censorship simulation will no longer necessarily avoid the Moat
       calls, so these can be tested.
    2. It would be possible to perform the internet test when
       auto-bootstrapping as well.
    3. When auto-bootstrapping, if "Bootstrapping" produces an error other
       than a "BootstrapError", we can end it early.
    4. No longer set TorConnect internals.
    5. More fine-grained control over censorship simulation and offline
       simulation.
    
  • 1086febd
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Bug 40597: Implement TorSettings module
    
    Bug 41710: Remove StateCallback.
    
  • 55d915e5
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Bug 40597: Implement TorSettings module
    
    Bug 41710: Return early from TorConnect.init if not enabled.
    
  • 802af522
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Bug 40597: Implement TorSettings module
    
    Bug 41710: Replace StateCallback in TorConnect.
    
    Instead of managing the abstract "State" we directly manager the user
    "Stage". We provide backward compatibility with the "State" for android
    and about:torconnect. Eventually this logic can be dropped from these
    endpoints and they can listen for changes in the "Stage" instead.
    
    The behaviour for about:torconnect is mostly the same as before, with
    some exceptions:
    
    1. If the user sees the "Offline" state, and starts and cancels the
       bootstrap, they should return the to the "Offline" state, rather
       than "ConnectToTor".
    2. Trying to start a bootstrap via the UI before the settings have
       loaded will do nothing.
    3. Pressing a breadcrumb whilst bootstrapping will now also cancel the
       bootstrap.
    
  • cc17defe
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser
    
    Bug 42550 - Switch about:torconnect to use TorConnect.stage to control
    the shown stage and sync pages.
    
    Now TorConnect entirely controls which stage should be shown to the
    user, and "about:torconnect" simply relays the user actions up to
    TorConnect to handle. In particular, we stop sending out
    "torconnect:broadcast-user-action" to sync pages.
    
    We also show "Try Again" if the user cancels the first bootstrap
    attempt without an error.
    
    We also do not try and sync the selected region between pages. However
    all pages should still show the *actually* submitted region after a
    bootstrap fails. E.g. to confirm their location.
    
    We also allow the user to re-select "Automatic" when they use
    breadcrumbs to go back a stage.
    
    Also change gTorConnectTitlebarStatus and gTorConnectUrlbarButton to use
    TorConnectStage.
    
  • 771381c5
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Bug 40597: Implement TorSettings module
    
    Bug 41710: Switch TorConnect.openTorConnect to use new methods.
    
  • fafc6ff3
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
    
    Bug 41710: Switch from TorConnect.state to TorConnect.stage.
    
  • 2ddd2029
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Bug 40597: Implement TorSettings module
    
    Bug 41710: Remove unused TorConnect properties.
    
  • 3a27b920
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Bug 42247: Android helpers for the TorProvider
    
    Bug 41710: Switch Android to new TorConnect methods and add TODOs.
    
  • 4aa64ccc
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Lox integration
    
    Bug 41710: Switch from TorConnectState to TorConnectStage.
    
  • 56837486
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser
    
    Bug 41710: Move viewTorLogs and openTorPreferences to TorConnectParent.
    
  • ebe8a652
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Bug 40597: Implement TorSettings module
    
    Bug 41710: Move viewTorLogs to TorConnectParent.
    
  • 8f95d948
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! [android] Enable the connect assist experiments on alpha
    
    Bug 41710: Remove onSettingsRequested.
    
  • bba68db8
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! [android] Add Tor integration and UI
    
    Bug 41710: Remove onSettingsRequested.
    
  • a6be0160
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Temporary changes to about:torconnect for Android.
    
    Bug 41710: Remove onSettingsRequested.
    
    TorConnect.openTorPreferences was removed.
    
  • be125ecd
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! Bug 40597: Implement TorSettings module
    
    Bug 41710: Remove the beginBootstrap, beginAutoBootstrap, and
    cancelBootstrap methods.
    
  • 1f6fc087
    by Henry Wilkes at 2024-11-13T08:23:22+00:00
    fixup! [android] Add Tor integration and UI
    
    Bug 41710: Fix onBootstrapProgress for new TorConnect.
    
    The bootstrap progress signal is now released every time the stage
    changes.
    

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() {
    

  • _______________________________________________
    tor-commits mailing list -- tor-commits@xxxxxxxxxxxxxxxxxxxx
    To unsubscribe send an email to tor-commits-leave@xxxxxxxxxxxxxxxxxxxx