aboutsummaryrefslogtreecommitdiff
path: root/desktop/src
diff options
context:
space:
mode:
authorMiguel Jacq <mig@mig5.net>2021-11-12 10:56:15 +1100
committerMiguel Jacq <mig@mig5.net>2021-11-12 10:56:15 +1100
commitdbae142a873c0bb326d0b6fa9ab3a4872280fe9b (patch)
treea2563db6480eb0d39d32ce6518147b57a5aafaa1 /desktop/src
parent627c185fcb3c369f291b285910421d9cdcbf2f86 (diff)
parentda94b92d1807fd2126f748836c79bcbf38a4c0a4 (diff)
downloadonionshare-dbae142a873c0bb326d0b6fa9ab3a4872280fe9b.tar.gz
onionshare-dbae142a873c0bb326d0b6fa9ab3a4872280fe9b.zip
Resolve conflicts in locale
Diffstat (limited to 'desktop/src')
-rw-r--r--desktop/src/onionshare/gui_common.py75
-rw-r--r--desktop/src/onionshare/main_window.py47
-rw-r--r--desktop/src/onionshare/moat_dialog.py372
-rw-r--r--desktop/src/onionshare/resources/images/dark_tor_settings.pngbin0 -> 3600 bytes
-rw-r--r--desktop/src/onionshare/resources/images/light_tor_settings.pngbin0 -> 1503 bytes
-rw-r--r--desktop/src/onionshare/resources/locale/en.json43
-rw-r--r--desktop/src/onionshare/settings_dialog.py1114
-rw-r--r--desktop/src/onionshare/settings_tab.py354
-rw-r--r--desktop/src/onionshare/tab/mode/__init__.py52
-rw-r--r--desktop/src/onionshare/tab/mode/chat_mode/__init__.py6
-rw-r--r--desktop/src/onionshare/tab/mode/receive_mode/__init__.py6
-rw-r--r--desktop/src/onionshare/tab/mode/share_mode/__init__.py6
-rw-r--r--desktop/src/onionshare/tab/mode/website_mode/__init__.py6
-rw-r--r--desktop/src/onionshare/tab/tab.py1
-rw-r--r--desktop/src/onionshare/tab_widget.py193
-rw-r--r--desktop/src/onionshare/tor_connection.py (renamed from desktop/src/onionshare/tor_connection_dialog.py)172
-rw-r--r--desktop/src/onionshare/tor_settings_tab.py895
-rw-r--r--desktop/src/onionshare/widgets.py3
18 files changed, 2098 insertions, 1247 deletions
diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py
index 182d63f2..0db0f051 100644
--- a/desktop/src/onionshare/gui_common.py
+++ b/desktop/src/onionshare/gui_common.py
@@ -93,6 +93,7 @@ class GuiCommon:
share_zip_progess_bar_chunk_color = "#4E064F"
history_background_color = "#ffffff"
history_label_color = "#000000"
+ settings_error_color = "#FF0000"
if color_mode == "dark":
header_color = "#F2F2F2"
title_color = "#F2F2F2"
@@ -103,6 +104,7 @@ class GuiCommon:
share_zip_progess_bar_border_color = "#F2F2F2"
history_background_color = "#191919"
history_label_color = "#ffffff"
+ settings_error_color = "#FF9999"
return {
# OnionShareGui styles
@@ -205,14 +207,14 @@ class GuiCommon:
"downloads_uploads_not_empty": """
QWidget{
background-color: """
- + history_background_color
- +""";
+ + history_background_color
+ + """;
}""",
"downloads_uploads_empty": """
QWidget {
background-color: """
- + history_background_color
- +""";
+ + history_background_color
+ + """;
border: 1px solid #999999;
}
QWidget QLabel {
@@ -263,7 +265,7 @@ class GuiCommon:
+ """;
width: 10px;
}""",
- "history_default_label" : """
+ "history_default_label": """
QLabel {
color: """
+ history_label_color
@@ -281,6 +283,11 @@ class GuiCommon:
QLabel {
color: #cc0000;
}""",
+ "tor_not_connected_label": """
+ QLabel {
+ font-size: 16px;
+ font-style: italic;
+ }""",
# New tab
"new_tab_button_image": """
QLabel {
@@ -392,44 +399,50 @@ class GuiCommon:
QPushButton {
padding: 5px 10px;
}""",
- # Settings dialog
- "settings_version": """
- QLabel {
- color: #666666;
- }""",
- "settings_tor_status": """
- QLabel {
- background-color: #ffffff;
- color: #000000;
- padding: 10px;
- }""",
- "settings_whats_this": """
- QLabel {
- font-size: 12px;
- }""",
- "settings_connect_to_tor": """
+ # Tor Settings dialogs
+ "tor_settings_error": """
QLabel {
- font-style: italic;
- }""",
+ color: """
+ + settings_error_color
+ + """;
+ }
+ """,
}
def get_tor_paths(self):
if self.common.platform == "Linux":
- tor_path = shutil.which("tor")
- obfs4proxy_file_path = shutil.which("obfs4proxy")
- prefix = os.path.dirname(os.path.dirname(tor_path))
- tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip")
- tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6")
- elif self.common.platform == "Windows":
+ base_path = self.get_resource_path("tor")
+ if os.path.exists(base_path):
+ tor_path = os.path.join(base_path, "tor")
+ tor_geo_ip_file_path = os.path.join(base_path, "geoip")
+ tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6")
+ obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy")
+ snowflake_file_path = os.path.join(base_path, "snowflake-client")
+ meek_client_file_path = os.path.join(base_path, "meek-client")
+ else:
+ # Fallback to looking in the path
+ tor_path = shutil.which("tor")
+ obfs4proxy_file_path = shutil.which("obfs4proxy")
+ snowflake_file_path = shutil.which("snowflake-client")
+ meek_client_file_path = shutil.which("meek-client")
+ prefix = os.path.dirname(os.path.dirname(tor_path))
+ tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip")
+ tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6")
+
+ if self.common.platform == "Windows":
base_path = self.get_resource_path("tor")
tor_path = os.path.join(base_path, "Tor", "tor.exe")
obfs4proxy_file_path = os.path.join(base_path, "Tor", "obfs4proxy.exe")
+ snowflake_file_path = os.path.join(base_path, "Tor", "snowflake-client.exe")
+ meek_client_file_path = os.path.join(base_path, "Tor", "meek-client.exe")
tor_geo_ip_file_path = os.path.join(base_path, "Data", "Tor", "geoip")
tor_geo_ipv6_file_path = os.path.join(base_path, "Data", "Tor", "geoip6")
elif self.common.platform == "Darwin":
base_path = self.get_resource_path("tor")
tor_path = os.path.join(base_path, "tor")
obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy")
+ snowflake_file_path = os.path.join(base_path, "snowflake-client")
+ meek_client_file_path = os.path.join(base_path, "meek-client")
tor_geo_ip_file_path = os.path.join(base_path, "geoip")
tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6")
elif self.common.platform == "BSD":
@@ -437,12 +450,16 @@ class GuiCommon:
tor_geo_ip_file_path = "/usr/local/share/tor/geoip"
tor_geo_ipv6_file_path = "/usr/local/share/tor/geoip6"
obfs4proxy_file_path = "/usr/local/bin/obfs4proxy"
+ meek_client_file_path = "/usr/local/bin/meek-client"
+ snowflake_file_path = "/usr/local/bin/snowflake-client"
return (
tor_path,
tor_geo_ip_file_path,
tor_geo_ipv6_file_path,
obfs4proxy_file_path,
+ snowflake_file_path,
+ meek_client_file_path,
)
@staticmethod
diff --git a/desktop/src/onionshare/main_window.py b/desktop/src/onionshare/main_window.py
index d87092b6..546592a1 100644
--- a/desktop/src/onionshare/main_window.py
+++ b/desktop/src/onionshare/main_window.py
@@ -18,12 +18,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
+import os
import time
from PySide2 import QtCore, QtWidgets, QtGui
from . import strings
-from .tor_connection_dialog import TorConnectionDialog
-from .settings_dialog import SettingsDialog
+from .tor_connection import TorConnectionDialog
from .widgets import Alert
from .update_checker import UpdateThread
from .tab_widget import TabWidget
@@ -106,6 +106,24 @@ class MainWindow(QtWidgets.QMainWindow):
)
self.status_bar.addPermanentWidget(self.status_bar.server_status_indicator)
+ # Tor settings button
+ self.tor_settings_button = QtWidgets.QPushButton()
+ self.tor_settings_button.setDefault(False)
+ self.tor_settings_button.setFixedSize(40, 50)
+ self.tor_settings_button.setIcon(
+ QtGui.QIcon(
+ GuiCommon.get_resource_path(
+ "images/{}_tor_settings.png".format(self.common.gui.color_mode)
+ )
+ )
+ )
+ self.tor_settings_button.clicked.connect(self.open_tor_settings)
+ self.tor_settings_button.setStyleSheet(self.common.gui.css["settings_button"])
+ self.status_bar.addPermanentWidget(self.tor_settings_button)
+
+ if os.environ.get("ONIONSHARE_HIDE_TOR_SETTINGS") == "1":
+ self.tor_settings_button.hide()
+
# Settings button
self.settings_button = QtWidgets.QPushButton()
self.settings_button.setDefault(False)
@@ -145,7 +163,7 @@ class MainWindow(QtWidgets.QMainWindow):
# Start the "Connecting to Tor" dialog, which calls onion.connect()
tor_con = TorConnectionDialog(self.common)
tor_con.canceled.connect(self.tor_connection_canceled)
- tor_con.open_settings.connect(self.tor_connection_open_settings)
+ tor_con.open_tor_settings.connect(self.tor_connection_open_tor_settings)
if not self.common.gui.local_only:
tor_con.start()
self.settings_have_changed()
@@ -200,7 +218,7 @@ class MainWindow(QtWidgets.QMainWindow):
"_tor_connection_canceled",
"Settings button clicked",
)
- self.open_settings()
+ self.open_tor_settings()
if a.clickedButton() == quit_button:
# Quit
@@ -214,23 +232,28 @@ class MainWindow(QtWidgets.QMainWindow):
# Wait 100ms before asking
QtCore.QTimer.singleShot(100, ask)
- def tor_connection_open_settings(self):
+ def tor_connection_open_tor_settings(self):
"""
- The TorConnectionDialog wants to open the Settings dialog
+ The TorConnectionDialog wants to open the Tor Settings dialog
"""
- self.common.log("MainWindow", "tor_connection_open_settings")
+ self.common.log("MainWindow", "tor_connection_open_tor_settings")
# Wait 1ms for the event loop to finish closing the TorConnectionDialog
- QtCore.QTimer.singleShot(1, self.open_settings)
+ QtCore.QTimer.singleShot(1, self.open_tor_settings)
+
+ def open_tor_settings(self):
+ """
+ Open the TorSettingsTab
+ """
+ self.common.log("MainWindow", "open_tor_settings")
+ self.tabs.open_tor_settings_tab()
def open_settings(self):
"""
- Open the SettingsDialog.
+ Open the SettingsTab
"""
self.common.log("MainWindow", "open_settings")
- d = SettingsDialog(self.common)
- d.settings_saved.connect(self.settings_have_changed)
- d.exec_()
+ self.tabs.open_settings_tab()
def settings_have_changed(self):
self.common.log("OnionShareGui", "settings_have_changed")
diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py
new file mode 100644
index 00000000..84a52390
--- /dev/null
+++ b/desktop/src/onionshare/moat_dialog.py
@@ -0,0 +1,372 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2021 Micah Lee, et al. <micah@micahflee.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from PySide2 import QtCore, QtWidgets, QtGui
+import requests
+import os
+import base64
+import json
+
+from . import strings
+from .gui_common import GuiCommon
+from onionshare_cli.meek import MeekNotFound, MeekNotRunning
+
+
+class MoatDialog(QtWidgets.QDialog):
+ """
+ Moat dialog: Request a bridge from torproject.org
+ """
+
+ got_bridges = QtCore.Signal(str)
+
+ def __init__(self, common, meek):
+ super(MoatDialog, self).__init__()
+
+ self.common = common
+
+ self.common.log("MoatDialog", "__init__")
+
+ self.meek = meek
+
+ self.setModal(True)
+ self.setWindowTitle(strings._("gui_settings_bridge_moat_button"))
+ self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png")))
+
+ # Label
+ self.label = QtWidgets.QLabel()
+
+ # CAPTCHA image
+ self.captcha = QtWidgets.QLabel()
+ self.captcha.setFixedSize(400, 125) # this is the size of the CAPTCHA image
+
+ # Solution input
+ self.solution_lineedit = QtWidgets.QLineEdit()
+ self.solution_lineedit.setPlaceholderText(strings._("moat_captcha_placeholder"))
+ self.solution_lineedit.editingFinished.connect(
+ self.solution_lineedit_editing_finished
+ )
+ self.submit_button = QtWidgets.QPushButton(strings._("moat_captcha_submit"))
+ self.submit_button.clicked.connect(self.submit_clicked)
+ solution_layout = QtWidgets.QHBoxLayout()
+ solution_layout.addWidget(self.solution_lineedit)
+ solution_layout.addWidget(self.submit_button)
+
+ # Error label
+ self.error_label = QtWidgets.QLabel()
+ self.error_label.setStyleSheet(self.common.gui.css["tor_settings_error"])
+ self.error_label.hide()
+
+ # Buttons
+ self.reload_button = QtWidgets.QPushButton(strings._("moat_captcha_reload"))
+ self.reload_button.clicked.connect(self.reload_clicked)
+ self.cancel_button = QtWidgets.QPushButton(
+ strings._("gui_settings_button_cancel")
+ )
+ self.cancel_button.clicked.connect(self.cancel_clicked)
+ buttons_layout = QtWidgets.QHBoxLayout()
+ buttons_layout.addStretch()
+ buttons_layout.addWidget(self.reload_button)
+ buttons_layout.addWidget(self.cancel_button)
+
+ # Layout
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(self.label)
+ layout.addWidget(self.captcha)
+ layout.addLayout(solution_layout)
+ layout.addStretch()
+ layout.addWidget(self.error_label)
+ layout.addLayout(buttons_layout)
+
+ self.setLayout(layout)
+ self.cancel_button.setFocus()
+
+ self.reload_clicked()
+
+ def reload_clicked(self):
+ """
+ Reload button clicked.
+ """
+ self.common.log("MoatDialog", "reload_clicked")
+
+ self.label.setText(strings._("moat_contact_label"))
+ self.error_label.hide()
+
+ self.captcha.hide()
+ self.solution_lineedit.hide()
+ self.reload_button.hide()
+ self.submit_button.hide()
+
+ # BridgeDB fetch
+ self.t_fetch = MoatThread(self.common, self.meek, "fetch")
+ self.t_fetch.bridgedb_error.connect(self.bridgedb_error)
+ self.t_fetch.captcha_ready.connect(self.captcha_ready)
+ self.t_fetch.start()
+
+ def submit_clicked(self):
+ """
+ Submit button clicked.
+ """
+ self.error_label.hide()
+ self.solution_lineedit.setEnabled(False)
+
+ solution = self.solution_lineedit.text().strip()
+ if len(solution) == 0:
+ self.common.log("MoatDialog", "submit_clicked", "solution is blank")
+ self.error_label.setText(strings._("moat_solution_empty_error"))
+ self.error_label.show()
+ return
+
+ # BridgeDB check
+ self.t_check = MoatThread(
+ self.common,
+ self.meek,
+ "check",
+ {
+ "transport": self.transport,
+ "challenge": self.challenge,
+ "solution": self.solution_lineedit.text(),
+ },
+ )
+ self.t_check.bridgedb_error.connect(self.bridgedb_error)
+ self.t_check.captcha_error.connect(self.captcha_error)
+ self.t_check.bridges_ready.connect(self.bridges_ready)
+ self.t_check.start()
+
+ def cancel_clicked(self):
+ """
+ Cancel button clicked.
+ """
+ self.common.log("MoatDialog", "cancel_clicked")
+ self.close()
+
+ def bridgedb_error(self):
+ self.common.log("MoatDialog", "bridgedb_error")
+ self.error_label.setText(strings._("moat_bridgedb_error"))
+ self.error_label.show()
+
+ self.solution_lineedit.setEnabled(True)
+
+ def captcha_error(self, msg):
+ self.common.log("MoatDialog", "captcha_error")
+ if msg == "":
+ self.error_label.setText(strings._("moat_captcha_error"))
+ else:
+ self.error_label.setText(msg)
+ self.error_label.show()
+
+ self.solution_lineedit.setEnabled(True)
+
+ def captcha_ready(self, transport, image, challenge):
+ self.common.log("MoatDialog", "captcha_ready")
+
+ self.transport = transport
+ self.challenge = challenge
+
+ # Save captcha image to disk, so we can load it
+ captcha_data = base64.b64decode(image)
+ captcha_filename = os.path.join(self.common.build_tmp_dir(), "captcha.jpg")
+ with open(captcha_filename, "wb") as f:
+ f.write(captcha_data)
+
+ self.captcha.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(captcha_filename)))
+ os.remove(captcha_filename)
+
+ self.label.setText(strings._("moat_captcha_label"))
+ self.captcha.show()
+ self.solution_lineedit.setEnabled(True)
+ self.solution_lineedit.setText("")
+ self.solution_lineedit.show()
+ self.solution_lineedit.setFocus()
+ self.reload_button.show()
+ self.submit_button.show()
+
+ def solution_lineedit_editing_finished(self):
+ self.common.log("MoatDialog", "solution_lineedit_editing_finished")
+
+ def bridges_ready(self, bridges):
+ self.common.log("MoatDialog", "bridges_ready", bridges)
+ self.got_bridges.emit(bridges)
+ self.close()
+
+
+class MoatThread(QtCore.QThread):
+ """
+ This does all of the communicating with BridgeDB in a separate thread.
+
+ Valid actions are:
+ - "fetch": requests a new CAPTCHA
+ - "check": sends a CAPTCHA solution
+
+ """
+
+ bridgedb_error = QtCore.Signal()
+ captcha_error = QtCore.Signal(str)
+ captcha_ready = QtCore.Signal(str, str, str)
+ bridges_ready = QtCore.Signal(str)
+
+ def __init__(self, common, meek, action, data={}):
+ super(MoatThread, self).__init__()
+ self.common = common
+ self.common.log("MoatThread", "__init__", f"action={action}")
+
+ self.meek = meek
+ self.transport = "obfs4"
+ self.action = action
+ self.data = data
+
+ def run(self):
+
+ # Start Meek so that we can do domain fronting
+ try:
+ self.meek.start()
+ except MeekNotFound:
+ self.common.log("MoatThread", "run", f"Could not find meek-client")
+ self.bridgedb_error.emit()
+ return
+ except MeekNotRunning:
+ self.common.log(
+ "MoatThread", "run", f"Ran meek-client, but there was an error"
+ )
+ self.bridgedb_error.emit()
+ return
+
+ # We should only fetch bridges if we can domain front,
+ # but we can override this in local-only mode.
+ if not self.meek.meek_proxies and not self.common.gui.local_only:
+ self.common.log(
+ "MoatThread", "run", f"Could not identify meek proxies to make request"
+ )
+ self.bridgedb_error.emit()
+ return
+
+ if self.action == "fetch":
+ self.common.log("MoatThread", "run", f"starting fetch")
+
+ # Request a bridge
+ r = requests.post(
+ "https://bridges.torproject.org/moat/fetch",
+ headers={"Content-Type": "application/vnd.api+json"},
+ proxies=self.meek.meek_proxies,
+ json={
+ "data": [
+ {
+ "version": "0.1.0",
+ "type": "client-transports",
+ "supported": ["obfs4", "snowflake"],
+ }
+ ]
+ },
+ )
+
+ self.meek.cleanup()
+
+ if r.status_code != 200:
+ self.common.log("MoatThread", "run", f"status_code={r.status_code}")
+ self.bridgedb_error.emit()
+ return
+
+ try:
+ moat_res = r.json()
+ if "errors" in moat_res:
+ self.common.log("MoatThread", "run", f"errors={moat_res['errors']}")
+ self.bridgedb_error.emit()
+ return
+ if "data" not in moat_res:
+ self.common.log("MoatThread", "run", f"no data")
+ self.bridgedb_error.emit()
+ return
+ if moat_res["data"][0]["type"] != "moat-challenge":
+ self.common.log("MoatThread", "run", f"type != moat-challange")
+ self.bridgedb_error.emit()
+ return
+
+ transport = moat_res["data"][0]["transport"]
+ image = moat_res["data"][0]["image"]
+ challenge = moat_res["data"][0]["challenge"]
+
+ self.captcha_ready.emit(transport, image, challenge)
+ except Exception as e:
+ self.common.log("MoatThread", "run", f"hit exception: {e}")
+ self.bridgedb_error.emit()
+ return
+
+ elif self.action == "check":
+ self.common.log("MoatThread", "run", f"starting check")
+
+ # Check the CAPTCHA
+ r = requests.post(
+ "https://bridges.torproject.org/moat/check",
+ headers={"Content-Type": "application/vnd.api+json"},
+ proxies=self.meek.meek_proxies,
+ json={
+ "data": [
+ {
+ "id": "2",
+ "type": "moat-solution",
+ "version": "0.1.0",
+ "transport": self.data["transport"],
+ "challenge": self.data["challenge"],
+ "solution": self.data["solution"],
+ "qrcode": "false",
+ }
+ ]
+ },
+ )
+
+ self.meek.cleanup()
+
+ if r.status_code != 200:
+ self.common.log("MoatThread", "run", f"status_code={r.status_code}")
+ self.bridgedb_error.emit()
+ return
+
+ try:
+ moat_res = r.json()
+ self.common.log(
+ "MoatThread",
+ "run",
+ f"got bridges:\n{json.dumps(moat_res,indent=2)}",
+ )
+
+ if "errors" in moat_res:
+ self.common.log("MoatThread", "run", f"errors={moat_res['errors']}")
+ if moat_res["errors"][0]["code"] == 419:
+ self.captcha_error.emit("")
+ return
+ else:
+ errors = " ".join([e["detail"] for e in moat_res["errors"]])
+ self.captcha_error.emit(errors)
+ return
+
+ if moat_res["data"][0]["type"] != "moat-bridges":
+ self.common.log("MoatThread", "run", f"type != moat-bridges")
+ self.bridgedb_error.emit()
+ return
+
+ bridges = moat_res["data"][0]["bridges"]
+ self.bridges_ready.emit("\n".join(bridges))
+
+ except Exception as e:
+ self.common.log("MoatThread", "run", f"hit exception: {e}")
+ self.bridgedb_error.emit()
+ return
+
+ else:
+ self.common.log("MoatThread", "run", f"invalid action: {self.action}")
diff --git a/desktop/src/onionshare/resources/images/dark_tor_settings.png b/desktop/src/onionshare/resources/images/dark_tor_settings.png
new file mode 100644
index 00000000..0b44bd95
--- /dev/null
+++ b/desktop/src/onionshare/resources/images/dark_tor_settings.png
Binary files differ
diff --git a/desktop/src/onionshare/resources/images/light_tor_settings.png b/desktop/src/onionshare/resources/images/light_tor_settings.png
new file mode 100644
index 00000000..e8db08eb
--- /dev/null
+++ b/desktop/src/onionshare/resources/images/light_tor_settings.png
Binary files differ
diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json
index 437a3eda..db5fbfe5 100644
--- a/desktop/src/onionshare/resources/locale/en.json
+++ b/desktop/src/onionshare/resources/locale/en.json
@@ -40,6 +40,7 @@
"gui_please_wait_no_button": "Starting…",
"gui_please_wait": "Starting… Click to cancel.",
"zip_progress_bar_format": "Compressing: %p%",
+ "gui_tor_settings_window_title": "Tor Settings",
"gui_settings_window_title": "Settings",
"gui_settings_autoupdate_label": "Check for new version",
"gui_settings_autoupdate_option": "Notify me when a new version is available",
@@ -49,29 +50,34 @@
"gui_settings_connection_type_label": "How should OnionShare connect to Tor?",
"gui_settings_connection_type_bundled_option": "Use the Tor version built into OnionShare",
"gui_settings_connection_type_automatic_option": "Attempt auto-configuration with Tor Browser",
+ "gui_settings_controller_extras_label": "Tor settings",
"gui_settings_connection_type_control_port_option": "Connect using control port",
"gui_settings_connection_type_socket_file_option": "Connect using socket file",
"gui_settings_connection_type_test_button": "Test Connection to Tor",
"gui_settings_control_port_label": "Control port",
"gui_settings_socket_file_label": "Socket file",
"gui_settings_socks_label": "SOCKS port",
- "gui_settings_authenticate_label": "Tor authentication settings",
"gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication",
"gui_settings_authenticate_password_option": "Password",
"gui_settings_password_label": "Password",
- "gui_settings_tor_bridges": "Tor bridge support",
- "gui_settings_tor_bridges_no_bridges_radio_option": "Don't use bridges",
- "gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 pluggable transports",
- "gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Use built-in obfs4 pluggable transports (requires obfs4proxy)",
- "gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek_lite (Azure) pluggable transports",
- "gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy": "Use built-in meek_lite (Azure) pluggable transports (requires obfs4proxy)",
- "gui_settings_meek_lite_expensive_warning": "Warning: The meek_lite bridges are very costly for the Tor Project to run.<br><br>Only use them if unable to connect to Tor directly, via obfs4 transports, or other normal bridges.",
- "gui_settings_tor_bridges_custom_radio_option": "Use custom bridges",
- "gui_settings_tor_bridges_custom_label": "You can get bridges from <a href=\"https://bridges.torproject.org/options\">https://bridges.torproject.org</a>",
- "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.",
+ "gui_settings_tor_bridges": "Connect using a Tor bridge?",
+ "gui_settings_tor_bridges_label": "Bridges help you access the Tor Network in places where Tor is blocked. Depending on where you are, one bridge may work better than another.",
+ "gui_settings_bridge_use_checkbox": "Use a bridge",
+ "gui_settings_bridge_radio_builtin": "Select a built-in bridge",
+ "gui_settings_bridge_none_radio_option": "Don't use a bridge",
+ "gui_settings_meek_lite_expensive_warning": "Warning: The meek-azure bridges are very costly for the Tor Project to run.<br><br>Only use them if unable to connect to Tor directly, via obfs4 transports, or other normal bridges.",
+ "gui_settings_bridge_moat_radio_option": "Request a bridge from torproject.org",
+ "gui_settings_bridge_moat_button": "Request a New Bridge",
+ "gui_settings_bridge_custom_radio_option": "Provide a bridge you learned about from a trusted source",
+ "gui_settings_bridge_custom_placeholder": "type address:port (one per line)",
+ "gui_settings_moat_bridges_invalid": "You have not requested a bridge from torproject.org yet.",
+ "gui_settings_tor_bridges_invalid": "None of the bridges you added work. Double-check them or add others.",
+ "gui_settings_stop_active_tabs_label": "There are services running in some of your tabs.\nYou must stop all services to change your Tor settings.",
"gui_settings_button_save": "Save",
"gui_settings_button_cancel": "Cancel",
"gui_settings_button_help": "Help",
+ "gui_settings_version_label": "You are using OnionShare {}",
+ "gui_settings_help_label": "Need help? See <a href='https://docs.onionshare.org'>docs.onionshare.org</a>",
"settings_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports client authentication: {}.\nSupports next-gen .onion addresses: {}.",
"connecting_to_tor": "Connecting to the Tor network",
"update_available": "New OnionShare out. <a href='{}'>Click here</a> to get it.<br><br>You are using {} and the latest is {}.",
@@ -125,7 +131,7 @@
"error_cannot_create_data_dir": "Could not create OnionShare data folder: {}",
"gui_receive_mode_warning": "Receive mode lets people upload files to your computer.<br><br><b>Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.</b>",
"gui_open_folder_error": "Failed to open folder with xdg-open. The file is here: {}",
- "gui_settings_language_label": "Preferred language",
+ "gui_settings_language_label": "Language",
"gui_settings_theme_label": "Theme",
"gui_settings_theme_auto": "Auto",
"gui_settings_theme_light": "Light",
@@ -197,7 +203,7 @@
"mode_settings_receive_disable_files_checkbox": "Disable uploading files",
"mode_settings_receive_webhook_url_checkbox": "Use notification webhook",
"mode_settings_website_disable_csp_checkbox": "Don't send default Content Security Policy header (allows your website to use third-party resources)",
- "mode_settings_website_custom_csp_checkbox": "Send a Custom Content Security Policy header",
+ "mode_settings_website_custom_csp_checkbox": "Send a custom Content Security Policy header",
"gui_all_modes_transfer_finished_range": "Transferred {} - {}",
"gui_all_modes_transfer_finished": "Transferred {}",
"gui_all_modes_transfer_canceled_range": "Canceled {} - {}",
@@ -216,5 +222,14 @@
"gui_rendezvous_cleanup_quit_early": "Quit Early",
"error_port_not_available": "OnionShare port not available",
"history_receive_read_message_button": "Read Message",
- "error_tor_protocol_error": "There was an error with Tor: {}"
+ "error_tor_protocol_error": "There was an error with Tor: {}",
+ "moat_contact_label": "Contacting BridgeDB...",
+ "moat_captcha_label": "Solve the CAPTCHA to request a bridge.",
+ "moat_captcha_placeholder": "Enter the characters from the image",
+ "moat_captcha_submit": "Submit",
+ "moat_captcha_reload": "Reload",
+ "moat_bridgedb_error": "Error contacting BridgeDB.",
+ "moat_captcha_error": "The solution is not correct. Please try again.",
+ "moat_solution_empty_error": "You must enter the characters from the image",
+ "mode_tor_not_connected_label": "OnionShare is not connected to the Tor network"
}
diff --git a/desktop/src/onionshare/settings_dialog.py b/desktop/src/onionshare/settings_dialog.py
deleted file mode 100644
index e8d2752c..00000000
--- a/desktop/src/onionshare/settings_dialog.py
+++ /dev/null
@@ -1,1114 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-OnionShare | https://onionshare.org/
-
-Copyright (C) 2014-2021 Micah Lee, et al. <micah@micahflee.com>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""
-
-from PySide2 import QtCore, QtWidgets, QtGui
-from PySide2.QtCore import Slot,Qt
-from PySide2.QtGui import QPalette, QColor
-import sys
-import platform
-import datetime
-import re
-import os
-from onionshare_cli.settings import Settings
-from onionshare_cli.onion import (
- Onion,
- TorErrorInvalidSetting,
- TorErrorAutomatic,
- TorErrorSocketPort,
- TorErrorSocketFile,
- TorErrorMissingPassword,
- TorErrorUnreadableCookieFile,
- TorErrorAuthError,
- TorErrorProtocolError,
- BundledTorTimeout,
- BundledTorBroken,
- TorTooOldEphemeral,
- TorTooOldStealth,
- PortNotAvailable,
-)
-
-from . import strings
-from .widgets import Alert
-from .update_checker import (
- UpdateThread)
-from .tor_connection_dialog import TorConnectionDialog
-from .gui_common import GuiCommon
-
-
-class SettingsDialog(QtWidgets.QDialog):
- """
- Settings dialog.
- """
-
- settings_saved = QtCore.Signal()
-
- def __init__(self, common):
- super(SettingsDialog, self).__init__()
-
- self.common = common
-
- self.common.log("SettingsDialog", "__init__")
-
- self.setModal(True)
- self.setWindowTitle(strings._("gui_settings_window_title"))
- self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png")))
-
- self.system = platform.system()
-
- # If ONIONSHARE_HIDE_TOR_SETTINGS=1, hide Tor settings in the dialog
- self.hide_tor_settings = os.environ.get("ONIONSHARE_HIDE_TOR_SETTINGS") == "1"
-
- # Automatic updates options
-
- # Autoupdate
- self.autoupdate_checkbox = QtWidgets.QCheckBox()
- self.autoupdate_checkbox.setCheckState(QtCore.Qt.Unchecked)
- self.autoupdate_checkbox.setText(strings._("gui_settings_autoupdate_option"))
-
- # Last update time
- self.autoupdate_timestamp = QtWidgets.QLabel()
-
- # Check for updates button
- self.check_for_updates_button = QtWidgets.QPushButton(
- strings._("gui_settings_autoupdate_check_button")
- )
- self.check_for_updates_button.clicked.connect(self.check_for_updates)
- # We can't check for updates if not connected to Tor
- if not self.common.gui.onion.connected_to_tor:
- self.check_for_updates_button.setEnabled(False)
-
- # Autoupdate options layout
- autoupdate_group_layout = QtWidgets.QVBoxLayout()
- autoupdate_group_layout.addWidget(self.autoupdate_checkbox)
- autoupdate_group_layout.addWidget(self.autoupdate_timestamp)
- autoupdate_group_layout.addWidget(self.check_for_updates_button)
- autoupdate_group = QtWidgets.QGroupBox(
- strings._("gui_settings_autoupdate_label")
- )
- autoupdate_group.setLayout(autoupdate_group_layout)
-
- # Autoupdate is only available for Windows and Mac (Linux updates using package manager)
- if self.system != "Windows" and self.system != "Darwin":
- autoupdate_group.hide()
-
- # Language settings
- language_label = QtWidgets.QLabel(strings._("gui_settings_language_label"))
- self.language_combobox = QtWidgets.QComboBox()
- # Populate the dropdown with all of OnionShare's available languages
- language_names_to_locales = {
- v: k for k, v in self.common.settings.available_locales.items()
- }
- language_names = list(language_names_to_locales)
- language_names.sort()
- for language_name in language_names:
- locale = language_names_to_locales[language_name]
- self.language_combobox.addItem(language_name, locale)
- language_layout = QtWidgets.QHBoxLayout()
- language_layout.addWidget(language_label)
- language_layout.addWidget(self.language_combobox)
- language_layout.addStretch()
-
- #Theme Settings
- theme_label = QtWidgets.QLabel(strings._("gui_settings_theme_label"))
- self.theme_combobox = QtWidgets.QComboBox()
- theme_choices = [
- strings._("gui_settings_theme_auto"),
- strings._("gui_settings_theme_light"),
- strings._("gui_settings_theme_dark")
- ]
- self.theme_combobox.addItems(theme_choices)
- theme_layout = QtWidgets.QHBoxLayout()
- theme_layout.addWidget(theme_label)
- theme_layout.addWidget(self.theme_combobox)
- theme_layout.addStretch()
-
- # Connection type: either automatic, control port, or socket file
-
- # Bundled Tor
- self.connection_type_bundled_radio = QtWidgets.QRadioButton(
- strings._("gui_settings_connection_type_bundled_option")
- )
- self.connection_type_bundled_radio.toggled.connect(
- self.connection_type_bundled_toggled
- )
-
- # Bundled Tor doesn't work on dev mode in Windows or Mac
- if (self.system == "Windows" or self.system == "Darwin") and getattr(
- sys, "onionshare_dev_mode", False
- ):
- self.connection_type_bundled_radio.setEnabled(False)
-
- # Bridge options for bundled tor
-
- # No bridges option radio
- self.tor_bridges_no_bridges_radio = QtWidgets.QRadioButton(
- strings._("gui_settings_tor_bridges_no_bridges_radio_option")
- )
- self.tor_bridges_no_bridges_radio.toggled.connect(
- self.tor_bridges_no_bridges_radio_toggled
- )
-
- # obfs4 option radio
- # if the obfs4proxy binary is missing, we can't use obfs4 transports
- (
- self.tor_path,
- self.tor_geo_ip_file_path,
- self.tor_geo_ipv6_file_path,
- self.obfs4proxy_file_path,
- ) = self.common.gui.get_tor_paths()
- if not self.obfs4proxy_file_path or not os.path.isfile(
- self.obfs4proxy_file_path
- ):
- self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton(
- strings._("gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy")
- )
- self.tor_bridges_use_obfs4_radio.setEnabled(False)
- else:
- self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton(
- strings._("gui_settings_tor_bridges_obfs4_radio_option")
- )
- self.tor_bridges_use_obfs4_radio.toggled.connect(
- self.tor_bridges_use_obfs4_radio_toggled
- )
-
- # meek_lite-azure option radio
- # if the obfs4proxy binary is missing, we can't use meek_lite-azure transports
- (
- self.tor_path,
- self.tor_geo_ip_file_path,
- self.tor_geo_ipv6_file_path,
- self.obfs4proxy_file_path,
- ) = self.common.gui.get_tor_paths()
- if not self.obfs4proxy_file_path or not os.path.isfile(
- self.obfs4proxy_file_path
- ):
- self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(
- strings._(
- "gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy"
- )
- )
- self.tor_bridges_use_meek_lite_azure_radio.setEnabled(False)
- else:
- self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(
- strings._("gui_settings_tor_bridges_meek_lite_azure_radio_option")
- )
- self.tor_bridges_use_meek_lite_azure_radio.toggled.connect(
- self.tor_bridges_use_meek_lite_azure_radio_toggled
- )
-
- # Custom bridges radio and textbox
- self.tor_bridges_use_custom_radio = QtWidgets.QRadioButton(
- strings._("gui_settings_tor_bridges_custom_radio_option")
- )
- self.tor_bridges_use_custom_radio.toggled.connect(
- self.tor_bridges_use_custom_radio_toggled
- )
-
- self.tor_bridges_use_custom_label = QtWidgets.QLabel(
- strings._("gui_settings_tor_bridges_custom_label")
- )
- self.tor_bridges_use_custom_label.setTextInteractionFlags(
- QtCore.Qt.TextBrowserInteraction
- )
- self.tor_bridges_use_custom_label.setOpenExternalLinks(True)
- self.tor_bridges_use_custom_textbox = QtWidgets.QPlainTextEdit()
- self.tor_bridges_use_custom_textbox.setMaximumHeight(200)
- self.tor_bridges_use_custom_textbox.setPlaceholderText(
- "[address:port] [identifier]"
- )
-
- tor_bridges_use_custom_textbox_options_layout = QtWidgets.QVBoxLayout()
- tor_bridges_use_custom_textbox_options_layout.addWidget(
- self.tor_bridges_use_custom_label
- )
- tor_bridges_use_custom_textbox_options_layout.addWidget(
- self.tor_bridges_use_custom_textbox
- )
-
- self.tor_bridges_use_custom_textbox_options = QtWidgets.QWidget()
- self.tor_bridges_use_custom_textbox_options.setLayout(
- tor_bridges_use_custom_textbox_options_layout
- )
- self.tor_bridges_use_custom_textbox_options.hide()
-
- # Bridges layout/widget
- bridges_layout = QtWidgets.QVBoxLayout()
- bridges_layout.addWidget(self.tor_bridges_no_bridges_radio)
- bridges_layout.addWidget(self.tor_bridges_use_obfs4_radio)
- bridges_layout.addWidget(self.tor_bridges_use_meek_lite_azure_radio)
- bridges_layout.addWidget(self.tor_bridges_use_custom_radio)
- bridges_layout.addWidget(self.tor_bridges_use_custom_textbox_options)
-
- self.bridges = QtWidgets.QWidget()
- self.bridges.setLayout(bridges_layout)
-
- # Automatic
- self.connection_type_automatic_radio = QtWidgets.QRadioButton(
- strings._("gui_settings_connection_type_automatic_option")
- )
- self.connection_type_automatic_radio.toggled.connect(
- self.connection_type_automatic_toggled
- )
-
- # Control port
- self.connection_type_control_port_radio = QtWidgets.QRadioButton(
- strings._("gui_settings_connection_type_control_port_option")
- )
- self.connection_type_control_port_radio.toggled.connect(
- self.connection_type_control_port_toggled
- )
-
- connection_type_control_port_extras_label = QtWidgets.QLabel(
- strings._("gui_settings_control_port_label")
- )
- self.connection_type_control_port_extras_address = QtWidgets.QLineEdit()
- self.connection_type_control_port_extras_port = QtWidgets.QLineEdit()
- connection_type_control_port_extras_layout = QtWidgets.QHBoxLayout()
- connection_type_control_port_extras_layout.addWidget(
- connection_type_control_port_extras_label
- )
- connection_type_control_port_extras_layout.addWidget(
- self.connection_type_control_port_extras_address
- )
- connection_type_control_port_extras_layout.addWidget(
- self.connection_type_control_port_extras_port
- )
-
- self.connection_type_control_port_extras = QtWidgets.QWidget()
- self.connection_type_control_port_extras.setLayout(
- connection_type_control_port_extras_layout
- )
- self.connection_type_control_port_extras.hide()
-
- # Socket file
- self.connection_type_socket_file_radio = QtWidgets.QRadioButton(
- strings._("gui_settings_connection_type_socket_file_option")
- )
- self.connection_type_socket_file_radio.toggled.connect(
- self.connection_type_socket_file_toggled
- )
-
- connection_type_socket_file_extras_label = QtWidgets.QLabel(
- strings._("gui_settings_socket_file_label")
- )
- self.connection_type_socket_file_extras_path = QtWidgets.QLineEdit()
- connection_type_socket_file_extras_layout = QtWidgets.QHBoxLayout()
- connection_type_socket_file_extras_layout.addWidget(
- connection_type_socket_file_extras_label
- )
- connection_type_socket_file_extras_layout.addWidget(
- self.connection_type_socket_file_extras_path
- )
-
- self.connection_type_socket_file_extras = QtWidgets.QWidget()
- self.connection_type_socket_file_extras.setLayout(
- connection_type_socket_file_extras_layout
- )
- self.connection_type_socket_file_extras.hide()
-
- # Tor SOCKS address and port
- gui_settings_socks_label = QtWidgets.QLabel(
- strings._("gui_settings_socks_label")
- )
- self.connection_type_socks_address = QtWidgets.QLineEdit()
- self.connection_type_socks_port = QtWidgets.QLineEdit()
- connection_type_socks_layout = QtWidgets.QHBoxLayout()
- connection_type_socks_layout.addWidget(gui_settings_socks_label)
- connection_type_socks_layout.addWidget(self.connection_type_socks_address)
- connection_type_socks_layout.addWidget(self.connection_type_socks_port)
-
- self.connection_type_socks = QtWidgets.QWidget()
- self.connection_type_socks.setLayout(connection_type_socks_layout)
- self.connection_type_socks.hide()
-
- # Authentication options
-
- # No authentication
- self.authenticate_no_auth_radio = QtWidgets.QRadioButton(
- strings._("gui_settings_authenticate_no_auth_option")
- )
- self.authenticate_no_auth_radio.toggled.connect(
- self.authenticate_no_auth_toggled
- )
-
- # Password
- self.authenticate_password_radio = QtWidgets.QRadioButton(
- strings._("gui_settings_authenticate_password_option")
- )
- self.authenticate_password_radio.toggled.connect(
- self.authenticate_password_toggled
- )
-
- authenticate_password_extras_label = QtWidgets.QLabel(
- strings._("gui_settings_password_label")
- )
- self.authenticate_password_extras_password = QtWidgets.QLineEdit("")
- authenticate_password_extras_layout = QtWidgets.QHBoxLayout()
- authenticate_password_extras_layout.addWidget(
- authenticate_password_extras_label
- )
- authenticate_password_extras_layout.addWidget(
- self.authenticate_password_extras_password
- )
-
- self.authenticate_password_extras = QtWidgets.QWidget()
- self.authenticate_password_extras.setLayout(authenticate_password_extras_layout)
- self.authenticate_password_extras.hide()
-
- # Authentication options layout
- authenticate_group_layout = QtWidgets.QVBoxLayout()
- authenticate_group_layout.addWidget(self.authenticate_no_auth_radio)
- authenticate_group_layout.addWidget(self.authenticate_password_radio)
- authenticate_group_layout.addWidget(self.authenticate_password_extras)
- self.authenticate_group = QtWidgets.QGroupBox(
- strings._("gui_settings_authenticate_label")
- )
- self.authenticate_group.setLayout(authenticate_group_layout)
-
- # Put the radios into their own group so they are exclusive
- connection_type_radio_group_layout = QtWidgets.QVBoxLayout()
- connection_type_radio_group_layout.addWidget(self.connection_type_bundled_radio)
- connection_type_radio_group_layout.addWidget(
- self.connection_type_automatic_radio
- )
- connection_type_radio_group_layout.addWidget(
- self.connection_type_control_port_radio
- )
- connection_type_radio_group_layout.addWidget(
- self.connection_type_socket_file_radio
- )
- connection_type_radio_group = QtWidgets.QGroupBox(
- strings._("gui_settings_connection_type_label")
- )
- connection_type_radio_group.setLayout(connection_type_radio_group_layout)
-
- # The Bridges options are not exclusive (enabling Bridges offers obfs4 or custom bridges)
- connection_type_bridges_radio_group_layout = QtWidgets.QVBoxLayout()
- connection_type_bridges_radio_group_layout.addWidget(self.bridges)
- self.connection_type_bridges_radio_group = QtWidgets.QGroupBox(
- strings._("gui_settings_tor_bridges")
- )
- self.connection_type_bridges_radio_group.setLayout(
- connection_type_bridges_radio_group_layout
- )
- self.connection_type_bridges_radio_group.hide()
-
- # Test tor settings button
- self.connection_type_test_button = QtWidgets.QPushButton(
- strings._("gui_settings_connection_type_test_button")
- )
- self.connection_type_test_button.clicked.connect(self.test_tor_clicked)
- connection_type_test_button_layout = QtWidgets.QHBoxLayout()
- connection_type_test_button_layout.addWidget(self.connection_type_test_button)
- connection_type_test_button_layout.addStretch()
-
- # Connection type layout
- connection_type_layout = QtWidgets.QVBoxLayout()
- connection_type_layout.addWidget(self.connection_type_control_port_extras)
- connection_type_layout.addWidget(self.connection_type_socket_file_extras)
- connection_type_layout.addWidget(self.connection_type_socks)
- connection_type_layout.addWidget(self.authenticate_group)
- connection_type_layout.addWidget(self.connection_type_bridges_radio_group)
- connection_type_layout.addLayout(connection_type_test_button_layout)
-
- # Buttons
- self.save_button = QtWidgets.QPushButton(strings._("gui_settings_button_save"))
- self.save_button.clicked.connect(self.save_clicked)
- self.cancel_button = QtWidgets.QPushButton(
- strings._("gui_settings_button_cancel")
- )
- self.cancel_button.clicked.connect(self.cancel_clicked)
- version_label = QtWidgets.QLabel(f"OnionShare {self.common.version}")
- version_label.setStyleSheet(self.common.gui.css["settings_version"])
- self.help_button = QtWidgets.QPushButton(strings._("gui_settings_button_help"))
- self.help_button.clicked.connect(self.help_clicked)
- buttons_layout = QtWidgets.QHBoxLayout()
- buttons_layout.addWidget(version_label)
- buttons_layout.addWidget(self.help_button)
- buttons_layout.addStretch()
- buttons_layout.addWidget(self.save_button)
- buttons_layout.addWidget(self.cancel_button)
-
- # Tor network connection status
- self.tor_status = QtWidgets.QLabel()
- self.tor_status.setStyleSheet(self.common.gui.css["settings_tor_status"])
- self.tor_status.hide()
-
- # Layout
- tor_layout = QtWidgets.QVBoxLayout()
- tor_layout.addWidget(connection_type_radio_group)
- tor_layout.addLayout(connection_type_layout)
- tor_layout.addWidget(self.tor_status)
- tor_layout.addStretch()
-
- layout = QtWidgets.QVBoxLayout()
- if not self.hide_tor_settings:
- layout.addLayout(tor_layout)
- layout.addSpacing(20)
- layout.addWidget(autoupdate_group)
- if autoupdate_group.isVisible():
- layout.addSpacing(20)
- layout.addLayout(language_layout)
- layout.addSpacing(20)
- layout.addLayout(theme_layout)
- layout.addSpacing(20)
- layout.addStretch()
- layout.addLayout(buttons_layout)
-
- self.setLayout(layout)
- self.cancel_button.setFocus()
-
- self.reload_settings()
-
- def reload_settings(self):
- # Load settings, and fill them in
- self.old_settings = Settings(self.common)
- self.old_settings.load()
-
- use_autoupdate = self.old_settings.get("use_autoupdate")
- if use_autoupdate:
- self.autoupdate_checkbox.setCheckState(QtCore.Qt.Checked)
- else:
- self.autoupdate_checkbox.setCheckState(QtCore.Qt.Unchecked)
-
- autoupdate_timestamp = self.old_settings.get("autoupdate_timestamp")
- self._update_autoupdate_timestamp(autoupdate_timestamp)
-
- locale = self.old_settings.get("locale")
- locale_index = self.language_combobox.findData(locale)
- self.language_combobox.setCurrentIndex(locale_index)
-
- theme_choice = self.old_settings.get("theme")
- self.theme_combobox.setCurrentIndex(theme_choice)
-
- connection_type = self.old_settings.get("connection_type")
- if connection_type == "bundled":
- if self.connection_type_bundled_radio.isEnabled():
- self.connection_type_bundled_radio.setChecked(True)
- else:
- # If bundled tor is disabled, fallback to automatic
- self.connection_type_automatic_radio.setChecked(True)
- elif connection_type == "automatic":
- self.connection_type_automatic_radio.setChecked(True)
- elif connection_type == "control_port":
- self.connection_type_control_port_radio.setChecked(True)
- elif connection_type == "socket_file":
- self.connection_type_socket_file_radio.setChecked(True)
- self.connection_type_control_port_extras_address.setText(
- self.old_settings.get("control_port_address")
- )
- self.connection_type_control_port_extras_port.setText(
- str(self.old_settings.get("control_port_port"))
- )
- self.connection_type_socket_file_extras_path.setText(
- self.old_settings.get("socket_file_path")
- )
- self.connection_type_socks_address.setText(
- self.old_settings.get("socks_address")
- )
- self.connection_type_socks_port.setText(
- str(self.old_settings.get("socks_port"))
- )
- auth_type = self.old_settings.get("auth_type")
- if auth_type == "no_auth":
- self.authenticate_no_auth_radio.setChecked(True)
- elif auth_type == "password":
- self.authenticate_password_radio.setChecked(True)
- self.authenticate_password_extras_password.setText(
- self.old_settings.get("auth_password")
- )
-
- if self.old_settings.get("no_bridges"):
- self.tor_bridges_no_bridges_radio.setChecked(True)
- self.tor_bridges_use_obfs4_radio.setChecked(False)
- self.tor_bridges_use_meek_lite_azure_radio.setChecked(False)
- self.tor_bridges_use_custom_radio.setChecked(False)
- else:
- self.tor_bridges_no_bridges_radio.setChecked(False)
- self.tor_bridges_use_obfs4_radio.setChecked(
- self.old_settings.get("tor_bridges_use_obfs4")
- )
- self.tor_bridges_use_meek_lite_azure_radio.setChecked(
- self.old_settings.get("tor_bridges_use_meek_lite_azure")
- )
-
- if self.old_settings.get("tor_bridges_use_custom_bridges"):
- self.tor_bridges_use_custom_radio.setChecked(True)
- # Remove the 'Bridge' lines at the start of each bridge.
- # They are added automatically to provide compatibility with
- # copying/pasting bridges provided from https://bridges.torproject.org
- new_bridges = []
- bridges = self.old_settings.get("tor_bridges_use_custom_bridges").split(
- "Bridge "
- )
- for bridge in bridges:
- new_bridges.append(bridge)
- new_bridges = "".join(new_bridges)
- self.tor_bridges_use_custom_textbox.setPlainText(new_bridges)
-
- def connection_type_bundled_toggled(self, checked):
- """
- Connection type bundled was toggled. If checked, hide authentication fields.
- """
- self.common.log("SettingsDialog", "connection_type_bundled_toggled")
- if self.hide_tor_settings:
- return
- if checked:
- self.authenticate_group.hide()
- self.connection_type_socks.hide()
- self.connection_type_bridges_radio_group.show()
-
- def tor_bridges_no_bridges_radio_toggled(self, checked):
- """
- 'No bridges' option was toggled. If checked, enable other bridge options.
- """
- if self.hide_tor_settings:
- return
- if checked:
- self.tor_bridges_use_custom_textbox_options.hide()
-
- def tor_bridges_use_obfs4_radio_toggled(self, checked):
- """
- obfs4 bridges option was toggled. If checked, disable custom bridge options.
- """
- if self.hide_tor_settings:
- return
- if checked:
- self.tor_bridges_use_custom_textbox_options.hide()
-
- def tor_bridges_use_meek_lite_azure_radio_toggled(self, checked):
- """
- meek_lite_azure bridges option was toggled. If checked, disable custom bridge options.
- """
- if self.hide_tor_settings:
- return
- if checked:
- self.tor_bridges_use_custom_textbox_options.hide()
- # Alert the user about meek's costliness if it looks like they're turning it on
- if not self.old_settings.get("tor_bridges_use_meek_lite_azure"):
- Alert(
- self.common,
- strings._("gui_settings_meek_lite_expensive_warning"),
- QtWidgets.QMessageBox.Warning,
- )
-
- def tor_bridges_use_custom_radio_toggled(self, checked):
- """
- Custom bridges option was toggled. If checked, show custom bridge options.
- """
- if self.hide_tor_settings:
- return
- if checked:
- self.tor_bridges_use_custom_textbox_options.show()
-
- def connection_type_automatic_toggled(self, checked):
- """
- Connection type automatic was toggled. If checked, hide authentication fields.
- """
- self.common.log("SettingsDialog", "connection_type_automatic_toggled")
- if self.hide_tor_settings:
- return
- if checked:
- self.authenticate_group.hide()
- self.connection_type_socks.hide()
- self.connection_type_bridges_radio_group.hide()
-
- def connection_type_control_port_toggled(self, checked):
- """
- Connection type control port was toggled. If checked, show extra fields
- for Tor control address and port. If unchecked, hide those extra fields.
- """
- self.common.log("SettingsDialog", "connection_type_control_port_toggled")
- if self.hide_tor_settings:
- return
- if checked:
- self.authenticate_group.show()
- self.connection_type_control_port_extras.show()
- self.connection_type_socks.show()
- self.connection_type_bridges_radio_group.hide()
- else:
- self.connection_type_control_port_extras.hide()
-
- def connection_type_socket_file_toggled(self, checked):
- """
- Connection type socket file was toggled. If checked, show extra fields
- for socket file. If unchecked, hide those extra fields.
- """
- self.common.log("SettingsDialog", "connection_type_socket_file_toggled")
- if self.hide_tor_settings:
- return
- if checked:
- self.authenticate_group.show()
- self.connection_type_socket_file_extras.show()
- self.connection_type_socks.show()
- self.connection_type_bridges_radio_group.hide()
- else:
- self.connection_type_socket_file_extras.hide()
-
- def authenticate_no_auth_toggled(self, checked):
- """
- Authentication option no authentication was toggled.
- """
- self.common.log("SettingsDialog", "authenticate_no_auth_toggled")
-
- def authenticate_password_toggled(self, checked):
- """
- Authentication option password was toggled. If checked, show extra fields
- for password auth. If unchecked, hide those extra fields.
- """
- self.common.log("SettingsDialog", "authenticate_password_toggled")
- if checked:
- self.authenticate_password_extras.show()
- else:
- self.authenticate_password_extras.hide()
-
- def test_tor_clicked(self):
- """
- Test Tor Settings button clicked. With the given settings, see if we can
- successfully connect and authenticate to Tor.
- """
- self.common.log("SettingsDialog", "test_tor_clicked")
- settings = self.settings_from_fields()
-
- try:
- # Show Tor connection status if connection type is bundled tor
- if settings.get("connection_type") == "bundled":
- self.tor_status.show()
- self._disable_buttons()
-
- def tor_status_update_func(progress, summary):
- self._tor_status_update(progress, summary)
- return True
-
- else:
- tor_status_update_func = None
-
- onion = Onion(
- self.common,
- use_tmp_dir=True,
- get_tor_paths=self.common.gui.get_tor_paths,
- )
- onion.connect(
- custom_settings=settings,
- tor_status_update_func=tor_status_update_func,
- )
-
- # If an exception hasn't been raised yet, the Tor settings work
- Alert(
- self.common,
- strings._("settings_test_success").format(
- onion.tor_version,
- onion.supports_ephemeral,
- onion.supports_stealth,
- onion.supports_v3_onions,
- ),
- )
-
- # Clean up
- onion.cleanup()
-
- except (
- TorErrorInvalidSetting,
- TorErrorAutomatic,
- TorErrorSocketPort,
- TorErrorSocketFile,
- TorErrorMissingPassword,
- TorErrorUnreadableCookieFile,
- TorErrorAuthError,
- TorErrorProtocolError,
- BundledTorTimeout,
- BundledTorBroken,
- TorTooOldEphemeral,
- TorTooOldStealth,
- PortNotAvailable,
- ) as e:
- message = self.common.gui.get_translated_tor_error(e)
- Alert(
- self.common,
- message,
- QtWidgets.QMessageBox.Warning,
- )
- if settings.get("connection_type") == "bundled":
- self.tor_status.hide()
- self._enable_buttons()
-
- def check_for_updates(self):
- """
- Check for Updates button clicked. Manually force an update check.
- """
- self.common.log("SettingsDialog", "check_for_updates")
- # Disable buttons
- self._disable_buttons()
- self.common.gui.qtapp.processEvents()
-
- def update_timestamp():
- # Update the last checked label
- settings = Settings(self.common)
- settings.load()
- autoupdate_timestamp = settings.get("autoupdate_timestamp")
- self._update_autoupdate_timestamp(autoupdate_timestamp)
-
- def close_forced_update_thread():
- forced_update_thread.quit()
- # Enable buttons
- self._enable_buttons()
- # Update timestamp
- update_timestamp()
-
- # Check for updates
- def update_available(update_url, installed_version, latest_version):
- Alert(
- self.common,
- strings._("update_available").format(
- update_url, installed_version, latest_version
- ),
- )
- close_forced_update_thread()
-
- def update_not_available():
- Alert(self.common, strings._("update_not_available"))
- close_forced_update_thread()
-
- def update_error():
- Alert(
- self.common,
- strings._("update_error_check_error"),
- QtWidgets.QMessageBox.Warning,
- )
- close_forced_update_thread()
-
- def update_invalid_version(latest_version):
- Alert(
- self.common,
- strings._("update_error_invalid_latest_version").format(latest_version),
- QtWidgets.QMessageBox.Warning,
- )
- close_forced_update_thread()
-
- forced_update_thread = UpdateThread(self.common, self.common.gui.onion, force=True)
- forced_update_thread.update_available.connect(update_available)
- forced_update_thread.update_not_available.connect(update_not_available)
- forced_update_thread.update_error.connect(update_error)
- forced_update_thread.update_invalid_version.connect(update_invalid_version)
- forced_update_thread.start()
-
- def save_clicked(self):
- """
- Save button clicked. Save current settings to disk.
- """
- self.common.log("SettingsDialog", "save_clicked")
-
- def changed(s1, s2, keys):
- """
- Compare the Settings objects s1 and s2 and return true if any values
- have changed for the given keys.
- """
- for key in keys:
- if s1.get(key) != s2.get(key):
- return True
- return False
-
- settings = self.settings_from_fields()
- if settings:
- # If language changed, inform user they need to restart OnionShare
- if changed(settings, self.old_settings, ["locale"]):
- # Look up error message in different locale
- new_locale = settings.get("locale")
- if (
- new_locale in strings.translations
- and "gui_settings_language_changed_notice"
- in strings.translations[new_locale]
- ):
- notice = strings.translations[new_locale][
- "gui_settings_language_changed_notice"
- ]
- else:
- notice = strings._("gui_settings_language_changed_notice")
- Alert(self.common, notice, QtWidgets.QMessageBox.Information)
-
-
- # If color mode changed, inform user they need to restart OnionShare
- if changed(settings, self.old_settings, ["theme"]):
- notice = strings._("gui_color_mode_changed_notice")
- Alert(self.common, notice, QtWidgets.QMessageBox.Information)
-
- # Save the new settings
- settings.save()
-
- # If Tor isn't connected, or if Tor settings have changed, Reinitialize
- # the Onion object
- reboot_onion = False
- if not self.common.gui.local_only:
- if self.common.gui.onion.is_authenticated():
- self.common.log(
- "SettingsDialog", "save_clicked", "Connected to Tor"
- )
-
- if changed(
- settings,
- self.old_settings,
- [
- "connection_type",
- "control_port_address",
- "control_port_port",
- "socks_address",
- "socks_port",
- "socket_file_path",
- "auth_type",
- "auth_password",
- "no_bridges",
- "tor_bridges_use_obfs4",
- "tor_bridges_use_meek_lite_azure",
- "tor_bridges_use_custom_bridges",
- ],
- ):
-
- reboot_onion = True
-
- else:
- self.common.log(
- "SettingsDialog", "save_clicked", "Not connected to Tor"
- )
- # Tor isn't connected, so try connecting
- reboot_onion = True
-
- # Do we need to reinitialize Tor?
- if reboot_onion:
- # Reinitialize the Onion object
- self.common.log(
- "SettingsDialog", "save_clicked", "rebooting the Onion"
- )
- self.common.gui.onion.cleanup()
-
- tor_con = TorConnectionDialog(self.common, settings)
- tor_con.start()
-
- self.common.log(
- "SettingsDialog",
- "save_clicked",
- f"Onion done rebooting, connected to Tor: {self.common.gui.onion.connected_to_tor}",
- )
-
- if (
- self.common.gui.onion.is_authenticated()
- and not tor_con.wasCanceled()
- ):
- self.settings_saved.emit()
- self.close()
-
- else:
- self.settings_saved.emit()
- self.close()
- else:
- self.settings_saved.emit()
- self.close()
-
- def cancel_clicked(self):
- """
- Cancel button clicked.
- """
- self.common.log("SettingsDialog", "cancel_clicked")
- if (
- not self.common.gui.local_only
- and not self.common.gui.onion.is_authenticated()
- ):
- Alert(
- self.common,
- strings._("gui_tor_connection_canceled"),
- QtWidgets.QMessageBox.Warning,
- )
- sys.exit()
- else:
- self.close()
-
- def help_clicked(self):
- """
- Help button clicked.
- """
- self.common.log("SettingsDialog", "help_clicked")
- SettingsDialog.open_help()
-
- @staticmethod
- def open_help():
- help_url = "https://docs.onionshare.org/"
- QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_url))
-
- def settings_from_fields(self):
- """
- Return a Settings object that's full of values from the settings dialog.
- """
- self.common.log("SettingsDialog", "settings_from_fields")
- settings = Settings(self.common)
- settings.load() # To get the last update timestamp
-
- # Theme
- theme_index = self.theme_combobox.currentIndex()
- settings.set("theme",theme_index)
-
- # Language
- locale_index = self.language_combobox.currentIndex()
- locale = self.language_combobox.itemData(locale_index)
- settings.set("locale", locale)
-
- # Tor connection
- if self.connection_type_bundled_radio.isChecked():
- settings.set("connection_type", "bundled")
- if self.connection_type_automatic_radio.isChecked():
- settings.set("connection_type", "automatic")
- if self.connection_type_control_port_radio.isChecked():
- settings.set("connection_type", "control_port")
- if self.connection_type_socket_file_radio.isChecked():
- settings.set("connection_type", "socket_file")
-
- if self.autoupdate_checkbox.isChecked():
- settings.set("use_autoupdate", True)
- else:
- settings.set("use_autoupdate", False)
-
- settings.set(
- "control_port_address",
- self.connection_type_control_port_extras_address.text(),
- )
- settings.set(
- "control_port_port", self.connection_type_control_port_extras_port.text()
- )
- settings.set(
- "socket_file_path", self.connection_type_socket_file_extras_path.text()
- )
-
- settings.set("socks_address", self.connection_type_socks_address.text())
- settings.set("socks_port", self.connection_type_socks_port.text())
-
- if self.authenticate_no_auth_radio.isChecked():
- settings.set("auth_type", "no_auth")
- if self.authenticate_password_radio.isChecked():
- settings.set("auth_type", "password")
-
- settings.set("auth_password", self.authenticate_password_extras_password.text())
-
- # Whether we use bridges
- if self.tor_bridges_no_bridges_radio.isChecked():
- settings.set("no_bridges", True)
- settings.set("tor_bridges_use_obfs4", False)
- settings.set("tor_bridges_use_meek_lite_azure", False)
- settings.set("tor_bridges_use_custom_bridges", "")
- if self.tor_bridges_use_obfs4_radio.isChecked():
- settings.set("no_bridges", False)
- settings.set("tor_bridges_use_obfs4", True)
- settings.set("tor_bridges_use_meek_lite_azure", False)
- settings.set("tor_bridges_use_custom_bridges", "")
- if self.tor_bridges_use_meek_lite_azure_radio.isChecked():
- settings.set("no_bridges", False)
- settings.set("tor_bridges_use_obfs4", False)
- settings.set("tor_bridges_use_meek_lite_azure", True)
- settings.set("tor_bridges_use_custom_bridges", "")
- if self.tor_bridges_use_custom_radio.isChecked():
- settings.set("no_bridges", False)
- settings.set("tor_bridges_use_obfs4", False)
- settings.set("tor_bridges_use_meek_lite_azure", False)
-
- # Insert a 'Bridge' line at the start of each bridge.
- # This makes it easier to copy/paste a set of bridges
- # provided from https://bridges.torproject.org
- new_bridges = []
- bridges = self.tor_bridges_use_custom_textbox.toPlainText().split("\n")
- bridges_valid = False
- for bridge in bridges:
- if bridge != "":
- # Check the syntax of the custom bridge to make sure it looks legitimate
- ipv4_pattern = re.compile(
- "(obfs4\s+)?(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):([0-9]+)(\s+)([A-Z0-9]+)(.+)$"
- )
- ipv6_pattern = re.compile(
- "(obfs4\s+)?\[(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\]:[0-9]+\s+[A-Z0-9]+(.+)$"
- )
- meek_lite_pattern = re.compile(
- "(meek_lite)(\s)+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+)(\s)+([0-9A-Z]+)(\s)+url=(.+)(\s)+front=(.+)"
- )
- if (
- ipv4_pattern.match(bridge)
- or ipv6_pattern.match(bridge)
- or meek_lite_pattern.match(bridge)
- ):
- new_bridges.append("".join(["Bridge ", bridge, "\n"]))
- bridges_valid = True
-
- if bridges_valid:
- new_bridges = "".join(new_bridges)
- settings.set("tor_bridges_use_custom_bridges", new_bridges)
- else:
- Alert(self.common, strings._("gui_settings_tor_bridges_invalid"))
- settings.set("no_bridges", True)
- return False
-
- return settings
-
- def closeEvent(self, e):
- self.common.log("SettingsDialog", "closeEvent")
-
- # On close, if Tor isn't connected, then quit OnionShare altogether
- if not self.common.gui.local_only:
- if not self.common.gui.onion.is_authenticated():
- self.common.log(
- "SettingsDialog", "closeEvent", "Closing while not connected to Tor"
- )
-
- # Wait 1ms for the event loop to finish, then quit
- QtCore.QTimer.singleShot(1, self.common.gui.qtapp.quit)
-
- def _update_autoupdate_timestamp(self, autoupdate_timestamp):
- self.common.log("SettingsDialog", "_update_autoupdate_timestamp")
-
- if autoupdate_timestamp:
- dt = datetime.datetime.fromtimestamp(autoupdate_timestamp)
- last_checked = dt.strftime("%B %d, %Y %H:%M")
- else:
- last_checked = strings._("gui_settings_autoupdate_timestamp_never")
- self.autoupdate_timestamp.setText(
- strings._("gui_settings_autoupdate_timestamp").format(last_checked)
- )
-
- def _tor_status_update(self, progress, summary):
- self.tor_status.setText(
- f"<strong>{strings._('connecting_to_tor')}</strong><br>{progress}% {summary}"
- )
- self.common.gui.qtapp.processEvents()
- if "Done" in summary:
- self.tor_status.hide()
- self._enable_buttons()
-
- def _disable_buttons(self):
- self.common.log("SettingsDialog", "_disable_buttons")
-
- self.check_for_updates_button.setEnabled(False)
- self.connection_type_test_button.setEnabled(False)
- self.save_button.setEnabled(False)
- self.cancel_button.setEnabled(False)
-
- def _enable_buttons(self):
- self.common.log("SettingsDialog", "_enable_buttons")
- # We can't check for updates if we're still not connected to Tor
- if not self.common.gui.onion.connected_to_tor:
- self.check_for_updates_button.setEnabled(False)
- else:
- self.check_for_updates_button.setEnabled(True)
- self.connection_type_test_button.setEnabled(True)
- self.save_button.setEnabled(True)
- self.cancel_button.setEnabled(True)
diff --git a/desktop/src/onionshare/settings_tab.py b/desktop/src/onionshare/settings_tab.py
new file mode 100644
index 00000000..cfa3261e
--- /dev/null
+++ b/desktop/src/onionshare/settings_tab.py
@@ -0,0 +1,354 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2021 Micah Lee, et al. <micah@micahflee.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from PySide2 import QtCore, QtWidgets, QtGui
+import platform
+import datetime
+from onionshare_cli.settings import Settings
+
+from . import strings
+from .widgets import Alert
+from .update_checker import UpdateThread
+
+
+class SettingsTab(QtWidgets.QWidget):
+ """
+ Settings dialog.
+ """
+
+ close_this_tab = QtCore.Signal()
+
+ def __init__(self, common, tab_id):
+ super(SettingsTab, self).__init__()
+
+ self.common = common
+ self.common.log("SettingsTab", "__init__")
+
+ self.system = platform.system()
+ self.tab_id = tab_id
+
+ # Automatic updates options
+
+ # Autoupdate
+ self.autoupdate_checkbox = QtWidgets.QCheckBox()
+ self.autoupdate_checkbox.setCheckState(QtCore.Qt.Unchecked)
+ self.autoupdate_checkbox.setText(strings._("gui_settings_autoupdate_option"))
+
+ # Last update time
+ self.autoupdate_timestamp = QtWidgets.QLabel()
+
+ # Check for updates button
+ self.check_for_updates_button = QtWidgets.QPushButton(
+ strings._("gui_settings_autoupdate_check_button")
+ )
+ self.check_for_updates_button.clicked.connect(self.check_for_updates)
+ # We can't check for updates if not connected to Tor
+ if not self.common.gui.onion.connected_to_tor:
+ self.check_for_updates_button.setEnabled(False)
+
+ # Autoupdate options layout
+ autoupdate_group_layout = QtWidgets.QVBoxLayout()
+ autoupdate_group_layout.addWidget(self.autoupdate_checkbox)
+ autoupdate_group_layout.addWidget(self.autoupdate_timestamp)
+ autoupdate_group_layout.addWidget(self.check_for_updates_button)
+ autoupdate_group = QtWidgets.QGroupBox(
+ strings._("gui_settings_autoupdate_label")
+ )
+ autoupdate_group.setLayout(autoupdate_group_layout)
+
+ autoupdate_layout = QtWidgets.QHBoxLayout()
+ autoupdate_layout.addStretch()
+ autoupdate_layout.addWidget(autoupdate_group)
+ autoupdate_layout.addStretch()
+ autoupdate_widget = QtWidgets.QWidget()
+ autoupdate_widget.setLayout(autoupdate_layout)
+
+ # Autoupdate is only available for Windows and Mac (Linux updates using package manager)
+ if self.system != "Windows" and self.system != "Darwin":
+ autoupdate_widget.hide()
+
+ # Language settings
+ language_label = QtWidgets.QLabel(strings._("gui_settings_language_label"))
+ self.language_combobox = QtWidgets.QComboBox()
+ # Populate the dropdown with all of OnionShare's available languages
+ language_names_to_locales = {
+ v: k for k, v in self.common.settings.available_locales.items()
+ }
+ language_names = list(language_names_to_locales)
+ language_names.sort()
+ for language_name in language_names:
+ locale = language_names_to_locales[language_name]
+ self.language_combobox.addItem(language_name, locale)
+ language_layout = QtWidgets.QHBoxLayout()
+ language_layout.addStretch()
+ language_layout.addWidget(language_label)
+ language_layout.addWidget(self.language_combobox)
+ language_layout.addStretch()
+
+ # Theme Settings
+ theme_label = QtWidgets.QLabel(strings._("gui_settings_theme_label"))
+ self.theme_combobox = QtWidgets.QComboBox()
+ theme_choices = [
+ strings._("gui_settings_theme_auto"),
+ strings._("gui_settings_theme_light"),
+ strings._("gui_settings_theme_dark"),
+ ]
+ self.theme_combobox.addItems(theme_choices)
+ theme_layout = QtWidgets.QHBoxLayout()
+ theme_layout.addStretch()
+ theme_layout.addWidget(theme_label)
+ theme_layout.addWidget(self.theme_combobox)
+ theme_layout.addStretch()
+
+ # Version and help
+ version_label = QtWidgets.QLabel(
+ strings._("gui_settings_version_label").format(self.common.version)
+ )
+ version_label.setAlignment(QtCore.Qt.AlignHCenter)
+ help_label = QtWidgets.QLabel(strings._("gui_settings_help_label"))
+ help_label.setAlignment(QtCore.Qt.AlignHCenter)
+ help_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
+ help_label.setOpenExternalLinks(True)
+
+ # Buttons
+ self.save_button = QtWidgets.QPushButton(strings._("gui_settings_button_save"))
+ self.save_button.clicked.connect(self.save_clicked)
+ buttons_layout = QtWidgets.QHBoxLayout()
+ buttons_layout.addStretch()
+ buttons_layout.addWidget(self.save_button)
+ buttons_layout.addStretch()
+
+ # Layout
+ layout = QtWidgets.QVBoxLayout()
+ layout.addStretch()
+ layout.addWidget(autoupdate_widget)
+ if autoupdate_widget.isVisible():
+ layout.addSpacing(20)
+ layout.addLayout(language_layout)
+ layout.addLayout(theme_layout)
+ layout.addSpacing(20)
+ layout.addWidget(version_label)
+ layout.addWidget(help_label)
+ layout.addSpacing(20)
+ layout.addLayout(buttons_layout)
+ layout.addStretch()
+
+ self.setLayout(layout)
+
+ self.reload_settings()
+
+ if self.common.gui.onion.connected_to_tor:
+ self.tor_is_connected()
+ else:
+ self.tor_is_disconnected()
+
+ def reload_settings(self):
+ # Load settings, and fill them in
+ self.old_settings = Settings(self.common)
+ self.old_settings.load()
+
+ use_autoupdate = self.old_settings.get("use_autoupdate")
+ if use_autoupdate:
+ self.autoupdate_checkbox.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.autoupdate_checkbox.setCheckState(QtCore.Qt.Unchecked)
+
+ autoupdate_timestamp = self.old_settings.get("autoupdate_timestamp")
+ self._update_autoupdate_timestamp(autoupdate_timestamp)
+
+ locale = self.old_settings.get("locale")
+ locale_index = self.language_combobox.findData(locale)
+ self.language_combobox.setCurrentIndex(locale_index)
+
+ theme_choice = self.old_settings.get("theme")
+ self.theme_combobox.setCurrentIndex(theme_choice)
+
+ def check_for_updates(self):
+ """
+ Check for Updates button clicked. Manually force an update check.
+ """
+ self.common.log("SettingsTab", "check_for_updates")
+ # Disable buttons
+ self._disable_buttons()
+ self.common.gui.qtapp.processEvents()
+
+ def update_timestamp():
+ # Update the last checked label
+ settings = Settings(self.common)
+ settings.load()
+ autoupdate_timestamp = settings.get("autoupdate_timestamp")
+ self._update_autoupdate_timestamp(autoupdate_timestamp)
+
+ def close_forced_update_thread():
+ forced_update_thread.quit()
+ # Enable buttons
+ self._enable_buttons()
+ # Update timestamp
+ update_timestamp()
+
+ # Check for updates
+ def update_available(update_url, installed_version, latest_version):
+ Alert(
+ self.common,
+ strings._("update_available").format(
+ update_url, installed_version, latest_version
+ ),
+ )
+ close_forced_update_thread()
+
+ def update_not_available():
+ Alert(self.common, strings._("update_not_available"))
+ close_forced_update_thread()
+
+ def update_error():
+ Alert(
+ self.common,
+ strings._("update_error_check_error"),
+ QtWidgets.QMessageBox.Warning,
+ )
+ close_forced_update_thread()
+
+ def update_invalid_version(latest_version):
+ Alert(
+ self.common,
+ strings._("update_error_invalid_latest_version").format(latest_version),
+ QtWidgets.QMessageBox.Warning,
+ )
+ close_forced_update_thread()
+
+ forced_update_thread = UpdateThread(
+ self.common, self.common.gui.onion, force=True
+ )
+ forced_update_thread.update_available.connect(update_available)
+ forced_update_thread.update_not_available.connect(update_not_available)
+ forced_update_thread.update_error.connect(update_error)
+ forced_update_thread.update_invalid_version.connect(update_invalid_version)
+ forced_update_thread.start()
+
+ def save_clicked(self):
+ """
+ Save button clicked. Save current settings to disk.
+ """
+ self.common.log("SettingsTab", "save_clicked")
+
+ def changed(s1, s2, keys):
+ """
+ Compare the Settings objects s1 and s2 and return true if any values
+ have changed for the given keys.
+ """
+ for key in keys:
+ if s1.get(key) != s2.get(key):
+ return True
+ return False
+
+ settings = self.settings_from_fields()
+ if settings:
+ # If language changed, inform user they need to restart OnionShare
+ if changed(settings, self.old_settings, ["locale"]):
+ # Look up error message in different locale
+ new_locale = settings.get("locale")
+ if (
+ new_locale in strings.translations
+ and "gui_settings_language_changed_notice"
+ in strings.translations[new_locale]
+ ):
+ notice = strings.translations[new_locale][
+ "gui_settings_language_changed_notice"
+ ]
+ else:
+ notice = strings._("gui_settings_language_changed_notice")
+ Alert(self.common, notice, QtWidgets.QMessageBox.Information)
+
+ # If color mode changed, inform user they need to restart OnionShare
+ if changed(settings, self.old_settings, ["theme"]):
+ notice = strings._("gui_color_mode_changed_notice")
+ Alert(self.common, notice, QtWidgets.QMessageBox.Information)
+
+ # Save the new settings
+ settings.save()
+ self.close_this_tab.emit()
+
+ def help_clicked(self):
+ """
+ Help button clicked.
+ """
+ self.common.log("SettingsTab", "help_clicked")
+ SettingsTab.open_help()
+
+ @staticmethod
+ def open_help():
+ help_url = "https://docs.onionshare.org/"
+ QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_url))
+
+ def settings_from_fields(self):
+ """
+ Return a Settings object that's full of values from the settings dialog.
+ """
+ self.common.log("SettingsTab", "settings_from_fields")
+ settings = Settings(self.common)
+ settings.load() # To get the last update timestamp
+
+ # Theme
+ theme_index = self.theme_combobox.currentIndex()
+ settings.set("theme", theme_index)
+
+ # Language
+ locale_index = self.language_combobox.currentIndex()
+ locale = self.language_combobox.itemData(locale_index)
+ settings.set("locale", locale)
+
+ return settings
+
+ def settings_have_changed(self):
+ # Global settings have changed
+ self.common.log("SettingsTab", "settings_have_changed")
+
+ def _update_autoupdate_timestamp(self, autoupdate_timestamp):
+ self.common.log("SettingsTab", "_update_autoupdate_timestamp")
+
+ if autoupdate_timestamp:
+ dt = datetime.datetime.fromtimestamp(autoupdate_timestamp)
+ last_checked = dt.strftime("%B %d, %Y %H:%M")
+ else:
+ last_checked = strings._("gui_settings_autoupdate_timestamp_never")
+ self.autoupdate_timestamp.setText(
+ strings._("gui_settings_autoupdate_timestamp").format(last_checked)
+ )
+
+ def _disable_buttons(self):
+ self.common.log("SettingsTab", "_disable_buttons")
+
+ self.check_for_updates_button.setEnabled(False)
+ self.save_button.setEnabled(False)
+
+ def _enable_buttons(self):
+ self.common.log("SettingsTab", "_enable_buttons")
+ # We can't check for updates if we're still not connected to Tor
+ if not self.common.gui.onion.connected_to_tor:
+ self.check_for_updates_button.setEnabled(False)
+ else:
+ self.check_for_updates_button.setEnabled(True)
+ self.save_button.setEnabled(True)
+
+ def tor_is_connected(self):
+ self.check_for_updates_button.show()
+
+ def tor_is_disconnected(self):
+ self.check_for_updates_button.hide()
diff --git a/desktop/src/onionshare/tab/mode/__init__.py b/desktop/src/onionshare/tab/mode/__init__.py
index d4f2c23a..c9b5cad1 100644
--- a/desktop/src/onionshare/tab/mode/__init__.py
+++ b/desktop/src/onionshare/tab/mode/__init__.py
@@ -28,7 +28,7 @@ from .mode_settings_widget import ModeSettingsWidget
from ..server_status import ServerStatus
from ... import strings
from ...threads import OnionThread, AutoStartTimer
-from ...widgets import Alert
+from ...widgets import Alert, MinimumSizeWidget
class Mode(QtWidgets.QWidget):
@@ -101,6 +101,38 @@ class Mode(QtWidgets.QWidget):
self.primary_action = QtWidgets.QWidget()
self.primary_action.setLayout(self.primary_action_layout)
+ # It's up to the downstream Mode to add stuff to self.content_layout
+ # self.content_layout shows the actual content of the mode
+ # self.tor_not_connected_layout is displayed when Tor isn't connected
+ self.content_layout = QtWidgets.QVBoxLayout()
+ self.content_widget = QtWidgets.QWidget()
+ self.content_widget.setLayout(self.content_layout)
+
+ tor_not_connected_label = QtWidgets.QLabel(
+ strings._("mode_tor_not_connected_label")
+ )
+ tor_not_connected_label.setAlignment(QtCore.Qt.AlignHCenter)
+ tor_not_connected_label.setStyleSheet(
+ self.common.gui.css["tor_not_connected_label"]
+ )
+ self.tor_not_connected_layout = QtWidgets.QVBoxLayout()
+ self.tor_not_connected_layout.addStretch()
+ self.tor_not_connected_layout.addWidget(tor_not_connected_label)
+ self.tor_not_connected_layout.addWidget(MinimumSizeWidget(700, 0))
+ self.tor_not_connected_layout.addStretch()
+ self.tor_not_connected_widget = QtWidgets.QWidget()
+ self.tor_not_connected_widget.setLayout(self.tor_not_connected_layout)
+
+ self.wrapper_layout = QtWidgets.QVBoxLayout()
+ self.wrapper_layout.addWidget(self.content_widget)
+ self.wrapper_layout.addWidget(self.tor_not_connected_widget)
+ self.setLayout(self.wrapper_layout)
+
+ if self.common.gui.onion.connected_to_tor:
+ self.tor_connection_started()
+ else:
+ self.tor_connection_stopped()
+
def init(self):
"""
Add custom initialization here.
@@ -524,3 +556,21 @@ class Mode(QtWidgets.QWidget):
Used in both Share and Website modes, so implemented here.
"""
self.history.cancel(event["data"]["id"])
+
+ def tor_connection_started(self):
+ """
+ This is called on every Mode when Tor is connected
+ """
+ self.content_widget.show()
+ self.tor_not_connected_widget.hide()
+
+ def tor_connection_stopped(self):
+ """
+ This is called on every Mode when Tor is disconnected
+ """
+ if self.common.gui.local_only:
+ self.tor_connection_started()
+ return
+
+ self.content_widget.hide()
+ self.tor_not_connected_widget.show()
diff --git a/desktop/src/onionshare/tab/mode/chat_mode/__init__.py b/desktop/src/onionshare/tab/mode/chat_mode/__init__.py
index e7a17ce7..1081fe9d 100644
--- a/desktop/src/onionshare/tab/mode/chat_mode/__init__.py
+++ b/desktop/src/onionshare/tab/mode/chat_mode/__init__.py
@@ -98,10 +98,8 @@ class ChatMode(Mode):
self.column_layout.addWidget(self.image)
self.column_layout.addLayout(self.main_layout)
- # Wrapper layout
- self.wrapper_layout = QtWidgets.QVBoxLayout()
- self.wrapper_layout.addLayout(self.column_layout)
- self.setLayout(self.wrapper_layout)
+ # Content layout
+ self.content_layout.addLayout(self.column_layout)
def get_type(self):
"""
diff --git a/desktop/src/onionshare/tab/mode/receive_mode/__init__.py b/desktop/src/onionshare/tab/mode/receive_mode/__init__.py
index d5036d1d..b2b2fc5a 100644
--- a/desktop/src/onionshare/tab/mode/receive_mode/__init__.py
+++ b/desktop/src/onionshare/tab/mode/receive_mode/__init__.py
@@ -198,10 +198,8 @@ class ReceiveMode(Mode):
self.column_layout.addLayout(row_layout)
self.column_layout.addWidget(self.history, stretch=1)
- # Wrapper layout
- self.wrapper_layout = QtWidgets.QVBoxLayout()
- self.wrapper_layout.addLayout(self.column_layout)
- self.setLayout(self.wrapper_layout)
+ # Content layout
+ self.content_layout.addLayout(self.column_layout)
def get_type(self):
"""
diff --git a/desktop/src/onionshare/tab/mode/share_mode/__init__.py b/desktop/src/onionshare/tab/mode/share_mode/__init__.py
index 5d3e3c35..7be93f1d 100644
--- a/desktop/src/onionshare/tab/mode/share_mode/__init__.py
+++ b/desktop/src/onionshare/tab/mode/share_mode/__init__.py
@@ -169,10 +169,8 @@ class ShareMode(Mode):
self.column_layout.addLayout(self.main_layout)
self.column_layout.addWidget(self.history, stretch=1)
- # Wrapper layout
- self.wrapper_layout = QtWidgets.QVBoxLayout()
- self.wrapper_layout.addLayout(self.column_layout)
- self.setLayout(self.wrapper_layout)
+ # Content layout
+ self.content_layout.addLayout(self.column_layout)
# Always start with focus on file selection
self.file_selection.setFocus()
diff --git a/desktop/src/onionshare/tab/mode/website_mode/__init__.py b/desktop/src/onionshare/tab/mode/website_mode/__init__.py
index 6d1df88c..0acbc1a2 100644
--- a/desktop/src/onionshare/tab/mode/website_mode/__init__.py
+++ b/desktop/src/onionshare/tab/mode/website_mode/__init__.py
@@ -188,10 +188,8 @@ class WebsiteMode(Mode):
self.column_layout.addLayout(self.main_layout)
self.column_layout.addWidget(self.history, stretch=1)
- # Wrapper layout
- self.wrapper_layout = QtWidgets.QVBoxLayout()
- self.wrapper_layout.addLayout(self.column_layout)
- self.setLayout(self.wrapper_layout)
+ # Content layout
+ self.content_layout.addLayout(self.column_layout)
# Always start with focus on file selection
self.file_selection.setFocus()
diff --git a/desktop/src/onionshare/tab/tab.py b/desktop/src/onionshare/tab/tab.py
index 7276b8da..2e592771 100644
--- a/desktop/src/onionshare/tab/tab.py
+++ b/desktop/src/onionshare/tab/tab.py
@@ -96,7 +96,6 @@ class Tab(QtWidgets.QWidget):
tab_id,
system_tray,
status_bar,
- mode_settings=None,
filenames=None,
):
super(Tab, self).__init__()
diff --git a/desktop/src/onionshare/tab_widget.py b/desktop/src/onionshare/tab_widget.py
index a955ea53..7162fcc4 100644
--- a/desktop/src/onionshare/tab_widget.py
+++ b/desktop/src/onionshare/tab_widget.py
@@ -26,6 +26,8 @@ from . import strings
from .tab import Tab
from .threads import EventHandlerThread
from .gui_common import GuiCommon
+from .tor_settings_tab import TorSettingsTab
+from .settings_tab import SettingsTab
class TabWidget(QtWidgets.QTabWidget):
@@ -43,9 +45,12 @@ class TabWidget(QtWidgets.QTabWidget):
self.system_tray = system_tray
self.status_bar = status_bar
- # Keep track of tabs in a dictionary
+ # Keep track of tabs in a dictionary that maps tab_id to tab.
+ # Each tab has a unique, auto-incremented id (tab_id). This is different than the
+ # tab's index, which changes as tabs are re-arranged.
self.tabs = {}
self.current_tab_id = 0 # Each tab has a unique id
+ self.tor_settings_tab = None
# Define the new tab button
self.new_tab_button = QtWidgets.QPushButton("+", parent=self)
@@ -89,9 +94,12 @@ class TabWidget(QtWidgets.QTabWidget):
self.event_handler_t.wait(50)
# Clean up each tab
- for index in range(self.count()):
- tab = self.widget(index)
- tab.cleanup()
+ for tab_id in self.tabs:
+ if not (
+ type(self.tabs[tab_id]) is SettingsTab
+ or type(self.tabs[tab_id]) is TorSettingsTab
+ ):
+ self.tabs[tab_id].cleanup()
def move_new_tab_button(self):
# Find the width of all tabs
@@ -114,8 +122,28 @@ class TabWidget(QtWidgets.QTabWidget):
def tab_changed(self):
# Active tab was changed
- tab_id = self.currentIndex()
+ tab = self.widget(self.currentIndex())
+ if not tab:
+ self.common.log(
+ "TabWidget",
+ "tab_changed",
+ f"tab at index {self.currentIndex()} does not exist",
+ )
+ return
+
+ tab_id = tab.tab_id
self.common.log("TabWidget", "tab_changed", f"Tab was changed to {tab_id}")
+
+ # If it's Settings or Tor Settings, ignore
+ if (
+ type(self.tabs[tab_id]) is SettingsTab
+ or type(self.tabs[tab_id]) is TorSettingsTab
+ ):
+ # Blank the server status indicator
+ self.status_bar.server_status_image_label.clear()
+ self.status_bar.server_status_label.clear()
+ return
+
try:
mode = self.tabs[tab_id].get_mode()
if mode:
@@ -158,23 +186,6 @@ class TabWidget(QtWidgets.QTabWidget):
index = self.addTab(tab, strings._("gui_new_tab"))
self.setCurrentIndex(index)
- # In macOS, manually create a close button because tabs don't seem to have them otherwise
- if self.common.platform == "Darwin":
-
- def close_tab():
- self.tabBar().tabCloseRequested.emit(self.indexOf(tab))
-
- tab.close_button = QtWidgets.QPushButton()
- tab.close_button.setFlat(True)
- tab.close_button.setFixedWidth(40)
- tab.close_button.setIcon(
- QtGui.QIcon(GuiCommon.get_resource_path("images/close_tab.png"))
- )
- tab.close_button.clicked.connect(close_tab)
- self.tabBar().setTabButton(
- index, QtWidgets.QTabBar.RightSide, tab.close_button
- )
-
tab.init(mode_settings)
# Make sure the title is set
@@ -187,6 +198,44 @@ class TabWidget(QtWidgets.QTabWidget):
# Bring the window to front, in case this is being added by an event
self.bring_to_front.emit()
+ def open_settings_tab(self):
+ self.common.log("TabWidget", "open_settings_tab")
+
+ # See if a settings tab is already open, and if so switch to it
+ for tab_id in self.tabs:
+ if type(self.tabs[tab_id]) is SettingsTab:
+ self.setCurrentIndex(self.indexOf(self.tabs[tab_id]))
+ return
+
+ settings_tab = SettingsTab(self.common, self.current_tab_id)
+ settings_tab.close_this_tab.connect(self.close_settings_tab)
+ self.tabs[self.current_tab_id] = settings_tab
+ self.current_tab_id += 1
+ index = self.addTab(settings_tab, strings._("gui_settings_window_title"))
+ self.setCurrentIndex(index)
+
+ def open_tor_settings_tab(self):
+ self.common.log("TabWidget", "open_tor_settings_tab")
+
+ # See if a settings tab is already open, and if so switch to it
+ for tab_id in self.tabs:
+ if type(self.tabs[tab_id]) is TorSettingsTab:
+ self.setCurrentIndex(self.indexOf(self.tabs[tab_id]))
+ return
+
+ self.tor_settings_tab = TorSettingsTab(
+ self.common, self.current_tab_id, self.are_tabs_active(), self.status_bar
+ )
+ self.tor_settings_tab.close_this_tab.connect(self.close_tor_settings_tab)
+ self.tor_settings_tab.tor_is_connected.connect(self.tor_is_connected)
+ self.tor_settings_tab.tor_is_disconnected.connect(self.tor_is_disconnected)
+ self.tabs[self.current_tab_id] = self.tor_settings_tab
+ self.current_tab_id += 1
+ index = self.addTab(
+ self.tor_settings_tab, strings._("gui_tor_settings_window_title")
+ )
+ self.setCurrentIndex(index)
+
def change_title(self, tab_id, title):
shortened_title = title
if len(shortened_title) > 11:
@@ -200,6 +249,11 @@ class TabWidget(QtWidgets.QTabWidget):
index = self.indexOf(self.tabs[tab_id])
self.setTabIcon(index, QtGui.QIcon(GuiCommon.get_resource_path(icon_path)))
+ # The icon changes when the server status changes, so if we have an open
+ # Tor Settings tab, tell it to update
+ if self.tor_settings_tab:
+ self.tor_settings_tab.active_tabs_changed(self.are_tabs_active())
+
def change_persistent(self, tab_id, is_persistent):
self.common.log(
"TabWidget",
@@ -223,10 +277,14 @@ class TabWidget(QtWidgets.QTabWidget):
def save_persistent_tabs(self):
# Figure out the order of persistent tabs to save in settings
persistent_tabs = []
- for index in range(self.count()):
- tab = self.widget(index)
- if tab.settings.get("persistent", "enabled"):
- persistent_tabs.append(tab.settings.id)
+ for tab_id in self.tabs:
+ if not (
+ type(self.tabs[tab_id]) is SettingsTab
+ or type(self.tabs[tab_id]) is TorSettingsTab
+ ):
+ tab = self.widget(self.indexOf(self.tabs[tab_id]))
+ if tab.settings.get("persistent", "enabled"):
+ persistent_tabs.append(tab.settings.id)
# Only save if tabs have actually moved
if persistent_tabs != self.common.settings.get("persistent_tabs"):
self.common.settings.set("persistent_tabs", persistent_tabs)
@@ -235,10 +293,16 @@ class TabWidget(QtWidgets.QTabWidget):
def close_tab(self, index):
self.common.log("TabWidget", "close_tab", f"{index}")
tab = self.widget(index)
- if tab.close_tab():
- # If the tab is persistent, delete the settings file from disk
- if tab.settings.get("persistent", "enabled"):
- tab.settings.delete()
+ tab_id = tab.tab_id
+
+ if (
+ type(self.tabs[tab_id]) is SettingsTab
+ or type(self.tabs[tab_id]) is TorSettingsTab
+ ):
+ self.common.log("TabWidget", "closing a settings tab")
+
+ if type(self.tabs[tab_id]) is TorSettingsTab:
+ self.tor_settings_tab = None
# Remove the tab
self.removeTab(index)
@@ -248,17 +312,56 @@ class TabWidget(QtWidgets.QTabWidget):
if self.count() == 0:
self.new_tab_clicked()
- self.save_persistent_tabs()
+ else:
+ self.common.log("TabWidget", "closing a service tab")
+ if tab.close_tab():
+ self.common.log("TabWidget", "user is okay with closing the tab")
+
+ # If the tab is persistent, delete the settings file from disk
+ if tab.settings.get("persistent", "enabled"):
+ tab.settings.delete()
+
+ self.save_persistent_tabs()
+
+ # Remove the tab
+ self.removeTab(index)
+ del self.tabs[tab.tab_id]
+
+ # If the last tab is closed, open a new one
+ if self.count() == 0:
+ self.new_tab_clicked()
+ else:
+ self.common.log("TabWidget", "user does not want to close the tab")
+
+ def close_settings_tab(self):
+ self.common.log("TabWidget", "close_settings_tab")
+ for tab_id in self.tabs:
+ if type(self.tabs[tab_id]) is SettingsTab:
+ index = self.indexOf(self.tabs[tab_id])
+ self.close_tab(index)
+ return
+
+ def close_tor_settings_tab(self):
+ self.common.log("TabWidget", "close_tor_settings_tab")
+ for tab_id in self.tabs:
+ if type(self.tabs[tab_id]) is TorSettingsTab:
+ index = self.indexOf(self.tabs[tab_id])
+ self.close_tab(index)
+ return
def are_tabs_active(self):
"""
See if there are active servers in any open tabs
"""
for tab_id in self.tabs:
- mode = self.tabs[tab_id].get_mode()
- if mode:
- if mode.server_status.status != mode.server_status.STATUS_STOPPED:
- return True
+ if not (
+ type(self.tabs[tab_id]) is SettingsTab
+ or type(self.tabs[tab_id]) is TorSettingsTab
+ ):
+ mode = self.tabs[tab_id].get_mode()
+ if mode:
+ if mode.server_status.status != mode.server_status.STATUS_STOPPED:
+ return True
return False
def paintEvent(self, event):
@@ -273,6 +376,26 @@ class TabWidget(QtWidgets.QTabWidget):
super(TabWidget, self).resizeEvent(event)
self.move_new_tab_button()
+ def tor_is_connected(self):
+ for tab_id in self.tabs:
+ if type(self.tabs[tab_id]) is SettingsTab:
+ self.tabs[tab_id].tor_is_connected()
+ else:
+ if not type(self.tabs[tab_id]) is TorSettingsTab:
+ mode = self.tabs[tab_id].get_mode()
+ if mode:
+ mode.tor_connection_started()
+
+ def tor_is_disconnected(self):
+ for tab_id in self.tabs:
+ if type(self.tabs[tab_id]) is SettingsTab:
+ self.tabs[tab_id].tor_is_disconnected()
+ else:
+ if not type(self.tabs[tab_id]) is TorSettingsTab:
+ mode = self.tabs[tab_id].get_mode()
+ if mode:
+ mode.tor_connection_stopped()
+
class TabBar(QtWidgets.QTabBar):
"""
diff --git a/desktop/src/onionshare/tor_connection_dialog.py b/desktop/src/onionshare/tor_connection.py
index b5c2f61c..2cc599c4 100644
--- a/desktop/src/onionshare/tor_connection_dialog.py
+++ b/desktop/src/onionshare/tor_connection.py
@@ -48,12 +48,16 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
Connecting to Tor dialog.
"""
- open_settings = QtCore.Signal()
+ open_tor_settings = QtCore.Signal()
+ success = QtCore.Signal()
- def __init__(self, common, custom_settings=False):
+ def __init__(
+ self, common, custom_settings=False, testing_settings=False, onion=None
+ ):
super(TorConnectionDialog, self).__init__(None)
self.common = common
+ self.testing_settings = testing_settings
if custom_settings:
self.settings = custom_settings
@@ -62,7 +66,15 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
self.common.log("TorConnectionDialog", "__init__")
- self.setWindowTitle("OnionShare")
+ if self.testing_settings:
+ self.title = strings._("gui_settings_connection_type_test_button")
+ self.onion = onion
+ else:
+ self.title = "OnionShare"
+ self.onion = self.common.gui.onion
+
+ self.setWindowTitle(self.title)
+
self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png")))
self.setModal(True)
self.setFixedSize(400, 150)
@@ -105,14 +117,13 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
def _connected_to_tor(self):
self.common.log("TorConnectionDialog", "_connected_to_tor")
self.active = False
-
# Close the dialog after connecting
self.setValue(self.maximum())
def _canceled_connecting_to_tor(self):
self.common.log("TorConnectionDialog", "_canceled_connecting_to_tor")
self.active = False
- self.common.gui.onion.cleanup()
+ self.onion.cleanup()
# Cancel connecting to Tor
QtCore.QTimer.singleShot(1, self.cancel)
@@ -121,47 +132,160 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
self.common.log("TorConnectionDialog", "_error_connecting_to_tor")
self.active = False
- def alert_and_open_settings():
- # Display the exception in an alert box
- Alert(
- self.common,
- f"{msg}\n\n{strings._('gui_tor_connection_error_settings')}",
- QtWidgets.QMessageBox.Warning,
- )
+ if self.testing_settings:
+ # If testing, just display the error but don't open settings
+ def alert():
+ Alert(self.common, msg, QtWidgets.QMessageBox.Warning, title=self.title)
- # Open settings
- self.open_settings.emit()
+ else:
+ # If not testing, open settings after displaying the error
+ def alert():
+ Alert(
+ self.common,
+ f"{msg}\n\n{strings._('gui_tor_connection_error_settings')}",
+ QtWidgets.QMessageBox.Warning,
+ title=self.title,
+ )
+
+ # Open settings
+ self.open_tor_settings.emit()
- QtCore.QTimer.singleShot(1, alert_and_open_settings)
+ QtCore.QTimer.singleShot(1, alert)
# Cancel connecting to Tor
QtCore.QTimer.singleShot(1, self.cancel)
+class TorConnectionWidget(QtWidgets.QWidget):
+ """
+ Connecting to Tor widget, with a progress bar
+ """
+
+ open_tor_settings = QtCore.Signal()
+ success = QtCore.Signal()
+ fail = QtCore.Signal(str)
+
+ def __init__(self, common, status_bar):
+ super(TorConnectionWidget, self).__init__(None)
+ self.common = common
+ self.common.log("TorConnectionWidget", "__init__")
+
+ self.status_bar = status_bar
+ self.label = QtWidgets.QLabel(strings._("connecting_to_tor"))
+ self.label.setAlignment(QtCore.Qt.AlignHCenter)
+
+ self.progress = QtWidgets.QProgressBar()
+ self.progress.setRange(0, 100)
+ self.cancel_button = QtWidgets.QPushButton(
+ strings._("gui_settings_button_cancel")
+ )
+ self.cancel_button.clicked.connect(self.cancel_clicked)
+
+ progress_layout = QtWidgets.QHBoxLayout()
+ progress_layout.addWidget(self.progress)
+ progress_layout.addWidget(self.cancel_button)
+
+ inner_layout = QtWidgets.QVBoxLayout()
+ inner_layout.addWidget(self.label)
+ inner_layout.addLayout(progress_layout)
+
+ layout = QtWidgets.QHBoxLayout()
+ layout.addStretch()
+ layout.addLayout(inner_layout)
+ layout.addStretch()
+ self.setLayout(layout)
+
+ # Start displaying the status at 0
+ self._tor_status_update(0, "")
+
+ def start(self, custom_settings=False, testing_settings=False, onion=None):
+ self.common.log("TorConnectionWidget", "start")
+ self.was_canceled = False
+
+ self.testing_settings = testing_settings
+
+ if custom_settings:
+ self.settings = custom_settings
+ else:
+ self.settings = self.common.settings
+
+ if self.testing_settings:
+ self.onion = onion
+ else:
+ self.onion = self.common.gui.onion
+
+ t = TorConnectionThread(self.common, self.settings, self)
+ t.tor_status_update.connect(self._tor_status_update)
+ t.connected_to_tor.connect(self._connected_to_tor)
+ t.canceled_connecting_to_tor.connect(self._canceled_connecting_to_tor)
+ t.error_connecting_to_tor.connect(self._error_connecting_to_tor)
+ t.start()
+
+ # The main thread needs to remain active, and checking for Qt events,
+ # until the thread is finished. Otherwise it won't be able to handle
+ # accepting signals.
+ self.active = True
+ while self.active:
+ time.sleep(0.1)
+ self.common.gui.qtapp.processEvents()
+
+ def cancel_clicked(self):
+ self.was_canceled = True
+ self.fail.emit("")
+
+ def wasCanceled(self):
+ return self.was_canceled
+
+ def _tor_status_update(self, progress, summary):
+ self.progress.setValue(int(progress))
+ self.label.setText(
+ f"<strong>{strings._('connecting_to_tor')}</strong><br>{summary}"
+ )
+
+ def _connected_to_tor(self):
+ self.common.log("TorConnectionWidget", "_connected_to_tor")
+ self.active = False
+ self.status_bar.clearMessage()
+
+ # Close the dialog after connecting
+ self.progress.setValue(self.progress.maximum())
+
+ self.success.emit()
+
+ def _canceled_connecting_to_tor(self):
+ self.common.log("TorConnectionWidget", "_canceled_connecting_to_tor")
+ self.active = False
+ self.onion.cleanup()
+
+ # Cancel connecting to Tor
+ QtCore.QTimer.singleShot(1, self.cancel_clicked)
+
+ def _error_connecting_to_tor(self, msg):
+ self.common.log("TorConnectionWidget", "_error_connecting_to_tor")
+ self.active = False
+ self.fail.emit(msg)
+
+
class TorConnectionThread(QtCore.QThread):
tor_status_update = QtCore.Signal(str, str)
connected_to_tor = QtCore.Signal()
canceled_connecting_to_tor = QtCore.Signal()
error_connecting_to_tor = QtCore.Signal(str)
- def __init__(self, common, settings, dialog):
+ def __init__(self, common, settings, parent):
super(TorConnectionThread, self).__init__()
-
self.common = common
-
self.common.log("TorConnectionThread", "__init__")
-
self.settings = settings
-
- self.dialog = dialog
+ self.parent = parent
def run(self):
self.common.log("TorConnectionThread", "run")
# Connect to the Onion
try:
- self.common.gui.onion.connect(self.settings, False, self._tor_status_update)
- if self.common.gui.onion.connected_to_tor:
+ self.parent.onion.connect(self.settings, False, self._tor_status_update)
+ if self.parent.onion.connected_to_tor:
self.connected_to_tor.emit()
else:
self.canceled_connecting_to_tor.emit()
@@ -197,4 +321,4 @@ class TorConnectionThread(QtCore.QThread):
self.tor_status_update.emit(progress, summary)
# Return False if the dialog was canceled
- return not self.dialog.wasCanceled()
+ return not self.parent.wasCanceled()
diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py
new file mode 100644
index 00000000..e28e5260
--- /dev/null
+++ b/desktop/src/onionshare/tor_settings_tab.py
@@ -0,0 +1,895 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2021 Micah Lee, et al. <micah@micahflee.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from PySide2 import QtCore, QtWidgets, QtGui
+import sys
+import platform
+import re
+import os
+
+from onionshare_cli.meek import Meek
+from onionshare_cli.settings import Settings
+from onionshare_cli.onion import Onion
+
+from . import strings
+from .widgets import Alert
+from .tor_connection import TorConnectionWidget
+from .moat_dialog import MoatDialog
+
+
+class TorSettingsTab(QtWidgets.QWidget):
+ """
+ Settings dialog.
+ """
+
+ close_this_tab = QtCore.Signal()
+ tor_is_connected = QtCore.Signal()
+ tor_is_disconnected = QtCore.Signal()
+
+ def __init__(self, common, tab_id, are_tabs_active, status_bar):
+ super(TorSettingsTab, self).__init__()
+
+ self.common = common
+ self.common.log("TorSettingsTab", "__init__")
+
+ self.status_bar = status_bar
+ self.meek = Meek(common, get_tor_paths=self.common.gui.get_tor_paths)
+
+ self.system = platform.system()
+ self.tab_id = tab_id
+
+ # Connection type: either automatic, control port, or socket file
+
+ # Bundled Tor
+ self.connection_type_bundled_radio = QtWidgets.QRadioButton(
+ strings._("gui_settings_connection_type_bundled_option")
+ )
+ self.connection_type_bundled_radio.toggled.connect(
+ self.connection_type_bundled_toggled
+ )
+
+ # Bundled Tor doesn't work on dev mode in Windows or Mac
+ if (self.system == "Windows" or self.system == "Darwin") and getattr(
+ sys, "onionshare_dev_mode", False
+ ):
+ self.connection_type_bundled_radio.setEnabled(False)
+
+ # Bridge options for bundled tor
+
+ (
+ self.tor_path,
+ self.tor_geo_ip_file_path,
+ self.tor_geo_ipv6_file_path,
+ self.obfs4proxy_file_path,
+ self.snowflake_file_path,
+ self.meek_client_file_path,
+ ) = self.common.gui.get_tor_paths()
+
+ bridges_label = QtWidgets.QLabel(strings._("gui_settings_tor_bridges_label"))
+ bridges_label.setWordWrap(True)
+
+ self.bridge_use_checkbox = QtWidgets.QCheckBox(
+ strings._("gui_settings_bridge_use_checkbox")
+ )
+ self.bridge_use_checkbox.stateChanged.connect(
+ self.bridge_use_checkbox_state_changed
+ )
+
+ # Built-in bridge
+ self.bridge_builtin_radio = QtWidgets.QRadioButton(
+ strings._("gui_settings_bridge_radio_builtin")
+ )
+ self.bridge_builtin_radio.toggled.connect(self.bridge_builtin_radio_toggled)
+ self.bridge_builtin_dropdown = QtWidgets.QComboBox()
+ self.bridge_builtin_dropdown.currentTextChanged.connect(
+ self.bridge_builtin_dropdown_changed
+ )
+ if self.obfs4proxy_file_path and os.path.isfile(self.obfs4proxy_file_path):
+ self.bridge_builtin_dropdown.addItem("obfs4")
+ self.bridge_builtin_dropdown.addItem("meek-azure")
+ if self.snowflake_file_path and os.path.isfile(self.snowflake_file_path):
+ self.bridge_builtin_dropdown.addItem("snowflake")
+
+ # Request a bridge from torproject.org (moat)
+ self.bridge_moat_radio = QtWidgets.QRadioButton(
+ strings._("gui_settings_bridge_moat_radio_option")
+ )
+ self.bridge_moat_radio.toggled.connect(self.bridge_moat_radio_toggled)
+ self.bridge_moat_button = QtWidgets.QPushButton(
+ strings._("gui_settings_bridge_moat_button")
+ )
+ self.bridge_moat_button.clicked.connect(self.bridge_moat_button_clicked)
+ self.bridge_moat_textbox = QtWidgets.QPlainTextEdit()
+ self.bridge_moat_textbox.setMinimumHeight(100)
+ self.bridge_moat_textbox.setMaximumHeight(100)
+ self.bridge_moat_textbox.setReadOnly(True)
+ self.bridge_moat_textbox.setWordWrapMode(QtGui.QTextOption.NoWrap)
+ bridge_moat_textbox_options_layout = QtWidgets.QVBoxLayout()
+ bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_button)
+ bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_textbox)
+ self.bridge_moat_textbox_options = QtWidgets.QWidget()
+ self.bridge_moat_textbox_options.setLayout(bridge_moat_textbox_options_layout)
+ self.bridge_moat_textbox_options.hide()
+
+ # Custom bridges radio and textbox
+ self.bridge_custom_radio = QtWidgets.QRadioButton(
+ strings._("gui_settings_bridge_custom_radio_option")
+ )
+ self.bridge_custom_radio.toggled.connect(self.bridge_custom_radio_toggled)
+ self.bridge_custom_textbox = QtWidgets.QPlainTextEdit()
+ self.bridge_custom_textbox.setMinimumHeight(100)
+ self.bridge_custom_textbox.setMaximumHeight(100)
+ self.bridge_custom_textbox.setPlaceholderText(
+ strings._("gui_settings_bridge_custom_placeholder")
+ )
+
+ bridge_custom_textbox_options_layout = QtWidgets.QVBoxLayout()
+ bridge_custom_textbox_options_layout.addWidget(self.bridge_custom_textbox)
+
+ self.bridge_custom_textbox_options = QtWidgets.QWidget()
+ self.bridge_custom_textbox_options.setLayout(
+ bridge_custom_textbox_options_layout
+ )
+ self.bridge_custom_textbox_options.hide()
+
+ # Bridge settings layout
+ bridge_settings_layout = QtWidgets.QVBoxLayout()
+ bridge_settings_layout.addWidget(self.bridge_builtin_radio)
+ bridge_settings_layout.addWidget(self.bridge_builtin_dropdown)
+ bridge_settings_layout.addWidget(self.bridge_moat_radio)
+ bridge_settings_layout.addWidget(self.bridge_moat_textbox_options)
+ bridge_settings_layout.addWidget(self.bridge_custom_radio)
+ bridge_settings_layout.addWidget(self.bridge_custom_textbox_options)
+ self.bridge_settings = QtWidgets.QWidget()
+ self.bridge_settings.setLayout(bridge_settings_layout)
+
+ # Bridges layout/widget
+ bridges_layout = QtWidgets.QVBoxLayout()
+ bridges_layout.addWidget(bridges_label)
+ bridges_layout.addWidget(self.bridge_use_checkbox)
+ bridges_layout.addWidget(self.bridge_settings)
+
+ self.bridges = QtWidgets.QWidget()
+ self.bridges.setLayout(bridges_layout)
+
+ # Automatic
+ self.connection_type_automatic_radio = QtWidgets.QRadioButton(
+ strings._("gui_settings_connection_type_automatic_option")
+ )
+ self.connection_type_automatic_radio.toggled.connect(
+ self.connection_type_automatic_toggled
+ )
+
+ # Control port
+ self.connection_type_control_port_radio = QtWidgets.QRadioButton(
+ strings._("gui_settings_connection_type_control_port_option")
+ )
+ self.connection_type_control_port_radio.toggled.connect(
+ self.connection_type_control_port_toggled
+ )
+
+ connection_type_control_port_extras_label = QtWidgets.QLabel(
+ strings._("gui_settings_control_port_label")
+ )
+ self.connection_type_control_port_extras_address = QtWidgets.QLineEdit()
+ self.connection_type_control_port_extras_port = QtWidgets.QLineEdit()
+ connection_type_control_port_extras_layout = QtWidgets.QHBoxLayout()
+ connection_type_control_port_extras_layout.addWidget(
+ connection_type_control_port_extras_label
+ )
+ connection_type_control_port_extras_layout.addWidget(
+ self.connection_type_control_port_extras_address
+ )
+ connection_type_control_port_extras_layout.addWidget(
+ self.connection_type_control_port_extras_port
+ )
+
+ self.connection_type_control_port_extras = QtWidgets.QWidget()
+ self.connection_type_control_port_extras.setLayout(
+ connection_type_control_port_extras_layout
+ )
+ self.connection_type_control_port_extras.hide()
+
+ # Socket file
+ self.connection_type_socket_file_radio = QtWidgets.QRadioButton(
+ strings._("gui_settings_connection_type_socket_file_option")
+ )
+ self.connection_type_socket_file_radio.toggled.connect(
+ self.connection_type_socket_file_toggled
+ )
+
+ connection_type_socket_file_extras_label = QtWidgets.QLabel(
+ strings._("gui_settings_socket_file_label")
+ )
+ self.connection_type_socket_file_extras_path = QtWidgets.QLineEdit()
+ connection_type_socket_file_extras_layout = QtWidgets.QHBoxLayout()
+ connection_type_socket_file_extras_layout.addWidget(
+ connection_type_socket_file_extras_label
+ )
+ connection_type_socket_file_extras_layout.addWidget(
+ self.connection_type_socket_file_extras_path
+ )
+
+ self.connection_type_socket_file_extras = QtWidgets.QWidget()
+ self.connection_type_socket_file_extras.setLayout(
+ connection_type_socket_file_extras_layout
+ )
+ self.connection_type_socket_file_extras.hide()
+
+ # Tor SOCKS address and port
+ gui_settings_socks_label = QtWidgets.QLabel(
+ strings._("gui_settings_socks_label")
+ )
+ self.connection_type_socks_address = QtWidgets.QLineEdit()
+ self.connection_type_socks_port = QtWidgets.QLineEdit()
+ connection_type_socks_layout = QtWidgets.QHBoxLayout()
+ connection_type_socks_layout.addWidget(gui_settings_socks_label)
+ connection_type_socks_layout.addWidget(self.connection_type_socks_address)
+ connection_type_socks_layout.addWidget(self.connection_type_socks_port)
+
+ self.connection_type_socks = QtWidgets.QWidget()
+ self.connection_type_socks.setLayout(connection_type_socks_layout)
+ self.connection_type_socks.hide()
+
+ # Authentication options
+ self.authenticate_no_auth_checkbox = QtWidgets.QCheckBox(
+ strings._("gui_settings_authenticate_no_auth_option")
+ )
+ self.authenticate_no_auth_checkbox.toggled.connect(
+ self.authenticate_no_auth_toggled
+ )
+
+ authenticate_password_extras_label = QtWidgets.QLabel(
+ strings._("gui_settings_password_label")
+ )
+ self.authenticate_password_extras_password = QtWidgets.QLineEdit("")
+ authenticate_password_extras_layout = QtWidgets.QHBoxLayout()
+ authenticate_password_extras_layout.addWidget(
+ authenticate_password_extras_label
+ )
+ authenticate_password_extras_layout.addWidget(
+ self.authenticate_password_extras_password
+ )
+
+ self.authenticate_password_extras = QtWidgets.QWidget()
+ self.authenticate_password_extras.setLayout(authenticate_password_extras_layout)
+ self.authenticate_password_extras.hide()
+
+ # Group for Tor settings
+ tor_settings_layout = QtWidgets.QVBoxLayout()
+ tor_settings_layout.addWidget(self.connection_type_control_port_extras)
+ tor_settings_layout.addWidget(self.connection_type_socket_file_extras)
+ tor_settings_layout.addWidget(self.connection_type_socks)
+ tor_settings_layout.addWidget(self.authenticate_no_auth_checkbox)
+ tor_settings_layout.addWidget(self.authenticate_password_extras)
+ self.tor_settings_group = QtWidgets.QGroupBox(
+ strings._("gui_settings_controller_extras_label")
+ )
+ self.tor_settings_group.setLayout(tor_settings_layout)
+ self.tor_settings_group.hide()
+
+ # Put the radios into their own group so they are exclusive
+ connection_type_radio_group_layout = QtWidgets.QVBoxLayout()
+ connection_type_radio_group_layout.addWidget(self.connection_type_bundled_radio)
+ connection_type_radio_group_layout.addWidget(
+ self.connection_type_automatic_radio
+ )
+ connection_type_radio_group_layout.addWidget(
+ self.connection_type_control_port_radio
+ )
+ connection_type_radio_group_layout.addWidget(
+ self.connection_type_socket_file_radio
+ )
+ connection_type_radio_group_layout.addStretch()
+ connection_type_radio_group = QtWidgets.QGroupBox(
+ strings._("gui_settings_connection_type_label")
+ )
+ connection_type_radio_group.setLayout(connection_type_radio_group_layout)
+
+ # The Bridges options are not exclusive (enabling Bridges offers obfs4 or custom bridges)
+ connection_type_bridges_radio_group_layout = QtWidgets.QVBoxLayout()
+ connection_type_bridges_radio_group_layout.addWidget(self.bridges)
+ self.connection_type_bridges_radio_group = QtWidgets.QGroupBox(
+ strings._("gui_settings_tor_bridges")
+ )
+ self.connection_type_bridges_radio_group.setLayout(
+ connection_type_bridges_radio_group_layout
+ )
+ self.connection_type_bridges_radio_group.hide()
+
+ # Connection type layout
+ connection_type_layout = QtWidgets.QVBoxLayout()
+ connection_type_layout.addWidget(self.tor_settings_group)
+ connection_type_layout.addWidget(self.connection_type_bridges_radio_group)
+ connection_type_layout.addStretch()
+
+ # Settings are in columns
+ columns_layout = QtWidgets.QHBoxLayout()
+ columns_layout.addWidget(connection_type_radio_group)
+ columns_layout.addSpacing(20)
+ columns_layout.addLayout(connection_type_layout, stretch=1)
+ columns_wrapper = QtWidgets.QWidget()
+ columns_wrapper.setFixedHeight(400)
+ columns_wrapper.setLayout(columns_layout)
+
+ # Tor connection widget
+ self.tor_con = TorConnectionWidget(self.common, self.status_bar)
+ self.tor_con.success.connect(self.tor_con_success)
+ self.tor_con.fail.connect(self.tor_con_fail)
+ self.tor_con.hide()
+ self.tor_con_type = None
+
+ # Error label
+ self.error_label = QtWidgets.QLabel()
+ self.error_label.setStyleSheet(self.common.gui.css["tor_settings_error"])
+ self.error_label.setWordWrap(True)
+
+ # Buttons
+ self.test_tor_button = QtWidgets.QPushButton(
+ strings._("gui_settings_connection_type_test_button")
+ )
+ self.test_tor_button.clicked.connect(self.test_tor_clicked)
+ self.save_button = QtWidgets.QPushButton(strings._("gui_settings_button_save"))
+ self.save_button.clicked.connect(self.save_clicked)
+ buttons_layout = QtWidgets.QHBoxLayout()
+ buttons_layout.addWidget(self.error_label, stretch=1)
+ buttons_layout.addSpacing(20)
+ buttons_layout.addWidget(self.test_tor_button)
+ buttons_layout.addWidget(self.save_button)
+
+ # Main layout
+ main_layout = QtWidgets.QVBoxLayout()
+ main_layout.addWidget(columns_wrapper)
+ main_layout.addStretch()
+ main_layout.addWidget(self.tor_con)
+ main_layout.addStretch()
+ main_layout.addLayout(buttons_layout)
+ self.main_widget = QtWidgets.QWidget()
+ self.main_widget.setLayout(main_layout)
+
+ # Tabs are active label
+ active_tabs_label = QtWidgets.QLabel(
+ strings._("gui_settings_stop_active_tabs_label")
+ )
+ active_tabs_label.setAlignment(QtCore.Qt.AlignHCenter)
+
+ # Active tabs layout
+ active_tabs_layout = QtWidgets.QVBoxLayout()
+ active_tabs_layout.addStretch()
+ active_tabs_layout.addWidget(active_tabs_label)
+ active_tabs_layout.addStretch()
+ self.active_tabs_widget = QtWidgets.QWidget()
+ self.active_tabs_widget.setLayout(active_tabs_layout)
+
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(self.main_widget)
+ layout.addWidget(self.active_tabs_widget)
+ self.setLayout(layout)
+
+ self.active_tabs_changed(are_tabs_active)
+ self.reload_settings()
+
+ def reload_settings(self):
+ # Load settings, and fill them in
+ self.old_settings = Settings(self.common)
+ self.old_settings.load()
+
+ connection_type = self.old_settings.get("connection_type")
+ if connection_type == "bundled":
+ if self.connection_type_bundled_radio.isEnabled():
+ self.connection_type_bundled_radio.setChecked(True)
+ else:
+ # If bundled tor is disabled, fallback to automatic
+ self.connection_type_automatic_radio.setChecked(True)
+ elif connection_type == "automatic":
+ self.connection_type_automatic_radio.setChecked(True)
+ elif connection_type == "control_port":
+ self.connection_type_control_port_radio.setChecked(True)
+ elif connection_type == "socket_file":
+ self.connection_type_socket_file_radio.setChecked(True)
+ self.connection_type_control_port_extras_address.setText(
+ self.old_settings.get("control_port_address")
+ )
+ self.connection_type_control_port_extras_port.setText(
+ str(self.old_settings.get("control_port_port"))
+ )
+ self.connection_type_socket_file_extras_path.setText(
+ self.old_settings.get("socket_file_path")
+ )
+ self.connection_type_socks_address.setText(
+ self.old_settings.get("socks_address")
+ )
+ self.connection_type_socks_port.setText(
+ str(self.old_settings.get("socks_port"))
+ )
+ auth_type = self.old_settings.get("auth_type")
+ if auth_type == "no_auth":
+ self.authenticate_no_auth_checkbox.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.authenticate_no_auth_checkbox.setChecked(QtCore.Qt.Unchecked)
+ self.authenticate_password_extras_password.setText(
+ self.old_settings.get("auth_password")
+ )
+
+ if self.old_settings.get("bridges_enabled"):
+ self.bridge_use_checkbox.setCheckState(QtCore.Qt.Checked)
+ self.bridge_settings.show()
+
+ bridges_type = self.old_settings.get("bridges_type")
+ if bridges_type == "built-in":
+ self.bridge_builtin_radio.setChecked(True)
+ self.bridge_builtin_dropdown.show()
+ self.bridge_moat_radio.setChecked(False)
+ self.bridge_moat_textbox_options.hide()
+ self.bridge_custom_radio.setChecked(False)
+ self.bridge_custom_textbox_options.hide()
+
+ bridges_builtin_pt = self.old_settings.get("bridges_builtin_pt")
+ if bridges_builtin_pt == "obfs4":
+ self.bridge_builtin_dropdown.setCurrentText("obfs4")
+ elif bridges_builtin_pt == "meek-azure":
+ self.bridge_builtin_dropdown.setCurrentText("meek-azure")
+ else:
+ self.bridge_builtin_dropdown.setCurrentText("snowflake")
+
+ self.bridge_moat_textbox_options.hide()
+ self.bridge_custom_textbox_options.hide()
+
+ elif bridges_type == "moat":
+ self.bridge_builtin_radio.setChecked(False)
+ self.bridge_builtin_dropdown.hide()
+ self.bridge_moat_radio.setChecked(True)
+ self.bridge_moat_textbox_options.show()
+ self.bridge_custom_radio.setChecked(False)
+ self.bridge_custom_textbox_options.hide()
+
+ else:
+ self.bridge_builtin_radio.setChecked(False)
+ self.bridge_builtin_dropdown.hide()
+ self.bridge_moat_radio.setChecked(False)
+ self.bridge_moat_textbox_options.hide()
+ self.bridge_custom_radio.setChecked(True)
+ self.bridge_custom_textbox_options.show()
+
+ bridges_moat = self.old_settings.get("bridges_moat")
+ self.bridge_moat_textbox.document().setPlainText(bridges_moat)
+ bridges_custom = self.old_settings.get("bridges_custom")
+ self.bridge_custom_textbox.document().setPlainText(bridges_custom)
+
+ else:
+ self.bridge_use_checkbox.setCheckState(QtCore.Qt.Unchecked)
+ self.bridge_settings.hide()
+
+ def active_tabs_changed(self, are_tabs_active):
+ if are_tabs_active:
+ self.main_widget.hide()
+ self.active_tabs_widget.show()
+ else:
+ self.main_widget.show()
+ self.active_tabs_widget.hide()
+
+ def connection_type_bundled_toggled(self, checked):
+ """
+ Connection type bundled was toggled
+ """
+ self.common.log("TorSettingsTab", "connection_type_bundled_toggled")
+ if checked:
+ self.tor_settings_group.hide()
+ self.connection_type_socks.hide()
+ self.connection_type_bridges_radio_group.show()
+
+ def bridge_use_checkbox_state_changed(self, state):
+ """
+ 'Use a bridge' checkbox changed
+ """
+ if state == QtCore.Qt.Checked:
+ self.bridge_settings.show()
+ self.bridge_builtin_radio.click()
+ self.bridge_builtin_dropdown.setCurrentText("obfs4")
+ else:
+ self.bridge_settings.hide()
+
+ def bridge_builtin_radio_toggled(self, checked):
+ """
+ 'Select a built-in bridge' radio button toggled
+ """
+ if checked:
+ self.bridge_builtin_dropdown.show()
+ self.bridge_custom_textbox_options.hide()
+ self.bridge_moat_textbox_options.hide()
+
+ def bridge_builtin_dropdown_changed(self, selection):
+ """
+ Build-in bridge selection changed
+ """
+ if selection == "meek-azure":
+ # Alert the user about meek's costliness if it looks like they're turning it on
+ if not self.old_settings.get("bridges_builtin_pt") == "meek-azure":
+ Alert(
+ self.common,
+ strings._("gui_settings_meek_lite_expensive_warning"),
+ QtWidgets.QMessageBox.Warning,
+ )
+
+ def bridge_moat_radio_toggled(self, checked):
+ """
+ Moat (request bridge) bridges option was toggled. If checked, show moat bridge options.
+ """
+ if checked:
+ self.bridge_builtin_dropdown.hide()
+ self.bridge_custom_textbox_options.hide()
+ self.bridge_moat_textbox_options.show()
+
+ def bridge_moat_button_clicked(self):
+ """
+ Request new bridge button clicked
+ """
+ self.common.log("TorSettingsTab", "bridge_moat_button_clicked")
+
+ moat_dialog = MoatDialog(self.common, self.meek)
+ moat_dialog.got_bridges.connect(self.bridge_moat_got_bridges)
+ moat_dialog.exec_()
+
+ def bridge_moat_got_bridges(self, bridges):
+ """
+ Got new bridges from moat
+ """
+ self.common.log("TorSettingsTab", "bridge_moat_got_bridges")
+ self.bridge_moat_textbox.document().setPlainText(bridges)
+ self.bridge_moat_textbox.show()
+
+ def bridge_custom_radio_toggled(self, checked):
+ """
+ Custom bridges option was toggled. If checked, show custom bridge options.
+ """
+ if checked:
+ self.bridge_builtin_dropdown.hide()
+ self.bridge_moat_textbox_options.hide()
+ self.bridge_custom_textbox_options.show()
+
+ def connection_type_automatic_toggled(self, checked):
+ """
+ Connection type automatic was toggled. If checked, hide authentication fields.
+ """
+ self.common.log("TorSettingsTab", "connection_type_automatic_toggled")
+ if checked:
+ self.tor_settings_group.hide()
+ self.connection_type_socks.hide()
+ self.connection_type_bridges_radio_group.hide()
+
+ def connection_type_control_port_toggled(self, checked):
+ """
+ Connection type control port was toggled. If checked, show extra fields
+ for Tor control address and port. If unchecked, hide those extra fields.
+ """
+ self.common.log("TorSettingsTab", "connection_type_control_port_toggled")
+ if checked:
+ self.tor_settings_group.show()
+ self.connection_type_control_port_extras.show()
+ self.connection_type_socks.show()
+ self.connection_type_bridges_radio_group.hide()
+ else:
+ self.connection_type_control_port_extras.hide()
+
+ def connection_type_socket_file_toggled(self, checked):
+ """
+ Connection type socket file was toggled. If checked, show extra fields
+ for socket file. If unchecked, hide those extra fields.
+ """
+ self.common.log("TorSettingsTab", "connection_type_socket_file_toggled")
+ if checked:
+ self.tor_settings_group.show()
+ self.connection_type_socket_file_extras.show()
+ self.connection_type_socks.show()
+ self.connection_type_bridges_radio_group.hide()
+ else:
+ self.connection_type_socket_file_extras.hide()
+
+ def authenticate_no_auth_toggled(self, checked):
+ """
+ Authentication option no authentication was toggled.
+ """
+ self.common.log("TorSettingsTab", "authenticate_no_auth_toggled")
+ if checked:
+ self.authenticate_password_extras.hide()
+ else:
+ self.authenticate_password_extras.show()
+
+ def test_tor_clicked(self):
+ """
+ Test Tor Settings button clicked. With the given settings, see if we can
+ successfully connect and authenticate to Tor.
+ """
+ self.common.log("TorSettingsTab", "test_tor_clicked")
+
+ self.error_label.setText("")
+
+ settings = self.settings_from_fields()
+ if not settings:
+ return
+
+ self.test_tor_button.hide()
+ self.save_button.hide()
+
+ self.test_onion = Onion(
+ self.common,
+ use_tmp_dir=True,
+ get_tor_paths=self.common.gui.get_tor_paths,
+ )
+
+ self.tor_con_type = "test"
+ self.tor_con.show()
+ self.tor_con.start(settings, True, self.test_onion)
+
+ def save_clicked(self):
+ """
+ Save button clicked. Save current settings to disk.
+ """
+ self.common.log("TorSettingsTab", "save_clicked")
+
+ self.error_label.setText("")
+
+ def changed(s1, s2, keys):
+ """
+ Compare the Settings objects s1 and s2 and return true if any values
+ have changed for the given keys.
+ """
+ for key in keys:
+ if s1.get(key) != s2.get(key):
+ return True
+ return False
+
+ settings = self.settings_from_fields()
+ if settings:
+ # Save the new settings
+ settings.save()
+
+ # If Tor isn't connected, or if Tor settings have changed, Reinitialize
+ # the Onion object
+ reboot_onion = False
+ if not self.common.gui.local_only:
+ if self.common.gui.onion.is_authenticated():
+ self.common.log(
+ "TorSettingsTab", "save_clicked", "Connected to Tor"
+ )
+
+ if changed(
+ settings,
+ self.old_settings,
+ [
+ "connection_type",
+ "control_port_address",
+ "control_port_port",
+ "socks_address",
+ "socks_port",
+ "socket_file_path",
+ "auth_type",
+ "auth_password",
+ "bridges_enabled",
+ "bridges_type",
+ "bridges_builtin_pt",
+ "bridges_moat",
+ "bridges_custom",
+ ],
+ ):
+
+ reboot_onion = True
+
+ else:
+ self.common.log(
+ "TorSettingsTab", "save_clicked", "Not connected to Tor"
+ )
+ # Tor isn't connected, so try connecting
+ reboot_onion = True
+
+ # Do we need to reinitialize Tor?
+ if reboot_onion:
+ # Tell the tabs that Tor is disconnected
+ self.tor_is_disconnected.emit()
+
+ # Reinitialize the Onion object
+ self.common.log(
+ "TorSettingsTab", "save_clicked", "rebooting the Onion"
+ )
+ self.common.gui.onion.cleanup()
+
+ self.test_tor_button.hide()
+ self.save_button.hide()
+
+ self.tor_con_type = "save"
+ self.tor_con.show()
+ self.tor_con.start(settings)
+ else:
+ self.close_this_tab.emit()
+ else:
+ self.close_this_tab.emit()
+
+ def tor_con_success(self):
+ """
+ Finished testing tor connection.
+ """
+ self.tor_con.hide()
+ self.test_tor_button.show()
+ self.save_button.show()
+
+ if self.tor_con_type == "test":
+ Alert(
+ self.common,
+ strings._("settings_test_success").format(
+ self.test_onion.tor_version,
+ self.test_onion.supports_ephemeral,
+ self.test_onion.supports_stealth,
+ self.test_onion.supports_v3_onions,
+ ),
+ title=strings._("gui_settings_connection_type_test_button"),
+ )
+ self.test_onion.cleanup()
+
+ elif self.tor_con_type == "save":
+ if (
+ self.common.gui.onion.is_authenticated()
+ and not self.tor_con.wasCanceled()
+ ):
+ # Tell the tabs that Tor is connected
+ self.tor_is_connected.emit()
+ # Close the tab
+ self.close_this_tab.emit()
+
+ self.tor_con_type = None
+
+ def tor_con_fail(self, msg):
+ """
+ Finished testing tor connection.
+ """
+ self.tor_con.hide()
+ self.test_tor_button.show()
+ self.save_button.show()
+
+ self.error_label.setText(msg)
+
+ if self.tor_con_type == "test":
+ self.test_onion.cleanup()
+
+ self.tor_con_type = None
+
+ def settings_from_fields(self):
+ """
+ Return a Settings object that's full of values from the settings dialog.
+ """
+ self.common.log("TorSettingsTab", "settings_from_fields")
+ settings = Settings(self.common)
+ settings.load() # To get the last update timestamp
+
+ # Tor connection
+ if self.connection_type_bundled_radio.isChecked():
+ settings.set("connection_type", "bundled")
+ if self.connection_type_automatic_radio.isChecked():
+ settings.set("connection_type", "automatic")
+ if self.connection_type_control_port_radio.isChecked():
+ settings.set("connection_type", "control_port")
+ if self.connection_type_socket_file_radio.isChecked():
+ settings.set("connection_type", "socket_file")
+
+ settings.set(
+ "control_port_address",
+ self.connection_type_control_port_extras_address.text(),
+ )
+ settings.set(
+ "control_port_port", self.connection_type_control_port_extras_port.text()
+ )
+ settings.set(
+ "socket_file_path", self.connection_type_socket_file_extras_path.text()
+ )
+
+ settings.set("socks_address", self.connection_type_socks_address.text())
+ settings.set("socks_port", self.connection_type_socks_port.text())
+
+ if self.authenticate_no_auth_checkbox.checkState() == QtCore.Qt.Checked:
+ settings.set("auth_type", "no_auth")
+ else:
+ settings.set("auth_type", "password")
+
+ settings.set("auth_password", self.authenticate_password_extras_password.text())
+
+ # Whether we use bridges
+ if self.bridge_use_checkbox.checkState() == QtCore.Qt.Checked:
+ settings.set("bridges_enabled", True)
+
+ if self.bridge_builtin_radio.isChecked():
+ settings.set("bridges_type", "built-in")
+
+ selection = self.bridge_builtin_dropdown.currentText()
+ settings.set("bridges_builtin_pt", selection)
+
+ if self.bridge_moat_radio.isChecked():
+ settings.set("bridges_type", "moat")
+ moat_bridges = self.bridge_moat_textbox.toPlainText()
+ if (
+ self.connection_type_bundled_radio.isChecked()
+ and moat_bridges.strip() == ""
+ ):
+ self.error_label.setText(
+ strings._("gui_settings_moat_bridges_invalid")
+ )
+ return False
+
+ settings.set("bridges_moat", moat_bridges)
+
+ if self.bridge_custom_radio.isChecked():
+ settings.set("bridges_type", "custom")
+
+ new_bridges = []
+ bridges = self.bridge_custom_textbox.toPlainText().split("\n")
+ bridges_valid = False
+ for bridge in bridges:
+ if bridge != "":
+ # Check the syntax of the custom bridge to make sure it looks legitimate
+ ipv4_pattern = re.compile(
+ "(obfs4\s+)?(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):([0-9]+)(\s+)([A-Z0-9]+)(.+)$"
+ )
+ ipv6_pattern = re.compile(
+ "(obfs4\s+)?\[(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\]:[0-9]+\s+[A-Z0-9]+(.+)$"
+ )
+ meek_lite_pattern = re.compile(
+ "(meek_lite)(\s)+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+)(\s)+([0-9A-Z]+)(\s)+url=(.+)(\s)+front=(.+)"
+ )
+ snowflake_pattern = re.compile(
+ "(snowflake)(\s)+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+)(\s)+([0-9A-Z]+)"
+ )
+ if (
+ ipv4_pattern.match(bridge)
+ or ipv6_pattern.match(bridge)
+ or meek_lite_pattern.match(bridge)
+ or snowflake_pattern.match(bridge)
+ ):
+ new_bridges.append(bridge)
+ bridges_valid = True
+
+ if bridges_valid:
+ new_bridges = "\n".join(new_bridges) + "\n"
+ settings.set("bridges_custom", new_bridges)
+ else:
+ self.error_label.setText(
+ strings._("gui_settings_tor_bridges_invalid")
+ )
+ return False
+ else:
+ settings.set("bridges_enabled", False)
+
+ return settings
+
+ def closeEvent(self, e):
+ self.common.log("TorSettingsTab", "closeEvent")
+
+ # On close, if Tor isn't connected, then quit OnionShare altogether
+ if not self.common.gui.local_only:
+ if not self.common.gui.onion.is_authenticated():
+ self.common.log(
+ "TorSettingsTab",
+ "closeEvent",
+ "Closing while not connected to Tor",
+ )
+
+ # Wait 1ms for the event loop to finish, then quit
+ QtCore.QTimer.singleShot(1, self.common.gui.qtapp.quit)
+
+ def settings_have_changed(self):
+ # Global settings have changed
+ self.common.log("TorSettingsTab", "settings_have_changed")
diff --git a/desktop/src/onionshare/widgets.py b/desktop/src/onionshare/widgets.py
index b396c43f..761df212 100644
--- a/desktop/src/onionshare/widgets.py
+++ b/desktop/src/onionshare/widgets.py
@@ -37,6 +37,7 @@ class Alert(QtWidgets.QMessageBox):
icon=QtWidgets.QMessageBox.NoIcon,
buttons=QtWidgets.QMessageBox.Ok,
autostart=True,
+ title="OnionShare",
):
super(Alert, self).__init__(None)
@@ -44,7 +45,7 @@ class Alert(QtWidgets.QMessageBox):
self.common.log("Alert", "__init__")
- self.setWindowTitle("OnionShare")
+ self.setWindowTitle(title)
self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png")))
self.setText(message)
self.setIcon(icon)