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

[tor-commits] [tor-browser/tor-browser-91.5.0esr-11.5-2] Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#tor



commit e51211f5380a30a70236c9d734e666ab3b63bb9e
Author: Richard Pospesel <richard@xxxxxxxxxxxxxx>
Date:   Mon Sep 16 15:25:39 2019 -0700

    Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#tor
    
    This patch adds a new about:preferences#tor page which allows modifying
    bridge, proxy, and firewall settings from within Tor Browser. All of the
    functionality present in tor-launcher's Network Configuration panel is
    present:
    
     - Setting built-in bridges
     - Requesting bridges from BridgeDB via moat
     - Using user-provided bridges
     - Configuring SOCKS4, SOCKS5, and HTTP/HTTPS proxies
     - Setting firewall ports
     - Viewing and Copying Tor's logs
     - The Networking Settings in General preferences has been removed
---
 browser/components/moz.build                       |   1 +
 browser/components/preferences/main.inc.xhtml      |  54 --
 browser/components/preferences/main.js             |  14 -
 browser/components/preferences/preferences.js      |   9 +
 browser/components/preferences/preferences.xhtml   |   5 +
 .../torpreferences/content/requestBridgeDialog.jsm | 209 +++++
 .../content/requestBridgeDialog.xhtml              |  35 +
 .../torpreferences/content/torCategory.inc.xhtml   |   9 +
 .../torpreferences/content/torLogDialog.jsm        |  66 ++
 .../torpreferences/content/torLogDialog.xhtml      |  23 +
 .../components/torpreferences/content/torPane.js   | 940 +++++++++++++++++++++
 .../torpreferences/content/torPane.xhtml           | 157 ++++
 .../torpreferences/content/torPreferences.css      | 189 +++++
 .../torpreferences/content/torPreferencesIcon.svg  |   8 +
 browser/components/torpreferences/jar.mn           |  10 +
 browser/components/torpreferences/moz.build        |   1 +
 16 files changed, 1662 insertions(+), 68 deletions(-)

diff --git a/browser/components/moz.build b/browser/components/moz.build
index 1bc09f4093fb..66de87290bd8 100644
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -53,6 +53,7 @@ DIRS += [
     "syncedtabs",
     "uitour",
     "urlbar",
+    "torpreferences",
     "translation",
 ]
 
diff --git a/browser/components/preferences/main.inc.xhtml b/browser/components/preferences/main.inc.xhtml
index a89b89f723a8..594711e61474 100644
--- a/browser/components/preferences/main.inc.xhtml
+++ b/browser/components/preferences/main.inc.xhtml
@@ -671,58 +671,4 @@
     <label id="cfrFeaturesLearnMore" class="learnMore" data-l10n-id="browsing-cfr-recommendations-learn-more" is="text-link"/>
   </hbox>
 </groupbox>
-
-<hbox id="networkProxyCategory"
-      class="subcategory"
-      hidden="true"
-      data-category="paneGeneral">
-  <html:h1 data-l10n-id="network-settings-title"/>
-</hbox>
-
-<!-- Network Settings-->
-<groupbox id="connectionGroup" data-category="paneGeneral" hidden="true">
-  <label class="search-header" hidden="true"><html:h2 data-l10n-id="network-settings-title"/></label>
-
-  <hbox align="center">
-    <hbox align="center" flex="1">
-      <description id="connectionSettingsDescription" control="connectionSettings"/>
-      <spacer width="5"/>
-      <label id="connectionSettingsLearnMore" class="learnMore" is="text-link"
-        data-l10n-id="network-proxy-connection-learn-more">
-      </label>
-      <separator orient="vertical"/>
-    </hbox>
-
-    <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
-    <hbox>
-      <button id="connectionSettings"
-              is="highlightable-button"
-              class="accessory-button"
-              data-l10n-id="network-proxy-connection-settings"
-              searchkeywords="doh trr"
-              search-l10n-ids="
-                connection-window.title,
-                connection-proxy-option-no.label,
-                connection-proxy-option-auto.label,
-                connection-proxy-option-system.label,
-                connection-proxy-option-manual.label,
-                connection-proxy-http,
-                connection-proxy-https,
-                connection-proxy-http-port,
-                connection-proxy-socks,
-                connection-proxy-socks4,
-                connection-proxy-socks5,
-                connection-proxy-noproxy,
-                connection-proxy-noproxy-desc,
-                connection-proxy-https-sharing.label,
-                connection-proxy-autotype.label,
-                connection-proxy-reload.label,
-                connection-proxy-autologin.label,
-                connection-proxy-socks-remote-dns.label,
-                connection-dns-over-https.label,
-                connection-dns-over-https-url-custom.label,
-            " />
-    </hbox>
-  </hbox>
-</groupbox>
 </html:template>
diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js
index 2a6ba4a3d8e4..501ba9144a31 100644
--- a/browser/components/preferences/main.js
+++ b/browser/components/preferences/main.js
@@ -368,15 +368,6 @@ var gMainPane = {
     });
     this.updatePerformanceSettingsBox({ duringChangeEvent: false });
     this.displayUseSystemLocale();
-    let connectionSettingsLink = document.getElementById(
-      "connectionSettingsLearnMore"
-    );
-    let connectionSettingsUrl =
-      Services.urlFormatter.formatURLPref("app.support.baseURL") +
-      "prefs-connection-settings";
-    connectionSettingsLink.setAttribute("href", connectionSettingsUrl);
-    this.updateProxySettingsUI();
-    initializeProxyUI(gMainPane);
 
     if (Services.prefs.getBoolPref("intl.multilingual.enabled")) {
       gMainPane.initBrowserLocale();
@@ -510,11 +501,6 @@ var gMainPane = {
       "change",
       gMainPane.updateHardwareAcceleration.bind(gMainPane)
     );
-    setEventListener(
-      "connectionSettings",
-      "command",
-      gMainPane.showConnections
-    );
     setEventListener(
       "browserContainersCheckbox",
       "command",
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
index a3656f827ffc..ce338584142e 100644
--- a/browser/components/preferences/preferences.js
+++ b/browser/components/preferences/preferences.js
@@ -13,6 +13,7 @@
 /* import-globals-from findInPage.js */
 /* import-globals-from ../../base/content/utilityOverlay.js */
 /* import-globals-from ../../../toolkit/content/preferencesBindings.js */
+/* import-globals-from ../torpreferences/content/torPane.js */
 
 "use strict";
 
@@ -136,6 +137,14 @@ function init_all() {
     register_module("paneSync", gSyncPane);
   }
   register_module("paneSearchResults", gSearchResultsPane);
+  if (gTorPane.enabled) {
+    document.getElementById("category-tor").hidden = false;
+    register_module("paneTor", gTorPane);
+  } else {
+    // Remove the pane from the DOM so it doesn't get incorrectly included in search results.
+    document.getElementById("template-paneTor").remove();
+  }
+
   gSearchResultsPane.init();
   gMainPane.preInit();
 
diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml
index 32184867ac17..0923005c8b90 100644
--- a/browser/components/preferences/preferences.xhtml
+++ b/browser/components/preferences/preferences.xhtml
@@ -12,6 +12,7 @@
 <?xml-stylesheet href="chrome://browser/skin/preferences/search.css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/containers.css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/privacy.css"?>
+<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
 
 <!DOCTYPE html [
 <!ENTITY % aboutTorDTD SYSTEM "chrome://torbutton/locale/aboutTor.dtd">
@@ -154,6 +155,9 @@
           <image class="category-icon"/>
           <label class="category-name" flex="1" data-l10n-id="pane-experimental-title"></label>
         </richlistitem>
+
+#include ../torpreferences/content/torCategory.inc.xhtml
+
       </richlistbox>
 
       <spacer flex="1"/>
@@ -207,6 +211,7 @@
 #include containers.inc.xhtml
 #include sync.inc.xhtml
 #include experimental.inc.xhtml
+#include ../torpreferences/content/torPane.xhtml
         </vbox>
       </vbox>
     </vbox>
diff --git a/browser/components/torpreferences/content/requestBridgeDialog.jsm b/browser/components/torpreferences/content/requestBridgeDialog.jsm
new file mode 100644
index 000000000000..44ae11762def
--- /dev/null
+++ b/browser/components/torpreferences/content/requestBridgeDialog.jsm
@@ -0,0 +1,209 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["RequestBridgeDialog"];
+
+const { BridgeDB } = ChromeUtils.import("resource:///modules/BridgeDB.jsm");
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
+class RequestBridgeDialog {
+  constructor() {
+    this._dialog = null;
+    this._submitButton = null;
+    this._dialogDescription = null;
+    this._captchaImage = null;
+    this._captchaEntryTextbox = null;
+    this._captchaRefreshButton = null;
+    this._incorrectCaptchaHbox = null;
+    this._incorrectCaptchaLabel = null;
+    this._bridges = [];
+  }
+
+  static get selectors() {
+    return {
+      submitButton:
+        "accept" /* not really a selector but a key for dialog's getButton */,
+      dialogDescription: "description#torPreferences-requestBridge-description",
+      captchaImage: "image#torPreferences-requestBridge-captchaImage",
+      captchaEntryTextbox: "input#torPreferences-requestBridge-captchaTextbox",
+      refreshCaptchaButton:
+        "button#torPreferences-requestBridge-refreshCaptchaButton",
+      incorrectCaptchaHbox:
+        "hbox#torPreferences-requestBridge-incorrectCaptchaHbox",
+      incorrectCaptchaLabel:
+        "label#torPreferences-requestBridge-incorrectCaptchaError",
+    };
+  }
+
+  _populateXUL(dialog) {
+    const selectors = RequestBridgeDialog.selectors;
+
+    this._dialog = dialog;
+    const dialogWin = dialog.parentElement;
+    dialogWin.setAttribute(
+      "title",
+      TorStrings.settings.requestBridgeDialogTitle
+    );
+    // user may have opened a Request Bridge dialog in another tab, so update the
+    // CAPTCHA image or close out the dialog if we have a bridge list
+    this._dialog.addEventListener("focusin", () => {
+      const uri = BridgeDB.currentCaptchaImage;
+      const bridges = BridgeDB.currentBridges;
+
+      // new captcha image
+      if (uri) {
+        this._setcaptchaImage(uri);
+      } else if (bridges) {
+        this._bridges = bridges;
+        this._submitButton.disabled = false;
+        this._dialog.cancelDialog();
+      }
+    });
+
+    this._submitButton = this._dialog.getButton(selectors.submitButton);
+    this._submitButton.setAttribute("label", TorStrings.settings.submitCaptcha);
+    this._submitButton.disabled = true;
+    this._dialog.addEventListener("dialogaccept", e => {
+      e.preventDefault();
+      this.onSubmitCaptcha();
+    });
+
+    this._dialogDescription = this._dialog.querySelector(
+      selectors.dialogDescription
+    );
+    this._dialogDescription.textContent =
+      TorStrings.settings.contactingBridgeDB;
+
+    this._captchaImage = this._dialog.querySelector(selectors.captchaImage);
+
+    // request captcha from bridge db
+    BridgeDB.requestNewCaptchaImage().then(uri => {
+      this._setcaptchaImage(uri);
+    });
+
+    this._captchaEntryTextbox = this._dialog.querySelector(
+      selectors.captchaEntryTextbox
+    );
+    this._captchaEntryTextbox.setAttribute(
+      "placeholder",
+      TorStrings.settings.captchaTextboxPlaceholder
+    );
+    this._captchaEntryTextbox.disabled = true;
+    // disable submit if entry textbox is empty
+    this._captchaEntryTextbox.oninput = () => {
+      this._submitButton.disabled = this._captchaEntryTextbox.value == "";
+    };
+
+    this._captchaRefreshButton = this._dialog.querySelector(
+      selectors.refreshCaptchaButton
+    );
+    this._captchaRefreshButton.disabled = true;
+
+    this._incorrectCaptchaHbox = this._dialog.querySelector(
+      selectors.incorrectCaptchaHbox
+    );
+    this._incorrectCaptchaLabel = this._dialog.querySelector(
+      selectors.incorrectCaptchaLabel
+    );
+    this._incorrectCaptchaLabel.setAttribute(
+      "value",
+      TorStrings.settings.incorrectCaptcha
+    );
+
+    return true;
+  }
+
+  _setcaptchaImage(uri) {
+    if (uri != this._captchaImage.src) {
+      this._captchaImage.src = uri;
+      this._dialogDescription.textContent = TorStrings.settings.solveTheCaptcha;
+      this._setUIDisabled(false);
+      this._captchaEntryTextbox.focus();
+      this._captchaEntryTextbox.select();
+    }
+  }
+
+  _setUIDisabled(disabled) {
+    this._submitButton.disabled = this._captchaGuessIsEmpty() || disabled;
+    this._captchaEntryTextbox.disabled = disabled;
+    this._captchaRefreshButton.disabled = disabled;
+  }
+
+  _captchaGuessIsEmpty() {
+    return this._captchaEntryTextbox.value == "";
+  }
+
+  init(window, dialog) {
+    // defer to later until firefox has populated the dialog with all our elements
+    window.setTimeout(() => {
+      this._populateXUL(dialog);
+    }, 0);
+  }
+
+  close() {
+    BridgeDB.close();
+  }
+
+  /*
+    Event Handlers
+  */
+  onSubmitCaptcha() {
+    let captchaText = this._captchaEntryTextbox.value.trim();
+    // noop if the field is empty
+    if (captchaText == "") {
+      return;
+    }
+
+    // freeze ui while we make request
+    this._setUIDisabled(true);
+    this._incorrectCaptchaHbox.style.visibility = "hidden";
+
+    BridgeDB.submitCaptchaGuess(captchaText)
+      .then(aBridges => {
+        if (aBridges) {
+          this._bridges = aBridges;
+          this._submitButton.disabled = false;
+          // This was successful, but use cancelDialog() to close, since
+          // we intercept the `dialogaccept` event.
+          this._dialog.cancelDialog();
+        } else {
+          this._bridges = [];
+          this._setUIDisabled(false);
+          this._incorrectCaptchaHbox.style.visibility = "visible";
+        }
+      })
+      .catch(aError => {
+        // TODO: handle other errors properly here when we do the bridge settings re-design
+        this._bridges = [];
+        this._setUIDisabled(false);
+        this._incorrectCaptchaHbox.style.visibility = "visible";
+        console.log(eError);
+      });
+  }
+
+  onRefreshCaptcha() {
+    this._setUIDisabled(true);
+    this._captchaImage.src = "";
+    this._dialogDescription.textContent =
+      TorStrings.settings.contactingBridgeDB;
+    this._captchaEntryTextbox.value = "";
+    this._incorrectCaptchaHbox.style.visibility = "hidden";
+
+    BridgeDB.requestNewCaptchaImage().then(uri => {
+      this._setcaptchaImage(uri);
+    });
+  }
+
+  openDialog(gSubDialog, aCloseCallback) {
+    gSubDialog.open(
+      "chrome://browser/content/torpreferences/requestBridgeDialog.xhtml",
+      {
+        features: "resizable=yes",
+        closingCallback: () => {
+          this.close();
+          aCloseCallback(this._bridges);
+        }
+      },
+      this,
+    );
+  }
+}
diff --git a/browser/components/torpreferences/content/requestBridgeDialog.xhtml b/browser/components/torpreferences/content/requestBridgeDialog.xhtml
new file mode 100644
index 000000000000..64c4507807fb
--- /dev/null
+++ b/browser/components/torpreferences/content/requestBridgeDialog.xhtml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
+
+<window type="child"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+        xmlns:html="http://www.w3.org/1999/xhtml";>
+<dialog id="torPreferences-requestBridge-dialog"
+        buttons="accept,cancel">
+  <!-- ok, so &#8203; is a zero-width space. We need to have *something* in the innerText so that XUL knows how tall the
+       description node is so that it can determine how large to make the dialog element's inner draw area. If we have
+       nothing in the innerText, then it collapse to 0 height, and the contents of the dialog ends up partially hidden >:( -->
+  <description id="torPreferences-requestBridge-description">&#8203;</description>
+  <!-- init to transparent 400x125 png -->
+  <image id="torPreferences-requestBridge-captchaImage" flex="1"/>
+  <hbox id="torPreferences-requestBridge-inputHbox">
+    <html:input id="torPreferences-requestBridge-captchaTextbox" type="text" style="-moz-box-flex: 1;"/>
+    <button id="torPreferences-requestBridge-refreshCaptchaButton"
+            image="chrome://browser/skin/reload.svg"
+            oncommand="requestBridgeDialog.onRefreshCaptcha();"/>
+  </hbox>
+  <hbox id="torPreferences-requestBridge-incorrectCaptchaHbox" align="center">
+    <image id="torPreferences-requestBridge-errorIcon" />
+    <label id="torPreferences-requestBridge-incorrectCaptchaError" flex="1"/>
+  </hbox>
+  <script type="application/javascript"><![CDATA[
+    "use strict";
+
+    let requestBridgeDialog = window.arguments[0];
+    let dialog = document.getElementById("torPreferences-requestBridge-dialog");
+    requestBridgeDialog.init(window, dialog);
+  ]]></script>
+</dialog>
+</window>
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torCategory.inc.xhtml b/browser/components/torpreferences/content/torCategory.inc.xhtml
new file mode 100644
index 000000000000..abe56200f571
--- /dev/null
+++ b/browser/components/torpreferences/content/torCategory.inc.xhtml
@@ -0,0 +1,9 @@
+<richlistitem id="category-tor"
+              class="category"
+              value="paneTor"
+              helpTopic="prefs-tor"
+              align="center"
+              hidden="true">
+  <image class="category-icon"/>
+  <label id="torPreferences-labelCategory" class="category-name" flex="1" value="Tor"/>
+</richlistitem>
diff --git a/browser/components/torpreferences/content/torLogDialog.jsm b/browser/components/torpreferences/content/torLogDialog.jsm
new file mode 100644
index 000000000000..ecc684d878c2
--- /dev/null
+++ b/browser/components/torpreferences/content/torLogDialog.jsm
@@ -0,0 +1,66 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorLogDialog"];
+
+const { TorProtocolService } = ChromeUtils.import(
+  "resource:///modules/TorProtocolService.jsm"
+);
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
+class TorLogDialog {
+  constructor() {
+    this._dialog = null;
+    this._logTextarea = null;
+    this._copyLogButton = null;
+  }
+
+  static get selectors() {
+    return {
+      copyLogButton: "extra1",
+      logTextarea: "textarea#torPreferences-torDialog-textarea",
+    };
+  }
+
+  _populateXUL(aDialog) {
+    this._dialog = aDialog;
+    const dialogWin = this._dialog.parentElement;
+    dialogWin.setAttribute("title", TorStrings.settings.torLogDialogTitle);
+
+    this._logTextarea = this._dialog.querySelector(
+      TorLogDialog.selectors.logTextarea
+    );
+
+    this._copyLogButton = this._dialog.getButton(
+      TorLogDialog.selectors.copyLogButton
+    );
+    this._copyLogButton.setAttribute("label", TorStrings.settings.copyLog);
+    this._copyLogButton.addEventListener("command", () => {
+      this.copyTorLog();
+    });
+
+    this._logTextarea.value = TorProtocolService.getLog();
+  }
+
+  init(window, aDialog) {
+    // defer to later until firefox has populated the dialog with all our elements
+    window.setTimeout(() => {
+      this._populateXUL(aDialog);
+    }, 0);
+  }
+
+  copyTorLog() {
+    // Copy tor log messages to the system clipboard.
+    let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+      Ci.nsIClipboardHelper
+    );
+    clipboard.copyString(this._logTextarea.value);
+  }
+
+  openDialog(gSubDialog) {
+    gSubDialog.open(
+      "chrome://browser/content/torpreferences/torLogDialog.xhtml",
+      { features: "resizable=yes" },
+      this
+    );
+  }
+}
diff --git a/browser/components/torpreferences/content/torLogDialog.xhtml b/browser/components/torpreferences/content/torLogDialog.xhtml
new file mode 100644
index 000000000000..9c17f8132978
--- /dev/null
+++ b/browser/components/torpreferences/content/torLogDialog.xhtml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
+
+<window type="child"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+        xmlns:html="http://www.w3.org/1999/xhtml";>
+<dialog id="torPreferences-torLog-dialog"
+    buttons="accept,extra1">
+  <html:textarea
+    id="torPreferences-torDialog-textarea"
+    multiline="true"
+    readonly="true"/>
+  <script type="application/javascript"><![CDATA[
+    "use strict";
+
+    let torLogDialog = window.arguments[0];
+    let dialog = document.getElementById("torPreferences-torLog-dialog");
+    torLogDialog.init(window, dialog);
+  ]]></script>
+</dialog>
+</window>
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torPane.js b/browser/components/torpreferences/content/torPane.js
new file mode 100644
index 000000000000..58eec7ff74aa
--- /dev/null
+++ b/browser/components/torpreferences/content/torPane.js
@@ -0,0 +1,940 @@
+"use strict";
+
+/* global Services */
+
+const { TorSettings, TorSettingsTopics, TorSettingsData, TorBridgeSource, TorBuiltinBridgeTypes, TorProxyType } = ChromeUtils.import(
+  "resource:///modules/TorSettings.jsm"
+);
+
+const { TorProtocolService } = ChromeUtils.import(
+  "resource:///modules/TorProtocolService.jsm"
+);
+
+const { TorConnect, TorConnectTopics, TorConnectState } = ChromeUtils.import(
+  "resource:///modules/TorConnect.jsm"
+);
+
+const { TorLogDialog } = ChromeUtils.import(
+  "chrome://browser/content/torpreferences/torLogDialog.jsm"
+);
+
+const { RequestBridgeDialog } = ChromeUtils.import(
+  "chrome://browser/content/torpreferences/requestBridgeDialog.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorStrings",
+  "resource:///modules/TorStrings.jsm"
+);
+
+/*
+  Tor Pane
+
+  Code for populating the XUL in about:preferences#tor, handling input events, interfacing with tor-launcher
+*/
+const gTorPane = (function() {
+  /* CSS selectors for all of the Tor Network DOM elements we need to access */
+  const selectors = {
+    category: {
+      title: "label#torPreferences-labelCategory",
+    },
+    messageBox: {
+      box: "div#torPreferences-connectMessageBox",
+      message: "td#torPreferences-connectMessageBox-message",
+      button: "button#torPreferences-connectMessageBox-button",
+    },
+    torPreferences: {
+      header: "h1#torPreferences-header",
+      description: "span#torPreferences-description",
+      learnMore: "label#torPreferences-learnMore",
+    },
+    quickstart: {
+      header: "h2#torPreferences-quickstart-header",
+      description: "span#torPreferences-quickstart-description",
+      enableQuickstartCheckbox: "checkbox#torPreferences-quickstart-toggle",
+    },
+    bridges: {
+      header: "h2#torPreferences-bridges-header",
+      description: "span#torPreferences-bridges-description",
+      learnMore: "label#torPreferences-bridges-learnMore",
+      useBridgeCheckbox: "checkbox#torPreferences-bridges-toggle",
+      bridgeSelectionRadiogroup:
+        "radiogroup#torPreferences-bridges-bridgeSelection",
+      builtinBridgeOption: "radio#torPreferences-bridges-radioBuiltin",
+      builtinBridgeList: "menulist#torPreferences-bridges-builtinList",
+      requestBridgeOption: "radio#torPreferences-bridges-radioRequestBridge",
+      requestBridgeButton: "button#torPreferences-bridges-buttonRequestBridge",
+      requestBridgeTextarea:
+        "textarea#torPreferences-bridges-textareaRequestBridge",
+      provideBridgeOption: "radio#torPreferences-bridges-radioProvideBridge",
+      provideBridgeDescription:
+        "description#torPreferences-bridges-descriptionProvideBridge",
+      provideBridgeTextarea:
+        "textarea#torPreferences-bridges-textareaProvideBridge",
+    },
+    advanced: {
+      header: "h2#torPreferences-advanced-header",
+      description: "span#torPreferences-advanced-description",
+      learnMore: "label#torPreferences-advanced-learnMore",
+      useProxyCheckbox: "checkbox#torPreferences-advanced-toggleProxy",
+      proxyTypeLabel: "label#torPreferences-localProxy-type",
+      proxyTypeList: "menulist#torPreferences-localProxy-builtinList",
+      proxyAddressLabel: "label#torPreferences-localProxy-address",
+      proxyAddressTextbox: "input#torPreferences-localProxy-textboxAddress",
+      proxyPortLabel: "label#torPreferences-localProxy-port",
+      proxyPortTextbox: "input#torPreferences-localProxy-textboxPort",
+      proxyUsernameLabel: "label#torPreferences-localProxy-username",
+      proxyUsernameTextbox: "input#torPreferences-localProxy-textboxUsername",
+      proxyPasswordLabel: "label#torPreferences-localProxy-password",
+      proxyPasswordTextbox: "input#torPreferences-localProxy-textboxPassword",
+      useFirewallCheckbox: "checkbox#torPreferences-advanced-toggleFirewall",
+      firewallAllowedPortsLabel: "label#torPreferences-advanced-allowedPorts",
+      firewallAllowedPortsTextbox:
+        "input#torPreferences-advanced-textboxAllowedPorts",
+      torLogsLabel: "label#torPreferences-torLogs",
+      torLogsButton: "button#torPreferences-buttonTorLogs",
+    },
+  }; /* selectors */
+
+  let retval = {
+    // cached frequently accessed DOM elements
+    _messageBox: null,
+    _messageBoxMessage: null,
+    _messageBoxButton: null,
+    _enableQuickstartCheckbox: null,
+    _useBridgeCheckbox: null,
+    _bridgeSelectionRadiogroup: null,
+    _builtinBridgeOption: null,
+    _builtinBridgeMenulist: null,
+    _requestBridgeOption: null,
+    _requestBridgeButton: null,
+    _requestBridgeTextarea: null,
+    _provideBridgeOption: null,
+    _provideBridgeTextarea: null,
+    _useProxyCheckbox: null,
+    _proxyTypeLabel: null,
+    _proxyTypeMenulist: null,
+    _proxyAddressLabel: null,
+    _proxyAddressTextbox: null,
+    _proxyPortLabel: null,
+    _proxyPortTextbox: null,
+    _proxyUsernameLabel: null,
+    _proxyUsernameTextbox: null,
+    _proxyPasswordLabel: null,
+    _proxyPasswordTextbox: null,
+    _useFirewallCheckbox: null,
+    _allowedPortsLabel: null,
+    _allowedPortsTextbox: null,
+
+    // tor network settings
+    _bridgeSettings: null,
+    _proxySettings: null,
+    _firewallSettings: null,
+
+    // disables the provided list of elements
+    _setElementsDisabled(elements, disabled) {
+      for (let currentElement of elements) {
+        currentElement.disabled = disabled;
+      }
+    },
+
+    // populate xul with strings and cache the relevant elements
+    _populateXUL() {
+      // saves tor settings to disk when navigate away from about:preferences
+      window.addEventListener("blur", val => {
+        TorProtocolService.flushSettings();
+      });
+
+      document
+        .querySelector(selectors.category.title)
+        .setAttribute("value", TorStrings.settings.categoryTitle);
+
+      let prefpane = document.getElementById("mainPrefPane");
+
+      // 'Connect to Tor' Message Bar
+
+      this._messageBox = prefpane.querySelector(selectors.messageBox.box);
+      this._messageBoxMessage = prefpane.querySelector(selectors.messageBox.message);
+      this._messageBoxButton = prefpane.querySelector(selectors.messageBox.button);
+      // wire up connect button
+      this._messageBoxButton.addEventListener("click", () => {
+        TorConnect.beginBootstrap();
+        TorConnect.openTorConnect();
+      });
+
+      this._populateMessagebox = () => {
+        if (TorConnect.shouldShowTorConnect &&
+            TorConnect.state === TorConnectState.Configuring) {
+          // set messagebox style and text
+          if (TorProtocolService.torBootstrapErrorOccurred()) {
+            this._messageBox.parentNode.style.display = null;
+            this._messageBox.className = "error";
+            this._messageBoxMessage.innerText = TorStrings.torConnect.tryAgainMessage;
+            this._messageBoxButton.innerText = TorStrings.torConnect.tryAgain;
+          } else {
+            this._messageBox.parentNode.style.display = null;
+            this._messageBox.className = "warning";
+            this._messageBoxMessage.innerText = TorStrings.torConnect.connectMessage;
+            this._messageBoxButton.innerText = TorStrings.torConnect.torConnectButton;
+          }
+        } else {
+          // we need to explicitly hide the groupbox, as switching between
+          // the tor pane and other panes will 'unhide' (via the 'hidden'
+          // attribute) the groupbox, offsetting all of the content down
+          // by the groupbox's margin (even if content is 0 height)
+          this._messageBox.parentNode.style.display = "none";
+          this._messageBox.className = "hidden";
+          this._messageBoxMessage.innerText = "";
+          this._messageBoxButton.innerText = "";
+        }
+      }
+      this._populateMessagebox();
+      Services.obs.addObserver(this, TorConnectTopics.StateChange);
+
+      // update the messagebox whenever we come back to the page
+      window.addEventListener("focus", val => {
+        this._populateMessagebox();
+      });
+
+      // Heading
+      prefpane.querySelector(selectors.torPreferences.header).innerText =
+        TorStrings.settings.torPreferencesHeading;
+      prefpane.querySelector(selectors.torPreferences.description).textContent =
+        TorStrings.settings.torPreferencesDescription;
+      {
+        let learnMore = prefpane.querySelector(
+          selectors.torPreferences.learnMore
+        );
+        learnMore.setAttribute("value", TorStrings.settings.learnMore);
+        learnMore.setAttribute(
+          "href",
+          TorStrings.settings.learnMoreTorBrowserURL
+        );
+      }
+
+      // Quickstart
+      prefpane.querySelector(selectors.quickstart.header).innerText =
+        TorStrings.settings.quickstartHeading;
+      prefpane.querySelector(selectors.quickstart.description).textContent =
+        TorStrings.settings.quickstartDescription;
+
+      this._enableQuickstartCheckbox = prefpane.querySelector(
+        selectors.quickstart.enableQuickstartCheckbox
+      );
+      this._enableQuickstartCheckbox.setAttribute(
+        "label",
+        TorStrings.settings.quickstartCheckbox
+      );
+      this._enableQuickstartCheckbox.addEventListener("command", e => {
+        const checked = this._enableQuickstartCheckbox.checked;
+        TorSettings.quickstart.enabled = checked;
+        TorSettings.saveToPrefs().applySettings();
+      });
+      this._enableQuickstartCheckbox.checked = TorSettings.quickstart.enabled;
+      Services.obs.addObserver(this, TorSettingsTopics.SettingChanged);
+
+      // Bridge setup
+      prefpane.querySelector(selectors.bridges.header).innerText =
+        TorStrings.settings.bridgesHeading;
+      prefpane.querySelector(selectors.bridges.description).textContent =
+        TorStrings.settings.bridgesDescription;
+      {
+        let learnMore = prefpane.querySelector(selectors.bridges.learnMore);
+        learnMore.setAttribute("value", TorStrings.settings.learnMore);
+        learnMore.setAttribute("href", TorStrings.settings.learnMoreBridgesURL);
+      }
+
+      this._useBridgeCheckbox = prefpane.querySelector(
+        selectors.bridges.useBridgeCheckbox
+      );
+      this._useBridgeCheckbox.setAttribute(
+        "label",
+        TorStrings.settings.useBridge
+      );
+      this._useBridgeCheckbox.addEventListener("command", e => {
+        const checked = this._useBridgeCheckbox.checked;
+        gTorPane.onToggleBridge(checked).onUpdateBridgeSettings();
+      });
+      this._bridgeSelectionRadiogroup = prefpane.querySelector(
+        selectors.bridges.bridgeSelectionRadiogroup
+      );
+      this._bridgeSelectionRadiogroup.value = TorBridgeSource.BuiltIn;
+      this._bridgeSelectionRadiogroup.addEventListener("command", e => {
+        const value = this._bridgeSelectionRadiogroup.value;
+        gTorPane.onSelectBridgeOption(value).onUpdateBridgeSettings();
+      });
+
+      // Builtin bridges
+      this._builtinBridgeOption = prefpane.querySelector(
+        selectors.bridges.builtinBridgeOption
+      );
+      this._builtinBridgeOption.setAttribute(
+        "label",
+        TorStrings.settings.selectBridge
+      );
+      this._builtinBridgeOption.setAttribute("value", TorBridgeSource.BuiltIn);
+      this._builtinBridgeMenulist = prefpane.querySelector(
+        selectors.bridges.builtinBridgeList
+      );
+      this._builtinBridgeMenulist.addEventListener("command", e => {
+        gTorPane.onUpdateBridgeSettings();
+      });
+
+      // Request bridge
+      this._requestBridgeOption = prefpane.querySelector(
+        selectors.bridges.requestBridgeOption
+      );
+      this._requestBridgeOption.setAttribute(
+        "label",
+        TorStrings.settings.requestBridgeFromTorProject
+      );
+      this._requestBridgeOption.setAttribute("value", TorBridgeSource.BridgeDB);
+      this._requestBridgeButton = prefpane.querySelector(
+        selectors.bridges.requestBridgeButton
+      );
+      this._requestBridgeButton.setAttribute(
+        "label",
+        TorStrings.settings.requestNewBridge
+      );
+      this._requestBridgeButton.addEventListener("command", () =>
+        gTorPane.onRequestBridge()
+      );
+      this._requestBridgeTextarea = prefpane.querySelector(
+        selectors.bridges.requestBridgeTextarea
+      );
+
+      // Provide a bridge
+      this._provideBridgeOption = prefpane.querySelector(
+        selectors.bridges.provideBridgeOption
+      );
+      this._provideBridgeOption.setAttribute(
+        "label",
+        TorStrings.settings.provideBridge
+      );
+      this._provideBridgeOption.setAttribute(
+        "value",
+        TorBridgeSource.UserProvided
+      );
+      prefpane.querySelector(
+        selectors.bridges.provideBridgeDescription
+      ).textContent = TorStrings.settings.provideBridgeDirections;
+      this._provideBridgeTextarea = prefpane.querySelector(
+        selectors.bridges.provideBridgeTextarea
+      );
+      this._provideBridgeTextarea.setAttribute(
+        "placeholder",
+        TorStrings.settings.provideBridgePlaceholder
+      );
+      this._provideBridgeTextarea.addEventListener("blur", () => {
+        gTorPane.onUpdateBridgeSettings();
+      });
+
+      // Advanced setup
+      prefpane.querySelector(selectors.advanced.header).innerText =
+        TorStrings.settings.advancedHeading;
+      prefpane.querySelector(selectors.advanced.description).textContent =
+        TorStrings.settings.advancedDescription;
+      {
+        let learnMore = prefpane.querySelector(selectors.advanced.learnMore);
+        learnMore.setAttribute("value", TorStrings.settings.learnMore);
+        learnMore.setAttribute(
+          "href",
+          TorStrings.settings.learnMoreNetworkSettingsURL
+        );
+      }
+
+      // Local Proxy
+      this._useProxyCheckbox = prefpane.querySelector(
+        selectors.advanced.useProxyCheckbox
+      );
+      this._useProxyCheckbox.setAttribute(
+        "label",
+        TorStrings.settings.useLocalProxy
+      );
+      this._useProxyCheckbox.addEventListener("command", e => {
+        const checked = this._useProxyCheckbox.checked;
+        gTorPane.onToggleProxy(checked).onUpdateProxySettings();
+      });
+      this._proxyTypeLabel = prefpane.querySelector(
+        selectors.advanced.proxyTypeLabel
+      );
+      this._proxyTypeLabel.setAttribute("value", TorStrings.settings.proxyType);
+
+      let mockProxies = [
+        {
+          value: TorProxyType.Socks4,
+          label: TorStrings.settings.proxyTypeSOCKS4,
+        },
+        {
+          value: TorProxyType.Socks5,
+          label: TorStrings.settings.proxyTypeSOCKS5,
+        },
+        { value: TorProxyType.HTTPS, label: TorStrings.settings.proxyTypeHTTP },
+      ];
+      this._proxyTypeMenulist = prefpane.querySelector(
+        selectors.advanced.proxyTypeList
+      );
+      this._proxyTypeMenulist.addEventListener("command", e => {
+        const value = this._proxyTypeMenulist.value;
+        gTorPane.onSelectProxyType(value).onUpdateProxySettings();
+      });
+      for (let currentProxy of mockProxies) {
+        let menuEntry = document.createXULElement("menuitem");
+        menuEntry.setAttribute("value", currentProxy.value);
+        menuEntry.setAttribute("label", currentProxy.label);
+        this._proxyTypeMenulist
+          .querySelector("menupopup")
+          .appendChild(menuEntry);
+      }
+
+      this._proxyAddressLabel = prefpane.querySelector(
+        selectors.advanced.proxyAddressLabel
+      );
+      this._proxyAddressLabel.setAttribute(
+        "value",
+        TorStrings.settings.proxyAddress
+      );
+      this._proxyAddressTextbox = prefpane.querySelector(
+        selectors.advanced.proxyAddressTextbox
+      );
+      this._proxyAddressTextbox.setAttribute(
+        "placeholder",
+        TorStrings.settings.proxyAddressPlaceholder
+      );
+      this._proxyAddressTextbox.addEventListener("blur", () => {
+        gTorPane.onUpdateProxySettings();
+      });
+      this._proxyPortLabel = prefpane.querySelector(
+        selectors.advanced.proxyPortLabel
+      );
+      this._proxyPortLabel.setAttribute("value", TorStrings.settings.proxyPort);
+      this._proxyPortTextbox = prefpane.querySelector(
+        selectors.advanced.proxyPortTextbox
+      );
+      this._proxyPortTextbox.addEventListener("blur", () => {
+        gTorPane.onUpdateProxySettings();
+      });
+      this._proxyUsernameLabel = prefpane.querySelector(
+        selectors.advanced.proxyUsernameLabel
+      );
+      this._proxyUsernameLabel.setAttribute(
+        "value",
+        TorStrings.settings.proxyUsername
+      );
+      this._proxyUsernameTextbox = prefpane.querySelector(
+        selectors.advanced.proxyUsernameTextbox
+      );
+      this._proxyUsernameTextbox.setAttribute(
+        "placeholder",
+        TorStrings.settings.proxyUsernamePasswordPlaceholder
+      );
+      this._proxyUsernameTextbox.addEventListener("blur", () => {
+        gTorPane.onUpdateProxySettings();
+      });
+      this._proxyPasswordLabel = prefpane.querySelector(
+        selectors.advanced.proxyPasswordLabel
+      );
+      this._proxyPasswordLabel.setAttribute(
+        "value",
+        TorStrings.settings.proxyPassword
+      );
+      this._proxyPasswordTextbox = prefpane.querySelector(
+        selectors.advanced.proxyPasswordTextbox
+      );
+      this._proxyPasswordTextbox.setAttribute(
+        "placeholder",
+        TorStrings.settings.proxyUsernamePasswordPlaceholder
+      );
+      this._proxyPasswordTextbox.addEventListener("blur", () => {
+        gTorPane.onUpdateProxySettings();
+      });
+
+      // Local firewall
+      this._useFirewallCheckbox = prefpane.querySelector(
+        selectors.advanced.useFirewallCheckbox
+      );
+      this._useFirewallCheckbox.setAttribute(
+        "label",
+        TorStrings.settings.useFirewall
+      );
+      this._useFirewallCheckbox.addEventListener("command", e => {
+        const checked = this._useFirewallCheckbox.checked;
+        gTorPane.onToggleFirewall(checked).onUpdateFirewallSettings();
+      });
+      this._allowedPortsLabel = prefpane.querySelector(
+        selectors.advanced.firewallAllowedPortsLabel
+      );
+      this._allowedPortsLabel.setAttribute(
+        "value",
+        TorStrings.settings.allowedPorts
+      );
+      this._allowedPortsTextbox = prefpane.querySelector(
+        selectors.advanced.firewallAllowedPortsTextbox
+      );
+      this._allowedPortsTextbox.setAttribute(
+        "placeholder",
+        TorStrings.settings.allowedPortsPlaceholder
+      );
+      this._allowedPortsTextbox.addEventListener("blur", () => {
+        gTorPane.onUpdateFirewallSettings();
+      });
+
+      // Tor logs
+      prefpane
+        .querySelector(selectors.advanced.torLogsLabel)
+        .setAttribute("value", TorStrings.settings.showTorDaemonLogs);
+      let torLogsButton = prefpane.querySelector(
+        selectors.advanced.torLogsButton
+      );
+      torLogsButton.setAttribute("label", TorStrings.settings.showLogs);
+      torLogsButton.addEventListener("command", () => {
+        gTorPane.onViewTorLogs();
+      });
+
+      // Disable all relevant elements by default
+      this._setElementsDisabled(
+        [
+          this._builtinBridgeOption,
+          this._builtinBridgeMenulist,
+          this._requestBridgeOption,
+          this._requestBridgeButton,
+          this._requestBridgeTextarea,
+          this._provideBridgeOption,
+          this._provideBridgeTextarea,
+          this._proxyTypeLabel,
+          this._proxyTypeMenulist,
+          this._proxyAddressLabel,
+          this._proxyAddressTextbox,
+          this._proxyPortLabel,
+          this._proxyPortTextbox,
+          this._proxyUsernameLabel,
+          this._proxyUsernameTextbox,
+          this._proxyPasswordLabel,
+          this._proxyPasswordTextbox,
+          this._allowedPortsLabel,
+          this._allowedPortsTextbox,
+        ],
+        true
+      );
+
+      // init bridge UI
+      for (let currentBridge of TorBuiltinBridgeTypes) {
+        let menuEntry = document.createXULElement("menuitem");
+        menuEntry.setAttribute("value", currentBridge);
+        menuEntry.setAttribute("label", currentBridge);
+        this._builtinBridgeMenulist
+          .querySelector("menupopup")
+          .appendChild(menuEntry);
+      }
+
+      if (TorSettings.bridges.enabled) {
+        this.onSelectBridgeOption(TorSettings.bridges.source);
+        this.onToggleBridge(
+          TorSettings.bridges.source != TorBridgeSource.Invalid
+        );
+        switch (TorSettings.bridges.source) {
+          case TorBridgeSource.Invalid:
+            break;
+          case TorBridgeSource.BuiltIn:
+            this._builtinBridgeMenulist.value = TorSettings.bridges.builtin_type;
+            break;
+          case TorBridgeSource.BridgeDB:
+            this._requestBridgeTextarea.value = TorSettings.bridges.bridge_strings.join("\n");
+            break;
+          case TorBridgeSource.UserProvided:
+            this._provideBridgeTextarea.value = TorSettings.bridges.bridge_strings.join("\n");
+            break;
+        }
+      }
+
+      // init proxy UI
+      if (TorSettings.proxy.enabled) {
+        this.onToggleProxy(true);
+        this.onSelectProxyType(TorSettings.proxy.type);
+        this._proxyAddressTextbox.value = TorSettings.proxy.address;
+        this._proxyPortTextbox.value = TorSettings.proxy.port;
+        this._proxyUsernameTextbox.value = TorSettings.proxy.username;
+        this._proxyPasswordTextbox.value = TorSettings.proxy.password;
+      }
+
+      // init firewall
+      if (TorSettings.firewall.enabled) {
+        this.onToggleFirewall(true);
+        this._allowedPortsTextbox.value = TorSettings.firewall.allowed_ports.join(", ");
+      }
+    },
+
+    init() {
+      this._populateXUL();
+
+      let onUnload = () => {
+        window.removeEventListener("unload", onUnload);
+        gTorPane.uninit();
+      };
+      window.addEventListener("unload", onUnload);
+    },
+
+    uninit() {
+      // unregister our observer topics
+      Services.obs.removeObserver(this, TorSettingsTopics.SettingChanged);
+      Services.obs.removeObserver(this, TorConnectTopics.StateChange);
+    },
+
+    // whether the page should be present in about:preferences
+    get enabled() {
+      return TorProtocolService.ownsTorDaemon;
+    },
+
+    //
+    // Callbacks
+    //
+
+    observe(subject, topic, data) {
+      switch (topic) {
+       // triggered when a TorSettings param has changed
+        case TorSettingsTopics.SettingChanged: {
+          let obj = subject?.wrappedJSObject;
+          switch(data) {
+            case TorSettingsData.QuickStartEnabled: {
+              this._enableQuickstartCheckbox.checked = obj.value;
+              break;
+            }
+          }
+          break;
+        }
+        // triggered when tor connect state changes and we may
+        // need to update the messagebox
+        case TorConnectTopics.StateChange: {
+          this._populateMessagebox();
+          break;
+        }
+      }
+    },
+
+    // callback when using bridges toggled
+    onToggleBridge(enabled) {
+      this._useBridgeCheckbox.checked = enabled;
+      let disabled = !enabled;
+
+      // first disable all the bridge related elements
+      this._setElementsDisabled(
+        [
+          this._builtinBridgeOption,
+          this._builtinBridgeMenulist,
+          this._requestBridgeOption,
+          this._requestBridgeButton,
+          this._requestBridgeTextarea,
+          this._provideBridgeOption,
+          this._provideBridgeTextarea,
+        ],
+        disabled
+      );
+
+      // and selectively re-enable based on the radiogroup's current value
+      if (enabled) {
+        this.onSelectBridgeOption(this._bridgeSelectionRadiogroup.value);
+      } else {
+        this.onSelectBridgeOption(TorBridgeSource.Invalid);
+      }
+      return this;
+    },
+
+    // callback when a bridge option is selected
+    onSelectBridgeOption(source) {
+      if (typeof source === "string") {
+        source = parseInt(source);
+      }
+
+      // disable all of the bridge elements under radio buttons
+      this._setElementsDisabled(
+        [
+          this._builtinBridgeMenulist,
+          this._requestBridgeButton,
+          this._requestBridgeTextarea,
+          this._provideBridgeTextarea,
+        ],
+        true
+      );
+
+      if (source != TorBridgeSource.Invalid) {
+        this._bridgeSelectionRadiogroup.value = source;
+      }
+
+      switch (source) {
+        case TorBridgeSource.BuiltIn: {
+          this._setElementsDisabled([this._builtinBridgeMenulist], false);
+          break;
+        }
+        case TorBridgeSource.BridgeDB: {
+          this._setElementsDisabled(
+            [this._requestBridgeButton, this._requestBridgeTextarea],
+            false
+          );
+          break;
+        }
+        case TorBridgeSource.UserProvided: {
+          this._setElementsDisabled([this._provideBridgeTextarea], false);
+          break;
+        }
+      }
+      return this;
+    },
+
+    // called when the request bridge button is activated
+    onRequestBridge() {
+      let requestBridgeDialog = new RequestBridgeDialog();
+      requestBridgeDialog.openDialog(
+        gSubDialog,
+        aBridges => {
+          if (aBridges.length > 0) {
+            let bridgeStrings = aBridges.join("\n");
+            TorSettings.bridges.enabled = true;
+            TorSettings.bridges.source = TorBridgeSource.BridgeDB;
+            TorSettings.bridges.bridge_strings = bridgeStrings;
+            TorSettings.saveToPrefs();
+            TorSettings.applySettings().then((result) => {
+              this._requestBridgeTextarea.value = bridgeStrings;
+            });
+          }
+        }
+      );
+      return this;
+    },
+
+    // pushes bridge settings from UI to tor
+    onUpdateBridgeSettings() {
+      let source = this._useBridgeCheckbox.checked
+        ? parseInt(this._bridgeSelectionRadiogroup.value)
+        : TorBridgeSource.Invalid;
+
+      switch (source) {
+        case TorBridgeSource.Invalid: {
+          TorSettings.bridges.enabled = false;
+        }
+        break;
+        case TorBridgeSource.BuiltIn: {
+          // if there is a built-in bridge already selected, use that
+          let bridgeType = this._builtinBridgeMenulist.value;
+          console.log(`bridge type: ${bridgeType}`);
+          if (bridgeType) {
+            TorSettings.bridges.enabled = true;
+            TorSettings.bridges.source = TorBridgeSource.BuiltIn;
+            TorSettings.bridges.builtin_type = bridgeType;
+          } else {
+            TorSettings.bridges.enabled = false;
+          }
+          break;
+        }
+        case TorBridgeSource.BridgeDB: {
+          // if there are bridgedb bridges saved in the text area, use them
+          let bridgeStrings = this._requestBridgeTextarea.value;
+          if (bridgeStrings) {
+            TorSettings.bridges.enabled = true;
+            TorSettings.bridges.source = TorBridgeSource.BridgeDB;
+            TorSettings.bridges.bridge_strings = bridgeStrings;
+          } else {
+            TorSettings.bridges.enabled = false;
+          }
+          break;
+        }
+        case TorBridgeSource.UserProvided: {
+          // if bridges already exist in the text area, use them
+          let bridgeStrings = this._provideBridgeTextarea.value;
+          if (bridgeStrings) {
+            TorSettings.bridges.enabled = true;
+            TorSettings.bridges.source = TorBridgeSource.UserProvided;
+            TorSettings.bridges.bridge_strings = bridgeStrings;
+          } else {
+            TorSettings.bridges.enabled = false;
+          }
+          break;
+        }
+      }
+      TorSettings.saveToPrefs();
+      TorSettings.applySettings();
+
+      return this;
+    },
+
+    // callback when proxy is toggled
+    onToggleProxy(enabled) {
+      this._useProxyCheckbox.checked = enabled;
+      let disabled = !enabled;
+
+      this._setElementsDisabled(
+        [
+          this._proxyTypeLabel,
+          this._proxyTypeMenulist,
+          this._proxyAddressLabel,
+          this._proxyAddressTextbox,
+          this._proxyPortLabel,
+          this._proxyPortTextbox,
+          this._proxyUsernameLabel,
+          this._proxyUsernameTextbox,
+          this._proxyPasswordLabel,
+          this._proxyPasswordTextbox,
+        ],
+        disabled
+      );
+      this.onSelectProxyType(this._proxyTypeMenulist.value);
+      return this;
+    },
+
+    // callback when proxy type is changed
+    onSelectProxyType(value) {
+      if (typeof value === "string") {
+        value = parseInt(value);
+      }
+
+      this._proxyTypeMenulist.value = value;
+      switch (value) {
+        case TorProxyType.Invalid: {
+          this._setElementsDisabled(
+            [
+              this._proxyAddressLabel,
+              this._proxyAddressTextbox,
+              this._proxyPortLabel,
+              this._proxyPortTextbox,
+              this._proxyUsernameLabel,
+              this._proxyUsernameTextbox,
+              this._proxyPasswordLabel,
+              this._proxyPasswordTextbox,
+            ],
+            true
+          ); // DISABLE
+
+          this._proxyAddressTextbox.value = "";
+          this._proxyPortTextbox.value = "";
+          this._proxyUsernameTextbox.value = "";
+          this._proxyPasswordTextbox.value = "";
+          break;
+        }
+        case TorProxyType.Socks4: {
+          this._setElementsDisabled(
+            [
+              this._proxyAddressLabel,
+              this._proxyAddressTextbox,
+              this._proxyPortLabel,
+              this._proxyPortTextbox,
+            ],
+            false
+          ); // ENABLE
+          this._setElementsDisabled(
+            [
+              this._proxyUsernameLabel,
+              this._proxyUsernameTextbox,
+              this._proxyPasswordLabel,
+              this._proxyPasswordTextbox,
+            ],
+            true
+          ); // DISABLE
+
+          this._proxyUsernameTextbox.value = "";
+          this._proxyPasswordTextbox.value = "";
+          break;
+        }
+        case TorProxyType.Socks5:
+        case TorProxyType.HTTPS: {
+          this._setElementsDisabled(
+            [
+              this._proxyAddressLabel,
+              this._proxyAddressTextbox,
+              this._proxyPortLabel,
+              this._proxyPortTextbox,
+              this._proxyUsernameLabel,
+              this._proxyUsernameTextbox,
+              this._proxyPasswordLabel,
+              this._proxyPasswordTextbox,
+            ],
+            false
+          ); // ENABLE
+          break;
+        }
+      }
+      return this;
+    },
+
+    // pushes proxy settings from UI to tor
+    onUpdateProxySettings() {
+      const type = this._useProxyCheckbox.checked
+        ? parseInt(this._proxyTypeMenulist.value)
+        : TorProxyType.Invalid;
+      const address = this._proxyAddressTextbox.value;
+      const port = this._proxyPortTextbox.value;
+      const username = this._proxyUsernameTextbox.value;
+      const password = this._proxyPasswordTextbox.value;
+
+      switch (type) {
+        case TorProxyType.Invalid:
+          TorSettings.proxy.enabled = false;
+          break;
+        case TorProxyType.Socks4:
+          TorSettings.proxy.enabled = true;
+          TorSettings.proxy.type = type;
+          TorSettings.proxy.address = address;
+          TorSettings.proxy.port = port;
+
+          break;
+        case TorProxyType.Socks5:
+          TorSettings.proxy.enabled = true;
+          TorSettings.proxy.type = type;
+          TorSettings.proxy.address = address;
+          TorSettings.proxy.port = port;
+          TorSettings.proxy.username = username;
+          TorSettings.proxy.password = password;
+          break;
+        case TorProxyType.HTTPS:
+          TorSettings.proxy.enabled = true;
+          TorSettings.proxy.type = type;
+          TorSettings.proxy.address = address;
+          TorSettings.proxy.port = port;
+          TorSettings.proxy.username = username;
+          TorSettings.proxy.password = password;
+          break;
+      }
+      TorSettings.saveToPrefs();
+      TorSettings.applySettings();
+
+      return this;
+    },
+
+    // callback when firewall proxy is toggled
+    onToggleFirewall(enabled) {
+      this._useFirewallCheckbox.checked = enabled;
+      let disabled = !enabled;
+
+      this._setElementsDisabled(
+        [this._allowedPortsLabel, this._allowedPortsTextbox],
+        disabled
+      );
+
+      return this;
+    },
+
+    // pushes firewall settings from UI to tor
+    onUpdateFirewallSettings() {
+
+      let portListString = this._useFirewallCheckbox.checked
+        ? this._allowedPortsTextbox.value
+        : "";
+
+      if (portListString) {
+        TorSettings.firewall.enabled = true;
+        TorSettings.firewall.allowed_ports = portListString;
+      } else {
+        TorSettings.firewall.enabled = false;
+      }
+      TorSettings.saveToPrefs();
+      TorSettings.applySettings();
+
+      return this;
+    },
+
+    onViewTorLogs() {
+      let torLogDialog = new TorLogDialog();
+      torLogDialog.openDialog(gSubDialog);
+    },
+  };
+  return retval;
+})(); /* gTorPane */
diff --git a/browser/components/torpreferences/content/torPane.xhtml b/browser/components/torpreferences/content/torPane.xhtml
new file mode 100644
index 000000000000..7c8071f2cf10
--- /dev/null
+++ b/browser/components/torpreferences/content/torPane.xhtml
@@ -0,0 +1,157 @@
+<!-- Tor panel -->
+
+<script type="application/javascript"
+        src="chrome://browser/content/torpreferences/torPane.js"/>
+<html:template id="template-paneTor">
+
+<!-- Tor Connect Message Box -->
+<groupbox data-category="paneTor" hidden="true">
+  <html:div id="torPreferences-connectMessageBox"
+            class="subcategory"
+            data-category="paneTor"
+            hidden="true">
+    <html:table >
+      <html:tr>
+        <html:td>
+          <html:div id="torPreferences-connectMessageBox-icon"/>
+        </html:td>
+        <html:td id="torPreferences-connectMessageBox-message">
+        </html:td>
+        <html:td>
+          <html:button id="torPreferences-connectMessageBox-button">
+          </html:button>
+        </html:td>
+      </html:tr>
+    </html:table>
+  </html:div>
+</groupbox>
+
+<hbox id="torPreferencesCategory"
+      class="subcategory"
+      data-category="paneTor"
+      hidden="true">
+  <html:h1 id="torPreferences-header"/>
+</hbox>
+
+<groupbox data-category="paneTor"
+          hidden="true">
+  <description flex="1">
+    <html:span id="torPreferences-description" class="tail-with-learn-more"/>
+    <label id="torPreferences-learnMore" class="learnMore text-link" is="text-link"/>
+  </description>
+</groupbox>
+
+<!-- Quickstart -->
+<groupbox id="torPreferences-quickstart-group"
+          data-category="paneTor"
+          hidden="true">
+  <html:h2 id="torPreferences-quickstart-header"/>
+  <description flex="1">
+    <html:span id="torPreferences-quickstart-description"/>
+  </description>
+  <checkbox id="torPreferences-quickstart-toggle"/>
+</groupbox>
+
+<!-- Bridges -->
+<groupbox id="torPreferences-bridges-group"
+          data-category="paneTor"
+          hidden="true">
+  <html:h2 id="torPreferences-bridges-header"/>
+  <description flex="1">
+    <html:span id="torPreferences-bridges-description" class="tail-with-learn-more"/>
+    <label id="torPreferences-bridges-learnMore" class="learnMore text-link" is="text-link"/>
+  </description>
+  <checkbox id="torPreferences-bridges-toggle"/>
+  <radiogroup id="torPreferences-bridges-bridgeSelection">
+    <hbox class="indent">
+      <radio id="torPreferences-bridges-radioBuiltin"/>
+      <spacer flex="1"/>
+      <menulist id="torPreferences-bridges-builtinList" class="torMarginFix">
+        <menupopup/>
+      </menulist>
+    </hbox>
+    <vbox class="indent">
+      <hbox>
+        <radio id="torPreferences-bridges-radioRequestBridge"/>
+        <space flex="1"/>
+        <button id="torPreferences-bridges-buttonRequestBridge" class="torMarginFix"/>
+      </hbox>
+      <html:textarea
+        id="torPreferences-bridges-textareaRequestBridge"
+        class="indent torMarginFix"
+        multiline="true"
+        rows="3"
+        readonly="true"/>
+    </vbox>
+    <hbox class="indent" flex="1">
+      <vbox flex="1">
+        <radio id="torPreferences-bridges-radioProvideBridge"/>
+        <description id="torPreferences-bridges-descriptionProvideBridge" class="indent"/>
+        <html:textarea
+          id="torPreferences-bridges-textareaProvideBridge"
+          class="indent torMarginFix"
+          multiline="true"
+          rows="3"/>
+      </vbox>
+    </hbox>
+  </radiogroup>
+</groupbox>
+
+<!-- Advanced -->
+<groupbox id="torPreferences-advanced-group"
+          data-category="paneTor"
+          hidden="true">
+  <html:h2 id="torPreferences-advanced-header"/>
+  <description flex="1">
+    <html:span id="torPreferences-advanced-description" class="tail-with-learn-more"/>
+    <label id="torPreferences-advanced-learnMore" class="learnMore text-link" is="text-link" style="display:none"/>
+  </description>
+  <box id="torPreferences-advanced-grid">
+    <!-- Local Proxy -->
+    <hbox class="torPreferences-advanced-checkbox-container">
+      <checkbox id="torPreferences-advanced-toggleProxy"/>
+    </hbox>
+    <hbox class="indent" align="center">
+      <label id="torPreferences-localProxy-type"/>
+    </hbox>
+    <hbox align="center">
+      <spacer flex="1"/>
+      <menulist id="torPreferences-localProxy-builtinList" class="torMarginFix">
+        <menupopup/>
+      </menulist>
+    </hbox>
+    <hbox class="indent" align="center">
+      <label id="torPreferences-localProxy-address"/>
+    </hbox>
+    <hbox align="center">
+      <html:input id="torPreferences-localProxy-textboxAddress" type="text" class="torMarginFix"/>
+      <label id="torPreferences-localProxy-port"/>
+      <!-- proxy-port-input class style pulled from preferences.css and used in the vanilla proxy setup menu -->
+      <html:input id="torPreferences-localProxy-textboxPort" class="proxy-port-input torMarginFix" hidespinbuttons="true" type="number" min="0" max="65535" maxlength="5"/>
+    </hbox>
+    <hbox class="indent" align="center">
+      <label id="torPreferences-localProxy-username"/>
+    </hbox>
+    <hbox align="center">
+      <html:input id="torPreferences-localProxy-textboxUsername" type="text" class="torMarginFix"/>
+      <label id="torPreferences-localProxy-password"/>
+      <html:input id="torPreferences-localProxy-textboxPassword" class="torMarginFix" type="password"/>
+    </hbox>
+    <!-- Firewall -->
+    <hbox class="torPreferences-advanced-checkbox-container">
+      <checkbox id="torPreferences-advanced-toggleFirewall"/>
+    </hbox>
+    <hbox class="indent" align="center">
+      <label id="torPreferences-advanced-allowedPorts"/>
+    </hbox>
+    <hbox align="center">
+      <html:input id="torPreferences-advanced-textboxAllowedPorts" type="text" class="torMarginFix" value="80,443"/>
+    </hbox>
+  </box>
+  <hbox id="torPreferences-torDaemon-hbox" align="center">
+    <label id="torPreferences-torLogs"/>
+    <spacer flex="1"/>
+    <button id="torPreferences-buttonTorLogs" class="torMarginFix"/>
+  </hbox>
+</groupbox>
+</html:template>
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torPreferences.css b/browser/components/torpreferences/content/torPreferences.css
new file mode 100644
index 000000000000..b6eb0a740e5e
--- /dev/null
+++ b/browser/components/torpreferences/content/torPreferences.css
@@ -0,0 +1,189 @@
+@import url("chrome://branding/content/tor-styles.css");
+
+#category-tor > .category-icon {
+  list-style-image: url("chrome://browser/content/torpreferences/torPreferencesIcon.svg");
+}
+
+/* Connect Message Box */
+
+#torPreferences-connectMessageBox {
+  display: block;
+  position: relative;
+
+  width: auto;
+  min-height: 32px;
+  border-radius: 4px;
+  padding: 8px;
+}
+
+#torPreferences-connectMessageBox.hidden {
+  display: none;
+}
+
+#torPreferences-connectMessageBox.error {
+  background-color: var(--red-60);
+  color: white;
+}
+
+#torPreferences-connectMessageBox.warning {
+  background-color: var(--purple-50);
+  color: white;
+}
+
+#torPreferences-connectMessageBox table {
+  border-collapse: collapse;
+}
+
+#torPreferences-connectMessageBox td {
+  vertical-align: middle;
+}
+
+#torPreferences-connectMessageBox td:first-child {
+  width: 16px;
+}
+
+#torPreferences-connectMessageBox-icon {
+  width: 16px;
+  height: 16px;
+
+  mask-repeat: no-repeat !important;
+  mask-size: 16px !important;
+}
+
+#torPreferences-connectMessageBox.error #torPreferences-connectMessageBox-icon
+{
+  mask: url("chrome://browser/skin/onion-slash.svg");
+  background-color: white;
+}
+
+#torPreferences-connectMessageBox.warning #torPreferences-connectMessageBox-icon
+{
+  mask: url("chrome://browser/skin/onion.svg");
+  background-color: white;
+}
+
+#torPreferences-connectMessageBox-message {
+  line-height: 16px;
+  font-size: 1.11em;
+  padding-left: 8px!important;
+}
+
+#torPreferences-connectMessageBox-button {
+  display: block;
+  width: auto;
+
+  border-radius: 4px;
+  border: 0;
+
+  padding-inline: 18px;
+  padding-block: 8px;
+  margin-block: 0px;
+  margin-inline-start: 8px;
+  margin-inline-end: 0px;
+
+  font-size: 1.0em;
+  font-weight: 600;
+  white-space: nowrap;
+
+  color: white;
+}
+
+#torPreferences-connectMessageBox.error #torPreferences-connectMessageBox-button {
+  background-color: var(--red-70);
+}
+
+#torPreferences-connectMessageBox.error #torPreferences-connectMessageBox-button:hover {
+  background-color: var(--red-80);
+}
+
+#torPreferences-connectMessageBox.error #torPreferences-connectMessageBox-button:active {
+  background-color: var(--red-90);
+}
+
+#torPreferences-connectMessageBox.warning #torPreferences-connectMessageBox-button {
+  background-color: var(--purple-70);
+}
+
+#torPreferences-connectMessageBox.warning #torPreferences-connectMessageBox-button:hover {
+  background-color: var(--purple-80);
+}
+
+#torPreferences-connectMessageBox.warning #torPreferences-connectMessageBox-button:active {
+  background-color: var(--purple-90);
+}
+
+/* Advanced Settings */
+
+#torPreferences-advanced-grid {
+  display: grid;
+  grid-template-columns: auto 1fr;
+}
+
+.torPreferences-advanced-checkbox-container {
+  grid-column: 1 / 3;
+}
+
+#torPreferences-localProxy-textboxAddress,
+#torPreferences-localProxy-textboxUsername,
+#torPreferences-localProxy-textboxPassword,
+#torPreferences-advanced-textboxAllowedPorts {
+  -moz-box-flex: 1;
+}
+
+hbox#torPreferences-torDaemon-hbox {
+  margin-top: 20px;
+}
+
+description#torPreferences-requestBridge-description {
+  /*margin-bottom: 1em;*/
+  min-height: 2em;
+}
+
+image#torPreferences-requestBridge-captchaImage {
+  margin: 1em;
+  min-height: 125px;
+}
+
+button#torPreferences-requestBridge-refreshCaptchaButton {
+  min-width: initial;
+}
+
+dialog#torPreferences-requestBridge-dialog > hbox {
+  margin-bottom: 1em;
+}
+
+/*
+ Various elements that really should be lining up don't because they have inconsistent margins
+*/
+.torMarginFix {
+  margin-left : 4px;
+  margin-right : 4px;
+}
+
+/*
+  This hbox is hidden by css here by default so that the
+  xul dialog allocates enough screen space for the error message
+  element, otherwise it gets cut off since dialog's overflow is hidden
+*/
+hbox#torPreferences-requestBridge-incorrectCaptchaHbox {
+  visibility: hidden;
+}
+
+image#torPreferences-requestBridge-errorIcon {
+  list-style-image: url("chrome://browser/skin/warning.svg");
+}
+
+groupbox#torPreferences-bridges-group textarea {
+  white-space: pre;
+  overflow: auto;
+}
+
+textarea#torPreferences-torDialog-textarea {
+  -moz-box-flex: 1;
+  font-family: monospace;
+  font-size: 0.8em;
+  white-space: pre;
+  overflow: auto;
+  /* 10 lines */
+  min-height: 20em;
+}
\ No newline at end of file
diff --git a/browser/components/torpreferences/content/torPreferencesIcon.svg b/browser/components/torpreferences/content/torPreferencesIcon.svg
new file mode 100644
index 000000000000..382a061774aa
--- /dev/null
+++ b/browser/components/torpreferences/content/torPreferencesIcon.svg
@@ -0,0 +1,8 @@
+<svg fill="context-fill" fill-opacity="context-fill-opacity" viewBox="0 0 16 16" width="16" height="16" xmlns="http://www.w3.org/2000/svg";>
+  <g clip-rule="evenodd" fill-rule="evenodd">
+    <path d="m11 8c0 1.65686-1.34314 3-3 3-1.65685 0-3-1.34314-3-3 0-1.65685 1.34315-3 3-3 1.65686 0 3 1.34315 3 3zm-1.17187 0c0 1.00965-.81848 1.82813-1.82813 1.82813-1.00964 0-1.82812-.81848-1.82812-1.82813 0-1.00964.81848-1.82812 1.82812-1.82812 1.00965 0 1.82813.81848 1.82813 1.82812z"/>
+    <path d="m7.99999 13.25c2.89951 0 5.25001-2.3505 5.25001-5.25001 0-2.89949-2.3505-5.25-5.25001-5.25-2.89949 0-5.25 2.35051-5.25 5.25 0 2.89951 2.35051 5.25001 5.25 5.25001zm0-1.1719c2.25231 0 4.07811-1.8258 4.07811-4.07811 0-2.25228-1.8258-4.07812-4.07811-4.07812-2.25228 0-4.07812 1.82584-4.07812 4.07812 0 2.25231 1.82584 4.07811 4.07812 4.07811z"/>
+    <path d="m8 15.5c4.1421 0 7.5-3.3579 7.5-7.5 0-4.14214-3.3579-7.5-7.5-7.5-4.14214 0-7.5 3.35786-7.5 7.5 0 4.1421 3.35786 7.5 7.5 7.5zm0-1.1719c3.4949 0 6.3281-2.8332 6.3281-6.3281 0-3.49493-2.8332-6.32812-6.3281-6.32812-3.49493 0-6.32812 2.83319-6.32812 6.32812 0 3.4949 2.83319 6.3281 6.32812 6.3281z"/>
+  </g>
+  <path d="m.5 8c0 4.1421 3.35786 7.5 7.5 7.5v-15c-4.14214 0-7.5 3.35786-7.5 7.5z"/>
+</svg>
\ No newline at end of file
diff --git a/browser/components/torpreferences/jar.mn b/browser/components/torpreferences/jar.mn
new file mode 100644
index 000000000000..552c92b2feff
--- /dev/null
+++ b/browser/components/torpreferences/jar.mn
@@ -0,0 +1,10 @@
+browser.jar:
+    content/browser/torpreferences/requestBridgeDialog.xhtml         (content/requestBridgeDialog.xhtml)
+    content/browser/torpreferences/requestBridgeDialog.jsm           (content/requestBridgeDialog.jsm)
+    content/browser/torpreferences/torCategory.inc.xhtml             (content/torCategory.inc.xhtml)
+    content/browser/torpreferences/torLogDialog.jsm                  (content/torLogDialog.jsm)
+    content/browser/torpreferences/torLogDialog.xhtml                (content/torLogDialog.xhtml)
+    content/browser/torpreferences/torPane.js                        (content/torPane.js)
+    content/browser/torpreferences/torPane.xhtml                     (content/torPane.xhtml)
+    content/browser/torpreferences/torPreferences.css                (content/torPreferences.css)
+    content/browser/torpreferences/torPreferencesIcon.svg            (content/torPreferencesIcon.svg)
diff --git a/browser/components/torpreferences/moz.build b/browser/components/torpreferences/moz.build
new file mode 100644
index 000000000000..2661ad7cb9f3
--- /dev/null
+++ b/browser/components/torpreferences/moz.build
@@ -0,0 +1 @@
+JAR_MANIFESTS += ["jar.mn"]



_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits