This is an automated email from the git hooks/post-receive script.
richard pushed a commit to branch tor-browser-91.9.0esr-11.5-1 in repository tor-browser.
commit d846a522268fd45365d782e1db1b11a9488ab77b Author: Richard Pospesel richard@torproject.org AuthorDate: Mon Sep 16 15:25:39 2019 -0700
Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
This patch adds a new about:preferences#connection 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
Bug 40774: Update about:preferences page to match new UI designs --- 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 | 6 + .../torpreferences/content/bridgeQrDialog.jsm | 51 + .../torpreferences/content/bridgeQrDialog.xhtml | 23 + .../torpreferences/content/builtinBridgeDialog.jsm | 142 +++ .../content/builtinBridgeDialog.xhtml | 43 + .../content/connectionCategory.inc.xhtml | 9 + .../torpreferences/content/connectionPane.js | 1315 ++++++++++++++++++++ .../torpreferences/content/connectionPane.xhtml | 177 +++ .../content/connectionSettingsDialog.jsm | 393 ++++++ .../content/connectionSettingsDialog.xhtml | 62 + .../components/torpreferences/content/network.svg | 6 + .../torpreferences/content/provideBridgeDialog.jsm | 69 + .../content/provideBridgeDialog.xhtml | 21 + .../torpreferences/content/requestBridgeDialog.jsm | 211 ++++ .../content/requestBridgeDialog.xhtml | 35 + .../torpreferences/content/torLogDialog.jsm | 84 ++ .../torpreferences/content/torLogDialog.xhtml | 23 + .../torpreferences/content/torPreferences.css | 541 ++++++++ .../torpreferences/content/torPreferencesIcon.svg | 8 + browser/components/torpreferences/jar.mn | 19 + browser/components/torpreferences/moz.build | 1 + 25 files changed, 3249 insertions(+), 68 deletions(-)
diff --git a/browser/components/moz.build b/browser/components/moz.build index 1bc09f4093fb7..66de87290bd83 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 a89b89f723a80..594711e614747 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 2a6ba4a3d8e42..501ba9144a314 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 a3656f827ffc6..5981bcd38fc83 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/connectionPane.js */
"use strict";
@@ -136,6 +137,14 @@ function init_all() { register_module("paneSync", gSyncPane); } register_module("paneSearchResults", gSearchResultsPane); + if (gConnectionPane.enabled) { + document.getElementById("category-connection").hidden = false; + register_module("paneConnection", gConnectionPane); + } else { + // Remove the pane from the DOM so it doesn't get incorrectly included in search results. + document.getElementById("template-paneConnection").remove(); + } + gSearchResultsPane.init(); gMainPane.preInit();
diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml index 32184867ac179..9ee09581de32a 100644 --- a/browser/components/preferences/preferences.xhtml +++ b/browser/components/preferences/preferences.xhtml @@ -6,12 +6,14 @@ <?xml-stylesheet href="chrome://global/skin/global.css"?>
<?xml-stylesheet href="chrome://global/skin/in-content/common.css"?> +<?xml-stylesheet href="chrome://global/skin/in-content/toggle-button.css"?> <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> <?xml-stylesheet href="chrome://browser/content/preferences/dialogs/handlers.css"?> <?xml-stylesheet href="chrome://browser/skin/preferences/applications.css"?> <?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 +156,9 @@ <image class="category-icon"/> <label class="category-name" flex="1" data-l10n-id="pane-experimental-title"></label> </richlistitem> + +#include ../torpreferences/content/connectionCategory.inc.xhtml + </richlistbox>
<spacer flex="1"/> @@ -207,6 +212,7 @@ #include containers.inc.xhtml #include sync.inc.xhtml #include experimental.inc.xhtml +#include ../torpreferences/content/connectionPane.xhtml </vbox> </vbox> </vbox> diff --git a/browser/components/torpreferences/content/bridgeQrDialog.jsm b/browser/components/torpreferences/content/bridgeQrDialog.jsm new file mode 100644 index 0000000000000..e63347742ea50 --- /dev/null +++ b/browser/components/torpreferences/content/bridgeQrDialog.jsm @@ -0,0 +1,51 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["BridgeQrDialog"]; + +const { QRCode } = ChromeUtils.import("resource://gre/modules/QRCode.jsm"); + +const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm"); + +class BridgeQrDialog { + constructor() { + this._bridgeString = ""; + } + + static get selectors() { + return { + target: "#bridgeQr-target", + }; + } + + _populateXUL(window, dialog) { + dialog.parentElement.setAttribute("title", TorStrings.settings.scanQrTitle); + const target = dialog.querySelector(BridgeQrDialog.selectors.target); + const style = window.getComputedStyle(target); + const width = style.width.substr(0, style.width.length - 2); + const height = style.height.substr(0, style.height.length - 2); + new QRCode(target, { + text: this._bridgeString, + width, + height, + colorDark: style.color, + colorLight: style.backgroundColor, + document: window.document, + }); + } + + init(window, dialog) { + // Defer to later until Firefox has populated the dialog with all our elements + window.setTimeout(() => { + this._populateXUL(window, dialog); + }, 0); + } + + openDialog(gSubDialog, bridgeString) { + this._bridgeString = bridgeString; + gSubDialog.open( + "chrome://browser/content/torpreferences/bridgeQrDialog.xhtml", + { features: "resizable=yes" }, + this + ); + } +} diff --git a/browser/components/torpreferences/content/bridgeQrDialog.xhtml b/browser/components/torpreferences/content/bridgeQrDialog.xhtml new file mode 100644 index 0000000000000..2a49e4c0e7d9e --- /dev/null +++ b/browser/components/torpreferences/content/bridgeQrDialog.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%22%3E +<dialog id="bridgeQr-dialog" buttons="accept"> + <html:div id="bridgeQr-container"> + <html:div id="bridgeQr-target"/> + <html:div id="bridgeQr-onionBox"/> + <html:div id="bridgeQr-onion"/> + </html:div> + <script type="application/javascript"><![CDATA[ + "use strict"; + + let dialogObject = window.arguments[0]; + let dialogElement = document.getElementById("bridgeQr-dialog"); + dialogObject.init(window, dialogElement); + ]]></script> +</dialog> +</window> diff --git a/browser/components/torpreferences/content/builtinBridgeDialog.jsm b/browser/components/torpreferences/content/builtinBridgeDialog.jsm new file mode 100644 index 0000000000000..1d4dda8f5ca9c --- /dev/null +++ b/browser/components/torpreferences/content/builtinBridgeDialog.jsm @@ -0,0 +1,142 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["BuiltinBridgeDialog"]; + +const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm"); + +const { + TorSettings, + TorBridgeSource, + TorBuiltinBridgeTypes, +} = ChromeUtils.import("resource:///modules/TorSettings.jsm"); + +class BuiltinBridgeDialog { + constructor() { + this._dialog = null; + this._bridgeType = ""; + this._windowPadding = 0; + } + + static get selectors() { + return { + header: "#torPreferences-builtinBridge-header", + description: "#torPreferences-builtinBridge-description", + radiogroup: "#torPreferences-builtinBridge-typeSelection", + obfsRadio: "#torPreferences-builtinBridges-radioObfs", + obfsDescr: "#torPreferences-builtinBridges-descrObfs", + snowflakeRadio: "#torPreferences-builtinBridges-radioSnowflake", + snowflakeDescr: "#torPreferences-builtinBridges-descrSnowflake", + meekAzureRadio: "#torPreferences-builtinBridges-radioMeekAzure", + meekAzureDescr: "#torPreferences-builtinBridges-descrMeekAzure", + }; + } + + _populateXUL(window, aDialog) { + const selectors = BuiltinBridgeDialog.selectors; + + this._dialog = aDialog; + const dialogWin = this._dialog.parentElement; + { + dialogWin.setAttribute("title", TorStrings.settings.builtinBridgeTitle); + let windowStyle = window.getComputedStyle(dialogWin); + this._windowPadding = + parseFloat(windowStyle.paddingLeft) + + parseFloat(windowStyle.paddingRight); + } + const initialWidth = dialogWin.clientWidth - this._windowPadding; + + this._dialog.querySelector(selectors.header).textContent = + TorStrings.settings.builtinBridgeHeader; + this._dialog.querySelector(selectors.description).textContent = + TorStrings.settings.builtinBridgeDescription; + let radioGroup = this._dialog.querySelector(selectors.radiogroup); + + let types = { + obfs4: { + elemRadio: this._dialog.querySelector(selectors.obfsRadio), + elemDescr: this._dialog.querySelector(selectors.obfsDescr), + label: TorStrings.settings.builtinBridgeObfs4, + descr: TorStrings.settings.builtinBridgeObfs4Description, + }, + snowflake: { + elemRadio: this._dialog.querySelector(selectors.snowflakeRadio), + elemDescr: this._dialog.querySelector(selectors.snowflakeDescr), + label: TorStrings.settings.builtinBridgeSnowflake, + descr: TorStrings.settings.builtinBridgeSnowflakeDescription, + }, + "meek-azure": { + elemRadio: this._dialog.querySelector(selectors.meekAzureRadio), + elemDescr: this._dialog.querySelector(selectors.meekAzureDescr), + label: TorStrings.settings.builtinBridgeMeekAzure, + descr: TorStrings.settings.builtinBridgeMeekAzureDescription, + }, + }; + + TorBuiltinBridgeTypes.forEach(type => { + types[type].elemRadio.parentElement.setAttribute("hidden", "false"); + types[type].elemDescr.parentElement.setAttribute("hidden", "false"); + types[type].elemRadio.setAttribute("label", types[type].label); + types[type].elemDescr.textContent = types[type].descr; + }); + + if ( + TorSettings.bridges.enabled && + TorSettings.bridges.source == TorBridgeSource.BuiltIn + ) { + radioGroup.selectedItem = + types[TorSettings.bridges.builtin_type]?.elemRadio; + this._bridgeType = TorSettings.bridges.builtin_type; + } else { + radioGroup.selectedItem = null; + this._bridgeType = ""; + } + + // Use the initial width, because the window is expanded when we add texts + this.resized(initialWidth); + + this._dialog.addEventListener("dialogaccept", e => { + this._bridgeType = radioGroup.value; + }); + this._dialog.addEventListener("dialoghelp", e => { + window.top.openTrustedLinkIn( + "https://tb-manual.torproject.org/circumvention/", + "tab" + ); + }); + } + + resized(width) { + if (this._dialog === null) { + return; + } + const dialogWin = this._dialog.parentElement; + if (width === undefined) { + width = dialogWin.clientWidth - this._windowPadding; + } + let windowPos = dialogWin.getBoundingClientRect(); + dialogWin.querySelectorAll("div").forEach(div => { + let divPos = div.getBoundingClientRect(); + div.style.width = width - (divPos.left - windowPos.left) + "px"; + }); + } + + init(window, aDialog) { + // defer to later until firefox has populated the dialog with all our elements + window.setTimeout(() => { + this._populateXUL(window, aDialog); + }, 0); + } + + openDialog(gSubDialog, aCloseCallback) { + gSubDialog.open( + "chrome://browser/content/torpreferences/builtinBridgeDialog.xhtml", + { + features: "resizable=yes", + closingCallback: () => { + aCloseCallback(this._bridgeType); + }, + }, + this + ); + } +} diff --git a/browser/components/torpreferences/content/builtinBridgeDialog.xhtml b/browser/components/torpreferences/content/builtinBridgeDialog.xhtml new file mode 100644 index 0000000000000..c1bf202ca1be5 --- /dev/null +++ b/browser/components/torpreferences/content/builtinBridgeDialog.xhtml @@ -0,0 +1,43 @@ +<?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%22%3E +<dialog id="torPreferences-builtinBridge-dialog" + buttons="help,accept,cancel"> + <html:h3 id="torPreferences-builtinBridge-header">​</html:h3> + <description> + <html:div id="torPreferences-builtinBridge-description">​<br/>​</html:div> + </description> + <radiogroup id="torPreferences-builtinBridge-typeSelection"> + <hbox hidden="true"> + <radio id="torPreferences-builtinBridges-radioObfs" value="obfs4"/> + </hbox> + <hbox hidden="true" class="indent"> + <html:div id="torPreferences-builtinBridges-descrObfs"></html:div> + </hbox> + <hbox hidden="true"> + <radio id="torPreferences-builtinBridges-radioSnowflake" value="snowflake"/> + </hbox> + <hbox hidden="true" class="indent"> + <html:div id="torPreferences-builtinBridges-descrSnowflake"></html:div> + </hbox> + <hbox hidden="true"> + <radio id="torPreferences-builtinBridges-radioMeekAzure" value="meek-azure"/> + </hbox> + <hbox hidden="true" class="indent"> + <html:div id="torPreferences-builtinBridges-descrMeekAzure"></html:div> + </hbox> + </radiogroup> + <script type="application/javascript"><![CDATA[ + "use strict"; + + let builtinBridgeDialog = window.arguments[0]; + let dialog = document.getElementById("torPreferences-builtinBridge-dialog"); + builtinBridgeDialog.init(window, dialog); + ]]></script> +</dialog> +</window> diff --git a/browser/components/torpreferences/content/connectionCategory.inc.xhtml b/browser/components/torpreferences/content/connectionCategory.inc.xhtml new file mode 100644 index 0000000000000..15cf24cfe6950 --- /dev/null +++ b/browser/components/torpreferences/content/connectionCategory.inc.xhtml @@ -0,0 +1,9 @@ +<richlistitem id="category-connection" + class="category" + value="paneConnection" + helpTopic="prefs-connection" + align="center" + hidden="true"> + <image class="category-icon"/> + <label id="torPreferences-labelCategory" class="category-name" flex="1" value="Connection"/> +</richlistitem> diff --git a/browser/components/torpreferences/content/connectionPane.js b/browser/components/torpreferences/content/connectionPane.js new file mode 100644 index 0000000000000..309d6498a0c80 --- /dev/null +++ b/browser/components/torpreferences/content/connectionPane.js @@ -0,0 +1,1315 @@ +"use strict"; + +/* global Services, gSubDialog */ + +const { setTimeout, clearTimeout } = ChromeUtils.import( + "resource://gre/modules/Timer.jsm" +); + +const { + TorSettings, + TorSettingsTopics, + TorSettingsData, + TorBridgeSource, +} = ChromeUtils.import("resource:///modules/TorSettings.jsm"); + +const { TorProtocolService } = ChromeUtils.import( + "resource:///modules/TorProtocolService.jsm" +); + +const { + TorConnect, + TorConnectTopics, + TorConnectState, + TorCensorshipLevel, +} = ChromeUtils.import("resource:///modules/TorConnect.jsm"); + +const { TorLogDialog } = ChromeUtils.import( + "chrome://browser/content/torpreferences/torLogDialog.jsm" +); + +const { ConnectionSettingsDialog } = ChromeUtils.import( + "chrome://browser/content/torpreferences/connectionSettingsDialog.jsm" +); + +const { BridgeQrDialog } = ChromeUtils.import( + "chrome://browser/content/torpreferences/bridgeQrDialog.jsm" +); + +const { BuiltinBridgeDialog } = ChromeUtils.import( + "chrome://browser/content/torpreferences/builtinBridgeDialog.jsm" +); + +const { RequestBridgeDialog } = ChromeUtils.import( + "chrome://browser/content/torpreferences/requestBridgeDialog.jsm" +); + +const { ProvideBridgeDialog } = ChromeUtils.import( + "chrome://browser/content/torpreferences/provideBridgeDialog.jsm" +); + +const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm"); + +const { QRCode } = ChromeUtils.import("resource://gre/modules/QRCode.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "TorStrings", + "resource:///modules/TorStrings.jsm" +); + +const InternetStatus = Object.freeze({ + Unknown: 0, + Online: 1, + Offline: -1, +}); + +/* + Connection Pane + + Code for populating the XUL in about:preferences#connection, handling input events, interfacing with tor-launcher +*/ +const gConnectionPane = (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", + }, + status: { + internetLabel: "#torPreferences-status-internet-label", + internetTest: "#torPreferences-status-internet-test", + internetIcon: "#torPreferences-status-internet-statusIcon", + internetStatus: "#torPreferences-status-internet-status", + torLabel: "#torPreferences-status-tor-label", + torIcon: "#torPreferences-status-tor-statusIcon", + torStatus: "#torPreferences-status-tor-status", + }, + quickstart: { + header: "h2#torPreferences-quickstart-header", + description: "span#torPreferences-quickstart-description", + enableQuickstartCheckbox: "checkbox#torPreferences-quickstart-toggle", + }, + bridges: { + header: "h1#torPreferences-bridges-header", + description: "span#torPreferences-bridges-description", + learnMore: "label#torPreferences-bridges-learnMore", + locationGroup: "#torPreferences-bridges-locationGroup", + locationLabel: "#torPreferences-bridges-locationLabel", + location: "#torPreferences-bridges-location", + locationEntries: "#torPreferences-bridges-locationEntries", + chooseForMe: "#torPreferences-bridges-buttonChooseBridgeForMe", + currentHeader: "#torPreferences-currentBridges-header", + currentHeaderText: "#torPreferences-currentBridges-headerText", + switch: "#torPreferences-currentBridges-switch", + cards: "#torPreferences-currentBridges-cards", + cardTemplate: "#torPreferences-bridgeCard-template", + card: ".torPreferences-bridgeCard", + cardId: ".torPreferences-bridgeCard-id", + cardHeadingAddr: ".torPreferences-bridgeCard-headingAddr", + cardConnectedLabel: ".torPreferences-bridgeCard-connectedLabel", + cardOptions: ".torPreferences-bridgeCard-options", + cardMenu: "#torPreferences-bridgeCard-menu", + cardQrGrid: ".torPreferences-bridgeCard-grid", + cardQrContainer: ".torPreferences-bridgeCard-qr", + cardQr: ".torPreferences-bridgeCard-qrCode", + cardShare: ".torPreferences-bridgeCard-share", + cardAddr: ".torPreferences-bridgeCard-addr", + cardLearnMore: ".torPreferences-bridgeCard-learnMore", + cardCopy: ".torPreferences-bridgeCard-copyButton", + showAll: "#torPreferences-currentBridges-showAll", + removeAll: "#torPreferences-currentBridges-removeAll", + addHeader: "#torPreferences-addBridge-header", + addBuiltinLabel: "#torPreferences-addBridge-labelBuiltinBridge", + addBuiltinButton: "#torPreferences-addBridge-buttonBuiltinBridge", + requestLabel: "#torPreferences-addBridge-labelRequestBridge", + requestButton: "#torPreferences-addBridge-buttonRequestBridge", + enterLabel: "#torPreferences-addBridge-labelEnterBridge", + enterButton: "#torPreferences-addBridge-buttonEnterBridge", + }, + advanced: { + header: "h1#torPreferences-advanced-header", + label: "#torPreferences-advanced-label", + button: "#torPreferences-advanced-button", + torLogsLabel: "label#torPreferences-torLogs", + torLogsButton: "button#torPreferences-buttonTorLogs", + }, + }; /* selectors */ + + let retval = { + // cached frequently accessed DOM elements + _enableQuickstartCheckbox: null, + + _internetStatus: InternetStatus.Unknown, + + _controller: null, + + _currentBridge: "", + + // 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 + + const messageBox = prefpane.querySelector(selectors.messageBox.box); + const messageBoxMessage = prefpane.querySelector( + selectors.messageBox.message + ); + const messageBoxButton = prefpane.querySelector( + selectors.messageBox.button + ); + // wire up connect button + messageBoxButton.addEventListener("click", () => { + TorConnect.beginBootstrap(); + TorConnect.openTorConnect(); + }); + + this._populateMessagebox = () => { + if ( + TorConnect.shouldShowTorConnect && + TorConnect.state === TorConnectState.Configuring + ) { + // set messagebox style and text + if (TorProtocolService.torBootstrapErrorOccurred()) { + messageBox.parentNode.style.display = null; + messageBox.className = "error"; + messageBoxMessage.innerText = TorStrings.torConnect.tryAgainMessage; + messageBoxButton.innerText = TorStrings.torConnect.tryAgain; + } else { + messageBox.parentNode.style.display = null; + messageBox.className = "warning"; + messageBoxMessage.innerText = TorStrings.torConnect.connectMessage; + 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) + messageBox.parentNode.style.display = "none"; + messageBox.className = "hidden"; + messageBoxMessage.innerText = ""; + messageBoxButton.innerText = ""; + } + }; + this._populateMessagebox(); + + // Heading + prefpane.querySelector(selectors.torPreferences.header).innerText = + TorStrings.settings.categoryTitle; + 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 + ); + } + + // Internet and Tor status + prefpane.querySelector(selectors.status.internetLabel).textContent = + TorStrings.settings.statusInternetLabel; + prefpane.querySelector(selectors.status.torLabel).textContent = + TorStrings.settings.statusTorLabel; + const internetTest = prefpane.querySelector( + selectors.status.internetTest + ); + internetTest.setAttribute( + "label", + TorStrings.settings.statusInternetTest + ); + internetTest.addEventListener("command", async () => { + this.onInternetTest(); + }); + const internetIcon = prefpane.querySelector( + selectors.status.internetIcon + ); + const internetStatus = prefpane.querySelector( + selectors.status.internetStatus + ); + const torIcon = prefpane.querySelector(selectors.status.torIcon); + const torStatus = prefpane.querySelector(selectors.status.torStatus); + this._populateStatus = () => { + switch (this._internetStatus) { + case InternetStatus.Unknown: + internetTest.removeAttribute("hidden"); + break; + case InternetStatus.Online: + internetTest.setAttribute("hidden", "true"); + internetIcon.className = "online"; + internetStatus.textContent = + TorStrings.settings.statusInternetOnline; + break; + case InternetStatus.Offline: + internetTest.setAttribute("hidden", "true"); + internetIcon.className = "offline"; + internetStatus.textContent = + TorStrings.settings.statusInternetOffline; + break; + } + if (TorConnect.state === TorConnectState.Bootstrapped) { + torIcon.className = "connected"; + torStatus.textContent = TorStrings.settings.statusTorConnected; + } else if ( + TorConnect.detectedCensorshipLevel > TorCensorshipLevel.None + ) { + torIcon.className = "blocked"; + torStatus.textContent = TorStrings.settings.statusTorBlocked; + } else { + torIcon.className = ""; + torStatus.textContent = TorStrings.settings.statusTorNotConnected; + } + }; + this._populateStatus(); + + // 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); + } + + // Location + { + const locationGroup = prefpane.querySelector( + selectors.bridges.locationGroup + ); + prefpane.querySelector(selectors.bridges.locationLabel).textContent = + TorStrings.settings.bridgeLocation; + const location = prefpane.querySelector(selectors.bridges.location); + const locationEntries = prefpane.querySelector( + selectors.bridges.locationEntries + ); + const chooseForMe = prefpane.querySelector( + selectors.bridges.chooseForMe + ); + chooseForMe.setAttribute( + "label", + TorStrings.settings.bridgeChooseForMe + ); + chooseForMe.addEventListener("command", e => { + TorConnect.beginAutoBootstrap(location.value); + }); + this._populateLocations = () => { + let value = location.value; + locationEntries.textContent = ""; + + { + const item = document.createXULElement("menuitem"); + item.setAttribute("value", ""); + item.setAttribute( + "label", + TorStrings.settings.bridgeLocationAutomatic + ); + locationEntries.appendChild(item); + } + + const codes = TorConnect.countryCodes; + const items = codes.map(code => { + const item = document.createXULElement("menuitem"); + item.setAttribute("value", code); + item.setAttribute( + "label", + TorConnect.countryNames[code] + ? TorConnect.countryNames[code] + : code + ); + return item; + }); + items.sort((left, right) => + left.textContent.localeCompare(right.textContent) + ); + locationEntries.append(...items); + location.value = value; + }; + this._showAutoconfiguration = () => { + if ( + !TorConnect.shouldShowTorConnect || + !TorProtocolService.torBootstrapErrorOccurred() + ) { + locationGroup.setAttribute("hidden", "true"); + return; + } + // Populate locations, even though we will show only the automatic + // item for a moment. In my opinion showing the button immediately is + // better then waiting for the Moat query to finish (after a while) + // and showing the controls only after that. + this._populateLocations(); + locationGroup.removeAttribute("hidden"); + if (!TorConnect.countryCodes.length) { + TorConnect.getCountryCodes().then(() => this._populateLocations()); + } + }; + this._showAutoconfiguration(); + } + + // Bridge cards + const bridgeHeader = prefpane.querySelector( + selectors.bridges.currentHeader + ); + bridgeHeader.querySelector( + selectors.bridges.currentHeaderText + ).textContent = TorStrings.settings.bridgeCurrent; + const bridgeSwitch = bridgeHeader.querySelector(selectors.bridges.switch); + bridgeSwitch.addEventListener("change", () => { + TorSettings.bridges.enabled = bridgeSwitch.checked; + TorSettings.saveToPrefs(); + TorSettings.applySettings().then(result => { + this._populateBridgeCards(); + }); + }); + const bridgeTemplate = prefpane.querySelector( + selectors.bridges.cardTemplate + ); + { + const learnMore = bridgeTemplate.querySelector( + selectors.bridges.cardLearnMore + ); + learnMore.setAttribute("value", TorStrings.settings.learnMore); + learnMore.setAttribute("href", "about:blank"); + } + bridgeTemplate.querySelector( + selectors.bridges.cardConnectedLabel + ).textContent = TorStrings.settings.statusTorConnected; + bridgeTemplate + .querySelector(selectors.bridges.cardCopy) + .setAttribute("label", TorStrings.settings.bridgeCopy); + bridgeTemplate.querySelector(selectors.bridges.cardShare).textContent = + TorStrings.settings.bridgeShare; + const bridgeCards = prefpane.querySelector(selectors.bridges.cards); + const bridgeMenu = prefpane.querySelector(selectors.bridges.cardMenu); + + this._addBridgeCard = bridgeString => { + const card = bridgeTemplate.cloneNode(true); + card.removeAttribute("id"); + const grid = card.querySelector(selectors.bridges.cardQrGrid); + card.addEventListener("click", e => { + let target = e.target; + let apply = true; + while (target !== null && target !== card && apply) { + // Deal with mixture of "command" and "click" events + apply = !target.classList?.contains("stop-click"); + target = target.parentElement; + } + if (apply) { + if (card.classList.toggle("expanded")) { + grid.classList.add("to-animate"); + grid.style.height = `${grid.scrollHeight}px`; + } else { + // Be sure we still have the to-animate class + grid.classList.add("to-animate"); + grid.style.height = ""; + } + } + }); + const emojis = makeBridgeId(bridgeString).map(e => { + const span = document.createElement("span"); + span.className = "emoji"; + span.textContent = e; + return span; + }); + const idString = TorStrings.settings.bridgeId; + const id = card.querySelector(selectors.bridges.cardId); + const details = parseBridgeLine(bridgeString); + if (details && details.id !== undefined) { + card.setAttribute("data-bridge-id", details.id); + } + // TODO: properly handle "vanilla" bridges? + const type = + details && details.transport !== undefined + ? details.transport + : "vanilla"; + for (const piece of idString.split(/(#[12])/)) { + if (piece == "#1") { + id.append(type); + } else if (piece == "#2") { + id.append(...emojis); + } else { + id.append(piece); + } + } + card.querySelector( + selectors.bridges.cardHeadingAddr + ).textContent = bridgeString; + const optionsButton = card.querySelector(selectors.bridges.cardOptions); + if (TorSettings.bridges.source === TorBridgeSource.BuiltIn) { + optionsButton.setAttribute("hidden", "true"); + } else { + // Cloning the menupopup element does not work as expected. + // Therefore, we use only one, and just before opening it, we remove + // its previous items, and add the ones relative to the bridge whose + // button has been pressed. + optionsButton.addEventListener("click", () => { + const menuItem = document.createXULElement("menuitem"); + menuItem.setAttribute("label", TorStrings.settings.remove); + menuItem.classList.add("menuitem-iconic"); + menuItem.image = "chrome://global/skin/icons/delete.svg"; + menuItem.addEventListener("command", e => { + const strings = TorSettings.bridges.bridge_strings; + const index = strings.indexOf(bridgeString); + if (index !== -1) { + strings.splice(index, 1); + } + TorSettings.bridges.enabled = + bridgeSwitch.checked && !!strings.length; + TorSettings.bridges.bridge_strings = strings.join("\n"); + TorSettings.saveToPrefs(); + TorSettings.applySettings().then(result => { + this._populateBridgeCards(); + }); + }); + if (bridgeMenu.firstChild) { + bridgeMenu.firstChild.remove(); + } + bridgeMenu.append(menuItem); + bridgeMenu.openPopup(optionsButton, { + position: "bottomleft topleft", + }); + }); + } + const bridgeAddr = card.querySelector(selectors.bridges.cardAddr); + bridgeAddr.setAttribute("value", bridgeString); + const bridgeCopy = card.querySelector(selectors.bridges.cardCopy); + let restoreTimeout = null; + bridgeCopy.addEventListener("command", e => { + this.onCopyBridgeAddress(bridgeAddr); + const label = bridgeCopy.querySelector("label"); + label.setAttribute("value", TorStrings.settings.copied); + bridgeCopy.classList.add("primary"); + + const RESTORE_TIME = 1200; + if (restoreTimeout !== null) { + clearTimeout(restoreTimeout); + } + restoreTimeout = setTimeout(() => { + label.setAttribute("value", TorStrings.settings.bridgeCopy); + bridgeCopy.classList.remove("primary"); + restoreTimeout = null; + }, RESTORE_TIME); + }); + if (details && details.id === this._currentBridge) { + card.classList.add("currently-connected"); + bridgeCards.prepend(card); + } else { + bridgeCards.append(card); + } + // Add the QR only after appending the card, to have the computed style + try { + const container = card.querySelector(selectors.bridges.cardQr); + const style = getComputedStyle(container); + const width = style.width.substr(0, style.width.length - 2); + const height = style.height.substr(0, style.height.length - 2); + new QRCode(container, { + text: bridgeString, + width, + height, + colorDark: style.color, + colorLight: style.backgroundColor, + document, + }); + container.parentElement.addEventListener("click", () => { + this.onShowQr(bridgeString); + }); + } catch (err) { + // TODO: Add a generic image in case of errors such as code overflow. + // It should never happen with correct codes, but after all this + // content can be generated by users... + console.error("Could not generate the QR code for the bridge:", err); + } + }; + this._checkBridgeCardsHeight = () => { + for (const card of bridgeCards.children) { + // Expanded cards have the height set manually to their details for + // the CSS animation. However, when resizing the window, we may need + // to adjust their height. + if (card.classList.contains("expanded")) { + const grid = card.querySelector(selectors.bridges.cardQrGrid); + // Reset it first, to avoid having a height that is higher than + // strictly needed. Also, remove the to-animate class, because the + // animation interferes with this process! + grid.classList.remove("to-animate"); + grid.style.height = ""; + grid.style.height = `${grid.scrollHeight}px`; + } + } + }; + this._currentBridgesExpanded = false; + const showAll = prefpane.querySelector(selectors.bridges.showAll); + showAll.setAttribute("label", TorStrings.settings.bridgeShowAll); + showAll.addEventListener("command", () => { + this._currentBridgesExpanded = true; + this._populateBridgeCards(); + }); + const removeAll = prefpane.querySelector(selectors.bridges.removeAll); + removeAll.setAttribute("label", TorStrings.settings.bridgeRemoveAll); + removeAll.addEventListener("command", () => { + this.onRemoveAllBridges(); + }); + this._populateBridgeCards = async () => { + const collapseThreshold = 4; + + let newStrings = new Set(TorSettings.bridges.bridge_strings); + const numBridges = newStrings.size; + if (!newStrings.size) { + bridgeHeader.setAttribute("hidden", "true"); + bridgeCards.setAttribute("hidden", "true"); + showAll.setAttribute("hidden", "true"); + removeAll.setAttribute("hidden", "true"); + bridgeCards.textContent = ""; + return; + } + bridgeHeader.removeAttribute("hidden"); + bridgeCards.removeAttribute("hidden"); + bridgeSwitch.checked = TorSettings.bridges.enabled; + bridgeCards.classList.toggle("disabled", !TorSettings.bridges.enabled); + + let shownCards = 0; + const toShow = this._currentBridgesExpanded + ? numBridges + : collapseThreshold; + + // Do not remove all the old cards, because it makes scrollbar "jump" + const currentCards = bridgeCards.querySelectorAll( + selectors.bridges.card + ); + for (const card of currentCards) { + const string = card.querySelector(selectors.bridges.cardAddr).value; + const hadString = newStrings.delete(string); + if (!hadString || shownCards == toShow) { + card.remove(); + } else { + shownCards++; + } + } + + // Add only the new strings that remained in the set + for (const bridge of newStrings) { + if (shownCards >= toShow) { + if (this._currentBridge === "") { + break; + } else if (!bridge.includes(this._currentBridge)) { + continue; + } + } + this._addBridgeCard(bridge); + shownCards++; + } + + // If we know the connected bridge, we may have added more than the ones + // we should actually show (but the connected ones have been prepended, + // if needed). So, remove any exceeding ones. + while (shownCards > toShow) { + bridgeCards.lastElementChild.remove(); + shownCards--; + } + + // And finally update the buttons + if (numBridges > collapseThreshold && !this._currentBridgesExpanded) { + showAll.removeAttribute("hidden"); + if (TorSettings.bridges.enabled) { + showAll.classList.add("primary"); + } else { + showAll.classList.remove("primary"); + } + removeAll.setAttribute("hidden", "true"); + if (TorSettings.bridges.enabled) { + // We do not want both collapsed and disabled at the same time, + // because we use collapsed only to display a gradient on the list. + bridgeCards.classList.add("list-collapsed"); + } + } else { + showAll.setAttribute("hidden", "true"); + removeAll.removeAttribute("hidden"); + bridgeCards.classList.remove("list-collapsed"); + } + }; + this._populateBridgeCards(); + this._updateConnectedBridges = () => { + for (const card of bridgeCards.querySelectorAll( + ".currently-connected" + )) { + card.classList.remove("currently-connected"); + } + if (this._currentBridge === "") { + return; + } + // Make sure we have the connected bridge in the list + this._populateBridgeCards(); + // At the moment, IDs do not have to be unique (and it is a concrete + // case also with built-in bridges!). E.g., one line for the IPv4 + // address and one for the IPv6 address, so use querySelectorAll + const cards = bridgeCards.querySelectorAll( + `[data-bridge-id="${this._currentBridge}"]` + ); + for (const card of cards) { + card.classList.add("currently-connected"); + } + const placeholder = document.createElement("span"); + bridgeCards.prepend(placeholder); + placeholder.replaceWith(...cards); + }; + try { + const { controller } = ChromeUtils.import( + "resource://torbutton/modules/tor-control-port.js", + {} + ); + // Avoid the cache because we set our custom event watcher, and at the + // moment, watchers cannot be removed from a controller. + controller(true).then(aController => { + this._controller = aController; + // Getting the circuits may be enough, if we have bootstrapped for a + // while, but at the beginning it gives many bridges as connected, + // because tor pokes all the bridges to find the best one. + // Also, watching circuit events does not work, at the moment, but in + // any case, checking the stream has the advantage that we can see if + // it really used for a connection, rather than tor having created + // this circuit to check if the bridge can be used. We do this by + // checking if the stream has SOCKS username, which actually contains + // the destination of the stream. + this._controller.watchEvent( + "STREAM", + event => + event.StreamStatus === "SUCCEEDED" && "SOCKS_USERNAME" in event, + async event => { + const circuitStatuses = await this._controller.getInfo( + "circuit-status" + ); + if (!circuitStatuses) { + return; + } + for (const status of circuitStatuses) { + if (status.id === event.CircuitID && status.circuit.length) { + // The id in the circuit begins with a $ sign + const bridgeId = status.circuit[0][0].substr(1); + if (bridgeId !== this._currentBridge) { + this._currentBridge = bridgeId; + this._updateConnectedBridges(); + } + break; + } + } + } + ); + }); + } catch (err) { + console.warn( + "We could not load torbutton, bridge statuses will not be updated", + err + ); + } + + // Add a new bridge + prefpane.querySelector(selectors.bridges.addHeader).textContent = + TorStrings.settings.bridgeAdd; + prefpane + .querySelector(selectors.bridges.addBuiltinLabel) + .setAttribute("value", TorStrings.settings.bridgeSelectBrowserBuiltin); + { + let button = prefpane.querySelector(selectors.bridges.addBuiltinButton); + button.setAttribute("label", TorStrings.settings.bridgeSelectBuiltin); + button.addEventListener("command", e => { + this.onAddBuiltinBridge(); + }); + } + prefpane + .querySelector(selectors.bridges.requestLabel) + .setAttribute("value", TorStrings.settings.bridgeRequestFromTorProject); + { + let button = prefpane.querySelector(selectors.bridges.requestButton); + button.setAttribute("label", TorStrings.settings.bridgeRequest); + button.addEventListener("command", e => { + this.onRequestBridge(); + }); + } + prefpane + .querySelector(selectors.bridges.enterLabel) + .setAttribute("value", TorStrings.settings.bridgeEnterKnown); + { + const button = prefpane.querySelector(selectors.bridges.enterButton); + button.setAttribute("label", TorStrings.settings.bridgeAddManually); + button.addEventListener("command", e => { + this.onAddBridgeManually(); + }); + } + + Services.obs.addObserver(this, TorConnectTopics.StateChange); + + // Advanced setup + prefpane.querySelector(selectors.advanced.header).innerText = + TorStrings.settings.advancedHeading; + prefpane.querySelector(selectors.advanced.label).textContent = + TorStrings.settings.advancedLabel; + { + let settingsButton = prefpane.querySelector(selectors.advanced.button); + settingsButton.setAttribute( + "label", + TorStrings.settings.advancedButton + ); + settingsButton.addEventListener("command", () => { + this.onAdvancedSettings(); + }); + } + + // 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", () => { + this.onViewTorLogs(); + }); + }, + + init() { + this._populateXUL(); + + let onUnload = () => { + window.removeEventListener("unload", onUnload); + gConnectionPane.uninit(); + }; + window.addEventListener("unload", onUnload); + + window.addEventListener("resize", () => { + this._checkBridgeCardsHeight(); + }); + }, + + uninit() { + // unregister our observer topics + Services.obs.removeObserver(this, TorSettingsTopics.SettingChanged); + Services.obs.removeObserver(this, TorConnectTopics.StateChange); + + if (this._controller !== null) { + this._controller.close(); + this._controller = null; + } + }, + + // 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.onStateChange(); + break; + } + } + }, + + async onInternetTest() { + const mrpc = new MoatRPC(); + let status = null; + try { + await mrpc.init(); + status = await mrpc.testInternetConnection(); + } catch (err) { + console.log("Error while checking the Internet connection", err); + } finally { + mrpc.uninit(); + } + if (status) { + this._internetStatus = status.successful + ? InternetStatus.Online + : InternetStatus.Offline; + this._populateStatus(); + } + }, + + onStateChange() { + this._populateMessagebox(); + this._populateStatus(); + this._showAutoconfiguration(); + this._populateBridgeCards(); + }, + + onShowQr(bridgeString) { + const dialog = new BridgeQrDialog(); + dialog.openDialog(gSubDialog, bridgeString); + }, + + onCopyBridgeAddress(addressElem) { + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + ); + clipboard.copyString(addressElem.value); + }, + + onRemoveAllBridges() { + TorSettings.bridges.enabled = false; + TorSettings.bridges.bridge_strings = ""; + TorSettings.saveToPrefs(); + TorSettings.applySettings().then(result => { + this._populateBridgeCards(); + }); + }, + + onAddBuiltinBridge() { + let builtinBridgeDialog = new BuiltinBridgeDialog(); + + let sizeObserver = null; + { + let ds = document.querySelector("#dialogStack"); + let boxObserver; + boxObserver = new MutationObserver(() => { + let dialogBox = document.querySelector(".dialogBox"); + if (dialogBox) { + sizeObserver = new MutationObserver(mutations => { + for (const m of mutations) { + if (m.attributeName === "style") { + builtinBridgeDialog.resized(); + break; + } + } + }); + sizeObserver.observe(dialogBox, { attributes: true }); + boxObserver.disconnect(); + } + }); + boxObserver.observe(ds, { childList: true, subtree: true }); + } + + builtinBridgeDialog.openDialog(gSubDialog, aBridgeType => { + sizeObserver.disconnect(); + + if (!aBridgeType) { + TorSettings.bridges.enabled = false; + TorSettings.bridges.builtin_type = ""; + } else { + TorSettings.bridges.enabled = true; + TorSettings.bridges.source = TorBridgeSource.BuiltIn; + TorSettings.bridges.builtin_type = aBridgeType; + } + TorSettings.saveToPrefs(); + TorSettings.applySettings().then(result => { + this._populateBridgeCards(); + }); + }); + }, + + // called when the request bridge button is activated + onRequestBridge() { + let requestBridgeDialog = new RequestBridgeDialog(); + requestBridgeDialog.openDialog(gSubDialog, aBridges => { + if (aBridges.length) { + 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._populateBridgeCards(); + }); + } else { + TorSettings.bridges.enabled = false; + } + }); + }, + + onAddBridgeManually() { + let provideBridgeDialog = new ProvideBridgeDialog(); + provideBridgeDialog.openDialog(gSubDialog, aBridgeString => { + if (aBridgeString.length) { + TorSettings.bridges.enabled = true; + TorSettings.bridges.source = TorBridgeSource.UserProvided; + TorSettings.bridges.bridge_strings = aBridgeString; + TorSettings.saveToPrefs(); + TorSettings.applySettings().then(result => { + this._populateBridgeCards(); + }); + } else { + TorSettings.bridges.enabled = false; + TorSettings.bridges.source = TorBridgeSource.Invalid; + } + }); + }, + + onAdvancedSettings() { + let connectionSettingsDialog = new ConnectionSettingsDialog(); + connectionSettingsDialog.openDialog(gSubDialog); + }, + + onViewTorLogs() { + let torLogDialog = new TorLogDialog(); + torLogDialog.openDialog(gSubDialog); + }, + }; + return retval; +})(); /* gConnectionPane */ + +function makeBridgeId(bridgeString) { + // JS uses UTF-16. While most of these emojis are surrogate pairs, a few + // ones fit one UTF-16 character. So we could not use neither indices, + // nor substr, nor some function to split the string. + const emojis = [ + "😄️", + "😒️", + "😉", + "😭️", + "😂️", + "😎️", + "🤩️", + "😘", + "😜️", + "😏️", + "😷", + "🤢", + "🤕", + "🤧", + "🥵", + "🥶", + "🥴", + "😵️", + "🤮️", + "🤑", + "🤔", + "🫢", + "🤐", + "😮💨", + "😐", + "🤤", + "😴", + "🤯", + "🤠", + "🥳", + "🥸", + "🤓", + "🧐", + "😨", + "😳", + "🥺", + "🤬", + "😈", + "👿", + "💀", + "💩", + "🤡", + "👺", + "👻", + "👽", + "🦴", + "🤖", + "😸", + "🙈", + "🙉", + "🙊", + "💋", + "💖", + "💯", + "💢", + "💧", + "💨", + "💭", + "💤", + "👋", + "👌", + "✌", + "👍", + "👎", + "🤛", + "🙌", + "💪", + "🙏", + "✍", + "🧠", + "👀", + "👂", + "👅", + "🦷", + "🐾", + "🐶", + "🦊", + "🦝", + "🐈", + "🦁", + "🐯", + "🐴", + "🦄", + "🦓", + "🐮", + "🐷", + "🐑", + "🐪", + "🐘", + "🐭", + "🐰", + "🦔", + "🦇", + "🐻", + "🐨", + "🐼", + "🐔", + "🦨", + "🦘", + "🐦", + "🐧", + "🦩", + "🦉", + "🦜", + "🪶", + "🐸", + "🐊", + "🐢", + "🦎", + "🐍", + "🦖", + "🦀", + "🐬", + "🐙", + "🐌", + "🐝", + "🐞", + "🌸", + "🌲", + "🌵", + "🍀", + "🍁", + "🍇", + "🍉", + "🍊", + "🍋", + "🍌", + "🍍", + "🍎", + "🥥", + "🍐", + "🍒", + "🍓", + "🫐", + "🥝", + "🥔", + "🥕", + "🧅", + "🌰", + "🍄", + "🍞", + "🥞", + "🧀", + "🍖", + "🍔", + "🍟", + "🍕", + "🥚", + "🍿", + "🧂", + "🍙", + "🍦", + "🍩", + "🍪", + "🎂", + "🍬", + "🍭", + "🥛", + "☕", + "🫖", + "🍾", + "🍷", + "🍹", + "🍺", + "🍴", + "🥄", + "🫙", + "🧭", + "🌋", + "🪵", + "🏡", + "🏢", + "🏰", + "⛲", + "⛺", + "🎡", + "🚂", + "🚘", + "🚜", + "🚲", + "🚔", + "🚨", + "⛽", + "🚥", + "🚧", + "⚓", + "⛵", + "🛟", + "🪂", + "🚀", + "⌛", + "⏰", + "🌂", + "🌞", + "🌙", + "🌟", + "⛅", + "⚡", + "🔥", + "🌊", + "🎃", + "🎈", + "🎉", + "✨", + "🎀", + "🎁", + "🏆", + "🏅", + "🔮", + "🪄", + "🎾", + "🎳", + "🎲", + "🎭", + "🎨", + "🧵", + "🎩", + "📢", + "🔔", + "🎵", + "🎤", + "🎧", + "🎷", + "🎸", + "🥁", + "🔋", + "🔌", + "💻", + "💾", + "💿", + "🎬", + "📺", + "📷", + "🎮", + "🧩", + "🔍", + "💡", + "📖", + "💰", + "💼", + "📈", + "📌", + "📎", + "🔒", + "🔑", + "🔧", + "🪛", + "🔩", + "🧲", + "🔬", + "🔭", + "📡", + "🚪", + "🪑", + "⛔", + "🚩", + ]; + + // FNV-1a implementation that is compatible with other languages + const prime = 0x01000193; + const offset = 0x811c9dc5; + let hash = offset; + const encoder = new TextEncoder(); + for (const charCode of encoder.encode(bridgeString)) { + hash = Math.imul(hash ^ charCode, prime); + } + + const hashBytes = [ + ((hash & 0x7f000000) >> 24) | (hash < 0 ? 0x80 : 0), + (hash & 0x00ff0000) >> 16, + (hash & 0x0000ff00) >> 8, + hash & 0x000000ff, + ]; + return hashBytes.map(b => emojis[b]); +} + +function parseBridgeLine(line) { + const re = /^([^\s]+\s+)?([0-9a-fA-F.[]:]+:[0-9]{1,5})\s*([0-9a-fA-F]{40})(\s+.+)?/; + const matches = line.match(re); + if (!matches) { + return null; + } + let bridge = { addr: matches[2] }; + if (matches[1] !== undefined) { + bridge.transport = matches[1].trim(); + } + if (matches[3] !== undefined) { + bridge.id = matches[3].toUpperCase(); + } + if (matches[4] !== undefined) { + bridge.args = matches[4].trim(); + } + return bridge; +} diff --git a/browser/components/torpreferences/content/connectionPane.xhtml b/browser/components/torpreferences/content/connectionPane.xhtml new file mode 100644 index 0000000000000..67f98685d8038 --- /dev/null +++ b/browser/components/torpreferences/content/connectionPane.xhtml @@ -0,0 +1,177 @@ +<!-- Tor panel --> + +<script type="application/javascript" + src="chrome://browser/content/torpreferences/connectionPane.js"/> +<html:template id="template-paneConnection"> + +<!-- Tor Connect Message Box --> +<groupbox data-category="paneConnection" hidden="true"> + <html:div id="torPreferences-connectMessageBox" + class="subcategory" + data-category="paneConnection" + 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="paneConnection" + hidden="true"> + <html:h1 id="torPreferences-header"/> +</hbox> + +<groupbox data-category="paneConnection" + 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> + +<groupbox id="torPreferences-status-group" + data-category="paneConnection"> + <hbox id="torPreferences-status-box"> + <image id="torPreferences-status-internet-icon"/> + <html:span id="torPreferences-status-internet-label"/> + <button id="torPreferences-status-internet-test"/> + <image id="torPreferences-status-internet-statusIcon"/> + <html:span id="torPreferences-status-internet-status"/> + <image id="torPreferences-status-tor-icon"/> + <html:span id="torPreferences-status-tor-label"/> + <image id="torPreferences-status-tor-statusIcon"/> + <html:span id="torPreferences-status-tor-status"/> + </hbox> +</groupbox> + +<!-- Quickstart --> +<groupbox id="torPreferences-quickstart-group" + data-category="paneConnection" + 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 --> +<hbox class="subcategory" + data-category="paneConnection" + hidden="true"> + <html:h1 id="torPreferences-bridges-header"/> +</hbox> +<groupbox id="torPreferences-bridges-group" + data-category="paneConnection" + hidden="true"> + <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> + <hbox align="center" id="torPreferences-bridges-locationGroup" hidden="true"> + <label id="torPreferences-bridges-locationLabel" + control="torPreferences-bridges-location"/> + <spacer flex="1"/> + <menulist id="torPreferences-bridges-location"> + <menupopup id="torPreferences-bridges-locationEntries"/> + </menulist> + <button id="torPreferences-bridges-buttonChooseBridgeForMe" class="torMarginFix primary"/> + </hbox> + <html:h2 id="torPreferences-currentBridges-header"> + <html:span id="torPreferences-currentBridges-headerText"/> + <html:input type="checkbox" id="torPreferences-currentBridges-switch" class="toggle-button"/> + </html:h2> + <menupopup id="torPreferences-bridgeCard-menu"/> + <vbox id="torPreferences-bridgeCard-template" class="torPreferences-bridgeCard"> + <hbox class="torPreferences-bridgeCard-heading"> + <html:div class="torPreferences-bridgeCard-id"/> + <html:div class="torPreferences-bridgeCard-headingAddr"/> + <html:div class="torPreferences-bridgeCard-buttons"> + <html:span class="torPreferences-bridgeCard-connectedBadge"> + <image class="torPreferences-bridgeCard-connectedIcon"/> + <html:span class="torPreferences-bridgeCard-connectedLabel"/> + </html:span> + <html:button class="torPreferences-bridgeCard-options stop-click"/> + </html:div> + </hbox> + <box class="torPreferences-bridgeCard-grid"> + <box class="torPreferences-bridgeCard-qrWrapper"> + <box class="torPreferences-bridgeCard-qr stop-click"> + <html:div class="torPreferences-bridgeCard-qrCode"/> + <html:div class="torPreferences-bridgeCard-qrOnionBox"/> + <html:div class="torPreferences-bridgeCard-qrOnion"/> + </box> + <html:div class="torPreferences-bridgeCard-filler"/> + </box> + <description class="torPreferences-bridgeCard-share"></description> + <hbox class="torPreferences-bridgeCard-addrBox"> + <html:input class="torPreferences-bridgeCard-addr torMarginFix stop-click" type="text" readonly="readonly"/> + </hbox> + <hbox class="torPreferences-bridgeCard-learnMoreBox" align="center"> + <label class="torPreferences-bridgeCard-learnMore learnMore text-link stop-click" is="text-link"/> + </hbox> + <hbox class="torPreferences-bridgeCard-copy" align="center"> + <button class="torPreferences-bridgeCard-copyButton stop-click"/> + </hbox> + </box> + </vbox> + <vbox id="torPreferences-currentBridges-cards"></vbox> + <vbox align="center"> + <button id="torPreferences-currentBridges-showAll"/> + <button id="torPreferences-currentBridges-removeAll" class="primary danger-button"/> + </vbox> + <html:h2 id="torPreferences-addBridge-header"></html:h2> + <hbox align="center"> + <label id="torPreferences-addBridge-labelBuiltinBridge"/> + <space flex="1"/> + <button id="torPreferences-addBridge-buttonBuiltinBridge" class="torMarginFix"/> + </hbox> + <hbox align="center"> + <label id="torPreferences-addBridge-labelRequestBridge"/> + <space flex="1"/> + <button id="torPreferences-addBridge-buttonRequestBridge" class="torMarginFix"/> + </hbox> + <hbox align="center"> + <label id="torPreferences-addBridge-labelEnterBridge"/> + <space flex="1"/> + <button id="torPreferences-addBridge-buttonEnterBridge" class="torMarginFix"/> + </hbox> +</groupbox> + +<!-- Advanced --> +<hbox class="subcategory" + data-category="paneConnection" + hidden="true"> + <html:h1 id="torPreferences-advanced-header"/> +</hbox> +<groupbox id="torPreferences-advanced-group" + data-category="paneConnection" + hidden="true"> + <box id="torPreferences-advanced-grid"> + <hbox id="torPreferences-advanced-hbox" align="center"> + <label id="torPreferences-advanced-label"/> + </hbox> + <hbox align="center"> + <button id="torPreferences-advanced-button"/> + </hbox> + <hbox id="torPreferences-torDaemon-hbox" align="center"> + <label id="torPreferences-torLogs"/> + </hbox> + <hbox align="center" data-subcategory="viewlogs"> + <button id="torPreferences-buttonTorLogs"/> + </hbox> + </box> +</groupbox> +</html:template> diff --git a/browser/components/torpreferences/content/connectionSettingsDialog.jsm b/browser/components/torpreferences/content/connectionSettingsDialog.jsm new file mode 100644 index 0000000000000..abc177c43f884 --- /dev/null +++ b/browser/components/torpreferences/content/connectionSettingsDialog.jsm @@ -0,0 +1,393 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["ConnectionSettingsDialog"]; + +const { TorSettings, TorProxyType } = ChromeUtils.import( + "resource:///modules/TorSettings.jsm" +); + +const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm"); + +class ConnectionSettingsDialog { + constructor() { + this._dialog = null; + this._useProxyCheckbox = null; + this._proxyTypeLabel = null; + this._proxyTypeMenulist = null; + this._proxyAddressLabel = null; + this._proxyAddressTextbox = null; + this._proxyPortLabel = null; + this._proxyPortTextbox = null; + this._proxyUsernameLabel = null; + this._proxyUsernameTextbox = null; + this._proxyPasswordLabel = null; + this._proxyPasswordTextbox = null; + this._useFirewallCheckbox = null; + this._allowedPortsLabel = null; + this._allowedPortsTextbox = null; + } + + static get selectors() { + return { + header: "#torPreferences-connection-header", + useProxyCheckbox: "checkbox#torPreferences-connection-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-connection-toggleFirewall", + firewallAllowedPortsLabel: "label#torPreferences-connection-allowedPorts", + firewallAllowedPortsTextbox: + "input#torPreferences-connection-textboxAllowedPorts", + }; + } + + // disables the provided list of elements + _setElementsDisabled(elements, disabled) { + for (let currentElement of elements) { + currentElement.disabled = disabled; + } + } + + _populateXUL(window, aDialog) { + const selectors = ConnectionSettingsDialog.selectors; + + this._dialog = aDialog; + const dialogWin = this._dialog.parentElement; + dialogWin.setAttribute( + "title", + TorStrings.settings.connectionSettingsDialogTitle + ); + this._dialog.querySelector(selectors.header).textContent = + TorStrings.settings.connectionSettingsDialogHeader; + + // Local Proxy + this._useProxyCheckbox = this._dialog.querySelector( + selectors.useProxyCheckbox + ); + this._useProxyCheckbox.setAttribute( + "label", + TorStrings.settings.useLocalProxy + ); + this._useProxyCheckbox.addEventListener("command", e => { + const checked = this._useProxyCheckbox.checked; + this.onToggleProxy(checked); + }); + this._proxyTypeLabel = this._dialog.querySelector(selectors.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 = this._dialog.querySelector( + selectors.proxyTypeList + ); + this._proxyTypeMenulist.addEventListener("command", e => { + const value = this._proxyTypeMenulist.value; + this.onSelectProxyType(value); + }); + for (let currentProxy of mockProxies) { + let menuEntry = window.document.createXULElement("menuitem"); + menuEntry.setAttribute("value", currentProxy.value); + menuEntry.setAttribute("label", currentProxy.label); + this._proxyTypeMenulist.querySelector("menupopup").appendChild(menuEntry); + } + + this._proxyAddressLabel = this._dialog.querySelector( + selectors.proxyAddressLabel + ); + this._proxyAddressLabel.setAttribute( + "value", + TorStrings.settings.proxyAddress + ); + this._proxyAddressTextbox = this._dialog.querySelector( + selectors.proxyAddressTextbox + ); + this._proxyAddressTextbox.setAttribute( + "placeholder", + TorStrings.settings.proxyAddressPlaceholder + ); + this._proxyAddressTextbox.addEventListener("blur", e => { + let value = this._proxyAddressTextbox.value.trim(); + let colon = value.lastIndexOf(":"); + if (colon != -1) { + let maybePort = parseInt(value.substr(colon + 1)); + if (!isNaN(maybePort) && maybePort > 0 && maybePort < 65536) { + this._proxyAddressTextbox.value = value.substr(0, colon); + this._proxyPortTextbox.value = maybePort; + } + } + }); + this._proxyPortLabel = this._dialog.querySelector(selectors.proxyPortLabel); + this._proxyPortLabel.setAttribute("value", TorStrings.settings.proxyPort); + this._proxyPortTextbox = this._dialog.querySelector( + selectors.proxyPortTextbox + ); + this._proxyUsernameLabel = this._dialog.querySelector( + selectors.proxyUsernameLabel + ); + this._proxyUsernameLabel.setAttribute( + "value", + TorStrings.settings.proxyUsername + ); + this._proxyUsernameTextbox = this._dialog.querySelector( + selectors.proxyUsernameTextbox + ); + this._proxyUsernameTextbox.setAttribute( + "placeholder", + TorStrings.settings.proxyUsernamePasswordPlaceholder + ); + this._proxyPasswordLabel = this._dialog.querySelector( + selectors.proxyPasswordLabel + ); + this._proxyPasswordLabel.setAttribute( + "value", + TorStrings.settings.proxyPassword + ); + this._proxyPasswordTextbox = this._dialog.querySelector( + selectors.proxyPasswordTextbox + ); + this._proxyPasswordTextbox.setAttribute( + "placeholder", + TorStrings.settings.proxyUsernamePasswordPlaceholder + ); + + this.onToggleProxy(false); + 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; + } + + // Local firewall + this._useFirewallCheckbox = this._dialog.querySelector( + selectors.useFirewallCheckbox + ); + this._useFirewallCheckbox.setAttribute( + "label", + TorStrings.settings.useFirewall + ); + this._useFirewallCheckbox.addEventListener("command", e => { + const checked = this._useFirewallCheckbox.checked; + this.onToggleFirewall(checked); + }); + this._allowedPortsLabel = this._dialog.querySelector( + selectors.firewallAllowedPortsLabel + ); + this._allowedPortsLabel.setAttribute( + "value", + TorStrings.settings.allowedPorts + ); + this._allowedPortsTextbox = this._dialog.querySelector( + selectors.firewallAllowedPortsTextbox + ); + this._allowedPortsTextbox.setAttribute( + "placeholder", + TorStrings.settings.allowedPortsPlaceholder + ); + + this.onToggleFirewall(false); + if (TorSettings.firewall.enabled) { + this.onToggleFirewall(true); + this._allowedPortsTextbox.value = TorSettings.firewall.allowed_ports.join( + ", " + ); + } + + this._dialog.addEventListener("dialogaccept", e => { + this._applySettings(); + }); + } + + // 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 + ); + if (enabled) { + this.onSelectProxyType(this._proxyTypeMenulist.value); + } + } + + // 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; + } + } + } + + // callback when firewall proxy is toggled + onToggleFirewall(enabled) { + this._useFirewallCheckbox.checked = enabled; + let disabled = !enabled; + + this._setElementsDisabled( + [this._allowedPortsLabel, this._allowedPortsTextbox], + disabled + ); + } + + // pushes settings from UI to tor + _applySettings() { + 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; + } + + 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(); + } + + init(window, aDialog) { + // defer to later until firefox has populated the dialog with all our elements + window.setTimeout(() => { + this._populateXUL(window, aDialog); + }, 0); + } + + openDialog(gSubDialog) { + gSubDialog.open( + "chrome://browser/content/torpreferences/connectionSettingsDialog.xhtml", + { features: "resizable=yes" }, + this + ); + } +} diff --git a/browser/components/torpreferences/content/connectionSettingsDialog.xhtml b/browser/components/torpreferences/content/connectionSettingsDialog.xhtml new file mode 100644 index 0000000000000..88aa979256f02 --- /dev/null +++ b/browser/components/torpreferences/content/connectionSettingsDialog.xhtml @@ -0,0 +1,62 @@ +<?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%22%3E +<dialog id="torPreferences-connection-dialog" + buttons="accept,cancel"> + <html:h3 id="torPreferences-connection-header">​</html:h3> + <box id="torPreferences-connection-grid"> + <!-- Local Proxy --> + <hbox class="torPreferences-connection-checkbox-container"> + <checkbox id="torPreferences-connection-toggleProxy" label="​"/> + </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-connection-checkbox-container"> + <checkbox id="torPreferences-connection-toggleFirewall" label="​"/> + </hbox> + <hbox class="indent" align="center"> + <label id="torPreferences-connection-allowedPorts"/> + </hbox> + <hbox align="center"> + <html:input id="torPreferences-connection-textboxAllowedPorts" type="text" class="torMarginFix" value="80,443"/> + </hbox> + </box> + <script type="application/javascript"><![CDATA[ + "use strict"; + + let connectionSettingsDialog = window.arguments[0]; + let dialog = document.getElementById("torPreferences-connection-dialog"); + connectionSettingsDialog.init(window, dialog); + ]]></script> +</dialog> +</window> diff --git a/browser/components/torpreferences/content/network.svg b/browser/components/torpreferences/content/network.svg new file mode 100644 index 0000000000000..e1689b5e6d649 --- /dev/null +++ b/browser/components/torpreferences/content/network.svg @@ -0,0 +1,6 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity"> + <path d="M8.5 1a7.5 7.5 0 1 0 0 15 7.5 7.5 0 0 0 0-15zm2.447 1.75a6.255 6.255 0 0 1 3.756 5.125l-2.229 0A9.426 9.426 0 0 0 10.54 2.75l.407 0zm-2.049 0a8.211 8.211 0 0 1 2.321 5.125l-5.438 0A8.211 8.211 0 0 1 8.102 2.75l.796 0zm-2.846 0 .408 0a9.434 9.434 0 0 0-1.934 5.125l-2.229 0A6.254 6.254 0 0 1 6.052 2.75zm0 11.5a6.252 6.252 0 0 1-3.755-5.125l2.229 0A9.426 9.426 0 0 0 6.46 14.25l-.408 0zm2.05 0a8.211 8.211 0 0 1-2.321-5.125l5.437 0a8.211 8.211 0 0 1-2.321 5.125l-.795 0zm2.846 0-.40 [...] +</svg> diff --git a/browser/components/torpreferences/content/provideBridgeDialog.jsm b/browser/components/torpreferences/content/provideBridgeDialog.jsm new file mode 100644 index 0000000000000..b73a6f533afa6 --- /dev/null +++ b/browser/components/torpreferences/content/provideBridgeDialog.jsm @@ -0,0 +1,69 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["ProvideBridgeDialog"]; + +const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm"); + +const { TorSettings, TorBridgeSource } = ChromeUtils.import( + "resource:///modules/TorSettings.jsm" +); + +class ProvideBridgeDialog { + constructor() { + this._dialog = null; + this._textarea = null; + this._bridgeString = ""; + } + + static get selectors() { + return { + header: "#torPreferences-provideBridge-header", + textarea: "#torPreferences-provideBridge-textarea", + }; + } + + _populateXUL(aDialog) { + const selectors = ProvideBridgeDialog.selectors; + + this._dialog = aDialog; + const dialogWin = this._dialog.parentElement; + dialogWin.setAttribute("title", TorStrings.settings.provideBridgeTitle); + this._dialog.querySelector(selectors.header).textContent = + TorStrings.settings.provideBridgeHeader; + this._textarea = this._dialog.querySelector(selectors.textarea); + this._textarea.setAttribute( + "placeholder", + TorStrings.settings.provideBridgePlaceholder + ); + if ( + TorSettings.bridges.enabled && + TorSettings.bridges.source == TorBridgeSource.UserProvided + ) { + this._textarea.value = TorSettings.bridges.bridge_strings.join("\n"); + } + + this._dialog.addEventListener("dialogaccept", e => { + this._bridgeString = this._textarea.value; + }); + } + + init(window, aDialog) { + // defer to later until firefox has populated the dialog with all our elements + window.setTimeout(() => { + this._populateXUL(aDialog); + }, 0); + } + + openDialog(gSubDialog, aCloseCallback) { + gSubDialog.open( + "chrome://browser/content/torpreferences/provideBridgeDialog.xhtml", + { + features: "resizable=yes", + closingCallback: () => { + aCloseCallback(this._bridgeString); + }, + }, + this + ); + } +} diff --git a/browser/components/torpreferences/content/provideBridgeDialog.xhtml b/browser/components/torpreferences/content/provideBridgeDialog.xhtml new file mode 100644 index 0000000000000..28d19cadaf9c9 --- /dev/null +++ b/browser/components/torpreferences/content/provideBridgeDialog.xhtml @@ -0,0 +1,21 @@ +<?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%22%3E +<dialog id="torPreferences-provideBridge-dialog" + buttons="help,accept,cancel"> + <html:h3 id="torPreferences-provideBridge-header">​</html:h3> + <html:textarea id="torPreferences-provideBridge-textarea" multiline="true" rows="3"/> + <script type="application/javascript"><![CDATA[ + "use strict"; + + let provideBridgeDialog = window.arguments[0]; + let dialog = document.getElementById("torPreferences-provideBridge-dialog"); + provideBridgeDialog.init(window, dialog); + ]]></script> +</dialog> +</window> diff --git a/browser/components/torpreferences/content/requestBridgeDialog.jsm b/browser/components/torpreferences/content/requestBridgeDialog.jsm new file mode 100644 index 0000000000000..f14bbdcbbb448 --- /dev/null +++ b/browser/components/torpreferences/content/requestBridgeDialog.jsm @@ -0,0 +1,211 @@ +"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._dialogHeader = 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 */, + dialogHeader: "h3#torPreferences-requestBridge-header", + 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(window, 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._dialog.addEventListener("dialoghelp", e => { + window.top.openTrustedLinkIn( + "https://tb-manual.torproject.org/bridges/", + "tab" + ); + }); + + this._dialogHeader = this._dialog.querySelector(selectors.dialogHeader); + this._dialogHeader.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._dialogHeader.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(window, 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(aError); + }); + } + + onRefreshCaptcha() { + this._setUIDisabled(true); + this._captchaImage.src = ""; + this._dialogHeader.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 0000000000000..b7286528a8a5a --- /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%22%3E +<dialog id="torPreferences-requestBridge-dialog" + buttons="help,accept,cancel"> + <!-- ok, so ​ is a zero-width space. We need to have *something* in the innerText so that XUL knows how tall the + title 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 >:( --> + <html:h3 id="torPreferences-requestBridge-header">​</html:h3> + <!-- 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> diff --git a/browser/components/torpreferences/content/torLogDialog.jsm b/browser/components/torpreferences/content/torLogDialog.jsm new file mode 100644 index 0000000000000..94a57b9b165ee --- /dev/null +++ b/browser/components/torpreferences/content/torLogDialog.jsm @@ -0,0 +1,84 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["TorLogDialog"]; + +const { setTimeout, clearTimeout } = ChromeUtils.import( + "resource://gre/modules/Timer.jsm" +); + +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; + this._restoreButtonTimeout = 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(); + const label = this._copyLogButton.querySelector("label"); + label.setAttribute("value", TorStrings.settings.copied); + this._copyLogButton.classList.add("primary"); + + const RESTORE_TIME = 1200; + if (this._restoreButtonTimeout !== null) { + clearTimeout(this._restoreButtonTimeout); + } + this._restoreButtonTimeout = setTimeout(() => { + label.setAttribute("value", TorStrings.settings.copyLog); + this._copyLogButton.classList.remove("primary"); + this._restoreButtonTimeout = null; + }, RESTORE_TIME); + }); + + 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 0000000000000..9c17f8132978d --- /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%22%3E +<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/torPreferences.css b/browser/components/torpreferences/content/torPreferences.css new file mode 100644 index 0000000000000..31b6e29d679f3 --- /dev/null +++ b/browser/components/torpreferences/content/torPreferences.css @@ -0,0 +1,541 @@ +@import url("chrome://branding/content/tor-styles.css"); + +#category-connection > .category-icon { + list-style-image: url("chrome://browser/content/torpreferences/torPreferencesIcon.svg"); +} + +html:dir(rtl) input[type="checkbox"].toggle-button::before { + /* For some reason, the rule from toggle-button.css is not applied... */ + scale: -1; +} + +/* 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; + padding-inline-start: 8px; +} + +#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); +} + +/* Status */ +#torPreferences-status-box { + display: flex; + align-items: center; +} + +#torPreferences-status-internet-icon, #torPreferences-status-tor-icon { + width: 18px; + height: 18px; + margin-inline-end: 8px; +} + +#torPreferences-status-internet-label, #torPreferences-status-tor-label { + font-weight: bold; +} + +#torPreferences-status-internet-icon { + list-style-image: url("chrome://devtools/skin/images/globe.svg"); +} + +#torPreferences-status-internet-statusIcon.online, +#torPreferences-status-internet-statusIcon.offline, +#torPreferences-status-tor-statusIcon { + margin-inline-start: 12px; + margin-inline-end: 9px; +} + +#torPreferences-status-internet-statusIcon.online, #torPreferences-status-tor-statusIcon.connected { + list-style-image: url("chrome://devtools/skin/images/check.svg"); + -moz-context-properties: fill; + fill: var(--purple-60); +} + +#torPreferences-status-internet-status { + margin-inline-end: 32px; +} + +#torPreferences-status-tor-icon { + list-style-image: url("chrome://browser/skin/onion.svg"); +} + +#torPreferences-status-internet-icon, #torPreferences-status-tor-icon { + -moz-context-properties: fill; + fill: var(--in-content-text-color); +} + +#torPreferences-status-tor-statusIcon, #torPreferences-status-internet-statusIcon.offline { + list-style-image: url("chrome://browser/skin/warning.svg"); +} + +#torPreferences-status-tor-statusIcon.blocked { + -moz-context-properties: fill; + fill: var(--red-60); +} + +/* Bridge settings */ +#torPreferences-bridges-location { + width: 280px; +} + +/* Bridge cards */ +:root { + --bridgeCard-animation-time: 0.25s; +} + +#torPreferences-currentBridges-cards { + /* The padding is needed because the mask-image creates an unexpected result + otherwise... */ + padding: 24px 4px; +} + +#torPreferences-currentBridges-cards.list-collapsed { + mask-image: linear-gradient(rgb(0, 0, 0), rgba(0, 0, 0, 0.1)); +} + +#torPreferences-currentBridges-cards.disabled { + opacity: 0.4; +} + +.torPreferences-bridgeCard { + padding: 16px 12px; + /* define border-radius here because of the transition */ + border-radius: 4px; + transition: margin var(--bridgeCard-animation-time), box-shadow 150ms; +} + +.torPreferences-bridgeCard.expanded { + margin: 12px 0; + background: var(--in-content-box-background); + box-shadow: var(--card-shadow); +} + +.torPreferences-bridgeCard:hover { + background: var(--in-content-box-background); + box-shadow: var(--card-shadow-hover); + cursor: pointer; +} + +.torPreferences-bridgeCard-heading { + display: flex; + align-items: center; +} + +.torPreferences-bridgeCard-id { + font-weight: 700; +} + +.torPreferences-bridgeCard-id .emoji { + margin-inline-start: 4px; + padding: 4px; + font-size: 20px; + border-radius: 4px; + background: var(--in-content-button-background-hover); +} + +.torPreferences-bridgeCard-headingAddr { + /* flex extends the element when needed, but without setting a width (any) the + overflow + ellipses does not work. */ + width: 20px; + flex: 1; + margin: 0 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.expanded .torPreferences-bridgeCard-headingAddr { + display: none; +} + +.torPreferences-bridgeCard-buttons { + display: flex; + align-items: center; + margin-inline-start: auto; + align-self: center; +} + +.torPreferences-bridgeCard-connectedBadge { + display: none; + padding: 8px 12px; + border-radius: 16px; + background: rgba(128, 0, 215, 0.1); + color: var(--purple-60); +} + +.currently-connected .torPreferences-bridgeCard-connectedBadge { + display: flex; +} + +.torPreferences-bridgeCard-connectedIcon { + margin-inline-start: 1px; + margin-inline-end: 7px; + list-style-image: url("chrome://devtools/skin/images/check.svg"); + -moz-context-properties: fill; + fill: var(--purple-60); +} + +.torPreferences-bridgeCard-options { + width: 24px; + min-width: 0; + height: 24px; + min-height: 0; + margin-inline-start: 8px; + padding: 1px; + background-image: url("chrome://global/skin/icons/more.svg"); + background-repeat: no-repeat; + background-position: center center; + fill: currentColor; + -moz-context-properties: fill; +} + +.torPreferences-bridgeCard-qrWrapper { + grid-area: bridge-qr; + display: flex; + flex-direction: column; +} + +.torPreferences-bridgeCard-qr { + width: 126px; + position: relative; +} + +.torPreferences-bridgeCard-qrCode { + width: 112px; + height: 112px; + /* Define these colors, as they will be passed to the QR code library */ + background: var(--in-content-box-background); + color: var(--in-content-text-color); +} + +.torPreferences-bridgeCard-qrOnionBox { + width: 28px; + height: 28px; + position: absolute; + top: 42px; + inset-inline-start: 42px; + background: var(--in-content-box-background); +} + +.torPreferences-bridgeCard-qrOnion { + width: 16px; + height: 16px; + position: absolute; + top: 48px; + inset-inline-start: 48px; + + mask: url("chrome://browser/skin/onion.svg"); + mask-repeat: no-repeat; + mask-size: 16px; + background: var(--in-content-text-color); +} + +.torPreferences-bridgeCard-qr:hover .torPreferences-bridgeCard-qrOnionBox { + background: var(--in-content-text-color); +} + +.torPreferences-bridgeCard-qr:hover .torPreferences-bridgeCard-qrOnion { + mask: url("chrome://global/skin/icons/search-glass.svg"); + background: var(--in-content-box-background); +} + +.torPreferences-bridgeCard-filler { + flex: 1; +} + +.torPreferences-bridgeCard-grid { + height: 0; /* We will set it in JS when expanding it! */ + display: grid; + grid-template-rows: auto 1fr; + grid-template-columns: auto 1fr auto; + grid-template-areas: + 'bridge-qr bridge-share bridge-share' + 'bridge-qr bridge-address bridge-address' + 'bridge-qr bridge-learn-more bridge-copy'; + padding-top: 12px; + visibility: hidden; +} + +.expanded .torPreferences-bridgeCard-grid { + visibility: visible; +} + +.torPreferences-bridgeCard-grid.to-animate { + transition: height var(--bridgeCard-animation-time) ease-out, visibility var(--bridgeCard-animation-time); + overflow: hidden; +} + +.torPreferences-bridgeCard-share { + grid-area: bridge-share; +} + +.torPreferences-bridgeCard-addrBox { + grid-area: bridge-address; + display: flex; + align-items: center; + justify-content: center; + margin: 8px 0; +} + +.torPreferences-bridgeCard-addr { + width: 100%; +} + +.torPreferences-bridgeCard-leranMoreBox { + grid-area: bridge-learn-more; +} + +.torPreferences-bridgeCard-copy { + grid-area: bridge-copy; +} + +#torPreferences-bridgeCard-template { + display: none; +} + +/* Advanced Settings */ +#torPreferences-advanced-grid { + display: grid; + grid-template-columns: 1fr auto; +} + +#torPreferences-advanced-group button { + min-width: 150px; +} + +#torPreferences-advanced-hbox, #torPreferences-torDaemon-hbox { + padding-inline-end: 15px; +} + +h3#torPreferences-requestBridge-header { + margin: 0; +} + +image#torPreferences-requestBridge-captchaImage { + margin: 16px 0 8px 0; + min-height: 140px; +} + +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; +} + +/* Show bridge QR dialog */ +#bridgeQr-container { + position: relative; + height: 300px; +} + +#bridgeQr-target { + position: absolute; + width: 300px; + height: 300px; + left: calc(50% - 150px); + background: var(--in-content-box-background); + color: var(--in-content-text-color); +} + +#bridgeQr-onionBox { + position: absolute; + width: 70px; + height: 70px; + top: 115px; + left: calc(50% - 35px); + background-color: var(--in-content-box-background); +} + +#bridgeQr-onion { + position: absolute; + width: 38px; + height: 38px; + top: 131px; + left: calc(50% - 19px); + mask: url("chrome://browser/skin/onion.svg"); + mask-repeat: no-repeat; + mask-size: 38px; + background: var(--in-content-text-color); +} + +/* Builtin bridge dialog */ +#torPreferences-builtinBridge-header { + margin: 8px 0 10px 0; +} + +#torPreferences-builtinBridge-description { + margin-bottom: 18px; +} + +#torPreferences-builtinBridge-typeSelection { + margin-bottom: 16px; + min-height: 14em; /* Hack: make room for at least 4 lines of content for 3 types + 2 for spacing */ +} + +#torPreferences-builtinBridge-typeSelection radio label { + font-weight: 700; +} + +/* Request bridge dialog */ +/* + 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; +} + +/* Provide bridge dialog */ +#torPreferences-provideBridge-header { + margin-top: 8px; +} + +/* Connection settings dialog */ +#torPreferences-connection-header { + margin: 4px 0 14px 0; +} + +#torPreferences-connection-grid { + display: grid; + grid-template-columns: auto 1fr; +} + +.torPreferences-connection-checkbox-container { + grid-column: 1 / 3; +} + +#torPreferences-localProxy-textboxAddress, +#torPreferences-localProxy-textboxUsername, +#torPreferences-localProxy-textboxPassword, +#torPreferences-connection-textboxAllowedPorts { + -moz-box-flex: 1; +} + +/* Tor logs dialog */ +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; +} diff --git a/browser/components/torpreferences/content/torPreferencesIcon.svg b/browser/components/torpreferences/content/torPreferencesIcon.svg new file mode 100644 index 0000000000000..382a061774aaa --- /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 0000000000000..ed3bb441084c9 --- /dev/null +++ b/browser/components/torpreferences/jar.mn @@ -0,0 +1,19 @@ +browser.jar: + content/browser/torpreferences/bridgeQrDialog.xhtml (content/bridgeQrDialog.xhtml) + content/browser/torpreferences/bridgeQrDialog.jsm (content/bridgeQrDialog.jsm) + content/browser/torpreferences/builtinBridgeDialog.xhtml (content/builtinBridgeDialog.xhtml) + content/browser/torpreferences/builtinBridgeDialog.jsm (content/builtinBridgeDialog.jsm) + content/browser/torpreferences/connectionSettingsDialog.xhtml (content/connectionSettingsDialog.xhtml) + content/browser/torpreferences/connectionSettingsDialog.jsm (content/connectionSettingsDialog.jsm) + content/browser/torpreferences/network.svg (content/network.svg) + content/browser/torpreferences/provideBridgeDialog.xhtml (content/provideBridgeDialog.xhtml) + content/browser/torpreferences/provideBridgeDialog.jsm (content/provideBridgeDialog.jsm) + content/browser/torpreferences/requestBridgeDialog.xhtml (content/requestBridgeDialog.xhtml) + content/browser/torpreferences/requestBridgeDialog.jsm (content/requestBridgeDialog.jsm) + content/browser/torpreferences/connectionCategory.inc.xhtml (content/connectionCategory.inc.xhtml) + content/browser/torpreferences/torLogDialog.jsm (content/torLogDialog.jsm) + content/browser/torpreferences/torLogDialog.xhtml (content/torLogDialog.xhtml) + content/browser/torpreferences/connectionPane.js (content/connectionPane.js) + content/browser/torpreferences/connectionPane.xhtml (content/connectionPane.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 0000000000000..2661ad7cb9f3d --- /dev/null +++ b/browser/components/torpreferences/moz.build @@ -0,0 +1 @@ +JAR_MANIFESTS += ["jar.mn"]