From 9e434abd9c962e222e07dbb31075bf86cb947f4f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 18:59:27 -0700 Subject: [PATCH 001/135] Add psutil dependency --- BUILD.md | 4 ++-- install/requirements.txt | 1 + stdeb.cfg | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/BUILD.md b/BUILD.md index 9456e617..7df56466 100644 --- a/BUILD.md +++ b/BUILD.md @@ -14,13 +14,13 @@ Install the needed dependencies: For Debian-like distros: ``` -apt install -y python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python-nautilus tor obfs4proxy python3-pytest build-essential fakeroot python3-all python3-stdeb dh-python python3-flask-httpauth python3-distutils +apt install -y python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python-nautilus tor obfs4proxy python3-pytest build-essential fakeroot python3-all python3-stdeb dh-python python3-flask-httpauth python3-distutils python3-psutil ``` For Fedora-like distros: ``` -dnf install -y python3-flask python3-flask-httpauth python3-stem python3-qt5 python3-crypto python3-pysocks nautilus-python tor obfs4 python3-pytest rpm-build +dnf install -y python3-flask python3-flask-httpauth python3-stem python3-qt5 python3-crypto python3-pysocks nautilus-python tor obfs4 python3-pytest rpm-build python3-psutil ``` After that you can try both the CLI and the GUI version of OnionShare: diff --git a/install/requirements.txt b/install/requirements.txt index 36b9fa4f..729456fe 100644 --- a/install/requirements.txt +++ b/install/requirements.txt @@ -11,6 +11,7 @@ Jinja2==2.10.1 macholib==1.11 MarkupSafe==1.1.1 pefile==2019.4.18 +psutil==5.6.3 pycryptodome==3.9.0 PyInstaller==3.5 PyQt5==5.13.1 diff --git a/stdeb.cfg b/stdeb.cfg index 51ff9a0c..96fa3ba4 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -1,6 +1,6 @@ [DEFAULT] Package3: onionshare -Depends3: python3, python3-flask, python3-flask-httpauth, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python3-distutils, python-nautilus, tor, obfs4proxy -Build-Depends: python3, python3-all, python3-pytest, python3-requests, python3-flask, python3-flask-httpauth, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python3-distutils +Depends3: python3, python3-flask, python3-flask-httpauth, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python3-distutils, python-nautilus, tor, obfs4proxy, python3-psutil +Build-Depends: python3, python3-all, python3-pytest, python3-requests, python3-flask, python3-flask-httpauth, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python3-distutils, python3-psutil Suite: disco X-Python3-Version: >= 3.6 From 30949cf1960c5c662feba8aa5a4c0d1f67549878 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 19:18:56 -0700 Subject: [PATCH 002/135] Detect if another onionshare-gui process is running --- onionshare_gui/__init__.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 73e0d305..4a4578d3 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -23,13 +23,14 @@ import sys import platform import argparse import signal -from .widgets import Alert +import psutil from PyQt5 import QtCore, QtWidgets from onionshare.common import Common from onionshare.onion import Onion from onionshare.onionshare import OnionShare +from .widgets import Alert from .onionshare_gui import OnionShareGui @@ -132,6 +133,30 @@ def main(): if not valid: sys.exit() + # Is there another onionshare-gui running? + existing_pid = None + for proc in psutil.process_iter(attrs=["pid", "name", "cmdline"]): + if proc.info["pid"] == os.getpid(): + continue + + if proc.info["name"] == "onionshare-gui": + existing_pid = proc.info["pid"] + break + else: + # Dev mode onionshare? + if proc.info["cmdline"] and len(proc.info["cmdline"]) >= 2: + if ( + os.path.basename(proc.info["cmdline"][0]).lower() == "python" + and os.path.basename(proc.info["cmdline"][1]) == "onionshare-gui" + ): + existing_pid = proc.info["pid"] + break + + if existing_pid: + print(f"Opening tab in existing OnionShare window (pid {proc.info['pid']})") + # TODO: open tab + return + # Start the Onion onion = Onion(common) From 4f2ce994172b6bd2b22a44b22d35ce1922e41d8c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 19:41:20 -0700 Subject: [PATCH 003/135] Rename OnionShareGui to MainWindow --- onionshare_gui/__init__.py | 4 +-- .../{onionshare_gui.py => main_window.py} | 28 +++++++++---------- tests/GuiBaseTest.py | 4 +-- tests/GuiWebsiteTest.py | 4 +-- tests/TorGuiBaseTest.py | 4 +-- 5 files changed, 22 insertions(+), 22 deletions(-) rename onionshare_gui/{onionshare_gui.py => main_window.py} (97%) diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 4a4578d3..68f5d863 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -31,7 +31,7 @@ from onionshare.onion import Onion from onionshare.onionshare import OnionShare from .widgets import Alert -from .onionshare_gui import OnionShareGui +from .main_window import MainWindow class Application(QtWidgets.QApplication): @@ -164,7 +164,7 @@ def main(): app = OnionShare(common, onion, local_only) # Launch the gui - gui = OnionShareGui(common, onion, qtapp, app, filenames, config, local_only) + gui = MainWindow(common, onion, qtapp, app, filenames, config, local_only) # Clean up when app quits def shutdown(): diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/main_window.py similarity index 97% rename from onionshare_gui/onionshare_gui.py rename to onionshare_gui/main_window.py index bb206ec6..ae030e2d 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/main_window.py @@ -34,9 +34,9 @@ from .update_checker import UpdateThread from .server_status import ServerStatus -class OnionShareGui(QtWidgets.QMainWindow): +class MainWindow(QtWidgets.QMainWindow): """ - OnionShareGui is the main window for the GUI that contains all of the + MainWindow is the main window for the GUI that contains all of the GUI elements. """ @@ -47,10 +47,10 @@ class OnionShareGui(QtWidgets.QMainWindow): def __init__( self, common, onion, qtapp, app, filenames, config=False, local_only=False ): - super(OnionShareGui, self).__init__() + super(MainWindow, self).__init__() self.common = common - self.common.log("OnionShareGui", "__init__") + self.common.log("MainWindow", "__init__") self.setMinimumWidth(820) self.setMinimumHeight(660) @@ -344,19 +344,19 @@ class OnionShareGui(QtWidgets.QMainWindow): def share_mode_clicked(self): if self.mode != self.MODE_SHARE: - self.common.log("OnionShareGui", "share_mode_clicked") + self.common.log("MainWindow", "share_mode_clicked") self.mode = self.MODE_SHARE self.update_mode_switcher() def receive_mode_clicked(self): if self.mode != self.MODE_RECEIVE: - self.common.log("OnionShareGui", "receive_mode_clicked") + self.common.log("MainWindow", "receive_mode_clicked") self.mode = self.MODE_RECEIVE self.update_mode_switcher() def website_mode_clicked(self): if self.mode != self.MODE_WEBSITE: - self.common.log("OnionShareGui", "website_mode_clicked") + self.common.log("MainWindow", "website_mode_clicked") self.mode = self.MODE_WEBSITE self.update_mode_switcher() @@ -451,7 +451,7 @@ class OnionShareGui(QtWidgets.QMainWindow): If the user cancels before Tor finishes connecting, ask if they want to quit, or open settings. """ - self.common.log("OnionShareGui", "_tor_connection_canceled") + self.common.log("MainWindow", "_tor_connection_canceled") def ask(): a = Alert( @@ -497,7 +497,7 @@ class OnionShareGui(QtWidgets.QMainWindow): """ The TorConnectionDialog wants to open the Settings dialog """ - self.common.log("OnionShareGui", "_tor_connection_open_settings") + self.common.log("MainWindow", "_tor_connection_open_settings") # Wait 1ms for the event loop to finish closing the TorConnectionDialog QtCore.QTimer.singleShot(1, self.open_settings) @@ -506,7 +506,7 @@ class OnionShareGui(QtWidgets.QMainWindow): """ Open the SettingsDialog. """ - self.common.log("OnionShareGui", "open_settings") + self.common.log("MainWindow", "open_settings") def reload_settings(): self.common.log( @@ -674,7 +674,7 @@ class OnionShareGui(QtWidgets.QMainWindow): """ When the URL gets copied to the clipboard, display this in the status bar. """ - self.common.log("OnionShareGui", "copy_url") + self.common.log("MainWindow", "copy_url") self.system_tray.showMessage( strings._("gui_copied_url_title"), strings._("gui_copied_url") ) @@ -683,7 +683,7 @@ class OnionShareGui(QtWidgets.QMainWindow): """ When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar. """ - self.common.log("OnionShareGui", "copy_hidservauth") + self.common.log("MainWindow", "copy_hidservauth") self.system_tray.showMessage( strings._("gui_copied_hidservauth_title"), strings._("gui_copied_hidservauth"), @@ -723,7 +723,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.settings_action.setEnabled(not active) def closeEvent(self, e): - self.common.log("OnionShareGui", "closeEvent") + self.common.log("MainWindow", "closeEvent") self.system_tray.hide() try: if self.mode == OnionShareGui.MODE_SHARE: @@ -733,7 +733,7 @@ class OnionShareGui(QtWidgets.QMainWindow): else: server_status = self.receive_mode.server_status if server_status.status != server_status.STATUS_STOPPED: - self.common.log("OnionShareGui", "closeEvent, opening warning dialog") + self.common.log("MainWindow", "closeEvent, opening warning dialog") dialog = QtWidgets.QMessageBox() dialog.setWindowTitle(strings._("gui_quit_title")) if self.mode == OnionShareGui.MODE_SHARE: diff --git a/tests/GuiBaseTest.py b/tests/GuiBaseTest.py index daa7cb09..068bb5c5 100644 --- a/tests/GuiBaseTest.py +++ b/tests/GuiBaseTest.py @@ -11,7 +11,7 @@ from onionshare.common import Common from onionshare.settings import Settings from onionshare.onion import Onion from onionshare.web import Web -from onionshare_gui import Application, OnionShare, OnionShareGui +from onionshare_gui import Application, OnionShare, MainWindow from onionshare_gui.mode.share_mode import ShareMode from onionshare_gui.mode.receive_mode import ReceiveMode from onionshare_gui.mode.website_mode import WebsiteMode @@ -53,7 +53,7 @@ class GuiBaseTest(object): web = Web(common, False, True) open("/tmp/settings.json", "w").write(json.dumps(test_settings)) - gui = OnionShareGui( + gui = MainWindow( common, testonion, qtapp, diff --git a/tests/GuiWebsiteTest.py b/tests/GuiWebsiteTest.py index 79b44e2e..8c733442 100644 --- a/tests/GuiWebsiteTest.py +++ b/tests/GuiWebsiteTest.py @@ -10,7 +10,7 @@ from onionshare.common import Common from onionshare.settings import Settings from onionshare.onion import Onion from onionshare.web import Web -from onionshare_gui import Application, OnionShare, OnionShareGui +from onionshare_gui import Application, OnionShare, MainWindow from .GuiShareTest import GuiShareTest @@ -45,7 +45,7 @@ class GuiWebsiteTest(GuiShareTest): web = Web(common, False, True) open("/tmp/settings.json", "w").write(json.dumps(test_settings)) - gui = OnionShareGui( + gui = MainWindow( common, testonion, qtapp, diff --git a/tests/TorGuiBaseTest.py b/tests/TorGuiBaseTest.py index 611d3efa..ab5ed508 100644 --- a/tests/TorGuiBaseTest.py +++ b/tests/TorGuiBaseTest.py @@ -10,7 +10,7 @@ from onionshare.common import Common from onionshare.settings import Settings from onionshare.onion import Onion from onionshare.web import Web -from onionshare_gui import Application, OnionShare, OnionShareGui +from onionshare_gui import Application, OnionShare, MainWindow from onionshare_gui.mode.share_mode import ShareMode from onionshare_gui.mode.receive_mode import ReceiveMode @@ -54,7 +54,7 @@ class TorGuiBaseTest(GuiBaseTest): web = Web(common, False, False) open("/tmp/settings.json", "w").write(json.dumps(test_settings)) - gui = OnionShareGui( + gui = MainWindow( common, testonion, qtapp, From bba52868151068097cc86f3aea48026f48f13a04 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 20:01:09 -0700 Subject: [PATCH 004/135] Make a new onionshare_gui.GuiCommon object, and move css from onionshare.Common into it --- onionshare/common.py | 233 +---------------- onionshare_gui/__init__.py | 3 +- onionshare_gui/gui_common.py | 254 +++++++++++++++++++ onionshare_gui/main_window.py | 27 +- onionshare_gui/mode/file_selection.py | 14 +- onionshare_gui/mode/history.py | 36 +-- onionshare_gui/mode/share_mode/__init__.py | 6 +- onionshare_gui/mode/website_mode/__init__.py | 4 +- onionshare_gui/server_status.py | 16 +- onionshare_gui/settings_dialog.py | 20 +- 10 files changed, 323 insertions(+), 290 deletions(-) create mode 100644 onionshare_gui/gui_common.py diff --git a/onionshare/common.py b/onionshare/common.py index d97f0ccb..ac79f43b 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -32,7 +32,7 @@ import time from .settings import Settings -class Common(object): +class Common: """ The Common object is shared amongst all parts of OnionShare. """ @@ -187,237 +187,6 @@ class Common(object): r = random.SystemRandom() return "-".join(r.choice(wordlist) for _ in range(2)) - def define_css(self): - """ - This defines all of the stylesheets used in GUI mode, to avoid repeating code. - This method is only called in GUI mode. - """ - self.css = { - # OnionShareGui styles - "mode_switcher_selected_style": """ - QPushButton { - color: #ffffff; - background-color: #4e064f; - border: 0; - border-right: 1px solid #69266b; - font-weight: bold; - border-radius: 0; - }""", - "mode_switcher_unselected_style": """ - QPushButton { - color: #ffffff; - background-color: #601f61; - border: 0; - font-weight: normal; - border-radius: 0; - }""", - "settings_button": """ - QPushButton { - background-color: #601f61; - border: 0; - border-left: 1px solid #69266b; - 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; - }""", - # Common styles between modes and their child widgets - "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 { - color: #3f7fcf; - } - """, - "server_status_button_stopped": """ - QPushButton { - background-color: #5fa416; - color: #ffffff; - padding: 10px; - border: 0; - border-radius: 5px; - }""", - "server_status_button_working": """ - QPushButton { - background-color: #4c8211; - color: #ffffff; - padding: 10px; - border: 0; - border-radius: 5px; - font-style: italic; - }""", - "server_status_button_started": """ - QPushButton { - background-color: #d0011b; - color: #ffffff; - padding: 10px; - border: 0; - border-radius: 5px; - }""", - "downloads_uploads_empty": """ - QWidget { - background-color: #ffffff; - 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 #4e064f; - background-color: #ffffff !important; - text-align: center; - color: #9b9b9b; - font-size: 14px; - } - QProgressBar::chunk { - background-color: #4e064f; - width: 10px; - }""", - "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; - }""", - # Share mode and child widget styles - "share_zip_progess_bar": """ - QProgressBar { - border: 1px solid #4e064f; - background-color: #ffffff !important; - text-align: center; - color: #9b9b9b; - } - QProgressBar::chunk { - border: 0px; - background-color: #4e064f; - width: 10px; - }""", - "share_filesize_warning": """ - QLabel { - padding: 10px 0; - font-weight: bold; - color: #333333; - } - """, - "share_file_selection_drop_here_label": """ - QLabel { - color: #999999; - }""", - "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; - }""", - # 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": """ - QLabel { - font-style: italic; - }""", - } - @staticmethod def random_string(num_bytes, output_len=None): """ diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 68f5d863..0dff4229 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -30,6 +30,7 @@ from onionshare.common import Common from onionshare.onion import Onion from onionshare.onionshare import OnionShare +from .gui_common import GuiCommon from .widgets import Alert from .main_window import MainWindow @@ -61,7 +62,7 @@ def main(): The main() function implements all of the logic that the GUI version of onionshare uses. """ common = Common() - common.define_css() + common.gui = GuiCommon(common) # Display OnionShare banner print(f"OnionShare {common.version} | https://onionshare.org/") diff --git a/onionshare_gui/gui_common.py b/onionshare_gui/gui_common.py new file mode 100644 index 00000000..fac17bd1 --- /dev/null +++ b/onionshare_gui/gui_common.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +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 . +""" + + +class GuiCommon: + """ + The shared code for all of the OnionShare GUI. + """ + + def __init__(self, common): + self.common = common + + self.css = { + # OnionShareGui styles + "mode_switcher_selected_style": """ + QPushButton { + color: #ffffff; + background-color: #4e064f; + border: 0; + border-right: 1px solid #69266b; + font-weight: bold; + border-radius: 0; + }""", + "mode_switcher_unselected_style": """ + QPushButton { + color: #ffffff; + background-color: #601f61; + border: 0; + font-weight: normal; + border-radius: 0; + }""", + "settings_button": """ + QPushButton { + background-color: #601f61; + border: 0; + border-left: 1px solid #69266b; + 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; + }""", + # Common styles between modes and their child widgets + "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 { + color: #3f7fcf; + } + """, + "server_status_button_stopped": """ + QPushButton { + background-color: #5fa416; + color: #ffffff; + padding: 10px; + border: 0; + border-radius: 5px; + }""", + "server_status_button_working": """ + QPushButton { + background-color: #4c8211; + color: #ffffff; + padding: 10px; + border: 0; + border-radius: 5px; + font-style: italic; + }""", + "server_status_button_started": """ + QPushButton { + background-color: #d0011b; + color: #ffffff; + padding: 10px; + border: 0; + border-radius: 5px; + }""", + "downloads_uploads_empty": """ + QWidget { + background-color: #ffffff; + 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 #4e064f; + background-color: #ffffff !important; + text-align: center; + color: #9b9b9b; + font-size: 14px; + } + QProgressBar::chunk { + background-color: #4e064f; + width: 10px; + }""", + "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; + }""", + # Share mode and child widget styles + "share_zip_progess_bar": """ + QProgressBar { + border: 1px solid #4e064f; + background-color: #ffffff !important; + text-align: center; + color: #9b9b9b; + } + QProgressBar::chunk { + border: 0px; + background-color: #4e064f; + width: 10px; + }""", + "share_filesize_warning": """ + QLabel { + padding: 10px 0; + font-weight: bold; + color: #333333; + } + """, + "share_file_selection_drop_here_label": """ + QLabel { + color: #999999; + }""", + "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; + }""", + # 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": """ + QLabel { + font-style: italic; + }""", + } diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index ae030e2d..4c26e118 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -36,8 +36,7 @@ from .server_status import ServerStatus class MainWindow(QtWidgets.QMainWindow): """ - MainWindow is the main window for the GUI that contains all of the - GUI elements. + MainWindow is the OnionShare main window, which contains the GUI elements, including all open tabs """ MODE_SHARE = "share" @@ -121,7 +120,7 @@ class MainWindow(QtWidgets.QMainWindow): QtGui.QIcon(self.common.get_resource_path("images/settings.png")) ) self.settings_button.clicked.connect(self.open_settings) - self.settings_button.setStyleSheet(self.common.css["settings_button"]) + self.settings_button.setStyleSheet(self.common.gui.css["settings_button"]) mode_switcher_layout = QtWidgets.QHBoxLayout() mode_switcher_layout.setSpacing(0) mode_switcher_layout.addWidget(self.share_mode_button) @@ -143,7 +142,7 @@ class MainWindow(QtWidgets.QMainWindow): self.server_status_image_label.setFixedWidth(20) self.server_status_label = QtWidgets.QLabel("") self.server_status_label.setStyleSheet( - self.common.css["server_status_indicator_label"] + self.common.gui.css["server_status_indicator_label"] ) server_status_indicator_layout = QtWidgets.QHBoxLayout() server_status_indicator_layout.addWidget(self.server_status_image_label) @@ -154,7 +153,7 @@ class MainWindow(QtWidgets.QMainWindow): # Status bar self.status_bar = QtWidgets.QStatusBar() self.status_bar.setSizeGripEnabled(False) - self.status_bar.setStyleSheet(self.common.css["status_bar"]) + self.status_bar.setStyleSheet(self.common.gui.css["status_bar"]) self.status_bar.addPermanentWidget(self.server_status_indicator) self.setStatusBar(self.status_bar) @@ -299,13 +298,13 @@ class MainWindow(QtWidgets.QMainWindow): # and show and hide widgets to switch modes if self.mode == self.MODE_SHARE: self.share_mode_button.setStyleSheet( - self.common.css["mode_switcher_selected_style"] + self.common.gui.css["mode_switcher_selected_style"] ) self.receive_mode_button.setStyleSheet( - self.common.css["mode_switcher_unselected_style"] + self.common.gui.css["mode_switcher_unselected_style"] ) self.website_mode_button.setStyleSheet( - self.common.css["mode_switcher_unselected_style"] + self.common.gui.css["mode_switcher_unselected_style"] ) self.receive_mode.hide() @@ -313,13 +312,13 @@ class MainWindow(QtWidgets.QMainWindow): self.website_mode.hide() elif self.mode == self.MODE_WEBSITE: self.share_mode_button.setStyleSheet( - self.common.css["mode_switcher_unselected_style"] + self.common.gui.css["mode_switcher_unselected_style"] ) self.receive_mode_button.setStyleSheet( - self.common.css["mode_switcher_unselected_style"] + self.common.gui.css["mode_switcher_unselected_style"] ) self.website_mode_button.setStyleSheet( - self.common.css["mode_switcher_selected_style"] + self.common.gui.css["mode_switcher_selected_style"] ) self.receive_mode.hide() @@ -327,13 +326,13 @@ class MainWindow(QtWidgets.QMainWindow): self.website_mode.show() else: self.share_mode_button.setStyleSheet( - self.common.css["mode_switcher_unselected_style"] + self.common.gui.css["mode_switcher_unselected_style"] ) self.receive_mode_button.setStyleSheet( - self.common.css["mode_switcher_selected_style"] + self.common.gui.css["mode_switcher_selected_style"] ) self.website_mode_button.setStyleSheet( - self.common.css["mode_switcher_unselected_style"] + self.common.gui.css["mode_switcher_unselected_style"] ) self.share_mode.hide() diff --git a/onionshare_gui/mode/file_selection.py b/onionshare_gui/mode/file_selection.py index 62cff0a7..7a9cf5e4 100644 --- a/onionshare_gui/mode/file_selection.py +++ b/onionshare_gui/mode/file_selection.py @@ -50,7 +50,9 @@ class DropHereLabel(QtWidgets.QLabel): ) else: self.setText(strings._("gui_drag_and_drop")) - self.setStyleSheet(self.common.css["share_file_selection_drop_here_label"]) + self.setStyleSheet( + self.common.gui.css["share_file_selection_drop_here_label"] + ) self.hide() @@ -75,7 +77,7 @@ class DropCountLabel(QtWidgets.QLabel): self.setAcceptDrops(True) self.setAlignment(QtCore.Qt.AlignCenter) self.setText(strings._("gui_drag_and_drop")) - self.setStyleSheet(self.common.css["share_file_selection_drop_count_label"]) + self.setStyleSheet(self.common.gui.css["share_file_selection_drop_count_label"]) self.hide() def dragEnterEvent(self, event): @@ -169,7 +171,7 @@ class FileList(QtWidgets.QListWidget): dragEnterEvent for dragging files and directories into the widget. """ if event.mimeData().hasUrls: - self.setStyleSheet(self.common.css["share_file_list_drag_enter"]) + self.setStyleSheet(self.common.gui.css["share_file_list_drag_enter"]) count = len(event.mimeData().urls()) self.drop_count.setText(f"+{count}") @@ -189,7 +191,7 @@ class FileList(QtWidgets.QListWidget): """ dragLeaveEvent for dragging files and directories into the widget. """ - self.setStyleSheet(self.common.css["share_file_list_drag_leave"]) + self.setStyleSheet(self.common.gui.css["share_file_list_drag_leave"]) self.drop_count.hide() event.accept() self.update() @@ -217,7 +219,7 @@ class FileList(QtWidgets.QListWidget): else: event.ignore() - self.setStyleSheet(self.common.css["share_file_list_drag_leave"]) + self.setStyleSheet(self.common.gui.css["share_file_list_drag_leave"]) self.drop_count.hide() self.files_dropped.emit() @@ -254,7 +256,7 @@ class FileList(QtWidgets.QListWidget): # Item's filename attribute and size labels item.filename = filename item_size = QtWidgets.QLabel(size_readable) - item_size.setStyleSheet(self.common.css["share_file_list_item_size"]) + item_size.setStyleSheet(self.common.gui.css["share_file_list_item_size"]) item.basename = os.path.basename(filename.rstrip("/")) # Use the basename as the method with which to sort the list diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index 67b23072..a9a46841 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -122,7 +122,7 @@ class ShareHistoryItem(HistoryItem): self.progress_bar.setMaximum(total_bytes) self.progress_bar.setValue(0) self.progress_bar.setStyleSheet( - self.common.css["downloads_uploads_progress_bar"] + self.common.gui.css["downloads_uploads_progress_bar"] ) self.progress_bar.total_bytes = total_bytes @@ -193,7 +193,7 @@ class ReceiveHistoryItemFile(QtWidgets.QWidget): # File size label self.filesize_label = QtWidgets.QLabel() - self.filesize_label.setStyleSheet(self.common.css["receive_file_size"]) + self.filesize_label.setStyleSheet(self.common.gui.css["receive_file_size"]) self.filesize_label.hide() # Folder button @@ -290,14 +290,14 @@ class ReceiveHistoryItem(HistoryItem): self.progress_bar.setMinimum(0) self.progress_bar.setValue(0) self.progress_bar.setStyleSheet( - self.common.css["downloads_uploads_progress_bar"] + self.common.gui.css["downloads_uploads_progress_bar"] ) # This layout contains file widgets self.files_layout = QtWidgets.QVBoxLayout() self.files_layout.setContentsMargins(0, 0, 0, 0) files_widget = QtWidgets.QWidget() - files_widget.setStyleSheet(self.common.css["receive_file"]) + files_widget.setStyleSheet(self.common.gui.css["receive_file"]) files_widget.setLayout(self.files_layout) # Layout @@ -405,7 +405,7 @@ class IndividualFileHistoryItem(HistoryItem): self.started_dt.strftime("%b %d, %I:%M%p") ) self.timestamp_label.setStyleSheet( - self.common.css["history_individual_file_timestamp_label"] + self.common.gui.css["history_individual_file_timestamp_label"] ) self.path_label = QtWidgets.QLabel(self.path) self.status_code_label = QtWidgets.QLabel() @@ -417,7 +417,7 @@ class IndividualFileHistoryItem(HistoryItem): self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter) self.progress_bar.setValue(0) self.progress_bar.setStyleSheet( - self.common.css["downloads_uploads_progress_bar"] + self.common.gui.css["downloads_uploads_progress_bar"] ) # Text layout @@ -438,11 +438,11 @@ class IndividualFileHistoryItem(HistoryItem): self.status_code_label.setText(str(data["status_code"])) if data["status_code"] >= 200 and data["status_code"] < 300: self.status_code_label.setStyleSheet( - self.common.css["history_individual_file_status_code_label_2xx"] + self.common.gui.css["history_individual_file_status_code_label_2xx"] ) if data["status_code"] >= 400 and data["status_code"] < 500: self.status_code_label.setStyleSheet( - self.common.css["history_individual_file_status_code_label_4xx"] + self.common.gui.css["history_individual_file_status_code_label_4xx"] ) self.status = HistoryItem.STATUS_FINISHED self.progress_bar.hide() @@ -464,7 +464,7 @@ class IndividualFileHistoryItem(HistoryItem): if downloaded_bytes == self.progress_bar.total_bytes: self.status_code_label.setText("200") self.status_code_label.setStyleSheet( - self.common.css["history_individual_file_status_code_label_2xx"] + self.common.gui.css["history_individual_file_status_code_label_2xx"] ) self.progress_bar.hide() self.status = HistoryItem.STATUS_FINISHED @@ -586,19 +586,19 @@ class History(QtWidgets.QWidget): # In progress, completed, and requests labels self.in_progress_label = QtWidgets.QLabel() - self.in_progress_label.setStyleSheet(self.common.css["mode_info_label"]) + self.in_progress_label.setStyleSheet(self.common.gui.css["mode_info_label"]) self.completed_label = QtWidgets.QLabel() - self.completed_label.setStyleSheet(self.common.css["mode_info_label"]) + self.completed_label.setStyleSheet(self.common.gui.css["mode_info_label"]) self.requests_label = QtWidgets.QLabel() - self.requests_label.setStyleSheet(self.common.css["mode_info_label"]) + self.requests_label.setStyleSheet(self.common.gui.css["mode_info_label"]) # Header self.header_label = QtWidgets.QLabel(header_text) - self.header_label.setStyleSheet(self.common.css["downloads_uploads_label"]) + self.header_label.setStyleSheet(self.common.gui.css["downloads_uploads_label"]) self.clear_button = QtWidgets.QPushButton( strings._("gui_all_modes_clear_history") ) - self.clear_button.setStyleSheet(self.common.css["downloads_uploads_clear"]) + self.clear_button.setStyleSheet(self.common.gui.css["downloads_uploads_clear"]) self.clear_button.setFlat(True) self.clear_button.clicked.connect(self.reset) header_layout = QtWidgets.QHBoxLayout() @@ -615,14 +615,16 @@ class History(QtWidgets.QWidget): self.empty_image.setPixmap(empty_image) self.empty_text = QtWidgets.QLabel(empty_text) self.empty_text.setAlignment(QtCore.Qt.AlignCenter) - self.empty_text.setStyleSheet(self.common.css["downloads_uploads_empty_text"]) + self.empty_text.setStyleSheet( + self.common.gui.css["downloads_uploads_empty_text"] + ) empty_layout = QtWidgets.QVBoxLayout() empty_layout.addStretch() empty_layout.addWidget(self.empty_image) empty_layout.addWidget(self.empty_text) empty_layout.addStretch() self.empty = QtWidgets.QWidget() - self.empty.setStyleSheet(self.common.css["downloads_uploads_empty"]) + self.empty.setStyleSheet(self.common.gui.css["downloads_uploads_empty"]) self.empty.setLayout(empty_layout) # When there are items @@ -759,7 +761,7 @@ class ToggleHistory(QtWidgets.QPushButton): self.indicator_count = 0 self.indicator_label = QtWidgets.QLabel(parent=self) self.indicator_label.setStyleSheet( - self.common.css["download_uploads_indicator"] + self.common.gui.css["download_uploads_indicator"] ) self.update_indicator() diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index d0cc6a04..c2ba1338 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -69,7 +69,9 @@ class ShareMode(Mode): # Filesize warning self.filesize_warning = QtWidgets.QLabel() self.filesize_warning.setWordWrap(True) - self.filesize_warning.setStyleSheet(self.common.css["share_filesize_warning"]) + self.filesize_warning.setStyleSheet( + self.common.gui.css["share_filesize_warning"] + ) self.filesize_warning.hide() # Download history @@ -372,7 +374,7 @@ class ZipProgressBar(QtWidgets.QProgressBar): self.setMinimumWidth(200) self.setValue(0) self.setFormat(strings._("zip_progress_bar_format")) - self.setStyleSheet(self.common.css["share_zip_progess_bar"]) + self.setStyleSheet(self.common.gui.css["share_zip_progess_bar"]) self._total_files_size = total_files_size self._processed_size = 0 diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/mode/website_mode/__init__.py index 8cd2eca6..a9ddef99 100644 --- a/onionshare_gui/mode/website_mode/__init__.py +++ b/onionshare_gui/mode/website_mode/__init__.py @@ -71,7 +71,9 @@ class WebsiteMode(Mode): # Filesize warning self.filesize_warning = QtWidgets.QLabel() self.filesize_warning.setWordWrap(True) - self.filesize_warning.setStyleSheet(self.common.css["share_filesize_warning"]) + self.filesize_warning.setStyleSheet( + self.common.gui.css["share_filesize_warning"] + ) self.filesize_warning.hide() # Download history diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 5732ce91..f1ec4559 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -151,11 +151,13 @@ class ServerStatus(QtWidgets.QWidget): self.url.setFont(url_font) self.url.setWordWrap(True) self.url.setMinimumSize(self.url.sizeHint()) - self.url.setStyleSheet(self.common.css["server_status_url"]) + self.url.setStyleSheet(self.common.gui.css["server_status_url"]) self.copy_url_button = QtWidgets.QPushButton(strings._("gui_copy_url")) self.copy_url_button.setFlat(True) - self.copy_url_button.setStyleSheet(self.common.css["server_status_url_buttons"]) + self.copy_url_button.setStyleSheet( + self.common.gui.css["server_status_url_buttons"] + ) self.copy_url_button.setMinimumHeight(65) self.copy_url_button.clicked.connect(self.copy_url) self.copy_hidservauth_button = QtWidgets.QPushButton( @@ -163,7 +165,7 @@ class ServerStatus(QtWidgets.QWidget): ) self.copy_hidservauth_button.setFlat(True) self.copy_hidservauth_button.setStyleSheet( - self.common.css["server_status_url_buttons"] + self.common.gui.css["server_status_url_buttons"] ) self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth) url_buttons_layout = QtWidgets.QHBoxLayout() @@ -329,7 +331,7 @@ class ServerStatus(QtWidgets.QWidget): if self.status == self.STATUS_STOPPED: self.server_button.setStyleSheet( - self.common.css["server_status_button_stopped"] + self.common.gui.css["server_status_button_stopped"] ) self.server_button.setEnabled(True) if self.mode == ServerStatus.MODE_SHARE: @@ -345,7 +347,7 @@ class ServerStatus(QtWidgets.QWidget): self.autostop_timer_container.show() elif self.status == self.STATUS_STARTED: self.server_button.setStyleSheet( - self.common.css["server_status_button_started"] + self.common.gui.css["server_status_button_started"] ) self.server_button.setEnabled(True) if self.mode == ServerStatus.MODE_SHARE: @@ -367,7 +369,7 @@ class ServerStatus(QtWidgets.QWidget): ) elif self.status == self.STATUS_WORKING: self.server_button.setStyleSheet( - self.common.css["server_status_button_working"] + self.common.gui.css["server_status_button_working"] ) self.server_button.setEnabled(True) if self.autostart_timer_datetime: @@ -385,7 +387,7 @@ class ServerStatus(QtWidgets.QWidget): self.autostop_timer_container.hide() else: self.server_button.setStyleSheet( - self.common.css["server_status_button_working"] + self.common.gui.css["server_status_button_working"] ) self.server_button.setEnabled(False) self.server_button.setText(strings._("gui_please_wait")) diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 4c03bd23..c285b4e6 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -76,7 +76,7 @@ class SettingsDialog(QtWidgets.QDialog): "https://github.com/micahflee/onionshare/wiki/Public-Mode" ) ) - public_mode_label.setStyleSheet(self.common.css["settings_whats_this"]) + public_mode_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) public_mode_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) public_mode_label.setOpenExternalLinks(True) public_mode_label.setMinimumSize(public_mode_label.sizeHint()) @@ -99,7 +99,7 @@ class SettingsDialog(QtWidgets.QDialog): "https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Start-Timer" ) ) - autostart_timer_label.setStyleSheet(self.common.css["settings_whats_this"]) + autostart_timer_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) autostart_timer_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) autostart_timer_label.setOpenExternalLinks(True) autostart_timer_label.setMinimumSize(public_mode_label.sizeHint()) @@ -122,7 +122,7 @@ class SettingsDialog(QtWidgets.QDialog): "https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Stop-Timer" ) ) - autostop_timer_label.setStyleSheet(self.common.css["settings_whats_this"]) + autostop_timer_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) autostop_timer_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) autostop_timer_label.setOpenExternalLinks(True) autostop_timer_label.setMinimumSize(public_mode_label.sizeHint()) @@ -149,7 +149,7 @@ class SettingsDialog(QtWidgets.QDialog): strings._("gui_connect_to_tor_for_onion_settings") ) self.connect_to_tor_label.setStyleSheet( - self.common.css["settings_connect_to_tor"] + self.common.gui.css["settings_connect_to_tor"] ) # Whether or not to save the Onion private key for reuse (persistent URL mode) @@ -163,7 +163,7 @@ class SettingsDialog(QtWidgets.QDialog): "https://github.com/micahflee/onionshare/wiki/Using-a-Persistent-URL" ) ) - save_private_key_label.setStyleSheet(self.common.css["settings_whats_this"]) + save_private_key_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) save_private_key_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) save_private_key_label.setOpenExternalLinks(True) save_private_key_layout = QtWidgets.QHBoxLayout() @@ -188,7 +188,7 @@ class SettingsDialog(QtWidgets.QDialog): "https://github.com/micahflee/onionshare/wiki/Legacy-Addresses" ) ) - use_legacy_v2_onions_label.setStyleSheet(self.common.css["settings_whats_this"]) + use_legacy_v2_onions_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) use_legacy_v2_onions_label.setTextInteractionFlags( QtCore.Qt.TextBrowserInteraction ) @@ -211,7 +211,7 @@ class SettingsDialog(QtWidgets.QDialog): "https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services" ) ) - use_stealth_label.setStyleSheet(self.common.css["settings_whats_this"]) + use_stealth_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) use_stealth_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) use_stealth_label.setOpenExternalLinks(True) use_stealth_label.setMinimumSize(use_stealth_label.sizeHint()) @@ -305,7 +305,7 @@ class SettingsDialog(QtWidgets.QDialog): "https://github.com/micahflee/onionshare/wiki/Content-Security-Policy" ) ) - csp_header_label.setStyleSheet(self.common.css["settings_whats_this"]) + csp_header_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) csp_header_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) csp_header_label.setOpenExternalLinks(True) csp_header_label.setMinimumSize(csp_header_label.sizeHint()) @@ -673,7 +673,7 @@ class SettingsDialog(QtWidgets.QDialog): ) self.cancel_button.clicked.connect(self.cancel_clicked) version_label = QtWidgets.QLabel(f"OnionShare {self.common.version}") - version_label.setStyleSheet(self.common.css["settings_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() @@ -685,7 +685,7 @@ class SettingsDialog(QtWidgets.QDialog): # Tor network connection status self.tor_status = QtWidgets.QLabel() - self.tor_status.setStyleSheet(self.common.css["settings_tor_status"]) + self.tor_status.setStyleSheet(self.common.gui.css["settings_tor_status"]) self.tor_status.hide() # Layout From e51be47844e9ec7215fe28550dd6c8d11ade7e59 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 20:05:08 -0700 Subject: [PATCH 005/135] Move MODE_SHARE, MODE_RECEIVE, and MODE_WEBSITE into GuiCommon --- onionshare_gui/gui_common.py | 4 ++++ onionshare_gui/main_window.py | 40 +++++++++++++++------------------ onionshare_gui/server_status.py | 28 ++++++++++------------- 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/onionshare_gui/gui_common.py b/onionshare_gui/gui_common.py index fac17bd1..9964a322 100644 --- a/onionshare_gui/gui_common.py +++ b/onionshare_gui/gui_common.py @@ -24,6 +24,10 @@ class GuiCommon: The shared code for all of the OnionShare GUI. """ + MODE_SHARE = "share" + MODE_RECEIVE = "receive" + MODE_WEBSITE = "website" + def __init__(self, common): self.common = common diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index 4c26e118..de658a1b 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -39,10 +39,6 @@ class MainWindow(QtWidgets.QMainWindow): MainWindow is the OnionShare main window, which contains the GUI elements, including all open tabs """ - MODE_SHARE = "share" - MODE_RECEIVE = "receive" - MODE_WEBSITE = "website" - def __init__( self, common, onion, qtapp, app, filenames, config=False, local_only=False ): @@ -58,7 +54,7 @@ class MainWindow(QtWidgets.QMainWindow): self.app = app self.local_only = local_only - self.mode = self.MODE_SHARE + self.mode = self.common.gui.MODE_SHARE self.setWindowTitle("OnionShare") self.setWindowIcon( @@ -296,7 +292,7 @@ class MainWindow(QtWidgets.QMainWindow): def update_mode_switcher(self): # Based on the current mode, switch the mode switcher button styles, # and show and hide widgets to switch modes - if self.mode == self.MODE_SHARE: + if self.mode == self.common.gui.MODE_SHARE: self.share_mode_button.setStyleSheet( self.common.gui.css["mode_switcher_selected_style"] ) @@ -310,7 +306,7 @@ class MainWindow(QtWidgets.QMainWindow): self.receive_mode.hide() self.share_mode.show() self.website_mode.hide() - elif self.mode == self.MODE_WEBSITE: + elif self.mode == self.common.gui.MODE_WEBSITE: self.share_mode_button.setStyleSheet( self.common.gui.css["mode_switcher_unselected_style"] ) @@ -342,26 +338,26 @@ class MainWindow(QtWidgets.QMainWindow): self.update_server_status_indicator() def share_mode_clicked(self): - if self.mode != self.MODE_SHARE: + if self.mode != self.common.gui.MODE_SHARE: self.common.log("MainWindow", "share_mode_clicked") - self.mode = self.MODE_SHARE + self.mode = self.common.gui.MODE_SHARE self.update_mode_switcher() def receive_mode_clicked(self): - if self.mode != self.MODE_RECEIVE: + if self.mode != self.common.gui.MODE_RECEIVE: self.common.log("MainWindow", "receive_mode_clicked") - self.mode = self.MODE_RECEIVE + self.mode = self.common.gui.MODE_RECEIVE self.update_mode_switcher() def website_mode_clicked(self): - if self.mode != self.MODE_WEBSITE: + if self.mode != self.common.gui.MODE_WEBSITE: self.common.log("MainWindow", "website_mode_clicked") - self.mode = self.MODE_WEBSITE + self.mode = self.common.gui.MODE_WEBSITE self.update_mode_switcher() def update_server_status_indicator(self): # Set the status image - if self.mode == self.MODE_SHARE: + if self.mode == self.common.gui.MODE_SHARE: # Share mode if self.share_mode.server_status.status == ServerStatus.STATUS_STOPPED: self.server_status_image_label.setPixmap( @@ -389,7 +385,7 @@ class MainWindow(QtWidgets.QMainWindow): self.server_status_label.setText( strings._("gui_status_indicator_share_started") ) - elif self.mode == self.MODE_WEBSITE: + elif self.mode == self.common.gui.MODE_WEBSITE: # Website mode if self.website_mode.server_status.status == ServerStatus.STATUS_STOPPED: self.server_status_image_label.setPixmap( @@ -591,9 +587,9 @@ class MainWindow(QtWidgets.QMainWindow): self.website_mode.handle_tor_broke() # Process events from the web object - if self.mode == self.MODE_SHARE: + if self.mode == self.common.gui.MODE_SHARE: mode = self.share_mode - elif self.mode == self.MODE_WEBSITE: + elif self.mode == self.common.gui.MODE_WEBSITE: mode = self.website_mode else: mode = self.receive_mode @@ -700,11 +696,11 @@ class MainWindow(QtWidgets.QMainWindow): """ if active: self.settings_button.hide() - if self.mode == self.MODE_SHARE: + if self.mode == self.common.gui.MODE_SHARE: self.share_mode_button.show() self.receive_mode_button.hide() self.website_mode_button.hide() - elif self.mode == self.MODE_WEBSITE: + elif self.mode == self.common.gui.MODE_WEBSITE: self.share_mode_button.hide() self.receive_mode_button.hide() self.website_mode_button.show() @@ -725,9 +721,9 @@ class MainWindow(QtWidgets.QMainWindow): self.common.log("MainWindow", "closeEvent") self.system_tray.hide() try: - if self.mode == OnionShareGui.MODE_SHARE: + if self.mode == self.common.gui.MODE_WEBSITE: server_status = self.share_mode.server_status - if self.mode == OnionShareGui.MODE_WEBSITE: + if self.mode == self.common.gui.MODE_WEBSITE: server_status = self.website_mode.server_status else: server_status = self.receive_mode.server_status @@ -735,7 +731,7 @@ class MainWindow(QtWidgets.QMainWindow): self.common.log("MainWindow", "closeEvent, opening warning dialog") dialog = QtWidgets.QMessageBox() dialog.setWindowTitle(strings._("gui_quit_title")) - if self.mode == OnionShareGui.MODE_SHARE: + if self.mode == self.common.gui.MODE_WEBSITE: dialog.setText(strings._("gui_share_quit_warning")) else: dialog.setText(strings._("gui_receive_quit_warning")) diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index f1ec4559..18352109 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -39,10 +39,6 @@ class ServerStatus(QtWidgets.QWidget): url_copied = QtCore.pyqtSignal() hidservauth_copied = QtCore.pyqtSignal() - MODE_SHARE = "share" - MODE_RECEIVE = "receive" - MODE_WEBSITE = "website" - STATUS_STOPPED = 0 STATUS_WORKING = 1 STATUS_STARTED = 2 @@ -192,8 +188,8 @@ class ServerStatus(QtWidgets.QWidget): """ self.mode = share_mode - if (self.mode == ServerStatus.MODE_SHARE) or ( - self.mode == ServerStatus.MODE_WEBSITE + if (self.mode == self.common.gui.MODE_SHARE) or ( + self.mode == self.common.gui.MODE_WEBSITE ): self.file_selection = file_selection @@ -248,11 +244,11 @@ class ServerStatus(QtWidgets.QWidget): info_image = self.common.get_resource_path("images/info.png") - if self.mode == ServerStatus.MODE_SHARE: + if self.mode == self.common.gui.MODE_SHARE: self.url_description.setText( strings._("gui_share_url_description").format(info_image) ) - elif self.mode == ServerStatus.MODE_WEBSITE: + elif self.mode == self.common.gui.MODE_WEBSITE: self.url_description.setText( strings._("gui_website_url_description").format(info_image) ) @@ -263,7 +259,7 @@ class ServerStatus(QtWidgets.QWidget): # Show a Tool Tip explaining the lifecycle of this URL if self.common.settings.get("save_private_key"): - if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get( + if self.mode == self.common.gui.MODE_SHARE and self.common.settings.get( "close_after_first_download" ): self.url_description.setToolTip( @@ -272,7 +268,7 @@ class ServerStatus(QtWidgets.QWidget): else: self.url_description.setToolTip(strings._("gui_url_label_persistent")) else: - if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get( + if self.mode == self.common.gui.MODE_SHARE and self.common.settings.get( "close_after_first_download" ): self.url_description.setToolTip(strings._("gui_url_label_onetime")) @@ -317,12 +313,12 @@ class ServerStatus(QtWidgets.QWidget): # Button if ( - self.mode == ServerStatus.MODE_SHARE + self.mode == self.common.gui.MODE_SHARE and self.file_selection.get_num_files() == 0 ): self.server_button.hide() elif ( - self.mode == ServerStatus.MODE_WEBSITE + self.mode == self.common.gui.MODE_WEBSITE and self.file_selection.get_num_files() == 0 ): self.server_button.hide() @@ -334,9 +330,9 @@ class ServerStatus(QtWidgets.QWidget): self.common.gui.css["server_status_button_stopped"] ) self.server_button.setEnabled(True) - if self.mode == ServerStatus.MODE_SHARE: + if self.mode == self.common.gui.MODE_SHARE: self.server_button.setText(strings._("gui_share_start_server")) - elif self.mode == ServerStatus.MODE_WEBSITE: + elif self.mode == self.common.gui.MODE_WEBSITE: self.server_button.setText(strings._("gui_share_start_server")) else: self.server_button.setText(strings._("gui_receive_start_server")) @@ -350,9 +346,9 @@ class ServerStatus(QtWidgets.QWidget): self.common.gui.css["server_status_button_started"] ) self.server_button.setEnabled(True) - if self.mode == ServerStatus.MODE_SHARE: + if self.mode == self.common.gui.MODE_SHARE: self.server_button.setText(strings._("gui_share_stop_server")) - elif self.mode == ServerStatus.MODE_WEBSITE: + elif self.mode == self.common.gui.MODE_WEBSITE: self.server_button.setText(strings._("gui_share_stop_server")) else: self.server_button.setText(strings._("gui_receive_stop_server")) From 2a07a3572fca87e128e185ec11d411d2f97860fd Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 20:11:45 -0700 Subject: [PATCH 006/135] Move Onion and OnionShare app objects into the main window --- onionshare_gui/__init__.py | 13 ++----------- onionshare_gui/main_window.py | 31 +++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 0dff4229..6f2b72ab 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -27,8 +27,6 @@ import psutil from PyQt5 import QtCore, QtWidgets from onionshare.common import Common -from onionshare.onion import Onion -from onionshare.onionshare import OnionShare from .gui_common import GuiCommon from .widgets import Alert @@ -158,19 +156,12 @@ def main(): # TODO: open tab return - # Start the Onion - onion = Onion(common) - - # Start the OnionShare app - app = OnionShare(common, onion, local_only) - # Launch the gui - gui = MainWindow(common, onion, qtapp, app, filenames, config, local_only) + main_window = MainWindow(common, qtapp, filenames, config, local_only) # Clean up when app quits def shutdown(): - onion.cleanup() - app.cleanup() + main_window.cleanup() qtapp.aboutToQuit.connect(shutdown) diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index de658a1b..6db40473 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -23,6 +23,9 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.web import Web +from onionshare.onion import Onion +from onionshare.onionshare import OnionShare + from .mode.share_mode import ShareMode from .mode.receive_mode import ReceiveMode from .mode.website_mode import WebsiteMode @@ -39,23 +42,27 @@ class MainWindow(QtWidgets.QMainWindow): MainWindow is the OnionShare main window, which contains the GUI elements, including all open tabs """ - def __init__( - self, common, onion, qtapp, app, filenames, config=False, local_only=False - ): + def __init__(self, common, qtapp, filenames, config=False, local_only=False): super(MainWindow, self).__init__() self.common = common self.common.log("MainWindow", "__init__") - self.setMinimumWidth(820) - self.setMinimumHeight(660) - self.onion = onion self.qtapp = qtapp - self.app = app self.local_only = local_only self.mode = self.common.gui.MODE_SHARE + # Start the Onion + self.onion = Onion(common) + + # Start the OnionShare app + self.app = OnionShare(common, self.onion, local_only) + + # Initialize the window + self.setMinimumWidth(820) + self.setMinimumHeight(660) + self.setWindowTitle("OnionShare") self.setWindowIcon( QtGui.QIcon(self.common.get_resource_path("images/logo.png")) @@ -157,7 +164,7 @@ class MainWindow(QtWidgets.QMainWindow): self.share_mode = ShareMode( self.common, qtapp, - app, + self.app, self.status_bar, self.server_status_label, self.system_tray, @@ -188,7 +195,7 @@ class MainWindow(QtWidgets.QMainWindow): self.receive_mode = ReceiveMode( self.common, qtapp, - app, + self.app, self.status_bar, self.server_status_label, self.system_tray, @@ -221,7 +228,7 @@ class MainWindow(QtWidgets.QMainWindow): self.website_mode = WebsiteMode( self.common, qtapp, - app, + self.app, self.status_bar, self.server_status_label, self.system_tray, @@ -756,3 +763,7 @@ class MainWindow(QtWidgets.QMainWindow): except: e.accept() + + def cleanup(self): + self.onion.cleanup() + self.app.cleanup() From b8dc0551d31f6133a1194de0ed98ba30684ab923 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 21:36:30 -0700 Subject: [PATCH 007/135] Move more logic into GuiCommon and out of MainWindow --- onionshare_gui/__init__.py | 6 +++-- onionshare_gui/gui_common.py | 15 ++++++++++- onionshare_gui/main_window.py | 47 ++++++++++++++++------------------- 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 6f2b72ab..cb4f3e12 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -60,7 +60,6 @@ def main(): The main() function implements all of the logic that the GUI version of onionshare uses. """ common = Common() - common.gui = GuiCommon(common) # Display OnionShare banner print(f"OnionShare {common.version} | https://onionshare.org/") @@ -156,8 +155,11 @@ def main(): # TODO: open tab return + # Attach the GUI common parts to the common object + common.gui = GuiCommon(common, qtapp, local_only, config) + # Launch the gui - main_window = MainWindow(common, qtapp, filenames, config, local_only) + main_window = MainWindow(common, filenames) # Clean up when app quits def shutdown(): diff --git a/onionshare_gui/gui_common.py b/onionshare_gui/gui_common.py index 9964a322..940d813b 100644 --- a/onionshare_gui/gui_common.py +++ b/onionshare_gui/gui_common.py @@ -17,6 +17,7 @@ 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 . """ +from onionshare import strings class GuiCommon: @@ -28,8 +29,20 @@ class GuiCommon: MODE_RECEIVE = "receive" MODE_WEBSITE = "website" - def __init__(self, common): + def __init__(self, common, qtapp, local_only, config): self.common = common + self.qtapp = qtapp + self.local_only = local_only + + # Load settings, if a custom config was passed in + self.config = config + if self.config: + self.common.load_settings(self.config) + else: + self.common.load_settings() + + # Load strings + strings.load_strings(self.common) self.css = { # OnionShareGui styles diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index 6db40473..1f800000 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -42,22 +42,19 @@ class MainWindow(QtWidgets.QMainWindow): MainWindow is the OnionShare main window, which contains the GUI elements, including all open tabs """ - def __init__(self, common, qtapp, filenames, config=False, local_only=False): + def __init__(self, common, filenames): super(MainWindow, self).__init__() self.common = common self.common.log("MainWindow", "__init__") - self.qtapp = qtapp - self.local_only = local_only - self.mode = self.common.gui.MODE_SHARE # Start the Onion self.onion = Onion(common) # Start the OnionShare app - self.app = OnionShare(common, self.onion, local_only) + self.app = OnionShare(common, self.onion, self.common.gui.local_only) # Initialize the window self.setMinimumWidth(820) @@ -68,15 +65,6 @@ class MainWindow(QtWidgets.QMainWindow): QtGui.QIcon(self.common.get_resource_path("images/logo.png")) ) - # Load settings, if a custom config was passed in - self.config = config - if self.config: - self.common.load_settings(self.config) - else: - self.common.load_settings() - - strings.load_strings(self.common) - # System tray menu = QtWidgets.QMenu() self.settings_action = menu.addAction(strings._("gui_settings_window_title")) @@ -87,6 +75,7 @@ class MainWindow(QtWidgets.QMainWindow): exit_action.triggered.connect(self.close) self.system_tray = QtWidgets.QSystemTrayIcon(self) + # The convention is Mac systray icons are always grayscale if self.common.platform == "Darwin": self.system_tray.setIcon( @@ -163,13 +152,13 @@ class MainWindow(QtWidgets.QMainWindow): # Share mode self.share_mode = ShareMode( self.common, - qtapp, + self.common.gui.qtapp, self.app, self.status_bar, self.server_status_label, self.system_tray, filenames, - self.local_only, + self.common.gui.local_only, ) self.share_mode.init() self.share_mode.server_status.server_started.connect( @@ -194,13 +183,13 @@ class MainWindow(QtWidgets.QMainWindow): # Receive mode self.receive_mode = ReceiveMode( self.common, - qtapp, + self.common.gui.qtapp, self.app, self.status_bar, self.server_status_label, self.system_tray, None, - self.local_only, + self.common.gui.local_only, ) self.receive_mode.init() self.receive_mode.server_status.server_started.connect( @@ -227,7 +216,7 @@ class MainWindow(QtWidgets.QMainWindow): # Website mode self.website_mode = WebsiteMode( self.common, - qtapp, + self.common.gui.qtapp, self.app, self.status_bar, self.server_status_label, @@ -284,10 +273,10 @@ class MainWindow(QtWidgets.QMainWindow): self.timer.timeout.connect(self.timer_callback) # Start the "Connecting to Tor" dialog, which calls onion.connect() - tor_con = TorConnectionDialog(self.common, self.qtapp, self.onion) + tor_con = TorConnectionDialog(self.common, self.common.gui.qtapp, self.onion) tor_con.canceled.connect(self._tor_connection_canceled) tor_con.open_settings.connect(self._tor_connection_open_settings) - if not self.local_only: + if not self.common.gui.local_only: tor_con.start() # Start the timer @@ -490,7 +479,7 @@ class MainWindow(QtWidgets.QMainWindow): ) # Wait 1ms for the event loop to finish, then quit - QtCore.QTimer.singleShot(1, self.qtapp.quit) + QtCore.QTimer.singleShot(1, self.common.gui.qtapp.quit) # Wait 100ms before asking QtCore.QTimer.singleShot(100, ask) @@ -519,7 +508,7 @@ class MainWindow(QtWidgets.QMainWindow): # We might've stopped the main requests timer if a Tor connection failed. # If we've reloaded settings, we probably succeeded in obtaining a new # connection. If so, restart the timer. - if not self.local_only: + if not self.common.gui.local_only: if self.onion.is_authenticated(): if not self.timer.isActive(): self.timer.start(500) @@ -543,7 +532,11 @@ class MainWindow(QtWidgets.QMainWindow): self.website_mode.server_status.autostart_timer_container.hide() d = SettingsDialog( - self.common, self.onion, self.qtapp, self.config, self.local_only + self.common, + self.onion, + self.common.gui.qtapp, + self.common.gui.config, + self.common.gui.local_only, ) d.settings_saved.connect(reload_settings) d.exec_() @@ -568,7 +561,9 @@ class MainWindow(QtWidgets.QMainWindow): ), ) - self.update_thread = UpdateThread(self.common, self.onion, self.config) + self.update_thread = UpdateThread( + self.common, self.onion, self.common.gui.config + ) self.update_thread.update_available.connect(update_available) self.update_thread.start() @@ -579,7 +574,7 @@ class MainWindow(QtWidgets.QMainWindow): """ self.update() - if not self.local_only: + if not self.common.gui.local_only: # Have we lost connection to Tor somehow? if not self.onion.is_authenticated(): self.timer.stop() From c77db82e67d828ef2aa34ba31288a3e41b933547 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 22:08:47 -0700 Subject: [PATCH 008/135] Move all of the normal onionshare logic into Tab, and make a new placeholder GUI for the main window --- onionshare_gui/gui_common.py | 4 + onionshare_gui/main_window.py | 551 +---------------- onionshare_gui/settings_dialog.py | 53 +- onionshare_gui/tab/__init__.py | 20 + onionshare_gui/{ => tab}/mode/__init__.py | 0 .../{ => tab}/mode/file_selection.py | 0 onionshare_gui/{ => tab}/mode/history.py | 0 .../{ => tab}/mode/receive_mode/__init__.py | 0 .../{ => tab}/mode/share_mode/__init__.py | 0 .../{ => tab}/mode/share_mode/threads.py | 0 .../{ => tab}/mode/website_mode/__init__.py | 0 onionshare_gui/{ => tab}/server_status.py | 0 onionshare_gui/tab/tab.py | 562 ++++++++++++++++++ onionshare_gui/tor_connection_dialog.py | 18 +- 14 files changed, 634 insertions(+), 574 deletions(-) create mode 100644 onionshare_gui/tab/__init__.py rename onionshare_gui/{ => tab}/mode/__init__.py (100%) rename onionshare_gui/{ => tab}/mode/file_selection.py (100%) rename onionshare_gui/{ => tab}/mode/history.py (100%) rename onionshare_gui/{ => tab}/mode/receive_mode/__init__.py (100%) rename onionshare_gui/{ => tab}/mode/share_mode/__init__.py (100%) rename onionshare_gui/{ => tab}/mode/share_mode/threads.py (100%) rename onionshare_gui/{ => tab}/mode/website_mode/__init__.py (100%) rename onionshare_gui/{ => tab}/server_status.py (100%) create mode 100644 onionshare_gui/tab/tab.py diff --git a/onionshare_gui/gui_common.py b/onionshare_gui/gui_common.py index 940d813b..73951b67 100644 --- a/onionshare_gui/gui_common.py +++ b/onionshare_gui/gui_common.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ from onionshare import strings +from onionshare.onion import Onion class GuiCommon: @@ -44,6 +45,9 @@ class GuiCommon: # Load strings strings.load_strings(self.common) + # Start the Onion + self.onion = Onion(common) + self.css = { # OnionShareGui styles "mode_switcher_selected_style": """ diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index 1f800000..e3ab5808 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -23,18 +23,10 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.web import Web -from onionshare.onion import Onion -from onionshare.onionshare import OnionShare - -from .mode.share_mode import ShareMode -from .mode.receive_mode import ReceiveMode -from .mode.website_mode import WebsiteMode - from .tor_connection_dialog import TorConnectionDialog from .settings_dialog import SettingsDialog from .widgets import Alert from .update_checker import UpdateThread -from .server_status import ServerStatus class MainWindow(QtWidgets.QMainWindow): @@ -48,18 +40,9 @@ class MainWindow(QtWidgets.QMainWindow): self.common = common self.common.log("MainWindow", "__init__") - self.mode = self.common.gui.MODE_SHARE - - # Start the Onion - self.onion = Onion(common) - - # Start the OnionShare app - self.app = OnionShare(common, self.onion, self.common.gui.local_only) - # Initialize the window self.setMinimumWidth(820) self.setMinimumHeight(660) - self.setWindowTitle("OnionShare") self.setWindowIcon( QtGui.QIcon(self.common.get_resource_path("images/logo.png")) @@ -88,48 +71,7 @@ class MainWindow(QtWidgets.QMainWindow): self.system_tray.setContextMenu(menu) self.system_tray.show() - # Mode switcher, to switch between share files and receive files - self.share_mode_button = QtWidgets.QPushButton( - strings._("gui_mode_share_button") - ) - self.share_mode_button.setFixedHeight(50) - self.share_mode_button.clicked.connect(self.share_mode_clicked) - self.receive_mode_button = QtWidgets.QPushButton( - strings._("gui_mode_receive_button") - ) - self.receive_mode_button.setFixedHeight(50) - self.receive_mode_button.clicked.connect(self.receive_mode_clicked) - self.website_mode_button = QtWidgets.QPushButton( - strings._("gui_mode_website_button") - ) - self.website_mode_button.setFixedHeight(50) - self.website_mode_button.clicked.connect(self.website_mode_clicked) - self.settings_button = QtWidgets.QPushButton() - self.settings_button.setDefault(False) - self.settings_button.setFixedWidth(40) - self.settings_button.setFixedHeight(50) - self.settings_button.setIcon( - QtGui.QIcon(self.common.get_resource_path("images/settings.png")) - ) - self.settings_button.clicked.connect(self.open_settings) - self.settings_button.setStyleSheet(self.common.gui.css["settings_button"]) - mode_switcher_layout = QtWidgets.QHBoxLayout() - mode_switcher_layout.setSpacing(0) - mode_switcher_layout.addWidget(self.share_mode_button) - mode_switcher_layout.addWidget(self.receive_mode_button) - mode_switcher_layout.addWidget(self.website_mode_button) - mode_switcher_layout.addWidget(self.settings_button) - # Server status indicator on the status bar - self.server_status_image_stopped = QtGui.QImage( - self.common.get_resource_path("images/server_stopped.png") - ) - self.server_status_image_working = QtGui.QImage( - self.common.get_resource_path("images/server_working.png") - ) - self.server_status_image_started = QtGui.QImage( - self.common.get_resource_path("images/server_started.png") - ) self.server_status_image_label = QtWidgets.QLabel() self.server_status_image_label.setFixedWidth(20) self.server_status_label = QtWidgets.QLabel("") @@ -149,300 +91,35 @@ class MainWindow(QtWidgets.QMainWindow): self.status_bar.addPermanentWidget(self.server_status_indicator) self.setStatusBar(self.status_bar) - # Share mode - self.share_mode = ShareMode( - self.common, - self.common.gui.qtapp, - self.app, - self.status_bar, - self.server_status_label, - self.system_tray, - filenames, - self.common.gui.local_only, - ) - self.share_mode.init() - self.share_mode.server_status.server_started.connect( - self.update_server_status_indicator - ) - self.share_mode.server_status.server_stopped.connect( - self.update_server_status_indicator - ) - self.share_mode.start_server_finished.connect( - self.update_server_status_indicator - ) - self.share_mode.stop_server_finished.connect( - self.update_server_status_indicator - ) - self.share_mode.stop_server_finished.connect(self.stop_server_finished) - self.share_mode.start_server_finished.connect(self.clear_message) - self.share_mode.server_status.button_clicked.connect(self.clear_message) - self.share_mode.server_status.url_copied.connect(self.copy_url) - self.share_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth) - self.share_mode.set_server_active.connect(self.set_server_active) - - # Receive mode - self.receive_mode = ReceiveMode( - self.common, - self.common.gui.qtapp, - self.app, - self.status_bar, - self.server_status_label, - self.system_tray, - None, - self.common.gui.local_only, - ) - self.receive_mode.init() - self.receive_mode.server_status.server_started.connect( - self.update_server_status_indicator - ) - self.receive_mode.server_status.server_stopped.connect( - self.update_server_status_indicator - ) - self.receive_mode.start_server_finished.connect( - self.update_server_status_indicator - ) - self.receive_mode.stop_server_finished.connect( - self.update_server_status_indicator - ) - self.receive_mode.stop_server_finished.connect(self.stop_server_finished) - self.receive_mode.start_server_finished.connect(self.clear_message) - self.receive_mode.server_status.button_clicked.connect(self.clear_message) - self.receive_mode.server_status.url_copied.connect(self.copy_url) - self.receive_mode.server_status.hidservauth_copied.connect( - self.copy_hidservauth - ) - self.receive_mode.set_server_active.connect(self.set_server_active) - - # Website mode - self.website_mode = WebsiteMode( - self.common, - self.common.gui.qtapp, - self.app, - self.status_bar, - self.server_status_label, - self.system_tray, - filenames, - ) - self.website_mode.init() - self.website_mode.server_status.server_started.connect( - self.update_server_status_indicator - ) - self.website_mode.server_status.server_stopped.connect( - self.update_server_status_indicator - ) - self.website_mode.start_server_finished.connect( - self.update_server_status_indicator - ) - self.website_mode.stop_server_finished.connect( - self.update_server_status_indicator - ) - self.website_mode.stop_server_finished.connect(self.stop_server_finished) - self.website_mode.start_server_finished.connect(self.clear_message) - self.website_mode.server_status.button_clicked.connect(self.clear_message) - self.website_mode.server_status.url_copied.connect(self.copy_url) - self.website_mode.server_status.hidservauth_copied.connect( - self.copy_hidservauth - ) - self.website_mode.set_server_active.connect(self.set_server_active) - - self.update_mode_switcher() - self.update_server_status_indicator() - - # Layouts - contents_layout = QtWidgets.QVBoxLayout() - contents_layout.setContentsMargins(10, 0, 10, 0) - contents_layout.addWidget(self.receive_mode) - contents_layout.addWidget(self.share_mode) - contents_layout.addWidget(self.website_mode) + # Placeholder label + label = QtWidgets.QLabel("coming soon...") + # Layout layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) - layout.addLayout(mode_switcher_layout) - layout.addLayout(contents_layout) + layout.addWidget(label) central_widget = QtWidgets.QWidget() central_widget.setLayout(layout) self.setCentralWidget(central_widget) self.show() - # The server isn't active yet - self.set_server_active(False) - - # Create the timer - self.timer = QtCore.QTimer() - self.timer.timeout.connect(self.timer_callback) - # Start the "Connecting to Tor" dialog, which calls onion.connect() - tor_con = TorConnectionDialog(self.common, self.common.gui.qtapp, self.onion) - tor_con.canceled.connect(self._tor_connection_canceled) - tor_con.open_settings.connect(self._tor_connection_open_settings) + tor_con = TorConnectionDialog(self.common) + tor_con.canceled.connect(self.tor_connection_canceled) + tor_con.open_settings.connect(self.tor_connection_open_settings) if not self.common.gui.local_only: tor_con.start() - # Start the timer - self.timer.start(500) - # After connecting to Tor, check for updates self.check_for_updates() - def update_mode_switcher(self): - # Based on the current mode, switch the mode switcher button styles, - # and show and hide widgets to switch modes - if self.mode == self.common.gui.MODE_SHARE: - self.share_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_selected_style"] - ) - self.receive_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_unselected_style"] - ) - self.website_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_unselected_style"] - ) - - self.receive_mode.hide() - self.share_mode.show() - self.website_mode.hide() - elif self.mode == self.common.gui.MODE_WEBSITE: - self.share_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_unselected_style"] - ) - self.receive_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_unselected_style"] - ) - self.website_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_selected_style"] - ) - - self.receive_mode.hide() - self.share_mode.hide() - self.website_mode.show() - else: - self.share_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_unselected_style"] - ) - self.receive_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_selected_style"] - ) - self.website_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_unselected_style"] - ) - - self.share_mode.hide() - self.receive_mode.show() - self.website_mode.hide() - - self.update_server_status_indicator() - - def share_mode_clicked(self): - if self.mode != self.common.gui.MODE_SHARE: - self.common.log("MainWindow", "share_mode_clicked") - self.mode = self.common.gui.MODE_SHARE - self.update_mode_switcher() - - def receive_mode_clicked(self): - if self.mode != self.common.gui.MODE_RECEIVE: - self.common.log("MainWindow", "receive_mode_clicked") - self.mode = self.common.gui.MODE_RECEIVE - self.update_mode_switcher() - - def website_mode_clicked(self): - if self.mode != self.common.gui.MODE_WEBSITE: - self.common.log("MainWindow", "website_mode_clicked") - self.mode = self.common.gui.MODE_WEBSITE - self.update_mode_switcher() - - def update_server_status_indicator(self): - # Set the status image - if self.mode == self.common.gui.MODE_SHARE: - # Share mode - if self.share_mode.server_status.status == ServerStatus.STATUS_STOPPED: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_stopped) - ) - self.server_status_label.setText( - strings._("gui_status_indicator_share_stopped") - ) - elif self.share_mode.server_status.status == ServerStatus.STATUS_WORKING: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_working) - ) - if self.share_mode.server_status.autostart_timer_datetime: - self.server_status_label.setText( - strings._("gui_status_indicator_share_scheduled") - ) - else: - self.server_status_label.setText( - strings._("gui_status_indicator_share_working") - ) - elif self.share_mode.server_status.status == ServerStatus.STATUS_STARTED: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_started) - ) - self.server_status_label.setText( - strings._("gui_status_indicator_share_started") - ) - elif self.mode == self.common.gui.MODE_WEBSITE: - # Website mode - if self.website_mode.server_status.status == ServerStatus.STATUS_STOPPED: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_stopped) - ) - self.server_status_label.setText( - strings._("gui_status_indicator_share_stopped") - ) - elif self.website_mode.server_status.status == ServerStatus.STATUS_WORKING: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_working) - ) - self.server_status_label.setText( - strings._("gui_status_indicator_share_working") - ) - elif self.website_mode.server_status.status == ServerStatus.STATUS_STARTED: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_started) - ) - self.server_status_label.setText( - strings._("gui_status_indicator_share_started") - ) - else: - # Receive mode - if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_stopped) - ) - self.server_status_label.setText( - strings._("gui_status_indicator_receive_stopped") - ) - elif self.receive_mode.server_status.status == ServerStatus.STATUS_WORKING: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_working) - ) - if self.receive_mode.server_status.autostart_timer_datetime: - self.server_status_label.setText( - strings._("gui_status_indicator_receive_scheduled") - ) - else: - self.server_status_label.setText( - strings._("gui_status_indicator_receive_working") - ) - elif self.receive_mode.server_status.status == ServerStatus.STATUS_STARTED: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_started) - ) - self.server_status_label.setText( - strings._("gui_status_indicator_receive_started") - ) - - def stop_server_finished(self): - # When the server stopped, cleanup the ephemeral onion service - self.onion.cleanup(stop_tor=False) - - def _tor_connection_canceled(self): + def tor_connection_canceled(self): """ If the user cancels before Tor finishes connecting, ask if they want to quit, or open settings. """ - self.common.log("MainWindow", "_tor_connection_canceled") + self.common.log("MainWindow", "tor_connection_canceled") def ask(): a = Alert( @@ -484,11 +161,11 @@ class MainWindow(QtWidgets.QMainWindow): # Wait 100ms before asking QtCore.QTimer.singleShot(100, ask) - def _tor_connection_open_settings(self): + def tor_connection_open_settings(self): """ The TorConnectionDialog wants to open the Settings dialog """ - self.common.log("MainWindow", "_tor_connection_open_settings") + self.common.log("MainWindow", "tor_connection_open_settings") # Wait 1ms for the event loop to finish closing the TorConnectionDialog QtCore.QTimer.singleShot(1, self.open_settings) @@ -509,7 +186,7 @@ class MainWindow(QtWidgets.QMainWindow): # If we've reloaded settings, we probably succeeded in obtaining a new # connection. If so, restart the timer. if not self.common.gui.local_only: - if self.onion.is_authenticated(): + if self.common.gui.onion.is_authenticated(): if not self.timer.isActive(): self.timer.start(500) self.share_mode.on_reload_settings() @@ -531,13 +208,7 @@ class MainWindow(QtWidgets.QMainWindow): self.receive_mode.server_status.autostart_timer_container.hide() self.website_mode.server_status.autostart_timer_container.hide() - d = SettingsDialog( - self.common, - self.onion, - self.common.gui.qtapp, - self.common.gui.config, - self.common.gui.local_only, - ) + d = SettingsDialog(self.common) d.settings_saved.connect(reload_settings) d.exec_() @@ -562,203 +233,17 @@ class MainWindow(QtWidgets.QMainWindow): ) self.update_thread = UpdateThread( - self.common, self.onion, self.common.gui.config + self.common, self.common.gui.onion, self.common.gui.config ) self.update_thread.update_available.connect(update_available) self.update_thread.start() - def timer_callback(self): - """ - Check for messages communicated from the web app, and update the GUI accordingly. Also, - call ShareMode and ReceiveMode's timer_callbacks. - """ - self.update() - - if not self.common.gui.local_only: - # Have we lost connection to Tor somehow? - if not self.onion.is_authenticated(): - self.timer.stop() - self.status_bar.showMessage(strings._("gui_tor_connection_lost")) - self.system_tray.showMessage( - strings._("gui_tor_connection_lost"), - strings._("gui_tor_connection_error_settings"), - ) - - self.share_mode.handle_tor_broke() - self.receive_mode.handle_tor_broke() - self.website_mode.handle_tor_broke() - - # Process events from the web object - if self.mode == self.common.gui.MODE_SHARE: - mode = self.share_mode - elif self.mode == self.common.gui.MODE_WEBSITE: - mode = self.website_mode - else: - mode = self.receive_mode - - events = [] - - done = False - while not done: - try: - r = mode.web.q.get(False) - events.append(r) - except queue.Empty: - done = True - - for event in events: - if event["type"] == Web.REQUEST_LOAD: - mode.handle_request_load(event) - - elif event["type"] == Web.REQUEST_STARTED: - mode.handle_request_started(event) - - elif event["type"] == Web.REQUEST_RATE_LIMIT: - mode.handle_request_rate_limit(event) - - elif event["type"] == Web.REQUEST_PROGRESS: - mode.handle_request_progress(event) - - elif event["type"] == Web.REQUEST_CANCELED: - mode.handle_request_canceled(event) - - elif event["type"] == Web.REQUEST_UPLOAD_FILE_RENAMED: - mode.handle_request_upload_file_renamed(event) - - elif event["type"] == Web.REQUEST_UPLOAD_SET_DIR: - mode.handle_request_upload_set_dir(event) - - elif event["type"] == Web.REQUEST_UPLOAD_FINISHED: - mode.handle_request_upload_finished(event) - - elif event["type"] == Web.REQUEST_UPLOAD_CANCELED: - mode.handle_request_upload_canceled(event) - - elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_STARTED: - mode.handle_request_individual_file_started(event) - - elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_PROGRESS: - mode.handle_request_individual_file_progress(event) - - elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_CANCELED: - mode.handle_request_individual_file_canceled(event) - - if event["type"] == Web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE: - Alert( - self.common, - strings._("error_cannot_create_data_dir").format( - event["data"]["receive_mode_dir"] - ), - ) - - if event["type"] == Web.REQUEST_OTHER: - if ( - event["path"] != "/favicon.ico" - and event["path"] != f"/{mode.web.shutdown_password}/shutdown" - ): - self.status_bar.showMessage( - f"{strings._('other_page_loaded')}: {event['path']}" - ) - - if event["type"] == Web.REQUEST_INVALID_PASSWORD: - self.status_bar.showMessage( - f"[#{mode.web.invalid_passwords_count}] {strings._('incorrect_password')}: {event['data']}" - ) - - mode.timer_callback() - - def copy_url(self): - """ - When the URL gets copied to the clipboard, display this in the status bar. - """ - self.common.log("MainWindow", "copy_url") - self.system_tray.showMessage( - strings._("gui_copied_url_title"), strings._("gui_copied_url") - ) - - def copy_hidservauth(self): - """ - When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar. - """ - self.common.log("MainWindow", "copy_hidservauth") - self.system_tray.showMessage( - strings._("gui_copied_hidservauth_title"), - strings._("gui_copied_hidservauth"), - ) - - def clear_message(self): - """ - Clear messages from the status bar. - """ - self.status_bar.clearMessage() - - def set_server_active(self, active): - """ - Disable the Settings and Receive Files buttons while an Share Files server is active. - """ - if active: - self.settings_button.hide() - if self.mode == self.common.gui.MODE_SHARE: - self.share_mode_button.show() - self.receive_mode_button.hide() - self.website_mode_button.hide() - elif self.mode == self.common.gui.MODE_WEBSITE: - self.share_mode_button.hide() - self.receive_mode_button.hide() - self.website_mode_button.show() - else: - self.share_mode_button.hide() - self.receive_mode_button.show() - self.website_mode_button.hide() - else: - self.settings_button.show() - self.share_mode_button.show() - self.receive_mode_button.show() - self.website_mode_button.show() - - # Disable settings menu action when server is active - self.settings_action.setEnabled(not active) - def closeEvent(self, e): self.common.log("MainWindow", "closeEvent") self.system_tray.hide() - try: - if self.mode == self.common.gui.MODE_WEBSITE: - server_status = self.share_mode.server_status - if self.mode == self.common.gui.MODE_WEBSITE: - server_status = self.website_mode.server_status - else: - server_status = self.receive_mode.server_status - if server_status.status != server_status.STATUS_STOPPED: - self.common.log("MainWindow", "closeEvent, opening warning dialog") - dialog = QtWidgets.QMessageBox() - dialog.setWindowTitle(strings._("gui_quit_title")) - if self.mode == self.common.gui.MODE_WEBSITE: - dialog.setText(strings._("gui_share_quit_warning")) - else: - dialog.setText(strings._("gui_receive_quit_warning")) - dialog.setIcon(QtWidgets.QMessageBox.Critical) - quit_button = dialog.addButton( - strings._("gui_quit_warning_quit"), QtWidgets.QMessageBox.YesRole - ) - dont_quit_button = dialog.addButton( - strings._("gui_quit_warning_dont_quit"), - QtWidgets.QMessageBox.NoRole, - ) - dialog.setDefaultButton(dont_quit_button) - reply = dialog.exec_() - - # Quit - if reply == 0: - self.stop_server() - e.accept() - # Don't Quit - else: - e.ignore() - - except: - e.accept() + # TODO: Run the tab's close_event + e.accept() def cleanup(self): - self.onion.cleanup() - self.app.cleanup() + self.common.gui.onion.cleanup() + # TODO: Run the tab's cleanup diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index c285b4e6..aaa1fb31 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -40,18 +40,13 @@ class SettingsDialog(QtWidgets.QDialog): settings_saved = QtCore.pyqtSignal() - def __init__(self, common, onion, qtapp, config=False, local_only=False): + def __init__(self, common): super(SettingsDialog, self).__init__() self.common = common self.common.log("SettingsDialog", "__init__") - self.onion = onion - self.qtapp = qtapp - self.config = config - self.local_only = local_only - self.setModal(True) self.setWindowTitle(strings._("gui_settings_window_title")) self.setWindowIcon( @@ -346,7 +341,7 @@ class SettingsDialog(QtWidgets.QDialog): ) 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.onion.connected_to_tor: + if not self.common.gui.onion.connected_to_tor: self.check_for_updates_button.setEnabled(False) # Autoupdate options layout @@ -721,7 +716,7 @@ class SettingsDialog(QtWidgets.QDialog): def reload_settings(self): # Load settings, and fill them in - self.old_settings = Settings(self.common, self.config) + self.old_settings = Settings(self.common, self.common.gui.config) self.old_settings.load() close_after_first_download = self.old_settings.get("close_after_first_download") @@ -861,12 +856,12 @@ class SettingsDialog(QtWidgets.QDialog): self.tor_bridges_use_custom_textbox.setPlainText(new_bridges) # If we're connected to Tor, show onion service settings, show label if not - if self.onion.is_authenticated(): + if self.common.gui.onion.is_authenticated(): self.connect_to_tor_label.hide() self.onion_settings_widget.show() # If v3 onion services are supported, allow using legacy mode - if self.onion.supports_v3_onions: + if self.common.gui.onion.supports_v3_onions: self.common.log("SettingsDialog", "__init__", "v3 onions are supported") self.use_legacy_v2_onions_checkbox.show() else: @@ -1005,7 +1000,7 @@ class SettingsDialog(QtWidgets.QDialog): "hidservauth_copy_button_clicked", "HidServAuth was copied to clipboard", ) - clipboard = self.qtapp.clipboard() + clipboard = self.common.gui.qtapp.clipboard() clipboard.setText(self.old_settings.get("hidservauth_string")) def use_legacy_v2_onions_checkbox_clicked(self, checked): @@ -1068,7 +1063,7 @@ class SettingsDialog(QtWidgets.QDialog): onion = Onion(self.common) onion.connect( custom_settings=settings, - config=self.config, + config=self.common.gui.config, tor_status_update_func=tor_status_update_func, ) @@ -1110,11 +1105,11 @@ class SettingsDialog(QtWidgets.QDialog): self.common.log("SettingsDialog", "check_for_updates") # Disable buttons self._disable_buttons() - self.qtapp.processEvents() + self.common.gui.qtapp.processEvents() def update_timestamp(): # Update the last checked label - settings = Settings(self.common, self.config) + settings = Settings(self.common, self.common.gui.config) settings.load() autoupdate_timestamp = settings.get("autoupdate_timestamp") self._update_autoupdate_timestamp(autoupdate_timestamp) @@ -1157,7 +1152,7 @@ class SettingsDialog(QtWidgets.QDialog): close_forced_update_thread() forced_update_thread = UpdateThread( - self.common, self.onion, self.config, force=True + self.common, self.onion, self.common.gui.config, force=True ) forced_update_thread.update_available.connect(update_available) forced_update_thread.update_not_available.connect(update_not_available) @@ -1205,8 +1200,8 @@ class SettingsDialog(QtWidgets.QDialog): # If Tor isn't connected, or if Tor settings have changed, Reinitialize # the Onion object reboot_onion = False - if not self.local_only: - if self.onion.is_authenticated(): + if not self.common.gui.local_only: + if self.common.gui.onion.is_authenticated(): self.common.log( "SettingsDialog", "save_clicked", "Connected to Tor" ) @@ -1245,20 +1240,18 @@ class SettingsDialog(QtWidgets.QDialog): self.common.log( "SettingsDialog", "save_clicked", "rebooting the Onion" ) - self.onion.cleanup() + self.common.gui.onion.cleanup() - tor_con = TorConnectionDialog( - self.common, self.qtapp, self.onion, settings - ) + tor_con = TorConnectionDialog(self.common, settings) tor_con.start() self.common.log( "SettingsDialog", "save_clicked", - f"Onion done rebooting, connected to Tor: {self.onion.connected_to_tor}", + f"Onion done rebooting, connected to Tor: {self.common.gui.onion.connected_to_tor}", ) - if self.onion.is_authenticated() and not tor_con.wasCanceled(): + if self.common.gui.onion.is_authenticated() and not tor_con.wasCanceled(): self.settings_saved.emit() self.close() @@ -1274,7 +1267,7 @@ class SettingsDialog(QtWidgets.QDialog): Cancel button clicked. """ self.common.log("SettingsDialog", "cancel_clicked") - if not self.local_only and not self.onion.is_authenticated(): + if not self.common.gui.local_only and not self.common.gui.onion.is_authenticated(): Alert( self.common, strings._("gui_tor_connection_canceled"), @@ -1301,7 +1294,7 @@ class SettingsDialog(QtWidgets.QDialog): Return a Settings object that's full of values from the settings dialog. """ self.common.log("SettingsDialog", "settings_from_fields") - settings = Settings(self.common, self.config) + settings = Settings(self.common, self.common.gui.config) settings.load() # To get the last update timestamp settings.set( @@ -1448,14 +1441,14 @@ class SettingsDialog(QtWidgets.QDialog): self.common.log("SettingsDialog", "closeEvent") # On close, if Tor isn't connected, then quit OnionShare altogether - if not self.local_only: - if not self.onion.is_authenticated(): + 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.qtapp.quit) + QtCore.QTimer.singleShot(1, self.common.gui.qtapp.quit) def _update_autoupdate_timestamp(self, autoupdate_timestamp): self.common.log("SettingsDialog", "_update_autoupdate_timestamp") @@ -1473,7 +1466,7 @@ class SettingsDialog(QtWidgets.QDialog): self.tor_status.setText( f"{strings._('connecting_to_tor')}
{progress}% {summary}" ) - self.qtapp.processEvents() + self.common.gui.qtapp.processEvents() if "Done" in summary: self.tor_status.hide() self._enable_buttons() @@ -1489,7 +1482,7 @@ class SettingsDialog(QtWidgets.QDialog): 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.onion.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) diff --git a/onionshare_gui/tab/__init__.py b/onionshare_gui/tab/__init__.py new file mode 100644 index 00000000..ca346a7d --- /dev/null +++ b/onionshare_gui/tab/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +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 . +""" +from .tab import Tab diff --git a/onionshare_gui/mode/__init__.py b/onionshare_gui/tab/mode/__init__.py similarity index 100% rename from onionshare_gui/mode/__init__.py rename to onionshare_gui/tab/mode/__init__.py diff --git a/onionshare_gui/mode/file_selection.py b/onionshare_gui/tab/mode/file_selection.py similarity index 100% rename from onionshare_gui/mode/file_selection.py rename to onionshare_gui/tab/mode/file_selection.py diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/tab/mode/history.py similarity index 100% rename from onionshare_gui/mode/history.py rename to onionshare_gui/tab/mode/history.py diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/tab/mode/receive_mode/__init__.py similarity index 100% rename from onionshare_gui/mode/receive_mode/__init__.py rename to onionshare_gui/tab/mode/receive_mode/__init__.py diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py similarity index 100% rename from onionshare_gui/mode/share_mode/__init__.py rename to onionshare_gui/tab/mode/share_mode/__init__.py diff --git a/onionshare_gui/mode/share_mode/threads.py b/onionshare_gui/tab/mode/share_mode/threads.py similarity index 100% rename from onionshare_gui/mode/share_mode/threads.py rename to onionshare_gui/tab/mode/share_mode/threads.py diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/tab/mode/website_mode/__init__.py similarity index 100% rename from onionshare_gui/mode/website_mode/__init__.py rename to onionshare_gui/tab/mode/website_mode/__init__.py diff --git a/onionshare_gui/server_status.py b/onionshare_gui/tab/server_status.py similarity index 100% rename from onionshare_gui/server_status.py rename to onionshare_gui/tab/server_status.py diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py new file mode 100644 index 00000000..a921fe98 --- /dev/null +++ b/onionshare_gui/tab/tab.py @@ -0,0 +1,562 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +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 . +""" +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare import strings +from onionshare.onionshare import OnionShare +from onionshare.web import Web + +from .mode.share_mode import ShareMode +from .mode.receive_mode import ReceiveMode +from .mode.website_mode import WebsiteMode + +from .server_status import ServerStatus + +from ..widgets import Alert + + +class Tab(QtWidgets.QWidget): + """ + A GUI tab, you know, sort of like in a web browser + """ + + def __init__(self, common, system_tray, status_bar, filenames): + super(Tab, self).__init__() + self.common = common + self.common.log("Tab", "__init__") + + self.system_tray = system_tray + self.status_bar = status_bar + + self.mode = self.common.gui.MODE_SHARE + + # Start the OnionShare app + self.app = OnionShare(common, self.common.gui.onion, self.common.gui.local_only) + + # Mode switcher, to switch between share files and receive files + self.share_mode_button = QtWidgets.QPushButton( + strings._("gui_mode_share_button") + ) + self.share_mode_button.setFixedHeight(50) + self.share_mode_button.clicked.connect(self.share_mode_clicked) + self.receive_mode_button = QtWidgets.QPushButton( + strings._("gui_mode_receive_button") + ) + self.receive_mode_button.setFixedHeight(50) + self.receive_mode_button.clicked.connect(self.receive_mode_clicked) + self.website_mode_button = QtWidgets.QPushButton( + strings._("gui_mode_website_button") + ) + self.website_mode_button.setFixedHeight(50) + self.website_mode_button.clicked.connect(self.website_mode_clicked) + self.settings_button = QtWidgets.QPushButton() + self.settings_button.setDefault(False) + self.settings_button.setFixedWidth(40) + self.settings_button.setFixedHeight(50) + self.settings_button.setIcon( + QtGui.QIcon(self.common.get_resource_path("images/settings.png")) + ) + self.settings_button.clicked.connect(self.open_settings) + self.settings_button.setStyleSheet(self.common.gui.css["settings_button"]) + mode_switcher_layout = QtWidgets.QHBoxLayout() + mode_switcher_layout.setSpacing(0) + mode_switcher_layout.addWidget(self.share_mode_button) + mode_switcher_layout.addWidget(self.receive_mode_button) + mode_switcher_layout.addWidget(self.website_mode_button) + mode_switcher_layout.addWidget(self.settings_button) + + # Server status indicator icons + self.server_status_image_stopped = QtGui.QImage( + self.common.get_resource_path("images/server_stopped.png") + ) + self.server_status_image_working = QtGui.QImage( + self.common.get_resource_path("images/server_working.png") + ) + self.server_status_image_started = QtGui.QImage( + self.common.get_resource_path("images/server_started.png") + ) + + # Share mode + self.share_mode = ShareMode( + self.common, + self.common.gui.qtapp, + self.app, + self.status_bar, + self.server_status_label, + self.system_tray, + filenames, + self.common.gui.local_only, + ) + self.share_mode.init() + self.share_mode.server_status.server_started.connect( + self.update_server_status_indicator + ) + self.share_mode.server_status.server_stopped.connect( + self.update_server_status_indicator + ) + self.share_mode.start_server_finished.connect( + self.update_server_status_indicator + ) + self.share_mode.stop_server_finished.connect( + self.update_server_status_indicator + ) + self.share_mode.stop_server_finished.connect(self.stop_server_finished) + self.share_mode.start_server_finished.connect(self.clear_message) + self.share_mode.server_status.button_clicked.connect(self.clear_message) + self.share_mode.server_status.url_copied.connect(self.copy_url) + self.share_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth) + self.share_mode.set_server_active.connect(self.set_server_active) + + # Receive mode + self.receive_mode = ReceiveMode( + self.common, + self.common.gui.qtapp, + self.app, + self.status_bar, + self.server_status_label, + self.system_tray, + None, + self.common.gui.local_only, + ) + self.receive_mode.init() + self.receive_mode.server_status.server_started.connect( + self.update_server_status_indicator + ) + self.receive_mode.server_status.server_stopped.connect( + self.update_server_status_indicator + ) + self.receive_mode.start_server_finished.connect( + self.update_server_status_indicator + ) + self.receive_mode.stop_server_finished.connect( + self.update_server_status_indicator + ) + self.receive_mode.stop_server_finished.connect(self.stop_server_finished) + self.receive_mode.start_server_finished.connect(self.clear_message) + self.receive_mode.server_status.button_clicked.connect(self.clear_message) + self.receive_mode.server_status.url_copied.connect(self.copy_url) + self.receive_mode.server_status.hidservauth_copied.connect( + self.copy_hidservauth + ) + self.receive_mode.set_server_active.connect(self.set_server_active) + + # Website mode + self.website_mode = WebsiteMode( + self.common, + self.common.gui.qtapp, + self.app, + self.status_bar, + self.server_status_label, + self.system_tray, + filenames, + ) + self.website_mode.init() + self.website_mode.server_status.server_started.connect( + self.update_server_status_indicator + ) + self.website_mode.server_status.server_stopped.connect( + self.update_server_status_indicator + ) + self.website_mode.start_server_finished.connect( + self.update_server_status_indicator + ) + self.website_mode.stop_server_finished.connect( + self.update_server_status_indicator + ) + self.website_mode.stop_server_finished.connect(self.stop_server_finished) + self.website_mode.start_server_finished.connect(self.clear_message) + self.website_mode.server_status.button_clicked.connect(self.clear_message) + self.website_mode.server_status.url_copied.connect(self.copy_url) + self.website_mode.server_status.hidservauth_copied.connect( + self.copy_hidservauth + ) + self.website_mode.set_server_active.connect(self.set_server_active) + + self.update_mode_switcher() + self.update_server_status_indicator() + + # Layouts + contents_layout = QtWidgets.QVBoxLayout() + contents_layout.setContentsMargins(10, 0, 10, 0) + contents_layout.addWidget(self.receive_mode) + contents_layout.addWidget(self.share_mode) + contents_layout.addWidget(self.website_mode) + + layout = QtWidgets.QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addLayout(mode_switcher_layout) + layout.addLayout(contents_layout) + self.setLayout(layout) + + # The server isn't active yet + self.set_server_active(False) + + # Create the timer + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.timer_callback) + + # Start the timer + self.timer.start(500) + + def update_mode_switcher(self): + # Based on the current mode, switch the mode switcher button styles, + # and show and hide widgets to switch modes + if self.mode == self.common.gui.MODE_SHARE: + self.share_mode_button.setStyleSheet( + self.common.gui.css["mode_switcher_selected_style"] + ) + self.receive_mode_button.setStyleSheet( + self.common.gui.css["mode_switcher_unselected_style"] + ) + self.website_mode_button.setStyleSheet( + self.common.gui.css["mode_switcher_unselected_style"] + ) + + self.receive_mode.hide() + self.share_mode.show() + self.website_mode.hide() + elif self.mode == self.common.gui.MODE_WEBSITE: + self.share_mode_button.setStyleSheet( + self.common.gui.css["mode_switcher_unselected_style"] + ) + self.receive_mode_button.setStyleSheet( + self.common.gui.css["mode_switcher_unselected_style"] + ) + self.website_mode_button.setStyleSheet( + self.common.gui.css["mode_switcher_selected_style"] + ) + + self.receive_mode.hide() + self.share_mode.hide() + self.website_mode.show() + else: + self.share_mode_button.setStyleSheet( + self.common.gui.css["mode_switcher_unselected_style"] + ) + self.receive_mode_button.setStyleSheet( + self.common.gui.css["mode_switcher_selected_style"] + ) + self.website_mode_button.setStyleSheet( + self.common.gui.css["mode_switcher_unselected_style"] + ) + + self.share_mode.hide() + self.receive_mode.show() + self.website_mode.hide() + + self.update_server_status_indicator() + + def share_mode_clicked(self): + if self.mode != self.common.gui.MODE_SHARE: + self.common.log("Tab", "share_mode_clicked") + self.mode = self.common.gui.MODE_SHARE + self.update_mode_switcher() + + def receive_mode_clicked(self): + if self.mode != self.common.gui.MODE_RECEIVE: + self.common.log("Tab", "receive_mode_clicked") + self.mode = self.common.gui.MODE_RECEIVE + self.update_mode_switcher() + + def website_mode_clicked(self): + if self.mode != self.common.gui.MODE_WEBSITE: + self.common.log("Tab", "website_mode_clicked") + self.mode = self.common.gui.MODE_WEBSITE + self.update_mode_switcher() + + def update_server_status_indicator(self): + # Set the status image + if self.mode == self.common.gui.MODE_SHARE: + # Share mode + if self.share_mode.server_status.status == ServerStatus.STATUS_STOPPED: + self.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.server_status_image_stopped) + ) + self.server_status_label.setText( + strings._("gui_status_indicator_share_stopped") + ) + elif self.share_mode.server_status.status == ServerStatus.STATUS_WORKING: + self.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.server_status_image_working) + ) + if self.share_mode.server_status.autostart_timer_datetime: + self.server_status_label.setText( + strings._("gui_status_indicator_share_scheduled") + ) + else: + self.server_status_label.setText( + strings._("gui_status_indicator_share_working") + ) + elif self.share_mode.server_status.status == ServerStatus.STATUS_STARTED: + self.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.server_status_image_started) + ) + self.server_status_label.setText( + strings._("gui_status_indicator_share_started") + ) + elif self.mode == self.common.gui.MODE_WEBSITE: + # Website mode + if self.website_mode.server_status.status == ServerStatus.STATUS_STOPPED: + self.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.server_status_image_stopped) + ) + self.server_status_label.setText( + strings._("gui_status_indicator_share_stopped") + ) + elif self.website_mode.server_status.status == ServerStatus.STATUS_WORKING: + self.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.server_status_image_working) + ) + self.server_status_label.setText( + strings._("gui_status_indicator_share_working") + ) + elif self.website_mode.server_status.status == ServerStatus.STATUS_STARTED: + self.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.server_status_image_started) + ) + self.server_status_label.setText( + strings._("gui_status_indicator_share_started") + ) + else: + # Receive mode + if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED: + self.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.server_status_image_stopped) + ) + self.server_status_label.setText( + strings._("gui_status_indicator_receive_stopped") + ) + elif self.receive_mode.server_status.status == ServerStatus.STATUS_WORKING: + self.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.server_status_image_working) + ) + if self.receive_mode.server_status.autostart_timer_datetime: + self.server_status_label.setText( + strings._("gui_status_indicator_receive_scheduled") + ) + else: + self.server_status_label.setText( + strings._("gui_status_indicator_receive_working") + ) + elif self.receive_mode.server_status.status == ServerStatus.STATUS_STARTED: + self.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.server_status_image_started) + ) + self.server_status_label.setText( + strings._("gui_status_indicator_receive_started") + ) + + def stop_server_finished(self): + # When the server stopped, cleanup the ephemeral onion service + self.common.gui.onion.cleanup(stop_tor=False) + + def timer_callback(self): + """ + Check for messages communicated from the web app, and update the GUI accordingly. Also, + call ShareMode and ReceiveMode's timer_callbacks. + """ + self.update() + + if not self.common.gui.local_only: + # Have we lost connection to Tor somehow? + if not self.common.gui.onion.is_authenticated(): + self.timer.stop() + self.status_bar.showMessage(strings._("gui_tor_connection_lost")) + self.system_tray.showMessage( + strings._("gui_tor_connection_lost"), + strings._("gui_tor_connection_error_settings"), + ) + + self.share_mode.handle_tor_broke() + self.receive_mode.handle_tor_broke() + self.website_mode.handle_tor_broke() + + # Process events from the web object + if self.mode == self.common.gui.MODE_SHARE: + mode = self.share_mode + elif self.mode == self.common.gui.MODE_WEBSITE: + mode = self.website_mode + else: + mode = self.receive_mode + + events = [] + + done = False + while not done: + try: + r = mode.web.q.get(False) + events.append(r) + except queue.Empty: + done = True + + for event in events: + if event["type"] == Web.REQUEST_LOAD: + mode.handle_request_load(event) + + elif event["type"] == Web.REQUEST_STARTED: + mode.handle_request_started(event) + + elif event["type"] == Web.REQUEST_RATE_LIMIT: + mode.handle_request_rate_limit(event) + + elif event["type"] == Web.REQUEST_PROGRESS: + mode.handle_request_progress(event) + + elif event["type"] == Web.REQUEST_CANCELED: + mode.handle_request_canceled(event) + + elif event["type"] == Web.REQUEST_UPLOAD_FILE_RENAMED: + mode.handle_request_upload_file_renamed(event) + + elif event["type"] == Web.REQUEST_UPLOAD_SET_DIR: + mode.handle_request_upload_set_dir(event) + + elif event["type"] == Web.REQUEST_UPLOAD_FINISHED: + mode.handle_request_upload_finished(event) + + elif event["type"] == Web.REQUEST_UPLOAD_CANCELED: + mode.handle_request_upload_canceled(event) + + elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_STARTED: + mode.handle_request_individual_file_started(event) + + elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_PROGRESS: + mode.handle_request_individual_file_progress(event) + + elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_CANCELED: + mode.handle_request_individual_file_canceled(event) + + if event["type"] == Web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE: + Alert( + self.common, + strings._("error_cannot_create_data_dir").format( + event["data"]["receive_mode_dir"] + ), + ) + + if event["type"] == Web.REQUEST_OTHER: + if ( + event["path"] != "/favicon.ico" + and event["path"] != f"/{mode.web.shutdown_password}/shutdown" + ): + self.status_bar.showMessage( + f"{strings._('other_page_loaded')}: {event['path']}" + ) + + if event["type"] == Web.REQUEST_INVALID_PASSWORD: + self.status_bar.showMessage( + f"[#{mode.web.invalid_passwords_count}] {strings._('incorrect_password')}: {event['data']}" + ) + + mode.timer_callback() + + def copy_url(self): + """ + When the URL gets copied to the clipboard, display this in the status bar. + """ + self.common.log("Tab", "copy_url") + self.system_tray.showMessage( + strings._("gui_copied_url_title"), strings._("gui_copied_url") + ) + + def copy_hidservauth(self): + """ + When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar. + """ + self.common.log("Tab", "copy_hidservauth") + self.system_tray.showMessage( + strings._("gui_copied_hidservauth_title"), + strings._("gui_copied_hidservauth"), + ) + + def set_server_active(self, active): + """ + Disable the Settings and Receive Files buttons while an Share Files server is active. + """ + if active: + self.settings_button.hide() + if self.mode == self.common.gui.MODE_SHARE: + self.share_mode_button.show() + self.receive_mode_button.hide() + self.website_mode_button.hide() + elif self.mode == self.common.gui.MODE_WEBSITE: + self.share_mode_button.hide() + self.receive_mode_button.hide() + self.website_mode_button.show() + else: + self.share_mode_button.hide() + self.receive_mode_button.show() + self.website_mode_button.hide() + else: + self.settings_button.show() + self.share_mode_button.show() + self.receive_mode_button.show() + self.website_mode_button.show() + + # Disable settings menu action when server is active + self.settings_action.setEnabled(not active) + + def clear_message(self): + """ + Clear messages from the status bar. + """ + self.status_bar.clearMessage() + + def close_event(self, e): + self.common.log("Tab", "close_event") + try: + if self.mode == self.common.gui.MODE_WEBSITE: + server_status = self.share_mode.server_status + if self.mode == self.common.gui.MODE_WEBSITE: + server_status = self.website_mode.server_status + else: + server_status = self.receive_mode.server_status + if server_status.status != server_status.STATUS_STOPPED: + self.common.log("MainWindow", "closeEvent, opening warning dialog") + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle(strings._("gui_quit_title")) + if self.mode == self.common.gui.MODE_WEBSITE: + dialog.setText(strings._("gui_share_quit_warning")) + else: + dialog.setText(strings._("gui_receive_quit_warning")) + dialog.setIcon(QtWidgets.QMessageBox.Critical) + quit_button = dialog.addButton( + strings._("gui_quit_warning_quit"), QtWidgets.QMessageBox.YesRole + ) + dont_quit_button = dialog.addButton( + strings._("gui_quit_warning_dont_quit"), + QtWidgets.QMessageBox.NoRole, + ) + dialog.setDefaultButton(dont_quit_button) + reply = dialog.exec_() + + # Quit + if reply == 0: + self.stop_server() + e.accept() + # Don't Quit + else: + e.ignore() + + except: + e.accept() + + def cleanup(self): + self.app.cleanup() diff --git a/onionshare_gui/tor_connection_dialog.py b/onionshare_gui/tor_connection_dialog.py index 95e61eb3..37c0ebc3 100644 --- a/onionshare_gui/tor_connection_dialog.py +++ b/onionshare_gui/tor_connection_dialog.py @@ -32,7 +32,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): open_settings = QtCore.pyqtSignal() - def __init__(self, common, qtapp, onion, custom_settings=False): + def __init__(self, common, custom_settings=False): super(TorConnectionDialog, self).__init__(None) self.common = common @@ -44,9 +44,6 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): self.common.log("TorConnectionDialog", "__init__") - self.qtapp = qtapp - self.onion = onion - self.setWindowTitle("OnionShare") self.setWindowIcon( QtGui.QIcon(self.common.get_resource_path("images/logo.png")) @@ -68,7 +65,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): def start(self): self.common.log("TorConnectionDialog", "start") - t = TorConnectionThread(self.common, self.settings, self, self.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) @@ -81,7 +78,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): self.active = True while self.active: time.sleep(0.1) - self.qtapp.processEvents() + self.common.gui.qtapp.processEvents() def _tor_status_update(self, progress, summary): self.setValue(int(progress)) @@ -99,7 +96,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): def _canceled_connecting_to_tor(self): self.common.log("TorConnectionDialog", "_canceled_connecting_to_tor") self.active = False - self.onion.cleanup() + self.common.gui.onion.cleanup() # Cancel connecting to Tor QtCore.QTimer.singleShot(1, self.cancel) @@ -131,7 +128,7 @@ class TorConnectionThread(QtCore.QThread): canceled_connecting_to_tor = QtCore.pyqtSignal() error_connecting_to_tor = QtCore.pyqtSignal(str) - def __init__(self, common, settings, dialog, onion): + def __init__(self, common, settings, dialog): super(TorConnectionThread, self).__init__() self.common = common @@ -141,15 +138,14 @@ class TorConnectionThread(QtCore.QThread): self.settings = settings self.dialog = dialog - self.onion = onion def run(self): self.common.log("TorConnectionThread", "run") # Connect to the Onion try: - self.onion.connect(self.settings, False, self._tor_status_update) - if self.onion.connected_to_tor: + self.common.gui.onion.connect(self.settings, False, self._tor_status_update) + if self.common.gui.onion.connected_to_tor: self.connected_to_tor.emit() else: self.canceled_connecting_to_tor.emit() From 6449e5183e2430d0525a884173be1295cd39d6af Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 26 Oct 2019 21:14:47 -0700 Subject: [PATCH 009/135] Add a single tab, and fix several issues with moving all the tab code into its own object --- onionshare_gui/main_window.py | 46 +++++++----- onionshare_gui/tab/mode/__init__.py | 5 +- onionshare_gui/tab/mode/file_selection.py | 2 +- onionshare_gui/tab/mode/history.py | 2 +- .../tab/mode/share_mode/__init__.py | 2 +- .../tab/mode/website_mode/__init__.py | 2 +- onionshare_gui/tab/server_status.py | 2 +- onionshare_gui/tab/tab.py | 75 ++++++++++--------- 8 files changed, 72 insertions(+), 64 deletions(-) diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index e3ab5808..4fd45d61 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -17,7 +17,6 @@ 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 . """ -import queue from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings @@ -27,6 +26,7 @@ from .tor_connection_dialog import TorConnectionDialog from .settings_dialog import SettingsDialog from .widgets import Alert from .update_checker import UpdateThread +from .tab import Tab class MainWindow(QtWidgets.QMainWindow): @@ -71,33 +71,41 @@ class MainWindow(QtWidgets.QMainWindow): self.system_tray.setContextMenu(menu) self.system_tray.show() - # Server status indicator on the status bar - self.server_status_image_label = QtWidgets.QLabel() - self.server_status_image_label.setFixedWidth(20) - self.server_status_label = QtWidgets.QLabel("") - self.server_status_label.setStyleSheet( - self.common.gui.css["server_status_indicator_label"] - ) - server_status_indicator_layout = QtWidgets.QHBoxLayout() - server_status_indicator_layout.addWidget(self.server_status_image_label) - server_status_indicator_layout.addWidget(self.server_status_label) - self.server_status_indicator = QtWidgets.QWidget() - self.server_status_indicator.setLayout(server_status_indicator_layout) - # Status bar self.status_bar = QtWidgets.QStatusBar() self.status_bar.setSizeGripEnabled(False) self.status_bar.setStyleSheet(self.common.gui.css["status_bar"]) - self.status_bar.addPermanentWidget(self.server_status_indicator) self.setStatusBar(self.status_bar) - # Placeholder label - label = QtWidgets.QLabel("coming soon...") + # Server status indicator on the status bar + self.status_bar.server_status_image_label = QtWidgets.QLabel() + self.status_bar.server_status_image_label.setFixedWidth(20) + self.status_bar.server_status_label = QtWidgets.QLabel("") + self.status_bar.server_status_label.setStyleSheet( + self.common.gui.css["server_status_indicator_label"] + ) + server_status_indicator_layout = QtWidgets.QHBoxLayout() + server_status_indicator_layout.addWidget( + self.status_bar.server_status_image_label + ) + server_status_indicator_layout.addWidget(self.status_bar.server_status_label) + self.status_bar.server_status_indicator = QtWidgets.QWidget() + self.status_bar.server_status_indicator.setLayout( + server_status_indicator_layout + ) + self.status_bar.addPermanentWidget(self.status_bar.server_status_indicator) + + # Tabs + self.tabs = QtWidgets.QTabWidget() + + # Start with one tab + self.tab = Tab(self.common, self.system_tray, self.status_bar, filenames) + self.tabs.addTab(self.tab, "Tab 1") # Layout layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(label) + # layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.tabs) central_widget = QtWidgets.QWidget() central_widget.setLayout(layout) diff --git a/onionshare_gui/tab/mode/__init__.py b/onionshare_gui/tab/mode/__init__.py index 7180f24f..e993f5e2 100644 --- a/onionshare_gui/tab/mode/__init__.py +++ b/onionshare_gui/tab/mode/__init__.py @@ -25,9 +25,8 @@ from onionshare.common import AutoStopTimer from .history import IndividualFileHistoryItem from ..server_status import ServerStatus -from ..threads import OnionThread -from ..threads import AutoStartTimer -from ..widgets import Alert +from ...threads import OnionThread, AutoStartTimer +from ...widgets import Alert class Mode(QtWidgets.QWidget): diff --git a/onionshare_gui/tab/mode/file_selection.py b/onionshare_gui/tab/mode/file_selection.py index 7a9cf5e4..67a23fc5 100644 --- a/onionshare_gui/tab/mode/file_selection.py +++ b/onionshare_gui/tab/mode/file_selection.py @@ -22,7 +22,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings -from ..widgets import Alert, AddFileDialog +from ...widgets import Alert, AddFileDialog class DropHereLabel(QtWidgets.QLabel): diff --git a/onionshare_gui/tab/mode/history.py b/onionshare_gui/tab/mode/history.py index a9a46841..0797320e 100644 --- a/onionshare_gui/tab/mode/history.py +++ b/onionshare_gui/tab/mode/history.py @@ -24,7 +24,7 @@ from datetime import datetime from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings -from ..widgets import Alert +from ...widgets import Alert class HistoryItem(QtWidgets.QWidget): diff --git a/onionshare_gui/tab/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py index c2ba1338..1d495cd5 100644 --- a/onionshare_gui/tab/mode/share_mode/__init__.py +++ b/onionshare_gui/tab/mode/share_mode/__init__.py @@ -29,7 +29,7 @@ from ..file_selection import FileSelection from .threads import CompressThread from .. import Mode from ..history import History, ToggleHistory, ShareHistoryItem -from ...widgets import Alert +from ....widgets import Alert class ShareMode(Mode): diff --git a/onionshare_gui/tab/mode/website_mode/__init__.py b/onionshare_gui/tab/mode/website_mode/__init__.py index a9ddef99..6da08bd2 100644 --- a/onionshare_gui/tab/mode/website_mode/__init__.py +++ b/onionshare_gui/tab/mode/website_mode/__init__.py @@ -31,7 +31,7 @@ from onionshare.web import Web from ..file_selection import FileSelection from .. import Mode from ..history import History, ToggleHistory -from ...widgets import Alert +from ....widgets import Alert class WebsiteMode(Mode): diff --git a/onionshare_gui/tab/server_status.py b/onionshare_gui/tab/server_status.py index 18352109..94651c5a 100644 --- a/onionshare_gui/tab/server_status.py +++ b/onionshare_gui/tab/server_status.py @@ -23,7 +23,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings -from .widgets import Alert +from ..widgets import Alert class ServerStatus(QtWidgets.QWidget): diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index a921fe98..92758c9d 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -17,6 +17,7 @@ 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 . """ +import queue from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings @@ -73,7 +74,7 @@ class Tab(QtWidgets.QWidget): self.settings_button.setIcon( QtGui.QIcon(self.common.get_resource_path("images/settings.png")) ) - self.settings_button.clicked.connect(self.open_settings) + # self.settings_button.clicked.connect(self.open_settings) self.settings_button.setStyleSheet(self.common.gui.css["settings_button"]) mode_switcher_layout = QtWidgets.QHBoxLayout() mode_switcher_layout.setSpacing(0) @@ -83,13 +84,13 @@ class Tab(QtWidgets.QWidget): mode_switcher_layout.addWidget(self.settings_button) # Server status indicator icons - self.server_status_image_stopped = QtGui.QImage( + self.status_bar.server_status_image_stopped = QtGui.QImage( self.common.get_resource_path("images/server_stopped.png") ) - self.server_status_image_working = QtGui.QImage( + self.status_bar.server_status_image_working = QtGui.QImage( self.common.get_resource_path("images/server_working.png") ) - self.server_status_image_started = QtGui.QImage( + self.status_bar.server_status_image_started = QtGui.QImage( self.common.get_resource_path("images/server_started.png") ) @@ -99,7 +100,7 @@ class Tab(QtWidgets.QWidget): self.common.gui.qtapp, self.app, self.status_bar, - self.server_status_label, + self.status_bar.server_status_label, self.system_tray, filenames, self.common.gui.local_only, @@ -130,7 +131,7 @@ class Tab(QtWidgets.QWidget): self.common.gui.qtapp, self.app, self.status_bar, - self.server_status_label, + self.status_bar.server_status_label, self.system_tray, None, self.common.gui.local_only, @@ -163,7 +164,7 @@ class Tab(QtWidgets.QWidget): self.common.gui.qtapp, self.app, self.status_bar, - self.server_status_label, + self.status_bar.server_status_label, self.system_tray, filenames, ) @@ -286,80 +287,80 @@ class Tab(QtWidgets.QWidget): if self.mode == self.common.gui.MODE_SHARE: # Share mode if self.share_mode.server_status.status == ServerStatus.STATUS_STOPPED: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_stopped) + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_stopped) ) - self.server_status_label.setText( + self.status_bar.server_status_label.setText( strings._("gui_status_indicator_share_stopped") ) elif self.share_mode.server_status.status == ServerStatus.STATUS_WORKING: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_working) + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_working) ) if self.share_mode.server_status.autostart_timer_datetime: - self.server_status_label.setText( + self.status_bar.server_status_label.setText( strings._("gui_status_indicator_share_scheduled") ) else: - self.server_status_label.setText( + self.status_bar.server_status_label.setText( strings._("gui_status_indicator_share_working") ) elif self.share_mode.server_status.status == ServerStatus.STATUS_STARTED: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_started) + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_started) ) - self.server_status_label.setText( + self.status_bar.server_status_label.setText( strings._("gui_status_indicator_share_started") ) elif self.mode == self.common.gui.MODE_WEBSITE: # Website mode if self.website_mode.server_status.status == ServerStatus.STATUS_STOPPED: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_stopped) + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_stopped) ) - self.server_status_label.setText( + self.status_bar.server_status_label.setText( strings._("gui_status_indicator_share_stopped") ) elif self.website_mode.server_status.status == ServerStatus.STATUS_WORKING: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_working) + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_working) ) - self.server_status_label.setText( + self.status_bar.server_status_label.setText( strings._("gui_status_indicator_share_working") ) elif self.website_mode.server_status.status == ServerStatus.STATUS_STARTED: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_started) + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_started) ) - self.server_status_label.setText( + self.status_bar.server_status_label.setText( strings._("gui_status_indicator_share_started") ) else: # Receive mode if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_stopped) + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_stopped) ) - self.server_status_label.setText( + self.status_bar.server_status_label.setText( strings._("gui_status_indicator_receive_stopped") ) elif self.receive_mode.server_status.status == ServerStatus.STATUS_WORKING: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_working) + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_working) ) if self.receive_mode.server_status.autostart_timer_datetime: - self.server_status_label.setText( + self.status_bar.server_status_label.setText( strings._("gui_status_indicator_receive_scheduled") ) else: - self.server_status_label.setText( + self.status_bar.server_status_label.setText( strings._("gui_status_indicator_receive_working") ) elif self.receive_mode.server_status.status == ServerStatus.STATUS_STARTED: - self.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.server_status_image_started) + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_started) ) - self.server_status_label.setText( + self.status_bar.server_status_label.setText( strings._("gui_status_indicator_receive_started") ) @@ -511,7 +512,7 @@ class Tab(QtWidgets.QWidget): self.website_mode_button.show() # Disable settings menu action when server is active - self.settings_action.setEnabled(not active) + # self.settings_action.setEnabled(not active) def clear_message(self): """ From 144985b1c1aad379f43f1ec15bbf757672c0cb77 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 26 Oct 2019 21:56:57 -0700 Subject: [PATCH 010/135] Tabs start out with new tab options, and remove the mode switcher from tabs --- onionshare_gui/gui_common.py | 6 + onionshare_gui/main_window.py | 2 +- onionshare_gui/tab/tab.py | 214 ++++++++++++++++------------------ share/locale/en.json | 10 +- 4 files changed, 113 insertions(+), 119 deletions(-) diff --git a/onionshare_gui/gui_common.py b/onionshare_gui/gui_common.py index 73951b67..3344d879 100644 --- a/onionshare_gui/gui_common.py +++ b/onionshare_gui/gui_common.py @@ -50,6 +50,12 @@ class GuiCommon: self.css = { # OnionShareGui styles + "new_tab_button": """ + QPushButton { + font-weight: bold; + font-size: 30px; + color: #601f61; + }""", "mode_switcher_selected_style": """ QPushButton { color: #ffffff; diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index 4fd45d61..5d81feba 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -100,7 +100,7 @@ class MainWindow(QtWidgets.QMainWindow): # Start with one tab self.tab = Tab(self.common, self.system_tray, self.status_bar, filenames) - self.tabs.addTab(self.tab, "Tab 1") + self.tabs.addTab(self.tab, "New Tab") # Layout layout = QtWidgets.QVBoxLayout() diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 92758c9d..141122ac 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -45,28 +45,62 @@ class Tab(QtWidgets.QWidget): self.system_tray = system_tray self.status_bar = status_bar + self.filenames = filenames self.mode = self.common.gui.MODE_SHARE # Start the OnionShare app self.app = OnionShare(common, self.common.gui.onion, self.common.gui.local_only) - # Mode switcher, to switch between share files and receive files - self.share_mode_button = QtWidgets.QPushButton( - strings._("gui_mode_share_button") + # New tab widget + share_button = QtWidgets.QPushButton(strings._("gui_new_tab_share_button")) + share_button.setStyleSheet(self.common.gui.css["new_tab_button"]) + share_description = QtWidgets.QLabel(strings._("gui_new_tab_share_description")) + share_description.setWordWrap(True) + share_button.clicked.connect(self.share_mode_clicked) + + receive_button = QtWidgets.QPushButton(strings._("gui_new_tab_receive_button")) + receive_button.setStyleSheet(self.common.gui.css["new_tab_button"]) + receive_button.clicked.connect(self.receive_mode_clicked) + receive_description = QtWidgets.QLabel( + strings._("gui_new_tab_receive_description") ) - self.share_mode_button.setFixedHeight(50) - self.share_mode_button.clicked.connect(self.share_mode_clicked) - self.receive_mode_button = QtWidgets.QPushButton( - strings._("gui_mode_receive_button") + receive_description.setWordWrap(True) + + website_button = QtWidgets.QPushButton(strings._("gui_new_tab_website_button")) + website_button.setStyleSheet(self.common.gui.css["new_tab_button"]) + website_button.clicked.connect(self.website_mode_clicked) + website_description = QtWidgets.QLabel( + strings._("gui_new_tab_website_description") ) - self.receive_mode_button.setFixedHeight(50) - self.receive_mode_button.clicked.connect(self.receive_mode_clicked) - self.website_mode_button = QtWidgets.QPushButton( - strings._("gui_mode_website_button") - ) - self.website_mode_button.setFixedHeight(50) - self.website_mode_button.clicked.connect(self.website_mode_clicked) + website_description.setWordWrap(True) + + new_tab_layout = QtWidgets.QVBoxLayout() + new_tab_layout.addStretch(1) + new_tab_layout.addWidget(share_button) + new_tab_layout.addWidget(share_description) + new_tab_layout.addSpacing(50) + new_tab_layout.addWidget(receive_button) + new_tab_layout.addWidget(receive_description) + new_tab_layout.addSpacing(50) + new_tab_layout.addWidget(website_button) + new_tab_layout.addWidget(website_description) + new_tab_layout.addStretch(3) + + new_tab_inner = QtWidgets.QWidget() + new_tab_inner.setFixedWidth(500) + new_tab_inner.setLayout(new_tab_layout) + + new_tab_outer_layout = QtWidgets.QHBoxLayout() + new_tab_outer_layout.addStretch() + new_tab_outer_layout.addWidget(new_tab_inner) + new_tab_outer_layout.addStretch() + + self.new_tab = QtWidgets.QWidget() + self.new_tab.setLayout(new_tab_outer_layout) + self.new_tab.show() + + # Settings button, but this is gonna disappear self.settings_button = QtWidgets.QPushButton() self.settings_button.setDefault(False) self.settings_button.setFixedWidth(40) @@ -76,12 +110,6 @@ class Tab(QtWidgets.QWidget): ) # self.settings_button.clicked.connect(self.open_settings) self.settings_button.setStyleSheet(self.common.gui.css["settings_button"]) - mode_switcher_layout = QtWidgets.QHBoxLayout() - mode_switcher_layout.setSpacing(0) - mode_switcher_layout.addWidget(self.share_mode_button) - mode_switcher_layout.addWidget(self.receive_mode_button) - mode_switcher_layout.addWidget(self.website_mode_button) - mode_switcher_layout.addWidget(self.settings_button) # Server status indicator icons self.status_bar.server_status_image_stopped = QtGui.QImage( @@ -94,7 +122,24 @@ class Tab(QtWidgets.QWidget): self.common.get_resource_path("images/server_started.png") ) - # Share mode + # Layout + self.layout = QtWidgets.QVBoxLayout() + self.layout.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.new_tab) + self.setLayout(self.layout) + + # The server isn't active yet + self.set_server_active(False) + + # Create the timer + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.timer_callback) + + def share_mode_clicked(self): + self.common.log("Tab", "share_mode_clicked") + self.mode = self.common.gui.MODE_SHARE + self.new_tab.hide() + self.share_mode = ShareMode( self.common, self.common.gui.qtapp, @@ -102,9 +147,12 @@ class Tab(QtWidgets.QWidget): self.status_bar, self.status_bar.server_status_label, self.system_tray, - filenames, + self.filenames, self.common.gui.local_only, ) + self.layout.addWidget(self.share_mode) + self.share_mode.show() + self.share_mode.init() self.share_mode.server_status.server_started.connect( self.update_server_status_indicator @@ -125,7 +173,14 @@ class Tab(QtWidgets.QWidget): self.share_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth) self.share_mode.set_server_active.connect(self.set_server_active) - # Receive mode + self.update_server_status_indicator() + self.timer.start(500) + + def receive_mode_clicked(self): + self.common.log("Tab", "receive_mode_clicked") + self.mode = self.common.gui.MODE_RECEIVE + self.new_tab.hide() + self.receive_mode = ReceiveMode( self.common, self.common.gui.qtapp, @@ -136,6 +191,9 @@ class Tab(QtWidgets.QWidget): None, self.common.gui.local_only, ) + self.layout.addWidget(self.receive_mode) + self.receive_mode.show() + self.receive_mode.init() self.receive_mode.server_status.server_started.connect( self.update_server_status_indicator @@ -158,7 +216,14 @@ class Tab(QtWidgets.QWidget): ) self.receive_mode.set_server_active.connect(self.set_server_active) - # Website mode + self.update_server_status_indicator() + self.timer.start(500) + + def website_mode_clicked(self): + self.common.log("Tab", "website_mode_clicked") + self.mode = self.common.gui.MODE_WEBSITE + self.new_tab.hide() + self.website_mode = WebsiteMode( self.common, self.common.gui.qtapp, @@ -166,8 +231,11 @@ class Tab(QtWidgets.QWidget): self.status_bar, self.status_bar.server_status_label, self.system_tray, - filenames, + self.filenames, ) + self.layout.addWidget(self.website_mode) + self.website_mode.show() + self.website_mode.init() self.website_mode.server_status.server_started.connect( self.update_server_status_indicator @@ -190,98 +258,9 @@ class Tab(QtWidgets.QWidget): ) self.website_mode.set_server_active.connect(self.set_server_active) - self.update_mode_switcher() self.update_server_status_indicator() - - # Layouts - contents_layout = QtWidgets.QVBoxLayout() - contents_layout.setContentsMargins(10, 0, 10, 0) - contents_layout.addWidget(self.receive_mode) - contents_layout.addWidget(self.share_mode) - contents_layout.addWidget(self.website_mode) - - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addLayout(mode_switcher_layout) - layout.addLayout(contents_layout) - self.setLayout(layout) - - # The server isn't active yet - self.set_server_active(False) - - # Create the timer - self.timer = QtCore.QTimer() - self.timer.timeout.connect(self.timer_callback) - - # Start the timer self.timer.start(500) - def update_mode_switcher(self): - # Based on the current mode, switch the mode switcher button styles, - # and show and hide widgets to switch modes - if self.mode == self.common.gui.MODE_SHARE: - self.share_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_selected_style"] - ) - self.receive_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_unselected_style"] - ) - self.website_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_unselected_style"] - ) - - self.receive_mode.hide() - self.share_mode.show() - self.website_mode.hide() - elif self.mode == self.common.gui.MODE_WEBSITE: - self.share_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_unselected_style"] - ) - self.receive_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_unselected_style"] - ) - self.website_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_selected_style"] - ) - - self.receive_mode.hide() - self.share_mode.hide() - self.website_mode.show() - else: - self.share_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_unselected_style"] - ) - self.receive_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_selected_style"] - ) - self.website_mode_button.setStyleSheet( - self.common.gui.css["mode_switcher_unselected_style"] - ) - - self.share_mode.hide() - self.receive_mode.show() - self.website_mode.hide() - - self.update_server_status_indicator() - - def share_mode_clicked(self): - if self.mode != self.common.gui.MODE_SHARE: - self.common.log("Tab", "share_mode_clicked") - self.mode = self.common.gui.MODE_SHARE - self.update_mode_switcher() - - def receive_mode_clicked(self): - if self.mode != self.common.gui.MODE_RECEIVE: - self.common.log("Tab", "receive_mode_clicked") - self.mode = self.common.gui.MODE_RECEIVE - self.update_mode_switcher() - - def website_mode_clicked(self): - if self.mode != self.common.gui.MODE_WEBSITE: - self.common.log("Tab", "website_mode_clicked") - self.mode = self.common.gui.MODE_WEBSITE - self.update_mode_switcher() - def update_server_status_indicator(self): # Set the status image if self.mode == self.common.gui.MODE_SHARE: @@ -335,7 +314,7 @@ class Tab(QtWidgets.QWidget): self.status_bar.server_status_label.setText( strings._("gui_status_indicator_share_started") ) - else: + elif self.mode == self.common.gui.MODE_RECEIVE: # Receive mode if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED: self.status_bar.server_status_image_label.setPixmap( @@ -491,6 +470,8 @@ class Tab(QtWidgets.QWidget): """ Disable the Settings and Receive Files buttons while an Share Files server is active. """ + pass + """ if active: self.settings_button.hide() if self.mode == self.common.gui.MODE_SHARE: @@ -512,7 +493,8 @@ class Tab(QtWidgets.QWidget): self.website_mode_button.show() # Disable settings menu action when server is active - # self.settings_action.setEnabled(not active) + self.settings_action.setEnabled(not active) + """ def clear_message(self): """ diff --git a/share/locale/en.json b/share/locale/en.json index a0ac9944..df3b6a5d 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -179,5 +179,11 @@ "days_first_letter": "d", "hours_first_letter": "h", "minutes_first_letter": "m", - "seconds_first_letter": "s" -} + "seconds_first_letter": "s", + "gui_new_tab_share_button": "Share Files", + "gui_new_tab_share_description": "Choose files on your computer to send to someone else. The person or people who you want to send files to will need to use Tor Browser to download them from you.", + "gui_new_tab_receive_button": "Receive Files", + "gui_new_tab_receive_description": "Turn your computer into an online dropbox. People will be able to use Tor Browser to send files to your computer.", + "gui_new_tab_website_button": "Publish Website", + "gui_new_tab_website_description": "Host a static HTML onion website from your computer." +} \ No newline at end of file From 3ceab336f944f657c037330173ea4a136cfb1de2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 26 Oct 2019 22:39:59 -0700 Subject: [PATCH 011/135] One attempt at making a new tab button --- onionshare_gui/gui_common.py | 10 ++++++++++ onionshare_gui/main_window.py | 20 +++++++++++++++++--- share/locale/en.json | 2 ++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/onionshare_gui/gui_common.py b/onionshare_gui/gui_common.py index 3344d879..9241dbf6 100644 --- a/onionshare_gui/gui_common.py +++ b/onionshare_gui/gui_common.py @@ -50,6 +50,16 @@ class GuiCommon: self.css = { # OnionShareGui styles + "tab_bar_new_tab": """ + QTabBar::tab:last { + border: 0; + margin: 3px; + }""", + "tab_bar_new_tab_button": """ + QToolButton { + font-weight: bold; + font-size: 20px; + }""", "new_tab_button": """ QPushButton { font-weight: bold; diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index 5d81feba..dfd6c10c 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -97,10 +97,24 @@ class MainWindow(QtWidgets.QMainWindow): # Tabs self.tabs = QtWidgets.QTabWidget() + self.tabs.setStyleSheet(self.common.gui.css["tab_bar_new_tab"]) + self.tabs.setMovable(True) + self.tabs.setTabsClosable(True) + self.tabs.setUsesScrollButtons(True) - # Start with one tab - self.tab = Tab(self.common, self.system_tray, self.status_bar, filenames) - self.tabs.addTab(self.tab, "New Tab") + # New tab button + new_tab_button = QtWidgets.QToolButton() + new_tab_button.setStyleSheet(self.common.gui.css["tab_bar_new_tab_button"]) + new_tab_button.setText("+") + new_tab_button.setAutoRaise(True) + self.tabs.insertTab(0, QtWidgets.QWidget(), "") + self.tabs.tabBar().setTabButton(0, QtWidgets.QTabBar.RightSide, new_tab_button) + self.tabs.tabBar().setTabToolTip(0, strings._("gui_new_tab_tooltip")) + + # Start with a tab + new_tab = Tab(self.common, self.system_tray, self.status_bar, filenames) + self.tabs.insertTab(0, new_tab, strings._("gui_new_tab")) + self.tabs.setCurrentIndex(0) # Layout layout = QtWidgets.QVBoxLayout() diff --git a/share/locale/en.json b/share/locale/en.json index df3b6a5d..b88e35a6 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -180,6 +180,8 @@ "hours_first_letter": "h", "minutes_first_letter": "m", "seconds_first_letter": "s", + "gui_new_tab": "New Tab", + "gui_new_tab_tooltip": "Open a new tab", "gui_new_tab_share_button": "Share Files", "gui_new_tab_share_description": "Choose files on your computer to send to someone else. The person or people who you want to send files to will need to use Tor Browser to download them from you.", "gui_new_tab_receive_button": "Receive Files", From f83d88b29e73064b53526db1c43188893b7811b0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 14:21:40 -0700 Subject: [PATCH 012/135] Make new tab button login in the QTabWidget instead of QTabBar --- onionshare_gui/gui_common.py | 7 +-- onionshare_gui/main_window.py | 23 ++------ onionshare_gui/tab/tab.py | 2 +- onionshare_gui/tab_widget.py | 102 ++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 25 deletions(-) create mode 100644 onionshare_gui/tab_widget.py diff --git a/onionshare_gui/gui_common.py b/onionshare_gui/gui_common.py index 9241dbf6..9ab16470 100644 --- a/onionshare_gui/gui_common.py +++ b/onionshare_gui/gui_common.py @@ -50,13 +50,8 @@ class GuiCommon: self.css = { # OnionShareGui styles - "tab_bar_new_tab": """ - QTabBar::tab:last { - border: 0; - margin: 3px; - }""", "tab_bar_new_tab_button": """ - QToolButton { + QPushButton { font-weight: bold; font-size: 20px; }""", diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index dfd6c10c..497fde62 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -26,7 +26,7 @@ from .tor_connection_dialog import TorConnectionDialog from .settings_dialog import SettingsDialog from .widgets import Alert from .update_checker import UpdateThread -from .tab import Tab +from .tab_widget import TabWidget class MainWindow(QtWidgets.QMainWindow): @@ -96,25 +96,12 @@ class MainWindow(QtWidgets.QMainWindow): self.status_bar.addPermanentWidget(self.status_bar.server_status_indicator) # Tabs - self.tabs = QtWidgets.QTabWidget() - self.tabs.setStyleSheet(self.common.gui.css["tab_bar_new_tab"]) - self.tabs.setMovable(True) - self.tabs.setTabsClosable(True) - self.tabs.setUsesScrollButtons(True) - - # New tab button - new_tab_button = QtWidgets.QToolButton() - new_tab_button.setStyleSheet(self.common.gui.css["tab_bar_new_tab_button"]) - new_tab_button.setText("+") - new_tab_button.setAutoRaise(True) - self.tabs.insertTab(0, QtWidgets.QWidget(), "") - self.tabs.tabBar().setTabButton(0, QtWidgets.QTabBar.RightSide, new_tab_button) - self.tabs.tabBar().setTabToolTip(0, strings._("gui_new_tab_tooltip")) + self.tabs = TabWidget(self.common, self.system_tray, self.status_bar) # Start with a tab - new_tab = Tab(self.common, self.system_tray, self.status_bar, filenames) - self.tabs.insertTab(0, new_tab, strings._("gui_new_tab")) - self.tabs.setCurrentIndex(0) + # new_tab = Tab(self.common, self.system_tray, self.status_bar, filenames) + # self.tabs.insertTab(0, new_tab, strings._("gui_new_tab")) + # self.tabs.setCurrentIndex(0) # Layout layout = QtWidgets.QVBoxLayout() diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 141122ac..6a76583f 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -38,7 +38,7 @@ class Tab(QtWidgets.QWidget): A GUI tab, you know, sort of like in a web browser """ - def __init__(self, common, system_tray, status_bar, filenames): + def __init__(self, common, system_tray, status_bar, filenames=None): super(Tab, self).__init__() self.common = common self.common.log("Tab", "__init__") diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py new file mode 100644 index 00000000..48388144 --- /dev/null +++ b/onionshare_gui/tab_widget.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +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 . +""" +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare import strings + +from .tab import Tab + + +class TabWidget(QtWidgets.QTabWidget): + """ + A custom tab widget, that has a "+" button for adding new tabs + """ + + def __init__(self, common, system_tray, status_bar): + super(TabWidget, self).__init__() + self.common = common + self.common.log("TabWidget", "__init__") + + self.system_tray = system_tray + self.status_bar = status_bar + + # Definte the new tab button + self.new_tab_button = QtWidgets.QPushButton("+", parent=self) + self.new_tab_button.setFlat(True) + self.new_tab_button.setAutoFillBackground(True) + self.new_tab_button.setFixedSize(30, 30) + self.new_tab_button.clicked.connect(self.new_tab_clicked) + self.new_tab_button.setStyleSheet(self.common.gui.css["tab_bar_new_tab_button"]) + self.new_tab_button.setToolTip(strings._("gui_new_tab_tooltip")) + + # Use a custom tab bar + tab_bar = TabBar() + tab_bar.move_new_tab_button.connect(self.move_new_tab_button) + self.setTabBar(tab_bar) + + # Set up the tab widget + self.setMovable(True) + self.setTabsClosable(True) + self.setUsesScrollButtons(True) + + self.move_new_tab_button() + + def move_new_tab_button(self): + # Find the width of all tabs + tabs_width = sum( + [self.tabBar().tabRect(i).width() for i in range(self.count())] + ) + + # The current positoin of the new tab button + pos = self.new_tab_button.pos() + + # If there are so many tabs it scrolls, move the button to the left of the scroll buttons + if tabs_width > self.width(): + pos.setX(self.width() - 61) + else: + # Otherwise move the button to the right of the tabs + pos.setX(self.tabBar().sizeHint().width()) + + self.new_tab_button.move(pos) + self.new_tab_button.raise_() + + def resizeEvent(self, event): + # Make sure to move new tab button on each resize + super(TabWidget, self).resizeEvent(event) + self.move_new_tab_button() + + def new_tab_clicked(self): + # Add a new tab + tab = Tab(self.common, self.system_tray, self.status_bar) + self.addTab(tab, "New Tab") + + +class TabBar(QtWidgets.QTabBar): + """ + A custom tab bar + """ + + move_new_tab_button = QtCore.pyqtSignal() + + def __init__(self): + super(TabBar, self).__init__() + + def tabLayoutChange(self): + self.move_new_tab_button.emit() From 4b4020c629dc2c3e3db7f2ec4516f79e46aa0fec Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 14:23:45 -0700 Subject: [PATCH 013/135] When you open a new tab, make that the current tab --- onionshare_gui/tab_widget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index 48388144..be2916e4 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -85,7 +85,8 @@ class TabWidget(QtWidgets.QTabWidget): def new_tab_clicked(self): # Add a new tab tab = Tab(self.common, self.system_tray, self.status_bar) - self.addTab(tab, "New Tab") + index = self.addTab(tab, "New Tab") + self.setCurrentIndex(index) class TabBar(QtWidgets.QTabBar): From fd2046b97629f7469c265b356cacb0e0c26635ab Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 14:35:11 -0700 Subject: [PATCH 014/135] After choosing the tab type, the title of the tab changes --- onionshare_gui/tab/tab.py | 11 ++++++++++- onionshare_gui/tab_widget.py | 19 ++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 6a76583f..2a74e6e8 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -38,11 +38,14 @@ class Tab(QtWidgets.QWidget): A GUI tab, you know, sort of like in a web browser """ - def __init__(self, common, system_tray, status_bar, filenames=None): + change_title = QtCore.pyqtSignal(int, str) + + def __init__(self, common, tab_id, system_tray, status_bar, filenames=None): super(Tab, self).__init__() self.common = common self.common.log("Tab", "__init__") + self.tab_id = tab_id self.system_tray = system_tray self.status_bar = status_bar self.filenames = filenames @@ -173,6 +176,8 @@ class Tab(QtWidgets.QWidget): self.share_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth) self.share_mode.set_server_active.connect(self.set_server_active) + self.change_title.emit(self.tab_id, strings._("gui_new_tab_share_button")) + self.update_server_status_indicator() self.timer.start(500) @@ -216,6 +221,8 @@ class Tab(QtWidgets.QWidget): ) self.receive_mode.set_server_active.connect(self.set_server_active) + self.change_title.emit(self.tab_id, strings._("gui_new_tab_receive_button")) + self.update_server_status_indicator() self.timer.start(500) @@ -258,6 +265,8 @@ class Tab(QtWidgets.QWidget): ) self.website_mode.set_server_active.connect(self.set_server_active) + self.change_title.emit(self.tab_id, strings._("gui_new_tab_website_button")) + self.update_server_status_indicator() self.timer.start(500) diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index be2916e4..ed184268 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -37,7 +37,11 @@ class TabWidget(QtWidgets.QTabWidget): self.system_tray = system_tray self.status_bar = status_bar - # Definte the new tab button + # Keep track of tabs in a dictionary + self.tabs = {} + self.tab_id = 0 # each tab has a unique id + + # Define the new tab button self.new_tab_button = QtWidgets.QPushButton("+", parent=self) self.new_tab_button.setFlat(True) self.new_tab_button.setAutoFillBackground(True) @@ -83,11 +87,20 @@ class TabWidget(QtWidgets.QTabWidget): self.move_new_tab_button() def new_tab_clicked(self): - # Add a new tab - tab = Tab(self.common, self.system_tray, self.status_bar) + # Create the tab + tab = Tab(self.common, self.tab_id, self.system_tray, self.status_bar) + tab.change_title.connect(self.change_title) + self.tabs[self.tab_id] = tab + self.tab_id += 1 + + # Add it index = self.addTab(tab, "New Tab") self.setCurrentIndex(index) + def change_title(self, tab_id, title): + index = self.indexOf(self.tabs[tab_id]) + self.setTabText(index, title) + class TabBar(QtWidgets.QTabBar): """ From df658a0e7057aa34fee5cadd9c50c17622f0bed1 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 15:01:14 -0700 Subject: [PATCH 015/135] Allow closing tabs, and throw warning when trying to close tabs that contain an active server --- onionshare_gui/tab/tab.py | 85 +++++++++++++++++++++--------------- onionshare_gui/tab_widget.py | 19 +++++--- share/locale/en.json | 8 +++- 3 files changed, 70 insertions(+), 42 deletions(-) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 2a74e6e8..bb0e8d84 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -50,7 +50,7 @@ class Tab(QtWidgets.QWidget): self.status_bar = status_bar self.filenames = filenames - self.mode = self.common.gui.MODE_SHARE + self.mode = None # Start the OnionShare app self.app = OnionShare(common, self.common.gui.onion, self.common.gui.local_only) @@ -511,44 +511,57 @@ class Tab(QtWidgets.QWidget): """ self.status_bar.clearMessage() - def close_event(self, e): - self.common.log("Tab", "close_event") - try: - if self.mode == self.common.gui.MODE_WEBSITE: - server_status = self.share_mode.server_status - if self.mode == self.common.gui.MODE_WEBSITE: - server_status = self.website_mode.server_status + def close_tab(self): + self.common.log("Tab", "close_tab") + if self.mode is None: + return True + + if self.mode == self.common.gui.MODE_SHARE: + server_status = self.share_mode.server_status + elif self.mode == self.common.gui.MODE_RECEIVE: + server_status = self.receive_mode.server_status + else: + server_status = self.website_mode.server_status + + if server_status.status == server_status.STATUS_STOPPED: + return True + else: + self.common.log("Tab", "close_tab, opening warning dialog") + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle(strings._("gui_close_tab_warning_title")) + if self.mode == self.common.gui.MODE_SHARE: + dialog.setText(strings._("gui_close_tab_warning_share_description")) + elif self.mode == self.common.gui.MODE_RECEIVE: + dialog.setText(strings._("gui_close_tab_warning_receive_description")) else: - server_status = self.receive_mode.server_status - if server_status.status != server_status.STATUS_STOPPED: - self.common.log("MainWindow", "closeEvent, opening warning dialog") - dialog = QtWidgets.QMessageBox() - dialog.setWindowTitle(strings._("gui_quit_title")) - if self.mode == self.common.gui.MODE_WEBSITE: - dialog.setText(strings._("gui_share_quit_warning")) - else: - dialog.setText(strings._("gui_receive_quit_warning")) - dialog.setIcon(QtWidgets.QMessageBox.Critical) - quit_button = dialog.addButton( - strings._("gui_quit_warning_quit"), QtWidgets.QMessageBox.YesRole - ) - dont_quit_button = dialog.addButton( - strings._("gui_quit_warning_dont_quit"), - QtWidgets.QMessageBox.NoRole, - ) - dialog.setDefaultButton(dont_quit_button) - reply = dialog.exec_() + dialog.setText(strings._("gui_close_tab_warning_website_description")) + dialog.setIcon(QtWidgets.QMessageBox.Critical) + dialog.addButton( + strings._("gui_close_tab_warning_close"), QtWidgets.QMessageBox.YesRole + ) + cancel_button = dialog.addButton( + strings._("gui_close_tab_warning_cancel"), QtWidgets.QMessageBox.NoRole + ) + dialog.setDefaultButton(cancel_button) + reply = dialog.exec_() - # Quit - if reply == 0: - self.stop_server() - e.accept() - # Don't Quit - else: - e.ignore() + # Close + if reply == 0: + self.common.log("Tab", "close_tab", "close, closing tab") - except: - e.accept() + if self.mode == self.common.gui.MODE_SHARE: + self.share_mode.stop_server() + elif self.mode == self.common.gui.MODE_RECEIVE: + self.receive_mode.stop_server() + else: + self.website_mode.stop_server() + + self.app.cleanup() + return True + # Cancel + else: + self.common.log("Tab", "close_tab", "cancel, keeping tab open") + return False def cleanup(self): self.app.cleanup() diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index ed184268..4a94743f 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -60,6 +60,8 @@ class TabWidget(QtWidgets.QTabWidget): self.setTabsClosable(True) self.setUsesScrollButtons(True) + self.tabCloseRequested.connect(self.close_tab) + self.move_new_tab_button() def move_new_tab_button(self): @@ -81,11 +83,6 @@ class TabWidget(QtWidgets.QTabWidget): self.new_tab_button.move(pos) self.new_tab_button.raise_() - def resizeEvent(self, event): - # Make sure to move new tab button on each resize - super(TabWidget, self).resizeEvent(event) - self.move_new_tab_button() - def new_tab_clicked(self): # Create the tab tab = Tab(self.common, self.tab_id, self.system_tray, self.status_bar) @@ -101,6 +98,18 @@ class TabWidget(QtWidgets.QTabWidget): index = self.indexOf(self.tabs[tab_id]) self.setTabText(index, title) + def close_tab(self, index): + self.common.log("TabWidget", "close_tab", f"{index}") + tab = self.widget(index) + if tab.close_tab(): + self.removeTab(index) + del self.tabs[tab.tab_id] + + def resizeEvent(self, event): + # Make sure to move new tab button on each resize + super(TabWidget, self).resizeEvent(event) + self.move_new_tab_button() + class TabBar(QtWidgets.QTabBar): """ diff --git a/share/locale/en.json b/share/locale/en.json index b88e35a6..b7212f37 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -187,5 +187,11 @@ "gui_new_tab_receive_button": "Receive Files", "gui_new_tab_receive_description": "Turn your computer into an online dropbox. People will be able to use Tor Browser to send files to your computer.", "gui_new_tab_website_button": "Publish Website", - "gui_new_tab_website_description": "Host a static HTML onion website from your computer." + "gui_new_tab_website_description": "Host a static HTML onion website from your computer.", + "gui_close_tab_warning_title": "Are you sure?", + "gui_close_tab_warning_share_description": "You're in the process of sending files. Are you sure you want to close this tab?", + "gui_close_tab_warning_receive_description": "You're in the process of receiving files. Are you sure you want to close this tab?", + "gui_close_tab_warning_website_description": "You're actively hosting a website. Are you sure you want to close this tab?", + "gui_close_tab_warning_close": "Close", + "gui_close_tab_warning_cancel": "Cancel" } \ No newline at end of file From bbc26473c4efca9c584c39fa566f2574b0da8af4 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 15:04:04 -0700 Subject: [PATCH 016/135] Open a new tab to begin with, and open a new tab when the last tab is closed --- onionshare_gui/main_window.py | 7 ++----- onionshare_gui/tab_widget.py | 4 ++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index 497fde62..b10333b5 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -98,14 +98,11 @@ class MainWindow(QtWidgets.QMainWindow): # Tabs self.tabs = TabWidget(self.common, self.system_tray, self.status_bar) - # Start with a tab - # new_tab = Tab(self.common, self.system_tray, self.status_bar, filenames) - # self.tabs.insertTab(0, new_tab, strings._("gui_new_tab")) - # self.tabs.setCurrentIndex(0) + # Start with opening the first tab + self.tabs.new_tab_clicked() # Layout layout = QtWidgets.QVBoxLayout() - # layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.tabs) central_widget = QtWidgets.QWidget() diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index 4a94743f..6207dde4 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -105,6 +105,10 @@ class TabWidget(QtWidgets.QTabWidget): 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() + def resizeEvent(self, event): # Make sure to move new tab button on each resize super(TabWidget, self).resizeEvent(event) From cb83fca6bc9f53f87d65ada16ed437a30634f889 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 15:21:46 -0700 Subject: [PATCH 017/135] Add purple headers to each mode --- onionshare_gui/gui_common.py | 19 ++++++------------- onionshare_gui/tab/mode/__init__.py | 13 +++++++++++++ .../tab/mode/receive_mode/__init__.py | 4 ++++ .../tab/mode/share_mode/__init__.py | 4 ++++ .../tab/mode/website_mode/__init__.py | 4 ++++ onionshare_gui/tab/tab.py | 6 +++--- onionshare_gui/tab_widget.py | 4 +++- 7 files changed, 37 insertions(+), 17 deletions(-) diff --git a/onionshare_gui/gui_common.py b/onionshare_gui/gui_common.py index 9ab16470..4507d998 100644 --- a/onionshare_gui/gui_common.py +++ b/onionshare_gui/gui_common.py @@ -50,33 +50,26 @@ class GuiCommon: self.css = { # OnionShareGui styles - "tab_bar_new_tab_button": """ + "tab_widget_new_tab_button": """ QPushButton { font-weight: bold; font-size: 20px; }""", - "new_tab_button": """ + "mode_new_tab_button": """ QPushButton { font-weight: bold; font-size: 30px; color: #601f61; }""", - "mode_switcher_selected_style": """ - QPushButton { + "mode_header_label": """ + QLabel { color: #ffffff; background-color: #4e064f; border: 0; - border-right: 1px solid #69266b; font-weight: bold; + font-size: 18px; border-radius: 0; - }""", - "mode_switcher_unselected_style": """ - QPushButton { - color: #ffffff; - background-color: #601f61; - border: 0; - font-weight: normal; - border-radius: 0; + padding: 10px 0 10px 0; }""", "settings_button": """ QPushButton { diff --git a/onionshare_gui/tab/mode/__init__.py b/onionshare_gui/tab/mode/__init__.py index e993f5e2..7ab321f8 100644 --- a/onionshare_gui/tab/mode/__init__.py +++ b/onionshare_gui/tab/mode/__init__.py @@ -75,6 +75,19 @@ class Mode(QtWidgets.QWidget): self.web_thread = None self.startup_thread = None + # Header + # Note: It's up to the downstream Mode to add this to its layout + self.header_label = QtWidgets.QLabel() + self.header_label.setStyleSheet(self.common.gui.css["mode_header_label"]) + self.header_label.setAlignment(QtCore.Qt.AlignHCenter) + + header_layout = QtWidgets.QVBoxLayout() + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(self.header_label) + + self.header = QtWidgets.QWidget() + self.header.setLayout(header_layout) + # Server status self.server_status = ServerStatus( self.common, self.qtapp, self.app, None, self.local_only diff --git a/onionshare_gui/tab/mode/receive_mode/__init__.py b/onionshare_gui/tab/mode/receive_mode/__init__.py index a0507949..46b1dd57 100644 --- a/onionshare_gui/tab/mode/receive_mode/__init__.py +++ b/onionshare_gui/tab/mode/receive_mode/__init__.py @@ -38,6 +38,9 @@ class ReceiveMode(Mode): # Create the Web object self.web = Web(self.common, True, "receive") + # Header + self.header_label.setText(strings._("gui_new_tab_receive_button")) + # Server status self.server_status.set_mode("receive") self.server_status.server_started_finished.connect(self.update_primary_action) @@ -86,6 +89,7 @@ class ReceiveMode(Mode): # Main layout self.main_layout = QtWidgets.QVBoxLayout() + self.main_layout.addWidget(self.header) self.main_layout.addLayout(top_bar_layout) self.main_layout.addWidget(receive_warning) self.main_layout.addWidget(self.primary_action) diff --git a/onionshare_gui/tab/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py index 1d495cd5..9bf875cb 100644 --- a/onionshare_gui/tab/mode/share_mode/__init__.py +++ b/onionshare_gui/tab/mode/share_mode/__init__.py @@ -47,6 +47,9 @@ class ShareMode(Mode): # Create the Web object self.web = Web(self.common, True, "share") + # Header + self.header_label.setText(strings._("gui_new_tab_share_button")) + # File selection self.file_selection = FileSelection(self.common, self) if self.filenames: @@ -118,6 +121,7 @@ class ShareMode(Mode): # Main layout self.main_layout = QtWidgets.QVBoxLayout() + self.main_layout.addWidget(self.header) self.main_layout.addLayout(top_bar_layout) self.main_layout.addLayout(self.file_selection) self.main_layout.addWidget(self.primary_action) diff --git a/onionshare_gui/tab/mode/website_mode/__init__.py b/onionshare_gui/tab/mode/website_mode/__init__.py index 6da08bd2..b9e7b647 100644 --- a/onionshare_gui/tab/mode/website_mode/__init__.py +++ b/onionshare_gui/tab/mode/website_mode/__init__.py @@ -49,6 +49,9 @@ class WebsiteMode(Mode): # Create the Web object self.web = Web(self.common, True, "website") + # Header + self.header_label.setText(strings._("gui_new_tab_website_button")) + # File selection self.file_selection = FileSelection(self.common, self) if self.filenames: @@ -120,6 +123,7 @@ class WebsiteMode(Mode): # Main layout self.main_layout = QtWidgets.QVBoxLayout() + self.main_layout.addWidget(self.header) self.main_layout.addLayout(top_bar_layout) self.main_layout.addLayout(self.file_selection) self.main_layout.addWidget(self.primary_action) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index bb0e8d84..b9416bc7 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -57,13 +57,13 @@ class Tab(QtWidgets.QWidget): # New tab widget share_button = QtWidgets.QPushButton(strings._("gui_new_tab_share_button")) - share_button.setStyleSheet(self.common.gui.css["new_tab_button"]) + share_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"]) share_description = QtWidgets.QLabel(strings._("gui_new_tab_share_description")) share_description.setWordWrap(True) share_button.clicked.connect(self.share_mode_clicked) receive_button = QtWidgets.QPushButton(strings._("gui_new_tab_receive_button")) - receive_button.setStyleSheet(self.common.gui.css["new_tab_button"]) + receive_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"]) receive_button.clicked.connect(self.receive_mode_clicked) receive_description = QtWidgets.QLabel( strings._("gui_new_tab_receive_description") @@ -71,7 +71,7 @@ class Tab(QtWidgets.QWidget): receive_description.setWordWrap(True) website_button = QtWidgets.QPushButton(strings._("gui_new_tab_website_button")) - website_button.setStyleSheet(self.common.gui.css["new_tab_button"]) + website_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"]) website_button.clicked.connect(self.website_mode_clicked) website_description = QtWidgets.QLabel( strings._("gui_new_tab_website_description") diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index 6207dde4..189c742f 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -47,7 +47,9 @@ class TabWidget(QtWidgets.QTabWidget): self.new_tab_button.setAutoFillBackground(True) self.new_tab_button.setFixedSize(30, 30) self.new_tab_button.clicked.connect(self.new_tab_clicked) - self.new_tab_button.setStyleSheet(self.common.gui.css["tab_bar_new_tab_button"]) + self.new_tab_button.setStyleSheet( + self.common.gui.css["tab_widget_new_tab_button"] + ) self.new_tab_button.setToolTip(strings._("gui_new_tab_tooltip")) # Use a custom tab bar From 656d3d1564503898df493e015b5c27184fc168a6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 15:26:56 -0700 Subject: [PATCH 018/135] Add settings button to the status bar --- onionshare_gui/gui_common.py | 2 -- onionshare_gui/main_window.py | 12 ++++++++++++ onionshare_gui/tab/tab.py | 11 ----------- share/images/settings.png | Bin 443 -> 1157 bytes 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/onionshare_gui/gui_common.py b/onionshare_gui/gui_common.py index 4507d998..5008c03a 100644 --- a/onionshare_gui/gui_common.py +++ b/onionshare_gui/gui_common.py @@ -73,9 +73,7 @@ class GuiCommon: }""", "settings_button": """ QPushButton { - background-color: #601f61; border: 0; - border-left: 1px solid #69266b; border-radius: 0; }""", "server_status_indicator_label": """ diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index b10333b5..6d8f034d 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -95,6 +95,18 @@ class MainWindow(QtWidgets.QMainWindow): ) self.status_bar.addPermanentWidget(self.status_bar.server_status_indicator) + # Settings button + self.settings_button = QtWidgets.QPushButton() + self.settings_button.setDefault(False) + self.settings_button.setFixedWidth(40) + self.settings_button.setFixedHeight(50) + self.settings_button.setIcon( + QtGui.QIcon(self.common.get_resource_path("images/settings.png")) + ) + self.settings_button.clicked.connect(self.open_settings) + self.settings_button.setStyleSheet(self.common.gui.css["settings_button"]) + self.status_bar.addPermanentWidget(self.settings_button) + # Tabs self.tabs = TabWidget(self.common, self.system_tray, self.status_bar) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index b9416bc7..8cdf3afa 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -103,17 +103,6 @@ class Tab(QtWidgets.QWidget): self.new_tab.setLayout(new_tab_outer_layout) self.new_tab.show() - # Settings button, but this is gonna disappear - self.settings_button = QtWidgets.QPushButton() - self.settings_button.setDefault(False) - self.settings_button.setFixedWidth(40) - self.settings_button.setFixedHeight(50) - self.settings_button.setIcon( - QtGui.QIcon(self.common.get_resource_path("images/settings.png")) - ) - # self.settings_button.clicked.connect(self.open_settings) - self.settings_button.setStyleSheet(self.common.gui.css["settings_button"]) - # Server status indicator icons self.status_bar.server_status_image_stopped = QtGui.QImage( self.common.get_resource_path("images/server_stopped.png") diff --git a/share/images/settings.png b/share/images/settings.png index ec35400a174af4f7117b000982aaf28d78c64263..b6f8fa55a773b20342b78cb6317ec63af8bf158d 100644 GIT binary patch delta 1092 zcmV-K1iSmY1BD5YBYy(kdQ@0+Qek%>aB^>EX>4U6ba`-PAZ2)IW&i+q+U=K7lH({0 zhTl2G9sx-RiQ`}cRda(q{{CRv>7;w6YG>zRA7UyP%R<2a36g~V_m2_&!bL@5NNS!- z&JkBCsc=QZ<8@tkifPr?L)!1?*&gl}43pq4*JJM6zrwDM4u7`&>3FsWJ9ix4f%c1! zfHFsBG`64bK*E>(Vks&i;t45V!N|b25n06h`5|T}UG8*^vb^kRo;3P_r0M zi>`J?9i@D)UAUVs;@zzm?PA?_d6pu?ydCn3M%zyMy>?5XCu)z8pI(^ULwwP0jOJtX zYjrhcD3!xr)PKiPRtt^?q?FYyqZmGlwyjc797{YQdIQY^weOS zDJJd=xtaN7SvH6Kh0g^g%|e3>cv=t#j}z1cX1?b_YuvKNJIOI}BX}|)7@^0P623G3 zPPqe(F;kZ)`mGh_#WMjhjY)3S@B$DTTT@;7t`a`wO@I6ZRzOgf%#H=tcw8Y4rAKbb z;w zb@Alt*?-O5i`T+MaD-{e#X|8?O05{QB6LOX3XfV3KIBM;9{I4tk8;!#>2s!2&wSeH zXE|%*CJo!aHHVGi1c`}PmDQ)p2H<`u4*c8g3Mmpg%n#4dbbz&WK zvwM*HCAXmIUvcA~kqZ;upCA_|x=rpYw{NJmy1r3l0h+sTY8qwWV1q;V%3kMp(r@3x ze+}>s^bYh6^bYh6^bYh6^#2Jo@Z$l0_`*Mu2( zgoKJH6h!=O_Dy)R@7}qaWQv_U^X$y*GfUdm@tyPKR6=_l!ih+1v=zePNNh-hc7=0{ zpc}`Lb~E2b7sh{afm)tyF_-X|AS=SmW@J5hYk;7hzo62ARm>zt@ru3B)+EXWxH9fx zK5^?oB(Yk6{lruT8o*Peg;dqQu@7N%e{3!Cl0~+g!*0fOSk}C~=L5YF8T_Z^=b%Y2 z9R?a3bTvYDn0UmEQrL{56|oT)bzg09d20000< KMNUMnLSTX)1`)#m delta 375 zcmV--0f_#E3A+Q3Ba;mSE`Q<#7y>O8AP@3M00033@$Spa7JBJD?Vd zZ3YPq1|#IGNsMF!#OrDwZUXF1N^SzB?&WG? zXbjbe*tyfhgS@1o^K1mSfO3y#yOKtn(x(j=mDF(ND=j*b=5l7%l2i^Z-%|*D2Imtq zd+qh6maL`Z;U79)T?3QAEN}rlr~QGhB4UqO{==t8%oWTJ5WJW4BI!Fho1_~r;RiYe V;ohxS& Date: Sun, 27 Oct 2019 15:52:45 -0700 Subject: [PATCH 019/135] Add persistent pin --- onionshare_gui/main_window.py | 14 +++++++-- onionshare_gui/tab/tab.py | 45 ++++++++++++++++++++------- onionshare_gui/tab_widget.py | 5 +++ share/images/persistent_disabled.png | Bin 0 -> 1299 bytes share/images/persistent_enabled.png | Bin 0 -> 4368 bytes 5 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 share/images/persistent_disabled.png create mode 100644 share/images/persistent_enabled.png diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index 6d8f034d..c2db80ec 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -77,6 +77,17 @@ class MainWindow(QtWidgets.QMainWindow): self.status_bar.setStyleSheet(self.common.gui.css["status_bar"]) self.setStatusBar(self.status_bar) + # Server status indicator icons + self.status_bar.server_status_image_stopped = QtGui.QImage( + self.common.get_resource_path("images/server_stopped.png") + ) + self.status_bar.server_status_image_working = QtGui.QImage( + self.common.get_resource_path("images/server_working.png") + ) + self.status_bar.server_status_image_started = QtGui.QImage( + self.common.get_resource_path("images/server_started.png") + ) + # Server status indicator on the status bar self.status_bar.server_status_image_label = QtWidgets.QLabel() self.status_bar.server_status_image_label.setFixedWidth(20) @@ -98,8 +109,7 @@ class MainWindow(QtWidgets.QMainWindow): # Settings button self.settings_button = QtWidgets.QPushButton() self.settings_button.setDefault(False) - self.settings_button.setFixedWidth(40) - self.settings_button.setFixedHeight(50) + self.settings_button.setFixedSize(40, 50) self.settings_button.setIcon( QtGui.QIcon(self.common.get_resource_path("images/settings.png")) ) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 8cdf3afa..a164a96d 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -103,17 +103,6 @@ class Tab(QtWidgets.QWidget): self.new_tab.setLayout(new_tab_outer_layout) self.new_tab.show() - # Server status indicator icons - self.status_bar.server_status_image_stopped = QtGui.QImage( - self.common.get_resource_path("images/server_stopped.png") - ) - self.status_bar.server_status_image_working = QtGui.QImage( - self.common.get_resource_path("images/server_working.png") - ) - self.status_bar.server_status_image_started = QtGui.QImage( - self.common.get_resource_path("images/server_started.png") - ) - # Layout self.layout = QtWidgets.QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) @@ -127,6 +116,17 @@ class Tab(QtWidgets.QWidget): self.timer = QtCore.QTimer() self.timer.timeout.connect(self.timer_callback) + # Settings for this tab + self.tab_settings = {"persistence": False} + + # Persistence button + self.persistence_button = QtWidgets.QPushButton() + self.persistence_button.setDefault(False) + self.persistence_button.setFlat(True) + self.persistence_button.setFixedSize(30, 30) + self.persistence_button.clicked.connect(self.persistence_button_clicked) + self.update_persistence_button() + def share_mode_clicked(self): self.common.log("Tab", "share_mode_clicked") self.mode = self.common.gui.MODE_SHARE @@ -500,6 +500,29 @@ class Tab(QtWidgets.QWidget): """ self.status_bar.clearMessage() + def persistence_button_clicked(self): + self.common.log("Tab", "persistence_button_clicked") + if self.tab_settings["persistence"]: + self.tab_settings["persistence"] = False + else: + self.tab_settings["persistence"] = True + self.update_persistence_button() + + def update_persistence_button(self): + self.common.log("Tab", "update_persistence_button") + if self.tab_settings["persistence"]: + self.persistence_button.setIcon( + QtGui.QIcon( + self.common.get_resource_path("images/persistent_enabled.png") + ) + ) + else: + self.persistence_button.setIcon( + QtGui.QIcon( + self.common.get_resource_path("images/persistent_disabled.png") + ) + ) + def close_tab(self): self.common.log("Tab", "close_tab") if self.mode is None: diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index 189c742f..73dea2bd 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -100,6 +100,11 @@ class TabWidget(QtWidgets.QTabWidget): index = self.indexOf(self.tabs[tab_id]) self.setTabText(index, title) + # Now that a mode has been selected, add persistence button + self.tabBar().setTabButton( + index, QtWidgets.QTabBar.LeftSide, self.tabs[tab_id].persistence_button + ) + def close_tab(self, index): self.common.log("TabWidget", "close_tab", f"{index}") tab = self.widget(index) diff --git a/share/images/persistent_disabled.png b/share/images/persistent_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..2f2e34ab2c484ab5666fb67ef7d68af1112a8bd7 GIT binary patch literal 1299 zcmV+u1?>8XP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+U=K3lH@21h4-9dj$la$iQ`~?BKih%{CQx?S(#Pc z5!2I)S!5DvV_7`leX_GE^uK?M@E1Omo?C+3*BpyS8&xz2RNb-XV6zDLjT zaNl5r1ZTM}(~eU?Tpt50?)do}4_3F1Z=rqTLr~_-jLy#UEhK!!Xu4~eDT2a*yO10>&Vejgfn+H&h9isdj41kws~L~qdl&BRi+OkN#k-^IxI9}C zjPepe~yo8!X3pNt{ZL+LFatkf%os$5rlRZ41C6%&k}q2?!hsg-wYL zV>TlA=-^YZa+b&^2#_i_3Il9J65u$`xiQ9Q<+Ab>#;XM&RFW9b-~j@x7!~zn(NRN1 zMPpFaq^7P}OOh0mrj#trcT_R4XllvK+_DuHkFK8F+&z2AoCU8i%_&>Ro^vh*lNLx9 zj4sG1<>XV&cJN?XOsVJWstJYMldd;;oZqo4hx6~{&Z@HCD9i`M`*Pgm{@41(O zS{pLr(2<4>A9<9U+NSzUjb6z8ni_3tlUQ7N_MnEa*@pzp*NIMMAjXkE+$I4OG*4!S zQ;a;xO=d@CJQT{HMmpg%n#4dbc48fLvwM*HCAXmQUvcA~kqZ;upCA_|x=rpYw{NJm zvVKuy0h&+Y^z@K{7aJ0~7xprLlYaXg{;R-S=q>aXdJDaU-a>Do|BukX9}oCfGyDS} z^^rg05RH5Q000JJOGiWi{{a60|De66lK=n!32;bRa{vGf6951U69E94oEQKA00(qQ zO+^Re3L6$Q4%L+kfdBvjwMj%lR7l6Y)=g{FP!z`T-^{cYwQrzJbu1#Nh`1FhqAvUz zepkPM1y?QvTk9w~mP#$Pq8*KHP9T(Kl1!4G&4ruf{&U`*a|ep8hfO@gJjPnI!4&8t z+(91+e&C>Ma0boWb?oA#EAS@np0VG@8n(Lt7eO<-6zkVE!DZ0Qk^mpK0j>(o3U3|n zkTwB7#J!r|UBFU!J;aqbYf|-RdUz3Q8}<)h@TRN|23Ww=mcYG;#BHqLb5R5a@$h%# zt>2R33ehhYb|*H7F%_cYY(3Sez5`c)ht^5D{_+! zh*qIW!kOY@7f==QxKb1PI@+@_xL*_c98aqV(xBw|T!=wcXoAH!cihCIgOI_h(99m- zW)twFNC0w$)Gv_5$5@^<)bR6+aObP{FW$#l$tkCX(U$2f;Nz(7)smJO<2!ETBDoOX z?al%ggCq&<-J0^=VZ1ZUwbgRe$=>NP)`iXC2{v+jFT;Ua9@n)r7vA+)H)3znC0On@ zx*yQ_xSQ`F?HUY6G3&2Ip)F!`9^m|}U&1X+&I_F7eS|-_{~r@jV*rh3v5Wu!002ov JPDHLkV1n_-Xv+Wq literal 0 HcmV?d00001 diff --git a/share/images/persistent_enabled.png b/share/images/persistent_enabled.png new file mode 100644 index 0000000000000000000000000000000000000000..68a5a74f60280cc95948590c36b429689ae0703f GIT binary patch literal 4368 zcmV+r5%2DaP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3&scH26WhW}#~y#x$}0!lbz5ITPlAXj! z?oGaK>sX{A3Iu9+2RhjQ{Pzw2#gD|%gVfe_s78Kd$jFnE9$!DNV~X;;zy7fIXX(fB z@cBVzsBkQ=KlMGHCgb(n7n;1s*N@|&?e{$WJ<TL`8A&2e~0n@ z-jwgQ^ZqUEnd0-}uYXG;7^gB=a>!OviM;z4b72K#l<8x<=hCu|RL(0$f0tj!E_|MT z3Et<~m)OPm9xp$JB1C;(pkHG29#8exv3nx)L)O0C`G*f|M~mP8d>6ZWPruh*yPKj! zO2d0oA6t3OxNs55@_bEsHGV7S?RgDeoh@#He#qwHwL=;y6WN<2oix);_8fQGEK#y$ z!ku&8xjbW?@Q=Koq=>)L&5iV&P&&EUWUVlqKi86;`;O;+J9X}S5?AKLWQlM5^a{Tk z{7+t=6W!~jn^Nqbv0`1>3y@`a=KP(jNJzTxnARu0&ntXaH}Si~7BX0$Fn2aM<9bc8 zYWh}N<;ip5x*8?KSGlKl-V+ca?p;|-Dr6ukg`G6TmUByqICkD`a>DlkicUpR`%{9MeQm5RaKih zM$J~5wA!?_*4t>amoB|_?XCAd`W$%_Agm4@EsQ?Kn3J1Mrk;Fza>bl0ud-y-rK_#J z#+sY**|O`_-FDw&&y!C%wExl5!s%z6X~`%h6RW1ytlO}8skK|K+F4W&d6AnM8=zEfP&7Rv)GeJ_RM+C;>a{b z&15ZAPtHl6F)}EYa=qy@ci%Gik9iAK{wi&XK;arb;ebSt~UHx^G@NoiMfC-BxLL zS-WI>qu({pzB17a?E;E>U1RMgy7F8zDny=o zt8p_?->24fIw@V!w07D%b(7q0ou>21SZYC9u5D(Mv&~dalA7x7+_m~CRrY7c@Mjk) z$*5COJ}Z|sS(Y{Gs~x4gB1NJ>bcm7IZY<*r0o2c=SVgHc0$B;lsq<^pz;0cMHes}; z6Kn7|!q6*AzXD!amGN2(W%_Mfv^MZmVt zHgs7<4-tD(VL(Es*GCFtTgh8S=~KNJ8|ac{?oLfCDz?4DQqsQ{?H*z&ss9%U7Xt zHeGKAeue(JLpgQyDx&d=ehxnjP`xiGqL2Z)ziI#Qj~7&d74u!Pd!l5{>Zxx%u)$)v z${jPbEWY+YruVkr?0i*FmW?lMM2nrbMMdeHv)Wk&zSyTD0XU%`;THjHtzLQ%wm$+b zH>x(nG~1briMj8g9B7JCyd08T1rb5h)&P!FLN^jbMjBN}Xg6Ff0M-&GDW4_ooxc#V z^We%b{9QLSbBfJjQi%W_O-~y!H);dc@U5Xbiae$8l4_l8vlhtOX3=?D%BYGpDoYc< zf40+D#fr?p6dA&%SK6%oBBT!Drh-D~rLMCREi^?NB|D2SE5)T1;*bD4XNr%8w0Bc| z2XcRL48QyU)}kHI7TFP}T*v%HEmSb1Uv=!1%w^jqt}3TB!y3M3)K2kXg`hz z5qI^R96_!r$j9kV0q(37e5SI&d(hUIuom7%>j|uf&hb(u1F#Gv;3o;N%Dc&AQeHrr3H8c*;3f}Ku_ab1+k}y50J%K#)A}R zuu4bOb9P0jItrn;2#LN@xCYYf-_w8G(^7dz+|vtfHPO?;dg(;<-o{arMilqNBC` zaM?z_xqXVRRPVzmt5&Dj+NYti^m5svcua1Spf7i+%$MDk!>{=co zKrYWB&HT`r8~GD@TTf=%fANVcX1#J;WG^A zCNt-~raRJfUbnag$O~1e`{dzIz;s~kn1ueSEnn>4ZG>dC00-m7s34><0{O6;t%A3; zcgFbr#eyJaL!ZKL3xR^d2`F!IPkwP8_==4Y0a04?$l3N2q`P&T)6*j}&2KTseW2zy zJyufiJvhxGj3Fkl>)>`K{y0NlYZCvP^r$U!sRkXLL%hxf z4(&kbgfJsDkc}4AJ$b44O2u6dHe;gsfw2*B zs%%m@TaHl^g+Xttm!=Ld^}IYlcIzU+oEok_CwAaVXtG399yyAj0?MG{$-v?U3BWU; zI0H3H2SNrVGkZcWOk6gC2B_j&WEWva-@6J)h|#i2Y4)`?iox*!|JtjgRrmBQ#NZEN z9--&kKl9N;vY*Ea*HN)4!VP+&8zpJZKTa}Tya=)BuDE6| z74K#Dgq{ftB7_AaUm+CS=5{d`jnRhGDG6Yu@aA2Gj0<2wq^NGWiPB7FSU> z*f7fNoLMSrg7TvDoF#AVh&>QY`-0OcSN3POm1DscC#_D`(2p5#7C4dD;^^ z@%|)0Fk%>uRf0JnrRj5T&{!lWXz0B2Um!Wyt*z0t9FgFgn?3;`DQOQl4iCa;v>ql z=SR3PHI0D(=})*Ekx*X11Ly)Fq5RiDVht9OE?4^lP)*1ytkT3sj8+jn_?qcn2}uO3 zf)8kP`UTZ=h`TjVI_xHH#vlO8_F;rX{+e~eA6`iIu!rc$)?wu5Q6hR6R-yht1TV-# zWH5dC0%#C&Man&onEW{``7-RS5{s+DEh*4T%4uDH*89LlY>Tu(kaQ$?aNStmN!PEjB3JT@W)JW(yaT+v zaI0`jbXDHyoG|8{4G&CV*8zyffsJsJx~fri8S#@{VEZFBJUdhXObmJvcdoN$8V2Z% z_j4!C19)zv?J(NY(~PK z0CDbow^mxo$ls=$NE;phn5F^@P%{$UGr%zl|DAT- zA`u5mUZCdc|9&{Z|Mzx9+)a0Asd4eD8A{qNTVVe}3l{TEtPZiH-5)Cy@&A!M{-^Ez z(_^3i!|`_$5g30M1p;mVpHX?F+Npj4$XKWc=(^IC>@fq;C3KNSMbUgD>M^a@Ze$j?k>gwgIId2&3xmbT3~;-^*^j^rV9>9>1DopfXRqMDgw+}_rA}8e z^VmlAATSaTd<3{^BRgy(yBa64OAW47r?-HIK2Jkx4A|%x)e)dhCxMf|W?=Y6*OD10 z@h=9{KH1}4RFA_ASjfq>r|&Zz;YE( zdhp|59w;y&Mh=Al=Yj&CeFxi97Tm}QJqw%$o|grG%?WMlju^RC80-uFT9-f?=&=7v zj0~6Y@w2SxRi_PL)OV`(iDgNQ3={&Z(@(%kR?4W;KkD=@aJ{YO9|jhZ5>Y+w>v(3K zs?&GCIp3R`8m$|7+Ed9aD}TL9!1 Date: Sun, 27 Oct 2019 16:01:30 -0700 Subject: [PATCH 020/135] Add warning about closing a persistent tab --- onionshare_gui/tab/tab.py | 94 +++++++++++++++++++++------------------ share/locale/en.json | 1 + 2 files changed, 51 insertions(+), 44 deletions(-) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index a164a96d..316012c6 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -117,7 +117,7 @@ class Tab(QtWidgets.QWidget): self.timer.timeout.connect(self.timer_callback) # Settings for this tab - self.tab_settings = {"persistence": False} + self.tab_settings = {"persistent": False} # Persistence button self.persistence_button = QtWidgets.QPushButton() @@ -502,15 +502,15 @@ class Tab(QtWidgets.QWidget): def persistence_button_clicked(self): self.common.log("Tab", "persistence_button_clicked") - if self.tab_settings["persistence"]: - self.tab_settings["persistence"] = False + if self.tab_settings["persistent"]: + self.tab_settings["persistent"] = False else: - self.tab_settings["persistence"] = True + self.tab_settings["persistent"] = True self.update_persistence_button() def update_persistence_button(self): self.common.log("Tab", "update_persistence_button") - if self.tab_settings["persistence"]: + if self.tab_settings["persistent"]: self.persistence_button.setIcon( QtGui.QIcon( self.common.get_resource_path("images/persistent_enabled.png") @@ -528,52 +528,58 @@ class Tab(QtWidgets.QWidget): if self.mode is None: return True - if self.mode == self.common.gui.MODE_SHARE: - server_status = self.share_mode.server_status - elif self.mode == self.common.gui.MODE_RECEIVE: - server_status = self.receive_mode.server_status + if self.tab_settings["persistent"]: + dialog_text = strings._("gui_close_tab_warning_persistent_description") else: - server_status = self.website_mode.server_status - - if server_status.status == server_status.STATUS_STOPPED: - return True - else: - self.common.log("Tab", "close_tab, opening warning dialog") - dialog = QtWidgets.QMessageBox() - dialog.setWindowTitle(strings._("gui_close_tab_warning_title")) if self.mode == self.common.gui.MODE_SHARE: - dialog.setText(strings._("gui_close_tab_warning_share_description")) + server_status = self.share_mode.server_status elif self.mode == self.common.gui.MODE_RECEIVE: - dialog.setText(strings._("gui_close_tab_warning_receive_description")) + server_status = self.receive_mode.server_status else: - dialog.setText(strings._("gui_close_tab_warning_website_description")) - dialog.setIcon(QtWidgets.QMessageBox.Critical) - dialog.addButton( - strings._("gui_close_tab_warning_close"), QtWidgets.QMessageBox.YesRole - ) - cancel_button = dialog.addButton( - strings._("gui_close_tab_warning_cancel"), QtWidgets.QMessageBox.NoRole - ) - dialog.setDefaultButton(cancel_button) - reply = dialog.exec_() + server_status = self.website_mode.server_status - # Close - if reply == 0: - self.common.log("Tab", "close_tab", "close, closing tab") - - if self.mode == self.common.gui.MODE_SHARE: - self.share_mode.stop_server() - elif self.mode == self.common.gui.MODE_RECEIVE: - self.receive_mode.stop_server() - else: - self.website_mode.stop_server() - - self.app.cleanup() + if server_status.status == server_status.STATUS_STOPPED: return True - # Cancel else: - self.common.log("Tab", "close_tab", "cancel, keeping tab open") - return False + if self.mode == self.common.gui.MODE_SHARE: + dialog_text = strings._("gui_close_tab_warning_share_description") + elif self.mode == self.common.gui.MODE_RECEIVE: + dialog_text = strings._("gui_close_tab_warning_receive_description") + else: + dialog_text = strings._("gui_close_tab_warning_website_description") + + # Open the warning dialog + self.common.log("Tab", "close_tab, opening warning dialog") + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle(strings._("gui_close_tab_warning_title")) + dialog.setText(dialog_text) + dialog.setIcon(QtWidgets.QMessageBox.Critical) + dialog.addButton( + strings._("gui_close_tab_warning_close"), QtWidgets.QMessageBox.YesRole + ) + cancel_button = dialog.addButton( + strings._("gui_close_tab_warning_cancel"), QtWidgets.QMessageBox.NoRole + ) + dialog.setDefaultButton(cancel_button) + reply = dialog.exec_() + + # Close + if reply == 0: + self.common.log("Tab", "close_tab", "close, closing tab") + + if self.mode == self.common.gui.MODE_SHARE: + self.share_mode.stop_server() + elif self.mode == self.common.gui.MODE_RECEIVE: + self.receive_mode.stop_server() + else: + self.website_mode.stop_server() + + self.app.cleanup() + return True + # Cancel + else: + self.common.log("Tab", "close_tab", "cancel, keeping tab open") + return False def cleanup(self): self.app.cleanup() diff --git a/share/locale/en.json b/share/locale/en.json index b7212f37..387e57b3 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -189,6 +189,7 @@ "gui_new_tab_website_button": "Publish Website", "gui_new_tab_website_description": "Host a static HTML onion website from your computer.", "gui_close_tab_warning_title": "Are you sure?", + "gui_close_tab_warning_persistent_description": "This tab is persistent. If you close it you'll lose the onion address that it's using. Are you sure you want to close it?", "gui_close_tab_warning_share_description": "You're in the process of sending files. Are you sure you want to close this tab?", "gui_close_tab_warning_receive_description": "You're in the process of receiving files. Are you sure you want to close this tab?", "gui_close_tab_warning_website_description": "You're actively hosting a website. Are you sure you want to close this tab?", From b7a095d64d681a690f430ec879a1e3298ac00f0d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 16:18:56 -0700 Subject: [PATCH 021/135] Show warning when quitting while any tabs are active --- onionshare_gui/main_window.py | 26 +++++++++++++++++++++++++- onionshare_gui/tab/tab.py | 25 ++++++++++--------------- onionshare_gui/tab_widget.py | 10 ++++++++++ share/locale/en.json | 11 +++++------ 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index c2db80ec..a518ab33 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -268,8 +268,32 @@ class MainWindow(QtWidgets.QMainWindow): def closeEvent(self, e): self.common.log("MainWindow", "closeEvent") + + if self.tabs.are_tabs_active(): + # Open the warning dialog + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle(strings._("gui_quit_warning_title")) + dialog.setText(strings._("gui_quit_warning_description")) + dialog.setIcon(QtWidgets.QMessageBox.Critical) + dialog.addButton( + strings._("gui_quit_warning_quit"), QtWidgets.QMessageBox.YesRole + ) + cancel_button = dialog.addButton( + strings._("gui_quit_warning_cancel"), QtWidgets.QMessageBox.NoRole + ) + dialog.setDefaultButton(cancel_button) + reply = dialog.exec_() + + # Close + if reply == 0: + self.system_tray.hide() + e.accept() + # Cancel + else: + e.ignore() + return + self.system_tray.hide() - # TODO: Run the tab's close_event e.accept() def cleanup(self): diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 316012c6..4b4127fd 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -500,6 +500,14 @@ class Tab(QtWidgets.QWidget): """ self.status_bar.clearMessage() + def get_mode(self): + if self.mode == self.common.gui.MODE_SHARE: + return self.share_mode + elif self.mode == self.common.gui.MODE_RECEIVE: + return self.receive_mode + else: + return self.website_mode + def persistence_button_clicked(self): self.common.log("Tab", "persistence_button_clicked") if self.tab_settings["persistent"]: @@ -531,13 +539,7 @@ class Tab(QtWidgets.QWidget): if self.tab_settings["persistent"]: dialog_text = strings._("gui_close_tab_warning_persistent_description") else: - if self.mode == self.common.gui.MODE_SHARE: - server_status = self.share_mode.server_status - elif self.mode == self.common.gui.MODE_RECEIVE: - server_status = self.receive_mode.server_status - else: - server_status = self.website_mode.server_status - + server_status = self.get_mode().server_status if server_status.status == server_status.STATUS_STOPPED: return True else: @@ -566,14 +568,7 @@ class Tab(QtWidgets.QWidget): # Close if reply == 0: self.common.log("Tab", "close_tab", "close, closing tab") - - if self.mode == self.common.gui.MODE_SHARE: - self.share_mode.stop_server() - elif self.mode == self.common.gui.MODE_RECEIVE: - self.receive_mode.stop_server() - else: - self.website_mode.stop_server() - + self.get_mode().stop_server() self.app.cleanup() return True # Cancel diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index 73dea2bd..f9e48951 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -116,6 +116,16 @@ class TabWidget(QtWidgets.QTabWidget): if self.count() == 0: self.new_tab_clicked() + 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.server_status.status != mode.server_status.STATUS_STOPPED: + return True + return False + def resizeEvent(self, event): # Make sure to move new tab button on each resize super(TabWidget, self).resizeEvent(event) diff --git a/share/locale/en.json b/share/locale/en.json index 387e57b3..8948d317 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -30,11 +30,6 @@ "gui_copied_hidservauth": "HidServAuth line copied to clipboard", "gui_waiting_to_start": "Scheduled to start in {}. Click to cancel.", "gui_please_wait": "Starting… Click to cancel.", - "gui_quit_title": "Not so fast", - "gui_share_quit_warning": "You're in the process of sending files. Are you sure you want to quit OnionShare?", - "gui_receive_quit_warning": "You're in the process of receiving files. Are you sure you want to quit OnionShare?", - "gui_quit_warning_quit": "Quit", - "gui_quit_warning_dont_quit": "Cancel", "error_rate_limit": "Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.", "zip_progress_bar_format": "Compressing: %p%", "error_stealth_not_supported": "To use client authorization, you need at least both Tor 0.2.9.1-alpha (or Tor Browser 6.5) and python3-stem 1.5.0.", @@ -194,5 +189,9 @@ "gui_close_tab_warning_receive_description": "You're in the process of receiving files. Are you sure you want to close this tab?", "gui_close_tab_warning_website_description": "You're actively hosting a website. Are you sure you want to close this tab?", "gui_close_tab_warning_close": "Close", - "gui_close_tab_warning_cancel": "Cancel" + "gui_close_tab_warning_cancel": "Cancel", + "gui_quit_warning_title": "Are you sure?", + "gui_quit_warning_description": "Sharing is active in some of your tabs. If you quit, all of your tabs will close. Are you sure you want to quit?", + "gui_quit_warning_quit": "Quit", + "gui_quit_warning_cancel": "Cancel" } \ No newline at end of file From a14464d834585567e6c86c2d0d3c9438b9c983c1 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 16:32:12 -0700 Subject: [PATCH 022/135] Show the server status in the tab as an icon --- onionshare_gui/tab/tab.py | 71 +++++++++++++++++------------------- onionshare_gui/tab_widget.py | 5 +++ 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 4b4127fd..d495d64e 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -39,6 +39,7 @@ class Tab(QtWidgets.QWidget): """ change_title = QtCore.pyqtSignal(int, str) + change_icon = QtCore.pyqtSignal(int, str) def __init__(self, common, tab_id, system_tray, status_bar, filenames=None): super(Tab, self).__init__() @@ -264,83 +265,77 @@ class Tab(QtWidgets.QWidget): if self.mode == self.common.gui.MODE_SHARE: # Share mode if self.share_mode.server_status.status == ServerStatus.STATUS_STOPPED: - self.status_bar.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.status_bar.server_status_image_stopped) - ) - self.status_bar.server_status_label.setText( + self.set_server_status_indicator_stopped( strings._("gui_status_indicator_share_stopped") ) elif self.share_mode.server_status.status == ServerStatus.STATUS_WORKING: - self.status_bar.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.status_bar.server_status_image_working) - ) if self.share_mode.server_status.autostart_timer_datetime: - self.status_bar.server_status_label.setText( + self.set_server_status_indicator_working( strings._("gui_status_indicator_share_scheduled") ) else: - self.status_bar.server_status_label.setText( + self.set_server_status_indicator_working( strings._("gui_status_indicator_share_working") ) elif self.share_mode.server_status.status == ServerStatus.STATUS_STARTED: - self.status_bar.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.status_bar.server_status_image_started) - ) - self.status_bar.server_status_label.setText( + self.set_server_status_indicator_started( strings._("gui_status_indicator_share_started") ) elif self.mode == self.common.gui.MODE_WEBSITE: # Website mode if self.website_mode.server_status.status == ServerStatus.STATUS_STOPPED: - self.status_bar.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.status_bar.server_status_image_stopped) - ) - self.status_bar.server_status_label.setText( + self.set_server_status_indicator_stopped( strings._("gui_status_indicator_share_stopped") ) elif self.website_mode.server_status.status == ServerStatus.STATUS_WORKING: - self.status_bar.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.status_bar.server_status_image_working) - ) - self.status_bar.server_status_label.setText( + self.set_server_status_indicator_working( strings._("gui_status_indicator_share_working") ) elif self.website_mode.server_status.status == ServerStatus.STATUS_STARTED: - self.status_bar.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.status_bar.server_status_image_started) - ) - self.status_bar.server_status_label.setText( + self.set_server_status_indicator_started( strings._("gui_status_indicator_share_started") ) elif self.mode == self.common.gui.MODE_RECEIVE: # Receive mode if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED: - self.status_bar.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.status_bar.server_status_image_stopped) - ) - self.status_bar.server_status_label.setText( + self.set_server_status_indicator_stopped( strings._("gui_status_indicator_receive_stopped") ) elif self.receive_mode.server_status.status == ServerStatus.STATUS_WORKING: - self.status_bar.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.status_bar.server_status_image_working) - ) if self.receive_mode.server_status.autostart_timer_datetime: - self.status_bar.server_status_label.setText( + self.set_server_status_indicator_working( strings._("gui_status_indicator_receive_scheduled") ) else: - self.status_bar.server_status_label.setText( + self.set_server_status_indicator_working( strings._("gui_status_indicator_receive_working") ) elif self.receive_mode.server_status.status == ServerStatus.STATUS_STARTED: - self.status_bar.server_status_image_label.setPixmap( - QtGui.QPixmap.fromImage(self.status_bar.server_status_image_started) - ) - self.status_bar.server_status_label.setText( + self.set_server_status_indicator_started( strings._("gui_status_indicator_receive_started") ) + def set_server_status_indicator_stopped(self, label_text): + self.change_icon.emit(self.tab_id, "images/server_stopped.png") + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_stopped) + ) + self.status_bar.server_status_label.setText(label_text) + + def set_server_status_indicator_working(self, label_text): + self.change_icon.emit(self.tab_id, "images/server_working.png") + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_working) + ) + self.status_bar.server_status_label.setText(label_text) + + def set_server_status_indicator_started(self, label_text): + self.change_icon.emit(self.tab_id, "images/server_started.png") + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_started) + ) + self.status_bar.server_status_label.setText(label_text) + def stop_server_finished(self): # When the server stopped, cleanup the ephemeral onion service self.common.gui.onion.cleanup(stop_tor=False) diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index f9e48951..b3857cf4 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -89,6 +89,7 @@ class TabWidget(QtWidgets.QTabWidget): # Create the tab tab = Tab(self.common, self.tab_id, self.system_tray, self.status_bar) tab.change_title.connect(self.change_title) + tab.change_icon.connect(self.change_icon) self.tabs[self.tab_id] = tab self.tab_id += 1 @@ -105,6 +106,10 @@ class TabWidget(QtWidgets.QTabWidget): index, QtWidgets.QTabBar.LeftSide, self.tabs[tab_id].persistence_button ) + def change_icon(self, tab_id, icon_path): + index = self.indexOf(self.tabs[tab_id]) + self.setTabIcon(index, QtGui.QIcon(self.common.get_resource_path(icon_path))) + def close_tab(self, index): self.common.log("TabWidget", "close_tab", f"{index}") tab = self.widget(index) From f4e3f92c7b93ff3d772c0d6973ed7430063fd0b8 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 16:36:32 -0700 Subject: [PATCH 023/135] Fix a few issues related to opening settings and quitting --- onionshare_gui/main_window.py | 8 ++------ onionshare_gui/tab/tab.py | 13 ++++++++----- onionshare_gui/tab_widget.py | 5 +++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index a518ab33..9d0840a2 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -237,14 +237,10 @@ class MainWindow(QtWidgets.QMainWindow): self.website_mode.server_status.autostart_timer_container.hide() d = SettingsDialog(self.common) - d.settings_saved.connect(reload_settings) + # d.settings_saved.connect(reload_settings) + # TODO: move the reload_settings logic into tabs d.exec_() - # When settings close, refresh the server status UI - self.share_mode.server_status.update() - self.receive_mode.server_status.update() - self.website_mode.server_status.update() - def check_for_updates(self): """ Check for updates in a new thread, if enabled. diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index d495d64e..cf3050d0 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -496,12 +496,15 @@ class Tab(QtWidgets.QWidget): self.status_bar.clearMessage() def get_mode(self): - if self.mode == self.common.gui.MODE_SHARE: - return self.share_mode - elif self.mode == self.common.gui.MODE_RECEIVE: - return self.receive_mode + if self.mode: + if self.mode == self.common.gui.MODE_SHARE: + return self.share_mode + elif self.mode == self.common.gui.MODE_RECEIVE: + return self.receive_mode + else: + return self.website_mode else: - return self.website_mode + return None def persistence_button_clicked(self): self.common.log("Tab", "persistence_button_clicked") diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index b3857cf4..11dcfec6 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -127,8 +127,9 @@ class TabWidget(QtWidgets.QTabWidget): """ for tab_id in self.tabs: mode = self.tabs[tab_id].get_mode() - if mode.server_status.status != mode.server_status.STATUS_STOPPED: - return True + if mode: + if mode.server_status.status != mode.server_status.STATUS_STOPPED: + return True return False def resizeEvent(self, event): From 784d6e81445e991292f14009c5c5cdbc80266a0a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 18:16:48 -0700 Subject: [PATCH 024/135] Show settings at the top of each mode, both mode-specific settings and setting that exist for all mode types --- onionshare_gui/gui_common.py | 6 + onionshare_gui/main_window.py | 2 +- onionshare_gui/tab/mode/__init__.py | 37 ++--- onionshare_gui/tab/mode/mode_settings.py | 132 ++++++++++++++++++ .../tab/mode/receive_mode/__init__.py | 34 +++++ .../tab/mode/share_mode/__init__.py | 9 ++ .../tab/mode/website_mode/__init__.py | 7 + onionshare_gui/tab/tab.py | 61 ++++---- share/locale/en.json | 14 +- 9 files changed, 243 insertions(+), 59 deletions(-) create mode 100644 onionshare_gui/tab/mode/mode_settings.py diff --git a/onionshare_gui/gui_common.py b/onionshare_gui/gui_common.py index 5008c03a..c0502c3d 100644 --- a/onionshare_gui/gui_common.py +++ b/onionshare_gui/gui_common.py @@ -91,6 +91,12 @@ class GuiCommon: border: 0px; }""", # Common styles between modes and their child widgets + "mode_settings_toggle_advanced": """ + QPushButton { + color: #3f7fcf; + text-align: left; + } + """, "mode_info_label": """ QLabel { font-size: 12px; diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index 9d0840a2..c1bb57bf 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -42,7 +42,7 @@ class MainWindow(QtWidgets.QMainWindow): # Initialize the window self.setMinimumWidth(820) - self.setMinimumHeight(660) + self.setMinimumHeight(700) self.setWindowTitle("OnionShare") self.setWindowIcon( QtGui.QIcon(self.common.get_resource_path("images/logo.png")) diff --git a/onionshare_gui/tab/mode/__init__.py b/onionshare_gui/tab/mode/__init__.py index 7ab321f8..2418d72c 100644 --- a/onionshare_gui/tab/mode/__init__.py +++ b/onionshare_gui/tab/mode/__init__.py @@ -23,6 +23,7 @@ from onionshare import strings from onionshare.common import AutoStopTimer from .history import IndividualFileHistoryItem +from .mode_settings import ModeSettings from ..server_status import ServerStatus from ...threads import OnionThread, AutoStartTimer @@ -42,34 +43,23 @@ class Mode(QtWidgets.QWidget): starting_server_early = QtCore.pyqtSignal() set_server_active = QtCore.pyqtSignal(bool) - def __init__( - self, - common, - qtapp, - app, - status_bar, - server_status_label, - system_tray, - filenames=None, - local_only=False, - ): + def __init__(self, tab): super(Mode, self).__init__() - self.common = common - self.qtapp = qtapp - self.app = app + self.tab = tab - self.status_bar = status_bar - self.server_status_label = server_status_label - self.system_tray = system_tray + self.common = tab.common + self.qtapp = self.common.gui.qtapp + self.app = tab.app - self.filenames = filenames + self.status_bar = tab.status_bar + self.server_status_label = tab.status_bar.server_status_label + self.system_tray = tab.system_tray + + self.filenames = tab.filenames # The web object gets created in init() self.web = None - # Local mode is passed from OnionShareGui - self.local_only = local_only - # Threads start out as None self.onion_thread = None self.web_thread = None @@ -81,16 +71,19 @@ class Mode(QtWidgets.QWidget): self.header_label.setStyleSheet(self.common.gui.css["mode_header_label"]) self.header_label.setAlignment(QtCore.Qt.AlignHCenter) + self.mode_settings = ModeSettings(self.common) + header_layout = QtWidgets.QVBoxLayout() header_layout.setContentsMargins(0, 0, 0, 0) header_layout.addWidget(self.header_label) + header_layout.addWidget(self.mode_settings) self.header = QtWidgets.QWidget() self.header.setLayout(header_layout) # Server status self.server_status = ServerStatus( - self.common, self.qtapp, self.app, None, self.local_only + self.common, self.qtapp, self.app, None, self.common.gui.local_only ) self.server_status.server_started.connect(self.start_server) self.server_status.server_stopped.connect(self.stop_server) diff --git a/onionshare_gui/tab/mode/mode_settings.py b/onionshare_gui/tab/mode/mode_settings.py new file mode 100644 index 00000000..7cf373ff --- /dev/null +++ b/onionshare_gui/tab/mode/mode_settings.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +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 . +""" +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare import strings + + +class ModeSettings(QtWidgets.QWidget): + """ + A settings widget + """ + + def __init__(self, common): + super(ModeSettings, self).__init__() + self.common = common + + # Downstream Mode need to fill in this layout with its settings + self.mode_specific_layout = QtWidgets.QVBoxLayout() + + # Persistent + self.persistent_checkbox = QtWidgets.QCheckBox() + self.persistent_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.persistent_checkbox.setText(strings._("mode_settings_persistent_checkbox")) + + # Public + self.public_checkbox = QtWidgets.QCheckBox() + self.public_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.public_checkbox.setText(strings._("mode_settings_public_checkbox")) + + # Whether or not to use an auto-start timer + self.autostart_timer_checkbox = QtWidgets.QCheckBox() + self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.autostart_timer_checkbox.setText( + strings._("mode_settings_autostart_timer_checkbox") + ) + + # Whether or not to use an auto-stop timer + self.autostop_timer_checkbox = QtWidgets.QCheckBox() + self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.autostop_timer_checkbox.setText( + strings._("mode_settings_autostop_timer_checkbox") + ) + + # Legacy address + self.legacy_checkbox = QtWidgets.QCheckBox() + self.legacy_checkbox.clicked.connect(self.update_ui) + self.legacy_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.legacy_checkbox.setText(strings._("mode_settings_legacy_checkbox")) + + # Client auth + self.client_auth_checkbox = QtWidgets.QCheckBox() + self.client_auth_checkbox.clicked.connect(self.update_ui) + self.client_auth_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.client_auth_checkbox.setText( + strings._("mode_settings_client_auth_checkbox") + ) + + # Toggle advanced settings + self.toggle_advanced_button = QtWidgets.QPushButton() + self.toggle_advanced_button.clicked.connect(self.toggle_advanced_clicked) + self.toggle_advanced_button.setFlat(True) + self.toggle_advanced_button.setStyleSheet( + self.common.gui.css["mode_settings_toggle_advanced"] + ) + + # Advanced group itself + advanced_layout = QtWidgets.QVBoxLayout() + advanced_layout.setContentsMargins(0, 0, 0, 0) + advanced_layout.addWidget(self.public_checkbox) + advanced_layout.addWidget(self.autostart_timer_checkbox) + advanced_layout.addWidget(self.autostop_timer_checkbox) + advanced_layout.addWidget(self.legacy_checkbox) + advanced_layout.addWidget(self.client_auth_checkbox) + self.advanced_widget = QtWidgets.QWidget() + self.advanced_widget.setLayout(advanced_layout) + self.advanced_widget.hide() + + layout = QtWidgets.QVBoxLayout() + layout.addLayout(self.mode_specific_layout) + layout.addWidget(self.persistent_checkbox) + layout.addWidget(self.advanced_widget) + layout.addWidget(self.toggle_advanced_button) + self.setLayout(layout) + + self.update_ui() + + def update_ui(self): + # Update text on advanced group toggle button + if self.advanced_widget.isVisible(): + self.toggle_advanced_button.setText( + strings._("mode_settings_advanced_toggle_hide") + ) + else: + self.toggle_advanced_button.setText( + strings._("mode_settings_advanced_toggle_show") + ) + + # Client auth is only a legacy option + if self.client_auth_checkbox.isChecked(): + self.legacy_checkbox.setChecked(True) + self.legacy_checkbox.setEnabled(False) + else: + self.legacy_checkbox.setEnabled(True) + if self.legacy_checkbox.isChecked(): + self.client_auth_checkbox.show() + else: + self.client_auth_checkbox.hide() + + def toggle_advanced_clicked(self): + if self.advanced_widget.isVisible(): + self.advanced_widget.hide() + else: + self.advanced_widget.show() + + self.update_ui() diff --git a/onionshare_gui/tab/mode/receive_mode/__init__.py b/onionshare_gui/tab/mode/receive_mode/__init__.py index 46b1dd57..d7b31756 100644 --- a/onionshare_gui/tab/mode/receive_mode/__init__.py +++ b/onionshare_gui/tab/mode/receive_mode/__init__.py @@ -41,6 +41,23 @@ class ReceiveMode(Mode): # Header self.header_label.setText(strings._("gui_new_tab_receive_button")) + data_dir_label = QtWidgets.QLabel( + strings._("mode_settings_receive_data_dir_label") + ) + self.data_dir_lineedit = QtWidgets.QLineEdit() + self.data_dir_lineedit.setReadOnly(True) + self.data_dir_lineedit.setText(self.tab.tab_settings["receive"]["data_dir"]) + data_dir_button = QtWidgets.QPushButton( + strings._("mode_settings_receive_data_dir_browse_button") + ) + data_dir_button.clicked.connect(self.data_dir_button_clicked) + data_dir_layout = QtWidgets.QHBoxLayout() + data_dir_layout.addWidget(data_dir_label) + data_dir_layout.addWidget(self.data_dir_lineedit) + data_dir_layout.addWidget(data_dir_button) + + self.mode_settings.mode_specific_layout.addLayout(data_dir_layout) + # Server status self.server_status.set_mode("receive") self.server_status.server_started_finished.connect(self.update_primary_action) @@ -102,6 +119,23 @@ class ReceiveMode(Mode): self.wrapper_layout.addWidget(self.history, stretch=1) self.setLayout(self.wrapper_layout) + def data_dir_button_clicked(self): + """ + Browse for a new OnionShare data directory + """ + data_dir = self.data_dir_lineedit.text() + selected_dir = QtWidgets.QFileDialog.getExistingDirectory( + self, strings._("mode_settings_receive_data_dir_label"), data_dir + ) + + if selected_dir: + self.common.log( + "ReceiveMode", + "data_dir_button_clicked", + f"selected dir: {selected_dir}", + ) + self.data_dir_lineedit.setText(selected_dir) + def get_stop_server_autostop_timer_text(self): """ Return the string to put on the stop server button, if there's an auto-stop timer diff --git a/onionshare_gui/tab/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py index 9bf875cb..70187d99 100644 --- a/onionshare_gui/tab/mode/share_mode/__init__.py +++ b/onionshare_gui/tab/mode/share_mode/__init__.py @@ -50,6 +50,15 @@ class ShareMode(Mode): # Header self.header_label.setText(strings._("gui_new_tab_share_button")) + self.autostop_sharing_checkbox = QtWidgets.QCheckBox() + self.autostop_sharing_checkbox.setCheckState(QtCore.Qt.Checked) + self.autostop_sharing_checkbox.setText( + strings._("mode_settings_share_autostop_sharing_checkbox") + ) + self.mode_settings.mode_specific_layout.addWidget( + self.autostop_sharing_checkbox + ) + # File selection self.file_selection = FileSelection(self.common, self) if self.filenames: diff --git a/onionshare_gui/tab/mode/website_mode/__init__.py b/onionshare_gui/tab/mode/website_mode/__init__.py index b9e7b647..af5f9060 100644 --- a/onionshare_gui/tab/mode/website_mode/__init__.py +++ b/onionshare_gui/tab/mode/website_mode/__init__.py @@ -52,6 +52,13 @@ class WebsiteMode(Mode): # Header self.header_label.setText(strings._("gui_new_tab_website_button")) + self.disable_csp_checkbox = QtWidgets.QCheckBox() + self.disable_csp_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.disable_csp_checkbox.setText( + strings._("mode_settings_website_disable_csp_checkbox") + ) + self.mode_settings.mode_specific_layout.addWidget(self.disable_csp_checkbox) + # File selection self.file_selection = FileSelection(self.common, self) if self.filenames: diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index cf3050d0..19c21636 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -118,7 +118,24 @@ class Tab(QtWidgets.QWidget): self.timer.timeout.connect(self.timer_callback) # Settings for this tab - self.tab_settings = {"persistent": False} + self.tab_settings = { + "persistent": { + "enabled": False, + "private_key": None, + "hidservauth": None, + "password": None, + }, + "general": { + "public": False, + "autostart_timer": False, + "autostop_timer": False, + "legacy_addresses": False, + "client_auth": False, + }, + "share": {"autostop_sharing": True}, + "receive": {"data_dir": self.common.settings.build_default_data_dir()}, + "website": {"disable_csp": False}, + } # Persistence button self.persistence_button = QtWidgets.QPushButton() @@ -133,16 +150,7 @@ class Tab(QtWidgets.QWidget): self.mode = self.common.gui.MODE_SHARE self.new_tab.hide() - self.share_mode = ShareMode( - self.common, - self.common.gui.qtapp, - self.app, - self.status_bar, - self.status_bar.server_status_label, - self.system_tray, - self.filenames, - self.common.gui.local_only, - ) + self.share_mode = ShareMode(self) self.layout.addWidget(self.share_mode) self.share_mode.show() @@ -176,16 +184,7 @@ class Tab(QtWidgets.QWidget): self.mode = self.common.gui.MODE_RECEIVE self.new_tab.hide() - self.receive_mode = ReceiveMode( - self.common, - self.common.gui.qtapp, - self.app, - self.status_bar, - self.status_bar.server_status_label, - self.system_tray, - None, - self.common.gui.local_only, - ) + self.receive_mode = ReceiveMode(self) self.layout.addWidget(self.receive_mode) self.receive_mode.show() @@ -221,15 +220,7 @@ class Tab(QtWidgets.QWidget): self.mode = self.common.gui.MODE_WEBSITE self.new_tab.hide() - self.website_mode = WebsiteMode( - self.common, - self.common.gui.qtapp, - self.app, - self.status_bar, - self.status_bar.server_status_label, - self.system_tray, - self.filenames, - ) + self.website_mode = WebsiteMode(self) self.layout.addWidget(self.website_mode) self.website_mode.show() @@ -508,15 +499,15 @@ class Tab(QtWidgets.QWidget): def persistence_button_clicked(self): self.common.log("Tab", "persistence_button_clicked") - if self.tab_settings["persistent"]: - self.tab_settings["persistent"] = False + if self.tab_settings["persistent"]["enabled"]: + self.tab_settings["persistent"]["enabled"] = False else: - self.tab_settings["persistent"] = True + self.tab_settings["persistent"]["enabled"] = True self.update_persistence_button() def update_persistence_button(self): self.common.log("Tab", "update_persistence_button") - if self.tab_settings["persistent"]: + if self.tab_settings["persistent"]["enabled"]: self.persistence_button.setIcon( QtGui.QIcon( self.common.get_resource_path("images/persistent_enabled.png") @@ -534,7 +525,7 @@ class Tab(QtWidgets.QWidget): if self.mode is None: return True - if self.tab_settings["persistent"]: + if self.tab_settings["persistent"]["enabled"]: dialog_text = strings._("gui_close_tab_warning_persistent_description") else: server_status = self.get_mode().server_status diff --git a/share/locale/en.json b/share/locale/en.json index 8948d317..14695658 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -193,5 +193,17 @@ "gui_quit_warning_title": "Are you sure?", "gui_quit_warning_description": "Sharing is active in some of your tabs. If you quit, all of your tabs will close. Are you sure you want to quit?", "gui_quit_warning_quit": "Quit", - "gui_quit_warning_cancel": "Cancel" + "gui_quit_warning_cancel": "Cancel", + "mode_settings_advanced_toggle_show": "Show advanced settings", + "mode_settings_advanced_toggle_hide": "Hide advanced settings", + "mode_settings_persistent_checkbox": "Save this tab, and automatically open it when I open OnionShare", + "mode_settings_public_checkbox": "Don't use a password", + "mode_settings_autostart_timer_checkbox": "Start onion service at scheduled time", + "mode_settings_autostop_timer_checkbox": "Stop onion service at scheduled time", + "mode_settings_legacy_checkbox": "Use a legacy address (v2 onion service, not recommended)", + "mode_settings_client_auth_checkbox": "Use client authorization", + "mode_settings_share_autostop_sharing_checkbox": "Stop sharing after files have been sent (uncheck to allow downloading individual files)", + "mode_settings_receive_data_dir_label": "Save files to", + "mode_settings_receive_data_dir_browse_button": "Browse", + "mode_settings_website_disable_csp_checkbox": "Disable Content Security Policy header (allows your website to use third-party resources)" } \ No newline at end of file From 5e93b50f9cdf1741c9cf70618cab3c67239c8369 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 18:21:04 -0700 Subject: [PATCH 025/135] Show mode settings above the columns in each mode, instead of as part of the main column --- onionshare_gui/tab/mode/receive_mode/__init__.py | 12 ++++++++---- onionshare_gui/tab/mode/share_mode/__init__.py | 12 ++++++++---- onionshare_gui/tab/mode/website_mode/__init__.py | 12 ++++++++---- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/onionshare_gui/tab/mode/receive_mode/__init__.py b/onionshare_gui/tab/mode/receive_mode/__init__.py index d7b31756..498ca2aa 100644 --- a/onionshare_gui/tab/mode/receive_mode/__init__.py +++ b/onionshare_gui/tab/mode/receive_mode/__init__.py @@ -106,17 +106,21 @@ class ReceiveMode(Mode): # Main layout self.main_layout = QtWidgets.QVBoxLayout() - self.main_layout.addWidget(self.header) self.main_layout.addLayout(top_bar_layout) self.main_layout.addWidget(receive_warning) self.main_layout.addWidget(self.primary_action) self.main_layout.addStretch() self.main_layout.addWidget(self.min_width_widget) + # Column layout + self.column_layout = QtWidgets.QHBoxLayout() + self.column_layout.addLayout(self.main_layout) + self.column_layout.addWidget(self.history, stretch=1) + # Wrapper layout - self.wrapper_layout = QtWidgets.QHBoxLayout() - self.wrapper_layout.addLayout(self.main_layout) - self.wrapper_layout.addWidget(self.history, stretch=1) + self.wrapper_layout = QtWidgets.QVBoxLayout() + self.wrapper_layout.addWidget(self.header) + self.wrapper_layout.addLayout(self.column_layout) self.setLayout(self.wrapper_layout) def data_dir_button_clicked(self): diff --git a/onionshare_gui/tab/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py index 70187d99..71db2a50 100644 --- a/onionshare_gui/tab/mode/share_mode/__init__.py +++ b/onionshare_gui/tab/mode/share_mode/__init__.py @@ -130,16 +130,20 @@ class ShareMode(Mode): # Main layout self.main_layout = QtWidgets.QVBoxLayout() - self.main_layout.addWidget(self.header) self.main_layout.addLayout(top_bar_layout) self.main_layout.addLayout(self.file_selection) self.main_layout.addWidget(self.primary_action) self.main_layout.addWidget(self.min_width_widget) + # Column layout + self.column_layout = QtWidgets.QHBoxLayout() + self.column_layout.addLayout(self.main_layout) + self.column_layout.addWidget(self.history, stretch=1) + # Wrapper layout - self.wrapper_layout = QtWidgets.QHBoxLayout() - self.wrapper_layout.addLayout(self.main_layout) - self.wrapper_layout.addWidget(self.history, stretch=1) + self.wrapper_layout = QtWidgets.QVBoxLayout() + self.wrapper_layout.addWidget(self.header) + self.wrapper_layout.addLayout(self.column_layout) self.setLayout(self.wrapper_layout) # Always start with focus on file selection diff --git a/onionshare_gui/tab/mode/website_mode/__init__.py b/onionshare_gui/tab/mode/website_mode/__init__.py index af5f9060..179a5d05 100644 --- a/onionshare_gui/tab/mode/website_mode/__init__.py +++ b/onionshare_gui/tab/mode/website_mode/__init__.py @@ -130,16 +130,20 @@ class WebsiteMode(Mode): # Main layout self.main_layout = QtWidgets.QVBoxLayout() - self.main_layout.addWidget(self.header) self.main_layout.addLayout(top_bar_layout) self.main_layout.addLayout(self.file_selection) self.main_layout.addWidget(self.primary_action) self.main_layout.addWidget(self.min_width_widget) + # Column layout + self.column_layout = QtWidgets.QHBoxLayout() + self.column_layout.addLayout(self.main_layout) + self.column_layout.addWidget(self.history, stretch=1) + # Wrapper layout - self.wrapper_layout = QtWidgets.QHBoxLayout() - self.wrapper_layout.addLayout(self.main_layout) - self.wrapper_layout.addWidget(self.history, stretch=1) + self.wrapper_layout = QtWidgets.QVBoxLayout() + self.wrapper_layout.addWidget(self.header) + self.wrapper_layout.addLayout(self.column_layout) self.setLayout(self.wrapper_layout) # Always start with focus on file selection From b017b3268c6830581a95142c7d89bd379596c781 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 18:41:24 -0700 Subject: [PATCH 026/135] Make the persistent button an image label instead, and only show it whne the persistent checkbox is checked --- onionshare_gui/tab/mode/__init__.py | 4 +- onionshare_gui/tab/mode/mode_settings.py | 9 ++++- onionshare_gui/tab/tab.py | 49 +++++++++-------------- onionshare_gui/tab_widget.py | 21 +++++++--- share/images/persistent_disabled.png | Bin 1299 -> 0 bytes share/images/persistent_enabled.png | Bin 4368 -> 3398 bytes 6 files changed, 45 insertions(+), 38 deletions(-) delete mode 100644 share/images/persistent_disabled.png diff --git a/onionshare_gui/tab/mode/__init__.py b/onionshare_gui/tab/mode/__init__.py index 2418d72c..13cbd520 100644 --- a/onionshare_gui/tab/mode/__init__.py +++ b/onionshare_gui/tab/mode/__init__.py @@ -42,6 +42,7 @@ class Mode(QtWidgets.QWidget): starting_server_error = QtCore.pyqtSignal(str) starting_server_early = QtCore.pyqtSignal() set_server_active = QtCore.pyqtSignal(bool) + change_persistent = QtCore.pyqtSignal(int, bool) def __init__(self, tab): super(Mode, self).__init__() @@ -71,7 +72,8 @@ class Mode(QtWidgets.QWidget): self.header_label.setStyleSheet(self.common.gui.css["mode_header_label"]) self.header_label.setAlignment(QtCore.Qt.AlignHCenter) - self.mode_settings = ModeSettings(self.common) + self.mode_settings = ModeSettings(self.common, self.tab.tab_id) + self.mode_settings.change_persistent.connect(self.change_persistent) header_layout = QtWidgets.QVBoxLayout() header_layout.setContentsMargins(0, 0, 0, 0) diff --git a/onionshare_gui/tab/mode/mode_settings.py b/onionshare_gui/tab/mode/mode_settings.py index 7cf373ff..834680d8 100644 --- a/onionshare_gui/tab/mode/mode_settings.py +++ b/onionshare_gui/tab/mode/mode_settings.py @@ -27,15 +27,19 @@ class ModeSettings(QtWidgets.QWidget): A settings widget """ - def __init__(self, common): + change_persistent = QtCore.pyqtSignal(int, bool) + + def __init__(self, common, tab_id): super(ModeSettings, self).__init__() self.common = common + self.tab_id = tab_id # Downstream Mode need to fill in this layout with its settings self.mode_specific_layout = QtWidgets.QVBoxLayout() # Persistent self.persistent_checkbox = QtWidgets.QCheckBox() + self.persistent_checkbox.clicked.connect(self.persistent_checkbox_clicked) self.persistent_checkbox.setCheckState(QtCore.Qt.Unchecked) self.persistent_checkbox.setText(strings._("mode_settings_persistent_checkbox")) @@ -123,6 +127,9 @@ class ModeSettings(QtWidgets.QWidget): else: self.client_auth_checkbox.hide() + def persistent_checkbox_clicked(self): + self.change_persistent.emit(self.tab_id, self.persistent_checkbox.isChecked()) + def toggle_advanced_clicked(self): if self.advanced_widget.isVisible(): self.advanced_widget.hide() diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 19c21636..afae4b26 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -40,6 +40,7 @@ class Tab(QtWidgets.QWidget): change_title = QtCore.pyqtSignal(int, str) change_icon = QtCore.pyqtSignal(int, str) + change_persistent = QtCore.pyqtSignal(int, bool) def __init__(self, common, tab_id, system_tray, status_bar, filenames=None): super(Tab, self).__init__() @@ -56,7 +57,7 @@ class Tab(QtWidgets.QWidget): # Start the OnionShare app self.app = OnionShare(common, self.common.gui.onion, self.common.gui.local_only) - # New tab widget + # Widgets to display on a new tab share_button = QtWidgets.QPushButton(strings._("gui_new_tab_share_button")) share_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"]) share_description = QtWidgets.QLabel(strings._("gui_new_tab_share_description")) @@ -137,13 +138,16 @@ class Tab(QtWidgets.QWidget): "website": {"disable_csp": False}, } - # Persistence button - self.persistence_button = QtWidgets.QPushButton() - self.persistence_button.setDefault(False) - self.persistence_button.setFlat(True) - self.persistence_button.setFixedSize(30, 30) - self.persistence_button.clicked.connect(self.persistence_button_clicked) - self.update_persistence_button() + # Persistent image + self.persistent_image_label = QtWidgets.QLabel() + self.persistent_image_label.setPixmap( + QtGui.QPixmap.fromImage( + QtGui.QImage( + self.common.get_resource_path("images/persistent_enabled.png") + ) + ) + ) + self.persistent_image_label.setFixedSize(30, 30) def share_mode_clicked(self): self.common.log("Tab", "share_mode_clicked") @@ -151,6 +155,8 @@ class Tab(QtWidgets.QWidget): self.new_tab.hide() self.share_mode = ShareMode(self) + self.share_mode.change_persistent.connect(self.change_persistent) + self.layout.addWidget(self.share_mode) self.share_mode.show() @@ -185,6 +191,8 @@ class Tab(QtWidgets.QWidget): self.new_tab.hide() self.receive_mode = ReceiveMode(self) + self.receive_mode.change_persistent.connect(self.change_persistent) + self.layout.addWidget(self.receive_mode) self.receive_mode.show() @@ -221,6 +229,8 @@ class Tab(QtWidgets.QWidget): self.new_tab.hide() self.website_mode = WebsiteMode(self) + self.website_mode.change_persistent.connect(self.change_persistent) + self.layout.addWidget(self.website_mode) self.website_mode.show() @@ -497,29 +507,6 @@ class Tab(QtWidgets.QWidget): else: return None - def persistence_button_clicked(self): - self.common.log("Tab", "persistence_button_clicked") - if self.tab_settings["persistent"]["enabled"]: - self.tab_settings["persistent"]["enabled"] = False - else: - self.tab_settings["persistent"]["enabled"] = True - self.update_persistence_button() - - def update_persistence_button(self): - self.common.log("Tab", "update_persistence_button") - if self.tab_settings["persistent"]["enabled"]: - self.persistence_button.setIcon( - QtGui.QIcon( - self.common.get_resource_path("images/persistent_enabled.png") - ) - ) - else: - self.persistence_button.setIcon( - QtGui.QIcon( - self.common.get_resource_path("images/persistent_disabled.png") - ) - ) - def close_tab(self): self.common.log("Tab", "close_tab") if self.mode is None: diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index 11dcfec6..29dbda15 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -90,6 +90,7 @@ class TabWidget(QtWidgets.QTabWidget): tab = Tab(self.common, self.tab_id, self.system_tray, self.status_bar) tab.change_title.connect(self.change_title) tab.change_icon.connect(self.change_icon) + tab.change_persistent.connect(self.change_persistent) self.tabs[self.tab_id] = tab self.tab_id += 1 @@ -101,15 +102,25 @@ class TabWidget(QtWidgets.QTabWidget): index = self.indexOf(self.tabs[tab_id]) self.setTabText(index, title) - # Now that a mode has been selected, add persistence button - self.tabBar().setTabButton( - index, QtWidgets.QTabBar.LeftSide, self.tabs[tab_id].persistence_button - ) - def change_icon(self, tab_id, icon_path): index = self.indexOf(self.tabs[tab_id]) self.setTabIcon(index, QtGui.QIcon(self.common.get_resource_path(icon_path))) + def change_persistent(self, tab_id, is_persistent): + index = self.indexOf(self.tabs[tab_id]) + if is_persistent: + self.tabBar().setTabButton( + index, + QtWidgets.QTabBar.LeftSide, + self.tabs[tab_id].persistent_image_label, + ) + else: + invisible_widget = QtWidgets.QWidget() + invisible_widget.setFixedSize(0, 0) + self.tabBar().setTabButton( + index, QtWidgets.QTabBar.LeftSide, invisible_widget + ) + def close_tab(self, index): self.common.log("TabWidget", "close_tab", f"{index}") tab = self.widget(index) diff --git a/share/images/persistent_disabled.png b/share/images/persistent_disabled.png deleted file mode 100644 index 2f2e34ab2c484ab5666fb67ef7d68af1112a8bd7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1299 zcmV+u1?>8XP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+U=K3lH@21h4-9dj$la$iQ`~?BKih%{CQx?S(#Pc z5!2I)S!5DvV_7`leX_GE^uK?M@E1Omo?C+3*BpyS8&xz2RNb-XV6zDLjT zaNl5r1ZTM}(~eU?Tpt50?)do}4_3F1Z=rqTLr~_-jLy#UEhK!!Xu4~eDT2a*yO10>&Vejgfn+H&h9isdj41kws~L~qdl&BRi+OkN#k-^IxI9}C zjPepe~yo8!X3pNt{ZL+LFatkf%os$5rlRZ41C6%&k}q2?!hsg-wYL zV>TlA=-^YZa+b&^2#_i_3Il9J65u$`xiQ9Q<+Ab>#;XM&RFW9b-~j@x7!~zn(NRN1 zMPpFaq^7P}OOh0mrj#trcT_R4XllvK+_DuHkFK8F+&z2AoCU8i%_&>Ro^vh*lNLx9 zj4sG1<>XV&cJN?XOsVJWstJYMldd;;oZqo4hx6~{&Z@HCD9i`M`*Pgm{@41(O zS{pLr(2<4>A9<9U+NSzUjb6z8ni_3tlUQ7N_MnEa*@pzp*NIMMAjXkE+$I4OG*4!S zQ;a;xO=d@CJQT{HMmpg%n#4dbc48fLvwM*HCAXmQUvcA~kqZ;upCA_|x=rpYw{NJm zvVKuy0h&+Y^z@K{7aJ0~7xprLlYaXg{;R-S=q>aXdJDaU-a>Do|BukX9}oCfGyDS} z^^rg05RH5Q000JJOGiWi{{a60|De66lK=n!32;bRa{vGf6951U69E94oEQKA00(qQ zO+^Re3L6$Q4%L+kfdBvjwMj%lR7l6Y)=g{FP!z`T-^{cYwQrzJbu1#Nh`1FhqAvUz zepkPM1y?QvTk9w~mP#$Pq8*KHP9T(Kl1!4G&4ruf{&U`*a|ep8hfO@gJjPnI!4&8t z+(91+e&C>Ma0boWb?oA#EAS@np0VG@8n(Lt7eO<-6zkVE!DZ0Qk^mpK0j>(o3U3|n zkTwB7#J!r|UBFU!J;aqbYf|-RdUz3Q8}<)h@TRN|23Ww=mcYG;#BHqLb5R5a@$h%# zt>2R33ehhYb|*H7F%_cYY(3Sez5`c)ht^5D{_+! zh*qIW!kOY@7f==QxKb1PI@+@_xL*_c98aqV(xBw|T!=wcXoAH!cihCIgOI_h(99m- zW)twFNC0w$)Gv_5$5@^<)bR6+aObP{FW$#l$tkCX(U$2f;Nz(7)smJO<2!ETBDoOX z?al%ggCq&<-J0^=VZ1ZUwbgRe$=>NP)`iXC2{v+jFT;Ua9@n)r7vA+)H)3znC0On@ zx*yQ_xSQ`F?HUY6G3&2Ip)F!`9^m|}U&1X+&I_F7eS|-_{~r@jV*rh3v5Wu!002ov JPDHLkV1n_-Xv+Wq diff --git a/share/images/persistent_enabled.png b/share/images/persistent_enabled.png index 68a5a74f60280cc95948590c36b429689ae0703f..6c295db50e97005b63f86e615a2660d2912e2da2 100644 GIT binary patch delta 3383 zcmV-74aoA4BE}kjiBL{Q4GJ0x0000DNk~Le0000K0000K2nGNE0F8+q4FCWNc6wA; zbW&k=AaHVTW@&6?Aar?fWgumEX=VTbc-pO)YnJ3H4ut=+idlj|fM7X1&zT*}^5+wF zRoAm$_f~eNog}tF5ekLUt^fUZyZ`Vfp+pyxmRxhb_=`1v*3dX9*Zp@N{T0vq{UJWz z`S;`Q^^3reaFqLFUeEl-c=~aHKJ$HlKkjrq_bJbfo-bZ*DE8#Q$uqA!H#W~-sS7qx!2PV z!FxUX5WA~?J>&8|6zVv9cn+G{O*gsu717!dlS38XW4t*yK6BM zsqCIjy=>*2F_D|ZI~tjn@;`B|=T&(Xwm1m-1kKKCIA47rvfD>Lee>I|a~yuK#28i} z+#!S!!!Qc`P_gI@$AH6ynw)!y(FLgr6H*I#Ib-+W930&=pf)z3e&p{HX<2t zO0Z{tbB+2lH>7Y6BP{@tB#QzK2?SU%CitVVz@e5TMVC^_NmEHRwJbU2lylZ>8r4)Z zYSOH#MXR9MDtyY|v+Zv*;_ zIMT>NM;Udr8Kz9qf99!fmRV<8enF)bS6X?0=_;$Pw&B{A9e3J!>n^+Q_KMnr>U-4u z2jqT^ntxC;#NwH!KTu;>_2m}ckt<8*tjxX0MLq_nyg z-=v_^l|a{3cWOLDfQEn-uJbI}*ECy*arY4{5`oN9CnL$~t@>$^Z2Ze|wR3j89Q}E& zUK3y_M}sKTx>DV_)l7V0#d0=fPd!tAd*2qf)cV$bA(_K+d@cK~%#?Q**{x0WDVDy# zwleC&Qf*X`>0|P#`cjgeLC`-wrjBO!4OpJFjJ*hX;;~We6N`&G&nxB{d}tj8osbpe zrNzo(;(`IOO3gkUFS;8PH`4 zbmLos1~K8C@2Ub?0ucsS83?^x8?`+EQE6XJe)kBILcJd`Wd%#95X(^K(MivylmwZ| zdLI(-|F@rEEOT}?i10;t_aG$>b4!vyiq#A^X9yS!fsUc`I8rBV3i1FY@@B={E7W$B z;?i?j%pjph4un3m^Q@JmjNe^<)@zK&gmbpjwR8N_$c5tW6F~-8YA-jkp8C6DK+wnPLLct$HG~b9l>t3&}N=byd>8 z*d2(+U|DZM<^GjOHw*UfOq4srSQWZ#Tl2dTAiqaRd8}Yt+m`)1ppvqwgiA-%K?UO5 zbtTBBPhySrDs*mXO(@C2BGKHeLkaXAb;F|WcO(Y zTgDHz3mQ!6Dwu%g8#NPuXE7JlfkeF?tq(_=68+ls*9!ag=)U@b%-y#NEMyK62cJ3& z>Uv6r-~9zLMUoI|sU1Bc#dcz?XN7~xux_R0j%{~EE+*nV%3Vuhy{5_X*rQdd^)-{H zC|$r$fyO=z1fA<@kGYM-30*)QSbEQbTDR`2VwQ*cD$GavbxkXOUtyn`>w3#|)UMnf zdlDzysLQ8pAg54UaR!k2i*!^#Ga*^PTo=p%a2&PTP@Af@P;}Hl@$9* zV%nr>4r!~2#S^21hfQWLtlcDQh*!4QIv+4OiQ8!H!E947$vDv_t}kK#cmzRzQ}%v< z-_FE4lXa{nV#L&cYPSzKm8LVCRqb4@^cu-cs3c8^8Dogq!d9X5u(AwipRjip+|uUa zMhkCItWjJRiwt|kyfm^|F>~u1&+-RJD?}I85ZJLQLJC)_`7V5uc08o{<$=%_dX|9` z5@=RYObU9aGni|$$lp=(^*cM@KKX(Mvvd#bzy(#FKwtGL2dkIl7(=Cu6K95W;joDjQq-GH+ z{w?X|6tTjnu!W@!fk!izlVAey*iW(!uK2Q@ zt;^9bB(M2lz)u+WCBdv0(gwGNdq4guVE!)jDC#eND!Ka|i&&;-?{t$DN$?gOFG)$D zHDWWn7ri)im;J_x3Otnq3vQJI?}dzECn@ihEG*6RY{v&wa`>HFb#I?tTiBfjs>j_* zwI6wgA5;36X~PEEQmYY_nrp1})vEtRqJ4tj`Vt!Et@KKL5UFAN$@E^}@k!sVg%nCR zlvhK4YR_pxXXeMYq-C1YPUXostd7a2tiS&1RyhRo%ywaIuu}7(7)568Jlz{;1p%^p3!}(+#DWfKdT&jf z`BY3-`?k`mlYUMWfKi{nEZDrcpH4`Vb&Giz3>vuZVZuUoT5eKsT0_J@je>+YtHlZD zo1Mez04iHid$5ibe}n3nOjF#(4Cry<0QS7u4)WK_9RT#$M=iGB=8o~&+5J;lcn3)SwH<`L} zkwxi3TQ?%ujVqUm5v1KHC?amU@{h2hTX*R!q!tTG7e**?a~m*SG|(`ZOqR6vSUC1qC_~vTyN3cMr{if8VWNf$LohgwFL0`vHV^M`p|+6XJzU0%d-hLYDhSt_ zvAJ`5aQk3j7PAW#?Buu#@Cv#?xRb@~L>9A{#jM|ot#Z5A_P~RY3*lV--1<{s!`j3e ze^T`DC$3X8LWes6}=vQtRCivs-%Nd;1_&`osmabFV^*oemNDV?gimz+^tG_?isLaSSppP%l+m3 z!QQeZALQoa{J)8018$6+GV4vx{gVt77ya-fbj}v>n@{Jsegmw#p!E;%zi4 zXG^$+KdNJ|Rb#n;n~kJ#Cm-bFfm_9v**JR-yHzePmCDslzhf?$+f739JN`x+buU+r zf6Zdn%?J5@;Eo;5*ersuf}e1AxxcJbS}WGfgqcV6P+!H(3ibw`$MyL*|1da$+Ocgq zPD2Zy;~jj9%ke0{jdDXi$Yn9>R#7UeC~Y1I_@Am_c$i3jtA3=9o&Yhfx%A6DPCftt N002ovPDHLkV1gNon7RM} delta 4361 zcmV+k5%%uJ8jvD?iBL{Q4GJ0x0000DNk~Le0000U0000U2nGNE06Q?Qq5uF6etJ|` zbW&k=AaHVTW@&6?Aar?fWgumEX=VTbc-pO*X?EK>lZO9e6}MJpDb<`-6`Oi{CkO^1iOWCo1_h zp5A|l@&4YF@3!;)E$x}&^Wv|6OCuPkGFWoRR#J()`xkRz1!a`!W4!0mvX4~GD@T8q zU&k(do_+~`-sjnu*v0uCFF%GNM15bNUt;tgPxaTadm{8h*1p~OhYxH=i{Jlz7rT2; zzt>*7o1#QY!+TR7TY1j7a1qM#d`)>Zek3y@`a=KP(jNJzTxnARu0&ntXaH}Si~7BX0$ zFn2aM<9bc8YWh}N<;ip5x*8?KSGlKl-V+ca?p;|-Dr6ukg`G6TmUByqICk6ZwNweUR;{h} zI_lJ-qE%I!I!4V_nzY)qwbt8cvzIQtcI~bAKKdMa6d@2viqwe%%(|D3h-&YEXTp6lATtTEi$#}vuoqNuDt>D3)@)=`(lVGWU;p3swFqZ~1qb3!b|FkIV&6-8*xC zKj-ZiSv&J#l=39VyU=4A%LUlD!1~O6IwyTQF1f;ZX{%|zX|8n6k-o2{N-kSjD>VYT zZ(cc_Fty#?R%v%xyJUJT7D^yfml|g|+#eEI%Tlu1r?km5H}9IG-!;#^GSLj}0*ZTG zW9=ro@?0}2M4o!9aWhfhr`B~kDP7Wkw07D%b(7q0ou>21SZYC9u5D(Mv&~dalA7x7 z+_m~CRrY7c@Mjk)$*5COJ}Z|sS(Y{Gs~x4gB1NJ>bcm7IZY<*r0o2c=SVgHc0$B;l zsq<^pz;0cMHes};6Kn7|!q6*AzXD!amGN2(W%_MpzZm z_{|561{_8imSNi>9aP6IGQ2Qj>RPg>FNh6SfOzE4$N~s3=oH##Z0!`s;wOz3&!}qw z@2f}~#66Wgh$N`=CDhWxO?fAOCxl50(}rNwtq6!bR%QDz2w@xP6yvkf_R>?eKWpb$ zUe%4T2p=b($8J`xQ3LhfhMvfd6w@Y>V8km5ThShEPH3uCl61WO!2Tc4Q?Ode^F%AW2V~%K_MSbpu2sg$dZVPe^ZUQgJttGuc`~qbI0o3Jwh2xmIw~ zFDs2r>pk%?H*z&ss9%U7XtHeGKAeue(JLpgQyDx&d=ehxnjP`xjID58)7y1!}v@Q)W% zffe&zvU{Rr&g!XeJ+Q%Ixyl_gv@E{%K&JP$-|T!}Zp*o5@rSOt! zoo%xg$l7Mnd0Wb;iZv=r6TpAA(^$od%)k^G!lqZ+to#$NWVtR4}Apb?n^jI%g3D zgdN+X*MN2;g->z|)cR9sKaK|xclDecL9Qvt$LUW2?yMDjrn13%(AJr->mshsl29Dd z!#N*NO~Wu-ER>fX*YKF()iS&VhLJle^0cejAwH67JGP4M=~QfN^Dx zLhYpUMyIcT{S)HfG*kgr(FkdsQ>om7%>j|uf&hb(u1F#Gv;3o;N%Dc&AQeHrr3H8c z*;3f}Ku_ab1+k}y50J%K#)A}Ruu4bOb9P0jItrn;2#LN@xCYYf-_w8G(^7dz+|vtf zHPO?;dg(;<-o{ar-f-K3G8nI1$p8$zLzV|mVg1f>~tK;GM21Hb}Mn^oLsxIB9>!_ctw}{(F z$G_7@&r!h?Zb2{Jcx`z@5Ty!gEI772wJXh#1gE334fllf^c2{o0{*;LaU$}fajQ&p z_JhZNC~xADJFr=;pjT{)>Y}2fwf=C~M!vayimp`e!zim)bVU*kz-Lr4OcLwH*AYn6 zrizt~!s@S%g~Wf?${)w@(+BKY9wI<4+}`L_)^gHu0)+`IAu(Jx;&Y1bN1LRSW_Xca zYs1E0ng%qIx_c^vo!8Ls1h415P9-!X0Yr{}BW0{nOJr>}vNfMIvRTlMx=|nv`=WFt zrWS9dJGdV3rs{IbzXsxWaUcreGYse^Gv~dgJJNJsx3~t#3stH6)>`K{y0NlYZCvP^r$U!sRkXLL%hy^1rF^%=!7sMHIR)K)jfHs_)5iH4>yzJkY?hC zNdV5L>m)B8R%G+(Wb?chhR=gAz&BL2r@v5kcnga8R6ISLgUrK=N%~}G-gjKsT*_~-hfQv9*77R2r|!b5 z@P1WA5a>zMo`oVt$HhNRGF`lX2(js|xMnXE?`8Ldo(T&ggaspCAr#!^b}<)?(T3D1 z31Fr0=3Rx13t&Q|sBXE5(oAMq%z~j+6o@fHX*`>O$^IPTAbXd&1uKey05CWAzqej`bd=|FJ`h5fN&F0aT@b zDIN6MaYNs&6x4kc6l|azc4vK*15F{XYM>U>*f7fNoLMSrg7TvDoF#AVh&>QY`-0Oc zSN3POm1DscC#_D`(2p5#7C4dD;^^@%|)0Fk%>uRf0JnrRj5T&{ z!lWXz0B2Um!Wyt*z0t9Mc;oW?!HV2bghX(upXSd?nQl}16_q{}tTp?wGKXel7`QR= z7zK5)Sq{AfAg+*5)P#Xjn)zlgHIinwAL1j*v*$;+F*S{V|LIS-9Fb67zys(4A))-& zL1GOSk}g;K15i!KE3DGQM~qexJ@}gGUI|GAtbz|{bovF=bcnluHBdV2CT_+c0L%7a zghc+Db;BQCNcOOY=*iY$efa`t5OPJzJ&&0DIV|}y?5z@u ztHUiR&`Zi`U4Pd5z(#C~v_X(`BzSP$Sl&uM4q9b^do^!tH9U4Gw*&N}(!f&8>3~(} z6Av6@*3uGsHTV;Mfm_QVvsgk1luwU~_kkw@=Nl@VhNGni({H^aLkiRJ1?{WOaq?`f zLnRT|>r_5p>tjfXH+zcfqNAlESMrNy5AZU)1H8O&t8hznRo>{FFy@^N4@_a#0f@(e zjc}8?s!??r@snL(`y)0yJ5&Hn40;iFuCrzu2I!6We&7{j4!C19)zv?J(NY(~PK0CDbow^mxo$ls=$NE;phn5F^@ zP%{$UGri>Q?!T7H7zF}t|DREL zq}r)|0mxXW2k5%emFzJC(Is?|Mn%zlB{q@eG&;srYAg5_eb6X z9eM=%HRG2)369-(Ta1YwZ+!YYjYUJm~YZ|p!-7QJ3e zlcNeUe;XDvHnP?v*Z=?m6G=otR7l6Ympy0{K@`V-xy4Zef~2w#6v-4e5lIli4}@Ux z5Ehx!?;J<{`8Zf0!S2FY1M)n{u5)ga@ zf4FKRJ8UDn8Yi(!4X#zEw}6K}PeW=9*ytG55ui>dfs?>yVE9JYk{KuQFB{p{etD+7 zxD?Q=I=$I*$@InK1TY62s{-1wNyJI~!$$TEaKO7DD3q-0RHr+@aurZ|@Z(?}C@>*L z4ut^cf&!m?2isE?+{g(%3!Da?mj!>#e+g~sju^RC80-uFT9-f?=&=7vj0~6Y@w2Sx zRi_PL)OV`(iDgNQ3={&Z(@(%kR?4W;KkD=@aJ{YO9|jhZ5>Y+w>v(3Ks?&GCIp3R` z8m$|7+Ed9aD}TL9!1 Date: Sun, 27 Oct 2019 18:48:25 -0700 Subject: [PATCH 027/135] Make checking the persistent checkbox update the mode_settings dict, so closing tabs will catch that it's persistent --- onionshare_gui/tab/mode/__init__.py | 2 +- onionshare_gui/tab/mode/mode_settings.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/onionshare_gui/tab/mode/__init__.py b/onionshare_gui/tab/mode/__init__.py index 13cbd520..8018a4eb 100644 --- a/onionshare_gui/tab/mode/__init__.py +++ b/onionshare_gui/tab/mode/__init__.py @@ -72,7 +72,7 @@ class Mode(QtWidgets.QWidget): self.header_label.setStyleSheet(self.common.gui.css["mode_header_label"]) self.header_label.setAlignment(QtCore.Qt.AlignHCenter) - self.mode_settings = ModeSettings(self.common, self.tab.tab_id) + self.mode_settings = ModeSettings(self.common, self.tab) self.mode_settings.change_persistent.connect(self.change_persistent) header_layout = QtWidgets.QVBoxLayout() diff --git a/onionshare_gui/tab/mode/mode_settings.py b/onionshare_gui/tab/mode/mode_settings.py index 834680d8..2a4f2903 100644 --- a/onionshare_gui/tab/mode/mode_settings.py +++ b/onionshare_gui/tab/mode/mode_settings.py @@ -29,10 +29,10 @@ class ModeSettings(QtWidgets.QWidget): change_persistent = QtCore.pyqtSignal(int, bool) - def __init__(self, common, tab_id): + def __init__(self, common, tab): super(ModeSettings, self).__init__() self.common = common - self.tab_id = tab_id + self.tab = tab # Downstream Mode need to fill in this layout with its settings self.mode_specific_layout = QtWidgets.QVBoxLayout() @@ -128,7 +128,13 @@ class ModeSettings(QtWidgets.QWidget): self.client_auth_checkbox.hide() def persistent_checkbox_clicked(self): - self.change_persistent.emit(self.tab_id, self.persistent_checkbox.isChecked()) + self.tab.tab_settings["persistent"][ + "enabled" + ] = self.persistent_checkbox.isChecked() + + self.change_persistent.emit( + self.tab.tab_id, self.persistent_checkbox.isChecked() + ) def toggle_advanced_clicked(self): if self.advanced_widget.isVisible(): From f2d4bddc23f6a4cd65801af9c687e89235a764fe Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 10:15:18 -0700 Subject: [PATCH 028/135] When any setting is changed, update the tab settings dict --- onionshare_gui/tab/mode/mode_settings.py | 32 ++++++++++++++++++- .../tab/mode/receive_mode/__init__.py | 4 ++- .../tab/mode/share_mode/__init__.py | 12 +++++++ .../tab/mode/website_mode/__init__.py | 10 ++++++ onionshare_gui/tab/tab.py | 2 +- 5 files changed, 57 insertions(+), 3 deletions(-) diff --git a/onionshare_gui/tab/mode/mode_settings.py b/onionshare_gui/tab/mode/mode_settings.py index 2a4f2903..b36183d8 100644 --- a/onionshare_gui/tab/mode/mode_settings.py +++ b/onionshare_gui/tab/mode/mode_settings.py @@ -24,7 +24,7 @@ from onionshare import strings class ModeSettings(QtWidgets.QWidget): """ - A settings widget + All of the common settings for each mode are in this widget """ change_persistent = QtCore.pyqtSignal(int, bool) @@ -45,11 +45,15 @@ class ModeSettings(QtWidgets.QWidget): # Public self.public_checkbox = QtWidgets.QCheckBox() + self.public_checkbox.clicked.connect(self.public_checkbox_clicked) self.public_checkbox.setCheckState(QtCore.Qt.Unchecked) self.public_checkbox.setText(strings._("mode_settings_public_checkbox")) # Whether or not to use an auto-start timer self.autostart_timer_checkbox = QtWidgets.QCheckBox() + self.autostart_timer_checkbox.clicked.connect( + self.autostart_timer_checkbox_clicked + ) self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) self.autostart_timer_checkbox.setText( strings._("mode_settings_autostart_timer_checkbox") @@ -57,6 +61,9 @@ class ModeSettings(QtWidgets.QWidget): # Whether or not to use an auto-stop timer self.autostop_timer_checkbox = QtWidgets.QCheckBox() + self.autostop_timer_checkbox.clicked.connect( + self.autostop_timer_checkbox_clicked + ) self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) self.autostop_timer_checkbox.setText( strings._("mode_settings_autostop_timer_checkbox") @@ -64,12 +71,14 @@ class ModeSettings(QtWidgets.QWidget): # Legacy address self.legacy_checkbox = QtWidgets.QCheckBox() + self.legacy_checkbox.clicked.connect(self.legacy_checkbox_clicked) self.legacy_checkbox.clicked.connect(self.update_ui) self.legacy_checkbox.setCheckState(QtCore.Qt.Unchecked) self.legacy_checkbox.setText(strings._("mode_settings_legacy_checkbox")) # Client auth self.client_auth_checkbox = QtWidgets.QCheckBox() + self.client_auth_checkbox.clicked.connect(self.client_auth_checkbox_clicked) self.client_auth_checkbox.clicked.connect(self.update_ui) self.client_auth_checkbox.setCheckState(QtCore.Qt.Unchecked) self.client_auth_checkbox.setText( @@ -136,6 +145,27 @@ class ModeSettings(QtWidgets.QWidget): self.tab.tab_id, self.persistent_checkbox.isChecked() ) + def public_checkbox_clicked(self): + self.tab.tab_settings["general"]["public"] = self.public_checkbox.isChecked() + + def autostart_timer_checkbox_clicked(self): + self.tab.tab_settings["general"][ + "autostart_timer" + ] = self.autostart_timer_checkbox.isChecked() + + def autostop_timer_checkbox_clicked(self): + self.tab.tab_settings["general"][ + "autostop_timer" + ] = self.autostop_timer_checkbox.isChecked() + + def legacy_checkbox_clicked(self): + self.tab.tab_settings["general"]["legacy"] = self.legacy_checkbox.isChecked() + + def client_auth_checkbox_clicked(self): + self.tab.tab_settings["general"][ + "client_auth" + ] = self.client_auth_checkbox.isChecked() + def toggle_advanced_clicked(self): if self.advanced_widget.isVisible(): self.advanced_widget.hide() diff --git a/onionshare_gui/tab/mode/receive_mode/__init__.py b/onionshare_gui/tab/mode/receive_mode/__init__.py index 498ca2aa..97645e1e 100644 --- a/onionshare_gui/tab/mode/receive_mode/__init__.py +++ b/onionshare_gui/tab/mode/receive_mode/__init__.py @@ -41,6 +41,7 @@ class ReceiveMode(Mode): # Header self.header_label.setText(strings._("gui_new_tab_receive_button")) + # Settings data_dir_label = QtWidgets.QLabel( strings._("mode_settings_receive_data_dir_label") ) @@ -125,7 +126,7 @@ class ReceiveMode(Mode): def data_dir_button_clicked(self): """ - Browse for a new OnionShare data directory + Browse for a new OnionShare data directory, and save to tab settings """ data_dir = self.data_dir_lineedit.text() selected_dir = QtWidgets.QFileDialog.getExistingDirectory( @@ -139,6 +140,7 @@ class ReceiveMode(Mode): f"selected dir: {selected_dir}", ) self.data_dir_lineedit.setText(selected_dir) + self.tab.tab_settings["receive"]["data_dir"] = data_dir def get_stop_server_autostop_timer_text(self): """ diff --git a/onionshare_gui/tab/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py index 71db2a50..3ba9afdc 100644 --- a/onionshare_gui/tab/mode/share_mode/__init__.py +++ b/onionshare_gui/tab/mode/share_mode/__init__.py @@ -50,7 +50,11 @@ class ShareMode(Mode): # Header self.header_label.setText(strings._("gui_new_tab_share_button")) + # Settings self.autostop_sharing_checkbox = QtWidgets.QCheckBox() + self.autostop_sharing_checkbox.clicked.connect( + self.autostop_sharing_checkbox_clicked + ) self.autostop_sharing_checkbox.setCheckState(QtCore.Qt.Checked) self.autostop_sharing_checkbox.setText( strings._("mode_settings_share_autostop_sharing_checkbox") @@ -149,6 +153,14 @@ class ShareMode(Mode): # Always start with focus on file selection self.file_selection.setFocus() + def autostop_sharing_checkbox_clicked(self): + """ + Save autostop sharing setting to the tab settings + """ + self.tab.tab_settings["share"][ + "autostop_sharing" + ] = self.autostop_sharing_checkbox.isChecked() + def get_stop_server_autostop_timer_text(self): """ Return the string to put on the stop server button, if there's an auto-stop timer diff --git a/onionshare_gui/tab/mode/website_mode/__init__.py b/onionshare_gui/tab/mode/website_mode/__init__.py index 179a5d05..8e163cec 100644 --- a/onionshare_gui/tab/mode/website_mode/__init__.py +++ b/onionshare_gui/tab/mode/website_mode/__init__.py @@ -52,7 +52,9 @@ class WebsiteMode(Mode): # Header self.header_label.setText(strings._("gui_new_tab_website_button")) + # Settings self.disable_csp_checkbox = QtWidgets.QCheckBox() + self.disable_csp_checkbox.clicked.connect(self.disable_csp_checkbox_clicked) self.disable_csp_checkbox.setCheckState(QtCore.Qt.Unchecked) self.disable_csp_checkbox.setText( strings._("mode_settings_website_disable_csp_checkbox") @@ -149,6 +151,14 @@ class WebsiteMode(Mode): # Always start with focus on file selection self.file_selection.setFocus() + def disable_csp_checkbox_clicked(self): + """ + Save disable CSP setting to the tab settings + """ + self.tab.tab_settings["website"][ + "disable_csp" + ] = self.disable_csp_checkbox.isChecked() + def get_stop_server_autostop_timer_text(self): """ Return the string to put on the stop server button, if there's an auto-stop timer diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index afae4b26..62ff2d26 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -130,7 +130,7 @@ class Tab(QtWidgets.QWidget): "public": False, "autostart_timer": False, "autostop_timer": False, - "legacy_addresses": False, + "legacy": False, "client_auth": False, }, "share": {"autostop_sharing": True}, From 2984577f28e90ec7780a94f80f2b6f7c3398611a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 10:24:12 -0700 Subject: [PATCH 029/135] Get/set tab settings using a getter and setter function --- onionshare_gui/tab/mode/mode_settings.py | 28 +++++++++---------- .../tab/mode/receive_mode/__init__.py | 2 +- .../tab/mode/share_mode/__init__.py | 8 +++--- .../tab/mode/website_mode/__init__.py | 6 ++-- onionshare_gui/tab/tab.py | 26 ++++++++++------- 5 files changed, 38 insertions(+), 32 deletions(-) diff --git a/onionshare_gui/tab/mode/mode_settings.py b/onionshare_gui/tab/mode/mode_settings.py index b36183d8..c5ca3a8e 100644 --- a/onionshare_gui/tab/mode/mode_settings.py +++ b/onionshare_gui/tab/mode/mode_settings.py @@ -137,34 +137,34 @@ class ModeSettings(QtWidgets.QWidget): self.client_auth_checkbox.hide() def persistent_checkbox_clicked(self): - self.tab.tab_settings["persistent"][ - "enabled" - ] = self.persistent_checkbox.isChecked() + self.tab.set_tab_setting( + "persistent", "enabled", self.persistent_checkbox.isChecked() + ) self.change_persistent.emit( self.tab.tab_id, self.persistent_checkbox.isChecked() ) def public_checkbox_clicked(self): - self.tab.tab_settings["general"]["public"] = self.public_checkbox.isChecked() + self.tab.set_tab_setting("general", "public", self.public_checkbox.isChecked()) def autostart_timer_checkbox_clicked(self): - self.tab.tab_settings["general"][ - "autostart_timer" - ] = self.autostart_timer_checkbox.isChecked() + self.tab.set_tab_setting( + "general", "autostart_timer", self.autostart_timer_checkbox.isChecked() + ) def autostop_timer_checkbox_clicked(self): - self.tab.tab_settings["general"][ - "autostop_timer" - ] = self.autostop_timer_checkbox.isChecked() + self.tab.set_tab_setting( + "general", "autostop_timer", self.autostop_timer_checkbox.isChecked() + ) def legacy_checkbox_clicked(self): - self.tab.tab_settings["general"]["legacy"] = self.legacy_checkbox.isChecked() + self.tab.set_tab_setting("general", "legacy", self.legacy_checkbox.isChecked()) def client_auth_checkbox_clicked(self): - self.tab.tab_settings["general"][ - "client_auth" - ] = self.client_auth_checkbox.isChecked() + self.tab.set_tab_setting( + "general", "client_auth", self.client_auth_checkbox.isChecked() + ) def toggle_advanced_clicked(self): if self.advanced_widget.isVisible(): diff --git a/onionshare_gui/tab/mode/receive_mode/__init__.py b/onionshare_gui/tab/mode/receive_mode/__init__.py index 97645e1e..f0a2c6e9 100644 --- a/onionshare_gui/tab/mode/receive_mode/__init__.py +++ b/onionshare_gui/tab/mode/receive_mode/__init__.py @@ -140,7 +140,7 @@ class ReceiveMode(Mode): f"selected dir: {selected_dir}", ) self.data_dir_lineedit.setText(selected_dir) - self.tab.tab_settings["receive"]["data_dir"] = data_dir + self.tab.set_tab_setting("receive", "data_dir", data_dir) def get_stop_server_autostop_timer_text(self): """ diff --git a/onionshare_gui/tab/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py index 3ba9afdc..8d595e7f 100644 --- a/onionshare_gui/tab/mode/share_mode/__init__.py +++ b/onionshare_gui/tab/mode/share_mode/__init__.py @@ -157,9 +157,9 @@ class ShareMode(Mode): """ Save autostop sharing setting to the tab settings """ - self.tab.tab_settings["share"][ - "autostop_sharing" - ] = self.autostop_sharing_checkbox.isChecked() + self.tab.set_tab_setting( + "share", "autostop_sharing", self.autostop_sharing_checkbox.isChecked() + ) def get_stop_server_autostop_timer_text(self): """ @@ -309,7 +309,7 @@ class ShareMode(Mode): self.history.update_in_progress() # Close on finish? - if self.common.settings.get("close_after_first_download"): + if self.tab.tab_settings["share"]["autostop_sharing"]: self.server_status.stop_server() self.status_bar.clearMessage() self.server_status_label.setText(strings._("closing_automatically")) diff --git a/onionshare_gui/tab/mode/website_mode/__init__.py b/onionshare_gui/tab/mode/website_mode/__init__.py index 8e163cec..1ba969cc 100644 --- a/onionshare_gui/tab/mode/website_mode/__init__.py +++ b/onionshare_gui/tab/mode/website_mode/__init__.py @@ -155,9 +155,9 @@ class WebsiteMode(Mode): """ Save disable CSP setting to the tab settings """ - self.tab.tab_settings["website"][ - "disable_csp" - ] = self.disable_csp_checkbox.isChecked() + self.tab.set_tab_setting( + "website", "disable_csp", self.disable_csp_checkbox.isChecked() + ) def get_stop_server_autostop_timer_text(self): """ diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 62ff2d26..d121c9b5 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -118,6 +118,17 @@ class Tab(QtWidgets.QWidget): self.timer = QtCore.QTimer() self.timer.timeout.connect(self.timer_callback) + # Persistent image + self.persistent_image_label = QtWidgets.QLabel() + self.persistent_image_label.setPixmap( + QtGui.QPixmap.fromImage( + QtGui.QImage( + self.common.get_resource_path("images/persistent_enabled.png") + ) + ) + ) + self.persistent_image_label.setFixedSize(30, 30) + # Settings for this tab self.tab_settings = { "persistent": { @@ -138,16 +149,11 @@ class Tab(QtWidgets.QWidget): "website": {"disable_csp": False}, } - # Persistent image - self.persistent_image_label = QtWidgets.QLabel() - self.persistent_image_label.setPixmap( - QtGui.QPixmap.fromImage( - QtGui.QImage( - self.common.get_resource_path("images/persistent_enabled.png") - ) - ) - ) - self.persistent_image_label.setFixedSize(30, 30) + def get_tab_setting(self, group, key): + return self.tab_settings[group][key] + + def set_tab_setting(self, group, key, val): + self.tab_settings[group][key] = val def share_mode_clicked(self): self.common.log("Tab", "share_mode_clicked") From 9534a440512e848b35a6c9e4535d70ed8d373a95 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 10:43:20 -0700 Subject: [PATCH 030/135] Make a new ModeSettings class in onionshare, and use this instead of tab_settings --- onionshare/mode_settings.py | 54 +++++++++++++++++++ onionshare/web/web.py | 11 +++- onionshare_gui/tab/mode/__init__.py | 7 ++- ...de_settings.py => mode_settings_widget.py} | 27 +++++----- .../tab/mode/receive_mode/__init__.py | 4 +- .../tab/mode/share_mode/__init__.py | 4 +- .../tab/mode/website_mode/__init__.py | 2 +- onionshare_gui/tab/tab.py | 26 +-------- 8 files changed, 88 insertions(+), 47 deletions(-) create mode 100644 onionshare/mode_settings.py rename onionshare_gui/tab/mode/{mode_settings.py => mode_settings_widget.py} (90%) diff --git a/onionshare/mode_settings.py b/onionshare/mode_settings.py new file mode 100644 index 00000000..9557abcd --- /dev/null +++ b/onionshare/mode_settings.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +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 . +""" + + +class ModeSettings: + """ + This stores the settings for a single instance of an OnionShare mode. In CLI there + is only one TabSettings, and in the GUI there is a separate TabSettings for each tab + """ + + def __init__(self, common): + self.common = common + + self.settings = { + "persistent": { + "enabled": False, + "private_key": None, + "hidservauth": None, + "password": None, + }, + "general": { + "public": False, + "autostart_timer": False, + "autostop_timer": False, + "legacy": False, + "client_auth": False, + }, + "share": {"autostop_sharing": True}, + "receive": {"data_dir": self.common.settings.build_default_data_dir()}, + "website": {"disable_csp": False}, + } + + def get(self, group, key): + return self.settings[group][key] + + def set(self, group, key, val): + self.settings[group][key] = val diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 16dfffd0..604faf02 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -60,10 +60,19 @@ class Web: REQUEST_OTHER = 13 REQUEST_INVALID_PASSWORD = 14 - def __init__(self, common, is_gui, mode="share"): + def __init__( + self, common, is_gui, tab_settings_get=None, tab_settings_set=None, mode="share" + ): + """ + tab_settings_get and tab_settings_set are getter and setter functions for tab settings + """ + self.common = common self.common.log("Web", "__init__", f"is_gui={is_gui}, mode={mode}") + self.settings_get = tab_settings_get + self.settings_set = tab_settings_set + # The flask app self.app = Flask( __name__, diff --git a/onionshare_gui/tab/mode/__init__.py b/onionshare_gui/tab/mode/__init__.py index 8018a4eb..8399c962 100644 --- a/onionshare_gui/tab/mode/__init__.py +++ b/onionshare_gui/tab/mode/__init__.py @@ -23,7 +23,7 @@ from onionshare import strings from onionshare.common import AutoStopTimer from .history import IndividualFileHistoryItem -from .mode_settings import ModeSettings +from .mode_settings_widget import ModeSettingsWidget from ..server_status import ServerStatus from ...threads import OnionThread, AutoStartTimer @@ -47,6 +47,7 @@ class Mode(QtWidgets.QWidget): def __init__(self, tab): super(Mode, self).__init__() self.tab = tab + self.settings = tab.mode_settings self.common = tab.common self.qtapp = self.common.gui.qtapp @@ -72,7 +73,9 @@ class Mode(QtWidgets.QWidget): self.header_label.setStyleSheet(self.common.gui.css["mode_header_label"]) self.header_label.setAlignment(QtCore.Qt.AlignHCenter) - self.mode_settings = ModeSettings(self.common, self.tab) + self.mode_settings = ModeSettingsWidget( + self.common, self.tab.tab_id, self.tab.mode_settings + ) self.mode_settings.change_persistent.connect(self.change_persistent) header_layout = QtWidgets.QVBoxLayout() diff --git a/onionshare_gui/tab/mode/mode_settings.py b/onionshare_gui/tab/mode/mode_settings_widget.py similarity index 90% rename from onionshare_gui/tab/mode/mode_settings.py rename to onionshare_gui/tab/mode/mode_settings_widget.py index c5ca3a8e..23fcd087 100644 --- a/onionshare_gui/tab/mode/mode_settings.py +++ b/onionshare_gui/tab/mode/mode_settings_widget.py @@ -22,17 +22,18 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings -class ModeSettings(QtWidgets.QWidget): +class ModeSettingsWidget(QtWidgets.QWidget): """ All of the common settings for each mode are in this widget """ change_persistent = QtCore.pyqtSignal(int, bool) - def __init__(self, common, tab): - super(ModeSettings, self).__init__() + def __init__(self, common, tab_id, mode_settings): + super(ModeSettingsWidget, self).__init__() self.common = common - self.tab = tab + self.tab_id = tab_id + self.settings = mode_settings # Downstream Mode need to fill in this layout with its settings self.mode_specific_layout = QtWidgets.QVBoxLayout() @@ -137,32 +138,28 @@ class ModeSettings(QtWidgets.QWidget): self.client_auth_checkbox.hide() def persistent_checkbox_clicked(self): - self.tab.set_tab_setting( - "persistent", "enabled", self.persistent_checkbox.isChecked() - ) + self.settings.set("persistent", "enabled", self.persistent_checkbox.isChecked()) - self.change_persistent.emit( - self.tab.tab_id, self.persistent_checkbox.isChecked() - ) + self.change_persistent.emit(self.tab_id, self.persistent_checkbox.isChecked()) def public_checkbox_clicked(self): - self.tab.set_tab_setting("general", "public", self.public_checkbox.isChecked()) + self.settings.set("general", "public", self.public_checkbox.isChecked()) def autostart_timer_checkbox_clicked(self): - self.tab.set_tab_setting( + self.settings.set( "general", "autostart_timer", self.autostart_timer_checkbox.isChecked() ) def autostop_timer_checkbox_clicked(self): - self.tab.set_tab_setting( + self.settings.set( "general", "autostop_timer", self.autostop_timer_checkbox.isChecked() ) def legacy_checkbox_clicked(self): - self.tab.set_tab_setting("general", "legacy", self.legacy_checkbox.isChecked()) + self.settings.set("general", "legacy", self.legacy_checkbox.isChecked()) def client_auth_checkbox_clicked(self): - self.tab.set_tab_setting( + self.settings.set( "general", "client_auth", self.client_auth_checkbox.isChecked() ) diff --git a/onionshare_gui/tab/mode/receive_mode/__init__.py b/onionshare_gui/tab/mode/receive_mode/__init__.py index f0a2c6e9..3a9c2fbe 100644 --- a/onionshare_gui/tab/mode/receive_mode/__init__.py +++ b/onionshare_gui/tab/mode/receive_mode/__init__.py @@ -47,7 +47,7 @@ class ReceiveMode(Mode): ) self.data_dir_lineedit = QtWidgets.QLineEdit() self.data_dir_lineedit.setReadOnly(True) - self.data_dir_lineedit.setText(self.tab.tab_settings["receive"]["data_dir"]) + self.data_dir_lineedit.setText(self.settings.get("receive", "data_dir")) data_dir_button = QtWidgets.QPushButton( strings._("mode_settings_receive_data_dir_browse_button") ) @@ -140,7 +140,7 @@ class ReceiveMode(Mode): f"selected dir: {selected_dir}", ) self.data_dir_lineedit.setText(selected_dir) - self.tab.set_tab_setting("receive", "data_dir", data_dir) + self.settings.set("receive", "data_dir", data_dir) def get_stop_server_autostop_timer_text(self): """ diff --git a/onionshare_gui/tab/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py index 8d595e7f..57d6475c 100644 --- a/onionshare_gui/tab/mode/share_mode/__init__.py +++ b/onionshare_gui/tab/mode/share_mode/__init__.py @@ -157,7 +157,7 @@ class ShareMode(Mode): """ Save autostop sharing setting to the tab settings """ - self.tab.set_tab_setting( + self.settings.set( "share", "autostop_sharing", self.autostop_sharing_checkbox.isChecked() ) @@ -309,7 +309,7 @@ class ShareMode(Mode): self.history.update_in_progress() # Close on finish? - if self.tab.tab_settings["share"]["autostop_sharing"]: + if self.settings.get("share", "autostop_sharing"): self.server_status.stop_server() self.status_bar.clearMessage() self.server_status_label.setText(strings._("closing_automatically")) diff --git a/onionshare_gui/tab/mode/website_mode/__init__.py b/onionshare_gui/tab/mode/website_mode/__init__.py index 1ba969cc..e6ed785d 100644 --- a/onionshare_gui/tab/mode/website_mode/__init__.py +++ b/onionshare_gui/tab/mode/website_mode/__init__.py @@ -155,7 +155,7 @@ class WebsiteMode(Mode): """ Save disable CSP setting to the tab settings """ - self.tab.set_tab_setting( + self.settings.set( "website", "disable_csp", self.disable_csp_checkbox.isChecked() ) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index d121c9b5..60809b57 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -23,6 +23,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.onionshare import OnionShare from onionshare.web import Web +from onionshare.mode_settings import ModeSettings from .mode.share_mode import ShareMode from .mode.receive_mode import ReceiveMode @@ -130,30 +131,7 @@ class Tab(QtWidgets.QWidget): self.persistent_image_label.setFixedSize(30, 30) # Settings for this tab - self.tab_settings = { - "persistent": { - "enabled": False, - "private_key": None, - "hidservauth": None, - "password": None, - }, - "general": { - "public": False, - "autostart_timer": False, - "autostop_timer": False, - "legacy": False, - "client_auth": False, - }, - "share": {"autostop_sharing": True}, - "receive": {"data_dir": self.common.settings.build_default_data_dir()}, - "website": {"disable_csp": False}, - } - - def get_tab_setting(self, group, key): - return self.tab_settings[group][key] - - def set_tab_setting(self, group, key, val): - self.tab_settings[group][key] = val + self.mode_settings = ModeSettings(self.common) def share_mode_clicked(self): self.common.log("Tab", "share_mode_clicked") From c426cfceb2b9cebae8dcb6e85305438ebcb17997 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 11:43:05 -0700 Subject: [PATCH 031/135] Rename mode_settings_widget to be more clear, and fix one place that was still using tab_settings --- onionshare_gui/tab/mode/__init__.py | 6 +++--- onionshare_gui/tab/mode/receive_mode/__init__.py | 2 +- onionshare_gui/tab/mode/share_mode/__init__.py | 2 +- onionshare_gui/tab/mode/website_mode/__init__.py | 4 +++- onionshare_gui/tab/tab.py | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/onionshare_gui/tab/mode/__init__.py b/onionshare_gui/tab/mode/__init__.py index 8399c962..cae046e0 100644 --- a/onionshare_gui/tab/mode/__init__.py +++ b/onionshare_gui/tab/mode/__init__.py @@ -73,15 +73,15 @@ class Mode(QtWidgets.QWidget): self.header_label.setStyleSheet(self.common.gui.css["mode_header_label"]) self.header_label.setAlignment(QtCore.Qt.AlignHCenter) - self.mode_settings = ModeSettingsWidget( + self.mode_settings_widget = ModeSettingsWidget( self.common, self.tab.tab_id, self.tab.mode_settings ) - self.mode_settings.change_persistent.connect(self.change_persistent) + self.mode_settings_widget.change_persistent.connect(self.change_persistent) header_layout = QtWidgets.QVBoxLayout() header_layout.setContentsMargins(0, 0, 0, 0) header_layout.addWidget(self.header_label) - header_layout.addWidget(self.mode_settings) + header_layout.addWidget(self.mode_settings_widget) self.header = QtWidgets.QWidget() self.header.setLayout(header_layout) diff --git a/onionshare_gui/tab/mode/receive_mode/__init__.py b/onionshare_gui/tab/mode/receive_mode/__init__.py index 3a9c2fbe..665fceba 100644 --- a/onionshare_gui/tab/mode/receive_mode/__init__.py +++ b/onionshare_gui/tab/mode/receive_mode/__init__.py @@ -57,7 +57,7 @@ class ReceiveMode(Mode): data_dir_layout.addWidget(self.data_dir_lineedit) data_dir_layout.addWidget(data_dir_button) - self.mode_settings.mode_specific_layout.addLayout(data_dir_layout) + self.mode_settings_widget.mode_specific_layout.addLayout(data_dir_layout) # Server status self.server_status.set_mode("receive") diff --git a/onionshare_gui/tab/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py index 57d6475c..fc94944b 100644 --- a/onionshare_gui/tab/mode/share_mode/__init__.py +++ b/onionshare_gui/tab/mode/share_mode/__init__.py @@ -59,7 +59,7 @@ class ShareMode(Mode): self.autostop_sharing_checkbox.setText( strings._("mode_settings_share_autostop_sharing_checkbox") ) - self.mode_settings.mode_specific_layout.addWidget( + self.mode_settings_widget.mode_specific_layout.addWidget( self.autostop_sharing_checkbox ) diff --git a/onionshare_gui/tab/mode/website_mode/__init__.py b/onionshare_gui/tab/mode/website_mode/__init__.py index e6ed785d..82d7b310 100644 --- a/onionshare_gui/tab/mode/website_mode/__init__.py +++ b/onionshare_gui/tab/mode/website_mode/__init__.py @@ -59,7 +59,9 @@ class WebsiteMode(Mode): self.disable_csp_checkbox.setText( strings._("mode_settings_website_disable_csp_checkbox") ) - self.mode_settings.mode_specific_layout.addWidget(self.disable_csp_checkbox) + self.mode_settings_widget.mode_specific_layout.addWidget( + self.disable_csp_checkbox + ) # File selection self.file_selection = FileSelection(self.common, self) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 60809b57..e1913071 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -496,7 +496,7 @@ class Tab(QtWidgets.QWidget): if self.mode is None: return True - if self.tab_settings["persistent"]["enabled"]: + if self.mode_settings.get("persistent", "enabled"): dialog_text = strings._("gui_close_tab_warning_persistent_description") else: server_status = self.get_mode().server_status From 12a1d73f91586cab4fee019cc96af859d9fd2f19 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 14:35:51 -0700 Subject: [PATCH 032/135] Make the Web object load from mode settings instead of global settings --- onionshare/web/receive_mode.py | 2 +- onionshare/web/share_mode.py | 4 ++-- onionshare/web/web.py | 14 ++++---------- onionshare_gui/tab/mode/receive_mode/__init__.py | 2 +- onionshare_gui/tab/mode/share_mode/__init__.py | 2 +- onionshare_gui/tab/mode/website_mode/__init__.py | 2 +- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index c69a821d..55f0df8c 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -292,7 +292,7 @@ class ReceiveModeRequest(Request): date_dir = now.strftime("%Y-%m-%d") time_dir = now.strftime("%H.%M.%S") self.receive_mode_dir = os.path.join( - self.web.common.settings.get("data_dir"), date_dir, time_dir + self.web.settings.get("website", "data_dir"), date_dir, time_dir ) # Create that directory, which shouldn't exist yet diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index da20c328..7e4f1672 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -18,8 +18,8 @@ class ShareModeWeb(SendBaseModeWeb): self.common.log("ShareModeWeb", "init") # Allow downloading individual files if "Stop sharing after files have been sent" is unchecked - self.download_individual_files = not self.common.settings.get( - "close_after_first_download" + self.download_individual_files = not self.web.settings.get( + "share", "autostop_sharing" ) def define_routes(self): diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 604faf02..07fa5b6b 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -60,9 +60,7 @@ class Web: REQUEST_OTHER = 13 REQUEST_INVALID_PASSWORD = 14 - def __init__( - self, common, is_gui, tab_settings_get=None, tab_settings_set=None, mode="share" - ): + def __init__(self, common, is_gui, mode_settings, mode="share"): """ tab_settings_get and tab_settings_set are getter and setter functions for tab settings """ @@ -70,8 +68,7 @@ class Web: self.common = common self.common.log("Web", "__init__", f"is_gui={is_gui}, mode={mode}") - self.settings_get = tab_settings_get - self.settings_set = tab_settings_set + self.settings = mode_settings # The flask app self.app = Flask( @@ -195,7 +192,7 @@ class Web: return None # If public mode is disabled, require authentication - if not self.common.settings.get("public_mode"): + if not self.settings.get("general", "public"): @self.auth.login_required def _check_login(): @@ -293,10 +290,7 @@ class Web: for header, value in self.security_headers: r.headers.set(header, value) # Set a CSP header unless in website mode and the user has disabled it - if ( - not self.common.settings.get("csp_header_disabled") - or self.mode != "website" - ): + if not self.settings.get("website", "disable_csp") or self.mode != "website": r.headers.set( "Content-Security-Policy", "default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:;", diff --git a/onionshare_gui/tab/mode/receive_mode/__init__.py b/onionshare_gui/tab/mode/receive_mode/__init__.py index 665fceba..0c6ff031 100644 --- a/onionshare_gui/tab/mode/receive_mode/__init__.py +++ b/onionshare_gui/tab/mode/receive_mode/__init__.py @@ -36,7 +36,7 @@ class ReceiveMode(Mode): Custom initialization for ReceiveMode. """ # Create the Web object - self.web = Web(self.common, True, "receive") + self.web = Web(self.common, True, self.settings, "receive") # Header self.header_label.setText(strings._("gui_new_tab_receive_button")) diff --git a/onionshare_gui/tab/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py index fc94944b..42478be4 100644 --- a/onionshare_gui/tab/mode/share_mode/__init__.py +++ b/onionshare_gui/tab/mode/share_mode/__init__.py @@ -45,7 +45,7 @@ class ShareMode(Mode): self.compress_thread = None # Create the Web object - self.web = Web(self.common, True, "share") + self.web = Web(self.common, True, self.settings, "share") # Header self.header_label.setText(strings._("gui_new_tab_share_button")) diff --git a/onionshare_gui/tab/mode/website_mode/__init__.py b/onionshare_gui/tab/mode/website_mode/__init__.py index 82d7b310..51ee17ef 100644 --- a/onionshare_gui/tab/mode/website_mode/__init__.py +++ b/onionshare_gui/tab/mode/website_mode/__init__.py @@ -47,7 +47,7 @@ class WebsiteMode(Mode): Custom initialization for ReceiveMode. """ # Create the Web object - self.web = Web(self.common, True, "website") + self.web = Web(self.common, True, self.settings, "website") # Header self.header_label.setText(strings._("gui_new_tab_website_button")) From b8f12994962a402555fcd153f56e1c23d71c5d57 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 14:56:40 -0700 Subject: [PATCH 033/135] Fix CLI to work with mode settings --- onionshare/__init__.py | 11 +++++++++-- onionshare/onion.py | 8 ++++++++ onionshare/web/receive_mode.py | 19 +++++++------------ share/locale/en.json | 1 - 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index a2d6d4a1..a85de871 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -26,6 +26,7 @@ from .common import Common from .web import Web from .onion import * from .onionshare import OnionShare +from .mode_settings import ModeSettings def build_url(common, app, web): @@ -177,14 +178,20 @@ def main(cwd=None): # Verbose mode? common.verbose = verbose + # Mode settings + mode_settings = ModeSettings(common) + # Create the Web object - web = Web(common, False, mode) + web = Web(common, False, mode_settings, mode) # Start the Onion object onion = Onion(common) try: onion.connect( - custom_settings=False, config=config, connect_timeout=connect_timeout + custom_settings=False, + config=config, + connect_timeout=connect_timeout, + local_only=local_only, ) except KeyboardInterrupt: print("") diff --git a/onionshare/onion.py b/onionshare/onion.py index 95a31244..15709bd6 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -191,7 +191,14 @@ class Onion(object): config=False, tor_status_update_func=None, connect_timeout=120, + local_only=False, ): + if local_only: + self.common.log( + "Onion", "connect", "--local-only, so skip trying to connect" + ) + return + self.common.log("Onion", "connect") # Either use settings that are passed in, or use them from common @@ -205,6 +212,7 @@ class Onion(object): self.settings = self.common.settings strings.load_strings(self.common) + # The Tor controller self.c = None diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 55f0df8c..17613fdd 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -292,7 +292,7 @@ class ReceiveModeRequest(Request): date_dir = now.strftime("%Y-%m-%d") time_dir = now.strftime("%H.%M.%S") self.receive_mode_dir = os.path.join( - self.web.settings.get("website", "data_dir"), date_dir, time_dir + self.web.settings.get("receive", "data_dir"), date_dir, time_dir ) # Create that directory, which shouldn't exist yet @@ -358,14 +358,9 @@ class ReceiveModeRequest(Request): except: self.content_length = 0 - print( - "{}: {}".format( - datetime.now().strftime("%b %d, %I:%M%p"), - strings._("receive_mode_upload_starting").format( - self.web.common.human_readable_filesize(self.content_length) - ), - ) - ) + date_str = datetime.now().strftime("%b %d, %I:%M%p") + size_str = self.web.common.human_readable_filesize(self.content_length) + print(f"{date_str}: Upload of total size {size_str} is starting") # Don't tell the GUI that a request has started until we start receiving files self.told_gui_about_request = False @@ -453,10 +448,10 @@ class ReceiveModeRequest(Request): if self.previous_file != filename: self.previous_file = filename - print( - f"\r=> {self.web.common.human_readable_filesize(self.progress[filename]['uploaded_bytes'])} {filename}", - end="", + size_str = self.web.common.human_readable_filesize( + self.progress[filename]["uploaded_bytes"] ) + print(f"\r=> {size_str} {filename} ", end="") # Update the GUI on the upload progress if self.told_gui_about_request: diff --git a/share/locale/en.json b/share/locale/en.json index 14695658..5425a336 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -170,7 +170,6 @@ "gui_website_mode_no_files": "No Website Shared Yet", "gui_receive_mode_no_files": "No Files Received Yet", "gui_receive_mode_autostop_timer_waiting": "Waiting to finish receiving", - "receive_mode_upload_starting": "Upload of total size {} is starting", "days_first_letter": "d", "hours_first_letter": "h", "minutes_first_letter": "m", From 9f0e031c8bec382953cf97eec0beeadec49458ed Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 15:37:21 -0700 Subject: [PATCH 034/135] Refactor onionshare CLI to accept and use all mode settings --- onionshare/__init__.py | 198 ++++++++++++++++++++----------- onionshare/onionshare.py | 7 -- onionshare/web/send_base_mode.py | 3 +- onionshare/web/share_mode.py | 19 ++- onionshare/web/web.py | 10 +- 5 files changed, 146 insertions(+), 91 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index a85de871..e7c7158c 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -29,9 +29,9 @@ from .onionshare import OnionShare from .mode_settings import ModeSettings -def build_url(common, app, web): +def build_url(mode_settings, app, web): # Build the URL - if common.settings.get("public_mode"): + if mode_settings.get("general", "public"): return f"http://{app.onion_host}" else: return f"http://onionshare:{web.password}@{app.onion_host}" @@ -56,32 +56,21 @@ def main(cwd=None): parser = argparse.ArgumentParser( formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=28) ) + # Select modes + parser.add_argument( + "--receive", action="store_true", dest="receive", help="Receive files" + ) + parser.add_argument( + "--website", action="store_true", dest="website", help="Publish website" + ) + # Tor connection-related args parser.add_argument( "--local-only", action="store_true", dest="local_only", + default=False, help="Don't use Tor (only for development)", ) - parser.add_argument( - "--stay-open", - action="store_true", - dest="stay_open", - help="Continue sharing after files have been sent", - ) - parser.add_argument( - "--auto-start-timer", - metavar="", - dest="autostart_timer", - default=0, - help="Schedule this share to start N seconds from now", - ) - parser.add_argument( - "--auto-stop-timer", - metavar="", - dest="autostop_timer", - default=0, - help="Stop sharing after a given amount of seconds", - ) parser.add_argument( "--connect-timeout", metavar="", @@ -89,30 +78,79 @@ def main(cwd=None): default=120, help="Give up connecting to Tor after a given amount of seconds (default: 120)", ) - parser.add_argument( - "--stealth", - action="store_true", - dest="stealth", - help="Use client authorization (advanced)", - ) - parser.add_argument( - "--receive", - action="store_true", - dest="receive", - help="Receive shares instead of sending them", - ) - parser.add_argument( - "--website", - action="store_true", - dest="website", - help="Publish a static website", - ) parser.add_argument( "--config", metavar="config", - default=False, - help="Custom JSON config file location (optional)", + default=None, + help="Filename of custom global settings", ) + # Persistent file + parser.add_argument( + "--persistent", + metavar="persistent", + default=None, + help="Filename of persistent session", + ) + # General args + parser.add_argument( + "--public", + action="store_true", + dest="public", + default=False, + help="Don't use a password", + ) + parser.add_argument( + "--auto-start-timer", + metavar="", + dest="autostart_timer", + default=0, + help="Start onion service at scheduled time (N seconds from now)", + ) + parser.add_argument( + "--auto-stop-timer", + metavar="", + dest="autostop_timer", + default=0, + help="Stop onion service at schedule time (N seconds from now)", + ) + parser.add_argument( + "--legacy", + action="store_true", + dest="legacy", + default=False, + help="Use legacy address (v2 onion service, not recommended)", + ) + parser.add_argument( + "--client-auth", + action="store_true", + dest="client_auth", + default=False, + help="Use client authorization (requires --legacy)", + ) + # Share args + parser.add_argument( + "--autostop-sharing", + action="store_true", + dest="autostop_sharing", + default=True, + help="Share files: Stop sharing after files have been sent", + ) + # Receive args + parser.add_argument( + "--data-dir", + metavar="data_dir", + default=None, + help="Receive files: Save files received to this directory", + ) + # Website args + parser.add_argument( + "--disable_csp", + action="store_true", + dest="disable_csp", + default=False, + help="Publish website: Disable Content Security Policy header (allows your website to use third-party resources)", + ) + # Other parser.add_argument( "-v", "--verbose", @@ -132,16 +170,21 @@ def main(cwd=None): for i in range(len(filenames)): filenames[i] = os.path.abspath(filenames[i]) - local_only = bool(args.local_only) - verbose = bool(args.verbose) - stay_open = bool(args.stay_open) - autostart_timer = int(args.autostart_timer) - autostop_timer = int(args.autostop_timer) - connect_timeout = int(args.connect_timeout) - stealth = bool(args.stealth) receive = bool(args.receive) website = bool(args.website) + local_only = bool(args.local_only) + connect_timeout = int(args.connect_timeout) config = args.config + persistent = args.persistent + public = bool(args.public) + autostart_timer = int(args.autostart_timer) + autostop_timer = int(args.autostop_timer) + legacy = bool(args.legacy) + client_auth = bool(args.client_auth) + autostop_sharing = bool(args.autostop_sharing) + data_dir = args.data_dir + disable_csp = bool(args.disable_csp) + verbose = bool(args.verbose) if receive: mode = "receive" @@ -169,6 +212,13 @@ def main(cwd=None): if not valid: sys.exit() + # client_auth can only be set if legacy is also set + if client_auth and not legacy: + print( + "Client authentication (--client-auth) is only supported with with legacy onion services (--legacy)" + ) + sys.exit() + # Re-load settings, if a custom config was passed in if config: common.load_settings(config) @@ -180,6 +230,20 @@ def main(cwd=None): # Mode settings mode_settings = ModeSettings(common) + mode_settings.set("general", "public", public) + mode_settings.set("general", "autostart_timer", autostart_timer) + mode_settings.set("general", "autostop_timer", autostop_timer) + mode_settings.set("general", "legacy", legacy) + mode_settings.set("general", "client_auth", client_auth) + if mode == "share": + mode_settings.set("share", "autostop_sharing", autostop_sharing) + if mode == "receive": + if data_dir: + mode_settings.set("receive", "data_dir", data_dir) + if mode == "website": + mode_settings.set("website", "disable_csp", disable_csp) + + # TODO: handle persistent # Create the Web object web = Web(common, False, mode_settings, mode) @@ -202,36 +266,35 @@ def main(cwd=None): # Start the onionshare app try: common.settings.load() - if not common.settings.get("public_mode"): - web.generate_password(common.settings.get("password")) + if not mode_settings.get("general", "public"): + web.generate_password(mode_settings.get("persistent", "password")) else: web.password = None app = OnionShare(common, onion, local_only, autostop_timer) - app.set_stealth(stealth) app.choose_port() # Delay the startup if a startup timer was set if autostart_timer > 0: # Can't set a schedule that is later than the auto-stop timer - if app.autostop_timer > 0 and app.autostop_timer < autostart_timer: + if autostop_timer > 0 and autostop_timer < autostart_timer: print( "The auto-stop time can't be the same or earlier than the auto-start time. Please update it to start sharing." ) sys.exit() app.start_onion_service(False, True) - url = build_url(common, app, web) + url = build_url(mode_settings, app, web) schedule = datetime.now() + timedelta(seconds=autostart_timer) if mode == "receive": print( - f"Files sent to you appear in this folder: {common.settings.get('data_dir')}" + f"Files sent to you appear in this folder: {mode_settings.get('receive', 'data_dir')}" ) print("") print( "Warning: Receive mode lets people upload files to your computer. 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." ) print("") - if stealth: + if mode_settings.get("general", "client_auth"): print( f"Give this address and HidServAuth lineto your sender, and tell them it won't be accessible until: {schedule.strftime('%I:%M:%S%p, %b %d, %y')}" ) @@ -241,7 +304,7 @@ def main(cwd=None): f"Give this address to your sender, and tell them it won't be accessible until: {schedule.strftime('%I:%M:%S%p, %b %d, %y')}" ) else: - if stealth: + if mode_settings.get("general", "client_auth"): print( f"Give this address and HidServAuth line to your recipient, and tell them it won't be accessible until: {schedule.strftime('%I:%M:%S%p, %b %d, %y')}" ) @@ -291,10 +354,7 @@ def main(cwd=None): print("") # Start OnionShare http service in new thread - t = threading.Thread( - target=web.start, - args=(app.port, stay_open, common.settings.get("public_mode"), web.password), - ) + t = threading.Thread(target=web.start, args=(app.port,)) t.daemon = True t.start() @@ -307,13 +367,13 @@ def main(cwd=None): app.autostop_timer_thread.start() # Save the web password if we are using a persistent private key - if common.settings.get("save_private_key"): - if not common.settings.get("password"): - common.settings.set("password", web.password) - common.settings.save() + if mode_settings.get("persistent", "enabled"): + if not mode_settings.get("persistent", "password"): + mode_settings.set("persistent", "password", web.password) + # mode_settings.save() # Build the URL - url = build_url(common, app, web) + url = build_url(mode_settings, app, web) print("") if autostart_timer > 0: @@ -321,7 +381,7 @@ def main(cwd=None): else: if mode == "receive": print( - f"Files sent to you appear in this folder: {common.settings.get('data_dir')}" + f"Files sent to you appear in this folder: {mode_settings.get('receive', 'data_dir')}" ) print("") print( @@ -329,7 +389,7 @@ def main(cwd=None): ) print("") - if stealth: + if mode_settings.get("general", "client_auth"): print("Give this address and HidServAuth to the sender:") print(url) print(app.auth_string) @@ -337,7 +397,7 @@ def main(cwd=None): print("Give this address to the sender:") print(url) else: - if stealth: + if mode_settings.get("general", "client_auth"): print("Give this address and HidServAuth line to the recipient:") print(url) print(app.auth_string) diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index 955f813d..a5c03ea3 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -42,7 +42,6 @@ class OnionShare(object): self.hidserv_dir = None self.onion_host = None self.port = None - self.stealth = None # files and dirs to delete on shutdown self.cleanup_filenames = [] @@ -55,12 +54,6 @@ class OnionShare(object): # init auto-stop timer thread self.autostop_timer_thread = None - def set_stealth(self, stealth): - self.common.log("OnionShare", f"set_stealth", "stealth={stealth}") - - self.stealth = stealth - self.onion.stealth = stealth - def choose_port(self): """ Choose a random port. diff --git a/onionshare/web/send_base_mode.py b/onionshare/web/send_base_mode.py index c2086f15..020b65e0 100644 --- a/onionshare/web/send_base_mode.py +++ b/onionshare/web/send_base_mode.py @@ -26,8 +26,7 @@ class SendBaseModeWeb: self.gzip_filesize = None self.zip_writer = None - # If "Stop After First Download" is checked (stay_open == False), only allow - # one download at a time. + # If autostop_sharing, only allow one download at a time self.download_in_progress = False # This tracks the history id diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 7e4f1672..60c8eca0 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -37,7 +37,10 @@ class ShareModeWeb(SendBaseModeWeb): # Deny new downloads if "Stop sharing after files have been sent" is checked and there is # currently a download - deny_download = not self.web.stay_open and self.download_in_progress + deny_download = ( + not self.web.settings.get("share", "autostop_sharing") + and self.download_in_progress + ) if deny_download: r = make_response( render_template("denied.html"), @@ -60,7 +63,10 @@ class ShareModeWeb(SendBaseModeWeb): """ # Deny new downloads if "Stop After First Download" is checked and there is # currently a download - deny_download = not self.web.stay_open and self.download_in_progress + deny_download = ( + not self.web.settings.get("share", "autostop_sharing") + and self.download_in_progress + ) if deny_download: r = make_response( render_template( @@ -96,7 +102,7 @@ class ShareModeWeb(SendBaseModeWeb): def generate(): # Starting a new download - if not self.web.stay_open: + if not self.web.settings.get("share", "autostop_sharing"): self.download_in_progress = True chunk_size = 102400 # 100kb @@ -161,11 +167,14 @@ class ShareModeWeb(SendBaseModeWeb): sys.stdout.write("\n") # Download is finished - if not self.web.stay_open: + if not self.web.settings.get("share", "autostop_sharing"): self.download_in_progress = False # Close the server, if necessary - if not self.web.stay_open and not canceled: + if ( + not self.web.settings.get("share", "autostop_sharing") + and not canceled + ): print("Stopped because transfer is complete") self.web.running = False try: diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 07fa5b6b..a143a22f 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -352,17 +352,11 @@ class Web: pass self.running = False - def start(self, port, stay_open=False, public_mode=False, password=None): + def start(self, port): """ Start the flask web server. """ - self.common.log( - "Web", - "start", - f"port={port}, stay_open={stay_open}, public_mode={public_mode}, password={password}", - ) - - self.stay_open = stay_open + self.common.log("Web", "start", f"port={port}") # Make sure the stop_q is empty when starting a new server while not self.stop_q.empty(): From 598db21dcdea4b98c4a4e5216ff2d80d7cbd4b26 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 15:39:12 -0700 Subject: [PATCH 035/135] Remove custom config from GUI CLI args, because GUI users can configure OnionShare in the GUI --- onionshare_gui/__init__.py | 12 +----------- onionshare_gui/gui_common.py | 10 +++------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index cb4f3e12..c57848a4 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -95,12 +95,6 @@ def main(): nargs="+", help="List of files or folders to share", ) - parser.add_argument( - "--config", - metavar="config", - default=False, - help="Custom JSON config file location (optional)", - ) args = parser.parse_args() filenames = args.filenames @@ -108,10 +102,6 @@ def main(): for i in range(len(filenames)): filenames[i] = os.path.abspath(filenames[i]) - config = args.config - if config: - common.load_settings(config) - local_only = bool(args.local_only) verbose = bool(args.verbose) @@ -156,7 +146,7 @@ def main(): return # Attach the GUI common parts to the common object - common.gui = GuiCommon(common, qtapp, local_only, config) + common.gui = GuiCommon(common, qtapp, local_only) # Launch the gui main_window = MainWindow(common, filenames) diff --git a/onionshare_gui/gui_common.py b/onionshare_gui/gui_common.py index c0502c3d..54086353 100644 --- a/onionshare_gui/gui_common.py +++ b/onionshare_gui/gui_common.py @@ -30,17 +30,13 @@ class GuiCommon: MODE_RECEIVE = "receive" MODE_WEBSITE = "website" - def __init__(self, common, qtapp, local_only, config): + def __init__(self, common, qtapp, local_only): self.common = common self.qtapp = qtapp self.local_only = local_only - # Load settings, if a custom config was passed in - self.config = config - if self.config: - self.common.load_settings(self.config) - else: - self.common.load_settings() + # Load settings + self.common.load_settings() # Load strings strings.load_strings(self.common) From 87918c5d89f26bce515879895ed0a4ae736dbd2a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 17:01:47 -0700 Subject: [PATCH 036/135] Fix CLI tests, and also fix bug related to autostop_sharing that the tests found --- onionshare/__init__.py | 6 +++--- onionshare/mode_settings.py | 22 +++++++++++++++++++++- onionshare/onionshare.py | 6 ++++-- onionshare/settings.py | 20 +------------------- onionshare/web/share_mode.py | 13 +++++-------- onionshare_gui/threads.py | 6 ++++-- tests/test_onionshare.py | 35 ++++++++++++----------------------- tests/test_onionshare_web.py | 21 ++++++++++----------- 8 files changed, 60 insertions(+), 69 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index e7c7158c..7bc18bff 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -282,7 +282,7 @@ def main(cwd=None): ) sys.exit() - app.start_onion_service(False, True) + app.start_onion_service(mode_settings, False, True) url = build_url(mode_settings, app, web) schedule = datetime.now() + timedelta(seconds=autostart_timer) if mode == "receive": @@ -318,9 +318,9 @@ def main(cwd=None): print("Waiting for the scheduled time before starting...") app.onion.cleanup(False) time.sleep(autostart_timer) - app.start_onion_service() + app.start_onion_service(mode_settings) else: - app.start_onion_service() + app.start_onion_service(mode_settings) except KeyboardInterrupt: print("") sys.exit() diff --git a/onionshare/mode_settings.py b/onionshare/mode_settings.py index 9557abcd..dfc0b939 100644 --- a/onionshare/mode_settings.py +++ b/onionshare/mode_settings.py @@ -17,6 +17,8 @@ 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 . """ +import os +import pwd class ModeSettings: @@ -43,7 +45,7 @@ class ModeSettings: "client_auth": False, }, "share": {"autostop_sharing": True}, - "receive": {"data_dir": self.common.settings.build_default_data_dir()}, + "receive": {"data_dir": self.build_default_data_dir()}, "website": {"disable_csp": False}, } @@ -52,3 +54,21 @@ class ModeSettings: def set(self, group, key, val): self.settings[group][key] = val + + def build_default_data_dir(self): + """ + Returns the path of the default Downloads directory for receive mode. + """ + + if self.common.platform == "Darwin": + # We can't use os.path.expanduser() in macOS because in the sandbox it + # returns the path to the sandboxed homedir + real_homedir = pwd.getpwuid(os.getuid()).pw_dir + return os.path.join(real_homedir, "OnionShare") + elif self.common.platform == "Windows": + # On Windows, os.path.expanduser() needs to use backslash, or else it + # retains the forward slash, which breaks opening the folder in explorer. + return os.path.expanduser("~\OnionShare") + else: + # All other OSes + return os.path.expanduser("~/OnionShare") diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index a5c03ea3..f4828140 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -63,7 +63,9 @@ class OnionShare(object): except: raise OSError(strings._("no_available_port")) - def start_onion_service(self, await_publication=True, save_scheduled_key=False): + def start_onion_service( + self, mode_settings, await_publication=True, save_scheduled_key=False + ): """ Start the onionshare onion service. """ @@ -83,7 +85,7 @@ class OnionShare(object): self.port, await_publication, save_scheduled_key ) - if self.stealth: + if mode_settings.get("general", "client_auth"): self.auth_string = self.onion.auth_string def cleanup(self): diff --git a/onionshare/settings.py b/onionshare/settings.py index 00854204..6d6528a4 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -122,7 +122,7 @@ class Settings(object): "public_mode": False, "password": "", "hidservauth_string": "", - "data_dir": self.build_default_data_dir(), + "data_dir": "", "csp_header_disabled": False, "locale": None, # this gets defined in fill_in_defaults() } @@ -163,24 +163,6 @@ class Settings(object): """ return os.path.join(self.common.build_data_dir(), "onionshare.json") - def build_default_data_dir(self): - """ - Returns the path of the default Downloads directory for receive mode. - """ - - if self.common.platform == "Darwin": - # We can't use os.path.expanduser() in macOS because in the sandbox it - # returns the path to the sandboxed homedir - real_homedir = pwd.getpwuid(os.getuid()).pw_dir - return os.path.join(real_homedir, "OnionShare") - elif self.common.platform == "Windows": - # On Windows, os.path.expanduser() needs to use backslash, or else it - # retains the forward slash, which breaks opening the folder in explorer. - return os.path.expanduser("~\OnionShare") - else: - # All other OSes - return os.path.expanduser("~/OnionShare") - def load(self): """ Load the settings from file. diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 60c8eca0..16a16a0b 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -38,7 +38,7 @@ class ShareModeWeb(SendBaseModeWeb): # Deny new downloads if "Stop sharing after files have been sent" is checked and there is # currently a download deny_download = ( - not self.web.settings.get("share", "autostop_sharing") + self.web.settings.get("share", "autostop_sharing") and self.download_in_progress ) if deny_download: @@ -64,7 +64,7 @@ class ShareModeWeb(SendBaseModeWeb): # Deny new downloads if "Stop After First Download" is checked and there is # currently a download deny_download = ( - not self.web.settings.get("share", "autostop_sharing") + self.web.settings.get("share", "autostop_sharing") and self.download_in_progress ) if deny_download: @@ -102,7 +102,7 @@ class ShareModeWeb(SendBaseModeWeb): def generate(): # Starting a new download - if not self.web.settings.get("share", "autostop_sharing"): + if self.web.settings.get("share", "autostop_sharing"): self.download_in_progress = True chunk_size = 102400 # 100kb @@ -167,14 +167,11 @@ class ShareModeWeb(SendBaseModeWeb): sys.stdout.write("\n") # Download is finished - if not self.web.settings.get("share", "autostop_sharing"): + if self.web.settings.get("share", "autostop_sharing"): self.download_in_progress = False # Close the server, if necessary - if ( - not self.web.settings.get("share", "autostop_sharing") - and not canceled - ): + if self.web.settings.get("share", "autostop_sharing") and not canceled: print("Stopped because transfer is complete") self.web.running = False try: diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 090574c1..a507b70f 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -61,7 +61,7 @@ class OnionThread(QtCore.QThread): try: if self.mode.obtain_onion_early: self.mode.app.start_onion_service( - await_publication=False, save_scheduled_key=True + self.mode.settings, await_publication=False, save_scheduled_key=True ) # wait for modules in thread to load, preventing a thread-related cx_Freeze crash time.sleep(0.2) @@ -69,7 +69,9 @@ class OnionThread(QtCore.QThread): # Unregister the onion so we can use it in the next OnionThread self.mode.app.onion.cleanup(False) else: - self.mode.app.start_onion_service(await_publication=True) + self.mode.app.start_onion_service( + self.mode.settings, await_publication=True + ) # wait for modules in thread to load, preventing a thread-related cx_Freeze crash time.sleep(0.2) # start onionshare http service in new thread diff --git a/tests/test_onionshare.py b/tests/test_onionshare.py index 64b16b1f..0addf6d5 100644 --- a/tests/test_onionshare.py +++ b/tests/test_onionshare.py @@ -23,13 +23,13 @@ import pytest from onionshare import OnionShare from onionshare.common import Common +from onionshare.mode_settings import ModeSettings class MyOnion: - def __init__(self, stealth=False): + def __init__(self): self.auth_string = "TestHidServAuth" self.private_key = "" - self.stealth = stealth self.scheduled_key = None @staticmethod @@ -43,38 +43,27 @@ def onionshare_obj(): return OnionShare(common, MyOnion()) +@pytest.fixture +def mode_settings_obj(): + common = Common() + return ModeSettings(common) + + class TestOnionShare: def test_init(self, onionshare_obj): assert onionshare_obj.hidserv_dir is None assert onionshare_obj.onion_host is None - assert onionshare_obj.stealth is None assert onionshare_obj.cleanup_filenames == [] assert onionshare_obj.local_only is False - def test_set_stealth_true(self, onionshare_obj): - onionshare_obj.set_stealth(True) - assert onionshare_obj.stealth is True - assert onionshare_obj.onion.stealth is True - - def test_set_stealth_false(self, onionshare_obj): - onionshare_obj.set_stealth(False) - assert onionshare_obj.stealth is False - assert onionshare_obj.onion.stealth is False - - def test_start_onion_service(self, onionshare_obj): - onionshare_obj.set_stealth(False) - onionshare_obj.start_onion_service() + def test_start_onion_service(self, onionshare_obj, mode_settings_obj): + onionshare_obj.start_onion_service(mode_settings_obj) assert 17600 <= onionshare_obj.port <= 17650 assert onionshare_obj.onion_host == "test_service_id.onion" - def test_start_onion_service_stealth(self, onionshare_obj): - onionshare_obj.set_stealth(True) - onionshare_obj.start_onion_service() - assert onionshare_obj.auth_string == "TestHidServAuth" - - def test_start_onion_service_local_only(self, onionshare_obj): + def test_start_onion_service_local_only(self, onionshare_obj, mode_settings_obj): onionshare_obj.local_only = True - onionshare_obj.start_onion_service() + onionshare_obj.start_onion_service(mode_settings_obj) assert onionshare_obj.onion_host == "127.0.0.1:{}".format(onionshare_obj.port) def test_cleanup(self, onionshare_obj, temp_dir_1024, temp_file_1024): diff --git a/tests/test_onionshare_web.py b/tests/test_onionshare_web.py index c3a0807c..2ce2f758 100644 --- a/tests/test_onionshare_web.py +++ b/tests/test_onionshare_web.py @@ -36,6 +36,7 @@ from onionshare.common import Common from onionshare import strings from onionshare.web import Web from onionshare.settings import Settings +from onionshare.mode_settings import ModeSettings DEFAULT_ZW_FILENAME_REGEX = re.compile(r"^onionshare_[a-z2-7]{6}.zip$") RANDOM_STR_REGEX = re.compile(r"^[a-z2-7]+$") @@ -45,9 +46,9 @@ def web_obj(common_obj, mode, num_files=0): """ Creates a Web object, in either share mode or receive mode, ready for testing """ common_obj.settings = Settings(common_obj) strings.load_strings(common_obj) - web = Web(common_obj, False, mode) + mode_settings = ModeSettings(common_obj) + web = Web(common_obj, False, mode_settings, mode) web.generate_password() - web.stay_open = True web.running = True web.app.testing = True @@ -56,7 +57,7 @@ def web_obj(common_obj, mode, num_files=0): if mode == "share": # Add files files = [] - for i in range(num_files): + for _ in range(num_files): with tempfile.NamedTemporaryFile(delete=False) as tmp_file: tmp_file.write(b"*" * 1024) files.append(tmp_file.name) @@ -94,9 +95,9 @@ class TestWeb: assert res.status_code == 200 assert res.mimetype == "application/zip" - def test_share_mode_close_after_first_download_on(self, common_obj, temp_file_1024): + def test_share_mode_autostop_sharing_on(self, common_obj, temp_file_1024): web = web_obj(common_obj, "share", 3) - web.stay_open = False + web.settings.set("share", "autostop_sharing", True) assert web.running == True @@ -109,11 +110,9 @@ class TestWeb: assert web.running == False - def test_share_mode_close_after_first_download_off( - self, common_obj, temp_file_1024 - ): + def test_share_mode_autostop_sharing_off(self, common_obj, temp_file_1024): web = web_obj(common_obj, "share", 3) - web.stay_open = True + web.settings.set("share", "autostop_sharing", False) assert web.running == True @@ -147,7 +146,7 @@ class TestWeb: def test_public_mode_on(self, common_obj): web = web_obj(common_obj, "receive") - common_obj.settings.set("public_mode", True) + web.settings.set("general", "public", True) with web.app.test_client() as c: # Loading / should work without auth @@ -157,7 +156,7 @@ class TestWeb: def test_public_mode_off(self, common_obj): web = web_obj(common_obj, "receive") - common_obj.settings.set("public_mode", False) + web.settings.set("general", "public", False) with web.app.test_client() as c: # Load / without auth From b815b0e9e2853f14a2bb07ce0782b3f21d355c23 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 17:06:13 -0700 Subject: [PATCH 037/135] Remove mode settings from global settings object --- onionshare/mode_settings.py | 4 ++-- onionshare/settings.py | 12 ------------ tests/test_onionshare_settings.py | 14 -------------- 3 files changed, 2 insertions(+), 28 deletions(-) diff --git a/onionshare/mode_settings.py b/onionshare/mode_settings.py index dfc0b939..9caf6345 100644 --- a/onionshare/mode_settings.py +++ b/onionshare/mode_settings.py @@ -45,7 +45,7 @@ class ModeSettings: "client_auth": False, }, "share": {"autostop_sharing": True}, - "receive": {"data_dir": self.build_default_data_dir()}, + "receive": {"data_dir": self.build_default_receive_data_dir()}, "website": {"disable_csp": False}, } @@ -55,7 +55,7 @@ class ModeSettings: def set(self, group, key, val): self.settings[group][key] = val - def build_default_data_dir(self): + def build_default_receive_data_dir(self): """ Returns the path of the default Downloads directory for receive mode. """ diff --git a/onionshare/settings.py b/onionshare/settings.py index 6d6528a4..11227817 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -106,24 +106,12 @@ class Settings(object): "socket_file_path": "/var/run/tor/control", "auth_type": "no_auth", "auth_password": "", - "close_after_first_download": True, - "autostop_timer": False, - "autostart_timer": False, - "use_stealth": False, "use_autoupdate": True, "autoupdate_timestamp": None, "no_bridges": True, "tor_bridges_use_obfs4": False, "tor_bridges_use_meek_lite_azure": False, "tor_bridges_use_custom_bridges": "", - "use_legacy_v2_onions": False, - "save_private_key": False, - "private_key": "", - "public_mode": False, - "password": "", - "hidservauth_string": "", - "data_dir": "", - "csp_header_disabled": False, "locale": None, # this gets defined in fill_in_defaults() } self._settings = {} diff --git a/tests/test_onionshare_settings.py b/tests/test_onionshare_settings.py index 0bce2f94..9c81642f 100644 --- a/tests/test_onionshare_settings.py +++ b/tests/test_onionshare_settings.py @@ -50,24 +50,12 @@ class TestSettings: "socket_file_path": "/var/run/tor/control", "auth_type": "no_auth", "auth_password": "", - "close_after_first_download": True, - "autostop_timer": False, - "autostart_timer": False, - "use_stealth": False, "use_autoupdate": True, "autoupdate_timestamp": None, "no_bridges": True, "tor_bridges_use_obfs4": False, "tor_bridges_use_meek_lite_azure": False, "tor_bridges_use_custom_bridges": "", - "use_legacy_v2_onions": False, - "save_private_key": False, - "private_key": "", - "password": "", - "hidservauth_string": "", - "data_dir": os.path.expanduser("~/OnionShare"), - "public_mode": False, - "csp_header_disabled": False, } for key in settings_obj._settings: # Skip locale, it will not always default to the same thing @@ -125,8 +113,6 @@ class TestSettings: assert settings_obj.get("socket_file_path") == "/var/run/tor/control" assert settings_obj.get("auth_type") == "no_auth" assert settings_obj.get("auth_password") == "" - assert settings_obj.get("close_after_first_download") is True - assert settings_obj.get("use_stealth") is False assert settings_obj.get("use_autoupdate") is True assert settings_obj.get("autoupdate_timestamp") is None assert settings_obj.get("autoupdate_timestamp") is None From 4b416141c2bc629a5e11642f5a87df2848adcbad Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 17:13:06 -0700 Subject: [PATCH 038/135] Stop worrying about common.gui.config in settings and update check --- onionshare/onion.py | 4 ++-- onionshare_gui/settings_dialog.py | 9 ++++----- onionshare_gui/update_checker.py | 14 ++++++-------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 15709bd6..0f335dc2 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -187,8 +187,8 @@ class Onion(object): def connect( self, - custom_settings=False, - config=False, + custom_settings=None, + config=None, tor_status_update_func=None, connect_timeout=120, local_only=False, diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index aaa1fb31..2d93b6e4 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -716,7 +716,7 @@ class SettingsDialog(QtWidgets.QDialog): def reload_settings(self): # Load settings, and fill them in - self.old_settings = Settings(self.common, self.common.gui.config) + self.old_settings = Settings(self.common) self.old_settings.load() close_after_first_download = self.old_settings.get("close_after_first_download") @@ -1063,7 +1063,6 @@ class SettingsDialog(QtWidgets.QDialog): onion = Onion(self.common) onion.connect( custom_settings=settings, - config=self.common.gui.config, tor_status_update_func=tor_status_update_func, ) @@ -1109,7 +1108,7 @@ class SettingsDialog(QtWidgets.QDialog): def update_timestamp(): # Update the last checked label - settings = Settings(self.common, self.common.gui.config) + settings = Settings(self.common) settings.load() autoupdate_timestamp = settings.get("autoupdate_timestamp") self._update_autoupdate_timestamp(autoupdate_timestamp) @@ -1152,7 +1151,7 @@ class SettingsDialog(QtWidgets.QDialog): close_forced_update_thread() forced_update_thread = UpdateThread( - self.common, self.onion, self.common.gui.config, force=True + self.common, self.onion, force=True ) forced_update_thread.update_available.connect(update_available) forced_update_thread.update_not_available.connect(update_not_available) @@ -1294,7 +1293,7 @@ class SettingsDialog(QtWidgets.QDialog): Return a Settings object that's full of values from the settings dialog. """ self.common.log("SettingsDialog", "settings_from_fields") - settings = Settings(self.common, self.common.gui.config) + settings = Settings(self.common) settings.load() # To get the last update timestamp settings.set( diff --git a/onionshare_gui/update_checker.py b/onionshare_gui/update_checker.py index 2b0edec9..452bcb5b 100644 --- a/onionshare_gui/update_checker.py +++ b/onionshare_gui/update_checker.py @@ -61,19 +61,18 @@ class UpdateChecker(QtCore.QObject): update_error = QtCore.pyqtSignal() update_invalid_version = QtCore.pyqtSignal(str) - def __init__(self, common, onion, config=False): + def __init__(self, common, onion): super(UpdateChecker, self).__init__() self.common = common self.common.log("UpdateChecker", "__init__") self.onion = onion - self.config = config - def check(self, force=False, config=False): + def check(self, force=False): self.common.log("UpdateChecker", "check", f"force={force}") # Load the settings - settings = Settings(self.common, config) + settings = Settings(self.common) settings.load() # If force=True, then definitely check @@ -188,27 +187,26 @@ class UpdateThread(QtCore.QThread): update_error = QtCore.pyqtSignal() update_invalid_version = QtCore.pyqtSignal(str) - def __init__(self, common, onion, config=False, force=False): + def __init__(self, common, onion, force=False): super(UpdateThread, self).__init__() self.common = common self.common.log("UpdateThread", "__init__") self.onion = onion - self.config = config self.force = force def run(self): self.common.log("UpdateThread", "run") - u = UpdateChecker(self.common, self.onion, self.config) + u = UpdateChecker(self.common, self.onion) u.update_available.connect(self._update_available) u.update_not_available.connect(self._update_not_available) u.update_error.connect(self._update_error) u.update_invalid_version.connect(self._update_invalid_version) try: - u.check(config=self.config, force=self.force) + u.check(force=self.force) except Exception as e: # If update check fails, silently ignore self.common.log("UpdateThread", "run", str(e)) From 52d5a5193b29fa87ba6a9a17e7b5dfa5c8a488b1 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 17:24:06 -0700 Subject: [PATCH 039/135] Remove all mode settings from settings dialog --- onionshare_gui/settings_dialog.py | 470 +----------------------------- 1 file changed, 14 insertions(+), 456 deletions(-) diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 2d93b6e4..110cfac7 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -58,273 +58,6 @@ class SettingsDialog(QtWidgets.QDialog): # If ONIONSHARE_HIDE_TOR_SETTINGS=1, hide Tor settings in the dialog self.hide_tor_settings = os.environ.get("ONIONSHARE_HIDE_TOR_SETTINGS") == "1" - # General settings - - # Use a password or not ('public mode') - self.public_mode_checkbox = QtWidgets.QCheckBox() - self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.public_mode_checkbox.setText( - strings._("gui_settings_public_mode_checkbox") - ) - public_mode_label = QtWidgets.QLabel( - strings._("gui_settings_whats_this").format( - "https://github.com/micahflee/onionshare/wiki/Public-Mode" - ) - ) - public_mode_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) - public_mode_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) - public_mode_label.setOpenExternalLinks(True) - public_mode_label.setMinimumSize(public_mode_label.sizeHint()) - public_mode_layout = QtWidgets.QHBoxLayout() - public_mode_layout.addWidget(self.public_mode_checkbox) - public_mode_layout.addWidget(public_mode_label) - public_mode_layout.addStretch() - public_mode_layout.setContentsMargins(0, 0, 0, 0) - self.public_mode_widget = QtWidgets.QWidget() - self.public_mode_widget.setLayout(public_mode_layout) - - # Whether or not to use an auto-start timer - self.autostart_timer_checkbox = QtWidgets.QCheckBox() - self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Checked) - self.autostart_timer_checkbox.setText( - strings._("gui_settings_autostart_timer_checkbox") - ) - autostart_timer_label = QtWidgets.QLabel( - strings._("gui_settings_whats_this").format( - "https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Start-Timer" - ) - ) - autostart_timer_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) - autostart_timer_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) - autostart_timer_label.setOpenExternalLinks(True) - autostart_timer_label.setMinimumSize(public_mode_label.sizeHint()) - autostart_timer_layout = QtWidgets.QHBoxLayout() - autostart_timer_layout.addWidget(self.autostart_timer_checkbox) - autostart_timer_layout.addWidget(autostart_timer_label) - autostart_timer_layout.addStretch() - autostart_timer_layout.setContentsMargins(0, 0, 0, 0) - self.autostart_timer_widget = QtWidgets.QWidget() - self.autostart_timer_widget.setLayout(autostart_timer_layout) - - # Whether or not to use an auto-stop timer - self.autostop_timer_checkbox = QtWidgets.QCheckBox() - self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Checked) - self.autostop_timer_checkbox.setText( - strings._("gui_settings_autostop_timer_checkbox") - ) - autostop_timer_label = QtWidgets.QLabel( - strings._("gui_settings_whats_this").format( - "https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Stop-Timer" - ) - ) - autostop_timer_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) - autostop_timer_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) - autostop_timer_label.setOpenExternalLinks(True) - autostop_timer_label.setMinimumSize(public_mode_label.sizeHint()) - autostop_timer_layout = QtWidgets.QHBoxLayout() - autostop_timer_layout.addWidget(self.autostop_timer_checkbox) - autostop_timer_layout.addWidget(autostop_timer_label) - autostop_timer_layout.addStretch() - autostop_timer_layout.setContentsMargins(0, 0, 0, 0) - self.autostop_timer_widget = QtWidgets.QWidget() - self.autostop_timer_widget.setLayout(autostop_timer_layout) - - # General settings layout - general_group_layout = QtWidgets.QVBoxLayout() - general_group_layout.addWidget(self.public_mode_widget) - general_group_layout.addWidget(self.autostart_timer_widget) - general_group_layout.addWidget(self.autostop_timer_widget) - general_group = QtWidgets.QGroupBox(strings._("gui_settings_general_label")) - general_group.setLayout(general_group_layout) - - # Onion settings - - # Label telling user to connect to Tor for onion service settings - self.connect_to_tor_label = QtWidgets.QLabel( - strings._("gui_connect_to_tor_for_onion_settings") - ) - self.connect_to_tor_label.setStyleSheet( - self.common.gui.css["settings_connect_to_tor"] - ) - - # Whether or not to save the Onion private key for reuse (persistent URL mode) - self.save_private_key_checkbox = QtWidgets.QCheckBox() - self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.save_private_key_checkbox.setText( - strings._("gui_save_private_key_checkbox") - ) - save_private_key_label = QtWidgets.QLabel( - strings._("gui_settings_whats_this").format( - "https://github.com/micahflee/onionshare/wiki/Using-a-Persistent-URL" - ) - ) - save_private_key_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) - save_private_key_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) - save_private_key_label.setOpenExternalLinks(True) - save_private_key_layout = QtWidgets.QHBoxLayout() - save_private_key_layout.addWidget(self.save_private_key_checkbox) - save_private_key_layout.addWidget(save_private_key_label) - save_private_key_layout.addStretch() - save_private_key_layout.setContentsMargins(0, 0, 0, 0) - self.save_private_key_widget = QtWidgets.QWidget() - self.save_private_key_widget.setLayout(save_private_key_layout) - - # Whether or not to use legacy v2 onions - self.use_legacy_v2_onions_checkbox = QtWidgets.QCheckBox() - self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.use_legacy_v2_onions_checkbox.setText( - strings._("gui_use_legacy_v2_onions_checkbox") - ) - self.use_legacy_v2_onions_checkbox.clicked.connect( - self.use_legacy_v2_onions_checkbox_clicked - ) - use_legacy_v2_onions_label = QtWidgets.QLabel( - strings._("gui_settings_whats_this").format( - "https://github.com/micahflee/onionshare/wiki/Legacy-Addresses" - ) - ) - use_legacy_v2_onions_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) - use_legacy_v2_onions_label.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction - ) - use_legacy_v2_onions_label.setOpenExternalLinks(True) - use_legacy_v2_onions_layout = QtWidgets.QHBoxLayout() - use_legacy_v2_onions_layout.addWidget(self.use_legacy_v2_onions_checkbox) - use_legacy_v2_onions_layout.addWidget(use_legacy_v2_onions_label) - use_legacy_v2_onions_layout.addStretch() - use_legacy_v2_onions_layout.setContentsMargins(0, 0, 0, 0) - self.use_legacy_v2_onions_widget = QtWidgets.QWidget() - self.use_legacy_v2_onions_widget.setLayout(use_legacy_v2_onions_layout) - - # Stealth - self.stealth_checkbox = QtWidgets.QCheckBox() - self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.stealth_checkbox.setText(strings._("gui_settings_stealth_option")) - self.stealth_checkbox.clicked.connect(self.stealth_checkbox_clicked_connect) - use_stealth_label = QtWidgets.QLabel( - strings._("gui_settings_whats_this").format( - "https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services" - ) - ) - use_stealth_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) - use_stealth_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) - use_stealth_label.setOpenExternalLinks(True) - use_stealth_label.setMinimumSize(use_stealth_label.sizeHint()) - use_stealth_layout = QtWidgets.QHBoxLayout() - use_stealth_layout.addWidget(self.stealth_checkbox) - use_stealth_layout.addWidget(use_stealth_label) - use_stealth_layout.addStretch() - use_stealth_layout.setContentsMargins(0, 0, 0, 0) - self.use_stealth_widget = QtWidgets.QWidget() - self.use_stealth_widget.setLayout(use_stealth_layout) - - self.hidservauth_details = QtWidgets.QLabel( - strings._("gui_settings_stealth_hidservauth_string") - ) - self.hidservauth_details.setWordWrap(True) - self.hidservauth_details.setMinimumSize(self.hidservauth_details.sizeHint()) - self.hidservauth_details.hide() - - self.hidservauth_copy_button = QtWidgets.QPushButton( - strings._("gui_copy_hidservauth") - ) - self.hidservauth_copy_button.clicked.connect( - self.hidservauth_copy_button_clicked - ) - self.hidservauth_copy_button.hide() - - # Onion settings widget - onion_settings_layout = QtWidgets.QVBoxLayout() - onion_settings_layout.setContentsMargins(0, 0, 0, 0) - onion_settings_layout.addWidget(self.save_private_key_widget) - onion_settings_layout.addWidget(self.use_legacy_v2_onions_widget) - onion_settings_layout.addWidget(self.use_stealth_widget) - onion_settings_layout.addWidget(self.hidservauth_details) - onion_settings_layout.addWidget(self.hidservauth_copy_button) - self.onion_settings_widget = QtWidgets.QWidget() - self.onion_settings_widget.setLayout(onion_settings_layout) - - # Onion settings layout - onion_group_layout = QtWidgets.QVBoxLayout() - onion_group_layout.addWidget(self.connect_to_tor_label) - onion_group_layout.addWidget(self.onion_settings_widget) - onion_group = QtWidgets.QGroupBox(strings._("gui_settings_onion_label")) - onion_group.setLayout(onion_group_layout) - - # Sharing options - - # Close after first download - self.close_after_first_download_checkbox = QtWidgets.QCheckBox() - self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Checked) - self.close_after_first_download_checkbox.setText( - strings._("gui_settings_close_after_first_download_option") - ) - individual_downloads_label = QtWidgets.QLabel( - strings._("gui_settings_individual_downloads_label") - ) - - # Sharing options layout - sharing_group_layout = QtWidgets.QVBoxLayout() - sharing_group_layout.addWidget(self.close_after_first_download_checkbox) - sharing_group_layout.addWidget(individual_downloads_label) - sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label")) - sharing_group.setLayout(sharing_group_layout) - - # OnionShare data dir - data_dir_label = QtWidgets.QLabel(strings._("gui_settings_data_dir_label")) - self.data_dir_lineedit = QtWidgets.QLineEdit() - self.data_dir_lineedit.setReadOnly(True) - data_dir_button = QtWidgets.QPushButton( - strings._("gui_settings_data_dir_browse_button") - ) - data_dir_button.clicked.connect(self.data_dir_button_clicked) - data_dir_layout = QtWidgets.QHBoxLayout() - data_dir_layout.addWidget(data_dir_label) - data_dir_layout.addWidget(self.data_dir_lineedit) - data_dir_layout.addWidget(data_dir_button) - - # Receiving options layout - receiving_group_layout = QtWidgets.QVBoxLayout() - receiving_group_layout.addLayout(data_dir_layout) - receiving_group = QtWidgets.QGroupBox(strings._("gui_settings_receiving_label")) - receiving_group.setLayout(receiving_group_layout) - - # Option to disable Content Security Policy (for website sharing) - self.csp_header_disabled_checkbox = QtWidgets.QCheckBox() - self.csp_header_disabled_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.csp_header_disabled_checkbox.setText( - strings._("gui_settings_csp_header_disabled_option") - ) - csp_header_label = QtWidgets.QLabel( - strings._("gui_settings_whats_this").format( - "https://github.com/micahflee/onionshare/wiki/Content-Security-Policy" - ) - ) - csp_header_label.setStyleSheet(self.common.gui.css["settings_whats_this"]) - csp_header_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) - csp_header_label.setOpenExternalLinks(True) - csp_header_label.setMinimumSize(csp_header_label.sizeHint()) - csp_header_layout = QtWidgets.QHBoxLayout() - csp_header_layout.addWidget(self.csp_header_disabled_checkbox) - csp_header_layout.addWidget(csp_header_label) - csp_header_layout.addStretch() - csp_header_layout.setContentsMargins(0, 0, 0, 0) - self.csp_header_widget = QtWidgets.QWidget() - self.csp_header_widget.setLayout(csp_header_layout) - - # Website settings widget - website_settings_layout = QtWidgets.QVBoxLayout() - website_settings_layout.setContentsMargins(0, 0, 0, 0) - website_settings_layout.addWidget(self.csp_header_widget) - self.website_settings_widget = QtWidgets.QWidget() - self.website_settings_widget.setLayout(website_settings_layout) - - # Website mode options layout - website_group_layout = QtWidgets.QVBoxLayout() - website_group_layout.addWidget(self.website_settings_widget) - website_group = QtWidgets.QGroupBox(strings._("gui_settings_website_label")) - website_group.setLayout(website_group_layout) - # Automatic updates options # Autoupdate @@ -684,29 +417,22 @@ class SettingsDialog(QtWidgets.QDialog): self.tor_status.hide() # Layout - left_col_layout = QtWidgets.QVBoxLayout() - left_col_layout.addWidget(general_group) - left_col_layout.addWidget(onion_group) - left_col_layout.addWidget(sharing_group) - left_col_layout.addWidget(receiving_group) - left_col_layout.addWidget(website_group) - left_col_layout.addWidget(autoupdate_group) - left_col_layout.addLayout(language_layout) - left_col_layout.addStretch() - - right_col_layout = QtWidgets.QVBoxLayout() - right_col_layout.addWidget(connection_type_radio_group) - right_col_layout.addLayout(connection_type_layout) - right_col_layout.addWidget(self.tor_status) - right_col_layout.addStretch() - - col_layout = QtWidgets.QHBoxLayout() - col_layout.addLayout(left_col_layout) - if not self.hide_tor_settings: - col_layout.addLayout(right_col_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() - layout.addLayout(col_layout) + 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.addStretch() layout.addLayout(buttons_layout) self.setLayout(layout) @@ -719,64 +445,6 @@ class SettingsDialog(QtWidgets.QDialog): self.old_settings = Settings(self.common) self.old_settings.load() - close_after_first_download = self.old_settings.get("close_after_first_download") - if close_after_first_download: - self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Checked) - else: - self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Unchecked) - - csp_header_disabled = self.old_settings.get("csp_header_disabled") - if csp_header_disabled: - self.csp_header_disabled_checkbox.setCheckState(QtCore.Qt.Checked) - else: - self.csp_header_disabled_checkbox.setCheckState(QtCore.Qt.Unchecked) - - autostart_timer = self.old_settings.get("autostart_timer") - if autostart_timer: - self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Checked) - else: - self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) - - autostop_timer = self.old_settings.get("autostop_timer") - if autostop_timer: - self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Checked) - else: - self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) - - save_private_key = self.old_settings.get("save_private_key") - if save_private_key: - self.save_private_key_checkbox.setCheckState(QtCore.Qt.Checked) - else: - self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked) - - use_legacy_v2_onions = self.old_settings.get("use_legacy_v2_onions") - - if use_legacy_v2_onions: - self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Checked) - self.use_stealth_widget.show() - else: - self.use_stealth_widget.hide() - - data_dir = self.old_settings.get("data_dir") - self.data_dir_lineedit.setText(data_dir) - - public_mode = self.old_settings.get("public_mode") - if public_mode: - self.public_mode_checkbox.setCheckState(QtCore.Qt.Checked) - else: - self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked) - - use_stealth = self.old_settings.get("use_stealth") - if use_stealth: - self.stealth_checkbox.setCheckState(QtCore.Qt.Checked) - # Legacy v2 mode is forced on if Stealth is enabled - self.use_legacy_v2_onions_checkbox.setEnabled(False) - if save_private_key and self.old_settings.get("hidservauth_string") != "": - self.hidservauth_details.show() - self.hidservauth_copy_button.show() - else: - self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked) - use_autoupdate = self.old_settings.get("use_autoupdate") if use_autoupdate: self.autoupdate_checkbox.setCheckState(QtCore.Qt.Checked) @@ -855,25 +523,6 @@ class SettingsDialog(QtWidgets.QDialog): new_bridges = "".join(new_bridges) self.tor_bridges_use_custom_textbox.setPlainText(new_bridges) - # If we're connected to Tor, show onion service settings, show label if not - if self.common.gui.onion.is_authenticated(): - self.connect_to_tor_label.hide() - self.onion_settings_widget.show() - - # If v3 onion services are supported, allow using legacy mode - if self.common.gui.onion.supports_v3_onions: - self.common.log("SettingsDialog", "__init__", "v3 onions are supported") - self.use_legacy_v2_onions_checkbox.show() - else: - self.common.log( - "SettingsDialog", "__init__", "v3 onions are not supported" - ) - self.use_legacy_v2_onions_widget.hide() - self.use_legacy_v2_onions_checkbox_clicked(True) - else: - self.connect_to_tor_label.show() - self.onion_settings_widget.hide() - def connection_type_bundled_toggled(self, checked): """ Connection type bundled was toggled. If checked, hide authentication fields. @@ -990,55 +639,6 @@ class SettingsDialog(QtWidgets.QDialog): else: self.authenticate_password_extras.hide() - def hidservauth_copy_button_clicked(self): - """ - Toggle the 'Copy HidServAuth' button - to copy the saved HidServAuth to clipboard. - """ - self.common.log( - "SettingsDialog", - "hidservauth_copy_button_clicked", - "HidServAuth was copied to clipboard", - ) - clipboard = self.common.gui.qtapp.clipboard() - clipboard.setText(self.old_settings.get("hidservauth_string")) - - def use_legacy_v2_onions_checkbox_clicked(self, checked): - """ - Show the legacy settings if the legacy mode is enabled. - """ - if checked: - self.use_stealth_widget.show() - else: - self.use_stealth_widget.hide() - - def stealth_checkbox_clicked_connect(self, checked): - """ - Prevent the v2 legacy mode being switched off if stealth is enabled - """ - if checked: - self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Checked) - self.use_legacy_v2_onions_checkbox.setEnabled(False) - else: - self.use_legacy_v2_onions_checkbox.setEnabled(True) - - def data_dir_button_clicked(self): - """ - Browse for a new OnionShare data directory - """ - data_dir = self.data_dir_lineedit.text() - selected_dir = QtWidgets.QFileDialog.getExistingDirectory( - self, strings._("gui_settings_data_dir_label"), data_dir - ) - - if selected_dir: - self.common.log( - "SettingsDialog", - "data_dir_button_clicked", - f"selected dir: {selected_dir}", - ) - self.data_dir_lineedit.setText(selected_dir) - def test_tor_clicked(self): """ Test Tor Settings button clicked. With the given settings, see if we can @@ -1296,48 +896,6 @@ class SettingsDialog(QtWidgets.QDialog): settings = Settings(self.common) settings.load() # To get the last update timestamp - settings.set( - "close_after_first_download", - self.close_after_first_download_checkbox.isChecked(), - ) - settings.set( - "csp_header_disabled", self.csp_header_disabled_checkbox.isChecked() - ) - settings.set("autostart_timer", self.autostart_timer_checkbox.isChecked()) - settings.set("autostop_timer", self.autostop_timer_checkbox.isChecked()) - - # Complicated logic here to force v2 onion mode on or off depending on other settings - if self.use_legacy_v2_onions_checkbox.isChecked(): - use_legacy_v2_onions = True - else: - use_legacy_v2_onions = False - - if self.save_private_key_checkbox.isChecked(): - settings.set("save_private_key", True) - settings.set("private_key", self.old_settings.get("private_key")) - settings.set("password", self.old_settings.get("password")) - settings.set( - "hidservauth_string", self.old_settings.get("hidservauth_string") - ) - else: - settings.set("save_private_key", False) - settings.set("private_key", "") - settings.set("password", "") - # Also unset the HidServAuth if we are removing our reusable private key - settings.set("hidservauth_string", "") - - if use_legacy_v2_onions: - settings.set("use_legacy_v2_onions", True) - else: - settings.set("use_legacy_v2_onions", False) - - settings.set("data_dir", self.data_dir_lineedit.text()) - settings.set("public_mode", self.public_mode_checkbox.isChecked()) - settings.set("use_stealth", self.stealth_checkbox.isChecked()) - # Always unset the HidServAuth if Stealth mode is unset - if not self.stealth_checkbox.isChecked(): - settings.set("hidservauth_string", "") - # Language locale_index = self.language_combobox.currentIndex() locale = self.language_combobox.itemData(locale_index) From ed1c3e1bc8def9c781c1b6bc6131bca0db468999 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 17:39:27 -0700 Subject: [PATCH 040/135] Refactor ServerStatus to use mode settings --- onionshare_gui/tab/mode/__init__.py | 21 ++++++++---- onionshare_gui/tab/server_status.py | 51 +++++++++++++++-------------- onionshare_gui/threads.py | 7 +--- 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/onionshare_gui/tab/mode/__init__.py b/onionshare_gui/tab/mode/__init__.py index cae046e0..e797dad9 100644 --- a/onionshare_gui/tab/mode/__init__.py +++ b/onionshare_gui/tab/mode/__init__.py @@ -88,7 +88,12 @@ class Mode(QtWidgets.QWidget): # Server status self.server_status = ServerStatus( - self.common, self.qtapp, self.app, None, self.common.gui.local_only + self.common, + self.qtapp, + self.app, + self.settings, + None, + self.common.gui.local_only, ) self.server_status.server_started.connect(self.start_server) self.server_status.server_stopped.connect(self.stop_server) @@ -169,8 +174,8 @@ class Mode(QtWidgets.QWidget): # If the auto-stop timer has stopped, stop the server if self.server_status.status == ServerStatus.STATUS_STARTED: - if self.app.autostop_timer_thread and self.common.settings.get( - "autostop_timer" + if self.app.autostop_timer_thread and self.settings.get( + "general", "autostop_timer" ): if self.autostop_timer_datetime_delta > 0: now = QtCore.QDateTime.currentDateTime() @@ -217,14 +222,15 @@ class Mode(QtWidgets.QWidget): self.common.log("Mode", "start_server") self.start_server_custom() - self.set_server_active.emit(True) - self.app.set_stealth(self.common.settings.get("use_stealth")) # Clear the status bar self.status_bar.clearMessage() self.server_status_label.setText("") + # Hide the mode settings + self.mode_settings_widget.hide() + # Ensure we always get a new random port each time we might launch an OnionThread self.app.port = None @@ -307,7 +313,7 @@ class Mode(QtWidgets.QWidget): self.start_server_step3_custom() - if self.common.settings.get("autostop_timer"): + if self.settings.get("general", "autostop_timer"): # Convert the date value to seconds between now and then now = QtCore.QDateTime.currentDateTime() self.autostop_timer_datetime_delta = now.secsTo( @@ -395,6 +401,9 @@ class Mode(QtWidgets.QWidget): self.set_server_active.emit(False) self.stop_server_finished.emit() + # Show the mode settings + self.mode_settings_widget.show() + def stop_server_custom(self): """ Add custom initialization here. diff --git a/onionshare_gui/tab/server_status.py b/onionshare_gui/tab/server_status.py index 94651c5a..278d519a 100644 --- a/onionshare_gui/tab/server_status.py +++ b/onionshare_gui/tab/server_status.py @@ -43,7 +43,9 @@ class ServerStatus(QtWidgets.QWidget): STATUS_WORKING = 1 STATUS_STARTED = 2 - def __init__(self, common, qtapp, app, file_selection=None, local_only=False): + def __init__( + self, common, qtapp, app, mode_settings, file_selection=None, local_only=False + ): super(ServerStatus, self).__init__() self.common = common @@ -53,6 +55,7 @@ class ServerStatus(QtWidgets.QWidget): self.qtapp = qtapp self.app = app + self.settings = mode_settings self.web = None self.autostart_timer_datetime = None @@ -258,9 +261,9 @@ class ServerStatus(QtWidgets.QWidget): ) # Show a Tool Tip explaining the lifecycle of this URL - if self.common.settings.get("save_private_key"): - if self.mode == self.common.gui.MODE_SHARE and self.common.settings.get( - "close_after_first_download" + if self.settings.get("persistent", "enabled"): + if self.mode == self.common.gui.MODE_SHARE and self.settings.get( + "share", "autostop_sharing" ): self.url_description.setToolTip( strings._("gui_url_label_onetime_and_persistent") @@ -268,8 +271,8 @@ class ServerStatus(QtWidgets.QWidget): else: self.url_description.setToolTip(strings._("gui_url_label_persistent")) else: - if self.mode == self.common.gui.MODE_SHARE and self.common.settings.get( - "close_after_first_download" + if self.mode == self.common.gui.MODE_SHARE and self.settings.get( + "share", "autostop_sharing" ): self.url_description.setToolTip(strings._("gui_url_label_onetime")) else: @@ -279,7 +282,7 @@ class ServerStatus(QtWidgets.QWidget): self.url.show() self.copy_url_button.show() - if self.app.stealth: + if self.settings.get("general", "client_auth"): self.copy_hidservauth_button.show() else: self.copy_hidservauth_button.hide() @@ -295,15 +298,15 @@ class ServerStatus(QtWidgets.QWidget): self.common.settings.load() self.show_url() - if self.common.settings.get("save_private_key"): - if not self.common.settings.get("password"): - self.common.settings.set("password", self.web.password) - self.common.settings.save() + if self.settings.get("persistent", "enabled"): + if not self.settings.get("persistent", "password"): + self.settings.set("persistent", "password", self.web.password) + self.settings.save() - if self.common.settings.get("autostart_timer"): + if self.settings.get("general", "autostart_timer"): self.autostart_timer_container.hide() - if self.common.settings.get("autostop_timer"): + if self.settings.get("general", "autostop_timer"): self.autostop_timer_container.hide() else: self.url_description.hide() @@ -337,9 +340,9 @@ class ServerStatus(QtWidgets.QWidget): else: self.server_button.setText(strings._("gui_receive_start_server")) self.server_button.setToolTip("") - if self.common.settings.get("autostart_timer"): + if self.settings.get("general", "autostart_timer"): self.autostart_timer_container.show() - if self.common.settings.get("autostop_timer"): + if self.settings.get("general", "autostop_timer"): self.autostop_timer_container.show() elif self.status == self.STATUS_STARTED: self.server_button.setStyleSheet( @@ -352,9 +355,9 @@ class ServerStatus(QtWidgets.QWidget): self.server_button.setText(strings._("gui_share_stop_server")) else: self.server_button.setText(strings._("gui_receive_stop_server")) - if self.common.settings.get("autostart_timer"): + if self.settings.get("general", "autostart_timer"): self.autostart_timer_container.hide() - if self.common.settings.get("autostop_timer"): + if self.settings.get("general", "autostop_timer"): self.autostop_timer_container.hide() self.server_button.setToolTip( strings._("gui_stop_server_autostop_timer_tooltip").format( @@ -379,7 +382,7 @@ class ServerStatus(QtWidgets.QWidget): ) else: self.server_button.setText(strings._("gui_please_wait")) - if self.common.settings.get("autostop_timer"): + if self.settings.get("general", "autostop_timer"): self.autostop_timer_container.hide() else: self.server_button.setStyleSheet( @@ -387,7 +390,7 @@ class ServerStatus(QtWidgets.QWidget): ) self.server_button.setEnabled(False) self.server_button.setText(strings._("gui_please_wait")) - if self.common.settings.get("autostart_timer"): + if self.settings.get("general", "autostart_timer"): self.autostart_timer_container.hide() self.server_button.setToolTip( strings._("gui_start_server_autostart_timer_tooltip").format( @@ -396,7 +399,7 @@ class ServerStatus(QtWidgets.QWidget): ) ) ) - if self.common.settings.get("autostop_timer"): + if self.settings.get("general", "autostop_timer"): self.autostop_timer_container.hide() def server_button_clicked(self): @@ -405,7 +408,7 @@ class ServerStatus(QtWidgets.QWidget): """ if self.status == self.STATUS_STOPPED: can_start = True - if self.common.settings.get("autostart_timer"): + if self.settings.get("general", "autostart_timer"): if self.local_only: self.autostart_timer_datetime = ( self.autostart_timer_widget.dateTime().toPyDateTime() @@ -427,7 +430,7 @@ class ServerStatus(QtWidgets.QWidget): strings._("gui_server_autostart_timer_expired"), QtWidgets.QMessageBox.Warning, ) - if self.common.settings.get("autostop_timer"): + if self.settings.get("general", "autostop_timer"): if self.local_only: self.autostop_timer_datetime = ( self.autostop_timer_widget.dateTime().toPyDateTime() @@ -450,7 +453,7 @@ class ServerStatus(QtWidgets.QWidget): strings._("gui_server_autostop_timer_expired"), QtWidgets.QMessageBox.Warning, ) - if self.common.settings.get("autostart_timer"): + if self.settings.get("general", "autostart_timer"): if self.autostop_timer_datetime <= self.autostart_timer_datetime: Alert( self.common, @@ -537,7 +540,7 @@ class ServerStatus(QtWidgets.QWidget): """ Returns the OnionShare URL. """ - if self.common.settings.get("public_mode"): + if self.settings.get("general", "public"): url = f"http://{self.app.onion_host}" else: url = f"http://onionshare:{self.web.password}@{self.app.onion_host}" diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index a507b70f..90a0fbb9 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -111,12 +111,7 @@ class WebThread(QtCore.QThread): def run(self): self.mode.common.log("WebThread", "run") - self.mode.web.start( - self.mode.app.port, - self.mode.app.stay_open, - self.mode.common.settings.get("public_mode"), - self.mode.web.password, - ) + self.mode.web.start(self.mode.app.port) self.success.emit() From f04452777daa3303b4a5c711f514543074a8da87 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 18:08:14 -0700 Subject: [PATCH 041/135] Show and hide autostart/autostop timer widgets when the mode settings are toggled --- onionshare_gui/tab/mode/__init__.py | 41 ++++++----- .../tab/mode/mode_settings_widget.py | 3 + onionshare_gui/tab/server_status.py | 71 ++++++++++--------- 3 files changed, 64 insertions(+), 51 deletions(-) diff --git a/onionshare_gui/tab/mode/__init__.py b/onionshare_gui/tab/mode/__init__.py index e797dad9..3ed1172a 100644 --- a/onionshare_gui/tab/mode/__init__.py +++ b/onionshare_gui/tab/mode/__init__.py @@ -67,25 +67,6 @@ class Mode(QtWidgets.QWidget): self.web_thread = None self.startup_thread = None - # Header - # Note: It's up to the downstream Mode to add this to its layout - self.header_label = QtWidgets.QLabel() - self.header_label.setStyleSheet(self.common.gui.css["mode_header_label"]) - self.header_label.setAlignment(QtCore.Qt.AlignHCenter) - - self.mode_settings_widget = ModeSettingsWidget( - self.common, self.tab.tab_id, self.tab.mode_settings - ) - self.mode_settings_widget.change_persistent.connect(self.change_persistent) - - header_layout = QtWidgets.QVBoxLayout() - header_layout.setContentsMargins(0, 0, 0, 0) - header_layout.addWidget(self.header_label) - header_layout.addWidget(self.mode_settings_widget) - - self.header = QtWidgets.QWidget() - self.header.setLayout(header_layout) - # Server status self.server_status = ServerStatus( self.common, @@ -105,6 +86,28 @@ class Mode(QtWidgets.QWidget): self.starting_server_early.connect(self.start_server_early) self.starting_server_error.connect(self.start_server_error) + # Header + # Note: It's up to the downstream Mode to add this to its layout + self.header_label = QtWidgets.QLabel() + self.header_label.setStyleSheet(self.common.gui.css["mode_header_label"]) + self.header_label.setAlignment(QtCore.Qt.AlignHCenter) + + self.mode_settings_widget = ModeSettingsWidget( + self.common, self.tab.tab_id, self.tab.mode_settings + ) + self.mode_settings_widget.change_persistent.connect(self.change_persistent) + self.mode_settings_widget.update_server_status.connect( + self.server_status.update + ) + + header_layout = QtWidgets.QVBoxLayout() + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(self.header_label) + header_layout.addWidget(self.mode_settings_widget) + + self.header = QtWidgets.QWidget() + self.header.setLayout(header_layout) + # Primary action # Note: It's up to the downstream Mode to add this to its layout self.primary_action_layout = QtWidgets.QVBoxLayout() diff --git a/onionshare_gui/tab/mode/mode_settings_widget.py b/onionshare_gui/tab/mode/mode_settings_widget.py index 23fcd087..8b6136cd 100644 --- a/onionshare_gui/tab/mode/mode_settings_widget.py +++ b/onionshare_gui/tab/mode/mode_settings_widget.py @@ -28,6 +28,7 @@ class ModeSettingsWidget(QtWidgets.QWidget): """ change_persistent = QtCore.pyqtSignal(int, bool) + update_server_status = QtCore.pyqtSignal() def __init__(self, common, tab_id, mode_settings): super(ModeSettingsWidget, self).__init__() @@ -149,11 +150,13 @@ class ModeSettingsWidget(QtWidgets.QWidget): self.settings.set( "general", "autostart_timer", self.autostart_timer_checkbox.isChecked() ) + self.update_server_status.emit() def autostop_timer_checkbox_clicked(self): self.settings.set( "general", "autostop_timer", self.autostop_timer_checkbox.isChecked() ) + self.update_server_status.emit() def legacy_checkbox_clicked(self): self.settings.set("general", "legacy", self.legacy_checkbox.isChecked()) diff --git a/onionshare_gui/tab/server_status.py b/onionshare_gui/tab/server_status.py index 278d519a..317e9cc8 100644 --- a/onionshare_gui/tab/server_status.py +++ b/onionshare_gui/tab/server_status.py @@ -291,6 +291,10 @@ class ServerStatus(QtWidgets.QWidget): """ Update the GUI elements based on the current state. """ + self.common.log("ServerStatus", "update") + autostart_timer = self.settings.get("general", "autostart_timer") + autostop_timer = self.settings.get("general", "autostop_timer") + # Set the URL fields if self.status == self.STATUS_STARTED: # The backend Onion may have saved new settings, such as the private key. @@ -302,18 +306,47 @@ class ServerStatus(QtWidgets.QWidget): if not self.settings.get("persistent", "password"): self.settings.set("persistent", "password", self.web.password) self.settings.save() - - if self.settings.get("general", "autostart_timer"): - self.autostart_timer_container.hide() - - if self.settings.get("general", "autostop_timer"): - self.autostop_timer_container.hide() else: self.url_description.hide() self.url.hide() self.copy_url_button.hide() self.copy_hidservauth_button.hide() + # Autostart and autostop timers + if self.status == self.STATUS_STOPPED: + if autostart_timer: + self.autostart_timer_container.show() + else: + self.autostart_timer_container.hide() + if autostop_timer: + self.autostop_timer_container.show() + else: + self.autostop_timer_container.hide() + elif self.status == self.STATUS_STARTED: + self.autostart_timer_container.hide() + self.autostop_timer_container.hide() + + if autostop_timer: + self.server_button.setToolTip( + strings._("gui_stop_server_autostop_timer_tooltip").format( + self.autostop_timer_widget.dateTime().toString( + "h:mm AP, MMMM dd, yyyy" + ) + ) + ) + elif self.status == self.STATUS_WORKING: + self.autostart_timer_container.hide() + self.autostop_timer_container.hide() + + if autostart_timer: + self.server_button.setToolTip( + strings._("gui_start_server_autostart_timer_tooltip").format( + self.autostart_timer_widget.dateTime().toString( + "h:mm AP, MMMM dd, yyyy" + ) + ) + ) + # Button if ( self.mode == self.common.gui.MODE_SHARE @@ -340,10 +373,6 @@ class ServerStatus(QtWidgets.QWidget): else: self.server_button.setText(strings._("gui_receive_start_server")) self.server_button.setToolTip("") - if self.settings.get("general", "autostart_timer"): - self.autostart_timer_container.show() - if self.settings.get("general", "autostop_timer"): - self.autostop_timer_container.show() elif self.status == self.STATUS_STARTED: self.server_button.setStyleSheet( self.common.gui.css["server_status_button_started"] @@ -355,17 +384,6 @@ class ServerStatus(QtWidgets.QWidget): self.server_button.setText(strings._("gui_share_stop_server")) else: self.server_button.setText(strings._("gui_receive_stop_server")) - if self.settings.get("general", "autostart_timer"): - self.autostart_timer_container.hide() - if self.settings.get("general", "autostop_timer"): - self.autostop_timer_container.hide() - self.server_button.setToolTip( - strings._("gui_stop_server_autostop_timer_tooltip").format( - self.autostop_timer_widget.dateTime().toString( - "h:mm AP, MMMM dd, yyyy" - ) - ) - ) elif self.status == self.STATUS_WORKING: self.server_button.setStyleSheet( self.common.gui.css["server_status_button_working"] @@ -390,17 +408,6 @@ class ServerStatus(QtWidgets.QWidget): ) self.server_button.setEnabled(False) self.server_button.setText(strings._("gui_please_wait")) - if self.settings.get("general", "autostart_timer"): - self.autostart_timer_container.hide() - self.server_button.setToolTip( - strings._("gui_start_server_autostart_timer_tooltip").format( - self.autostart_timer_widget.dateTime().toString( - "h:mm AP, MMMM dd, yyyy" - ) - ) - ) - if self.settings.get("general", "autostop_timer"): - self.autostop_timer_container.hide() def server_button_clicked(self): """ From 9f920f43532b009f8683cf7a929dd8be7269d983 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 18:29:24 -0700 Subject: [PATCH 042/135] Move mode settings widget into the primary action layout, and tweak window size --- onionshare_gui/main_window.py | 2 +- onionshare_gui/tab/mode/__init__.py | 25 ++++++------------- .../tab/mode/receive_mode/__init__.py | 5 ++-- .../tab/mode/share_mode/__init__.py | 6 ++--- .../tab/mode/website_mode/__init__.py | 6 ++--- onionshare_gui/widgets.py | 11 ++++++++ 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index c1bb57bf..e6f5606e 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -41,7 +41,7 @@ class MainWindow(QtWidgets.QMainWindow): self.common.log("MainWindow", "__init__") # Initialize the window - self.setMinimumWidth(820) + self.setMinimumWidth(1040) self.setMinimumHeight(700) self.setWindowTitle("OnionShare") self.setWindowIcon( diff --git a/onionshare_gui/tab/mode/__init__.py b/onionshare_gui/tab/mode/__init__.py index 3ed1172a..2c95d445 100644 --- a/onionshare_gui/tab/mode/__init__.py +++ b/onionshare_gui/tab/mode/__init__.py @@ -86,12 +86,7 @@ class Mode(QtWidgets.QWidget): self.starting_server_early.connect(self.start_server_early) self.starting_server_error.connect(self.start_server_error) - # Header - # Note: It's up to the downstream Mode to add this to its layout - self.header_label = QtWidgets.QLabel() - self.header_label.setStyleSheet(self.common.gui.css["mode_header_label"]) - self.header_label.setAlignment(QtCore.Qt.AlignHCenter) - + # Mode settings widget self.mode_settings_widget = ModeSettingsWidget( self.common, self.tab.tab_id, self.tab.mode_settings ) @@ -100,26 +95,20 @@ class Mode(QtWidgets.QWidget): self.server_status.update ) - header_layout = QtWidgets.QVBoxLayout() - header_layout.setContentsMargins(0, 0, 0, 0) - header_layout.addWidget(self.header_label) - header_layout.addWidget(self.mode_settings_widget) - - self.header = QtWidgets.QWidget() - self.header.setLayout(header_layout) + # Header + # Note: It's up to the downstream Mode to add this to its layout + self.header_label = QtWidgets.QLabel() + self.header_label.setStyleSheet(self.common.gui.css["mode_header_label"]) + self.header_label.setAlignment(QtCore.Qt.AlignHCenter) # Primary action # Note: It's up to the downstream Mode to add this to its layout self.primary_action_layout = QtWidgets.QVBoxLayout() + self.primary_action_layout.addWidget(self.mode_settings_widget) self.primary_action_layout.addWidget(self.server_status) self.primary_action = QtWidgets.QWidget() self.primary_action.setLayout(self.primary_action_layout) - # Hack to allow a minimum width on the main layout - # Note: It's up to the downstream Mode to add this to its layout - self.min_width_widget = QtWidgets.QWidget() - self.min_width_widget.setMinimumWidth(600) - def init(self): """ Add custom initialization here. diff --git a/onionshare_gui/tab/mode/receive_mode/__init__.py b/onionshare_gui/tab/mode/receive_mode/__init__.py index 0c6ff031..f7bcbd1c 100644 --- a/onionshare_gui/tab/mode/receive_mode/__init__.py +++ b/onionshare_gui/tab/mode/receive_mode/__init__.py @@ -24,6 +24,7 @@ from onionshare.web import Web from ..history import History, ToggleHistory, ReceiveHistoryItem from .. import Mode +from ....widgets import MinimumWidthWidget class ReceiveMode(Mode): @@ -111,7 +112,7 @@ class ReceiveMode(Mode): self.main_layout.addWidget(receive_warning) self.main_layout.addWidget(self.primary_action) self.main_layout.addStretch() - self.main_layout.addWidget(self.min_width_widget) + self.main_layout.addWidget(MinimumWidthWidget(700)) # Column layout self.column_layout = QtWidgets.QHBoxLayout() @@ -120,7 +121,7 @@ class ReceiveMode(Mode): # Wrapper layout self.wrapper_layout = QtWidgets.QVBoxLayout() - self.wrapper_layout.addWidget(self.header) + self.wrapper_layout.addWidget(self.header_label) self.wrapper_layout.addLayout(self.column_layout) self.setLayout(self.wrapper_layout) diff --git a/onionshare_gui/tab/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py index 42478be4..7d195e42 100644 --- a/onionshare_gui/tab/mode/share_mode/__init__.py +++ b/onionshare_gui/tab/mode/share_mode/__init__.py @@ -29,7 +29,7 @@ from ..file_selection import FileSelection from .threads import CompressThread from .. import Mode from ..history import History, ToggleHistory, ShareHistoryItem -from ....widgets import Alert +from ....widgets import Alert, MinimumWidthWidget class ShareMode(Mode): @@ -137,7 +137,7 @@ class ShareMode(Mode): self.main_layout.addLayout(top_bar_layout) self.main_layout.addLayout(self.file_selection) self.main_layout.addWidget(self.primary_action) - self.main_layout.addWidget(self.min_width_widget) + self.main_layout.addWidget(MinimumWidthWidget(700)) # Column layout self.column_layout = QtWidgets.QHBoxLayout() @@ -146,7 +146,7 @@ class ShareMode(Mode): # Wrapper layout self.wrapper_layout = QtWidgets.QVBoxLayout() - self.wrapper_layout.addWidget(self.header) + self.wrapper_layout.addWidget(self.header_label) self.wrapper_layout.addLayout(self.column_layout) self.setLayout(self.wrapper_layout) diff --git a/onionshare_gui/tab/mode/website_mode/__init__.py b/onionshare_gui/tab/mode/website_mode/__init__.py index 51ee17ef..4a099488 100644 --- a/onionshare_gui/tab/mode/website_mode/__init__.py +++ b/onionshare_gui/tab/mode/website_mode/__init__.py @@ -31,7 +31,7 @@ from onionshare.web import Web from ..file_selection import FileSelection from .. import Mode from ..history import History, ToggleHistory -from ....widgets import Alert +from ....widgets import Alert, MinimumWidthWidget class WebsiteMode(Mode): @@ -137,7 +137,7 @@ class WebsiteMode(Mode): self.main_layout.addLayout(top_bar_layout) self.main_layout.addLayout(self.file_selection) self.main_layout.addWidget(self.primary_action) - self.main_layout.addWidget(self.min_width_widget) + self.main_layout.addWidget(MinimumWidthWidget(700)) # Column layout self.column_layout = QtWidgets.QHBoxLayout() @@ -146,7 +146,7 @@ class WebsiteMode(Mode): # Wrapper layout self.wrapper_layout = QtWidgets.QVBoxLayout() - self.wrapper_layout.addWidget(self.header) + self.wrapper_layout.addWidget(self.header_label) self.wrapper_layout.addLayout(self.column_layout) self.setLayout(self.wrapper_layout) diff --git a/onionshare_gui/widgets.py b/onionshare_gui/widgets.py index d16485fe..74ef2c88 100644 --- a/onionshare_gui/widgets.py +++ b/onionshare_gui/widgets.py @@ -79,3 +79,14 @@ class AddFileDialog(QtWidgets.QFileDialog): def accept(self): self.common.log("AddFileDialog", "accept") QtWidgets.QDialog.accept(self) + + +class MinimumWidthWidget(QtWidgets.QWidget): + """ + An empty widget with a minimum width, just to force layouts to behave + """ + + def __init__(self, width): + super(MinimumWidthWidget, self).__init__() + self.setMinimumWidth(width) + From 01435d1eda8328018b4b373fbc5777850bad88fd Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 19:03:37 -0700 Subject: [PATCH 043/135] Move autostart and autostop timer widgets into the mode settings widget --- onionshare_gui/tab/mode/__init__.py | 18 +- .../tab/mode/mode_settings_widget.py | 79 +++++++- onionshare_gui/tab/server_status.py | 183 ++++-------------- 3 files changed, 116 insertions(+), 164 deletions(-) diff --git a/onionshare_gui/tab/mode/__init__.py b/onionshare_gui/tab/mode/__init__.py index 2c95d445..e6b07da3 100644 --- a/onionshare_gui/tab/mode/__init__.py +++ b/onionshare_gui/tab/mode/__init__.py @@ -67,12 +67,19 @@ class Mode(QtWidgets.QWidget): self.web_thread = None self.startup_thread = None + # Mode settings widget + self.mode_settings_widget = ModeSettingsWidget( + self.common, self.tab.tab_id, self.settings + ) + self.mode_settings_widget.change_persistent.connect(self.change_persistent) + # Server status self.server_status = ServerStatus( self.common, self.qtapp, self.app, self.settings, + self.mode_settings_widget, None, self.common.gui.local_only, ) @@ -86,15 +93,6 @@ class Mode(QtWidgets.QWidget): self.starting_server_early.connect(self.start_server_early) self.starting_server_error.connect(self.start_server_error) - # Mode settings widget - self.mode_settings_widget = ModeSettingsWidget( - self.common, self.tab.tab_id, self.tab.mode_settings - ) - self.mode_settings_widget.change_persistent.connect(self.change_persistent) - self.mode_settings_widget.update_server_status.connect( - self.server_status.update - ) - # Header # Note: It's up to the downstream Mode to add this to its layout self.header_label = QtWidgets.QLabel() @@ -144,7 +142,7 @@ class Mode(QtWidgets.QWidget): now = QtCore.QDateTime.currentDateTime() if self.server_status.local_only: seconds_remaining = now.secsTo( - self.server_status.autostart_timer_widget.dateTime() + self.mode_settings_widget.autostart_timer_widget.dateTime() ) else: seconds_remaining = now.secsTo( diff --git a/onionshare_gui/tab/mode/mode_settings_widget.py b/onionshare_gui/tab/mode/mode_settings_widget.py index 8b6136cd..867c27cf 100644 --- a/onionshare_gui/tab/mode/mode_settings_widget.py +++ b/onionshare_gui/tab/mode/mode_settings_widget.py @@ -28,7 +28,6 @@ class ModeSettingsWidget(QtWidgets.QWidget): """ change_persistent = QtCore.pyqtSignal(int, bool) - update_server_status = QtCore.pyqtSignal() def __init__(self, common, tab_id, mode_settings): super(ModeSettingsWidget, self).__init__() @@ -61,6 +60,26 @@ class ModeSettingsWidget(QtWidgets.QWidget): strings._("mode_settings_autostart_timer_checkbox") ) + # The autostart timer widget + self.autostart_timer_widget = QtWidgets.QDateTimeEdit() + self.autostart_timer_widget.setDisplayFormat("hh:mm A MMM d, yy") + self.autostart_timer_widget.setDateTime( + QtCore.QDateTime.currentDateTime().addSecs(300) # 5 minutes in the future + ) + self.autostart_timer_widget.setMinimumDateTime( + QtCore.QDateTime.currentDateTime().addSecs(60) + ) + self.autostart_timer_widget.setCurrentSection( + QtWidgets.QDateTimeEdit.MinuteSection + ) + self.autostart_timer_widget.hide() + + # Autostart timer layout + autostart_timer_layout = QtWidgets.QHBoxLayout() + autostart_timer_layout.setContentsMargins(0, 0, 0, 0) + autostart_timer_layout.addWidget(self.autostart_timer_checkbox) + autostart_timer_layout.addWidget(self.autostart_timer_widget) + # Whether or not to use an auto-stop timer self.autostop_timer_checkbox = QtWidgets.QCheckBox() self.autostop_timer_checkbox.clicked.connect( @@ -71,6 +90,26 @@ class ModeSettingsWidget(QtWidgets.QWidget): strings._("mode_settings_autostop_timer_checkbox") ) + # The autostop timer widget + self.autostop_timer_widget = QtWidgets.QDateTimeEdit() + self.autostop_timer_widget.setDisplayFormat("hh:mm A MMM d, yy") + self.autostop_timer_widget.setDateTime( + QtCore.QDateTime.currentDateTime().addSecs(300) + ) + self.autostop_timer_widget.setMinimumDateTime( + QtCore.QDateTime.currentDateTime().addSecs(60) + ) + self.autostop_timer_widget.setCurrentSection( + QtWidgets.QDateTimeEdit.MinuteSection + ) + self.autostop_timer_widget.hide() + + # Autostop timer layout + autostop_timer_layout = QtWidgets.QHBoxLayout() + autostop_timer_layout.setContentsMargins(0, 0, 0, 0) + autostop_timer_layout.addWidget(self.autostop_timer_checkbox) + autostop_timer_layout.addWidget(self.autostop_timer_widget) + # Legacy address self.legacy_checkbox = QtWidgets.QCheckBox() self.legacy_checkbox.clicked.connect(self.legacy_checkbox_clicked) @@ -99,8 +138,8 @@ class ModeSettingsWidget(QtWidgets.QWidget): advanced_layout = QtWidgets.QVBoxLayout() advanced_layout.setContentsMargins(0, 0, 0, 0) advanced_layout.addWidget(self.public_checkbox) - advanced_layout.addWidget(self.autostart_timer_checkbox) - advanced_layout.addWidget(self.autostop_timer_checkbox) + advanced_layout.addLayout(autostart_timer_layout) + advanced_layout.addLayout(autostop_timer_layout) advanced_layout.addWidget(self.legacy_checkbox) advanced_layout.addWidget(self.client_auth_checkbox) self.advanced_widget = QtWidgets.QWidget() @@ -150,13 +189,21 @@ class ModeSettingsWidget(QtWidgets.QWidget): self.settings.set( "general", "autostart_timer", self.autostart_timer_checkbox.isChecked() ) - self.update_server_status.emit() + + if self.autostart_timer_checkbox.isChecked(): + self.autostart_timer_widget.show() + else: + self.autostart_timer_widget.hide() def autostop_timer_checkbox_clicked(self): self.settings.set( "general", "autostop_timer", self.autostop_timer_checkbox.isChecked() ) - self.update_server_status.emit() + + if self.autostop_timer_checkbox.isChecked(): + self.autostop_timer_widget.show() + else: + self.autostop_timer_widget.hide() def legacy_checkbox_clicked(self): self.settings.set("general", "legacy", self.legacy_checkbox.isChecked()) @@ -173,3 +220,25 @@ class ModeSettingsWidget(QtWidgets.QWidget): self.advanced_widget.show() self.update_ui() + + def autostart_timer_reset(self): + """ + Reset the auto-start timer in the UI after stopping a share + """ + self.autostart_timer_widget.setDateTime( + QtCore.QDateTime.currentDateTime().addSecs(300) + ) + self.autostart_timer_widget.setMinimumDateTime( + QtCore.QDateTime.currentDateTime().addSecs(60) + ) + + def autostop_timer_reset(self): + """ + Reset the auto-stop timer in the UI after stopping a share + """ + self.autostop_timer_widget.setDateTime( + QtCore.QDateTime.currentDateTime().addSecs(300) + ) + self.autostop_timer_widget.setMinimumDateTime( + QtCore.QDateTime.currentDateTime().addSecs(60) + ) diff --git a/onionshare_gui/tab/server_status.py b/onionshare_gui/tab/server_status.py index 317e9cc8..2b2c2ec4 100644 --- a/onionshare_gui/tab/server_status.py +++ b/onionshare_gui/tab/server_status.py @@ -44,7 +44,14 @@ class ServerStatus(QtWidgets.QWidget): STATUS_STARTED = 2 def __init__( - self, common, qtapp, app, mode_settings, file_selection=None, local_only=False + self, + common, + qtapp, + app, + mode_settings, + mode_settings_widget, + file_selection=None, + local_only=False, ): super(ServerStatus, self).__init__() @@ -56,6 +63,7 @@ class ServerStatus(QtWidgets.QWidget): self.qtapp = qtapp self.app = app self.settings = mode_settings + self.mode_settings_widget = mode_settings_widget self.web = None self.autostart_timer_datetime = None @@ -63,80 +71,6 @@ class ServerStatus(QtWidgets.QWidget): self.resizeEvent(None) - # Auto-start timer layout - self.autostart_timer_label = QtWidgets.QLabel( - strings._("gui_settings_autostart_timer") - ) - self.autostart_timer_widget = QtWidgets.QDateTimeEdit() - self.autostart_timer_widget.setDisplayFormat("hh:mm A MMM d, yy") - if self.local_only: - # For testing - self.autostart_timer_widget.setDateTime( - QtCore.QDateTime.currentDateTime().addSecs(15) - ) - self.autostart_timer_widget.setMinimumDateTime( - QtCore.QDateTime.currentDateTime() - ) - else: - # Set proposed timer to be 5 minutes into the future - self.autostart_timer_widget.setDateTime( - QtCore.QDateTime.currentDateTime().addSecs(300) - ) - # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 60s from now - self.autostart_timer_widget.setMinimumDateTime( - QtCore.QDateTime.currentDateTime().addSecs(60) - ) - self.autostart_timer_widget.setCurrentSection( - QtWidgets.QDateTimeEdit.MinuteSection - ) - autostart_timer_layout = QtWidgets.QHBoxLayout() - autostart_timer_layout.addWidget(self.autostart_timer_label) - autostart_timer_layout.addWidget(self.autostart_timer_widget) - - # Auto-start timer container, so it can all be hidden and shown as a group - autostart_timer_container_layout = QtWidgets.QVBoxLayout() - autostart_timer_container_layout.addLayout(autostart_timer_layout) - self.autostart_timer_container = QtWidgets.QWidget() - self.autostart_timer_container.setLayout(autostart_timer_container_layout) - self.autostart_timer_container.hide() - - # Auto-stop timer layout - self.autostop_timer_label = QtWidgets.QLabel( - strings._("gui_settings_autostop_timer") - ) - self.autostop_timer_widget = QtWidgets.QDateTimeEdit() - self.autostop_timer_widget.setDisplayFormat("hh:mm A MMM d, yy") - if self.local_only: - # For testing - self.autostop_timer_widget.setDateTime( - QtCore.QDateTime.currentDateTime().addSecs(15) - ) - self.autostop_timer_widget.setMinimumDateTime( - QtCore.QDateTime.currentDateTime() - ) - else: - # Set proposed timer to be 5 minutes into the future - self.autostop_timer_widget.setDateTime( - QtCore.QDateTime.currentDateTime().addSecs(300) - ) - # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 60s from now - self.autostop_timer_widget.setMinimumDateTime( - QtCore.QDateTime.currentDateTime().addSecs(60) - ) - self.autostop_timer_widget.setCurrentSection( - QtWidgets.QDateTimeEdit.MinuteSection - ) - autostop_timer_layout = QtWidgets.QHBoxLayout() - autostop_timer_layout.addWidget(self.autostop_timer_label) - autostop_timer_layout.addWidget(self.autostop_timer_widget) - - # Auto-stop timer container, so it can all be hidden and shown as a group - autostop_timer_container_layout = QtWidgets.QVBoxLayout() - autostop_timer_container_layout.addLayout(autostop_timer_layout) - self.autostop_timer_container = QtWidgets.QWidget() - self.autostop_timer_container.setLayout(autostop_timer_container_layout) - self.autostop_timer_container.hide() - # Server layout self.server_button = QtWidgets.QPushButton() self.server_button.clicked.connect(self.server_button_clicked) @@ -181,8 +115,6 @@ class ServerStatus(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout() layout.addWidget(self.server_button) layout.addLayout(url_layout) - layout.addWidget(self.autostart_timer_container) - layout.addWidget(self.autostop_timer_container) self.setLayout(layout) def set_mode(self, share_mode, file_selection=None): @@ -215,30 +147,6 @@ class ServerStatus(QtWidgets.QWidget): except: pass - def autostart_timer_reset(self): - """ - Reset the auto-start timer in the UI after stopping a share - """ - self.autostart_timer_widget.setDateTime( - QtCore.QDateTime.currentDateTime().addSecs(300) - ) - if not self.local_only: - self.autostart_timer_widget.setMinimumDateTime( - QtCore.QDateTime.currentDateTime().addSecs(60) - ) - - def autostop_timer_reset(self): - """ - Reset the auto-stop timer in the UI after stopping a share - """ - self.autostop_timer_widget.setDateTime( - QtCore.QDateTime.currentDateTime().addSecs(300) - ) - if not self.local_only: - self.autostop_timer_widget.setMinimumDateTime( - QtCore.QDateTime.currentDateTime().addSecs(60) - ) - def show_url(self): """ Show the URL in the UI. @@ -292,9 +200,6 @@ class ServerStatus(QtWidgets.QWidget): Update the GUI elements based on the current state. """ self.common.log("ServerStatus", "update") - autostart_timer = self.settings.get("general", "autostart_timer") - autostop_timer = self.settings.get("general", "autostop_timer") - # Set the URL fields if self.status == self.STATUS_STARTED: # The backend Onion may have saved new settings, such as the private key. @@ -306,47 +211,21 @@ class ServerStatus(QtWidgets.QWidget): if not self.settings.get("persistent", "password"): self.settings.set("persistent", "password", self.web.password) self.settings.save() + + if self.settings.get("general", "autostop_timer"): + self.server_button.setToolTip( + strings._("gui_stop_server_autostop_timer_tooltip").format( + self.mode_settings_widget.autostop_timer_widget.dateTime().toString( + "h:mm AP, MMMM dd, yyyy" + ) + ) + ) else: self.url_description.hide() self.url.hide() self.copy_url_button.hide() self.copy_hidservauth_button.hide() - # Autostart and autostop timers - if self.status == self.STATUS_STOPPED: - if autostart_timer: - self.autostart_timer_container.show() - else: - self.autostart_timer_container.hide() - if autostop_timer: - self.autostop_timer_container.show() - else: - self.autostop_timer_container.hide() - elif self.status == self.STATUS_STARTED: - self.autostart_timer_container.hide() - self.autostop_timer_container.hide() - - if autostop_timer: - self.server_button.setToolTip( - strings._("gui_stop_server_autostop_timer_tooltip").format( - self.autostop_timer_widget.dateTime().toString( - "h:mm AP, MMMM dd, yyyy" - ) - ) - ) - elif self.status == self.STATUS_WORKING: - self.autostart_timer_container.hide() - self.autostop_timer_container.hide() - - if autostart_timer: - self.server_button.setToolTip( - strings._("gui_start_server_autostart_timer_tooltip").format( - self.autostart_timer_widget.dateTime().toString( - "h:mm AP, MMMM dd, yyyy" - ) - ) - ) - # Button if ( self.mode == self.common.gui.MODE_SHARE @@ -390,18 +269,24 @@ class ServerStatus(QtWidgets.QWidget): ) self.server_button.setEnabled(True) if self.autostart_timer_datetime: - self.autostart_timer_container.hide() self.server_button.setToolTip( strings._("gui_start_server_autostart_timer_tooltip").format( - self.autostart_timer_widget.dateTime().toString( + self.mode_settings_widget.autostart_timer_widget.dateTime().toString( "h:mm AP, MMMM dd, yyyy" ) ) ) else: self.server_button.setText(strings._("gui_please_wait")) - if self.settings.get("general", "autostop_timer"): - self.autostop_timer_container.hide() + + if self.settings.get("general", "autostart_timer"): + self.server_button.setToolTip( + strings._("gui_start_server_autostart_timer_tooltip").format( + self.mode_settings_widget.autostart_timer_widget.dateTime().toString( + "h:mm AP, MMMM dd, yyyy" + ) + ) + ) else: self.server_button.setStyleSheet( self.common.gui.css["server_status_button_working"] @@ -418,11 +303,11 @@ class ServerStatus(QtWidgets.QWidget): if self.settings.get("general", "autostart_timer"): if self.local_only: self.autostart_timer_datetime = ( - self.autostart_timer_widget.dateTime().toPyDateTime() + self.mode_settings_widget.autostart_timer_widget.dateTime().toPyDateTime() ) else: self.autostart_timer_datetime = ( - self.autostart_timer_widget.dateTime() + self.mode_settings_widget.autostart_timer_widget.dateTime() .toPyDateTime() .replace(second=0, microsecond=0) ) @@ -440,12 +325,12 @@ class ServerStatus(QtWidgets.QWidget): if self.settings.get("general", "autostop_timer"): if self.local_only: self.autostop_timer_datetime = ( - self.autostop_timer_widget.dateTime().toPyDateTime() + self.mode_settings_widget.autostop_timer_widget.dateTime().toPyDateTime() ) else: # Get the timer chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen self.autostop_timer_datetime = ( - self.autostop_timer_widget.dateTime() + self.mode_settings_widget.autostop_timer_widget.dateTime() .toPyDateTime() .replace(second=0, microsecond=0) ) @@ -500,8 +385,8 @@ class ServerStatus(QtWidgets.QWidget): Stop the server. """ self.status = self.STATUS_WORKING - self.autostart_timer_reset() - self.autostop_timer_reset() + self.mode_settings_widget.autostart_timer_reset() + self.mode_settings_widget.autostop_timer_reset() self.update() self.server_stopped.emit() From 1286dfa2f981a18f5476650a4a4161741edaa3a5 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 19:10:41 -0700 Subject: [PATCH 044/135] Change size of persistent image to 20x20 --- onionshare_gui/tab/tab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index e1913071..2e9f0df2 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -128,7 +128,7 @@ class Tab(QtWidgets.QWidget): ) ) ) - self.persistent_image_label.setFixedSize(30, 30) + self.persistent_image_label.setFixedSize(20, 20) # Settings for this tab self.mode_settings = ModeSettings(self.common) From 69bd16527890c06b573b9f6b939bf72895daae95 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 19:12:06 -0700 Subject: [PATCH 045/135] Add stub save function in ModeSettings --- onionshare/mode_settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/onionshare/mode_settings.py b/onionshare/mode_settings.py index 9caf6345..2eab8f6e 100644 --- a/onionshare/mode_settings.py +++ b/onionshare/mode_settings.py @@ -72,3 +72,7 @@ class ModeSettings: else: # All other OSes return os.path.expanduser("~/OnionShare") + + def save(self): + # TODO: save settings, if persistent + pass From 1b36fe4036e5963ef702273b8d2218f5c043c4ed Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 21:00:23 -0700 Subject: [PATCH 046/135] Make ModeSettings be able to save and load --- onionshare/__init__.py | 53 +++++++++++++------------- onionshare/common.py | 15 ++++++-- onionshare/mode_settings.py | 75 +++++++++++++++++++++++++++++++++---- 3 files changed, 107 insertions(+), 36 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 7bc18bff..f532ec0e 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -73,21 +73,21 @@ def main(cwd=None): ) parser.add_argument( "--connect-timeout", - metavar="", + metavar="SECONDS", dest="connect_timeout", default=120, help="Give up connecting to Tor after a given amount of seconds (default: 120)", ) parser.add_argument( "--config", - metavar="config", + metavar="FILENAME", default=None, help="Filename of custom global settings", ) # Persistent file parser.add_argument( "--persistent", - metavar="persistent", + metavar="FILENAME", default=None, help="Filename of persistent session", ) @@ -101,14 +101,14 @@ def main(cwd=None): ) parser.add_argument( "--auto-start-timer", - metavar="", + metavar="SECONDS", dest="autostart_timer", default=0, help="Start onion service at scheduled time (N seconds from now)", ) parser.add_argument( "--auto-stop-timer", - metavar="", + metavar="SECONDS", dest="autostop_timer", default=0, help="Stop onion service at schedule time (N seconds from now)", @@ -174,8 +174,8 @@ def main(cwd=None): website = bool(args.website) local_only = bool(args.local_only) connect_timeout = int(args.connect_timeout) - config = args.config - persistent = args.persistent + config_filename = args.config + persistent_filename = args.persistent public = bool(args.public) autostart_timer = int(args.autostart_timer) autostop_timer = int(args.autostop_timer) @@ -220,8 +220,8 @@ def main(cwd=None): sys.exit() # Re-load settings, if a custom config was passed in - if config: - common.load_settings(config) + if config_filename: + common.load_settings(config_filename) else: common.load_settings() @@ -229,21 +229,24 @@ def main(cwd=None): common.verbose = verbose # Mode settings - mode_settings = ModeSettings(common) - mode_settings.set("general", "public", public) - mode_settings.set("general", "autostart_timer", autostart_timer) - mode_settings.set("general", "autostop_timer", autostop_timer) - mode_settings.set("general", "legacy", legacy) - mode_settings.set("general", "client_auth", client_auth) - if mode == "share": - mode_settings.set("share", "autostop_sharing", autostop_sharing) - if mode == "receive": - if data_dir: - mode_settings.set("receive", "data_dir", data_dir) - if mode == "website": - mode_settings.set("website", "disable_csp", disable_csp) - - # TODO: handle persistent + if persistent_filename: + mode_settings = ModeSettings(common, persistent_filename) + mode_settings.set("persistent", "enabled", True) + else: + mode_settings = ModeSettings(common) + if mode_settings.just_created: + mode_settings.set("general", "public", public) + mode_settings.set("general", "autostart_timer", autostart_timer) + mode_settings.set("general", "autostop_timer", autostop_timer) + mode_settings.set("general", "legacy", legacy) + mode_settings.set("general", "client_auth", client_auth) + if mode == "share": + mode_settings.set("share", "autostop_sharing", autostop_sharing) + if mode == "receive": + if data_dir: + mode_settings.set("receive", "data_dir", data_dir) + if mode == "website": + mode_settings.set("website", "disable_csp", disable_csp) # Create the Web object web = Web(common, False, mode_settings, mode) @@ -253,7 +256,7 @@ def main(cwd=None): try: onion.connect( custom_settings=False, - config=config, + config=config_filename, connect_timeout=connect_timeout, local_only=local_only, ) diff --git a/onionshare/common.py b/onionshare/common.py index ac79f43b..5245ddf9 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -177,15 +177,24 @@ class Common: os.makedirs(onionshare_data_dir, 0o700, True) return onionshare_data_dir - def build_password(self): + def build_persistent_dir(self): """ - Returns a random string made from two words from the wordlist, such as "deter-trig". + Returns the path to the folder that holds persistent files + """ + onionshare_data_dir = self.build_data_dir() + persistent_dir = os.path.join(onionshare_data_dir, "persistent") + os.makedirs(persistent_dir, 0o700, True) + return persistent_dir + + def build_password(self, word_count=2): + """ + Returns a random string made of words from the wordlist, such as "deter-trig". """ with open(self.get_resource_path("wordlist.txt")) as f: wordlist = f.read().split() r = random.SystemRandom() - return "-".join(r.choice(wordlist) for _ in range(2)) + return "-".join(r.choice(wordlist) for _ in range(word_count)) @staticmethod def random_string(num_bytes, output_len=None): diff --git a/onionshare/mode_settings.py b/onionshare/mode_settings.py index 2eab8f6e..ffc32d06 100644 --- a/onionshare/mode_settings.py +++ b/onionshare/mode_settings.py @@ -19,6 +19,7 @@ along with this program. If not, see . """ import os import pwd +import json class ModeSettings: @@ -27,10 +28,10 @@ class ModeSettings: is only one TabSettings, and in the GUI there is a separate TabSettings for each tab """ - def __init__(self, common): + def __init__(self, common, filename=None): self.common = common - self.settings = { + self.default_settings = { "persistent": { "enabled": False, "private_key": None, @@ -44,16 +45,41 @@ class ModeSettings: "legacy": False, "client_auth": False, }, - "share": {"autostop_sharing": True}, + "share": {"autostop_sharing": True, "filenames": []}, "receive": {"data_dir": self.build_default_receive_data_dir()}, - "website": {"disable_csp": False}, + "website": {"disable_csp": False, "filenames": []}, } + self._settings = {} + + self.just_created = False + self.id = self.common.build_password(3) + + self.load(filename) + + def fill_in_defaults(self): + """ + If there are any missing settings from self._settings, replace them with + their default values. + """ + for key in self.default_settings: + if key in self._settings: + for inner_key in self.default_settings[key]: + if inner_key not in self._settings[key]: + self._settings[key][inner_key] = self.default_settings[key][ + inner_key + ] + else: + self._settings[key] = self.default_settings[key] def get(self, group, key): - return self.settings[group][key] + return self._settings[group][key] def set(self, group, key, val): - self.settings[group][key] = val + self._settings[group][key] = val + self.common.log( + "ModeSettings", "set", f"updating {self.id}: {group}.{key} = {val}" + ) + self.save() def build_default_receive_data_dir(self): """ @@ -73,6 +99,39 @@ class ModeSettings: # All other OSes return os.path.expanduser("~/OnionShare") + def load(self, filename=None): + # Load persistent settings from disk. If the file doesn't exist, create it + if filename: + self.filename = filename + else: + # Give it a persistent filename + self.filename = os.path.join( + self.common.build_persistent_dir(), f"{self.id}.json" + ) + + if os.path.exists(self.filename): + try: + with open(self.filename, "r") as f: + self._settings = json.load(f) + self.fill_in_defaults() + self.common.log("ModeSettings", "load", f"loaded {self.filename}") + return + except: + pass + + # If loading settings didn't work, create the settings file + self.common.log("ModeSettings", "load", f"creating {self.filename}") + self.fill_in_defaults() + self.just_created = True + def save(self): - # TODO: save settings, if persistent - pass + # Save persistent setting to disk + if not self.get("persistent", "enabled"): + self.common.log( + "ModeSettings", "save", f"{self.id}: not persistent, so not saving" + ) + return + + if self.filename: + with open(self.filename, "w") as file: + file.write(json.dumps(self._settings, indent=2)) From a5f8fee065af1758f7247be1ecc32d7d34acdfb0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 21:45:55 -0700 Subject: [PATCH 047/135] Starting to make persistent tabs persistent --- onionshare/mode_settings.py | 9 +++-- onionshare/settings.py | 1 + onionshare_gui/main_window.py | 9 ++++- onionshare_gui/tab/mode/__init__.py | 4 +- .../tab/mode/mode_settings_widget.py | 40 ++++++++++++++----- .../tab/mode/share_mode/__init__.py | 6 ++- .../tab/mode/website_mode/__init__.py | 6 ++- onionshare_gui/tab/tab.py | 27 +++++++++++-- onionshare_gui/tab_widget.py | 38 +++++++++++++++++- 9 files changed, 115 insertions(+), 25 deletions(-) diff --git a/onionshare/mode_settings.py b/onionshare/mode_settings.py index ffc32d06..3b14dbe8 100644 --- a/onionshare/mode_settings.py +++ b/onionshare/mode_settings.py @@ -28,12 +28,13 @@ class ModeSettings: is only one TabSettings, and in the GUI there is a separate TabSettings for each tab """ - def __init__(self, common, filename=None): + def __init__(self, common, filename=None, id=None): self.common = common self.default_settings = { "persistent": { "enabled": False, + "mode": None, "private_key": None, "hidservauth": None, "password": None, @@ -52,7 +53,10 @@ class ModeSettings: self._settings = {} self.just_created = False - self.id = self.common.build_password(3) + if id: + self.id = id + else: + self.id = self.common.build_password(3) self.load(filename) @@ -104,7 +108,6 @@ class ModeSettings: if filename: self.filename = filename else: - # Give it a persistent filename self.filename = os.path.join( self.common.build_persistent_dir(), f"{self.id}.json" ) diff --git a/onionshare/settings.py b/onionshare/settings.py index 11227817..f9348a8e 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -112,6 +112,7 @@ class Settings(object): "tor_bridges_use_obfs4": False, "tor_bridges_use_meek_lite_azure": False, "tor_bridges_use_custom_bridges": "", + "persistent_tabs": [], "locale": None, # this gets defined in fill_in_defaults() } self._settings = {} diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index e6f5606e..22035b69 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -120,8 +120,13 @@ class MainWindow(QtWidgets.QMainWindow): # Tabs self.tabs = TabWidget(self.common, self.system_tray, self.status_bar) - # Start with opening the first tab - self.tabs.new_tab_clicked() + # If we have saved persistent tabs, try opening those + if len(self.common.settings.get("persistent_tabs")) > 0: + for mode_settings_id in self.common.settings.get("persistent_tabs"): + self.tabs.load_tab(mode_settings_id) + else: + # Start with opening the first tab + self.tabs.new_tab_clicked() # Layout layout = QtWidgets.QVBoxLayout() diff --git a/onionshare_gui/tab/mode/__init__.py b/onionshare_gui/tab/mode/__init__.py index e6b07da3..7696f79e 100644 --- a/onionshare_gui/tab/mode/__init__.py +++ b/onionshare_gui/tab/mode/__init__.py @@ -47,7 +47,7 @@ class Mode(QtWidgets.QWidget): def __init__(self, tab): super(Mode, self).__init__() self.tab = tab - self.settings = tab.mode_settings + self.settings = tab.settings self.common = tab.common self.qtapp = self.common.gui.qtapp @@ -69,7 +69,7 @@ class Mode(QtWidgets.QWidget): # Mode settings widget self.mode_settings_widget = ModeSettingsWidget( - self.common, self.tab.tab_id, self.settings + self.common, self.tab, self.settings ) self.mode_settings_widget.change_persistent.connect(self.change_persistent) diff --git a/onionshare_gui/tab/mode/mode_settings_widget.py b/onionshare_gui/tab/mode/mode_settings_widget.py index 867c27cf..462fe728 100644 --- a/onionshare_gui/tab/mode/mode_settings_widget.py +++ b/onionshare_gui/tab/mode/mode_settings_widget.py @@ -29,10 +29,10 @@ class ModeSettingsWidget(QtWidgets.QWidget): change_persistent = QtCore.pyqtSignal(int, bool) - def __init__(self, common, tab_id, mode_settings): + def __init__(self, common, tab, mode_settings): super(ModeSettingsWidget, self).__init__() self.common = common - self.tab_id = tab_id + self.tab = tab self.settings = mode_settings # Downstream Mode need to fill in this layout with its settings @@ -41,24 +41,33 @@ class ModeSettingsWidget(QtWidgets.QWidget): # Persistent self.persistent_checkbox = QtWidgets.QCheckBox() self.persistent_checkbox.clicked.connect(self.persistent_checkbox_clicked) - self.persistent_checkbox.setCheckState(QtCore.Qt.Unchecked) self.persistent_checkbox.setText(strings._("mode_settings_persistent_checkbox")) + if self.settings.get("persistent", "enabled"): + self.persistent_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.persistent_checkbox.setCheckState(QtCore.Qt.Unchecked) # Public self.public_checkbox = QtWidgets.QCheckBox() self.public_checkbox.clicked.connect(self.public_checkbox_clicked) - self.public_checkbox.setCheckState(QtCore.Qt.Unchecked) self.public_checkbox.setText(strings._("mode_settings_public_checkbox")) + if self.settings.get("general", "public"): + self.public_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.public_checkbox.setCheckState(QtCore.Qt.Unchecked) # Whether or not to use an auto-start timer self.autostart_timer_checkbox = QtWidgets.QCheckBox() self.autostart_timer_checkbox.clicked.connect( self.autostart_timer_checkbox_clicked ) - self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) self.autostart_timer_checkbox.setText( strings._("mode_settings_autostart_timer_checkbox") ) + if self.settings.get("general", "autostart_timer"): + self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) # The autostart timer widget self.autostart_timer_widget = QtWidgets.QDateTimeEdit() @@ -85,10 +94,13 @@ class ModeSettingsWidget(QtWidgets.QWidget): self.autostop_timer_checkbox.clicked.connect( self.autostop_timer_checkbox_clicked ) - self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) self.autostop_timer_checkbox.setText( strings._("mode_settings_autostop_timer_checkbox") ) + if self.settings.get("general", "autostop_timer"): + self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) # The autostop timer widget self.autostop_timer_widget = QtWidgets.QDateTimeEdit() @@ -114,17 +126,23 @@ class ModeSettingsWidget(QtWidgets.QWidget): self.legacy_checkbox = QtWidgets.QCheckBox() self.legacy_checkbox.clicked.connect(self.legacy_checkbox_clicked) self.legacy_checkbox.clicked.connect(self.update_ui) - self.legacy_checkbox.setCheckState(QtCore.Qt.Unchecked) self.legacy_checkbox.setText(strings._("mode_settings_legacy_checkbox")) + if self.settings.get("general", "legacy"): + self.legacy_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.legacy_checkbox.setCheckState(QtCore.Qt.Unchecked) # Client auth self.client_auth_checkbox = QtWidgets.QCheckBox() self.client_auth_checkbox.clicked.connect(self.client_auth_checkbox_clicked) self.client_auth_checkbox.clicked.connect(self.update_ui) - self.client_auth_checkbox.setCheckState(QtCore.Qt.Unchecked) self.client_auth_checkbox.setText( strings._("mode_settings_client_auth_checkbox") ) + if self.settings.get("general", "client_auth"): + self.client_auth_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.client_auth_checkbox.setCheckState(QtCore.Qt.Unchecked) # Toggle advanced settings self.toggle_advanced_button = QtWidgets.QPushButton() @@ -179,8 +197,10 @@ class ModeSettingsWidget(QtWidgets.QWidget): def persistent_checkbox_clicked(self): self.settings.set("persistent", "enabled", self.persistent_checkbox.isChecked()) - - self.change_persistent.emit(self.tab_id, self.persistent_checkbox.isChecked()) + self.settings.set("persistent", "mode", self.tab.mode) + self.change_persistent.emit( + self.tab.tab_id, self.persistent_checkbox.isChecked() + ) def public_checkbox_clicked(self): self.settings.set("general", "public", self.public_checkbox.isChecked()) diff --git a/onionshare_gui/tab/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py index 7d195e42..8d9b323a 100644 --- a/onionshare_gui/tab/mode/share_mode/__init__.py +++ b/onionshare_gui/tab/mode/share_mode/__init__.py @@ -55,10 +55,14 @@ class ShareMode(Mode): self.autostop_sharing_checkbox.clicked.connect( self.autostop_sharing_checkbox_clicked ) - self.autostop_sharing_checkbox.setCheckState(QtCore.Qt.Checked) self.autostop_sharing_checkbox.setText( strings._("mode_settings_share_autostop_sharing_checkbox") ) + if self.settings.get("share", "autostop_sharing"): + self.autostop_sharing_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.autostop_sharing_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.mode_settings_widget.mode_specific_layout.addWidget( self.autostop_sharing_checkbox ) diff --git a/onionshare_gui/tab/mode/website_mode/__init__.py b/onionshare_gui/tab/mode/website_mode/__init__.py index 4a099488..db8dbf09 100644 --- a/onionshare_gui/tab/mode/website_mode/__init__.py +++ b/onionshare_gui/tab/mode/website_mode/__init__.py @@ -55,10 +55,14 @@ class WebsiteMode(Mode): # Settings self.disable_csp_checkbox = QtWidgets.QCheckBox() self.disable_csp_checkbox.clicked.connect(self.disable_csp_checkbox_clicked) - self.disable_csp_checkbox.setCheckState(QtCore.Qt.Unchecked) self.disable_csp_checkbox.setText( strings._("mode_settings_website_disable_csp_checkbox") ) + if self.settings.get("website", "disable_csp"): + self.disable_csp_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.disable_csp_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.mode_settings_widget.mode_specific_layout.addWidget( self.disable_csp_checkbox ) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 2e9f0df2..04e4bf9d 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -43,7 +43,15 @@ class Tab(QtWidgets.QWidget): change_icon = QtCore.pyqtSignal(int, str) change_persistent = QtCore.pyqtSignal(int, bool) - def __init__(self, common, tab_id, system_tray, status_bar, filenames=None): + def __init__( + self, + common, + tab_id, + system_tray, + status_bar, + mode_settings=None, + filenames=None, + ): super(Tab, self).__init__() self.common = common self.common.log("Tab", "__init__") @@ -130,8 +138,19 @@ class Tab(QtWidgets.QWidget): ) self.persistent_image_label.setFixedSize(20, 20) - # Settings for this tab - self.mode_settings = ModeSettings(self.common) + if mode_settings: + # Load this tab + self.settings = mode_settings + mode = self.settings.get("persistent", "mode") + if mode == "share": + self.share_mode_clicked() + elif mode == "receive": + self.receive_mode_clicked() + elif mode == "website": + self.website_mode_clicked() + else: + # This is a new tab + self.settings = ModeSettings(self.common) def share_mode_clicked(self): self.common.log("Tab", "share_mode_clicked") @@ -496,7 +515,7 @@ class Tab(QtWidgets.QWidget): if self.mode is None: return True - if self.mode_settings.get("persistent", "enabled"): + if self.settings.get("persistent", "enabled"): dialog_text = strings._("gui_close_tab_warning_persistent_description") else: server_status = self.get_mode().server_status diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index 29dbda15..2d53ea32 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -20,6 +20,7 @@ along with this program. If not, see . from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings +from onionshare.mode_settings import ModeSettings from .tab import Tab @@ -86,15 +87,29 @@ class TabWidget(QtWidgets.QTabWidget): self.new_tab_button.raise_() def new_tab_clicked(self): - # Create the tab + # Create a new tab tab = Tab(self.common, self.tab_id, self.system_tray, self.status_bar) + self.add_tab(tab) + + def load_tab(self, mode_settings_id): + # Load the tab's mode settings + mode_settings = ModeSettings(self.common, id=mode_settings_id) + tab = Tab( + self.common, + self.tab_id, + self.system_tray, + self.status_bar, + mode_settings=mode_settings, + ) + self.add_tab(tab) + + def add_tab(self, tab): tab.change_title.connect(self.change_title) tab.change_icon.connect(self.change_icon) tab.change_persistent.connect(self.change_persistent) self.tabs[self.tab_id] = tab self.tab_id += 1 - # Add it index = self.addTab(tab, "New Tab") self.setCurrentIndex(index) @@ -121,6 +136,18 @@ class TabWidget(QtWidgets.QTabWidget): index, QtWidgets.QTabBar.LeftSide, invisible_widget ) + self.save_persistent_tabs() + + 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) + self.common.settings.set("persistent_tabs", persistent_tabs) + self.common.settings.save() + def close_tab(self, index): self.common.log("TabWidget", "close_tab", f"{index}") tab = self.widget(index) @@ -143,6 +170,13 @@ class TabWidget(QtWidgets.QTabWidget): return True return False + def changeEvent(self, event): + # TODO: later when I have internet, figure out the right event for re-ordering tabs + + # If tabs get move + super(TabWidget, self).changeEvent(event) + self.save_persistent_tabs() + def resizeEvent(self, event): # Make sure to move new tab button on each resize super(TabWidget, self).resizeEvent(event) From 7819a894be8fa17c4ed7626d3451f3cfd385643a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 22:55:22 -0700 Subject: [PATCH 048/135] Make it so when you open the GUI, all of the persistent tabs automatically open as well --- onionshare_gui/tab/mode/file_selection.py | 22 +++++++++++++++ .../tab/mode/share_mode/__init__.py | 4 +-- onionshare_gui/tab/tab.py | 3 ++ onionshare_gui/tab_widget.py | 28 +++++++++---------- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/onionshare_gui/tab/mode/file_selection.py b/onionshare_gui/tab/mode/file_selection.py index 67a23fc5..baa460f3 100644 --- a/onionshare_gui/tab/mode/file_selection.py +++ b/onionshare_gui/tab/mode/file_selection.py @@ -383,6 +383,9 @@ class FileSelection(QtWidgets.QVBoxLayout): # Update the file list self.file_list.update() + # Save the latest file list to mode settings + self.save_filenames() + def add(self): """ Add button clicked. @@ -452,6 +455,25 @@ class FileSelection(QtWidgets.QVBoxLayout): """ return len(range(self.file_list.count())) + def get_filenames(self): + """ + Return the list of file and folder names + """ + filenames = [] + for index in range(self.file_list.count()): + filenames.append(self.file_list.item(index).filename) + return filenames + + def save_filenames(self): + """ + Save the filenames to mode settings + """ + filenames = self.get_filenames() + if self.parent.tab.mode == self.common.gui.MODE_SHARE: + self.parent.settings.set("share", "filenames", filenames) + elif self.parent.tab.mode == self.common.gui.MODE_WEBSITE: + self.parent.settings.set("website", "filenames", filenames) + def setFocus(self): """ Set the Qt app focus on the file selection box. diff --git a/onionshare_gui/tab/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py index 8d9b323a..9d22e401 100644 --- a/onionshare_gui/tab/mode/share_mode/__init__.py +++ b/onionshare_gui/tab/mode/share_mode/__init__.py @@ -204,9 +204,7 @@ class ShareMode(Mode): """ # Add progress bar to the status bar, indicating the compressing of files. self._zip_progress_bar = ZipProgressBar(self.common, 0) - self.filenames = [] - for index in range(self.file_selection.file_list.count()): - self.filenames.append(self.file_selection.file_list.item(index).filename) + self.filenames = self.file_selection.get_filenames() self._zip_progress_bar.total_files_size = ShareMode._compute_total_size( self.filenames diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 04e4bf9d..cf648d8c 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -138,15 +138,18 @@ class Tab(QtWidgets.QWidget): ) self.persistent_image_label.setFixedSize(20, 20) + def init(self, mode_settings=None): if mode_settings: # Load this tab self.settings = mode_settings mode = self.settings.get("persistent", "mode") if mode == "share": + self.filenames = self.settings.get("share", "filenames") self.share_mode_clicked() elif mode == "receive": self.receive_mode_clicked() elif mode == "website": + self.filenames = self.settings.get("website", "filenames") self.website_mode_clicked() else: # This is a new tab diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index 2d53ea32..70ebfb66 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -40,7 +40,7 @@ class TabWidget(QtWidgets.QTabWidget): # Keep track of tabs in a dictionary self.tabs = {} - self.tab_id = 0 # each tab has a unique id + self.current_tab_id = 0 # Each tab has a unique id # Define the new tab button self.new_tab_button = QtWidgets.QPushButton("+", parent=self) @@ -88,31 +88,29 @@ class TabWidget(QtWidgets.QTabWidget): def new_tab_clicked(self): # Create a new tab - tab = Tab(self.common, self.tab_id, self.system_tray, self.status_bar) - self.add_tab(tab) + self.add_tab() def load_tab(self, mode_settings_id): # Load the tab's mode settings mode_settings = ModeSettings(self.common, id=mode_settings_id) - tab = Tab( - self.common, - self.tab_id, - self.system_tray, - self.status_bar, - mode_settings=mode_settings, - ) - self.add_tab(tab) + self.add_tab(mode_settings) - def add_tab(self, tab): + def add_tab(self, mode_settings=None): + tab = Tab(self.common, self.current_tab_id, self.system_tray, self.status_bar) tab.change_title.connect(self.change_title) tab.change_icon.connect(self.change_icon) tab.change_persistent.connect(self.change_persistent) - self.tabs[self.tab_id] = tab - self.tab_id += 1 - index = self.addTab(tab, "New Tab") + self.tabs[self.current_tab_id] = tab + self.current_tab_id += 1 + + index = self.addTab(tab, strings._("gui_new_tab")) self.setCurrentIndex(index) + tab.init(mode_settings) + # If it's persistent, set the persistent image in the tab + self.change_persistent(tab.tab_id, tab.settings.get("persistent", "enabled")) + def change_title(self, tab_id, title): index = self.indexOf(self.tabs[tab_id]) self.setTabText(index, title) From 1bca467ce36654dd5abebfc53f4902c06391d756 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 23:06:26 -0700 Subject: [PATCH 049/135] When disabling persistence, or closing a persistent tab, delete the mode settings file for that tab --- onionshare/mode_settings.py | 8 +++++--- onionshare_gui/tab/mode/mode_settings_widget.py | 4 ++++ onionshare_gui/tab_widget.py | 7 +++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/onionshare/mode_settings.py b/onionshare/mode_settings.py index 3b14dbe8..5728bbc1 100644 --- a/onionshare/mode_settings.py +++ b/onionshare/mode_settings.py @@ -130,11 +130,13 @@ class ModeSettings: def save(self): # Save persistent setting to disk if not self.get("persistent", "enabled"): - self.common.log( - "ModeSettings", "save", f"{self.id}: not persistent, so not saving" - ) return if self.filename: with open(self.filename, "w") as file: file.write(json.dumps(self._settings, indent=2)) + + def delete(self): + # Delete the file from disk + if os.path.exists(self.filename): + os.remove(self.filename) diff --git a/onionshare_gui/tab/mode/mode_settings_widget.py b/onionshare_gui/tab/mode/mode_settings_widget.py index 462fe728..a6a43df6 100644 --- a/onionshare_gui/tab/mode/mode_settings_widget.py +++ b/onionshare_gui/tab/mode/mode_settings_widget.py @@ -202,6 +202,10 @@ class ModeSettingsWidget(QtWidgets.QWidget): self.tab.tab_id, self.persistent_checkbox.isChecked() ) + # If disabling persistence, delete the file from disk + if not self.persistent_checkbox.isChecked(): + self.settings.delete() + def public_checkbox_clicked(self): self.settings.set("general", "public", self.public_checkbox.isChecked()) diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index 70ebfb66..09407600 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -150,6 +150,11 @@ class TabWidget(QtWidgets.QTabWidget): 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() + + # Remove the tab self.removeTab(index) del self.tabs[tab.tab_id] @@ -157,6 +162,8 @@ class TabWidget(QtWidgets.QTabWidget): if self.count() == 0: self.new_tab_clicked() + self.save_persistent_tabs() + def are_tabs_active(self): """ See if there are active servers in any open tabs From d61fc45862acbccd07c4f0e972f5c2ab18697f0a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 23:32:44 -0700 Subject: [PATCH 050/135] Make it so passing in --persistent [filename] in the CLI, with no other args, will load that persistent mode settings file and start the server, without needing to do other validations like passing in a list of filenames --- onionshare/__init__.py | 56 ++++++++++++++++++++++++--------------- onionshare_gui/threads.py | 7 ++--- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index f532ec0e..5b95e3a1 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -193,24 +193,8 @@ def main(cwd=None): else: mode = "share" - # In share an website mode, you must supply a list of filenames - if mode == "share" or mode == "website": - # Make sure filenames given if not using receiver mode - if len(filenames) == 0: - parser.print_help() - sys.exit() - - # Validate filenames - valid = True - for filename in filenames: - if not os.path.isfile(filename) and not os.path.isdir(filename): - print(f"{filename} is not a valid file.") - valid = False - if not os.access(filename, os.R_OK): - print(f"{filename} is not a readable file.") - valid = False - if not valid: - sys.exit() + # Verbose mode? + common.verbose = verbose # client_auth can only be set if legacy is also set if client_auth and not legacy: @@ -225,16 +209,15 @@ def main(cwd=None): else: common.load_settings() - # Verbose mode? - common.verbose = verbose - # Mode settings if persistent_filename: mode_settings = ModeSettings(common, persistent_filename) mode_settings.set("persistent", "enabled", True) else: mode_settings = ModeSettings(common) + if mode_settings.just_created: + # This means the mode settings were just created, not loaded from disk mode_settings.set("general", "public", public) mode_settings.set("general", "autostart_timer", autostart_timer) mode_settings.set("general", "autostop_timer", autostop_timer) @@ -247,6 +230,37 @@ def main(cwd=None): mode_settings.set("receive", "data_dir", data_dir) if mode == "website": mode_settings.set("website", "disable_csp", disable_csp) + else: + # See what the persistent mode was + mode = mode_settings.get("persistent", "mode") + + # In share an website mode, you must supply a list of filenames + if mode == "share" or mode == "website": + # Unless you passed in a persistent filename, in which case get the filenames from + # the mode settings + if persistent_filename and not mode_settings.just_created: + filenames = mode_settings.get(mode, "filenames") + + else: + # Make sure filenames given if not using receiver mode + if len(filenames) == 0: + if persistent_filename: + mode_settings.delete() + + parser.print_help() + sys.exit() + + # Validate filenames + valid = True + for filename in filenames: + if not os.path.isfile(filename) and not os.path.isdir(filename): + print(f"{filename} is not a valid file.") + valid = False + if not os.access(filename, os.R_OK): + print(f"{filename} is not a readable file.") + valid = False + if not valid: + sys.exit() # Create the Web object web = Web(common, False, mode_settings, mode) diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 90a0fbb9..fa0f6c7b 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -47,15 +47,12 @@ class OnionThread(QtCore.QThread): self.mode.web.generate_static_url_path() # Choose port and password early, because we need them to exist in advance for scheduled shares - self.mode.app.stay_open = not self.mode.common.settings.get( - "close_after_first_download" - ) if not self.mode.app.port: self.mode.app.choose_port() - if not self.mode.common.settings.get("public_mode"): + if not self.mode.settings.get("general", "public"): if not self.mode.web.password: self.mode.web.generate_password( - self.mode.common.settings.get("password") + self.mode.settings.get("persistent", "password") ) try: From 70f8e4384d6125b6a5c3f4b85722be93ca62acbb Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 23:36:37 -0700 Subject: [PATCH 051/135] Fix settings test, now that we added the persistent_tabs setting --- tests/test_onionshare_settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_onionshare_settings.py b/tests/test_onionshare_settings.py index 9c81642f..1b21ff3b 100644 --- a/tests/test_onionshare_settings.py +++ b/tests/test_onionshare_settings.py @@ -56,6 +56,7 @@ class TestSettings: "tor_bridges_use_obfs4": False, "tor_bridges_use_meek_lite_azure": False, "tor_bridges_use_custom_bridges": "", + "persistent_tabs": [], } for key in settings_obj._settings: # Skip locale, it will not always default to the same thing From 1787adabc741d4ce75131fca833b560ee7aa485e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 3 Nov 2019 00:47:51 -0700 Subject: [PATCH 052/135] Start writing tabs tests --- onionshare/common.py | 29 ++--- onionshare_gui/tab/tab.py | 28 +++-- tests2/__init__.py | 0 tests2/conftest.py | 192 +++++++++++++++++++++++++++++++ tests2/test_tabs.py | 231 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 455 insertions(+), 25 deletions(-) create mode 100644 tests2/__init__.py create mode 100644 tests2/conftest.py create mode 100644 tests2/test_tabs.py diff --git a/onionshare/common.py b/onionshare/common.py index 5245ddf9..cf713818 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -159,20 +159,23 @@ class Common: """ Returns the path of the OnionShare data directory. """ - if self.platform == "Windows": - try: - appdata = os.environ["APPDATA"] - onionshare_data_dir = f"{appdata}\\OnionShare" - except: - # If for some reason we don't have the 'APPDATA' environment variable - # (like running tests in Linux while pretending to be in Windows) - onionshare_data_dir = os.path.expanduser("~/.config/onionshare") - elif self.platform == "Darwin": - onionshare_data_dir = os.path.expanduser( - "~/Library/Application Support/OnionShare" - ) + if getattr(sys, "onionshare_test_mode", False): + onionshare_data_dir = os.path.expanduser("~/.config/onionshare-testdata") else: - onionshare_data_dir = os.path.expanduser("~/.config/onionshare") + if self.platform == "Windows": + try: + appdata = os.environ["APPDATA"] + onionshare_data_dir = f"{appdata}\\OnionShare" + except: + # If for some reason we don't have the 'APPDATA' environment variable + # (like running tests in Linux while pretending to be in Windows) + onionshare_data_dir = os.path.expanduser("~/.config/onionshare") + elif self.platform == "Darwin": + onionshare_data_dir = os.path.expanduser( + "~/Library/Application Support/OnionShare" + ) + else: + onionshare_data_dir = os.path.expanduser("~/.config/onionshare") os.makedirs(onionshare_data_dir, 0o700, True) return onionshare_data_dir diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index cf648d8c..53dc4ad5 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -67,23 +67,27 @@ class Tab(QtWidgets.QWidget): self.app = OnionShare(common, self.common.gui.onion, self.common.gui.local_only) # Widgets to display on a new tab - share_button = QtWidgets.QPushButton(strings._("gui_new_tab_share_button")) - share_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"]) + self.share_button = QtWidgets.QPushButton(strings._("gui_new_tab_share_button")) + self.share_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"]) share_description = QtWidgets.QLabel(strings._("gui_new_tab_share_description")) share_description.setWordWrap(True) - share_button.clicked.connect(self.share_mode_clicked) + self.share_button.clicked.connect(self.share_mode_clicked) - receive_button = QtWidgets.QPushButton(strings._("gui_new_tab_receive_button")) - receive_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"]) - receive_button.clicked.connect(self.receive_mode_clicked) + self.receive_button = QtWidgets.QPushButton( + strings._("gui_new_tab_receive_button") + ) + self.receive_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"]) + self.receive_button.clicked.connect(self.receive_mode_clicked) receive_description = QtWidgets.QLabel( strings._("gui_new_tab_receive_description") ) receive_description.setWordWrap(True) - website_button = QtWidgets.QPushButton(strings._("gui_new_tab_website_button")) - website_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"]) - website_button.clicked.connect(self.website_mode_clicked) + self.website_button = QtWidgets.QPushButton( + strings._("gui_new_tab_website_button") + ) + self.website_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"]) + self.website_button.clicked.connect(self.website_mode_clicked) website_description = QtWidgets.QLabel( strings._("gui_new_tab_website_description") ) @@ -91,13 +95,13 @@ class Tab(QtWidgets.QWidget): new_tab_layout = QtWidgets.QVBoxLayout() new_tab_layout.addStretch(1) - new_tab_layout.addWidget(share_button) + new_tab_layout.addWidget(self.share_button) new_tab_layout.addWidget(share_description) new_tab_layout.addSpacing(50) - new_tab_layout.addWidget(receive_button) + new_tab_layout.addWidget(self.receive_button) new_tab_layout.addWidget(receive_description) new_tab_layout.addSpacing(50) - new_tab_layout.addWidget(website_button) + new_tab_layout.addWidget(self.website_button) new_tab_layout.addWidget(website_description) new_tab_layout.addStretch(3) diff --git a/tests2/__init__.py b/tests2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests2/conftest.py b/tests2/conftest.py new file mode 100644 index 00000000..8d8e77f6 --- /dev/null +++ b/tests2/conftest.py @@ -0,0 +1,192 @@ +import sys + +# Force tests to look for resources in the source code tree +sys.onionshare_dev_mode = True + +# Let OnionShare know the tests are running, to avoid colliding with settings files +sys.onionshare_test_mode = True + +import os +import shutil +import tempfile + +import pytest + +from onionshare import common, web, settings, strings + + +def pytest_addoption(parser): + parser.addoption( + "--rungui", action="store_true", default=False, help="run GUI tests" + ) + parser.addoption( + "--runtor", action="store_true", default=False, help="run tor tests" + ) + + +def pytest_collection_modifyitems(config, items): + if not config.getoption("--runtor"): + # --runtor given in cli: do not skip tor tests + skip_tor = pytest.mark.skip(reason="need --runtor option to run") + for item in items: + if "tor" in item.keywords: + item.add_marker(skip_tor) + + if not config.getoption("--rungui"): + # --rungui given in cli: do not skip GUI tests + skip_gui = pytest.mark.skip(reason="need --rungui option to run") + for item in items: + if "gui" in item.keywords: + item.add_marker(skip_gui) + + +@pytest.fixture +def temp_dir_1024(): + """ Create a temporary directory that has a single file of a + particular size (1024 bytes). + """ + + tmp_dir = tempfile.mkdtemp() + tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir) + with open(tmp_file, "wb") as f: + f.write(b"*" * 1024) + return tmp_dir + + +# pytest > 2.9 only needs @pytest.fixture +@pytest.yield_fixture +def temp_dir_1024_delete(): + """ Create a temporary directory that has a single file of a + particular size (1024 bytes). The temporary directory (including + the file inside) will be deleted after fixture usage. + """ + + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir) + with open(tmp_file, "wb") as f: + f.write(b"*" * 1024) + yield tmp_dir + + +@pytest.fixture +def temp_file_1024(): + """ Create a temporary file of a particular size (1024 bytes). """ + + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_file.write(b"*" * 1024) + return tmp_file.name + + +# pytest > 2.9 only needs @pytest.fixture +@pytest.yield_fixture +def temp_file_1024_delete(): + """ + Create a temporary file of a particular size (1024 bytes). + The temporary file will be deleted after fixture usage. + """ + + with tempfile.NamedTemporaryFile() as tmp_file: + tmp_file.write(b"*" * 1024) + tmp_file.flush() + yield tmp_file.name + + +# pytest > 2.9 only needs @pytest.fixture +@pytest.yield_fixture(scope="session") +def custom_zw(): + zw = web.share_mode.ZipWriter( + common.Common(), + zip_filename=common.Common.random_string(4, 6), + processed_size_callback=lambda _: "custom_callback", + ) + yield zw + zw.close() + os.remove(zw.zip_filename) + + +# pytest > 2.9 only needs @pytest.fixture +@pytest.yield_fixture(scope="session") +def default_zw(): + zw = web.share_mode.ZipWriter(common.Common()) + yield zw + zw.close() + tmp_dir = os.path.dirname(zw.zip_filename) + shutil.rmtree(tmp_dir) + + +@pytest.fixture +def locale_en(monkeypatch): + monkeypatch.setattr("locale.getdefaultlocale", lambda: ("en_US", "UTF-8")) + + +@pytest.fixture +def locale_fr(monkeypatch): + monkeypatch.setattr("locale.getdefaultlocale", lambda: ("fr_FR", "UTF-8")) + + +@pytest.fixture +def locale_invalid(monkeypatch): + monkeypatch.setattr("locale.getdefaultlocale", lambda: ("xx_XX", "UTF-8")) + + +@pytest.fixture +def locale_ru(monkeypatch): + monkeypatch.setattr("locale.getdefaultlocale", lambda: ("ru_RU", "UTF-8")) + + +@pytest.fixture +def platform_darwin(monkeypatch): + monkeypatch.setattr("platform.system", lambda: "Darwin") + + +@pytest.fixture # (scope="session") +def platform_linux(monkeypatch): + monkeypatch.setattr("platform.system", lambda: "Linux") + + +@pytest.fixture +def platform_windows(monkeypatch): + monkeypatch.setattr("platform.system", lambda: "Windows") + + +@pytest.fixture +def sys_argv_sys_prefix(monkeypatch): + monkeypatch.setattr("sys.argv", [sys.prefix]) + + +@pytest.fixture +def sys_frozen(monkeypatch): + monkeypatch.setattr("sys.frozen", True, raising=False) + + +@pytest.fixture +def sys_meipass(monkeypatch): + monkeypatch.setattr("sys._MEIPASS", os.path.expanduser("~"), raising=False) + + +@pytest.fixture # (scope="session") +def sys_onionshare_dev_mode(monkeypatch): + monkeypatch.setattr("sys.onionshare_dev_mode", True, raising=False) + + +@pytest.fixture +def time_time_100(monkeypatch): + monkeypatch.setattr("time.time", lambda: 100) + + +@pytest.fixture +def time_strftime(monkeypatch): + monkeypatch.setattr("time.strftime", lambda _: "Jun 06 2013 11:05:00") + + +@pytest.fixture +def common_obj(): + return common.Common() + + +@pytest.fixture +def settings_obj(sys_onionshare_dev_mode, platform_linux): + _common = common.Common() + _common.version = "DUMMY_VERSION_1.2.3" + strings.load_strings(_common) + return settings.Settings(_common) diff --git a/tests2/test_tabs.py b/tests2/test_tabs.py new file mode 100644 index 00000000..8e8bf775 --- /dev/null +++ b/tests2/test_tabs.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +import pytest +import unittest + +import json +import os +import requests +import shutil +import base64 +import tempfile +import secrets + +from PyQt5 import QtCore, QtTest, QtWidgets + +from onionshare import strings +from onionshare.common import Common +from onionshare.settings import Settings +from onionshare.onion import Onion +from onionshare.web import Web +from onionshare_gui import Application, MainWindow, GuiCommon + + +class TestTabs(unittest.TestCase): + @classmethod + def setUpClass(cls): + common = Common() + qtapp = Application(common) + common.gui = GuiCommon(common, qtapp, local_only=True) + cls.gui = MainWindow(common, filenames=None) + cls.gui.qtapp = qtapp + + # Create some random files to test with + cls.tmpdir = tempfile.TemporaryDirectory() + cls.tmpfiles = [] + for _ in range(10): + filename = os.path.join(cls.tmpdir.name, f"{secrets.token_hex(4)}.txt") + with open(filename, "w") as file: + file.write(secrets.token_hex(10)) + cls.tmpfiles.append(filename) + + @classmethod + def tearDownClass(cls): + cls.gui.cleanup() + + @pytest.mark.gui + def test_001_gui_loaded(self): + """Test that the GUI actually is shown""" + self.assertTrue(self.gui.show) + + @pytest.mark.gui + def test_002_window_title_seen(self): + """Test that the window title is OnionShare""" + self.assertEqual(self.gui.windowTitle(), "OnionShare") + + @pytest.mark.gui + def test_003_settings_button_is_visible(self): + """Test that the settings button is visible""" + self.assertTrue(self.gui.settings_button.isVisible()) + + @pytest.mark.gui + def test_004_server_status_bar_is_visible(self): + """Test that the status bar is visible""" + self.assertTrue(self.gui.status_bar.isVisible()) + + @pytest.mark.gui + def test_005_starts_with_one_new_tab(self): + """There should be one "New Tab" tab open""" + self.assertEqual(self.gui.tabs.count(), 1) + self.assertTrue(self.gui.tabs.widget(0).new_tab.isVisible()) + + @pytest.mark.gui + def test_006_new_tab_button_opens_new_tabs(self): + """Clicking the "+" button should open new tabs""" + self.assertEqual(self.gui.tabs.count(), 1) + QtTest.QTest.mouseClick(self.gui.tabs.new_tab_button, QtCore.Qt.LeftButton) + QtTest.QTest.mouseClick(self.gui.tabs.new_tab_button, QtCore.Qt.LeftButton) + QtTest.QTest.mouseClick(self.gui.tabs.new_tab_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.tabs.count(), 4) + + @pytest.mark.gui + def test_007_close_tab_button_closes_tabs(self): + """Clicking the "x" button should close tabs""" + self.assertEqual(self.gui.tabs.count(), 4) + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + self.assertEqual(self.gui.tabs.count(), 1) + + @pytest.mark.gui + def test_008_closing_last_tab_opens_new_one(self): + """Closing the last tab should open a new tab""" + self.assertEqual(self.gui.tabs.count(), 1) + + # Click share button + QtTest.QTest.mouseClick( + self.gui.tabs.widget(0).share_button, QtCore.Qt.LeftButton + ) + self.assertFalse(self.gui.tabs.widget(0).new_tab.isVisible()) + self.assertTrue(self.gui.tabs.widget(0).share_mode.isVisible()) + + # Close the tab + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + + # A new tab should be opened + self.assertEqual(self.gui.tabs.count(), 1) + self.assertTrue(self.gui.tabs.widget(0).new_tab.isVisible()) + + @pytest.mark.gui + def test_009_new_tab_mode_buttons_show_correct_modes(self): + """Clicking the mode buttons in a new tab should change the mode of the tab""" + + # New tab, share files + QtTest.QTest.mouseClick(self.gui.tabs.new_tab_button, QtCore.Qt.LeftButton) + QtTest.QTest.mouseClick( + self.gui.tabs.widget(1).share_button, QtCore.Qt.LeftButton + ) + self.assertFalse(self.gui.tabs.widget(1).new_tab.isVisible()) + self.assertTrue(self.gui.tabs.widget(1).share_mode.isVisible()) + + # New tab, receive files + QtTest.QTest.mouseClick(self.gui.tabs.new_tab_button, QtCore.Qt.LeftButton) + QtTest.QTest.mouseClick( + self.gui.tabs.widget(2).receive_button, QtCore.Qt.LeftButton + ) + self.assertFalse(self.gui.tabs.widget(2).new_tab.isVisible()) + self.assertTrue(self.gui.tabs.widget(2).receive_mode.isVisible()) + + # New tab, publish website + QtTest.QTest.mouseClick(self.gui.tabs.new_tab_button, QtCore.Qt.LeftButton) + QtTest.QTest.mouseClick( + self.gui.tabs.widget(3).website_button, QtCore.Qt.LeftButton + ) + self.assertFalse(self.gui.tabs.widget(3).new_tab.isVisible()) + self.assertTrue(self.gui.tabs.widget(3).website_mode.isVisible()) + + # Close tabs + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + + @pytest.mark.gui + def test_010_close_share_tab_while_server_started_should_warn(self): + """Closing a share mode tab when the server is running should throw a warning""" + pass + """ + tab = self.gui.tabs.widget(0) + + # Share files + QtTest.QTest.mouseClick(tab.share_button, QtCore.Qt.LeftButton) + self.assertFalse(tab.new_tab.isVisible()) + self.assertTrue(tab.share_mode.isVisible()) + + # Add files + for filename in self.tmpfiles: + tab.share_mode.server_status.file_selection.file_list.add_file(filename) + + # Start the server + self.assertEqual( + tab.share_mode.server_status.status, + tab.share_mode.server_status.STATUS_STOPPED, + ) + QtTest.QTest.mouseClick( + tab.share_mode.server_status.server_button, QtCore.Qt.LeftButton + ) + self.assertEqual( + tab.share_mode.server_status.status, + tab.share_mode.server_status.STATUS_WORKING, + ) + QtTest.QTest.qWait(1000) + self.assertEqual( + tab.share_mode.server_status.status, + tab.share_mode.server_status.STATUS_STARTED, + ) + + # Close tab + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + + # The active window should now be a dialog + dialog = self.gui.qtapp.activeWindow() + self.assertEqual(type(dialog), QtWidgets.QMessageBox) + + # Reject it -- the share mode tab should still be open + dialog.reject() + self.assertFalse(tab.new_tab.isVisible()) + self.assertTrue(tab.share_mode.isVisible()) + + # Close tab + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + + # This time accept it -- the share mode tab should be closed + dialog = self.gui.qtapp.activeWindow() + self.assertEqual(type(dialog), QtWidgets.QMessageBox) + dialog.accept() + + self.assertTrue(self.gui.tabs.widget(0).new_tab.isVisible()) + """ + + +if __name__ == "__main__": + unittest.main() From 6d75468aaab5d2086025ca53ee8d69d84608a77c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 7 Nov 2019 13:25:47 +0800 Subject: [PATCH 053/135] Start writing tab tests, and figure out how to test the modal dialogs --- onionshare_gui/tab/tab.py | 29 ++++++++++++++++------------- tests2/test_tabs.py | 25 +++++++++++-------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 53dc4ad5..4bc45ac9 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -142,6 +142,19 @@ class Tab(QtWidgets.QWidget): ) self.persistent_image_label.setFixedSize(20, 20) + # Create the close warning dialog -- the dialog widget needs to be in the constructor + # in order to test it + self.close_dialog = QtWidgets.QMessageBox() + self.close_dialog.setWindowTitle(strings._("gui_close_tab_warning_title")) + self.close_dialog.setIcon(QtWidgets.QMessageBox.Critical) + self.close_dialog.accept_button = self.close_dialog.addButton( + strings._("gui_close_tab_warning_close"), QtWidgets.QMessageBox.AcceptRole + ) + self.close_dialog.reject_button = self.close_dialog.addButton( + strings._("gui_close_tab_warning_cancel"), QtWidgets.QMessageBox.RejectRole + ) + self.close_dialog.setDefaultButton(self.close_dialog.reject_button) + def init(self, mode_settings=None): if mode_settings: # Load this tab @@ -538,21 +551,11 @@ class Tab(QtWidgets.QWidget): # Open the warning dialog self.common.log("Tab", "close_tab, opening warning dialog") - dialog = QtWidgets.QMessageBox() - dialog.setWindowTitle(strings._("gui_close_tab_warning_title")) - dialog.setText(dialog_text) - dialog.setIcon(QtWidgets.QMessageBox.Critical) - dialog.addButton( - strings._("gui_close_tab_warning_close"), QtWidgets.QMessageBox.YesRole - ) - cancel_button = dialog.addButton( - strings._("gui_close_tab_warning_cancel"), QtWidgets.QMessageBox.NoRole - ) - dialog.setDefaultButton(cancel_button) - reply = dialog.exec_() + self.close_dialog.setText(dialog_text) + self.close_dialog.exec_() # Close - if reply == 0: + if self.close_dialog.clickedButton() == self.close_dialog.accept_button: self.common.log("Tab", "close_tab", "close, closing tab") self.get_mode().stop_server() self.app.cleanup() diff --git a/tests2/test_tabs.py b/tests2/test_tabs.py index 8e8bf775..9606b33c 100644 --- a/tests2/test_tabs.py +++ b/tests2/test_tabs.py @@ -166,8 +166,6 @@ class TestTabs(unittest.TestCase): @pytest.mark.gui def test_010_close_share_tab_while_server_started_should_warn(self): """Closing a share mode tab when the server is running should throw a warning""" - pass - """ tab = self.gui.tabs.widget(0) # Share files @@ -197,34 +195,33 @@ class TestTabs(unittest.TestCase): tab.share_mode.server_status.STATUS_STARTED, ) + # Prepare to reject the dialog + QtCore.QTimer.singleShot(1000, tab.close_dialog.reject_button.click) + # Close tab QtTest.QTest.mouseClick( self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), QtCore.Qt.LeftButton, ) + QtTest.QTest.qWait(1000) - # The active window should now be a dialog - dialog = self.gui.qtapp.activeWindow() - self.assertEqual(type(dialog), QtWidgets.QMessageBox) - - # Reject it -- the share mode tab should still be open - dialog.reject() + # The tab should still be open self.assertFalse(tab.new_tab.isVisible()) self.assertTrue(tab.share_mode.isVisible()) + # Prepare to accept the dialog + QtCore.QTimer.singleShot(1000, tab.close_dialog.accept_button.click) + # Close tab QtTest.QTest.mouseClick( self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), QtCore.Qt.LeftButton, ) + QtTest.QTest.qWait(1000) - # This time accept it -- the share mode tab should be closed - dialog = self.gui.qtapp.activeWindow() - self.assertEqual(type(dialog), QtWidgets.QMessageBox) - dialog.accept() - + # The tab should be closed + # QtTest.QTest.qWait(5000) self.assertTrue(self.gui.tabs.widget(0).new_tab.isVisible()) - """ if __name__ == "__main__": From 9ae0d991102e49b6c2495571781e7d8e14bbffc6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 7 Nov 2019 13:34:24 +0800 Subject: [PATCH 054/135] Test closing active tabs on all modes --- tests2/test_tabs.py | 111 ++++++++++++++++++++++++++++---------------- 1 file changed, 72 insertions(+), 39 deletions(-) diff --git a/tests2/test_tabs.py b/tests2/test_tabs.py index 9606b33c..e9a367d1 100644 --- a/tests2/test_tabs.py +++ b/tests2/test_tabs.py @@ -42,6 +42,52 @@ class TestTabs(unittest.TestCase): def tearDownClass(cls): cls.gui.cleanup() + def close_tab_with_active_server(self, tab): + # Start the server + self.assertEqual( + tab.get_mode().server_status.status, + tab.get_mode().server_status.STATUS_STOPPED, + ) + QtTest.QTest.mouseClick( + tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton + ) + self.assertEqual( + tab.get_mode().server_status.status, + tab.get_mode().server_status.STATUS_WORKING, + ) + QtTest.QTest.qWait(1000) + self.assertEqual( + tab.get_mode().server_status.status, + tab.get_mode().server_status.STATUS_STARTED, + ) + + # Prepare to reject the dialog + QtCore.QTimer.singleShot(1000, tab.close_dialog.reject_button.click) + + # Close tab + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + QtTest.QTest.qWait(1000) + + # The tab should still be open + self.assertFalse(tab.new_tab.isVisible()) + self.assertTrue(tab.get_mode().isVisible()) + + # Prepare to accept the dialog + QtCore.QTimer.singleShot(1000, tab.close_dialog.accept_button.click) + + # Close tab + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + QtTest.QTest.qWait(1000) + + # The tab should be closed + self.assertTrue(self.gui.tabs.widget(0).new_tab.isVisible()) + @pytest.mark.gui def test_001_gui_loaded(self): """Test that the GUI actually is shown""" @@ -177,51 +223,38 @@ class TestTabs(unittest.TestCase): for filename in self.tmpfiles: tab.share_mode.server_status.file_selection.file_list.add_file(filename) - # Start the server - self.assertEqual( - tab.share_mode.server_status.status, - tab.share_mode.server_status.STATUS_STOPPED, - ) - QtTest.QTest.mouseClick( - tab.share_mode.server_status.server_button, QtCore.Qt.LeftButton - ) - self.assertEqual( - tab.share_mode.server_status.status, - tab.share_mode.server_status.STATUS_WORKING, - ) - QtTest.QTest.qWait(1000) - self.assertEqual( - tab.share_mode.server_status.status, - tab.share_mode.server_status.STATUS_STARTED, - ) + # Test closing it + self.close_tab_with_active_server(tab) - # Prepare to reject the dialog - QtCore.QTimer.singleShot(1000, tab.close_dialog.reject_button.click) + @pytest.mark.gui + def test_011_close_receive_tab_while_server_started_should_warn(self): + """Closing a recieve mode tab when the server is running should throw a warning""" + tab = self.gui.tabs.widget(0) - # Close tab - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) - QtTest.QTest.qWait(1000) - - # The tab should still be open + # Receive files + QtTest.QTest.mouseClick(tab.receive_button, QtCore.Qt.LeftButton) self.assertFalse(tab.new_tab.isVisible()) - self.assertTrue(tab.share_mode.isVisible()) + self.assertTrue(tab.receive_mode.isVisible()) - # Prepare to accept the dialog - QtCore.QTimer.singleShot(1000, tab.close_dialog.accept_button.click) + # Test closing it + self.close_tab_with_active_server(tab) - # Close tab - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) - QtTest.QTest.qWait(1000) + @pytest.mark.gui + def test_012_close_website_tab_while_server_started_should_warn(self): + """Closing a website mode tab when the server is running should throw a warning""" + tab = self.gui.tabs.widget(0) - # The tab should be closed - # QtTest.QTest.qWait(5000) - self.assertTrue(self.gui.tabs.widget(0).new_tab.isVisible()) + # Publish website + QtTest.QTest.mouseClick(tab.website_button, QtCore.Qt.LeftButton) + self.assertFalse(tab.new_tab.isVisible()) + self.assertTrue(tab.website_mode.isVisible()) + + # Add files + for filename in self.tmpfiles: + tab.website_mode.server_status.file_selection.file_list.add_file(filename) + + # Test closing it + self.close_tab_with_active_server(tab) if __name__ == "__main__": From 786ff5d5bfd7b521af5ee0537383294db4be0880 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 15:31:02 +0800 Subject: [PATCH 055/135] Test that persistent tabs show warnings, save persistent settings files --- tests2/test_tabs.py | 149 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 115 insertions(+), 34 deletions(-) diff --git a/tests2/test_tabs.py b/tests2/test_tabs.py index e9a367d1..62701ab1 100644 --- a/tests2/test_tabs.py +++ b/tests2/test_tabs.py @@ -42,6 +42,56 @@ class TestTabs(unittest.TestCase): def tearDownClass(cls): cls.gui.cleanup() + # Shared test methods + + def verify_new_tab(self, tab): + # Make sure the new tab widget is showing, and no mode has been started + self.assertTrue(tab.new_tab.isVisible()) + self.assertFalse(hasattr(tab, "share_mode")) + self.assertFalse(hasattr(tab, "receive_mode")) + self.assertFalse(hasattr(tab, "website_mode")) + + def new_share_tab(self): + tab = self.gui.tabs.widget(0) + self.verify_new_tab(tab) + + # Share files + QtTest.QTest.mouseClick(tab.share_button, QtCore.Qt.LeftButton) + self.assertFalse(tab.new_tab.isVisible()) + self.assertTrue(tab.share_mode.isVisible()) + + # Add files + for filename in self.tmpfiles: + tab.share_mode.server_status.file_selection.file_list.add_file(filename) + + return tab + + def new_receive_tab(self): + tab = self.gui.tabs.widget(0) + self.verify_new_tab(tab) + + # Receive files + QtTest.QTest.mouseClick(tab.receive_button, QtCore.Qt.LeftButton) + self.assertFalse(tab.new_tab.isVisible()) + self.assertTrue(tab.receive_mode.isVisible()) + + return tab + + def new_website_tab(self): + tab = self.gui.tabs.widget(0) + self.verify_new_tab(tab) + + # Publish website + QtTest.QTest.mouseClick(tab.website_button, QtCore.Qt.LeftButton) + self.assertFalse(tab.new_tab.isVisible()) + self.assertTrue(tab.website_mode.isVisible()) + + # Add files + for filename in self.tmpfiles: + tab.website_mode.server_status.file_selection.file_list.add_file(filename) + + return tab + def close_tab_with_active_server(self, tab): # Start the server self.assertEqual( @@ -69,7 +119,6 @@ class TestTabs(unittest.TestCase): self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), QtCore.Qt.LeftButton, ) - QtTest.QTest.qWait(1000) # The tab should still be open self.assertFalse(tab.new_tab.isVisible()) @@ -83,11 +132,54 @@ class TestTabs(unittest.TestCase): self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), QtCore.Qt.LeftButton, ) - QtTest.QTest.qWait(1000) # The tab should be closed self.assertTrue(self.gui.tabs.widget(0).new_tab.isVisible()) + def close_persistent_tab(self, tab): + # There shouldn't be a persistent settings file + self.assertFalse(os.path.exists(tab.settings.filename)) + + # Click the persistent checkbox + tab.get_mode().server_status.mode_settings_widget.persistent_checkbox.click() + QtTest.QTest.qWait(100) + + # There should be a persistent settings file now + self.assertTrue(os.path.exists(tab.settings.filename)) + + # Prepare to reject the dialog + QtCore.QTimer.singleShot(1000, tab.close_dialog.reject_button.click) + + # Close tab + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + + # The tab should still be open + self.assertFalse(tab.new_tab.isVisible()) + self.assertTrue(tab.get_mode().isVisible()) + + # There should be a persistent settings file still + self.assertTrue(os.path.exists(tab.settings.filename)) + + # Prepare to accept the dialog + QtCore.QTimer.singleShot(1000, tab.close_dialog.accept_button.click) + + # Close tab + QtTest.QTest.mouseClick( + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), + QtCore.Qt.LeftButton, + ) + + # The tab should be closed + self.assertTrue(self.gui.tabs.widget(0).new_tab.isVisible()) + + # The persistent settings file should be deleted + self.assertFalse(os.path.exists(tab.settings.filename)) + + # Tests + @pytest.mark.gui def test_001_gui_loaded(self): """Test that the GUI actually is shown""" @@ -212,50 +304,39 @@ class TestTabs(unittest.TestCase): @pytest.mark.gui def test_010_close_share_tab_while_server_started_should_warn(self): """Closing a share mode tab when the server is running should throw a warning""" - tab = self.gui.tabs.widget(0) - - # Share files - QtTest.QTest.mouseClick(tab.share_button, QtCore.Qt.LeftButton) - self.assertFalse(tab.new_tab.isVisible()) - self.assertTrue(tab.share_mode.isVisible()) - - # Add files - for filename in self.tmpfiles: - tab.share_mode.server_status.file_selection.file_list.add_file(filename) - - # Test closing it + tab = self.new_share_tab() self.close_tab_with_active_server(tab) @pytest.mark.gui def test_011_close_receive_tab_while_server_started_should_warn(self): """Closing a recieve mode tab when the server is running should throw a warning""" - tab = self.gui.tabs.widget(0) - - # Receive files - QtTest.QTest.mouseClick(tab.receive_button, QtCore.Qt.LeftButton) - self.assertFalse(tab.new_tab.isVisible()) - self.assertTrue(tab.receive_mode.isVisible()) - - # Test closing it + tab = self.new_receive_tab() self.close_tab_with_active_server(tab) @pytest.mark.gui def test_012_close_website_tab_while_server_started_should_warn(self): """Closing a website mode tab when the server is running should throw a warning""" - tab = self.gui.tabs.widget(0) - - # Publish website - QtTest.QTest.mouseClick(tab.website_button, QtCore.Qt.LeftButton) - self.assertFalse(tab.new_tab.isVisible()) - self.assertTrue(tab.website_mode.isVisible()) - - # Add files - for filename in self.tmpfiles: - tab.website_mode.server_status.file_selection.file_list.add_file(filename) - - # Test closing it + tab = self.new_website_tab() self.close_tab_with_active_server(tab) + @pytest.mark.gui + def test_013_close_persistent_share_tab_shows_warning(self): + """Closing a share mode tab that's persistent should show a warning""" + tab = self.new_share_tab() + self.close_persistent_tab(tab) + + @pytest.mark.gui + def test_014_close_persistent_receive_tab_shows_warning(self): + """Closing a receive mode tab that's persistent should show a warning""" + tab = self.new_receive_tab() + self.close_persistent_tab(tab) + + @pytest.mark.gui + def test_015_close_persistent_website_tab_shows_warning(self): + """Closing a website mode tab that's persistent should show a warning""" + tab = self.new_website_tab() + self.close_persistent_tab(tab) + if __name__ == "__main__": unittest.main() From cef0d7711bde8bb58069ce5b15ba516b9fa47345 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 15:40:37 +0800 Subject: [PATCH 056/135] Test closing window --- onionshare_gui/main_window.py | 30 +++++++++++++++++------------- tests2/test_tabs.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index 22035b69..06b51c9b 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -147,6 +147,20 @@ class MainWindow(QtWidgets.QMainWindow): # After connecting to Tor, check for updates self.check_for_updates() + # Create the close warning dialog -- the dialog widget needs to be in the constructor + # in order to test it + self.close_dialog = QtWidgets.QMessageBox() + self.close_dialog.setWindowTitle(strings._("gui_quit_warning_title")) + self.close_dialog.setText(strings._("gui_quit_warning_description")) + self.close_dialog.setIcon(QtWidgets.QMessageBox.Critical) + self.close_dialog.accept_button = self.close_dialog.addButton( + strings._("gui_quit_warning_quit"), QtWidgets.QMessageBox.AcceptRole + ) + self.close_dialog.reject_button = self.close_dialog.addButton( + strings._("gui_quit_warning_cancel"), QtWidgets.QMessageBox.NoRole + ) + self.close_dialog.setDefaultButton(self.close_dialog.reject_button) + def tor_connection_canceled(self): """ If the user cancels before Tor finishes connecting, ask if they want to @@ -272,21 +286,11 @@ class MainWindow(QtWidgets.QMainWindow): if self.tabs.are_tabs_active(): # Open the warning dialog - dialog = QtWidgets.QMessageBox() - dialog.setWindowTitle(strings._("gui_quit_warning_title")) - dialog.setText(strings._("gui_quit_warning_description")) - dialog.setIcon(QtWidgets.QMessageBox.Critical) - dialog.addButton( - strings._("gui_quit_warning_quit"), QtWidgets.QMessageBox.YesRole - ) - cancel_button = dialog.addButton( - strings._("gui_quit_warning_cancel"), QtWidgets.QMessageBox.NoRole - ) - dialog.setDefaultButton(cancel_button) - reply = dialog.exec_() + self.common.log("MainWindow", "closeEvent, opening warning dialog") + self.close_dialog.exec_() # Close - if reply == 0: + if self.close_dialog.clickedButton() == self.close_dialog.accept_button: self.system_tray.hide() e.accept() # Cancel diff --git a/tests2/test_tabs.py b/tests2/test_tabs.py index 62701ab1..d1c19530 100644 --- a/tests2/test_tabs.py +++ b/tests2/test_tabs.py @@ -337,6 +337,38 @@ class TestTabs(unittest.TestCase): tab = self.new_website_tab() self.close_persistent_tab(tab) + @pytest.mark.gui + def test_016_quit_with_server_started_should_warn(self): + """Quitting OnionShare with any active servers should show a warning""" + tab = self.new_share_tab() + + # Start the server + self.assertEqual( + tab.get_mode().server_status.status, + tab.get_mode().server_status.STATUS_STOPPED, + ) + QtTest.QTest.mouseClick( + tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton + ) + self.assertEqual( + tab.get_mode().server_status.status, + tab.get_mode().server_status.STATUS_WORKING, + ) + QtTest.QTest.qWait(1000) + self.assertEqual( + tab.get_mode().server_status.status, + tab.get_mode().server_status.STATUS_STARTED, + ) + + # Prepare to reject the dialog + QtCore.QTimer.singleShot(1000, self.gui.close_dialog.reject_button.click) + + # Close the window + self.gui.close() + + # The window should still be open + self.assertTrue(self.gui.isVisible()) + if __name__ == "__main__": unittest.main() From 92ea9bcf38c5c32cbc72c1d2936a29bd9bd08907 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 15:57:12 +0800 Subject: [PATCH 057/135] Make the TabTests based off GuiBaseTest --- tests2/gui_base_test.py | 352 ++++++++++++++++++++++ tests2/{test_tabs.py => test_gui_tabs.py} | 69 ++--- 2 files changed, 369 insertions(+), 52 deletions(-) create mode 100644 tests2/gui_base_test.py rename tests2/{test_tabs.py => test_gui_tabs.py} (83%) diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py new file mode 100644 index 00000000..bb44627f --- /dev/null +++ b/tests2/gui_base_test.py @@ -0,0 +1,352 @@ +import pytest +import unittest + +import json +import os +import requests +import shutil +import base64 +import tempfile +import secrets + +from PyQt5 import QtCore, QtTest, QtWidgets + +from onionshare import strings +from onionshare.common import Common +from onionshare.settings import Settings +from onionshare.onion import Onion +from onionshare.web import Web + +from onionshare_gui import Application, MainWindow, GuiCommon +from onionshare_gui.tab.mode.share_mode import ShareMode +from onionshare_gui.tab.mode.receive_mode import ReceiveMode +from onionshare_gui.tab.mode.website_mode import WebsiteMode + + +class GuiBaseTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + common = Common() + qtapp = Application(common) + common.gui = GuiCommon(common, qtapp, local_only=True) + cls.gui = MainWindow(common, filenames=None) + cls.gui.qtapp = qtapp + + # Create some random files to test with + cls.tmpdir = tempfile.TemporaryDirectory() + cls.tmpfiles = [] + for _ in range(10): + filename = os.path.join(cls.tmpdir.name, f"{secrets.token_hex(4)}.txt") + with open(filename, "w") as file: + file.write(secrets.token_hex(10)) + cls.tmpfiles.append(filename) + + @classmethod + def tearDownClass(cls): + cls.gui.cleanup() + + # Shared test methods + + def gui_loaded(self): + """Test that the GUI actually is shown""" + self.assertTrue(self.gui.show) + + def window_title_seen(self): + """Test that the window title is OnionShare""" + self.assertEqual(self.gui.windowTitle(), "OnionShare") + + def settings_button_is_visible(self): + """Test that the settings button is visible""" + self.assertTrue(self.gui.settings_button.isVisible()) + + def settings_button_is_hidden(self): + """Test that the settings button is hidden when the server starts""" + self.assertFalse(self.gui.settings_button.isVisible()) + + def server_status_bar_is_visible(self): + """Test that the status bar is visible""" + self.assertTrue(self.gui.status_bar.isVisible()) + + def click_mode(self, mode): + """Test that we can switch Mode by clicking the button""" + if type(mode) == ReceiveMode: + QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) + if type(mode) == ShareMode: + QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_SHARE) + if type(mode) == WebsiteMode: + QtTest.QTest.mouseClick(self.gui.website_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_WEBSITE) + + def click_toggle_history(self, mode): + """Test that we can toggle Download or Upload history by clicking the toggle button""" + currently_visible = mode.history.isVisible() + QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton) + self.assertEqual(mode.history.isVisible(), not currently_visible) + + def history_indicator(self, mode, public_mode, indicator_count="1"): + """Test that we can make sure the history is toggled off, do an action, and the indiciator works""" + # Make sure history is toggled off + if mode.history.isVisible(): + QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton) + self.assertFalse(mode.history.isVisible()) + + # Indicator should not be visible yet + self.assertFalse(mode.toggle_history.indicator_label.isVisible()) + + if type(mode) == ReceiveMode: + # Upload a file + files = {"file[]": open("/tmp/test.txt", "rb")} + url = f"http://127.0.0.1:{self.gui.app.port}/upload" + if public_mode: + requests.post(url, files=files) + else: + requests.post( + url, + files=files, + auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password), + ) + QtTest.QTest.qWait(2000) + + if type(mode) == ShareMode: + # Download files + url = f"http://127.0.0.1:{self.gui.app.port}/download" + if public_mode: + requests.get(url) + else: + requests.get( + url, + auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password), + ) + QtTest.QTest.qWait(2000) + + # Indicator should be visible, have a value of "1" + self.assertTrue(mode.toggle_history.indicator_label.isVisible()) + self.assertEqual(mode.toggle_history.indicator_label.text(), indicator_count) + + # Toggle history back on, indicator should be hidden again + QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton) + self.assertFalse(mode.toggle_history.indicator_label.isVisible()) + + def history_is_not_visible(self, mode): + """Test that the History section is not visible""" + self.assertFalse(mode.history.isVisible()) + + def history_is_visible(self, mode): + """Test that the History section is visible""" + self.assertTrue(mode.history.isVisible()) + + def server_working_on_start_button_pressed(self, mode): + """Test we can start the service""" + # Should be in SERVER_WORKING state + QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(mode.server_status.status, 1) + + def toggle_indicator_is_reset(self, mode): + self.assertEqual(mode.toggle_history.indicator_count, 0) + self.assertFalse(mode.toggle_history.indicator_label.isVisible()) + + def server_status_indicator_says_starting(self, mode): + """Test that the Server Status indicator shows we are Starting""" + self.assertEqual( + mode.server_status_label.text(), + strings._("gui_status_indicator_share_working"), + ) + + def server_status_indicator_says_scheduled(self, mode): + """Test that the Server Status indicator shows we are Scheduled""" + self.assertEqual( + mode.server_status_label.text(), + strings._("gui_status_indicator_share_scheduled"), + ) + + def server_is_started(self, mode, startup_time=2000): + """Test that the server has started""" + QtTest.QTest.qWait(startup_time) + # Should now be in SERVER_STARTED state + self.assertEqual(mode.server_status.status, 2) + + def web_server_is_running(self): + """Test that the web server has started""" + try: + r = requests.get(f"http://127.0.0.1:{self.gui.app.port}/") + self.assertTrue(True) + except requests.exceptions.ConnectionError: + self.assertTrue(False) + + def have_a_password(self, mode, public_mode): + """Test that we have a valid password""" + if not public_mode: + self.assertRegex(mode.server_status.web.password, r"(\w+)-(\w+)") + else: + self.assertIsNone(mode.server_status.web.password, r"(\w+)-(\w+)") + + def add_button_visible(self, mode): + """Test that the add button should be visible""" + self.assertTrue(mode.server_status.file_selection.add_button.isVisible()) + + def url_description_shown(self, mode): + """Test that the URL label is showing""" + self.assertTrue(mode.server_status.url_description.isVisible()) + + def have_copy_url_button(self, mode, public_mode): + """Test that the Copy URL button is shown and that the clipboard is correct""" + self.assertTrue(mode.server_status.copy_url_button.isVisible()) + + QtTest.QTest.mouseClick( + mode.server_status.copy_url_button, QtCore.Qt.LeftButton + ) + clipboard = self.gui.qtapp.clipboard() + if public_mode: + self.assertEqual(clipboard.text(), f"http://127.0.0.1:{self.gui.app.port}") + else: + self.assertEqual( + clipboard.text(), + f"http://onionshare:{mode.server_status.web.password}@127.0.0.1:{self.gui.app.port}", + ) + + def server_status_indicator_says_started(self, mode): + """Test that the Server Status indicator shows we are started""" + if type(mode) == ReceiveMode: + self.assertEqual( + mode.server_status_label.text(), + strings._("gui_status_indicator_receive_started"), + ) + if type(mode) == ShareMode: + self.assertEqual( + mode.server_status_label.text(), + strings._("gui_status_indicator_share_started"), + ) + + def web_page(self, mode, string, public_mode): + """Test that the web page contains a string""" + + url = f"http://127.0.0.1:{self.gui.app.port}/" + if public_mode: + r = requests.get(url) + else: + r = requests.get( + url, auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password) + ) + + self.assertTrue(string in r.text) + + def history_widgets_present(self, mode): + """Test that the relevant widgets are present in the history view after activity has taken place""" + self.assertFalse(mode.history.empty.isVisible()) + self.assertTrue(mode.history.not_empty.isVisible()) + + def counter_incremented(self, mode, count): + """Test that the counter has incremented""" + self.assertEqual(mode.history.completed_count, count) + + def server_is_stopped(self, mode, stay_open): + """Test that the server stops when we click Stop""" + if ( + type(mode) == ReceiveMode + or (type(mode) == ShareMode and stay_open) + or (type(mode) == WebsiteMode) + ): + QtTest.QTest.mouseClick( + mode.server_status.server_button, QtCore.Qt.LeftButton + ) + self.assertEqual(mode.server_status.status, 0) + + def web_server_is_stopped(self): + """Test that the web server also stopped""" + QtTest.QTest.qWait(2000) + + try: + r = requests.get(f"http://127.0.0.1:{self.gui.app.port}/") + self.assertTrue(False) + except requests.exceptions.ConnectionError: + self.assertTrue(True) + + def server_status_indicator_says_closed(self, mode, stay_open): + """Test that the Server Status indicator shows we closed""" + if type(mode) == ReceiveMode: + self.assertEqual( + self.gui.receive_mode.server_status_label.text(), + strings._("gui_status_indicator_receive_stopped"), + ) + if type(mode) == ShareMode: + if stay_open: + self.assertEqual( + self.gui.share_mode.server_status_label.text(), + strings._("gui_status_indicator_share_stopped"), + ) + else: + self.assertEqual( + self.gui.share_mode.server_status_label.text(), + strings._("closing_automatically"), + ) + + def clear_all_history_items(self, mode, count): + if count == 0: + QtTest.QTest.mouseClick(mode.history.clear_button, QtCore.Qt.LeftButton) + self.assertEquals(len(mode.history.item_list.items.keys()), count) + + # Auto-stop timer tests + def set_timeout(self, mode, timeout): + """Test that the timeout can be set""" + timer = QtCore.QDateTime.currentDateTime().addSecs(timeout) + mode.server_status.autostop_timer_widget.setDateTime(timer) + self.assertTrue(mode.server_status.autostop_timer_widget.dateTime(), timer) + + def autostop_timer_widget_hidden(self, mode): + """Test that the auto-stop timer widget is hidden when share has started""" + self.assertFalse(mode.server_status.autostop_timer_container.isVisible()) + + def server_timed_out(self, mode, wait): + """Test that the server has timed out after the timer ran out""" + QtTest.QTest.qWait(wait) + # We should have timed out now + self.assertEqual(mode.server_status.status, 0) + + # Auto-start timer tests + def set_autostart_timer(self, mode, timer): + """Test that the timer can be set""" + schedule = QtCore.QDateTime.currentDateTime().addSecs(timer) + mode.server_status.autostart_timer_widget.setDateTime(schedule) + self.assertTrue(mode.server_status.autostart_timer_widget.dateTime(), schedule) + + def autostart_timer_widget_hidden(self, mode): + """Test that the auto-start timer widget is hidden when share has started""" + self.assertFalse(mode.server_status.autostart_timer_container.isVisible()) + + def scheduled_service_started(self, mode, wait): + """Test that the server has timed out after the timer ran out""" + QtTest.QTest.qWait(wait) + # We should have started now + self.assertEqual(mode.server_status.status, 2) + + def cancel_the_share(self, mode): + """Test that we can cancel a share before it's started up """ + self.server_working_on_start_button_pressed(mode) + self.server_status_indicator_says_scheduled(mode) + self.add_delete_buttons_hidden() + self.settings_button_is_hidden() + self.set_autostart_timer(mode, 10) + QtTest.QTest.mousePress(mode.server_status.server_button, QtCore.Qt.LeftButton) + QtTest.QTest.qWait(2000) + QtTest.QTest.mouseRelease( + mode.server_status.server_button, QtCore.Qt.LeftButton + ) + self.assertEqual(mode.server_status.status, 0) + self.server_is_stopped(mode, False) + self.web_server_is_stopped() + + # Hack to close an Alert dialog that would otherwise block tests + def accept_dialog(self): + window = self.gui.qtapp.activeWindow() + if window: + window.close() + + # Grouped tests follow from here + + def run_all_common_setup_tests(self): + self.gui_loaded() + self.window_title_seen() + self.settings_button_is_visible() + self.server_status_bar_is_visible() diff --git a/tests2/test_tabs.py b/tests2/test_gui_tabs.py similarity index 83% rename from tests2/test_tabs.py rename to tests2/test_gui_tabs.py index d1c19530..7b03d0fc 100644 --- a/tests2/test_tabs.py +++ b/tests2/test_gui_tabs.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 import pytest import unittest @@ -19,29 +18,10 @@ from onionshare.onion import Onion from onionshare.web import Web from onionshare_gui import Application, MainWindow, GuiCommon +from .gui_base_test import GuiBaseTest -class TestTabs(unittest.TestCase): - @classmethod - def setUpClass(cls): - common = Common() - qtapp = Application(common) - common.gui = GuiCommon(common, qtapp, local_only=True) - cls.gui = MainWindow(common, filenames=None) - cls.gui.qtapp = qtapp - - # Create some random files to test with - cls.tmpdir = tempfile.TemporaryDirectory() - cls.tmpfiles = [] - for _ in range(10): - filename = os.path.join(cls.tmpdir.name, f"{secrets.token_hex(4)}.txt") - with open(filename, "w") as file: - file.write(secrets.token_hex(10)) - cls.tmpfiles.append(filename) - - @classmethod - def tearDownClass(cls): - cls.gui.cleanup() +class TestTabs(GuiBaseTest): # Shared test methods def verify_new_tab(self, tab): @@ -181,33 +161,18 @@ class TestTabs(unittest.TestCase): # Tests @pytest.mark.gui - def test_001_gui_loaded(self): - """Test that the GUI actually is shown""" - self.assertTrue(self.gui.show) + def test_01_common_tests(self): + """Run all common tests""" + self.run_all_common_setup_tests() @pytest.mark.gui - def test_002_window_title_seen(self): - """Test that the window title is OnionShare""" - self.assertEqual(self.gui.windowTitle(), "OnionShare") - - @pytest.mark.gui - def test_003_settings_button_is_visible(self): - """Test that the settings button is visible""" - self.assertTrue(self.gui.settings_button.isVisible()) - - @pytest.mark.gui - def test_004_server_status_bar_is_visible(self): - """Test that the status bar is visible""" - self.assertTrue(self.gui.status_bar.isVisible()) - - @pytest.mark.gui - def test_005_starts_with_one_new_tab(self): + def test_02_starts_with_one_new_tab(self): """There should be one "New Tab" tab open""" self.assertEqual(self.gui.tabs.count(), 1) self.assertTrue(self.gui.tabs.widget(0).new_tab.isVisible()) @pytest.mark.gui - def test_006_new_tab_button_opens_new_tabs(self): + def test_03_new_tab_button_opens_new_tabs(self): """Clicking the "+" button should open new tabs""" self.assertEqual(self.gui.tabs.count(), 1) QtTest.QTest.mouseClick(self.gui.tabs.new_tab_button, QtCore.Qt.LeftButton) @@ -216,7 +181,7 @@ class TestTabs(unittest.TestCase): self.assertEqual(self.gui.tabs.count(), 4) @pytest.mark.gui - def test_007_close_tab_button_closes_tabs(self): + def test_04_close_tab_button_closes_tabs(self): """Clicking the "x" button should close tabs""" self.assertEqual(self.gui.tabs.count(), 4) QtTest.QTest.mouseClick( @@ -234,7 +199,7 @@ class TestTabs(unittest.TestCase): self.assertEqual(self.gui.tabs.count(), 1) @pytest.mark.gui - def test_008_closing_last_tab_opens_new_one(self): + def test_05_closing_last_tab_opens_new_one(self): """Closing the last tab should open a new tab""" self.assertEqual(self.gui.tabs.count(), 1) @@ -256,7 +221,7 @@ class TestTabs(unittest.TestCase): self.assertTrue(self.gui.tabs.widget(0).new_tab.isVisible()) @pytest.mark.gui - def test_009_new_tab_mode_buttons_show_correct_modes(self): + def test_06_new_tab_mode_buttons_show_correct_modes(self): """Clicking the mode buttons in a new tab should change the mode of the tab""" # New tab, share files @@ -302,43 +267,43 @@ class TestTabs(unittest.TestCase): ) @pytest.mark.gui - def test_010_close_share_tab_while_server_started_should_warn(self): + def test_07_close_share_tab_while_server_started_should_warn(self): """Closing a share mode tab when the server is running should throw a warning""" tab = self.new_share_tab() self.close_tab_with_active_server(tab) @pytest.mark.gui - def test_011_close_receive_tab_while_server_started_should_warn(self): + def test_08_close_receive_tab_while_server_started_should_warn(self): """Closing a recieve mode tab when the server is running should throw a warning""" tab = self.new_receive_tab() self.close_tab_with_active_server(tab) @pytest.mark.gui - def test_012_close_website_tab_while_server_started_should_warn(self): + def test_09_close_website_tab_while_server_started_should_warn(self): """Closing a website mode tab when the server is running should throw a warning""" tab = self.new_website_tab() self.close_tab_with_active_server(tab) @pytest.mark.gui - def test_013_close_persistent_share_tab_shows_warning(self): + def test_10_close_persistent_share_tab_shows_warning(self): """Closing a share mode tab that's persistent should show a warning""" tab = self.new_share_tab() self.close_persistent_tab(tab) @pytest.mark.gui - def test_014_close_persistent_receive_tab_shows_warning(self): + def test_11_close_persistent_receive_tab_shows_warning(self): """Closing a receive mode tab that's persistent should show a warning""" tab = self.new_receive_tab() self.close_persistent_tab(tab) @pytest.mark.gui - def test_015_close_persistent_website_tab_shows_warning(self): + def test_12_close_persistent_website_tab_shows_warning(self): """Closing a website mode tab that's persistent should show a warning""" tab = self.new_website_tab() self.close_persistent_tab(tab) @pytest.mark.gui - def test_016_quit_with_server_started_should_warn(self): + def test_13_quit_with_server_started_should_warn(self): """Quitting OnionShare with any active servers should show a warning""" tab = self.new_share_tab() From eee1fa4e801714ea761832e03aab4d4ca07c04d8 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 16:20:38 +0800 Subject: [PATCH 058/135] Start moving over share tests --- tests2/gui_base_test.py | 36 +--- tests2/test_gui_share.py | 388 +++++++++++++++++++++++++++++++++++++++ tests2/test_gui_tabs.py | 5 + 3 files changed, 395 insertions(+), 34 deletions(-) create mode 100644 tests2/test_gui_share.py diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index bb44627f..66f5e19d 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -43,6 +43,7 @@ class GuiBaseTest(unittest.TestCase): @classmethod def tearDownClass(cls): + cls.gui.close() cls.gui.cleanup() # Shared test methods @@ -304,39 +305,6 @@ class GuiBaseTest(unittest.TestCase): # We should have timed out now self.assertEqual(mode.server_status.status, 0) - # Auto-start timer tests - def set_autostart_timer(self, mode, timer): - """Test that the timer can be set""" - schedule = QtCore.QDateTime.currentDateTime().addSecs(timer) - mode.server_status.autostart_timer_widget.setDateTime(schedule) - self.assertTrue(mode.server_status.autostart_timer_widget.dateTime(), schedule) - - def autostart_timer_widget_hidden(self, mode): - """Test that the auto-start timer widget is hidden when share has started""" - self.assertFalse(mode.server_status.autostart_timer_container.isVisible()) - - def scheduled_service_started(self, mode, wait): - """Test that the server has timed out after the timer ran out""" - QtTest.QTest.qWait(wait) - # We should have started now - self.assertEqual(mode.server_status.status, 2) - - def cancel_the_share(self, mode): - """Test that we can cancel a share before it's started up """ - self.server_working_on_start_button_pressed(mode) - self.server_status_indicator_says_scheduled(mode) - self.add_delete_buttons_hidden() - self.settings_button_is_hidden() - self.set_autostart_timer(mode, 10) - QtTest.QTest.mousePress(mode.server_status.server_button, QtCore.Qt.LeftButton) - QtTest.QTest.qWait(2000) - QtTest.QTest.mouseRelease( - mode.server_status.server_button, QtCore.Qt.LeftButton - ) - self.assertEqual(mode.server_status.status, 0) - self.server_is_stopped(mode, False) - self.web_server_is_stopped() - # Hack to close an Alert dialog that would otherwise block tests def accept_dialog(self): window = self.gui.qtapp.activeWindow() @@ -344,7 +312,7 @@ class GuiBaseTest(unittest.TestCase): window.close() # Grouped tests follow from here - + def run_all_common_setup_tests(self): self.gui_loaded() self.window_title_seen() diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py new file mode 100644 index 00000000..a6bd9e3b --- /dev/null +++ b/tests2/test_gui_share.py @@ -0,0 +1,388 @@ +import pytest +import unittest + +import json +import os +import requests +import shutil +import base64 +import tempfile +import secrets + +from PyQt5 import QtCore, QtTest, QtWidgets + +from onionshare import strings +from onionshare.common import Common +from onionshare.settings import Settings +from onionshare.onion import Onion +from onionshare.web import Web +from onionshare_gui import Application, MainWindow, GuiCommon + +from .gui_base_test import GuiBaseTest + + +class TestShare(GuiBaseTest): + # Shared test methods + + # Persistence tests + def have_same_password(self, password): + """Test that we have the same password""" + self.assertEqual(self.gui.share_mode.server_status.web.password, password) + + # Share-specific tests + + def file_selection_widget_has_files(self, num=2): + """Test that the number of items in the list is as expected""" + self.assertEqual( + self.gui.share_mode.server_status.file_selection.get_num_files(), num + ) + + def deleting_all_files_hides_delete_button(self): + """Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button""" + rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect( + self.gui.share_mode.server_status.file_selection.file_list.item(0) + ) + QtTest.QTest.mouseClick( + self.gui.share_mode.server_status.file_selection.file_list.viewport(), + QtCore.Qt.LeftButton, + pos=rect.center(), + ) + # Delete button should be visible + self.assertTrue( + self.gui.share_mode.server_status.file_selection.delete_button.isVisible() + ) + # Click delete, delete button should still be visible since we have one more file + QtTest.QTest.mouseClick( + self.gui.share_mode.server_status.file_selection.delete_button, + QtCore.Qt.LeftButton, + ) + + rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect( + self.gui.share_mode.server_status.file_selection.file_list.item(0) + ) + QtTest.QTest.mouseClick( + self.gui.share_mode.server_status.file_selection.file_list.viewport(), + QtCore.Qt.LeftButton, + pos=rect.center(), + ) + self.assertTrue( + self.gui.share_mode.server_status.file_selection.delete_button.isVisible() + ) + QtTest.QTest.mouseClick( + self.gui.share_mode.server_status.file_selection.delete_button, + QtCore.Qt.LeftButton, + ) + + # No more files, the delete button should be hidden + self.assertFalse( + self.gui.share_mode.server_status.file_selection.delete_button.isVisible() + ) + + def add_a_file_and_delete_using_its_delete_widget(self): + """Test that we can also delete a file by clicking on its [X] widget""" + self.gui.share_mode.server_status.file_selection.file_list.add_file( + "/etc/hosts" + ) + QtTest.QTest.mouseClick( + self.gui.share_mode.server_status.file_selection.file_list.item( + 0 + ).item_button, + QtCore.Qt.LeftButton, + ) + self.file_selection_widget_has_files(0) + + def file_selection_widget_read_files(self): + """Re-add some files to the list so we can share""" + self.gui.share_mode.server_status.file_selection.file_list.add_file( + "/etc/hosts" + ) + self.gui.share_mode.server_status.file_selection.file_list.add_file( + "/tmp/test.txt" + ) + self.file_selection_widget_has_files(2) + + def add_large_file(self): + """Add a large file to the share""" + size = 1024 * 1024 * 155 + with open("/tmp/large_file", "wb") as fout: + fout.write(os.urandom(size)) + self.gui.share_mode.server_status.file_selection.file_list.add_file( + "/tmp/large_file" + ) + + def add_delete_buttons_hidden(self): + """Test that the add and delete buttons are hidden when the server starts""" + self.assertFalse( + self.gui.share_mode.server_status.file_selection.add_button.isVisible() + ) + self.assertFalse( + self.gui.share_mode.server_status.file_selection.delete_button.isVisible() + ) + + def download_share(self, public_mode): + """Test that we can download the share""" + url = f"http://127.0.0.1:{self.gui.app.port}/download" + if public_mode: + r = requests.get(url) + else: + r = requests.get( + url, + auth=requests.auth.HTTPBasicAuth( + "onionshare", self.gui.share_mode.server_status.web.password + ), + ) + + tmp_file = tempfile.NamedTemporaryFile() + with open(tmp_file.name, "wb") as f: + f.write(r.content) + + zip = zipfile.ZipFile(tmp_file.name) + QtTest.QTest.qWait(2000) + self.assertEqual("onionshare", zip.read("test.txt").decode("utf-8")) + + def individual_file_is_viewable_or_not(self, public_mode, stay_open): + """Test whether an individual file is viewable (when in stay_open mode) and that it isn't (when not in stay_open mode)""" + url = f"http://127.0.0.1:{self.gui.app.port}" + download_file_url = f"http://127.0.0.1:{self.gui.app.port}/test.txt" + if public_mode: + r = requests.get(url) + else: + r = requests.get( + url, + auth=requests.auth.HTTPBasicAuth( + "onionshare", self.gui.share_mode.server_status.web.password + ), + ) + + if stay_open: + self.assertTrue('a href="test.txt"' in r.text) + + if public_mode: + r = requests.get(download_file_url) + else: + r = requests.get( + download_file_url, + auth=requests.auth.HTTPBasicAuth( + "onionshare", self.gui.share_mode.server_status.web.password + ), + ) + + tmp_file = tempfile.NamedTemporaryFile() + with open(tmp_file.name, "wb") as f: + f.write(r.content) + + with open(tmp_file.name, "r") as f: + self.assertEqual("onionshare", f.read()) + else: + self.assertFalse('a href="/test.txt"' in r.text) + if public_mode: + r = requests.get(download_file_url) + else: + r = requests.get( + download_file_url, + auth=requests.auth.HTTPBasicAuth( + "onionshare", self.gui.share_mode.server_status.web.password + ), + ) + self.assertEqual(r.status_code, 404) + self.download_share(public_mode) + + QtTest.QTest.qWait(2000) + + def hit_401(self, public_mode): + """Test that the server stops after too many 401s, or doesn't when in public_mode""" + url = f"http://127.0.0.1:{self.gui.app.port}/" + + for _ in range(20): + password_guess = self.gui.common.build_password() + r = requests.get( + url, auth=requests.auth.HTTPBasicAuth("onionshare", password_guess) + ) + + # A nasty hack to avoid the Alert dialog that blocks the rest of the test + if not public_mode: + QtCore.QTimer.singleShot(1000, self.accept_dialog) + + # In public mode, we should still be running (no rate-limiting) + if public_mode: + self.web_server_is_running() + # In non-public mode, we should be shut down (rate-limiting) + else: + self.web_server_is_stopped() + + # Grouped tests follow from here + + def run_all_share_mode_setup_tests(self): + """Tests in share mode prior to starting a share""" + self.click_mode(self.gui.share_mode) + self.file_selection_widget_has_files() + self.history_is_not_visible(self.gui.share_mode) + self.click_toggle_history(self.gui.share_mode) + self.history_is_visible(self.gui.share_mode) + self.deleting_all_files_hides_delete_button() + self.add_a_file_and_delete_using_its_delete_widget() + self.file_selection_widget_read_files() + + def run_all_share_mode_started_tests(self, public_mode, startup_time=2000): + """Tests in share mode after starting a share""" + self.server_working_on_start_button_pressed(self.gui.share_mode) + self.server_status_indicator_says_starting(self.gui.share_mode) + self.add_delete_buttons_hidden() + self.settings_button_is_hidden() + self.server_is_started(self.gui.share_mode, startup_time) + self.web_server_is_running() + self.have_a_password(self.gui.share_mode, public_mode) + self.url_description_shown(self.gui.share_mode) + self.have_copy_url_button(self.gui.share_mode, public_mode) + self.server_status_indicator_says_started(self.gui.share_mode) + + def run_all_share_mode_download_tests(self, public_mode, stay_open): + """Tests in share mode after downloading a share""" + self.web_page(self.gui.share_mode, "Total size", public_mode) + self.download_share(public_mode) + self.history_widgets_present(self.gui.share_mode) + self.server_is_stopped(self.gui.share_mode, stay_open) + self.web_server_is_stopped() + self.server_status_indicator_says_closed(self.gui.share_mode, stay_open) + self.add_button_visible(self.gui.share_mode) + self.server_working_on_start_button_pressed(self.gui.share_mode) + self.toggle_indicator_is_reset(self.gui.share_mode) + self.server_is_started(self.gui.share_mode) + self.history_indicator(self.gui.share_mode, public_mode) + + def run_all_share_mode_individual_file_download_tests(self, public_mode, stay_open): + """Tests in share mode after downloading a share""" + self.web_page(self.gui.share_mode, "Total size", public_mode) + self.individual_file_is_viewable_or_not(public_mode, stay_open) + self.history_widgets_present(self.gui.share_mode) + self.server_is_stopped(self.gui.share_mode, stay_open) + self.web_server_is_stopped() + self.server_status_indicator_says_closed(self.gui.share_mode, stay_open) + self.add_button_visible(self.gui.share_mode) + self.server_working_on_start_button_pressed(self.gui.share_mode) + self.server_is_started(self.gui.share_mode) + self.history_indicator(self.gui.share_mode, public_mode) + + def run_all_share_mode_tests(self, public_mode, stay_open): + """End-to-end share tests""" + self.run_all_share_mode_setup_tests() + self.run_all_share_mode_started_tests(public_mode) + self.run_all_share_mode_download_tests(public_mode, stay_open) + + def run_all_clear_all_button_tests(self, public_mode, stay_open): + """Test the Clear All history button""" + self.run_all_share_mode_setup_tests() + self.run_all_share_mode_started_tests(public_mode) + self.individual_file_is_viewable_or_not(public_mode, stay_open) + self.history_widgets_present(self.gui.share_mode) + self.clear_all_history_items(self.gui.share_mode, 0) + self.individual_file_is_viewable_or_not(public_mode, stay_open) + self.clear_all_history_items(self.gui.share_mode, 2) + + def run_all_share_mode_individual_file_tests(self, public_mode, stay_open): + """Tests in share mode when viewing an individual file""" + self.run_all_share_mode_setup_tests() + self.run_all_share_mode_started_tests(public_mode) + self.run_all_share_mode_individual_file_download_tests(public_mode, stay_open) + + def run_all_large_file_tests(self, public_mode, stay_open): + """Same as above but with a larger file""" + self.run_all_share_mode_setup_tests() + self.add_large_file() + self.run_all_share_mode_started_tests(public_mode, startup_time=15000) + self.assertTrue(self.gui.share_mode.filesize_warning.isVisible()) + self.server_is_stopped(self.gui.share_mode, stay_open) + self.web_server_is_stopped() + self.server_status_indicator_says_closed(self.gui.share_mode, stay_open) + + def run_all_share_mode_persistent_tests(self, public_mode, stay_open): + """Same as end-to-end share tests but also test the password is the same on multiple shared""" + self.run_all_share_mode_setup_tests() + self.run_all_share_mode_started_tests(public_mode) + password = self.gui.share_mode.server_status.web.password + self.run_all_share_mode_download_tests(public_mode, stay_open) + self.have_same_password(password) + + def run_all_share_mode_timer_tests(self, public_mode): + """Auto-stop timer tests in share mode""" + self.run_all_share_mode_setup_tests() + self.set_timeout(self.gui.share_mode, 5) + self.run_all_share_mode_started_tests(public_mode) + self.autostop_timer_widget_hidden(self.gui.share_mode) + self.server_timed_out(self.gui.share_mode, 10000) + self.web_server_is_stopped() + + def run_all_share_mode_autostart_timer_tests(self, public_mode): + """Auto-start timer tests in share mode""" + self.run_all_share_mode_setup_tests() + self.set_autostart_timer(self.gui.share_mode, 5) + self.server_working_on_start_button_pressed(self.gui.share_mode) + self.autostart_timer_widget_hidden(self.gui.share_mode) + self.server_status_indicator_says_scheduled(self.gui.share_mode) + self.web_server_is_stopped() + self.scheduled_service_started(self.gui.share_mode, 7000) + self.web_server_is_running() + + def run_all_share_mode_autostop_autostart_mismatch_tests(self, public_mode): + """Auto-stop timer tests in share mode""" + self.run_all_share_mode_setup_tests() + self.set_autostart_timer(self.gui.share_mode, 15) + self.set_timeout(self.gui.share_mode, 5) + QtCore.QTimer.singleShot(4000, self.accept_dialog) + QtTest.QTest.mouseClick( + self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton + ) + self.server_is_stopped(self.gui.share_mode, False) + + def run_all_share_mode_unreadable_file_tests(self): + """Attempt to share an unreadable file""" + self.run_all_share_mode_setup_tests() + QtCore.QTimer.singleShot(1000, self.accept_dialog) + self.gui.share_mode.server_status.file_selection.file_list.add_file( + "/tmp/nonexistent.txt" + ) + self.file_selection_widget_has_files(2) + + # Auto-start timer tests + def set_autostart_timer(self, mode, timer): + """Test that the timer can be set""" + schedule = QtCore.QDateTime.currentDateTime().addSecs(timer) + mode.server_status.autostart_timer_widget.setDateTime(schedule) + self.assertTrue(mode.server_status.autostart_timer_widget.dateTime(), schedule) + + def autostart_timer_widget_hidden(self, mode): + """Test that the auto-start timer widget is hidden when share has started""" + self.assertFalse(mode.server_status.autostart_timer_container.isVisible()) + + def scheduled_service_started(self, mode, wait): + """Test that the server has timed out after the timer ran out""" + QtTest.QTest.qWait(wait) + # We should have started now + self.assertEqual(mode.server_status.status, 2) + + def cancel_the_share(self, mode): + """Test that we can cancel a share before it's started up """ + self.server_working_on_start_button_pressed(mode) + self.server_status_indicator_says_scheduled(mode) + self.add_delete_buttons_hidden() + self.settings_button_is_hidden() + self.set_autostart_timer(mode, 10) + QtTest.QTest.mousePress(mode.server_status.server_button, QtCore.Qt.LeftButton) + QtTest.QTest.qWait(2000) + QtTest.QTest.mouseRelease( + mode.server_status.server_button, QtCore.Qt.LeftButton + ) + self.assertEqual(mode.server_status.status, 0) + self.server_is_stopped(mode, False) + self.web_server_is_stopped() + + # Tests + + @pytest.mark.gui + def test_common_tests(self): + """Run all common tests""" + self.run_all_common_setup_tests() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests2/test_gui_tabs.py b/tests2/test_gui_tabs.py index 7b03d0fc..0fd3feab 100644 --- a/tests2/test_gui_tabs.py +++ b/tests2/test_gui_tabs.py @@ -334,6 +334,11 @@ class TestTabs(GuiBaseTest): # The window should still be open self.assertTrue(self.gui.isVisible()) + # Stop the server + QtTest.QTest.mouseClick( + tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton + ) + if __name__ == "__main__": unittest.main() From e18d2c3ea278f9e6208423a3ba2295ca9178355f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 16:44:21 +0800 Subject: [PATCH 059/135] Add CLI tests in --- onionshare/common.py | 33 ++-- tests2/gui_base_test.py | 2 +- tests2/test_cli.py | 75 +++++++++ tests2/test_cli_common.py | 312 ++++++++++++++++++++++++++++++++++++ tests2/test_cli_settings.py | 158 ++++++++++++++++++ tests2/test_cli_strings.py | 65 ++++++++ tests2/test_cli_web.py | 241 ++++++++++++++++++++++++++++ 7 files changed, 869 insertions(+), 17 deletions(-) create mode 100644 tests2/test_cli.py create mode 100644 tests2/test_cli_common.py create mode 100644 tests2/test_cli_settings.py create mode 100644 tests2/test_cli_strings.py create mode 100644 tests2/test_cli_web.py diff --git a/onionshare/common.py b/onionshare/common.py index cf713818..e85403eb 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -159,23 +159,24 @@ class Common: """ Returns the path of the OnionShare data directory. """ - if getattr(sys, "onionshare_test_mode", False): - onionshare_data_dir = os.path.expanduser("~/.config/onionshare-testdata") - else: - if self.platform == "Windows": - try: - appdata = os.environ["APPDATA"] - onionshare_data_dir = f"{appdata}\\OnionShare" - except: - # If for some reason we don't have the 'APPDATA' environment variable - # (like running tests in Linux while pretending to be in Windows) - onionshare_data_dir = os.path.expanduser("~/.config/onionshare") - elif self.platform == "Darwin": - onionshare_data_dir = os.path.expanduser( - "~/Library/Application Support/OnionShare" - ) - else: + if self.platform == "Windows": + try: + appdata = os.environ["APPDATA"] + onionshare_data_dir = f"{appdata}\\OnionShare" + except: + # If for some reason we don't have the 'APPDATA' environment variable + # (like running tests in Linux while pretending to be in Windows) onionshare_data_dir = os.path.expanduser("~/.config/onionshare") + elif self.platform == "Darwin": + onionshare_data_dir = os.path.expanduser( + "~/Library/Application Support/OnionShare" + ) + else: + onionshare_data_dir = os.path.expanduser("~/.config/onionshare") + + # Modify the data dir if running tests + if getattr(sys, "onionshare_test_mode", False): + onionshare_data_dir += "-testdata" os.makedirs(onionshare_data_dir, 0o700, True) return onionshare_data_dir diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index 66f5e19d..2cd9c7b8 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -26,7 +26,7 @@ from onionshare_gui.tab.mode.website_mode import WebsiteMode class GuiBaseTest(unittest.TestCase): @classmethod def setUpClass(cls): - common = Common() + common = Common(verbose=True) qtapp = Application(common) common.gui = GuiCommon(common, qtapp, local_only=True) cls.gui = MainWindow(common, filenames=None) diff --git a/tests2/test_cli.py b/tests2/test_cli.py new file mode 100644 index 00000000..0addf6d5 --- /dev/null +++ b/tests2/test_cli.py @@ -0,0 +1,75 @@ +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +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 . +""" + +import os + +import pytest + +from onionshare import OnionShare +from onionshare.common import Common +from onionshare.mode_settings import ModeSettings + + +class MyOnion: + def __init__(self): + self.auth_string = "TestHidServAuth" + self.private_key = "" + self.scheduled_key = None + + @staticmethod + def start_onion_service(self, await_publication=True, save_scheduled_key=False): + return "test_service_id.onion" + + +@pytest.fixture +def onionshare_obj(): + common = Common() + return OnionShare(common, MyOnion()) + + +@pytest.fixture +def mode_settings_obj(): + common = Common() + return ModeSettings(common) + + +class TestOnionShare: + def test_init(self, onionshare_obj): + assert onionshare_obj.hidserv_dir is None + assert onionshare_obj.onion_host is None + assert onionshare_obj.cleanup_filenames == [] + assert onionshare_obj.local_only is False + + def test_start_onion_service(self, onionshare_obj, mode_settings_obj): + onionshare_obj.start_onion_service(mode_settings_obj) + assert 17600 <= onionshare_obj.port <= 17650 + assert onionshare_obj.onion_host == "test_service_id.onion" + + def test_start_onion_service_local_only(self, onionshare_obj, mode_settings_obj): + onionshare_obj.local_only = True + onionshare_obj.start_onion_service(mode_settings_obj) + assert onionshare_obj.onion_host == "127.0.0.1:{}".format(onionshare_obj.port) + + def test_cleanup(self, onionshare_obj, temp_dir_1024, temp_file_1024): + onionshare_obj.cleanup_filenames = [temp_dir_1024, temp_file_1024] + onionshare_obj.cleanup() + + assert os.path.exists(temp_dir_1024) is False + assert os.path.exists(temp_dir_1024) is False + assert onionshare_obj.cleanup_filenames == [] diff --git a/tests2/test_cli_common.py b/tests2/test_cli_common.py new file mode 100644 index 00000000..1f230295 --- /dev/null +++ b/tests2/test_cli_common.py @@ -0,0 +1,312 @@ +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +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 . +""" + +import contextlib +import inspect +import io +import os +import random +import re +import socket +import sys +import zipfile + +import pytest + +LOG_MSG_REGEX = re.compile( + r""" + ^\[Jun\ 06\ 2013\ 11:05:00\] + \ TestModule\.\.dummy_func + \ at\ 0x[a-f0-9]+>(:\ TEST_MSG)?$""", + re.VERBOSE, +) +PASSWORD_REGEX = re.compile(r"^([a-z]+)(-[a-z]+)?-([a-z]+)(-[a-z]+)?$") + + +# TODO: Improve the Common tests to test it all as a single class + + +class TestBuildPassword: + @pytest.mark.parametrize( + "test_input,expected", + ( + # VALID, two lowercase words, separated by a hyphen + ("syrup-enzyme", True), + ("caution-friday", True), + # VALID, two lowercase words, with one hyphenated compound word + ("drop-down-thimble", True), + ("unmixed-yo-yo", True), + # VALID, two lowercase hyphenated compound words, separated by hyphen + ("yo-yo-drop-down", True), + ("felt-tip-t-shirt", True), + ("hello-world", True), + # INVALID + ("Upper-Case", False), + ("digits-123", False), + ("too-many-hyphens-", False), + ("symbols-!@#$%", False), + ), + ) + def test_build_password_regex(self, test_input, expected): + """ Test that `PASSWORD_REGEX` accounts for the following patterns + + There are a few hyphenated words in `wordlist.txt`: + * drop-down + * felt-tip + * t-shirt + * yo-yo + + These words cause a few extra potential password patterns: + * word-word + * hyphenated-word-word + * word-hyphenated-word + * hyphenated-word-hyphenated-word + """ + + assert bool(PASSWORD_REGEX.match(test_input)) == expected + + def test_build_password_unique(self, common_obj, sys_onionshare_dev_mode): + assert common_obj.build_password() != common_obj.build_password() + + +class TestDirSize: + def test_temp_dir_size(self, common_obj, temp_dir_1024_delete): + """ dir_size() should return the total size (in bytes) of all files + in a particular directory. + """ + + assert common_obj.dir_size(temp_dir_1024_delete) == 1024 + + +class TestEstimatedTimeRemaining: + @pytest.mark.parametrize( + "test_input,expected", + ( + ((2, 676, 12), "8h14m16s"), + ((14, 1049, 30), "1h26m15s"), + ((21, 450, 1), "33m42s"), + ((31, 1115, 80), "11m39s"), + ((336, 989, 32), "2m12s"), + ((603, 949, 38), "36s"), + ((971, 1009, 83), "1s"), + ), + ) + def test_estimated_time_remaining( + self, common_obj, test_input, expected, time_time_100 + ): + assert common_obj.estimated_time_remaining(*test_input) == expected + + @pytest.mark.parametrize( + "test_input", + ( + (10, 20, 100), # if `time_elapsed == 0` + (0, 37, 99), # if `download_rate == 0` + ), + ) + def test_raises_zero_division_error(self, common_obj, test_input, time_time_100): + with pytest.raises(ZeroDivisionError): + common_obj.estimated_time_remaining(*test_input) + + +class TestFormatSeconds: + @pytest.mark.parametrize( + "test_input,expected", + ( + (0, "0s"), + (26, "26s"), + (60, "1m"), + (947.35, "15m47s"), + (1847, "30m47s"), + (2193.94, "36m34s"), + (3600, "1h"), + (13426.83, "3h43m47s"), + (16293, "4h31m33s"), + (18392.14, "5h6m32s"), + (86400, "1d"), + (129674, "1d12h1m14s"), + (56404.12, "15h40m4s"), + ), + ) + def test_format_seconds(self, common_obj, test_input, expected): + assert common_obj.format_seconds(test_input) == expected + + # TODO: test negative numbers? + @pytest.mark.parametrize("test_input", ("string", lambda: None, [], {}, set())) + def test_invalid_input_types(self, common_obj, test_input): + with pytest.raises(TypeError): + common_obj.format_seconds(test_input) + + +class TestGetAvailablePort: + @pytest.mark.parametrize( + "port_min,port_max", + ((random.randint(1024, 1500), random.randint(1800, 2048)) for _ in range(50)), + ) + def test_returns_an_open_port(self, common_obj, port_min, port_max): + """ get_available_port() should return an open port within the range """ + + port = common_obj.get_available_port(port_min, port_max) + assert port_min <= port <= port_max + with socket.socket() as tmpsock: + tmpsock.bind(("127.0.0.1", port)) + + +class TestGetPlatform: + def test_darwin(self, platform_darwin, common_obj): + assert common_obj.platform == "Darwin" + + def test_linux(self, platform_linux, common_obj): + assert common_obj.platform == "Linux" + + def test_windows(self, platform_windows, common_obj): + assert common_obj.platform == "Windows" + + +# TODO: double-check these tests +class TestGetResourcePath: + def test_onionshare_dev_mode(self, common_obj, sys_onionshare_dev_mode): + prefix = os.path.join( + os.path.dirname( + os.path.dirname( + os.path.abspath(inspect.getfile(inspect.currentframe())) + ) + ), + "share", + ) + assert common_obj.get_resource_path( + os.path.join(prefix, "test_filename") + ) == os.path.join(prefix, "test_filename") + + def test_linux(self, common_obj, platform_linux, sys_argv_sys_prefix): + prefix = os.path.join(sys.prefix, "share/onionshare") + assert common_obj.get_resource_path( + os.path.join(prefix, "test_filename") + ) == os.path.join(prefix, "test_filename") + + def test_frozen_darwin(self, common_obj, platform_darwin, sys_frozen, sys_meipass): + prefix = os.path.join(sys._MEIPASS, "share") + assert common_obj.get_resource_path( + os.path.join(prefix, "test_filename") + ) == os.path.join(prefix, "test_filename") + + +class TestGetTorPaths: + # @pytest.mark.skipif(sys.platform != 'Darwin', reason='requires MacOS') ? + def test_get_tor_paths_darwin( + self, platform_darwin, common_obj, sys_frozen, sys_meipass + ): + base_path = os.path.dirname( + os.path.dirname(os.path.dirname(common_obj.get_resource_path(""))) + ) + tor_path = os.path.join(base_path, "Resources", "Tor", "tor") + tor_geo_ip_file_path = os.path.join(base_path, "Resources", "Tor", "geoip") + tor_geo_ipv6_file_path = os.path.join(base_path, "Resources", "Tor", "geoip6") + obfs4proxy_file_path = os.path.join(base_path, "Resources", "Tor", "obfs4proxy") + assert common_obj.get_tor_paths() == ( + tor_path, + tor_geo_ip_file_path, + tor_geo_ipv6_file_path, + obfs4proxy_file_path, + ) + + # @pytest.mark.skipif(sys.platform != 'Linux', reason='requires Linux') ? + def test_get_tor_paths_linux(self, platform_linux, common_obj): + assert common_obj.get_tor_paths() == ( + "/usr/bin/tor", + "/usr/share/tor/geoip", + "/usr/share/tor/geoip6", + "/usr/bin/obfs4proxy", + ) + + # @pytest.mark.skipif(sys.platform != 'Windows', reason='requires Windows') ? + def test_get_tor_paths_windows(self, platform_windows, common_obj, sys_frozen): + base_path = os.path.join( + os.path.dirname(os.path.dirname(common_obj.get_resource_path(""))), "tor" + ) + tor_path = os.path.join(os.path.join(base_path, "Tor"), "tor.exe") + obfs4proxy_file_path = os.path.join( + os.path.join(base_path, "Tor"), "obfs4proxy.exe" + ) + tor_geo_ip_file_path = os.path.join( + os.path.join(os.path.join(base_path, "Data"), "Tor"), "geoip" + ) + tor_geo_ipv6_file_path = os.path.join( + os.path.join(os.path.join(base_path, "Data"), "Tor"), "geoip6" + ) + assert common_obj.get_tor_paths() == ( + tor_path, + tor_geo_ip_file_path, + tor_geo_ipv6_file_path, + obfs4proxy_file_path, + ) + + +class TestHumanReadableFilesize: + @pytest.mark.parametrize( + "test_input,expected", + ( + (1024 ** 0, "1.0 B"), + (1024 ** 1, "1.0 KiB"), + (1024 ** 2, "1.0 MiB"), + (1024 ** 3, "1.0 GiB"), + (1024 ** 4, "1.0 TiB"), + (1024 ** 5, "1.0 PiB"), + (1024 ** 6, "1.0 EiB"), + (1024 ** 7, "1.0 ZiB"), + (1024 ** 8, "1.0 YiB"), + ), + ) + def test_human_readable_filesize(self, common_obj, test_input, expected): + assert common_obj.human_readable_filesize(test_input) == expected + + +class TestLog: + @pytest.mark.parametrize( + "test_input", + ( + ( + "[Jun 06 2013 11:05:00]" + " TestModule..dummy_func" + " at 0xdeadbeef>" + ), + ( + "[Jun 06 2013 11:05:00]" + " TestModule..dummy_func" + " at 0xdeadbeef>: TEST_MSG" + ), + ), + ) + def test_log_msg_regex(self, test_input): + assert bool(LOG_MSG_REGEX.match(test_input)) + + def test_output(self, common_obj, time_strftime): + def dummy_func(): + pass + + common_obj.verbose = True + + # From: https://stackoverflow.com/questions/1218933 + with io.StringIO() as buf, contextlib.redirect_stdout(buf): + common_obj.log("TestModule", dummy_func) + common_obj.log("TestModule", dummy_func, "TEST_MSG") + output = buf.getvalue() + + line_one, line_two, _ = output.split("\n") + assert LOG_MSG_REGEX.match(line_one) + assert LOG_MSG_REGEX.match(line_two) diff --git a/tests2/test_cli_settings.py b/tests2/test_cli_settings.py new file mode 100644 index 00000000..14269490 --- /dev/null +++ b/tests2/test_cli_settings.py @@ -0,0 +1,158 @@ +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +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 . +""" + +import json +import os +import tempfile + +import pytest + +from onionshare import common, settings, strings + + +@pytest.fixture +def settings_obj(sys_onionshare_dev_mode, platform_linux): + _common = common.Common() + _common.version = "DUMMY_VERSION_1.2.3" + return settings.Settings(_common) + + +class TestSettings: + def test_init(self, settings_obj): + expected_settings = { + "version": "DUMMY_VERSION_1.2.3", + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "auth_type": "no_auth", + "auth_password": "", + "use_autoupdate": True, + "autoupdate_timestamp": None, + "no_bridges": True, + "tor_bridges_use_obfs4": False, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_custom_bridges": "", + "persistent_tabs": [], + } + for key in settings_obj._settings: + # Skip locale, it will not always default to the same thing + if key != "locale": + assert settings_obj._settings[key] == settings_obj.default_settings[key] + assert settings_obj._settings[key] == expected_settings[key] + + def test_fill_in_defaults(self, settings_obj): + del settings_obj._settings["version"] + settings_obj.fill_in_defaults() + assert settings_obj._settings["version"] == "DUMMY_VERSION_1.2.3" + + def test_load(self, settings_obj): + custom_settings = { + "version": "CUSTOM_VERSION", + "socks_port": 9999, + "use_stealth": True, + } + tmp_file, tmp_file_path = tempfile.mkstemp() + with open(tmp_file, "w") as f: + json.dump(custom_settings, f) + settings_obj.filename = tmp_file_path + settings_obj.load() + + assert settings_obj._settings["version"] == "CUSTOM_VERSION" + assert settings_obj._settings["socks_port"] == 9999 + assert settings_obj._settings["use_stealth"] is True + + os.remove(tmp_file_path) + assert os.path.exists(tmp_file_path) is False + + def test_save(self, monkeypatch, settings_obj): + monkeypatch.setattr(strings, "_", lambda _: "") + + settings_filename = "default_settings.json" + tmp_dir = tempfile.gettempdir() + settings_path = os.path.join(tmp_dir, settings_filename) + settings_obj.filename = settings_path + settings_obj.save() + with open(settings_path, "r") as f: + settings = json.load(f) + + assert settings_obj._settings == settings + + os.remove(settings_path) + assert os.path.exists(settings_path) is False + + def test_get(self, settings_obj): + assert settings_obj.get("version") == "DUMMY_VERSION_1.2.3" + assert settings_obj.get("connection_type") == "bundled" + assert settings_obj.get("control_port_address") == "127.0.0.1" + assert settings_obj.get("control_port_port") == 9051 + assert settings_obj.get("socks_address") == "127.0.0.1" + assert settings_obj.get("socks_port") == 9050 + assert settings_obj.get("socket_file_path") == "/var/run/tor/control" + assert settings_obj.get("auth_type") == "no_auth" + assert settings_obj.get("auth_password") == "" + assert settings_obj.get("use_autoupdate") is True + assert settings_obj.get("autoupdate_timestamp") is None + assert settings_obj.get("autoupdate_timestamp") is None + assert settings_obj.get("no_bridges") is True + assert settings_obj.get("tor_bridges_use_obfs4") is False + assert settings_obj.get("tor_bridges_use_meek_lite_azure") is False + assert settings_obj.get("tor_bridges_use_custom_bridges") == "" + + def test_set_version(self, settings_obj): + settings_obj.set("version", "CUSTOM_VERSION") + assert settings_obj._settings["version"] == "CUSTOM_VERSION" + + def test_set_control_port_port(self, settings_obj): + settings_obj.set("control_port_port", 999) + assert settings_obj._settings["control_port_port"] == 999 + + settings_obj.set("control_port_port", "NON_INTEGER") + assert settings_obj._settings["control_port_port"] == 9051 + + def test_set_socks_port(self, settings_obj): + settings_obj.set("socks_port", 888) + assert settings_obj._settings["socks_port"] == 888 + + settings_obj.set("socks_port", "NON_INTEGER") + assert settings_obj._settings["socks_port"] == 9050 + + def test_filename_darwin(self, monkeypatch, platform_darwin): + obj = settings.Settings(common.Common()) + assert obj.filename == os.path.expanduser( + "~/Library/Application Support/OnionShare-testdata/onionshare.json" + ) + + def test_filename_linux(self, monkeypatch, platform_linux): + obj = settings.Settings(common.Common()) + assert obj.filename == os.path.expanduser( + "~/.config/onionshare-testdata/onionshare.json" + ) + + def test_set_custom_bridge(self, settings_obj): + settings_obj.set( + "tor_bridges_use_custom_bridges", + "Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E", + ) + assert ( + settings_obj._settings["tor_bridges_use_custom_bridges"] + == "Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E" + ) diff --git a/tests2/test_cli_strings.py b/tests2/test_cli_strings.py new file mode 100644 index 00000000..7ad65191 --- /dev/null +++ b/tests2/test_cli_strings.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +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 . +""" + +import types + +import pytest + +from onionshare import strings +from onionshare.settings import Settings + +# # Stub get_resource_path so it finds the correct path while running tests +# def get_resource_path(filename): +# resources_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'share') +# path = os.path.join(resources_dir, filename) +# return path +# common.get_resource_path = get_resource_path + + +def test_underscore_is_function(): + assert callable(strings._) and isinstance(strings._, types.FunctionType) + + +class TestLoadStrings: + def test_load_strings_defaults_to_english( + self, common_obj, locale_en, sys_onionshare_dev_mode + ): + """ load_strings() loads English by default """ + common_obj.settings = Settings(common_obj) + strings.load_strings(common_obj) + assert strings._("preparing_files") == "Compressing files." + + def test_load_strings_loads_other_languages( + self, common_obj, locale_fr, sys_onionshare_dev_mode + ): + """ load_strings() loads other languages in different locales """ + common_obj.settings = Settings(common_obj) + common_obj.settings.set("locale", "fr") + strings.load_strings(common_obj) + assert strings._("preparing_files") == "Compression des fichiers." + + def test_load_invalid_locale( + self, common_obj, locale_invalid, sys_onionshare_dev_mode + ): + """ load_strings() raises a KeyError for an invalid locale """ + with pytest.raises(KeyError): + common_obj.settings = Settings(common_obj) + common_obj.settings.set("locale", "XX") + strings.load_strings(common_obj) diff --git a/tests2/test_cli_web.py b/tests2/test_cli_web.py new file mode 100644 index 00000000..2ce2f758 --- /dev/null +++ b/tests2/test_cli_web.py @@ -0,0 +1,241 @@ +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +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 . +""" + +import contextlib +import inspect +import io +import os +import random +import re +import socket +import sys +import zipfile +import tempfile +import base64 + +import pytest +from werkzeug.datastructures import Headers + +from onionshare.common import Common +from onionshare import strings +from onionshare.web import Web +from onionshare.settings import Settings +from onionshare.mode_settings import ModeSettings + +DEFAULT_ZW_FILENAME_REGEX = re.compile(r"^onionshare_[a-z2-7]{6}.zip$") +RANDOM_STR_REGEX = re.compile(r"^[a-z2-7]+$") + + +def web_obj(common_obj, mode, num_files=0): + """ Creates a Web object, in either share mode or receive mode, ready for testing """ + common_obj.settings = Settings(common_obj) + strings.load_strings(common_obj) + mode_settings = ModeSettings(common_obj) + web = Web(common_obj, False, mode_settings, mode) + web.generate_password() + web.running = True + + web.app.testing = True + + # Share mode + if mode == "share": + # Add files + files = [] + for _ in range(num_files): + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_file.write(b"*" * 1024) + files.append(tmp_file.name) + web.share_mode.set_file_info(files) + # Receive mode + else: + pass + + return web + + +class TestWeb: + def test_share_mode(self, common_obj): + web = web_obj(common_obj, "share", 3) + assert web.mode == "share" + with web.app.test_client() as c: + # Load / without auth + res = c.get("/") + res.get_data() + assert res.status_code == 401 + + # Load / with invalid auth + res = c.get("/", headers=self._make_auth_headers("invalid")) + res.get_data() + assert res.status_code == 401 + + # Load / with valid auth + res = c.get("/", headers=self._make_auth_headers(web.password)) + res.get_data() + assert res.status_code == 200 + + # Download + res = c.get("/download", headers=self._make_auth_headers(web.password)) + res.get_data() + assert res.status_code == 200 + assert res.mimetype == "application/zip" + + def test_share_mode_autostop_sharing_on(self, common_obj, temp_file_1024): + web = web_obj(common_obj, "share", 3) + web.settings.set("share", "autostop_sharing", True) + + assert web.running == True + + with web.app.test_client() as c: + # Download the first time + res = c.get("/download", headers=self._make_auth_headers(web.password)) + res.get_data() + assert res.status_code == 200 + assert res.mimetype == "application/zip" + + assert web.running == False + + def test_share_mode_autostop_sharing_off(self, common_obj, temp_file_1024): + web = web_obj(common_obj, "share", 3) + web.settings.set("share", "autostop_sharing", False) + + assert web.running == True + + with web.app.test_client() as c: + # Download the first time + res = c.get("/download", headers=self._make_auth_headers(web.password)) + res.get_data() + assert res.status_code == 200 + assert res.mimetype == "application/zip" + assert web.running == True + + def test_receive_mode(self, common_obj): + web = web_obj(common_obj, "receive") + assert web.mode == "receive" + + with web.app.test_client() as c: + # Load / without auth + res = c.get("/") + res.get_data() + assert res.status_code == 401 + + # Load / with invalid auth + res = c.get("/", headers=self._make_auth_headers("invalid")) + res.get_data() + assert res.status_code == 401 + + # Load / with valid auth + res = c.get("/", headers=self._make_auth_headers(web.password)) + res.get_data() + assert res.status_code == 200 + + def test_public_mode_on(self, common_obj): + web = web_obj(common_obj, "receive") + web.settings.set("general", "public", True) + + with web.app.test_client() as c: + # Loading / should work without auth + res = c.get("/") + data1 = res.get_data() + assert res.status_code == 200 + + def test_public_mode_off(self, common_obj): + web = web_obj(common_obj, "receive") + web.settings.set("general", "public", False) + + with web.app.test_client() as c: + # Load / without auth + res = c.get("/") + res.get_data() + assert res.status_code == 401 + + # But static resources should work without auth + res = c.get(f"{web.static_url_path}/css/style.css") + res.get_data() + assert res.status_code == 200 + + # Load / with valid auth + res = c.get("/", headers=self._make_auth_headers(web.password)) + res.get_data() + assert res.status_code == 200 + + def _make_auth_headers(self, password): + auth = base64.b64encode(b"onionshare:" + password.encode()).decode() + h = Headers() + h.add("Authorization", "Basic " + auth) + return h + + +class TestZipWriterDefault: + @pytest.mark.parametrize( + "test_input", + ( + f"onionshare_{''.join(random.choice('abcdefghijklmnopqrstuvwxyz234567') for _ in range(6))}.zip" + for _ in range(50) + ), + ) + def test_default_zw_filename_regex(self, test_input): + assert bool(DEFAULT_ZW_FILENAME_REGEX.match(test_input)) + + def test_zw_filename(self, default_zw): + zw_filename = os.path.basename(default_zw.zip_filename) + assert bool(DEFAULT_ZW_FILENAME_REGEX.match(zw_filename)) + + def test_zipfile_filename_matches_zipwriter_filename(self, default_zw): + assert default_zw.z.filename == default_zw.zip_filename + + def test_zipfile_allow_zip64(self, default_zw): + assert default_zw.z._allowZip64 is True + + def test_zipfile_mode(self, default_zw): + assert default_zw.z.mode == "w" + + def test_callback(self, default_zw): + assert default_zw.processed_size_callback(None) is None + + def test_add_file(self, default_zw, temp_file_1024_delete): + default_zw.add_file(temp_file_1024_delete) + zipfile_info = default_zw.z.getinfo(os.path.basename(temp_file_1024_delete)) + + assert zipfile_info.compress_type == zipfile.ZIP_DEFLATED + assert zipfile_info.file_size == 1024 + + def test_add_directory(self, temp_dir_1024_delete, default_zw): + previous_size = default_zw._size # size before adding directory + default_zw.add_dir(temp_dir_1024_delete) + assert default_zw._size == previous_size + 1024 + + +class TestZipWriterCustom: + @pytest.mark.parametrize( + "test_input", + ( + Common.random_string( + random.randint(2, 50), random.choice((None, random.randint(2, 50))) + ) + for _ in range(50) + ), + ) + def test_random_string_regex(self, test_input): + assert bool(RANDOM_STR_REGEX.match(test_input)) + + def test_custom_filename(self, custom_zw): + assert bool(RANDOM_STR_REGEX.match(custom_zw.zip_filename)) + + def test_custom_callback(self, custom_zw): + assert custom_zw.processed_size_callback(None) == "custom_callback" From f767268b5d158e16bd4c6ccff874ca5c2cbffe8c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 16:57:50 +0800 Subject: [PATCH 060/135] Remove unnecessary imports from tests --- tests2/test_gui_share.py | 19 +------------------ tests2/test_gui_tabs.py | 19 ------------------- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index a6bd9e3b..e7accb01 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -1,22 +1,9 @@ import pytest -import unittest - -import json import os import requests -import shutil -import base64 import tempfile -import secrets -from PyQt5 import QtCore, QtTest, QtWidgets - -from onionshare import strings -from onionshare.common import Common -from onionshare.settings import Settings -from onionshare.onion import Onion -from onionshare.web import Web -from onionshare_gui import Application, MainWindow, GuiCommon +from PyQt5 import QtCore, QtTest from .gui_base_test import GuiBaseTest @@ -382,7 +369,3 @@ class TestShare(GuiBaseTest): def test_common_tests(self): """Run all common tests""" self.run_all_common_setup_tests() - - -if __name__ == "__main__": - unittest.main() diff --git a/tests2/test_gui_tabs.py b/tests2/test_gui_tabs.py index 0fd3feab..5423867f 100644 --- a/tests2/test_gui_tabs.py +++ b/tests2/test_gui_tabs.py @@ -1,23 +1,8 @@ import pytest -import unittest - -import json import os -import requests -import shutil -import base64 -import tempfile -import secrets from PyQt5 import QtCore, QtTest, QtWidgets -from onionshare import strings -from onionshare.common import Common -from onionshare.settings import Settings -from onionshare.onion import Onion -from onionshare.web import Web -from onionshare_gui import Application, MainWindow, GuiCommon - from .gui_base_test import GuiBaseTest @@ -338,7 +323,3 @@ class TestTabs(GuiBaseTest): QtTest.QTest.mouseClick( tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton ) - - -if __name__ == "__main__": - unittest.main() From 7ff0e9d7727250b2d31f39bd4010f994790b7e5c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 17:08:21 +0800 Subject: [PATCH 061/135] Make wait times in TestTabs much shorter, which makes tests run much quicker --- tests2/test_gui_tabs.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests2/test_gui_tabs.py b/tests2/test_gui_tabs.py index 5423867f..3e25bb56 100644 --- a/tests2/test_gui_tabs.py +++ b/tests2/test_gui_tabs.py @@ -70,14 +70,14 @@ class TestTabs(GuiBaseTest): tab.get_mode().server_status.status, tab.get_mode().server_status.STATUS_WORKING, ) - QtTest.QTest.qWait(1000) + QtTest.QTest.qWait(500) self.assertEqual( tab.get_mode().server_status.status, tab.get_mode().server_status.STATUS_STARTED, ) # Prepare to reject the dialog - QtCore.QTimer.singleShot(1000, tab.close_dialog.reject_button.click) + QtCore.QTimer.singleShot(200, tab.close_dialog.reject_button.click) # Close tab QtTest.QTest.mouseClick( @@ -90,7 +90,7 @@ class TestTabs(GuiBaseTest): self.assertTrue(tab.get_mode().isVisible()) # Prepare to accept the dialog - QtCore.QTimer.singleShot(1000, tab.close_dialog.accept_button.click) + QtCore.QTimer.singleShot(200, tab.close_dialog.accept_button.click) # Close tab QtTest.QTest.mouseClick( @@ -113,7 +113,7 @@ class TestTabs(GuiBaseTest): self.assertTrue(os.path.exists(tab.settings.filename)) # Prepare to reject the dialog - QtCore.QTimer.singleShot(1000, tab.close_dialog.reject_button.click) + QtCore.QTimer.singleShot(200, tab.close_dialog.reject_button.click) # Close tab QtTest.QTest.mouseClick( @@ -129,7 +129,7 @@ class TestTabs(GuiBaseTest): self.assertTrue(os.path.exists(tab.settings.filename)) # Prepare to accept the dialog - QtCore.QTimer.singleShot(1000, tab.close_dialog.accept_button.click) + QtCore.QTimer.singleShot(200, tab.close_dialog.accept_button.click) # Close tab QtTest.QTest.mouseClick( @@ -304,14 +304,14 @@ class TestTabs(GuiBaseTest): tab.get_mode().server_status.status, tab.get_mode().server_status.STATUS_WORKING, ) - QtTest.QTest.qWait(1000) + QtTest.QTest.qWait(500) self.assertEqual( tab.get_mode().server_status.status, tab.get_mode().server_status.STATUS_STARTED, ) # Prepare to reject the dialog - QtCore.QTimer.singleShot(1000, self.gui.close_dialog.reject_button.click) + QtCore.QTimer.singleShot(200, self.gui.close_dialog.reject_button.click) # Close the window self.gui.close() From 6ae9c7b19ef1ce4795911c47d3a238ab0adc0938 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 17:25:30 +0800 Subject: [PATCH 062/135] Click in the tests by running .click() --- tests2/test_gui_tabs.py | 106 +++++++++++----------------------------- 1 file changed, 28 insertions(+), 78 deletions(-) diff --git a/tests2/test_gui_tabs.py b/tests2/test_gui_tabs.py index 3e25bb56..2ddbd0e1 100644 --- a/tests2/test_gui_tabs.py +++ b/tests2/test_gui_tabs.py @@ -21,7 +21,7 @@ class TestTabs(GuiBaseTest): self.verify_new_tab(tab) # Share files - QtTest.QTest.mouseClick(tab.share_button, QtCore.Qt.LeftButton) + tab.share_button.click() self.assertFalse(tab.new_tab.isVisible()) self.assertTrue(tab.share_mode.isVisible()) @@ -36,7 +36,7 @@ class TestTabs(GuiBaseTest): self.verify_new_tab(tab) # Receive files - QtTest.QTest.mouseClick(tab.receive_button, QtCore.Qt.LeftButton) + tab.receive_button.click() self.assertFalse(tab.new_tab.isVisible()) self.assertTrue(tab.receive_mode.isVisible()) @@ -47,7 +47,7 @@ class TestTabs(GuiBaseTest): self.verify_new_tab(tab) # Publish website - QtTest.QTest.mouseClick(tab.website_button, QtCore.Qt.LeftButton) + tab.website_button.click() self.assertFalse(tab.new_tab.isVisible()) self.assertTrue(tab.website_mode.isVisible()) @@ -63,9 +63,7 @@ class TestTabs(GuiBaseTest): tab.get_mode().server_status.status, tab.get_mode().server_status.STATUS_STOPPED, ) - QtTest.QTest.mouseClick( - tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton - ) + tab.get_mode().server_status.server_button.click() self.assertEqual( tab.get_mode().server_status.status, tab.get_mode().server_status.STATUS_WORKING, @@ -80,10 +78,7 @@ class TestTabs(GuiBaseTest): QtCore.QTimer.singleShot(200, tab.close_dialog.reject_button.click) # Close tab - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() # The tab should still be open self.assertFalse(tab.new_tab.isVisible()) @@ -93,10 +88,7 @@ class TestTabs(GuiBaseTest): QtCore.QTimer.singleShot(200, tab.close_dialog.accept_button.click) # Close tab - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() # The tab should be closed self.assertTrue(self.gui.tabs.widget(0).new_tab.isVisible()) @@ -116,10 +108,7 @@ class TestTabs(GuiBaseTest): QtCore.QTimer.singleShot(200, tab.close_dialog.reject_button.click) # Close tab - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() # The tab should still be open self.assertFalse(tab.new_tab.isVisible()) @@ -132,10 +121,7 @@ class TestTabs(GuiBaseTest): QtCore.QTimer.singleShot(200, tab.close_dialog.accept_button.click) # Close tab - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() # The tab should be closed self.assertTrue(self.gui.tabs.widget(0).new_tab.isVisible()) @@ -160,27 +146,18 @@ class TestTabs(GuiBaseTest): def test_03_new_tab_button_opens_new_tabs(self): """Clicking the "+" button should open new tabs""" self.assertEqual(self.gui.tabs.count(), 1) - QtTest.QTest.mouseClick(self.gui.tabs.new_tab_button, QtCore.Qt.LeftButton) - QtTest.QTest.mouseClick(self.gui.tabs.new_tab_button, QtCore.Qt.LeftButton) - QtTest.QTest.mouseClick(self.gui.tabs.new_tab_button, QtCore.Qt.LeftButton) + self.gui.tabs.new_tab_button.click() + self.gui.tabs.new_tab_button.click() + self.gui.tabs.new_tab_button.click() self.assertEqual(self.gui.tabs.count(), 4) @pytest.mark.gui def test_04_close_tab_button_closes_tabs(self): """Clicking the "x" button should close tabs""" self.assertEqual(self.gui.tabs.count(), 4) - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() self.assertEqual(self.gui.tabs.count(), 1) @pytest.mark.gui @@ -189,17 +166,12 @@ class TestTabs(GuiBaseTest): self.assertEqual(self.gui.tabs.count(), 1) # Click share button - QtTest.QTest.mouseClick( - self.gui.tabs.widget(0).share_button, QtCore.Qt.LeftButton - ) + self.gui.tabs.widget(0).share_button.click() self.assertFalse(self.gui.tabs.widget(0).new_tab.isVisible()) self.assertTrue(self.gui.tabs.widget(0).share_mode.isVisible()) # Close the tab - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() # A new tab should be opened self.assertEqual(self.gui.tabs.count(), 1) @@ -210,46 +182,28 @@ class TestTabs(GuiBaseTest): """Clicking the mode buttons in a new tab should change the mode of the tab""" # New tab, share files - QtTest.QTest.mouseClick(self.gui.tabs.new_tab_button, QtCore.Qt.LeftButton) - QtTest.QTest.mouseClick( - self.gui.tabs.widget(1).share_button, QtCore.Qt.LeftButton - ) + self.gui.tabs.new_tab_button.click() + self.gui.tabs.widget(1).share_button.click() self.assertFalse(self.gui.tabs.widget(1).new_tab.isVisible()) self.assertTrue(self.gui.tabs.widget(1).share_mode.isVisible()) # New tab, receive files - QtTest.QTest.mouseClick(self.gui.tabs.new_tab_button, QtCore.Qt.LeftButton) - QtTest.QTest.mouseClick( - self.gui.tabs.widget(2).receive_button, QtCore.Qt.LeftButton - ) + self.gui.tabs.new_tab_button.click() + self.gui.tabs.widget(2).receive_button.click() self.assertFalse(self.gui.tabs.widget(2).new_tab.isVisible()) self.assertTrue(self.gui.tabs.widget(2).receive_mode.isVisible()) # New tab, publish website - QtTest.QTest.mouseClick(self.gui.tabs.new_tab_button, QtCore.Qt.LeftButton) - QtTest.QTest.mouseClick( - self.gui.tabs.widget(3).website_button, QtCore.Qt.LeftButton - ) + self.gui.tabs.new_tab_button.click() + self.gui.tabs.widget(3).website_button.click() self.assertFalse(self.gui.tabs.widget(3).new_tab.isVisible()) self.assertTrue(self.gui.tabs.widget(3).website_mode.isVisible()) # Close tabs - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) - QtTest.QTest.mouseClick( - self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide), - QtCore.Qt.LeftButton, - ) + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() @pytest.mark.gui def test_07_close_share_tab_while_server_started_should_warn(self): @@ -297,9 +251,7 @@ class TestTabs(GuiBaseTest): tab.get_mode().server_status.status, tab.get_mode().server_status.STATUS_STOPPED, ) - QtTest.QTest.mouseClick( - tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton - ) + tab.get_mode().server_status.server_button.click() self.assertEqual( tab.get_mode().server_status.status, tab.get_mode().server_status.STATUS_WORKING, @@ -320,6 +272,4 @@ class TestTabs(GuiBaseTest): self.assertTrue(self.gui.isVisible()) # Stop the server - QtTest.QTest.mouseClick( - tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton - ) + tab.get_mode().server_status.server_button.click() From c185b001fcd98aa818b6ca59131332d5b001b432 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 17:27:17 +0800 Subject: [PATCH 063/135] Make singleShot wait times 0 ms to speed up the tests --- tests2/test_gui_tabs.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests2/test_gui_tabs.py b/tests2/test_gui_tabs.py index 2ddbd0e1..eada8176 100644 --- a/tests2/test_gui_tabs.py +++ b/tests2/test_gui_tabs.py @@ -75,7 +75,7 @@ class TestTabs(GuiBaseTest): ) # Prepare to reject the dialog - QtCore.QTimer.singleShot(200, tab.close_dialog.reject_button.click) + QtCore.QTimer.singleShot(0, tab.close_dialog.reject_button.click) # Close tab self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() @@ -85,7 +85,7 @@ class TestTabs(GuiBaseTest): self.assertTrue(tab.get_mode().isVisible()) # Prepare to accept the dialog - QtCore.QTimer.singleShot(200, tab.close_dialog.accept_button.click) + QtCore.QTimer.singleShot(0, tab.close_dialog.accept_button.click) # Close tab self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() @@ -105,7 +105,7 @@ class TestTabs(GuiBaseTest): self.assertTrue(os.path.exists(tab.settings.filename)) # Prepare to reject the dialog - QtCore.QTimer.singleShot(200, tab.close_dialog.reject_button.click) + QtCore.QTimer.singleShot(0, tab.close_dialog.reject_button.click) # Close tab self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() @@ -118,7 +118,7 @@ class TestTabs(GuiBaseTest): self.assertTrue(os.path.exists(tab.settings.filename)) # Prepare to accept the dialog - QtCore.QTimer.singleShot(200, tab.close_dialog.accept_button.click) + QtCore.QTimer.singleShot(0, tab.close_dialog.accept_button.click) # Close tab self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() @@ -263,7 +263,7 @@ class TestTabs(GuiBaseTest): ) # Prepare to reject the dialog - QtCore.QTimer.singleShot(200, self.gui.close_dialog.reject_button.click) + QtCore.QTimer.singleShot(0, self.gui.close_dialog.reject_button.click) # Close the window self.gui.close() From ed098e89814464d0259ba6423bb85fe4c4d7d391 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 18:09:10 +0800 Subject: [PATCH 064/135] Take public mode checkbox outside advanced settings so it's always shown --- onionshare_gui/tab/mode/mode_settings_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare_gui/tab/mode/mode_settings_widget.py b/onionshare_gui/tab/mode/mode_settings_widget.py index a6a43df6..34a6db9a 100644 --- a/onionshare_gui/tab/mode/mode_settings_widget.py +++ b/onionshare_gui/tab/mode/mode_settings_widget.py @@ -155,7 +155,6 @@ class ModeSettingsWidget(QtWidgets.QWidget): # Advanced group itself advanced_layout = QtWidgets.QVBoxLayout() advanced_layout.setContentsMargins(0, 0, 0, 0) - advanced_layout.addWidget(self.public_checkbox) advanced_layout.addLayout(autostart_timer_layout) advanced_layout.addLayout(autostop_timer_layout) advanced_layout.addWidget(self.legacy_checkbox) @@ -167,6 +166,7 @@ class ModeSettingsWidget(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout() layout.addLayout(self.mode_specific_layout) layout.addWidget(self.persistent_checkbox) + layout.addWidget(self.public_checkbox) layout.addWidget(self.advanced_widget) layout.addWidget(self.toggle_advanced_button) self.setLayout(layout) From 3f04ffb69ed4eb42ab64603bb3114ec13d2324a4 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 19:11:30 +0800 Subject: [PATCH 065/135] Start refactoring all of the share mode tests to work with tabs --- tests2/gui_base_test.py | 304 +++++++++++++++++----------- tests2/test_gui_share.py | 426 ++++++++++++++++++++------------------- tests2/test_gui_tabs.py | 56 +---- 3 files changed, 406 insertions(+), 380 deletions(-) diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index 2cd9c7b8..acc0b964 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -40,6 +40,9 @@ class GuiBaseTest(unittest.TestCase): with open(filename, "w") as file: file.write(secrets.token_hex(10)) cls.tmpfiles.append(filename) + cls.tmpfile_test = os.path.join(cls.tmpdir.name, "test.txt") + with open(cls.tmpfile_test, "w") as file: + file.write("onionshare") @classmethod def tearDownClass(cls): @@ -48,6 +51,64 @@ class GuiBaseTest(unittest.TestCase): # Shared test methods + def verify_new_tab(self, tab): + # Make sure the new tab widget is showing, and no mode has been started + self.assertTrue(tab.new_tab.isVisible()) + self.assertFalse(hasattr(tab, "share_mode")) + self.assertFalse(hasattr(tab, "receive_mode")) + self.assertFalse(hasattr(tab, "website_mode")) + + def new_share_tab(self): + tab = self.gui.tabs.widget(0) + self.verify_new_tab(tab) + + # Share files + tab.share_button.click() + self.assertFalse(tab.new_tab.isVisible()) + self.assertTrue(tab.share_mode.isVisible()) + + return tab + + def new_share_tab_with_files(self): + tab = self.new_share_tab() + + # Add files + for filename in self.tmpfiles: + tab.share_mode.server_status.file_selection.file_list.add_file(filename) + + return tab + + def new_receive_tab(self): + tab = self.gui.tabs.widget(0) + self.verify_new_tab(tab) + + # Receive files + tab.receive_button.click() + self.assertFalse(tab.new_tab.isVisible()) + self.assertTrue(tab.receive_mode.isVisible()) + + return tab + + def new_website_tab(self): + tab = self.gui.tabs.widget(0) + self.verify_new_tab(tab) + + # Publish website + tab.website_button.click() + self.assertFalse(tab.new_tab.isVisible()) + self.assertTrue(tab.website_mode.isVisible()) + + return tab + + def new_website_tab_with_files(self): + tab = self.new_website_tab() + + # Add files + for filename in self.tmpfiles: + tab.website_mode.server_status.file_selection.file_list.add_file(filename) + + return tab + def gui_loaded(self): """Test that the GUI actually is shown""" self.assertTrue(self.gui.show) @@ -56,265 +117,268 @@ class GuiBaseTest(unittest.TestCase): """Test that the window title is OnionShare""" self.assertEqual(self.gui.windowTitle(), "OnionShare") - def settings_button_is_visible(self): - """Test that the settings button is visible""" - self.assertTrue(self.gui.settings_button.isVisible()) - - def settings_button_is_hidden(self): - """Test that the settings button is hidden when the server starts""" - self.assertFalse(self.gui.settings_button.isVisible()) - def server_status_bar_is_visible(self): """Test that the status bar is visible""" self.assertTrue(self.gui.status_bar.isVisible()) - def click_mode(self, mode): - """Test that we can switch Mode by clicking the button""" - if type(mode) == ReceiveMode: - QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) - self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) - if type(mode) == ShareMode: - QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton) - self.assertTrue(self.gui.mode, self.gui.MODE_SHARE) - if type(mode) == WebsiteMode: - QtTest.QTest.mouseClick(self.gui.website_mode_button, QtCore.Qt.LeftButton) - self.assertTrue(self.gui.mode, self.gui.MODE_WEBSITE) + def mode_settings_widget_is_visible(self, tab): + """Test that the mode settings are visible""" + self.assertTrue(tab.get_mode().mode_settings_widget.isVisible()) - def click_toggle_history(self, mode): + def mode_settings_widget_is_hidden(self, tab): + """Test that the mode settings are hidden when the server starts""" + self.assertFalse(tab.get_mode().mode_settings_widget.isVisible()) + + def click_toggle_history(self, tab): """Test that we can toggle Download or Upload history by clicking the toggle button""" - currently_visible = mode.history.isVisible() - QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton) - self.assertEqual(mode.history.isVisible(), not currently_visible) + currently_visible = tab.get_mode().history.isVisible() + QtTest.QTest.mouseClick(tab.get_mode().toggle_history, QtCore.Qt.LeftButton) + self.assertEqual(tab.get_mode().history.isVisible(), not currently_visible) - def history_indicator(self, mode, public_mode, indicator_count="1"): + def history_indicator(self, tab, indicator_count="1"): """Test that we can make sure the history is toggled off, do an action, and the indiciator works""" # Make sure history is toggled off - if mode.history.isVisible(): - QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton) - self.assertFalse(mode.history.isVisible()) + if tab.get_mode().history.isVisible(): + QtTest.QTest.mouseClick(tab.get_mode().toggle_history, QtCore.Qt.LeftButton) + self.assertFalse(tab.get_mode().history.isVisible()) # Indicator should not be visible yet - self.assertFalse(mode.toggle_history.indicator_label.isVisible()) + self.assertFalse(tab.get_mode().toggle_history.indicator_label.isVisible()) - if type(mode) == ReceiveMode: + if type(tab.get_mode()) == ReceiveMode: # Upload a file - files = {"file[]": open("/tmp/test.txt", "rb")} - url = f"http://127.0.0.1:{self.gui.app.port}/upload" - if public_mode: + files = {"file[]": open(self.tmpfiles[0], "rb")} + url = f"http://127.0.0.1:{tab.app.port}/upload" + if tab.settings.get("general", "public"): requests.post(url, files=files) else: requests.post( url, files=files, - auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password), + auth=requests.auth.HTTPBasicAuth( + "onionshare", tab.get_mode().web.password + ), ) QtTest.QTest.qWait(2000) - if type(mode) == ShareMode: + if type(tab.get_mode()) == ShareMode: # Download files - url = f"http://127.0.0.1:{self.gui.app.port}/download" - if public_mode: + url = f"http://127.0.0.1:{tab.app.port}/download" + if tab.settings.get("general", "public"): requests.get(url) else: requests.get( url, - auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password), + auth=requests.auth.HTTPBasicAuth( + "onionshare", tab.get_mode().web.password + ), ) QtTest.QTest.qWait(2000) # Indicator should be visible, have a value of "1" - self.assertTrue(mode.toggle_history.indicator_label.isVisible()) - self.assertEqual(mode.toggle_history.indicator_label.text(), indicator_count) + self.assertTrue(tab.get_mode().toggle_history.indicator_label.isVisible()) + self.assertEqual( + tab.get_mode().toggle_history.indicator_label.text(), indicator_count + ) # Toggle history back on, indicator should be hidden again - QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton) - self.assertFalse(mode.toggle_history.indicator_label.isVisible()) + QtTest.QTest.mouseClick(tab.get_mode().toggle_history, QtCore.Qt.LeftButton) + self.assertFalse(tab.get_mode().toggle_history.indicator_label.isVisible()) - def history_is_not_visible(self, mode): + def history_is_not_visible(self, tab): """Test that the History section is not visible""" - self.assertFalse(mode.history.isVisible()) + self.assertFalse(tab.get_mode().history.isVisible()) - def history_is_visible(self, mode): + def history_is_visible(self, tab): """Test that the History section is visible""" - self.assertTrue(mode.history.isVisible()) + self.assertTrue(tab.get_mode().history.isVisible()) - def server_working_on_start_button_pressed(self, mode): + def server_working_on_start_button_pressed(self, tab): """Test we can start the service""" # Should be in SERVER_WORKING state - QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEqual(mode.server_status.status, 1) + QtTest.QTest.mouseClick( + tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton + ) + self.assertEqual(tab.get_mode().server_status.status, 1) - def toggle_indicator_is_reset(self, mode): - self.assertEqual(mode.toggle_history.indicator_count, 0) - self.assertFalse(mode.toggle_history.indicator_label.isVisible()) + def toggle_indicator_is_reset(self, tab): + self.assertEqual(tab.get_mode().toggle_history.indicator_count, 0) + self.assertFalse(tab.get_mode().toggle_history.indicator_label.isVisible()) - def server_status_indicator_says_starting(self, mode): + def server_status_indicator_says_starting(self, tab): """Test that the Server Status indicator shows we are Starting""" self.assertEqual( - mode.server_status_label.text(), + tab.get_mode().server_status_label.text(), strings._("gui_status_indicator_share_working"), ) - def server_status_indicator_says_scheduled(self, mode): + def server_status_indicator_says_scheduled(self, tab): """Test that the Server Status indicator shows we are Scheduled""" self.assertEqual( - mode.server_status_label.text(), + tab.get_mode().server_status_label.text(), strings._("gui_status_indicator_share_scheduled"), ) - def server_is_started(self, mode, startup_time=2000): + def server_is_started(self, tab, startup_time=2000): """Test that the server has started""" QtTest.QTest.qWait(startup_time) # Should now be in SERVER_STARTED state - self.assertEqual(mode.server_status.status, 2) + self.assertEqual(tab.get_mode().server_status.status, 2) - def web_server_is_running(self): + def web_server_is_running(self, tab): """Test that the web server has started""" try: - r = requests.get(f"http://127.0.0.1:{self.gui.app.port}/") + requests.get(f"http://127.0.0.1:{tab.app.port}/") self.assertTrue(True) except requests.exceptions.ConnectionError: self.assertTrue(False) - def have_a_password(self, mode, public_mode): + def have_a_password(self, tab): """Test that we have a valid password""" - if not public_mode: - self.assertRegex(mode.server_status.web.password, r"(\w+)-(\w+)") + if not tab.settings.get("general", "public"): + self.assertRegex(tab.get_mode().server_status.web.password, r"(\w+)-(\w+)") else: - self.assertIsNone(mode.server_status.web.password, r"(\w+)-(\w+)") + self.assertIsNone(tab.get_mode().server_status.web.password, r"(\w+)-(\w+)") - def add_button_visible(self, mode): + def add_button_visible(self, tab): """Test that the add button should be visible""" - self.assertTrue(mode.server_status.file_selection.add_button.isVisible()) + self.assertTrue( + tab.get_mode().server_status.file_selection.add_button.isVisible() + ) - def url_description_shown(self, mode): + def url_description_shown(self, tab): """Test that the URL label is showing""" - self.assertTrue(mode.server_status.url_description.isVisible()) + self.assertTrue(tab.get_mode().server_status.url_description.isVisible()) - def have_copy_url_button(self, mode, public_mode): + def have_copy_url_button(self, tab): """Test that the Copy URL button is shown and that the clipboard is correct""" - self.assertTrue(mode.server_status.copy_url_button.isVisible()) + self.assertTrue(tab.get_mode().server_status.copy_url_button.isVisible()) QtTest.QTest.mouseClick( - mode.server_status.copy_url_button, QtCore.Qt.LeftButton + tab.get_mode().server_status.copy_url_button, QtCore.Qt.LeftButton ) - clipboard = self.gui.qtapp.clipboard() - if public_mode: - self.assertEqual(clipboard.text(), f"http://127.0.0.1:{self.gui.app.port}") + clipboard = tab.common.gui.qtapp.clipboard() + if tab.settings.get("general", "public"): + self.assertEqual(clipboard.text(), f"http://127.0.0.1:{tab.app.port}") else: self.assertEqual( clipboard.text(), - f"http://onionshare:{mode.server_status.web.password}@127.0.0.1:{self.gui.app.port}", + f"http://onionshare:{tab.get_mode().server_status.web.password}@127.0.0.1:{tab.app.port}", ) - def server_status_indicator_says_started(self, mode): + def server_status_indicator_says_started(self, tab): """Test that the Server Status indicator shows we are started""" - if type(mode) == ReceiveMode: + if type(tab.get_mode()) == ReceiveMode: self.assertEqual( - mode.server_status_label.text(), + tab.get_mode().server_status_label.text(), strings._("gui_status_indicator_receive_started"), ) - if type(mode) == ShareMode: + if type(tab.get_mode()) == ShareMode: self.assertEqual( - mode.server_status_label.text(), + tab.get_mode().server_status_label.text(), strings._("gui_status_indicator_share_started"), ) - def web_page(self, mode, string, public_mode): + def web_page(self, tab, string): """Test that the web page contains a string""" - url = f"http://127.0.0.1:{self.gui.app.port}/" - if public_mode: + url = f"http://127.0.0.1:{tab.app.port}/" + if tab.settings.get("general", "public"): r = requests.get(url) else: r = requests.get( - url, auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password) + url, + auth=requests.auth.HTTPBasicAuth( + "onionshare", tab.get_mode().web.password + ), ) self.assertTrue(string in r.text) - def history_widgets_present(self, mode): + def history_widgets_present(self, tab): """Test that the relevant widgets are present in the history view after activity has taken place""" - self.assertFalse(mode.history.empty.isVisible()) - self.assertTrue(mode.history.not_empty.isVisible()) + self.assertFalse(tab.get_mode().history.empty.isVisible()) + self.assertTrue(tab.get_mode().history.not_empty.isVisible()) - def counter_incremented(self, mode, count): + def counter_incremented(self, tab, count): """Test that the counter has incremented""" - self.assertEqual(mode.history.completed_count, count) + self.assertEqual(tab.get_mode().history.completed_count, count) - def server_is_stopped(self, mode, stay_open): + def server_is_stopped(self, tab): """Test that the server stops when we click Stop""" if ( - type(mode) == ReceiveMode - or (type(mode) == ShareMode and stay_open) - or (type(mode) == WebsiteMode) + type(tab.get_mode()) == ReceiveMode + or ( + type(tab.get_mode()) == ShareMode + and not tab.settings.get("share", "autostop_sharing") + ) + or (type(tab.get_mode()) == WebsiteMode) ): QtTest.QTest.mouseClick( - mode.server_status.server_button, QtCore.Qt.LeftButton + tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton ) - self.assertEqual(mode.server_status.status, 0) + self.assertEqual(tab.get_mode().server_status.status, 0) - def web_server_is_stopped(self): + def web_server_is_stopped(self, tab): """Test that the web server also stopped""" QtTest.QTest.qWait(2000) try: - r = requests.get(f"http://127.0.0.1:{self.gui.app.port}/") + r = requests.get(f"http://127.0.0.1:{tab.app.port}/") self.assertTrue(False) except requests.exceptions.ConnectionError: self.assertTrue(True) - def server_status_indicator_says_closed(self, mode, stay_open): + def server_status_indicator_says_closed(self, tab): """Test that the Server Status indicator shows we closed""" - if type(mode) == ReceiveMode: + if type(tab.get_mode()) == ReceiveMode: self.assertEqual( - self.gui.receive_mode.server_status_label.text(), + tab.get_mode().server_status_label.text(), strings._("gui_status_indicator_receive_stopped"), ) - if type(mode) == ShareMode: - if stay_open: + if type(tab.get_mode()) == ShareMode: + if tab.settings.get("share", "autostop_sharing"): self.assertEqual( - self.gui.share_mode.server_status_label.text(), + tab.get_mode().server_status_label.text(), strings._("gui_status_indicator_share_stopped"), ) else: self.assertEqual( - self.gui.share_mode.server_status_label.text(), + tab.get_mode().server_status_label.text(), strings._("closing_automatically"), ) - def clear_all_history_items(self, mode, count): + def clear_all_history_items(self, tab, count): if count == 0: - QtTest.QTest.mouseClick(mode.history.clear_button, QtCore.Qt.LeftButton) - self.assertEquals(len(mode.history.item_list.items.keys()), count) + QtTest.QTest.mouseClick( + tab.get_mode().history.clear_button, QtCore.Qt.LeftButton + ) + self.assertEquals(len(tab.get_mode().history.item_list.items.keys()), count) # Auto-stop timer tests - def set_timeout(self, mode, timeout): + def set_timeout(self, tab, timeout): """Test that the timeout can be set""" timer = QtCore.QDateTime.currentDateTime().addSecs(timeout) - mode.server_status.autostop_timer_widget.setDateTime(timer) - self.assertTrue(mode.server_status.autostop_timer_widget.dateTime(), timer) + tab.get_mode().server_status.autostop_timer_widget.setDateTime(timer) + self.assertTrue( + tab.get_mode().server_status.autostop_timer_widget.dateTime(), timer + ) - def autostop_timer_widget_hidden(self, mode): + def autostop_timer_widget_hidden(self, tab): """Test that the auto-stop timer widget is hidden when share has started""" - self.assertFalse(mode.server_status.autostop_timer_container.isVisible()) + self.assertFalse( + tab.get_mode().server_status.autostop_timer_container.isVisible() + ) - def server_timed_out(self, mode, wait): + def server_timed_out(self, tab, wait): """Test that the server has timed out after the timer ran out""" QtTest.QTest.qWait(wait) # We should have timed out now - self.assertEqual(mode.server_status.status, 0) - - # Hack to close an Alert dialog that would otherwise block tests - def accept_dialog(self): - window = self.gui.qtapp.activeWindow() - if window: - window.close() + self.assertEqual(tab.get_mode().server_status.status, 0) # Grouped tests follow from here def run_all_common_setup_tests(self): self.gui_loaded() self.window_title_seen() - self.settings_button_is_visible() self.server_status_bar_is_visible() diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index e7accb01..09a40cb1 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -2,6 +2,7 @@ import pytest import os import requests import tempfile +import zipfile from PyQt5 import QtCore, QtTest @@ -12,110 +13,101 @@ class TestShare(GuiBaseTest): # Shared test methods # Persistence tests - def have_same_password(self, password): + def have_same_password(self, tab, password): """Test that we have the same password""" - self.assertEqual(self.gui.share_mode.server_status.web.password, password) + self.assertEqual(tab.get_mode().server_status.web.password, password) # Share-specific tests - def file_selection_widget_has_files(self, num=2): + def file_selection_widget_has_files(self, tab, num=2): """Test that the number of items in the list is as expected""" self.assertEqual( - self.gui.share_mode.server_status.file_selection.get_num_files(), num + tab.get_mode().server_status.file_selection.get_num_files(), num ) - def deleting_all_files_hides_delete_button(self): + def deleting_all_files_hides_delete_button(self, tab): """Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button""" - rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect( - self.gui.share_mode.server_status.file_selection.file_list.item(0) + rect = tab.get_mode().server_status.file_selection.file_list.visualItemRect( + tab.get_mode().server_status.file_selection.file_list.item(0) ) QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.file_selection.file_list.viewport(), + tab.get_mode().server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center(), ) # Delete button should be visible self.assertTrue( - self.gui.share_mode.server_status.file_selection.delete_button.isVisible() + tab.get_mode().server_status.file_selection.delete_button.isVisible() ) # Click delete, delete button should still be visible since we have one more file QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.file_selection.delete_button, + tab.get_mode().server_status.file_selection.delete_button, QtCore.Qt.LeftButton, ) - - rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect( - self.gui.share_mode.server_status.file_selection.file_list.item(0) + rect = tab.get_mode().server_status.file_selection.file_list.visualItemRect( + tab.get_mode().server_status.file_selection.file_list.item(0) ) QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.file_selection.file_list.viewport(), + tab.get_mode().server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center(), ) self.assertTrue( - self.gui.share_mode.server_status.file_selection.delete_button.isVisible() + tab.get_mode().server_status.file_selection.delete_button.isVisible() ) QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.file_selection.delete_button, + tab.get_mode().server_status.file_selection.delete_button, QtCore.Qt.LeftButton, ) # No more files, the delete button should be hidden self.assertFalse( - self.gui.share_mode.server_status.file_selection.delete_button.isVisible() + tab.get_mode().server_status.file_selection.delete_button.isVisible() ) - def add_a_file_and_delete_using_its_delete_widget(self): + def add_a_file_and_delete_using_its_delete_widget(self, tab): """Test that we can also delete a file by clicking on its [X] widget""" - self.gui.share_mode.server_status.file_selection.file_list.add_file( - "/etc/hosts" - ) + tab.get_mode().server_status.file_selection.file_list.add_file(self.tmpfiles[0]) QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.file_selection.file_list.item( - 0 - ).item_button, + tab.get_mode().server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton, ) - self.file_selection_widget_has_files(0) + self.file_selection_widget_has_files(tab, 0) - def file_selection_widget_read_files(self): + def file_selection_widget_read_files(self, tab): """Re-add some files to the list so we can share""" - self.gui.share_mode.server_status.file_selection.file_list.add_file( - "/etc/hosts" - ) - self.gui.share_mode.server_status.file_selection.file_list.add_file( - "/tmp/test.txt" - ) - self.file_selection_widget_has_files(2) + tab.get_mode().server_status.file_selection.file_list.add_file(self.tmpfiles[0]) + tab.get_mode().server_status.file_selection.file_list.add_file(self.tmpfiles[1]) + self.file_selection_widget_has_files(tab, 2) - def add_large_file(self): + def add_large_file(self, tab): """Add a large file to the share""" size = 1024 * 1024 * 155 with open("/tmp/large_file", "wb") as fout: fout.write(os.urandom(size)) - self.gui.share_mode.server_status.file_selection.file_list.add_file( + tab.get_mode().server_status.file_selection.file_list.add_file( "/tmp/large_file" ) - def add_delete_buttons_hidden(self): + def add_delete_buttons_hidden(self, tab): """Test that the add and delete buttons are hidden when the server starts""" self.assertFalse( - self.gui.share_mode.server_status.file_selection.add_button.isVisible() + tab.get_mode().server_status.file_selection.add_button.isVisible() ) self.assertFalse( - self.gui.share_mode.server_status.file_selection.delete_button.isVisible() + tab.get_mode().server_status.file_selection.delete_button.isVisible() ) - def download_share(self, public_mode): + def download_share(self, tab): """Test that we can download the share""" - url = f"http://127.0.0.1:{self.gui.app.port}/download" - if public_mode: + url = f"http://127.0.0.1:{tab.app.port}/download" + if tab.settings.get("general", "public"): r = requests.get(url) else: r = requests.get( url, auth=requests.auth.HTTPBasicAuth( - "onionshare", self.gui.share_mode.server_status.web.password + "onionshare", tab.get_mode().server_status.web.password ), ) @@ -127,30 +119,30 @@ class TestShare(GuiBaseTest): QtTest.QTest.qWait(2000) self.assertEqual("onionshare", zip.read("test.txt").decode("utf-8")) - def individual_file_is_viewable_or_not(self, public_mode, stay_open): - """Test whether an individual file is viewable (when in stay_open mode) and that it isn't (when not in stay_open mode)""" - url = f"http://127.0.0.1:{self.gui.app.port}" - download_file_url = f"http://127.0.0.1:{self.gui.app.port}/test.txt" - if public_mode: + def individual_file_is_viewable_or_not(self, tab): + """Test whether an individual file is viewable (when in autostop_sharing is false) and that it isn't (when not in autostop_sharing is true)""" + url = f"http://127.0.0.1:{tab.app.port}" + download_file_url = f"http://127.0.0.1:{tab.app.port}/test.txt" + if tab.settings.get("general", "public"): r = requests.get(url) else: r = requests.get( url, auth=requests.auth.HTTPBasicAuth( - "onionshare", self.gui.share_mode.server_status.web.password + "onionshare", tab.get_mode().server_status.web.password ), ) - if stay_open: + if not tab.settings.get("share", "autostop_sharing"): self.assertTrue('a href="test.txt"' in r.text) - if public_mode: + if tab.settings.get("general", "public"): r = requests.get(download_file_url) else: r = requests.get( download_file_url, auth=requests.auth.HTTPBasicAuth( - "onionshare", self.gui.share_mode.server_status.web.password + "onionshare", tab.get_mode().server_status.web.password ), ) @@ -162,210 +154,228 @@ class TestShare(GuiBaseTest): self.assertEqual("onionshare", f.read()) else: self.assertFalse('a href="/test.txt"' in r.text) - if public_mode: + if tab.settings.get("general", "public"): r = requests.get(download_file_url) else: r = requests.get( download_file_url, auth=requests.auth.HTTPBasicAuth( - "onionshare", self.gui.share_mode.server_status.web.password + "onionshare", tab.get_mode().server_status.web.password ), ) self.assertEqual(r.status_code, 404) - self.download_share(public_mode) + self.download_share(tab) QtTest.QTest.qWait(2000) - def hit_401(self, public_mode): + def hit_401(self, tab): """Test that the server stops after too many 401s, or doesn't when in public_mode""" - url = f"http://127.0.0.1:{self.gui.app.port}/" + url = f"http://127.0.0.1:{tab.app.port}/" for _ in range(20): password_guess = self.gui.common.build_password() - r = requests.get( + requests.get( url, auth=requests.auth.HTTPBasicAuth("onionshare", password_guess) ) # A nasty hack to avoid the Alert dialog that blocks the rest of the test - if not public_mode: + if not tab.settings.get("general", "public"): QtCore.QTimer.singleShot(1000, self.accept_dialog) # In public mode, we should still be running (no rate-limiting) - if public_mode: - self.web_server_is_running() + if tab.settings.get("general", "public"): + self.web_server_is_running(tab) # In non-public mode, we should be shut down (rate-limiting) else: - self.web_server_is_stopped() - - # Grouped tests follow from here - - def run_all_share_mode_setup_tests(self): - """Tests in share mode prior to starting a share""" - self.click_mode(self.gui.share_mode) - self.file_selection_widget_has_files() - self.history_is_not_visible(self.gui.share_mode) - self.click_toggle_history(self.gui.share_mode) - self.history_is_visible(self.gui.share_mode) - self.deleting_all_files_hides_delete_button() - self.add_a_file_and_delete_using_its_delete_widget() - self.file_selection_widget_read_files() - - def run_all_share_mode_started_tests(self, public_mode, startup_time=2000): - """Tests in share mode after starting a share""" - self.server_working_on_start_button_pressed(self.gui.share_mode) - self.server_status_indicator_says_starting(self.gui.share_mode) - self.add_delete_buttons_hidden() - self.settings_button_is_hidden() - self.server_is_started(self.gui.share_mode, startup_time) - self.web_server_is_running() - self.have_a_password(self.gui.share_mode, public_mode) - self.url_description_shown(self.gui.share_mode) - self.have_copy_url_button(self.gui.share_mode, public_mode) - self.server_status_indicator_says_started(self.gui.share_mode) - - def run_all_share_mode_download_tests(self, public_mode, stay_open): - """Tests in share mode after downloading a share""" - self.web_page(self.gui.share_mode, "Total size", public_mode) - self.download_share(public_mode) - self.history_widgets_present(self.gui.share_mode) - self.server_is_stopped(self.gui.share_mode, stay_open) - self.web_server_is_stopped() - self.server_status_indicator_says_closed(self.gui.share_mode, stay_open) - self.add_button_visible(self.gui.share_mode) - self.server_working_on_start_button_pressed(self.gui.share_mode) - self.toggle_indicator_is_reset(self.gui.share_mode) - self.server_is_started(self.gui.share_mode) - self.history_indicator(self.gui.share_mode, public_mode) - - def run_all_share_mode_individual_file_download_tests(self, public_mode, stay_open): - """Tests in share mode after downloading a share""" - self.web_page(self.gui.share_mode, "Total size", public_mode) - self.individual_file_is_viewable_or_not(public_mode, stay_open) - self.history_widgets_present(self.gui.share_mode) - self.server_is_stopped(self.gui.share_mode, stay_open) - self.web_server_is_stopped() - self.server_status_indicator_says_closed(self.gui.share_mode, stay_open) - self.add_button_visible(self.gui.share_mode) - self.server_working_on_start_button_pressed(self.gui.share_mode) - self.server_is_started(self.gui.share_mode) - self.history_indicator(self.gui.share_mode, public_mode) - - def run_all_share_mode_tests(self, public_mode, stay_open): - """End-to-end share tests""" - self.run_all_share_mode_setup_tests() - self.run_all_share_mode_started_tests(public_mode) - self.run_all_share_mode_download_tests(public_mode, stay_open) - - def run_all_clear_all_button_tests(self, public_mode, stay_open): - """Test the Clear All history button""" - self.run_all_share_mode_setup_tests() - self.run_all_share_mode_started_tests(public_mode) - self.individual_file_is_viewable_or_not(public_mode, stay_open) - self.history_widgets_present(self.gui.share_mode) - self.clear_all_history_items(self.gui.share_mode, 0) - self.individual_file_is_viewable_or_not(public_mode, stay_open) - self.clear_all_history_items(self.gui.share_mode, 2) - - def run_all_share_mode_individual_file_tests(self, public_mode, stay_open): - """Tests in share mode when viewing an individual file""" - self.run_all_share_mode_setup_tests() - self.run_all_share_mode_started_tests(public_mode) - self.run_all_share_mode_individual_file_download_tests(public_mode, stay_open) - - def run_all_large_file_tests(self, public_mode, stay_open): - """Same as above but with a larger file""" - self.run_all_share_mode_setup_tests() - self.add_large_file() - self.run_all_share_mode_started_tests(public_mode, startup_time=15000) - self.assertTrue(self.gui.share_mode.filesize_warning.isVisible()) - self.server_is_stopped(self.gui.share_mode, stay_open) - self.web_server_is_stopped() - self.server_status_indicator_says_closed(self.gui.share_mode, stay_open) - - def run_all_share_mode_persistent_tests(self, public_mode, stay_open): - """Same as end-to-end share tests but also test the password is the same on multiple shared""" - self.run_all_share_mode_setup_tests() - self.run_all_share_mode_started_tests(public_mode) - password = self.gui.share_mode.server_status.web.password - self.run_all_share_mode_download_tests(public_mode, stay_open) - self.have_same_password(password) - - def run_all_share_mode_timer_tests(self, public_mode): - """Auto-stop timer tests in share mode""" - self.run_all_share_mode_setup_tests() - self.set_timeout(self.gui.share_mode, 5) - self.run_all_share_mode_started_tests(public_mode) - self.autostop_timer_widget_hidden(self.gui.share_mode) - self.server_timed_out(self.gui.share_mode, 10000) - self.web_server_is_stopped() - - def run_all_share_mode_autostart_timer_tests(self, public_mode): - """Auto-start timer tests in share mode""" - self.run_all_share_mode_setup_tests() - self.set_autostart_timer(self.gui.share_mode, 5) - self.server_working_on_start_button_pressed(self.gui.share_mode) - self.autostart_timer_widget_hidden(self.gui.share_mode) - self.server_status_indicator_says_scheduled(self.gui.share_mode) - self.web_server_is_stopped() - self.scheduled_service_started(self.gui.share_mode, 7000) - self.web_server_is_running() - - def run_all_share_mode_autostop_autostart_mismatch_tests(self, public_mode): - """Auto-stop timer tests in share mode""" - self.run_all_share_mode_setup_tests() - self.set_autostart_timer(self.gui.share_mode, 15) - self.set_timeout(self.gui.share_mode, 5) - QtCore.QTimer.singleShot(4000, self.accept_dialog) - QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton - ) - self.server_is_stopped(self.gui.share_mode, False) - - def run_all_share_mode_unreadable_file_tests(self): - """Attempt to share an unreadable file""" - self.run_all_share_mode_setup_tests() - QtCore.QTimer.singleShot(1000, self.accept_dialog) - self.gui.share_mode.server_status.file_selection.file_list.add_file( - "/tmp/nonexistent.txt" - ) - self.file_selection_widget_has_files(2) + self.web_server_is_stopped(tab) # Auto-start timer tests - def set_autostart_timer(self, mode, timer): + + def set_autostart_timer(self, tab, mode, timer): """Test that the timer can be set""" schedule = QtCore.QDateTime.currentDateTime().addSecs(timer) mode.server_status.autostart_timer_widget.setDateTime(schedule) self.assertTrue(mode.server_status.autostart_timer_widget.dateTime(), schedule) - def autostart_timer_widget_hidden(self, mode): + def autostart_timer_widget_hidden(self, tab, mode): """Test that the auto-start timer widget is hidden when share has started""" self.assertFalse(mode.server_status.autostart_timer_container.isVisible()) - def scheduled_service_started(self, mode, wait): + def scheduled_service_started(self, tab, mode, wait): """Test that the server has timed out after the timer ran out""" QtTest.QTest.qWait(wait) # We should have started now self.assertEqual(mode.server_status.status, 2) - def cancel_the_share(self, mode): + def cancel_the_share(self, tab, mode): """Test that we can cancel a share before it's started up """ - self.server_working_on_start_button_pressed(mode) - self.server_status_indicator_says_scheduled(mode) - self.add_delete_buttons_hidden() - self.settings_button_is_hidden() - self.set_autostart_timer(mode, 10) + self.server_working_on_start_button_pressed(tab) + self.server_status_indicator_says_scheduled(tab) + self.add_delete_buttons_hidden(tab) + self.mode_settings_widget_is_hidden(tab) + self.set_autostart_timer(tab, 10) QtTest.QTest.mousePress(mode.server_status.server_button, QtCore.Qt.LeftButton) QtTest.QTest.qWait(2000) QtTest.QTest.mouseRelease( mode.server_status.server_button, QtCore.Qt.LeftButton ) self.assertEqual(mode.server_status.status, 0) - self.server_is_stopped(mode, False) - self.web_server_is_stopped() + self.server_is_stopped(tab) + self.web_server_is_stopped(tab) + + # Grouped tests follow from here + + def run_all_share_mode_setup_tests(self, tab): + """Tests in share mode prior to starting a share""" + tab.get_mode().server_status.file_selection.file_list.add_file(self.tmpfiles[0]) + tab.get_mode().server_status.file_selection.file_list.add_file(self.tmpfiles[1]) + self.file_selection_widget_has_files(tab, 2) + self.history_is_not_visible(tab) + self.click_toggle_history(tab) + self.history_is_visible(tab) + self.deleting_all_files_hides_delete_button(tab) + self.add_a_file_and_delete_using_its_delete_widget(tab) + self.file_selection_widget_read_files(tab) + + def run_all_share_mode_started_tests(self, tab, startup_time=2000): + """Tests in share mode after starting a share""" + self.server_working_on_start_button_pressed(tab) + self.server_status_indicator_says_starting(tab) + self.add_delete_buttons_hidden(tab) + self.mode_settings_widget_is_hidden(tab) + self.server_is_started(tab, startup_time) + self.web_server_is_running(tab) + self.have_a_password(tab) + self.url_description_shown(tab) + self.have_copy_url_button(tab) + self.server_status_indicator_says_started(tab) + + def run_all_share_mode_download_tests(self, tab): + """Tests in share mode after downloading a share""" + tab.get_mode().server_status.file_selection.file_list.add_file( + self.tmpfile_test + ) + self.web_page(tab, "Total size") + self.download_share(tab) + self.history_widgets_present(tab) + self.server_is_stopped(tab) + self.web_server_is_stopped(tab) + self.server_status_indicator_says_closed(tab) + self.add_button_visible(tab) + self.server_working_on_start_button_pressed(tab) + self.toggle_indicator_is_reset(tab) + self.server_is_started(tab) + self.history_indicator(tab) + + def run_all_share_mode_individual_file_download_tests(self, tab): + """Tests in share mode after downloading a share""" + self.web_page(tab, "Total size") + self.individual_file_is_viewable_or_not(tab) + self.history_widgets_present(tab) + self.server_is_stopped(tab) + self.web_server_is_stopped(tab) + self.server_status_indicator_says_closed(tab) + self.add_button_visible(tab) + self.server_working_on_start_button_pressed(tab) + self.server_is_started(tab) + self.history_indicator(tab) + + def run_all_share_mode_tests(self, tab): + """End-to-end share tests""" + self.run_all_share_mode_setup_tests(tab) + self.run_all_share_mode_started_tests(tab) + self.run_all_share_mode_download_tests(tab) + + def run_all_clear_all_button_tests(self, tab): + """Test the Clear All history button""" + self.run_all_share_mode_setup_tests(tab) + self.run_all_share_mode_started_tests(tab) + self.individual_file_is_viewable_or_not(tab) + self.history_widgets_present(tab) + self.clear_all_history_items(tab, 0) + self.individual_file_is_viewable_or_not(tab) + self.clear_all_history_items(tab, 2) + + def run_all_share_mode_individual_file_tests(self, tab): + """Tests in share mode when viewing an individual file""" + self.run_all_share_mode_setup_tests(tab) + self.run_all_share_mode_started_tests(tab) + self.run_all_share_mode_individual_file_download_tests(tab) + + def run_all_large_file_tests(self, tab): + """Same as above but with a larger file""" + self.run_all_share_mode_setup_tests(tab) + self.add_large_file(tab) + self.run_all_share_mode_started_tests(tab, startup_time=15000) + self.assertTrue(tab.filesize_warning.isVisible()) + self.server_is_stopped(tab) + self.web_server_is_stopped(tab) + self.server_status_indicator_says_closed(tab) + + def run_all_share_mode_persistent_tests(self, tab): + """Same as end-to-end share tests but also test the password is the same on multiple shared""" + self.run_all_share_mode_setup_tests(tab) + self.run_all_share_mode_started_tests(tab) + password = tab.get_mode().server_status.web.password + self.run_all_share_mode_download_tests(tab) + self.have_same_password(tab, password) + + def run_all_share_mode_timer_tests(self, tab): + """Auto-stop timer tests in share mode""" + self.run_all_share_mode_setup_tests(tab) + self.set_timeout(tab, 5) + self.run_all_share_mode_started_tests(tab) + self.autostop_timer_widget_hidden(tab) + self.server_timed_out(tab, 10000) + self.web_server_is_stopped(tab) + + def run_all_share_mode_autostart_timer_tests(self, tab): + """Auto-start timer tests in share mode""" + self.run_all_share_mode_setup_tests(tab) + self.set_autostart_timer(tab, 5) + self.server_working_on_start_button_pressed(tab) + self.autostart_timer_widget_hidden(tab) + self.server_status_indicator_says_scheduled(tab) + self.web_server_is_stopped(tab) + self.scheduled_service_started(tab, 7000) + self.web_server_is_running(tab) + + def run_all_share_mode_autostop_autostart_mismatch_tests(self, tab): + """Auto-stop timer tests in share mode""" + self.run_all_share_mode_setup_tests(tab) + self.set_autostart_timer(tab, 15) + self.set_timeout(tab, 5) + QtCore.QTimer.singleShot(4000, self.accept_dialog) + QtTest.QTest.mouseClick( + tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton + ) + self.server_is_stopped(tab, False) + + def run_all_share_mode_unreadable_file_tests(self, tab): + """Attempt to share an unreadable file""" + self.run_all_share_mode_setup_tests(tab) + QtCore.QTimer.singleShot(1000, self.accept_dialog) + tab.get_mode().server_status.file_selection.file_list.add_file( + "/tmp/nonexistent.txt" + ) + self.file_selection_widget_has_files(tab, 2) # Tests @pytest.mark.gui - def test_common_tests(self): - """Run all common tests""" - self.run_all_common_setup_tests() + def test_tmp(self): + tab = self.new_share_tab() + self.run_all_share_mode_setup_tests(tab) + self.run_all_share_mode_started_tests(tab) + self.run_all_share_mode_download_tests(tab) + self.run_all_share_mode_individual_file_download_tests(tab) + self.run_all_share_mode_tests(tab) + self.run_all_clear_all_button_tests(tab) + self.run_all_share_mode_individual_file_tests(tab) + self.run_all_large_file_tests(tab) + self.run_all_share_mode_persistent_tests(tab) + self.run_all_share_mode_timer_tests(tab) + self.run_all_share_mode_autostart_timer_tests(tab) + self.run_all_share_mode_autostop_autostart_mismatch_tests(tab) + self.run_all_share_mode_unreadable_file_tests(tab) + diff --git a/tests2/test_gui_tabs.py b/tests2/test_gui_tabs.py index eada8176..47a3d75d 100644 --- a/tests2/test_gui_tabs.py +++ b/tests2/test_gui_tabs.py @@ -9,54 +9,6 @@ from .gui_base_test import GuiBaseTest class TestTabs(GuiBaseTest): # Shared test methods - def verify_new_tab(self, tab): - # Make sure the new tab widget is showing, and no mode has been started - self.assertTrue(tab.new_tab.isVisible()) - self.assertFalse(hasattr(tab, "share_mode")) - self.assertFalse(hasattr(tab, "receive_mode")) - self.assertFalse(hasattr(tab, "website_mode")) - - def new_share_tab(self): - tab = self.gui.tabs.widget(0) - self.verify_new_tab(tab) - - # Share files - tab.share_button.click() - self.assertFalse(tab.new_tab.isVisible()) - self.assertTrue(tab.share_mode.isVisible()) - - # Add files - for filename in self.tmpfiles: - tab.share_mode.server_status.file_selection.file_list.add_file(filename) - - return tab - - def new_receive_tab(self): - tab = self.gui.tabs.widget(0) - self.verify_new_tab(tab) - - # Receive files - tab.receive_button.click() - self.assertFalse(tab.new_tab.isVisible()) - self.assertTrue(tab.receive_mode.isVisible()) - - return tab - - def new_website_tab(self): - tab = self.gui.tabs.widget(0) - self.verify_new_tab(tab) - - # Publish website - tab.website_button.click() - self.assertFalse(tab.new_tab.isVisible()) - self.assertTrue(tab.website_mode.isVisible()) - - # Add files - for filename in self.tmpfiles: - tab.website_mode.server_status.file_selection.file_list.add_file(filename) - - return tab - def close_tab_with_active_server(self, tab): # Start the server self.assertEqual( @@ -208,7 +160,7 @@ class TestTabs(GuiBaseTest): @pytest.mark.gui def test_07_close_share_tab_while_server_started_should_warn(self): """Closing a share mode tab when the server is running should throw a warning""" - tab = self.new_share_tab() + tab = self.new_share_tab_with_files() self.close_tab_with_active_server(tab) @pytest.mark.gui @@ -220,13 +172,13 @@ class TestTabs(GuiBaseTest): @pytest.mark.gui def test_09_close_website_tab_while_server_started_should_warn(self): """Closing a website mode tab when the server is running should throw a warning""" - tab = self.new_website_tab() + tab = self.new_website_tab_with_files() self.close_tab_with_active_server(tab) @pytest.mark.gui def test_10_close_persistent_share_tab_shows_warning(self): """Closing a share mode tab that's persistent should show a warning""" - tab = self.new_share_tab() + tab = self.new_share_tab_with_files() self.close_persistent_tab(tab) @pytest.mark.gui @@ -238,7 +190,7 @@ class TestTabs(GuiBaseTest): @pytest.mark.gui def test_12_close_persistent_website_tab_shows_warning(self): """Closing a website mode tab that's persistent should show a warning""" - tab = self.new_website_tab() + tab = self.new_website_tab_with_files() self.close_persistent_tab(tab) @pytest.mark.gui From 6c89ce2f28cd30a42491cd1bc5e2f85826b967c3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 19:59:20 +0800 Subject: [PATCH 066/135] Add test_autostart_and_autostop_timer_mismatch, and make it pass --- .../tab/mode/mode_settings_widget.py | 58 +++++++++------- tests2/gui_base_test.py | 6 +- tests2/test_gui_share.py | 66 +++++++++---------- 3 files changed, 70 insertions(+), 60 deletions(-) diff --git a/onionshare_gui/tab/mode/mode_settings_widget.py b/onionshare_gui/tab/mode/mode_settings_widget.py index 34a6db9a..881f893c 100644 --- a/onionshare_gui/tab/mode/mode_settings_widget.py +++ b/onionshare_gui/tab/mode/mode_settings_widget.py @@ -72,12 +72,7 @@ class ModeSettingsWidget(QtWidgets.QWidget): # The autostart timer widget self.autostart_timer_widget = QtWidgets.QDateTimeEdit() self.autostart_timer_widget.setDisplayFormat("hh:mm A MMM d, yy") - self.autostart_timer_widget.setDateTime( - QtCore.QDateTime.currentDateTime().addSecs(300) # 5 minutes in the future - ) - self.autostart_timer_widget.setMinimumDateTime( - QtCore.QDateTime.currentDateTime().addSecs(60) - ) + self.autostart_timer_reset() self.autostart_timer_widget.setCurrentSection( QtWidgets.QDateTimeEdit.MinuteSection ) @@ -105,12 +100,7 @@ class ModeSettingsWidget(QtWidgets.QWidget): # The autostop timer widget self.autostop_timer_widget = QtWidgets.QDateTimeEdit() self.autostop_timer_widget.setDisplayFormat("hh:mm A MMM d, yy") - self.autostop_timer_widget.setDateTime( - QtCore.QDateTime.currentDateTime().addSecs(300) - ) - self.autostop_timer_widget.setMinimumDateTime( - QtCore.QDateTime.currentDateTime().addSecs(60) - ) + self.autostop_timer_reset() self.autostop_timer_widget.setCurrentSection( QtWidgets.QDateTimeEdit.MinuteSection ) @@ -249,20 +239,40 @@ class ModeSettingsWidget(QtWidgets.QWidget): """ Reset the auto-start timer in the UI after stopping a share """ - self.autostart_timer_widget.setDateTime( - QtCore.QDateTime.currentDateTime().addSecs(300) - ) - self.autostart_timer_widget.setMinimumDateTime( - QtCore.QDateTime.currentDateTime().addSecs(60) - ) + if self.common.gui.local_only: + # For testing + self.autostart_timer_widget.setDateTime( + QtCore.QDateTime.currentDateTime().addSecs(15) + ) + self.autostart_timer_widget.setMinimumDateTime( + QtCore.QDateTime.currentDateTime() + ) + else: + self.autostart_timer_widget.setDateTime( + QtCore.QDateTime.currentDateTime().addSecs( + 300 + ) # 5 minutes in the future + ) + self.autostart_timer_widget.setMinimumDateTime( + QtCore.QDateTime.currentDateTime().addSecs(60) + ) def autostop_timer_reset(self): """ Reset the auto-stop timer in the UI after stopping a share """ - self.autostop_timer_widget.setDateTime( - QtCore.QDateTime.currentDateTime().addSecs(300) - ) - self.autostop_timer_widget.setMinimumDateTime( - QtCore.QDateTime.currentDateTime().addSecs(60) - ) + if self.common.gui.local_only: + # For testing + self.autostop_timer_widget.setDateTime( + QtCore.QDateTime.currentDateTime().addSecs(15) + ) + self.autostop_timer_widget.setMinimumDateTime( + QtCore.QDateTime.currentDateTime() + ) + else: + self.autostop_timer_widget.setDateTime( + QtCore.QDateTime.currentDateTime().addSecs(300) + ) + self.autostop_timer_widget.setMinimumDateTime( + QtCore.QDateTime.currentDateTime().addSecs(60) + ) diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index acc0b964..85f7672d 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -359,15 +359,15 @@ class GuiBaseTest(unittest.TestCase): def set_timeout(self, tab, timeout): """Test that the timeout can be set""" timer = QtCore.QDateTime.currentDateTime().addSecs(timeout) - tab.get_mode().server_status.autostop_timer_widget.setDateTime(timer) + tab.get_mode().mode_settings_widget.autostop_timer_widget.setDateTime(timer) self.assertTrue( - tab.get_mode().server_status.autostop_timer_widget.dateTime(), timer + tab.get_mode().mode_settings_widget.autostop_timer_widget.dateTime(), timer ) def autostop_timer_widget_hidden(self, tab): """Test that the auto-stop timer widget is hidden when share has started""" self.assertFalse( - tab.get_mode().server_status.autostop_timer_container.isVisible() + tab.get_mode().mode_settings_widget.autostop_timer_container.isVisible() ) def server_timed_out(self, tab, wait): diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index 09a40cb1..412367d0 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -191,21 +191,26 @@ class TestShare(GuiBaseTest): # Auto-start timer tests - def set_autostart_timer(self, tab, mode, timer): + def set_autostart_timer(self, tab, timer): """Test that the timer can be set""" schedule = QtCore.QDateTime.currentDateTime().addSecs(timer) - mode.server_status.autostart_timer_widget.setDateTime(schedule) - self.assertTrue(mode.server_status.autostart_timer_widget.dateTime(), schedule) + tab.get_mode().mode_settings_widget.autostart_timer_widget.setDateTime(schedule) + self.assertTrue( + tab.get_mode().mode_settings_widget.autostart_timer_widget.dateTime(), + schedule, + ) def autostart_timer_widget_hidden(self, tab, mode): """Test that the auto-start timer widget is hidden when share has started""" - self.assertFalse(mode.server_status.autostart_timer_container.isVisible()) + self.assertFalse( + tab.get_mode().mode_settings_widget.autostart_timer_widget.isVisible() + ) def scheduled_service_started(self, tab, mode, wait): """Test that the server has timed out after the timer ran out""" QtTest.QTest.qWait(wait) # We should have started now - self.assertEqual(mode.server_status.status, 2) + self.assertEqual(tab.get_mode().server_status.status, 2) def cancel_the_share(self, tab, mode): """Test that we can cancel a share before it's started up """ @@ -214,12 +219,14 @@ class TestShare(GuiBaseTest): self.add_delete_buttons_hidden(tab) self.mode_settings_widget_is_hidden(tab) self.set_autostart_timer(tab, 10) - QtTest.QTest.mousePress(mode.server_status.server_button, QtCore.Qt.LeftButton) + QtTest.QTest.mousePress( + tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton + ) QtTest.QTest.qWait(2000) QtTest.QTest.mouseRelease( - mode.server_status.server_button, QtCore.Qt.LeftButton + tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton ) - self.assertEqual(mode.server_status.status, 0) + self.assertEqual(tab.get_mode().server_status.status, 0) self.server_is_stopped(tab) self.web_server_is_stopped(tab) @@ -340,17 +347,6 @@ class TestShare(GuiBaseTest): self.scheduled_service_started(tab, 7000) self.web_server_is_running(tab) - def run_all_share_mode_autostop_autostart_mismatch_tests(self, tab): - """Auto-stop timer tests in share mode""" - self.run_all_share_mode_setup_tests(tab) - self.set_autostart_timer(tab, 15) - self.set_timeout(tab, 5) - QtCore.QTimer.singleShot(4000, self.accept_dialog) - QtTest.QTest.mouseClick( - tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton - ) - self.server_is_stopped(tab, False) - def run_all_share_mode_unreadable_file_tests(self, tab): """Attempt to share an unreadable file""" self.run_all_share_mode_setup_tests(tab) @@ -363,19 +359,23 @@ class TestShare(GuiBaseTest): # Tests @pytest.mark.gui - def test_tmp(self): + def test_autostart_and_autostop_timer_mismatch(self): tab = self.new_share_tab() - self.run_all_share_mode_setup_tests(tab) - self.run_all_share_mode_started_tests(tab) - self.run_all_share_mode_download_tests(tab) - self.run_all_share_mode_individual_file_download_tests(tab) - self.run_all_share_mode_tests(tab) - self.run_all_clear_all_button_tests(tab) - self.run_all_share_mode_individual_file_tests(tab) - self.run_all_large_file_tests(tab) - self.run_all_share_mode_persistent_tests(tab) - self.run_all_share_mode_timer_tests(tab) - self.run_all_share_mode_autostart_timer_tests(tab) - self.run_all_share_mode_autostop_autostart_mismatch_tests(tab) - self.run_all_share_mode_unreadable_file_tests(tab) + def accept_dialog(): + window = tab.common.gui.qtapp.activeWindow() + if window: + window.close() + + tab.get_mode().mode_settings_widget.toggle_advanced_button.click() + tab.get_mode().mode_settings_widget.autostart_timer_checkbox.click() + tab.get_mode().mode_settings_widget.autostop_timer_checkbox.click() + + self.run_all_common_setup_tests() + + self.run_all_share_mode_setup_tests(tab) + self.set_autostart_timer(tab, 15) + self.set_timeout(tab, 5) + QtCore.QTimer.singleShot(200, accept_dialog) + tab.get_mode().server_status.server_button.click() + self.server_is_stopped(tab) From 29ebdd850eb1a71183c7c4c9dbd9eb85b1eaefd4 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 20:06:24 +0800 Subject: [PATCH 067/135] Start adding test_autostart_timer --- tests2/gui_base_test.py | 4 ++++ tests2/test_gui_share.py | 37 +++++++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index 85f7672d..77aa65bd 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -109,6 +109,10 @@ class GuiBaseTest(unittest.TestCase): return tab + def close_all_tabs(self): + for _ in range(self.gui.tabs.count()): + self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() + def gui_loaded(self): """Test that the GUI actually is shown""" self.assertTrue(self.gui.show) diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index 412367d0..c8a7ebba 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -200,7 +200,7 @@ class TestShare(GuiBaseTest): schedule, ) - def autostart_timer_widget_hidden(self, tab, mode): + def autostart_timer_widget_hidden(self, tab): """Test that the auto-start timer widget is hidden when share has started""" self.assertFalse( tab.get_mode().mode_settings_widget.autostart_timer_widget.isVisible() @@ -336,17 +336,6 @@ class TestShare(GuiBaseTest): self.server_timed_out(tab, 10000) self.web_server_is_stopped(tab) - def run_all_share_mode_autostart_timer_tests(self, tab): - """Auto-start timer tests in share mode""" - self.run_all_share_mode_setup_tests(tab) - self.set_autostart_timer(tab, 5) - self.server_working_on_start_button_pressed(tab) - self.autostart_timer_widget_hidden(tab) - self.server_status_indicator_says_scheduled(tab) - self.web_server_is_stopped(tab) - self.scheduled_service_started(tab, 7000) - self.web_server_is_running(tab) - def run_all_share_mode_unreadable_file_tests(self, tab): """Attempt to share an unreadable file""" self.run_all_share_mode_setup_tests(tab) @@ -360,6 +349,9 @@ class TestShare(GuiBaseTest): @pytest.mark.gui def test_autostart_and_autostop_timer_mismatch(self): + """ + If autostart timer is after autostop timer, a warning should be thrown + """ tab = self.new_share_tab() def accept_dialog(): @@ -379,3 +371,24 @@ class TestShare(GuiBaseTest): QtCore.QTimer.singleShot(200, accept_dialog) tab.get_mode().server_status.server_button.click() self.server_is_stopped(tab) + + self.close_all_tabs() + + @pytest.mark.gui + def test_autostart_timer(self): + """ + Autostart timer should automatically start + """ + tab = self.new_share_tab() + self.run_all_common_setup_tests() + + self.run_all_share_mode_setup_tests(tab) + self.set_autostart_timer(tab, 5) + self.server_working_on_start_button_pressed(tab) + self.autostart_timer_widget_hidden(tab) + self.server_status_indicator_says_scheduled(tab) + self.web_server_is_stopped(tab) + self.scheduled_service_started(tab, 7000) + self.web_server_is_running(tab) + + self.close_all_tabs() From 4834e9352f788dc34236eb31e063cb703c7f2741 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 20:34:54 +0800 Subject: [PATCH 068/135] Fix bug with canceling scheduled share --- onionshare_gui/tab/server_status.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/onionshare_gui/tab/server_status.py b/onionshare_gui/tab/server_status.py index 2b2c2ec4..33e1f37c 100644 --- a/onionshare_gui/tab/server_status.py +++ b/onionshare_gui/tab/server_status.py @@ -398,8 +398,8 @@ class ServerStatus(QtWidgets.QWidget): "ServerStatus", "cancel_server", "Canceling the server mid-startup" ) self.status = self.STATUS_WORKING - self.autostart_timer_reset() - self.autostop_timer_reset() + self.mode_settings_widget.autostart_timer_reset() + self.mode_settings_widget.autostop_timer_reset() self.update() self.server_canceled.emit() From 09104c6a67b9cd60e989449693d05f82ad45043b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 20:44:52 +0800 Subject: [PATCH 069/135] Finished test_autostart_timer, and changed more clicks to use .click() --- tests2/gui_base_test.py | 26 +++++++------------- tests2/test_gui_share.py | 51 ++++++++++++++++++++-------------------- 2 files changed, 34 insertions(+), 43 deletions(-) diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index 77aa65bd..7cfa223c 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -136,14 +136,14 @@ class GuiBaseTest(unittest.TestCase): def click_toggle_history(self, tab): """Test that we can toggle Download or Upload history by clicking the toggle button""" currently_visible = tab.get_mode().history.isVisible() - QtTest.QTest.mouseClick(tab.get_mode().toggle_history, QtCore.Qt.LeftButton) + tab.get_mode().toggle_history.click() self.assertEqual(tab.get_mode().history.isVisible(), not currently_visible) def history_indicator(self, tab, indicator_count="1"): """Test that we can make sure the history is toggled off, do an action, and the indiciator works""" # Make sure history is toggled off if tab.get_mode().history.isVisible(): - QtTest.QTest.mouseClick(tab.get_mode().toggle_history, QtCore.Qt.LeftButton) + tab.get_mode().toggle_history.click() self.assertFalse(tab.get_mode().history.isVisible()) # Indicator should not be visible yet @@ -186,7 +186,7 @@ class GuiBaseTest(unittest.TestCase): ) # Toggle history back on, indicator should be hidden again - QtTest.QTest.mouseClick(tab.get_mode().toggle_history, QtCore.Qt.LeftButton) + tab.get_mode().toggle_history.click() self.assertFalse(tab.get_mode().toggle_history.indicator_label.isVisible()) def history_is_not_visible(self, tab): @@ -200,9 +200,7 @@ class GuiBaseTest(unittest.TestCase): def server_working_on_start_button_pressed(self, tab): """Test we can start the service""" # Should be in SERVER_WORKING state - QtTest.QTest.mouseClick( - tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton - ) + tab.get_mode().server_status.server_button.click() self.assertEqual(tab.get_mode().server_status.status, 1) def toggle_indicator_is_reset(self, tab): @@ -258,9 +256,7 @@ class GuiBaseTest(unittest.TestCase): """Test that the Copy URL button is shown and that the clipboard is correct""" self.assertTrue(tab.get_mode().server_status.copy_url_button.isVisible()) - QtTest.QTest.mouseClick( - tab.get_mode().server_status.copy_url_button, QtCore.Qt.LeftButton - ) + tab.get_mode().server_status.copy_url_button.click() clipboard = tab.common.gui.qtapp.clipboard() if tab.settings.get("general", "public"): self.assertEqual(clipboard.text(), f"http://127.0.0.1:{tab.app.port}") @@ -318,17 +314,15 @@ class GuiBaseTest(unittest.TestCase): ) or (type(tab.get_mode()) == WebsiteMode) ): - QtTest.QTest.mouseClick( - tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton - ) + tab.get_mode().server_status.server_button.click() self.assertEqual(tab.get_mode().server_status.status, 0) def web_server_is_stopped(self, tab): """Test that the web server also stopped""" - QtTest.QTest.qWait(2000) + QtTest.QTest.qWait(200) try: - r = requests.get(f"http://127.0.0.1:{tab.app.port}/") + requests.get(f"http://127.0.0.1:{tab.app.port}/") self.assertTrue(False) except requests.exceptions.ConnectionError: self.assertTrue(True) @@ -354,9 +348,7 @@ class GuiBaseTest(unittest.TestCase): def clear_all_history_items(self, tab, count): if count == 0: - QtTest.QTest.mouseClick( - tab.get_mode().history.clear_button, QtCore.Qt.LeftButton - ) + tab.get_mode().history.clear_button.click() self.assertEquals(len(tab.get_mode().history.item_list.items.keys()), count) # Auto-stop timer tests diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index c8a7ebba..55010b16 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -40,10 +40,7 @@ class TestShare(GuiBaseTest): tab.get_mode().server_status.file_selection.delete_button.isVisible() ) # Click delete, delete button should still be visible since we have one more file - QtTest.QTest.mouseClick( - tab.get_mode().server_status.file_selection.delete_button, - QtCore.Qt.LeftButton, - ) + tab.get_mode().server_status.file_selection.delete_button.click() rect = tab.get_mode().server_status.file_selection.file_list.visualItemRect( tab.get_mode().server_status.file_selection.file_list.item(0) ) @@ -55,10 +52,7 @@ class TestShare(GuiBaseTest): self.assertTrue( tab.get_mode().server_status.file_selection.delete_button.isVisible() ) - QtTest.QTest.mouseClick( - tab.get_mode().server_status.file_selection.delete_button, - QtCore.Qt.LeftButton, - ) + tab.get_mode().server_status.file_selection.delete_button.click() # No more files, the delete button should be hidden self.assertFalse( @@ -68,10 +62,9 @@ class TestShare(GuiBaseTest): def add_a_file_and_delete_using_its_delete_widget(self, tab): """Test that we can also delete a file by clicking on its [X] widget""" tab.get_mode().server_status.file_selection.file_list.add_file(self.tmpfiles[0]) - QtTest.QTest.mouseClick( - tab.get_mode().server_status.file_selection.file_list.item(0).item_button, - QtCore.Qt.LeftButton, - ) + tab.get_mode().server_status.file_selection.file_list.item( + 0 + ).item_button.click() self.file_selection_widget_has_files(tab, 0) def file_selection_widget_read_files(self, tab): @@ -116,7 +109,7 @@ class TestShare(GuiBaseTest): f.write(r.content) zip = zipfile.ZipFile(tmp_file.name) - QtTest.QTest.qWait(2000) + QtTest.QTest.qWait(50) self.assertEqual("onionshare", zip.read("test.txt").decode("utf-8")) def individual_file_is_viewable_or_not(self, tab): @@ -166,7 +159,7 @@ class TestShare(GuiBaseTest): self.assertEqual(r.status_code, 404) self.download_share(tab) - QtTest.QTest.qWait(2000) + QtTest.QTest.qWait(50) def hit_401(self, tab): """Test that the server stops after too many 401s, or doesn't when in public_mode""" @@ -180,7 +173,7 @@ class TestShare(GuiBaseTest): # A nasty hack to avoid the Alert dialog that blocks the rest of the test if not tab.settings.get("general", "public"): - QtCore.QTimer.singleShot(1000, self.accept_dialog) + QtCore.QTimer.singleShot(0, self.accept_dialog) # In public mode, we should still be running (no rate-limiting) if tab.settings.get("general", "public"): @@ -206,13 +199,13 @@ class TestShare(GuiBaseTest): tab.get_mode().mode_settings_widget.autostart_timer_widget.isVisible() ) - def scheduled_service_started(self, tab, mode, wait): + def scheduled_service_started(self, tab, wait): """Test that the server has timed out after the timer ran out""" QtTest.QTest.qWait(wait) # We should have started now self.assertEqual(tab.get_mode().server_status.status, 2) - def cancel_the_share(self, tab, mode): + def cancel_the_share(self, tab): """Test that we can cancel a share before it's started up """ self.server_working_on_start_button_pressed(tab) self.server_status_indicator_says_scheduled(tab) @@ -222,7 +215,7 @@ class TestShare(GuiBaseTest): QtTest.QTest.mousePress( tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton ) - QtTest.QTest.qWait(2000) + QtTest.QTest.qWait(50) QtTest.QTest.mouseRelease( tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton ) @@ -339,7 +332,7 @@ class TestShare(GuiBaseTest): def run_all_share_mode_unreadable_file_tests(self, tab): """Attempt to share an unreadable file""" self.run_all_share_mode_setup_tests(tab) - QtCore.QTimer.singleShot(1000, self.accept_dialog) + QtCore.QTimer.singleShot(0, self.accept_dialog) tab.get_mode().server_status.file_selection.file_list.add_file( "/tmp/nonexistent.txt" ) @@ -353,18 +346,16 @@ class TestShare(GuiBaseTest): If autostart timer is after autostop timer, a warning should be thrown """ tab = self.new_share_tab() + tab.get_mode().mode_settings_widget.toggle_advanced_button.click() + tab.get_mode().mode_settings_widget.autostart_timer_checkbox.click() + tab.get_mode().mode_settings_widget.autostop_timer_checkbox.click() def accept_dialog(): window = tab.common.gui.qtapp.activeWindow() if window: window.close() - tab.get_mode().mode_settings_widget.toggle_advanced_button.click() - tab.get_mode().mode_settings_widget.autostart_timer_checkbox.click() - tab.get_mode().mode_settings_widget.autostop_timer_checkbox.click() - self.run_all_common_setup_tests() - self.run_all_share_mode_setup_tests(tab) self.set_autostart_timer(tab, 15) self.set_timeout(tab, 5) @@ -380,15 +371,23 @@ class TestShare(GuiBaseTest): Autostart timer should automatically start """ tab = self.new_share_tab() + tab.get_mode().mode_settings_widget.toggle_advanced_button.click() + tab.get_mode().mode_settings_widget.autostart_timer_checkbox.click() + self.run_all_common_setup_tests() self.run_all_share_mode_setup_tests(tab) - self.set_autostart_timer(tab, 5) + self.set_autostart_timer(tab, 2) self.server_working_on_start_button_pressed(tab) self.autostart_timer_widget_hidden(tab) self.server_status_indicator_says_scheduled(tab) self.web_server_is_stopped(tab) - self.scheduled_service_started(tab, 7000) + self.scheduled_service_started(tab, 2200) self.web_server_is_running(tab) + QtTest.QTest.qWait(200) + tab.get_mode().server_status.server_button.click() + QtTest.QTest.qWait(200) + self.server_is_stopped(tab) + self.web_server_is_stopped(tab) self.close_all_tabs() From 08298d9ffdeade1fb5cf678e921d3a428c6d3a29 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 20:48:38 +0800 Subject: [PATCH 070/135] Added test_autostart_timer_too_short --- tests2/test_gui_share.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index 55010b16..a6f52db6 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -391,3 +391,26 @@ class TestShare(GuiBaseTest): self.web_server_is_stopped(tab) self.close_all_tabs() + + @pytest.mark.gui + def test_autostart_timer_too_short(self): + """ + Autostart timer should throw a warning if the scheduled time is too soon + """ + tab = self.new_share_tab() + tab.get_mode().mode_settings_widget.toggle_advanced_button.click() + tab.get_mode().mode_settings_widget.autostart_timer_checkbox.click() + + def accept_dialog(): + window = tab.common.gui.qtapp.activeWindow() + if window: + window.close() + + self.run_all_common_setup_tests() + self.run_all_share_mode_setup_tests(tab) + # Set a low timeout + self.set_autostart_timer(tab, 2) + QtTest.QTest.qWait(2200) + QtCore.QTimer.singleShot(200, accept_dialog) + tab.get_mode().server_status.server_button.click() + self.assertEqual(tab.get_mode().server_status.status, 0) From 44b534de6c1d534f2e85b54156e6a488ef5cfaee Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 20:55:05 +0800 Subject: [PATCH 071/135] Added test_autostart_timer_cancel --- tests2/gui_base_test.py | 10 ++++++++++ tests2/test_gui_share.py | 25 +++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index 7cfa223c..d112fb28 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -304,6 +304,16 @@ class GuiBaseTest(unittest.TestCase): """Test that the counter has incremented""" self.assertEqual(tab.get_mode().history.completed_count, count) + def stop_running_server(self, tab): + """Stop a server that's running""" + self.assertNotEqual(tab.get_mode().server_status.status, 0) + + tab.get_mode().server_status.server_button.click() + QtTest.QTest.qWait(200) + + self.server_is_stopped(tab) + self.web_server_is_stopped(tab) + def server_is_stopped(self, tab): """Test that the server stops when we click Stop""" if ( diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index a6f52db6..6811c483 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -215,7 +215,7 @@ class TestShare(GuiBaseTest): QtTest.QTest.mousePress( tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton ) - QtTest.QTest.qWait(50) + QtTest.QTest.qWait(100) QtTest.QTest.mouseRelease( tab.get_mode().server_status.server_button, QtCore.Qt.LeftButton ) @@ -384,12 +384,8 @@ class TestShare(GuiBaseTest): self.web_server_is_stopped(tab) self.scheduled_service_started(tab, 2200) self.web_server_is_running(tab) - QtTest.QTest.qWait(200) - tab.get_mode().server_status.server_button.click() - QtTest.QTest.qWait(200) - self.server_is_stopped(tab) - self.web_server_is_stopped(tab) + self.stop_running_server(tab) self.close_all_tabs() @pytest.mark.gui @@ -414,3 +410,20 @@ class TestShare(GuiBaseTest): QtCore.QTimer.singleShot(200, accept_dialog) tab.get_mode().server_status.server_button.click() self.assertEqual(tab.get_mode().server_status.status, 0) + + self.close_all_tabs() + + @pytest.mark.gui + def test_autostart_timer_cancel(self): + """ + Test canceling a scheduled share + """ + tab = self.new_share_tab() + tab.get_mode().mode_settings_widget.toggle_advanced_button.click() + tab.get_mode().mode_settings_widget.autostart_timer_checkbox.click() + + self.run_all_common_setup_tests() + self.run_all_share_mode_setup_tests(tab) + self.cancel_the_share(tab) + + self.close_all_tabs() From f0fd0857ae16dd4cf712c496196ed63c337ba06b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 21:36:54 +0800 Subject: [PATCH 072/135] Fix bug when handling a broken tor connection --- onionshare_gui/tab/tab.py | 5 +---- tests2/gui_base_test.py | 3 +++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 4bc45ac9..7c4926dd 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -380,10 +380,7 @@ class Tab(QtWidgets.QWidget): strings._("gui_tor_connection_lost"), strings._("gui_tor_connection_error_settings"), ) - - self.share_mode.handle_tor_broke() - self.receive_mode.handle_tor_broke() - self.website_mode.handle_tor_broke() + self.get_mode().handle_tor_broke() # Process events from the web object if self.mode == self.common.gui.MODE_SHARE: diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index d112fb28..3b27e928 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -46,7 +46,10 @@ class GuiBaseTest(unittest.TestCase): @classmethod def tearDownClass(cls): + # Quit + QtCore.QTimer.singleShot(0, cls.gui.close_dialog.accept_button.click) cls.gui.close() + cls.gui.cleanup() # Shared test methods From 1e98ccb696cc189741d92bd1b5c6016c63c25503 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 22:08:08 +0800 Subject: [PATCH 073/135] Add test_clear_all_button --- tests2/test_gui_share.py | 70 ++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index 6811c483..deefb4ec 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -19,7 +19,7 @@ class TestShare(GuiBaseTest): # Share-specific tests - def file_selection_widget_has_files(self, tab, num=2): + def file_selection_widget_has_files(self, tab, num=3): """Test that the number of items in the list is as expected""" self.assertEqual( tab.get_mode().server_status.file_selection.get_num_files(), num @@ -61,17 +61,19 @@ class TestShare(GuiBaseTest): def add_a_file_and_delete_using_its_delete_widget(self, tab): """Test that we can also delete a file by clicking on its [X] widget""" + num_files = tab.get_mode().server_status.file_selection.get_num_files() tab.get_mode().server_status.file_selection.file_list.add_file(self.tmpfiles[0]) tab.get_mode().server_status.file_selection.file_list.item( 0 ).item_button.click() - self.file_selection_widget_has_files(tab, 0) + self.file_selection_widget_has_files(tab, num_files) def file_selection_widget_read_files(self, tab): """Re-add some files to the list so we can share""" + num_files = tab.get_mode().server_status.file_selection.get_num_files() tab.get_mode().server_status.file_selection.file_list.add_file(self.tmpfiles[0]) tab.get_mode().server_status.file_selection.file_list.add_file(self.tmpfiles[1]) - self.file_selection_widget_has_files(tab, 2) + self.file_selection_widget_has_files(tab, num_files + 2) def add_large_file(self, tab): """Add a large file to the share""" @@ -113,7 +115,10 @@ class TestShare(GuiBaseTest): self.assertEqual("onionshare", zip.read("test.txt").decode("utf-8")) def individual_file_is_viewable_or_not(self, tab): - """Test whether an individual file is viewable (when in autostop_sharing is false) and that it isn't (when not in autostop_sharing is true)""" + """ + Test that an individual file is viewable (when in autostop_sharing is false) or that it + isn't (when not in autostop_sharing is true) + """ url = f"http://127.0.0.1:{tab.app.port}" download_file_url = f"http://127.0.0.1:{tab.app.port}/test.txt" if tab.settings.get("general", "public"): @@ -126,9 +131,21 @@ class TestShare(GuiBaseTest): ), ) - if not tab.settings.get("share", "autostop_sharing"): + if tab.settings.get("share", "autostop_sharing"): + self.assertFalse('a href="/test.txt"' in r.text) + if tab.settings.get("general", "public"): + r = requests.get(download_file_url) + else: + r = requests.get( + download_file_url, + auth=requests.auth.HTTPBasicAuth( + "onionshare", tab.get_mode().server_status.web.password + ), + ) + self.assertEqual(r.status_code, 404) + self.download_share(tab) + else: self.assertTrue('a href="test.txt"' in r.text) - if tab.settings.get("general", "public"): r = requests.get(download_file_url) else: @@ -145,21 +162,8 @@ class TestShare(GuiBaseTest): with open(tmp_file.name, "r") as f: self.assertEqual("onionshare", f.read()) - else: - self.assertFalse('a href="/test.txt"' in r.text) - if tab.settings.get("general", "public"): - r = requests.get(download_file_url) - else: - r = requests.get( - download_file_url, - auth=requests.auth.HTTPBasicAuth( - "onionshare", tab.get_mode().server_status.web.password - ), - ) - self.assertEqual(r.status_code, 404) - self.download_share(tab) - QtTest.QTest.qWait(50) + QtTest.QTest.qWait(500) def hit_401(self, tab): """Test that the server stops after too many 401s, or doesn't when in public_mode""" @@ -227,9 +231,12 @@ class TestShare(GuiBaseTest): def run_all_share_mode_setup_tests(self, tab): """Tests in share mode prior to starting a share""" + tab.get_mode().server_status.file_selection.file_list.add_file( + self.tmpfile_test + ) tab.get_mode().server_status.file_selection.file_list.add_file(self.tmpfiles[0]) tab.get_mode().server_status.file_selection.file_list.add_file(self.tmpfiles[1]) - self.file_selection_widget_has_files(tab, 2) + self.file_selection_widget_has_files(tab, 3) self.history_is_not_visible(tab) self.click_toggle_history(tab) self.history_is_visible(tab) @@ -290,7 +297,17 @@ class TestShare(GuiBaseTest): """Test the Clear All history button""" self.run_all_share_mode_setup_tests(tab) self.run_all_share_mode_started_tests(tab) + print( + "history items: {}".format( + len(tab.get_mode().history.item_list.items.keys()) + ) + ) self.individual_file_is_viewable_or_not(tab) + print( + "history items: {}".format( + len(tab.get_mode().history.item_list.items.keys()) + ) + ) self.history_widgets_present(tab) self.clear_all_history_items(tab, 0) self.individual_file_is_viewable_or_not(tab) @@ -427,3 +444,14 @@ class TestShare(GuiBaseTest): self.cancel_the_share(tab) self.close_all_tabs() + + @pytest.mark.gui + def test_clear_all_button(self): + """ + Test canceling a scheduled share + """ + tab = self.new_share_tab() + tab.get_mode().autostop_sharing_checkbox.click() + + self.run_all_common_setup_tests() + self.run_all_clear_all_button_tests(tab) From eff5fb1c4ab18e4b5a87d087f3e02545cdeb0b3b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 9 Nov 2019 00:42:49 +0800 Subject: [PATCH 074/135] Added test_public_mode --- tests2/gui_base_test.py | 14 +++----------- tests2/test_gui_share.py | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index 3b27e928..d505625c 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -114,6 +114,8 @@ class GuiBaseTest(unittest.TestCase): def close_all_tabs(self): for _ in range(self.gui.tabs.count()): + tab = self.gui.tabs.widget(0) + QtCore.QTimer.singleShot(0, tab.close_dialog.accept_button.click) self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() def gui_loaded(self): @@ -307,16 +309,6 @@ class GuiBaseTest(unittest.TestCase): """Test that the counter has incremented""" self.assertEqual(tab.get_mode().history.completed_count, count) - def stop_running_server(self, tab): - """Stop a server that's running""" - self.assertNotEqual(tab.get_mode().server_status.status, 0) - - tab.get_mode().server_status.server_button.click() - QtTest.QTest.qWait(200) - - self.server_is_stopped(tab) - self.web_server_is_stopped(tab) - def server_is_stopped(self, tab): """Test that the server stops when we click Stop""" if ( @@ -348,7 +340,7 @@ class GuiBaseTest(unittest.TestCase): strings._("gui_status_indicator_receive_stopped"), ) if type(tab.get_mode()) == ShareMode: - if tab.settings.get("share", "autostop_sharing"): + if not tab.settings.get("share", "autostop_sharing"): self.assertEqual( tab.get_mode().server_status_label.text(), strings._("gui_status_indicator_share_stopped"), diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index deefb4ec..3ca4871a 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -114,6 +114,8 @@ class TestShare(GuiBaseTest): QtTest.QTest.qWait(50) self.assertEqual("onionshare", zip.read("test.txt").decode("utf-8")) + QtTest.QTest.qWait(500) + def individual_file_is_viewable_or_not(self, tab): """ Test that an individual file is viewable (when in autostop_sharing is false) or that it @@ -402,7 +404,6 @@ class TestShare(GuiBaseTest): self.scheduled_service_started(tab, 2200) self.web_server_is_running(tab) - self.stop_running_server(tab) self.close_all_tabs() @pytest.mark.gui @@ -455,3 +456,18 @@ class TestShare(GuiBaseTest): self.run_all_common_setup_tests() self.run_all_clear_all_button_tests(tab) + + self.close_all_tabs() + + @pytest.mark.gui + def test_public_mode(self): + """ + Public mode shouldn't have a password + """ + tab = self.new_share_tab() + tab.get_mode().mode_settings_widget.public_checkbox.click() + + self.run_all_common_setup_tests() + self.run_all_share_mode_tests(tab) + + self.close_all_tabs() From e49786451a241e629394b38029f2ad474ae982d5 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 9 Nov 2019 00:53:03 +0800 Subject: [PATCH 075/135] Added test_autostop_sharing, test_download --- tests2/test_gui_share.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index 3ca4871a..c39327a7 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -471,3 +471,28 @@ class TestShare(GuiBaseTest): self.run_all_share_mode_tests(tab) self.close_all_tabs() + + @pytest.mark.gui + def test_autostop_sharing(self): + """ + Autostop sharing after first download + """ + tab = self.new_share_tab() + tab.get_mode().autostop_sharing_checkbox.click() + + self.run_all_common_setup_tests() + self.run_all_share_mode_tests(tab) + + self.close_all_tabs() + + @pytest.mark.gui + def test_download(self): + """ + Test downloading in share mode + """ + tab = self.new_share_tab() + + self.run_all_common_setup_tests() + self.run_all_share_mode_tests(tab) + + self.close_all_tabs() From b681b534205bf94646508f99074dc8d9e0d7891a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 9 Nov 2019 00:57:25 +0800 Subject: [PATCH 076/135] Added test_individual_files, test_individual_files_without_autostop_sharing --- tests2/test_gui_share.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index c39327a7..049068b2 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -473,9 +473,9 @@ class TestShare(GuiBaseTest): self.close_all_tabs() @pytest.mark.gui - def test_autostop_sharing(self): + def test_without_autostop_sharing(self): """ - Autostop sharing after first download + Disable autostop sharing after first download """ tab = self.new_share_tab() tab.get_mode().autostop_sharing_checkbox.click() @@ -496,3 +496,28 @@ class TestShare(GuiBaseTest): self.run_all_share_mode_tests(tab) self.close_all_tabs() + + @pytest.mark.gui + def test_individual_files_without_autostop_sharing(self): + """ + Test downloading individual files with autostop sharing disabled + """ + tab = self.new_share_tab() + tab.get_mode().autostop_sharing_checkbox.click() + + self.run_all_common_setup_tests() + self.run_all_share_mode_individual_file_tests(tab) + + self.close_all_tabs() + + @pytest.mark.gui + def test_individual_files(self): + """ + Test downloading individual files + """ + tab = self.new_share_tab() + + self.run_all_common_setup_tests() + self.run_all_share_mode_individual_file_tests(tab) + + self.close_all_tabs() From 78d5f4ff5060f649c112e5224e627b4859faf5a7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 9 Nov 2019 01:31:22 +0800 Subject: [PATCH 077/135] Clean up CLI test use of temporary files --- tests2/conftest.py | 37 +++++++++++++++++++++++----------- tests2/test_cli_settings.py | 10 +++++----- tests2/test_cli_web.py | 28 +++++++++++++------------- tests2/test_gui_share.py | 40 +++++++++++++++++++------------------ 4 files changed, 65 insertions(+), 50 deletions(-) diff --git a/tests2/conftest.py b/tests2/conftest.py index 8d8e77f6..7ebe95c8 100644 --- a/tests2/conftest.py +++ b/tests2/conftest.py @@ -15,6 +15,10 @@ import pytest from onionshare import common, web, settings, strings +# The temporary directory for CLI tests +test_temp_dir = None + + def pytest_addoption(parser): parser.addoption( "--rungui", action="store_true", default=False, help="run GUI tests" @@ -41,51 +45,60 @@ def pytest_collection_modifyitems(config, items): @pytest.fixture -def temp_dir_1024(): +def temp_dir(): + """Creates a persistent temporary directory for the CLI tests to use""" + global test_temp_dir + if not test_temp_dir: + test_temp_dir = tempfile.mkdtemp() + return test_temp_dir + + +@pytest.fixture +def temp_dir_1024(temp_dir): """ Create a temporary directory that has a single file of a particular size (1024 bytes). """ - tmp_dir = tempfile.mkdtemp() - tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir) + new_temp_dir = tempfile.mkdtemp(dir=temp_dir) + tmp_file, tmp_file_path = tempfile.mkstemp(dir=new_temp_dir) with open(tmp_file, "wb") as f: f.write(b"*" * 1024) - return tmp_dir + return new_temp_dir # pytest > 2.9 only needs @pytest.fixture @pytest.yield_fixture -def temp_dir_1024_delete(): +def temp_dir_1024_delete(temp_dir): """ Create a temporary directory that has a single file of a particular size (1024 bytes). The temporary directory (including the file inside) will be deleted after fixture usage. """ - with tempfile.TemporaryDirectory() as tmp_dir: - tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir) + with tempfile.TemporaryDirectory(dir=temp_dir) as new_temp_dir: + tmp_file, tmp_file_path = tempfile.mkstemp(dir=new_temp_dir) with open(tmp_file, "wb") as f: f.write(b"*" * 1024) - yield tmp_dir + yield new_temp_dir @pytest.fixture -def temp_file_1024(): +def temp_file_1024(temp_dir): """ Create a temporary file of a particular size (1024 bytes). """ - with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + with tempfile.NamedTemporaryFile(delete=False, dir=temp_dir) as tmp_file: tmp_file.write(b"*" * 1024) return tmp_file.name # pytest > 2.9 only needs @pytest.fixture @pytest.yield_fixture -def temp_file_1024_delete(): +def temp_file_1024_delete(temp_dir): """ Create a temporary file of a particular size (1024 bytes). The temporary file will be deleted after fixture usage. """ - with tempfile.NamedTemporaryFile() as tmp_file: + with tempfile.NamedTemporaryFile(dir=temp_dir) as tmp_file: tmp_file.write(b"*" * 1024) tmp_file.flush() yield tmp_file.name diff --git a/tests2/test_cli_settings.py b/tests2/test_cli_settings.py index 14269490..7a1e8de5 100644 --- a/tests2/test_cli_settings.py +++ b/tests2/test_cli_settings.py @@ -64,13 +64,13 @@ class TestSettings: settings_obj.fill_in_defaults() assert settings_obj._settings["version"] == "DUMMY_VERSION_1.2.3" - def test_load(self, settings_obj): + def test_load(self, temp_dir, settings_obj): custom_settings = { "version": "CUSTOM_VERSION", "socks_port": 9999, "use_stealth": True, } - tmp_file, tmp_file_path = tempfile.mkstemp() + tmp_file, tmp_file_path = tempfile.mkstemp(dir=temp_dir) with open(tmp_file, "w") as f: json.dump(custom_settings, f) settings_obj.filename = tmp_file_path @@ -83,12 +83,12 @@ class TestSettings: os.remove(tmp_file_path) assert os.path.exists(tmp_file_path) is False - def test_save(self, monkeypatch, settings_obj): + def test_save(self, monkeypatch, temp_dir, settings_obj): monkeypatch.setattr(strings, "_", lambda _: "") settings_filename = "default_settings.json" - tmp_dir = tempfile.gettempdir() - settings_path = os.path.join(tmp_dir, settings_filename) + new_temp_dir = tempfile.mkdtemp(dir=temp_dir) + settings_path = os.path.join(new_temp_dir, settings_filename) settings_obj.filename = settings_path settings_obj.save() with open(settings_path, "r") as f: diff --git a/tests2/test_cli_web.py b/tests2/test_cli_web.py index 2ce2f758..2e7d427b 100644 --- a/tests2/test_cli_web.py +++ b/tests2/test_cli_web.py @@ -42,7 +42,7 @@ DEFAULT_ZW_FILENAME_REGEX = re.compile(r"^onionshare_[a-z2-7]{6}.zip$") RANDOM_STR_REGEX = re.compile(r"^[a-z2-7]+$") -def web_obj(common_obj, mode, num_files=0): +def web_obj(temp_dir, common_obj, mode, num_files=0): """ Creates a Web object, in either share mode or receive mode, ready for testing """ common_obj.settings = Settings(common_obj) strings.load_strings(common_obj) @@ -58,7 +58,7 @@ def web_obj(common_obj, mode, num_files=0): # Add files files = [] for _ in range(num_files): - with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + with tempfile.NamedTemporaryFile(delete=False, dir=temp_dir) as tmp_file: tmp_file.write(b"*" * 1024) files.append(tmp_file.name) web.share_mode.set_file_info(files) @@ -70,8 +70,8 @@ def web_obj(common_obj, mode, num_files=0): class TestWeb: - def test_share_mode(self, common_obj): - web = web_obj(common_obj, "share", 3) + def test_share_mode(self, temp_dir, common_obj): + web = web_obj(temp_dir, common_obj, "share", 3) assert web.mode == "share" with web.app.test_client() as c: # Load / without auth @@ -95,8 +95,8 @@ class TestWeb: assert res.status_code == 200 assert res.mimetype == "application/zip" - def test_share_mode_autostop_sharing_on(self, common_obj, temp_file_1024): - web = web_obj(common_obj, "share", 3) + def test_share_mode_autostop_sharing_on(self, temp_dir, common_obj, temp_file_1024): + web = web_obj(temp_dir, common_obj, "share", 3) web.settings.set("share", "autostop_sharing", True) assert web.running == True @@ -110,8 +110,8 @@ class TestWeb: assert web.running == False - def test_share_mode_autostop_sharing_off(self, common_obj, temp_file_1024): - web = web_obj(common_obj, "share", 3) + def test_share_mode_autostop_sharing_off(self, temp_dir, common_obj, temp_file_1024): + web = web_obj(temp_dir, common_obj, "share", 3) web.settings.set("share", "autostop_sharing", False) assert web.running == True @@ -124,8 +124,8 @@ class TestWeb: assert res.mimetype == "application/zip" assert web.running == True - def test_receive_mode(self, common_obj): - web = web_obj(common_obj, "receive") + def test_receive_mode(self, temp_dir, common_obj): + web = web_obj(temp_dir, common_obj, "receive") assert web.mode == "receive" with web.app.test_client() as c: @@ -144,8 +144,8 @@ class TestWeb: res.get_data() assert res.status_code == 200 - def test_public_mode_on(self, common_obj): - web = web_obj(common_obj, "receive") + def test_public_mode_on(self, temp_dir, common_obj): + web = web_obj(temp_dir, common_obj, "receive") web.settings.set("general", "public", True) with web.app.test_client() as c: @@ -154,8 +154,8 @@ class TestWeb: data1 = res.get_data() assert res.status_code == 200 - def test_public_mode_off(self, common_obj): - web = web_obj(common_obj, "receive") + def test_public_mode_off(self, temp_dir, common_obj): + web = web_obj(temp_dir, common_obj, "receive") web.settings.set("general", "public", False) with web.app.test_client() as c: diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index 049068b2..ea20171b 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -75,15 +75,6 @@ class TestShare(GuiBaseTest): tab.get_mode().server_status.file_selection.file_list.add_file(self.tmpfiles[1]) self.file_selection_widget_has_files(tab, num_files + 2) - def add_large_file(self, tab): - """Add a large file to the share""" - size = 1024 * 1024 * 155 - with open("/tmp/large_file", "wb") as fout: - fout.write(os.urandom(size)) - tab.get_mode().server_status.file_selection.file_list.add_file( - "/tmp/large_file" - ) - def add_delete_buttons_hidden(self, tab): """Test that the add and delete buttons are hidden when the server starts""" self.assertFalse( @@ -321,16 +312,6 @@ class TestShare(GuiBaseTest): self.run_all_share_mode_started_tests(tab) self.run_all_share_mode_individual_file_download_tests(tab) - def run_all_large_file_tests(self, tab): - """Same as above but with a larger file""" - self.run_all_share_mode_setup_tests(tab) - self.add_large_file(tab) - self.run_all_share_mode_started_tests(tab, startup_time=15000) - self.assertTrue(tab.filesize_warning.isVisible()) - self.server_is_stopped(tab) - self.web_server_is_stopped(tab) - self.server_status_indicator_says_closed(tab) - def run_all_share_mode_persistent_tests(self, tab): """Same as end-to-end share tests but also test the password is the same on multiple shared""" self.run_all_share_mode_setup_tests(tab) @@ -521,3 +502,24 @@ class TestShare(GuiBaseTest): self.run_all_share_mode_individual_file_tests(tab) self.close_all_tabs() + + @pytest.mark.gui + def test_large_download(self): + """ + Test a large download + """ + tab = self.new_share_tab() + + self.run_all_common_setup_tests() + self.run_all_share_mode_setup_tests(tab) + tab.get_mode().server_status.file_selection.file_list.add_file( + self.tmpfile_large + ) + self.run_all_share_mode_started_tests(tab, startup_time=15000) + self.assertTrue(tab.get_mode().filesize_warning.isVisible()) + self.download_share(tab) + self.server_is_stopped(tab) + self.web_server_is_stopped(tab) + self.server_status_indicator_says_closed(tab) + + self.close_all_tabs() From 35880c0070b3e942a46fc8e8b6bb37ce72494c76 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 9 Nov 2019 01:38:35 +0800 Subject: [PATCH 078/135] Add test_large_download, and clean up some test code --- tests2/conftest.py | 5 ++++- tests2/gui_base_test.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests2/conftest.py b/tests2/conftest.py index 7ebe95c8..200f526d 100644 --- a/tests2/conftest.py +++ b/tests2/conftest.py @@ -124,7 +124,10 @@ def default_zw(): yield zw zw.close() tmp_dir = os.path.dirname(zw.zip_filename) - shutil.rmtree(tmp_dir) + try: + shutil.rmtree(tmp_dir, ignore_errors=True) + except: + pass @pytest.fixture diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index d505625c..f85c09bf 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -40,10 +40,18 @@ class GuiBaseTest(unittest.TestCase): with open(filename, "w") as file: file.write(secrets.token_hex(10)) cls.tmpfiles.append(filename) + + # A file called "test.txt" cls.tmpfile_test = os.path.join(cls.tmpdir.name, "test.txt") with open(cls.tmpfile_test, "w") as file: file.write("onionshare") + # A large file + size = 1024 * 1024 * 155 + cls.tmpfile_large = os.path.join(cls.tmpdir.name, "large_file") + with open(cls.tmpfile_large, "wb") as fout: + fout.write(os.urandom(size)) + @classmethod def tearDownClass(cls): # Quit @@ -51,6 +59,10 @@ class GuiBaseTest(unittest.TestCase): cls.gui.close() cls.gui.cleanup() + try: + shutil.rmtree(cls.tmpdir.name, ignore_errors=True) + except: + pass # Shared test methods @@ -354,7 +366,7 @@ class GuiBaseTest(unittest.TestCase): def clear_all_history_items(self, tab, count): if count == 0: tab.get_mode().history.clear_button.click() - self.assertEquals(len(tab.get_mode().history.item_list.items.keys()), count) + self.assertEqual(len(tab.get_mode().history.item_list.items.keys()), count) # Auto-stop timer tests def set_timeout(self, tab, timeout): From 48425e230f09cd066ab118b3acbb0b1682f312ab Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 10:10:22 -0800 Subject: [PATCH 079/135] Before running tests delete test common data dir, and after running tests stop trying to delete the tmpdir because it gets deteleted automatically --- tests2/gui_base_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index f85c09bf..2b3cdf89 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -27,6 +27,10 @@ class GuiBaseTest(unittest.TestCase): @classmethod def setUpClass(cls): common = Common(verbose=True) + + # Delete any old test data that might exist + shutil.rmtree(common.build_data_dir(), ignore_errors=True) + qtapp = Application(common) common.gui = GuiCommon(common, qtapp, local_only=True) cls.gui = MainWindow(common, filenames=None) @@ -59,10 +63,6 @@ class GuiBaseTest(unittest.TestCase): cls.gui.close() cls.gui.cleanup() - try: - shutil.rmtree(cls.tmpdir.name, ignore_errors=True) - except: - pass # Shared test methods From 53e5515f380f07b91f183fb295592d78d5fdfb03 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 10:25:24 -0800 Subject: [PATCH 080/135] Added test_persistent_password --- tests2/test_gui_share.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index ea20171b..65141ac3 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -12,13 +12,6 @@ from .gui_base_test import GuiBaseTest class TestShare(GuiBaseTest): # Shared test methods - # Persistence tests - def have_same_password(self, tab, password): - """Test that we have the same password""" - self.assertEqual(tab.get_mode().server_status.web.password, password) - - # Share-specific tests - def file_selection_widget_has_files(self, tab, num=3): """Test that the number of items in the list is as expected""" self.assertEqual( @@ -312,14 +305,6 @@ class TestShare(GuiBaseTest): self.run_all_share_mode_started_tests(tab) self.run_all_share_mode_individual_file_download_tests(tab) - def run_all_share_mode_persistent_tests(self, tab): - """Same as end-to-end share tests but also test the password is the same on multiple shared""" - self.run_all_share_mode_setup_tests(tab) - self.run_all_share_mode_started_tests(tab) - password = tab.get_mode().server_status.web.password - self.run_all_share_mode_download_tests(tab) - self.have_same_password(tab, password) - def run_all_share_mode_timer_tests(self, tab): """Auto-stop timer tests in share mode""" self.run_all_share_mode_setup_tests(tab) @@ -523,3 +508,22 @@ class TestShare(GuiBaseTest): self.server_status_indicator_says_closed(tab) self.close_all_tabs() + + @pytest.mark.gui + def test_persistent_password(self): + """ + Test a large download + """ + tab = self.new_share_tab() + tab.get_mode().mode_settings_widget.persistent_checkbox.click() + + self.run_all_common_setup_tests() + self.run_all_share_mode_setup_tests(tab) + self.run_all_share_mode_started_tests(tab) + password = tab.get_mode().server_status.web.password + self.run_all_share_mode_download_tests(tab) + self.run_all_share_mode_started_tests(tab) + self.assertEqual(tab.get_mode().server_status.web.password, password) + self.run_all_share_mode_download_tests(tab) + + self.close_all_tabs() From 366d77f37a4833e5371e6db4bb06ef73f99a7844 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 10:40:23 -0800 Subject: [PATCH 081/135] Added test_autostop_timer, test_autostop_timer_too_short, and test_unreadable_file --- tests2/gui_base_test.py | 6 +-- tests2/test_gui_share.py | 85 +++++++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 22 deletions(-) diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index 2b3cdf89..d7b5a438 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -59,7 +59,7 @@ class GuiBaseTest(unittest.TestCase): @classmethod def tearDownClass(cls): # Quit - QtCore.QTimer.singleShot(0, cls.gui.close_dialog.accept_button.click) + QtCore.QTimer.singleShot(200, cls.gui.close_dialog.accept_button.click) cls.gui.close() cls.gui.cleanup() @@ -127,7 +127,7 @@ class GuiBaseTest(unittest.TestCase): def close_all_tabs(self): for _ in range(self.gui.tabs.count()): tab = self.gui.tabs.widget(0) - QtCore.QTimer.singleShot(0, tab.close_dialog.accept_button.click) + QtCore.QTimer.singleShot(200, tab.close_dialog.accept_button.click) self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click() def gui_loaded(self): @@ -380,7 +380,7 @@ class GuiBaseTest(unittest.TestCase): def autostop_timer_widget_hidden(self, tab): """Test that the auto-stop timer widget is hidden when share has started""" self.assertFalse( - tab.get_mode().mode_settings_widget.autostop_timer_container.isVisible() + tab.get_mode().mode_settings_widget.autostop_timer_widget.isVisible() ) def server_timed_out(self, tab, wait): diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index 65141ac3..8191b8f1 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -163,7 +163,7 @@ class TestShare(GuiBaseTest): # A nasty hack to avoid the Alert dialog that blocks the rest of the test if not tab.settings.get("general", "public"): - QtCore.QTimer.singleShot(0, self.accept_dialog) + QtCore.QTimer.singleShot(200, self.accept_dialog) # In public mode, we should still be running (no rate-limiting) if tab.settings.get("general", "public"): @@ -305,24 +305,6 @@ class TestShare(GuiBaseTest): self.run_all_share_mode_started_tests(tab) self.run_all_share_mode_individual_file_download_tests(tab) - def run_all_share_mode_timer_tests(self, tab): - """Auto-stop timer tests in share mode""" - self.run_all_share_mode_setup_tests(tab) - self.set_timeout(tab, 5) - self.run_all_share_mode_started_tests(tab) - self.autostop_timer_widget_hidden(tab) - self.server_timed_out(tab, 10000) - self.web_server_is_stopped(tab) - - def run_all_share_mode_unreadable_file_tests(self, tab): - """Attempt to share an unreadable file""" - self.run_all_share_mode_setup_tests(tab) - QtCore.QTimer.singleShot(0, self.accept_dialog) - tab.get_mode().server_status.file_selection.file_list.add_file( - "/tmp/nonexistent.txt" - ) - self.file_selection_widget_has_files(tab, 2) - # Tests @pytest.mark.gui @@ -527,3 +509,68 @@ class TestShare(GuiBaseTest): self.run_all_share_mode_download_tests(tab) self.close_all_tabs() + + @pytest.mark.gui + def test_autostop_timer(self): + """ + Test the autostop timer + """ + tab = self.new_share_tab() + tab.get_mode().mode_settings_widget.toggle_advanced_button.click() + tab.get_mode().mode_settings_widget.autostop_timer_checkbox.click() + + self.run_all_common_setup_tests() + self.run_all_share_mode_setup_tests(tab) + self.set_timeout(tab, 5) + self.run_all_share_mode_started_tests(tab) + self.autostop_timer_widget_hidden(tab) + self.server_timed_out(tab, 10000) + self.web_server_is_stopped(tab) + + self.close_all_tabs() + + @pytest.mark.gui + def test_autostop_timer_too_short(self): + """ + Test the autostop timer when the timeout is too short + """ + tab = self.new_share_tab() + tab.get_mode().mode_settings_widget.toggle_advanced_button.click() + tab.get_mode().mode_settings_widget.autostop_timer_checkbox.click() + + def accept_dialog(): + window = tab.common.gui.qtapp.activeWindow() + if window: + window.close() + + self.run_all_common_setup_tests() + self.run_all_share_mode_setup_tests(tab) + # Set a low timeout + self.set_timeout(tab, 2) + QtTest.QTest.qWait(2100) + QtCore.QTimer.singleShot(2200, accept_dialog) + tab.get_mode().server_status.server_button.click() + self.assertEqual(tab.get_mode().server_status.status, 0) + + self.close_all_tabs() + + @pytest.mark.gui + def test_unreadable_file(self): + """ + Sharing an unreadable file should throw a warning + """ + tab = self.new_share_tab() + + def accept_dialog(): + window = tab.common.gui.qtapp.activeWindow() + if window: + window.close() + + self.run_all_share_mode_setup_tests(tab) + QtCore.QTimer.singleShot(200, accept_dialog) + tab.get_mode().server_status.file_selection.file_list.add_file( + "/tmp/nonexistent.txt" + ) + self.file_selection_widget_has_files(tab, 3) + + self.close_all_tabs() From 4305c0dbcfd92c1c4d9bf9d0c9065e96990f5eff Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 10:56:45 -0800 Subject: [PATCH 082/135] Added test_401_triggers_ratelimit, test_401_public_skips_ratelimit --- tests2/test_gui_share.py | 50 +++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index 8191b8f1..d2709807 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -152,28 +152,33 @@ class TestShare(GuiBaseTest): QtTest.QTest.qWait(500) def hit_401(self, tab): - """Test that the server stops after too many 401s, or doesn't when in public_mode""" - url = f"http://127.0.0.1:{tab.app.port}/" + """Test that the server stops after too many 401s, or doesn't when in public mode""" + # In non-public mode, get ready to accept the dialog + if not tab.settings.get("general", "public"): + def accept_dialog(): + window = tab.common.gui.qtapp.activeWindow() + if window: + window.close() + + QtCore.QTimer.singleShot(1000, accept_dialog) + + # Make 20 requests with guessed passwords + url = f"http://127.0.0.1:{tab.app.port}/" for _ in range(20): password_guess = self.gui.common.build_password() requests.get( url, auth=requests.auth.HTTPBasicAuth("onionshare", password_guess) ) - # A nasty hack to avoid the Alert dialog that blocks the rest of the test - if not tab.settings.get("general", "public"): - QtCore.QTimer.singleShot(200, self.accept_dialog) - # In public mode, we should still be running (no rate-limiting) if tab.settings.get("general", "public"): self.web_server_is_running(tab) + # In non-public mode, we should be shut down (rate-limiting) else: self.web_server_is_stopped(tab) - # Auto-start timer tests - def set_autostart_timer(self, tab, timer): """Test that the timer can be set""" schedule = QtCore.QDateTime.currentDateTime().addSecs(timer) @@ -574,3 +579,32 @@ class TestShare(GuiBaseTest): self.file_selection_widget_has_files(tab, 3) self.close_all_tabs() + + @pytest.mark.gui + def test_401_triggers_ratelimit(self): + """ + Rate limit should be triggered + """ + tab = self.new_share_tab() + tab.get_mode().autostop_sharing_checkbox.click() + + self.run_all_common_setup_tests() + self.run_all_share_mode_tests(tab) + self.hit_401(tab) + + self.close_all_tabs() + + @pytest.mark.gui + def test_401_public_skips_ratelimit(self): + """ + Public mode should skip the rate limit + """ + tab = self.new_share_tab() + tab.get_mode().autostop_sharing_checkbox.click() + tab.get_mode().mode_settings_widget.public_checkbox.click() + + self.run_all_common_setup_tests() + self.run_all_share_mode_tests(tab) + self.hit_401(tab) + + self.close_all_tabs() From 270ff21e7e15a45f195ed467bd182673dd59772a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 11:22:02 -0800 Subject: [PATCH 083/135] Start adding receive tests, including test_clear_all_button --- tests2/test_gui_receive.py | 178 +++++++++++++++++++++++++++++++++++++ tests2/test_gui_share.py | 10 --- 2 files changed, 178 insertions(+), 10 deletions(-) create mode 100644 tests2/test_gui_receive.py diff --git a/tests2/test_gui_receive.py b/tests2/test_gui_receive.py new file mode 100644 index 00000000..e0db1f2a --- /dev/null +++ b/tests2/test_gui_receive.py @@ -0,0 +1,178 @@ +import pytest +import os +import requests +from datetime import datetime, timedelta + +from PyQt5 import QtCore, QtTest + +from .gui_base_test import GuiBaseTest + + +class TestReceive(GuiBaseTest): + # Shared test methods + + def upload_file( + self, tab, file_to_upload, expected_basename, identical_files_at_once=False + ): + """Test that we can upload the file""" + + # Wait 2 seconds to make sure the filename, based on timestamp, isn't accidentally reused + QtTest.QTest.qWait(2000) + + files = {"file[]": open(file_to_upload, "rb")} + url = f"http://127.0.0.1:{tab.app.port}/upload" + if tab.settings.get("general", "public"): + r = requests.post(url, files=files) + if identical_files_at_once: + # Send a duplicate upload to test for collisions + r = requests.post(url, files=files) + else: + r = requests.post( + url, + files=files, + auth=requests.auth.HTTPBasicAuth( + "onionshare", tab.get_mode().web.password + ), + ) + if identical_files_at_once: + # Send a duplicate upload to test for collisions + r = requests.post( + url, + files=files, + auth=requests.auth.HTTPBasicAuth( + "onionshare", tab.get_mode().web.password + ), + ) + + QtTest.QTest.qWait(2000) + + # Make sure the file is within the last 10 seconds worth of fileames + exists = False + now = datetime.now() + for i in range(10): + date_dir = now.strftime("%Y-%m-%d") + if identical_files_at_once: + time_dir = now.strftime("%H.%M.%S-1") + else: + time_dir = now.strftime("%H.%M.%S") + receive_mode_dir = os.path.join( + tab.settings.get("receive", "data_dir"), date_dir, time_dir + ) + expected_filename = os.path.join(receive_mode_dir, expected_basename) + if os.path.exists(expected_filename): + exists = True + break + now = now - timedelta(seconds=1) + + self.assertTrue(exists) + + def upload_file_should_fail(self, tab): + """Test that we can't upload the file when permissions are wrong, and expected content is shown""" + files = {"file[]": open(self.tmpfile_test, "rb")} + url = f"http://127.0.0.1:{tab.app.port}/upload" + if tab.settings.get("general", "public"): + r = requests.post(url, files=files) + else: + r = requests.post( + url, + files=files, + auth=requests.auth.HTTPBasicAuth( + "onionshare", tab.get_mode().web.password + ), + ) + + QtCore.QTimer.singleShot(1000, self.accept_dialog) + self.assertTrue("Error uploading, please inform the OnionShare user" in r.text) + + def upload_dir_permissions(self, mode=0o755): + """Manipulate the permissions on the upload dir in between tests""" + os.chmod("/tmp/OnionShare", mode) + + def try_without_auth_in_non_public_mode(self, tab): + r = requests.post(f"http://127.0.0.1:{tab.app.port}/upload") + self.assertEqual(r.status_code, 401) + r = requests.get(f"http://127.0.0.1:{tab.app.port}/close") + self.assertEqual(r.status_code, 401) + + # 'Grouped' tests follow from here + + def run_all_receive_mode_setup_tests(self, tab): + """Set up a share in Receive mode and start it""" + self.history_is_not_visible(tab) + self.click_toggle_history(tab) + self.history_is_visible(tab) + self.server_working_on_start_button_pressed(tab) + self.server_status_indicator_says_starting(tab) + self.server_is_started(tab) + self.web_server_is_running(tab) + self.have_a_password(tab) + self.url_description_shown(tab) + self.have_copy_url_button(tab) + self.server_status_indicator_says_started(tab) + self.web_page(tab, "Select the files you want to send, then click") + + def run_all_receive_mode_tests(self, tab): + """Upload files in receive mode and stop the share""" + self.run_all_receive_mode_setup_tests(tab) + if not tab.settings.get("general", "public"): + self.try_without_auth_in_non_public_mode(tab) + self.upload_file(tab, self.tmpfile_test, "test.txt") + self.history_widgets_present(tab) + self.counter_incremented(tab, 1) + self.upload_file(tab, self.tmpfile_test, "test.txt") + self.counter_incremented(tab, 2) + self.upload_file(tab, "/tmp/testdir/test", "test") + self.counter_incremented(tab, 3) + self.upload_file(tab, "/tmp/testdir/test", "test") + self.counter_incremented(tab, 4) + # Test uploading the same file twice at the same time, and make sure no collisions + self.upload_file(tab, self.tmpfile_test, "test.txt", True) + self.counter_incremented(tab, 6) + self.history_indicator(tab, "2") + self.server_is_stopped(tab, False) + self.web_server_is_stopped(tab) + self.server_status_indicator_says_closed(tab) + self.server_working_on_start_button_pressed(tab) + self.server_is_started(tab) + self.history_indicator(tab, "2") + + def run_all_receive_mode_unwritable_dir_tests(self, tab): + """Attempt to upload (unwritable) files in receive mode and stop the share""" + self.run_all_receive_mode_setup_tests(tab) + self.upload_dir_permissions(0o400) + self.upload_file_should_fail(tab) + self.server_is_stopped(tab, True) + self.web_server_is_stopped(tab) + self.server_status_indicator_says_closed(tab, False) + self.upload_dir_permissions(0o755) + + def run_all_receive_mode_timer_tests(self, tab): + """Auto-stop timer tests in receive mode""" + self.run_all_receive_mode_setup_tests(tab) + self.set_timeout(tab, 5) + self.autostop_timer_widget_hidden(tab) + self.server_timed_out(tab, 15000) + self.web_server_is_stopped(tab) + + def run_all_clear_all_button_tests(self, tab): + """Test the Clear All history button""" + self.run_all_receive_mode_setup_tests(tab) + self.upload_file(tab, self.tmpfile_test, "test.txt") + self.history_widgets_present(tab) + self.clear_all_history_items(tab, 0) + self.upload_file(tab, self.tmpfile_test, "test.txt") + self.clear_all_history_items(tab, 2) + + # Tests + + @pytest.mark.gui + def test_clear_all_button(self): + """ + Public mode should skip the rate limit + """ + tab = self.new_receive_tab() + + self.run_all_common_setup_tests() + self.run_all_clear_all_button_tests(tab) + + self.close_all_tabs() diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index d2709807..1eaf9311 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -288,17 +288,7 @@ class TestShare(GuiBaseTest): """Test the Clear All history button""" self.run_all_share_mode_setup_tests(tab) self.run_all_share_mode_started_tests(tab) - print( - "history items: {}".format( - len(tab.get_mode().history.item_list.items.keys()) - ) - ) self.individual_file_is_viewable_or_not(tab) - print( - "history items: {}".format( - len(tab.get_mode().history.item_list.items.keys()) - ) - ) self.history_widgets_present(tab) self.clear_all_history_items(tab, 0) self.individual_file_is_viewable_or_not(tab) From 329339dab848b33503dcade9258965351ab7432a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 11:26:28 -0800 Subject: [PATCH 084/135] Add test_autostop_timer --- tests2/test_gui_receive.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/tests2/test_gui_receive.py b/tests2/test_gui_receive.py index e0db1f2a..13358086 100644 --- a/tests2/test_gui_receive.py +++ b/tests2/test_gui_receive.py @@ -146,14 +146,6 @@ class TestReceive(GuiBaseTest): self.server_status_indicator_says_closed(tab, False) self.upload_dir_permissions(0o755) - def run_all_receive_mode_timer_tests(self, tab): - """Auto-stop timer tests in receive mode""" - self.run_all_receive_mode_setup_tests(tab) - self.set_timeout(tab, 5) - self.autostop_timer_widget_hidden(tab) - self.server_timed_out(tab, 15000) - self.web_server_is_stopped(tab) - def run_all_clear_all_button_tests(self, tab): """Test the Clear All history button""" self.run_all_receive_mode_setup_tests(tab) @@ -168,7 +160,7 @@ class TestReceive(GuiBaseTest): @pytest.mark.gui def test_clear_all_button(self): """ - Public mode should skip the rate limit + Clear all history items should work """ tab = self.new_receive_tab() @@ -176,3 +168,21 @@ class TestReceive(GuiBaseTest): self.run_all_clear_all_button_tests(tab) self.close_all_tabs() + + @pytest.mark.gui + def test_autostop_timer(self): + """ + Test autostop timer + """ + tab = self.new_receive_tab() + tab.get_mode().mode_settings_widget.toggle_advanced_button.click() + tab.get_mode().mode_settings_widget.autostop_timer_checkbox.click() + + self.run_all_common_setup_tests() + self.run_all_receive_mode_setup_tests(tab) + self.set_timeout(tab, 5) + self.autostop_timer_widget_hidden(tab) + self.server_timed_out(tab, 15000) + self.web_server_is_stopped(tab) + + self.close_all_tabs() From 4dde85e4f6b0f3f94224d5ee2f2355b537cf974b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 11:33:59 -0800 Subject: [PATCH 085/135] Added test_upload --- tests2/gui_base_test.py | 5 +++++ tests2/test_gui_receive.py | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index d7b5a438..aaab7b80 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -50,6 +50,11 @@ class GuiBaseTest(unittest.TestCase): with open(cls.tmpfile_test, "w") as file: file.write("onionshare") + # A file called "test2.txt" + cls.tmpfile_test2 = os.path.join(cls.tmpdir.name, "test2.txt") + with open(cls.tmpfile_test2, "w") as file: + file.write("onionshare2") + # A large file size = 1024 * 1024 * 155 cls.tmpfile_large = os.path.join(cls.tmpdir.name, "large_file") diff --git a/tests2/test_gui_receive.py b/tests2/test_gui_receive.py index 13358086..026de6f7 100644 --- a/tests2/test_gui_receive.py +++ b/tests2/test_gui_receive.py @@ -121,15 +121,15 @@ class TestReceive(GuiBaseTest): self.counter_incremented(tab, 1) self.upload_file(tab, self.tmpfile_test, "test.txt") self.counter_incremented(tab, 2) - self.upload_file(tab, "/tmp/testdir/test", "test") + self.upload_file(tab, self.tmpfile_test2, "test2.txt") self.counter_incremented(tab, 3) - self.upload_file(tab, "/tmp/testdir/test", "test") + self.upload_file(tab, self.tmpfile_test2, "test2.txt") self.counter_incremented(tab, 4) # Test uploading the same file twice at the same time, and make sure no collisions self.upload_file(tab, self.tmpfile_test, "test.txt", True) self.counter_incremented(tab, 6) self.history_indicator(tab, "2") - self.server_is_stopped(tab, False) + self.server_is_stopped(tab) self.web_server_is_stopped(tab) self.server_status_indicator_says_closed(tab) self.server_working_on_start_button_pressed(tab) @@ -186,3 +186,15 @@ class TestReceive(GuiBaseTest): self.web_server_is_stopped(tab) self.close_all_tabs() + + @pytest.mark.gui + def test_upload(self): + """ + Test uploading files + """ + tab = self.new_receive_tab() + + self.run_all_common_setup_tests() + self.run_all_receive_mode_tests(tab) + + self.close_all_tabs() From 699884df80934446ee79bbe6b696b9ca7d064650 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 13:33:01 -0800 Subject: [PATCH 086/135] Add test_upload_non_writable_dir, test_public_upload, and test_public_upload_non_writable_dir --- tests2/gui_base_test.py | 1 + tests2/test_gui_receive.py | 92 ++++++++++++++++++++++++++++---------- 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index aaab7b80..dba528be 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -73,6 +73,7 @@ class GuiBaseTest(unittest.TestCase): def verify_new_tab(self, tab): # Make sure the new tab widget is showing, and no mode has been started + QtTest.QTest.qWait(500) self.assertTrue(tab.new_tab.isVisible()) self.assertFalse(hasattr(tab, "share_mode")) self.assertFalse(hasattr(tab, "receive_mode")) diff --git a/tests2/test_gui_receive.py b/tests2/test_gui_receive.py index 026de6f7..25f5cd3b 100644 --- a/tests2/test_gui_receive.py +++ b/tests2/test_gui_receive.py @@ -1,6 +1,7 @@ import pytest import os import requests +import shutil from datetime import datetime, timedelta from PyQt5 import QtCore, QtTest @@ -16,18 +17,18 @@ class TestReceive(GuiBaseTest): ): """Test that we can upload the file""" - # Wait 2 seconds to make sure the filename, based on timestamp, isn't accidentally reused - QtTest.QTest.qWait(2000) + # Wait 1.1 seconds to make sure the filename, based on timestamp, isn't accidentally reused + QtTest.QTest.qWait(1100) files = {"file[]": open(file_to_upload, "rb")} url = f"http://127.0.0.1:{tab.app.port}/upload" if tab.settings.get("general", "public"): - r = requests.post(url, files=files) + requests.post(url, files=files) if identical_files_at_once: # Send a duplicate upload to test for collisions - r = requests.post(url, files=files) + requests.post(url, files=files) else: - r = requests.post( + requests.post( url, files=files, auth=requests.auth.HTTPBasicAuth( @@ -36,7 +37,7 @@ class TestReceive(GuiBaseTest): ) if identical_files_at_once: # Send a duplicate upload to test for collisions - r = requests.post( + requests.post( url, files=files, auth=requests.auth.HTTPBasicAuth( @@ -44,12 +45,12 @@ class TestReceive(GuiBaseTest): ), ) - QtTest.QTest.qWait(2000) + QtTest.QTest.qWait(500) # Make sure the file is within the last 10 seconds worth of fileames exists = False now = datetime.now() - for i in range(10): + for _ in range(10): date_dir = now.strftime("%Y-%m-%d") if identical_files_at_once: time_dir = now.strftime("%H.%M.%S-1") @@ -81,12 +82,13 @@ class TestReceive(GuiBaseTest): ), ) - QtCore.QTimer.singleShot(1000, self.accept_dialog) - self.assertTrue("Error uploading, please inform the OnionShare user" in r.text) + def accept_dialog(): + window = tab.common.gui.qtapp.activeWindow() + if window: + window.close() - def upload_dir_permissions(self, mode=0o755): - """Manipulate the permissions on the upload dir in between tests""" - os.chmod("/tmp/OnionShare", mode) + QtCore.QTimer.singleShot(200, accept_dialog) + self.assertTrue("Error uploading, please inform the OnionShare user" in r.text) def try_without_auth_in_non_public_mode(self, tab): r = requests.post(f"http://127.0.0.1:{tab.app.port}/upload") @@ -136,16 +138,6 @@ class TestReceive(GuiBaseTest): self.server_is_started(tab) self.history_indicator(tab, "2") - def run_all_receive_mode_unwritable_dir_tests(self, tab): - """Attempt to upload (unwritable) files in receive mode and stop the share""" - self.run_all_receive_mode_setup_tests(tab) - self.upload_dir_permissions(0o400) - self.upload_file_should_fail(tab) - self.server_is_stopped(tab, True) - self.web_server_is_stopped(tab) - self.server_status_indicator_says_closed(tab, False) - self.upload_dir_permissions(0o755) - def run_all_clear_all_button_tests(self, tab): """Test the Clear All history button""" self.run_all_receive_mode_setup_tests(tab) @@ -155,6 +147,24 @@ class TestReceive(GuiBaseTest): self.upload_file(tab, self.tmpfile_test, "test.txt") self.clear_all_history_items(tab, 2) + def run_all_upload_non_writable_dir_tests(self, tab): + """Test uploading a file when the data_dir is non-writable""" + upload_dir = os.path.join(self.tmpdir.name, "OnionShare") + shutil.rmtree(upload_dir, ignore_errors=True) + os.makedirs(upload_dir, 0o700) + + # Set the upload dir setting + tab.get_mode().data_dir_lineedit.setText(upload_dir) + tab.settings.set("receive", "data_dir", upload_dir) + + self.run_all_receive_mode_setup_tests(tab) + os.chmod(upload_dir, 0o400) + self.upload_file_should_fail(tab) + self.server_is_stopped(tab) + self.web_server_is_stopped(tab) + self.server_status_indicator_says_closed(tab) + os.chmod(upload_dir, 0o700) + # Tests @pytest.mark.gui @@ -198,3 +208,39 @@ class TestReceive(GuiBaseTest): self.run_all_receive_mode_tests(tab) self.close_all_tabs() + + @pytest.mark.gui + def test_upload_non_writable_dir(self): + """ + Test uploading files to a non-writable directory + """ + tab = self.new_receive_tab() + + self.run_all_upload_non_writable_dir_tests(tab) + + self.close_all_tabs() + + @pytest.mark.gui + def test_public_upload(self): + """ + Test uploading files in public mode + """ + tab = self.new_receive_tab() + tab.get_mode().mode_settings_widget.public_checkbox.click() + + self.run_all_common_setup_tests() + self.run_all_receive_mode_tests(tab) + + self.close_all_tabs() + + @pytest.mark.gui + def test_public_upload_non_writable_dir(self): + """ + Test uploading files to a non-writable directory in public mode + """ + tab = self.new_receive_tab() + tab.get_mode().mode_settings_widget.public_checkbox.click() + + self.run_all_upload_non_writable_dir_tests(tab) + + self.close_all_tabs() From 29620cb39c0de65dee632b30e86e5131eabe8486 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 13:36:35 -0800 Subject: [PATCH 087/135] When shutting down the web server, only use basic auth if there is a password -- this avoids warnings when running tests --- onionshare/web/web.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/onionshare/web/web.py b/onionshare/web/web.py index a143a22f..bfdd2cac 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -386,10 +386,15 @@ class Web: # To stop flask, load http://shutdown:[shutdown_password]@127.0.0.1/[shutdown_password]/shutdown # (We're putting the shutdown_password in the path as well to make routing simpler) if self.running: - requests.get( - f"http://127.0.0.1:{port}/{self.shutdown_password}/shutdown", - auth=requests.auth.HTTPBasicAuth("onionshare", self.password), - ) + if self.password: + requests.get( + f"http://127.0.0.1:{port}/{self.shutdown_password}/shutdown", + auth=requests.auth.HTTPBasicAuth("onionshare", self.password), + ) + else: + requests.get( + f"http://127.0.0.1:{port}/{self.shutdown_password}/shutdown" + ) # Reset any password that was in use self.password = None From 05df88bf89b4b697f6a371bc3bbc6ea3de929166 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 13:51:08 -0800 Subject: [PATCH 088/135] Add website tests --- tests2/gui_base_test.py | 22 ++++++++ tests2/test_gui_share.py | 15 ------ tests2/test_gui_website.py | 106 +++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 tests2/test_gui_website.py diff --git a/tests2/gui_base_test.py b/tests2/gui_base_test.py index dba528be..dc883c05 100644 --- a/tests2/gui_base_test.py +++ b/tests2/gui_base_test.py @@ -55,6 +55,13 @@ class GuiBaseTest(unittest.TestCase): with open(cls.tmpfile_test2, "w") as file: file.write("onionshare2") + # A file called "index.html" + cls.tmpfile_index_html = os.path.join(cls.tmpdir.name, "index.html") + with open(cls.tmpfile_index_html, "w") as file: + file.write( + "

This is a test website hosted by OnionShare

" + ) + # A large file size = 1024 * 1024 * 155 cls.tmpfile_large = os.path.join(cls.tmpdir.name, "large_file") @@ -374,6 +381,21 @@ class GuiBaseTest(unittest.TestCase): tab.get_mode().history.clear_button.click() self.assertEqual(len(tab.get_mode().history.item_list.items.keys()), count) + def file_selection_widget_has_files(self, tab, num=3): + """Test that the number of items in the list is as expected""" + self.assertEqual( + tab.get_mode().server_status.file_selection.get_num_files(), num + ) + + def add_delete_buttons_hidden(self, tab): + """Test that the add and delete buttons are hidden when the server starts""" + self.assertFalse( + tab.get_mode().server_status.file_selection.add_button.isVisible() + ) + self.assertFalse( + tab.get_mode().server_status.file_selection.delete_button.isVisible() + ) + # Auto-stop timer tests def set_timeout(self, tab, timeout): """Test that the timeout can be set""" diff --git a/tests2/test_gui_share.py b/tests2/test_gui_share.py index 1eaf9311..387931ec 100644 --- a/tests2/test_gui_share.py +++ b/tests2/test_gui_share.py @@ -12,12 +12,6 @@ from .gui_base_test import GuiBaseTest class TestShare(GuiBaseTest): # Shared test methods - def file_selection_widget_has_files(self, tab, num=3): - """Test that the number of items in the list is as expected""" - self.assertEqual( - tab.get_mode().server_status.file_selection.get_num_files(), num - ) - def deleting_all_files_hides_delete_button(self, tab): """Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button""" rect = tab.get_mode().server_status.file_selection.file_list.visualItemRect( @@ -68,15 +62,6 @@ class TestShare(GuiBaseTest): tab.get_mode().server_status.file_selection.file_list.add_file(self.tmpfiles[1]) self.file_selection_widget_has_files(tab, num_files + 2) - def add_delete_buttons_hidden(self, tab): - """Test that the add and delete buttons are hidden when the server starts""" - self.assertFalse( - tab.get_mode().server_status.file_selection.add_button.isVisible() - ) - self.assertFalse( - tab.get_mode().server_status.file_selection.delete_button.isVisible() - ) - def download_share(self, tab): """Test that we can download the share""" url = f"http://127.0.0.1:{tab.app.port}/download" diff --git a/tests2/test_gui_website.py b/tests2/test_gui_website.py new file mode 100644 index 00000000..c88a4910 --- /dev/null +++ b/tests2/test_gui_website.py @@ -0,0 +1,106 @@ +import pytest +import os +import requests +import shutil +from datetime import datetime, timedelta + +from PyQt5 import QtCore, QtTest + +from .gui_base_test import GuiBaseTest + + +class TestWebsite(GuiBaseTest): + # Shared test methods + + def view_website(self, tab): + """Test that we can download the share""" + url = f"http://127.0.0.1:{tab.app.port}/" + if tab.settings.get("general", "public"): + r = requests.get(url) + else: + r = requests.get( + url, + auth=requests.auth.HTTPBasicAuth( + "onionshare", tab.get_mode().server_status.web.password + ), + ) + + QtTest.QTest.qWait(500) + self.assertTrue("This is a test website hosted by OnionShare" in r.text) + + def check_csp_header(self, tab): + """Test that the CSP header is present when enabled or vice versa""" + url = f"http://127.0.0.1:{tab.app.port}/" + if tab.settings.get("general", "public"): + r = requests.get(url) + else: + r = requests.get( + url, + auth=requests.auth.HTTPBasicAuth( + "onionshare", tab.get_mode().server_status.web.password + ), + ) + + QtTest.QTest.qWait(500) + if tab.settings.get("website", "disable_csp"): + self.assertFalse("Content-Security-Policy" in r.headers) + else: + self.assertTrue("Content-Security-Policy" in r.headers) + + def run_all_website_mode_setup_tests(self, tab): + """Tests in website mode prior to starting a share""" + tab.get_mode().server_status.file_selection.file_list.add_file( + self.tmpfile_index_html + ) + for filename in self.tmpfiles: + tab.get_mode().server_status.file_selection.file_list.add_file(filename) + + self.file_selection_widget_has_files(tab, 11) + self.history_is_not_visible(tab) + self.click_toggle_history(tab) + self.history_is_visible(tab) + + def run_all_website_mode_started_tests(self, tab, startup_time=500): + """Tests in website mode after starting a share""" + self.server_working_on_start_button_pressed(tab) + self.server_status_indicator_says_starting(tab) + self.add_delete_buttons_hidden(tab) + self.server_is_started(tab, startup_time) + self.web_server_is_running(tab) + self.have_a_password(tab) + self.url_description_shown(tab) + self.have_copy_url_button(tab) + self.server_status_indicator_says_started(tab) + + def run_all_website_mode_download_tests(self, tab): + """Tests in website mode after viewing the site""" + self.run_all_website_mode_setup_tests(tab) + self.run_all_website_mode_started_tests(tab, startup_time=500) + self.view_website(tab) + self.check_csp_header(tab) + self.history_widgets_present(tab) + self.server_is_stopped(tab) + self.web_server_is_stopped(tab) + self.server_status_indicator_says_closed(tab) + self.add_button_visible(tab) + + # Tests + + @pytest.mark.gui + def test_website(self): + """ + Test website mode + """ + tab = self.new_website_tab() + self.run_all_website_mode_download_tests(tab) + self.close_all_tabs() + + @pytest.mark.gui + def test_csp_enabled(self): + """ + Test disabling CSP + """ + tab = self.new_website_tab() + tab.get_mode().disable_csp_checkbox.click() + self.run_all_website_mode_download_tests(tab) + self.close_all_tabs() From 16245d33e3b0c886d1201c0ef88a7c14b8d5b40e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 13:55:15 -0800 Subject: [PATCH 089/135] Delete the old tests and replace them with the new tests --- tests/GuiBaseTest.py | 383 ------------------ tests/GuiReceiveTest.py | 169 -------- tests/GuiShareTest.py | 328 --------------- tests/GuiWebsiteTest.py | 136 ------- tests/SettingsGuiBaseTest.py | 333 --------------- tests/TorGuiBaseTest.py | 176 -------- tests/TorGuiReceiveTest.py | 61 --- tests/TorGuiShareTest.py | 91 ----- tests/conftest.py | 45 +- {tests2 => tests}/gui_base_test.py | 0 ...re_401_public_mode_skips_ratelimit_test.py | 27 -- ..._onionshare_401_triggers_ratelimit_test.py | 27 -- ...tting_during_share_prompts_warning_test.py | 34 -- ...hare_receive_mode_clear_all_button_test.py | 26 -- ...ocal_onionshare_receive_mode_timer_test.py | 26 -- ...ceive_mode_upload_non_writable_dir_test.py | 26 -- ...pload_public_mode_non_writable_dir_test.py | 26 -- ...re_receive_mode_upload_public_mode_test.py | 26 -- ...cal_onionshare_receive_mode_upload_test.py | 26 -- ...onshare_settings_dialog_legacy_tor_test.py | 27 -- ..._onionshare_settings_dialog_no_tor_test.py | 27 -- ..._onionshare_settings_dialog_v3_tor_test.py | 27 -- ...ostart_and_autostop_timer_mismatch_test.py | 30 -- ...onshare_share_mode_autostart_timer_test.py | 26 -- ...are_mode_autostart_timer_too_short_test.py | 35 -- ...onionshare_share_mode_cancel_share_test.py | 27 -- ...nshare_share_mode_clear_all_button_test.py | 26 -- ...re_share_mode_download_public_mode_test.py | 26 -- ...hare_share_mode_download_stay_open_test.py | 26 -- ...cal_onionshare_share_mode_download_test.py | 26 -- ...ode_individual_file_view_stay_open_test.py | 26 -- ...re_share_mode_individual_file_view_test.py | 26 -- ...ionshare_share_mode_large_download_test.py | 26 -- ...are_share_mode_password_persistent_test.py | 31 -- .../local_onionshare_share_mode_timer_test.py | 26 -- ...onshare_share_mode_timer_too_short_test.py | 35 -- ...onshare_share_mode_unreadable_file_test.py | 26 -- ...nionshare_website_mode_csp_enabled_test.py | 26 -- tests/local_onionshare_website_mode_test.py | 26 -- ...onshare_790_cancel_on_second_share_test.py | 30 -- ...re_receive_mode_upload_public_mode_test.py | 27 -- tests/onionshare_receive_mode_upload_test.py | 27 -- ...onionshare_share_mode_cancel_share_test.py | 28 -- ...re_share_mode_download_public_mode_test.py | 27 -- ...hare_share_mode_download_stay_open_test.py | 27 -- tests/onionshare_share_mode_download_test.py | 27 -- .../onionshare_share_mode_persistent_test.py | 33 -- tests/onionshare_share_mode_stealth_test.py | 30 -- tests/onionshare_share_mode_timer_test.py | 27 -- ...e_share_mode_tor_connection_killed_test.py | 27 -- tests/onionshare_share_mode_v2_onion_test.py | 28 -- {tests2 => tests}/test_cli.py | 0 {tests2 => tests}/test_cli_common.py | 0 {tests2 => tests}/test_cli_settings.py | 0 {tests2 => tests}/test_cli_strings.py | 0 {tests2 => tests}/test_cli_web.py | 0 {tests2 => tests}/test_gui_receive.py | 0 {tests2 => tests}/test_gui_share.py | 0 {tests2 => tests}/test_gui_tabs.py | 0 {tests2 => tests}/test_gui_website.py | 0 tests/test_helpers.py | 38 -- tests/test_onionshare.py | 75 ---- tests/test_onionshare_common.py | 312 -------------- tests/test_onionshare_settings.py | 166 -------- tests/test_onionshare_strings.py | 65 --- tests/test_onionshare_web.py | 241 ----------- tests2/__init__.py | 0 tests2/conftest.py | 208 ---------- 68 files changed, 32 insertions(+), 3928 deletions(-) delete mode 100644 tests/GuiBaseTest.py delete mode 100644 tests/GuiReceiveTest.py delete mode 100644 tests/GuiShareTest.py delete mode 100644 tests/GuiWebsiteTest.py delete mode 100644 tests/SettingsGuiBaseTest.py delete mode 100644 tests/TorGuiBaseTest.py delete mode 100644 tests/TorGuiReceiveTest.py delete mode 100644 tests/TorGuiShareTest.py rename {tests2 => tests}/gui_base_test.py (100%) delete mode 100644 tests/local_onionshare_401_public_mode_skips_ratelimit_test.py delete mode 100644 tests/local_onionshare_401_triggers_ratelimit_test.py delete mode 100644 tests/local_onionshare_quitting_during_share_prompts_warning_test.py delete mode 100644 tests/local_onionshare_receive_mode_clear_all_button_test.py delete mode 100644 tests/local_onionshare_receive_mode_timer_test.py delete mode 100644 tests/local_onionshare_receive_mode_upload_non_writable_dir_test.py delete mode 100644 tests/local_onionshare_receive_mode_upload_public_mode_non_writable_dir_test.py delete mode 100644 tests/local_onionshare_receive_mode_upload_public_mode_test.py delete mode 100644 tests/local_onionshare_receive_mode_upload_test.py delete mode 100644 tests/local_onionshare_settings_dialog_legacy_tor_test.py delete mode 100644 tests/local_onionshare_settings_dialog_no_tor_test.py delete mode 100644 tests/local_onionshare_settings_dialog_v3_tor_test.py delete mode 100644 tests/local_onionshare_share_mode_autostart_and_autostop_timer_mismatch_test.py delete mode 100644 tests/local_onionshare_share_mode_autostart_timer_test.py delete mode 100644 tests/local_onionshare_share_mode_autostart_timer_too_short_test.py delete mode 100644 tests/local_onionshare_share_mode_cancel_share_test.py delete mode 100644 tests/local_onionshare_share_mode_clear_all_button_test.py delete mode 100644 tests/local_onionshare_share_mode_download_public_mode_test.py delete mode 100644 tests/local_onionshare_share_mode_download_stay_open_test.py delete mode 100644 tests/local_onionshare_share_mode_download_test.py delete mode 100644 tests/local_onionshare_share_mode_individual_file_view_stay_open_test.py delete mode 100644 tests/local_onionshare_share_mode_individual_file_view_test.py delete mode 100644 tests/local_onionshare_share_mode_large_download_test.py delete mode 100644 tests/local_onionshare_share_mode_password_persistent_test.py delete mode 100644 tests/local_onionshare_share_mode_timer_test.py delete mode 100644 tests/local_onionshare_share_mode_timer_too_short_test.py delete mode 100644 tests/local_onionshare_share_mode_unreadable_file_test.py delete mode 100644 tests/local_onionshare_website_mode_csp_enabled_test.py delete mode 100644 tests/local_onionshare_website_mode_test.py delete mode 100644 tests/onionshare_790_cancel_on_second_share_test.py delete mode 100644 tests/onionshare_receive_mode_upload_public_mode_test.py delete mode 100644 tests/onionshare_receive_mode_upload_test.py delete mode 100644 tests/onionshare_share_mode_cancel_share_test.py delete mode 100644 tests/onionshare_share_mode_download_public_mode_test.py delete mode 100644 tests/onionshare_share_mode_download_stay_open_test.py delete mode 100644 tests/onionshare_share_mode_download_test.py delete mode 100644 tests/onionshare_share_mode_persistent_test.py delete mode 100644 tests/onionshare_share_mode_stealth_test.py delete mode 100644 tests/onionshare_share_mode_timer_test.py delete mode 100644 tests/onionshare_share_mode_tor_connection_killed_test.py delete mode 100644 tests/onionshare_share_mode_v2_onion_test.py rename {tests2 => tests}/test_cli.py (100%) rename {tests2 => tests}/test_cli_common.py (100%) rename {tests2 => tests}/test_cli_settings.py (100%) rename {tests2 => tests}/test_cli_strings.py (100%) rename {tests2 => tests}/test_cli_web.py (100%) rename {tests2 => tests}/test_gui_receive.py (100%) rename {tests2 => tests}/test_gui_share.py (100%) rename {tests2 => tests}/test_gui_tabs.py (100%) rename {tests2 => tests}/test_gui_website.py (100%) delete mode 100644 tests/test_helpers.py delete mode 100644 tests/test_onionshare.py delete mode 100644 tests/test_onionshare_common.py delete mode 100644 tests/test_onionshare_settings.py delete mode 100644 tests/test_onionshare_strings.py delete mode 100644 tests/test_onionshare_web.py delete mode 100644 tests2/__init__.py delete mode 100644 tests2/conftest.py diff --git a/tests/GuiBaseTest.py b/tests/GuiBaseTest.py deleted file mode 100644 index 068bb5c5..00000000 --- a/tests/GuiBaseTest.py +++ /dev/null @@ -1,383 +0,0 @@ -import json -import os -import requests -import shutil -import base64 - -from PyQt5 import QtCore, QtTest - -from onionshare import strings -from onionshare.common import Common -from onionshare.settings import Settings -from onionshare.onion import Onion -from onionshare.web import Web -from onionshare_gui import Application, OnionShare, MainWindow -from onionshare_gui.mode.share_mode import ShareMode -from onionshare_gui.mode.receive_mode import ReceiveMode -from onionshare_gui.mode.website_mode import WebsiteMode - - -class GuiBaseTest(object): - @staticmethod - def set_up(test_settings): - """Create GUI with given settings""" - # Create our test file - testfile = open("/tmp/test.txt", "w") - testfile.write("onionshare") - testfile.close() - - # Create a test dir and files - if not os.path.exists("/tmp/testdir"): - testdir = os.mkdir("/tmp/testdir") - testfile = open("/tmp/testdir/test", "w") - testfile.write("onionshare") - testfile.close() - - common = Common() - common.settings = Settings(common) - common.define_css() - strings.load_strings(common) - - # Get all of the settings in test_settings - test_settings["data_dir"] = "/tmp/OnionShare" - for key, val in common.settings.default_settings.items(): - if key not in test_settings: - test_settings[key] = val - - # Start the Onion - testonion = Onion(common) - global qtapp - qtapp = Application(common) - app = OnionShare(common, testonion, True, 0) - - web = Web(common, False, True) - open("/tmp/settings.json", "w").write(json.dumps(test_settings)) - - gui = MainWindow( - common, - testonion, - qtapp, - app, - ["/tmp/test.txt", "/tmp/testdir"], - "/tmp/settings.json", - True, - ) - return gui - - @staticmethod - def tear_down(): - """Clean up after tests""" - try: - os.remove("/tmp/test.txt") - os.remove("/tmp/settings.json") - os.remove("/tmp/large_file") - os.remove("/tmp/download.zip") - os.remove("/tmp/webpage") - shutil.rmtree("/tmp/testdir") - shutil.rmtree("/tmp/OnionShare") - except: - pass - - def gui_loaded(self): - """Test that the GUI actually is shown""" - self.assertTrue(self.gui.show) - - def windowTitle_seen(self): - """Test that the window title is OnionShare""" - self.assertEqual(self.gui.windowTitle(), "OnionShare") - - def settings_button_is_visible(self): - """Test that the settings button is visible""" - self.assertTrue(self.gui.settings_button.isVisible()) - - def settings_button_is_hidden(self): - """Test that the settings button is hidden when the server starts""" - self.assertFalse(self.gui.settings_button.isVisible()) - - def server_status_bar_is_visible(self): - """Test that the status bar is visible""" - self.assertTrue(self.gui.status_bar.isVisible()) - - def click_mode(self, mode): - """Test that we can switch Mode by clicking the button""" - if type(mode) == ReceiveMode: - QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) - self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) - if type(mode) == ShareMode: - QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton) - self.assertTrue(self.gui.mode, self.gui.MODE_SHARE) - if type(mode) == WebsiteMode: - QtTest.QTest.mouseClick(self.gui.website_mode_button, QtCore.Qt.LeftButton) - self.assertTrue(self.gui.mode, self.gui.MODE_WEBSITE) - - def click_toggle_history(self, mode): - """Test that we can toggle Download or Upload history by clicking the toggle button""" - currently_visible = mode.history.isVisible() - QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton) - self.assertEqual(mode.history.isVisible(), not currently_visible) - - def history_indicator(self, mode, public_mode, indicator_count="1"): - """Test that we can make sure the history is toggled off, do an action, and the indiciator works""" - # Make sure history is toggled off - if mode.history.isVisible(): - QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton) - self.assertFalse(mode.history.isVisible()) - - # Indicator should not be visible yet - self.assertFalse(mode.toggle_history.indicator_label.isVisible()) - - if type(mode) == ReceiveMode: - # Upload a file - files = {"file[]": open("/tmp/test.txt", "rb")} - url = f"http://127.0.0.1:{self.gui.app.port}/upload" - if public_mode: - r = requests.post(url, files=files) - else: - r = requests.post( - url, - files=files, - auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password), - ) - QtTest.QTest.qWait(2000) - - if type(mode) == ShareMode: - # Download files - url = f"http://127.0.0.1:{self.gui.app.port}/download" - if public_mode: - r = requests.get(url) - else: - r = requests.get( - url, - auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password), - ) - QtTest.QTest.qWait(2000) - - # Indicator should be visible, have a value of "1" - self.assertTrue(mode.toggle_history.indicator_label.isVisible()) - self.assertEqual(mode.toggle_history.indicator_label.text(), indicator_count) - - # Toggle history back on, indicator should be hidden again - QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton) - self.assertFalse(mode.toggle_history.indicator_label.isVisible()) - - def history_is_not_visible(self, mode): - """Test that the History section is not visible""" - self.assertFalse(mode.history.isVisible()) - - def history_is_visible(self, mode): - """Test that the History section is visible""" - self.assertTrue(mode.history.isVisible()) - - def server_working_on_start_button_pressed(self, mode): - """Test we can start the service""" - # Should be in SERVER_WORKING state - QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEqual(mode.server_status.status, 1) - - def toggle_indicator_is_reset(self, mode): - self.assertEqual(mode.toggle_history.indicator_count, 0) - self.assertFalse(mode.toggle_history.indicator_label.isVisible()) - - def server_status_indicator_says_starting(self, mode): - """Test that the Server Status indicator shows we are Starting""" - self.assertEqual( - mode.server_status_label.text(), - strings._("gui_status_indicator_share_working"), - ) - - def server_status_indicator_says_scheduled(self, mode): - """Test that the Server Status indicator shows we are Scheduled""" - self.assertEqual( - mode.server_status_label.text(), - strings._("gui_status_indicator_share_scheduled"), - ) - - def server_is_started(self, mode, startup_time=2000): - """Test that the server has started""" - QtTest.QTest.qWait(startup_time) - # Should now be in SERVER_STARTED state - self.assertEqual(mode.server_status.status, 2) - - def web_server_is_running(self): - """Test that the web server has started""" - try: - r = requests.get(f"http://127.0.0.1:{self.gui.app.port}/") - self.assertTrue(True) - except requests.exceptions.ConnectionError: - self.assertTrue(False) - - def have_a_password(self, mode, public_mode): - """Test that we have a valid password""" - if not public_mode: - self.assertRegex(mode.server_status.web.password, r"(\w+)-(\w+)") - else: - self.assertIsNone(mode.server_status.web.password, r"(\w+)-(\w+)") - - def add_button_visible(self, mode): - """Test that the add button should be visible""" - self.assertTrue(mode.server_status.file_selection.add_button.isVisible()) - - def url_description_shown(self, mode): - """Test that the URL label is showing""" - self.assertTrue(mode.server_status.url_description.isVisible()) - - def have_copy_url_button(self, mode, public_mode): - """Test that the Copy URL button is shown and that the clipboard is correct""" - self.assertTrue(mode.server_status.copy_url_button.isVisible()) - - QtTest.QTest.mouseClick( - mode.server_status.copy_url_button, QtCore.Qt.LeftButton - ) - clipboard = self.gui.qtapp.clipboard() - if public_mode: - self.assertEqual(clipboard.text(), f"http://127.0.0.1:{self.gui.app.port}") - else: - self.assertEqual( - clipboard.text(), - f"http://onionshare:{mode.server_status.web.password}@127.0.0.1:{self.gui.app.port}", - ) - - def server_status_indicator_says_started(self, mode): - """Test that the Server Status indicator shows we are started""" - if type(mode) == ReceiveMode: - self.assertEqual( - mode.server_status_label.text(), - strings._("gui_status_indicator_receive_started"), - ) - if type(mode) == ShareMode: - self.assertEqual( - mode.server_status_label.text(), - strings._("gui_status_indicator_share_started"), - ) - - def web_page(self, mode, string, public_mode): - """Test that the web page contains a string""" - - url = f"http://127.0.0.1:{self.gui.app.port}/" - if public_mode: - r = requests.get(url) - else: - r = requests.get( - url, auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password) - ) - - self.assertTrue(string in r.text) - - def history_widgets_present(self, mode): - """Test that the relevant widgets are present in the history view after activity has taken place""" - self.assertFalse(mode.history.empty.isVisible()) - self.assertTrue(mode.history.not_empty.isVisible()) - - def counter_incremented(self, mode, count): - """Test that the counter has incremented""" - self.assertEqual(mode.history.completed_count, count) - - def server_is_stopped(self, mode, stay_open): - """Test that the server stops when we click Stop""" - if ( - type(mode) == ReceiveMode - or (type(mode) == ShareMode and stay_open) - or (type(mode) == WebsiteMode) - ): - QtTest.QTest.mouseClick( - mode.server_status.server_button, QtCore.Qt.LeftButton - ) - self.assertEqual(mode.server_status.status, 0) - - def web_server_is_stopped(self): - """Test that the web server also stopped""" - QtTest.QTest.qWait(2000) - - try: - r = requests.get(f"http://127.0.0.1:{self.gui.app.port}/") - self.assertTrue(False) - except requests.exceptions.ConnectionError: - self.assertTrue(True) - - def server_status_indicator_says_closed(self, mode, stay_open): - """Test that the Server Status indicator shows we closed""" - if type(mode) == ReceiveMode: - self.assertEqual( - self.gui.receive_mode.server_status_label.text(), - strings._("gui_status_indicator_receive_stopped"), - ) - if type(mode) == ShareMode: - if stay_open: - self.assertEqual( - self.gui.share_mode.server_status_label.text(), - strings._("gui_status_indicator_share_stopped"), - ) - else: - self.assertEqual( - self.gui.share_mode.server_status_label.text(), - strings._("closing_automatically"), - ) - - def clear_all_history_items(self, mode, count): - if count == 0: - QtTest.QTest.mouseClick(mode.history.clear_button, QtCore.Qt.LeftButton) - self.assertEquals(len(mode.history.item_list.items.keys()), count) - - # Auto-stop timer tests - def set_timeout(self, mode, timeout): - """Test that the timeout can be set""" - timer = QtCore.QDateTime.currentDateTime().addSecs(timeout) - mode.server_status.autostop_timer_widget.setDateTime(timer) - self.assertTrue(mode.server_status.autostop_timer_widget.dateTime(), timer) - - def autostop_timer_widget_hidden(self, mode): - """Test that the auto-stop timer widget is hidden when share has started""" - self.assertFalse(mode.server_status.autostop_timer_container.isVisible()) - - def server_timed_out(self, mode, wait): - """Test that the server has timed out after the timer ran out""" - QtTest.QTest.qWait(wait) - # We should have timed out now - self.assertEqual(mode.server_status.status, 0) - - # Auto-start timer tests - def set_autostart_timer(self, mode, timer): - """Test that the timer can be set""" - schedule = QtCore.QDateTime.currentDateTime().addSecs(timer) - mode.server_status.autostart_timer_widget.setDateTime(schedule) - self.assertTrue(mode.server_status.autostart_timer_widget.dateTime(), schedule) - - def autostart_timer_widget_hidden(self, mode): - """Test that the auto-start timer widget is hidden when share has started""" - self.assertFalse(mode.server_status.autostart_timer_container.isVisible()) - - def scheduled_service_started(self, mode, wait): - """Test that the server has timed out after the timer ran out""" - QtTest.QTest.qWait(wait) - # We should have started now - self.assertEqual(mode.server_status.status, 2) - - def cancel_the_share(self, mode): - """Test that we can cancel a share before it's started up """ - self.server_working_on_start_button_pressed(mode) - self.server_status_indicator_says_scheduled(mode) - self.add_delete_buttons_hidden() - self.settings_button_is_hidden() - self.set_autostart_timer(mode, 10) - QtTest.QTest.mousePress(mode.server_status.server_button, QtCore.Qt.LeftButton) - QtTest.QTest.qWait(2000) - QtTest.QTest.mouseRelease( - mode.server_status.server_button, QtCore.Qt.LeftButton - ) - self.assertEqual(mode.server_status.status, 0) - self.server_is_stopped(mode, False) - self.web_server_is_stopped() - - # Hack to close an Alert dialog that would otherwise block tests - def accept_dialog(self): - window = self.gui.qtapp.activeWindow() - if window: - window.close() - - # 'Grouped' tests follow from here - - def run_all_common_setup_tests(self): - self.gui_loaded() - self.windowTitle_seen() - self.settings_button_is_visible() - self.server_status_bar_is_visible() diff --git a/tests/GuiReceiveTest.py b/tests/GuiReceiveTest.py deleted file mode 100644 index 380702fd..00000000 --- a/tests/GuiReceiveTest.py +++ /dev/null @@ -1,169 +0,0 @@ -import os -import requests -from datetime import datetime, timedelta -from PyQt5 import QtCore, QtTest -from .GuiBaseTest import GuiBaseTest - - -class GuiReceiveTest(GuiBaseTest): - def upload_file( - self, - public_mode, - file_to_upload, - expected_basename, - identical_files_at_once=False, - ): - """Test that we can upload the file""" - - # Wait 2 seconds to make sure the filename, based on timestamp, isn't accidentally reused - QtTest.QTest.qWait(2000) - - files = {"file[]": open(file_to_upload, "rb")} - url = f"http://127.0.0.1:{self.gui.app.port}/upload" - if public_mode: - r = requests.post(url, files=files) - if identical_files_at_once: - # Send a duplicate upload to test for collisions - r = requests.post(url, files=files) - else: - r = requests.post( - url, - files=files, - auth=requests.auth.HTTPBasicAuth( - "onionshare", self.gui.receive_mode.web.password - ), - ) - if identical_files_at_once: - # Send a duplicate upload to test for collisions - r = requests.post( - url, - files=files, - auth=requests.auth.HTTPBasicAuth( - "onionshare", self.gui.receive_mode.web.password - ), - ) - - QtTest.QTest.qWait(2000) - - # Make sure the file is within the last 10 seconds worth of fileames - exists = False - now = datetime.now() - for i in range(10): - date_dir = now.strftime("%Y-%m-%d") - if identical_files_at_once: - time_dir = now.strftime("%H.%M.%S-1") - else: - time_dir = now.strftime("%H.%M.%S") - receive_mode_dir = os.path.join( - self.gui.common.settings.get("data_dir"), date_dir, time_dir - ) - expected_filename = os.path.join(receive_mode_dir, expected_basename) - if os.path.exists(expected_filename): - exists = True - break - now = now - timedelta(seconds=1) - - self.assertTrue(exists) - - def upload_file_should_fail(self, public_mode): - """Test that we can't upload the file when permissions are wrong, and expected content is shown""" - files = {"file[]": open("/tmp/test.txt", "rb")} - url = f"http://127.0.0.1:{self.gui.app.port}/upload" - if public_mode: - r = requests.post(url, files=files) - else: - r = requests.post( - url, - files=files, - auth=requests.auth.HTTPBasicAuth( - "onionshare", self.gui.receive_mode.web.password - ), - ) - - QtCore.QTimer.singleShot(1000, self.accept_dialog) - self.assertTrue("Error uploading, please inform the OnionShare user" in r.text) - - def upload_dir_permissions(self, mode=0o755): - """Manipulate the permissions on the upload dir in between tests""" - os.chmod("/tmp/OnionShare", mode) - - def try_without_auth_in_non_public_mode(self): - r = requests.post(f"http://127.0.0.1:{self.gui.app.port}/upload") - self.assertEqual(r.status_code, 401) - r = requests.get(f"http://127.0.0.1:{self.gui.app.port}/close") - self.assertEqual(r.status_code, 401) - - # 'Grouped' tests follow from here - - def run_all_receive_mode_setup_tests(self, public_mode): - """Set up a share in Receive mode and start it""" - self.click_mode(self.gui.receive_mode) - self.history_is_not_visible(self.gui.receive_mode) - self.click_toggle_history(self.gui.receive_mode) - self.history_is_visible(self.gui.receive_mode) - self.server_working_on_start_button_pressed(self.gui.receive_mode) - self.server_status_indicator_says_starting(self.gui.receive_mode) - self.settings_button_is_hidden() - self.server_is_started(self.gui.receive_mode) - self.web_server_is_running() - self.have_a_password(self.gui.receive_mode, public_mode) - self.url_description_shown(self.gui.receive_mode) - self.have_copy_url_button(self.gui.receive_mode, public_mode) - self.server_status_indicator_says_started(self.gui.receive_mode) - self.web_page( - self.gui.receive_mode, - "Select the files you want to send, then click", - public_mode, - ) - - def run_all_receive_mode_tests(self, public_mode): - """Upload files in receive mode and stop the share""" - self.run_all_receive_mode_setup_tests(public_mode) - if not public_mode: - self.try_without_auth_in_non_public_mode() - self.upload_file(public_mode, "/tmp/test.txt", "test.txt") - self.history_widgets_present(self.gui.receive_mode) - self.counter_incremented(self.gui.receive_mode, 1) - self.upload_file(public_mode, "/tmp/test.txt", "test.txt") - self.counter_incremented(self.gui.receive_mode, 2) - self.upload_file(public_mode, "/tmp/testdir/test", "test") - self.counter_incremented(self.gui.receive_mode, 3) - self.upload_file(public_mode, "/tmp/testdir/test", "test") - self.counter_incremented(self.gui.receive_mode, 4) - # Test uploading the same file twice at the same time, and make sure no collisions - self.upload_file(public_mode, "/tmp/test.txt", "test.txt", True) - self.counter_incremented(self.gui.receive_mode, 6) - self.history_indicator(self.gui.receive_mode, public_mode, "2") - self.server_is_stopped(self.gui.receive_mode, False) - self.web_server_is_stopped() - self.server_status_indicator_says_closed(self.gui.receive_mode, False) - self.server_working_on_start_button_pressed(self.gui.receive_mode) - self.server_is_started(self.gui.receive_mode) - self.history_indicator(self.gui.receive_mode, public_mode, "2") - - def run_all_receive_mode_unwritable_dir_tests(self, public_mode): - """Attempt to upload (unwritable) files in receive mode and stop the share""" - self.run_all_receive_mode_setup_tests(public_mode) - self.upload_dir_permissions(0o400) - self.upload_file_should_fail(public_mode) - self.server_is_stopped(self.gui.receive_mode, True) - self.web_server_is_stopped() - self.server_status_indicator_says_closed(self.gui.receive_mode, False) - self.upload_dir_permissions(0o755) - - def run_all_receive_mode_timer_tests(self, public_mode): - """Auto-stop timer tests in receive mode""" - self.run_all_receive_mode_setup_tests(public_mode) - self.set_timeout(self.gui.receive_mode, 5) - self.autostop_timer_widget_hidden(self.gui.receive_mode) - self.server_timed_out(self.gui.receive_mode, 15000) - self.web_server_is_stopped() - - def run_all_clear_all_button_tests(self, public_mode): - """Test the Clear All history button""" - self.run_all_receive_mode_setup_tests(public_mode) - self.upload_file(public_mode, "/tmp/test.txt", "test.txt") - self.history_widgets_present(self.gui.receive_mode) - self.clear_all_history_items(self.gui.receive_mode, 0) - self.upload_file(public_mode, "/tmp/test.txt", "test.txt") - self.clear_all_history_items(self.gui.receive_mode, 2) diff --git a/tests/GuiShareTest.py b/tests/GuiShareTest.py deleted file mode 100644 index 4a30dad5..00000000 --- a/tests/GuiShareTest.py +++ /dev/null @@ -1,328 +0,0 @@ -import os -import requests -import socks -import zipfile -import tempfile -from PyQt5 import QtCore, QtTest -from .GuiBaseTest import GuiBaseTest - - -class GuiShareTest(GuiBaseTest): - # Persistence tests - def have_same_password(self, password): - """Test that we have the same password""" - self.assertEqual(self.gui.share_mode.server_status.web.password, password) - - # Share-specific tests - - def file_selection_widget_has_files(self, num=2): - """Test that the number of items in the list is as expected""" - self.assertEqual( - self.gui.share_mode.server_status.file_selection.get_num_files(), num - ) - - def deleting_all_files_hides_delete_button(self): - """Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button""" - rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect( - self.gui.share_mode.server_status.file_selection.file_list.item(0) - ) - QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.file_selection.file_list.viewport(), - QtCore.Qt.LeftButton, - pos=rect.center(), - ) - # Delete button should be visible - self.assertTrue( - self.gui.share_mode.server_status.file_selection.delete_button.isVisible() - ) - # Click delete, delete button should still be visible since we have one more file - QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.file_selection.delete_button, - QtCore.Qt.LeftButton, - ) - - rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect( - self.gui.share_mode.server_status.file_selection.file_list.item(0) - ) - QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.file_selection.file_list.viewport(), - QtCore.Qt.LeftButton, - pos=rect.center(), - ) - self.assertTrue( - self.gui.share_mode.server_status.file_selection.delete_button.isVisible() - ) - QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.file_selection.delete_button, - QtCore.Qt.LeftButton, - ) - - # No more files, the delete button should be hidden - self.assertFalse( - self.gui.share_mode.server_status.file_selection.delete_button.isVisible() - ) - - def add_a_file_and_delete_using_its_delete_widget(self): - """Test that we can also delete a file by clicking on its [X] widget""" - self.gui.share_mode.server_status.file_selection.file_list.add_file( - "/etc/hosts" - ) - QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.file_selection.file_list.item( - 0 - ).item_button, - QtCore.Qt.LeftButton, - ) - self.file_selection_widget_has_files(0) - - def file_selection_widget_read_files(self): - """Re-add some files to the list so we can share""" - self.gui.share_mode.server_status.file_selection.file_list.add_file( - "/etc/hosts" - ) - self.gui.share_mode.server_status.file_selection.file_list.add_file( - "/tmp/test.txt" - ) - self.file_selection_widget_has_files(2) - - def add_large_file(self): - """Add a large file to the share""" - size = 1024 * 1024 * 155 - with open("/tmp/large_file", "wb") as fout: - fout.write(os.urandom(size)) - self.gui.share_mode.server_status.file_selection.file_list.add_file( - "/tmp/large_file" - ) - - def add_delete_buttons_hidden(self): - """Test that the add and delete buttons are hidden when the server starts""" - self.assertFalse( - self.gui.share_mode.server_status.file_selection.add_button.isVisible() - ) - self.assertFalse( - self.gui.share_mode.server_status.file_selection.delete_button.isVisible() - ) - - def download_share(self, public_mode): - """Test that we can download the share""" - url = f"http://127.0.0.1:{self.gui.app.port}/download" - if public_mode: - r = requests.get(url) - else: - r = requests.get( - url, - auth=requests.auth.HTTPBasicAuth( - "onionshare", self.gui.share_mode.server_status.web.password - ), - ) - - tmp_file = tempfile.NamedTemporaryFile() - with open(tmp_file.name, "wb") as f: - f.write(r.content) - - zip = zipfile.ZipFile(tmp_file.name) - QtTest.QTest.qWait(2000) - self.assertEqual("onionshare", zip.read("test.txt").decode("utf-8")) - - def individual_file_is_viewable_or_not(self, public_mode, stay_open): - """Test whether an individual file is viewable (when in stay_open mode) and that it isn't (when not in stay_open mode)""" - url = f"http://127.0.0.1:{self.gui.app.port}" - download_file_url = f"http://127.0.0.1:{self.gui.app.port}/test.txt" - if public_mode: - r = requests.get(url) - else: - r = requests.get( - url, - auth=requests.auth.HTTPBasicAuth( - "onionshare", self.gui.share_mode.server_status.web.password - ), - ) - - if stay_open: - self.assertTrue('a href="test.txt"' in r.text) - - if public_mode: - r = requests.get(download_file_url) - else: - r = requests.get( - download_file_url, - auth=requests.auth.HTTPBasicAuth( - "onionshare", self.gui.share_mode.server_status.web.password - ), - ) - - tmp_file = tempfile.NamedTemporaryFile() - with open(tmp_file.name, "wb") as f: - f.write(r.content) - - with open(tmp_file.name, "r") as f: - self.assertEqual("onionshare", f.read()) - else: - self.assertFalse('a href="/test.txt"' in r.text) - if public_mode: - r = requests.get(download_file_url) - else: - r = requests.get( - download_file_url, - auth=requests.auth.HTTPBasicAuth( - "onionshare", self.gui.share_mode.server_status.web.password - ), - ) - self.assertEqual(r.status_code, 404) - self.download_share(public_mode) - - QtTest.QTest.qWait(2000) - - def hit_401(self, public_mode): - """Test that the server stops after too many 401s, or doesn't when in public_mode""" - url = f"http://127.0.0.1:{self.gui.app.port}/" - - for _ in range(20): - password_guess = self.gui.common.build_password() - r = requests.get( - url, auth=requests.auth.HTTPBasicAuth("onionshare", password_guess) - ) - - # A nasty hack to avoid the Alert dialog that blocks the rest of the test - if not public_mode: - QtCore.QTimer.singleShot(1000, self.accept_dialog) - - # In public mode, we should still be running (no rate-limiting) - if public_mode: - self.web_server_is_running() - # In non-public mode, we should be shut down (rate-limiting) - else: - self.web_server_is_stopped() - - # 'Grouped' tests follow from here - - def run_all_share_mode_setup_tests(self): - """Tests in share mode prior to starting a share""" - self.click_mode(self.gui.share_mode) - self.file_selection_widget_has_files() - self.history_is_not_visible(self.gui.share_mode) - self.click_toggle_history(self.gui.share_mode) - self.history_is_visible(self.gui.share_mode) - self.deleting_all_files_hides_delete_button() - self.add_a_file_and_delete_using_its_delete_widget() - self.file_selection_widget_read_files() - - def run_all_share_mode_started_tests(self, public_mode, startup_time=2000): - """Tests in share mode after starting a share""" - self.server_working_on_start_button_pressed(self.gui.share_mode) - self.server_status_indicator_says_starting(self.gui.share_mode) - self.add_delete_buttons_hidden() - self.settings_button_is_hidden() - self.server_is_started(self.gui.share_mode, startup_time) - self.web_server_is_running() - self.have_a_password(self.gui.share_mode, public_mode) - self.url_description_shown(self.gui.share_mode) - self.have_copy_url_button(self.gui.share_mode, public_mode) - self.server_status_indicator_says_started(self.gui.share_mode) - - def run_all_share_mode_download_tests(self, public_mode, stay_open): - """Tests in share mode after downloading a share""" - self.web_page(self.gui.share_mode, "Total size", public_mode) - self.download_share(public_mode) - self.history_widgets_present(self.gui.share_mode) - self.server_is_stopped(self.gui.share_mode, stay_open) - self.web_server_is_stopped() - self.server_status_indicator_says_closed(self.gui.share_mode, stay_open) - self.add_button_visible(self.gui.share_mode) - self.server_working_on_start_button_pressed(self.gui.share_mode) - self.toggle_indicator_is_reset(self.gui.share_mode) - self.server_is_started(self.gui.share_mode) - self.history_indicator(self.gui.share_mode, public_mode) - - def run_all_share_mode_individual_file_download_tests(self, public_mode, stay_open): - """Tests in share mode after downloading a share""" - self.web_page(self.gui.share_mode, "Total size", public_mode) - self.individual_file_is_viewable_or_not(public_mode, stay_open) - self.history_widgets_present(self.gui.share_mode) - self.server_is_stopped(self.gui.share_mode, stay_open) - self.web_server_is_stopped() - self.server_status_indicator_says_closed(self.gui.share_mode, stay_open) - self.add_button_visible(self.gui.share_mode) - self.server_working_on_start_button_pressed(self.gui.share_mode) - self.server_is_started(self.gui.share_mode) - self.history_indicator(self.gui.share_mode, public_mode) - - def run_all_share_mode_tests(self, public_mode, stay_open): - """End-to-end share tests""" - self.run_all_share_mode_setup_tests() - self.run_all_share_mode_started_tests(public_mode) - self.run_all_share_mode_download_tests(public_mode, stay_open) - - def run_all_clear_all_button_tests(self, public_mode, stay_open): - """Test the Clear All history button""" - self.run_all_share_mode_setup_tests() - self.run_all_share_mode_started_tests(public_mode) - self.individual_file_is_viewable_or_not(public_mode, stay_open) - self.history_widgets_present(self.gui.share_mode) - self.clear_all_history_items(self.gui.share_mode, 0) - self.individual_file_is_viewable_or_not(public_mode, stay_open) - self.clear_all_history_items(self.gui.share_mode, 2) - - def run_all_share_mode_individual_file_tests(self, public_mode, stay_open): - """Tests in share mode when viewing an individual file""" - self.run_all_share_mode_setup_tests() - self.run_all_share_mode_started_tests(public_mode) - self.run_all_share_mode_individual_file_download_tests(public_mode, stay_open) - - def run_all_large_file_tests(self, public_mode, stay_open): - """Same as above but with a larger file""" - self.run_all_share_mode_setup_tests() - self.add_large_file() - self.run_all_share_mode_started_tests(public_mode, startup_time=15000) - self.assertTrue(self.gui.share_mode.filesize_warning.isVisible()) - self.server_is_stopped(self.gui.share_mode, stay_open) - self.web_server_is_stopped() - self.server_status_indicator_says_closed(self.gui.share_mode, stay_open) - - def run_all_share_mode_persistent_tests(self, public_mode, stay_open): - """Same as end-to-end share tests but also test the password is the same on multiple shared""" - self.run_all_share_mode_setup_tests() - self.run_all_share_mode_started_tests(public_mode) - password = self.gui.share_mode.server_status.web.password - self.run_all_share_mode_download_tests(public_mode, stay_open) - self.have_same_password(password) - - def run_all_share_mode_timer_tests(self, public_mode): - """Auto-stop timer tests in share mode""" - self.run_all_share_mode_setup_tests() - self.set_timeout(self.gui.share_mode, 5) - self.run_all_share_mode_started_tests(public_mode) - self.autostop_timer_widget_hidden(self.gui.share_mode) - self.server_timed_out(self.gui.share_mode, 10000) - self.web_server_is_stopped() - - def run_all_share_mode_autostart_timer_tests(self, public_mode): - """Auto-start timer tests in share mode""" - self.run_all_share_mode_setup_tests() - self.set_autostart_timer(self.gui.share_mode, 5) - self.server_working_on_start_button_pressed(self.gui.share_mode) - self.autostart_timer_widget_hidden(self.gui.share_mode) - self.server_status_indicator_says_scheduled(self.gui.share_mode) - self.web_server_is_stopped() - self.scheduled_service_started(self.gui.share_mode, 7000) - self.web_server_is_running() - - def run_all_share_mode_autostop_autostart_mismatch_tests(self, public_mode): - """Auto-stop timer tests in share mode""" - self.run_all_share_mode_setup_tests() - self.set_autostart_timer(self.gui.share_mode, 15) - self.set_timeout(self.gui.share_mode, 5) - QtCore.QTimer.singleShot(4000, self.accept_dialog) - QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton - ) - self.server_is_stopped(self.gui.share_mode, False) - - def run_all_share_mode_unreadable_file_tests(self): - """Attempt to share an unreadable file""" - self.run_all_share_mode_setup_tests() - QtCore.QTimer.singleShot(1000, self.accept_dialog) - self.gui.share_mode.server_status.file_selection.file_list.add_file( - "/tmp/nonexistent.txt" - ) - self.file_selection_widget_has_files(2) diff --git a/tests/GuiWebsiteTest.py b/tests/GuiWebsiteTest.py deleted file mode 100644 index 8c733442..00000000 --- a/tests/GuiWebsiteTest.py +++ /dev/null @@ -1,136 +0,0 @@ -import json -import os -import requests -import socks -import zipfile -import tempfile -from PyQt5 import QtCore, QtTest -from onionshare import strings -from onionshare.common import Common -from onionshare.settings import Settings -from onionshare.onion import Onion -from onionshare.web import Web -from onionshare_gui import Application, OnionShare, MainWindow -from .GuiShareTest import GuiShareTest - - -class GuiWebsiteTest(GuiShareTest): - @staticmethod - def set_up(test_settings): - """Create GUI with given settings""" - # Create our test file - testfile = open("/tmp/index.html", "w") - testfile.write( - "

This is a test website hosted by OnionShare

" - ) - testfile.close() - - common = Common() - common.settings = Settings(common) - common.define_css() - strings.load_strings(common) - - # Get all of the settings in test_settings - test_settings["data_dir"] = "/tmp/OnionShare" - for key, val in common.settings.default_settings.items(): - if key not in test_settings: - test_settings[key] = val - - # Start the Onion - testonion = Onion(common) - global qtapp - qtapp = Application(common) - app = OnionShare(common, testonion, True, 0) - - web = Web(common, False, True) - open("/tmp/settings.json", "w").write(json.dumps(test_settings)) - - gui = MainWindow( - common, - testonion, - qtapp, - app, - ["/tmp/index.html"], - "/tmp/settings.json", - True, - ) - return gui - - @staticmethod - def tear_down(): - """Clean up after tests""" - try: - os.remove("/tmp/index.html") - os.remove("/tmp/settings.json") - except: - pass - - def view_website(self, public_mode): - """Test that we can download the share""" - url = f"http://127.0.0.1:{self.gui.app.port}/" - if public_mode: - r = requests.get(url) - else: - r = requests.get( - url, - auth=requests.auth.HTTPBasicAuth( - "onionshare", self.gui.website_mode.server_status.web.password - ), - ) - - QtTest.QTest.qWait(2000) - self.assertTrue("This is a test website hosted by OnionShare" in r.text) - - def check_csp_header(self, public_mode, csp_header_disabled): - """Test that the CSP header is present when enabled or vice versa""" - url = f"http://127.0.0.1:{self.gui.app.port}/" - if public_mode: - r = requests.get(url) - else: - r = requests.get( - url, - auth=requests.auth.HTTPBasicAuth( - "onionshare", self.gui.website_mode.server_status.web.password - ), - ) - - QtTest.QTest.qWait(2000) - if csp_header_disabled: - self.assertFalse("Content-Security-Policy" in r.headers) - else: - self.assertTrue("Content-Security-Policy" in r.headers) - - def run_all_website_mode_setup_tests(self): - """Tests in website mode prior to starting a share""" - self.click_mode(self.gui.website_mode) - self.file_selection_widget_has_files(1) - self.history_is_not_visible(self.gui.website_mode) - self.click_toggle_history(self.gui.website_mode) - self.history_is_visible(self.gui.website_mode) - - def run_all_website_mode_started_tests(self, public_mode, startup_time=2000): - """Tests in website mode after starting a share""" - self.server_working_on_start_button_pressed(self.gui.website_mode) - self.server_status_indicator_says_starting(self.gui.website_mode) - self.add_delete_buttons_hidden() - self.settings_button_is_hidden() - self.server_is_started(self.gui.website_mode, startup_time) - self.web_server_is_running() - self.have_a_password(self.gui.website_mode, public_mode) - self.url_description_shown(self.gui.website_mode) - self.have_copy_url_button(self.gui.website_mode, public_mode) - self.server_status_indicator_says_started(self.gui.website_mode) - - def run_all_website_mode_download_tests(self, public_mode): - """Tests in website mode after viewing the site""" - self.run_all_website_mode_setup_tests() - self.run_all_website_mode_started_tests(public_mode, startup_time=2000) - self.view_website(public_mode) - self.check_csp_header( - public_mode, self.gui.common.settings.get("csp_header_disabled") - ) - self.history_widgets_present(self.gui.website_mode) - self.server_is_stopped(self.gui.website_mode, False) - self.web_server_is_stopped() - self.server_status_indicator_says_closed(self.gui.website_mode, False) - self.add_button_visible(self.gui.website_mode) diff --git a/tests/SettingsGuiBaseTest.py b/tests/SettingsGuiBaseTest.py deleted file mode 100644 index 1aa6da25..00000000 --- a/tests/SettingsGuiBaseTest.py +++ /dev/null @@ -1,333 +0,0 @@ -import json -import os -import unittest -from PyQt5 import QtCore, QtTest - -from onionshare import strings -from onionshare.common import Common -from onionshare.settings import Settings -from onionshare.onion import Onion -from onionshare_gui import Application, OnionShare -from onionshare_gui.settings_dialog import SettingsDialog - - -class OnionStub(object): - def __init__(self, is_authenticated, supports_v3_onions): - self._is_authenticated = is_authenticated - self.supports_v3_onions = supports_v3_onions - - def is_authenticated(self): - return self._is_authenticated - - -class SettingsGuiBaseTest(object): - @staticmethod - def set_up(): - """Create the GUI""" - - # Default settings for the settings GUI tests - test_settings = { - "no_bridges": False, - "tor_bridges_use_custom_bridges": "Bridge 1.2.3.4:56 EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\nBridge 5.6.7.8:910 EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\nBridge 11.12.13.14:1516 EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\n", - } - - # Create our test file - testfile = open("/tmp/test.txt", "w") - testfile.write("onionshare") - testfile.close() - - common = Common() - common.settings = Settings(common) - common.define_css() - strings.load_strings(common) - - # Start the Onion - testonion = Onion(common) - global qtapp - qtapp = Application(common) - app = OnionShare(common, testonion, True, 0) - - for key, val in common.settings.default_settings.items(): - if key not in test_settings: - test_settings[key] = val - - open("/tmp/settings.json", "w").write(json.dumps(test_settings)) - - gui = SettingsDialog(common, testonion, qtapp, "/tmp/settings.json", True) - return gui - - @staticmethod - def tear_down(): - """Clean up after tests""" - os.remove("/tmp/settings.json") - - def run_settings_gui_tests(self): - self.gui.show() - - # Window is shown - self.assertTrue(self.gui.isVisible()) - self.assertEqual(self.gui.windowTitle(), strings._("gui_settings_window_title")) - - # Check for updates button is hidden - self.assertFalse(self.gui.check_for_updates_button.isVisible()) - - # public mode is off - self.assertFalse(self.gui.public_mode_checkbox.isChecked()) - # enable public mode - QtTest.QTest.mouseClick( - self.gui.public_mode_checkbox, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint(2, self.gui.public_mode_checkbox.height() / 2), - ) - self.assertTrue(self.gui.public_mode_checkbox.isChecked()) - - # autostop timer is off - self.assertFalse(self.gui.autostop_timer_checkbox.isChecked()) - # enable autostop timer - QtTest.QTest.mouseClick( - self.gui.autostop_timer_checkbox, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint(2, self.gui.autostop_timer_checkbox.height() / 2), - ) - self.assertTrue(self.gui.autostop_timer_checkbox.isChecked()) - - # legacy mode checkbox and related widgets - if self.gui.onion.is_authenticated(): - if self.gui.onion.supports_v3_onions: - # legacy mode is off - self.assertFalse(self.gui.use_legacy_v2_onions_checkbox.isChecked()) - # persistence is still available, stealth is hidden and disabled - self.assertTrue(self.gui.save_private_key_widget.isVisible()) - self.assertFalse(self.gui.save_private_key_checkbox.isChecked()) - self.assertFalse(self.gui.use_stealth_widget.isVisible()) - self.assertFalse(self.gui.stealth_checkbox.isChecked()) - self.assertFalse(self.gui.hidservauth_copy_button.isVisible()) - - # enable legacy mode - QtTest.QTest.mouseClick( - self.gui.use_legacy_v2_onions_checkbox, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint( - 2, self.gui.use_legacy_v2_onions_checkbox.height() / 2 - ), - ) - self.assertTrue(self.gui.use_legacy_v2_onions_checkbox.isChecked()) - self.assertTrue(self.gui.save_private_key_checkbox.isVisible()) - self.assertTrue(self.gui.use_stealth_widget.isVisible()) - - # enable persistent mode - QtTest.QTest.mouseClick( - self.gui.save_private_key_checkbox, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint( - 2, self.gui.save_private_key_checkbox.height() / 2 - ), - ) - self.assertTrue(self.gui.save_private_key_checkbox.isChecked()) - # enable stealth mode - QtTest.QTest.mouseClick( - self.gui.stealth_checkbox, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint(2, self.gui.stealth_checkbox.height() / 2), - ) - self.assertTrue(self.gui.stealth_checkbox.isChecked()) - # now that stealth is enabled, we can't turn off legacy mode - self.assertFalse(self.gui.use_legacy_v2_onions_checkbox.isEnabled()) - # disable stealth, persistence - QtTest.QTest.mouseClick( - self.gui.save_private_key_checkbox, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint( - 2, self.gui.save_private_key_checkbox.height() / 2 - ), - ) - QtTest.QTest.mouseClick( - self.gui.stealth_checkbox, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint(2, self.gui.stealth_checkbox.height() / 2), - ) - # legacy mode checkbox is enabled again - self.assertTrue(self.gui.use_legacy_v2_onions_checkbox.isEnabled()) - # uncheck legacy mode - QtTest.QTest.mouseClick( - self.gui.use_legacy_v2_onions_checkbox, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint( - 2, self.gui.use_legacy_v2_onions_checkbox.height() / 2 - ), - ) - # legacy options hidden again - self.assertTrue(self.gui.save_private_key_widget.isVisible()) - self.assertFalse(self.gui.use_stealth_widget.isVisible()) - - # re-enable legacy mode - QtTest.QTest.mouseClick( - self.gui.use_legacy_v2_onions_checkbox, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint( - 2, self.gui.use_legacy_v2_onions_checkbox.height() / 2 - ), - ) - - else: - # legacy mode setting is hidden - self.assertFalse(self.gui.use_legacy_v2_onions_checkbox.isVisible()) - # legacy options are showing - self.assertTrue(self.gui.save_private_key_widget.isVisible()) - self.assertTrue(self.gui.use_stealth_widget.isVisible()) - - # enable them all again so that we can see the setting stick in settings.json - QtTest.QTest.mouseClick( - self.gui.save_private_key_checkbox, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint(2, self.gui.save_private_key_checkbox.height() / 2), - ) - QtTest.QTest.mouseClick( - self.gui.stealth_checkbox, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint(2, self.gui.stealth_checkbox.height() / 2), - ) - else: - # None of the onion settings should appear - self.assertFalse(self.gui.use_legacy_v2_onions_checkbox.isVisible()) - self.assertFalse(self.gui.save_private_key_widget.isVisible()) - self.assertFalse(self.gui.save_private_key_checkbox.isChecked()) - self.assertFalse(self.gui.use_stealth_widget.isVisible()) - self.assertFalse(self.gui.stealth_checkbox.isChecked()) - self.assertFalse(self.gui.hidservauth_copy_button.isVisible()) - - # stay open toggled off, on - self.assertTrue(self.gui.close_after_first_download_checkbox.isChecked()) - QtTest.QTest.mouseClick( - self.gui.close_after_first_download_checkbox, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint( - 2, self.gui.close_after_first_download_checkbox.height() / 2 - ), - ) - self.assertFalse(self.gui.close_after_first_download_checkbox.isChecked()) - - # receive mode - self.gui.data_dir_lineedit.setText("/tmp/OnionShareSettingsTest") - - # bundled mode is enabled - self.assertTrue(self.gui.connection_type_bundled_radio.isEnabled()) - self.assertTrue(self.gui.connection_type_bundled_radio.isChecked()) - # bridge options are shown - self.assertTrue(self.gui.connection_type_bridges_radio_group.isVisible()) - # bridges are set to custom - self.assertFalse(self.gui.tor_bridges_no_bridges_radio.isChecked()) - self.assertTrue(self.gui.tor_bridges_use_custom_radio.isChecked()) - - # switch to obfs4 - QtTest.QTest.mouseClick( - self.gui.tor_bridges_use_obfs4_radio, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint(2, self.gui.tor_bridges_use_obfs4_radio.height() / 2), - ) - self.assertTrue(self.gui.tor_bridges_use_obfs4_radio.isChecked()) - - # custom bridges are hidden - self.assertFalse(self.gui.tor_bridges_use_custom_textbox_options.isVisible()) - # other modes are unchecked but enabled - self.assertTrue(self.gui.connection_type_automatic_radio.isEnabled()) - self.assertTrue(self.gui.connection_type_control_port_radio.isEnabled()) - self.assertTrue(self.gui.connection_type_socket_file_radio.isEnabled()) - self.assertFalse(self.gui.connection_type_automatic_radio.isChecked()) - self.assertFalse(self.gui.connection_type_control_port_radio.isChecked()) - self.assertFalse(self.gui.connection_type_socket_file_radio.isChecked()) - - # enable automatic mode - QtTest.QTest.mouseClick( - self.gui.connection_type_automatic_radio, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint(2, self.gui.connection_type_automatic_radio.height() / 2), - ) - self.assertTrue(self.gui.connection_type_automatic_radio.isChecked()) - # bundled is off - self.assertFalse(self.gui.connection_type_bundled_radio.isChecked()) - # bridges are hidden - self.assertFalse(self.gui.connection_type_bridges_radio_group.isVisible()) - - # auth type is hidden in bundled or automatic mode - self.assertFalse(self.gui.authenticate_no_auth_radio.isVisible()) - self.assertFalse(self.gui.authenticate_password_radio.isVisible()) - - # enable control port mode - QtTest.QTest.mouseClick( - self.gui.connection_type_control_port_radio, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint( - 2, self.gui.connection_type_control_port_radio.height() / 2 - ), - ) - self.assertTrue(self.gui.connection_type_control_port_radio.isChecked()) - # automatic is off - self.assertFalse(self.gui.connection_type_automatic_radio.isChecked()) - # auth options appear - self.assertTrue(self.gui.authenticate_no_auth_radio.isVisible()) - self.assertTrue(self.gui.authenticate_password_radio.isVisible()) - - # enable socket mode - QtTest.QTest.mouseClick( - self.gui.connection_type_socket_file_radio, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint( - 2, self.gui.connection_type_socket_file_radio.height() / 2 - ), - ) - self.assertTrue(self.gui.connection_type_socket_file_radio.isChecked()) - # control port is off - self.assertFalse(self.gui.connection_type_control_port_radio.isChecked()) - # auth options are still present - self.assertTrue(self.gui.authenticate_no_auth_radio.isVisible()) - self.assertTrue(self.gui.authenticate_password_radio.isVisible()) - - # re-enable bundled mode - QtTest.QTest.mouseClick( - self.gui.connection_type_bundled_radio, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint(2, self.gui.connection_type_bundled_radio.height() / 2), - ) - # go back to custom bridges - QtTest.QTest.mouseClick( - self.gui.tor_bridges_use_custom_radio, - QtCore.Qt.LeftButton, - pos=QtCore.QPoint(2, self.gui.tor_bridges_use_custom_radio.height() / 2), - ) - self.assertTrue(self.gui.tor_bridges_use_custom_radio.isChecked()) - self.assertTrue(self.gui.tor_bridges_use_custom_textbox.isVisible()) - self.assertFalse(self.gui.tor_bridges_use_obfs4_radio.isChecked()) - self.gui.tor_bridges_use_custom_textbox.setPlainText( - "94.242.249.2:83 E25A95F1DADB739F0A83EB0223A37C02FD519306\n148.251.90.59:7510 019F727CA6DCA6CA5C90B55E477B7D87981E75BC\n93.80.47.217:41727 A6A0D497D98097FCFE91D639548EE9E34C15CDD3" - ) - - # Test that the Settings Dialog can save the settings and close itself - QtTest.QTest.mouseClick(self.gui.save_button, QtCore.Qt.LeftButton) - self.assertFalse(self.gui.isVisible()) - - # Test our settings are reflected in the settings json - with open("/tmp/settings.json") as f: - data = json.load(f) - - self.assertTrue(data["public_mode"]) - self.assertTrue(data["autostop_timer"]) - - if self.gui.onion.is_authenticated(): - if self.gui.onion.supports_v3_onions: - self.assertTrue(data["use_legacy_v2_onions"]) - self.assertTrue(data["save_private_key"]) - self.assertTrue(data["use_stealth"]) - else: - self.assertFalse(data["use_legacy_v2_onions"]) - self.assertFalse(data["save_private_key"]) - self.assertFalse(data["use_stealth"]) - - self.assertEqual(data["data_dir"], "/tmp/OnionShareSettingsTest") - self.assertFalse(data["close_after_first_download"]) - self.assertEqual(data["connection_type"], "bundled") - self.assertFalse(data["tor_bridges_use_obfs4"]) - self.assertEqual( - data["tor_bridges_use_custom_bridges"], - "Bridge 94.242.249.2:83 E25A95F1DADB739F0A83EB0223A37C02FD519306\nBridge 148.251.90.59:7510 019F727CA6DCA6CA5C90B55E477B7D87981E75BC\nBridge 93.80.47.217:41727 A6A0D497D98097FCFE91D639548EE9E34C15CDD3\n", - ) diff --git a/tests/TorGuiBaseTest.py b/tests/TorGuiBaseTest.py deleted file mode 100644 index ab5ed508..00000000 --- a/tests/TorGuiBaseTest.py +++ /dev/null @@ -1,176 +0,0 @@ -import json -import os -import requests -import socks - -from PyQt5 import QtCore, QtTest - -from onionshare import strings -from onionshare.common import Common -from onionshare.settings import Settings -from onionshare.onion import Onion -from onionshare.web import Web -from onionshare_gui import Application, OnionShare, MainWindow -from onionshare_gui.mode.share_mode import ShareMode -from onionshare_gui.mode.receive_mode import ReceiveMode - -from .GuiBaseTest import GuiBaseTest - - -class TorGuiBaseTest(GuiBaseTest): - @staticmethod - def set_up(test_settings): - """Create GUI with given settings""" - # Create our test file - testfile = open("/tmp/test.txt", "w") - testfile.write("onionshare") - testfile.close() - - # Create a test dir and files - if not os.path.exists("/tmp/testdir"): - testdir = os.mkdir("/tmp/testdir") - testfile = open("/tmp/testdir/test.txt", "w") - testfile.write("onionshare") - testfile.close() - - common = Common() - common.settings = Settings(common) - common.define_css() - strings.load_strings(common) - - # Get all of the settings in test_settings - test_settings["connection_type"] = "automatic" - test_settings["data_dir"] = "/tmp/OnionShare" - for key, val in common.settings.default_settings.items(): - if key not in test_settings: - test_settings[key] = val - - # Start the Onion - testonion = Onion(common) - global qtapp - qtapp = Application(common) - app = OnionShare(common, testonion, False, 0) - - web = Web(common, False, False) - open("/tmp/settings.json", "w").write(json.dumps(test_settings)) - - gui = MainWindow( - common, - testonion, - qtapp, - app, - ["/tmp/test.txt", "/tmp/testdir"], - "/tmp/settings.json", - False, - ) - return gui - - def history_indicator(self, mode, public_mode): - """Test that we can make sure the history is toggled off, do an action, and the indiciator works""" - # Make sure history is toggled off - if mode.history.isVisible(): - QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton) - self.assertFalse(mode.history.isVisible()) - - # Indicator should not be visible yet - self.assertFalse(mode.toggle_history.indicator_label.isVisible()) - - # Set up connecting to the onion - (socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port() - session = requests.session() - session.proxies = {} - session.proxies["http"] = f"socks5h://{socks_address}:{socks_port}" - - if type(mode) == ReceiveMode: - # Upload a file - files = {"file[]": open("/tmp/test.txt", "rb")} - if not public_mode: - path = f"http://{self.gui.app.onion_host}/{mode.web.password}/upload" - else: - path = f"http://{self.gui.app.onion_host}/upload" - response = session.post(path, files=files) - QtTest.QTest.qWait(4000) - - if type(mode) == ShareMode: - # Download files - if public_mode: - path = f"http://{self.gui.app.onion_host}/download" - else: - path = f"http://{self.gui.app.onion_host}/{mode.web.password}/download" - response = session.get(path) - QtTest.QTest.qWait(4000) - - # Indicator should be visible, have a value of "1" - self.assertTrue(mode.toggle_history.indicator_label.isVisible()) - self.assertEqual(mode.toggle_history.indicator_label.text(), "1") - - # Toggle history back on, indicator should be hidden again - QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton) - self.assertFalse(mode.toggle_history.indicator_label.isVisible()) - - def have_an_onion_service(self): - """Test that we have a valid Onion URL""" - self.assertRegex(self.gui.app.onion_host, r"[a-z2-7].onion") - - def web_page(self, mode, string, public_mode): - """Test that the web page contains a string""" - (socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port() - socks.set_default_proxy(socks.SOCKS5, socks_address, socks_port) - s = socks.socksocket() - s.settimeout(60) - s.connect((self.gui.app.onion_host, 80)) - if not public_mode: - path = f"/{mode.server_status.web.password}" - else: - path = "/" - http_request = f"GET {path} HTTP/1.0\r\n" - http_request += f"Host: {self.gui.app.onion_host}\r\n" - http_request += "\r\n" - s.sendall(http_request.encode("utf-8")) - with open("/tmp/webpage", "wb") as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - f = open("/tmp/webpage") - self.assertTrue(string in f.read()) - f.close() - - def have_copy_url_button(self, mode, public_mode): - """Test that the Copy URL button is shown and that the clipboard is correct""" - self.assertTrue(mode.server_status.copy_url_button.isVisible()) - - QtTest.QTest.mouseClick( - mode.server_status.copy_url_button, QtCore.Qt.LeftButton - ) - clipboard = self.gui.qtapp.clipboard() - if public_mode: - self.assertEqual(clipboard.text(), f"http://{self.gui.app.onion_host}") - else: - self.assertEqual( - clipboard.text(), - f"http://{self.gui.app.onion_host}/{mode.server_status.web.password}", - ) - - # Stealth tests - def copy_have_hidserv_auth_button(self, mode): - """Test that the Copy HidservAuth button is shown""" - self.assertTrue(mode.server_status.copy_hidservauth_button.isVisible()) - - def hidserv_auth_string(self): - """Test the validity of the HidservAuth string""" - self.assertRegex( - self.gui.app.auth_string, - r"HidServAuth {} [a-zA-Z1-9]".format(self.gui.app.onion_host), - ) - - # Miscellaneous tests - def tor_killed_statusbar_message_shown(self, mode): - """Test that the status bar message shows Tor was disconnected""" - self.gui.app.onion.c = None - QtTest.QTest.qWait(1000) - self.assertTrue( - mode.status_bar.currentMessage(), strings._("gui_tor_connection_lost") - ) diff --git a/tests/TorGuiReceiveTest.py b/tests/TorGuiReceiveTest.py deleted file mode 100644 index a8944363..00000000 --- a/tests/TorGuiReceiveTest.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -import requests -from PyQt5 import QtTest -from .TorGuiBaseTest import TorGuiBaseTest - - -class TorGuiReceiveTest(TorGuiBaseTest): - def upload_file(self, public_mode, file_to_upload, expected_file): - """Test that we can upload the file""" - (socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port() - session = requests.session() - session.proxies = {} - session.proxies["http"] = f"socks5h://{socks_address}:{socks_port}" - files = {"file[]": open(file_to_upload, "rb")} - if not public_mode: - path = f"http://{self.gui.app.onion_host}/{self.gui.receive_mode.web.password}/upload" - else: - path = f"http://{self.gui.app.onion_host}/upload" - response = session.post(path, files=files) - QtTest.QTest.qWait(4000) - self.assertTrue(os.path.isfile(expected_file)) - - # 'Grouped' tests follow from here - - def run_all_receive_mode_tests(self, public_mode, receive_allow_receiver_shutdown): - """Run a full suite of tests in Receive mode""" - self.click_mode(self.gui.receive_mode) - self.history_is_not_visible(self.gui.receive_mode) - self.click_toggle_history(self.gui.receive_mode) - self.history_is_visible(self.gui.receive_mode) - self.server_working_on_start_button_pressed(self.gui.receive_mode) - self.server_status_indicator_says_starting(self.gui.receive_mode) - self.settings_button_is_hidden() - self.server_is_started(self.gui.receive_mode, startup_time=45000) - self.web_server_is_running() - self.have_an_onion_service() - self.have_a_password(self.gui.receive_mode, public_mode) - self.url_description_shown(self.gui.receive_mode) - self.have_copy_url_button(self.gui.receive_mode, public_mode) - self.server_status_indicator_says_started(self.gui.receive_mode) - self.web_page( - self.gui.receive_mode, - "Select the files you want to send, then click", - public_mode, - ) - self.upload_file(public_mode, "/tmp/test.txt", "/tmp/OnionShare/test.txt") - self.history_widgets_present(self.gui.receive_mode) - self.counter_incremented(self.gui.receive_mode, 1) - self.upload_file(public_mode, "/tmp/test.txt", "/tmp/OnionShare/test-2.txt") - self.counter_incremented(self.gui.receive_mode, 2) - self.upload_file(public_mode, "/tmp/testdir/test", "/tmp/OnionShare/test") - self.counter_incremented(self.gui.receive_mode, 3) - self.upload_file(public_mode, "/tmp/testdir/test", "/tmp/OnionShare/test-2") - self.counter_incremented(self.gui.receive_mode, 4) - self.history_indicator(self.gui.receive_mode, public_mode) - self.server_is_stopped(self.gui.receive_mode, False) - self.web_server_is_stopped() - self.server_status_indicator_says_closed(self.gui.receive_mode, False) - self.server_working_on_start_button_pressed(self.gui.receive_mode) - self.server_is_started(self.gui.receive_mode, startup_time=45000) - self.history_indicator(self.gui.receive_mode, public_mode) diff --git a/tests/TorGuiShareTest.py b/tests/TorGuiShareTest.py deleted file mode 100644 index 1c9c5b40..00000000 --- a/tests/TorGuiShareTest.py +++ /dev/null @@ -1,91 +0,0 @@ -import requests -import zipfile -from PyQt5 import QtTest -from .TorGuiBaseTest import TorGuiBaseTest -from .GuiShareTest import GuiShareTest - - -class TorGuiShareTest(TorGuiBaseTest, GuiShareTest): - def download_share(self, public_mode): - """Test downloading a share""" - # Set up connecting to the onion - (socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port() - session = requests.session() - session.proxies = {} - session.proxies["http"] = f"socks5h://{socks_address}:{socks_port}" - - # Download files - if public_mode: - path = f"http://{self.gui.app.onion_host}/download" - else: - path = f"http://{self.gui.app.onion_host}/{self.gui.share_mode.web.password}/download" - response = session.get(path, stream=True) - QtTest.QTest.qWait(4000) - - if response.status_code == 200: - with open("/tmp/download.zip", "wb") as file_to_write: - for chunk in response.iter_content(chunk_size=128): - file_to_write.write(chunk) - file_to_write.close() - zip = zipfile.ZipFile("/tmp/download.zip") - QtTest.QTest.qWait(4000) - self.assertEqual("onionshare", zip.read("test.txt").decode("utf-8")) - - # Persistence tests - def have_same_onion(self, onion): - """Test that we have the same onion""" - self.assertEqual(self.gui.app.onion_host, onion) - - # legacy v2 onion test - def have_v2_onion(self): - """Test that the onion is a v2 style onion""" - self.assertRegex(self.gui.app.onion_host, r"[a-z2-7].onion") - self.assertEqual(len(self.gui.app.onion_host), 22) - - # 'Grouped' tests follow from here - - def run_all_share_mode_started_tests(self, public_mode): - """Tests in share mode after starting a share""" - self.server_working_on_start_button_pressed(self.gui.share_mode) - self.server_status_indicator_says_starting(self.gui.share_mode) - self.add_delete_buttons_hidden() - self.settings_button_is_hidden() - self.server_is_started(self.gui.share_mode, startup_time=45000) - self.web_server_is_running() - self.have_an_onion_service() - self.have_a_password(self.gui.share_mode, public_mode) - self.url_description_shown(self.gui.share_mode) - self.have_copy_url_button(self.gui.share_mode, public_mode) - self.server_status_indicator_says_started(self.gui.share_mode) - - def run_all_share_mode_download_tests(self, public_mode, stay_open): - """Tests in share mode after downloading a share""" - self.web_page(self.gui.share_mode, "Total size", public_mode) - self.download_share(public_mode) - self.history_widgets_present(self.gui.share_mode) - self.server_is_stopped(self.gui.share_mode, stay_open) - self.web_server_is_stopped() - self.server_status_indicator_says_closed(self.gui.share_mode, stay_open) - self.add_button_visible(self.gui.share_mode) - self.server_working_on_start_button_pressed(self.gui.share_mode) - self.server_is_started(self.gui.share_mode, startup_time=45000) - self.history_indicator(self.gui.share_mode, public_mode) - - def run_all_share_mode_persistent_tests(self, public_mode, stay_open): - """Same as end-to-end share tests but also test the password is the same on multiple shared""" - self.run_all_share_mode_setup_tests() - self.run_all_share_mode_started_tests(public_mode) - password = self.gui.share_mode.server_status.web.password - onion = self.gui.app.onion_host - self.run_all_share_mode_download_tests(public_mode, stay_open) - self.have_same_onion(onion) - self.have_same_password(password) - - def run_all_share_mode_timer_tests(self, public_mode): - """Auto-stop timer tests in share mode""" - self.run_all_share_mode_setup_tests() - self.set_timeout(self.gui.share_mode, 120) - self.run_all_share_mode_started_tests(public_mode) - self.autostop_timer_widget_hidden(self.gui.share_mode) - self.server_timed_out(self.gui.share_mode, 125000) - self.web_server_is_stopped() diff --git a/tests/conftest.py b/tests/conftest.py index ac81d14d..200f526d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,9 @@ import sys # Force tests to look for resources in the source code tree sys.onionshare_dev_mode = True +# Let OnionShare know the tests are running, to avoid colliding with settings files +sys.onionshare_test_mode = True + import os import shutil import tempfile @@ -12,6 +15,10 @@ import pytest from onionshare import common, web, settings, strings +# The temporary directory for CLI tests +test_temp_dir = None + + def pytest_addoption(parser): parser.addoption( "--rungui", action="store_true", default=False, help="run GUI tests" @@ -38,51 +45,60 @@ def pytest_collection_modifyitems(config, items): @pytest.fixture -def temp_dir_1024(): +def temp_dir(): + """Creates a persistent temporary directory for the CLI tests to use""" + global test_temp_dir + if not test_temp_dir: + test_temp_dir = tempfile.mkdtemp() + return test_temp_dir + + +@pytest.fixture +def temp_dir_1024(temp_dir): """ Create a temporary directory that has a single file of a particular size (1024 bytes). """ - tmp_dir = tempfile.mkdtemp() - tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir) + new_temp_dir = tempfile.mkdtemp(dir=temp_dir) + tmp_file, tmp_file_path = tempfile.mkstemp(dir=new_temp_dir) with open(tmp_file, "wb") as f: f.write(b"*" * 1024) - return tmp_dir + return new_temp_dir # pytest > 2.9 only needs @pytest.fixture @pytest.yield_fixture -def temp_dir_1024_delete(): +def temp_dir_1024_delete(temp_dir): """ Create a temporary directory that has a single file of a particular size (1024 bytes). The temporary directory (including the file inside) will be deleted after fixture usage. """ - with tempfile.TemporaryDirectory() as tmp_dir: - tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir) + with tempfile.TemporaryDirectory(dir=temp_dir) as new_temp_dir: + tmp_file, tmp_file_path = tempfile.mkstemp(dir=new_temp_dir) with open(tmp_file, "wb") as f: f.write(b"*" * 1024) - yield tmp_dir + yield new_temp_dir @pytest.fixture -def temp_file_1024(): +def temp_file_1024(temp_dir): """ Create a temporary file of a particular size (1024 bytes). """ - with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + with tempfile.NamedTemporaryFile(delete=False, dir=temp_dir) as tmp_file: tmp_file.write(b"*" * 1024) return tmp_file.name # pytest > 2.9 only needs @pytest.fixture @pytest.yield_fixture -def temp_file_1024_delete(): +def temp_file_1024_delete(temp_dir): """ Create a temporary file of a particular size (1024 bytes). The temporary file will be deleted after fixture usage. """ - with tempfile.NamedTemporaryFile() as tmp_file: + with tempfile.NamedTemporaryFile(dir=temp_dir) as tmp_file: tmp_file.write(b"*" * 1024) tmp_file.flush() yield tmp_file.name @@ -108,7 +124,10 @@ def default_zw(): yield zw zw.close() tmp_dir = os.path.dirname(zw.zip_filename) - shutil.rmtree(tmp_dir) + try: + shutil.rmtree(tmp_dir, ignore_errors=True) + except: + pass @pytest.fixture diff --git a/tests2/gui_base_test.py b/tests/gui_base_test.py similarity index 100% rename from tests2/gui_base_test.py rename to tests/gui_base_test.py diff --git a/tests/local_onionshare_401_public_mode_skips_ratelimit_test.py b/tests/local_onionshare_401_public_mode_skips_ratelimit_test.py deleted file mode 100644 index 388a424b..00000000 --- a/tests/local_onionshare_401_public_mode_skips_ratelimit_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class Local401PublicModeRateLimitTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"close_after_first_download": False, "public_mode": True} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_tests(True, True) - self.hit_401(True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_401_triggers_ratelimit_test.py b/tests/local_onionshare_401_triggers_ratelimit_test.py deleted file mode 100644 index cdeb34db..00000000 --- a/tests/local_onionshare_401_triggers_ratelimit_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class Local401RateLimitTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"close_after_first_download": False} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_tests(False, True) - self.hit_401(False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_quitting_during_share_prompts_warning_test.py b/tests/local_onionshare_quitting_during_share_prompts_warning_test.py deleted file mode 100644 index 9a38e24a..00000000 --- a/tests/local_onionshare_quitting_during_share_prompts_warning_test.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest -from PyQt5 import QtCore, QtTest - -from .GuiShareTest import GuiShareTest - - -class LocalQuittingDuringSharePromptsWarningTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"close_after_first_download": False} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_tests(False, True) - # Prepare our auto-accept of prompt - QtCore.QTimer.singleShot(5000, self.accept_dialog) - # Try to close the app - self.gui.close() - # Server should still be running (we've been prompted first) - self.server_is_started(self.gui.share_mode, 0) - self.web_server_is_running() - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_receive_mode_clear_all_button_test.py b/tests/local_onionshare_receive_mode_clear_all_button_test.py deleted file mode 100644 index d69c3e59..00000000 --- a/tests/local_onionshare_receive_mode_clear_all_button_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiReceiveTest import GuiReceiveTest - - -class LocalReceiveModeClearAllButtonTest(unittest.TestCase, GuiReceiveTest): - @classmethod - def setUpClass(cls): - test_settings = {} - cls.gui = GuiReceiveTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiReceiveTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_clear_all_button_tests(False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_receive_mode_timer_test.py b/tests/local_onionshare_receive_mode_timer_test.py deleted file mode 100644 index f958a132..00000000 --- a/tests/local_onionshare_receive_mode_timer_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiReceiveTest import GuiReceiveTest - - -class LocalReceiveModeTimerTest(unittest.TestCase, GuiReceiveTest): - @classmethod - def setUpClass(cls): - test_settings = {"public_mode": False, "autostop_timer": True} - cls.gui = GuiReceiveTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiReceiveTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_receive_mode_timer_tests(False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_receive_mode_upload_non_writable_dir_test.py b/tests/local_onionshare_receive_mode_upload_non_writable_dir_test.py deleted file mode 100644 index f1451ba0..00000000 --- a/tests/local_onionshare_receive_mode_upload_non_writable_dir_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiReceiveTest import GuiReceiveTest - - -class LocalReceiveModeUnwritableTest(unittest.TestCase, GuiReceiveTest): - @classmethod - def setUpClass(cls): - test_settings = {"receive_allow_receiver_shutdown": True} - cls.gui = GuiReceiveTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiReceiveTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_receive_mode_unwritable_dir_tests(False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_receive_mode_upload_public_mode_non_writable_dir_test.py b/tests/local_onionshare_receive_mode_upload_public_mode_non_writable_dir_test.py deleted file mode 100644 index 6f0997f2..00000000 --- a/tests/local_onionshare_receive_mode_upload_public_mode_non_writable_dir_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiReceiveTest import GuiReceiveTest - - -class LocalReceivePublicModeUnwritableTest(unittest.TestCase, GuiReceiveTest): - @classmethod - def setUpClass(cls): - test_settings = {"public_mode": True, "receive_allow_receiver_shutdown": True} - cls.gui = GuiReceiveTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiReceiveTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_receive_mode_unwritable_dir_tests(True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_receive_mode_upload_public_mode_test.py b/tests/local_onionshare_receive_mode_upload_public_mode_test.py deleted file mode 100644 index 818bd593..00000000 --- a/tests/local_onionshare_receive_mode_upload_public_mode_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiReceiveTest import GuiReceiveTest - - -class LocalReceiveModePublicModeTest(unittest.TestCase, GuiReceiveTest): - @classmethod - def setUpClass(cls): - test_settings = {"public_mode": True, "receive_allow_receiver_shutdown": True} - cls.gui = GuiReceiveTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiReceiveTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_receive_mode_tests(True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_receive_mode_upload_test.py b/tests/local_onionshare_receive_mode_upload_test.py deleted file mode 100644 index 38888655..00000000 --- a/tests/local_onionshare_receive_mode_upload_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiReceiveTest import GuiReceiveTest - - -class LocalReceiveModeTest(unittest.TestCase, GuiReceiveTest): - @classmethod - def setUpClass(cls): - test_settings = {"receive_allow_receiver_shutdown": True} - cls.gui = GuiReceiveTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiReceiveTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_receive_mode_tests(False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_settings_dialog_legacy_tor_test.py b/tests/local_onionshare_settings_dialog_legacy_tor_test.py deleted file mode 100644 index 72d33241..00000000 --- a/tests/local_onionshare_settings_dialog_legacy_tor_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from onionshare import strings -from .SettingsGuiBaseTest import SettingsGuiBaseTest, OnionStub - - -class SettingsGuiTest(unittest.TestCase, SettingsGuiBaseTest): - @classmethod - def setUpClass(cls): - cls.gui = SettingsGuiBaseTest.set_up() - - @classmethod - def tearDownClass(cls): - SettingsGuiBaseTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui_legacy_tor(self): - self.gui.onion = OnionStub(True, False) - self.gui.reload_settings() - self.run_settings_gui_tests() - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_settings_dialog_no_tor_test.py b/tests/local_onionshare_settings_dialog_no_tor_test.py deleted file mode 100644 index b8c06243..00000000 --- a/tests/local_onionshare_settings_dialog_no_tor_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from onionshare import strings -from .SettingsGuiBaseTest import SettingsGuiBaseTest, OnionStub - - -class SettingsGuiTest(unittest.TestCase, SettingsGuiBaseTest): - @classmethod - def setUpClass(cls): - cls.gui = SettingsGuiBaseTest.set_up() - - @classmethod - def tearDownClass(cls): - SettingsGuiBaseTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui_no_tor(self): - self.gui.onion = OnionStub(False, False) - self.gui.reload_settings() - self.run_settings_gui_tests() - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_settings_dialog_v3_tor_test.py b/tests/local_onionshare_settings_dialog_v3_tor_test.py deleted file mode 100644 index d5abeabc..00000000 --- a/tests/local_onionshare_settings_dialog_v3_tor_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from onionshare import strings -from .SettingsGuiBaseTest import SettingsGuiBaseTest, OnionStub - - -class SettingsGuiTest(unittest.TestCase, SettingsGuiBaseTest): - @classmethod - def setUpClass(cls): - cls.gui = SettingsGuiBaseTest.set_up() - - @classmethod - def tearDownClass(cls): - SettingsGuiBaseTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui_v3_tor(self): - self.gui.onion = OnionStub(True, True) - self.gui.reload_settings() - self.run_settings_gui_tests() - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_autostart_and_autostop_timer_mismatch_test.py b/tests/local_onionshare_share_mode_autostart_and_autostop_timer_mismatch_test.py deleted file mode 100644 index 2a25bef1..00000000 --- a/tests/local_onionshare_share_mode_autostart_and_autostop_timer_mismatch_test.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModeAutoStartTimerTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = { - "public_mode": False, - "autostart_timer": True, - "autostop_timer": True, - } - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_autostop_autostart_mismatch_tests(False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_autostart_timer_test.py b/tests/local_onionshare_share_mode_autostart_timer_test.py deleted file mode 100644 index 776cff4f..00000000 --- a/tests/local_onionshare_share_mode_autostart_timer_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModeAutoStartTimerTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"public_mode": False, "autostart_timer": True} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_autostart_timer_tests(False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_autostart_timer_too_short_test.py b/tests/local_onionshare_share_mode_autostart_timer_too_short_test.py deleted file mode 100644 index 1c2040df..00000000 --- a/tests/local_onionshare_share_mode_autostart_timer_too_short_test.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest -from PyQt5 import QtCore, QtTest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModeAutoStartTimerTooShortTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"public_mode": False, "autostart_timer": True} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_setup_tests() - # Set a low timeout - self.set_autostart_timer(self.gui.share_mode, 2) - QtTest.QTest.qWait(3000) - QtCore.QTimer.singleShot(4000, self.accept_dialog) - QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton - ) - self.assertEqual(self.gui.share_mode.server_status.status, 0) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_cancel_share_test.py b/tests/local_onionshare_share_mode_cancel_share_test.py deleted file mode 100644 index d6ee051b..00000000 --- a/tests/local_onionshare_share_mode_cancel_share_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModeCancelTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"autostart_timer": True} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_setup_tests() - self.cancel_the_share(self.gui.share_mode) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_clear_all_button_test.py b/tests/local_onionshare_share_mode_clear_all_button_test.py deleted file mode 100644 index 1c11fe81..00000000 --- a/tests/local_onionshare_share_mode_clear_all_button_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModeClearAllButtonTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"close_after_first_download": False} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_clear_all_button_tests(False, True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_download_public_mode_test.py b/tests/local_onionshare_share_mode_download_public_mode_test.py deleted file mode 100644 index 6661eae7..00000000 --- a/tests/local_onionshare_share_mode_download_public_mode_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModePublicModeTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"public_mode": True} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_tests(True, False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_download_stay_open_test.py b/tests/local_onionshare_share_mode_download_stay_open_test.py deleted file mode 100644 index 04213865..00000000 --- a/tests/local_onionshare_share_mode_download_stay_open_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModeStayOpenTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"close_after_first_download": False} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_tests(False, True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_download_test.py b/tests/local_onionshare_share_mode_download_test.py deleted file mode 100644 index 1c0b69e9..00000000 --- a/tests/local_onionshare_share_mode_download_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModeTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_tests(False, False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_individual_file_view_stay_open_test.py b/tests/local_onionshare_share_mode_individual_file_view_stay_open_test.py deleted file mode 100644 index 18b3283a..00000000 --- a/tests/local_onionshare_share_mode_individual_file_view_stay_open_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModeIndividualFileViewStayOpenTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"close_after_first_download": False} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_individual_file_tests(False, True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_individual_file_view_test.py b/tests/local_onionshare_share_mode_individual_file_view_test.py deleted file mode 100644 index d41b2010..00000000 --- a/tests/local_onionshare_share_mode_individual_file_view_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModeIndividualFileViewTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"close_after_first_download": True} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_individual_file_tests(False, False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_large_download_test.py b/tests/local_onionshare_share_mode_large_download_test.py deleted file mode 100644 index a0458d03..00000000 --- a/tests/local_onionshare_share_mode_large_download_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModeLargeDownloadTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_large_file_tests(False, True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_password_persistent_test.py b/tests/local_onionshare_share_mode_password_persistent_test.py deleted file mode 100644 index 067815f7..00000000 --- a/tests/local_onionshare_share_mode_password_persistent_test.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModePersistentPasswordTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = { - "public_mode": False, - "password": "", - "save_private_key": True, - "close_after_first_download": False, - } - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_persistent_tests(False, True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_timer_test.py b/tests/local_onionshare_share_mode_timer_test.py deleted file mode 100644 index da200f97..00000000 --- a/tests/local_onionshare_share_mode_timer_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModeTimerTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"public_mode": False, "autostop_timer": True} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_timer_tests(False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_timer_too_short_test.py b/tests/local_onionshare_share_mode_timer_too_short_test.py deleted file mode 100644 index 63c2fdc2..00000000 --- a/tests/local_onionshare_share_mode_timer_too_short_test.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest -from PyQt5 import QtCore, QtTest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModeTimerTooShortTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"public_mode": False, "autostop_timer": True} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_setup_tests() - # Set a low timeout - self.set_timeout(self.gui.share_mode, 2) - QtTest.QTest.qWait(3000) - QtCore.QTimer.singleShot(4000, self.accept_dialog) - QtTest.QTest.mouseClick( - self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton - ) - self.assertEqual(self.gui.share_mode.server_status.status, 0) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_share_mode_unreadable_file_test.py b/tests/local_onionshare_share_mode_unreadable_file_test.py deleted file mode 100644 index 80f0fdb8..00000000 --- a/tests/local_onionshare_share_mode_unreadable_file_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiShareTest import GuiShareTest - - -class LocalShareModeUnReadableFileTest(unittest.TestCase, GuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {} - cls.gui = GuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_unreadable_file_tests() - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_website_mode_csp_enabled_test.py b/tests/local_onionshare_website_mode_csp_enabled_test.py deleted file mode 100644 index f9fdb983..00000000 --- a/tests/local_onionshare_website_mode_csp_enabled_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiWebsiteTest import GuiWebsiteTest - - -class LocalWebsiteModeCSPEnabledTest(unittest.TestCase, GuiWebsiteTest): - @classmethod - def setUpClass(cls): - test_settings = {"csp_header_disabled": False} - cls.gui = GuiWebsiteTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiWebsiteTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - # self.run_all_common_setup_tests() - self.run_all_website_mode_download_tests(False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/local_onionshare_website_mode_test.py b/tests/local_onionshare_website_mode_test.py deleted file mode 100644 index ba00e780..00000000 --- a/tests/local_onionshare_website_mode_test.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .GuiWebsiteTest import GuiWebsiteTest - - -class LocalWebsiteModeTest(unittest.TestCase, GuiWebsiteTest): - @classmethod - def setUpClass(cls): - test_settings = {"csp_header_disabled": True} - cls.gui = GuiWebsiteTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - GuiWebsiteTest.tear_down() - - @pytest.mark.gui - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - # self.run_all_common_setup_tests() - self.run_all_website_mode_download_tests(False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/onionshare_790_cancel_on_second_share_test.py b/tests/onionshare_790_cancel_on_second_share_test.py deleted file mode 100644 index 7a87c690..00000000 --- a/tests/onionshare_790_cancel_on_second_share_test.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .TorGuiShareTest import TorGuiShareTest - -# Tests #790 regression -class ShareModeCancelSecondShareTest(unittest.TestCase, TorGuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"close_after_first_download": True} - cls.gui = TorGuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - TorGuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.tor - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_tests(False, False) - self.cancel_the_share(self.gui.share_mode) - self.server_is_stopped(self.gui.share_mode, False) - self.web_server_is_stopped() - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/onionshare_receive_mode_upload_public_mode_test.py b/tests/onionshare_receive_mode_upload_public_mode_test.py deleted file mode 100644 index 31b1a8f6..00000000 --- a/tests/onionshare_receive_mode_upload_public_mode_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .TorGuiReceiveTest import TorGuiReceiveTest - - -class ReceiveModeTest(unittest.TestCase, TorGuiReceiveTest): - @classmethod - def setUpClass(cls): - test_settings = {"public_mode": True, "receive_allow_receiver_shutdown": True} - cls.gui = TorGuiReceiveTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - TorGuiReceiveTest.tear_down() - - @pytest.mark.gui - @pytest.mark.tor - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_receive_mode_tests(True, True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/onionshare_receive_mode_upload_test.py b/tests/onionshare_receive_mode_upload_test.py deleted file mode 100644 index ca695843..00000000 --- a/tests/onionshare_receive_mode_upload_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .TorGuiReceiveTest import TorGuiReceiveTest - - -class ReceiveModeTest(unittest.TestCase, TorGuiReceiveTest): - @classmethod - def setUpClass(cls): - test_settings = {"receive_allow_receiver_shutdown": True} - cls.gui = TorGuiReceiveTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - TorGuiReceiveTest.tear_down() - - @pytest.mark.gui - @pytest.mark.tor - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_receive_mode_tests(False, True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/onionshare_share_mode_cancel_share_test.py b/tests/onionshare_share_mode_cancel_share_test.py deleted file mode 100644 index 0483fbe1..00000000 --- a/tests/onionshare_share_mode_cancel_share_test.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .TorGuiShareTest import TorGuiShareTest - - -class ShareModeCancelTest(unittest.TestCase, TorGuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"autostart_timer": True} - cls.gui = TorGuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - TorGuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.tor - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_setup_tests() - self.cancel_the_share(self.gui.share_mode) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/onionshare_share_mode_download_public_mode_test.py b/tests/onionshare_share_mode_download_public_mode_test.py deleted file mode 100644 index 72554f8b..00000000 --- a/tests/onionshare_share_mode_download_public_mode_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .TorGuiShareTest import TorGuiShareTest - - -class ShareModePublicModeTest(unittest.TestCase, TorGuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"public_mode": True} - cls.gui = TorGuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - TorGuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.tor - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_tests(True, False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/onionshare_share_mode_download_stay_open_test.py b/tests/onionshare_share_mode_download_stay_open_test.py deleted file mode 100644 index 923eebf3..00000000 --- a/tests/onionshare_share_mode_download_stay_open_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .TorGuiShareTest import TorGuiShareTest - - -class ShareModeStayOpenTest(unittest.TestCase, TorGuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"close_after_first_download": False} - cls.gui = TorGuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - TorGuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.tor - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_tests(False, True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/onionshare_share_mode_download_test.py b/tests/onionshare_share_mode_download_test.py deleted file mode 100644 index 2bebd098..00000000 --- a/tests/onionshare_share_mode_download_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .TorGuiShareTest import TorGuiShareTest - - -class ShareModeTest(unittest.TestCase, TorGuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {} - cls.gui = TorGuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - TorGuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.tor - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_tests(False, False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/onionshare_share_mode_persistent_test.py b/tests/onionshare_share_mode_persistent_test.py deleted file mode 100644 index c5d44733..00000000 --- a/tests/onionshare_share_mode_persistent_test.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .TorGuiShareTest import TorGuiShareTest - - -class ShareModePersistentPasswordTest(unittest.TestCase, TorGuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = { - "use_legacy_v2_onions": True, - "public_mode": False, - "password": "", - "save_private_key": True, - "close_after_first_download": False, - } - cls.gui = TorGuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - TorGuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.tor - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_persistent_tests(False, True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/onionshare_share_mode_stealth_test.py b/tests/onionshare_share_mode_stealth_test.py deleted file mode 100644 index 3ee743d5..00000000 --- a/tests/onionshare_share_mode_stealth_test.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .TorGuiShareTest import TorGuiShareTest - - -class ShareModeStealthTest(unittest.TestCase, TorGuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"use_legacy_v2_onions": True, "use_stealth": True} - cls.gui = TorGuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - TorGuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.tor - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_setup_tests() - self.run_all_share_mode_started_tests(False) - self.hidserv_auth_string() - self.copy_have_hidserv_auth_button(self.gui.share_mode) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/onionshare_share_mode_timer_test.py b/tests/onionshare_share_mode_timer_test.py deleted file mode 100644 index 78a70bbf..00000000 --- a/tests/onionshare_share_mode_timer_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .TorGuiShareTest import TorGuiShareTest - - -class ShareModeTimerTest(unittest.TestCase, TorGuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"public_mode": False, "autostop_timer": True} - cls.gui = TorGuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - TorGuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.tor - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_timer_tests(False) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/onionshare_share_mode_tor_connection_killed_test.py b/tests/onionshare_share_mode_tor_connection_killed_test.py deleted file mode 100644 index 4702ab3e..00000000 --- a/tests/onionshare_share_mode_tor_connection_killed_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .TorGuiShareTest import TorGuiShareTest - - -class ShareModeTorConnectionKilledTest(unittest.TestCase, TorGuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {} - cls.gui = TorGuiShareTest.set_up(test_settings) - - @pytest.mark.gui - @pytest.mark.tor - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_setup_tests() - self.run_all_share_mode_started_tests(False) - self.tor_killed_statusbar_message_shown(self.gui.share_mode) - self.server_is_stopped(self.gui.share_mode, False) - self.web_server_is_stopped() - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/onionshare_share_mode_v2_onion_test.py b/tests/onionshare_share_mode_v2_onion_test.py deleted file mode 100644 index 152457ba..00000000 --- a/tests/onionshare_share_mode_v2_onion_test.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -import pytest -import unittest - -from .TorGuiShareTest import TorGuiShareTest - - -class ShareModeV2OnionTest(unittest.TestCase, TorGuiShareTest): - @classmethod - def setUpClass(cls): - test_settings = {"use_legacy_v2_onions": True} - cls.gui = TorGuiShareTest.set_up(test_settings) - - @classmethod - def tearDownClass(cls): - TorGuiShareTest.tear_down() - - @pytest.mark.gui - @pytest.mark.tor - @pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest") - def test_gui(self): - self.run_all_common_setup_tests() - self.run_all_share_mode_tests(False, False) - self.have_v2_onion() - - -if __name__ == "__main__": - unittest.main() diff --git a/tests2/test_cli.py b/tests/test_cli.py similarity index 100% rename from tests2/test_cli.py rename to tests/test_cli.py diff --git a/tests2/test_cli_common.py b/tests/test_cli_common.py similarity index 100% rename from tests2/test_cli_common.py rename to tests/test_cli_common.py diff --git a/tests2/test_cli_settings.py b/tests/test_cli_settings.py similarity index 100% rename from tests2/test_cli_settings.py rename to tests/test_cli_settings.py diff --git a/tests2/test_cli_strings.py b/tests/test_cli_strings.py similarity index 100% rename from tests2/test_cli_strings.py rename to tests/test_cli_strings.py diff --git a/tests2/test_cli_web.py b/tests/test_cli_web.py similarity index 100% rename from tests2/test_cli_web.py rename to tests/test_cli_web.py diff --git a/tests2/test_gui_receive.py b/tests/test_gui_receive.py similarity index 100% rename from tests2/test_gui_receive.py rename to tests/test_gui_receive.py diff --git a/tests2/test_gui_share.py b/tests/test_gui_share.py similarity index 100% rename from tests2/test_gui_share.py rename to tests/test_gui_share.py diff --git a/tests2/test_gui_tabs.py b/tests/test_gui_tabs.py similarity index 100% rename from tests2/test_gui_tabs.py rename to tests/test_gui_tabs.py diff --git a/tests2/test_gui_website.py b/tests/test_gui_website.py similarity index 100% rename from tests2/test_gui_website.py rename to tests/test_gui_website.py diff --git a/tests/test_helpers.py b/tests/test_helpers.py deleted file mode 100644 index 387fbf4a..00000000 --- a/tests/test_helpers.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee - -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 . -""" -import tempfile -import os - - -class MockSubprocess: - def __init__(self): - self.last_call = None - - def call(self, args): - self.last_call = args - - def last_call_args(self): - return self.last_call - - -def write_tempfile(text): - path = os.path.join(tempfile.mkdtemp(), "/test-file.txt") - with open(path, "w") as f: - f.write(text) - return path diff --git a/tests/test_onionshare.py b/tests/test_onionshare.py deleted file mode 100644 index 0addf6d5..00000000 --- a/tests/test_onionshare.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee - -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 . -""" - -import os - -import pytest - -from onionshare import OnionShare -from onionshare.common import Common -from onionshare.mode_settings import ModeSettings - - -class MyOnion: - def __init__(self): - self.auth_string = "TestHidServAuth" - self.private_key = "" - self.scheduled_key = None - - @staticmethod - def start_onion_service(self, await_publication=True, save_scheduled_key=False): - return "test_service_id.onion" - - -@pytest.fixture -def onionshare_obj(): - common = Common() - return OnionShare(common, MyOnion()) - - -@pytest.fixture -def mode_settings_obj(): - common = Common() - return ModeSettings(common) - - -class TestOnionShare: - def test_init(self, onionshare_obj): - assert onionshare_obj.hidserv_dir is None - assert onionshare_obj.onion_host is None - assert onionshare_obj.cleanup_filenames == [] - assert onionshare_obj.local_only is False - - def test_start_onion_service(self, onionshare_obj, mode_settings_obj): - onionshare_obj.start_onion_service(mode_settings_obj) - assert 17600 <= onionshare_obj.port <= 17650 - assert onionshare_obj.onion_host == "test_service_id.onion" - - def test_start_onion_service_local_only(self, onionshare_obj, mode_settings_obj): - onionshare_obj.local_only = True - onionshare_obj.start_onion_service(mode_settings_obj) - assert onionshare_obj.onion_host == "127.0.0.1:{}".format(onionshare_obj.port) - - def test_cleanup(self, onionshare_obj, temp_dir_1024, temp_file_1024): - onionshare_obj.cleanup_filenames = [temp_dir_1024, temp_file_1024] - onionshare_obj.cleanup() - - assert os.path.exists(temp_dir_1024) is False - assert os.path.exists(temp_dir_1024) is False - assert onionshare_obj.cleanup_filenames == [] diff --git a/tests/test_onionshare_common.py b/tests/test_onionshare_common.py deleted file mode 100644 index 1f230295..00000000 --- a/tests/test_onionshare_common.py +++ /dev/null @@ -1,312 +0,0 @@ -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee - -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 . -""" - -import contextlib -import inspect -import io -import os -import random -import re -import socket -import sys -import zipfile - -import pytest - -LOG_MSG_REGEX = re.compile( - r""" - ^\[Jun\ 06\ 2013\ 11:05:00\] - \ TestModule\.\.dummy_func - \ at\ 0x[a-f0-9]+>(:\ TEST_MSG)?$""", - re.VERBOSE, -) -PASSWORD_REGEX = re.compile(r"^([a-z]+)(-[a-z]+)?-([a-z]+)(-[a-z]+)?$") - - -# TODO: Improve the Common tests to test it all as a single class - - -class TestBuildPassword: - @pytest.mark.parametrize( - "test_input,expected", - ( - # VALID, two lowercase words, separated by a hyphen - ("syrup-enzyme", True), - ("caution-friday", True), - # VALID, two lowercase words, with one hyphenated compound word - ("drop-down-thimble", True), - ("unmixed-yo-yo", True), - # VALID, two lowercase hyphenated compound words, separated by hyphen - ("yo-yo-drop-down", True), - ("felt-tip-t-shirt", True), - ("hello-world", True), - # INVALID - ("Upper-Case", False), - ("digits-123", False), - ("too-many-hyphens-", False), - ("symbols-!@#$%", False), - ), - ) - def test_build_password_regex(self, test_input, expected): - """ Test that `PASSWORD_REGEX` accounts for the following patterns - - There are a few hyphenated words in `wordlist.txt`: - * drop-down - * felt-tip - * t-shirt - * yo-yo - - These words cause a few extra potential password patterns: - * word-word - * hyphenated-word-word - * word-hyphenated-word - * hyphenated-word-hyphenated-word - """ - - assert bool(PASSWORD_REGEX.match(test_input)) == expected - - def test_build_password_unique(self, common_obj, sys_onionshare_dev_mode): - assert common_obj.build_password() != common_obj.build_password() - - -class TestDirSize: - def test_temp_dir_size(self, common_obj, temp_dir_1024_delete): - """ dir_size() should return the total size (in bytes) of all files - in a particular directory. - """ - - assert common_obj.dir_size(temp_dir_1024_delete) == 1024 - - -class TestEstimatedTimeRemaining: - @pytest.mark.parametrize( - "test_input,expected", - ( - ((2, 676, 12), "8h14m16s"), - ((14, 1049, 30), "1h26m15s"), - ((21, 450, 1), "33m42s"), - ((31, 1115, 80), "11m39s"), - ((336, 989, 32), "2m12s"), - ((603, 949, 38), "36s"), - ((971, 1009, 83), "1s"), - ), - ) - def test_estimated_time_remaining( - self, common_obj, test_input, expected, time_time_100 - ): - assert common_obj.estimated_time_remaining(*test_input) == expected - - @pytest.mark.parametrize( - "test_input", - ( - (10, 20, 100), # if `time_elapsed == 0` - (0, 37, 99), # if `download_rate == 0` - ), - ) - def test_raises_zero_division_error(self, common_obj, test_input, time_time_100): - with pytest.raises(ZeroDivisionError): - common_obj.estimated_time_remaining(*test_input) - - -class TestFormatSeconds: - @pytest.mark.parametrize( - "test_input,expected", - ( - (0, "0s"), - (26, "26s"), - (60, "1m"), - (947.35, "15m47s"), - (1847, "30m47s"), - (2193.94, "36m34s"), - (3600, "1h"), - (13426.83, "3h43m47s"), - (16293, "4h31m33s"), - (18392.14, "5h6m32s"), - (86400, "1d"), - (129674, "1d12h1m14s"), - (56404.12, "15h40m4s"), - ), - ) - def test_format_seconds(self, common_obj, test_input, expected): - assert common_obj.format_seconds(test_input) == expected - - # TODO: test negative numbers? - @pytest.mark.parametrize("test_input", ("string", lambda: None, [], {}, set())) - def test_invalid_input_types(self, common_obj, test_input): - with pytest.raises(TypeError): - common_obj.format_seconds(test_input) - - -class TestGetAvailablePort: - @pytest.mark.parametrize( - "port_min,port_max", - ((random.randint(1024, 1500), random.randint(1800, 2048)) for _ in range(50)), - ) - def test_returns_an_open_port(self, common_obj, port_min, port_max): - """ get_available_port() should return an open port within the range """ - - port = common_obj.get_available_port(port_min, port_max) - assert port_min <= port <= port_max - with socket.socket() as tmpsock: - tmpsock.bind(("127.0.0.1", port)) - - -class TestGetPlatform: - def test_darwin(self, platform_darwin, common_obj): - assert common_obj.platform == "Darwin" - - def test_linux(self, platform_linux, common_obj): - assert common_obj.platform == "Linux" - - def test_windows(self, platform_windows, common_obj): - assert common_obj.platform == "Windows" - - -# TODO: double-check these tests -class TestGetResourcePath: - def test_onionshare_dev_mode(self, common_obj, sys_onionshare_dev_mode): - prefix = os.path.join( - os.path.dirname( - os.path.dirname( - os.path.abspath(inspect.getfile(inspect.currentframe())) - ) - ), - "share", - ) - assert common_obj.get_resource_path( - os.path.join(prefix, "test_filename") - ) == os.path.join(prefix, "test_filename") - - def test_linux(self, common_obj, platform_linux, sys_argv_sys_prefix): - prefix = os.path.join(sys.prefix, "share/onionshare") - assert common_obj.get_resource_path( - os.path.join(prefix, "test_filename") - ) == os.path.join(prefix, "test_filename") - - def test_frozen_darwin(self, common_obj, platform_darwin, sys_frozen, sys_meipass): - prefix = os.path.join(sys._MEIPASS, "share") - assert common_obj.get_resource_path( - os.path.join(prefix, "test_filename") - ) == os.path.join(prefix, "test_filename") - - -class TestGetTorPaths: - # @pytest.mark.skipif(sys.platform != 'Darwin', reason='requires MacOS') ? - def test_get_tor_paths_darwin( - self, platform_darwin, common_obj, sys_frozen, sys_meipass - ): - base_path = os.path.dirname( - os.path.dirname(os.path.dirname(common_obj.get_resource_path(""))) - ) - tor_path = os.path.join(base_path, "Resources", "Tor", "tor") - tor_geo_ip_file_path = os.path.join(base_path, "Resources", "Tor", "geoip") - tor_geo_ipv6_file_path = os.path.join(base_path, "Resources", "Tor", "geoip6") - obfs4proxy_file_path = os.path.join(base_path, "Resources", "Tor", "obfs4proxy") - assert common_obj.get_tor_paths() == ( - tor_path, - tor_geo_ip_file_path, - tor_geo_ipv6_file_path, - obfs4proxy_file_path, - ) - - # @pytest.mark.skipif(sys.platform != 'Linux', reason='requires Linux') ? - def test_get_tor_paths_linux(self, platform_linux, common_obj): - assert common_obj.get_tor_paths() == ( - "/usr/bin/tor", - "/usr/share/tor/geoip", - "/usr/share/tor/geoip6", - "/usr/bin/obfs4proxy", - ) - - # @pytest.mark.skipif(sys.platform != 'Windows', reason='requires Windows') ? - def test_get_tor_paths_windows(self, platform_windows, common_obj, sys_frozen): - base_path = os.path.join( - os.path.dirname(os.path.dirname(common_obj.get_resource_path(""))), "tor" - ) - tor_path = os.path.join(os.path.join(base_path, "Tor"), "tor.exe") - obfs4proxy_file_path = os.path.join( - os.path.join(base_path, "Tor"), "obfs4proxy.exe" - ) - tor_geo_ip_file_path = os.path.join( - os.path.join(os.path.join(base_path, "Data"), "Tor"), "geoip" - ) - tor_geo_ipv6_file_path = os.path.join( - os.path.join(os.path.join(base_path, "Data"), "Tor"), "geoip6" - ) - assert common_obj.get_tor_paths() == ( - tor_path, - tor_geo_ip_file_path, - tor_geo_ipv6_file_path, - obfs4proxy_file_path, - ) - - -class TestHumanReadableFilesize: - @pytest.mark.parametrize( - "test_input,expected", - ( - (1024 ** 0, "1.0 B"), - (1024 ** 1, "1.0 KiB"), - (1024 ** 2, "1.0 MiB"), - (1024 ** 3, "1.0 GiB"), - (1024 ** 4, "1.0 TiB"), - (1024 ** 5, "1.0 PiB"), - (1024 ** 6, "1.0 EiB"), - (1024 ** 7, "1.0 ZiB"), - (1024 ** 8, "1.0 YiB"), - ), - ) - def test_human_readable_filesize(self, common_obj, test_input, expected): - assert common_obj.human_readable_filesize(test_input) == expected - - -class TestLog: - @pytest.mark.parametrize( - "test_input", - ( - ( - "[Jun 06 2013 11:05:00]" - " TestModule..dummy_func" - " at 0xdeadbeef>" - ), - ( - "[Jun 06 2013 11:05:00]" - " TestModule..dummy_func" - " at 0xdeadbeef>: TEST_MSG" - ), - ), - ) - def test_log_msg_regex(self, test_input): - assert bool(LOG_MSG_REGEX.match(test_input)) - - def test_output(self, common_obj, time_strftime): - def dummy_func(): - pass - - common_obj.verbose = True - - # From: https://stackoverflow.com/questions/1218933 - with io.StringIO() as buf, contextlib.redirect_stdout(buf): - common_obj.log("TestModule", dummy_func) - common_obj.log("TestModule", dummy_func, "TEST_MSG") - output = buf.getvalue() - - line_one, line_two, _ = output.split("\n") - assert LOG_MSG_REGEX.match(line_one) - assert LOG_MSG_REGEX.match(line_two) diff --git a/tests/test_onionshare_settings.py b/tests/test_onionshare_settings.py deleted file mode 100644 index 1b21ff3b..00000000 --- a/tests/test_onionshare_settings.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee - -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 . -""" - -import json -import os -import tempfile - -import pytest - -from onionshare import common, settings, strings - - -@pytest.fixture -def os_path_expanduser(monkeypatch): - monkeypatch.setattr("os.path.expanduser", lambda path: path) - - -@pytest.fixture -def settings_obj(sys_onionshare_dev_mode, platform_linux): - _common = common.Common() - _common.version = "DUMMY_VERSION_1.2.3" - return settings.Settings(_common) - - -class TestSettings: - def test_init(self, settings_obj): - expected_settings = { - "version": "DUMMY_VERSION_1.2.3", - "connection_type": "bundled", - "control_port_address": "127.0.0.1", - "control_port_port": 9051, - "socks_address": "127.0.0.1", - "socks_port": 9050, - "socket_file_path": "/var/run/tor/control", - "auth_type": "no_auth", - "auth_password": "", - "use_autoupdate": True, - "autoupdate_timestamp": None, - "no_bridges": True, - "tor_bridges_use_obfs4": False, - "tor_bridges_use_meek_lite_azure": False, - "tor_bridges_use_custom_bridges": "", - "persistent_tabs": [], - } - for key in settings_obj._settings: - # Skip locale, it will not always default to the same thing - if key != "locale": - assert settings_obj._settings[key] == settings_obj.default_settings[key] - assert settings_obj._settings[key] == expected_settings[key] - - def test_fill_in_defaults(self, settings_obj): - del settings_obj._settings["version"] - settings_obj.fill_in_defaults() - assert settings_obj._settings["version"] == "DUMMY_VERSION_1.2.3" - - def test_load(self, settings_obj): - custom_settings = { - "version": "CUSTOM_VERSION", - "socks_port": 9999, - "use_stealth": True, - } - tmp_file, tmp_file_path = tempfile.mkstemp() - with open(tmp_file, "w") as f: - json.dump(custom_settings, f) - settings_obj.filename = tmp_file_path - settings_obj.load() - - assert settings_obj._settings["version"] == "CUSTOM_VERSION" - assert settings_obj._settings["socks_port"] == 9999 - assert settings_obj._settings["use_stealth"] is True - - os.remove(tmp_file_path) - assert os.path.exists(tmp_file_path) is False - - def test_save(self, monkeypatch, settings_obj): - monkeypatch.setattr(strings, "_", lambda _: "") - - settings_filename = "default_settings.json" - tmp_dir = tempfile.gettempdir() - settings_path = os.path.join(tmp_dir, settings_filename) - settings_obj.filename = settings_path - settings_obj.save() - with open(settings_path, "r") as f: - settings = json.load(f) - - assert settings_obj._settings == settings - - os.remove(settings_path) - assert os.path.exists(settings_path) is False - - def test_get(self, settings_obj): - assert settings_obj.get("version") == "DUMMY_VERSION_1.2.3" - assert settings_obj.get("connection_type") == "bundled" - assert settings_obj.get("control_port_address") == "127.0.0.1" - assert settings_obj.get("control_port_port") == 9051 - assert settings_obj.get("socks_address") == "127.0.0.1" - assert settings_obj.get("socks_port") == 9050 - assert settings_obj.get("socket_file_path") == "/var/run/tor/control" - assert settings_obj.get("auth_type") == "no_auth" - assert settings_obj.get("auth_password") == "" - assert settings_obj.get("use_autoupdate") is True - assert settings_obj.get("autoupdate_timestamp") is None - assert settings_obj.get("autoupdate_timestamp") is None - assert settings_obj.get("no_bridges") is True - assert settings_obj.get("tor_bridges_use_obfs4") is False - assert settings_obj.get("tor_bridges_use_meek_lite_azure") is False - assert settings_obj.get("tor_bridges_use_custom_bridges") == "" - - def test_set_version(self, settings_obj): - settings_obj.set("version", "CUSTOM_VERSION") - assert settings_obj._settings["version"] == "CUSTOM_VERSION" - - def test_set_control_port_port(self, settings_obj): - settings_obj.set("control_port_port", 999) - assert settings_obj._settings["control_port_port"] == 999 - - settings_obj.set("control_port_port", "NON_INTEGER") - assert settings_obj._settings["control_port_port"] == 9051 - - def test_set_socks_port(self, settings_obj): - settings_obj.set("socks_port", 888) - assert settings_obj._settings["socks_port"] == 888 - - settings_obj.set("socks_port", "NON_INTEGER") - assert settings_obj._settings["socks_port"] == 9050 - - def test_filename_darwin(self, monkeypatch, os_path_expanduser, platform_darwin): - obj = settings.Settings(common.Common()) - assert ( - obj.filename == "~/Library/Application Support/OnionShare/onionshare.json" - ) - - def test_filename_linux(self, monkeypatch, os_path_expanduser, platform_linux): - obj = settings.Settings(common.Common()) - assert obj.filename == "~/.config/onionshare/onionshare.json" - - def test_filename_windows(self, monkeypatch, platform_windows): - monkeypatch.setenv("APPDATA", "C:") - obj = settings.Settings(common.Common()) - assert obj.filename.replace("/", "\\") == "C:\\OnionShare\\onionshare.json" - - def test_set_custom_bridge(self, settings_obj): - settings_obj.set( - "tor_bridges_use_custom_bridges", - "Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E", - ) - assert ( - settings_obj._settings["tor_bridges_use_custom_bridges"] - == "Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E" - ) diff --git a/tests/test_onionshare_strings.py b/tests/test_onionshare_strings.py deleted file mode 100644 index 7ad65191..00000000 --- a/tests/test_onionshare_strings.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee - -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 . -""" - -import types - -import pytest - -from onionshare import strings -from onionshare.settings import Settings - -# # Stub get_resource_path so it finds the correct path while running tests -# def get_resource_path(filename): -# resources_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'share') -# path = os.path.join(resources_dir, filename) -# return path -# common.get_resource_path = get_resource_path - - -def test_underscore_is_function(): - assert callable(strings._) and isinstance(strings._, types.FunctionType) - - -class TestLoadStrings: - def test_load_strings_defaults_to_english( - self, common_obj, locale_en, sys_onionshare_dev_mode - ): - """ load_strings() loads English by default """ - common_obj.settings = Settings(common_obj) - strings.load_strings(common_obj) - assert strings._("preparing_files") == "Compressing files." - - def test_load_strings_loads_other_languages( - self, common_obj, locale_fr, sys_onionshare_dev_mode - ): - """ load_strings() loads other languages in different locales """ - common_obj.settings = Settings(common_obj) - common_obj.settings.set("locale", "fr") - strings.load_strings(common_obj) - assert strings._("preparing_files") == "Compression des fichiers." - - def test_load_invalid_locale( - self, common_obj, locale_invalid, sys_onionshare_dev_mode - ): - """ load_strings() raises a KeyError for an invalid locale """ - with pytest.raises(KeyError): - common_obj.settings = Settings(common_obj) - common_obj.settings.set("locale", "XX") - strings.load_strings(common_obj) diff --git a/tests/test_onionshare_web.py b/tests/test_onionshare_web.py deleted file mode 100644 index 2ce2f758..00000000 --- a/tests/test_onionshare_web.py +++ /dev/null @@ -1,241 +0,0 @@ -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee - -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 . -""" - -import contextlib -import inspect -import io -import os -import random -import re -import socket -import sys -import zipfile -import tempfile -import base64 - -import pytest -from werkzeug.datastructures import Headers - -from onionshare.common import Common -from onionshare import strings -from onionshare.web import Web -from onionshare.settings import Settings -from onionshare.mode_settings import ModeSettings - -DEFAULT_ZW_FILENAME_REGEX = re.compile(r"^onionshare_[a-z2-7]{6}.zip$") -RANDOM_STR_REGEX = re.compile(r"^[a-z2-7]+$") - - -def web_obj(common_obj, mode, num_files=0): - """ Creates a Web object, in either share mode or receive mode, ready for testing """ - common_obj.settings = Settings(common_obj) - strings.load_strings(common_obj) - mode_settings = ModeSettings(common_obj) - web = Web(common_obj, False, mode_settings, mode) - web.generate_password() - web.running = True - - web.app.testing = True - - # Share mode - if mode == "share": - # Add files - files = [] - for _ in range(num_files): - with tempfile.NamedTemporaryFile(delete=False) as tmp_file: - tmp_file.write(b"*" * 1024) - files.append(tmp_file.name) - web.share_mode.set_file_info(files) - # Receive mode - else: - pass - - return web - - -class TestWeb: - def test_share_mode(self, common_obj): - web = web_obj(common_obj, "share", 3) - assert web.mode == "share" - with web.app.test_client() as c: - # Load / without auth - res = c.get("/") - res.get_data() - assert res.status_code == 401 - - # Load / with invalid auth - res = c.get("/", headers=self._make_auth_headers("invalid")) - res.get_data() - assert res.status_code == 401 - - # Load / with valid auth - res = c.get("/", headers=self._make_auth_headers(web.password)) - res.get_data() - assert res.status_code == 200 - - # Download - res = c.get("/download", headers=self._make_auth_headers(web.password)) - res.get_data() - assert res.status_code == 200 - assert res.mimetype == "application/zip" - - def test_share_mode_autostop_sharing_on(self, common_obj, temp_file_1024): - web = web_obj(common_obj, "share", 3) - web.settings.set("share", "autostop_sharing", True) - - assert web.running == True - - with web.app.test_client() as c: - # Download the first time - res = c.get("/download", headers=self._make_auth_headers(web.password)) - res.get_data() - assert res.status_code == 200 - assert res.mimetype == "application/zip" - - assert web.running == False - - def test_share_mode_autostop_sharing_off(self, common_obj, temp_file_1024): - web = web_obj(common_obj, "share", 3) - web.settings.set("share", "autostop_sharing", False) - - assert web.running == True - - with web.app.test_client() as c: - # Download the first time - res = c.get("/download", headers=self._make_auth_headers(web.password)) - res.get_data() - assert res.status_code == 200 - assert res.mimetype == "application/zip" - assert web.running == True - - def test_receive_mode(self, common_obj): - web = web_obj(common_obj, "receive") - assert web.mode == "receive" - - with web.app.test_client() as c: - # Load / without auth - res = c.get("/") - res.get_data() - assert res.status_code == 401 - - # Load / with invalid auth - res = c.get("/", headers=self._make_auth_headers("invalid")) - res.get_data() - assert res.status_code == 401 - - # Load / with valid auth - res = c.get("/", headers=self._make_auth_headers(web.password)) - res.get_data() - assert res.status_code == 200 - - def test_public_mode_on(self, common_obj): - web = web_obj(common_obj, "receive") - web.settings.set("general", "public", True) - - with web.app.test_client() as c: - # Loading / should work without auth - res = c.get("/") - data1 = res.get_data() - assert res.status_code == 200 - - def test_public_mode_off(self, common_obj): - web = web_obj(common_obj, "receive") - web.settings.set("general", "public", False) - - with web.app.test_client() as c: - # Load / without auth - res = c.get("/") - res.get_data() - assert res.status_code == 401 - - # But static resources should work without auth - res = c.get(f"{web.static_url_path}/css/style.css") - res.get_data() - assert res.status_code == 200 - - # Load / with valid auth - res = c.get("/", headers=self._make_auth_headers(web.password)) - res.get_data() - assert res.status_code == 200 - - def _make_auth_headers(self, password): - auth = base64.b64encode(b"onionshare:" + password.encode()).decode() - h = Headers() - h.add("Authorization", "Basic " + auth) - return h - - -class TestZipWriterDefault: - @pytest.mark.parametrize( - "test_input", - ( - f"onionshare_{''.join(random.choice('abcdefghijklmnopqrstuvwxyz234567') for _ in range(6))}.zip" - for _ in range(50) - ), - ) - def test_default_zw_filename_regex(self, test_input): - assert bool(DEFAULT_ZW_FILENAME_REGEX.match(test_input)) - - def test_zw_filename(self, default_zw): - zw_filename = os.path.basename(default_zw.zip_filename) - assert bool(DEFAULT_ZW_FILENAME_REGEX.match(zw_filename)) - - def test_zipfile_filename_matches_zipwriter_filename(self, default_zw): - assert default_zw.z.filename == default_zw.zip_filename - - def test_zipfile_allow_zip64(self, default_zw): - assert default_zw.z._allowZip64 is True - - def test_zipfile_mode(self, default_zw): - assert default_zw.z.mode == "w" - - def test_callback(self, default_zw): - assert default_zw.processed_size_callback(None) is None - - def test_add_file(self, default_zw, temp_file_1024_delete): - default_zw.add_file(temp_file_1024_delete) - zipfile_info = default_zw.z.getinfo(os.path.basename(temp_file_1024_delete)) - - assert zipfile_info.compress_type == zipfile.ZIP_DEFLATED - assert zipfile_info.file_size == 1024 - - def test_add_directory(self, temp_dir_1024_delete, default_zw): - previous_size = default_zw._size # size before adding directory - default_zw.add_dir(temp_dir_1024_delete) - assert default_zw._size == previous_size + 1024 - - -class TestZipWriterCustom: - @pytest.mark.parametrize( - "test_input", - ( - Common.random_string( - random.randint(2, 50), random.choice((None, random.randint(2, 50))) - ) - for _ in range(50) - ), - ) - def test_random_string_regex(self, test_input): - assert bool(RANDOM_STR_REGEX.match(test_input)) - - def test_custom_filename(self, custom_zw): - assert bool(RANDOM_STR_REGEX.match(custom_zw.zip_filename)) - - def test_custom_callback(self, custom_zw): - assert custom_zw.processed_size_callback(None) == "custom_callback" diff --git a/tests2/__init__.py b/tests2/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests2/conftest.py b/tests2/conftest.py deleted file mode 100644 index 200f526d..00000000 --- a/tests2/conftest.py +++ /dev/null @@ -1,208 +0,0 @@ -import sys - -# Force tests to look for resources in the source code tree -sys.onionshare_dev_mode = True - -# Let OnionShare know the tests are running, to avoid colliding with settings files -sys.onionshare_test_mode = True - -import os -import shutil -import tempfile - -import pytest - -from onionshare import common, web, settings, strings - - -# The temporary directory for CLI tests -test_temp_dir = None - - -def pytest_addoption(parser): - parser.addoption( - "--rungui", action="store_true", default=False, help="run GUI tests" - ) - parser.addoption( - "--runtor", action="store_true", default=False, help="run tor tests" - ) - - -def pytest_collection_modifyitems(config, items): - if not config.getoption("--runtor"): - # --runtor given in cli: do not skip tor tests - skip_tor = pytest.mark.skip(reason="need --runtor option to run") - for item in items: - if "tor" in item.keywords: - item.add_marker(skip_tor) - - if not config.getoption("--rungui"): - # --rungui given in cli: do not skip GUI tests - skip_gui = pytest.mark.skip(reason="need --rungui option to run") - for item in items: - if "gui" in item.keywords: - item.add_marker(skip_gui) - - -@pytest.fixture -def temp_dir(): - """Creates a persistent temporary directory for the CLI tests to use""" - global test_temp_dir - if not test_temp_dir: - test_temp_dir = tempfile.mkdtemp() - return test_temp_dir - - -@pytest.fixture -def temp_dir_1024(temp_dir): - """ Create a temporary directory that has a single file of a - particular size (1024 bytes). - """ - - new_temp_dir = tempfile.mkdtemp(dir=temp_dir) - tmp_file, tmp_file_path = tempfile.mkstemp(dir=new_temp_dir) - with open(tmp_file, "wb") as f: - f.write(b"*" * 1024) - return new_temp_dir - - -# pytest > 2.9 only needs @pytest.fixture -@pytest.yield_fixture -def temp_dir_1024_delete(temp_dir): - """ Create a temporary directory that has a single file of a - particular size (1024 bytes). The temporary directory (including - the file inside) will be deleted after fixture usage. - """ - - with tempfile.TemporaryDirectory(dir=temp_dir) as new_temp_dir: - tmp_file, tmp_file_path = tempfile.mkstemp(dir=new_temp_dir) - with open(tmp_file, "wb") as f: - f.write(b"*" * 1024) - yield new_temp_dir - - -@pytest.fixture -def temp_file_1024(temp_dir): - """ Create a temporary file of a particular size (1024 bytes). """ - - with tempfile.NamedTemporaryFile(delete=False, dir=temp_dir) as tmp_file: - tmp_file.write(b"*" * 1024) - return tmp_file.name - - -# pytest > 2.9 only needs @pytest.fixture -@pytest.yield_fixture -def temp_file_1024_delete(temp_dir): - """ - Create a temporary file of a particular size (1024 bytes). - The temporary file will be deleted after fixture usage. - """ - - with tempfile.NamedTemporaryFile(dir=temp_dir) as tmp_file: - tmp_file.write(b"*" * 1024) - tmp_file.flush() - yield tmp_file.name - - -# pytest > 2.9 only needs @pytest.fixture -@pytest.yield_fixture(scope="session") -def custom_zw(): - zw = web.share_mode.ZipWriter( - common.Common(), - zip_filename=common.Common.random_string(4, 6), - processed_size_callback=lambda _: "custom_callback", - ) - yield zw - zw.close() - os.remove(zw.zip_filename) - - -# pytest > 2.9 only needs @pytest.fixture -@pytest.yield_fixture(scope="session") -def default_zw(): - zw = web.share_mode.ZipWriter(common.Common()) - yield zw - zw.close() - tmp_dir = os.path.dirname(zw.zip_filename) - try: - shutil.rmtree(tmp_dir, ignore_errors=True) - except: - pass - - -@pytest.fixture -def locale_en(monkeypatch): - monkeypatch.setattr("locale.getdefaultlocale", lambda: ("en_US", "UTF-8")) - - -@pytest.fixture -def locale_fr(monkeypatch): - monkeypatch.setattr("locale.getdefaultlocale", lambda: ("fr_FR", "UTF-8")) - - -@pytest.fixture -def locale_invalid(monkeypatch): - monkeypatch.setattr("locale.getdefaultlocale", lambda: ("xx_XX", "UTF-8")) - - -@pytest.fixture -def locale_ru(monkeypatch): - monkeypatch.setattr("locale.getdefaultlocale", lambda: ("ru_RU", "UTF-8")) - - -@pytest.fixture -def platform_darwin(monkeypatch): - monkeypatch.setattr("platform.system", lambda: "Darwin") - - -@pytest.fixture # (scope="session") -def platform_linux(monkeypatch): - monkeypatch.setattr("platform.system", lambda: "Linux") - - -@pytest.fixture -def platform_windows(monkeypatch): - monkeypatch.setattr("platform.system", lambda: "Windows") - - -@pytest.fixture -def sys_argv_sys_prefix(monkeypatch): - monkeypatch.setattr("sys.argv", [sys.prefix]) - - -@pytest.fixture -def sys_frozen(monkeypatch): - monkeypatch.setattr("sys.frozen", True, raising=False) - - -@pytest.fixture -def sys_meipass(monkeypatch): - monkeypatch.setattr("sys._MEIPASS", os.path.expanduser("~"), raising=False) - - -@pytest.fixture # (scope="session") -def sys_onionshare_dev_mode(monkeypatch): - monkeypatch.setattr("sys.onionshare_dev_mode", True, raising=False) - - -@pytest.fixture -def time_time_100(monkeypatch): - monkeypatch.setattr("time.time", lambda: 100) - - -@pytest.fixture -def time_strftime(monkeypatch): - monkeypatch.setattr("time.strftime", lambda _: "Jun 06 2013 11:05:00") - - -@pytest.fixture -def common_obj(): - return common.Common() - - -@pytest.fixture -def settings_obj(sys_onionshare_dev_mode, platform_linux): - _common = common.Common() - _common.version = "DUMMY_VERSION_1.2.3" - strings.load_strings(_common) - return settings.Settings(_common) From 6df5ab75f680892341af94a138b35ec24f29f29e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 14:22:00 -0800 Subject: [PATCH 090/135] Add some waits so the tests will pass consistently --- tests/test_gui_receive.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_gui_receive.py b/tests/test_gui_receive.py index 25f5cd3b..b3971144 100644 --- a/tests/test_gui_receive.py +++ b/tests/test_gui_receive.py @@ -69,6 +69,8 @@ class TestReceive(GuiBaseTest): def upload_file_should_fail(self, tab): """Test that we can't upload the file when permissions are wrong, and expected content is shown""" + QtTest.QTest.qWait(1000) + files = {"file[]": open(self.tmpfile_test, "rb")} url = f"http://127.0.0.1:{tab.app.port}/upload" if tab.settings.get("general", "public"): @@ -87,7 +89,7 @@ class TestReceive(GuiBaseTest): if window: window.close() - QtCore.QTimer.singleShot(200, accept_dialog) + QtCore.QTimer.singleShot(1000, accept_dialog) self.assertTrue("Error uploading, please inform the OnionShare user" in r.text) def try_without_auth_in_non_public_mode(self, tab): From 81584e12ff6eddf8755409be0136872c4086c561 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 14:41:08 -0800 Subject: [PATCH 091/135] Try increasing waits more --- tests/test_gui_receive.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_gui_receive.py b/tests/test_gui_receive.py index b3971144..4ee0abd8 100644 --- a/tests/test_gui_receive.py +++ b/tests/test_gui_receive.py @@ -17,8 +17,8 @@ class TestReceive(GuiBaseTest): ): """Test that we can upload the file""" - # Wait 1.1 seconds to make sure the filename, based on timestamp, isn't accidentally reused - QtTest.QTest.qWait(1100) + # Wait 2 seconds to make sure the filename, based on timestamp, isn't accidentally reused + QtTest.QTest.qWait(2000) files = {"file[]": open(file_to_upload, "rb")} url = f"http://127.0.0.1:{tab.app.port}/upload" @@ -45,7 +45,7 @@ class TestReceive(GuiBaseTest): ), ) - QtTest.QTest.qWait(500) + QtTest.QTest.qWait(1000) # Make sure the file is within the last 10 seconds worth of fileames exists = False From 28bc37d16fac74fa3b226d1b9456372b003c7ade Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 17:32:34 -0800 Subject: [PATCH 092/135] Start refactoring Onion to allow for managing a separate onion service for each tab --- onionshare/mode_settings.py | 3 +- onionshare/onion.py | 167 +++++++++++++----------------------- onionshare/onionshare.py | 8 +- onionshare_gui/tab/tab.py | 2 +- onionshare_gui/threads.py | 16 +++- 5 files changed, 82 insertions(+), 114 deletions(-) diff --git a/onionshare/mode_settings.py b/onionshare/mode_settings.py index 5728bbc1..5082b7d7 100644 --- a/onionshare/mode_settings.py +++ b/onionshare/mode_settings.py @@ -36,7 +36,7 @@ class ModeSettings: "enabled": False, "mode": None, "private_key": None, - "hidservauth": None, + "hidservauth_string": None, "password": None, }, "general": { @@ -45,6 +45,7 @@ class ModeSettings: "autostop_timer": False, "legacy": False, "client_auth": False, + "service_id": None, }, "share": {"autostop_sharing": True, "filenames": []}, "receive": {"data_dir": self.build_default_receive_data_dir()}, diff --git a/onionshare/onion.py b/onionshare/onion.py index 0f335dc2..cce9ebfe 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -152,14 +152,8 @@ class Onion(object): def __init__(self, common): self.common = common - self.common.log("Onion", "__init__") - self.stealth = False - self.service_id = None - self.scheduled_key = None - self.scheduled_auth_cookie = None - # Is bundled tor supported? if ( self.common.platform == "Windows" or self.common.platform == "Darwin" @@ -570,48 +564,32 @@ class Onion(object): else: return False - def start_onion_service(self, port, await_publication, save_scheduled_key=False): + def start_onion_service( + self, mode_settings, port, await_publication, save_scheduled_key=False + ): """ Start a onion service on port 80, pointing to the given port, and return the onion hostname. """ - self.common.log("Onion", "start_onion_service") - # Settings may have changed in the frontend but not updated in our settings object, - # such as persistence. Reload the settings now just to be sure. - self.settings.load() - - self.auth_string = None + self.common.log("Onion", "start_onion_service", f"port={port}") if not self.supports_ephemeral: raise TorTooOld(strings._("error_ephemeral_not_supported")) - if self.stealth and not self.supports_stealth: + if mode_settings.get("general", "client_auth") and not self.supports_stealth: raise TorTooOld(strings._("error_stealth_not_supported")) - if not save_scheduled_key: - print(f"Setting up onion service on port {port}.") - - if self.stealth: - if self.settings.get("hidservauth_string"): - hidservauth_string = self.settings.get("hidservauth_string").split()[2] - basic_auth = {"onionshare": hidservauth_string} - else: - if self.scheduled_auth_cookie: - basic_auth = {"onionshare": self.scheduled_auth_cookie} - else: - basic_auth = {"onionshare": None} + if mode_settings.get("general", "client_auth") and mode_settings.get( + "general", "hidservauth_string" + ): + auth_cookie = mode_settings.get("persistent", "hidservauth_string").split()[ + 2 + ] + basic_auth = {"onionshare": auth_cookie} else: basic_auth = None - if self.settings.get("private_key"): - key_content = self.settings.get("private_key") - if self.is_v2_key(key_content): - key_type = "RSA1024" - else: - # Assume it was a v3 key. Stem will throw an error if it's something illegible - key_type = "ED25519-V3" - - elif self.scheduled_key: - key_content = self.scheduled_key + if mode_settings.get("persistent", "private_key"): + key_content = mode_settings.get("persistent", "private_key") if self.is_v2_key(key_content): key_type = "RSA1024" else: @@ -621,9 +599,7 @@ class Onion(object): else: key_type = "NEW" # Work out if we can support v3 onion services, which are preferred - if self.supports_v3_onions and not self.settings.get( - "use_legacy_v2_onions" - ): + if self.supports_v3_onions and not mode_settings.get("general", "legacy"): key_content = "ED25519-V3" else: # fall back to v2 onion services @@ -634,87 +610,62 @@ class Onion(object): if ( key_type == "NEW" and key_content == "ED25519-V3" - and not self.settings.get("use_legacy_v2_onions") + and not mode_settings.get("general", "legacy") ): basic_auth = None - self.stealth = False debug_message = f"key_type={key_type}" if key_type == "NEW": debug_message += f", key_content={key_content}" self.common.log("Onion", "start_onion_service", debug_message) try: - if basic_auth != None: - res = self.c.create_ephemeral_hidden_service( - {80: port}, - await_publication=await_publication, - basic_auth=basic_auth, - key_type=key_type, - key_content=key_content, - ) - else: - # if the stem interface is older than 1.5.0, basic_auth isn't a valid keyword arg - res = self.c.create_ephemeral_hidden_service( - {80: port}, - await_publication=await_publication, - key_type=key_type, - key_content=key_content, - ) + res = self.c.create_ephemeral_hidden_service( + {80: port}, + await_publication=await_publication, + basic_auth=basic_auth, + key_type=key_type, + key_content=key_content, + ) except ProtocolError as e: raise TorErrorProtocolError( strings._("error_tor_protocol_error").format(e.args[0]) ) - self.service_id = res.service_id - onion_host = self.service_id + ".onion" + onion_host = res.service_id + ".onion" - # A new private key was generated and is in the Control port response. - if self.settings.get("save_private_key"): - if not self.settings.get("private_key"): - self.settings.set("private_key", res.private_key) + # Save the service_id + if not mode_settings.get("general", "service_id"): + mode_settings.set("general", "service_id", res.service_id) - # If we were scheduling a future share, register the private key for later re-use - if save_scheduled_key: - self.scheduled_key = res.private_key - else: - self.scheduled_key = None + # Save the private key and hidservauth string if persistence is enabled + if mode_settings.get("persistent", "enabled"): + if not mode_settings.get("persistent", "private_key"): + mode_settings.set("persistent", "private_key", res.private_key) + if mode_settings.get("general", "client_auth") and not mode_settings.get( + "persistent", "hidservauth_string" + ): + auth_cookie = list(res.client_auth.values())[0] + auth_string = f"HidServAuth {onion_host} {auth_cookie}" + mode_settings.set("persistent", "hidservauth_string", auth_string) - if self.stealth: - # Similar to the PrivateKey, the Control port only returns the ClientAuth - # in the response if it was responsible for creating the basic_auth password - # in the first place. - # If we sent the basic_auth (due to a saved hidservauth_string in the settings), - # there is no response here, so use the saved value from settings. - if self.settings.get("save_private_key"): - if self.settings.get("hidservauth_string"): - self.auth_string = self.settings.get("hidservauth_string") - else: - auth_cookie = list(res.client_auth.values())[0] - self.auth_string = f"HidServAuth {onion_host} {auth_cookie}" - self.settings.set("hidservauth_string", self.auth_string) - else: - if not self.scheduled_auth_cookie: - auth_cookie = list(res.client_auth.values())[0] - self.auth_string = f"HidServAuth {onion_host} {auth_cookie}" - if save_scheduled_key: - # Register the HidServAuth for the scheduled share - self.scheduled_auth_cookie = auth_cookie - else: - self.scheduled_auth_cookie = None - else: - self.auth_string = ( - f"HidServAuth {onion_host} {self.scheduled_auth_cookie}" - ) - if not save_scheduled_key: - # We've used the scheduled share's HidServAuth. Reset it to None for future shares - self.scheduled_auth_cookie = None + return onion_host - if onion_host is not None: - self.settings.save() - return onion_host - else: - raise TorErrorProtocolError(strings._("error_tor_protocol_error_unknown")) + def stop_onion_service(self, mode_settings): + """ + Stop a specific onion service + """ + onion_host = mode_settings.get("general", "service_id") + self.common.log("Onion", "stop_onion_service", f"onion host: {onion_host}") + + try: + self.c.remove_ephemeral_hidden_service( + mode_settings.get("general", "service_id") + ) + except: + self.common.log( + "Onion", "stop_onion_service", f"failed to remove {onion_host}" + ) def cleanup(self, stop_tor=True): """ @@ -725,22 +676,20 @@ class Onion(object): # Cleanup the ephemeral onion services, if we have any try: onions = self.c.list_ephemeral_hidden_services() - for onion in onions: + for service_id in onions: + onion_host = f"{service_id}.onion" try: self.common.log( - "Onion", "cleanup", f"trying to remove onion {onion}" + "Onion", "cleanup", f"trying to remove onion {onion_host}" ) - self.c.remove_ephemeral_hidden_service(onion) + self.c.remove_ephemeral_hidden_service(service_id) except: self.common.log( - "Onion", - "cleanup", - f"could not remove onion {onion}.. moving on anyway", + "Onion", "cleanup", f"failed to remove onion {onion_host}" ) pass except: pass - self.service_id = None if stop_tor: # Stop tor process diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index f4828140..1a337965 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -82,12 +82,18 @@ class OnionShare(object): return self.onion_host = self.onion.start_onion_service( - self.port, await_publication, save_scheduled_key + mode_settings, self.port, await_publication, save_scheduled_key ) if mode_settings.get("general", "client_auth"): self.auth_string = self.onion.auth_string + def stop_onion_service(self, mode_settings): + """ + Stop the onion service + """ + self.onion.stop_onion_service(mode_settings) + def cleanup(self): """ Shut everything down and clean up temporary files, etc. diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 7c4926dd..74c42cbd 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -362,7 +362,7 @@ class Tab(QtWidgets.QWidget): def stop_server_finished(self): # When the server stopped, cleanup the ephemeral onion service - self.common.gui.onion.cleanup(stop_tor=False) + self.get_mode().app.stop_onion_service(self.settings) def timer_callback(self): """ diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index fa0f6c7b..95b6dabd 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -20,7 +20,19 @@ along with this program. If not, see . import time from PyQt5 import QtCore -from onionshare.onion import * +from onionshare import strings +from onionshare.onion import ( + TorTooOld, + TorErrorInvalidSetting, + TorErrorAutomatic, + TorErrorSocketPort, + TorErrorSocketFile, + TorErrorMissingPassword, + TorErrorUnreadableCookieFile, + TorErrorAuthError, + TorErrorProtocolError, + BundledTorTimeout, +) class OnionThread(QtCore.QThread): @@ -64,7 +76,7 @@ class OnionThread(QtCore.QThread): time.sleep(0.2) self.success_early.emit() # Unregister the onion so we can use it in the next OnionThread - self.mode.app.onion.cleanup(False) + self.mode.app.start_onion_service(self.mode.settings) else: self.mode.app.start_onion_service( self.mode.settings, await_publication=True From a51fd596fe09dc8f2a3376eb958d8772058555ad Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 11 Nov 2019 17:47:03 +1100 Subject: [PATCH 093/135] Fix TypeError: start_onion_service() takes from 1 to 3 positional arguments but 4 were given --- tests/test_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 0addf6d5..3c85e60d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -33,7 +33,7 @@ class MyOnion: self.scheduled_key = None @staticmethod - def start_onion_service(self, await_publication=True, save_scheduled_key=False): + def start_onion_service(self, mode_settings_obj, await_publication=True, save_scheduled_key=False): return "test_service_id.onion" From 2b85129b29fd02aec6aeebe122dbe814a14a7ab4 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 11 Nov 2019 17:58:40 +1100 Subject: [PATCH 094/135] Add accept dialogs into the ratelimit tests, it seems to help --- tests/test_gui_share.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_gui_share.py b/tests/test_gui_share.py index 387931ec..c8b6292a 100644 --- a/tests/test_gui_share.py +++ b/tests/test_gui_share.py @@ -561,6 +561,11 @@ class TestShare(GuiBaseTest): Rate limit should be triggered """ tab = self.new_share_tab() + def accept_dialog(): + window = tab.common.gui.qtapp.activeWindow() + if window: + window.close() + tab.get_mode().autostop_sharing_checkbox.click() self.run_all_common_setup_tests() @@ -575,6 +580,11 @@ class TestShare(GuiBaseTest): Public mode should skip the rate limit """ tab = self.new_share_tab() + def accept_dialog(): + window = tab.common.gui.qtapp.activeWindow() + if window: + window.close() + tab.get_mode().autostop_sharing_checkbox.click() tab.get_mode().mode_settings_widget.public_checkbox.click() From 8322875de69ac1369c79ae19a03fd9f2acccd4fc Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 11 Nov 2019 18:08:01 +1100 Subject: [PATCH 095/135] Raising qWaits slightly --- tests/gui_base_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/gui_base_test.py b/tests/gui_base_test.py index dc883c05..c33891df 100644 --- a/tests/gui_base_test.py +++ b/tests/gui_base_test.py @@ -80,7 +80,7 @@ class GuiBaseTest(unittest.TestCase): def verify_new_tab(self, tab): # Make sure the new tab widget is showing, and no mode has been started - QtTest.QTest.qWait(500) + QtTest.QTest.qWait(1000) self.assertTrue(tab.new_tab.isVisible()) self.assertFalse(hasattr(tab, "share_mode")) self.assertFalse(hasattr(tab, "receive_mode")) @@ -349,7 +349,7 @@ class GuiBaseTest(unittest.TestCase): def web_server_is_stopped(self, tab): """Test that the web server also stopped""" - QtTest.QTest.qWait(200) + QtTest.QTest.qWait(800) try: requests.get(f"http://127.0.0.1:{tab.app.port}/") From d78d67adf6c05ea6431d25dc025408249a6981dd Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 27 Nov 2019 16:58:03 -0800 Subject: [PATCH 096/135] Fix module names in setup.py, and make data_files use relative paths --- setup.py | 52 ++++++++++++++++------------------------------------ 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/setup.py b/setup.py index 9af72fc1..e6953370 100644 --- a/setup.py +++ b/setup.py @@ -69,42 +69,21 @@ classifiers = [ "Environment :: Web Environment", ] data_files = [ - ( - os.path.join(sys.prefix, "share/applications"), - ["install/org.onionshare.OnionShare.desktop"], - ), - ( - os.path.join(sys.prefix, "share/icons/hicolor/scalable/apps"), - ["install/org.onionshare.OnionShare.svg"], - ), - ( - os.path.join(sys.prefix, "share/metainfo"), - ["install/org.onionshare.OnionShare.appdata.xml"], - ), - (os.path.join(sys.prefix, "share/onionshare"), file_list("share")), - (os.path.join(sys.prefix, "share/onionshare/images"), file_list("share/images")), - (os.path.join(sys.prefix, "share/onionshare/locale"), file_list("share/locale")), - ( - os.path.join(sys.prefix, "share/onionshare/templates"), - file_list("share/templates"), - ), - ( - os.path.join(sys.prefix, "share/onionshare/static/css"), - file_list("share/static/css"), - ), - ( - os.path.join(sys.prefix, "share/onionshare/static/img"), - file_list("share/static/img"), - ), - ( - os.path.join(sys.prefix, "share/onionshare/static/js"), - file_list("share/static/js"), - ), + ("share/applications", ["install/org.onionshare.OnionShare.desktop"],), + ("share/icons/hicolor/scalable/apps", ["install/org.onionshare.OnionShare.svg"],), + ("share/metainfo", ["install/org.onionshare.OnionShare.appdata.xml"],), + ("share/onionshare", file_list("share")), + ("share/onionshare/images", file_list("share/images")), + ("share/onionshare/locale", file_list("share/locale")), + ("share/onionshare/templates", file_list("share/templates"),), + ("share/onionshare/static/css", file_list("share/static/css"),), + ("share/onionshare/static/img", file_list("share/static/img"),), + ("share/onionshare/static/js", file_list("share/static/js"),), ] if not platform.system().endswith("BSD") and platform.system() != "DragonFly": data_files.append( ( - "/usr/share/nautilus-python/extensions/", + "share/nautilus-python/extensions/", ["install/scripts/onionshare-nautilus.py"], ) ) @@ -126,10 +105,11 @@ setup( "onionshare", "onionshare.web", "onionshare_gui", - "onionshare_gui.mode", - "onionshare_gui.mode.share_mode", - "onionshare_gui.mode.receive_mode", - "onionshare_gui.mode.website_mode", + "onionshare_gui.tab", + "onionshare_gui.tab.mode", + "onionshare_gui.tab.mode.share_mode", + "onionshare_gui.tab.mode.receive_mode", + "onionshare_gui.tab.mode.website_mode", ], include_package_data=True, scripts=["install/scripts/onionshare", "install/scripts/onionshare-gui"], From e7c683528d78fd81497d21114c9a0d4b96233470 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 12:24:26 -0800 Subject: [PATCH 097/135] Support handling events by monitoring an events folder for changes --- BUILD.md | 4 +- install/requirements.txt | 1 + onionshare_gui/__init__.py | 46 +++++++++-------- onionshare_gui/event_handler.py | 89 +++++++++++++++++++++++++++++++++ onionshare_gui/gui_common.py | 8 +++ onionshare_gui/main_window.py | 6 +++ onionshare_gui/tab_widget.py | 21 ++++++++ 7 files changed, 152 insertions(+), 23 deletions(-) create mode 100644 onionshare_gui/event_handler.py diff --git a/BUILD.md b/BUILD.md index 7df56466..acd195a2 100644 --- a/BUILD.md +++ b/BUILD.md @@ -14,13 +14,13 @@ Install the needed dependencies: For Debian-like distros: ``` -apt install -y python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python-nautilus tor obfs4proxy python3-pytest build-essential fakeroot python3-all python3-stdeb dh-python python3-flask-httpauth python3-distutils python3-psutil +apt install -y python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python-nautilus tor obfs4proxy python3-pytest build-essential fakeroot python3-all python3-stdeb dh-python python3-flask-httpauth python3-distutils python3-psutil python3-watchdog ``` For Fedora-like distros: ``` -dnf install -y python3-flask python3-flask-httpauth python3-stem python3-qt5 python3-crypto python3-pysocks nautilus-python tor obfs4 python3-pytest rpm-build python3-psutil +dnf install -y python3-flask python3-flask-httpauth python3-stem python3-qt5 python3-crypto python3-pysocks nautilus-python tor obfs4 python3-pytest rpm-build python3-psutil python3-watchdog ``` After that you can try both the CLI and the GUI version of OnionShare: diff --git a/install/requirements.txt b/install/requirements.txt index 729456fe..d2e8c62f 100644 --- a/install/requirements.txt +++ b/install/requirements.txt @@ -21,3 +21,4 @@ requests==2.22.0 stem==1.7.1 urllib3==1.25.3 Werkzeug==0.15.6 +watchdog==0.9.0 \ No newline at end of file diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index c57848a4..949d2ac0 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -23,9 +23,11 @@ import sys import platform import argparse import signal -import psutil from PyQt5 import QtCore, QtWidgets +if platform.system() == "Linux": + import psutil + from onionshare.common import Common from .gui_common import GuiCommon @@ -122,28 +124,30 @@ def main(): sys.exit() # Is there another onionshare-gui running? - existing_pid = None - for proc in psutil.process_iter(attrs=["pid", "name", "cmdline"]): - if proc.info["pid"] == os.getpid(): - continue + if common.platform == "Linux": + existing_pid = None + for proc in psutil.process_iter(attrs=["pid", "name", "cmdline"]): + if proc.info["pid"] == os.getpid(): + continue - if proc.info["name"] == "onionshare-gui": - existing_pid = proc.info["pid"] - break - else: - # Dev mode onionshare? - if proc.info["cmdline"] and len(proc.info["cmdline"]) >= 2: - if ( - os.path.basename(proc.info["cmdline"][0]).lower() == "python" - and os.path.basename(proc.info["cmdline"][1]) == "onionshare-gui" - ): - existing_pid = proc.info["pid"] - break + if proc.info["name"] == "onionshare-gui": + existing_pid = proc.info["pid"] + break + else: + # Dev mode onionshare? + if proc.info["cmdline"] and len(proc.info["cmdline"]) >= 2: + if ( + os.path.basename(proc.info["cmdline"][0]).lower() == "python" + and os.path.basename(proc.info["cmdline"][1]) + == "onionshare-gui" + ): + existing_pid = proc.info["pid"] + break - if existing_pid: - print(f"Opening tab in existing OnionShare window (pid {proc.info['pid']})") - # TODO: open tab - return + if existing_pid: + print(f"Opening tab in existing OnionShare window (pid {proc.info['pid']})") + # TODO: open tab + return # Attach the GUI common parts to the common object common.gui = GuiCommon(common, qtapp, local_only) diff --git a/onionshare_gui/event_handler.py b/onionshare_gui/event_handler.py new file mode 100644 index 00000000..f4d10c24 --- /dev/null +++ b/onionshare_gui/event_handler.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +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 . +""" +import json +import os +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler, FileModifiedEvent +from PyQt5 import QtCore + + +class EventHandler(FileSystemEventHandler, QtCore.QObject): + """ + To trigger an event, write a JSON line to the events file. When that file changes, + each line will be handled as an event. Valid events are: + {"type": "new_tab"} + {"type": "new_share_tab", "filenames": ["file1", "file2"]} + """ + + new_tab = QtCore.pyqtSignal() + new_share_tab = QtCore.pyqtSignal(list) + + def __init__(self, common): + super(EventHandler, self).__init__() + self.common = common + + def on_modified(self, event): + if ( + type(event) == FileModifiedEvent + and event.src_path == self.common.gui.events_filename + ): + # Read all the lines in the events, then delete it + with open(self.common.gui.events_filename, "r") as f: + lines = f.readlines() + os.remove(self.common.gui.events_filename) + + self.common.log( + "EventHandler", "on_modified", f"processing {len(lines)} lines" + ) + for line in lines: + try: + obj = json.loads(line) + if "type" not in obj: + self.common.log( + "EventHandler", + "on_modified", + f"event does not have a type: {obj}", + ) + continue + except json.decoder.JSONDecodeError: + self.common.log( + "EventHandler", "on_modified", f"ignoring invalid line: {line}" + ) + continue + + if obj["type"] == "new_tab": + self.common.log("EventHandler", "on_modified", "new_tab event") + self.new_tab.emit() + + elif obj["type"] == "new_share_tab": + if "filenames" in obj and type(obj["filenames"]) is list: + self.new_share_tab.emit(obj["filenames"]) + else: + self.common.log( + "EventHandler", + "on_modified", + f"invalid new_share_tab event: {obj}", + ) + + else: + self.common.log( + "EventHandler", "on_modified", f"invalid event type: {obj}" + ) + diff --git a/onionshare_gui/gui_common.py b/onionshare_gui/gui_common.py index 54086353..4381545e 100644 --- a/onionshare_gui/gui_common.py +++ b/onionshare_gui/gui_common.py @@ -17,6 +17,8 @@ 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 . """ +import os + from onionshare import strings from onionshare.onion import Onion @@ -44,6 +46,12 @@ class GuiCommon: # Start the Onion self.onion = Onion(common) + # Directory to watch for events + 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") + self.css = { # OnionShareGui styles "tab_widget_new_tab_button": """ diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index 06b51c9b..46c0c962 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -119,6 +119,7 @@ class MainWindow(QtWidgets.QMainWindow): # Tabs self.tabs = TabWidget(self.common, self.system_tray, self.status_bar) + self.tabs.bring_to_front.connect(self.bring_to_front) # If we have saved persistent tabs, try opening those if len(self.common.settings.get("persistent_tabs")) > 0: @@ -281,6 +282,11 @@ class MainWindow(QtWidgets.QMainWindow): self.update_thread.update_available.connect(update_available) self.update_thread.start() + def bring_to_front(self): + self.common.log("MainWindow", "bring_to_front") + self.raise_() + self.activateWindow() + def closeEvent(self, e): self.common.log("MainWindow", "closeEvent") diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index 09407600..926fa491 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -18,11 +18,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ from PyQt5 import QtCore, QtWidgets, QtGui +from watchdog.observers import Observer from onionshare import strings from onionshare.mode_settings import ModeSettings from .tab import Tab +from .event_handler import EventHandler class TabWidget(QtWidgets.QTabWidget): @@ -30,6 +32,8 @@ class TabWidget(QtWidgets.QTabWidget): A custom tab widget, that has a "+" button for adding new tabs """ + bring_to_front = QtCore.pyqtSignal() + def __init__(self, common, system_tray, status_bar): super(TabWidget, self).__init__() self.common = common @@ -67,6 +71,14 @@ class TabWidget(QtWidgets.QTabWidget): self.move_new_tab_button() + # Watch the events file for changes + self.event_handler = EventHandler(common) + self.event_handler.new_tab.connect(self.add_tab) + self.event_handler.new_share_tab.connect(self.new_share_tab) + self.observer = Observer() + self.observer.schedule(self.event_handler, self.common.gui.events_dir) + self.observer.start() + def move_new_tab_button(self): # Find the width of all tabs tabs_width = sum( @@ -95,6 +107,12 @@ class TabWidget(QtWidgets.QTabWidget): mode_settings = ModeSettings(self.common, id=mode_settings_id) self.add_tab(mode_settings) + def new_share_tab(self, filenames): + mode_settings = ModeSettings(self.common) + mode_settings.set("persistent", "mode", "share") + mode_settings.set("share", "filenames", filenames) + self.add_tab(mode_settings) + def add_tab(self, mode_settings=None): tab = Tab(self.common, self.current_tab_id, self.system_tray, self.status_bar) tab.change_title.connect(self.change_title) @@ -111,6 +129,9 @@ class TabWidget(QtWidgets.QTabWidget): # If it's persistent, set the persistent image in the tab self.change_persistent(tab.tab_id, tab.settings.get("persistent", "enabled")) + # Bring the window to front, in case this is being added by an event + self.bring_to_front.emit() + def change_title(self, tab_id, title): index = self.indexOf(self.tabs[tab_id]) self.setTabText(index, title) From 58bc25850736a6a0d71e1d234d1357af8d464e7a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 12:35:57 -0800 Subject: [PATCH 098/135] If there is an existing onionshare-gui process, open a new tab and quit --- install/requirements.txt | 3 +- onionshare_gui/__init__.py | 60 +++++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/install/requirements.txt b/install/requirements.txt index d2e8c62f..ee03cf62 100644 --- a/install/requirements.txt +++ b/install/requirements.txt @@ -21,4 +21,5 @@ requests==2.22.0 stem==1.7.1 urllib3==1.25.3 Werkzeug==0.15.6 -watchdog==0.9.0 \ No newline at end of file +watchdog==0.9.0 +psutil==5.6.7 \ No newline at end of file diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 949d2ac0..52e609a4 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -23,11 +23,10 @@ import sys import platform import argparse import signal +import json +import psutil from PyQt5 import QtCore, QtWidgets -if platform.system() == "Linux": - import psutil - from onionshare.common import Common from .gui_common import GuiCommon @@ -110,6 +109,9 @@ def main(): # Verbose mode? common.verbose = verbose + # Attach the GUI common parts to the common object + common.gui = GuiCommon(common, qtapp, local_only) + # Validation if filenames: valid = True @@ -124,33 +126,37 @@ def main(): sys.exit() # Is there another onionshare-gui running? - if common.platform == "Linux": - existing_pid = None - for proc in psutil.process_iter(attrs=["pid", "name", "cmdline"]): - if proc.info["pid"] == os.getpid(): - continue + existing_pid = None + for proc in psutil.process_iter(attrs=["pid", "name", "cmdline"]): + if proc.info["pid"] == os.getpid(): + continue - if proc.info["name"] == "onionshare-gui": - existing_pid = proc.info["pid"] - break - else: - # Dev mode onionshare? - if proc.info["cmdline"] and len(proc.info["cmdline"]) >= 2: - if ( - os.path.basename(proc.info["cmdline"][0]).lower() == "python" - and os.path.basename(proc.info["cmdline"][1]) - == "onionshare-gui" - ): - existing_pid = proc.info["pid"] - break + if proc.info["name"] == "onionshare-gui": + existing_pid = proc.info["pid"] + break + else: + # Dev mode onionshare? + if proc.info["cmdline"] and len(proc.info["cmdline"]) >= 2: + if ( + os.path.basename(proc.info["cmdline"][0]).lower() == "python" + and os.path.basename(proc.info["cmdline"][1]) == "onionshare-gui" + ): + existing_pid = proc.info["pid"] + break - if existing_pid: - print(f"Opening tab in existing OnionShare window (pid {proc.info['pid']})") - # TODO: open tab - return + if existing_pid: + print(f"Opening tab in existing OnionShare window (pid {proc.info['pid']})") - # Attach the GUI common parts to the common object - common.gui = GuiCommon(common, qtapp, local_only) + # Make an event for the existing OnionShare window + if filenames: + obj = {"type": "new_share_tab", "filenames": filenames} + else: + obj = {"type": "new_tab"} + + # Write that event to disk + with open(common.gui.events_filename, "a") as f: + f.write(json.dumps(obj) + "\n") + return # Launch the gui main_window = MainWindow(common, filenames) From 7e06872a395aecc6b375e0d3e9434800757941b7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 13:26:07 -0800 Subject: [PATCH 099/135] Make nautilus plugin work in python3 --- install/scripts/onionshare-nautilus.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/install/scripts/onionshare-nautilus.py b/install/scripts/onionshare-nautilus.py index dad2330c..776ca5de 100644 --- a/install/scripts/onionshare-nautilus.py +++ b/install/scripts/onionshare-nautilus.py @@ -3,7 +3,10 @@ import sys import json import locale import subprocess -import urllib +try: + import urllib.request +except: + import urllib import gi gi.require_version("Nautilus", "3.0") @@ -67,7 +70,10 @@ class OnionShareExtension(GObject.GObject, Nautilus.MenuProvider): def url2path(self, url): file_uri = url.get_activation_uri() arg_uri = file_uri[7:] - path = urllib.url2pathname(arg_uri) + try: + path = urllib.request.url2pathname(arg_uri) + except: + path = urllib.url2pathname(arg_uri) return path def exec_onionshare(self, filenames): From e316af0e1110de6498aa6b7719e68abb4936399e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 13:33:56 -0800 Subject: [PATCH 100/135] Open share tab if filenames are passed; and when detecting existing onionshare-gui processes, ignore zombies --- onionshare_gui/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 52e609a4..f0186a18 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -131,7 +131,7 @@ def main(): if proc.info["pid"] == os.getpid(): continue - if proc.info["name"] == "onionshare-gui": + if proc.info["name"] == "onionshare-gui" and proc.status() != "zombie": existing_pid = proc.info["pid"] break else: @@ -140,6 +140,7 @@ def main(): if ( os.path.basename(proc.info["cmdline"][0]).lower() == "python" and os.path.basename(proc.info["cmdline"][1]) == "onionshare-gui" + and proc.status() != "zombie" ): existing_pid = proc.info["pid"] break @@ -161,6 +162,10 @@ def main(): # Launch the gui main_window = MainWindow(common, filenames) + # If filenames were passed in, open them in a tab + if filenames: + main_window.tabs.new_share_tab(filenames) + # Clean up when app quits def shutdown(): main_window.cleanup() From 3fe6d43d9db83a60043324abe6ba6d3f2f2dffe2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 14:02:00 -0800 Subject: [PATCH 101/135] If running from onionshare CLI, use a new temporary tor data dir, and if running onionshare-gui, always use the same tor data dir --- onionshare/__init__.py | 2 +- onionshare/common.py | 19 +++++++++++++++++-- onionshare/onion.py | 24 +++++++++++++++--------- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 5b95e3a1..dede3d8a 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -266,7 +266,7 @@ def main(cwd=None): web = Web(common, False, mode_settings, mode) # Start the Onion object - onion = Onion(common) + onion = Onion(common, use_tmp_dir=True) try: onion.connect( custom_settings=False, diff --git a/onionshare/common.py b/onionshare/common.py index e85403eb..7048c174 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -181,15 +181,30 @@ class Common: os.makedirs(onionshare_data_dir, 0o700, True) return onionshare_data_dir + def build_tmp_dir(self): + """ + Returns path to a folder that can hold temporary files + """ + tmp_dir = os.path.join(self.build_data_dir(), "tmp") + os.makedirs(tmp_dir, 0o700, True) + return tmp_dir + def build_persistent_dir(self): """ Returns the path to the folder that holds persistent files """ - onionshare_data_dir = self.build_data_dir() - persistent_dir = os.path.join(onionshare_data_dir, "persistent") + persistent_dir = os.path.join(self.build_data_dir(), "persistent") os.makedirs(persistent_dir, 0o700, True) return persistent_dir + def build_tor_dir(self): + """ + Returns path to the tor data directory + """ + tor_dir = os.path.join(self.build_data_dir(), "tor_data") + os.makedirs(tor_dir, 0o700, True) + return tor_dir + def build_password(self, word_count=2): """ Returns a random string made of words from the wordlist, such as "deter-trig". diff --git a/onionshare/onion.py b/onionshare/onion.py index cce9ebfe..b04ab6b4 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -150,10 +150,12 @@ class Onion(object): is necessary for status updates to reach the GUI. """ - def __init__(self, common): + def __init__(self, common, use_tmp_dir=False): self.common = common self.common.log("Onion", "__init__") + self.use_tmp_dir = use_tmp_dir + # Is bundled tor supported? if ( self.common.platform == "Windows" or self.common.platform == "Darwin" @@ -217,24 +219,28 @@ class Onion(object): ) # Create a torrc for this session - self.tor_data_directory = tempfile.TemporaryDirectory( - dir=self.common.build_data_dir() - ) + if self.use_tmp_dir: + self.tor_data_directory = tempfile.TemporaryDirectory( + dir=self.common.build_tmp_dir() + ) + self.tor_data_directory_name = self.tor_data_directory.name + else: + self.tor_data_directory_name = self.common.build_tor_dir() self.common.log( - "Onion", "connect", f"tor_data_directory={self.tor_data_directory.name}" + "Onion", "connect", f"tor_data_directory_name={self.tor_data_directory_name}" ) # Create the torrc with open(self.common.get_resource_path("torrc_template")) as f: torrc_template = f.read() self.tor_cookie_auth_file = os.path.join( - self.tor_data_directory.name, "cookie" + self.tor_data_directory_name, "cookie" ) try: self.tor_socks_port = self.common.get_available_port(1000, 65535) except: raise OSError(strings._("no_available_port")) - self.tor_torrc = os.path.join(self.tor_data_directory.name, "torrc") + self.tor_torrc = os.path.join(self.tor_data_directory_name, "torrc") if self.common.platform == "Windows" or self.common.platform == "Darwin": # Windows doesn't support unix sockets, so it must use a network port. @@ -252,11 +258,11 @@ class Onion(object): torrc_template += "ControlSocket {{control_socket}}\n" self.tor_control_port = None self.tor_control_socket = os.path.join( - self.tor_data_directory.name, "control_socket" + self.tor_data_directory_name, "control_socket" ) torrc_template = torrc_template.replace( - "{{data_directory}}", self.tor_data_directory.name + "{{data_directory}}", self.tor_data_directory_name ) torrc_template = torrc_template.replace( "{{control_port}}", str(self.tor_control_port) From 695fecd21e3c274d596e88ce35a454b0a428bbd0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 14:31:48 -0800 Subject: [PATCH 102/135] When re-ordering tabs, save the correct order in settings so they open in the correct order again later --- onionshare_gui/tab_widget.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index 926fa491..3a8ef070 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -164,8 +164,10 @@ class TabWidget(QtWidgets.QTabWidget): tab = self.widget(index) if tab.settings.get("persistent", "enabled"): persistent_tabs.append(tab.settings.id) - self.common.settings.set("persistent_tabs", persistent_tabs) - self.common.settings.save() + # Only save if tabs have actually moved + if persistent_tabs != self.common.settings.get("persistent_tabs"): + self.common.settings.set("persistent_tabs", persistent_tabs) + self.common.settings.save() def close_tab(self, index): self.common.log("TabWidget", "close_tab", f"{index}") @@ -196,11 +198,11 @@ class TabWidget(QtWidgets.QTabWidget): return True return False - def changeEvent(self, event): - # TODO: later when I have internet, figure out the right event for re-ordering tabs - - # If tabs get move - super(TabWidget, self).changeEvent(event) + def paintEvent(self, event): + super(TabWidget, self).paintEvent(event) + # Save the order of persistent tabs whenever a new tab is switched to -- ideally we would + # do this whenever tabs gets moved, but paintEvent is the only event that seems to get triggered + # when this happens self.save_persistent_tabs() def resizeEvent(self, event): From 8530321e5d5e835b9c34e8476c3d3f4a7b7d11a0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 14:57:01 -0800 Subject: [PATCH 103/135] When Tor settings change, make sure the tabs know --- onionshare_gui/main_window.py | 48 ++++++++++------------------------- onionshare_gui/tab/tab.py | 13 ++++++++++ share/locale/en.json | 2 +- 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index 46c0c962..14328b26 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -144,6 +144,7 @@ class MainWindow(QtWidgets.QMainWindow): tor_con.open_settings.connect(self.tor_connection_open_settings) if not self.common.gui.local_only: tor_con.start() + self.settings_have_changed() # After connecting to Tor, check for updates self.check_for_updates() @@ -223,44 +224,21 @@ class MainWindow(QtWidgets.QMainWindow): Open the SettingsDialog. """ self.common.log("MainWindow", "open_settings") - - def reload_settings(): - self.common.log( - "OnionShareGui", "open_settings", "settings have changed, reloading" - ) - self.common.settings.load() - - # We might've stopped the main requests timer if a Tor connection failed. - # If we've reloaded settings, we probably succeeded in obtaining a new - # connection. If so, restart the timer. - if not self.common.gui.local_only: - if self.common.gui.onion.is_authenticated(): - if not self.timer.isActive(): - self.timer.start(500) - self.share_mode.on_reload_settings() - self.receive_mode.on_reload_settings() - self.website_mode.on_reload_settings() - self.status_bar.clearMessage() - - # If we switched off the auto-stop timer setting, ensure the widget is hidden. - if not self.common.settings.get("autostop_timer"): - self.share_mode.server_status.autostop_timer_container.hide() - self.receive_mode.server_status.autostop_timer_container.hide() - self.website_mode.server_status.autostop_timer_container.hide() - # If we switched off the auto-start timer setting, ensure the widget is hidden. - if not self.common.settings.get("autostart_timer"): - self.share_mode.server_status.autostart_timer_datetime = None - self.receive_mode.server_status.autostart_timer_datetime = None - self.website_mode.server_status.autostart_timer_datetime = None - self.share_mode.server_status.autostart_timer_container.hide() - self.receive_mode.server_status.autostart_timer_container.hide() - self.website_mode.server_status.autostart_timer_container.hide() - d = SettingsDialog(self.common) - # d.settings_saved.connect(reload_settings) - # TODO: move the reload_settings logic into tabs + d.settings_saved.connect(self.settings_have_changed) d.exec_() + def settings_have_changed(self): + self.common.log("OnionShareGui", "settings_have_changed") + + if self.common.gui.onion.is_authenticated(): + self.status_bar.clearMessage() + + # Tell each tab that settings have changed + for index in range(self.tabs.count()): + tab = self.tabs.widget(index) + tab.settings_have_changed() + def check_for_updates(self): """ Check for updates in a new thread, if enabled. diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 74c42cbd..db10ba97 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -527,6 +527,19 @@ class Tab(QtWidgets.QWidget): else: return None + def settings_have_changed(self): + # Global settings have changed + self.common.log("Tab", "settings_have_changed") + + # We might've stopped the main requests timer if a Tor connection failed. If we've reloaded + # settings, we probably succeeded in obtaining a new connection. If so, restart the timer. + if not self.common.gui.local_only: + if self.common.gui.onion.is_authenticated(): + if not self.timer.isActive(): + self.timer.start(500) + self.get_mode().on_reload_settings() + self.get_mode().primary_action.show() + def close_tab(self): self.common.log("Tab", "close_tab") if self.mode is None: diff --git a/share/locale/en.json b/share/locale/en.json index 5425a336..cca7d92e 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -88,7 +88,7 @@ "settings_error_unreadable_cookie_file": "Connected to the Tor controller, but password may be wrong, or your user is not permitted to read the cookie file.", "settings_error_bundled_tor_not_supported": "Using the Tor version that comes with OnionShare does not work in developer mode on Windows or macOS.", "settings_error_bundled_tor_timeout": "Taking too long to connect to Tor. Maybe you aren't connected to the Internet, or have an inaccurate system clock?", - "settings_error_bundled_tor_broken": "OnionShare could not connect to Tor in the background:\n{}", + "settings_error_bundled_tor_broken": "OnionShare could not connect to Tor:\n{}", "settings_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports client authentication: {}.\nSupports next-gen .onion addresses: {}.", "error_tor_protocol_error": "There was an error with Tor: {}", "error_tor_protocol_error_unknown": "There was an unknown error with Tor", From a0197e49d760ea20f27bea1614b116e0bdbcdcc1 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 14:58:04 -0800 Subject: [PATCH 104/135] Cleanup tabs on cleanup --- onionshare_gui/main_window.py | 4 +++- onionshare_gui/tab/tab.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index 14328b26..d3457583 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -286,5 +286,7 @@ class MainWindow(QtWidgets.QMainWindow): e.accept() def cleanup(self): + for index in range(self.tabs.count()): + tab = self.tabs.widget(index) + tab.cleanup() self.common.gui.onion.cleanup() - # TODO: Run the tab's cleanup diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index db10ba97..35a527c1 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -538,7 +538,6 @@ class Tab(QtWidgets.QWidget): if not self.timer.isActive(): self.timer.start(500) self.get_mode().on_reload_settings() - self.get_mode().primary_action.show() def close_tab(self): self.common.log("Tab", "close_tab") From 780340b3e4709fd982e159a8eef97f6c5cea0570 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 15:06:19 -0800 Subject: [PATCH 105/135] Stop using set_server_active, because all it was used for was to hide the buttons at the top, and we don't need that now that there are tabs --- onionshare_gui/tab/tab.py | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 35a527c1..eb36a042 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -124,9 +124,6 @@ class Tab(QtWidgets.QWidget): self.layout.addWidget(self.new_tab) self.setLayout(self.layout) - # The server isn't active yet - self.set_server_active(False) - # Create the timer self.timer = QtCore.QTimer() self.timer.timeout.connect(self.timer_callback) @@ -201,7 +198,6 @@ class Tab(QtWidgets.QWidget): self.share_mode.server_status.button_clicked.connect(self.clear_message) self.share_mode.server_status.url_copied.connect(self.copy_url) self.share_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth) - self.share_mode.set_server_active.connect(self.set_server_active) self.change_title.emit(self.tab_id, strings._("gui_new_tab_share_button")) @@ -239,7 +235,6 @@ class Tab(QtWidgets.QWidget): self.receive_mode.server_status.hidservauth_copied.connect( self.copy_hidservauth ) - self.receive_mode.set_server_active.connect(self.set_server_active) self.change_title.emit(self.tab_id, strings._("gui_new_tab_receive_button")) @@ -277,7 +272,6 @@ class Tab(QtWidgets.QWidget): self.website_mode.server_status.hidservauth_copied.connect( self.copy_hidservauth ) - self.website_mode.set_server_active.connect(self.set_server_active) self.change_title.emit(self.tab_id, strings._("gui_new_tab_website_button")) @@ -480,36 +474,6 @@ class Tab(QtWidgets.QWidget): strings._("gui_copied_hidservauth"), ) - def set_server_active(self, active): - """ - Disable the Settings and Receive Files buttons while an Share Files server is active. - """ - pass - """ - if active: - self.settings_button.hide() - if self.mode == self.common.gui.MODE_SHARE: - self.share_mode_button.show() - self.receive_mode_button.hide() - self.website_mode_button.hide() - elif self.mode == self.common.gui.MODE_WEBSITE: - self.share_mode_button.hide() - self.receive_mode_button.hide() - self.website_mode_button.show() - else: - self.share_mode_button.hide() - self.receive_mode_button.show() - self.website_mode_button.hide() - else: - self.settings_button.show() - self.share_mode_button.show() - self.receive_mode_button.show() - self.website_mode_button.show() - - # Disable settings menu action when server is active - self.settings_action.setEnabled(not active) - """ - def clear_message(self): """ Clear messages from the status bar. From 5a0ee67dc73b5620f32e909ff0512889dd9451e9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 15:36:53 -0800 Subject: [PATCH 106/135] Don't include psutil twice in requirements.txt --- install/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/install/requirements.txt b/install/requirements.txt index ee03cf62..d1884fa8 100644 --- a/install/requirements.txt +++ b/install/requirements.txt @@ -11,7 +11,6 @@ Jinja2==2.10.1 macholib==1.11 MarkupSafe==1.1.1 pefile==2019.4.18 -psutil==5.6.3 pycryptodome==3.9.0 PyInstaller==3.5 PyQt5==5.13.1 From b80fc5be1772c97c987bbca9fd69ec5fc69b5a05 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 15:46:55 -0800 Subject: [PATCH 107/135] Only reload settings if a mode has been selected --- onionshare_gui/tab/tab.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index eb36a042..45aa9781 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -501,7 +501,9 @@ class Tab(QtWidgets.QWidget): if self.common.gui.onion.is_authenticated(): if not self.timer.isActive(): self.timer.start(500) - self.get_mode().on_reload_settings() + mode = self.get_mode() + if mode: + mode.on_reload_settings() def close_tab(self): self.common.log("Tab", "close_tab") From bea9dc009d04c8dd8d5b96b3d73f81abfb269731 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 15:52:02 -0800 Subject: [PATCH 108/135] When settings change, only start the timer if a mode has been selected --- onionshare_gui/tab/tab.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/onionshare_gui/tab/tab.py b/onionshare_gui/tab/tab.py index 45aa9781..aa4518b5 100644 --- a/onionshare_gui/tab/tab.py +++ b/onionshare_gui/tab/tab.py @@ -377,12 +377,7 @@ class Tab(QtWidgets.QWidget): self.get_mode().handle_tor_broke() # Process events from the web object - if self.mode == self.common.gui.MODE_SHARE: - mode = self.share_mode - elif self.mode == self.common.gui.MODE_WEBSITE: - mode = self.website_mode - else: - mode = self.receive_mode + mode = self.get_mode() events = [] @@ -499,10 +494,10 @@ class Tab(QtWidgets.QWidget): # settings, we probably succeeded in obtaining a new connection. If so, restart the timer. if not self.common.gui.local_only: if self.common.gui.onion.is_authenticated(): - if not self.timer.isActive(): - self.timer.start(500) mode = self.get_mode() if mode: + if not self.timer.isActive(): + self.timer.start(500) mode.on_reload_settings() def close_tab(self): From 849176ac54b4cf0744f67a15b791bb0569e4e629 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 19:30:48 -0800 Subject: [PATCH 109/135] Fix typos in comments --- onionshare/__init__.py | 2 +- onionshare/mode_settings.py | 2 +- onionshare/web/web.py | 4 ---- onionshare_gui/tab/mode/mode_settings_widget.py | 2 +- onionshare_gui/tab_widget.py | 2 +- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index dede3d8a..704f0a51 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -234,7 +234,7 @@ def main(cwd=None): # See what the persistent mode was mode = mode_settings.get("persistent", "mode") - # In share an website mode, you must supply a list of filenames + # In share and website mode, you must supply a list of filenames if mode == "share" or mode == "website": # Unless you passed in a persistent filename, in which case get the filenames from # the mode settings diff --git a/onionshare/mode_settings.py b/onionshare/mode_settings.py index 5082b7d7..6e875f0d 100644 --- a/onionshare/mode_settings.py +++ b/onionshare/mode_settings.py @@ -25,7 +25,7 @@ import json class ModeSettings: """ This stores the settings for a single instance of an OnionShare mode. In CLI there - is only one TabSettings, and in the GUI there is a separate TabSettings for each tab + is only one ModeSettings, and in the GUI there is a separate ModeSettings for each tab """ def __init__(self, common, filename=None, id=None): diff --git a/onionshare/web/web.py b/onionshare/web/web.py index bfdd2cac..8c4373fb 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -61,10 +61,6 @@ class Web: REQUEST_INVALID_PASSWORD = 14 def __init__(self, common, is_gui, mode_settings, mode="share"): - """ - tab_settings_get and tab_settings_set are getter and setter functions for tab settings - """ - self.common = common self.common.log("Web", "__init__", f"is_gui={is_gui}, mode={mode}") diff --git a/onionshare_gui/tab/mode/mode_settings_widget.py b/onionshare_gui/tab/mode/mode_settings_widget.py index 881f893c..a5e42d6d 100644 --- a/onionshare_gui/tab/mode/mode_settings_widget.py +++ b/onionshare_gui/tab/mode/mode_settings_widget.py @@ -17,7 +17,7 @@ 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 . """ -from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt5 import QtCore, QtWidgets from onionshare import strings diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index 3a8ef070..be744ace 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -85,7 +85,7 @@ class TabWidget(QtWidgets.QTabWidget): [self.tabBar().tabRect(i).width() for i in range(self.count())] ) - # The current positoin of the new tab button + # The current position of the new tab button pos = self.new_tab_button.pos() # If there are so many tabs it scrolls, move the button to the left of the scroll buttons From c588783f57855719b6341ab305451daf6db8c917 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 20:32:28 -0800 Subject: [PATCH 110/135] Make cleaning up the onion more reliably kill the tor subprocess, and make iit so testing tor settings in the settings dialog always uses a tmp tor data dir --- onionshare/onion.py | 46 +++++++++++++++---------------- onionshare_gui/settings_dialog.py | 2 +- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index b04ab6b4..4c4112db 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -673,7 +673,7 @@ class Onion(object): "Onion", "stop_onion_service", f"failed to remove {onion_host}" ) - def cleanup(self, stop_tor=True): + def cleanup(self): """ Stop onion services that were created earlier. If there's a tor subprocess running, kill it. """ @@ -697,32 +697,30 @@ class Onion(object): except: pass - if stop_tor: - # Stop tor process - if self.tor_proc: - self.tor_proc.terminate() - time.sleep(0.2) - if not self.tor_proc.poll(): - try: - self.tor_proc.kill() - except: - pass - self.tor_proc = None + # Stop tor process + if self.tor_proc: + self.tor_proc.terminate() + time.sleep(0.2) + if self.tor_proc.poll() == None: + self.common.log("Onion", "cleanup", "Tried to terminate tor process but it's still running") + try: + self.tor_proc.kill() + time.sleep(0.2) + if self.tor_proc.poll() == None: + self.common.log("Onion", "cleanup", "Tried to kill tor process but it's still running") + except: + self.common.log("Onion", "cleanup", "Exception while killing tor process") + self.tor_proc = None - # Reset other Onion settings - self.connected_to_tor = False - self.stealth = False + # Reset other Onion settings + self.connected_to_tor = False - try: - # Delete the temporary tor data directory + try: + # Delete the temporary tor data directory + if self.use_tmp_dir: self.tor_data_directory.cleanup() - except AttributeError: - # Skip if cleanup was somehow run before connect - pass - except PermissionError: - # Skip if the directory is still open (#550) - # TODO: find a better solution - pass + except: + pass def get_tor_socks_port(self): """ diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 110cfac7..45eef270 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -660,7 +660,7 @@ class SettingsDialog(QtWidgets.QDialog): else: tor_status_update_func = None - onion = Onion(self.common) + onion = Onion(self.common, use_tmp_dir=True) onion.connect( custom_settings=settings, tor_status_update_func=tor_status_update_func, From 35e6e88ea1332675638f5e2ca533ab4804cec3eb Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 29 Nov 2019 16:38:34 +1100 Subject: [PATCH 111/135] Fix up autostart (scheduled shares) --- onionshare/onion.py | 113 +++++++++++++++++++++++++++----------- onionshare_gui/threads.py | 2 +- 2 files changed, 83 insertions(+), 32 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 4c4112db..dc71fcb4 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -155,6 +155,8 @@ class Onion(object): self.common.log("Onion", "__init__") self.use_tmp_dir = use_tmp_dir + self.scheduled_key = None + self.scheduled_auth_cookie = None # Is bundled tor supported? if ( @@ -584,14 +586,29 @@ class Onion(object): if mode_settings.get("general", "client_auth") and not self.supports_stealth: raise TorTooOld(strings._("error_stealth_not_supported")) - if mode_settings.get("general", "client_auth") and mode_settings.get( - "general", "hidservauth_string" - ): - auth_cookie = mode_settings.get("persistent", "hidservauth_string").split()[ - 2 - ] - basic_auth = {"onionshare": auth_cookie} + auth_cookie = None + if mode_settings.get("general", "client_auth"): + # If we have an auth cookie that's temporarily saved as part of a + # scheduled share, use that for the basic auth. + if self.scheduled_auth_cookie: + auth_cookie = self.scheduled_auth_cookie + else: + # If we don't have a scheduled share, but are using persistence, then + # we should be able to find a hidservauth_string in saved settings + if mode_settings.get( + "persistent", "hidservauth_string" + ): + auth_cookie = mode_settings.get("persistent", "hidservauth_string").split()[ + 2 + ] + if auth_cookie: + basic_auth = {"onionshare": auth_cookie} + # If we had neither a scheduled auth cookie or a persistent hidservauth string, + # set the cookie to 'None', which means Tor will create one for us + else: + basic_auth = {"onionshare": None} else: + # Not using client auth at all basic_auth = None if mode_settings.get("persistent", "private_key"): @@ -601,7 +618,15 @@ class Onion(object): else: # Assume it was a v3 key. Stem will throw an error if it's something illegible key_type = "ED25519-V3" - + elif self.scheduled_key: + # We have a private key prepared already as part of a scheduled share + # that is about to start. Use that private key instead of a new one. + key_content = self.scheduled_key + if self.is_v2_key(key_content): + key_type = "RSA1024" + else: + # Assume it was a v3 key. Stem will throw an error if it's something illegible + key_type = "ED25519-V3" else: key_type = "NEW" # Work out if we can support v3 onion services, which are preferred @@ -655,6 +680,31 @@ class Onion(object): auth_string = f"HidServAuth {onion_host} {auth_cookie}" mode_settings.set("persistent", "hidservauth_string", auth_string) + # If we were scheduling a future share, register the private key for later re-use + # Save the private key and hidservauth string if persistence is enabled + if save_scheduled_key: + self.scheduled_key = res.private_key + else: + self.scheduled_key = None + + # Likewise, save the hidservauth string if we were scheduling a share + if mode_settings.get("general", "client_auth"): + if not self.scheduled_auth_cookie: + auth_cookie = list(res.client_auth.values())[0] + self.auth_string = f"HidServAuth {onion_host} {auth_cookie}" + if save_scheduled_key: + # Register the HidServAuth for the scheduled share + self.scheduled_auth_cookie = auth_cookie + else: + self.scheduled_auth_cookie = None + else: + self.auth_string = ( + f"HidServAuth {onion_host} {self.scheduled_auth_cookie}" + ) + if not save_scheduled_key: + # We've used the scheduled share's HidServAuth. Reset it to None for future shares + self.scheduled_auth_cookie = None + return onion_host def stop_onion_service(self, mode_settings): @@ -673,7 +723,7 @@ class Onion(object): "Onion", "stop_onion_service", f"failed to remove {onion_host}" ) - def cleanup(self): + def cleanup(self, stop_tor=True): """ Stop onion services that were created earlier. If there's a tor subprocess running, kill it. """ @@ -697,30 +747,31 @@ class Onion(object): except: pass - # Stop tor process - if self.tor_proc: - self.tor_proc.terminate() - time.sleep(0.2) - if self.tor_proc.poll() == None: - self.common.log("Onion", "cleanup", "Tried to terminate tor process but it's still running") - try: - self.tor_proc.kill() - time.sleep(0.2) - if self.tor_proc.poll() == None: - self.common.log("Onion", "cleanup", "Tried to kill tor process but it's still running") - except: - self.common.log("Onion", "cleanup", "Exception while killing tor process") - self.tor_proc = None + if stop_tor: + # Stop tor process + if self.tor_proc: + self.tor_proc.terminate() + time.sleep(0.2) + if self.tor_proc.poll() == None: + self.common.log("Onion", "cleanup", "Tried to terminate tor process but it's still running") + try: + self.tor_proc.kill() + time.sleep(0.2) + if self.tor_proc.poll() == None: + self.common.log("Onion", "cleanup", "Tried to kill tor process but it's still running") + except: + self.common.log("Onion", "cleanup", "Exception while killing tor process") + self.tor_proc = None - # Reset other Onion settings - self.connected_to_tor = False + # Reset other Onion settings + self.connected_to_tor = False - try: - # Delete the temporary tor data directory - if self.use_tmp_dir: - self.tor_data_directory.cleanup() - except: - pass + try: + # Delete the temporary tor data directory + if self.use_tmp_dir: + self.tor_data_directory.cleanup() + except: + pass def get_tor_socks_port(self): """ diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 95b6dabd..332cbd56 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -76,7 +76,7 @@ class OnionThread(QtCore.QThread): time.sleep(0.2) self.success_early.emit() # Unregister the onion so we can use it in the next OnionThread - self.mode.app.start_onion_service(self.mode.settings) + self.mode.app.onion.cleanup(False) else: self.mode.app.start_onion_service( self.mode.settings, await_publication=True From e18c6bf76876d3e35c79d43bbcc81a49134182d2 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 29 Nov 2019 16:46:31 +1100 Subject: [PATCH 112/135] Remove duplicate comment --- onionshare/onion.py | 1 - 1 file changed, 1 deletion(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index dc71fcb4..d8140290 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -681,7 +681,6 @@ class Onion(object): mode_settings.set("persistent", "hidservauth_string", auth_string) # If we were scheduling a future share, register the private key for later re-use - # Save the private key and hidservauth string if persistence is enabled if save_scheduled_key: self.scheduled_key = res.private_key else: From 60de9fd335f6de482d55be3b5d953859eb251fa1 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 29 Nov 2019 16:52:00 +1100 Subject: [PATCH 113/135] Fix the autostop sharing - a mere history item count of > 0 should not be interpreted as an in-progress download when the timer runs out --- onionshare_gui/tab/mode/share_mode/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare_gui/tab/mode/share_mode/__init__.py b/onionshare_gui/tab/mode/share_mode/__init__.py index 9d22e401..1423d60a 100644 --- a/onionshare_gui/tab/mode/share_mode/__init__.py +++ b/onionshare_gui/tab/mode/share_mode/__init__.py @@ -176,7 +176,7 @@ class ShareMode(Mode): The auto-stop timer expired, should we stop the server? Returns a bool """ # If there were no attempts to download the share, or all downloads are done, we can stop - if self.web.share_mode.cur_history_id == 0 or self.web.done: + if self.history.in_progress_count == 0 or self.web.done: self.server_status.stop_server() self.server_status_label.setText(strings._("close_on_autostop_timer")) return True From 9ce2c5cedfeb284e82f8b5ff8034595c79fb3119 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 29 Nov 2019 18:40:45 +1100 Subject: [PATCH 114/135] Ensure we always set the service id, so we can stop the right one (particularly when scheduling a share) --- onionshare/onion.py | 4 +--- onionshare_gui/threads.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index d8140290..e368819c 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -666,8 +666,7 @@ class Onion(object): onion_host = res.service_id + ".onion" # Save the service_id - if not mode_settings.get("general", "service_id"): - mode_settings.set("general", "service_id", res.service_id) + mode_settings.set("general", "service_id", res.service_id) # Save the private key and hidservauth string if persistence is enabled if mode_settings.get("persistent", "enabled"): @@ -712,7 +711,6 @@ class Onion(object): """ onion_host = mode_settings.get("general", "service_id") self.common.log("Onion", "stop_onion_service", f"onion host: {onion_host}") - try: self.c.remove_ephemeral_hidden_service( mode_settings.get("general", "service_id") diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 332cbd56..bfa4f72d 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -76,7 +76,7 @@ class OnionThread(QtCore.QThread): time.sleep(0.2) self.success_early.emit() # Unregister the onion so we can use it in the next OnionThread - self.mode.app.onion.cleanup(False) + self.mode.app.stop_onion_service(self.mode.settings) else: self.mode.app.start_onion_service( self.mode.settings, await_publication=True From 19e378dff444a09a64a5ac41904a247f25e6b8b8 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 8 Dec 2019 09:52:29 -0800 Subject: [PATCH 115/135] Pass the correct args to UpdateChecker --- onionshare_gui/main_window.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index d3457583..0c9b179c 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -254,9 +254,7 @@ class MainWindow(QtWidgets.QMainWindow): ), ) - self.update_thread = UpdateThread( - self.common, self.common.gui.onion, self.common.gui.config - ) + self.update_thread = UpdateThread(self.common, self.common.gui.onion) self.update_thread.update_available.connect(update_available) self.update_thread.start() From 76d109747e7caa861ee1a05e70334679d1942b04 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 8 Dec 2019 10:13:56 -0800 Subject: [PATCH 116/135] Move private_key, hidservauth_string, and password from "persistent" mode settings to "onion" mode settings; and make it so the onion settings are always saved in each tab, even if the tab is not persistent, so if you stop and start a service in the same tab it has the same onion address and password --- onionshare/__init__.py | 6 ++-- onionshare/mode_settings.py | 5 ++- onionshare/onion.py | 53 +++++++++++++++++------------ onionshare/web/web.py | 12 +++---- onionshare_gui/tab/server_status.py | 7 ++-- onionshare_gui/threads.py | 2 +- 6 files changed, 45 insertions(+), 40 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 704f0a51..8d2077c2 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -284,7 +284,7 @@ def main(cwd=None): try: common.settings.load() if not mode_settings.get("general", "public"): - web.generate_password(mode_settings.get("persistent", "password")) + web.generate_password(mode_settings.get("onion", "password")) else: web.password = None app = OnionShare(common, onion, local_only, autostop_timer) @@ -385,8 +385,8 @@ def main(cwd=None): # Save the web password if we are using a persistent private key if mode_settings.get("persistent", "enabled"): - if not mode_settings.get("persistent", "password"): - mode_settings.set("persistent", "password", web.password) + if not mode_settings.get("onion", "password"): + mode_settings.set("onion", "password", web.password) # mode_settings.save() # Build the URL diff --git a/onionshare/mode_settings.py b/onionshare/mode_settings.py index 6e875f0d..9201721e 100644 --- a/onionshare/mode_settings.py +++ b/onionshare/mode_settings.py @@ -32,13 +32,12 @@ class ModeSettings: self.common = common self.default_settings = { - "persistent": { - "enabled": False, - "mode": None, + "onion": { "private_key": None, "hidservauth_string": None, "password": None, }, + "persistent": {"mode": None, "enabled": False}, "general": { "public": False, "autostart_timer": False, diff --git a/onionshare/onion.py b/onionshare/onion.py index e368819c..9520f381 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -229,7 +229,9 @@ class Onion(object): else: self.tor_data_directory_name = self.common.build_tor_dir() self.common.log( - "Onion", "connect", f"tor_data_directory_name={self.tor_data_directory_name}" + "Onion", + "connect", + f"tor_data_directory_name={self.tor_data_directory_name}", ) # Create the torrc @@ -595,12 +597,10 @@ class Onion(object): else: # If we don't have a scheduled share, but are using persistence, then # we should be able to find a hidservauth_string in saved settings - if mode_settings.get( - "persistent", "hidservauth_string" - ): - auth_cookie = mode_settings.get("persistent", "hidservauth_string").split()[ - 2 - ] + if mode_settings.get("onion", "hidservauth_string"): + auth_cookie = mode_settings.get( + "onion", "hidservauth_string" + ).split()[2] if auth_cookie: basic_auth = {"onionshare": auth_cookie} # If we had neither a scheduled auth cookie or a persistent hidservauth string, @@ -611,8 +611,8 @@ class Onion(object): # Not using client auth at all basic_auth = None - if mode_settings.get("persistent", "private_key"): - key_content = mode_settings.get("persistent", "private_key") + if mode_settings.get("onion", "private_key"): + key_content = mode_settings.get("onion", "private_key") if self.is_v2_key(key_content): key_type = "RSA1024" else: @@ -668,16 +668,15 @@ class Onion(object): # Save the service_id mode_settings.set("general", "service_id", res.service_id) - # Save the private key and hidservauth string if persistence is enabled - if mode_settings.get("persistent", "enabled"): - if not mode_settings.get("persistent", "private_key"): - mode_settings.set("persistent", "private_key", res.private_key) - if mode_settings.get("general", "client_auth") and not mode_settings.get( - "persistent", "hidservauth_string" - ): - auth_cookie = list(res.client_auth.values())[0] - auth_string = f"HidServAuth {onion_host} {auth_cookie}" - mode_settings.set("persistent", "hidservauth_string", auth_string) + # Save the private key and hidservauth string + if not mode_settings.get("onion", "private_key"): + mode_settings.set("onion", "private_key", res.private_key) + if mode_settings.get("general", "client_auth") and not mode_settings.get( + "onion", "hidservauth_string" + ): + auth_cookie = list(res.client_auth.values())[0] + auth_string = f"HidServAuth {onion_host} {auth_cookie}" + mode_settings.set("onion", "hidservauth_string", auth_string) # If we were scheduling a future share, register the private key for later re-use if save_scheduled_key: @@ -750,14 +749,24 @@ class Onion(object): self.tor_proc.terminate() time.sleep(0.2) if self.tor_proc.poll() == None: - self.common.log("Onion", "cleanup", "Tried to terminate tor process but it's still running") + self.common.log( + "Onion", + "cleanup", + "Tried to terminate tor process but it's still running", + ) try: self.tor_proc.kill() time.sleep(0.2) if self.tor_proc.poll() == None: - self.common.log("Onion", "cleanup", "Tried to kill tor process but it's still running") + self.common.log( + "Onion", + "cleanup", + "Tried to kill tor process but it's still running", + ) except: - self.common.log("Onion", "cleanup", "Exception while killing tor process") + self.common.log( + "Onion", "cleanup", "Exception while killing tor process" + ) self.tor_proc = None # Reset other Onion settings diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 8c4373fb..8582e694 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -304,16 +304,14 @@ class Web: """ self.q.put({"type": request_type, "path": path, "data": data}) - def generate_password(self, persistent_password=None): - self.common.log( - "Web", "generate_password", f"persistent_password={persistent_password}" - ) - if persistent_password != None and persistent_password != "": - self.password = persistent_password + def generate_password(self, saved_password=None): + self.common.log("Web", "generate_password", f"saved_password={saved_password}") + if saved_password != None and saved_password != "": + self.password = saved_password self.common.log( "Web", "generate_password", - f'persistent_password sent, so password is: "{self.password}"', + f'saved_password sent, so password is: "{self.password}"', ) else: self.password = self.common.build_password() diff --git a/onionshare_gui/tab/server_status.py b/onionshare_gui/tab/server_status.py index 33e1f37c..5f58877c 100644 --- a/onionshare_gui/tab/server_status.py +++ b/onionshare_gui/tab/server_status.py @@ -207,10 +207,9 @@ class ServerStatus(QtWidgets.QWidget): self.common.settings.load() self.show_url() - if self.settings.get("persistent", "enabled"): - if not self.settings.get("persistent", "password"): - self.settings.set("persistent", "password", self.web.password) - self.settings.save() + if not self.settings.get("onion", "password"): + self.settings.set("onion", "password", self.web.password) + self.settings.save() if self.settings.get("general", "autostop_timer"): self.server_button.setToolTip( diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index bfa4f72d..81d5ac5c 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -64,7 +64,7 @@ class OnionThread(QtCore.QThread): if not self.mode.settings.get("general", "public"): if not self.mode.web.password: self.mode.web.generate_password( - self.mode.settings.get("persistent", "password") + self.mode.settings.get("onion", "password") ) try: From de57a2ce551cbebd3644bcbdffacb8d30f0e005b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 8 Dec 2019 10:29:00 -0800 Subject: [PATCH 117/135] After you start a server in a tab, you can't change legacy/client auth settings, because this would require changing the saved onion key --- onionshare_gui/tab/mode/mode_settings_widget.py | 11 +++++++++++ onionshare_gui/tab/server_status.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/onionshare_gui/tab/mode/mode_settings_widget.py b/onionshare_gui/tab/mode/mode_settings_widget.py index a5e42d6d..34335494 100644 --- a/onionshare_gui/tab/mode/mode_settings_widget.py +++ b/onionshare_gui/tab/mode/mode_settings_widget.py @@ -185,6 +185,17 @@ class ModeSettingsWidget(QtWidgets.QWidget): else: self.client_auth_checkbox.hide() + # If the server has been started in the past, prevent changing legacy option + if self.settings.get("onion", "private_key"): + if self.legacy_checkbox.isChecked(): + # If using legacy, disable legacy and client auth options + self.legacy_checkbox.setEnabled(False) + self.client_auth_checkbox.setEnabled(False) + else: + # If using v3, hide legacy and client auth options + self.legacy_checkbox.hide() + self.client_auth_checkbox.hide() + def persistent_checkbox_clicked(self): self.settings.set("persistent", "enabled", self.persistent_checkbox.isChecked()) self.settings.set("persistent", "mode", self.tab.mode) diff --git a/onionshare_gui/tab/server_status.py b/onionshare_gui/tab/server_status.py index 5f58877c..0fbc11b6 100644 --- a/onionshare_gui/tab/server_status.py +++ b/onionshare_gui/tab/server_status.py @@ -225,6 +225,8 @@ class ServerStatus(QtWidgets.QWidget): self.copy_url_button.hide() self.copy_hidservauth_button.hide() + self.mode_settings_widget.update_ui() + # Button if ( self.mode == self.common.gui.MODE_SHARE From a217e54d28dd29ba1efc5314094f2b354320d69d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 8 Dec 2019 12:51:30 -0800 Subject: [PATCH 118/135] Refactor Onion to store all state for auto-start timer directly in the mode settings, and not in the Onion object itself --- onionshare/onion.py | 58 +++++---------------------------------- onionshare/onionshare.py | 6 ++-- onionshare_gui/threads.py | 2 +- 3 files changed, 10 insertions(+), 56 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 9520f381..88d72496 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -155,8 +155,6 @@ class Onion(object): self.common.log("Onion", "__init__") self.use_tmp_dir = use_tmp_dir - self.scheduled_key = None - self.scheduled_auth_cookie = None # Is bundled tor supported? if ( @@ -574,9 +572,7 @@ class Onion(object): else: return False - def start_onion_service( - self, mode_settings, port, await_publication, save_scheduled_key=False - ): + def start_onion_service(self, mode_settings, port, await_publication): """ Start a onion service on port 80, pointing to the given port, and return the onion hostname. @@ -590,22 +586,15 @@ class Onion(object): auth_cookie = None if mode_settings.get("general", "client_auth"): - # If we have an auth cookie that's temporarily saved as part of a - # scheduled share, use that for the basic auth. - if self.scheduled_auth_cookie: - auth_cookie = self.scheduled_auth_cookie - else: - # If we don't have a scheduled share, but are using persistence, then - # we should be able to find a hidservauth_string in saved settings - if mode_settings.get("onion", "hidservauth_string"): - auth_cookie = mode_settings.get( - "onion", "hidservauth_string" - ).split()[2] + if mode_settings.get("onion", "hidservauth_string"): + auth_cookie = mode_settings.get("onion", "hidservauth_string").split()[ + 2 + ] if auth_cookie: basic_auth = {"onionshare": auth_cookie} - # If we had neither a scheduled auth cookie or a persistent hidservauth string, - # set the cookie to 'None', which means Tor will create one for us else: + # If we had neither a scheduled auth cookie or a persistent hidservauth string, + # set the cookie to 'None', which means Tor will create one for us basic_auth = {"onionshare": None} else: # Not using client auth at all @@ -618,15 +607,6 @@ class Onion(object): else: # Assume it was a v3 key. Stem will throw an error if it's something illegible key_type = "ED25519-V3" - elif self.scheduled_key: - # We have a private key prepared already as part of a scheduled share - # that is about to start. Use that private key instead of a new one. - key_content = self.scheduled_key - if self.is_v2_key(key_content): - key_type = "RSA1024" - else: - # Assume it was a v3 key. Stem will throw an error if it's something illegible - key_type = "ED25519-V3" else: key_type = "NEW" # Work out if we can support v3 onion services, which are preferred @@ -678,30 +658,6 @@ class Onion(object): auth_string = f"HidServAuth {onion_host} {auth_cookie}" mode_settings.set("onion", "hidservauth_string", auth_string) - # If we were scheduling a future share, register the private key for later re-use - if save_scheduled_key: - self.scheduled_key = res.private_key - else: - self.scheduled_key = None - - # Likewise, save the hidservauth string if we were scheduling a share - if mode_settings.get("general", "client_auth"): - if not self.scheduled_auth_cookie: - auth_cookie = list(res.client_auth.values())[0] - self.auth_string = f"HidServAuth {onion_host} {auth_cookie}" - if save_scheduled_key: - # Register the HidServAuth for the scheduled share - self.scheduled_auth_cookie = auth_cookie - else: - self.scheduled_auth_cookie = None - else: - self.auth_string = ( - f"HidServAuth {onion_host} {self.scheduled_auth_cookie}" - ) - if not save_scheduled_key: - # We've used the scheduled share's HidServAuth. Reset it to None for future shares - self.scheduled_auth_cookie = None - return onion_host def stop_onion_service(self, mode_settings): diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index 1a337965..0fa25767 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -63,9 +63,7 @@ class OnionShare(object): except: raise OSError(strings._("no_available_port")) - def start_onion_service( - self, mode_settings, await_publication=True, save_scheduled_key=False - ): + def start_onion_service(self, mode_settings, await_publication=True): """ Start the onionshare onion service. """ @@ -82,7 +80,7 @@ class OnionShare(object): return self.onion_host = self.onion.start_onion_service( - mode_settings, self.port, await_publication, save_scheduled_key + mode_settings, self.port, await_publication ) if mode_settings.get("general", "client_auth"): diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 81d5ac5c..785e6ece 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -70,7 +70,7 @@ class OnionThread(QtCore.QThread): try: if self.mode.obtain_onion_early: self.mode.app.start_onion_service( - self.mode.settings, await_publication=False, save_scheduled_key=True + self.mode.settings, await_publication=False ) # wait for modules in thread to load, preventing a thread-related cx_Freeze crash time.sleep(0.2) From 579c87c37941c63dab616174e8246776cd2811b6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 11:32:18 -0700 Subject: [PATCH 119/135] No longer install packages from requirements.txt --- .circleci/config.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4555d3ca..15e04224 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,8 +25,6 @@ jobs: command: | sudo apt-get update sudo apt-get install -y python3-pip python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python3-stdeb python3-all python-nautilus xvfb obfs4proxy - sudo pip3 install -r install/requirements.txt - sudo pip3 install -r install/requirements-tests.txt sudo pip3 install pytest-cov flake8 # run tests! From 3b79a9ba2e79e409f2efe2fa993d8d85dff88267 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 11:35:33 -0700 Subject: [PATCH 120/135] Add python3-pytest and python3-pytestqt to circleci --- .circleci/config.yml | 2 +- BUILD.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 15e04224..796b2810 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,7 @@ jobs: name: install dependencies command: | sudo apt-get update - sudo apt-get install -y python3-pip python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python3-stdeb python3-all python-nautilus xvfb obfs4proxy + sudo apt-get install -y python3-pip python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python3-flask-httpauth python3-distutils python3-pytest python3-pytestqt python3-stdeb python3-all python-nautilus xvfb obfs4proxy sudo pip3 install pytest-cov flake8 # run tests! diff --git a/BUILD.md b/BUILD.md index 7be0cc28..16009bd8 100644 --- a/BUILD.md +++ b/BUILD.md @@ -33,7 +33,7 @@ Install the needed dependencies: #### For Debian-like distros: ``` -apt install -y python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python-nautilus tor obfs4proxy python3-pytest build-essential fakeroot python3-all python3-stdeb dh-python python3-flask-httpauth python3-distutils +apt install -y python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python-nautilus tor obfs4proxy python3-pytest python3-pytestqt build-essential fakeroot python3-all python3-stdeb dh-python python3-flask-httpauth python3-distutils ``` #### For Fedora-like distros: From 5c4e4ce10bd90ce103afaddfda1a625b8df8e062 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 11:45:37 -0700 Subject: [PATCH 121/135] Try using buster docker images --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 796b2810..24b7bb68 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,7 +13,7 @@ workflows: jobs: test-3.6: &test-template docker: - - image: circleci/python:3.6.6 + - image: circleci/python:3.6-buster working_directory: ~/repo @@ -44,4 +44,4 @@ jobs: test-3.7: <<: *test-template docker: - - image: circleci/python:3.7.1 + - image: circleci/python:3.7-buster From 3c6e0cbda10bed2f6a2a604c4421a7bdf3cdf4eb Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 12:33:35 -0700 Subject: [PATCH 122/135] Change python version to ^3.7, and use PyQt 5.14 instead of the very latest. Run tests from poetry --- .circleci/config.yml | 7 +- poetry.lock | 441 ++----------------------------------------- pyproject.toml | 4 +- 3 files changed, 20 insertions(+), 432 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 24b7bb68..18b7a5aa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,8 +24,9 @@ jobs: name: install dependencies command: | sudo apt-get update - sudo apt-get install -y python3-pip python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python3-flask-httpauth python3-distutils python3-pytest python3-pytestqt python3-stdeb python3-all python-nautilus xvfb obfs4proxy - sudo pip3 install pytest-cov flake8 + sudo apt-get install -y python3-pip xvfb + sudo pip3 install poetry flake8 + poetry install # run tests! - run: @@ -39,7 +40,7 @@ jobs: - run: name: run tests command: | - xvfb-run -s "-screen 0 1280x1024x24" pytest --rungui --cov=onionshare --cov=onionshare_gui --cov-report=term-missing -vvv --no-qt-log tests/ + xvfb-run -s "-screen 0 1280x1024x24" poetry run pytest --rungui -vvv --no-qt-log tests/ test-3.7: <<: *test-template diff --git a/poetry.lock b/poetry.lock index 479ea03b..7c4603ba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -44,14 +44,6 @@ optional = false python-versions = "*" version = "3.0.4" -[[package]] -category = "main" -description = "Composable command line interface toolkit" -name = "click" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "7.0" - [[package]] category = "main" description = "Composable command line interface toolkit" @@ -63,43 +55,12 @@ version = "7.1.1" [[package]] category = "dev" description = "Cross-platform colored terminal text." -marker = "sys_platform == \"win32\" and python_version == \"3.4\"" -name = "colorama" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.4.1" - -[[package]] -category = "dev" -description = "Cross-platform colored terminal text." -marker = "sys_platform == \"win32\" and python_version != \"3.4\" or sys_platform == \"win32\"" +marker = "sys_platform == \"win32\"" name = "colorama" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "0.4.3" -[[package]] -category = "dev" -description = "Updated configparser from Python 3.7 for Python 2.6+." -marker = "python_version < \"3\"" -name = "configparser" -optional = false -python-versions = ">=2.6" -version = "4.0.2" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytest-flake8", "pytest-black-multipy"] - -[[package]] -category = "dev" -description = "Backports and enhancements for the contextlib module" -marker = "python_version < \"3.4\"" -name = "contextlib2" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.6.0.post1" - [[package]] category = "dev" description = "Python 2.7 backport of the \"dis\" module from Python 3.5+" @@ -109,34 +70,6 @@ optional = false python-versions = "*" version = "0.1.3" -[[package]] -category = "dev" -description = "Display the Python traceback on a crash" -marker = "python_version == \"2.7\" and platform_python_implementation != \"PyPy\"" -name = "faulthandler" -optional = false -python-versions = "*" -version = "3.2" - -[[package]] -category = "main" -description = "A simple framework for building complex web applications." -name = "flask" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.0.4" - -[package.dependencies] -Jinja2 = ">=2.10" -Werkzeug = ">=0.14" -click = ">=5.1" -itsdangerous = ">=0.24" - -[package.extras] -dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] -docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] -dotenv = ["python-dotenv"] - [[package]] category = "main" description = "A simple framework for building complex web applications." @@ -167,15 +100,6 @@ version = "3.3.0" [package.dependencies] Flask = "*" -[[package]] -category = "dev" -description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" -marker = "python_version < \"3.0\"" -name = "funcsigs" -optional = false -python-versions = "*" -version = "1.0.2" - [[package]] category = "main" description = "Clean single-source support for Python 3 and 2" @@ -184,14 +108,6 @@ optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" version = "0.18.2" -[[package]] -category = "main" -description = "Internationalized Domain Names in Applications (IDNA)" -name = "idna" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.8" - [[package]] category = "main" description = "Internationalized Domain Names in Applications (IDNA)" @@ -200,22 +116,6 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "2.9" -[[package]] -category = "dev" -description = "Read metadata from Python packages" -marker = "python_version < \"3.8\"" -name = "importlib-metadata" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "1.1.3" - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "rst.linker"] -testing = ["packaging", "importlib-resources"] - [[package]] category = "dev" description = "Read metadata from Python packages" @@ -228,18 +128,6 @@ version = "1.5.0" [package.dependencies] zipp = ">=0.5" -[package.dependencies.configparser] -python = "<3" -version = ">=3.5" - -[package.dependencies.contextlib2] -python = "<3" -version = "*" - -[package.dependencies.pathlib2] -python = "<3" -version = "*" - [package.extras] docs = ["sphinx", "rst.linker"] testing = ["packaging", "importlib-resources"] @@ -252,20 +140,6 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "1.1.0" -[[package]] -category = "main" -description = "A very fast and expressive template engine." -name = "jinja2" -optional = false -python-versions = "*" -version = "2.10.3" - -[package.dependencies] -MarkupSafe = ">=0.23" - -[package.extras] -i18n = ["Babel (>=0.8)"] - [[package]] category = "main" description = "A very fast and expressive template engine." @@ -299,25 +173,6 @@ optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" version = "1.1.1" -[[package]] -category = "dev" -description = "More routines for operating on iterables, beyond itertools" -name = "more-itertools" -optional = false -python-versions = "*" -version = "5.0.0" - -[package.dependencies] -six = ">=1.0.0,<2.0.0" - -[[package]] -category = "dev" -description = "More routines for operating on iterables, beyond itertools" -name = "more-itertools" -optional = false -python-versions = ">=3.4" -version = "7.2.0" - [[package]] category = "dev" description = "More routines for operating on iterables, beyond itertools" @@ -338,22 +193,6 @@ version = "20.3" pyparsing = ">=2.0.2" six = "*" -[[package]] -category = "dev" -description = "Object-oriented filesystem paths" -marker = "python_version < \"3.6\"" -name = "pathlib2" -optional = false -python-versions = "*" -version = "2.3.5" - -[package.dependencies] -six = "*" - -[package.dependencies.scandir] -python = "<3.5" -version = "*" - [[package]] category = "main" description = "File system general utilities" @@ -416,19 +255,6 @@ optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "3.9.7" -[[package]] -category = "dev" -description = "PyInstaller bundles a Python application and all its dependencies into a single package." -marker = "sys_platform == \"darwin\"" -name = "pyinstaller" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.5" - -[package.dependencies] -altgraph = "*" -setuptools = "*" - [[package]] category = "dev" description = "PyInstaller bundles a Python application and all its dependencies into a single package." @@ -451,36 +277,17 @@ optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" version = "2.4.6" -[[package]] -category = "main" -description = "Python bindings for the Qt cross platform UI and application toolkit" -name = "pyqt5" -optional = false -python-versions = "*" -version = "5.13.2" - -[package.dependencies] -PyQt5_sip = ">=4.19.19,<13" - [[package]] category = "main" description = "Python bindings for the Qt cross platform application toolkit" name = "pyqt5" optional = false python-versions = ">=3.5" -version = "5.14.1" +version = "5.14.0" [package.dependencies] PyQt5-sip = ">=12.7,<13" -[[package]] -category = "main" -description = "Python extension module support for PyQt5" -name = "pyqt5-sip" -optional = false -python-versions = "*" -version = "4.19.19" - [[package]] category = "main" description = "The sip module support for PyQt5" @@ -497,54 +304,6 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "1.7.1" -[[package]] -category = "dev" -description = "pytest: simple powerful testing with Python" -name = "pytest" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "4.6.9" - -[package.dependencies] -atomicwrites = ">=1.0" -attrs = ">=17.4.0" -packaging = "*" -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -six = ">=1.10.0" -wcwidth = "*" - -[[package.dependencies.colorama]] -python = "<3.4.0 || >=3.5.0" -version = "*" - -[[package.dependencies.colorama]] -python = ">=3.4,<3.5" -version = "<=0.4.1" - -[[package.dependencies.more-itertools]] -python = "<2.8" -version = ">=4.0.0,<6.0.0" - -[[package.dependencies.more-itertools]] -python = ">=2.8" -version = ">=4.0.0" - -[package.dependencies.funcsigs] -python = "<3.0" -version = ">=1.0" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" - -[package.dependencies.pathlib2] -python = "<3.6" -version = ">=2.2.0" - -[package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "nose", "requests", "mock"] - [[package]] category = "dev" description = "pytest: simple powerful testing with Python" @@ -567,29 +326,10 @@ wcwidth = "*" python = "<3.8" version = ">=0.12" -[package.dependencies.pathlib2] -python = "<3.6" -version = ">=2.2.0" - [package.extras] checkqa-mypy = ["mypy (v0.761)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] -[[package]] -category = "dev" -description = "py.test plugin that activates the fault handler module for tests" -name = "pytest-faulthandler" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.6.0" - -[package.dependencies] -pytest = ">=4.0" - -[package.dependencies.faulthandler] -python = ">=2.7,<2.8" -version = "*" - [[package]] category = "dev" description = "py.test plugin that activates the fault handler module for tests (dummy package)" @@ -616,24 +356,6 @@ pytest = ">=3.0.0" dev = ["pre-commit", "tox"] doc = ["sphinx", "sphinx-rtd-theme"] -[[package]] -category = "main" -description = "Python HTTP for Humans." -name = "requests" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.21.0" - -[package.dependencies] -certifi = ">=2017.4.17" -chardet = ">=3.0.2,<3.1.0" -idna = ">=2.5,<2.9" -urllib3 = ">=1.21.1,<1.25" - -[package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] - [[package]] category = "main" description = "Python HTTP for Humans." @@ -652,15 +374,6 @@ urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] -[[package]] -category = "dev" -description = "scandir, a better directory iterator and faster os.walk()" -marker = "python_version < \"3.5\"" -name = "scandir" -optional = false -python-versions = "*" -version = "1.10.0" - [[package]] category = "dev" description = "Python 2 and 3 compatibility utilities" @@ -677,30 +390,6 @@ optional = false python-versions = "*" version = "1.8.0" -[[package]] -category = "main" -description = "HTTP library with thread-safe connection pooling, file post, and more." -name = "urllib3" -optional = false -python-versions = "*" -version = "1.22" - -[package.extras] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] - -[[package]] -category = "main" -description = "HTTP library with thread-safe connection pooling, file post, and more." -name = "urllib3" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" -version = "1.24.3" - -[package.extras] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] - [[package]] category = "main" description = "HTTP library with thread-safe connection pooling, file post, and more." @@ -736,19 +425,6 @@ optional = false python-versions = "*" version = "0.1.8" -[[package]] -category = "main" -description = "The comprehensive WSGI web application library." -name = "werkzeug" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.16.1" - -[package.extras] -dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] -termcolor = ["termcolor"] -watchdog = ["watchdog"] - [[package]] category = "main" description = "The comprehensive WSGI web application library." @@ -767,21 +443,16 @@ description = "Backport of pathlib-compatible object wrapper for zip files" marker = "python_version < \"3.8\"" name = "zipp" optional = false -python-versions = ">=2.7" -version = "1.2.0" - -[package.dependencies] -[package.dependencies.contextlib2] -python = "<3.4" -version = "*" +python-versions = ">=3.6" +version = "3.1.0" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pathlib2", "unittest2", "jaraco.itertools", "func-timeout"] +testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "47cac9d28916836244924702eea56fff904d64bde723fe212d1caa60f5f24891" -python-versions = "*" +content-hash = "41d68ea93701fdaa1aa56159195db7a65863e3b34cc7305ef4a3f5d02f2bdf13" +python-versions = "^3.7" [metadata.files] altgraph = [ @@ -805,38 +476,19 @@ chardet = [ {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, ] click = [ - {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, - {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"}, {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"}, ] colorama = [ - {file = "colorama-0.4.1-py2.py3-none-any.whl", hash = "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"}, - {file = "colorama-0.4.1.tar.gz", hash = "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d"}, {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ] -configparser = [ - {file = "configparser-4.0.2-py2.py3-none-any.whl", hash = "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c"}, - {file = "configparser-4.0.2.tar.gz", hash = "sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df"}, -] -contextlib2 = [ - {file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, - {file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, -] dis3 = [ {file = "dis3-0.1.3-py2-none-any.whl", hash = "sha256:61f7720dd0d8749d23fda3d7227ce74d73da11c2fade993a67ab2f9852451b14"}, {file = "dis3-0.1.3-py3-none-any.whl", hash = "sha256:30b6412d33d738663e8ded781b138f4b01116437f0872aa56aa3adba6aeff218"}, {file = "dis3-0.1.3.tar.gz", hash = "sha256:9259b881fc1df02ed12ac25f82d4a85b44241854330b1a651e40e0c675cb2d1e"}, ] -faulthandler = [ - {file = "faulthandler-3.2-cp27-cp27m-win32.whl", hash = "sha256:7bdc1d529988c081fe60da7691ed26466e06cc52843583a9e6ba4f897422d6c4"}, - {file = "faulthandler-3.2-cp27-cp27m-win_amd64.whl", hash = "sha256:de80f67157b4185925781ad8be303bac2bc72dc580135fbf5dbeb311404f5a57"}, - {file = "faulthandler-3.2.tar.gz", hash = "sha256:1ecdfd76368f02780eec6d9ec02af460190bf18ebfeb3999d7015c979b94cb23"}, -] flask = [ - {file = "Flask-1.0.4-py2.py3-none-any.whl", hash = "sha256:1a21ccca71cee5e55b6a367cc48c6eb47e3c447f76e64d41f3f3f931c17e7c96"}, - {file = "Flask-1.0.4.tar.gz", hash = "sha256:ed1330220a321138de53ec7c534c3d90cf2f7af938c7880fc3da13aa46bf870f"}, {file = "Flask-1.1.1-py2.py3-none-any.whl", hash = "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"}, {file = "Flask-1.1.1.tar.gz", hash = "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52"}, ] @@ -844,22 +496,14 @@ flask-httpauth = [ {file = "Flask-HTTPAuth-3.3.0.tar.gz", hash = "sha256:6ef8b761332e780f9ff74d5f9056c2616f52babc1998b01d9f361a1e439e61b9"}, {file = "Flask_HTTPAuth-3.3.0-py2.py3-none-any.whl", hash = "sha256:0149953720489407e51ec24bc2f86273597b7973d71cd51f9443bd0e2a89bd72"}, ] -funcsigs = [ - {file = "funcsigs-1.0.2-py2.py3-none-any.whl", hash = "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca"}, - {file = "funcsigs-1.0.2.tar.gz", hash = "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"}, -] future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] idna = [ - {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, - {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, ] importlib-metadata = [ - {file = "importlib_metadata-1.1.3-py2.py3-none-any.whl", hash = "sha256:7c7f8ac40673f507f349bef2eed21a0e5f01ddf5b2a7356a6c65eb2099b53764"}, - {file = "importlib_metadata-1.1.3.tar.gz", hash = "sha256:7a99fb4084ffe6dae374961ba7a6521b79c1d07c658ab3a28aa264ee1d1b14e3"}, {file = "importlib_metadata-1.5.0-py2.py3-none-any.whl", hash = "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"}, {file = "importlib_metadata-1.5.0.tar.gz", hash = "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302"}, ] @@ -868,8 +512,6 @@ itsdangerous = [ {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, ] jinja2 = [ - {file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"}, - {file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"}, {file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"}, {file = "Jinja2-2.11.1.tar.gz", hash = "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250"}, ] @@ -913,11 +555,6 @@ markupsafe = [ {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] more-itertools = [ - {file = "more-itertools-5.0.0.tar.gz", hash = "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4"}, - {file = "more_itertools-5.0.0-py2-none-any.whl", hash = "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc"}, - {file = "more_itertools-5.0.0-py3-none-any.whl", hash = "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"}, - {file = "more-itertools-7.2.0.tar.gz", hash = "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832"}, - {file = "more_itertools-7.2.0-py3-none-any.whl", hash = "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"}, {file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"}, {file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"}, ] @@ -925,10 +562,6 @@ packaging = [ {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, ] -pathlib2 = [ - {file = "pathlib2-2.3.5-py2.py3-none-any.whl", hash = "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db"}, - {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, -] pathtools = [ {file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"}, ] @@ -989,7 +622,6 @@ pycryptodome = [ {file = "pycryptodome-3.9.7.tar.gz", hash = "sha256:f1add21b6d179179b3c177c33d18a2186a09cc0d3af41ff5ed3f377360b869f2"}, ] pyinstaller = [ - {file = "PyInstaller-3.5.tar.gz", hash = "sha256:ee7504022d1332a3324250faf2135ea56ac71fdb6309cff8cd235de26b1d0a96"}, {file = "PyInstaller-3.6.tar.gz", hash = "sha256:3730fa80d088f8bb7084d32480eb87cbb4ddb64123363763cf8f2a1378c1c4b7"}, ] pyparsing = [ @@ -997,33 +629,13 @@ pyparsing = [ {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"}, ] pyqt5 = [ - {file = "PyQt5-5.13.2-5.13.2-cp35.cp36.cp37.cp38-abi3-macosx_10_6_intel.whl", hash = "sha256:3f79de6e9f29e858516cc36ffc2b992e262af841f3799246aec282b76a3eccdf"}, - {file = "PyQt5-5.13.2-5.13.2-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:1936c321301f678d4e6703d52860e1955e5c4964e6fd00a1f86725ce5c29083c"}, - {file = "PyQt5-5.13.2-5.13.2-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:14737bb4673868d15fa91dad79fe293d7a93d76c56d01b3757b350b8dcb32b2d"}, - {file = "PyQt5-5.13.2-5.13.2-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:509daab1c5aca22e3cf9508128abf38e6e5ae311d7426b21f4189ffd66b196e9"}, - {file = "PyQt5-5.14.1-5.14.1-cp35.cp36.cp37.cp38-abi3-macosx_10_6_intel.whl", hash = "sha256:a0bfe9fd718bca4de3e33000347e048f73126b6dc46530eb020b0251a638ee9d"}, - {file = "PyQt5-5.14.1-5.14.1-cp35.cp36.cp37.cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:713b9a201f5e7b2fca8691373e5d5c8c2552a51d87ca9ffbb1461e34e3241211"}, - {file = "PyQt5-5.14.1-5.14.1-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:2d94ec761fb656707050c68b41958e3a9f755bb1df96c064470f4096d2899e32"}, - {file = "PyQt5-5.14.1-5.14.1-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:31b142a868152d60c6323e0527edb692fdf05fd7cb4fe2fe9ce07d1ce560221a"}, - {file = "PyQt5-5.14.1.tar.gz", hash = "sha256:2f230f2dbd767099de7a0cb915abdf0cbc3256a0b5bb910eb09b99117db7a65b"}, + {file = "PyQt5-5.14.0-5.14.0-cp35.cp36.cp37.cp38-abi3-macosx_10_6_intel.whl", hash = "sha256:895d4101f7f8c82bc728d7eb9da1c756955ce27a0c945eafe7f234dd03402853"}, + {file = "PyQt5-5.14.0-5.14.0-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:a757ba71c51f428b52ba404e781e2f19b4436b2c31298b8313339d5817781b65"}, + {file = "PyQt5-5.14.0-5.14.0-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:cc3529c0f7cbbe7491073458d5d15e7518ce544ad8c627f485e5db8a27fcaf61"}, + {file = "PyQt5-5.14.0-5.14.0-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:0dcc128b72f83cce0fc7926c83f05a9b74b652b5eb31a4ab71693ac8829e73c8"}, + {file = "PyQt5-5.14.0.tar.gz", hash = "sha256:0145a6b7de15756366decb736c349a0cb510d706c83fda5b8cd9e0557bc1da72"}, ] pyqt5-sip = [ - {file = "PyQt5_sip-4.19.19-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:aade50f9a1b9d20f6aabe88e8999b10db57218f5c31950160f3f7957dd64e07c"}, - {file = "PyQt5_sip-4.19.19-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c309dbbd6c155e961bfd6893496afa5cd184cce6f7dffd87ea68ee048b6f97e1"}, - {file = "PyQt5_sip-4.19.19-cp35-none-win32.whl", hash = "sha256:7fbb6389c20aff4c3257e89bb1787effffcaf05c32d937c00094ae45846bffd5"}, - {file = "PyQt5_sip-4.19.19-cp35-none-win_amd64.whl", hash = "sha256:828d9911acc483672a2bae1cc1bf79f591eb3338faad1f2c798aa2f45459a318"}, - {file = "PyQt5_sip-4.19.19-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:ac9e5b282d1f0771a8310ed974afe1961ec31e9ae787d052c0e504ea46ae323a"}, - {file = "PyQt5_sip-4.19.19-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d7b26e0b6d81bf14c1239e6a891ac1303a7e882512d990ec330369c7269226d7"}, - {file = "PyQt5_sip-4.19.19-cp36-none-win32.whl", hash = "sha256:f8b7a3e05235ce58a38bf317f71a5cb4ab45d3b34dc57421dd8cea48e0e4023e"}, - {file = "PyQt5_sip-4.19.19-cp36-none-win_amd64.whl", hash = "sha256:a9460dac973deccc6ff2d90f18fd105cbaada147f84e5917ed79374dcb237758"}, - {file = "PyQt5_sip-4.19.19-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:ba41bd21b89c6713f7077b5f7d4a1c452989190aad5704e215560a266a1ecbab"}, - {file = "PyQt5_sip-4.19.19-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7b3b8c015e545fa30e42205fc1115b7c6afcb6acec790ce3f330a06323730523"}, - {file = "PyQt5_sip-4.19.19-cp37-none-win32.whl", hash = "sha256:59f5332f86f3ccd3ac94674fe91eae6e8aca26da7c6588917cabd0fe22af106d"}, - {file = "PyQt5_sip-4.19.19-cp37-none-win_amd64.whl", hash = "sha256:54b99a3057e8f01b90d49cca9ca566b1ea23d8920038760f44e75b90c62b9d5f"}, - {file = "PyQt5_sip-4.19.19-cp38-cp38-macosx_10_6_intel.whl", hash = "sha256:39d2677f4de46ed4d7aa3b612f31c74c881975efe51c6a23fbb1d9382e4cc850"}, - {file = "PyQt5_sip-4.19.19-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:304acf771b6033cb4bafc415939d227c91265d30664ed643b298d7e95f509f81"}, - {file = "PyQt5_sip-4.19.19-cp38-none-win32.whl", hash = "sha256:cfc21b1f80d4655ffa776c505a2576b4d148bbc52bb3e33fedbf6cfbdbc09d1b"}, - {file = "PyQt5_sip-4.19.19-cp38-none-win_amd64.whl", hash = "sha256:72be07a21b0f379987c4ec59bc86834a9719a2f9cfb49606a4d4e34dae9aa549"}, {file = "PyQt5_sip-12.7.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:f314f31f5fd39b06897f013f425137e511d45967150eb4e424a363d8138521c6"}, {file = "PyQt5_sip-12.7.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b42021229424aa44e99b3b49520b799fd64ff6ae8b53f79f903bbd85719a28e4"}, {file = "PyQt5_sip-12.7.1-cp35-cp35m-win32.whl", hash = "sha256:6b4860c4305980db509415d0af802f111d15f92016c9422eb753bc8883463456"}, @@ -1048,14 +660,10 @@ pysocks = [ {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, ] pytest = [ - {file = "pytest-4.6.9-py2.py3-none-any.whl", hash = "sha256:c77a5f30a90e0ce24db9eaa14ddfd38d4afb5ea159309bdd2dae55b931bc9324"}, - {file = "pytest-4.6.9.tar.gz", hash = "sha256:19e8f75eac01dd3f211edd465b39efbcbdc8fc5f7866d7dd49fedb30d8adf339"}, {file = "pytest-5.4.1-py3-none-any.whl", hash = "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172"}, {file = "pytest-5.4.1.tar.gz", hash = "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"}, ] pytest-faulthandler = [ - {file = "pytest-faulthandler-1.6.0.tar.gz", hash = "sha256:58ce36506476117231192d45697caaa29c44c209be577767d6f40be8bdf16eaf"}, - {file = "pytest_faulthandler-1.6.0-py2.py3-none-any.whl", hash = "sha256:9b73670671a011b26a24f3433ec8068c93bd043ed841fe13c7951abb430d4264"}, {file = "pytest-faulthandler-2.0.1.tar.gz", hash = "sha256:ed72bbce87ac344da81eb7d882196a457d4a1026a3da4a57154dacd85cd71ae5"}, {file = "pytest_faulthandler-2.0.1-py2.py3-none-any.whl", hash = "sha256:236430ba962fd1c910d670922be55fe5b25ea9bc3fc6561a0cafbb8759e7504d"}, ] @@ -1064,24 +672,9 @@ pytest-qt = [ {file = "pytest_qt-3.3.0-py2.py3-none-any.whl", hash = "sha256:5f8928288f50489d83f5d38caf2d7d9fcd6e7cf769947902caa4661dc7c851e3"}, ] requests = [ - {file = "requests-2.21.0-py2.py3-none-any.whl", hash = "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"}, - {file = "requests-2.21.0.tar.gz", hash = "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e"}, {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, ] -scandir = [ - {file = "scandir-1.10.0-cp27-cp27m-win32.whl", hash = "sha256:92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188"}, - {file = "scandir-1.10.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac"}, - {file = "scandir-1.10.0-cp34-cp34m-win32.whl", hash = "sha256:2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f"}, - {file = "scandir-1.10.0-cp34-cp34m-win_amd64.whl", hash = "sha256:2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e"}, - {file = "scandir-1.10.0-cp35-cp35m-win32.whl", hash = "sha256:2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f"}, - {file = "scandir-1.10.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32"}, - {file = "scandir-1.10.0-cp36-cp36m-win32.whl", hash = "sha256:2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022"}, - {file = "scandir-1.10.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4"}, - {file = "scandir-1.10.0-cp37-cp37m-win32.whl", hash = "sha256:67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173"}, - {file = "scandir-1.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d"}, - {file = "scandir-1.10.0.tar.gz", hash = "sha256:4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae"}, -] six = [ {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, @@ -1090,10 +683,6 @@ stem = [ {file = "stem-1.8.0.tar.gz", hash = "sha256:a0b48ea6224e95f22aa34c0bc3415f0eb4667ddeae3dfb5e32a6920c185568c2"}, ] urllib3 = [ - {file = "urllib3-1.22-py2.py3-none-any.whl", hash = "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b"}, - {file = "urllib3-1.22.tar.gz", hash = "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"}, - {file = "urllib3-1.24.3-py2.py3-none-any.whl", hash = "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb"}, - {file = "urllib3-1.24.3.tar.gz", hash = "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4"}, {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"}, {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"}, ] @@ -1105,12 +694,10 @@ wcwidth = [ {file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"}, ] werkzeug = [ - {file = "Werkzeug-0.16.1-py2.py3-none-any.whl", hash = "sha256:1e0dedc2acb1f46827daa2e399c1485c8fa17c0d8e70b6b875b4e7f54bf408d2"}, - {file = "Werkzeug-0.16.1.tar.gz", hash = "sha256:b353856d37dec59d6511359f97f6a4b2468442e454bd1c98298ddce53cac1f04"}, {file = "Werkzeug-1.0.0-py2.py3-none-any.whl", hash = "sha256:6dc65cf9091cf750012f56f2cad759fa9e879f511b5ff8685e456b4e3bf90d16"}, {file = "Werkzeug-1.0.0.tar.gz", hash = "sha256:169ba8a33788476292d04186ab33b01d6add475033dfc07215e6d219cc077096"}, ] zipp = [ - {file = "zipp-1.2.0-py2.py3-none-any.whl", hash = "sha256:e0d9e63797e483a30d27e09fffd308c59a700d365ec34e93cc100844168bf921"}, - {file = "zipp-1.2.0.tar.gz", hash = "sha256:c70410551488251b0fee67b460fb9a536af8d6f9f008ad10ac51f615b6a521b1"}, + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, ] diff --git a/pyproject.toml b/pyproject.toml index f72da02f..53ce5858 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Micah Lee "] license = "GPLv3+" [tool.poetry.dependencies] -python = "*" +python = "^3.7" altgraph = "*" certifi = "*" chardet = "*" @@ -21,7 +21,7 @@ macholib = "*" MarkupSafe = "*" pefile = "*" pycryptodome = "*" -PyQt5 = "*" +PyQt5 = "5.14" PyQt5-sip = "*" PySocks = "*" requests = "*" From 360458372fa2c3b1ac74a8e505e140603a850b7b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 12:35:26 -0700 Subject: [PATCH 123/135] Test with python 3.7 and 3.8, no longer 3.6 --- .circleci/config.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 18b7a5aa..e48400ea 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,13 +7,13 @@ workflows: version: 2 test: jobs: - - test-3.6 - test-3.7 + - test-3.8 jobs: - test-3.6: &test-template + test-3.7: &test-template docker: - - image: circleci/python:3.6-buster + - image: circleci/python:3.7-buster working_directory: ~/repo @@ -42,7 +42,7 @@ jobs: command: | xvfb-run -s "-screen 0 1280x1024x24" poetry run pytest --rungui -vvv --no-qt-log tests/ - test-3.7: + test-3.8: <<: *test-template docker: - - image: circleci/python:3.7-buster + - image: circleci/python:3.8-buster From db9abb789db6506010e5338600a3e3fd17bde0c1 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 15:32:25 -0700 Subject: [PATCH 124/135] Build Qt from source in circleci --- .circleci/config.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e48400ea..32199aff 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,6 +20,25 @@ jobs: steps: - checkout + # https://wiki.qt.io/Building_Qt_5_from_Git + - run: + name: build Qt5 from source + command: | + sudo sh -c 'echo "deb-src http://deb.debian.org/debian buster main" >> /etc/apt/sources.list' + sudo apt-get update + sudo apt-get build-dep qt5-default -y + sudo apt-get install -y libxcb-xinerama0-dev + sudo apt-get install -y build-essential perl python git + sudo apt-get install -y '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev + git clone git://code.qt.io/qt/qt5.git ~/qt5 + cd ~/qt5 + git checkout 5.14.0 + perl init-repository + export LLVM_INSTALL_DIR=/usr/llvm + ./configure -developer-build -opensource -confirm-license -nomake examples -nomake tests + make -j$(nproc) + make install + - run: name: install dependencies command: | @@ -28,7 +47,6 @@ jobs: sudo pip3 install poetry flake8 poetry install - # run tests! - run: name: run flake tests command: | From bf06bb10817ac8b4b193a3e18da1cb9406572d2a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 16:01:23 -0700 Subject: [PATCH 125/135] Try installing binaries instead of building from source --- .circleci/config.yml | 23 +++------- .circleci/qt-installer-script.js | 75 ++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 .circleci/qt-installer-script.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 32199aff..1a3ab7d0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,27 +20,16 @@ jobs: steps: - checkout - # https://wiki.qt.io/Building_Qt_5_from_Git - run: - name: build Qt5 from source + name: Install Qt5 binaries command: | - sudo sh -c 'echo "deb-src http://deb.debian.org/debian buster main" >> /etc/apt/sources.list' - sudo apt-get update - sudo apt-get build-dep qt5-default -y - sudo apt-get install -y libxcb-xinerama0-dev - sudo apt-get install -y build-essential perl python git - sudo apt-get install -y '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev - git clone git://code.qt.io/qt/qt5.git ~/qt5 - cd ~/qt5 - git checkout 5.14.0 - perl init-repository - export LLVM_INSTALL_DIR=/usr/llvm - ./configure -developer-build -opensource -confirm-license -nomake examples -nomake tests - make -j$(nproc) - make install + cd ~/ + wget https://download.qt.io/official_releases/qt/5.14/5.14.0/qt-opensource-linux-x64-5.14.0.run + chmod +x qt-opensource-linux-x64-5.14.0.run + ./qt-opensource-linux-x64-5.14.0.run --script ~/repo/.circleci/qt-installer-script.js --platform minimal --verbose - run: - name: install dependencies + name: Install dependencies command: | sudo apt-get update sudo apt-get install -y python3-pip xvfb diff --git a/.circleci/qt-installer-script.js b/.circleci/qt-installer-script.js new file mode 100644 index 00000000..d5860b68 --- /dev/null +++ b/.circleci/qt-installer-script.js @@ -0,0 +1,75 @@ +function Controller() { + installer.installationFinished.connect(proceed) +} + +function logCurrentPage() { + var pageName = page().objectName + var pagePrettyTitle = page().title + console.log("At page: " + pageName + " ('" + pagePrettyTitle + "')") +} + +function page() { + return gui.currentPageWidget() +} + +function proceed(button, delay) { + gui.clickButton(button || buttons.NextButton, delay) +} + +Controller.prototype.WelcomePageCallback = function() { + logCurrentPage() + proceed(buttons.NextButton, 2000) +} + +Controller.prototype.CredentialsPageCallback = function() { + logCurrentPage() + page().loginWidget.EmailLineEdit.text = installer.environmentVariable("QT_EMAIL"); + page().loginWidget.PasswordLineEdit.text = installer.environmentVariable("QT_PASSWORD"); + proceed() +} + +Controller.prototype.IntroductionPageCallback = function() { + logCurrentPage() + proceed() +} + +Controller.prototype.TargetDirectoryPageCallback = function() { + logCurrentPage() + proceed() +} + +Controller.prototype.ComponentSelectionPageCallback = function() { + logCurrentPage() + page().deselectAll() + page().selectComponent("qt.qt5.5140.gcc_64") + proceed() +} + +Controller.prototype.LicenseAgreementPageCallback = function() { + logCurrentPage() + page().AcceptLicenseRadioButton.checked = true + gui.clickButton(buttons.NextButton) +} + +Controller.prototype.ReadyForInstallationPageCallback = function() { + logCurrentPage() + proceed() +} + +Controller.prototype.PerformInstallationPageCallback = function() { + logCurrentPage() +} + +Controller.prototype.FinishedPageCallback = function() { + logCurrentPage() + page().LaunchQtCreatorCheckBoxForm.launchQtCreatorCheckBox.checked = false + proceed(buttons.FinishButton) +} + +Controller.prototype.DynamicTelemetryPluginFormCallback = function() { + logCurrentPage() + console.log(Object.keys(page().TelemetryPluginForm.statisticGroupBox)) + var radioButtons = page().TelemetryPluginForm.statisticGroupBox + radioButtons.disableStatisticRadioButton.checked = true + proceed() +} From ea4fbdd834165d9fde138de603130789448f657e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 16:28:59 -0700 Subject: [PATCH 126/135] Fix installing Qt binaries --- .circleci/config.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1a3ab7d0..2b1e3c8f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,10 +23,12 @@ jobs: - run: name: Install Qt5 binaries command: | + sudo apt-get update + sudo apt-get install xvfb libdbus-1-3 libxkbcommon-x11-0 libxkbcommon-x11-dev cd ~/ wget https://download.qt.io/official_releases/qt/5.14/5.14.0/qt-opensource-linux-x64-5.14.0.run chmod +x qt-opensource-linux-x64-5.14.0.run - ./qt-opensource-linux-x64-5.14.0.run --script ~/repo/.circleci/qt-installer-script.js --platform minimal --verbose + xvfb-run ./qt-opensource-linux-x64-5.14.0.run --script ~/repo/.circleci/qt-installer-script.js --platform minimal --verbose - run: name: Install dependencies From 34e59ad7d77b6bb01c4dcd0fa9cc313d8863bab4 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 16:33:32 -0700 Subject: [PATCH 127/135] Update comments and section names in CircleCI --- .circleci/config.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2b1e3c8f..509bbfb4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,8 @@ -# Python CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-python/ for more details -# +# To run the tests, CircleCI needs these environment variables: +# QT_EMAIL - email address for a Qt account +# QT_PASSWORD - password for a Qt account +# (Unfortunately you can't install Qt without logging in.) + version: 2 workflows: version: 2 @@ -39,7 +40,7 @@ jobs: poetry install - run: - name: run flake tests + name: Run flake tests command: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics @@ -47,7 +48,7 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - run: - name: run tests + name: Run unit tests command: | xvfb-run -s "-screen 0 1280x1024x24" poetry run pytest --rungui -vvv --no-qt-log tests/ From 290e6f5e7263e54d8b7432015aeb00bb7e23c552 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 17:08:41 -0700 Subject: [PATCH 128/135] Add a pytest.ini file to register custom markers, to avoid so many test warnings --- tests/pytest.ini | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/pytest.ini diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 00000000..393e0dd8 --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +markers = + gui: marks tests as a GUI test + tor: marks tests as a Tor GUI test \ No newline at end of file From 785512218d6f38baecbd0e64784cfdeb59190c0a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 17:10:15 -0700 Subject: [PATCH 129/135] Test python 3.6 as well --- .circleci/config.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 509bbfb4..8f8a51cc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,13 +8,14 @@ workflows: version: 2 test: jobs: + - test-3.6 - test-3.7 - test-3.8 jobs: - test-3.7: &test-template + test-3.6: &test-template docker: - - image: circleci/python:3.7-buster + - image: circleci/python:3.6-buster working_directory: ~/repo @@ -52,6 +53,11 @@ jobs: command: | xvfb-run -s "-screen 0 1280x1024x24" poetry run pytest --rungui -vvv --no-qt-log tests/ + test-3.7: + <<: *test-template + docker: + - image: circleci/python:3.7-buster + test-3.8: <<: *test-template docker: From 76d88d0648294ee6754749e56dd992074a71608e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 21:32:05 -0700 Subject: [PATCH 130/135] Update build instructions to include Linux instructions using the newest software, instead of just software from package repositories --- BUILD.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/BUILD.md b/BUILD.md index 88efab04..370f73b8 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,8 +1,8 @@ # Index * [Building OnionShare](#building-onionshare) * [Linux](#linux) - * [For Debian-like distros](#for-debian-like-distros) - * [For Fedora-like distros](#for-fedora-like-distros) + * [Use newest software](#use-newest-software) + * [Use package managers](#use-package-managers) * [macOS](#macos) * [Windows](#windows) * [Setting up your dev environment](#setting-up-your-dev-environment) @@ -28,15 +28,61 @@ cd onionshare ## Linux +### Use newest software + +The recommended way to develop OnionShare is to use the latest versions of all dependencies. + +First, install `tor` from either the [official Debian repository](https://support.torproject.org/apt/tor-deb-repo/), or from your package manager. + +Then download Qt 5.14.0 for Linux: + +```sh +cd ~/Downloads +wget https://download.qt.io/official_releases/qt/5.14/5.14.0/qt-opensource-linux-x64-5.14.0.run +``` + +If you'd like to check to make sure you have the exact installer I have, here is the sha256 checksum: + +```sh +sha256sum qt-opensource-linux-x64-5.14.0.run +4379f147c6793ec7e7349d2f9ee7d53b8ab6ea4e4edf8ee0574a75586a6a6e0e qt-opensource-linux-x64-5.14.0.run +``` + +Then make it executable and install Qt: + +```sh +chmod +x qt-opensource-linux-x64-5.14.0.run +./qt-opensource-linux-x64-5.14.0.run +``` + +You have to create a Qt account and login to install Qt. Choose the default installation folder in your home directory. The only component you need is `Qt 5.14.0` > `Desktop gcc 64-bit`. + +Install [poetry](https://python-poetry.org/docs/) from your package manager, or by doing `pip install --user poetry`. Then install dependencies: + +```sh +poetry install +``` + +You can run the CLI and the GUI versions of OnionShare like this: + +```sh +poetry run ./dev_scripts/onionshare +poetry run ./dev_scripts/onionshare-gui +``` + +### Use package managers + +Alternatively, you can install dependencies from package managers. + Install the needed dependencies: -#### For Debian-like distros: +**For Debian-like distros:** ``` apt install -y python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python-nautilus tor obfs4proxy python3-pytest python3-pytestqt build-essential fakeroot python3-all python3-stdeb dh-python python3-flask-httpauth python3-distutils python3-psutil python3-watchdog ``` -#### For Fedora-like distros: +**For Fedora-like distros:** ``` dnf install -y python3-flask python3-flask-httpauth python3-stem python3-qt5 python3-crypto python3-pysocks nautilus-python tor obfs4 python3-pytest rpm-build python3-psutil python3-watchdog From aebc96142fec62f20fdb0196a8f6b08018f6acc9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 21:32:53 -0700 Subject: [PATCH 131/135] Clean up watchdog thread when quitting, which avoids segfaults --- onionshare_gui/main_window.py | 4 +--- onionshare_gui/tab_widget.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/onionshare_gui/main_window.py b/onionshare_gui/main_window.py index 0c9b179c..1c745b1c 100644 --- a/onionshare_gui/main_window.py +++ b/onionshare_gui/main_window.py @@ -284,7 +284,5 @@ class MainWindow(QtWidgets.QMainWindow): e.accept() def cleanup(self): - for index in range(self.tabs.count()): - tab = self.tabs.widget(index) - tab.cleanup() + self.tabs.cleanup() self.common.gui.onion.cleanup() diff --git a/onionshare_gui/tab_widget.py b/onionshare_gui/tab_widget.py index be744ace..d69931c0 100644 --- a/onionshare_gui/tab_widget.py +++ b/onionshare_gui/tab_widget.py @@ -79,6 +79,16 @@ class TabWidget(QtWidgets.QTabWidget): self.observer.schedule(self.event_handler, self.common.gui.events_dir) self.observer.start() + def cleanup(self): + # Stop the event thread + self.observer.stop() + self.observer.join() + + # Clean up each tab + for index in range(self.count()): + tab = self.widget(index) + tab.cleanup() + def move_new_tab_button(self): # Find the width of all tabs tabs_width = sum( From c5b8938f1c79a131dab1630e675deb3cc3b5d39e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 29 Mar 2020 13:54:30 -0700 Subject: [PATCH 132/135] Update build instructions for how to run tests with poetry --- BUILD.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/BUILD.md b/BUILD.md index 370f73b8..b631d410 100644 --- a/BUILD.md +++ b/BUILD.md @@ -264,28 +264,22 @@ This will prompt you to codesign three binaries and execute one unsigned binary. # Running tests -OnionShare includes PyTest unit tests. To run the tests, first install some dependencies: +OnionShare includes PyTest unit tests. To run tests, you can run `pytest` against the `tests/` directory. ```sh -pip3 install -r install/requirements-tests.txt -``` - -Then you can run `pytest` against the `tests/` directory. - -```sh -pytest tests/ +poetry run pytest tests/ ``` You can run GUI tests like this: ```sh -pytest --rungui tests/ +poetry run pytest --rungui tests/ ``` If you would like to also run the GUI unit tests in 'tor' mode, start Tor Browser in the background, then run: ```sh -pytest --rungui --runtor tests/ +poetry run pytest --rungui --runtor tests/ ``` Keep in mind that the Tor tests take a lot longer to run than local mode, but they are also more comprehensive. @@ -293,7 +287,7 @@ Keep in mind that the Tor tests take a lot longer to run than local mode, but th You can also choose to wrap the tests in `xvfb-run` so that a ton of OnionShare windows don't pop up on your desktop (you may need to install the `xorg-x11-server-Xvfb` package), like this: ```sh -xvfb-run pytest --rungui tests/ +xvfb-run poetry run pytest --rungui -vvv --no-qt-log tests/ ``` # Making releases From 145021b2e52083fd91cfa9ee2562f05375c125cb Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 29 Mar 2020 13:55:29 -0700 Subject: [PATCH 133/135] Enable stacktraces of segfaults when running tests --- tests/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 200f526d..53d52725 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,8 @@ import sys +import faulthandler + +# Enable stacktraces of segmentation faults +faulthandler.enable(file=sys.stdout) # Force tests to look for resources in the source code tree sys.onionshare_dev_mode = True From d417754e4542085244fa1d35b9b561ca8d983b5e Mon Sep 17 00:00:00 2001 From: Saptak S Date: Thu, 2 Apr 2020 04:19:50 +0530 Subject: [PATCH 134/135] Adds bash script to run GUI tests individually --- .circleci/config.yml | 2 +- BUILD.md | 8 ++++---- tests/gui_base_test.py | 1 + tests/run.sh | 26 ++++++++++++++++++++++++++ 4 files changed, 32 insertions(+), 5 deletions(-) create mode 100755 tests/run.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 8f8a51cc..dd84e371 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -51,7 +51,7 @@ jobs: - run: name: Run unit tests command: | - xvfb-run -s "-screen 0 1280x1024x24" poetry run pytest --rungui -vvv --no-qt-log tests/ + xvfb-run -s "-screen 0 1280x1024x24" poetry run ./tests/run.sh --rungui test-3.7: <<: *test-template diff --git a/BUILD.md b/BUILD.md index b631d410..fd7e9ebe 100644 --- a/BUILD.md +++ b/BUILD.md @@ -267,19 +267,19 @@ This will prompt you to codesign three binaries and execute one unsigned binary. OnionShare includes PyTest unit tests. To run tests, you can run `pytest` against the `tests/` directory. ```sh -poetry run pytest tests/ +poetry run ./tests/run.sh ``` You can run GUI tests like this: ```sh -poetry run pytest --rungui tests/ +poetry run ./tests/run.sh --rungui ``` If you would like to also run the GUI unit tests in 'tor' mode, start Tor Browser in the background, then run: ```sh -poetry run pytest --rungui --runtor tests/ +poetry run ./tests/run.sh --rungui --runtor ``` Keep in mind that the Tor tests take a lot longer to run than local mode, but they are also more comprehensive. @@ -287,7 +287,7 @@ Keep in mind that the Tor tests take a lot longer to run than local mode, but th You can also choose to wrap the tests in `xvfb-run` so that a ton of OnionShare windows don't pop up on your desktop (you may need to install the `xorg-x11-server-Xvfb` package), like this: ```sh -xvfb-run poetry run pytest --rungui -vvv --no-qt-log tests/ +xvfb-run poetry run ./tests/run.sh --rungui ``` # Making releases diff --git a/tests/gui_base_test.py b/tests/gui_base_test.py index c33891df..87353cd7 100644 --- a/tests/gui_base_test.py +++ b/tests/gui_base_test.py @@ -71,6 +71,7 @@ class GuiBaseTest(unittest.TestCase): @classmethod def tearDownClass(cls): # Quit + cls.gui.qtapp.clipboard().clear() QtCore.QTimer.singleShot(200, cls.gui.close_dialog.accept_button.click) cls.gui.close() diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 00000000..3c792cd3 --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# The script runs python tests +# Firstly, all CLI tests are run +# Then, all the GUI tests are run individually +# to avoid segmentation fault + +PARAMS="" + +while [ ! $# -eq 0 ] +do + case "$1" in + --rungui) + PARAMS="$PARAMS --rungui" + ;; + --runtor) + PARAMS="$PARAMS --runtor" + ;; + esac + shift +done + +pytest $PARAMS -vvv ./tests/test_cli*.py +for filename in ./tests/test_gui_*.py; do + pytest $PARAMS -vvv --no-qt-log $filename +done From 72d79cdfe5c86f07f67565c43aa586c1518b0d82 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 1 Apr 2020 17:59:08 -0700 Subject: [PATCH 135/135] Revert "Enable stacktraces of segfaults when running tests" This reverts commit 145021b2e52083fd91cfa9ee2562f05375c125cb. --- tests/conftest.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 53d52725..200f526d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,4 @@ import sys -import faulthandler - -# Enable stacktraces of segmentation faults -faulthandler.enable(file=sys.stdout) # Force tests to look for resources in the source code tree sys.onionshare_dev_mode = True