onionshare/desktop/onionshare/gui_common.py
Miguel Jacq 22b04ed733
Rename 'Advanced settings' to 'Scheduling settings' and move all time-based settings there.
This includes the 'Automatically start this (persistent) onion when OnionShare starts' and 'stop sharing after files have been downloaded'.

Make the 'Custom title' be the first field in all modes, and make sure mode-specific settings are shown after others but before the Scheduling toggle, for a more consistent view of settings for each mode.
2025-02-28 14:25:24 +11:00

644 lines
23 KiB
Python

# -*- coding: utf-8 -*-
"""
OnionShare | https://onionshare.org/
Copyright (C) 2014-2022 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/>.
"""
import importlib.resources as importlib_resources
import os
import shutil
from PySide6 import QtCore, QtWidgets, QtGui
from . import strings
from onionshare_cli.onion import (
Onion,
TorErrorInvalidSetting,
TorErrorAutomatic,
TorErrorSocketPort,
TorErrorSocketFile,
TorErrorMissingPassword,
TorErrorUnreadableCookieFile,
TorErrorAuthError,
TorErrorProtocolError,
BundledTorTimeout,
BundledTorBroken,
TorTooOldEphemeral,
TorTooOldStealth,
PortNotAvailable,
)
from onionshare_cli.meek import Meek
from onionshare_cli.web.web import WaitressException
class GuiCommon:
"""
The shared code for all of the OnionShare GUI.
"""
MODE_SHARE = "share"
MODE_RECEIVE = "receive"
MODE_WEBSITE = "website"
MODE_CHAT = "chat"
def __init__(self, common, qtapp, local_only):
self.common = common
self.qtapp = qtapp
self.local_only = local_only
# Are we running in a flatpak package?
self.is_flatpak = os.path.exists("/.flatpak-info")
# Load settings
self.common.load_settings()
# Load strings
strings.load_strings(self.common, self.get_resource_path("locale"))
# Start the Onion
self.onion = Onion(common, get_tor_paths=self.get_tor_paths)
# Lock filename
self.lock_filename = os.path.join(self.common.build_data_dir(), "lock")
# Events filenames
self.events_dir = os.path.join(self.common.build_data_dir(), "events")
if not os.path.exists(self.events_dir):
os.makedirs(self.events_dir, 0o700, True)
self.events_filename = os.path.join(self.events_dir, "events")
# Instantiate Meek, which is used to bypass censorship
self.meek = Meek(self.common, get_tor_paths=self.get_tor_paths)
self.css = self.get_css(qtapp.color_mode)
self.color_mode = qtapp.color_mode
def get_css(self, color_mode):
header_color = "#4E064F" # purple in light
title_color = "#333333" # dark gray color in main window
stop_button_color = "#d0011b" # red button color for stopping server
new_tab_button_background = "#ffffff"
new_tab_button_border = "#efeff0"
new_tab_button_text_color = "#4e0d4e"
downloads_uploads_progress_bar_border_color = "#4E064F"
downloads_uploads_progress_bar_chunk_color = "#4E064F"
share_zip_progess_bar_border_color = "#4E064F"
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"
stop_button_color = "#C32F2F"
new_tab_button_background = "#5F5F5F"
new_tab_button_border = "#878787"
new_tab_button_text_color = "#FFFFFF"
share_zip_progess_bar_border_color = "#F2F2F2"
history_background_color = "#191919"
history_label_color = "#ffffff"
settings_error_color = "#FF9999"
return {
# OnionShareGui styles
"tab_widget": """
QTabBar::tab { width: 170px; height: 30px; }
""",
"tab_widget_new_tab_button": """
QPushButton {
font-weight: bold;
font-size: 20px;
}""",
"settings_subtab_bar": """
QTabBar::tab {
background: transparent;
}
QTabBar::tab:selected {
border-bottom: 3px solid;
border-color: #4E064F;
padding: 3px
}""",
"mode_new_tab_button": """
QPushButton {
font-weight: bold;
font-size: 30px;
color: #601f61;
}""",
"mode_header_label": """
QLabel {
color: """
+ header_color
+ """;
font-size: 48px;
margin-bottom: 16px;
}""",
"settings_button": """
QPushButton {
border: 0;
border-radius: 0;
}""",
"server_status_indicator_label": """
QLabel {
font-style: italic;
color: #666666;
padding: 2px;
}""",
"status_bar": """
QStatusBar {
font-style: italic;
color: #666666;
}
QStatusBar::item {
border: 0px;
}""",
"autoconnect_start_button": """
QPushButton {
background-color: #5fa416;
color: #ffffff;
padding: 10px;
border: 0;
border-radius: 5px;
}""",
"autoconnect_configure_button": """
QPushButton {
padding: 9px 29px;
color: #3f7fcf;
text-align: left;
}""",
"enable_autoconnect": """
QCheckBox {
margin-top: 30px;
background: #FCFCFC;
color: #000000;
border: 1px solid #DDDBDA;
border-radius: 8px;
padding: 24px 16px;
}
QCheckBox::indicator {
width: 0;
height: 0;
}""",
"autoconnect_countries_combobox": """
QComboBox {
padding: 10px;
font-size: 16px;
margin-left: 32px;
}
QComboBox:disabled {
color: #666666;
}
""",
"autoconnect_task_label": """
QLabel {
font-weight: bold;
}
""",
"autoconnect_failed_to_connect_label": """
QLabel {
font-size: 18px;
font-weight: bold;
}""",
"autoconnect_bridge_setting_options": """
QGroupBox {
border: 0;
border-color: transparent;
background-color: transparent;
font-weight: bold;
margin-top: 16px;
}
QGroupBox::title {
subcontrol-origin: margin;
}""",
# Common styles between modes and their child widgets
"mode_settings_toggle_scheduling": """
QPushButton {
color: #3f7fcf;
text-align: left;
}
""",
"mode_info_label": """
QLabel {
font-size: 12px;
color: #666666;
}
""",
"server_status_url": """
QLabel {
background-color: #ffffff;
color: #000000;
padding: 10px;
border: 1px solid #666666;
font-size: 12px;
}
""",
"server_status_url_buttons": """
QPushButton {
padding: 4px 8px;
text-align: center;
}
""",
"server_status_button_stopped": """
QPushButton {
background-color: #5fa416;
color: #ffffff;
padding: 10px 30px 10px 30px;
border: 0;
border-radius: 5px;
}""",
"server_status_button_working": """
QPushButton {
background-color: #4c8211;
color: #ffffff;
padding: 10px 30px 10px 30px;
border: 0;
border-radius: 5px;
font-style: italic;
}""",
"server_status_button_started": """
QPushButton {
background-color: """
+ stop_button_color
+ """;
color: #ffffff;
padding: 10px 30px 10px 30px;
border: 0;
border-radius: 5px;
}""",
"downloads_uploads_not_empty": """
QWidget{
background-color: """
+ history_background_color
+ """;
}""",
"downloads_uploads_empty": """
QWidget {
background-color: """
+ history_background_color
+ """;
border: 1px solid #999999;
}
QWidget QLabel {
background-color: none;
border: 0px;
}
""",
"downloads_uploads_empty_text": """
QLabel {
color: #999999;
}""",
"downloads_uploads_label": """
QLabel {
font-weight: bold;
font-size 14px;
text-align: center;
background-color: none;
border: none;
}""",
"downloads_uploads_clear": """
QPushButton {
color: #3f7fcf;
}
""",
"download_uploads_indicator": """
QLabel {
color: #ffffff;
background-color: #f44449;
font-weight: bold;
font-size: 10px;
padding: 2px;
border-radius: 7px;
text-align: center;
}""",
"downloads_uploads_progress_bar": """
QProgressBar {
border: 1px solid """
+ downloads_uploads_progress_bar_border_color
+ """;
background-color: #ffffff !important;
text-align: center;
color: #9b9b9b;
font-size: 14px;
}
QProgressBar::chunk {
background-color: """
+ downloads_uploads_progress_bar_chunk_color
+ """;
width: 10px;
}""",
"history_default_label": """
QLabel {
color: """
+ history_label_color
+ """;
}""",
"history_individual_file_timestamp_label": """
QLabel {
color: #666666;
}""",
"history_individual_file_status_code_label_2xx": """
QLabel {
color: #008800;
}""",
"history_individual_file_status_code_label_4xx": """
QLabel {
color: #cc0000;
}""",
"tor_not_connected_label": """
QLabel {
font-size: 16px;
font-style: italic;
}""",
# New tab
"new_tab_button_image": """
QLabel {
padding: 30px;
text-align: center;
}
""",
"new_tab_button_text": """
QLabel {
border: 1px solid """
+ new_tab_button_border
+ """;
border-radius: 4px;
background-color: """
+ new_tab_button_background
+ """;
text-align: center;
color: """
+ new_tab_button_text_color
+ """;
}
""",
"new_tab_title_text": """
QLabel {
text-align: center;
color: """
+ title_color
+ """;
font-size: 25px;
}
""",
# Share mode and child widget styles
"share_delete_all_files_button": """
QPushButton {
color: #3f7fcf;
}
""",
"share_zip_progess_bar": """
QProgressBar {
border: 1px solid """
+ share_zip_progess_bar_border_color
+ """;
background-color: #ffffff !important;
text-align: center;
color: #9b9b9b;
}
QProgressBar::chunk {
border: 0px;
background-color: """
+ share_zip_progess_bar_chunk_color
+ """;
width: 10px;
}""",
"share_filesize_warning": """
QLabel {
padding: 10px 0;
font-weight: bold;
color: """
+ title_color
+ """;
}
""",
"share_file_selection_drop_here_header_label": """
QLabel {
color: """
+ header_color
+ """;
font-size: 48px;
}""",
"share_file_selection_drop_here_label": """
QLabel {
color: #666666;
}""",
"share_file_selection_drop_count_label": """
QLabel {
color: #ffffff;
background-color: #f44449;
font-weight: bold;
padding: 5px 10px;
border-radius: 10px;
}""",
"share_file_list_drag_enter": """
FileList {
border: 3px solid #538ad0;
}
""",
"share_file_list_drag_leave": """
FileList {
border: none;
}
""",
"share_file_list_item_size": """
QLabel {
color: #666666;
font-size: 11px;
}""",
# Receive mode and child widget styles
"receive_file": """
QWidget {
background-color: #ffffff;
}
""",
"receive_file_size": """
QLabel {
color: #666666;
font-size: 11px;
}""",
"receive_message_button": """
QPushButton {
padding: 5px 10px;
}""",
"receive_options": """
QCheckBox:disabled {
color: #666666;
}""",
# Tor Settings dialogs
"tor_settings_error": """
QLabel {
color: """
+ settings_error_color
+ """;
}
""",
}
def get_tor_paths(self):
if self.common.platform == "Linux":
base_path = self.get_resource_path("tor")
if base_path and os.path.isdir(base_path):
self.common.log(
"GuiCommon", "get_tor_paths", "using paths in resources"
)
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
self.common.log("GuiCommon", "get_tor_paths", "using paths from 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.exe")
obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy.exe")
snowflake_file_path = os.path.join(base_path, "snowflake-client.exe")
meek_client_file_path = os.path.join(base_path, "meek-client.exe")
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 == "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":
tor_path = "/usr/local/bin/tor"
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
def get_resource_path(filename):
"""
Returns the absolute path of a resource
"""
try:
ref = importlib_resources.files("onionshare.resources") / filename
with importlib_resources.as_file(ref) as path:
return str(path)
except FileNotFoundError:
return None
@staticmethod
def get_translated_tor_error(e):
"""
Takes an exception defined in onion.py and returns a translated error message
"""
if type(e) is TorErrorInvalidSetting:
return strings._("settings_error_unknown")
elif type(e) is TorErrorAutomatic:
return strings._("settings_error_automatic")
elif type(e) is TorErrorSocketPort:
return strings._("settings_error_socket_port").format(e.args[0], e.args[1])
elif type(e) is TorErrorSocketFile:
return strings._("settings_error_socket_file").format(e.args[0])
elif type(e) is TorErrorMissingPassword:
return strings._("settings_error_missing_password")
elif type(e) is TorErrorUnreadableCookieFile:
return strings._("settings_error_unreadable_cookie_file")
elif type(e) is TorErrorAuthError:
return strings._("settings_error_auth").format(e.args[0], e.args[1])
elif type(e) is TorErrorProtocolError:
return strings._("error_tor_protocol_error").format(e.args[0])
elif type(e) is BundledTorTimeout:
return strings._("settings_error_bundled_tor_timeout")
elif type(e) is BundledTorBroken:
return strings._("settings_error_bundled_tor_broken").format(e.args[0])
elif type(e) is TorTooOldEphemeral:
return strings._("error_ephemeral_not_supported")
elif type(e) is TorTooOldStealth:
return strings._("error_stealth_not_supported")
elif type(e) is PortNotAvailable:
return strings._("error_port_not_available")
return None
@staticmethod
def get_translated_web_error(e):
"""
Takes an exception defined in web.py and returns a translated error message
"""
if type(e) is WaitressException:
return strings._("waitress_web_server_error")
class ToggleCheckbox(QtWidgets.QCheckBox):
def __init__(self, text):
super(ToggleCheckbox, self).__init__(text)
# Set default parameters
self.setCursor(QtCore.Qt.PointingHandCursor)
self.w = 50
self.h = 24
self.bg_color = "#D4D4D4"
self.circle_color = "#BDBDBD"
self.active_color = "#4E0D4E"
self.inactive_color = ""
def hitButton(self, pos):
return self.toggleRect.contains(pos)
def paintEvent(self, e):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtCore.Qt.NoPen)
opt = QtWidgets.QStyleOptionButton()
opt.initFrom(self)
self.initStyleOption(opt)
s = self.style()
s.drawControl(QtWidgets.QStyle.CE_CheckBox, opt, painter, self)
rect = QtCore.QRect(
s.subElementRect(QtWidgets.QStyle.SE_CheckBoxContents, opt, self)
)
x = (
rect.width() - rect.x() - self.w + 20
) # 20 is the padding between text and toggle
y = (
self.height() / 2 - self.h / 2 + 16
) # 16 is the padding top for the checkbox
self.toggleRect = QtCore.QRect(x, y, self.w, self.h)
painter.setBrush(QtGui.QColor(self.bg_color))
painter.drawRoundedRect(x, y, self.w, self.h, self.h / 2, self.h / 2)
if not self.isChecked():
painter.setBrush(QtGui.QColor(self.circle_color))
painter.drawEllipse(x, y - 3, self.h + 6, self.h + 6)
else:
painter.setBrush(QtGui.QColor(self.active_color))
painter.drawEllipse(
x + self.w - (self.h + 6), y - 3, self.h + 6, self.h + 6
)
painter.end()