From ec5c894fd099769f8327cf5a811b037d115baeed Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 18:59:27 -0700 Subject: [PATCH 001/142] 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 ef6c7e280e1dbaeb514ffc6cafc3e5c0758948ad Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 19:18:56 -0700 Subject: [PATCH 002/142] 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 0e44020bb6ab78dd551e6126c0a49ff63aa2e67c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 19:41:20 -0700 Subject: [PATCH 003/142] 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 bd832051e006dfe3c6c523db29cc5245ea1fbcc3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 20:01:09 -0700 Subject: [PATCH 004/142] 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 2c1225b757030d5e2ded067c1a29cf1d8757ba71 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 20:05:08 -0700 Subject: [PATCH 005/142] 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 b246f22e7ab553bb6c2af7c6390d78cf48db98ea Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 20:11:45 -0700 Subject: [PATCH 006/142] 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 940b89a30bb651e9f9c2d063007e1618debc88af Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 21:36:30 -0700 Subject: [PATCH 007/142] 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 1d903efeb2eee391e4a0faf49bdfa5be2f8b6f7f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 20 Oct 2019 22:08:47 -0700 Subject: [PATCH 008/142] 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 ed25b44e8495c021046cde307d27ebb9bafbf044 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 26 Oct 2019 21:14:47 -0700 Subject: [PATCH 009/142] 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 6190de5c53b4568bfb98b7c2ad0d83d58a1df259 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 26 Oct 2019 21:56:57 -0700 Subject: [PATCH 010/142] 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 509516c46af72ccdcacc02f5a492e4f489a8f350 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 26 Oct 2019 22:39:59 -0700 Subject: [PATCH 011/142] 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 985b2c4719205c11f99ef745267e53f88eff6218 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 14:21:40 -0700 Subject: [PATCH 012/142] 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 b9ac0ccb7deea8b4153de9fc9a350352246b3605 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 14:23:45 -0700 Subject: [PATCH 013/142] 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 62d63a8e24c2a261c6fdbd9a92c5a21829c2159d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 14:35:11 -0700 Subject: [PATCH 014/142] 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 68310070a48d70806bf1ee8210333363b8b716cd Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 15:01:14 -0700 Subject: [PATCH 015/142] 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 eb78b77073ea46e91652a5edc167eda9eba8fcc7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 15:04:04 -0700 Subject: [PATCH 016/142] 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 cc7c463e805017215f1dc6d0a2b80e2d614d976f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 15:21:46 -0700 Subject: [PATCH 017/142] 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 c3330919f40899af7107c8677d35cf62c4ffcef4 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 15:26:56 -0700 Subject: [PATCH 018/142] 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/142] 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/142] 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 9dc14e8f4e200e9c4944bc627f54238b368cfc0f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 16:18:56 -0700 Subject: [PATCH 021/142] 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 3052c58d6433ff1d77c5ee852df1d4225489a8ae Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 16:32:12 -0700 Subject: [PATCH 022/142] 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 9d1bd74fcc311d4cce98509ed602704d4a8022e7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 16:36:32 -0700 Subject: [PATCH 023/142] 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 f00df6356cf740d68b78076112f2a129f4df5a90 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 18:16:48 -0700 Subject: [PATCH 024/142] 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 b2bba929bf2e76ef52b063fd8d220868ed2bcf5d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 18:21:04 -0700 Subject: [PATCH 025/142] 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 55840b00383c4f71c6c16cc0ef67e5b47fc02b98 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 27 Oct 2019 18:41:24 -0700 Subject: [PATCH 026/142] 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/142] 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 bfd8c4aae6cb6023cf3a34337ffbeb258a1cf23c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 10:15:18 -0700 Subject: [PATCH 028/142] 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 c42c11648cc9eae16564642174f4922be09f26b9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 10:24:12 -0700 Subject: [PATCH 029/142] 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 61dc04a10572e5e1bc69ae6051b8d01ca62f87a2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 10:43:20 -0700 Subject: [PATCH 030/142] 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 1b635b16460e293dc373bec8c552ba6fd4fa8330 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 11:43:05 -0700 Subject: [PATCH 031/142] 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 30df0c4bd8ed8a132465d41b1625c7f49a720afa Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 14:35:51 -0700 Subject: [PATCH 032/142] 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 f6f4665e3014265399a6c1ab2beca4bcf9d76557 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 14:56:40 -0700 Subject: [PATCH 033/142] 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 a6f7796ccf0152987c1c69c5d0f69528c2000f18 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 15:37:21 -0700 Subject: [PATCH 034/142] 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 0431374ef850e582765a8745bc96ddf851ee63d3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 15:39:12 -0700 Subject: [PATCH 035/142] 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 16268b5b3e799028ce4a3119a0dc7aae375d7077 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 17:01:47 -0700 Subject: [PATCH 036/142] 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 1bc516f5694c02da6118ee414d8d4db4a10292a6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 17:06:13 -0700 Subject: [PATCH 037/142] 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 e9e7ddc7a84fbe4febd0a3aead39f01c93e1c7aa Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 17:13:06 -0700 Subject: [PATCH 038/142] 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 1d8d33f6fd049762ff1a9e595138787f60aac7fa Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 17:24:06 -0700 Subject: [PATCH 039/142] 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 764e404ff2719d22accf3d5768625b0db691b399 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 17:39:27 -0700 Subject: [PATCH 040/142] 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 7b33e74312e840570d9bf0e1539e95619bfabf05 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 18:08:14 -0700 Subject: [PATCH 041/142] 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 ff01c3485d9b2b7d8cdb9b7363dd646deb700685 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 18:29:24 -0700 Subject: [PATCH 042/142] 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 e93857ac06a4e6299e5e01c5c227e2fed642e101 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 19:03:37 -0700 Subject: [PATCH 043/142] 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 839070f879af2164f79531f1a3da9924979f5fc9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 19:10:41 -0700 Subject: [PATCH 044/142] 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 4a804d79017381d26e26fc03be5591a12ff99c27 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 19:12:06 -0700 Subject: [PATCH 045/142] 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 51cda4c52aa60779c98ff1ca8b5d25ce4fcbd00f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 21:00:23 -0700 Subject: [PATCH 046/142] 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 960322a36387cf246b214e1cba49227335ff3401 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 21:45:55 -0700 Subject: [PATCH 047/142] 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 b4e70bed579d0d72d53d2f4bbc01130d3d7b85da Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 22:55:22 -0700 Subject: [PATCH 048/142] 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 accb5c4e07b7a3d92d4efbc6cd7a1d378b1d5fba Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 23:06:26 -0700 Subject: [PATCH 049/142] 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 3b3c805a3476eddcea7ff7e1cfd2506786ef553c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 23:32:44 -0700 Subject: [PATCH 050/142] 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 2afb443246b58d373b324a31bb075fa2ad5fc1dd Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 2 Nov 2019 23:36:37 -0700 Subject: [PATCH 051/142] 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 66aea7146853ac9941f2178d2d08cd72fefa6e8b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 3 Nov 2019 00:47:51 -0700 Subject: [PATCH 052/142] 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 b5043d72a06c626b1ac9e33cb87445ee4c0d5125 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 7 Nov 2019 13:25:47 +0800 Subject: [PATCH 053/142] 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 7dbcde4a66ac0f1c59970c5fc4d733559baf9f65 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 7 Nov 2019 13:34:24 +0800 Subject: [PATCH 054/142] 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 0620a0e3e2be33b875b53eb1d5e0ae045315affe Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 15:31:02 +0800 Subject: [PATCH 055/142] 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 933611eb96e2d653df24127f5df78581493543fa Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 15:40:37 +0800 Subject: [PATCH 056/142] 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 42d431676a5d84c0d0f39c312ec0bc22fb04983a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 15:57:12 +0800 Subject: [PATCH 057/142] 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 865d36e52fc347710f0d9f2a510b8bb1bd1b0062 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 16:20:38 +0800 Subject: [PATCH 058/142] 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 535ade0096c25f330395b0bd0b88ae47f46a20ce Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 16:44:21 +0800 Subject: [PATCH 059/142] 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 38a765097466d1c9d36bcd6a710b042fbd81ca4c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 16:57:50 +0800 Subject: [PATCH 060/142] 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 6e05f4f3a1e5d8868043cc8f753460cd38e19f6a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 17:08:21 +0800 Subject: [PATCH 061/142] 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 f3226d30c46fb03e39b4cb492d069183b0c7b632 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 17:25:30 +0800 Subject: [PATCH 062/142] 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 c3199425c406397093ed32c15098693b9ad443d1 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 17:27:17 +0800 Subject: [PATCH 063/142] 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 35add6cca6ec19e363dfb73f6894940c56dbc765 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 18:09:10 +0800 Subject: [PATCH 064/142] 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 ec91e51ace379a1bff4cad3f332ea01086703510 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 19:11:30 +0800 Subject: [PATCH 065/142] 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 6f830f1206f8f7274edf166cbce1c1157e3f6dcf Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 19:59:20 +0800 Subject: [PATCH 066/142] 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 8c8ddcb2add585659816d45147f767975e40370d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 20:06:24 +0800 Subject: [PATCH 067/142] 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 663d44a19030f3c15bd2b399d61e431098707cce Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 20:34:54 +0800 Subject: [PATCH 068/142] 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 2fec66ba316154d5d7a2006e5dd6ba4c9ce3c107 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 20:44:52 +0800 Subject: [PATCH 069/142] 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 1695338a69f08ecfd7f2cb6f27a1542faad86590 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 20:48:38 +0800 Subject: [PATCH 070/142] 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 e2c94e49c9af04be247232fdcccab062743e8f35 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 20:55:05 +0800 Subject: [PATCH 071/142] 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 2a1963a8f516fccbf0974c9a26fc5fca3e8f7949 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 21:36:54 +0800 Subject: [PATCH 072/142] 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 d676fb7de549cdb972b079f3b6eab2a3f6ce03ce Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 8 Nov 2019 22:08:08 +0800 Subject: [PATCH 073/142] 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 e65d13fb83dbca303bbd892dd23c54b2e5a66fb4 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 9 Nov 2019 00:42:49 +0800 Subject: [PATCH 074/142] 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 39e4aa9b08f7f1e159af9d9820ee9b2aea106136 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 9 Nov 2019 00:53:03 +0800 Subject: [PATCH 075/142] 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 181cda71318cbdefc7f10b5dad4bf77eecbea6ac Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 9 Nov 2019 00:57:25 +0800 Subject: [PATCH 076/142] 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 53dcd64917e2f0ba8db1b7dc13cbc1c73e02c267 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 9 Nov 2019 01:31:22 +0800 Subject: [PATCH 077/142] 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 ef5c4d46e140adfb2b862ace282cd62cef054637 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 9 Nov 2019 01:38:35 +0800 Subject: [PATCH 078/142] 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 b08a9be2a9ad2e5a8452360818cbd6aae49cd1b1 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 10:10:22 -0800 Subject: [PATCH 079/142] 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 981773070aec2cee1dbd24261a4d0340407d138f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 10:25:24 -0800 Subject: [PATCH 080/142] 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 4d5a9fdff552e99497b527b1e1cf32722066329d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 10:40:23 -0800 Subject: [PATCH 081/142] 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 014c3ea0aa25dee130b4efe6d8e47f9750ac0fb5 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 10:56:45 -0800 Subject: [PATCH 082/142] 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 b850db7daaee157d6298c31d1ace19065d180bb2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 11:22:02 -0800 Subject: [PATCH 083/142] 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 66e492d7a52cd606431e13983228d066405f9fa6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 11:26:28 -0800 Subject: [PATCH 084/142] 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 350325aafe4102722deaa70b387918d3ec3d0011 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 11:33:59 -0800 Subject: [PATCH 085/142] 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 413baba376378322bb2cadc89cc5bbc626f3a3c0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 13:33:01 -0800 Subject: [PATCH 086/142] 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 817118572fde87535adace0d3049c9da813db9ab Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 13:36:35 -0800 Subject: [PATCH 087/142] 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 2c51f1fefbfb1660b03be0846698eee0190a674c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 13:51:08 -0800 Subject: [PATCH 088/142] 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 334c3e1799260fb73d1cb4fe4410eed68982e3ac Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 13:55:15 -0800 Subject: [PATCH 089/142] 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 23e3e8fc5ee201c5ba1ae44e3c2e57ea3b5ba085 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 14:22:00 -0800 Subject: [PATCH 090/142] 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 de76f400cd409afc41ec9082199ca5c8f5e518c8 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 14:41:08 -0800 Subject: [PATCH 091/142] 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 7dcc71c330d06e9cea9bf2a1ade3b6c58bf7b621 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 10 Nov 2019 17:32:34 -0800 Subject: [PATCH 092/142] 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 17da0fc4dde7da7d87bf1d8fe19fca6bd9bec87f Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 11 Nov 2019 17:47:03 +1100 Subject: [PATCH 093/142] 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 821302b8689c21e134e11f26c836b819959c7c97 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 11 Nov 2019 17:58:40 +1100 Subject: [PATCH 094/142] 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 e145f11ce348cc36cd0fcf2b671b3a39db907303 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 11 Nov 2019 18:08:01 +1100 Subject: [PATCH 095/142] 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 33de8082c664c07b7f6d4b3c96bc2cc82242ad40 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 27 Nov 2019 16:58:03 -0800 Subject: [PATCH 096/142] 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 92a7dd364e82600ed05699552db1141f07c8c219 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 12:24:26 -0800 Subject: [PATCH 097/142] 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 4d9625514c9d80cc52a3934c8884a392e69807f7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 12:35:57 -0800 Subject: [PATCH 098/142] 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 e136b1f1a202eb1813d483b4cae0179e7d26b70b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 13:26:07 -0800 Subject: [PATCH 099/142] 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 51268ff9f40996e179b611932f61ed5fc5f527c3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 13:33:56 -0800 Subject: [PATCH 100/142] 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 327f74b96d6e51e714f68a08a59462d95fe38686 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 14:02:00 -0800 Subject: [PATCH 101/142] 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 cb8e836079741780390a331b0617dd68148d3537 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 14:31:48 -0800 Subject: [PATCH 102/142] 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 eefa43d923ac2df1a34ba1d48204a9ff27576759 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 14:57:01 -0800 Subject: [PATCH 103/142] 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 9529e06234666ebd07505cc198f4fd697016dd0f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 14:58:04 -0800 Subject: [PATCH 104/142] 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 82c2f575b9f83a2679784980e452422db334034e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 15:06:19 -0800 Subject: [PATCH 105/142] 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 a3964c18ed05a62868791a119ae159d074b581cf Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 15:36:53 -0800 Subject: [PATCH 106/142] 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 41071ff44d8eae4463b11f6bb0c8f23569c3cb1b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 15:46:55 -0800 Subject: [PATCH 107/142] 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 b473744d26638b111ef8c652e163abe60c01fd2c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 15:52:02 -0800 Subject: [PATCH 108/142] 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 5cd2fc8ff72ea363e41806cd5807a773c8714540 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 19:30:48 -0800 Subject: [PATCH 109/142] 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 657d51de489f14c31372834555df654691fb4d1a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 28 Nov 2019 20:32:28 -0800 Subject: [PATCH 110/142] 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 065e849051a5b70c47c40347ce5ce2b551493333 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 29 Nov 2019 16:38:34 +1100 Subject: [PATCH 111/142] 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 df31e2fc586f311f42a8815a257a7c8e2967305d Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 29 Nov 2019 16:46:31 +1100 Subject: [PATCH 112/142] 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 981220d1ce3096ec7e30e6d375239edf6358a3e8 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 29 Nov 2019 16:52:00 +1100 Subject: [PATCH 113/142] 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 29e60fcacf198938c7daeb0f333ef45ad8cce24a Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 29 Nov 2019 18:40:45 +1100 Subject: [PATCH 114/142] 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 483cb9bf14929e678518b510baf00b29e6a084c5 Mon Sep 17 00:00:00 2001 From: Ned84 <41830741+Ned84@users.noreply.github.com> Date: Sat, 30 Nov 2019 22:08:37 +0100 Subject: [PATCH 115/142] Update BUILD.md --- BUILD.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/BUILD.md b/BUILD.md index 9456e617..9442f3de 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,3 +1,22 @@ +# Index +* [Building OnionShare](#building-onionshare) + * [Linux](#linux) + * [For Debian-like distros](#for-debian-like-distros) + * [For Fedora-like distros](#for-fedora-like-distros) + * [macOS](#macos) + * [Windows](#windows) + * [Setting up your dev environment](#setting-up-your-dev-environment) + * [To make a .exe](#to-make-a-exe) + * [To build the installer](#to-build-the-installer) +* [Running tests](#running-tests) +* [Making releases](#making-releases) + * [Changelog, version, and signed git tag](#changelog-version-and-signed-git-tag) + * [Linux release](#linux-release) + * [macOS release](#macos-release) + * [Windows release](#windows-release) + * [Source package](#source-package) + * [Publishing the release](#publishing-the-release) + # Building OnionShare Start by getting the source code: @@ -11,13 +30,13 @@ cd onionshare 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 build-essential fakeroot python3-all python3-stdeb dh-python python3-flask-httpauth python3-distutils ``` -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 From 6eef970faac1dcf51c562df211e85c4400ed49da Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 8 Dec 2019 09:52:29 -0800 Subject: [PATCH 116/142] 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 48070409f7a61575bdb2b9f4ab397ce2347e8061 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 8 Dec 2019 10:13:56 -0800 Subject: [PATCH 117/142] 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 5b6d986951dbdcd3809f72a9946d0ff50c063552 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 8 Dec 2019 10:29:00 -0800 Subject: [PATCH 118/142] 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 e7bd89c41dd3b6a69574b10f0eff0eadae202c08 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 8 Dec 2019 12:51:30 -0800 Subject: [PATCH 119/142] 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 f6fb08f6b2a2017bd0c13406c7450a24a5bf28f3 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 3 Jan 2020 14:21:31 -0500 Subject: [PATCH 120/142] Added ascii-logo --- onionshare/__init__.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index a2d6d4a1..b753d315 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -45,6 +45,33 @@ def main(cwd=None): # Display OnionShare banner print(f"OnionShare {common.version} | https://onionshare.org/") + reset='\033[0m' + purple='\033[45m' + white='\033[97m' + print(purple, white) + print(" ") + print(" @@@@@@@@@ ") + print(" @@@@@@@@@@@@@@@@@@@ ") + print(" @@@@@@@@@@@@@@@@@@@@@@@@@ ") + print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ") + print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ___ _ ") + print(" @@@@@@ @@@@@@@@@@@@@ / _ \ (_) ") + print(" @@@@ @ @@@@@@@@@@@ | | | |_ __ _ ___ _ __ ") + print(" @@@@@@@@ @@@@@@@@@@ | | | | '_ \| |/ _ \| '_ \ ") + print(" @@@@@@@@@@@@ @@@@@@@@@@ \ \_/ / | | | | (_) | | | | ") + print(" @@@@@@@@@@@@@@@@ @@@@@@@@@ \___/|_| |_|_|\___/|_| |_| ") + print(" @@@@@@@@@ @@@@@@@@@@@@@@@@ _____ _ ") + print(" @@@@@@@@@@ @@@@@@@@@@@@ / ___| | ") + print(" @@@@@@@@@@ @@@@@@@@ \ `--.| |__ __ _ _ __ ___ ") + print(" @@@@@@@@@@@ @ @@@@ `--. \ '_ \ / _` | '__/ _ \\") + print(" @@@@@@@@@@@@@ @@@@@@ /\__/ / | | | (_| | | | __/") + print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ \____/|_| |_|\__,_|_| \___|") + print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ") + print(" @@@@@@@@@@@@@@@@@@@@@@@@@ ") + print(" @@@@@@@@@@@@@@@@@@@ ") + print(" @@@@@@@@@ ") + print(" ") + print(reset) # OnionShare CLI in OSX needs to change current working directory (#132) if common.platform == "Darwin": From 6672981c36919a14975b21443b8ba6308f9adaba Mon Sep 17 00:00:00 2001 From: milotype <43657314+milotype@users.noreply.github.com> Date: Sat, 4 Jan 2020 20:59:17 +0100 Subject: [PATCH 121/142] Update org.onionshare.OnionShare.desktop Added croatian (hr) translation for Comment and Keywords --- install/org.onionshare.OnionShare.desktop | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install/org.onionshare.OnionShare.desktop b/install/org.onionshare.OnionShare.desktop index 536d73c6..02592a72 100644 --- a/install/org.onionshare.OnionShare.desktop +++ b/install/org.onionshare.OnionShare.desktop @@ -4,6 +4,7 @@ GenericName=OnionShare Client Comment=Share a file securely and anonymously over Tor Comment[da]=Del en fil sikkert og anonymt over Tor Comment[de]=Teile Dateien sicher und anonym über das Tor-Netzwerk +Comment[hr]=Dijeli datoteku sigurno i anonimno preko Tora Exec=/usr/bin/onionshare-gui Terminal=false Type=Application @@ -12,5 +13,6 @@ Categories=Network;FileTransfer; Keywords=tor;anonymity;privacy;onion service;file sharing;file hosting; Keywords[da]=tor;anonymitet;privatliv;onion-tjeneste;fildeling;filhosting; Keywords[de]=tor;Anonymität;Privatsphäre;Onion-Service;File-Sharing;File-Hosting; +Keywords[hr]=tor;anonimnost;privatnost;Onion usluga;dijeljenje datoteka;hosting datoteka; StartupNotify=true StartupWMClass=onionshare From d84e27c778e979dffe69249ced19742607a674fa Mon Sep 17 00:00:00 2001 From: Martin Rey <42996147+m-rey@users.noreply.github.com> Date: Fri, 28 Feb 2020 02:07:33 +0000 Subject: [PATCH 122/142] Fix openSUSE misspelling The correct spelling of openSUSE can be checked on https://en.opensuse.org/Portal:Distribution --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 9442f3de..da0db62c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -55,7 +55,7 @@ Create a .deb on Debian-like distros: `./install/build_deb.sh` Create a .rpm on Fedora-like distros: `./install/build_rpm.sh` -For OpenSuSE: There are instructions for building [in the wiki](https://github.com/micahflee/onionshare/wiki/Linux-Distribution-Support#opensuse-leap-150). +For openSUSE: There are instructions for building [in the wiki](https://github.com/micahflee/onionshare/wiki/Linux-Distribution-Support#opensuse-leap-150). For ArchLinux: There is a PKBUILD available [here](https://aur.archlinux.org/packages/onionshare/) that can be used to install OnionShare. From ab5d7d41ea8803d2e776f88fc14256e6d84da317 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 8 Mar 2020 12:38:25 +0530 Subject: [PATCH 123/142] Change colors --- onionshare/__init__.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index b753d315..246597dc 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -46,14 +46,12 @@ def main(cwd=None): # Display OnionShare banner print(f"OnionShare {common.version} | https://onionshare.org/") reset='\033[0m' - purple='\033[45m' - white='\033[97m' - print(purple, white) - print(" ") - print(" @@@@@@@@@ ") - print(" @@@@@@@@@@@@@@@@@@@ ") - print(" @@@@@@@@@@@@@@@@@@@@@@@@@ ") - print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ") + purple='\33[95m' + print(purple) + print(" @@@@@@@@@ ") + print(" @@@@@@@@@@@@@@@@@@@ ") + print(" @@@@@@@@@@@@@@@@@@@@@@@@@ ") + print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ") print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ___ _ ") print(" @@@@@@ @@@@@@@@@@@@@ / _ \ (_) ") print(" @@@@ @ @@@@@@@@@@@ | | | |_ __ _ ___ _ __ ") @@ -66,11 +64,10 @@ def main(cwd=None): print(" @@@@@@@@@@@ @ @@@@ `--. \ '_ \ / _` | '__/ _ \\") print(" @@@@@@@@@@@@@ @@@@@@ /\__/ / | | | (_| | | | __/") print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ \____/|_| |_|\__,_|_| \___|") - print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ") - print(" @@@@@@@@@@@@@@@@@@@@@@@@@ ") - print(" @@@@@@@@@@@@@@@@@@@ ") - print(" @@@@@@@@@ ") - print(" ") + print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ") + print(" @@@@@@@@@@@@@@@@@@@@@@@@@ ") + print(" @@@@@@@@@@@@@@@@@@@ ") + print(" @@@@@@@@@ ") print(reset) # OnionShare CLI in OSX needs to change current working directory (#132) From 9dfae675ead9629b70407ca5dcb452d5ee45d4af Mon Sep 17 00:00:00 2001 From: Saptak S Date: Wed, 18 Mar 2020 02:47:25 +0530 Subject: [PATCH 124/142] Switches from pipenv and requirements.txt to poetry to manage dependencies --- BUILD.md | 57 ++- install/requirements-tests.txt | 10 - install/requirements.txt | 22 - poetry.lock | 729 +++++++++++++++++++++++++++++++++ pyproject.toml | 50 +++ 5 files changed, 805 insertions(+), 63 deletions(-) delete mode 100644 install/requirements-tests.txt delete mode 100644 install/requirements.txt create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/BUILD.md b/BUILD.md index 9442f3de..b2810851 100644 --- a/BUILD.md +++ b/BUILD.md @@ -71,24 +71,17 @@ You may also need to run the command `/Applications/Python\ 3.7/Install\ Certifi Install Qt 5.13.1 for macOS from https://www.qt.io/offline-installers. I downloaded `qt-opensource-mac-x64-5.13.1.dmg`. In the installer, you can skip making an account, and all you need is `Qt` > `Qt 5.13.1` > `macOS`. -Now install pip dependencies. If you want to use a virtualenv, create it and activate it first: +If you don't have it already, install poetry (`pip3 install --user poetry`). Then install dependencies: ```sh -python3 -m venv venv -. venv/bin/activate -``` - -Then install the dependencies: - -```sh -pip3 install -r install/requirements.txt +poetry install ``` #### You can run both the CLI and GUI versions of OnionShare without building an bundle ```sh -./dev_scripts/onionshare -./dev_scripts/onionshare-gui +poetry run ./dev_scripts/onionshare +poetry run ./dev_scripts/onionshare-gui ``` #### To build the app bundle @@ -113,19 +106,25 @@ Now you should have `dist/OnionShare.pkg`. Download Python 3.7.4, 32-bit (x86) from https://www.python.org/downloads/release/python-374/. I downloaded `python-3.7.4.exe`. When installing it, make sure to check the "Add Python 3.7 to PATH" checkbox on the first page of the installer. -Open a command prompt, cd to the onionshare folder, and install dependencies with pip: +Install the Qt 5.13.1 from https://www.qt.io/offline-installers. I downloaded `qt-opensource-windows-x86-5.13.1.exe`. In the installer, you can skip making an account, and all you need `Qt` > `Qt 5.13.1` > `MSVC 2017 32-bit`. -```cmd -pip install -r install\requirements.txt +Install [poetry](https://python-poetry.org/). Open PowerShell, and run: + +``` +(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python ``` -Install the Qt 5.13.1 from https://www.qt.io/offline-installers. I downloaded `qt-opensource-windows-x86-5.13.1.exe`. In the installer, you can skip making an account, and all you need `Qt` > `Qt 5.13.1` > `MSVC 2017 32-bit`. +And add `%USERPROFILE%\.poetry\bin` to your path. Then open a command prompt and cd to the `dangerzone` folder, and install the poetry dependencies: + +``` +poetry install +``` After that you can try both the CLI and the GUI version of OnionShare: ``` -python dev_scripts\onionshare -python dev_scripts\onionshare-gui +poetry run python dev_scripts\onionshare +poetry run python dev_scripts\onionshare-gui ``` #### If you want to build a .exe @@ -160,19 +159,6 @@ cd "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\ vcvars32.bat ``` -Make sure you have a new enough `setuptools`: - -``` -pip install --upgrade setuptools -``` - -Now make sure you don't have PyInstaller installed from pip: - -``` -pip uninstall PyInstaller -rmdir C:\Users\user\AppData\Local\Programs\Python\Python37-32\Lib\site-packages\PyInstaller /S -``` - Change to a folder where you keep source code, and clone the PyInstaller git repo and checkout the `v3.5` tag: ``` @@ -183,6 +169,14 @@ git tag -v v3.5 (Note that ideally you would verify the git tag, but the PGP key that has signed the `v3.5` git tag for is not published anywhere, so this isn't possible. See [this issue](https://github.com/pyinstaller/pyinstaller/issues/4430).) +The next step is to compile the bootloader. We should do this all in dangerzone's poetry shell: + +``` +cd onionshare +poetry shell +cd ..\pyinstaller +``` + And compile the bootloader, following [these instructions](https://pythonhosted.org/PyInstaller/bootloader-building.html). To compile, run this: ``` @@ -190,11 +184,12 @@ cd bootloader python waf distclean all --target-arch=32bit --msvc_targets=x86 ``` -Finally, install the PyInstaller module into your local site-packages: +Finally, install the PyInstaller module into your poetry environment: ``` cd .. python setup.py install +exit ``` Now the next time you use PyInstaller to build OnionShare, the `.exe` file should not be flagged as malicious by anti-virus. diff --git a/install/requirements-tests.txt b/install/requirements-tests.txt deleted file mode 100644 index 4cb2106a..00000000 --- a/install/requirements-tests.txt +++ /dev/null @@ -1,10 +0,0 @@ -atomicwrites==1.3.0 -attrs==19.1.0 -more-itertools==7.2.0 -pluggy==0.13.0 -py==1.8.0 -pytest==5.1.2 -pytest-faulthandler==2.0.1 -pytest-qt==3.2.2 -six==1.12.0 -urllib3==1.25.3 \ No newline at end of file diff --git a/install/requirements.txt b/install/requirements.txt deleted file mode 100644 index 36b9fa4f..00000000 --- a/install/requirements.txt +++ /dev/null @@ -1,22 +0,0 @@ -altgraph==0.16.1 -certifi==2019.9.11 -chardet==3.0.4 -Click==7.0 -Flask==1.1.1 -Flask-HTTPAuth==3.3.0 -future==0.17.1 -idna==2.8 -itsdangerous==1.1.0 -Jinja2==2.10.1 -macholib==1.11 -MarkupSafe==1.1.1 -pefile==2019.4.18 -pycryptodome==3.9.0 -PyInstaller==3.5 -PyQt5==5.13.1 -PyQt5-sip==4.19.19 -PySocks==1.7.0 -requests==2.22.0 -stem==1.7.1 -urllib3==1.25.3 -Werkzeug==0.15.6 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..51bad950 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,729 @@ +[[package]] +category = "main" +description = "Python graph (network) package" +name = "altgraph" +optional = false +python-versions = "*" +version = "0.16.1" + +[[package]] +category = "main" +description = "An unobtrusive argparse wrapper with natural syntax" +name = "argh" +optional = false +python-versions = "*" +version = "0.26.2" + +[[package]] +category = "dev" +description = "Atomic file writes." +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.0" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.1.0" + +[package.extras] +dev = ["coverage", "hypothesis", "pympler", "pytest", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest", "six", "zope.interface"] + +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2019.9.11" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +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 = "dev" +description = "Cross-platform colored terminal text." +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 = "Python 2.7 backport of the \"dis\" module from Python 3.5+" +marker = "sys_platform == \"darwin\"" +name = "dis3" +optional = false +python-versions = "*" +version = "0.1.3" + +[[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.*, !=3.4.*" +version = "1.1.1" + +[package.dependencies] +Jinja2 = ">=2.10.1" +Werkzeug = ">=0.15" +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 = "Basic and Digest HTTP authentication for Flask routes" +name = "flask-httpauth" +optional = false +python-versions = "*" +version = "3.3.0" + +[package.dependencies] +Flask = "*" + +[[package]] +category = "main" +description = "Clean single-source support for Python 3 and 2" +name = "future" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.17.1" + +[[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 = "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.*,!=3.4.*,>=2.7" +version = "1.5.0" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "importlib-resources"] + +[[package]] +category = "main" +description = "Various helpers to pass data to untrusted environments and back." +name = "itsdangerous" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.0" + +[[package]] +category = "main" +description = "A small but fast and easy to use stand-alone template engine written in pure python." +name = "jinja2" +optional = false +python-versions = "*" +version = "2.10.1" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +category = "main" +description = "Mach-O header analysis and editing" +name = "macholib" +optional = false +python-versions = "*" +version = "1.11" + +[package.dependencies] +altgraph = ">=0.15" + +[[package]] +category = "main" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +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 = ">=3.4" +version = "7.2.0" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.3" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "main" +description = "File system general utilities" +name = "pathtools" +optional = false +python-versions = "*" +version = "0.1.2" + +[[package]] +category = "main" +description = "Python PE parsing module" +name = "pefile" +optional = false +python-versions = "*" +version = "2019.4.18" + +[package.dependencies] +future = "*" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.0" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "main" +description = "Cross-platform lib for process and system monitoring in Python." +name = "psutil" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "5.6.7" + +[package.extras] +enum = ["enum34"] + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.0" + +[[package]] +category = "main" +description = "Cryptographic library for Python" +name = "pycryptodome" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.9.0" + +[[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.*, !=3.4.*" +version = "3.6" + +[package.dependencies] +altgraph = "*" +dis3 = "*" +setuptools = "*" + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +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.1" + +[package.dependencies] +PyQt5_sip = ">=4.19.19,<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 = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +name = "pysocks" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.7.0" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "5.1.2" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +category = "dev" +description = "py.test plugin that activates the fault handler module for tests (dummy package)" +name = "pytest-faulthandler" +optional = false +python-versions = "*" +version = "2.0.1" + +[package.dependencies] +pytest = ">=5.0" + +[[package]] +category = "dev" +description = "pytest support for PyQt and PySide applications" +name = "pytest-qt" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.2.2" + +[package.dependencies] +pytest = ">=2.7.0" + +[package.extras] +doc = ["sphinx", "sphinx-rtd-theme"] + +[[package]] +category = "main" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "5.3" + +[[package]] +category = "main" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.22.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.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[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 = "dev" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "1.12.0" + +[[package]] +category = "main" +description = "Stem is a Python controller library that allows applications to interact with Tor (https://www.torproject.org/)." +name = "stem" +optional = false +python-versions = "*" +version = "1.7.1" + +[[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.25.3" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +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 = "Filesystem events monitoring" +name = "watchdog" +optional = false +python-versions = "*" +version = "0.9.0" + +[package.dependencies] +PyYAML = ">=3.10" +argh = ">=0.24.1" +pathtools = ">=0.1.1" + +[[package]] +category = "dev" +description = "Measures number of Terminal column cells of wide-character codes" +name = "wcwidth" +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.15.6" + +[package.extras] +dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] +termcolor = ["termcolor"] +watchdog = ["watchdog"] + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" +optional = false +python-versions = ">=3.6" +version = "3.1.0" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["jaraco.itertools", "func-timeout"] + +[metadata] +content-hash = "a4f3464f0a03ee100385754552f9fcf0c902ab9e58a16b4e3c99c4b29ee109f0" +python-versions = "^3.7" + +[metadata.files] +altgraph = [ + {file = "altgraph-0.16.1-py2.py3-none-any.whl", hash = "sha256:d6814989f242b2b43025cba7161fc1b8fb487a62cd49c49245d6fd01c18ac997"}, + {file = "altgraph-0.16.1.tar.gz", hash = "sha256:ddf5320017147ba7b810198e0b6619bd7b5563aa034da388cea8546b877f9b0c"}, +] +argh = [ + {file = "argh-0.26.2-py2.py3-none-any.whl", hash = "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3"}, + {file = "argh-0.26.2.tar.gz", hash = "sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65"}, +] +atomicwrites = [ + {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, + {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, +] +attrs = [ + {file = "attrs-19.1.0-py2.py3-none-any.whl", hash = "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79"}, + {file = "attrs-19.1.0.tar.gz", hash = "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"}, +] +certifi = [ + {file = "certifi-2019.9.11-py2.py3-none-any.whl", hash = "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"}, + {file = "certifi-2019.9.11.tar.gz", hash = "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {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"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +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"}, +] +flask = [ + {file = "Flask-1.1.1-py2.py3-none-any.whl", hash = "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"}, + {file = "Flask-1.1.1.tar.gz", hash = "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52"}, +] +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"}, +] +future = [ + {file = "future-0.17.1.tar.gz", hash = "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"}, +] +idna = [ + {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, + {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, +] +importlib-metadata = [ + {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"}, +] +itsdangerous = [ + {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, + {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, +] +jinja2 = [ + {file = "Jinja2-2.10.1-py2.py3-none-any.whl", hash = "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"}, + {file = "Jinja2-2.10.1.tar.gz", hash = "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013"}, +] +macholib = [ + {file = "macholib-1.11-py2.py3-none-any.whl", hash = "sha256:ac02d29898cf66f27510d8f39e9112ae00590adb4a48ec57b25028d6962b1ae1"}, + {file = "macholib-1.11.tar.gz", hash = "sha256:c4180ffc6f909bf8db6cd81cff4b6f601d575568f4d5dee148c830e9851eb9db"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +more-itertools = [ + {file = "more-itertools-7.2.0.tar.gz", hash = "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832"}, + {file = "more_itertools-7.2.0-py3-none-any.whl", hash = "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"}, +] +packaging = [ + {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, + {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, +] +pathtools = [ + {file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"}, +] +pefile = [ + {file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"}, +] +pluggy = [ + {file = "pluggy-0.13.0-py2.py3-none-any.whl", hash = "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6"}, + {file = "pluggy-0.13.0.tar.gz", hash = "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"}, +] +psutil = [ + {file = "psutil-5.6.7-cp27-none-win32.whl", hash = "sha256:1b1575240ca9a90b437e5a40db662acd87bbf181f6aa02f0204978737b913c6b"}, + {file = "psutil-5.6.7-cp27-none-win_amd64.whl", hash = "sha256:28f771129bfee9fc6b63d83a15d857663bbdcae3828e1cb926e91320a9b5b5cd"}, + {file = "psutil-5.6.7-cp35-cp35m-win32.whl", hash = "sha256:21231ef1c1a89728e29b98a885b8e0a8e00d09018f6da5cdc1f43f988471a995"}, + {file = "psutil-5.6.7-cp35-cp35m-win_amd64.whl", hash = "sha256:b74b43fecce384a57094a83d2778cdfc2e2d9a6afaadd1ebecb2e75e0d34e10d"}, + {file = "psutil-5.6.7-cp36-cp36m-win32.whl", hash = "sha256:e85f727ffb21539849e6012f47b12f6dd4c44965e56591d8dec6e8bc9ab96f4a"}, + {file = "psutil-5.6.7-cp36-cp36m-win_amd64.whl", hash = "sha256:b560f5cd86cf8df7bcd258a851ca1ad98f0d5b8b98748e877a0aec4e9032b465"}, + {file = "psutil-5.6.7-cp37-cp37m-win32.whl", hash = "sha256:094f899ac3ef72422b7e00411b4ed174e3c5a2e04c267db6643937ddba67a05b"}, + {file = "psutil-5.6.7-cp37-cp37m-win_amd64.whl", hash = "sha256:fd2e09bb593ad9bdd7429e779699d2d47c1268cbde4dda95fcd1bd17544a0217"}, + {file = "psutil-5.6.7-cp38-cp38-win32.whl", hash = "sha256:70387772f84fa5c3bb6a106915a2445e20ac8f9821c5914d7cbde148f4d7ff73"}, + {file = "psutil-5.6.7-cp38-cp38-win_amd64.whl", hash = "sha256:10b7f75cc8bd676cfc6fa40cd7d5c25b3f45a0e06d43becd7c2d2871cbb5e806"}, + {file = "psutil-5.6.7.tar.gz", hash = "sha256:ffad8eb2ac614518bbe3c0b8eb9dffdb3a8d2e3a7d5da51c5b974fb723a5c5aa"}, +] +py = [ + {file = "py-1.8.0-py2.py3-none-any.whl", hash = "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa"}, + {file = "py-1.8.0.tar.gz", hash = "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"}, +] +pycryptodome = [ + {file = "pycryptodome-3.9.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:71b041d43fe13004abc36ca720ac64ea489ee8a3407a25116481d0faf9d62494"}, + {file = "pycryptodome-3.9.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:a117047a220b3911d425affcd1cbc97a1af7ea7eb5d985d9964d42b4f0558489"}, + {file = "pycryptodome-3.9.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:7252498b427c421e306473ed344e58235eedd95c15fec2e1b33d333aefa1ea10"}, + {file = "pycryptodome-3.9.0-cp27-cp27m-win32.whl", hash = "sha256:c453ad968b67d66448543420ec39770c30bd16d986058255f058ab87c4f6cc1f"}, + {file = "pycryptodome-3.9.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c419943306756ddd1a1997120bb073733bc223365909c68185106d5521cbc0ef"}, + {file = "pycryptodome-3.9.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:1de815b847982f909dc2e5e2ca641b85cde80d95cc7e6a359c03d4b42cd21568"}, + {file = "pycryptodome-3.9.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a0962aea03933b99cf391c3e10dfef32f77915d5553464264cfbc6711f31d254"}, + {file = "pycryptodome-3.9.0-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:4f6cdddf1fe72e7f173e9734aa19b94cbd046b61a8559d650ff222e36021d5c1"}, + {file = "pycryptodome-3.9.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:8d2135c941d38f241e0e62dbdfc1ca5d9240527e61316126797f50b6f3e49825"}, + {file = "pycryptodome-3.9.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:0f29e1238ad3b6b6e2acd7ea1d8e8b382978a56503f2c48b67d5dc144d143cb0"}, + {file = "pycryptodome-3.9.0-cp34-cp34m-win32.whl", hash = "sha256:52d20b22c5b1fc952b4c686b99a6c55c3b0b0a673bec30570f156a72198f66ff"}, + {file = "pycryptodome-3.9.0-cp34-cp34m-win_amd64.whl", hash = "sha256:3701822a085dbebf678bfbdfbd6ebd92ffa80d5a544c9979984bf16a67c9790b"}, + {file = "pycryptodome-3.9.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:023c294367d7189ae224fb61bc8d49a2347704087c1c78dbd5ab114dd5b97761"}, + {file = "pycryptodome-3.9.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:18f376698e3ddcb1d3b312512ca78c9eed132e68ac6d0bf2e72452dfe213e96f"}, + {file = "pycryptodome-3.9.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a35a5c588248ba00eb976a8554211e584a55de286783bc69b12bdd7954052b4a"}, + {file = "pycryptodome-3.9.0-cp35-cp35m-win32.whl", hash = "sha256:233a04bb7bdd4b07e14d61d5166150942d872802daa4f049d49a453fe0659e94"}, + {file = "pycryptodome-3.9.0-cp35-cp35m-win_amd64.whl", hash = "sha256:f02382dc1bf91fb7123f2a3851fb1b526c871fa9359f387f2bcc847efc74ae52"}, + {file = "pycryptodome-3.9.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:33c07e1e36ec84524b49f99f11804d5e4d2188c643e84d914cb1e0a277ed3c79"}, + {file = "pycryptodome-3.9.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:600bf9dd5fbed0feee83950e2a8baacaa1f38b56c237fff270d31e47f8da9e52"}, + {file = "pycryptodome-3.9.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6840c9881e528224ebf72b3f73b3d11baf399e265106c9f4d9bae4f09615a93a"}, + {file = "pycryptodome-3.9.0-cp36-cp36m-win32.whl", hash = "sha256:d2d78644655629c7d1b9bf28e479d29facc0949d9ff095103ca9c2314b329ee0"}, + {file = "pycryptodome-3.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3f8e6851c0a45429f9b86c1597d3b831b0cff140b3e170a891fce55ef8dac2bb"}, + {file = "pycryptodome-3.9.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:5452b534fecf8bf57cf9106d00877f5f4ab7264e7a5e1f5ea8d15b04517d1255"}, + {file = "pycryptodome-3.9.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1ff619b8e4050799ca5ca0ffdf8eb0dbccba6997997866755f37e6aa7dde23fe"}, + {file = "pycryptodome-3.9.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c1a4f3f651471b9bf60b0d98fa8a994b8a73ff8ab4edc691e23243c853aaff9f"}, + {file = "pycryptodome-3.9.0-cp37-cp37m-win32.whl", hash = "sha256:5a7a9a4a7f8f0990fa97fee71c7f7e0c412925c515cfc6d4996961e92c9be8e5"}, + {file = "pycryptodome-3.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d7be60dc2126ee350ac7191549f5ab05c2dd76a5d5a3022249f395a401c6ea37"}, + {file = "pycryptodome-3.9.0.tar.gz", hash = "sha256:dbeb08ad850056747aa7d5f33273b7ce0b9a77910604a1be7b7a6f2ef076213f"}, +] +pyinstaller = [ + {file = "PyInstaller-3.6.tar.gz", hash = "sha256:3730fa80d088f8bb7084d32480eb87cbb4ddb64123363763cf8f2a1378c1c4b7"}, +] +pyparsing = [ + {file = "pyparsing-2.4.6-py2.py3-none-any.whl", hash = "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"}, + {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"}, +] +pyqt5 = [ + {file = "PyQt5-5.13.1-5.13.1-cp35.cp36.cp37.cp38-abi3-macosx_10_6_intel.whl", hash = "sha256:73f982b60819cc6d7cb0007b8b0cd11073e2546d74e0463726149e6a9d087caa"}, + {file = "PyQt5-5.13.1-5.13.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:f8e76070782e68ba200eb2b67babf89ac629df5e36d2f4c680ef34ae40c8f964"}, + {file = "PyQt5-5.13.1-5.13.1-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:6420d8c9de8746917bd2afe24f29b3722aabde36d1e9f8c34354f5b7fb7b987d"}, + {file = "PyQt5-5.13.1-5.13.1-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:8e6125f110133069e8f4a085022cfc6b4a0af5b3c9009999a749007c299e965d"}, +] +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"}, +] +pysocks = [ + {file = "PySocks-1.7.0-py27-none-any.whl", hash = "sha256:32238918ac0f19e9fd870a8692ac9bd14f5e8752b3c62624cda5851424642210"}, + {file = "PySocks-1.7.0-py3-none-any.whl", hash = "sha256:15d38914b60dbcb231d276f64882a20435c049450160e953ca7d313d1405f16f"}, + {file = "PySocks-1.7.0.tar.gz", hash = "sha256:d9031ea45fdfacbe59a99273e9f0448ddb33c1580fe3831c1b09557c5718977c"}, +] +pytest = [ + {file = "pytest-5.1.2-py3-none-any.whl", hash = "sha256:95d13143cc14174ca1a01ec68e84d76ba5d9d493ac02716fd9706c949a505210"}, + {file = "pytest-5.1.2.tar.gz", hash = "sha256:b78fe2881323bd44fd9bd76e5317173d4316577e7b1cddebae9136a4495ec865"}, +] +pytest-faulthandler = [ + {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"}, +] +pytest-qt = [ + {file = "pytest-qt-3.2.2.tar.gz", hash = "sha256:f6ecf4b38088ae1092cbd5beeaf714516d1f81f8938626a2eac546206cdfe7fa"}, + {file = "pytest_qt-3.2.2-py2.py3-none-any.whl", hash = "sha256:05b9c913f373fee19c69d8f2951e5ae3f123d7c3350c88015c8320f484b0b516"}, +] +pyyaml = [ + {file = "PyYAML-5.3-cp27-cp27m-win32.whl", hash = "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d"}, + {file = "PyYAML-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6"}, + {file = "PyYAML-5.3-cp35-cp35m-win32.whl", hash = "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e"}, + {file = "PyYAML-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689"}, + {file = "PyYAML-5.3-cp36-cp36m-win32.whl", hash = "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994"}, + {file = "PyYAML-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e"}, + {file = "PyYAML-5.3-cp37-cp37m-win32.whl", hash = "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5"}, + {file = "PyYAML-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf"}, + {file = "PyYAML-5.3-cp38-cp38-win32.whl", hash = "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811"}, + {file = "PyYAML-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20"}, + {file = "PyYAML-5.3.tar.gz", hash = "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"}, +] +requests = [ + {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, + {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, +] +six = [ + {file = "six-1.12.0-py2.py3-none-any.whl", hash = "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c"}, + {file = "six-1.12.0.tar.gz", hash = "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"}, +] +stem = [ + {file = "stem-1.7.1.tar.gz", hash = "sha256:c9eaf3116cb60c15995cbd3dec3a5cbc50e9bb6e062c4d6d42201e566f498ca2"}, +] +urllib3 = [ + {file = "urllib3-1.25.3-py2.py3-none-any.whl", hash = "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1"}, + {file = "urllib3-1.25.3.tar.gz", hash = "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"}, +] +watchdog = [ + {file = "watchdog-0.9.0.tar.gz", hash = "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d"}, +] +wcwidth = [ + {file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"}, + {file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"}, +] +werkzeug = [ + {file = "Werkzeug-0.15.6-py2.py3-none-any.whl", hash = "sha256:00d32beac38fcd48d329566f80d39f10ec2ed994efbecfb8dd4b320062d05902"}, + {file = "Werkzeug-0.15.6.tar.gz", hash = "sha256:0a24d43be6a7dce81bae05292356176d6c46d63e42a0dd3f9504b210a9cfaa43"}, +] +zipp = [ + {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 new file mode 100644 index 00000000..f7932650 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[tool.poetry] +name = "onionshare" +version = "2.2" +description = "OnionShare lets you securely and anonymously send and receive files. It works by starting a web server, making it accessible as a Tor onion service, and generating an unguessable web address so others can download files from you, or upload files to you. It does _not_ require setting up a separate server or using a third party file-sharing service." +authors = ["Micah Lee "] +license = "GPLv3+" + +[tool.poetry.dependencies] +python = "^3.7" +altgraph = "0.16.1" +certifi = "2019.9.11" +chardet = "3.0.4" +Click = "7.0" +Flask = "1.1.1" +Flask-HTTPAuth = "3.3.0" +future = "0.17.1" +idna = "2.8" +itsdangerous = "1.1.0" +Jinja2 = "2.10.1" +macholib = "1.11" +MarkupSafe = "1.1.1" +pefile = "2019.4.18" +pycryptodome = "3.9.0" +PyQt5 = "5.13.1" +PyQt5-sip = "4.19.19" +PySocks = "1.7.0" +requests = "2.22.0" +stem = "1.7.1" +urllib3 = "1.25.3" +Werkzeug = "0.15.6" +watchdog = "0.9.0" +psutil = "5.6.7" + +[tool.poetry.dev-dependencies] +atomicwrites = "1.3.0" +attrs = "19.1.0" +more-itertools = "7.2.0" +pluggy = "0.13.0" +py = "1.8.0" +pytest = "5.1.2" +pytest-faulthandler = "2.0.1" +pytest-qt = "3.2.2" +six = "1.12.0" +urllib3 = "1.25.3" +pyinstaller = {version = "^3.6", platform = "darwin"} +setuptools = {version = "^45.2.0", platform = "windows"} + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" From 28948b1e1874e2a0212b36849dd834f1b41e4c6d Mon Sep 17 00:00:00 2001 From: Saptak S Date: Sat, 21 Mar 2020 12:45:43 +0530 Subject: [PATCH 125/142] Update dependencies --- BUILD.md | 1 + poetry.lock | 671 ++++++++++++++++++++++++++++++++++++++----------- pyproject.toml | 74 +++--- 3 files changed, 567 insertions(+), 179 deletions(-) diff --git a/BUILD.md b/BUILD.md index b2810851..cc1f82f3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -259,6 +259,7 @@ This section documents the release process. Unless you're a core OnionShare deve Before making a release, all of these should be complete: * `share/version.txt` should have the correct version +* `pyproject.toml` should have the correct version * `install/onionshare.nsi` should have the correct version, for the Windows installer * `CHANGELOG.md` should be updated to include a list of all major changes since the last release * There must be a PGP-signed git tag for the version, e.g. for OnionShare 2.1, the tag must be `v2.1` diff --git a/poetry.lock b/poetry.lock index 51bad950..479ea03b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4,15 +4,7 @@ description = "Python graph (network) package" name = "altgraph" optional = false python-versions = "*" -version = "0.16.1" - -[[package]] -category = "main" -description = "An unobtrusive argparse wrapper with natural syntax" -name = "argh" -optional = false -python-versions = "*" -version = "0.26.2" +version = "0.17" [[package]] category = "dev" @@ -28,12 +20,13 @@ description = "Classes Without Boilerplate" name = "attrs" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.1.0" +version = "19.3.0" [package.extras] -dev = ["coverage", "hypothesis", "pympler", "pytest", "six", "zope.interface", "sphinx", "pre-commit"] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] docs = ["sphinx", "zope.interface"] -tests = ["coverage", "hypothesis", "pympler", "pytest", "six", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] [[package]] category = "main" @@ -41,7 +34,7 @@ description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false python-versions = "*" -version = "2019.9.11" +version = "2019.11.28" [[package]] category = "main" @@ -59,15 +52,54 @@ 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" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.1" + [[package]] category = "dev" description = "Cross-platform colored terminal text." -marker = "sys_platform == \"win32\"" +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\"" 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+" @@ -77,6 +109,34 @@ 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." @@ -107,13 +167,22 @@ 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" name = "future" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.17.1" +version = "0.18.2" [[package]] category = "main" @@ -123,6 +192,30 @@ 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)" +name = "idna" +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" @@ -135,6 +228,18 @@ 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"] @@ -149,11 +254,25 @@ version = "1.1.0" [[package]] category = "main" -description = "A small but fast and easy to use stand-alone template engine written in pure python." +description = "A very fast and expressive template engine." name = "jinja2" optional = false python-versions = "*" -version = "2.10.1" +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." +name = "jinja2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.11.1" [package.dependencies] MarkupSafe = ">=0.23" @@ -167,7 +286,7 @@ description = "Mach-O header analysis and editing" name = "macholib" optional = false python-versions = "*" -version = "1.11" +version = "1.14" [package.dependencies] altgraph = ">=0.15" @@ -180,6 +299,17 @@ 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" @@ -188,6 +318,14 @@ optional = false python-versions = ">=3.4" version = "7.2.0" +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.5" +version = "8.2.0" + [[package]] category = "dev" description = "Core utilities for Python packages" @@ -200,6 +338,22 @@ 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" @@ -225,7 +379,7 @@ description = "plugin and hook calling mechanisms for python" name = "pluggy" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.13.0" +version = "0.13.1" [package.dependencies] [package.dependencies.importlib-metadata] @@ -241,7 +395,7 @@ description = "Cross-platform lib for process and system monitoring in Python." name = "psutil" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "5.6.7" +version = "5.7.0" [package.extras] enum = ["enum34"] @@ -252,7 +406,7 @@ description = "library with cross-python path, ini-parsing, io, code, log facili name = "py" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.8.0" +version = "1.8.1" [[package]] category = "main" @@ -260,7 +414,20 @@ description = "Cryptographic library for Python" name = "pycryptodome" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.9.0" +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" @@ -290,11 +457,22 @@ description = "Python bindings for the Qt cross platform UI and application tool name = "pyqt5" optional = false python-versions = "*" -version = "5.13.1" +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" + +[package.dependencies] +PyQt5-sip = ">=12.7,<13" + [[package]] category = "main" description = "Python extension module support for PyQt5" @@ -303,13 +481,69 @@ optional = false python-versions = "*" version = "4.19.19" +[[package]] +category = "main" +description = "The sip module support for PyQt5" +name = "pyqt5-sip" +optional = false +python-versions = ">=3.5" +version = "12.7.1" + [[package]] category = "main" description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." name = "pysocks" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.7.0" +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" @@ -317,7 +551,7 @@ description = "pytest: simple powerful testing with Python" name = "pytest" optional = false python-versions = ">=3.5" -version = "5.1.2" +version = "5.4.1" [package.dependencies] atomicwrites = ">=1.0" @@ -333,9 +567,29 @@ 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)" @@ -353,21 +607,32 @@ description = "pytest support for PyQt and PySide applications" name = "pytest-qt" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.2.2" +version = "3.3.0" [package.dependencies] -pytest = ">=2.7.0" +pytest = ">=3.0.0" [package.extras] +dev = ["pre-commit", "tox"] doc = ["sphinx", "sphinx-rtd-theme"] [[package]] category = "main" -description = "YAML parser and emitter for Python" -name = "pyyaml" +description = "Python HTTP for Humans." +name = "requests" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "5.3" +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" @@ -375,25 +640,34 @@ description = "Python HTTP for Humans." name = "requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.22.0" +version = "2.23.0" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<3.1.0" -idna = ">=2.5,<2.9" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] +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" name = "six" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "1.12.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.14.0" [[package]] category = "main" @@ -401,7 +675,19 @@ description = "Stem is a Python controller library that allows applications to i name = "stem" optional = false python-versions = "*" -version = "1.7.1" +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" @@ -409,7 +695,19 @@ description = "HTTP library with thread-safe connection pooling, file post, and name = "urllib3" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" -version = "1.25.3" +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." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.8" [package.extras] brotli = ["brotlipy (>=0.6.0)"] @@ -422,13 +720,14 @@ description = "Filesystem events monitoring" name = "watchdog" optional = false python-versions = "*" -version = "0.9.0" +version = "0.10.2" [package.dependencies] -PyYAML = ">=3.10" -argh = ">=0.24.1" pathtools = ">=0.1.1" +[package.extras] +watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"] + [[package]] category = "dev" description = "Measures number of Terminal column cells of wide-character codes" @@ -443,50 +742,63 @@ 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.15.6" +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." +name = "werkzeug" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.0.0" + +[package.extras] +dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] +watchdog = ["watchdog"] + [[package]] category = "dev" description = "Backport of pathlib-compatible object wrapper for zip files" marker = "python_version < \"3.8\"" name = "zipp" optional = false -python-versions = ">=3.6" -version = "3.1.0" +python-versions = ">=2.7" +version = "1.2.0" + +[package.dependencies] +[package.dependencies.contextlib2] +python = "<3.4" +version = "*" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["jaraco.itertools", "func-timeout"] +testing = ["pathlib2", "unittest2", "jaraco.itertools", "func-timeout"] [metadata] -content-hash = "a4f3464f0a03ee100385754552f9fcf0c902ab9e58a16b4e3c99c4b29ee109f0" -python-versions = "^3.7" +content-hash = "47cac9d28916836244924702eea56fff904d64bde723fe212d1caa60f5f24891" +python-versions = "*" [metadata.files] altgraph = [ - {file = "altgraph-0.16.1-py2.py3-none-any.whl", hash = "sha256:d6814989f242b2b43025cba7161fc1b8fb487a62cd49c49245d6fd01c18ac997"}, - {file = "altgraph-0.16.1.tar.gz", hash = "sha256:ddf5320017147ba7b810198e0b6619bd7b5563aa034da388cea8546b877f9b0c"}, -] -argh = [ - {file = "argh-0.26.2-py2.py3-none-any.whl", hash = "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3"}, - {file = "argh-0.26.2.tar.gz", hash = "sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65"}, + {file = "altgraph-0.17-py2.py3-none-any.whl", hash = "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe"}, + {file = "altgraph-0.17.tar.gz", hash = "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa"}, ] atomicwrites = [ {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, ] attrs = [ - {file = "attrs-19.1.0-py2.py3-none-any.whl", hash = "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79"}, - {file = "attrs-19.1.0.tar.gz", hash = "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"}, + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, ] certifi = [ - {file = "certifi-2019.9.11-py2.py3-none-any.whl", hash = "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"}, - {file = "certifi-2019.9.11.tar.gz", hash = "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50"}, + {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, + {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, ] chardet = [ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, @@ -495,17 +807,36 @@ chardet = [ 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"}, ] @@ -513,14 +844,22 @@ 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.17.1.tar.gz", hash = "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"}, + {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"}, ] @@ -529,12 +868,14 @@ itsdangerous = [ {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, ] jinja2 = [ - {file = "Jinja2-2.10.1-py2.py3-none-any.whl", hash = "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"}, - {file = "Jinja2-2.10.1.tar.gz", hash = "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013"}, + {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"}, ] macholib = [ - {file = "macholib-1.11-py2.py3-none-any.whl", hash = "sha256:ac02d29898cf66f27510d8f39e9112ae00590adb4a48ec57b25028d6962b1ae1"}, - {file = "macholib-1.11.tar.gz", hash = "sha256:c4180ffc6f909bf8db6cd81cff4b6f601d575568f4d5dee148c830e9851eb9db"}, + {file = "macholib-1.14-py2.py3-none-any.whl", hash = "sha256:c500f02867515e6c60a27875b408920d18332ddf96b4035ef03beddd782d4281"}, + {file = "macholib-1.14.tar.gz", hash = "sha256:0c436bc847e7b1d9bda0560351bf76d7caf930fb585a828d13608839ef42c432"}, ] markupsafe = [ {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, @@ -572,13 +913,22 @@ 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"}, ] 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"}, ] @@ -586,57 +936,60 @@ pefile = [ {file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"}, ] pluggy = [ - {file = "pluggy-0.13.0-py2.py3-none-any.whl", hash = "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6"}, - {file = "pluggy-0.13.0.tar.gz", hash = "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"}, + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] psutil = [ - {file = "psutil-5.6.7-cp27-none-win32.whl", hash = "sha256:1b1575240ca9a90b437e5a40db662acd87bbf181f6aa02f0204978737b913c6b"}, - {file = "psutil-5.6.7-cp27-none-win_amd64.whl", hash = "sha256:28f771129bfee9fc6b63d83a15d857663bbdcae3828e1cb926e91320a9b5b5cd"}, - {file = "psutil-5.6.7-cp35-cp35m-win32.whl", hash = "sha256:21231ef1c1a89728e29b98a885b8e0a8e00d09018f6da5cdc1f43f988471a995"}, - {file = "psutil-5.6.7-cp35-cp35m-win_amd64.whl", hash = "sha256:b74b43fecce384a57094a83d2778cdfc2e2d9a6afaadd1ebecb2e75e0d34e10d"}, - {file = "psutil-5.6.7-cp36-cp36m-win32.whl", hash = "sha256:e85f727ffb21539849e6012f47b12f6dd4c44965e56591d8dec6e8bc9ab96f4a"}, - {file = "psutil-5.6.7-cp36-cp36m-win_amd64.whl", hash = "sha256:b560f5cd86cf8df7bcd258a851ca1ad98f0d5b8b98748e877a0aec4e9032b465"}, - {file = "psutil-5.6.7-cp37-cp37m-win32.whl", hash = "sha256:094f899ac3ef72422b7e00411b4ed174e3c5a2e04c267db6643937ddba67a05b"}, - {file = "psutil-5.6.7-cp37-cp37m-win_amd64.whl", hash = "sha256:fd2e09bb593ad9bdd7429e779699d2d47c1268cbde4dda95fcd1bd17544a0217"}, - {file = "psutil-5.6.7-cp38-cp38-win32.whl", hash = "sha256:70387772f84fa5c3bb6a106915a2445e20ac8f9821c5914d7cbde148f4d7ff73"}, - {file = "psutil-5.6.7-cp38-cp38-win_amd64.whl", hash = "sha256:10b7f75cc8bd676cfc6fa40cd7d5c25b3f45a0e06d43becd7c2d2871cbb5e806"}, - {file = "psutil-5.6.7.tar.gz", hash = "sha256:ffad8eb2ac614518bbe3c0b8eb9dffdb3a8d2e3a7d5da51c5b974fb723a5c5aa"}, + {file = "psutil-5.7.0-cp27-none-win32.whl", hash = "sha256:298af2f14b635c3c7118fd9183843f4e73e681bb6f01e12284d4d70d48a60953"}, + {file = "psutil-5.7.0-cp27-none-win_amd64.whl", hash = "sha256:75e22717d4dbc7ca529ec5063000b2b294fc9a367f9c9ede1f65846c7955fd38"}, + {file = "psutil-5.7.0-cp35-cp35m-win32.whl", hash = "sha256:f344ca230dd8e8d5eee16827596f1c22ec0876127c28e800d7ae20ed44c4b310"}, + {file = "psutil-5.7.0-cp35-cp35m-win_amd64.whl", hash = "sha256:e2d0c5b07c6fe5a87fa27b7855017edb0d52ee73b71e6ee368fae268605cc3f5"}, + {file = "psutil-5.7.0-cp36-cp36m-win32.whl", hash = "sha256:a02f4ac50d4a23253b68233b07e7cdb567bd025b982d5cf0ee78296990c22d9e"}, + {file = "psutil-5.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1413f4158eb50e110777c4f15d7c759521703bd6beb58926f1d562da40180058"}, + {file = "psutil-5.7.0-cp37-cp37m-win32.whl", hash = "sha256:d008ddc00c6906ec80040d26dc2d3e3962109e40ad07fd8a12d0284ce5e0e4f8"}, + {file = "psutil-5.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:73f35ab66c6c7a9ce82ba44b1e9b1050be2a80cd4dcc3352cc108656b115c74f"}, + {file = "psutil-5.7.0-cp38-cp38-win32.whl", hash = "sha256:60b86f327c198561f101a92be1995f9ae0399736b6eced8f24af41ec64fb88d4"}, + {file = "psutil-5.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:d84029b190c8a66a946e28b4d3934d2ca1528ec94764b180f7d6ea57b0e75e26"}, + {file = "psutil-5.7.0.tar.gz", hash = "sha256:685ec16ca14d079455892f25bd124df26ff9137664af445563c1bd36629b5e0e"}, ] py = [ - {file = "py-1.8.0-py2.py3-none-any.whl", hash = "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa"}, - {file = "py-1.8.0.tar.gz", hash = "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"}, + {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, + {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, ] pycryptodome = [ - {file = "pycryptodome-3.9.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:71b041d43fe13004abc36ca720ac64ea489ee8a3407a25116481d0faf9d62494"}, - {file = "pycryptodome-3.9.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:a117047a220b3911d425affcd1cbc97a1af7ea7eb5d985d9964d42b4f0558489"}, - {file = "pycryptodome-3.9.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:7252498b427c421e306473ed344e58235eedd95c15fec2e1b33d333aefa1ea10"}, - {file = "pycryptodome-3.9.0-cp27-cp27m-win32.whl", hash = "sha256:c453ad968b67d66448543420ec39770c30bd16d986058255f058ab87c4f6cc1f"}, - {file = "pycryptodome-3.9.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c419943306756ddd1a1997120bb073733bc223365909c68185106d5521cbc0ef"}, - {file = "pycryptodome-3.9.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:1de815b847982f909dc2e5e2ca641b85cde80d95cc7e6a359c03d4b42cd21568"}, - {file = "pycryptodome-3.9.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a0962aea03933b99cf391c3e10dfef32f77915d5553464264cfbc6711f31d254"}, - {file = "pycryptodome-3.9.0-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:4f6cdddf1fe72e7f173e9734aa19b94cbd046b61a8559d650ff222e36021d5c1"}, - {file = "pycryptodome-3.9.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:8d2135c941d38f241e0e62dbdfc1ca5d9240527e61316126797f50b6f3e49825"}, - {file = "pycryptodome-3.9.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:0f29e1238ad3b6b6e2acd7ea1d8e8b382978a56503f2c48b67d5dc144d143cb0"}, - {file = "pycryptodome-3.9.0-cp34-cp34m-win32.whl", hash = "sha256:52d20b22c5b1fc952b4c686b99a6c55c3b0b0a673bec30570f156a72198f66ff"}, - {file = "pycryptodome-3.9.0-cp34-cp34m-win_amd64.whl", hash = "sha256:3701822a085dbebf678bfbdfbd6ebd92ffa80d5a544c9979984bf16a67c9790b"}, - {file = "pycryptodome-3.9.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:023c294367d7189ae224fb61bc8d49a2347704087c1c78dbd5ab114dd5b97761"}, - {file = "pycryptodome-3.9.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:18f376698e3ddcb1d3b312512ca78c9eed132e68ac6d0bf2e72452dfe213e96f"}, - {file = "pycryptodome-3.9.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a35a5c588248ba00eb976a8554211e584a55de286783bc69b12bdd7954052b4a"}, - {file = "pycryptodome-3.9.0-cp35-cp35m-win32.whl", hash = "sha256:233a04bb7bdd4b07e14d61d5166150942d872802daa4f049d49a453fe0659e94"}, - {file = "pycryptodome-3.9.0-cp35-cp35m-win_amd64.whl", hash = "sha256:f02382dc1bf91fb7123f2a3851fb1b526c871fa9359f387f2bcc847efc74ae52"}, - {file = "pycryptodome-3.9.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:33c07e1e36ec84524b49f99f11804d5e4d2188c643e84d914cb1e0a277ed3c79"}, - {file = "pycryptodome-3.9.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:600bf9dd5fbed0feee83950e2a8baacaa1f38b56c237fff270d31e47f8da9e52"}, - {file = "pycryptodome-3.9.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6840c9881e528224ebf72b3f73b3d11baf399e265106c9f4d9bae4f09615a93a"}, - {file = "pycryptodome-3.9.0-cp36-cp36m-win32.whl", hash = "sha256:d2d78644655629c7d1b9bf28e479d29facc0949d9ff095103ca9c2314b329ee0"}, - {file = "pycryptodome-3.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3f8e6851c0a45429f9b86c1597d3b831b0cff140b3e170a891fce55ef8dac2bb"}, - {file = "pycryptodome-3.9.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:5452b534fecf8bf57cf9106d00877f5f4ab7264e7a5e1f5ea8d15b04517d1255"}, - {file = "pycryptodome-3.9.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1ff619b8e4050799ca5ca0ffdf8eb0dbccba6997997866755f37e6aa7dde23fe"}, - {file = "pycryptodome-3.9.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c1a4f3f651471b9bf60b0d98fa8a994b8a73ff8ab4edc691e23243c853aaff9f"}, - {file = "pycryptodome-3.9.0-cp37-cp37m-win32.whl", hash = "sha256:5a7a9a4a7f8f0990fa97fee71c7f7e0c412925c515cfc6d4996961e92c9be8e5"}, - {file = "pycryptodome-3.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d7be60dc2126ee350ac7191549f5ab05c2dd76a5d5a3022249f395a401c6ea37"}, - {file = "pycryptodome-3.9.0.tar.gz", hash = "sha256:dbeb08ad850056747aa7d5f33273b7ce0b9a77910604a1be7b7a6f2ef076213f"}, + {file = "pycryptodome-3.9.7-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:0e10f352ccbbcb5bb2dc4ecaf106564e65702a717d72ab260f9ac4c19753cfc2"}, + {file = "pycryptodome-3.9.7-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9c739b7795ccf2ef1fdad8d44e539a39ad300ee6786e804ea7f0c6a786eb5343"}, + {file = "pycryptodome-3.9.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9977086e0f93adb326379897437373871b80501e1d176fec63c7f46fb300c862"}, + {file = "pycryptodome-3.9.7-cp27-cp27m-win32.whl", hash = "sha256:83295a3fb5cf50c48631eb5b440cb5e9832d8c14d81d1d45f4497b67a9987de8"}, + {file = "pycryptodome-3.9.7-cp27-cp27m-win_amd64.whl", hash = "sha256:b1e332587b3b195542e77681389c296e1837ca01240399d88803a075447d3557"}, + {file = "pycryptodome-3.9.7-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9378c309aec1f8cd8bad361ed0816a440151b97a2a3f6ffdaba1d1a1fb76873a"}, + {file = "pycryptodome-3.9.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4f94368ce2d65873a87ad867eb3bf63f4ba81eb97a9ee66d38c2b71ce5a7439"}, + {file = "pycryptodome-3.9.7-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:f655addaaaa9974108d4808f4150652589cada96074c87115c52e575bfcd87d5"}, + {file = "pycryptodome-3.9.7-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:9a94fca11fdc161460bd8659c15b6adef45c1b20da86402256eaf3addfaab324"}, + {file = "pycryptodome-3.9.7-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ea83bcd9d6c03248ebd46e71ac313858e0afd5aa2fa81478c0e653242f3eb476"}, + {file = "pycryptodome-3.9.7-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:07024fc364869eae8d6ac0d316e089956e6aeffe42dbdcf44fe1320d96becf7f"}, + {file = "pycryptodome-3.9.7-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:426c188c83c10df71f053e04b4003b1437bae5cb37606440e498b00f160d71d0"}, + {file = "pycryptodome-3.9.7-cp35-cp35m-win32.whl", hash = "sha256:d61b012baa8c2b659e9890011358455c0019a4108536b811602d2f638c40802a"}, + {file = "pycryptodome-3.9.7-cp35-cp35m-win_amd64.whl", hash = "sha256:1f4752186298caf2e9ff5354f2e694d607ca7342aa313a62005235d46e28cf04"}, + {file = "pycryptodome-3.9.7-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:767ad0fb5d23efc36a4d5c2fc608ac603f3de028909bcf59abc943e0d0bc5a36"}, + {file = "pycryptodome-3.9.7-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:2fbc472e0b567318fe2052281d5a8c0ae70099b446679815f655e9fbc18c3a65"}, + {file = "pycryptodome-3.9.7-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9230fcb5d948c3fb40049bace4d33c5d254f8232c2c0bba05d2570aea3ba4520"}, + {file = "pycryptodome-3.9.7-cp36-cp36m-win32.whl", hash = "sha256:8f06556a8f7ea7b1e42eff39726bb0dca1c251205debae64e6eebea3cd7b438a"}, + {file = "pycryptodome-3.9.7-cp36-cp36m-win_amd64.whl", hash = "sha256:d6e1bc5c94873bec742afe2dfadce0d20445b18e75c47afc0c115b19e5dd38dd"}, + {file = "pycryptodome-3.9.7-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:3ec3dc2f80f71fd0c955ce48b81bfaf8914c6f63a41a738f28885a1c4892968a"}, + {file = "pycryptodome-3.9.7-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cff31f5a8977534f255f729d5d2467526f2b10563a30bbdade92223e0bf264bd"}, + {file = "pycryptodome-3.9.7-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ed5761b37615a1f222c5345bbf45272ae2cf8c7dff88a4f53a1e9f977cbb6d95"}, + {file = "pycryptodome-3.9.7-cp37-cp37m-win32.whl", hash = "sha256:f011cd0062e54658b7086a76f8cf0f4222812acc66e219e196ea2d0a8849d0ed"}, + {file = "pycryptodome-3.9.7-cp37-cp37m-win_amd64.whl", hash = "sha256:626c0a1d4d83ec6303f970a17158114f75c3ba1736f7f2983f7b40a265861bd8"}, + {file = "pycryptodome-3.9.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be56bde3312e022d9d1d6afa124556460ad5c844c2fc63642f6af723c098d35"}, + {file = "pycryptodome-3.9.7-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c818dc1f3eace93ee50c2b6b5c2becf7c418fa5dd1ba6fc0ef7db279ea21d5e4"}, + {file = "pycryptodome-3.9.7-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:09b6d6bcc01a4eb1a2b4deeff5aa602a108ec5aed8ac75ae554f97d1d7f0a5ad"}, + {file = "pycryptodome-3.9.7-cp38-cp38-win32.whl", hash = "sha256:7ac729d9091ed5478af2b4a4f44f5335a98febbc008af619e4569a59fe503e40"}, + {file = "pycryptodome-3.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:c109a26a21f21f695d369ff9b87f5d43e0d6c768d8384e10bc74142bed2e092e"}, + {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 = [ @@ -644,10 +997,15 @@ pyparsing = [ {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"}, ] pyqt5 = [ - {file = "PyQt5-5.13.1-5.13.1-cp35.cp36.cp37.cp38-abi3-macosx_10_6_intel.whl", hash = "sha256:73f982b60819cc6d7cb0007b8b0cd11073e2546d74e0463726149e6a9d087caa"}, - {file = "PyQt5-5.13.1-5.13.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:f8e76070782e68ba200eb2b67babf89ac629df5e36d2f4c680ef34ae40c8f964"}, - {file = "PyQt5-5.13.1-5.13.1-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:6420d8c9de8746917bd2afe24f29b3722aabde36d1e9f8c34354f5b7fb7b987d"}, - {file = "PyQt5-5.13.1-5.13.1-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:8e6125f110133069e8f4a085022cfc6b4a0af5b3c9009999a749007c299e965d"}, + {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"}, ] pyqt5-sip = [ {file = "PyQt5_sip-4.19.19-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:aade50f9a1b9d20f6aabe88e8999b10db57218f5c31950160f3f7957dd64e07c"}, @@ -666,64 +1024,93 @@ pyqt5-sip = [ {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"}, + {file = "PyQt5_sip-12.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:d46b0f8effc554de52a1466b1bd80e5cb4bce635a75ac4e7ad6247c965dec5b9"}, + {file = "PyQt5_sip-12.7.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3f665376d9e52faa9855c3736a66ce6d825f85c86d7774d3c393f09da23f4f86"}, + {file = "PyQt5_sip-12.7.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1115728644bbadcde5fc8a16e7918bd31915a42dd6fb36b10d4afb78c582753e"}, + {file = "PyQt5_sip-12.7.1-cp36-cp36m-win32.whl", hash = "sha256:cbeeae6b45234a1654657f79943f8bccd3d14b4e7496746c62cf6fbce69442c7"}, + {file = "PyQt5_sip-12.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8da842d3d7bf8931d1093105fb92702276b6dbb7e801abbaaa869405d616171a"}, + {file = "PyQt5_sip-12.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f4289276d355b6521dc2cc956189315da6f13adfb6bbab8f25ebd15e3bce1d4"}, + {file = "PyQt5_sip-12.7.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c1e730a9eb2ec3869ed5d81b0f99f6e2460fb4d77750444c0ec183b771d798f7"}, + {file = "PyQt5_sip-12.7.1-cp37-cp37m-win32.whl", hash = "sha256:b5b4906445fe980aee76f20400116b6904bf5f30d0767489c13370e42a764020"}, + {file = "PyQt5_sip-12.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7ffa39763097f64de129cf5cc770a651c3f65d2466b4fe05bef2bd2efbaa38e6"}, + {file = "PyQt5_sip-12.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:288c6dc18a8d6a20981c07b715b5695d9b66880778565f3792bc6e38f14f20fb"}, + {file = "PyQt5_sip-12.7.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ee1a12f09d5af2304273bfd2f6b43835c1467d5ed501a6c95f5405637fa7750a"}, + {file = "PyQt5_sip-12.7.1-cp38-cp38-win32.whl", hash = "sha256:8a18e6f45d482ddfe381789979d09ee13aa6450caa3a0476503891bccb3ac709"}, + {file = "PyQt5_sip-12.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:e28c3abc9b62a1b7e796891648b9f14f8167b31c8e7990fae79654777252bb4d"}, + {file = "PyQt5_sip-12.7.1.tar.gz", hash = "sha256:e6078f5ee7d31c102910d0c277a110e1c2a20a3fc88cd017a39e170120586d3f"}, ] pysocks = [ - {file = "PySocks-1.7.0-py27-none-any.whl", hash = "sha256:32238918ac0f19e9fd870a8692ac9bd14f5e8752b3c62624cda5851424642210"}, - {file = "PySocks-1.7.0-py3-none-any.whl", hash = "sha256:15d38914b60dbcb231d276f64882a20435c049450160e953ca7d313d1405f16f"}, - {file = "PySocks-1.7.0.tar.gz", hash = "sha256:d9031ea45fdfacbe59a99273e9f0448ddb33c1580fe3831c1b09557c5718977c"}, + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, ] pytest = [ - {file = "pytest-5.1.2-py3-none-any.whl", hash = "sha256:95d13143cc14174ca1a01ec68e84d76ba5d9d493ac02716fd9706c949a505210"}, - {file = "pytest-5.1.2.tar.gz", hash = "sha256:b78fe2881323bd44fd9bd76e5317173d4316577e7b1cddebae9136a4495ec865"}, + {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"}, ] pytest-qt = [ - {file = "pytest-qt-3.2.2.tar.gz", hash = "sha256:f6ecf4b38088ae1092cbd5beeaf714516d1f81f8938626a2eac546206cdfe7fa"}, - {file = "pytest_qt-3.2.2-py2.py3-none-any.whl", hash = "sha256:05b9c913f373fee19c69d8f2951e5ae3f123d7c3350c88015c8320f484b0b516"}, -] -pyyaml = [ - {file = "PyYAML-5.3-cp27-cp27m-win32.whl", hash = "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d"}, - {file = "PyYAML-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6"}, - {file = "PyYAML-5.3-cp35-cp35m-win32.whl", hash = "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e"}, - {file = "PyYAML-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689"}, - {file = "PyYAML-5.3-cp36-cp36m-win32.whl", hash = "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994"}, - {file = "PyYAML-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e"}, - {file = "PyYAML-5.3-cp37-cp37m-win32.whl", hash = "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5"}, - {file = "PyYAML-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf"}, - {file = "PyYAML-5.3-cp38-cp38-win32.whl", hash = "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811"}, - {file = "PyYAML-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20"}, - {file = "PyYAML-5.3.tar.gz", hash = "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"}, + {file = "pytest-qt-3.3.0.tar.gz", hash = "sha256:714b0bf86c5313413f2d300ac613515db3a1aef595051ab8ba2ffe619dbe8925"}, + {file = "pytest_qt-3.3.0-py2.py3-none-any.whl", hash = "sha256:5f8928288f50489d83f5d38caf2d7d9fcd6e7cf769947902caa4661dc7c851e3"}, ] requests = [ - {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, - {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, + {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.12.0-py2.py3-none-any.whl", hash = "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c"}, - {file = "six-1.12.0.tar.gz", hash = "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"}, + {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, + {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, ] stem = [ - {file = "stem-1.7.1.tar.gz", hash = "sha256:c9eaf3116cb60c15995cbd3dec3a5cbc50e9bb6e062c4d6d42201e566f498ca2"}, + {file = "stem-1.8.0.tar.gz", hash = "sha256:a0b48ea6224e95f22aa34c0bc3415f0eb4667ddeae3dfb5e32a6920c185568c2"}, ] urllib3 = [ - {file = "urllib3-1.25.3-py2.py3-none-any.whl", hash = "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1"}, - {file = "urllib3-1.25.3.tar.gz", hash = "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"}, + {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"}, ] watchdog = [ - {file = "watchdog-0.9.0.tar.gz", hash = "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d"}, + {file = "watchdog-0.10.2.tar.gz", hash = "sha256:c560efb643faed5ef28784b2245cf8874f939569717a4a12826a173ac644456b"}, ] wcwidth = [ {file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"}, {file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"}, ] werkzeug = [ - {file = "Werkzeug-0.15.6-py2.py3-none-any.whl", hash = "sha256:00d32beac38fcd48d329566f80d39f10ec2ed994efbecfb8dd4b320062d05902"}, - {file = "Werkzeug-0.15.6.tar.gz", hash = "sha256:0a24d43be6a7dce81bae05292356176d6c46d63e42a0dd3f9504b210a9cfaa43"}, + {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-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, - {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, + {file = "zipp-1.2.0-py2.py3-none-any.whl", hash = "sha256:e0d9e63797e483a30d27e09fffd308c59a700d365ec34e93cc100844168bf921"}, + {file = "zipp-1.2.0.tar.gz", hash = "sha256:c70410551488251b0fee67b460fb9a536af8d6f9f008ad10ac51f615b6a521b1"}, ] diff --git a/pyproject.toml b/pyproject.toml index f7932650..f72da02f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,48 +2,48 @@ name = "onionshare" version = "2.2" description = "OnionShare lets you securely and anonymously send and receive files. It works by starting a web server, making it accessible as a Tor onion service, and generating an unguessable web address so others can download files from you, or upload files to you. It does _not_ require setting up a separate server or using a third party file-sharing service." -authors = ["Micah Lee "] +authors = ["Micah Lee "] license = "GPLv3+" [tool.poetry.dependencies] -python = "^3.7" -altgraph = "0.16.1" -certifi = "2019.9.11" -chardet = "3.0.4" -Click = "7.0" -Flask = "1.1.1" -Flask-HTTPAuth = "3.3.0" -future = "0.17.1" -idna = "2.8" -itsdangerous = "1.1.0" -Jinja2 = "2.10.1" -macholib = "1.11" -MarkupSafe = "1.1.1" -pefile = "2019.4.18" -pycryptodome = "3.9.0" -PyQt5 = "5.13.1" -PyQt5-sip = "4.19.19" -PySocks = "1.7.0" -requests = "2.22.0" -stem = "1.7.1" -urllib3 = "1.25.3" -Werkzeug = "0.15.6" -watchdog = "0.9.0" -psutil = "5.6.7" +python = "*" +altgraph = "*" +certifi = "*" +chardet = "*" +Click = "*" +Flask = "*" +Flask-HTTPAuth = "*" +future = "*" +idna = "*" +itsdangerous = "*" +Jinja2 = "*" +macholib = "*" +MarkupSafe = "*" +pefile = "*" +pycryptodome = "*" +PyQt5 = "*" +PyQt5-sip = "*" +PySocks = "*" +requests = "*" +stem = "*" +urllib3 = "*" +Werkzeug = "*" +watchdog = "*" +psutil = "*" [tool.poetry.dev-dependencies] -atomicwrites = "1.3.0" -attrs = "19.1.0" -more-itertools = "7.2.0" -pluggy = "0.13.0" -py = "1.8.0" -pytest = "5.1.2" -pytest-faulthandler = "2.0.1" -pytest-qt = "3.2.2" -six = "1.12.0" -urllib3 = "1.25.3" -pyinstaller = {version = "^3.6", platform = "darwin"} -setuptools = {version = "^45.2.0", platform = "windows"} +atomicwrites = "*" +attrs = "*" +more-itertools = "*" +pluggy = "*" +py = "*" +pytest = "*" +pytest-faulthandler = "*" +pytest-qt = "*" +six = "*" +urllib3 = "*" +pyinstaller = {version = "*", platform = "darwin"} +setuptools = {version = "*", platform = "windows"} [build-system] requires = ["poetry>=0.12"] From 8101eb1d78f73123591509629efa710df6a28a57 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 11:32:18 -0700 Subject: [PATCH 126/142] 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 eb06b0148d7dd76bf8a1f8c0d9da87a3acc09982 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 11:35:33 -0700 Subject: [PATCH 127/142] 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 4465ca8a720d8d4e20e211f0e56ca2c9da12a337 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 11:45:37 -0700 Subject: [PATCH 128/142] 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 998451b5289188fde814bcd82cef47a38fbbbbc8 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 12:33:35 -0700 Subject: [PATCH 129/142] 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 d61c2de3012164f3273429ad20d246c6161eaae3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 12:35:26 -0700 Subject: [PATCH 130/142] 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 2ef5aee67c87a091dd19f42ba3da91b8ac355977 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 15:32:25 -0700 Subject: [PATCH 131/142] 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 70a8ce8394ad8b41273258404d0ddf1b7f4c3c70 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 16:01:23 -0700 Subject: [PATCH 132/142] 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 5ba5b23269340f381967669b97452eb2e089f76c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 16:28:59 -0700 Subject: [PATCH 133/142] 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 25db30bb2f34fd150417a1fae11ad4f913534131 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 16:33:32 -0700 Subject: [PATCH 134/142] 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 bcaf572322af6d308846bc7ea15dbfc90e565c60 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 17:08:41 -0700 Subject: [PATCH 135/142] 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 49b4612a38ce77bf6340f714e21dbf568e8b7323 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 17:10:15 -0700 Subject: [PATCH 136/142] 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 aca893755ce6cba1d210427b41e62a77218c65cf Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 21:32:05 -0700 Subject: [PATCH 137/142] 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 955ab04bbf278eb8d4753b299ab6177450e06eb2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Mar 2020 21:32:53 -0700 Subject: [PATCH 138/142] 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 2964dcac4d9b7377d68fbc505f3ad6646c4cd46a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 29 Mar 2020 13:54:30 -0700 Subject: [PATCH 139/142] 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 b129ffba86d7423c2b6c5e5d5e79e11c719fd560 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 29 Mar 2020 13:55:29 -0700 Subject: [PATCH 140/142] 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 acd41b3b70026c6661ab25157944a8944e68a9e0 Mon Sep 17 00:00:00 2001 From: Saptak S Date: Thu, 2 Apr 2020 04:19:50 +0530 Subject: [PATCH 141/142] 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 b57292867eee57044a71a6f19691370d00c67267 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 1 Apr 2020 17:59:08 -0700 Subject: [PATCH 142/142] Revert "Enable stacktraces of segfaults when running tests" This reverts commit b129ffba86d7423c2b6c5e5d5e79e11c719fd560. --- 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