From bf6de202b012df156335439452caa718294cb6d4 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 18 Sep 2018 11:51:32 +1000 Subject: [PATCH 001/123] Close the upload widget on reset so that it properly disappears from the Uploads window. --- onionshare_gui/receive_mode/uploads.py | 1 + 1 file changed, 1 insertion(+) diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py index 09834156..e928bd62 100644 --- a/onionshare_gui/receive_mode/uploads.py +++ b/onionshare_gui/receive_mode/uploads.py @@ -290,6 +290,7 @@ class Uploads(QtWidgets.QScrollArea): """ self.common.log('Uploads', 'reset') for upload in self.uploads.values(): + upload.close() self.uploads_layout.removeWidget(upload) self.uploads = {} From 174de57405ba176a873e1cc1cb08732dd7d207f9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 17 Sep 2018 20:55:54 -0700 Subject: [PATCH 002/123] Refactor all of the threading.Threads into QThreads, and quit them all when canceling the server. When canceling the compression thread, specifically mass a cancel message into the Web and ZipWriter objects to make the bail out on compression early --- onionshare/web.py | 25 ++++++--- onionshare_gui/mode.py | 55 +++++++++----------- onionshare_gui/onion_thread.py | 45 ---------------- onionshare_gui/share_mode/__init__.py | 43 ++++++++-------- onionshare_gui/share_mode/threads.py | 60 ++++++++++++++++++++++ onionshare_gui/threads.py | 74 +++++++++++++++++++++++++++ 6 files changed, 196 insertions(+), 106 deletions(-) delete mode 100644 onionshare_gui/onion_thread.py create mode 100644 onionshare_gui/share_mode/threads.py create mode 100644 onionshare_gui/threads.py diff --git a/onionshare/web.py b/onionshare/web.py index 10c130cb..e24e4665 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -104,6 +104,8 @@ class Web(object): self.file_info = [] self.zip_filename = None self.zip_filesize = None + self.zip_writer = None + self.cancel_compression = False self.security_headers = [ ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), @@ -534,14 +536,20 @@ class Web(object): self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) - # zip up the files and folders - z = ZipWriter(self.common, processed_size_callback=processed_size_callback) + # Zip up the files and folders + self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) + self.zip_filename = self.zip_writer.zip_filename for info in self.file_info['files']: - z.add_file(info['filename']) + self.zip_writer.add_file(info['filename']) + # Canceling early? + if self.cancel_compression: + self.zip_writer.close() + return + for info in self.file_info['dirs']: - z.add_dir(info['filename']) - z.close() - self.zip_filename = z.zip_filename + self.zip_writer.add_dir(info['filename']) + + self.zip_writer.close() self.zip_filesize = os.path.getsize(self.zip_filename) def _safe_select_jinja_autoescape(self, filename): @@ -653,6 +661,7 @@ class ZipWriter(object): """ def __init__(self, common, zip_filename=None, processed_size_callback=None): self.common = common + self.cancel_compression = False if zip_filename: self.zip_filename = zip_filename @@ -681,6 +690,10 @@ class ZipWriter(object): dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/' for dirpath, dirnames, filenames in os.walk(filename): for f in filenames: + # Canceling early? + if self.cancel_compression: + return + full_filename = os.path.join(dirpath, f) if not os.path.islink(full_filename): arc_filename = full_filename[len(dir_to_strip):] diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index 418afffd..feb2f5b6 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -17,15 +17,13 @@ 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 time -import threading from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.common import ShutdownTimer from .server_status import ServerStatus -from .onion_thread import OnionThread +from .threads import OnionThread from .widgets import Alert class Mode(QtWidgets.QWidget): @@ -56,6 +54,10 @@ class Mode(QtWidgets.QWidget): # The web object gets created in init() self.web = None + # Threads start out as None + self.onion_thread = None + self.web_thread = None + # Server status self.server_status = ServerStatus(self.common, self.qtapp, self.app) self.server_status.server_started.connect(self.start_server) @@ -138,34 +140,11 @@ class Mode(QtWidgets.QWidget): self.status_bar.clearMessage() self.server_status_label.setText('') - # Start the onion service in a new thread - def start_onion_service(self): - # Choose a port for the web app - self.app.choose_port() - - # Start http service in new thread - t = threading.Thread(target=self.web.start, args=(self.app.port, not self.common.settings.get('close_after_first_download'), self.common.settings.get('public_mode'), self.common.settings.get('slug'))) - t.daemon = True - t.start() - - # Wait for the web app slug to generate before continuing - if not self.common.settings.get('public_mode'): - while self.web.slug == None: - time.sleep(0.1) - - # Now start the onion service - try: - self.app.start_onion_service() - self.starting_server_step2.emit() - - except Exception as e: - self.starting_server_error.emit(e.args[0]) - return - self.common.log('Mode', 'start_server', 'Starting an onion thread') - self.t = OnionThread(self.common, function=start_onion_service, kwargs={'self': self}) - self.t.daemon = True - self.t.start() + self.onion_thread = OnionThread(self) + self.onion_thread.success.connect(self.starting_server_step2.emit) + self.onion_thread.error.connect(self.starting_server_error.emit) + self.onion_thread.start() def start_server_custom(self): """ @@ -243,10 +222,22 @@ class Mode(QtWidgets.QWidget): """ Cancel the server while it is preparing to start """ - if self.t: - self.t.quit() + self.cancel_server_custom() + + if self.onion_thread: + self.common.log('Mode', 'cancel_server: quitting onion thread') + self.onion_thread.quit() + if self.web_thread: + self.common.log('Mode', 'cancel_server: quitting web thread') + self.web_thread.quit() self.stop_server() + def cancel_server_custom(self): + """ + Add custom initialization here. + """ + pass + def stop_server(self): """ Stop the onionshare server. diff --git a/onionshare_gui/onion_thread.py b/onionshare_gui/onion_thread.py deleted file mode 100644 index 0a25e891..00000000 --- a/onionshare_gui/onion_thread.py +++ /dev/null @@ -1,45 +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 . -""" -from PyQt5 import QtCore - -class OnionThread(QtCore.QThread): - """ - A QThread for starting our Onion Service. - By using QThread rather than threading.Thread, we are able - to call quit() or terminate() on the startup if the user - decided to cancel (in which case do not proceed with obtaining - the Onion address and starting the web server). - """ - def __init__(self, common, function, kwargs=None): - super(OnionThread, self).__init__() - - self.common = common - - self.common.log('OnionThread', '__init__') - self.function = function - if not kwargs: - self.kwargs = {} - else: - self.kwargs = kwargs - - def run(self): - self.common.log('OnionThread', 'run') - - self.function(**self.kwargs) diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 37315bbe..d43fe99f 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.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 threading import os from PyQt5 import QtCore, QtWidgets, QtGui @@ -28,6 +27,7 @@ from onionshare.web import Web from .file_selection import FileSelection from .downloads import Downloads +from .threads import CompressThread from ..mode import Mode from ..widgets import Alert @@ -39,6 +39,9 @@ class ShareMode(Mode): """ Custom initialization for ReceiveMode. """ + # Threads start out as None + self.compress_thread = None + # Create the Web object self.web = Web(self.common, True, False) @@ -161,28 +164,13 @@ class ShareMode(Mode): self._zip_progress_bar.total_files_size = ShareMode._compute_total_size(self.filenames) self.status_bar.insertWidget(0, self._zip_progress_bar) - # Prepare the files for sending in a new thread - def finish_starting_server(self): - # Prepare files to share - def _set_processed_size(x): - if self._zip_progress_bar != None: - self._zip_progress_bar.update_processed_size_signal.emit(x) - - try: - self.web.set_file_info(self.filenames, processed_size_callback=_set_processed_size) - self.app.cleanup_filenames.append(self.web.zip_filename) - - # Only continue if the server hasn't been canceled - if self.server_status.status != self.server_status.STATUS_STOPPED: - self.starting_server_step3.emit() - self.start_server_finished.emit() - except OSError as e: - self.starting_server_error.emit(e.strerror) - return - - t = threading.Thread(target=finish_starting_server, kwargs={'self': self}) - t.daemon = True - t.start() + # prepare the files for sending in a new thread + self.compress_thread = CompressThread(self) + self.compress_thread.success.connect(self.starting_server_step3.emit) + self.compress_thread.success.connect(self.start_server_finished.emit) + self.compress_thread.error.connect(self.starting_server_error.emit) + self.server_status.server_canceled.connect(self.compress_thread.cancel) + self.compress_thread.start() def start_server_step3_custom(self): """ @@ -222,6 +210,15 @@ class ShareMode(Mode): self.update_downloads_in_progress() self.file_selection.file_list.adjustSize() + def cancel_server_custom(self): + """ + Stop the compression thread on cancel + """ + if self.compress_thread: + self.common.log('OnionShareGui', 'cancel_server: quitting compress thread') + self.compress_thread.cancel() + self.compress_thread.quit() + def handle_tor_broke_custom(self): """ Connection to Tor broke. diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py new file mode 100644 index 00000000..50789049 --- /dev/null +++ b/onionshare_gui/share_mode/threads.py @@ -0,0 +1,60 @@ +# -*- 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 + + +class CompressThread(QtCore.QThread): + """ + Compresses files to be shared + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(CompressThread, self).__init__() + self.mode = mode + self.mode.common.log('CompressThread', '__init__') + + # prepare files to share + def set_processed_size(self, x): + if self.mode._zip_progress_bar != None: + self.mode._zip_progress_bar.update_processed_size_signal.emit(x) + + def run(self): + self.mode.common.log('CompressThread', 'run') + + try: + if self.mode.web.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size): + self.success.emit() + else: + # Cancelled + pass + + self.mode.app.cleanup_filenames.append(self.mode.web.zip_filename) + except OSError as e: + self.error.emit(e.strerror) + + def cancel(self): + self.mode.common.log('CompressThread', 'cancel') + + # Let the Web and ZipWriter objects know that we're canceling compression early + self.mode.web.cancel_compression = True + if self.mode.web.zip_writer: + self.mode.web.zip_writer.cancel_compression = True diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py new file mode 100644 index 00000000..3c99d395 --- /dev/null +++ b/onionshare_gui/threads.py @@ -0,0 +1,74 @@ +# -*- 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 time +from PyQt5 import QtCore + + +class OnionThread(QtCore.QThread): + """ + Starts the onion service, and waits for it to finish + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(OnionThread, self).__init__() + self.mode = mode + self.mode.common.log('OnionThread', '__init__') + + # allow this thread to be terminated + self.setTerminationEnabled() + + def run(self): + self.mode.common.log('OnionThread', 'run') + + try: + self.mode.app.start_onion_service() + self.success.emit() + + except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e: + self.error.emit(e.args[0]) + return + + self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download') + + # start onionshare http service in new thread + self.mode.web_thread = WebThread(self.mode) + self.mode.web_thread.start() + + # wait for modules in thread to load, preventing a thread-related cx_Freeze crash + time.sleep(0.2) + + +class WebThread(QtCore.QThread): + """ + Starts the web service + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(WebThread, self).__init__() + self.mode = mode + self.mode.common.log('WebThread', '__init__') + + 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('slug')) From 72f76bf659a0aeba1125f2a3d322300308633055 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 13:39:09 -0700 Subject: [PATCH 003/123] We shouldn't call CompressThread.cancel() there because it's already called in a signal --- onionshare_gui/share_mode/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index d43fe99f..65ce1d52 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -216,7 +216,6 @@ class ShareMode(Mode): """ if self.compress_thread: self.common.log('OnionShareGui', 'cancel_server: quitting compress thread') - self.compress_thread.cancel() self.compress_thread.quit() def handle_tor_broke_custom(self): From c52c846227d892e0a68d15c3db7d09390cb3ada6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 13:42:13 -0700 Subject: [PATCH 004/123] Make Web.set_file_info return False on cancel --- onionshare/web.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index e24e4665..dc0effdb 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -544,13 +544,15 @@ class Web(object): # Canceling early? if self.cancel_compression: self.zip_writer.close() - return + return False for info in self.file_info['dirs']: - self.zip_writer.add_dir(info['filename']) + if not self.zip_writer.add_dir(info['filename']): + return False self.zip_writer.close() self.zip_filesize = os.path.getsize(self.zip_filename) + return True def _safe_select_jinja_autoescape(self, filename): if filename is None: @@ -692,8 +694,8 @@ class ZipWriter(object): for f in filenames: # Canceling early? if self.cancel_compression: - return - + return False + full_filename = os.path.join(dirpath, f) if not os.path.islink(full_filename): arc_filename = full_filename[len(dir_to_strip):] @@ -701,6 +703,8 @@ class ZipWriter(object): self._size += os.path.getsize(full_filename) self.processed_size_callback(self._size) + return True + def close(self): """ Close the zip archive. From 0234ff5f37eefb333e50fec102462a2e3d699dbc Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 16:28:54 -0700 Subject: [PATCH 005/123] Set self.cancel_compression to false in the set_file_info() function instead of Web's constructor, so it gets reset every time --- onionshare/web.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/onionshare/web.py b/onionshare/web.py index dc0effdb..38ad398e 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -105,7 +105,6 @@ class Web(object): self.zip_filename = None self.zip_filesize = None self.zip_writer = None - self.cancel_compression = False self.security_headers = [ ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), @@ -518,6 +517,8 @@ class Web(object): page will need to display. This includes zipping up the file in order to get the zip file's name and size. """ + self.cancel_compression = False + # build file info list self.file_info = {'files': [], 'dirs': []} for filename in filenames: From d63808f419df38cad9e9d9bd96f80778945f24c2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 17:44:54 -0700 Subject: [PATCH 006/123] Import onion exceptions that were missing --- onionshare_gui/threads.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 3c99d395..f4acc5e1 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -20,6 +20,8 @@ along with this program. If not, see . import time from PyQt5 import QtCore +from onionshare.onion import * + class OnionThread(QtCore.QThread): """ From c08f6f3db1b9d0ea545a83949b55958724537861 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 19 Sep 2018 10:57:12 +1000 Subject: [PATCH 007/123] #704 wrap the upload filename label if too long --- onionshare_gui/receive_mode/uploads.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py index 1aebe902..e04e0269 100644 --- a/onionshare_gui/receive_mode/uploads.py +++ b/onionshare_gui/receive_mode/uploads.py @@ -19,6 +19,7 @@ along with this program. If not, see . """ import os import subprocess +import textwrap from datetime import datetime from PyQt5 import QtCore, QtWidgets, QtGui @@ -305,10 +306,7 @@ class Uploads(QtWidgets.QScrollArea): try: for upload in self.uploads.values(): for item in upload.files.values(): - if item.filename_label_width > width: - item.filename_label.setText(item.filename[:25] + '[...]') - item.adjustSize() - if width > item.filename_label_width: - item.filename_label.setText(item.filename) + item.filename_label.setText(textwrap.fill(item.filename, 30)) + item.adjustSize() except: pass From 28674bf023b15f34900a53ebda72352f2be58cec Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 19 Sep 2018 14:37:07 +1000 Subject: [PATCH 008/123] Add 'clear history' button to downloads and uploads windows --- onionshare_gui/receive_mode/uploads.py | 8 ++++++++ onionshare_gui/share_mode/downloads.py | 7 +++++++ share/locale/en.json | 1 + 3 files changed, 16 insertions(+) diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py index d52626c5..1c9afdd2 100644 --- a/onionshare_gui/receive_mode/uploads.py +++ b/onionshare_gui/receive_mode/uploads.py @@ -232,6 +232,10 @@ class Uploads(QtWidgets.QScrollArea): uploads_label = QtWidgets.QLabel(strings._('gui_uploads', True)) uploads_label.setStyleSheet(self.common.css['downloads_uploads_label']) self.no_uploads_label = QtWidgets.QLabel(strings._('gui_no_uploads', True)) + self.clear_history_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) + self.clear_history_button.clicked.connect(self.reset) + self.clear_history_button.hide() + self.uploads_layout = QtWidgets.QVBoxLayout() @@ -239,6 +243,7 @@ class Uploads(QtWidgets.QScrollArea): layout = QtWidgets.QVBoxLayout() layout.addWidget(uploads_label) layout.addWidget(self.no_uploads_label) + layout.addWidget(self.clear_history_button) layout.addLayout(self.uploads_layout) layout.addStretch() widget.setLayout(layout) @@ -257,6 +262,8 @@ class Uploads(QtWidgets.QScrollArea): self.common.log('Uploads', 'add', 'upload_id: {}, content_length: {}'.format(upload_id, content_length)) # Hide the no_uploads_label self.no_uploads_label.hide() + # Show the clear_history_button + self.clear_history_button.show() # Add it to the list upload = Upload(self.common, upload_id, content_length) @@ -299,6 +306,7 @@ class Uploads(QtWidgets.QScrollArea): self.uploads = {} self.no_uploads_label.show() + self.clear_history_button.hide() self.resize(self.sizeHint()) def resizeEvent(self, event): diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index 538ddfd0..9aeef576 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -102,6 +102,9 @@ class Downloads(QtWidgets.QScrollArea): downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True)) downloads_label.setStyleSheet(self.common.css['downloads_uploads_label']) self.no_downloads_label = QtWidgets.QLabel(strings._('gui_no_downloads', True)) + self.clear_history_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) + self.clear_history_button.clicked.connect(self.reset) + self.clear_history_button.hide() self.downloads_layout = QtWidgets.QVBoxLayout() @@ -109,6 +112,7 @@ class Downloads(QtWidgets.QScrollArea): layout = QtWidgets.QVBoxLayout() layout.addWidget(downloads_label) layout.addWidget(self.no_downloads_label) + layout.addWidget(self.clear_history_button) layout.addLayout(self.downloads_layout) layout.addStretch() widget.setLayout(layout) @@ -126,6 +130,8 @@ class Downloads(QtWidgets.QScrollArea): """ # Hide the no_downloads_label self.no_downloads_label.hide() + # Show the clear_history_button + self.clear_history_button.show() # Add it to the list download = Download(self.common, download_id, total_bytes) @@ -154,4 +160,5 @@ class Downloads(QtWidgets.QScrollArea): self.downloads = {} self.no_downloads_label.show() + self.clear_history_button.hide() self.resize(self.sizeHint()) diff --git a/share/locale/en.json b/share/locale/en.json index 7d3daba8..250524be 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -181,6 +181,7 @@ "gui_uploads": "Upload History", "gui_uploads_window_tooltip": "Show/hide uploads", "gui_no_uploads": "No uploads yet.", + "gui_clear_history": "Clear history", "gui_upload_in_progress": "Upload Started {}", "gui_upload_finished_range": "Uploaded {} to {}", "gui_upload_finished": "Uploaded {}", From 20c97a61ab74ebbf33bb20a27082b0068539b7c8 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 19 Sep 2018 14:40:30 +1000 Subject: [PATCH 009/123] Send the public_mode argument to web.start in the WebThread --- onionshare_gui/threads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index f4acc5e1..9129600d 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -73,4 +73,4 @@ 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('slug')) + self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.common.settings.get('slug')) From abb270834b127feb0e283b357fbbce3afe3fed23 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 19 Sep 2018 14:53:33 +1000 Subject: [PATCH 010/123] Log the public_mode flag as well --- onionshare/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare/web.py b/onionshare/web.py index 38ad398e..067c5e07 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -619,7 +619,7 @@ class Web(object): """ Start the flask web server. """ - self.common.log('Web', 'start', 'port={}, stay_open={}, persistent_slug={}'.format(port, stay_open, persistent_slug)) + self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, persistent_slug={}'.format(port, stay_open, public_mode, persistent_slug)) if not public_mode: self.generate_slug(persistent_slug) From 8b704a007050048a0e51d90e89af3e15e7b218b4 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 19 Sep 2018 15:07:04 +1000 Subject: [PATCH 011/123] Start the WebThread earlier than the Onion service. This gives it a chance to generate its slug before the Onion Service finishes starting up, which can otherwise lead to a crash --- onionshare_gui/threads.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index f4acc5e1..2af0120d 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -41,14 +41,6 @@ class OnionThread(QtCore.QThread): def run(self): self.mode.common.log('OnionThread', 'run') - try: - self.mode.app.start_onion_service() - self.success.emit() - - except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e: - self.error.emit(e.args[0]) - return - self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download') # start onionshare http service in new thread @@ -58,6 +50,14 @@ class OnionThread(QtCore.QThread): # wait for modules in thread to load, preventing a thread-related cx_Freeze crash time.sleep(0.2) + try: + self.mode.app.start_onion_service() + self.success.emit() + + except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e: + self.error.emit(e.args[0]) + return + class WebThread(QtCore.QThread): """ From fc7afecb7bd1e4da0e8b419dd58097df61e6453c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 18:14:42 -0700 Subject: [PATCH 012/123] Choose a port *before* starting the web service --- onionshare_gui/threads.py | 1 + 1 file changed, 1 insertion(+) diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 2af0120d..83114aea 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -73,4 +73,5 @@ class WebThread(QtCore.QThread): def run(self): self.mode.common.log('WebThread', 'run') + self.mode.app.choose_port() self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('slug')) From 59003635a2c736cda1323e48935a25532ffeda56 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 18:50:42 -0700 Subject: [PATCH 013/123] Add a horizontal layout wrapper around the vertical layout, in order to optionally add horizontal widgets to Modes --- onionshare_gui/mode.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index feb2f5b6..7ae8d315 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -78,7 +78,11 @@ class Mode(QtWidgets.QWidget): # Layout self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.primary_action) - self.setLayout(self.layout) + + self.horizontal_layout_wrapper = QtWidgets.QHBoxLayout() + self.horizontal_layout_wrapper.addLayout(self.layout) + + self.setLayout(self.horizontal_layout_wrapper) def init(self): """ From 499f7b16381fb2f1a8ec3bb50d7128f9c0062d9a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 19:00:12 -0700 Subject: [PATCH 014/123] Always show uploads and downloads --- onionshare_gui/mode.py | 4 ++++ onionshare_gui/receive_mode/__init__.py | 3 ++- onionshare_gui/receive_mode/uploads.py | 1 - onionshare_gui/share_mode/__init__.py | 1 + onionshare_gui/share_mode/downloads.py | 1 - 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index 7ae8d315..4c21de76 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -78,6 +78,10 @@ class Mode(QtWidgets.QWidget): # Layout self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.primary_action) + # Hack to allow a minimum width on self.layout + min_width_widget = QtWidgets.QWidget() + min_width_widget.setMinimumWidth(450) + self.layout.addWidget(min_width_widget) self.horizontal_layout_wrapper = QtWidgets.QHBoxLayout() self.horizontal_layout_wrapper.addLayout(self.layout) diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index d414f3b0..0f3105e1 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -46,7 +46,7 @@ class ReceiveMode(Mode): self.server_status.web = self.web self.server_status.update() - # Downloads + # Uploads self.uploads = Uploads(self.common) self.uploads_in_progress = 0 self.uploads_completed = 0 @@ -86,6 +86,7 @@ class ReceiveMode(Mode): # Layout self.layout.insertWidget(0, self.receive_info) self.layout.insertWidget(0, self.info_widget) + self.horizontal_layout_wrapper.addWidget(self.uploads) def get_stop_server_shutdown_timeout_text(self): """ diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py index f77cdbf4..48574cc7 100644 --- a/onionshare_gui/receive_mode/uploads.py +++ b/onionshare_gui/receive_mode/uploads.py @@ -222,7 +222,6 @@ class Uploads(QtWidgets.QScrollArea): self.setWindowTitle(strings._('gui_uploads', True)) self.setWidgetResizable(True) - self.setMaximumHeight(600) self.setMinimumHeight(150) self.setMinimumWidth(350) self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 65ce1d52..28881220 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -116,6 +116,7 @@ class ShareMode(Mode): # Layout self.layout.insertLayout(0, self.file_selection) self.layout.insertWidget(0, self.info_widget) + self.horizontal_layout_wrapper.addWidget(self.downloads) # Always start with focus on file selection self.file_selection.setFocus() diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index 9aeef576..a34796f1 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -91,7 +91,6 @@ class Downloads(QtWidgets.QScrollArea): self.setWindowTitle(strings._('gui_downloads', True)) self.setWidgetResizable(True) - self.setMaximumHeight(600) self.setMinimumHeight(150) self.setMinimumWidth(350) self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) From 1314ddf1bec3a87c1bdbdb27412677194102aeea Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 19:36:16 -0700 Subject: [PATCH 015/123] Remove upload/download toggle buttons --- onionshare_gui/mode.py | 1 + onionshare_gui/receive_mode/__init__.py | 19 ------------------- onionshare_gui/share_mode/__init__.py | 19 ------------------- share/images/download_window_gray.png | Bin 440 -> 0 bytes share/images/download_window_green.png | Bin 761 -> 0 bytes share/images/upload_window_gray.png | Bin 298 -> 0 bytes share/images/upload_window_green.png | Bin 483 -> 0 bytes 7 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 share/images/download_window_gray.png delete mode 100644 share/images/download_window_green.png delete mode 100644 share/images/upload_window_gray.png delete mode 100644 share/images/upload_window_green.png diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index 4c21de76..dad58bf3 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -78,6 +78,7 @@ class Mode(QtWidgets.QWidget): # Layout self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.primary_action) + self.layout.addStretch() # Hack to allow a minimum width on self.layout min_width_widget = QtWidgets.QWidget() min_width_widget.setMinimumWidth(450) diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index 0f3105e1..74b67e32 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -53,12 +53,6 @@ class ReceiveMode(Mode): self.new_upload = False # For scrolling to the bottom of the uploads list # Information about share, and show uploads button - self.info_show_uploads = QtWidgets.QToolButton() - self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_gray.png'))) - self.info_show_uploads.setCheckable(True) - self.info_show_uploads.toggled.connect(self.uploads_toggled) - self.info_show_uploads.setToolTip(strings._('gui_uploads_window_tooltip', True)) - self.info_in_progress_uploads_count = QtWidgets.QLabel() self.info_in_progress_uploads_count.setStyleSheet(self.common.css['mode_info_label']) @@ -72,7 +66,6 @@ class ReceiveMode(Mode): self.info_layout.addStretch() self.info_layout.addWidget(self.info_in_progress_uploads_count) self.info_layout.addWidget(self.info_completed_uploads_count) - self.info_layout.addWidget(self.info_show_uploads) self.info_widget = QtWidgets.QWidget() self.info_widget.setLayout(self.info_layout) @@ -189,7 +182,6 @@ class ReceiveMode(Mode): self.uploads_in_progress = 0 self.update_uploads_completed() self.update_uploads_in_progress() - self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_gray.png'))) self.uploads.reset() def update_uploads_completed(self): @@ -211,7 +203,6 @@ class ReceiveMode(Mode): image = self.common.get_resource_path('images/share_in_progress_none.png') else: image = self.common.get_resource_path('images/share_in_progress.png') - self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_green.png'))) self.info_in_progress_uploads_count.setText(' {1:d}'.format(image, self.uploads_in_progress)) self.info_in_progress_uploads_count.setToolTip(strings._('info_in_progress_uploads_tooltip', True).format(self.uploads_in_progress)) @@ -226,13 +217,3 @@ class ReceiveMode(Mode): # Resize window self.adjustSize() - - def uploads_toggled(self, checked): - """ - When the 'Show/hide uploads' button is toggled, show or hide the uploads window. - """ - self.common.log('ReceiveMode', 'toggle_uploads') - if checked: - self.uploads.show() - else: - self.uploads.hide() diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 28881220..a9c6e8d7 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -79,12 +79,6 @@ class ShareMode(Mode): self.info_label = QtWidgets.QLabel() self.info_label.setStyleSheet(self.common.css['mode_info_label']) - self.info_show_downloads = QtWidgets.QToolButton() - self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png'))) - self.info_show_downloads.setCheckable(True) - self.info_show_downloads.toggled.connect(self.downloads_toggled) - self.info_show_downloads.setToolTip(strings._('gui_downloads_window_tooltip', True)) - self.info_in_progress_downloads_count = QtWidgets.QLabel() self.info_in_progress_downloads_count.setStyleSheet(self.common.css['mode_info_label']) @@ -99,7 +93,6 @@ class ShareMode(Mode): self.info_layout.addStretch() self.info_layout.addWidget(self.info_in_progress_downloads_count) self.info_layout.addWidget(self.info_completed_downloads_count) - self.info_layout.addWidget(self.info_show_downloads) self.info_widget = QtWidgets.QWidget() self.info_widget.setLayout(self.info_layout) @@ -316,16 +309,6 @@ class ShareMode(Mode): # Resize window self.adjustSize() - def downloads_toggled(self, checked): - """ - When the 'Show/hide downloads' button is toggled, show or hide the downloads window. - """ - self.common.log('ShareMode', 'toggle_downloads') - if checked: - self.downloads.show() - else: - self.downloads.hide() - def reset_info_counters(self): """ Set the info counters back to zero. @@ -334,7 +317,6 @@ class ShareMode(Mode): self.downloads_in_progress = 0 self.update_downloads_completed() self.update_downloads_in_progress() - self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png'))) self.downloads.reset() def update_downloads_completed(self): @@ -356,7 +338,6 @@ class ShareMode(Mode): image = self.common.get_resource_path('images/share_in_progress_none.png') else: image = self.common.get_resource_path('images/share_in_progress.png') - self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_green.png'))) self.info_in_progress_downloads_count.setText(' {1:d}'.format(image, self.downloads_in_progress)) self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(self.downloads_in_progress)) diff --git a/share/images/download_window_gray.png b/share/images/download_window_gray.png deleted file mode 100644 index bf9c168ef72f8c49afdaa4ba0be7eac407d072db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$0wn*`OvwRKEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZEE?e4U2r-o{kZwy(p zCYaOiKy8DKpM!wl(NmY)1w?eWcv^9@Dn_`syoq|kUUbUxumPh|b)L+Ft2_Jcgo3TS ze0`UmGnvut!*KA_CJt`Fw6h+ES533jo&E88eAI^bhi2+BI=(!zIk0JA^v|TM!*dy5 zb}#z&?DWLaKQDF$eO>zT!igih{f{53S{EwiD)?yex_lKsRk@HGW%EzIoyAe%B)wI( zGVR5s2;NsWH=8!s*EMM$P<85`5A=&_iEBhjN@7W>RdP`(kYX@0FtpG$Fw!+L4>2^e zGPSfaG|)CQv@$SApZeeliiX_$l+3hBbPX0(rsfb0JDxKO0W~mqy85}Sb4q9e0MrVb Ay8r+H diff --git a/share/images/download_window_green.png b/share/images/download_window_green.png deleted file mode 100644 index 8f9a899b1d3b4535755ae69894e61a3a27c8c4a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 761 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1OSkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#N0|O&>fKP}kP(j!s{ZaJ) z|Nkp?lWzfoBfKQYFBlk-$A0`crr`7Imrua&-+_UD{sgh{#r^pc&&Q^@o;*sX2&I%JiVDDj$bks6%!K{785nPwEFU?lP6D}IC<*w zYUi_yKvBY&kSaCQELJ2)*&9;qb~sPuXnZSl@hR zZ*tI(=`3*NN(72z;YDq^~Y?{>8a1}n4b+yV@msCD=RF}|}tqkgn+WyjaZh4ciur=E^o7HPh zS?PMrnRSDW`FGr%r-lbFL|pz~Ri5Lbabv^F3#}q^i^J`*o*a?Chc6!YYg-TgQu&X%Q~loCIHg9Y|H=v diff --git a/share/images/upload_window_green.png b/share/images/upload_window_green.png deleted file mode 100644 index 652ddaffab94dc23d69039aaf2099bf00585d5fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 483 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uer*=z{ncl6XF_nNPmQ*|NsBz#!j9G z3>U_dAirP+hi5m^K%69RcNc~ZR#^`qhqJ&VvY3H^TNs2H8D`Cq01C2~c>21sKVoHN zFqe6gFj)X76z=Kb7{YNq`NOAgAHRO)|G>?|#h17ty@A=Et(fIFpOd7da?2Jc=dFo3 zQv(zNS~o0BO>uU9`b2a=(4vJ4lL~DAEL_M`z*nB3!9VA)oYX1C=LSZGn;SA1X0!9? U^LuFs01boFyt=akR{0Du#)E&u=k From 871135cc750b1d5ec3cbf7818eff96ccc3989652 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 19:38:29 -0700 Subject: [PATCH 016/123] Only add a stretch at the bottom of the layout in receive mode, not share mode --- onionshare_gui/mode.py | 1 - onionshare_gui/receive_mode/__init__.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index dad58bf3..4c21de76 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -78,7 +78,6 @@ class Mode(QtWidgets.QWidget): # Layout self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.primary_action) - self.layout.addStretch() # Hack to allow a minimum width on self.layout min_width_widget = QtWidgets.QWidget() min_width_widget.setMinimumWidth(450) diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index 74b67e32..8712653b 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -79,6 +79,7 @@ class ReceiveMode(Mode): # Layout self.layout.insertWidget(0, self.receive_info) self.layout.insertWidget(0, self.info_widget) + self.layout.addStretch() self.horizontal_layout_wrapper.addWidget(self.uploads) def get_stop_server_shutdown_timeout_text(self): From beeebce6312cee691ef7e2320459396d0312e0e6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 19:39:59 -0700 Subject: [PATCH 017/123] Set minimum width for whole application --- onionshare_gui/onionshare_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index b63119bb..07c82e50 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -55,7 +55,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setWindowTitle('OnionShare') self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) - self.setMinimumWidth(450) + self.setMinimumWidth(850) # Load settings self.config = config From 5a96bcc77b7fced58491b1ad908ea610e63a8f11 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 20:17:23 -0700 Subject: [PATCH 018/123] Remove adjustSize that was causing issues --- onionshare_gui/onionshare_gui.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 07c82e50..8b61a18e 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -205,9 +205,6 @@ class OnionShareGui(QtWidgets.QMainWindow): self.update_server_status_indicator() - # Wait 1ms for the event loop to finish, then adjust size - QtCore.QTimer.singleShot(1, self.adjustSize) - def share_mode_clicked(self): if self.mode != self.MODE_SHARE: self.common.log('OnionShareGui', 'share_mode_clicked') From dbae5016895f1e29a15188aed4f21d0afb3350b4 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 21:44:37 -0700 Subject: [PATCH 019/123] Remove obsolete strings (#770) --- share/locale/en.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/share/locale/en.json b/share/locale/en.json index 250524be..e6b2b2c0 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -48,7 +48,6 @@ "gui_copy_url": "Copy Address", "gui_copy_hidservauth": "Copy HidServAuth", "gui_downloads": "Download History", - "gui_downloads_window_tooltip": "Show/hide downloads", "gui_no_downloads": "No downloads yet.", "gui_canceled": "Canceled", "gui_copied_url_title": "Copied OnionShare address", @@ -179,7 +178,6 @@ "systray_download_page_loaded_message": "A user loaded the download page", "systray_upload_page_loaded_message": "A user loaded the upload page", "gui_uploads": "Upload History", - "gui_uploads_window_tooltip": "Show/hide uploads", "gui_no_uploads": "No uploads yet.", "gui_clear_history": "Clear history", "gui_upload_in_progress": "Upload Started {}", From 324538bdd36382919d7f01c99b16b9b82b56603f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 09:14:56 -0700 Subject: [PATCH 020/123] When there is only 1 file being shared, don't zip it --- onionshare/__init__.py | 4 +- onionshare/web.py | 68 ++++++++++++++++----------- onionshare_gui/share_mode/threads.py | 2 +- screenshots/server.png | Bin 45923 -> 0 bytes share/templates/send.html | 24 +++++----- 5 files changed, 55 insertions(+), 43 deletions(-) delete mode 100644 screenshots/server.png diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 51210b6b..e04836b7 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -120,13 +120,13 @@ def main(cwd=None): print(strings._("preparing_files")) try: web.set_file_info(filenames) - app.cleanup_filenames.append(web.zip_filename) + app.cleanup_filenames.append(web.download_filename) except OSError as e: print(e.strerror) sys.exit(1) # Warn about sending large files over Tor - if web.zip_filesize >= 157286400: # 150mb + if web.download_filesize >= 157286400: # 150mb print('') print(strings._("large_filesize")) print('') diff --git a/onionshare/web.py b/onionshare/web.py index 067c5e07..2575230f 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -102,8 +102,9 @@ class Web(object): # Information about the file self.file_info = [] - self.zip_filename = None - self.zip_filesize = None + self.is_zipped = False + self.download_filename = None + self.download_filesize = None self.zip_writer = None self.security_headers = [ @@ -182,17 +183,19 @@ class Web(object): 'send.html', slug=self.slug, file_info=self.file_info, - filename=os.path.basename(self.zip_filename), - filesize=self.zip_filesize, - filesize_human=self.common.human_readable_filesize(self.zip_filesize))) + filename=os.path.basename(self.download_filename), + filesize=self.download_filesize, + filesize_human=self.common.human_readable_filesize(self.download_filesize), + is_zipped=self.is_zipped)) else: # If download is allowed to continue, serve download page r = make_response(render_template( 'send.html', file_info=self.file_info, - filename=os.path.basename(self.zip_filename), - filesize=self.zip_filesize, - filesize_human=self.common.human_readable_filesize(self.zip_filesize))) + filename=os.path.basename(self.download_filename), + filesize=self.download_filesize, + filesize_human=self.common.human_readable_filesize(self.download_filesize), + is_zipped=self.is_zipped)) return self.add_security_headers(r) @self.app.route("//download") @@ -231,8 +234,8 @@ class Web(object): 'id': download_id} ) - dirname = os.path.dirname(self.zip_filename) - basename = os.path.basename(self.zip_filename) + dirname = os.path.dirname(self.download_filename) + basename = os.path.basename(self.download_filename) def generate(): # The user hasn't canceled the download @@ -244,7 +247,7 @@ class Web(object): chunk_size = 102400 # 100kb - fp = open(self.zip_filename, 'rb') + fp = open(self.download_filename, 'rb') self.done = False canceled = False while not self.done: @@ -264,7 +267,7 @@ class Web(object): # tell GUI the progress downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / self.zip_filesize) * 100 + percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) if not self.gui_mode or self.common.platform == 'Linux' or self.common.platform == 'BSD': @@ -308,7 +311,7 @@ class Web(object): pass r = Response(generate()) - r.headers.set('Content-Length', self.zip_filesize) + r.headers.set('Content-Length', self.download_filesize) r.headers.set('Content-Disposition', 'attachment', filename=basename) r = self.add_security_headers(r) # guess content type @@ -517,8 +520,9 @@ class Web(object): page will need to display. This includes zipping up the file in order to get the zip file's name and size. """ + self.common.log("Web", "set_file_info") self.cancel_compression = False - + # build file info list self.file_info = {'files': [], 'dirs': []} for filename in filenames: @@ -537,22 +541,30 @@ class Web(object): self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) - # Zip up the files and folders - self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) - self.zip_filename = self.zip_writer.zip_filename - for info in self.file_info['files']: - self.zip_writer.add_file(info['filename']) - # Canceling early? - if self.cancel_compression: - self.zip_writer.close() - return False + # Check if there's only 1 file and no folders + if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: + self.is_zipped = False + self.download_filename = self.file_info['files'][0]['filename'] + self.download_filesize = self.file_info['files'][0]['size'] + else: + # Zip up the files and folders + self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) + self.download_filename = self.zip_writer.zip_filename + for info in self.file_info['files']: + self.zip_writer.add_file(info['filename']) + # Canceling early? + if self.cancel_compression: + self.zip_writer.close() + return False - for info in self.file_info['dirs']: - if not self.zip_writer.add_dir(info['filename']): - return False + for info in self.file_info['dirs']: + if not self.zip_writer.add_dir(info['filename']): + return False + + self.zip_writer.close() + self.download_filesize = os.path.getsize(self.download_filename) + self.is_zipped = True - self.zip_writer.close() - self.zip_filesize = os.path.getsize(self.zip_filename) return True def _safe_select_jinja_autoescape(self, filename): diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py index 50789049..9cda76b1 100644 --- a/onionshare_gui/share_mode/threads.py +++ b/onionshare_gui/share_mode/threads.py @@ -47,7 +47,7 @@ class CompressThread(QtCore.QThread): # Cancelled pass - self.mode.app.cleanup_filenames.append(self.mode.web.zip_filename) + self.mode.app.cleanup_filenames.append(self.mode.web.download_filename) except OSError as e: self.error.emit(e.strerror) diff --git a/screenshots/server.png b/screenshots/server.png deleted file mode 100644 index 8bdf29719f9e03b139b323383fdb649eada21e94..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45923 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 Bxw8NO diff --git a/share/templates/send.html b/share/templates/send.html index df1d3563..e7e1fde0 100644 --- a/share/templates/send.html +++ b/share/templates/send.html @@ -10,18 +10,18 @@

- - -

OnionShare

+
+
    +
  • Total size: {{ filesize_human }} {% if is_zipped %} (compressed){% endif %}
  • + {% if slug %} +
  • Download Files
  • + {% else %} +
  • Download Files
  • + {% endif %} +
+
+ +

OnionShare

From 696665815e5c93eeff196bfd46acbeb6f978dfc5 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 09:26:11 -0700 Subject: [PATCH 021/123] Make fedora and debian dependencies get added to packages built --- BUILD.md | 4 ++-- install/build_rpm.sh | 2 +- stdeb.cfg | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/BUILD.md b/BUILD.md index 51f5cadd..1feedf49 100644 --- a/BUILD.md +++ b/BUILD.md @@ -11,11 +11,11 @@ cd onionshare Install the needed dependencies: -For Debian-like distros: `apt install -y build-essential fakeroot python3-all python3-stdeb dh-python python3-flask python3-stem python3-pyqt5 python-nautilus python3-pytest tor obfs4proxy python3-cryptography python3-crypto python3-nacl python3-pip python3-socks python3-sha3` +For Debian-like distros: `apt install -y python3-flask python3-stem python3-pyqt5 python3-cryptography python3-crypto python3-nacl python3-socks python-nautilus tor obfs4proxy python3-pytest build-essential fakeroot python3-all python3-stdeb dh-python` On some older versions of Debian you may need to install pysha3 with `pip3 install pysha3` if python3-sha3 is not available. -For Fedora-like distros: `dnf install -y rpm-build python3-flask python3-stem python3-qt5 python3-pytest nautilus-python tor obfs4 python3-pynacl python3-cryptography python3-crypto python3-pip python3-pysocks` +For Fedora-like distros: `dnf install -y python3-flask python3-stem python3-qt5 python3-pynacl python3-cryptography python3-crypto python3-pysocks nautilus-python tor obfs4 python3-pytest rpm-build` After that you can try both the CLI and the GUI version of OnionShare: diff --git a/install/build_rpm.sh b/install/build_rpm.sh index c103262c..3f7a68ac 100755 --- a/install/build_rpm.sh +++ b/install/build_rpm.sh @@ -9,7 +9,7 @@ VERSION=`cat share/version.txt` rm -r build dist >/dev/null 2>&1 # build binary package -python3 setup.py bdist_rpm --requires="python3-flask, python3-stem, python3-qt5, nautilus-python, tor, obfs4" +python3 setup.py bdist_rpm --requires="python3-flask, python3-stem, python3-qt5, python3-pynacl, python3-cryptography, python3-crypto, python3-pysocks, nautilus-python, tor, obfs4" # install it echo "" diff --git a/stdeb.cfg b/stdeb.cfg index e190fe8b..2fc3d3bf 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -1,6 +1,6 @@ [DEFAULT] Package3: onionshare -Depends3: python3-flask, python3-stem, python3-pyqt5, python-nautilus, tor, obfs4proxy -Build-Depends: python3-pytest, python3-flask, python3-stem, python3-pyqt5 +Depends3: python3-flask, python3-stem, python3-pyqt5, python3-cryptography, python3-crypto, python3-nacl, python3-socks, python-nautilus, tor, obfs4proxy +Build-Depends: python3-pytest, python3-flask, python3-stem, python3-pyqt5, python3-cryptography, python3-crypto, python3-nacl, python3-socks, python-nautilus, tor, obfs4proxy Suite: bionic X-Python3-Version: >= 3.6 From 12a5b68d16084499d79f3338ce4dfb341773c1fa Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 09:30:44 -0700 Subject: [PATCH 022/123] Also package the new python modules --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1a665085..a36fecab 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,12 @@ setup( description=description, long_description=long_description, author=author, author_email=author_email, url=url, license=license, keywords=keywords, - packages=['onionshare', 'onionshare_gui'], + packages=[ + 'onionshare', + 'onionshare_gui', + 'onionshare_gui.share_mode', + 'onionshare_gui.receive_mode' + ], include_package_data=True, scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'], data_files=data_files From ea938e243985aa64848ec726fdf21d926b7cb222 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 12:25:07 -0700 Subject: [PATCH 023/123] Change more references to web.zip_filesize to be refer to web.download_filesize --- onionshare_gui/share_mode/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index a9c6e8d7..aec32305 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -177,7 +177,7 @@ class ShareMode(Mode): self._zip_progress_bar = None # Warn about sending large files over Tor - if self.web.zip_filesize >= 157286400: # 150mb + if self.web.download_filesize >= 157286400: # 150mb self.filesize_warning.setText(strings._("large_filesize", True)) self.filesize_warning.show() @@ -229,7 +229,7 @@ class ShareMode(Mode): """ Handle REQUEST_STARTED event. """ - self.downloads.add(event["data"]["id"], self.web.zip_filesize) + self.downloads.add(event["data"]["id"], self.web.download_filesize) self.downloads_in_progress += 1 self.update_downloads_in_progress() @@ -242,7 +242,7 @@ class ShareMode(Mode): self.downloads.update(event["data"]["id"], event["data"]["bytes"]) # Is the download complete? - if event["data"]["bytes"] == self.web.zip_filesize: + if event["data"]["bytes"] == self.web.download_filesize: self.system_tray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True)) # Update the total 'completed downloads' info From 5bc8e0a5e55bfaa5b6f4b06702d3537fb5c7a35a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 23:09:14 -0700 Subject: [PATCH 024/123] Smoothly quit when Ctrl-C is pressed --- onionshare_gui/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 13f0e8c7..99db635a 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -18,7 +18,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ from __future__ import division -import os, sys, platform, argparse +import os +import sys +import platform +import argparse +import signal from .widgets import Alert from PyQt5 import QtCore, QtWidgets @@ -58,6 +62,10 @@ def main(): strings.load_strings(common) print(strings._('version_string').format(common.version)) + # Allow Ctrl-C to smoothly quit the program instead of throwing an exception + # https://stackoverflow.com/questions/42814093/how-to-handle-ctrlc-in-python-app-with-pyqt + signal.signal(signal.SIGINT, signal.SIG_DFL) + # Start the Qt app global qtapp qtapp = Application(common) From 81fa5e052cf4e959670f02f738c35c1148857b91 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 23:18:17 -0700 Subject: [PATCH 025/123] Only add the download_filename to cleanup_filenames (which get deleted) if the file is zipped up. Otherwise, OnionShare deletes the original file --- onionshare/__init__.py | 3 ++- onionshare_gui/share_mode/threads.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index e04836b7..2f57ccf2 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -120,7 +120,8 @@ def main(cwd=None): print(strings._("preparing_files")) try: web.set_file_info(filenames) - app.cleanup_filenames.append(web.download_filename) + if web.is_zipped: + app.cleanup_filenames.append(web.download_filename) except OSError as e: print(e.strerror) sys.exit(1) diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py index 9cda76b1..dc43bf0a 100644 --- a/onionshare_gui/share_mode/threads.py +++ b/onionshare_gui/share_mode/threads.py @@ -47,7 +47,8 @@ class CompressThread(QtCore.QThread): # Cancelled pass - self.mode.app.cleanup_filenames.append(self.mode.web.download_filename) + if self.mode.web.is_zipped: + self.mode.app.cleanup_filenames.append(self.mode.web.download_filename) except OSError as e: self.error.emit(e.strerror) From 603be8a02c9984c5b9f97394b3038916bc56456a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 23:31:52 -0700 Subject: [PATCH 026/123] Make web a module, so I can split it into multiple files --- onionshare/{web.py => web/__init__.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename onionshare/{web.py => web/__init__.py} (99%) diff --git a/onionshare/web.py b/onionshare/web/__init__.py similarity index 99% rename from onionshare/web.py rename to onionshare/web/__init__.py index 2575230f..5a7b297f 100644 --- a/onionshare/web.py +++ b/onionshare/web/__init__.py @@ -40,8 +40,8 @@ from flask import ( ) from werkzeug.utils import secure_filename -from . import strings -from .common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable +from .. import strings +from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable # Stub out flask's show_server_banner function, to avoiding showing warnings that From 71ea9bf29edee513f456d6353d5e716e33ae75f7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 23:43:04 -0700 Subject: [PATCH 027/123] Split the web module into different files for receive mode and share mode logic --- onionshare/web/__init__.py | 857 +-------------------------------- onionshare/web/receive_mode.py | 156 ++++++ onionshare/web/share_mode.py | 60 +++ onionshare/web/web.py | 647 +++++++++++++++++++++++++ 4 files changed, 864 insertions(+), 856 deletions(-) create mode 100644 onionshare/web/receive_mode.py create mode 100644 onionshare/web/share_mode.py create mode 100644 onionshare/web/web.py diff --git a/onionshare/web/__init__.py b/onionshare/web/__init__.py index 5a7b297f..d45b4983 100644 --- a/onionshare/web/__init__.py +++ b/onionshare/web/__init__.py @@ -18,859 +18,4 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import hmac -import logging -import mimetypes -import os -import queue -import socket -import sys -import tempfile -import zipfile -import re -import io -from distutils.version import LooseVersion as Version -from urllib.request import urlopen -from datetime import datetime - -import flask -from flask import ( - Flask, Response, Request, request, render_template, abort, make_response, - flash, redirect, __version__ as flask_version -) -from werkzeug.utils import secure_filename - -from .. import strings -from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable - - -# Stub out flask's show_server_banner function, to avoiding showing warnings that -# are not applicable to OnionShare -def stubbed_show_server_banner(env, debug, app_import_path, eager_loading): - pass - -flask.cli.show_server_banner = stubbed_show_server_banner - - -class Web(object): - """ - The Web object is the OnionShare web server, powered by flask - """ - REQUEST_LOAD = 0 - REQUEST_STARTED = 1 - REQUEST_PROGRESS = 2 - REQUEST_OTHER = 3 - REQUEST_CANCELED = 4 - REQUEST_RATE_LIMIT = 5 - REQUEST_CLOSE_SERVER = 6 - REQUEST_UPLOAD_FILE_RENAMED = 7 - REQUEST_UPLOAD_FINISHED = 8 - REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE = 9 - REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE = 10 - - def __init__(self, common, gui_mode, receive_mode=False): - self.common = common - - # The flask app - self.app = Flask(__name__, - static_folder=self.common.get_resource_path('static'), - template_folder=self.common.get_resource_path('templates')) - self.app.secret_key = self.common.random_string(8) - - # Debug mode? - if self.common.debug: - self.debug_mode() - - # Are we running in GUI mode? - self.gui_mode = gui_mode - - # Are we using receive mode? - self.receive_mode = receive_mode - if self.receive_mode: - # Use custom WSGI middleware, to modify environ - self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self) - # Use a custom Request class to track upload progess - self.app.request_class = ReceiveModeRequest - - # Starting in Flask 0.11, render_template_string autoescapes template variables - # by default. To prevent content injection through template variables in - # earlier versions of Flask, we force autoescaping in the Jinja2 template - # engine if we detect a Flask version with insecure default behavior. - if Version(flask_version) < Version('0.11'): - # Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc - Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape - - # Information about the file - self.file_info = [] - self.is_zipped = False - self.download_filename = None - self.download_filesize = None - self.zip_writer = None - - self.security_headers = [ - ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), - ('X-Frame-Options', 'DENY'), - ('X-Xss-Protection', '1; mode=block'), - ('X-Content-Type-Options', 'nosniff'), - ('Referrer-Policy', 'no-referrer'), - ('Server', 'OnionShare') - ] - - self.q = queue.Queue() - - self.slug = None - - self.download_count = 0 - self.upload_count = 0 - - self.error404_count = 0 - - # If "Stop After First Download" is checked (stay_open == False), only allow - # one download at a time. - self.download_in_progress = False - - self.done = False - - # If the client closes the OnionShare window while a download is in progress, - # it should immediately stop serving the file. The client_cancel global is - # used to tell the download function that the client is canceling the download. - self.client_cancel = False - - # shutting down the server only works within the context of flask, so the easiest way to do it is over http - self.shutdown_slug = self.common.random_string(16) - - # Keep track if the server is running - self.running = False - - # Define the ewb app routes - self.common_routes() - if self.receive_mode: - self.receive_routes() - else: - self.send_routes() - - def send_routes(self): - """ - The web app routes for sharing files - """ - @self.app.route("/") - def index(slug_candidate): - self.check_slug_candidate(slug_candidate) - return index_logic() - - @self.app.route("/") - def index_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return index_logic() - - def index_logic(slug_candidate=''): - """ - Render the template for the onionshare landing page. - """ - self.add_request(Web.REQUEST_LOAD, request.path) - - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not self.stay_open and self.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return self.add_security_headers(r) - - # If download is allowed to continue, serve download page - if self.slug: - r = make_response(render_template( - 'send.html', - slug=self.slug, - file_info=self.file_info, - filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) - else: - # If download is allowed to continue, serve download page - r = make_response(render_template( - 'send.html', - file_info=self.file_info, - filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) - return self.add_security_headers(r) - - @self.app.route("//download") - def download(slug_candidate): - self.check_slug_candidate(slug_candidate) - return download_logic() - - @self.app.route("/download") - def download_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return download_logic() - - def download_logic(slug_candidate=''): - """ - Download the zip file. - """ - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not self.stay_open and self.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return self.add_security_headers(r) - - # Each download has a unique id - download_id = self.download_count - self.download_count += 1 - - # Prepare some variables to use inside generate() function below - # which is outside of the request context - shutdown_func = request.environ.get('werkzeug.server.shutdown') - path = request.path - - # Tell GUI the download started - self.add_request(Web.REQUEST_STARTED, path, { - 'id': download_id} - ) - - dirname = os.path.dirname(self.download_filename) - basename = os.path.basename(self.download_filename) - - def generate(): - # The user hasn't canceled the download - self.client_cancel = False - - # Starting a new download - if not self.stay_open: - self.download_in_progress = True - - chunk_size = 102400 # 100kb - - fp = open(self.download_filename, 'rb') - self.done = False - canceled = False - while not self.done: - # The user has canceled the download, so stop serving the file - if self.client_cancel: - self.add_request(Web.REQUEST_CANCELED, path, { - 'id': download_id - }) - break - - chunk = fp.read(chunk_size) - if chunk == b'': - self.done = True - else: - try: - yield chunk - - # tell GUI the progress - downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 - - # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) - if not self.gui_mode or self.common.platform == 'Linux' or self.common.platform == 'BSD': - sys.stdout.write( - "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent)) - sys.stdout.flush() - - self.add_request(Web.REQUEST_PROGRESS, path, { - 'id': download_id, - 'bytes': downloaded_bytes - }) - self.done = False - except: - # looks like the download was canceled - self.done = True - canceled = True - - # tell the GUI the download has canceled - self.add_request(Web.REQUEST_CANCELED, path, { - 'id': download_id - }) - - fp.close() - - if self.common.platform != 'Darwin': - sys.stdout.write("\n") - - # Download is finished - if not self.stay_open: - self.download_in_progress = False - - # Close the server, if necessary - if not self.stay_open and not canceled: - print(strings._("closing_automatically")) - self.running = False - try: - if shutdown_func is None: - raise RuntimeError('Not running with the Werkzeug Server') - shutdown_func() - except: - pass - - r = Response(generate()) - r.headers.set('Content-Length', self.download_filesize) - r.headers.set('Content-Disposition', 'attachment', filename=basename) - r = self.add_security_headers(r) - # guess content type - (content_type, _) = mimetypes.guess_type(basename, strict=False) - if content_type is not None: - r.headers.set('Content-Type', content_type) - return r - - def receive_routes(self): - """ - The web app routes for receiving files - """ - def index_logic(): - self.add_request(Web.REQUEST_LOAD, request.path) - - if self.common.settings.get('public_mode'): - upload_action = '/upload' - close_action = '/close' - else: - upload_action = '/{}/upload'.format(self.slug) - close_action = '/{}/close'.format(self.slug) - - r = make_response(render_template( - 'receive.html', - upload_action=upload_action, - close_action=close_action, - receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown'))) - return self.add_security_headers(r) - - @self.app.route("/") - def index(slug_candidate): - self.check_slug_candidate(slug_candidate) - return index_logic() - - @self.app.route("/") - def index_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return index_logic() - - - def upload_logic(slug_candidate=''): - """ - Upload files. - """ - # Make sure downloads_dir exists - valid = True - try: - self.common.validate_downloads_dir() - except DownloadsDirErrorCannotCreate: - self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) - print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir'))) - valid = False - except DownloadsDirErrorNotWritable: - self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) - print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir'))) - valid = False - if not valid: - flash('Error uploading, please inform the OnionShare user', 'error') - if self.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) - - files = request.files.getlist('file[]') - filenames = [] - print('') - for f in files: - if f.filename != '': - # Automatically rename the file, if a file of the same name already exists - filename = secure_filename(f.filename) - filenames.append(filename) - local_path = os.path.join(self.common.settings.get('downloads_dir'), filename) - if os.path.exists(local_path): - if '.' in filename: - # Add "-i", e.g. change "foo.txt" to "foo-2.txt" - parts = filename.split('.') - name = parts[:-1] - ext = parts[-1] - - i = 2 - valid = False - while not valid: - new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) - local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - else: - # If no extension, just add "-i", e.g. change "foo" to "foo-2" - i = 2 - valid = False - while not valid: - new_filename = '{}-{}'.format(filename, i) - local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - - basename = os.path.basename(local_path) - if f.filename != basename: - # Tell the GUI that the file has changed names - self.add_request(Web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { - 'id': request.upload_id, - 'old_filename': f.filename, - 'new_filename': basename - }) - - self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) - print(strings._('receive_mode_received_file').format(local_path)) - f.save(local_path) - - # Note that flash strings are on English, and not translated, on purpose, - # to avoid leaking the locale of the OnionShare user - if len(filenames) == 0: - flash('No files uploaded', 'info') - else: - for filename in filenames: - flash('Sent {}'.format(filename), 'info') - - if self.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) - - @self.app.route("//upload", methods=['POST']) - def upload(slug_candidate): - self.check_slug_candidate(slug_candidate) - return upload_logic(slug_candidate) - - @self.app.route("/upload", methods=['POST']) - def upload_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return upload_logic() - - - def close_logic(slug_candidate=''): - if self.common.settings.get('receive_allow_receiver_shutdown'): - self.force_shutdown() - r = make_response(render_template('closed.html')) - self.add_request(Web.REQUEST_CLOSE_SERVER, request.path) - return self.add_security_headers(r) - else: - return redirect('/{}'.format(slug_candidate)) - - @self.app.route("//close", methods=['POST']) - def close(slug_candidate): - self.check_slug_candidate(slug_candidate) - return close_logic(slug_candidate) - - @self.app.route("/close", methods=['POST']) - def close_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return close_logic() - - def common_routes(self): - """ - Common web app routes between sending and receiving - """ - @self.app.errorhandler(404) - def page_not_found(e): - """ - 404 error page. - """ - return self.error404() - - @self.app.route("//shutdown") - def shutdown(slug_candidate): - """ - Stop the flask web server, from the context of an http request. - """ - self.check_shutdown_slug_candidate(slug_candidate) - self.force_shutdown() - return "" - - def error404(self): - self.add_request(Web.REQUEST_OTHER, request.path) - if request.path != '/favicon.ico': - self.error404_count += 1 - - # In receive mode, with public mode enabled, skip rate limiting 404s - if not self.common.settings.get('public_mode'): - if self.error404_count == 20: - self.add_request(Web.REQUEST_RATE_LIMIT, request.path) - self.force_shutdown() - print(strings._('error_rate_limit')) - - r = make_response(render_template('404.html'), 404) - return self.add_security_headers(r) - - def add_security_headers(self, r): - """ - Add security headers to a request - """ - for header, value in self.security_headers: - r.headers.set(header, value) - return r - - def set_file_info(self, filenames, processed_size_callback=None): - """ - Using the list of filenames being shared, fill in details that the web - page will need to display. This includes zipping up the file in order to - get the zip file's name and size. - """ - self.common.log("Web", "set_file_info") - self.cancel_compression = False - - # build file info list - self.file_info = {'files': [], 'dirs': []} - for filename in filenames: - info = { - 'filename': filename, - 'basename': os.path.basename(filename.rstrip('/')) - } - if os.path.isfile(filename): - info['size'] = os.path.getsize(filename) - info['size_human'] = self.common.human_readable_filesize(info['size']) - self.file_info['files'].append(info) - if os.path.isdir(filename): - info['size'] = self.common.dir_size(filename) - info['size_human'] = self.common.human_readable_filesize(info['size']) - self.file_info['dirs'].append(info) - self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) - self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) - - # Check if there's only 1 file and no folders - if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: - self.is_zipped = False - self.download_filename = self.file_info['files'][0]['filename'] - self.download_filesize = self.file_info['files'][0]['size'] - else: - # Zip up the files and folders - self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) - self.download_filename = self.zip_writer.zip_filename - for info in self.file_info['files']: - self.zip_writer.add_file(info['filename']) - # Canceling early? - if self.cancel_compression: - self.zip_writer.close() - return False - - for info in self.file_info['dirs']: - if not self.zip_writer.add_dir(info['filename']): - return False - - self.zip_writer.close() - self.download_filesize = os.path.getsize(self.download_filename) - self.is_zipped = True - - return True - - def _safe_select_jinja_autoescape(self, filename): - if filename is None: - return True - return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) - - def add_request(self, request_type, path, data=None): - """ - Add a request to the queue, to communicate with the GUI. - """ - self.q.put({ - 'type': request_type, - 'path': path, - 'data': data - }) - - def generate_slug(self, persistent_slug=None): - self.common.log('Web', 'generate_slug', 'persistent_slug={}'.format(persistent_slug)) - if persistent_slug != None and persistent_slug != '': - self.slug = persistent_slug - self.common.log('Web', 'generate_slug', 'persistent_slug sent, so slug is: "{}"'.format(self.slug)) - else: - self.slug = self.common.build_slug() - self.common.log('Web', 'generate_slug', 'built random slug: "{}"'.format(self.slug)) - - def debug_mode(self): - """ - Turn on debugging mode, which will log flask errors to a debug file. - """ - temp_dir = tempfile.gettempdir() - log_handler = logging.FileHandler( - os.path.join(temp_dir, 'onionshare_server.log')) - log_handler.setLevel(logging.WARNING) - self.app.logger.addHandler(log_handler) - - def check_slug_candidate(self, slug_candidate): - self.common.log('Web', 'check_slug_candidate: slug_candidate={}'.format(slug_candidate)) - if self.common.settings.get('public_mode'): - abort(404) - if not hmac.compare_digest(self.slug, slug_candidate): - abort(404) - - def check_shutdown_slug_candidate(self, slug_candidate): - self.common.log('Web', 'check_shutdown_slug_candidate: slug_candidate={}'.format(slug_candidate)) - if not hmac.compare_digest(self.shutdown_slug, slug_candidate): - abort(404) - - def force_shutdown(self): - """ - Stop the flask web server, from the context of the flask app. - """ - # Shutdown the flask service - try: - func = request.environ.get('werkzeug.server.shutdown') - if func is None: - raise RuntimeError('Not running with the Werkzeug Server') - func() - except: - pass - self.running = False - - def start(self, port, stay_open=False, public_mode=False, persistent_slug=None): - """ - Start the flask web server. - """ - self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, persistent_slug={}'.format(port, stay_open, public_mode, persistent_slug)) - if not public_mode: - self.generate_slug(persistent_slug) - - self.stay_open = stay_open - - # In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220) - if os.path.exists('/usr/share/anon-ws-base-files/workstation'): - host = '0.0.0.0' - else: - host = '127.0.0.1' - - self.running = True - self.app.run(host=host, port=port, threaded=True) - - def stop(self, port): - """ - Stop the flask web server by loading /shutdown. - """ - - # If the user cancels the download, let the download function know to stop - # serving the file - self.client_cancel = True - - # To stop flask, load http://127.0.0.1://shutdown - if self.running: - try: - s = socket.socket() - s.connect(('127.0.0.1', port)) - s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(self.shutdown_slug)) - except: - try: - urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, self.shutdown_slug)).read() - except: - pass - - -class ZipWriter(object): - """ - ZipWriter accepts files and directories and compresses them into a zip file - with. If a zip_filename is not passed in, it will use the default onionshare - filename. - """ - def __init__(self, common, zip_filename=None, processed_size_callback=None): - self.common = common - self.cancel_compression = False - - if zip_filename: - self.zip_filename = zip_filename - else: - self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), self.common.random_string(4, 6)) - - self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True) - self.processed_size_callback = processed_size_callback - if self.processed_size_callback is None: - self.processed_size_callback = lambda _: None - self._size = 0 - self.processed_size_callback(self._size) - - def add_file(self, filename): - """ - Add a file to the zip archive. - """ - self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED) - self._size += os.path.getsize(filename) - self.processed_size_callback(self._size) - - def add_dir(self, filename): - """ - Add a directory, and all of its children, to the zip archive. - """ - dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/' - for dirpath, dirnames, filenames in os.walk(filename): - for f in filenames: - # Canceling early? - if self.cancel_compression: - return False - - full_filename = os.path.join(dirpath, f) - if not os.path.islink(full_filename): - arc_filename = full_filename[len(dir_to_strip):] - self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED) - self._size += os.path.getsize(full_filename) - self.processed_size_callback(self._size) - - return True - - def close(self): - """ - Close the zip archive. - """ - self.z.close() - - -class ReceiveModeWSGIMiddleware(object): - """ - Custom WSGI middleware in order to attach the Web object to environ, so - ReceiveModeRequest can access it. - """ - def __init__(self, app, web): - self.app = app - self.web = web - - def __call__(self, environ, start_response): - environ['web'] = self.web - return self.app(environ, start_response) - - -class ReceiveModeTemporaryFile(object): - """ - A custom TemporaryFile that tells ReceiveModeRequest every time data gets - written to it, in order to track the progress of uploads. - """ - def __init__(self, filename, write_func, close_func): - self.onionshare_filename = filename - self.onionshare_write_func = write_func - self.onionshare_close_func = close_func - - # Create a temporary file - self.f = tempfile.TemporaryFile('wb+') - - # Make all the file-like methods and attributes actually access the - # TemporaryFile, except for write - attrs = ['closed', 'detach', 'fileno', 'flush', 'isatty', 'mode', - 'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto', - 'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell', - 'truncate', 'writable', 'writelines'] - for attr in attrs: - setattr(self, attr, getattr(self.f, attr)) - - def write(self, b): - """ - Custom write method that calls out to onionshare_write_func - """ - bytes_written = self.f.write(b) - self.onionshare_write_func(self.onionshare_filename, bytes_written) - - def close(self): - """ - Custom close method that calls out to onionshare_close_func - """ - self.f.close() - self.onionshare_close_func(self.onionshare_filename) - - -class ReceiveModeRequest(Request): - """ - A custom flask Request object that keeps track of how much data has been - uploaded for each file, for receive mode. - """ - def __init__(self, environ, populate_request=True, shallow=False): - super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow) - self.web = environ['web'] - - # Is this a valid upload request? - self.upload_request = False - if self.method == 'POST': - if self.path == '/{}/upload'.format(self.web.slug): - self.upload_request = True - else: - if self.web.common.settings.get('public_mode'): - if self.path == '/upload': - self.upload_request = True - - if self.upload_request: - # A dictionary that maps filenames to the bytes uploaded so far - self.progress = {} - - # Create an upload_id, attach it to the request - self.upload_id = self.web.upload_count - self.web.upload_count += 1 - - # Figure out the content length - try: - self.content_length = int(self.headers['Content-Length']) - 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)) - )) - - # Tell the GUI - self.web.add_request(Web.REQUEST_STARTED, self.path, { - 'id': self.upload_id, - 'content_length': self.content_length - }) - - self.previous_file = None - - def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None): - """ - This gets called for each file that gets uploaded, and returns an file-like - writable stream. - """ - if self.upload_request: - self.progress[filename] = { - 'uploaded_bytes': 0, - 'complete': False - } - - return ReceiveModeTemporaryFile(filename, self.file_write_func, self.file_close_func) - - def close(self): - """ - Closing the request. - """ - super(ReceiveModeRequest, self).close() - if self.upload_request: - # Inform the GUI that the upload has finished - self.web.add_request(Web.REQUEST_UPLOAD_FINISHED, self.path, { - 'id': self.upload_id - }) - - def file_write_func(self, filename, length): - """ - This function gets called when a specific file is written to. - """ - if self.upload_request: - self.progress[filename]['uploaded_bytes'] += length - - if self.previous_file != filename: - if self.previous_file is not None: - print('') - self.previous_file = filename - - print('\r=> {:15s} {}'.format( - self.web.common.human_readable_filesize(self.progress[filename]['uploaded_bytes']), - filename - ), end='') - - # Update the GUI on the upload progress - self.web.add_request(Web.REQUEST_PROGRESS, self.path, { - 'id': self.upload_id, - 'progress': self.progress - }) - - def file_close_func(self, filename): - """ - This function gets called when a specific file is closed. - """ - self.progress[filename]['complete'] = True +from .web import Web diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py new file mode 100644 index 00000000..90accc8c --- /dev/null +++ b/onionshare/web/receive_mode.py @@ -0,0 +1,156 @@ +import tempfile +from datetime import datetime +from flask import Request + +from .. import strings + + +class ReceiveModeWSGIMiddleware(object): + """ + Custom WSGI middleware in order to attach the Web object to environ, so + ReceiveModeRequest can access it. + """ + def __init__(self, app, web): + self.app = app + self.web = web + + def __call__(self, environ, start_response): + environ['web'] = self.web + return self.app(environ, start_response) + + +class ReceiveModeTemporaryFile(object): + """ + A custom TemporaryFile that tells ReceiveModeRequest every time data gets + written to it, in order to track the progress of uploads. + """ + def __init__(self, filename, write_func, close_func): + self.onionshare_filename = filename + self.onionshare_write_func = write_func + self.onionshare_close_func = close_func + + # Create a temporary file + self.f = tempfile.TemporaryFile('wb+') + + # Make all the file-like methods and attributes actually access the + # TemporaryFile, except for write + attrs = ['closed', 'detach', 'fileno', 'flush', 'isatty', 'mode', + 'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto', + 'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell', + 'truncate', 'writable', 'writelines'] + for attr in attrs: + setattr(self, attr, getattr(self.f, attr)) + + def write(self, b): + """ + Custom write method that calls out to onionshare_write_func + """ + bytes_written = self.f.write(b) + self.onionshare_write_func(self.onionshare_filename, bytes_written) + + def close(self): + """ + Custom close method that calls out to onionshare_close_func + """ + self.f.close() + self.onionshare_close_func(self.onionshare_filename) + + +class ReceiveModeRequest(Request): + """ + A custom flask Request object that keeps track of how much data has been + uploaded for each file, for receive mode. + """ + def __init__(self, environ, populate_request=True, shallow=False): + super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow) + self.web = environ['web'] + + # Is this a valid upload request? + self.upload_request = False + if self.method == 'POST': + if self.path == '/{}/upload'.format(self.web.slug): + self.upload_request = True + else: + if self.web.common.settings.get('public_mode'): + if self.path == '/upload': + self.upload_request = True + + if self.upload_request: + # A dictionary that maps filenames to the bytes uploaded so far + self.progress = {} + + # Create an upload_id, attach it to the request + self.upload_id = self.web.upload_count + self.web.upload_count += 1 + + # Figure out the content length + try: + self.content_length = int(self.headers['Content-Length']) + 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)) + )) + + # Tell the GUI + self.web.add_request(self.web.REQUEST_STARTED, self.path, { + 'id': self.upload_id, + 'content_length': self.content_length + }) + + self.previous_file = None + + def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None): + """ + This gets called for each file that gets uploaded, and returns an file-like + writable stream. + """ + if self.upload_request: + self.progress[filename] = { + 'uploaded_bytes': 0, + 'complete': False + } + + return ReceiveModeTemporaryFile(filename, self.file_write_func, self.file_close_func) + + def close(self): + """ + Closing the request. + """ + super(ReceiveModeRequest, self).close() + if self.upload_request: + # Inform the GUI that the upload has finished + self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, { + 'id': self.upload_id + }) + + def file_write_func(self, filename, length): + """ + This function gets called when a specific file is written to. + """ + if self.upload_request: + self.progress[filename]['uploaded_bytes'] += length + + if self.previous_file != filename: + if self.previous_file is not None: + print('') + self.previous_file = filename + + print('\r=> {:15s} {}'.format( + self.web.common.human_readable_filesize(self.progress[filename]['uploaded_bytes']), + filename + ), end='') + + # Update the GUI on the upload progress + self.web.add_request(self.web.REQUEST_PROGRESS, self.path, { + 'id': self.upload_id, + 'progress': self.progress + }) + + def file_close_func(self, filename): + """ + This function gets called when a specific file is closed. + """ + self.progress[filename]['complete'] = True diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py new file mode 100644 index 00000000..f066bde4 --- /dev/null +++ b/onionshare/web/share_mode.py @@ -0,0 +1,60 @@ +import os +import tempfile +import zipfile + + +class ZipWriter(object): + """ + ZipWriter accepts files and directories and compresses them into a zip file + with. If a zip_filename is not passed in, it will use the default onionshare + filename. + """ + def __init__(self, common, zip_filename=None, processed_size_callback=None): + self.common = common + self.cancel_compression = False + + if zip_filename: + self.zip_filename = zip_filename + else: + self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), self.common.random_string(4, 6)) + + self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True) + self.processed_size_callback = processed_size_callback + if self.processed_size_callback is None: + self.processed_size_callback = lambda _: None + self._size = 0 + self.processed_size_callback(self._size) + + def add_file(self, filename): + """ + Add a file to the zip archive. + """ + self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED) + self._size += os.path.getsize(filename) + self.processed_size_callback(self._size) + + def add_dir(self, filename): + """ + Add a directory, and all of its children, to the zip archive. + """ + dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/' + for dirpath, dirnames, filenames in os.walk(filename): + for f in filenames: + # Canceling early? + if self.cancel_compression: + return False + + full_filename = os.path.join(dirpath, f) + if not os.path.islink(full_filename): + arc_filename = full_filename[len(dir_to_strip):] + self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED) + self._size += os.path.getsize(full_filename) + self.processed_size_callback(self._size) + + return True + + def close(self): + """ + Close the zip archive. + """ + self.z.close() diff --git a/onionshare/web/web.py b/onionshare/web/web.py new file mode 100644 index 00000000..ff149f21 --- /dev/null +++ b/onionshare/web/web.py @@ -0,0 +1,647 @@ +import hmac +import logging +import mimetypes +import os +import queue +import socket +import sys +import tempfile +from distutils.version import LooseVersion as Version +from urllib.request import urlopen + +import flask +from flask import ( + Flask, Response, request, render_template, abort, make_response, + flash, redirect, __version__ as flask_version +) +from werkzeug.utils import secure_filename + +from .. import strings +from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable + +from .share_mode import ZipWriter +from .receive_mode import ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest + + +# Stub out flask's show_server_banner function, to avoiding showing warnings that +# are not applicable to OnionShare +def stubbed_show_server_banner(env, debug, app_import_path, eager_loading): + pass + +flask.cli.show_server_banner = stubbed_show_server_banner + + +class Web(object): + """ + The Web object is the OnionShare web server, powered by flask + """ + REQUEST_LOAD = 0 + REQUEST_STARTED = 1 + REQUEST_PROGRESS = 2 + REQUEST_OTHER = 3 + REQUEST_CANCELED = 4 + REQUEST_RATE_LIMIT = 5 + REQUEST_CLOSE_SERVER = 6 + REQUEST_UPLOAD_FILE_RENAMED = 7 + REQUEST_UPLOAD_FINISHED = 8 + REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE = 9 + REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE = 10 + + def __init__(self, common, gui_mode, receive_mode=False): + self.common = common + + # The flask app + self.app = Flask(__name__, + static_folder=self.common.get_resource_path('static'), + template_folder=self.common.get_resource_path('templates')) + self.app.secret_key = self.common.random_string(8) + + # Debug mode? + if self.common.debug: + self.debug_mode() + + # Are we running in GUI mode? + self.gui_mode = gui_mode + + # Are we using receive mode? + self.receive_mode = receive_mode + if self.receive_mode: + # Use custom WSGI middleware, to modify environ + self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self) + # Use a custom Request class to track upload progess + self.app.request_class = ReceiveModeRequest + + # Starting in Flask 0.11, render_template_string autoescapes template variables + # by default. To prevent content injection through template variables in + # earlier versions of Flask, we force autoescaping in the Jinja2 template + # engine if we detect a Flask version with insecure default behavior. + if Version(flask_version) < Version('0.11'): + # Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc + Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape + + # Information about the file + self.file_info = [] + self.is_zipped = False + self.download_filename = None + self.download_filesize = None + self.zip_writer = None + + self.security_headers = [ + ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), + ('X-Frame-Options', 'DENY'), + ('X-Xss-Protection', '1; mode=block'), + ('X-Content-Type-Options', 'nosniff'), + ('Referrer-Policy', 'no-referrer'), + ('Server', 'OnionShare') + ] + + self.q = queue.Queue() + + self.slug = None + + self.download_count = 0 + self.upload_count = 0 + + self.error404_count = 0 + + # If "Stop After First Download" is checked (stay_open == False), only allow + # one download at a time. + self.download_in_progress = False + + self.done = False + + # If the client closes the OnionShare window while a download is in progress, + # it should immediately stop serving the file. The client_cancel global is + # used to tell the download function that the client is canceling the download. + self.client_cancel = False + + # shutting down the server only works within the context of flask, so the easiest way to do it is over http + self.shutdown_slug = self.common.random_string(16) + + # Keep track if the server is running + self.running = False + + # Define the ewb app routes + self.common_routes() + if self.receive_mode: + self.receive_routes() + else: + self.send_routes() + + def send_routes(self): + """ + The web app routes for sharing files + """ + @self.app.route("/") + def index(slug_candidate): + self.check_slug_candidate(slug_candidate) + return index_logic() + + @self.app.route("/") + def index_public(): + if not self.common.settings.get('public_mode'): + return self.error404() + return index_logic() + + def index_logic(slug_candidate=''): + """ + Render the template for the onionshare landing page. + """ + self.add_request(Web.REQUEST_LOAD, request.path) + + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not self.stay_open and self.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return self.add_security_headers(r) + + # If download is allowed to continue, serve download page + if self.slug: + r = make_response(render_template( + 'send.html', + slug=self.slug, + file_info=self.file_info, + filename=os.path.basename(self.download_filename), + filesize=self.download_filesize, + filesize_human=self.common.human_readable_filesize(self.download_filesize), + is_zipped=self.is_zipped)) + else: + # If download is allowed to continue, serve download page + r = make_response(render_template( + 'send.html', + file_info=self.file_info, + filename=os.path.basename(self.download_filename), + filesize=self.download_filesize, + filesize_human=self.common.human_readable_filesize(self.download_filesize), + is_zipped=self.is_zipped)) + return self.add_security_headers(r) + + @self.app.route("//download") + def download(slug_candidate): + self.check_slug_candidate(slug_candidate) + return download_logic() + + @self.app.route("/download") + def download_public(): + if not self.common.settings.get('public_mode'): + return self.error404() + return download_logic() + + def download_logic(slug_candidate=''): + """ + Download the zip file. + """ + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not self.stay_open and self.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return self.add_security_headers(r) + + # Each download has a unique id + download_id = self.download_count + self.download_count += 1 + + # Prepare some variables to use inside generate() function below + # which is outside of the request context + shutdown_func = request.environ.get('werkzeug.server.shutdown') + path = request.path + + # Tell GUI the download started + self.add_request(Web.REQUEST_STARTED, path, { + 'id': download_id} + ) + + dirname = os.path.dirname(self.download_filename) + basename = os.path.basename(self.download_filename) + + def generate(): + # The user hasn't canceled the download + self.client_cancel = False + + # Starting a new download + if not self.stay_open: + self.download_in_progress = True + + chunk_size = 102400 # 100kb + + fp = open(self.download_filename, 'rb') + self.done = False + canceled = False + while not self.done: + # The user has canceled the download, so stop serving the file + if self.client_cancel: + self.add_request(Web.REQUEST_CANCELED, path, { + 'id': download_id + }) + break + + chunk = fp.read(chunk_size) + if chunk == b'': + self.done = True + else: + try: + yield chunk + + # tell GUI the progress + downloaded_bytes = fp.tell() + percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 + + # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) + if not self.gui_mode or self.common.platform == 'Linux' or self.common.platform == 'BSD': + sys.stdout.write( + "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent)) + sys.stdout.flush() + + self.add_request(Web.REQUEST_PROGRESS, path, { + 'id': download_id, + 'bytes': downloaded_bytes + }) + self.done = False + except: + # looks like the download was canceled + self.done = True + canceled = True + + # tell the GUI the download has canceled + self.add_request(Web.REQUEST_CANCELED, path, { + 'id': download_id + }) + + fp.close() + + if self.common.platform != 'Darwin': + sys.stdout.write("\n") + + # Download is finished + if not self.stay_open: + self.download_in_progress = False + + # Close the server, if necessary + if not self.stay_open and not canceled: + print(strings._("closing_automatically")) + self.running = False + try: + if shutdown_func is None: + raise RuntimeError('Not running with the Werkzeug Server') + shutdown_func() + except: + pass + + r = Response(generate()) + r.headers.set('Content-Length', self.download_filesize) + r.headers.set('Content-Disposition', 'attachment', filename=basename) + r = self.add_security_headers(r) + # guess content type + (content_type, _) = mimetypes.guess_type(basename, strict=False) + if content_type is not None: + r.headers.set('Content-Type', content_type) + return r + + def receive_routes(self): + """ + The web app routes for receiving files + """ + def index_logic(): + self.add_request(Web.REQUEST_LOAD, request.path) + + if self.common.settings.get('public_mode'): + upload_action = '/upload' + close_action = '/close' + else: + upload_action = '/{}/upload'.format(self.slug) + close_action = '/{}/close'.format(self.slug) + + r = make_response(render_template( + 'receive.html', + upload_action=upload_action, + close_action=close_action, + receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown'))) + return self.add_security_headers(r) + + @self.app.route("/") + def index(slug_candidate): + self.check_slug_candidate(slug_candidate) + return index_logic() + + @self.app.route("/") + def index_public(): + if not self.common.settings.get('public_mode'): + return self.error404() + return index_logic() + + + def upload_logic(slug_candidate=''): + """ + Upload files. + """ + # Make sure downloads_dir exists + valid = True + try: + self.common.validate_downloads_dir() + except DownloadsDirErrorCannotCreate: + self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) + print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir'))) + valid = False + except DownloadsDirErrorNotWritable: + self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) + print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir'))) + valid = False + if not valid: + flash('Error uploading, please inform the OnionShare user', 'error') + if self.common.settings.get('public_mode'): + return redirect('/') + else: + return redirect('/{}'.format(slug_candidate)) + + files = request.files.getlist('file[]') + filenames = [] + print('') + for f in files: + if f.filename != '': + # Automatically rename the file, if a file of the same name already exists + filename = secure_filename(f.filename) + filenames.append(filename) + local_path = os.path.join(self.common.settings.get('downloads_dir'), filename) + if os.path.exists(local_path): + if '.' in filename: + # Add "-i", e.g. change "foo.txt" to "foo-2.txt" + parts = filename.split('.') + name = parts[:-1] + ext = parts[-1] + + i = 2 + valid = False + while not valid: + new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) + local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + else: + # If no extension, just add "-i", e.g. change "foo" to "foo-2" + i = 2 + valid = False + while not valid: + new_filename = '{}-{}'.format(filename, i) + local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + + basename = os.path.basename(local_path) + if f.filename != basename: + # Tell the GUI that the file has changed names + self.add_request(Web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { + 'id': request.upload_id, + 'old_filename': f.filename, + 'new_filename': basename + }) + + self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) + print(strings._('receive_mode_received_file').format(local_path)) + f.save(local_path) + + # Note that flash strings are on English, and not translated, on purpose, + # to avoid leaking the locale of the OnionShare user + if len(filenames) == 0: + flash('No files uploaded', 'info') + else: + for filename in filenames: + flash('Sent {}'.format(filename), 'info') + + if self.common.settings.get('public_mode'): + return redirect('/') + else: + return redirect('/{}'.format(slug_candidate)) + + @self.app.route("//upload", methods=['POST']) + def upload(slug_candidate): + self.check_slug_candidate(slug_candidate) + return upload_logic(slug_candidate) + + @self.app.route("/upload", methods=['POST']) + def upload_public(): + if not self.common.settings.get('public_mode'): + return self.error404() + return upload_logic() + + + def close_logic(slug_candidate=''): + if self.common.settings.get('receive_allow_receiver_shutdown'): + self.force_shutdown() + r = make_response(render_template('closed.html')) + self.add_request(Web.REQUEST_CLOSE_SERVER, request.path) + return self.add_security_headers(r) + else: + return redirect('/{}'.format(slug_candidate)) + + @self.app.route("//close", methods=['POST']) + def close(slug_candidate): + self.check_slug_candidate(slug_candidate) + return close_logic(slug_candidate) + + @self.app.route("/close", methods=['POST']) + def close_public(): + if not self.common.settings.get('public_mode'): + return self.error404() + return close_logic() + + def common_routes(self): + """ + Common web app routes between sending and receiving + """ + @self.app.errorhandler(404) + def page_not_found(e): + """ + 404 error page. + """ + return self.error404() + + @self.app.route("//shutdown") + def shutdown(slug_candidate): + """ + Stop the flask web server, from the context of an http request. + """ + self.check_shutdown_slug_candidate(slug_candidate) + self.force_shutdown() + return "" + + def error404(self): + self.add_request(Web.REQUEST_OTHER, request.path) + if request.path != '/favicon.ico': + self.error404_count += 1 + + # In receive mode, with public mode enabled, skip rate limiting 404s + if not self.common.settings.get('public_mode'): + if self.error404_count == 20: + self.add_request(Web.REQUEST_RATE_LIMIT, request.path) + self.force_shutdown() + print(strings._('error_rate_limit')) + + r = make_response(render_template('404.html'), 404) + return self.add_security_headers(r) + + def add_security_headers(self, r): + """ + Add security headers to a request + """ + for header, value in self.security_headers: + r.headers.set(header, value) + return r + + def set_file_info(self, filenames, processed_size_callback=None): + """ + Using the list of filenames being shared, fill in details that the web + page will need to display. This includes zipping up the file in order to + get the zip file's name and size. + """ + self.common.log("Web", "set_file_info") + self.cancel_compression = False + + # build file info list + self.file_info = {'files': [], 'dirs': []} + for filename in filenames: + info = { + 'filename': filename, + 'basename': os.path.basename(filename.rstrip('/')) + } + if os.path.isfile(filename): + info['size'] = os.path.getsize(filename) + info['size_human'] = self.common.human_readable_filesize(info['size']) + self.file_info['files'].append(info) + if os.path.isdir(filename): + info['size'] = self.common.dir_size(filename) + info['size_human'] = self.common.human_readable_filesize(info['size']) + self.file_info['dirs'].append(info) + self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) + self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) + + # Check if there's only 1 file and no folders + if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: + self.is_zipped = False + self.download_filename = self.file_info['files'][0]['filename'] + self.download_filesize = self.file_info['files'][0]['size'] + else: + # Zip up the files and folders + self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) + self.download_filename = self.zip_writer.zip_filename + for info in self.file_info['files']: + self.zip_writer.add_file(info['filename']) + # Canceling early? + if self.cancel_compression: + self.zip_writer.close() + return False + + for info in self.file_info['dirs']: + if not self.zip_writer.add_dir(info['filename']): + return False + + self.zip_writer.close() + self.download_filesize = os.path.getsize(self.download_filename) + self.is_zipped = True + + return True + + def _safe_select_jinja_autoescape(self, filename): + if filename is None: + return True + return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + + def add_request(self, request_type, path, data=None): + """ + Add a request to the queue, to communicate with the GUI. + """ + self.q.put({ + 'type': request_type, + 'path': path, + 'data': data + }) + + def generate_slug(self, persistent_slug=None): + self.common.log('Web', 'generate_slug', 'persistent_slug={}'.format(persistent_slug)) + if persistent_slug != None and persistent_slug != '': + self.slug = persistent_slug + self.common.log('Web', 'generate_slug', 'persistent_slug sent, so slug is: "{}"'.format(self.slug)) + else: + self.slug = self.common.build_slug() + self.common.log('Web', 'generate_slug', 'built random slug: "{}"'.format(self.slug)) + + def debug_mode(self): + """ + Turn on debugging mode, which will log flask errors to a debug file. + """ + temp_dir = tempfile.gettempdir() + log_handler = logging.FileHandler( + os.path.join(temp_dir, 'onionshare_server.log')) + log_handler.setLevel(logging.WARNING) + self.app.logger.addHandler(log_handler) + + def check_slug_candidate(self, slug_candidate): + self.common.log('Web', 'check_slug_candidate: slug_candidate={}'.format(slug_candidate)) + if self.common.settings.get('public_mode'): + abort(404) + if not hmac.compare_digest(self.slug, slug_candidate): + abort(404) + + def check_shutdown_slug_candidate(self, slug_candidate): + self.common.log('Web', 'check_shutdown_slug_candidate: slug_candidate={}'.format(slug_candidate)) + if not hmac.compare_digest(self.shutdown_slug, slug_candidate): + abort(404) + + def force_shutdown(self): + """ + Stop the flask web server, from the context of the flask app. + """ + # Shutdown the flask service + try: + func = request.environ.get('werkzeug.server.shutdown') + if func is None: + raise RuntimeError('Not running with the Werkzeug Server') + func() + except: + pass + self.running = False + + def start(self, port, stay_open=False, public_mode=False, persistent_slug=None): + """ + Start the flask web server. + """ + self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, persistent_slug={}'.format(port, stay_open, public_mode, persistent_slug)) + if not public_mode: + self.generate_slug(persistent_slug) + + self.stay_open = stay_open + + # In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220) + if os.path.exists('/usr/share/anon-ws-base-files/workstation'): + host = '0.0.0.0' + else: + host = '127.0.0.1' + + self.running = True + self.app.run(host=host, port=port, threaded=True) + + def stop(self, port): + """ + Stop the flask web server by loading /shutdown. + """ + + # If the user cancels the download, let the download function know to stop + # serving the file + self.client_cancel = True + + # To stop flask, load http://127.0.0.1://shutdown + if self.running: + try: + s = socket.socket() + s.connect(('127.0.0.1', port)) + s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(self.shutdown_slug)) + except: + try: + urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, self.shutdown_slug)).read() + except: + pass From 357985fd124938d0d6e8af8d6fb19fed13cfe2b1 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 23:45:13 -0700 Subject: [PATCH 028/123] Fix tests to point to new location of ZipWriter class --- test/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 610a43ea..8ac7efb8 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -64,7 +64,7 @@ def temp_file_1024_delete(): # pytest > 2.9 only needs @pytest.fixture @pytest.yield_fixture(scope='session') def custom_zw(): - zw = web.ZipWriter( + zw = web.share_mode.ZipWriter( common.Common(), zip_filename=common.Common.random_string(4, 6), processed_size_callback=lambda _: 'custom_callback' @@ -77,7 +77,7 @@ def custom_zw(): # pytest > 2.9 only needs @pytest.fixture @pytest.yield_fixture(scope='session') def default_zw(): - zw = web.ZipWriter(common.Common()) + zw = web.share_mode.ZipWriter(common.Common()) yield zw zw.close() tmp_dir = os.path.dirname(zw.zip_filename) From 8ce90fdd60e6d3d01445f9b72e2ad4a1190ce419 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 23:58:27 -0700 Subject: [PATCH 029/123] Refactor web to push share and receive mode logic into their respective files --- onionshare/web/receive_mode.py | 157 ++++++++++++++- onionshare/web/share_mode.py | 177 +++++++++++++++++ onionshare/web/web.py | 338 +-------------------------------- 3 files changed, 338 insertions(+), 334 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 90accc8c..0ebc9ccd 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -1,10 +1,165 @@ +import os import tempfile from datetime import datetime -from flask import Request +from flask import Request, request, render_template, make_response, flash, redirect +from werkzeug.utils import secure_filename +from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable from .. import strings +def receive_routes(web): + """ + The web app routes for receiving files + """ + def index_logic(): + web.add_request(web.REQUEST_LOAD, request.path) + + if web.common.settings.get('public_mode'): + upload_action = '/upload' + close_action = '/close' + else: + upload_action = '/{}/upload'.format(web.slug) + close_action = '/{}/close'.format(web.slug) + + r = make_response(render_template( + 'receive.html', + upload_action=upload_action, + close_action=close_action, + receive_allow_receiver_shutdown=web.common.settings.get('receive_allow_receiver_shutdown'))) + return web.add_security_headers(r) + + @web.app.route("/") + def index(slug_candidate): + web.check_slug_candidate(slug_candidate) + return index_logic() + + @web.app.route("/") + def index_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return index_logic() + + + def upload_logic(slug_candidate=''): + """ + Upload files. + """ + # Make sure downloads_dir exists + valid = True + try: + web.common.validate_downloads_dir() + except DownloadsDirErrorCannotCreate: + web.add_request(web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) + print(strings._('error_cannot_create_downloads_dir').format(web.common.settings.get('downloads_dir'))) + valid = False + except DownloadsDirErrorNotWritable: + web.add_request(web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) + print(strings._('error_downloads_dir_not_writable').format(web.common.settings.get('downloads_dir'))) + valid = False + if not valid: + flash('Error uploading, please inform the OnionShare user', 'error') + if web.common.settings.get('public_mode'): + return redirect('/') + else: + return redirect('/{}'.format(slug_candidate)) + + files = request.files.getlist('file[]') + filenames = [] + print('') + for f in files: + if f.filename != '': + # Automatically rename the file, if a file of the same name already exists + filename = secure_filename(f.filename) + filenames.append(filename) + local_path = os.path.join(web.common.settings.get('downloads_dir'), filename) + if os.path.exists(local_path): + if '.' in filename: + # Add "-i", e.g. change "foo.txt" to "foo-2.txt" + parts = filename.split('.') + name = parts[:-1] + ext = parts[-1] + + i = 2 + valid = False + while not valid: + new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) + local_path = os.path.join(web.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + else: + # If no extension, just add "-i", e.g. change "foo" to "foo-2" + i = 2 + valid = False + while not valid: + new_filename = '{}-{}'.format(filename, i) + local_path = os.path.join(web.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + + basename = os.path.basename(local_path) + if f.filename != basename: + # Tell the GUI that the file has changed names + web.add_request(web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { + 'id': request.upload_id, + 'old_filename': f.filename, + 'new_filename': basename + }) + + web.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) + print(strings._('receive_mode_received_file').format(local_path)) + f.save(local_path) + + # Note that flash strings are on English, and not translated, on purpose, + # to avoid leaking the locale of the OnionShare user + if len(filenames) == 0: + flash('No files uploaded', 'info') + else: + for filename in filenames: + flash('Sent {}'.format(filename), 'info') + + if web.common.settings.get('public_mode'): + return redirect('/') + else: + return redirect('/{}'.format(slug_candidate)) + + @web.app.route("//upload", methods=['POST']) + def upload(slug_candidate): + web.check_slug_candidate(slug_candidate) + return upload_logic(slug_candidate) + + @web.app.route("/upload", methods=['POST']) + def upload_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return upload_logic() + + + def close_logic(slug_candidate=''): + if web.common.settings.get('receive_allow_receiver_shutdown'): + web.force_shutdown() + r = make_response(render_template('closed.html')) + web.add_request(web.REQUEST_CLOSE_SERVER, request.path) + return web.add_security_headers(r) + else: + return redirect('/{}'.format(slug_candidate)) + + @web.app.route("//close", methods=['POST']) + def close(slug_candidate): + web.check_slug_candidate(slug_candidate) + return close_logic(slug_candidate) + + @web.app.route("/close", methods=['POST']) + def close_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return close_logic() + + class ReceiveModeWSGIMiddleware(object): """ Custom WSGI middleware in order to attach the Web object to environ, so diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index f066bde4..58cc9b99 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -1,6 +1,183 @@ import os +import sys import tempfile import zipfile +import mimetypes +from flask import Response, request, render_template, make_response + +from .. import strings + + +def share_routes(web): + """ + The web app routes for sharing files + """ + @web.app.route("/") + def index(slug_candidate): + web.check_slug_candidate(slug_candidate) + return index_logic() + + @web.app.route("/") + def index_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return index_logic() + + def index_logic(slug_candidate=''): + """ + Render the template for the onionshare landing page. + """ + web.add_request(web.REQUEST_LOAD, request.path) + + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not web.stay_open and web.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return web.add_security_headers(r) + + # If download is allowed to continue, serve download page + if web.slug: + r = make_response(render_template( + 'send.html', + slug=web.slug, + file_info=web.file_info, + filename=os.path.basename(web.download_filename), + filesize=web.download_filesize, + filesize_human=web.common.human_readable_filesize(web.download_filesize), + is_zipped=web.is_zipped)) + else: + # If download is allowed to continue, serve download page + r = make_response(render_template( + 'send.html', + file_info=web.file_info, + filename=os.path.basename(web.download_filename), + filesize=web.download_filesize, + filesize_human=web.common.human_readable_filesize(web.download_filesize), + is_zipped=web.is_zipped)) + return web.add_security_headers(r) + + @web.app.route("//download") + def download(slug_candidate): + web.check_slug_candidate(slug_candidate) + return download_logic() + + @web.app.route("/download") + def download_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return download_logic() + + def download_logic(slug_candidate=''): + """ + Download the zip file. + """ + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not web.stay_open and web.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return web.add_security_headers(r) + + # Each download has a unique id + download_id = web.download_count + web.download_count += 1 + + # Prepare some variables to use inside generate() function below + # which is outside of the request context + shutdown_func = request.environ.get('werkzeug.server.shutdown') + path = request.path + + # Tell GUI the download started + web.add_request(web.REQUEST_STARTED, path, { + 'id': download_id} + ) + + dirname = os.path.dirname(web.download_filename) + basename = os.path.basename(web.download_filename) + + def generate(): + # The user hasn't canceled the download + web.client_cancel = False + + # Starting a new download + if not web.stay_open: + web.download_in_progress = True + + chunk_size = 102400 # 100kb + + fp = open(web.download_filename, 'rb') + web.done = False + canceled = False + while not web.done: + # The user has canceled the download, so stop serving the file + if web.client_cancel: + web.add_request(web.REQUEST_CANCELED, path, { + 'id': download_id + }) + break + + chunk = fp.read(chunk_size) + if chunk == b'': + web.done = True + else: + try: + yield chunk + + # tell GUI the progress + downloaded_bytes = fp.tell() + percent = (1.0 * downloaded_bytes / web.download_filesize) * 100 + + # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) + if not web.gui_mode or web.common.platform == 'Linux' or web.common.platform == 'BSD': + sys.stdout.write( + "\r{0:s}, {1:.2f}% ".format(web.common.human_readable_filesize(downloaded_bytes), percent)) + sys.stdout.flush() + + web.add_request(web.REQUEST_PROGRESS, path, { + 'id': download_id, + 'bytes': downloaded_bytes + }) + web.done = False + except: + # looks like the download was canceled + web.done = True + canceled = True + + # tell the GUI the download has canceled + web.add_request(web.REQUEST_CANCELED, path, { + 'id': download_id + }) + + fp.close() + + if web.common.platform != 'Darwin': + sys.stdout.write("\n") + + # Download is finished + if not web.stay_open: + web.download_in_progress = False + + # Close the server, if necessary + if not web.stay_open and not canceled: + print(strings._("closing_automatically")) + web.running = False + try: + if shutdown_func is None: + raise RuntimeError('Not running with the Werkzeug Server') + shutdown_func() + except: + pass + + r = Response(generate()) + r.headers.set('Content-Length', web.download_filesize) + r.headers.set('Content-Disposition', 'attachment', filename=basename) + r = web.add_security_headers(r) + # guess content type + (content_type, _) = mimetypes.guess_type(basename, strict=False) + if content_type is not None: + r.headers.set('Content-Type', content_type) + return r class ZipWriter(object): diff --git a/onionshare/web/web.py b/onionshare/web/web.py index ff149f21..0a6e6964 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -1,6 +1,5 @@ import hmac import logging -import mimetypes import os import queue import socket @@ -10,17 +9,12 @@ from distutils.version import LooseVersion as Version from urllib.request import urlopen import flask -from flask import ( - Flask, Response, request, render_template, abort, make_response, - flash, redirect, __version__ as flask_version -) -from werkzeug.utils import secure_filename +from flask import Flask, request, render_template, abort, make_response, __version__ as flask_version from .. import strings -from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable -from .share_mode import ZipWriter -from .receive_mode import ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest +from .share_mode import share_routes, ZipWriter +from .receive_mode import receive_routes, ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest # Stub out flask's show_server_banner function, to avoiding showing warnings that @@ -124,331 +118,9 @@ class Web(object): # Define the ewb app routes self.common_routes() if self.receive_mode: - self.receive_routes() + receive_routes(self) else: - self.send_routes() - - def send_routes(self): - """ - The web app routes for sharing files - """ - @self.app.route("/") - def index(slug_candidate): - self.check_slug_candidate(slug_candidate) - return index_logic() - - @self.app.route("/") - def index_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return index_logic() - - def index_logic(slug_candidate=''): - """ - Render the template for the onionshare landing page. - """ - self.add_request(Web.REQUEST_LOAD, request.path) - - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not self.stay_open and self.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return self.add_security_headers(r) - - # If download is allowed to continue, serve download page - if self.slug: - r = make_response(render_template( - 'send.html', - slug=self.slug, - file_info=self.file_info, - filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) - else: - # If download is allowed to continue, serve download page - r = make_response(render_template( - 'send.html', - file_info=self.file_info, - filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) - return self.add_security_headers(r) - - @self.app.route("//download") - def download(slug_candidate): - self.check_slug_candidate(slug_candidate) - return download_logic() - - @self.app.route("/download") - def download_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return download_logic() - - def download_logic(slug_candidate=''): - """ - Download the zip file. - """ - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not self.stay_open and self.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return self.add_security_headers(r) - - # Each download has a unique id - download_id = self.download_count - self.download_count += 1 - - # Prepare some variables to use inside generate() function below - # which is outside of the request context - shutdown_func = request.environ.get('werkzeug.server.shutdown') - path = request.path - - # Tell GUI the download started - self.add_request(Web.REQUEST_STARTED, path, { - 'id': download_id} - ) - - dirname = os.path.dirname(self.download_filename) - basename = os.path.basename(self.download_filename) - - def generate(): - # The user hasn't canceled the download - self.client_cancel = False - - # Starting a new download - if not self.stay_open: - self.download_in_progress = True - - chunk_size = 102400 # 100kb - - fp = open(self.download_filename, 'rb') - self.done = False - canceled = False - while not self.done: - # The user has canceled the download, so stop serving the file - if self.client_cancel: - self.add_request(Web.REQUEST_CANCELED, path, { - 'id': download_id - }) - break - - chunk = fp.read(chunk_size) - if chunk == b'': - self.done = True - else: - try: - yield chunk - - # tell GUI the progress - downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 - - # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) - if not self.gui_mode or self.common.platform == 'Linux' or self.common.platform == 'BSD': - sys.stdout.write( - "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent)) - sys.stdout.flush() - - self.add_request(Web.REQUEST_PROGRESS, path, { - 'id': download_id, - 'bytes': downloaded_bytes - }) - self.done = False - except: - # looks like the download was canceled - self.done = True - canceled = True - - # tell the GUI the download has canceled - self.add_request(Web.REQUEST_CANCELED, path, { - 'id': download_id - }) - - fp.close() - - if self.common.platform != 'Darwin': - sys.stdout.write("\n") - - # Download is finished - if not self.stay_open: - self.download_in_progress = False - - # Close the server, if necessary - if not self.stay_open and not canceled: - print(strings._("closing_automatically")) - self.running = False - try: - if shutdown_func is None: - raise RuntimeError('Not running with the Werkzeug Server') - shutdown_func() - except: - pass - - r = Response(generate()) - r.headers.set('Content-Length', self.download_filesize) - r.headers.set('Content-Disposition', 'attachment', filename=basename) - r = self.add_security_headers(r) - # guess content type - (content_type, _) = mimetypes.guess_type(basename, strict=False) - if content_type is not None: - r.headers.set('Content-Type', content_type) - return r - - def receive_routes(self): - """ - The web app routes for receiving files - """ - def index_logic(): - self.add_request(Web.REQUEST_LOAD, request.path) - - if self.common.settings.get('public_mode'): - upload_action = '/upload' - close_action = '/close' - else: - upload_action = '/{}/upload'.format(self.slug) - close_action = '/{}/close'.format(self.slug) - - r = make_response(render_template( - 'receive.html', - upload_action=upload_action, - close_action=close_action, - receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown'))) - return self.add_security_headers(r) - - @self.app.route("/") - def index(slug_candidate): - self.check_slug_candidate(slug_candidate) - return index_logic() - - @self.app.route("/") - def index_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return index_logic() - - - def upload_logic(slug_candidate=''): - """ - Upload files. - """ - # Make sure downloads_dir exists - valid = True - try: - self.common.validate_downloads_dir() - except DownloadsDirErrorCannotCreate: - self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) - print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir'))) - valid = False - except DownloadsDirErrorNotWritable: - self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) - print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir'))) - valid = False - if not valid: - flash('Error uploading, please inform the OnionShare user', 'error') - if self.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) - - files = request.files.getlist('file[]') - filenames = [] - print('') - for f in files: - if f.filename != '': - # Automatically rename the file, if a file of the same name already exists - filename = secure_filename(f.filename) - filenames.append(filename) - local_path = os.path.join(self.common.settings.get('downloads_dir'), filename) - if os.path.exists(local_path): - if '.' in filename: - # Add "-i", e.g. change "foo.txt" to "foo-2.txt" - parts = filename.split('.') - name = parts[:-1] - ext = parts[-1] - - i = 2 - valid = False - while not valid: - new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) - local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - else: - # If no extension, just add "-i", e.g. change "foo" to "foo-2" - i = 2 - valid = False - while not valid: - new_filename = '{}-{}'.format(filename, i) - local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - - basename = os.path.basename(local_path) - if f.filename != basename: - # Tell the GUI that the file has changed names - self.add_request(Web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { - 'id': request.upload_id, - 'old_filename': f.filename, - 'new_filename': basename - }) - - self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) - print(strings._('receive_mode_received_file').format(local_path)) - f.save(local_path) - - # Note that flash strings are on English, and not translated, on purpose, - # to avoid leaking the locale of the OnionShare user - if len(filenames) == 0: - flash('No files uploaded', 'info') - else: - for filename in filenames: - flash('Sent {}'.format(filename), 'info') - - if self.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) - - @self.app.route("//upload", methods=['POST']) - def upload(slug_candidate): - self.check_slug_candidate(slug_candidate) - return upload_logic(slug_candidate) - - @self.app.route("/upload", methods=['POST']) - def upload_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return upload_logic() - - - def close_logic(slug_candidate=''): - if self.common.settings.get('receive_allow_receiver_shutdown'): - self.force_shutdown() - r = make_response(render_template('closed.html')) - self.add_request(Web.REQUEST_CLOSE_SERVER, request.path) - return self.add_security_headers(r) - else: - return redirect('/{}'.format(slug_candidate)) - - @self.app.route("//close", methods=['POST']) - def close(slug_candidate): - self.check_slug_candidate(slug_candidate) - return close_logic(slug_candidate) - - @self.app.route("/close", methods=['POST']) - def close_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return close_logic() + share_routes(self) def common_routes(self): """ From cc9f646f8b2021dc279d045146bcd42ca9d6dc39 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 11:14:32 -0700 Subject: [PATCH 030/123] Refactor web even more to all of the share and receive web logic into ShareModeWeb and ReceiveModeWeb classes --- onionshare/__init__.py | 2 +- onionshare/web/receive_mode.py | 280 ++++++++++--------- onionshare/web/share_mode.py | 349 ++++++++++++++---------- onionshare/web/web.py | 84 ++---- onionshare_gui/receive_mode/__init__.py | 2 +- onionshare_gui/share_mode/__init__.py | 2 +- onionshare_gui/share_mode/threads.py | 2 +- test/test_onionshare_web.py | 32 +-- 8 files changed, 387 insertions(+), 366 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 2f57ccf2..9c390fa8 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -119,7 +119,7 @@ def main(cwd=None): # Prepare files to share print(strings._("preparing_files")) try: - web.set_file_info(filenames) + web.share_mode.set_file_info(filenames) if web.is_zipped: app.cleanup_filenames.append(web.download_filename) except OSError as e: diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 0ebc9ccd..ab5f5f13 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -8,156 +8,164 @@ from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable from .. import strings -def receive_routes(web): +class ReceiveModeWeb(object): """ - The web app routes for receiving files + All of the web logic for receive mode """ - def index_logic(): - web.add_request(web.REQUEST_LOAD, request.path) + def __init__(self, web): + self.web = web + self.define_routes() - if web.common.settings.get('public_mode'): - upload_action = '/upload' - close_action = '/close' - else: - upload_action = '/{}/upload'.format(web.slug) - close_action = '/{}/close'.format(web.slug) - - r = make_response(render_template( - 'receive.html', - upload_action=upload_action, - close_action=close_action, - receive_allow_receiver_shutdown=web.common.settings.get('receive_allow_receiver_shutdown'))) - return web.add_security_headers(r) - - @web.app.route("/") - def index(slug_candidate): - web.check_slug_candidate(slug_candidate) - return index_logic() - - @web.app.route("/") - def index_public(): - if not web.common.settings.get('public_mode'): - return web.error404() - return index_logic() - - - def upload_logic(slug_candidate=''): + def define_routes(self): """ - Upload files. + The web app routes for receiving files """ - # Make sure downloads_dir exists - valid = True - try: - web.common.validate_downloads_dir() - except DownloadsDirErrorCannotCreate: - web.add_request(web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) - print(strings._('error_cannot_create_downloads_dir').format(web.common.settings.get('downloads_dir'))) - valid = False - except DownloadsDirErrorNotWritable: - web.add_request(web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) - print(strings._('error_downloads_dir_not_writable').format(web.common.settings.get('downloads_dir'))) - valid = False - if not valid: - flash('Error uploading, please inform the OnionShare user', 'error') - if web.common.settings.get('public_mode'): + def index_logic(): + self.web.add_request(self.web.REQUEST_LOAD, request.path) + + if self.web.common.settings.get('public_mode'): + upload_action = '/upload' + close_action = '/close' + else: + upload_action = '/{}/upload'.format(self.web.slug) + close_action = '/{}/close'.format(self.web.slug) + + r = make_response(render_template( + 'receive.html', + upload_action=upload_action, + close_action=close_action, + receive_allow_receiver_shutdown=self.web.common.settings.get('receive_allow_receiver_shutdown'))) + return self.web.add_security_headers(r) + + @self.web.app.route("/") + def index(slug_candidate): + self.web.check_slug_candidate(slug_candidate) + return index_logic() + + @self.web.app.route("/") + def index_public(): + if not self.web.common.settings.get('public_mode'): + return self.web.error404() + return index_logic() + + + def upload_logic(slug_candidate=''): + """ + Upload files. + """ + # Make sure downloads_dir exists + valid = True + try: + self.web.common.validate_downloads_dir() + except DownloadsDirErrorCannotCreate: + self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) + print(strings._('error_cannot_create_downloads_dir').format(self.web.common.settings.get('downloads_dir'))) + valid = False + except DownloadsDirErrorNotWritable: + self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) + print(strings._('error_downloads_dir_not_writable').format(self.web.common.settings.get('downloads_dir'))) + valid = False + if not valid: + flash('Error uploading, please inform the OnionShare user', 'error') + if self.web.common.settings.get('public_mode'): + return redirect('/') + else: + return redirect('/{}'.format(slug_candidate)) + + files = request.files.getlist('file[]') + filenames = [] + print('') + for f in files: + if f.filename != '': + # Automatically rename the file, if a file of the same name already exists + filename = secure_filename(f.filename) + filenames.append(filename) + local_path = os.path.join(self.web.common.settings.get('downloads_dir'), filename) + if os.path.exists(local_path): + if '.' in filename: + # Add "-i", e.g. change "foo.txt" to "foo-2.txt" + parts = filename.split('.') + name = parts[:-1] + ext = parts[-1] + + i = 2 + valid = False + while not valid: + new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) + local_path = os.path.join(self.web.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + else: + # If no extension, just add "-i", e.g. change "foo" to "foo-2" + i = 2 + valid = False + while not valid: + new_filename = '{}-{}'.format(filename, i) + local_path = os.path.join(self.web.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + + basename = os.path.basename(local_path) + if f.filename != basename: + # Tell the GUI that the file has changed names + self.web.add_request(self.web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { + 'id': request.upload_id, + 'old_filename': f.filename, + 'new_filename': basename + }) + + self.web.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) + print(strings._('receive_mode_received_file').format(local_path)) + f.save(local_path) + + # Note that flash strings are on English, and not translated, on purpose, + # to avoid leaking the locale of the OnionShare user + if len(filenames) == 0: + flash('No files uploaded', 'info') + else: + for filename in filenames: + flash('Sent {}'.format(filename), 'info') + + if self.web.common.settings.get('public_mode'): return redirect('/') else: return redirect('/{}'.format(slug_candidate)) - files = request.files.getlist('file[]') - filenames = [] - print('') - for f in files: - if f.filename != '': - # Automatically rename the file, if a file of the same name already exists - filename = secure_filename(f.filename) - filenames.append(filename) - local_path = os.path.join(web.common.settings.get('downloads_dir'), filename) - if os.path.exists(local_path): - if '.' in filename: - # Add "-i", e.g. change "foo.txt" to "foo-2.txt" - parts = filename.split('.') - name = parts[:-1] - ext = parts[-1] + @self.web.app.route("//upload", methods=['POST']) + def upload(slug_candidate): + self.web.check_slug_candidate(slug_candidate) + return upload_logic(slug_candidate) - i = 2 - valid = False - while not valid: - new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) - local_path = os.path.join(web.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - else: - # If no extension, just add "-i", e.g. change "foo" to "foo-2" - i = 2 - valid = False - while not valid: - new_filename = '{}-{}'.format(filename, i) - local_path = os.path.join(web.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - - basename = os.path.basename(local_path) - if f.filename != basename: - # Tell the GUI that the file has changed names - web.add_request(web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { - 'id': request.upload_id, - 'old_filename': f.filename, - 'new_filename': basename - }) - - web.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) - print(strings._('receive_mode_received_file').format(local_path)) - f.save(local_path) - - # Note that flash strings are on English, and not translated, on purpose, - # to avoid leaking the locale of the OnionShare user - if len(filenames) == 0: - flash('No files uploaded', 'info') - else: - for filename in filenames: - flash('Sent {}'.format(filename), 'info') - - if web.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) - - @web.app.route("//upload", methods=['POST']) - def upload(slug_candidate): - web.check_slug_candidate(slug_candidate) - return upload_logic(slug_candidate) - - @web.app.route("/upload", methods=['POST']) - def upload_public(): - if not web.common.settings.get('public_mode'): - return web.error404() - return upload_logic() + @self.web.app.route("/upload", methods=['POST']) + def upload_public(): + if not self.web.common.settings.get('public_mode'): + return self.web.error404() + return upload_logic() - def close_logic(slug_candidate=''): - if web.common.settings.get('receive_allow_receiver_shutdown'): - web.force_shutdown() - r = make_response(render_template('closed.html')) - web.add_request(web.REQUEST_CLOSE_SERVER, request.path) - return web.add_security_headers(r) - else: - return redirect('/{}'.format(slug_candidate)) + def close_logic(slug_candidate=''): + if self.web.common.settings.get('receive_allow_receiver_shutdown'): + self.web.force_shutdown() + r = make_response(render_template('closed.html')) + self.web.add_request(self.web.REQUEST_CLOSE_SERVER, request.path) + return self.web.add_security_headers(r) + else: + return redirect('/{}'.format(slug_candidate)) - @web.app.route("//close", methods=['POST']) - def close(slug_candidate): - web.check_slug_candidate(slug_candidate) - return close_logic(slug_candidate) + @self.web.app.route("//close", methods=['POST']) + def close(slug_candidate): + self.web.check_slug_candidate(slug_candidate) + return close_logic(slug_candidate) - @web.app.route("/close", methods=['POST']) - def close_public(): - if not web.common.settings.get('public_mode'): - return web.error404() - return close_logic() + @self.web.app.route("/close", methods=['POST']) + def close_public(): + if not self.web.common.settings.get('public_mode'): + return self.web.error404() + return close_logic() class ReceiveModeWSGIMiddleware(object): diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 58cc9b99..c8a411bb 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -8,176 +8,237 @@ from flask import Response, request, render_template, make_response from .. import strings -def share_routes(web): +class ShareModeWeb(object): """ - The web app routes for sharing files + All of the web logic for share mode """ - @web.app.route("/") - def index(slug_candidate): - web.check_slug_candidate(slug_candidate) - return index_logic() + def __init__(self, web): + self.web = web + self.define_routes() - @web.app.route("/") - def index_public(): - if not web.common.settings.get('public_mode'): - return web.error404() - return index_logic() - - def index_logic(slug_candidate=''): + def define_routes(self): """ - Render the template for the onionshare landing page. + The web app routes for sharing files """ - web.add_request(web.REQUEST_LOAD, request.path) + @self.web.app.route("/") + def index(slug_candidate): + self.web.check_slug_candidate(slug_candidate) + return index_logic() - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not web.stay_open and web.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return web.add_security_headers(r) + @self.web.app.route("/") + def index_public(): + if not self.web.common.settings.get('public_mode'): + return self.web.error404() + return index_logic() + + def index_logic(slug_candidate=''): + """ + Render the template for the onionshare landing page. + """ + self.web.add_request(self.web.REQUEST_LOAD, request.path) + + # 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.web.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return self.web.add_security_headers(r) - # If download is allowed to continue, serve download page - if web.slug: - r = make_response(render_template( - 'send.html', - slug=web.slug, - file_info=web.file_info, - filename=os.path.basename(web.download_filename), - filesize=web.download_filesize, - filesize_human=web.common.human_readable_filesize(web.download_filesize), - is_zipped=web.is_zipped)) - else: # If download is allowed to continue, serve download page - r = make_response(render_template( - 'send.html', - file_info=web.file_info, - filename=os.path.basename(web.download_filename), - filesize=web.download_filesize, - filesize_human=web.common.human_readable_filesize(web.download_filesize), - is_zipped=web.is_zipped)) - return web.add_security_headers(r) + if self.web.slug: + r = make_response(render_template( + 'send.html', + slug=self.web.slug, + file_info=self.web.file_info, + filename=os.path.basename(self.web.download_filename), + filesize=self.web.download_filesize, + filesize_human=self.web.common.human_readable_filesize(self.web.download_filesize), + is_zipped=self.web.is_zipped)) + else: + # If download is allowed to continue, serve download page + r = make_response(render_template( + 'send.html', + file_info=self.web.file_info, + filename=os.path.basename(self.web.download_filename), + filesize=self.web.download_filesize, + filesize_human=self.web.common.human_readable_filesize(self.web.download_filesize), + is_zipped=self.web.is_zipped)) + return self.web.add_security_headers(r) - @web.app.route("//download") - def download(slug_candidate): - web.check_slug_candidate(slug_candidate) - return download_logic() + @self.web.app.route("//download") + def download(slug_candidate): + self.web.check_slug_candidate(slug_candidate) + return download_logic() - @web.app.route("/download") - def download_public(): - if not web.common.settings.get('public_mode'): - return web.error404() - return download_logic() + @self.web.app.route("/download") + def download_public(): + if not self.web.common.settings.get('public_mode'): + return self.web.error404() + return download_logic() - def download_logic(slug_candidate=''): - """ - Download the zip file. - """ - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not web.stay_open and web.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return web.add_security_headers(r) + def download_logic(slug_candidate=''): + """ + Download the zip file. + """ + # 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.web.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return self.web.add_security_headers(r) - # Each download has a unique id - download_id = web.download_count - web.download_count += 1 + # Each download has a unique id + download_id = self.web.download_count + self.web.download_count += 1 - # Prepare some variables to use inside generate() function below - # which is outside of the request context - shutdown_func = request.environ.get('werkzeug.server.shutdown') - path = request.path + # Prepare some variables to use inside generate() function below + # which is outside of the request context + shutdown_func = request.environ.get('werkzeug.server.shutdown') + path = request.path - # Tell GUI the download started - web.add_request(web.REQUEST_STARTED, path, { - 'id': download_id} - ) + # Tell GUI the download started + self.web.add_request(self.web.REQUEST_STARTED, path, { + 'id': download_id} + ) - dirname = os.path.dirname(web.download_filename) - basename = os.path.basename(web.download_filename) + dirname = os.path.dirname(self.web.download_filename) + basename = os.path.basename(self.web.download_filename) - def generate(): - # The user hasn't canceled the download - web.client_cancel = False + def generate(): + # The user hasn't canceled the download + self.web.client_cancel = False - # Starting a new download - if not web.stay_open: - web.download_in_progress = True + # Starting a new download + if not self.web.stay_open: + self.web.download_in_progress = True - chunk_size = 102400 # 100kb + chunk_size = 102400 # 100kb - fp = open(web.download_filename, 'rb') - web.done = False - canceled = False - while not web.done: - # The user has canceled the download, so stop serving the file - if web.client_cancel: - web.add_request(web.REQUEST_CANCELED, path, { - 'id': download_id - }) - break - - chunk = fp.read(chunk_size) - if chunk == b'': - web.done = True - else: - try: - yield chunk - - # tell GUI the progress - downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / web.download_filesize) * 100 - - # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) - if not web.gui_mode or web.common.platform == 'Linux' or web.common.platform == 'BSD': - sys.stdout.write( - "\r{0:s}, {1:.2f}% ".format(web.common.human_readable_filesize(downloaded_bytes), percent)) - sys.stdout.flush() - - web.add_request(web.REQUEST_PROGRESS, path, { - 'id': download_id, - 'bytes': downloaded_bytes - }) - web.done = False - except: - # looks like the download was canceled - web.done = True - canceled = True - - # tell the GUI the download has canceled - web.add_request(web.REQUEST_CANCELED, path, { + fp = open(self.web.download_filename, 'rb') + self.web.done = False + canceled = False + while not self.web.done: + # The user has canceled the download, so stop serving the file + if self.web.client_cancel: + self.web.add_request(self.web.REQUEST_CANCELED, path, { 'id': download_id }) + break - fp.close() + chunk = fp.read(chunk_size) + if chunk == b'': + self.web.done = True + else: + try: + yield chunk - if web.common.platform != 'Darwin': - sys.stdout.write("\n") + # tell GUI the progress + downloaded_bytes = fp.tell() + percent = (1.0 * downloaded_bytes / self.web.download_filesize) * 100 - # Download is finished - if not web.stay_open: - web.download_in_progress = False + # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) + if not self.web.is_gui or self.web.common.platform == 'Linux' or self.web.common.platform == 'BSD': + sys.stdout.write( + "\r{0:s}, {1:.2f}% ".format(self.web.common.human_readable_filesize(downloaded_bytes), percent)) + sys.stdout.flush() - # Close the server, if necessary - if not web.stay_open and not canceled: - print(strings._("closing_automatically")) - web.running = False - try: - if shutdown_func is None: - raise RuntimeError('Not running with the Werkzeug Server') - shutdown_func() - except: - pass + self.web.add_request(self.web.REQUEST_PROGRESS, path, { + 'id': download_id, + 'bytes': downloaded_bytes + }) + self.web.done = False + except: + # looks like the download was canceled + self.web.done = True + canceled = True - r = Response(generate()) - r.headers.set('Content-Length', web.download_filesize) - r.headers.set('Content-Disposition', 'attachment', filename=basename) - r = web.add_security_headers(r) - # guess content type - (content_type, _) = mimetypes.guess_type(basename, strict=False) - if content_type is not None: - r.headers.set('Content-Type', content_type) - return r + # tell the GUI the download has canceled + self.web.add_request(self.web.REQUEST_CANCELED, path, { + 'id': download_id + }) + + fp.close() + + if self.web.common.platform != 'Darwin': + sys.stdout.write("\n") + + # Download is finished + if not self.web.stay_open: + self.web.download_in_progress = False + + # Close the server, if necessary + if not self.web.stay_open and not canceled: + print(strings._("closing_automatically")) + self.web.running = False + try: + if shutdown_func is None: + raise RuntimeError('Not running with the Werkzeug Server') + shutdown_func() + except: + pass + + r = Response(generate()) + r.headers.set('Content-Length', self.web.download_filesize) + r.headers.set('Content-Disposition', 'attachment', filename=basename) + r = self.web.add_security_headers(r) + # guess content type + (content_type, _) = mimetypes.guess_type(basename, strict=False) + if content_type is not None: + r.headers.set('Content-Type', content_type) + return r + + def set_file_info(self, filenames, processed_size_callback=None): + """ + Using the list of filenames being shared, fill in details that the web + page will need to display. This includes zipping up the file in order to + get the zip file's name and size. + """ + self.web.common.log("Web", "set_file_info") + self.web.cancel_compression = False + + # build file info list + self.web.file_info = {'files': [], 'dirs': []} + for filename in filenames: + info = { + 'filename': filename, + 'basename': os.path.basename(filename.rstrip('/')) + } + if os.path.isfile(filename): + info['size'] = os.path.getsize(filename) + info['size_human'] = self.web.common.human_readable_filesize(info['size']) + self.web.file_info['files'].append(info) + if os.path.isdir(filename): + info['size'] = self.web.common.dir_size(filename) + info['size_human'] = self.web.common.human_readable_filesize(info['size']) + self.web.file_info['dirs'].append(info) + self.web.file_info['files'] = sorted(self.web.file_info['files'], key=lambda k: k['basename']) + self.web.file_info['dirs'] = sorted(self.web.file_info['dirs'], key=lambda k: k['basename']) + + # Check if there's only 1 file and no folders + if len(self.web.file_info['files']) == 1 and len(self.web.file_info['dirs']) == 0: + self.web.is_zipped = False + self.web.download_filename = self.web.file_info['files'][0]['filename'] + self.web.download_filesize = self.web.file_info['files'][0]['size'] + else: + # Zip up the files and folders + self.web.zip_writer = ZipWriter(self.web.common, processed_size_callback=processed_size_callback) + self.web.download_filename = self.web.zip_writer.zip_filename + for info in self.web.file_info['files']: + self.web.zip_writer.add_file(info['filename']) + # Canceling early? + if self.web.cancel_compression: + self.web.zip_writer.close() + return False + + for info in self.web.file_info['dirs']: + if not self.web.zip_writer.add_dir(info['filename']): + return False + + self.web.zip_writer.close() + self.web.download_filesize = os.path.getsize(self.web.download_filename) + self.web.is_zipped = True + + return True class ZipWriter(object): diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 0a6e6964..7959ae0f 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -13,8 +13,8 @@ from flask import Flask, request, render_template, abort, make_response, __versi from .. import strings -from .share_mode import share_routes, ZipWriter -from .receive_mode import receive_routes, ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest +from .share_mode import ShareModeWeb +from .receive_mode import ReceiveModeWeb, ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest # Stub out flask's show_server_banner function, to avoiding showing warnings that @@ -41,7 +41,7 @@ class Web(object): REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE = 9 REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE = 10 - def __init__(self, common, gui_mode, receive_mode=False): + def __init__(self, common, is_gui, mode='share'): self.common = common # The flask app @@ -55,11 +55,11 @@ class Web(object): self.debug_mode() # Are we running in GUI mode? - self.gui_mode = gui_mode + self.is_gui = is_gui # Are we using receive mode? - self.receive_mode = receive_mode - if self.receive_mode: + self.mode = mode + if self.mode == 'receive': # Use custom WSGI middleware, to modify environ self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self) # Use a custom Request class to track upload progess @@ -115,14 +115,19 @@ class Web(object): # Keep track if the server is running self.running = False - # Define the ewb app routes - self.common_routes() - if self.receive_mode: - receive_routes(self) - else: - share_routes(self) + # Define the web app routes + self.define_common_routes() - def common_routes(self): + # Create the mode web object, which defines its own routes + self.share_mode = None + self.receive_mode = None + if self.mode == 'receive': + self.receive_mode = ReceiveModeWeb(self) + elif self.mode == 'share': + self.share_mode = ShareModeWeb(self) + + + def define_common_routes(self): """ Common web app routes between sending and receiving """ @@ -165,59 +170,6 @@ class Web(object): r.headers.set(header, value) return r - def set_file_info(self, filenames, processed_size_callback=None): - """ - Using the list of filenames being shared, fill in details that the web - page will need to display. This includes zipping up the file in order to - get the zip file's name and size. - """ - self.common.log("Web", "set_file_info") - self.cancel_compression = False - - # build file info list - self.file_info = {'files': [], 'dirs': []} - for filename in filenames: - info = { - 'filename': filename, - 'basename': os.path.basename(filename.rstrip('/')) - } - if os.path.isfile(filename): - info['size'] = os.path.getsize(filename) - info['size_human'] = self.common.human_readable_filesize(info['size']) - self.file_info['files'].append(info) - if os.path.isdir(filename): - info['size'] = self.common.dir_size(filename) - info['size_human'] = self.common.human_readable_filesize(info['size']) - self.file_info['dirs'].append(info) - self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) - self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) - - # Check if there's only 1 file and no folders - if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: - self.is_zipped = False - self.download_filename = self.file_info['files'][0]['filename'] - self.download_filesize = self.file_info['files'][0]['size'] - else: - # Zip up the files and folders - self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) - self.download_filename = self.zip_writer.zip_filename - for info in self.file_info['files']: - self.zip_writer.add_file(info['filename']) - # Canceling early? - if self.cancel_compression: - self.zip_writer.close() - return False - - for info in self.file_info['dirs']: - if not self.zip_writer.add_dir(info['filename']): - return False - - self.zip_writer.close() - self.download_filesize = os.path.getsize(self.download_filename) - self.is_zipped = True - - return True - def _safe_select_jinja_autoescape(self, filename): if filename is None: return True diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index 8712653b..5845b30a 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -34,7 +34,7 @@ class ReceiveMode(Mode): Custom initialization for ReceiveMode. """ # Create the Web object - self.web = Web(self.common, True, True) + self.web = Web(self.common, True, 'receive') # Server status self.server_status.set_mode('receive') diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index aec32305..d7ed74ed 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -43,7 +43,7 @@ class ShareMode(Mode): self.compress_thread = None # Create the Web object - self.web = Web(self.common, True, False) + self.web = Web(self.common, True, 'share') # File selection self.file_selection = FileSelection(self.common) diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py index dc43bf0a..4fb40bd0 100644 --- a/onionshare_gui/share_mode/threads.py +++ b/onionshare_gui/share_mode/threads.py @@ -41,7 +41,7 @@ class CompressThread(QtCore.QThread): self.mode.common.log('CompressThread', 'run') try: - if self.mode.web.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size): + if self.mode.web.share_mode.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size): self.success.emit() else: # Cancelled diff --git a/test/test_onionshare_web.py b/test/test_onionshare_web.py index 2209a0fd..24a0e163 100644 --- a/test/test_onionshare_web.py +++ b/test/test_onionshare_web.py @@ -38,11 +38,11 @@ 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, receive_mode, num_files=0): +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.load_settings() - web = Web(common_obj, False, receive_mode) + web = Web(common_obj, False, mode) web.generate_slug() web.stay_open = True web.running = True @@ -50,14 +50,14 @@ def web_obj(common_obj, receive_mode, num_files=0): web.app.testing = True # Share mode - if not receive_mode: + if mode == 'share': # Add files files = [] for i in range(num_files): with tempfile.NamedTemporaryFile(delete=False) as tmp_file: tmp_file.write(b'*' * 1024) files.append(tmp_file.name) - web.set_file_info(files) + web.share_mode.set_file_info(files) # Receive mode else: pass @@ -67,8 +67,8 @@ def web_obj(common_obj, receive_mode, num_files=0): class TestWeb: def test_share_mode(self, common_obj): - web = web_obj(common_obj, False, 3) - assert web.receive_mode is False + web = web_obj(common_obj, 'share', 3) + assert web.mode is 'share' with web.app.test_client() as c: # Load 404 pages res = c.get('/') @@ -91,7 +91,7 @@ class TestWeb: assert res.mimetype == 'application/zip' def test_share_mode_close_after_first_download_on(self, common_obj, temp_file_1024): - web = web_obj(common_obj, False, 3) + web = web_obj(common_obj, 'share', 3) web.stay_open = False assert web.running == True @@ -106,7 +106,7 @@ class TestWeb: assert web.running == False def test_share_mode_close_after_first_download_off(self, common_obj, temp_file_1024): - web = web_obj(common_obj, False, 3) + web = web_obj(common_obj, 'share', 3) web.stay_open = True assert web.running == True @@ -120,8 +120,8 @@ class TestWeb: assert web.running == True def test_receive_mode(self, common_obj): - web = web_obj(common_obj, True) - assert web.receive_mode is True + web = web_obj(common_obj, 'receive') + assert web.mode is 'receive' with web.app.test_client() as c: # Load 404 pages @@ -139,7 +139,7 @@ class TestWeb: assert res.status_code == 200 def test_receive_mode_allow_receiver_shutdown_on(self, common_obj): - web = web_obj(common_obj, True) + web = web_obj(common_obj, 'receive') common_obj.settings.set('receive_allow_receiver_shutdown', True) @@ -154,7 +154,7 @@ class TestWeb: assert web.running == False def test_receive_mode_allow_receiver_shutdown_off(self, common_obj): - web = web_obj(common_obj, True) + web = web_obj(common_obj, 'receive') common_obj.settings.set('receive_allow_receiver_shutdown', False) @@ -167,9 +167,9 @@ class TestWeb: # Should redirect to index, and server should still be running assert res.status_code == 302 assert web.running == True - + def test_public_mode_on(self, common_obj): - web = web_obj(common_obj, True) + web = web_obj(common_obj, 'receive') common_obj.settings.set('public_mode', True) with web.app.test_client() as c: @@ -182,9 +182,9 @@ class TestWeb: res = c.get('/{}'.format(web.slug)) data2 = res.get_data() assert res.status_code == 404 - + def test_public_mode_off(self, common_obj): - web = web_obj(common_obj, True) + web = web_obj(common_obj, 'receive') common_obj.settings.set('public_mode', False) with web.app.test_client() as c: From a86681e9038fffcd6d6cde66ed1cb10c87783d2f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 11:19:36 -0700 Subject: [PATCH 031/123] Refactor the CLI main function to explicitly use 'share' or 'receive' mode --- onionshare/__init__.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 9c390fa8..64655f4a 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -65,13 +65,18 @@ def main(cwd=None): receive = bool(args.receive) config = args.config + if receive: + mode = 'receive' + else: + mode = 'share' + # Make sure filenames given if not using receiver mode - if not receive and len(filenames) == 0: + if mode == 'share' and len(filenames) == 0: print(strings._('no_filenames')) sys.exit() # Validate filenames - if not receive: + if mode == 'share': valid = True for filename in filenames: if not os.path.isfile(filename) and not os.path.isdir(filename): @@ -90,7 +95,7 @@ def main(cwd=None): common.debug = debug # Create the Web object - web = Web(common, False, receive) + web = Web(common, False, mode) # Start the Onion object onion = Onion(common) @@ -116,21 +121,22 @@ def main(cwd=None): print(e.args[0]) sys.exit() - # Prepare files to share - print(strings._("preparing_files")) - try: - web.share_mode.set_file_info(filenames) - if web.is_zipped: - app.cleanup_filenames.append(web.download_filename) - except OSError as e: - print(e.strerror) - sys.exit(1) + if mode == 'share': + # Prepare files to share + print(strings._("preparing_files")) + try: + web.share_mode.set_file_info(filenames) + if web.is_zipped: + app.cleanup_filenames.append(web.download_filename) + except OSError as e: + print(e.strerror) + sys.exit(1) - # Warn about sending large files over Tor - if web.download_filesize >= 157286400: # 150mb - print('') - print(strings._("large_filesize")) - print('') + # Warn about sending large files over Tor + if web.download_filesize >= 157286400: # 150mb + print('') + print(strings._("large_filesize")) + print('') # Start OnionShare http service in new thread t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), common.settings.get('slug'))) @@ -158,7 +164,7 @@ def main(cwd=None): url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug) print('') - if receive: + if mode == 'receive': print(strings._('receive_mode_downloads_dir').format(common.settings.get('downloads_dir'))) print('') print(strings._('receive_mode_warning')) From 28fd67cbccfa205aeffee710b6ec3e5f5e6bf0b2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 11:36:19 -0700 Subject: [PATCH 032/123] Move more mode-specific logic out of the Web class and into the approprate mode web classes --- onionshare/__init__.py | 17 ++-- onionshare/web/receive_mode.py | 7 +- onionshare/web/share_mode.py | 103 ++++++++++++++---------- onionshare/web/web.py | 28 +------ onionshare_gui/receive_mode/__init__.py | 2 +- onionshare_gui/share_mode/__init__.py | 10 +-- onionshare_gui/share_mode/threads.py | 4 +- 7 files changed, 87 insertions(+), 84 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 64655f4a..9e3fefdd 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -126,14 +126,14 @@ def main(cwd=None): print(strings._("preparing_files")) try: web.share_mode.set_file_info(filenames) - if web.is_zipped: - app.cleanup_filenames.append(web.download_filename) + if web.share_mode.is_zipped: + app.cleanup_filenames.append(web.share_mode.download_filename) except OSError as e: print(e.strerror) sys.exit(1) # Warn about sending large files over Tor - if web.download_filesize >= 157286400: # 150mb + if web.share_mode.download_filesize >= 157286400: # 150mb print('') print(strings._("large_filesize")) print('') @@ -193,11 +193,12 @@ def main(cwd=None): if app.shutdown_timeout > 0: # if the shutdown timer was set and has run out, stop the server if not app.shutdown_timer.is_alive(): - # If there were no attempts to download the share, or all downloads are done, we can stop - if web.download_count == 0 or web.done: - print(strings._("close_on_timeout")) - web.stop(app.port) - break + if mode == 'share': + # If there were no attempts to download the share, or all downloads are done, we can stop + if web.share_mode.download_count == 0 or web.done: + print(strings._("close_on_timeout")) + web.stop(app.port) + break # Allow KeyboardInterrupt exception to be handled with threads # https://stackoverflow.com/questions/3788208/python-threading-ignores-keyboardinterrupt-exception time.sleep(0.2) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index ab5f5f13..3784ebf8 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -14,6 +14,9 @@ class ReceiveModeWeb(object): """ def __init__(self, web): self.web = web + + self.upload_count = 0 + self.define_routes() def define_routes(self): @@ -243,8 +246,8 @@ class ReceiveModeRequest(Request): self.progress = {} # Create an upload_id, attach it to the request - self.upload_id = self.web.upload_count - self.web.upload_count += 1 + self.upload_id = self.upload_count + self.upload_count += 1 # Figure out the content length try: diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index c8a411bb..21f0d1e5 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -14,6 +14,25 @@ class ShareModeWeb(object): """ def __init__(self, web): self.web = web + + # Information about the file to be shared + self.file_info = [] + self.is_zipped = False + self.download_filename = None + self.download_filesize = None + self.zip_writer = None + + self.download_count = 0 + + # If "Stop After First Download" is checked (stay_open == False), only allow + # one download at a time. + self.download_in_progress = False + + # If the client closes the OnionShare window while a download is in progress, + # it should immediately stop serving the file. The client_cancel global is + # used to tell the download function that the client is canceling the download. + self.client_cancel = False + self.define_routes() def define_routes(self): @@ -39,7 +58,7 @@ class ShareModeWeb(object): # 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.web.download_in_progress + deny_download = not self.web.stay_open and self.download_in_progress if deny_download: r = make_response(render_template('denied.html')) return self.web.add_security_headers(r) @@ -49,20 +68,20 @@ class ShareModeWeb(object): r = make_response(render_template( 'send.html', slug=self.web.slug, - file_info=self.web.file_info, - filename=os.path.basename(self.web.download_filename), - filesize=self.web.download_filesize, - filesize_human=self.web.common.human_readable_filesize(self.web.download_filesize), - is_zipped=self.web.is_zipped)) + file_info=self.file_info, + filename=os.path.basename(self.download_filename), + filesize=self.download_filesize, + filesize_human=self.web.common.human_readable_filesize(self.download_filesize), + is_zipped=self.is_zipped)) else: # If download is allowed to continue, serve download page r = make_response(render_template( 'send.html', - file_info=self.web.file_info, - filename=os.path.basename(self.web.download_filename), - filesize=self.web.download_filesize, - filesize_human=self.web.common.human_readable_filesize(self.web.download_filesize), - is_zipped=self.web.is_zipped)) + file_info=self.file_info, + filename=os.path.basename(self.download_filename), + filesize=self.download_filesize, + filesize_human=self.web.common.human_readable_filesize(self.download_filesize), + is_zipped=self.is_zipped)) return self.web.add_security_headers(r) @self.web.app.route("//download") @@ -82,14 +101,14 @@ class ShareModeWeb(object): """ # 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.web.download_in_progress + deny_download = not self.web.stay_open and self.download_in_progress if deny_download: r = make_response(render_template('denied.html')) return self.web.add_security_headers(r) # Each download has a unique id - download_id = self.web.download_count - self.web.download_count += 1 + download_id = self.download_count + self.download_count += 1 # Prepare some variables to use inside generate() function below # which is outside of the request context @@ -101,25 +120,25 @@ class ShareModeWeb(object): 'id': download_id} ) - dirname = os.path.dirname(self.web.download_filename) - basename = os.path.basename(self.web.download_filename) + dirname = os.path.dirname(self.download_filename) + basename = os.path.basename(self.download_filename) def generate(): # The user hasn't canceled the download - self.web.client_cancel = False + self.client_cancel = False # Starting a new download if not self.web.stay_open: - self.web.download_in_progress = True + self.download_in_progress = True chunk_size = 102400 # 100kb - fp = open(self.web.download_filename, 'rb') + fp = open(self.download_filename, 'rb') self.web.done = False canceled = False while not self.web.done: # The user has canceled the download, so stop serving the file - if self.web.client_cancel: + if self.client_cancel: self.web.add_request(self.web.REQUEST_CANCELED, path, { 'id': download_id }) @@ -134,7 +153,7 @@ class ShareModeWeb(object): # tell GUI the progress downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / self.web.download_filesize) * 100 + percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) if not self.web.is_gui or self.web.common.platform == 'Linux' or self.web.common.platform == 'BSD': @@ -164,7 +183,7 @@ class ShareModeWeb(object): # Download is finished if not self.web.stay_open: - self.web.download_in_progress = False + self.download_in_progress = False # Close the server, if necessary if not self.web.stay_open and not canceled: @@ -178,7 +197,7 @@ class ShareModeWeb(object): pass r = Response(generate()) - r.headers.set('Content-Length', self.web.download_filesize) + r.headers.set('Content-Length', self.download_filesize) r.headers.set('Content-Disposition', 'attachment', filename=basename) r = self.web.add_security_headers(r) # guess content type @@ -197,7 +216,7 @@ class ShareModeWeb(object): self.web.cancel_compression = False # build file info list - self.web.file_info = {'files': [], 'dirs': []} + self.file_info = {'files': [], 'dirs': []} for filename in filenames: info = { 'filename': filename, @@ -206,37 +225,37 @@ class ShareModeWeb(object): if os.path.isfile(filename): info['size'] = os.path.getsize(filename) info['size_human'] = self.web.common.human_readable_filesize(info['size']) - self.web.file_info['files'].append(info) + self.file_info['files'].append(info) if os.path.isdir(filename): info['size'] = self.web.common.dir_size(filename) info['size_human'] = self.web.common.human_readable_filesize(info['size']) - self.web.file_info['dirs'].append(info) - self.web.file_info['files'] = sorted(self.web.file_info['files'], key=lambda k: k['basename']) - self.web.file_info['dirs'] = sorted(self.web.file_info['dirs'], key=lambda k: k['basename']) + self.file_info['dirs'].append(info) + self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) + self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) # Check if there's only 1 file and no folders - if len(self.web.file_info['files']) == 1 and len(self.web.file_info['dirs']) == 0: - self.web.is_zipped = False - self.web.download_filename = self.web.file_info['files'][0]['filename'] - self.web.download_filesize = self.web.file_info['files'][0]['size'] + if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: + self.is_zipped = False + self.download_filename = self.file_info['files'][0]['filename'] + self.download_filesize = self.file_info['files'][0]['size'] else: # Zip up the files and folders - self.web.zip_writer = ZipWriter(self.web.common, processed_size_callback=processed_size_callback) - self.web.download_filename = self.web.zip_writer.zip_filename - for info in self.web.file_info['files']: - self.web.zip_writer.add_file(info['filename']) + self.zip_writer = ZipWriter(self.web.common, processed_size_callback=processed_size_callback) + self.download_filename = self.zip_writer.zip_filename + for info in self.file_info['files']: + self.zip_writer.add_file(info['filename']) # Canceling early? if self.web.cancel_compression: - self.web.zip_writer.close() + self.zip_writer.close() return False - for info in self.web.file_info['dirs']: - if not self.web.zip_writer.add_dir(info['filename']): + for info in self.file_info['dirs']: + if not self.zip_writer.add_dir(info['filename']): return False - self.web.zip_writer.close() - self.web.download_filesize = os.path.getsize(self.web.download_filename) - self.web.is_zipped = True + self.zip_writer.close() + self.download_filesize = os.path.getsize(self.download_filename) + self.is_zipped = True return True diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 7959ae0f..9046154a 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -73,13 +73,6 @@ class Web(object): # Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape - # Information about the file - self.file_info = [] - self.is_zipped = False - self.download_filename = None - self.download_filesize = None - self.zip_writer = None - self.security_headers = [ ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), ('X-Frame-Options', 'DENY'), @@ -90,25 +83,11 @@ class Web(object): ] self.q = queue.Queue() - self.slug = None - - self.download_count = 0 - self.upload_count = 0 - self.error404_count = 0 - # If "Stop After First Download" is checked (stay_open == False), only allow - # one download at a time. - self.download_in_progress = False - self.done = False - # If the client closes the OnionShare window while a download is in progress, - # it should immediately stop serving the file. The client_cancel global is - # used to tell the download function that the client is canceling the download. - self.client_cancel = False - # shutting down the server only works within the context of flask, so the easiest way to do it is over http self.shutdown_slug = self.common.random_string(16) @@ -254,9 +233,10 @@ class Web(object): Stop the flask web server by loading /shutdown. """ - # If the user cancels the download, let the download function know to stop - # serving the file - self.client_cancel = True + if self.mode == 'share': + # If the user cancels the download, let the download function know to stop + # serving the file + self.share_mode.client_cancel = True # To stop flask, load http://127.0.0.1://shutdown if self.running: diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index 5845b30a..590dec65 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -100,7 +100,7 @@ class ReceiveMode(Mode): Starting the server. """ # Reset web counters - self.web.upload_count = 0 + self.web.receive_mode.upload_count = 0 self.web.error404_count = 0 # Hide and reset the uploads if we have previously shared diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index d7ed74ed..52ec672e 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -125,7 +125,7 @@ class ShareMode(Mode): The shutdown 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.download_count == 0 or self.web.done: + if self.web.share_mode.download_count == 0 or self.web.done: self.server_status.stop_server() self.server_status_label.setText(strings._('close_on_timeout', True)) return True @@ -139,7 +139,7 @@ class ShareMode(Mode): Starting the server. """ # Reset web counters - self.web.download_count = 0 + self.web.share_mode.download_count = 0 self.web.error404_count = 0 # Hide and reset the downloads if we have previously shared @@ -177,7 +177,7 @@ class ShareMode(Mode): self._zip_progress_bar = None # Warn about sending large files over Tor - if self.web.download_filesize >= 157286400: # 150mb + if self.web.share_mode.download_filesize >= 157286400: # 150mb self.filesize_warning.setText(strings._("large_filesize", True)) self.filesize_warning.show() @@ -229,7 +229,7 @@ class ShareMode(Mode): """ Handle REQUEST_STARTED event. """ - self.downloads.add(event["data"]["id"], self.web.download_filesize) + self.downloads.add(event["data"]["id"], self.web.share_mode.download_filesize) self.downloads_in_progress += 1 self.update_downloads_in_progress() @@ -242,7 +242,7 @@ class ShareMode(Mode): self.downloads.update(event["data"]["id"], event["data"]["bytes"]) # Is the download complete? - if event["data"]["bytes"] == self.web.download_filesize: + if event["data"]["bytes"] == self.web.share_mode.download_filesize: self.system_tray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True)) # Update the total 'completed downloads' info diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py index 4fb40bd0..6e114d62 100644 --- a/onionshare_gui/share_mode/threads.py +++ b/onionshare_gui/share_mode/threads.py @@ -47,8 +47,8 @@ class CompressThread(QtCore.QThread): # Cancelled pass - if self.mode.web.is_zipped: - self.mode.app.cleanup_filenames.append(self.mode.web.download_filename) + if self.mode.web.share_mode.is_zipped: + self.mode.app.cleanup_filenames.append(self.mode.web.share_mode.download_filename) except OSError as e: self.error.emit(e.strerror) From 98aae9d83b73d150a13efe6d315b4e5dd3b10cb2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 11:41:49 -0700 Subject: [PATCH 033/123] Pass common into ShareModeWeb and ReceiveModeWeb --- onionshare/web/receive_mode.py | 35 ++++++++++++++++++---------------- onionshare/web/share_mode.py | 29 +++++++++++++++------------- onionshare/web/web.py | 5 +++-- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 3784ebf8..c422d74e 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -12,7 +12,10 @@ class ReceiveModeWeb(object): """ All of the web logic for receive mode """ - def __init__(self, web): + def __init__(self, common, web): + self.common = common + self.common.log('ReceiveModeWeb', '__init__') + self.web = web self.upload_count = 0 @@ -26,7 +29,7 @@ class ReceiveModeWeb(object): def index_logic(): self.web.add_request(self.web.REQUEST_LOAD, request.path) - if self.web.common.settings.get('public_mode'): + if self.common.settings.get('public_mode'): upload_action = '/upload' close_action = '/close' else: @@ -37,7 +40,7 @@ class ReceiveModeWeb(object): 'receive.html', upload_action=upload_action, close_action=close_action, - receive_allow_receiver_shutdown=self.web.common.settings.get('receive_allow_receiver_shutdown'))) + receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown'))) return self.web.add_security_headers(r) @self.web.app.route("/") @@ -47,7 +50,7 @@ class ReceiveModeWeb(object): @self.web.app.route("/") def index_public(): - if not self.web.common.settings.get('public_mode'): + if not self.common.settings.get('public_mode'): return self.web.error404() return index_logic() @@ -59,18 +62,18 @@ class ReceiveModeWeb(object): # Make sure downloads_dir exists valid = True try: - self.web.common.validate_downloads_dir() + self.common.validate_downloads_dir() except DownloadsDirErrorCannotCreate: self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) - print(strings._('error_cannot_create_downloads_dir').format(self.web.common.settings.get('downloads_dir'))) + print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir'))) valid = False except DownloadsDirErrorNotWritable: self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) - print(strings._('error_downloads_dir_not_writable').format(self.web.common.settings.get('downloads_dir'))) + print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir'))) valid = False if not valid: flash('Error uploading, please inform the OnionShare user', 'error') - if self.web.common.settings.get('public_mode'): + if self.common.settings.get('public_mode'): return redirect('/') else: return redirect('/{}'.format(slug_candidate)) @@ -83,7 +86,7 @@ class ReceiveModeWeb(object): # Automatically rename the file, if a file of the same name already exists filename = secure_filename(f.filename) filenames.append(filename) - local_path = os.path.join(self.web.common.settings.get('downloads_dir'), filename) + local_path = os.path.join(self.common.settings.get('downloads_dir'), filename) if os.path.exists(local_path): if '.' in filename: # Add "-i", e.g. change "foo.txt" to "foo-2.txt" @@ -95,7 +98,7 @@ class ReceiveModeWeb(object): valid = False while not valid: new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) - local_path = os.path.join(self.web.common.settings.get('downloads_dir'), new_filename) + local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) if os.path.exists(local_path): i += 1 else: @@ -106,7 +109,7 @@ class ReceiveModeWeb(object): valid = False while not valid: new_filename = '{}-{}'.format(filename, i) - local_path = os.path.join(self.web.common.settings.get('downloads_dir'), new_filename) + local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) if os.path.exists(local_path): i += 1 else: @@ -121,7 +124,7 @@ class ReceiveModeWeb(object): 'new_filename': basename }) - self.web.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) + self.common.log('ReceiveModeWeb', 'define_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) print(strings._('receive_mode_received_file').format(local_path)) f.save(local_path) @@ -133,7 +136,7 @@ class ReceiveModeWeb(object): for filename in filenames: flash('Sent {}'.format(filename), 'info') - if self.web.common.settings.get('public_mode'): + if self.common.settings.get('public_mode'): return redirect('/') else: return redirect('/{}'.format(slug_candidate)) @@ -145,13 +148,13 @@ class ReceiveModeWeb(object): @self.web.app.route("/upload", methods=['POST']) def upload_public(): - if not self.web.common.settings.get('public_mode'): + if not self.common.settings.get('public_mode'): return self.web.error404() return upload_logic() def close_logic(slug_candidate=''): - if self.web.common.settings.get('receive_allow_receiver_shutdown'): + if self.common.settings.get('receive_allow_receiver_shutdown'): self.web.force_shutdown() r = make_response(render_template('closed.html')) self.web.add_request(self.web.REQUEST_CLOSE_SERVER, request.path) @@ -166,7 +169,7 @@ class ReceiveModeWeb(object): @self.web.app.route("/close", methods=['POST']) def close_public(): - if not self.web.common.settings.get('public_mode'): + if not self.common.settings.get('public_mode'): return self.web.error404() return close_logic() diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 21f0d1e5..81e5a5b9 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -12,7 +12,10 @@ class ShareModeWeb(object): """ All of the web logic for share mode """ - def __init__(self, web): + def __init__(self, common, web): + self.common = common + self.common.log('ShareModeWeb', '__init__') + self.web = web # Information about the file to be shared @@ -46,7 +49,7 @@ class ShareModeWeb(object): @self.web.app.route("/") def index_public(): - if not self.web.common.settings.get('public_mode'): + if not self.common.settings.get('public_mode'): return self.web.error404() return index_logic() @@ -71,7 +74,7 @@ class ShareModeWeb(object): file_info=self.file_info, filename=os.path.basename(self.download_filename), filesize=self.download_filesize, - filesize_human=self.web.common.human_readable_filesize(self.download_filesize), + filesize_human=self.common.human_readable_filesize(self.download_filesize), is_zipped=self.is_zipped)) else: # If download is allowed to continue, serve download page @@ -80,7 +83,7 @@ class ShareModeWeb(object): file_info=self.file_info, filename=os.path.basename(self.download_filename), filesize=self.download_filesize, - filesize_human=self.web.common.human_readable_filesize(self.download_filesize), + filesize_human=self.common.human_readable_filesize(self.download_filesize), is_zipped=self.is_zipped)) return self.web.add_security_headers(r) @@ -91,7 +94,7 @@ class ShareModeWeb(object): @self.web.app.route("/download") def download_public(): - if not self.web.common.settings.get('public_mode'): + if not self.common.settings.get('public_mode'): return self.web.error404() return download_logic() @@ -156,9 +159,9 @@ class ShareModeWeb(object): percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) - if not self.web.is_gui or self.web.common.platform == 'Linux' or self.web.common.platform == 'BSD': + if not self.web.is_gui or self.common.platform == 'Linux' or self.common.platform == 'BSD': sys.stdout.write( - "\r{0:s}, {1:.2f}% ".format(self.web.common.human_readable_filesize(downloaded_bytes), percent)) + "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent)) sys.stdout.flush() self.web.add_request(self.web.REQUEST_PROGRESS, path, { @@ -178,7 +181,7 @@ class ShareModeWeb(object): fp.close() - if self.web.common.platform != 'Darwin': + if self.common.platform != 'Darwin': sys.stdout.write("\n") # Download is finished @@ -212,7 +215,7 @@ class ShareModeWeb(object): page will need to display. This includes zipping up the file in order to get the zip file's name and size. """ - self.web.common.log("Web", "set_file_info") + self.common.log("ShareModeWeb", "set_file_info") self.web.cancel_compression = False # build file info list @@ -224,11 +227,11 @@ class ShareModeWeb(object): } if os.path.isfile(filename): info['size'] = os.path.getsize(filename) - info['size_human'] = self.web.common.human_readable_filesize(info['size']) + info['size_human'] = self.common.human_readable_filesize(info['size']) self.file_info['files'].append(info) if os.path.isdir(filename): - info['size'] = self.web.common.dir_size(filename) - info['size_human'] = self.web.common.human_readable_filesize(info['size']) + info['size'] = self.common.dir_size(filename) + info['size_human'] = self.common.human_readable_filesize(info['size']) self.file_info['dirs'].append(info) self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) @@ -240,7 +243,7 @@ class ShareModeWeb(object): self.download_filesize = self.file_info['files'][0]['size'] else: # Zip up the files and folders - self.zip_writer = ZipWriter(self.web.common, processed_size_callback=processed_size_callback) + self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) self.download_filename = self.zip_writer.zip_filename for info in self.file_info['files']: self.zip_writer.add_file(info['filename']) diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 9046154a..52c4da16 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -43,6 +43,7 @@ class Web(object): def __init__(self, common, is_gui, mode='share'): self.common = common + self.common.log('Web', '__init__', 'is_gui={}, mode={}'.format(is_gui, mode)) # The flask app self.app = Flask(__name__, @@ -101,9 +102,9 @@ class Web(object): self.share_mode = None self.receive_mode = None if self.mode == 'receive': - self.receive_mode = ReceiveModeWeb(self) + self.receive_mode = ReceiveModeWeb(self.common, self) elif self.mode == 'share': - self.share_mode = ShareModeWeb(self) + self.share_mode = ShareModeWeb(self.common, self) def define_common_routes(self): From 2a309af6801ad6a54b3da89d2687a56c9200a517 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 12:29:23 -0700 Subject: [PATCH 034/123] If only sharing one file, compress it with gzip, and serve it with gzip compression if the browser supports it --- onionshare/__init__.py | 3 +- onionshare/web/share_mode.py | 62 +++++++++++++++++++++++++--- onionshare_gui/share_mode/threads.py | 3 +- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 9e3fefdd..4d6d77d0 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -126,8 +126,7 @@ def main(cwd=None): print(strings._("preparing_files")) try: web.share_mode.set_file_info(filenames) - if web.share_mode.is_zipped: - app.cleanup_filenames.append(web.share_mode.download_filename) + app.cleanup_filenames += web.share_mode.cleanup_filenames except OSError as e: print(e.strerror) sys.exit(1) diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 81e5a5b9..95bc8443 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -3,6 +3,7 @@ import sys import tempfile import zipfile import mimetypes +import gzip from flask import Response, request, render_template, make_response from .. import strings @@ -23,6 +24,7 @@ class ShareModeWeb(object): self.is_zipped = False self.download_filename = None self.download_filesize = None + self.gzip_filename = None self.zip_writer = None self.download_count = 0 @@ -118,12 +120,20 @@ class ShareModeWeb(object): shutdown_func = request.environ.get('werkzeug.server.shutdown') path = request.path + # If this is a zipped file, then serve as-is. If it's not zipped, then, + # if the http client supports gzip compression, gzip the file first + # and serve that + use_gzip = (not self.is_zipped) and ('gzip' in request.headers.get('Accept-Encoding', '').lower()) + if use_gzip: + file_to_download = self.gzip_filename + else: + file_to_download = self.download_filename + # Tell GUI the download started self.web.add_request(self.web.REQUEST_STARTED, path, { - 'id': download_id} - ) + 'id': download_id + }) - dirname = os.path.dirname(self.download_filename) basename = os.path.basename(self.download_filename) def generate(): @@ -136,7 +146,7 @@ class ShareModeWeb(object): chunk_size = 102400 # 100kb - fp = open(self.download_filename, 'rb') + fp = open(file_to_download, 'rb') self.web.done = False canceled = False while not self.web.done: @@ -200,7 +210,11 @@ class ShareModeWeb(object): pass r = Response(generate()) - r.headers.set('Content-Length', self.download_filesize) + if use_gzip: + r.headers.set('Content-Encoding', 'gzip') + r.headers.set('Content-Length', os.path.getsize(self.gzip_filename)) + else: + r.headers.set('Content-Length', self.download_filesize) r.headers.set('Content-Disposition', 'attachment', filename=basename) r = self.web.add_security_headers(r) # guess content type @@ -218,6 +232,8 @@ class ShareModeWeb(object): self.common.log("ShareModeWeb", "set_file_info") self.web.cancel_compression = False + self.cleanup_filenames = [] + # build file info list self.file_info = {'files': [], 'dirs': []} for filename in filenames: @@ -238,9 +254,18 @@ class ShareModeWeb(object): # Check if there's only 1 file and no folders if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: - self.is_zipped = False self.download_filename = self.file_info['files'][0]['filename'] self.download_filesize = self.file_info['files'][0]['size'] + + # Compress the file with gzip now, so we don't have to do it on each request + self.gzip_filename = tempfile.mkstemp('wb+')[1] + self._gzip_compress(self.download_filename, self.gzip_filename, 6, processed_size_callback) + + # Make sure the gzip file gets cleaned up when onionshare stops + self.cleanup_filenames.append(self.gzip_filename) + + self.is_zipped = False + else: # Zip up the files and folders self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) @@ -258,10 +283,35 @@ class ShareModeWeb(object): self.zip_writer.close() self.download_filesize = os.path.getsize(self.download_filename) + + # Make sure the zip file gets cleaned up when onionshare stops + self.cleanup_filenames.append(self.zip_writer.zip_filename) + self.is_zipped = True return True + def _gzip_compress(self, input_filename, output_filename, level, processed_size_callback=None): + """ + Compress a file with gzip, without loading the whole thing into memory + Thanks: https://stackoverflow.com/questions/27035296/python-how-to-gzip-a-large-text-file-without-memoryerror + """ + bytes_processed = 0 + blocksize = 1 << 16 # 64kB + with open(input_filename, 'rb') as input_file: + output_file = gzip.open(output_filename, 'wb', level) + while True: + if processed_size_callback is not None: + processed_size_callback(bytes_processed) + + block = input_file.read(blocksize) + if len(block) == 0: + break + output_file.write(block) + bytes_processed += blocksize + + output_file.close() + class ZipWriter(object): """ diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py index 6e114d62..d6022746 100644 --- a/onionshare_gui/share_mode/threads.py +++ b/onionshare_gui/share_mode/threads.py @@ -47,8 +47,7 @@ class CompressThread(QtCore.QThread): # Cancelled pass - if self.mode.web.share_mode.is_zipped: - self.mode.app.cleanup_filenames.append(self.mode.web.share_mode.download_filename) + self.mode.app.cleanup_filenames += self.mode.web.share_mode.cleanup_filenames except OSError as e: self.error.emit(e.strerror) From 44f408c9ac46094c3f987de4313afbf156710c8a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 13:38:01 -0700 Subject: [PATCH 035/123] Remove unused wait_for_hs string, and change tests to test with a different string. Also remove a few other unused strings --- share/locale/cs.json | 1 - share/locale/da.json | 3 --- share/locale/de.json | 1 - share/locale/en.json | 1 - share/locale/eo.json | 1 - share/locale/es.json | 1 - share/locale/fi.json | 1 - share/locale/fr.json | 1 - share/locale/it.json | 1 - share/locale/nl.json | 3 --- share/locale/tr.json | 1 - test/test_onionshare_strings.py | 12 ++---------- 12 files changed, 2 insertions(+), 25 deletions(-) diff --git a/share/locale/cs.json b/share/locale/cs.json index 40e48f87..a29c8b8e 100644 --- a/share/locale/cs.json +++ b/share/locale/cs.json @@ -1,7 +1,6 @@ { "config_onion_service": "Nastavuji onion service na portu {0:d}.", "preparing_files": "Připravuji soubory ke sdílení.", - "wait_for_hs": "Čekám na HS až bude připravena:", "give_this_url": "Dejte tuto URL osobě, které dané soubory posíláte:", "give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:", "ctrlc_to_stop": "Stiskněte Ctrl-C pro zastavení serveru", diff --git a/share/locale/da.json b/share/locale/da.json index 00539212..75416989 100644 --- a/share/locale/da.json +++ b/share/locale/da.json @@ -1,7 +1,6 @@ { "config_onion_service": "Konfigurerer onion-tjeneste på port {0:d}.", "preparing_files": "Forbereder filer som skal deles.", - "wait_for_hs": "Venter på at HS bliver klar:", "give_this_url": "Giv denne URL til personen du sender filen til:", "give_this_url_stealth": "Giv denne URL og HidServAuth-linje til personen du sender filen til:", "ctrlc_to_stop": "Tryk på Ctrl-C for at stoppe serveren", @@ -53,9 +52,7 @@ "error_stealth_not_supported": "For at oprette usynlige onion-tjenester, skal du mindst have Tor 0.2.9.1-alpha (eller Tor Browser 6.5) og mindst python3-stem 1.5.0.", "error_ephemeral_not_supported": "OnionShare kræver mindst Tor 0.2.7.1 og mindst python3-stem 1.4.0.", "gui_settings_window_title": "Indstillinger", - "gui_settings_stealth_label": "Usynlig (avanceret)", "gui_settings_stealth_option": "Opret usynlige onion-tjenester", - "gui_settings_stealth_option_details": "Det gør OnionShare mere sikker, men også mere besværlig for modtageren at oprette forbindelse til den.
Mere information.", "gui_settings_stealth_hidservauth_string": "Du har gemt den private nøgle til at blive brugt igen, så din HidServAuth-streng bruges også igen.\nKlik nedenfor, for at kopiere HidServAuth.", "gui_settings_autoupdate_label": "Søg efter opdateringer", "gui_settings_autoupdate_option": "Giv mig besked når der findes opdateringer", diff --git a/share/locale/de.json b/share/locale/de.json index 6c0fa861..1d0436a0 100644 --- a/share/locale/de.json +++ b/share/locale/de.json @@ -1,6 +1,5 @@ { "preparing_files": "Dateien werden vorbereitet.", - "wait_for_hs": "Warte auf HS:", "give_this_url": "Geben Sie diese URL der Person, der Sie die Datei zusenden möchten:", "ctrlc_to_stop": "Drücken Sie Strg+C um den Server anzuhalten", "not_a_file": "{0:s} ist keine Datei.", diff --git a/share/locale/en.json b/share/locale/en.json index e6b2b2c0..fbf83118 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -1,7 +1,6 @@ { "config_onion_service": "Configuring onion service on port {0:d}.", "preparing_files": "Preparing files to share.", - "wait_for_hs": "Waiting for HS to be ready:", "give_this_url": "Give this address to the person you're sending the file to:", "give_this_url_stealth": "Give this address and HidServAuth line to the person you're sending the file to:", "give_this_url_receive": "Give this address to the people sending you files:", diff --git a/share/locale/eo.json b/share/locale/eo.json index 18e73165..1fbda9bc 100644 --- a/share/locale/eo.json +++ b/share/locale/eo.json @@ -1,7 +1,6 @@ { "config_onion_service": "Agordas onion service je pordo {0:d}.", "preparing_files": "Preparas dosierojn por kundivido.", - "wait_for_hs": "Atendas al hidden sevice por esti preta:", "give_this_url": "Donu ĉi tiun URL al la persono al kiu vi sendas la dosieron:", "give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:", "ctrlc_to_stop": "Presu Ctrl-C por halti la servilon", diff --git a/share/locale/es.json b/share/locale/es.json index b829540a..8c9945a8 100644 --- a/share/locale/es.json +++ b/share/locale/es.json @@ -1,6 +1,5 @@ { "preparing_files": "Preparando los archivos para compartir.", - "wait_for_hs": "Esperando a que HS esté listo:", "give_this_url": "Entregue esta URL a la persona a la que está enviando el archivo:", "ctrlc_to_stop": "Pulse Ctrl-C para detener el servidor", "not_a_file": "{0:s} no es un archivo.", diff --git a/share/locale/fi.json b/share/locale/fi.json index 09186be8..8a360284 100644 --- a/share/locale/fi.json +++ b/share/locale/fi.json @@ -1,6 +1,5 @@ { "preparing_files": "Valmistellaan tiedostoja jaettavaksi.", - "wait_for_hs": "Odotetaan piilopalvelun valmistumista:", "give_this_url": "Anna tämä URL-osoite henkilölle, jolle lähetät tiedostot:", "ctrlc_to_stop": "Näppäin Ctrl-C pysäyttää palvelimen", "not_a_file": "{0:s} Ei ole tiedosto.", diff --git a/share/locale/fr.json b/share/locale/fr.json index b6f6eaa7..967e456e 100644 --- a/share/locale/fr.json +++ b/share/locale/fr.json @@ -1,6 +1,5 @@ { "preparing_files": "Préparation des fichiers à partager.", - "wait_for_hs": "En attente du HS:", "give_this_url": "Donnez cette URL à la personne qui doit recevoir le fichier :", "ctrlc_to_stop": "Ctrl-C arrête le serveur", "not_a_file": "{0:s} n'est pas un fichier.", diff --git a/share/locale/it.json b/share/locale/it.json index 304e0cb9..6c55ed20 100644 --- a/share/locale/it.json +++ b/share/locale/it.json @@ -1,6 +1,5 @@ { "preparing_files": "Preparazione dei files da condividere.", - "wait_for_hs": "In attesa che l'HS sia pronto:", "give_this_url": "Dai questo URL alla persona a cui vuoi inviare il file:", "ctrlc_to_stop": "Premi Ctrl-C per fermare il server", "not_a_file": "{0:s} non è un file.", diff --git a/share/locale/nl.json b/share/locale/nl.json index 67297ae0..833432ab 100644 --- a/share/locale/nl.json +++ b/share/locale/nl.json @@ -1,7 +1,6 @@ { "config_onion_service": "Onion service configureren op poort {0:d}.", "preparing_files": "Bestanden om te delen aan het voorbereiden.", - "wait_for_hs": "Wachten op gereed zijn van HS:", "give_this_url": "Geef deze URL aan de persoon aan wie je dit bestand verzend:", "give_this_url_stealth": "Geef deze URL en de HidServAuth regel aan de persoon aan wie je dit bestand verzend:", "ctrlc_to_stop": "Druk Ctrl-C om de server te stoppen", @@ -51,9 +50,7 @@ "error_stealth_not_supported": "Om een geheime onion service te maken heb je minstens Tor 0.2.9.1-alpha (of Tor Browser 6.5) en minstens python3-stem 1.5.0 nodig.", "error_ephemeral_not_supported": "OnionShare vereist minstens Tor 0.2.7.1 en minstens python3-stem 1.4.0.", "gui_settings_window_title": "Instellingen", - "gui_settings_stealth_label": "Stealth (geavanceerd)", "gui_settings_stealth_option": "Maak stealth onion services", - "gui_settings_stealth_option_details": "Dit maakt OnionShare veiliger, maar ook lastiger voor de ontvanger om te verbinden.
Meer informatie.", "gui_settings_autoupdate_label": "Controleer voor updates", "gui_settings_autoupdate_option": "Notificeer me als er updates beschikbaar zijn", "gui_settings_autoupdate_timestamp": "Laatste controle: {}", diff --git a/share/locale/tr.json b/share/locale/tr.json index 7b531bd6..71f16daa 100644 --- a/share/locale/tr.json +++ b/share/locale/tr.json @@ -1,6 +1,5 @@ { "preparing_files": "Paylaşmak için dosyalar hazırlanıyor.", - "wait_for_hs": "GH hazır olması bekleniyor:", "give_this_url": "Dosyayı gönderdiğin kişiye bu URL'i verin:", "ctrlc_to_stop": "Sunucuyu durdurmak için, Ctrl-C basın", "not_a_file": "{0:s} dosya değil.", diff --git a/test/test_onionshare_strings.py b/test/test_onionshare_strings.py index d1daa1e5..1d0b3206 100644 --- a/test/test_onionshare_strings.py +++ b/test/test_onionshare_strings.py @@ -47,22 +47,14 @@ class TestLoadStrings: self, common_obj, locale_en, sys_onionshare_dev_mode): """ load_strings() loads English by default """ strings.load_strings(common_obj) - assert strings._('wait_for_hs') == "Waiting for HS to be ready:" + assert strings._('preparing_files') == "Preparing files to share." def test_load_strings_loads_other_languages( self, common_obj, locale_fr, sys_onionshare_dev_mode): """ load_strings() loads other languages in different locales """ strings.load_strings(common_obj, "fr") - assert strings._('wait_for_hs') == "En attente du HS:" - - def test_load_partial_strings( - self, common_obj, locale_ru, sys_onionshare_dev_mode): - strings.load_strings(common_obj) - assert strings._("give_this_url") == ( - "Отправьте эту ссылку тому человеку, " - "которому вы хотите передать файл:") - assert strings._('wait_for_hs') == "Waiting for HS to be ready:" + assert strings._('preparing_files') == "Préparation des fichiers à partager." def test_load_invalid_locale( self, common_obj, locale_invalid, sys_onionshare_dev_mode): From eac4e44dc5defc6496da13d9b544572204941ca3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 13:58:42 -0700 Subject: [PATCH 036/123] Remove no_filenames string, and instead display CLI usage if you don't specify filenames --- onionshare/__init__.py | 2 +- share/locale/en.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 51210b6b..0bc0abe5 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -67,7 +67,7 @@ def main(cwd=None): # Make sure filenames given if not using receiver mode if not receive and len(filenames) == 0: - print(strings._('no_filenames')) + parser.print_help() sys.exit() # Validate filenames diff --git a/share/locale/en.json b/share/locale/en.json index fbf83118..512a69b4 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -8,7 +8,6 @@ "ctrlc_to_stop": "Press Ctrl+C to stop the server", "not_a_file": "{0:s} is not a valid file.", "not_a_readable_file": "{0:s} is not a readable file.", - "no_filenames": "You must specify a list of files to share.", "no_available_port": "Could not start the Onion service as there was no available port.", "other_page_loaded": "Address loaded", "close_on_timeout": "Stopped because timer expired", From 98fcf4d0ac50c1940e372c3c6d89f6ae111f1bfe Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 15:11:18 -0700 Subject: [PATCH 037/123] Remove the 'using_ephemeral' string --- onionshare/onion.py | 1 - share/locale/cs.json | 1 - share/locale/da.json | 1 - share/locale/en.json | 1 - share/locale/eo.json | 1 - share/locale/fi.json | 1 - share/locale/it.json | 1 - share/locale/nl.json | 1 - share/locale/tr.json | 1 - 9 files changed, 9 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 7a111eff..81b82923 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -427,7 +427,6 @@ class Onion(object): raise TorTooOld(strings._('error_stealth_not_supported')) print(strings._("config_onion_service").format(int(port))) - print(strings._('using_ephemeral')) if self.stealth: if self.settings.get('hidservauth_string'): diff --git a/share/locale/cs.json b/share/locale/cs.json index a29c8b8e..a595ce67 100644 --- a/share/locale/cs.json +++ b/share/locale/cs.json @@ -26,7 +26,6 @@ "gui_copied_url": "URL zkopírováno do schránky", "gui_copied_hidservauth": "Copied HidServAuth line to clipboard", "gui_please_wait": "Prosím čekejte...", - "using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication", "gui_download_upload_progress_complete": "%p%, Uplynulý čas: {0:s}", "gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", diff --git a/share/locale/da.json b/share/locale/da.json index 75416989..d414695b 100644 --- a/share/locale/da.json +++ b/share/locale/da.json @@ -39,7 +39,6 @@ "gui_copied_url": "Kopierede URL til udklipsholder", "gui_copied_hidservauth": "Kopierede HidServAuth-linje til udklipsholder", "gui_please_wait": "Vent venligst...", - "using_ephemeral": "Starter kortvarig Tor onion-tjeneste og afventer udgivelse", "gui_download_upload_progress_complete": "%p%, tid forløbet: {0:s}", "gui_download_upload_progress_starting": "{0:s}, %p% (udregner anslået ankomsttid)", "gui_download_upload_progress_eta": "{0:s}, anslået ankomsttid: {1:s}, %p%", diff --git a/share/locale/en.json b/share/locale/en.json index 512a69b4..d6937544 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -53,7 +53,6 @@ "gui_copied_hidservauth_title": "Copied HidServAuth", "gui_copied_hidservauth": "The HidServAuth line has been copied to clipboard", "gui_please_wait": "Starting… Click to cancel", - "using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication", "gui_download_upload_progress_complete": "%p%, Time Elapsed: {0:s}", "gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", diff --git a/share/locale/eo.json b/share/locale/eo.json index 1fbda9bc..9902e4ae 100644 --- a/share/locale/eo.json +++ b/share/locale/eo.json @@ -26,7 +26,6 @@ "gui_copied_url": "URL kopiita en tondujon", "gui_copied_hidservauth": "Copied HidServAuth line to clipboard", "gui_please_wait": "Bonvolu atendi...", - "using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication", "gui_download_upload_progress_complete": "%p%, Tempo pasinta: {0:s}", "gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", diff --git a/share/locale/fi.json b/share/locale/fi.json index 8a360284..d0ee80ff 100644 --- a/share/locale/fi.json +++ b/share/locale/fi.json @@ -21,6 +21,5 @@ "gui_canceled": "Peruutettu", "gui_copied_url": "URL-osoite kopioitu leikepöydälle", "gui_please_wait": "Odota...", - "using_ephemeral": "Käynnistetään lyhytaikainen Tor piilopalvelu ja odotetaan julkaisua", "zip_progress_bar_format": "Tiivistän tiedostoja: %p%" } diff --git a/share/locale/it.json b/share/locale/it.json index 6c55ed20..ebe2df4e 100644 --- a/share/locale/it.json +++ b/share/locale/it.json @@ -21,6 +21,5 @@ "gui_canceled": "Cancellati", "gui_copied_url": "URL Copiato nella clipboard", "gui_please_wait": "Attendere prego...", - "using_ephemeral": "Avviamento del servizio nascosto Tor ephemeral e attesa della pubblicazione", "zip_progress_bar_format": "Elaborazione files: %p%" } diff --git a/share/locale/nl.json b/share/locale/nl.json index 833432ab..abd14753 100644 --- a/share/locale/nl.json +++ b/share/locale/nl.json @@ -37,7 +37,6 @@ "gui_copied_url": "URL gekopieerd naar klembord", "gui_copied_hidservauth": "HidServAuth regel gekopieerd naar klembord", "gui_please_wait": "Moment geduld...", - "using_ephemeral": "Kortstondige Tor onion service gestart en in afwachting van publicatie", "gui_download_upload_progress_complete": "%p%, Tijd verstreken: {0:s}", "gui_download_upload_progress_starting": "{0:s}, %p% (ETA berekenen)", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", diff --git a/share/locale/tr.json b/share/locale/tr.json index 71f16daa..68807410 100644 --- a/share/locale/tr.json +++ b/share/locale/tr.json @@ -21,6 +21,5 @@ "gui_canceled": "İptal edilen", "gui_copied_url": "Panoya kopyalanan URL", "gui_please_wait": "Lütfen bekleyin...", - "using_ephemeral": "Geçici Tor gizli hizmetine bakılıyor ve yayımı bekleniyor", "zip_progress_bar_format": "Dosyalar hazırlanıyor: %p%" } From 09ccbf4a6078a9d416eeefb5a7e7e6c36051f660 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 16:22:37 -0700 Subject: [PATCH 038/123] Dynamically figure out the total size of the download based on the whether or not the client making the http request accepts gzip --- onionshare/web/share_mode.py | 24 +++++++++++++++++++----- onionshare_gui/share_mode/__init__.py | 7 ++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 95bc8443..2024e732 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -25,6 +25,7 @@ class ShareModeWeb(object): self.download_filename = None self.download_filesize = None self.gzip_filename = None + self.gzip_filesize = None self.zip_writer = None self.download_count = 0 @@ -69,13 +70,18 @@ class ShareModeWeb(object): return self.web.add_security_headers(r) # If download is allowed to continue, serve download page + if self.should_use_gzip(): + filesize = self.gzip_filesize + else: + filesize = self.download_filesize + if self.web.slug: r = make_response(render_template( 'send.html', slug=self.web.slug, file_info=self.file_info, filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, + filesize=filesize, filesize_human=self.common.human_readable_filesize(self.download_filesize), is_zipped=self.is_zipped)) else: @@ -84,7 +90,7 @@ class ShareModeWeb(object): 'send.html', file_info=self.file_info, filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, + filesize=filesize, filesize_human=self.common.human_readable_filesize(self.download_filesize), is_zipped=self.is_zipped)) return self.web.add_security_headers(r) @@ -123,7 +129,7 @@ class ShareModeWeb(object): # If this is a zipped file, then serve as-is. If it's not zipped, then, # if the http client supports gzip compression, gzip the file first # and serve that - use_gzip = (not self.is_zipped) and ('gzip' in request.headers.get('Accept-Encoding', '').lower()) + use_gzip = self.should_use_gzip() if use_gzip: file_to_download = self.gzip_filename else: @@ -131,7 +137,8 @@ class ShareModeWeb(object): # Tell GUI the download started self.web.add_request(self.web.REQUEST_STARTED, path, { - 'id': download_id + 'id': download_id, + 'use_gzip': use_gzip }) basename = os.path.basename(self.download_filename) @@ -212,7 +219,7 @@ class ShareModeWeb(object): r = Response(generate()) if use_gzip: r.headers.set('Content-Encoding', 'gzip') - r.headers.set('Content-Length', os.path.getsize(self.gzip_filename)) + r.headers.set('Content-Length', self.gzip_filesize) else: r.headers.set('Content-Length', self.download_filesize) r.headers.set('Content-Disposition', 'attachment', filename=basename) @@ -260,6 +267,7 @@ class ShareModeWeb(object): # Compress the file with gzip now, so we don't have to do it on each request self.gzip_filename = tempfile.mkstemp('wb+')[1] self._gzip_compress(self.download_filename, self.gzip_filename, 6, processed_size_callback) + self.gzip_filesize = os.path.getsize(self.gzip_filename) # Make sure the gzip file gets cleaned up when onionshare stops self.cleanup_filenames.append(self.gzip_filename) @@ -291,6 +299,12 @@ class ShareModeWeb(object): return True + def should_use_gzip(self): + """ + Should we use gzip for this browser? + """ + return (not self.is_zipped) and ('gzip' in request.headers.get('Accept-Encoding', '').lower()) + def _gzip_compress(self, input_filename, output_filename, level, processed_size_callback=None): """ Compress a file with gzip, without loading the whole thing into memory diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 52ec672e..ac6a1373 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -229,7 +229,11 @@ class ShareMode(Mode): """ Handle REQUEST_STARTED event. """ - self.downloads.add(event["data"]["id"], self.web.share_mode.download_filesize) + if event["data"]["use_gzip"]: + filesize = self.web.share_mode.gzip_filesize + else: + filesize = self.web.share_mode.download_filesize + self.downloads.add(event["data"]["id"], filesize) self.downloads_in_progress += 1 self.update_downloads_in_progress() @@ -388,6 +392,7 @@ class ZipProgressBar(QtWidgets.QProgressBar): def update_processed_size(self, val): self._processed_size = val + if self.processed_size < self.total_files_size: self.setValue(int((self.processed_size * 100) / self.total_files_size)) elif self.total_files_size != 0: From bacd2a1be6a42e2793826480d5b4a4d19cf93311 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 16:24:22 -0700 Subject: [PATCH 039/123] Include onionshare.web module in setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index a36fecab..94213f7c 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ setup( url=url, license=license, keywords=keywords, packages=[ 'onionshare', + 'onionshare.web', 'onionshare_gui', 'onionshare_gui.share_mode', 'onionshare_gui.receive_mode' From 4d125bd3dcd19de48a3c2550e04d797a4bed6668 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 17:08:11 -0700 Subject: [PATCH 040/123] Actually tell the GUI the progess --- onionshare/web/share_mode.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 2024e732..d4d6aed7 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -132,8 +132,10 @@ class ShareModeWeb(object): use_gzip = self.should_use_gzip() if use_gzip: file_to_download = self.gzip_filename + filesize = self.gzip_filesize else: file_to_download = self.download_filename + filesize = self.download_filesize # Tell GUI the download started self.web.add_request(self.web.REQUEST_STARTED, path, { @@ -173,7 +175,7 @@ class ShareModeWeb(object): # tell GUI the progress downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 + percent = (1.0 * downloaded_bytes / filesize) * 100 # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) if not self.web.is_gui or self.common.platform == 'Linux' or self.common.platform == 'BSD': @@ -219,9 +221,7 @@ class ShareModeWeb(object): r = Response(generate()) if use_gzip: r.headers.set('Content-Encoding', 'gzip') - r.headers.set('Content-Length', self.gzip_filesize) - else: - r.headers.set('Content-Length', self.download_filesize) + r.headers.set('Content-Length', filesize) r.headers.set('Content-Disposition', 'attachment', filename=basename) r = self.web.add_security_headers(r) # guess content type From a1cddeb9a956c799bbf7c823c705a48d69799309 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 17:42:36 -0700 Subject: [PATCH 041/123] Access .upload_count from the correct object after the web refactor --- onionshare/web/receive_mode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index c422d74e..4a6934a1 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -249,8 +249,8 @@ class ReceiveModeRequest(Request): self.progress = {} # Create an upload_id, attach it to the request - self.upload_id = self.upload_count - self.upload_count += 1 + self.upload_id = self.web.receive_mode.upload_count + self.web.receive_mode.upload_count += 1 # Figure out the content length try: From f09d53952127c1b36a94033f7b573bbeb3b38550 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 16:50:39 +1000 Subject: [PATCH 042/123] GUI unit tests in both share and receive mode --- .travis.yml | 3 +- unit_tests/__init__.py | 0 unit_tests/conftest.py | 160 ++++++++++ .../onionshare_receive_mode_upload_test.py | 258 ++++++++++++++++ ...re_receive_mode_upload_test_public_mode.py | 258 ++++++++++++++++ .../onionshare_share_mode_download_test.py | 287 ++++++++++++++++++ ...re_share_mode_download_test_public_mode.py | 287 ++++++++++++++++++ ...hare_share_mode_download_test_stay_open.py | 256 ++++++++++++++++ unit_tests/onionshare_timer_test.py | 142 +++++++++ unit_tests/run_unit_tests.sh | 5 + 10 files changed, 1655 insertions(+), 1 deletion(-) create mode 100644 unit_tests/__init__.py create mode 100644 unit_tests/conftest.py create mode 100644 unit_tests/onionshare_receive_mode_upload_test.py create mode 100644 unit_tests/onionshare_receive_mode_upload_test_public_mode.py create mode 100644 unit_tests/onionshare_share_mode_download_test.py create mode 100644 unit_tests/onionshare_share_mode_download_test_public_mode.py create mode 100644 unit_tests/onionshare_share_mode_download_test_stay_open.py create mode 100644 unit_tests/onionshare_timer_test.py create mode 100755 unit_tests/run_unit_tests.sh diff --git a/.travis.yml b/.travis.yml index afbaa887..71778af4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: # command to install dependencies install: - pip install -r install/requirements.txt - - pip install pytest-cov coveralls flake8 + - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering before_script: # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics @@ -17,5 +17,6 @@ before_script: - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests script: pytest --cov=onionshare test/ +script: cd unit_tests && bash run_unit_tests.sh after_success: - coveralls diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py new file mode 100644 index 00000000..8ac7efb8 --- /dev/null +++ b/unit_tests/conftest.py @@ -0,0 +1,160 @@ +import sys +# Force tests to look for resources in the source code tree +sys.onionshare_dev_mode = True + +import os +import shutil +import tempfile + +import pytest + +from onionshare import common, web, settings + +@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' + return settings.Settings(_common) diff --git a/unit_tests/onionshare_receive_mode_upload_test.py b/unit_tests/onionshare_receive_mode_upload_test.py new file mode 100644 index 00000000..b99faac0 --- /dev/null +++ b/unit_tests/onionshare_receive_mode_upload_test.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json +import requests + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + os.remove('/tmp/OnionShare/test.txt') + os.remove('/tmp/OnionShare/test-2.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded_and_tor_bootstrapped(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + @pytest.mark.run(order=5) + def test_info_widget_is_not_visible(self): + '''Test that the info widget along top of screen is not shown because we have a file''' + self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) + + @pytest.mark.run(order=6) + def test_click_receive_mode(self): + '''Test that we can switch to Receive Mode by clicking the button''' + QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) + + @pytest.mark.run(order=7) + def test_uploads_section_is_visible(self): + '''Test that the Uploads section is visible and that the No Uploads Yet label is present''' + self.assertTrue(self.gui.receive_mode.uploads.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + + @pytest.mark.run(order=8) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.receive_mode.server_status.status, 1) + + @pytest.mark.run(order=9) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=10) + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=11) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.receive_mode.server_status.status, 2) + + @pytest.mark.run(order=12) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + # Running in local mode, so we have no .onion + #@pytest.mark.run(order=13) + #def test_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') + # self.assertEqual(len(self.gui.app.onion_host), 62) + + @pytest.mark.run(order=14) + def test_have_a_slug(self): + '''Test that we have a valid slug''' + self.assertRegex(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') + + @pytest.mark.run(order=15) + def test_url_description_shown(self): + '''Test that the URL label is showing''' + self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) + + @pytest.mark.run(order=16) + def test_have_copy_url_button(self): + '''Test that the Copy URL button is shown''' + self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) + + @pytest.mark.run(order=17) + def test_server_status_indicator_says_sharing(self): + '''Test that the Server Status indicator shows we are Receiving''' + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + + @pytest.mark.run(order=18) + def test_web_page(self): + '''Test that the web page contains the term Select the files you want to send, then click''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET {} HTTP/1.0\r\n'.format(self.gui.receive_mode.server_status.web.slug) + http_request += 'Host: 127.0.0.1\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('Select the files you want to send, then click "Send Files"' in f.read()) + f.close() + + @pytest.mark.run(order=19) + def test_upload_file(self): + '''Test that we can upload the file''' + files = {'file[]': open('/tmp/test.txt', 'rb')} + response = requests.post('http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug), files=files) + QtTest.QTest.qWait(2000) + self.assertTrue(os.path.isfile('/tmp/OnionShare/test.txt')) + + @pytest.mark.run(order=20) + def test_uploads_widget_present(self): + '''Test that the No Uploads Yet label is hidden, that Clear History is present''' + self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) + + @pytest.mark.run(order=21) + def test_upload_count_incremented(self): + '''Test that the Upload Count has incremented''' + self.assertEquals(self.gui.receive_mode.uploads_completed, 1) + + @pytest.mark.run(order=22) + def test_upload_same_file_is_renamed(self): + '''Test that we can upload the same file and that it gets renamed''' + files = {'file[]': open('/tmp/test.txt', 'rb')} + response = requests.post('http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug), files=files) + QtTest.QTest.qWait(2000) + self.assertTrue(os.path.isfile('/tmp/OnionShare/test-2.txt')) + + @pytest.mark.run(order=23) + def test_upload_count_incremented_again(self): + '''Test that the Upload Count has incremented again''' + self.assertEquals(self.gui.receive_mode.uploads_completed, 2) + + @pytest.mark.run(order=24) + def test_server_is_stopped(self): + '''Test that the server stops when we click Stop''' + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.receive_mode.server_status.status, 0) + + @pytest.mark.run(order=25) + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + QtTest.QTest.qWait(2000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=26) + def test_server_status_indicator_says_closed(self): + '''Test that the Server Status indicator shows we closed''' + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/onionshare_receive_mode_upload_test_public_mode.py b/unit_tests/onionshare_receive_mode_upload_test_public_mode.py new file mode 100644 index 00000000..d309e5b1 --- /dev/null +++ b/unit_tests/onionshare_receive_mode_upload_test_public_mode.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json +import requests + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": True, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + os.remove('/tmp/OnionShare/test.txt') + os.remove('/tmp/OnionShare/test-2.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded_and_tor_bootstrapped(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + @pytest.mark.run(order=5) + def test_info_widget_is_not_visible(self): + '''Test that the info widget along top of screen is not shown because we have a file''' + self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) + + @pytest.mark.run(order=6) + def test_click_receive_mode(self): + '''Test that we can switch to Receive Mode by clicking the button''' + QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) + + @pytest.mark.run(order=7) + def test_uploads_section_is_visible(self): + '''Test that the Uploads section is visible and that the No Uploads Yet label is present''' + self.assertTrue(self.gui.receive_mode.uploads.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + + @pytest.mark.run(order=8) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.receive_mode.server_status.status, 1) + + @pytest.mark.run(order=9) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=10) + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=11) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.receive_mode.server_status.status, 2) + + @pytest.mark.run(order=12) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + # Running in local mode, so we have no .onion + #@pytest.mark.run(order=13) + #def test_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') + # self.assertEqual(len(self.gui.app.onion_host), 62) + + @pytest.mark.run(order=14) + def test_have_no_slug(self): + '''Test that we have a valid slug''' + self.assertIsNone(self.gui.share_mode.server_status.web.slug) + + @pytest.mark.run(order=15) + def test_url_description_shown(self): + '''Test that the URL label is showing''' + self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) + + @pytest.mark.run(order=16) + def test_have_copy_url_button(self): + '''Test that the Copy URL button is shown''' + self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) + + @pytest.mark.run(order=17) + def test_server_status_indicator_says_sharing(self): + '''Test that the Server Status indicator shows we are Receiving''' + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + + @pytest.mark.run(order=18) + def test_web_page(self): + '''Test that the web page contains the term Select the files you want to send, then click''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET / HTTP/1.0\r\n' + http_request += 'Host: 127.0.0.1\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('Select the files you want to send, then click "Send Files"' in f.read()) + f.close() + + @pytest.mark.run(order=19) + def test_upload_file(self): + '''Test that we can upload the file''' + files = {'file[]': open('/tmp/test.txt', 'rb')} + response = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port), files=files) + QtTest.QTest.qWait(2000) + self.assertTrue(os.path.isfile('/tmp/OnionShare/test.txt')) + + @pytest.mark.run(order=20) + def test_uploads_widget_present(self): + '''Test that the No Uploads Yet label is hidden, that Clear History is present''' + self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) + + @pytest.mark.run(order=21) + def test_upload_count_incremented(self): + '''Test that the Upload Count has incremented''' + self.assertEquals(self.gui.receive_mode.uploads_completed, 1) + + @pytest.mark.run(order=22) + def test_upload_same_file_is_renamed(self): + '''Test that we can upload the same file and that it gets renamed''' + files = {'file[]': open('/tmp/test.txt', 'rb')} + response = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port), files=files) + QtTest.QTest.qWait(2000) + self.assertTrue(os.path.isfile('/tmp/OnionShare/test-2.txt')) + + @pytest.mark.run(order=23) + def test_upload_count_incremented_again(self): + '''Test that the Upload Count has incremented again''' + self.assertEquals(self.gui.receive_mode.uploads_completed, 2) + + @pytest.mark.run(order=24) + def test_server_is_stopped(self): + '''Test that the server stops when we click Stop''' + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.receive_mode.server_status.status, 0) + + @pytest.mark.run(order=25) + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + QtTest.QTest.qWait(2000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=26) + def test_server_status_indicator_says_closed(self): + '''Test that the Server Status indicator shows we closed''' + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/onionshare_share_mode_download_test.py b/unit_tests/onionshare_share_mode_download_test.py new file mode 100644 index 00000000..aa8dcdaa --- /dev/null +++ b/unit_tests/onionshare_share_mode_download_test.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded_and_tor_bootstrapped(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + '''Test that the number of files in the list is 1''' + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + '''Test that the info widget along top of screen is shown because we have a file''' + self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + + @pytest.mark.run(order=7) + def test_downloads_section_is_visible(self): + '''Test that the Downloads section is visible and that the No Downloads Yet label is present''' + self.assertTrue(self.gui.share_mode.downloads.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + + @pytest.mark.run(order=8) + def test_deleting_only_file_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, and since there's no more files, the delete button should be hidden + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + @pytest.mark.run(order=9) + def test_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.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_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.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=13) + def test_add_delete_buttons_now_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()) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + # Running in local mode, so we have no .onion + #@pytest.mark.run(order=17) + #def test_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') + # self.assertEqual(len(self.gui.app.onion_host), 62) + + @pytest.mark.run(order=18) + def test_have_a_slug(self): + '''Test that we have a valid slug''' + self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + '''Test that the URL label is showing''' + self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + '''Test that the Copy URL button is shown''' + self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_sharing(self): + '''Test that the Server Status indicator shows we are Sharing''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + + @pytest.mark.run(order=22) + def test_web_page(self): + '''Test that the web page contains the term Total size''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET {} HTTP/1.0\r\n'.format(self.gui.share_mode.server_status.web.slug) + http_request += 'Host: 127.0.0.1\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('Total size' in f.read()) + f.close() + + @pytest.mark.run(order=23) + def test_download_share(self): + '''Test that we can download the share''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET {}/download HTTP/1.0\r\n'.format(self.gui.share_mode.server_status.web.slug) + http_request += 'Host: 127.0.0.1\r\n' + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/download.zip', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + zip = zipfile.ZipFile('/tmp/download.zip') + self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + + @pytest.mark.run(order=24) + def test_downloads_widget_present(self): + QtTest.QTest.qWait(1000) + '''Test that the No Downloads Yet label is hidden, that Clear History is present''' + self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + + @pytest.mark.run(order=25) + def test_server_is_stopped(self): + '''Test that the server stopped automatically when we downloaded the share''' + self.assertEquals(self.gui.share_mode.server_status.status, 0) + + @pytest.mark.run(order=26) + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=27) + def test_server_status_indicator_says_closed(self): + '''Test that the Server Status indicator shows we closed because download occurred''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) + + @pytest.mark.run(order=28) + def test_add_button_visible_again(self): + '''Test that the add button should be visible again''' + self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/onionshare_share_mode_download_test_public_mode.py b/unit_tests/onionshare_share_mode_download_test_public_mode.py new file mode 100644 index 00000000..59905149 --- /dev/null +++ b/unit_tests/onionshare_share_mode_download_test_public_mode.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": True, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded_and_tor_bootstrapped(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + '''Test that the number of files in the list is 1''' + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + '''Test that the info widget along top of screen is shown because we have a file''' + self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + + @pytest.mark.run(order=7) + def test_downloads_section_is_visible(self): + '''Test that the Downloads section is visible and that the No Downloads Yet label is present''' + self.assertTrue(self.gui.share_mode.downloads.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + + @pytest.mark.run(order=8) + def test_deleting_only_file_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, and since there's no more files, the delete button should be hidden + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + @pytest.mark.run(order=9) + def test_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.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_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.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=13) + def test_add_delete_buttons_now_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()) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + # Running in local mode, so we have no .onion + #@pytest.mark.run(order=17) + #def test_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') + # self.assertEqual(len(self.gui.app.onion_host), 62) + + @pytest.mark.run(order=18) + def test_have_no_slug(self): + '''Test that we have a valid slug''' + self.assertIsNone(self.gui.share_mode.server_status.web.slug) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + '''Test that the URL label is showing''' + self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + '''Test that the Copy URL button is shown''' + self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_sharing(self): + '''Test that the Server Status indicator shows we are Sharing''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + + @pytest.mark.run(order=22) + def test_web_page(self): + '''Test that the web page contains the term Total size''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET / HTTP/1.0\r\n' + http_request += 'Host: 127.0.0.1\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('Total size' in f.read()) + f.close() + + @pytest.mark.run(order=23) + def test_download_share(self): + '''Test that we can download the share''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET /download HTTP/1.0\r\n' + http_request += 'Host: 127.0.0.1\r\n' + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/download.zip', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + zip = zipfile.ZipFile('/tmp/download.zip') + self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + + @pytest.mark.run(order=24) + def test_downloads_widget_present(self): + QtTest.QTest.qWait(1000) + '''Test that the No Downloads Yet label is hidden, that Clear History is present''' + self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + + @pytest.mark.run(order=25) + def test_server_is_stopped(self): + '''Test that the server stopped automatically when we downloaded the share''' + self.assertEquals(self.gui.share_mode.server_status.status, 0) + + @pytest.mark.run(order=26) + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=27) + def test_server_status_indicator_says_closed(self): + '''Test that the Server Status indicator shows we closed because download occurred''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) + + @pytest.mark.run(order=28) + def test_add_button_visible_again(self): + '''Test that the add button should be visible again''' + self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/onionshare_share_mode_download_test_stay_open.py b/unit_tests/onionshare_share_mode_download_test_stay_open.py new file mode 100644 index 00000000..82a0ac87 --- /dev/null +++ b/unit_tests/onionshare_share_mode_download_test_stay_open.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": False, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded_and_tor_bootstrapped(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + '''Test that the number of files in the list is 1''' + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + '''Test that the info widget along top of screen is shown because we have a file''' + self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + + @pytest.mark.run(order=7) + def test_downloads_section_is_visible(self): + '''Test that the Downloads section is visible and that the No Downloads Yet label is present''' + self.assertTrue(self.gui.share_mode.downloads.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + + @pytest.mark.run(order=8) + def test_deleting_only_file_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, and since there's no more files, the delete button should be hidden + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + @pytest.mark.run(order=9) + def test_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.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_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.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=13) + def test_add_delete_buttons_now_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()) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + # Running in local mode, so we have no .onion + #@pytest.mark.run(order=17) + #def test_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') + # self.assertEqual(len(self.gui.app.onion_host), 62) + + @pytest.mark.run(order=18) + def test_have_a_slug(self): + '''Test that we have a valid slug''' + self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + '''Test that the URL label is showing''' + self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + '''Test that the Copy URL button is shown''' + self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_sharing(self): + '''Test that the Server Status indicator shows we are Sharing''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + + @pytest.mark.run(order=22) + def test_download_share(self): + '''Test that we can download the share''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET {}/download HTTP/1.0\r\n'.format(self.gui.share_mode.server_status.web.slug) + http_request += 'Host: 127.0.0.1\r\n' + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/download.zip', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + zip = zipfile.ZipFile('/tmp/download.zip') + self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + + @pytest.mark.run(order=23) + def test_downloads_widget_present(self): + QtTest.QTest.qWait(1000) + '''Test that the No Downloads Yet label is hidden, that Clear History is present''' + self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + + @pytest.mark.run(order=24) + def test_server_is_not_stopped(self): + '''Test that the server stayed open after we downloaded the share''' + self.assertEquals(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=25) + def test_web_service_is_running(self): + '''Test that the web server is still running''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.assertEquals(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=26) + def test_download_count_incremented(self): + '''Test that the Download Count has incremented''' + self.assertEquals(self.gui.share_mode.downloads_completed, 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/onionshare_timer_test.py b/unit_tests/onionshare_timer_test.py new file mode 100644 index 00000000..ed20c1c0 --- /dev/null +++ b/unit_tests/onionshare_timer_test.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": True, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_set_timeout(self): + '''Test that the timeout can be set''' + timer = QtCore.QDateTime.currentDateTime().addSecs(120) + self.gui.share_mode.server_status.shutdown_timeout.setDateTime(timer) + self.assertTrue(self.gui.share_mode.server_status.shutdown_timeout.dateTime(), timer) + + @pytest.mark.run(order=3) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + @pytest.mark.run(order=4) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=5) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=6) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=7) + def test_timeout_widget_hidden(self): + '''Test that the timeout widget is hidden when share has started''' + self.assertFalse(self.gui.share_mode.server_status.shutdown_timeout_container.isVisible()) + + @pytest.mark.run(order=8) + def test_server_timed_out(self): + '''Test that the server has timed out after the timer ran out''' + QtTest.QTest.qWait(100000) + # We should have timed out now + self.assertEqual(self.gui.share_mode.server_status.status, 0) + + @pytest.mark.run(order=9) + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/run_unit_tests.sh b/unit_tests/run_unit_tests.sh new file mode 100755 index 00000000..d15f8a6e --- /dev/null +++ b/unit_tests/run_unit_tests.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +for test in `ls -1 | egrep ^onionshare_`; do + py.test-3 $test -vvv || exit 1 +done From 12838f8e9dc5824e99bc3249c99d68c0059944e2 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 16:54:52 +1000 Subject: [PATCH 043/123] Try and make travis-friendly tests --- .travis.yml | 3 ++- unit_tests/run_unit_tests_travis.sh | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100755 unit_tests/run_unit_tests_travis.sh diff --git a/.travis.yml b/.travis.yml index 71778af4..03ae798a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ python: install: - pip install -r install/requirements.txt - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering + - apt-get install xvfb before_script: # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics @@ -17,6 +18,6 @@ before_script: - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests script: pytest --cov=onionshare test/ -script: cd unit_tests && bash run_unit_tests.sh +script: cd unit_tests && bash run_unit_tests_travis.sh after_success: - coveralls diff --git a/unit_tests/run_unit_tests_travis.sh b/unit_tests/run_unit_tests_travis.sh new file mode 100755 index 00000000..236c48ff --- /dev/null +++ b/unit_tests/run_unit_tests_travis.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +for test in `ls -1 | egrep ^onionshare_`; do + xvfb-run pytest $test -vvv || exit 1 +done From 7c9cc76cce1c30be370e2b264b9488b5e14ca919 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 16:57:26 +1000 Subject: [PATCH 044/123] xvfb might already be installed? --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 03ae798a..6771eca8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,6 @@ python: install: - pip install -r install/requirements.txt - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering - - apt-get install xvfb before_script: # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics From 174cfbd3fb2e4c63d67be913fc0ff8ff0dc6bfce Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 17:02:16 +1000 Subject: [PATCH 045/123] need pytest-qt --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6771eca8..c9c58e97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: # command to install dependencies install: - pip install -r install/requirements.txt - - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering + - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering pytest-qt before_script: # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics From d48d780686efbaf449ff0ef9517607718a31baf1 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 17:06:02 +1000 Subject: [PATCH 046/123] fighting with travis... --- .travis.yml | 1 + unit_tests/run_unit_tests_travis.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c9c58e97..7df1d4c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ python: install: - pip install -r install/requirements.txt - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering pytest-qt + - sudo apt-get install python3-pytest before_script: # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics diff --git a/unit_tests/run_unit_tests_travis.sh b/unit_tests/run_unit_tests_travis.sh index 236c48ff..ba9b5b4e 100755 --- a/unit_tests/run_unit_tests_travis.sh +++ b/unit_tests/run_unit_tests_travis.sh @@ -1,5 +1,5 @@ #!/bin/bash for test in `ls -1 | egrep ^onionshare_`; do - xvfb-run pytest $test -vvv || exit 1 + xvfb-run pytest-3 $test -vvv || exit 1 done From 9a505af3bb977581c894b6b155f8e3ddeef9d7b2 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 17:47:38 +1000 Subject: [PATCH 047/123] Add persistent slug test. Add test of clipboard contents in Share mode. Remove travis stuff that I couldn't get to work --- .travis.yml | 4 +- .../onionshare_share_mode_download_test.py | 7 +- unit_tests/onionshare_slug_persistent_test.py | 161 ++++++++++++++++++ unit_tests/run_unit_tests_travis.sh | 5 - 4 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 unit_tests/onionshare_slug_persistent_test.py delete mode 100755 unit_tests/run_unit_tests_travis.sh diff --git a/.travis.yml b/.travis.yml index 7df1d4c4..afbaa887 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,8 +9,7 @@ python: # command to install dependencies install: - pip install -r install/requirements.txt - - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering pytest-qt - - sudo apt-get install python3-pytest + - pip install pytest-cov coveralls flake8 before_script: # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics @@ -18,6 +17,5 @@ before_script: - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests script: pytest --cov=onionshare test/ -script: cd unit_tests && bash run_unit_tests_travis.sh after_success: - coveralls diff --git a/unit_tests/onionshare_share_mode_download_test.py b/unit_tests/onionshare_share_mode_download_test.py index aa8dcdaa..c4c8bee7 100644 --- a/unit_tests/onionshare_share_mode_download_test.py +++ b/unit_tests/onionshare_share_mode_download_test.py @@ -8,7 +8,7 @@ import zipfile import socks import json -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtCore, QtWidgets, QtTest, QtGui from onionshare.common import Common from onionshare.web import Web @@ -197,8 +197,11 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=20) def test_have_copy_url_button(self): - '''Test that the Copy URL button is shown''' + '''Test that the Copy URL button is shown and can be copied to clipboard''' self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.copy_url_button, QtCore.Qt.LeftButton) + clipboard = self.gui.qtapp.clipboard() + self.assertEquals(clipboard.text(), 'http://127.0.0.1:{}/{}'.format(self.gui.app.port, self.gui.share_mode.server_status.web.slug)) @pytest.mark.run(order=21) def test_server_status_indicator_says_sharing(self): diff --git a/unit_tests/onionshare_slug_persistent_test.py b/unit_tests/onionshare_slug_persistent_test.py new file mode 100644 index 00000000..d0dcd08a --- /dev/null +++ b/unit_tests/onionshare_slug_persistent_test.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + slug = '' + + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": True, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + @pytest.mark.run(order=3) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=4) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=5) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=6) + def test_have_a_slug(self): + '''Test that we have a valid slug''' + self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + global slug + slug = self.gui.share_mode.server_status.web.slug + + @pytest.mark.run(order=7) + def test_server_can_be_stopped(self): + '''Test we can stop the service''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_STOPPED state + self.assertEqual(self.gui.share_mode.server_status.status, 0) + + @pytest.mark.run(order=8) + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + QtTest.QTest.qWait(4000) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=9) + def test_server_started_again(self): + '''Test we can start the service again''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=10) + def test_have_same_slug(self): + '''Test that we have the same slug''' + self.assertEqual(self.gui.share_mode.server_status.web.slug, slug) + + @pytest.mark.run(order=11) + def test_server_is_stopped_again(self): + '''Test that we can stop the server''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + QtTest.QTest.qWait(1000) + self.assertEqual(self.gui.share_mode.server_status.status, 0) + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/run_unit_tests_travis.sh b/unit_tests/run_unit_tests_travis.sh deleted file mode 100755 index ba9b5b4e..00000000 --- a/unit_tests/run_unit_tests_travis.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -for test in `ls -1 | egrep ^onionshare_`; do - xvfb-run pytest-3 $test -vvv || exit 1 -done From cb3ed3eadde7c522faed15f9e9d8a5aa5f7f2bf2 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 17:57:53 +1000 Subject: [PATCH 048/123] One more travis test --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index afbaa887..65eac2c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: # command to install dependencies install: - pip install -r install/requirements.txt - - pip install pytest-cov coveralls flake8 + - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering pytest-qt pytest before_script: # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics @@ -17,5 +17,6 @@ before_script: - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests script: pytest --cov=onionshare test/ +script: xvfb-run py.test-3 unit_tests/onionshare_share_mode_download_test.py after_success: - coveralls From 157dde37e3253a0d5050b68dc07e914abb5aac56 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 18:01:17 +1000 Subject: [PATCH 049/123] pytest --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 65eac2c5..9aece101 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,6 @@ before_script: - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests script: pytest --cov=onionshare test/ -script: xvfb-run py.test-3 unit_tests/onionshare_share_mode_download_test.py +script: xvfb-run pytest unit_tests/onionshare_share_mode_download_test.py after_success: - coveralls From 4e68c62b9c0607bbfc965b033997712090022c26 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 18:07:14 +1000 Subject: [PATCH 050/123] Nope --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9aece101..afbaa887 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: # command to install dependencies install: - pip install -r install/requirements.txt - - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering pytest-qt pytest + - pip install pytest-cov coveralls flake8 before_script: # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics @@ -17,6 +17,5 @@ before_script: - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests script: pytest --cov=onionshare test/ -script: xvfb-run pytest unit_tests/onionshare_share_mode_download_test.py after_success: - coveralls From bd4f3e5fe7817214ab2a129c740b684a2eab56db Mon Sep 17 00:00:00 2001 From: Baccount Date: Sat, 22 Sep 2018 11:18:18 -0700 Subject: [PATCH 051/123] Update get-tor-windows.py --- install/get-tor-windows.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/get-tor-windows.py b/install/get-tor-windows.py index 0d16dd29..e5a24be9 100644 --- a/install/get-tor-windows.py +++ b/install/get-tor-windows.py @@ -33,9 +33,9 @@ import subprocess import requests def main(): - exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0/torbrowser-install-8.0_en-US.exe' - exe_filename = 'torbrowser-install-8.0_en-US.exe' - expected_exe_sha256 = '0682b44eff5877dfc2fe2fdd5b46e678d47adad86d564e7cb6654c5f60eb1ed2' + exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0.1/torbrowser-install-8.0.1_en-US.exe' + exe_filename = 'torbrowser-install-8.0.1_en-US.exe' + expected_exe_sha256 = 'bdf81d4282b991a6425c213c7b03b3f5c1f17bb02986b7fe9a1891e577e51639' # Build paths root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))) working_path = os.path.join(os.path.join(root_path, 'build'), 'tor') From 726a3e4b09a3b0f0d5421701c3926690f357ba92 Mon Sep 17 00:00:00 2001 From: Baccount Date: Sat, 22 Sep 2018 11:20:55 -0700 Subject: [PATCH 052/123] Upgrade Tor to 0.3.4.8 --- install/get-tor-osx.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/get-tor-osx.py b/install/get-tor-osx.py index 1d2c6f56..ae20fd74 100644 --- a/install/get-tor-osx.py +++ b/install/get-tor-osx.py @@ -35,9 +35,9 @@ import subprocess import requests def main(): - dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0/TorBrowser-8.0-osx64_en-US.dmg' - dmg_filename = 'TorBrowser-8.0-osx64_en-US.dmg' - expected_dmg_sha256 = '15603ae7b3a1942863c98acc92f509e4409db48fe22c9acae6b15c9cb9bf3088' + dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0.1/TorBrowser-8.0.1-osx64_en-US.dmg' + dmg_filename = 'TorBrowser-8.0.1-osx64_en-US.dmg' + expected_dmg_sha256 = 'fb1be2a0f850a65bae38747c3abbf9061742c5d7799e1693405078aaf38d2b08' # Build paths root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))) From 180a61d4bacf79f06f91d3394444ebda45e6af07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sun, 23 Sep 2018 06:22:05 +0200 Subject: [PATCH 053/123] Language rework --- share/locale/en.json | 248 +++++++++++++++++++++---------------------- 1 file changed, 124 insertions(+), 124 deletions(-) diff --git a/share/locale/en.json b/share/locale/en.json index d6937544..a36fc0a4 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -1,184 +1,184 @@ { - "config_onion_service": "Configuring onion service on port {0:d}.", - "preparing_files": "Preparing files to share.", - "give_this_url": "Give this address to the person you're sending the file to:", - "give_this_url_stealth": "Give this address and HidServAuth line to the person you're sending the file to:", - "give_this_url_receive": "Give this address to the people sending you files:", - "give_this_url_receive_stealth": "Give this address and HidServAuth line to the people sending you files:", + "config_onion_service": "Setting up onion service on port {0:d}.", + "preparing_files": "Preparing share.", + "give_this_url": "Give this address to the recipient:", + "give_this_url_stealth": "Give this inbox address and HidServAuth line to the recipient:", + "give_this_url_receive": "Give this inbox address to the sender:", + "give_this_url_receive_stealth": "Give this inbox address and HidServAuth to the sender:", "ctrlc_to_stop": "Press Ctrl+C to stop the server", "not_a_file": "{0:s} is not a valid file.", "not_a_readable_file": "{0:s} is not a readable file.", - "no_available_port": "Could not start the Onion service as there was no available port.", - "other_page_loaded": "Address loaded", - "close_on_timeout": "Stopped because timer expired", - "closing_automatically": "Stopped because download finished", - "timeout_download_still_running": "Waiting for download to complete", - "large_filesize": "Warning: Sending large files could take hours", + "no_available_port": "Could not find an available port to start the onion service", + "other_page_loaded": "Inbox address loaded", + "close_on_timeout": "Stopped because timeframe expired", + "closing_automatically": "Stopped because you have the complete share", + "timeout_download_still_running": "Awaiting completion of share transfer to you", + "large_filesize": "Warning: Sending a large share could take hours", "systray_menu_exit": "Quit", - "systray_download_started_title": "OnionShare Download Started", - "systray_download_started_message": "A user started downloading your files", - "systray_download_completed_title": "OnionShare Download Finished", - "systray_download_completed_message": "The user finished downloading your files", - "systray_download_canceled_title": "OnionShare Download Canceled", - "systray_download_canceled_message": "The user canceled the download", - "systray_upload_started_title": "OnionShare Upload Started", - "systray_upload_started_message": "A user started uploading files to your computer", - "help_local_only": "Do not attempt to use Tor: For development only", - "help_stay_open": "Keep onion service running after download has finished", - "help_shutdown_timeout": "Shut down the onion service after N seconds", - "help_stealth": "Create stealth onion service (advanced)", - "help_receive": "Receive files instead of sending them", - "help_debug": "Log application errors to stdout, and log web errors to disk", + "systray_download_started_title": "Sending…", + "systray_download_started_message": "Your share is being sent.", + "systray_download_completed_title": "Share sent", + "systray_download_completed_message": "Your share has been sent.", + "systray_download_canceled_title": "Share cancelled", + "systray_download_canceled_message": "The recipient cancelled the share.", + "systray_upload_started_title": "Receiving…", + "systray_upload_started_message": "Started share transfer to inbox.", + "help_local_only": "Avoid using Tor: (Only for development)", + "help_stay_open": "Keep sending after one completion", + "help_shutdown_timeout": "Stop sharing after a given amount of seconds", + "help_stealth": "Make share that requires HidServAuth (advanced)", + "help_receive": "Receive shares instead of sending them", + "help_debug": "Log OnionShare errors to stdout, and web errors to disk", "help_filename": "List of files or folders to share", - "help_config": "Path to a custom JSON config file (optional)", + "help_config": "Custom JSON config file location (optional)", "gui_drag_and_drop": "Drag and drop files and folders\nto start sharing", "gui_add": "Add", "gui_delete": "Delete", "gui_choose_items": "Choose", - "gui_share_start_server": "Start Sharing", - "gui_share_stop_server": "Stop Sharing", - "gui_share_stop_server_shutdown_timeout": "Stop Sharing ({}s remaining)", - "gui_share_stop_server_shutdown_timeout_tooltip": "Share will expire automatically at {}", - "gui_receive_start_server": "Start Receive Mode", - "gui_receive_stop_server": "Stop Receive Mode", - "gui_receive_stop_server_shutdown_timeout": "Stop Receive Mode ({}s remaining)", - "gui_receive_stop_server_shutdown_timeout_tooltip": "Receive mode will expire automatically at {}", + "gui_share_start_server": "Share", + "gui_share_stop_server": "Stop", + "gui_share_stop_server_shutdown_timeout": "Stop sharing ({}s remaining)", + "gui_share_stop_server_shutdown_timeout_tooltip": "Allowed share timeframe ends {}", + "gui_receive_start_server": "Receive", + "gui_receive_stop_server": "Stop", + "gui_receive_stop_server_shutdown_timeout": "Stop receiving ({}s remaining)", + "gui_receive_stop_server_shutdown_timeout_tooltip": "Allowed receive timeframe ends {}", "gui_copy_url": "Copy Address", "gui_copy_hidservauth": "Copy HidServAuth", - "gui_downloads": "Download History", - "gui_no_downloads": "No downloads yet.", + "gui_downloads": "History of sent shares", + "gui_no_downloads": "None yet.", "gui_canceled": "Canceled", - "gui_copied_url_title": "Copied OnionShare address", - "gui_copied_url": "The OnionShare address has been copied to clipboard", - "gui_copied_hidservauth_title": "Copied HidServAuth", - "gui_copied_hidservauth": "The HidServAuth line has been copied to clipboard", - "gui_please_wait": "Starting… Click to cancel", - "gui_download_upload_progress_complete": "%p%, Time Elapsed: {0:s}", - "gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)", + "gui_copied_url_title": "Copied", + "gui_copied_url": "OnionShare address copied to clipboard", + "gui_copied_hidservauth_title": "Copied", + "gui_copied_hidservauth": "HidServAuth line copied to clipboard", + "gui_please_wait": "Starting… Click to cancel.", + "gui_download_upload_progress_complete": "%p%, {0:s} elapsed.", + "gui_download_upload_progress_starting": "{0:s}, %p% (calculating end time.)", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", "version_string": "OnionShare {0:s} | https://onionshare.org/", - "gui_quit_title": "Transfer in Progress", - "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_title": "Not so fast", + "gui_share_quit_warning": "Quitting now means files en route elsewhere won't finish.", + "gui_receive_quit_warning": "Quitting now means files en route to you won't finish.", "gui_quit_warning_quit": "Quit", "gui_quit_warning_dont_quit": "Cancel", - "error_rate_limit": "An attacker might be trying to guess your address. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new address.", - "zip_progress_bar_format": "Compressing files: %p%", - "error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.", - "error_ephemeral_not_supported": "OnionShare requires at least Tor 0.2.7.1 and at least python3-stem 1.4.0.", + "error_rate_limit": "Too many wrong attempts on your sharing address means someone could be trying to guess it. Start OnionShare again and send a new address to share.", + "zip_progress_bar_format": "Making share smaller: %p%", + "error_stealth_not_supported": "To create shares that require knowing HidServAuth, you need at least both Tor 0.2.9.1-alpha (or Tor Browser 6.5) and python3-stem 1.5.0.", + "error_ephemeral_not_supported": "OnionShare requires at least both Tor 0.2.7.1 and python3-stem 1.4.0.", "gui_settings_window_title": "Settings", - "gui_settings_whats_this": "what's this?", - "gui_settings_stealth_option": "Create stealth onion services (legacy)", - "gui_settings_stealth_hidservauth_string": "You have saved the private key for reuse, so your HidServAuth string is also reused.\nClick below to copy the HidServAuth.", - "gui_settings_autoupdate_label": "Check for upgrades", - "gui_settings_autoupdate_option": "Notify me when upgrades are available", + "gui_settings_whats_this": "What's this?", + "gui_settings_stealth_option": "Create share that requires knowing HidServAuth (legacy)", + "gui_settings_stealth_hidservauth_string": "Having saved your private key for reuse, means you can now\nclick to copy your HidServAuth.", + "gui_settings_autoupdate_label": "Check for new version", + "gui_settings_autoupdate_option": "Notify me when a new version is available", "gui_settings_autoupdate_timestamp": "Last checked: {}", "gui_settings_autoupdate_timestamp_never": "Never", - "gui_settings_autoupdate_check_button": "Check For Upgrades", + "gui_settings_autoupdate_check_button": "Check for New Version", "gui_settings_general_label": "General settings", "gui_settings_sharing_label": "Sharing settings", - "gui_settings_close_after_first_download_option": "Stop sharing after first download", + "gui_settings_close_after_first_download_option": "Stop sharing after first completion", "gui_settings_connection_type_label": "How should OnionShare connect to Tor?", - "gui_settings_connection_type_bundled_option": "Use the Tor version that is bundled with OnionShare", - "gui_settings_connection_type_automatic_option": "Attempt automatic configuration with Tor Browser", + "gui_settings_connection_type_bundled_option": "Use the Tor version built into OnionShare", + "gui_settings_connection_type_automatic_option": "Attempt auto-configuration with Tor Browser", "gui_settings_connection_type_control_port_option": "Connect using control port", "gui_settings_connection_type_socket_file_option": "Connect using socket file", - "gui_settings_connection_type_test_button": "Test Tor Settings", + "gui_settings_connection_type_test_button": "Test Connection to Tor", "gui_settings_control_port_label": "Control port", "gui_settings_socket_file_label": "Socket file", "gui_settings_socks_label": "SOCKS port", - "gui_settings_authenticate_label": "Tor authentication settings", - "gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication", + "gui_settings_authenticate_label": "Tor identification settings", + "gui_settings_authenticate_no_auth_option": "None or cookie based", "gui_settings_authenticate_password_option": "Password", "gui_settings_password_label": "Password", - "gui_settings_tor_bridges": "Tor Bridge support", + "gui_settings_tor_bridges": "Tor bridge support", "gui_settings_tor_bridges_no_bridges_radio_option": "Don't use bridges", "gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 pluggable transports", "gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Use built-in obfs4 pluggable transports (requires obfs4proxy)", "gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek_lite (Azure) pluggable transports", "gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy": "Use built-in meek_lite (Azure) pluggable transports (requires obfs4proxy)", - "gui_settings_meek_lite_expensive_warning": "Warning: the meek_lite bridges are very costly for the Tor Project to run!

You should only use meek_lite bridges if you are having trouble connecting to Tor directly, via obfs4 transports or other normal bridges.", + "gui_settings_meek_lite_expensive_warning": "Warning: The meek_lite bridges are very costly for the Tor Project to run.

Only use them if unable to connect to Tor directly, via obfs4 transports, or other normal bridges.", "gui_settings_tor_bridges_custom_radio_option": "Use custom bridges", "gui_settings_tor_bridges_custom_label": "You can get bridges from https://bridges.torproject.org", - "gui_settings_tor_bridges_invalid": "None of the bridges you supplied seem to work.\nPlease try again by double-checking them or adding other ones.", + "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.", "gui_settings_button_save": "Save", "gui_settings_button_cancel": "Cancel", "gui_settings_button_help": "Help", - "gui_settings_shutdown_timeout_checkbox": "Use auto-stop timer", - "gui_settings_shutdown_timeout": "Stop the share at:", - "settings_saved": "Settings saved to {}", - "settings_error_unknown": "Can't connect to Tor controller because the settings don't make sense.", - "settings_error_automatic": "Can't connect to Tor controller. Is Tor Browser running in the background? If you don't have it you can get it from:\nhttps://www.torproject.org/.", - "settings_error_socket_port": "Can't connect to Tor controller on {}:{}.", - "settings_error_socket_file": "Can't connect to Tor controller using socket file {}.", + "gui_settings_shutdown_timeout_checkbox": "Stop sharing after a given timeframe", + "gui_settings_shutdown_timeout": "Stop sharing after:", + "settings_saved": "Settings saved in {}", + "settings_error_unknown": "Can't connect to Tor controller because it knows your settings aren't making sense.", + "settings_error_automatic": "Could not connect to the Tor controller. The Tor Browser (available from https://www.torproject.org/)\nneeds to be running in the background.", + "settings_error_socket_port": "Can't connect to the Tor controller at {}:{}.", + "settings_error_socket_file": "The Tor controller does not allow connection using the socket file {}.", "settings_error_auth": "Connected to {}:{}, but can't authenticate. Maybe this isn't a Tor controller?", - "settings_error_missing_password": "Connected to Tor controller, but it requires a password to authenticate.", - "settings_error_unreadable_cookie_file": "Connected to Tor controller, but can't authenticate because your password may be wrong, and your user lacks permission to read the cookie file.", - "settings_error_bundled_tor_not_supported": "Use of the Tor version bundled with OnionShare is not supported when using developer mode on Windows or macOS.", - "settings_error_bundled_tor_timeout": "Connecting to Tor is taking too long. Maybe your computer is offline, or your system clock isn't accurate.", + "settings_error_missing_password": "Connected to the Tor controller, type in the password for it.", + "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_test_success": "Congratulations, OnionShare can connect to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}\nSupports stealth onion services: {}", + "settings_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports shares that require HidServAuth: {}.", "error_tor_protocol_error": "There was an error with Tor: {}", "error_tor_protocol_error_unknown": "There was an unknown error with Tor", "error_invalid_private_key": "This private key type is unsupported", "connecting_to_tor": "Connecting to the Tor network", - "update_available": "A new version of OnionShare is available. Click here to download it.

Installed version: {}
Latest version: {}", - "update_error_check_error": "Error checking for updates: Maybe you're not connected to Tor, or maybe the OnionShare website is down.", - "update_error_invalid_latest_version": "Error checking for updates: The OnionShare website responded saying the latest version is '{}', but that doesn't appear to be a valid version string.", - "update_not_available": "You are running the latest version of OnionShare.", - "gui_tor_connection_ask": "Would you like to open OnionShare settings to troubleshoot connecting to Tor?", - "gui_tor_connection_ask_open_settings": "Open Settings", + "update_available": "New OnionShare out. Click here to get it.

You are using {} and the latest is {}.", + "update_error_check_error": "Could not check for new versions: The OnionShare website is saying the latest version is the unrecognizable '{}'…", + "update_error_invalid_latest_version": "Could not check for new version: Maybe you're not connected to Tor, or the OnionShare website is down?", + "update_not_available": "Running the latest OnionShare.", + "gui_tor_connection_ask": "Open the settings to sort out connection to Tor?", + "gui_tor_connection_ask_open_settings": "Yes", "gui_tor_connection_ask_quit": "Quit", - "gui_tor_connection_error_settings": "Try adjusting how OnionShare connects to the Tor network in Settings.", - "gui_tor_connection_canceled": "OnionShare could not connect to Tor.\n\nMake sure you're connected to the Internet, then re-open OnionShare to set up the Tor connection.", + "gui_tor_connection_error_settings": "Try changing how OnionShare connects to the Tor network in the settings.", + "gui_tor_connection_canceled": "Could not connect to Tor.\n\nEnsure you are connected to the Internet, then re-open OnionShare and set up its connection to Tor.", "gui_tor_connection_lost": "Disconnected from Tor.", - "gui_server_started_after_timeout": "The server started after your chosen auto-timeout.\nPlease start a new share.", - "gui_server_timeout_expired": "The chosen timeout has already expired.\nPlease update the timeout and then you may start sharing.", - "share_via_onionshare": "Share via OnionShare", + "gui_server_started_after_timeout": "The set sharing timeframe ran out before the server started.\nPlease make a new share.", + "gui_server_timeout_expired": "The chosen sharing timeframe has already expired.\nPlease update it to start sharing.", + "share_via_onionshare": "OnionShare it", "gui_use_legacy_v2_onions_checkbox": "Use legacy addresses", "gui_save_private_key_checkbox": "Use a persistent address (legacy)", - "gui_share_url_description": "Anyone with this link can download your files using the Tor Browser: ", - "gui_receive_url_description": "Anyone with this link can upload files to your computer using the Tor Browser: ", - "gui_url_label_persistent": "This share will not expire automatically unless a timer is set.

Every share will have the same address (to use one-time addresses, disable persistence in Settings)", - "gui_url_label_stay_open": "This share will not expire automatically unless a timer is set.", - "gui_url_label_onetime": "This share will expire after the first download", - "gui_url_label_onetime_and_persistent": "This share will expire after the first download

Every share will have the same address (to use one-time addresses, disable persistence in the Settings)", - "gui_status_indicator_share_stopped": "Ready to Share", + "gui_share_url_description": "Anyone with this OnionShare address can fetch your share using the Tor Browser: ", + "gui_receive_url_description": "Anyone with this link can upload files to your inbox folder using the Tor Browser: ", + "gui_url_label_persistent": "This share will not auto-expire unless a timeframe is set.

Every subsequent share reuse the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)", + "gui_url_label_stay_open": "This share will not auto-expire unless a timeframe is set.", + "gui_url_label_onetime": "This share will expire after first completion.", + "gui_url_label_onetime_and_persistent": "This share will not auto-expire unless a timeframe is set.

Every subsequent share will reuse the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)", + "gui_status_indicator_share_stopped": "Ready to share", "gui_status_indicator_share_working": "Starting…", "gui_status_indicator_share_started": "Sharing", - "gui_status_indicator_receive_stopped": "Ready to Receive", + "gui_status_indicator_receive_stopped": "Ready to fetch", "gui_status_indicator_receive_working": "Starting…", - "gui_status_indicator_receive_started": "Receiving", - "gui_file_info": "{} Files, {}", - "gui_file_info_single": "{} File, {}", - "info_in_progress_downloads_tooltip": "{} download(s) in progress", - "info_completed_downloads_tooltip": "{} download(s) completed", - "info_in_progress_uploads_tooltip": "{} upload(s) in progress", - "info_completed_uploads_tooltip": "{} upload(s) completed", - "error_cannot_create_downloads_dir": "Error creating downloads folder: {}", - "error_downloads_dir_not_writable": "The downloads folder isn't writable: {}", - "receive_mode_downloads_dir": "Files people send you will appear in this folder: {}", - "receive_mode_warning": "Warning: Receive mode lets someone else upload files to your computer. Some files can hack your computer if you open them! Only open files from people you trust, or if you know what you're doing.", - "gui_receive_mode_warning": "Some files can hack your computer if you open them!
Only open files from people you trust, or if you know what you're doing.", + "gui_status_indicator_receive_started": "Fetching", + "gui_file_info": "{} files, {}", + "gui_file_info_single": "{} file, {}", + "info_in_progress_downloads_tooltip": "{} incoming", + "info_completed_downloads_tooltip": "{} incoming completed", + "info_in_progress_uploads_tooltip": "{} outgoing", + "info_completed_uploads_tooltip": "{} outgoing completed", + "error_cannot_create_downloads_dir": "Could not create inbox: {}", + "error_downloads_dir_not_writable": "The inbox folder is write protected: {}", + "receive_mode_downloads_dir": "Files sent to you appear in this folder: {}", + "receive_mode_warning": "Warning: Receive mode lets someone else upload files to your computer, potentially taking control of it if you open them. Only open things from people you trust to know what they are doing.", + "gui_receive_mode_warning": "A share could contain files that take control of your computer if opened!
Only open things from people you trust to know what they are doing.", "receive_mode_upload_starting": "Upload of total size {} is starting", - "receive_mode_received_file": "Received file: {}", - "gui_mode_share_button": "Share Files", - "gui_mode_receive_button": "Receive Files", - "gui_settings_receiving_label": "Receiving settings", - "gui_settings_downloads_label": "Save files to", + "receive_mode_received_file": "Received: {}", + "gui_mode_share_button": "Share", + "gui_mode_receive_button": "Receive", + "gui_settings_receiving_label": "Reception settings", + "gui_settings_downloads_label": "Save shares to", "gui_settings_downloads_button": "Browse", "gui_settings_receive_allow_receiver_shutdown_checkbox": "Receive mode can be stopped by the sender", "gui_settings_public_mode_checkbox": "Public mode", - "systray_close_server_title": "OnionShare Server Closed", + "systray_close_server_title": "OnionShare Server Shut Down", "systray_close_server_message": "A user closed the server", "systray_page_loaded_title": "OnionShare Page Loaded", - "systray_download_page_loaded_message": "A user loaded the download page", - "systray_upload_page_loaded_message": "A user loaded the upload page", - "gui_uploads": "Upload History", - "gui_no_uploads": "No uploads yet.", - "gui_clear_history": "Clear history", - "gui_upload_in_progress": "Upload Started {}", - "gui_upload_finished_range": "Uploaded {} to {}", - "gui_upload_finished": "Uploaded {}", - "gui_open_folder_error_nautilus": "Cannot open folder the because nautilus is not available. You can find this file here: {}" + "systray_download_page_loaded_message": "Your page has been loaded", + "systray_upload_page_loaded_message": "Someone is browsing what you share", + "gui_uploads": "Sent Shares", + "gui_no_uploads": "Nothing sent yet.", + "gui_clear_history": "Clear", + "gui_upload_in_progress": "Delivery Started {}", + "gui_upload_finished_range": "Sent {} to {}", + "gui_upload_finished": "Sent {}", + "gui_open_folder_error_nautilus": "Cannot open folder without a file manager. The file is here: {}" } From d773a777cc8f804ce6fb9a366eb0e43576663789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sun, 23 Sep 2018 06:28:31 +0200 Subject: [PATCH 054/123] No "expired", Allowed sharing timeframe --- share/locale/en.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/share/locale/en.json b/share/locale/en.json index a36fc0a4..9269611d 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -10,7 +10,7 @@ "not_a_readable_file": "{0:s} is not a readable file.", "no_available_port": "Could not find an available port to start the onion service", "other_page_loaded": "Inbox address loaded", - "close_on_timeout": "Stopped because timeframe expired", + "close_on_timeout": "Stopped because timeframe ran out", "closing_automatically": "Stopped because you have the complete share", "timeout_download_still_running": "Awaiting completion of share transfer to you", "large_filesize": "Warning: Sending a large share could take hours", @@ -38,7 +38,7 @@ "gui_share_start_server": "Share", "gui_share_stop_server": "Stop", "gui_share_stop_server_shutdown_timeout": "Stop sharing ({}s remaining)", - "gui_share_stop_server_shutdown_timeout_tooltip": "Allowed share timeframe ends {}", + "gui_share_stop_server_shutdown_timeout_tooltip": "Allowed sharing timeframe ends {}", "gui_receive_start_server": "Receive", "gui_receive_stop_server": "Stop", "gui_receive_stop_server_shutdown_timeout": "Stop receiving ({}s remaining)", @@ -105,7 +105,7 @@ "gui_settings_button_cancel": "Cancel", "gui_settings_button_help": "Help", "gui_settings_shutdown_timeout_checkbox": "Stop sharing after a given timeframe", - "gui_settings_shutdown_timeout": "Stop sharing after:", + "gui_settings_shutdown_timeout": "Allowed sharing timeframe:", "settings_saved": "Settings saved in {}", "settings_error_unknown": "Can't connect to Tor controller because it knows your settings aren't making sense.", "settings_error_automatic": "Could not connect to the Tor controller. The Tor Browser (available from https://www.torproject.org/)\nneeds to be running in the background.", @@ -132,8 +132,8 @@ "gui_tor_connection_error_settings": "Try changing how OnionShare connects to the Tor network in the settings.", "gui_tor_connection_canceled": "Could not connect to Tor.\n\nEnsure you are connected to the Internet, then re-open OnionShare and set up its connection to Tor.", "gui_tor_connection_lost": "Disconnected from Tor.", - "gui_server_started_after_timeout": "The set sharing timeframe ran out before the server started.\nPlease make a new share.", - "gui_server_timeout_expired": "The chosen sharing timeframe has already expired.\nPlease update it to start sharing.", + "gui_server_started_after_timeout": "The chosen allowed sharing timeframe ran out before the server started.\nPlease make a new share.", + "gui_server_timeout_expired": "The chosen allowed sharing timeframe already ran out.\nPlease update it to start sharing.", "share_via_onionshare": "OnionShare it", "gui_use_legacy_v2_onions_checkbox": "Use legacy addresses", "gui_save_private_key_checkbox": "Use a persistent address (legacy)", From bead9d93e9a1ac077181872444960b660e2251b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sun, 23 Sep 2018 06:38:39 +0200 Subject: [PATCH 055/123] Spelling cancelled ;) --- share/locale/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/locale/en.json b/share/locale/en.json index 9269611d..bb6b1f37 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -47,7 +47,7 @@ "gui_copy_hidservauth": "Copy HidServAuth", "gui_downloads": "History of sent shares", "gui_no_downloads": "None yet.", - "gui_canceled": "Canceled", + "gui_canceled": "Cancelled", "gui_copied_url_title": "Copied", "gui_copied_url": "OnionShare address copied to clipboard", "gui_copied_hidservauth_title": "Copied", From 86f2c198d5a0e667573a850ead64dd4b04c45aea Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 23 Sep 2018 14:00:13 -0700 Subject: [PATCH 056/123] Fix some of the language on strings, and move back to "upload" and "download" terminology --- share/locale/en.json | 162 +++++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/share/locale/en.json b/share/locale/en.json index bb6b1f37..adec5a20 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -1,32 +1,32 @@ { "config_onion_service": "Setting up onion service on port {0:d}.", - "preparing_files": "Preparing share.", + "preparing_files": "Compressing files.", "give_this_url": "Give this address to the recipient:", - "give_this_url_stealth": "Give this inbox address and HidServAuth line to the recipient:", - "give_this_url_receive": "Give this inbox address to the sender:", - "give_this_url_receive_stealth": "Give this inbox address and HidServAuth to the sender:", + "give_this_url_stealth": "Give this address and HidServAuth line to the recipient:", + "give_this_url_receive": "Give this address to the sender:", + "give_this_url_receive_stealth": "Give this address and HidServAuth to the sender:", "ctrlc_to_stop": "Press Ctrl+C to stop the server", "not_a_file": "{0:s} is not a valid file.", "not_a_readable_file": "{0:s} is not a readable file.", "no_available_port": "Could not find an available port to start the onion service", - "other_page_loaded": "Inbox address loaded", - "close_on_timeout": "Stopped because timeframe ran out", - "closing_automatically": "Stopped because you have the complete share", - "timeout_download_still_running": "Awaiting completion of share transfer to you", + "other_page_loaded": "Address loaded", + "close_on_timeout": "Stopped because auto-stop timer ran out", + "closing_automatically": "Stopped because download finished", + "timeout_download_still_running": "Waiting for download to complete", "large_filesize": "Warning: Sending a large share could take hours", "systray_menu_exit": "Quit", - "systray_download_started_title": "Sending…", - "systray_download_started_message": "Your share is being sent.", - "systray_download_completed_title": "Share sent", - "systray_download_completed_message": "Your share has been sent.", - "systray_download_canceled_title": "Share cancelled", - "systray_download_canceled_message": "The recipient cancelled the share.", - "systray_upload_started_title": "Receiving…", - "systray_upload_started_message": "Started share transfer to inbox.", - "help_local_only": "Avoid using Tor: (Only for development)", - "help_stay_open": "Keep sending after one completion", + "systray_download_started_title": "OnionShare Download Started", + "systray_download_started_message": "A user started downloading your files", + "systray_download_completed_title": "OnionShare Download Finished", + "systray_download_completed_message": "The user finished downloading your files", + "systray_download_canceled_title": "OnionShare Download Canceled", + "systray_download_canceled_message": "The user canceled the download", + "systray_upload_started_title": "OnionShare Upload Started", + "systray_upload_started_message": "A user started uploading files to your computer", + "help_local_only": "Don't use Tor (only for development)", + "help_stay_open": "Keep sharing after first download", "help_shutdown_timeout": "Stop sharing after a given amount of seconds", - "help_stealth": "Make share that requires HidServAuth (advanced)", + "help_stealth": "Use client authorization (advanced)", "help_receive": "Receive shares instead of sending them", "help_debug": "Log OnionShare errors to stdout, and web errors to disk", "help_filename": "List of files or folders to share", @@ -35,40 +35,40 @@ "gui_add": "Add", "gui_delete": "Delete", "gui_choose_items": "Choose", - "gui_share_start_server": "Share", - "gui_share_stop_server": "Stop", - "gui_share_stop_server_shutdown_timeout": "Stop sharing ({}s remaining)", - "gui_share_stop_server_shutdown_timeout_tooltip": "Allowed sharing timeframe ends {}", - "gui_receive_start_server": "Receive", - "gui_receive_stop_server": "Stop", - "gui_receive_stop_server_shutdown_timeout": "Stop receiving ({}s remaining)", - "gui_receive_stop_server_shutdown_timeout_tooltip": "Allowed receive timeframe ends {}", + "gui_share_start_server": "Start sharing", + "gui_share_stop_server": "Stop sharing", + "gui_share_stop_server_shutdown_timeout": "Stop Sharing ({}s remaining)", + "gui_share_stop_server_shutdown_timeout_tooltip": "Auto-stop timer ends at {}", + "gui_receive_start_server": "Start Receive Mode", + "gui_receive_stop_server": "Stop Receive Mode", + "gui_receive_stop_server_shutdown_timeout": "Stop Receive Mode ({}s remaining)", + "gui_receive_stop_server_shutdown_timeout_tooltip": "Auto-stop timer ends at {}", "gui_copy_url": "Copy Address", "gui_copy_hidservauth": "Copy HidServAuth", - "gui_downloads": "History of sent shares", - "gui_no_downloads": "None yet.", - "gui_canceled": "Cancelled", - "gui_copied_url_title": "Copied", + "gui_downloads": "Download History", + "gui_no_downloads": "No downloads yet.", + "gui_canceled": "Canceled", + "gui_copied_url_title": "Copied OnionShare Address", "gui_copied_url": "OnionShare address copied to clipboard", - "gui_copied_hidservauth_title": "Copied", + "gui_copied_hidservauth_title": "Copied HidServAuth", "gui_copied_hidservauth": "HidServAuth line copied to clipboard", "gui_please_wait": "Starting… Click to cancel.", "gui_download_upload_progress_complete": "%p%, {0:s} elapsed.", - "gui_download_upload_progress_starting": "{0:s}, %p% (calculating end time.)", + "gui_download_upload_progress_starting": "{0:s}, %p% (calculating)", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", "version_string": "OnionShare {0:s} | https://onionshare.org/", "gui_quit_title": "Not so fast", - "gui_share_quit_warning": "Quitting now means files en route elsewhere won't finish.", - "gui_receive_quit_warning": "Quitting now means files en route to you won't finish.", + "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": "Too many wrong attempts on your sharing address means someone could be trying to guess it. Start OnionShare again and send a new address to share.", - "zip_progress_bar_format": "Making share smaller: %p%", - "error_stealth_not_supported": "To create shares that require knowing HidServAuth, you need at least both Tor 0.2.9.1-alpha (or Tor Browser 6.5) and python3-stem 1.5.0.", + "error_rate_limit": "Someone has made too many wrong attempts on your address, which means they could be trying to guess it, so OnionShare has stopped the server. Start sharing again and send the receipient 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.", "error_ephemeral_not_supported": "OnionShare requires at least both Tor 0.2.7.1 and python3-stem 1.4.0.", "gui_settings_window_title": "Settings", "gui_settings_whats_this": "What's this?", - "gui_settings_stealth_option": "Create share that requires knowing HidServAuth (legacy)", + "gui_settings_stealth_option": "Use client authorization (legacy)", "gui_settings_stealth_hidservauth_string": "Having saved your private key for reuse, means you can now\nclick to copy your HidServAuth.", "gui_settings_autoupdate_label": "Check for new version", "gui_settings_autoupdate_option": "Notify me when a new version is available", @@ -77,7 +77,7 @@ "gui_settings_autoupdate_check_button": "Check for New Version", "gui_settings_general_label": "General settings", "gui_settings_sharing_label": "Sharing settings", - "gui_settings_close_after_first_download_option": "Stop sharing after first completion", + "gui_settings_close_after_first_download_option": "Stop sharing after first download", "gui_settings_connection_type_label": "How should OnionShare connect to Tor?", "gui_settings_connection_type_bundled_option": "Use the Tor version built into OnionShare", "gui_settings_connection_type_automatic_option": "Attempt auto-configuration with Tor Browser", @@ -87,8 +87,8 @@ "gui_settings_control_port_label": "Control port", "gui_settings_socket_file_label": "Socket file", "gui_settings_socks_label": "SOCKS port", - "gui_settings_authenticate_label": "Tor identification settings", - "gui_settings_authenticate_no_auth_option": "None or cookie based", + "gui_settings_authenticate_label": "Tor authentication settings", + "gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication", "gui_settings_authenticate_password_option": "Password", "gui_settings_password_label": "Password", "gui_settings_tor_bridges": "Tor bridge support", @@ -104,15 +104,15 @@ "gui_settings_button_save": "Save", "gui_settings_button_cancel": "Cancel", "gui_settings_button_help": "Help", - "gui_settings_shutdown_timeout_checkbox": "Stop sharing after a given timeframe", - "gui_settings_shutdown_timeout": "Allowed sharing timeframe:", + "gui_settings_shutdown_timeout_checkbox": "Use auto-stop timer", + "gui_settings_shutdown_timeout": "Stop the share at:", "settings_saved": "Settings saved in {}", - "settings_error_unknown": "Can't connect to Tor controller because it knows your settings aren't making sense.", - "settings_error_automatic": "Could not connect to the Tor controller. The Tor Browser (available from https://www.torproject.org/)\nneeds to be running in the background.", + "settings_error_unknown": "Can't connect to Tor controller because your settings don't make sense.", + "settings_error_automatic": "Could not connect to the Tor controller. Is Tor Browser (available from https://www.torproject.org/)\nrunning in the background?", "settings_error_socket_port": "Can't connect to the Tor controller at {}:{}.", - "settings_error_socket_file": "The Tor controller does not allow connection using the socket file {}.", + "settings_error_socket_file": "Can't connect to the Tor controller using socket file {}.", "settings_error_auth": "Connected to {}:{}, but can't authenticate. Maybe this isn't a Tor controller?", - "settings_error_missing_password": "Connected to the Tor controller, type in the password for it.", + "settings_error_missing_password": "Connected to Tor controller, but it requires a password to authenticate.", "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?", @@ -125,60 +125,60 @@ "update_available": "New OnionShare out. Click here to get it.

You are using {} and the latest is {}.", "update_error_check_error": "Could not check for new versions: The OnionShare website is saying the latest version is the unrecognizable '{}'…", "update_error_invalid_latest_version": "Could not check for new version: Maybe you're not connected to Tor, or the OnionShare website is down?", - "update_not_available": "Running the latest OnionShare.", + "update_not_available": "You are running the latest OnionShare.", "gui_tor_connection_ask": "Open the settings to sort out connection to Tor?", "gui_tor_connection_ask_open_settings": "Yes", "gui_tor_connection_ask_quit": "Quit", "gui_tor_connection_error_settings": "Try changing how OnionShare connects to the Tor network in the settings.", "gui_tor_connection_canceled": "Could not connect to Tor.\n\nEnsure you are connected to the Internet, then re-open OnionShare and set up its connection to Tor.", "gui_tor_connection_lost": "Disconnected from Tor.", - "gui_server_started_after_timeout": "The chosen allowed sharing timeframe ran out before the server started.\nPlease make a new share.", - "gui_server_timeout_expired": "The chosen allowed sharing timeframe already ran out.\nPlease update it to start sharing.", + "gui_server_started_after_timeout": "The auto-stop timer ran out before the server started.\nPlease make a new share.", + "gui_server_timeout_expired": "The auto-stop timer already ran out.\nPlease update it to start sharing.", "share_via_onionshare": "OnionShare it", "gui_use_legacy_v2_onions_checkbox": "Use legacy addresses", "gui_save_private_key_checkbox": "Use a persistent address (legacy)", - "gui_share_url_description": "Anyone with this OnionShare address can fetch your share using the Tor Browser: ", - "gui_receive_url_description": "Anyone with this link can upload files to your inbox folder using the Tor Browser: ", - "gui_url_label_persistent": "This share will not auto-expire unless a timeframe is set.

Every subsequent share reuse the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)", - "gui_url_label_stay_open": "This share will not auto-expire unless a timeframe is set.", - "gui_url_label_onetime": "This share will expire after first completion.", - "gui_url_label_onetime_and_persistent": "This share will not auto-expire unless a timeframe is set.

Every subsequent share will reuse the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)", + "gui_share_url_description": "Anyone with this OnionShare address can download your files using the Tor Browser: ", + "gui_receive_url_description": "Anyone with this OnionShare address can upload files to your computer using the Tor Browser: ", + "gui_url_label_persistent": "This share will not auto-stop.

Every subsequent share reuses the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)", + "gui_url_label_stay_open": "This share will not auto-stop.", + "gui_url_label_onetime": "This share will stop after first completion.", + "gui_url_label_onetime_and_persistent": "This share will not auto-stop.

Every subsequent share will reuse the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)", "gui_status_indicator_share_stopped": "Ready to share", "gui_status_indicator_share_working": "Starting…", "gui_status_indicator_share_started": "Sharing", - "gui_status_indicator_receive_stopped": "Ready to fetch", + "gui_status_indicator_receive_stopped": "Ready to receive", "gui_status_indicator_receive_working": "Starting…", - "gui_status_indicator_receive_started": "Fetching", + "gui_status_indicator_receive_started": "Receiving", "gui_file_info": "{} files, {}", "gui_file_info_single": "{} file, {}", - "info_in_progress_downloads_tooltip": "{} incoming", - "info_completed_downloads_tooltip": "{} incoming completed", - "info_in_progress_uploads_tooltip": "{} outgoing", - "info_completed_uploads_tooltip": "{} outgoing completed", - "error_cannot_create_downloads_dir": "Could not create inbox: {}", - "error_downloads_dir_not_writable": "The inbox folder is write protected: {}", + "info_in_progress_downloads_tooltip": "{} download(s) in progress", + "info_completed_downloads_tooltip": "{} download(s) completed", + "info_in_progress_uploads_tooltip": "{} upload(s) in progress", + "info_completed_uploads_tooltip": "{} upload(s) completed", + "error_cannot_create_downloads_dir": "Could not create receive mode folder: {}", + "error_downloads_dir_not_writable": "The receive mode folder is write protected: {}", "receive_mode_downloads_dir": "Files sent to you appear in this folder: {}", - "receive_mode_warning": "Warning: Receive mode lets someone else upload files to your computer, potentially taking control of it if you open them. Only open things from people you trust to know what they are doing.", - "gui_receive_mode_warning": "A share could contain files that take control of your computer if opened!
Only open things from people you trust to know what they are doing.", + "receive_mode_warning": "Warning: Receive mode lets someone else 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.", + "gui_receive_mode_warning": "Receive mode lets someone else 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.", "receive_mode_upload_starting": "Upload of total size {} is starting", "receive_mode_received_file": "Received: {}", - "gui_mode_share_button": "Share", - "gui_mode_receive_button": "Receive", - "gui_settings_receiving_label": "Reception settings", - "gui_settings_downloads_label": "Save shares to", + "gui_mode_share_button": "Share Files", + "gui_mode_receive_button": "Receive Files", + "gui_settings_receiving_label": "Receiving settings", + "gui_settings_downloads_label": "Save files to", "gui_settings_downloads_button": "Browse", "gui_settings_receive_allow_receiver_shutdown_checkbox": "Receive mode can be stopped by the sender", "gui_settings_public_mode_checkbox": "Public mode", - "systray_close_server_title": "OnionShare Server Shut Down", + "systray_close_server_title": "OnionShare Server Closed", "systray_close_server_message": "A user closed the server", "systray_page_loaded_title": "OnionShare Page Loaded", - "systray_download_page_loaded_message": "Your page has been loaded", - "systray_upload_page_loaded_message": "Someone is browsing what you share", - "gui_uploads": "Sent Shares", - "gui_no_uploads": "Nothing sent yet.", - "gui_clear_history": "Clear", - "gui_upload_in_progress": "Delivery Started {}", - "gui_upload_finished_range": "Sent {} to {}", - "gui_upload_finished": "Sent {}", - "gui_open_folder_error_nautilus": "Cannot open folder without a file manager. The file is here: {}" + "systray_download_page_loaded_message": "A user loaded the download page", + "systray_upload_page_loaded_message": "A user loaded the upload page", + "gui_uploads": "Upload History", + "gui_no_uploads": "No uploads yet.", + "gui_clear_history": "Clear history", + "gui_upload_in_progress": "Upload Started {}", + "gui_upload_finished_range": "Uploaded {} to {}", + "gui_upload_finished": "Uploaded {}", + "gui_open_folder_error_nautilus": "Cannot open folder because nautilus is not available. The file is here: {}" } From 6193047d6bf239468572dfe72e70294ce785bc0a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 23 Sep 2018 14:36:36 -0700 Subject: [PATCH 057/123] Fixes a few strings after testing --- share/locale/en.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/share/locale/en.json b/share/locale/en.json index adec5a20..608fbfbc 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -108,7 +108,7 @@ "gui_settings_shutdown_timeout": "Stop the share at:", "settings_saved": "Settings saved in {}", "settings_error_unknown": "Can't connect to Tor controller because your settings don't make sense.", - "settings_error_automatic": "Could not connect to the Tor controller. Is Tor Browser (available from https://www.torproject.org/)\nrunning in the background?", + "settings_error_automatic": "Could not connect to the Tor controller. Is Tor Browser (available from torproject.org) running in the background?", "settings_error_socket_port": "Can't connect to the Tor controller at {}:{}.", "settings_error_socket_file": "Can't connect to the Tor controller using socket file {}.", "settings_error_auth": "Connected to {}:{}, but can't authenticate. Maybe this isn't a Tor controller?", @@ -117,7 +117,7 @@ "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_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports shares that require HidServAuth: {}.", + "settings_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports client authentication: {}.", "error_tor_protocol_error": "There was an error with Tor: {}", "error_tor_protocol_error_unknown": "There was an unknown error with Tor", "error_invalid_private_key": "This private key type is unsupported", @@ -158,8 +158,8 @@ "error_cannot_create_downloads_dir": "Could not create receive mode folder: {}", "error_downloads_dir_not_writable": "The receive mode folder is write protected: {}", "receive_mode_downloads_dir": "Files sent to you appear in this folder: {}", - "receive_mode_warning": "Warning: Receive mode lets someone else 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.", - "gui_receive_mode_warning": "Receive mode lets someone else 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.", + "receive_mode_warning": "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.", + "gui_receive_mode_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.", "receive_mode_upload_starting": "Upload of total size {} is starting", "receive_mode_received_file": "Received: {}", "gui_mode_share_button": "Share Files", From e460acbb9135a5a18d5c5d73f9dc36ef1550df22 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 23 Sep 2018 14:39:29 -0700 Subject: [PATCH 058/123] Fix locale test --- test/test_onionshare_strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_onionshare_strings.py b/test/test_onionshare_strings.py index 1d0b3206..d3d40c8f 100644 --- a/test/test_onionshare_strings.py +++ b/test/test_onionshare_strings.py @@ -47,7 +47,7 @@ class TestLoadStrings: self, common_obj, locale_en, sys_onionshare_dev_mode): """ load_strings() loads English by default """ strings.load_strings(common_obj) - assert strings._('preparing_files') == "Preparing files to share." + assert strings._('preparing_files') == "Compressing files." def test_load_strings_loads_other_languages( From de9bc975a4d1420a1bdfd52a90fc0b43fdc16975 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 24 Sep 2018 10:41:48 +1000 Subject: [PATCH 059/123] Refactor the unit tests to use common, abstracted tests --- unit_tests/commontests.py | 307 ++++++++++++++++++ .../onionshare_receive_mode_upload_test.py | 142 ++------ ...re_receive_mode_upload_test_public_mode.py | 144 ++------ .../onionshare_share_mode_download_test.py | 189 +++-------- ...re_share_mode_download_test_public_mode.py | 186 +++-------- ...hare_share_mode_download_test_stay_open.py | 171 ++++------ unit_tests/onionshare_slug_persistent_test.py | 108 +++--- unit_tests/onionshare_timer_test.py | 86 ++--- 8 files changed, 625 insertions(+), 708 deletions(-) create mode 100644 unit_tests/commontests.py diff --git a/unit_tests/commontests.py b/unit_tests/commontests.py new file mode 100644 index 00000000..1f6f5896 --- /dev/null +++ b/unit_tests/commontests.py @@ -0,0 +1,307 @@ +import os +import requests +import socket +import socks +import zipfile + +from PyQt5 import QtCore, QtTest +from onionshare import strings + +class CommonTests(object): + def test_gui_loaded(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + def test_info_widget_is_not_visible(self, mode): + '''Test that the info widget along top of screen is not shown''' + if mode == 'receive': + self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) + if mode == 'share': + self.assertFalse(self.gui.share_mode.info_widget.isVisible()) + + def test_info_widget_is_visible(self, mode): + '''Test that the info widget along top of screen is shown''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.info_widget.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + + def test_click_mode(self, mode): + '''Test that we can switch Mode by clicking the button''' + if mode == 'receive': + QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) + if mode == 'share': + QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_SHARE) + + def test_history_is_visible(self, mode): + '''Test that the History section is visible and that the relevant widget is present''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.uploads.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.downloads.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + + def test_server_working_on_start_button_pressed(self, mode): + '''Test we can start the service''' + # Should be in SERVER_WORKING state + if mode == 'receive': + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.receive_mode.server_status.status, 1) + if mode == 'share': + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + def test_server_status_indicator_says_starting(self, mode): + '''Test that the Server Status indicator shows we are Starting''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + if mode == 'share': + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + def test_a_server_is_started(self, mode): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + if mode == 'receive': + self.assertEqual(self.gui.receive_mode.server_status.status, 2) + if mode == 'share': + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + def test_have_a_slug(self, mode, public_mode): + '''Test that we have a valid slug''' + if mode == 'receive': + if not public_mode: + self.assertRegex(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') + else: + self.assertIsNone(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') + if mode == 'share': + if not public_mode: + self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + else: + self.assertIsNone(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + + + def test_url_description_shown(self, mode): + '''Test that the URL label is showing''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + + def test_have_copy_url_button(self, mode): + '''Test that the Copy URL button is shown''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + + def test_server_status_indicator_says_started(self, mode): + '''Test that the Server Status indicator shows we are started''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + if mode == 'share': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + + def test_web_page(self, mode, string, public_mode): + '''Test that the web page contains a string''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + if not public_mode: + if mode == 'receive': + path = '/{}'.format(self.gui.receive_mode.server_status.web.slug) + if mode == 'share': + path = '/{}'.format(self.gui.share_mode.server_status.web.slug) + else: + path = '/' + + http_request = 'GET {} HTTP/1.0\r\n'.format(path) + http_request += 'Host: 127.0.0.1\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 test_history_widgets_present(self, mode): + '''Test that the relevant widgets are present in the history view after activity has taken place''' + if mode == 'receive': + self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) + if mode == 'share': + self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + + def test_counter_incremented(self, mode, count): + '''Test that the counter has incremented''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.uploads_completed, count) + if mode == 'share': + self.assertEquals(self.gui.share_mode.downloads_completed, count) + + def test_server_is_stopped(self, mode, stay_open): + '''Test that the server stops when we click Stop''' + if mode == 'receive': + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.receive_mode.server_status.status, 0) + if mode == 'share': + if stay_open: + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.share_mode.server_status.status, 0) + + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + QtTest.QTest.qWait(2000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + def test_server_status_indicator_says_closed(self, mode, stay_open): + '''Test that the Server Status indicator shows we closed''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + if mode == 'share': + if stay_open: + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped', True)) + else: + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) + + # Auto-stop timer tests + def test_set_timeout(self, mode): + '''Test that the timeout can be set''' + timer = QtCore.QDateTime.currentDateTime().addSecs(120) + if mode == 'receive': + self.gui.receive_mode.server_status.shutdown_timeout.setDateTime(timer) + self.assertTrue(self.gui.receive_mode.server_status.shutdown_timeout.dateTime(), timer) + if mode == 'share': + self.gui.share_mode.server_status.shutdown_timeout.setDateTime(timer) + self.assertTrue(self.gui.share_mode.server_status.shutdown_timeout.dateTime(), timer) + + def test_timeout_widget_hidden(self, mode): + '''Test that the timeout widget is hidden when share has started''' + if mode == 'receive': + self.assertFalse(self.gui.receive_mode.server_status.shutdown_timeout_container.isVisible()) + if mode == 'share': + self.assertFalse(self.gui.share_mode.server_status.shutdown_timeout_container.isVisible()) + + def test_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 + if mode == 'receive': + self.assertEqual(self.gui.receive_mode.server_status.status, 0) + if mode == 'share': + self.assertEqual(self.gui.share_mode.server_status.status, 0) + + # Receive-specific tests + def test_upload_file(self, public_mode, expected_file): + '''Test that we can upload the file''' + files = {'file[]': open('/tmp/test.txt', 'rb')} + if not public_mode: + path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug) + else: + path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) + response = requests.post(path, files=files) + QtTest.QTest.qWait(2000) + self.assertTrue(os.path.isfile(expected_file)) + + # Share-specific tests + def test_file_selection_widget_has_a_file(self): + '''Test that the number of files in the list is 1''' + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + + def test_deleting_only_file_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, and since there's no more files, the delete button should be hidden + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + def test_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.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + + def test_file_selection_widget_readd_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.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + + def test_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 test_download_share(self, public_mode): + '''Test that we can download the share''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + if public_mode: + path = '/download' + else: + path = '{}/download'.format(self.gui.share_mode.web.slug) + + http_request = 'GET {} HTTP/1.0\r\n'.format(path) + http_request += 'Host: 127.0.0.1\r\n' + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/download.zip', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + zip = zipfile.ZipFile('/tmp/download.zip') + QtTest.QTest.qWait(2000) + self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + + def test_add_button_visible(self): + '''Test that the add button should be visible''' + self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + diff --git a/unit_tests/onionshare_receive_mode_upload_test.py b/unit_tests/onionshare_receive_mode_upload_test.py index b99faac0..bac622fb 100644 --- a/unit_tests/onionshare_receive_mode_upload_test.py +++ b/unit_tests/onionshare_receive_mode_upload_test.py @@ -2,20 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -import requests -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -83,176 +81,104 @@ class OnionShareGuiTest(unittest.TestCase): os.remove('/tmp/OnionShare/test-2.txt') @pytest.mark.run(order=1) - def test_gui_loaded_and_tor_bootstrapped(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) def test_windowTitle_seen(self): - '''Test that the window title is OnionShare''' - self.assertEqual(self.gui.windowTitle(), 'OnionShare') + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) def test_settings_button_is_visible(self): - '''Test that the settings button is visible''' - self.assertTrue(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) def test_server_status_bar_is_visible(self): - '''Test that the status bar is visible''' - self.assertTrue(self.gui.status_bar.isVisible()) + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) def test_info_widget_is_not_visible(self): - '''Test that the info widget along top of screen is not shown because we have a file''' - self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) + CommonTests.test_info_widget_is_not_visible(self, 'receive') @pytest.mark.run(order=6) - def test_click_receive_mode(self): - '''Test that we can switch to Receive Mode by clicking the button''' - QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) - self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) + def test_click_mode(self): + CommonTests.test_click_mode(self, 'receive') @pytest.mark.run(order=7) - def test_uploads_section_is_visible(self): - '''Test that the Uploads section is visible and that the No Uploads Yet label is present''' - self.assertTrue(self.gui.receive_mode.uploads.isVisible()) - self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'receive') @pytest.mark.run(order=8) def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.receive_mode.server_status.status, 1) + CommonTests.test_server_working_on_start_button_pressed(self, 'receive') @pytest.mark.run(order=9) def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + CommonTests.test_server_status_indicator_says_starting(self, 'receive') @pytest.mark.run(order=10) def test_settings_button_is_hidden(self): - '''Test that the settings button is hidden when the server starts''' - self.assertFalse(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_hidden(self) @pytest.mark.run(order=11) def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.receive_mode.server_status.status, 2) + CommonTests.test_a_server_is_started(self, 'receive') @pytest.mark.run(order=12) def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) - - # Running in local mode, so we have no .onion - #@pytest.mark.run(order=13) - #def test_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') - # self.assertEqual(len(self.gui.app.onion_host), 62) + CommonTests.test_a_web_server_is_running(self) @pytest.mark.run(order=14) def test_have_a_slug(self): - '''Test that we have a valid slug''' - self.assertRegex(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') + CommonTests.test_have_a_slug(self, 'receive', False) @pytest.mark.run(order=15) def test_url_description_shown(self): - '''Test that the URL label is showing''' - self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) + CommonTests.test_url_description_shown(self, 'receive') @pytest.mark.run(order=16) def test_have_copy_url_button(self): - '''Test that the Copy URL button is shown''' - self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) + CommonTests.test_have_copy_url_button(self, 'receive') @pytest.mark.run(order=17) - def test_server_status_indicator_says_sharing(self): - '''Test that the Server Status indicator shows we are Receiving''' - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'receive') @pytest.mark.run(order=18) def test_web_page(self): - '''Test that the web page contains the term Select the files you want to send, then click''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET {} HTTP/1.0\r\n'.format(self.gui.receive_mode.server_status.web.slug) - http_request += 'Host: 127.0.0.1\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('Select the files you want to send, then click "Send Files"' in f.read()) - f.close() + CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', False) @pytest.mark.run(order=19) def test_upload_file(self): - '''Test that we can upload the file''' - files = {'file[]': open('/tmp/test.txt', 'rb')} - response = requests.post('http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug), files=files) - QtTest.QTest.qWait(2000) - self.assertTrue(os.path.isfile('/tmp/OnionShare/test.txt')) + CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test.txt') @pytest.mark.run(order=20) - def test_uploads_widget_present(self): - '''Test that the No Uploads Yet label is hidden, that Clear History is present''' - self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) - self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'receive') @pytest.mark.run(order=21) - def test_upload_count_incremented(self): - '''Test that the Upload Count has incremented''' - self.assertEquals(self.gui.receive_mode.uploads_completed, 1) + def test_counter_incremented(self): + CommonTests.test_counter_incremented(self, 'receive', 1) @pytest.mark.run(order=22) def test_upload_same_file_is_renamed(self): - '''Test that we can upload the same file and that it gets renamed''' - files = {'file[]': open('/tmp/test.txt', 'rb')} - response = requests.post('http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug), files=files) - QtTest.QTest.qWait(2000) - self.assertTrue(os.path.isfile('/tmp/OnionShare/test-2.txt')) + CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test-2.txt') @pytest.mark.run(order=23) def test_upload_count_incremented_again(self): - '''Test that the Upload Count has incremented again''' - self.assertEquals(self.gui.receive_mode.uploads_completed, 2) + CommonTests.test_counter_incremented(self, 'receive', 2) @pytest.mark.run(order=24) def test_server_is_stopped(self): - '''Test that the server stops when we click Stop''' - QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.receive_mode.server_status.status, 0) + CommonTests.test_server_is_stopped(self, 'receive', False) @pytest.mark.run(order=25) def test_web_service_is_stopped(self): - '''Test that the web server also stopped''' - QtTest.QTest.qWait(2000) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + CommonTests.test_web_service_is_stopped(self) @pytest.mark.run(order=26) def test_server_status_indicator_says_closed(self): - '''Test that the Server Status indicator shows we closed''' - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) if __name__ == "__main__": unittest.main() diff --git a/unit_tests/onionshare_receive_mode_upload_test_public_mode.py b/unit_tests/onionshare_receive_mode_upload_test_public_mode.py index d309e5b1..8ed385f5 100644 --- a/unit_tests/onionshare_receive_mode_upload_test_public_mode.py +++ b/unit_tests/onionshare_receive_mode_upload_test_public_mode.py @@ -2,20 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -import requests -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -83,176 +81,104 @@ class OnionShareGuiTest(unittest.TestCase): os.remove('/tmp/OnionShare/test-2.txt') @pytest.mark.run(order=1) - def test_gui_loaded_and_tor_bootstrapped(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) def test_windowTitle_seen(self): - '''Test that the window title is OnionShare''' - self.assertEqual(self.gui.windowTitle(), 'OnionShare') + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) def test_settings_button_is_visible(self): - '''Test that the settings button is visible''' - self.assertTrue(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) def test_server_status_bar_is_visible(self): - '''Test that the status bar is visible''' - self.assertTrue(self.gui.status_bar.isVisible()) + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) def test_info_widget_is_not_visible(self): - '''Test that the info widget along top of screen is not shown because we have a file''' - self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) + CommonTests.test_info_widget_is_not_visible(self, 'receive') @pytest.mark.run(order=6) - def test_click_receive_mode(self): - '''Test that we can switch to Receive Mode by clicking the button''' - QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) - self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) + def test_click_mode(self): + CommonTests.test_click_mode(self, 'receive') @pytest.mark.run(order=7) - def test_uploads_section_is_visible(self): - '''Test that the Uploads section is visible and that the No Uploads Yet label is present''' - self.assertTrue(self.gui.receive_mode.uploads.isVisible()) - self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'receive') @pytest.mark.run(order=8) def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.receive_mode.server_status.status, 1) + CommonTests.test_server_working_on_start_button_pressed(self, 'receive') @pytest.mark.run(order=9) def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + CommonTests.test_server_status_indicator_says_starting(self, 'receive') @pytest.mark.run(order=10) def test_settings_button_is_hidden(self): - '''Test that the settings button is hidden when the server starts''' - self.assertFalse(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_hidden(self) @pytest.mark.run(order=11) def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.receive_mode.server_status.status, 2) + CommonTests.test_a_server_is_started(self, 'receive') @pytest.mark.run(order=12) def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) - - # Running in local mode, so we have no .onion - #@pytest.mark.run(order=13) - #def test_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') - # self.assertEqual(len(self.gui.app.onion_host), 62) + CommonTests.test_a_web_server_is_running(self) @pytest.mark.run(order=14) - def test_have_no_slug(self): - '''Test that we have a valid slug''' - self.assertIsNone(self.gui.share_mode.server_status.web.slug) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'receive', True) @pytest.mark.run(order=15) def test_url_description_shown(self): - '''Test that the URL label is showing''' - self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) + CommonTests.test_url_description_shown(self, 'receive') @pytest.mark.run(order=16) def test_have_copy_url_button(self): - '''Test that the Copy URL button is shown''' - self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) + CommonTests.test_have_copy_url_button(self, 'receive') @pytest.mark.run(order=17) - def test_server_status_indicator_says_sharing(self): - '''Test that the Server Status indicator shows we are Receiving''' - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'receive') @pytest.mark.run(order=18) def test_web_page(self): - '''Test that the web page contains the term Select the files you want to send, then click''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET / HTTP/1.0\r\n' - http_request += 'Host: 127.0.0.1\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('Select the files you want to send, then click "Send Files"' in f.read()) - f.close() + CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', True) @pytest.mark.run(order=19) def test_upload_file(self): - '''Test that we can upload the file''' - files = {'file[]': open('/tmp/test.txt', 'rb')} - response = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port), files=files) - QtTest.QTest.qWait(2000) - self.assertTrue(os.path.isfile('/tmp/OnionShare/test.txt')) + CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test.txt') @pytest.mark.run(order=20) - def test_uploads_widget_present(self): - '''Test that the No Uploads Yet label is hidden, that Clear History is present''' - self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) - self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'receive') @pytest.mark.run(order=21) - def test_upload_count_incremented(self): - '''Test that the Upload Count has incremented''' - self.assertEquals(self.gui.receive_mode.uploads_completed, 1) + def test_counter_incremented(self): + CommonTests.test_counter_incremented(self, 'receive', 1) @pytest.mark.run(order=22) def test_upload_same_file_is_renamed(self): - '''Test that we can upload the same file and that it gets renamed''' - files = {'file[]': open('/tmp/test.txt', 'rb')} - response = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port), files=files) - QtTest.QTest.qWait(2000) - self.assertTrue(os.path.isfile('/tmp/OnionShare/test-2.txt')) + CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test-2.txt') @pytest.mark.run(order=23) def test_upload_count_incremented_again(self): - '''Test that the Upload Count has incremented again''' - self.assertEquals(self.gui.receive_mode.uploads_completed, 2) + CommonTests.test_counter_incremented(self, 'receive', 2) @pytest.mark.run(order=24) def test_server_is_stopped(self): - '''Test that the server stops when we click Stop''' - QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.receive_mode.server_status.status, 0) + CommonTests.test_server_is_stopped(self, 'receive', False) @pytest.mark.run(order=25) def test_web_service_is_stopped(self): - '''Test that the web server also stopped''' - QtTest.QTest.qWait(2000) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + CommonTests.test_web_service_is_stopped(self) @pytest.mark.run(order=26) def test_server_status_indicator_says_closed(self): - '''Test that the Server Status indicator shows we closed''' - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) if __name__ == "__main__": unittest.main() diff --git a/unit_tests/onionshare_share_mode_download_test.py b/unit_tests/onionshare_share_mode_download_test.py index c4c8bee7..0ef03e97 100644 --- a/unit_tests/onionshare_share_mode_download_test.py +++ b/unit_tests/onionshare_share_mode_download_test.py @@ -2,19 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -from PyQt5 import QtCore, QtWidgets, QtTest, QtGui +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -80,210 +79,112 @@ class OnionShareGuiTest(unittest.TestCase): os.remove('/tmp/test.txt') @pytest.mark.run(order=1) - def test_gui_loaded_and_tor_bootstrapped(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) def test_windowTitle_seen(self): - '''Test that the window title is OnionShare''' - self.assertEqual(self.gui.windowTitle(), 'OnionShare') + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) def test_settings_button_is_visible(self): - '''Test that the settings button is visible''' - self.assertTrue(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) def test_server_status_bar_is_visible(self): - '''Test that the status bar is visible''' - self.assertTrue(self.gui.status_bar.isVisible()) + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) def test_file_selection_widget_has_a_file(self): - '''Test that the number of files in the list is 1''' - self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) def test_info_widget_is_visible(self): - '''Test that the info widget along top of screen is shown because we have a file''' - self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + CommonTests.test_info_widget_is_visible(self, 'share') @pytest.mark.run(order=7) - def test_downloads_section_is_visible(self): - '''Test that the Downloads section is visible and that the No Downloads Yet label is present''' - self.assertTrue(self.gui.share_mode.downloads.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') @pytest.mark.run(order=8) def test_deleting_only_file_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, and since there's no more files, the delete button should be hidden - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) - self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + CommonTests.test_deleting_only_file_hides_delete_button(self) @pytest.mark.run(order=9) def test_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.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) @pytest.mark.run(order=10) def test_file_selection_widget_readd_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.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + CommonTests.test_file_selection_widget_readd_files(self) @pytest.mark.run(order=11) def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.share_mode.server_status.status, 1) + CommonTests.test_server_working_on_start_button_pressed(self, 'share') @pytest.mark.run(order=12) def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + CommonTests.test_server_status_indicator_says_starting(self, 'share') @pytest.mark.run(order=13) - def test_add_delete_buttons_now_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 test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) @pytest.mark.run(order=14) def test_settings_button_is_hidden(self): - '''Test that the settings button is hidden when the server starts''' - self.assertFalse(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_hidden(self) @pytest.mark.run(order=15) def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.share_mode.server_status.status, 2) + CommonTests.test_a_server_is_started(self, 'share') @pytest.mark.run(order=16) def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + CommonTests.test_a_web_server_is_running(self) - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) - - # Running in local mode, so we have no .onion - #@pytest.mark.run(order=17) - #def test_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') - # self.assertEqual(len(self.gui.app.onion_host), 62) + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', False) @pytest.mark.run(order=18) - def test_have_a_slug(self): - '''Test that we have a valid slug''' - self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') @pytest.mark.run(order=19) - def test_url_description_shown(self): - '''Test that the URL label is showing''' - self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') @pytest.mark.run(order=20) - def test_have_copy_url_button(self): - '''Test that the Copy URL button is shown and can be copied to clipboard''' - self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.copy_url_button, QtCore.Qt.LeftButton) - clipboard = self.gui.qtapp.clipboard() - self.assertEquals(clipboard.text(), 'http://127.0.0.1:{}/{}'.format(self.gui.app.port, self.gui.share_mode.server_status.web.slug)) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') @pytest.mark.run(order=21) - def test_server_status_indicator_says_sharing(self): - '''Test that the Server Status indicator shows we are Sharing''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + def test_web_page(self): + CommonTests.test_web_page(self, 'share', 'Total size', False) @pytest.mark.run(order=22) - def test_web_page(self): - '''Test that the web page contains the term Total size''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET {} HTTP/1.0\r\n'.format(self.gui.share_mode.server_status.web.slug) - http_request += 'Host: 127.0.0.1\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('Total size' in f.read()) - f.close() + def test_download_share(self): + CommonTests.test_download_share(self, False) @pytest.mark.run(order=23) - def test_download_share(self): - '''Test that we can download the share''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET {}/download HTTP/1.0\r\n'.format(self.gui.share_mode.server_status.web.slug) - http_request += 'Host: 127.0.0.1\r\n' - http_request += '\r\n' - s.sendall(http_request.encode('utf-8')) - - with open('/tmp/download.zip', 'wb') as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - - zip = zipfile.ZipFile('/tmp/download.zip') - self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'share') @pytest.mark.run(order=24) - def test_downloads_widget_present(self): - QtTest.QTest.qWait(1000) - '''Test that the No Downloads Yet label is hidden, that Clear History is present''' - self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) @pytest.mark.run(order=25) - def test_server_is_stopped(self): - '''Test that the server stopped automatically when we downloaded the share''' - self.assertEquals(self.gui.share_mode.server_status.status, 0) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) @pytest.mark.run(order=26) - def test_web_service_is_stopped(self): - '''Test that the web server also stopped''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', False) @pytest.mark.run(order=27) - def test_server_status_indicator_says_closed(self): - '''Test that the Server Status indicator shows we closed because download occurred''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) - - @pytest.mark.run(order=28) - def test_add_button_visible_again(self): - '''Test that the add button should be visible again''' - self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) if __name__ == "__main__": diff --git a/unit_tests/onionshare_share_mode_download_test_public_mode.py b/unit_tests/onionshare_share_mode_download_test_public_mode.py index 59905149..417eb8b9 100644 --- a/unit_tests/onionshare_share_mode_download_test_public_mode.py +++ b/unit_tests/onionshare_share_mode_download_test_public_mode.py @@ -2,19 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -80,207 +79,112 @@ class OnionShareGuiTest(unittest.TestCase): os.remove('/tmp/test.txt') @pytest.mark.run(order=1) - def test_gui_loaded_and_tor_bootstrapped(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) def test_windowTitle_seen(self): - '''Test that the window title is OnionShare''' - self.assertEqual(self.gui.windowTitle(), 'OnionShare') + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) def test_settings_button_is_visible(self): - '''Test that the settings button is visible''' - self.assertTrue(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) def test_server_status_bar_is_visible(self): - '''Test that the status bar is visible''' - self.assertTrue(self.gui.status_bar.isVisible()) + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) def test_file_selection_widget_has_a_file(self): - '''Test that the number of files in the list is 1''' - self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) def test_info_widget_is_visible(self): - '''Test that the info widget along top of screen is shown because we have a file''' - self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + CommonTests.test_info_widget_is_visible(self, 'share') @pytest.mark.run(order=7) - def test_downloads_section_is_visible(self): - '''Test that the Downloads section is visible and that the No Downloads Yet label is present''' - self.assertTrue(self.gui.share_mode.downloads.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') @pytest.mark.run(order=8) def test_deleting_only_file_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, and since there's no more files, the delete button should be hidden - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) - self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + CommonTests.test_deleting_only_file_hides_delete_button(self) @pytest.mark.run(order=9) def test_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.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) @pytest.mark.run(order=10) def test_file_selection_widget_readd_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.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + CommonTests.test_file_selection_widget_readd_files(self) @pytest.mark.run(order=11) def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.share_mode.server_status.status, 1) + CommonTests.test_server_working_on_start_button_pressed(self, 'share') @pytest.mark.run(order=12) def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + CommonTests.test_server_status_indicator_says_starting(self, 'share') @pytest.mark.run(order=13) - def test_add_delete_buttons_now_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 test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) @pytest.mark.run(order=14) def test_settings_button_is_hidden(self): - '''Test that the settings button is hidden when the server starts''' - self.assertFalse(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_hidden(self) @pytest.mark.run(order=15) def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.share_mode.server_status.status, 2) + CommonTests.test_a_server_is_started(self, 'share') @pytest.mark.run(order=16) def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + CommonTests.test_a_web_server_is_running(self) - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) - - # Running in local mode, so we have no .onion - #@pytest.mark.run(order=17) - #def test_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') - # self.assertEqual(len(self.gui.app.onion_host), 62) + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', True) @pytest.mark.run(order=18) - def test_have_no_slug(self): - '''Test that we have a valid slug''' - self.assertIsNone(self.gui.share_mode.server_status.web.slug) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') @pytest.mark.run(order=19) - def test_url_description_shown(self): - '''Test that the URL label is showing''' - self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') @pytest.mark.run(order=20) - def test_have_copy_url_button(self): - '''Test that the Copy URL button is shown''' - self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') @pytest.mark.run(order=21) - def test_server_status_indicator_says_sharing(self): - '''Test that the Server Status indicator shows we are Sharing''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + def test_web_page(self): + CommonTests.test_web_page(self, 'share', 'Total size', True) @pytest.mark.run(order=22) - def test_web_page(self): - '''Test that the web page contains the term Total size''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET / HTTP/1.0\r\n' - http_request += 'Host: 127.0.0.1\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('Total size' in f.read()) - f.close() + def test_download_share(self): + CommonTests.test_download_share(self, True) @pytest.mark.run(order=23) - def test_download_share(self): - '''Test that we can download the share''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET /download HTTP/1.0\r\n' - http_request += 'Host: 127.0.0.1\r\n' - http_request += '\r\n' - s.sendall(http_request.encode('utf-8')) - - with open('/tmp/download.zip', 'wb') as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - - zip = zipfile.ZipFile('/tmp/download.zip') - self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'share') @pytest.mark.run(order=24) - def test_downloads_widget_present(self): - QtTest.QTest.qWait(1000) - '''Test that the No Downloads Yet label is hidden, that Clear History is present''' - self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) @pytest.mark.run(order=25) - def test_server_is_stopped(self): - '''Test that the server stopped automatically when we downloaded the share''' - self.assertEquals(self.gui.share_mode.server_status.status, 0) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) @pytest.mark.run(order=26) - def test_web_service_is_stopped(self): - '''Test that the web server also stopped''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', False) @pytest.mark.run(order=27) - def test_server_status_indicator_says_closed(self): - '''Test that the Server Status indicator shows we closed because download occurred''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) - - @pytest.mark.run(order=28) - def test_add_button_visible_again(self): - '''Test that the add button should be visible again''' - self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) if __name__ == "__main__": diff --git a/unit_tests/onionshare_share_mode_download_test_stay_open.py b/unit_tests/onionshare_share_mode_download_test_stay_open.py index 82a0ac87..c3177a38 100644 --- a/unit_tests/onionshare_share_mode_download_test_stay_open.py +++ b/unit_tests/onionshare_share_mode_download_test_stay_open.py @@ -2,19 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -51,7 +50,7 @@ class OnionShareGuiTest(unittest.TestCase): "hidservauth_string": "", "no_bridges": True, "private_key": "", - "public_mode": False, + "public_mode": True, "receive_allow_receiver_shutdown": True, "save_private_key": False, "shutdown_timeout": False, @@ -80,176 +79,124 @@ class OnionShareGuiTest(unittest.TestCase): os.remove('/tmp/test.txt') @pytest.mark.run(order=1) - def test_gui_loaded_and_tor_bootstrapped(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) def test_windowTitle_seen(self): - '''Test that the window title is OnionShare''' - self.assertEqual(self.gui.windowTitle(), 'OnionShare') + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) def test_settings_button_is_visible(self): - '''Test that the settings button is visible''' - self.assertTrue(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) def test_server_status_bar_is_visible(self): - '''Test that the status bar is visible''' - self.assertTrue(self.gui.status_bar.isVisible()) + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) def test_file_selection_widget_has_a_file(self): - '''Test that the number of files in the list is 1''' - self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) def test_info_widget_is_visible(self): - '''Test that the info widget along top of screen is shown because we have a file''' - self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + CommonTests.test_info_widget_is_visible(self, 'share') @pytest.mark.run(order=7) - def test_downloads_section_is_visible(self): - '''Test that the Downloads section is visible and that the No Downloads Yet label is present''' - self.assertTrue(self.gui.share_mode.downloads.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') @pytest.mark.run(order=8) def test_deleting_only_file_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, and since there's no more files, the delete button should be hidden - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) - self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + CommonTests.test_deleting_only_file_hides_delete_button(self) @pytest.mark.run(order=9) def test_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.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) @pytest.mark.run(order=10) def test_file_selection_widget_readd_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.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + CommonTests.test_file_selection_widget_readd_files(self) @pytest.mark.run(order=11) def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.share_mode.server_status.status, 1) + CommonTests.test_server_working_on_start_button_pressed(self, 'share') @pytest.mark.run(order=12) def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + CommonTests.test_server_status_indicator_says_starting(self, 'share') @pytest.mark.run(order=13) - def test_add_delete_buttons_now_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 test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) @pytest.mark.run(order=14) def test_settings_button_is_hidden(self): - '''Test that the settings button is hidden when the server starts''' - self.assertFalse(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_hidden(self) @pytest.mark.run(order=15) def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.share_mode.server_status.status, 2) + CommonTests.test_a_server_is_started(self, 'share') @pytest.mark.run(order=16) def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + CommonTests.test_a_web_server_is_running(self) - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) - - # Running in local mode, so we have no .onion - #@pytest.mark.run(order=17) - #def test_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') - # self.assertEqual(len(self.gui.app.onion_host), 62) + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', True) @pytest.mark.run(order=18) - def test_have_a_slug(self): - '''Test that we have a valid slug''' - self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') @pytest.mark.run(order=19) - def test_url_description_shown(self): - '''Test that the URL label is showing''' - self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') @pytest.mark.run(order=20) - def test_have_copy_url_button(self): - '''Test that the Copy URL button is shown''' - self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') @pytest.mark.run(order=21) - def test_server_status_indicator_says_sharing(self): - '''Test that the Server Status indicator shows we are Sharing''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + def test_web_page(self): + CommonTests.test_web_page(self, 'share', 'Total size', True) @pytest.mark.run(order=22) def test_download_share(self): - '''Test that we can download the share''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET {}/download HTTP/1.0\r\n'.format(self.gui.share_mode.server_status.web.slug) - http_request += 'Host: 127.0.0.1\r\n' - http_request += '\r\n' - s.sendall(http_request.encode('utf-8')) - - with open('/tmp/download.zip', 'wb') as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - - zip = zipfile.ZipFile('/tmp/download.zip') - self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + CommonTests.test_download_share(self, True) @pytest.mark.run(order=23) - def test_downloads_widget_present(self): - QtTest.QTest.qWait(1000) - '''Test that the No Downloads Yet label is hidden, that Clear History is present''' - self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'share') @pytest.mark.run(order=24) - def test_server_is_not_stopped(self): - '''Test that the server stayed open after we downloaded the share''' - self.assertEquals(self.gui.share_mode.server_status.status, 2) + def test_counter_incremented(self): + CommonTests.test_counter_incremented(self, 'share', 1) @pytest.mark.run(order=25) - def test_web_service_is_running(self): - '''Test that the web server is still running''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.assertEquals(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + def test_download_share_again(self): + CommonTests.test_download_share(self, True) @pytest.mark.run(order=26) - def test_download_count_incremented(self): - '''Test that the Download Count has incremented''' - self.assertEquals(self.gui.share_mode.downloads_completed, 1) + def test_counter_incremented_again(self): + CommonTests.test_counter_incremented(self, 'share', 2) + + @pytest.mark.run(order=27) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', True) + + @pytest.mark.run(order=28) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=29) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', True) + + @pytest.mark.run(order=30) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) if __name__ == "__main__": diff --git a/unit_tests/onionshare_slug_persistent_test.py b/unit_tests/onionshare_slug_persistent_test.py index d0dcd08a..05c9719a 100644 --- a/unit_tests/onionshare_slug_persistent_test.py +++ b/unit_tests/onionshare_slug_persistent_test.py @@ -2,19 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -83,79 +82,86 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=1) def test_gui_loaded(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) - def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.share_mode.server_status.status, 1) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) - def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) - def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.share_mode.server_status.status, 2) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) - def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') @pytest.mark.run(order=6) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=8) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=9) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=10) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=11) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=12) def test_have_a_slug(self): - '''Test that we have a valid slug''' - self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + CommonTests.test_have_a_slug(self, 'share', False) global slug slug = self.gui.share_mode.server_status.web.slug - @pytest.mark.run(order=7) - def test_server_can_be_stopped(self): - '''Test we can stop the service''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + @pytest.mark.run(order=13) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') - # Should be in SERVER_STOPPED state - self.assertEqual(self.gui.share_mode.server_status.status, 0) + @pytest.mark.run(order=14) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', True) - @pytest.mark.run(order=8) + @pytest.mark.run(order=15) def test_web_service_is_stopped(self): - '''Test that the web server also stopped''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - QtTest.QTest.qWait(4000) + CommonTests.test_web_service_is_stopped(self) - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + @pytest.mark.run(order=16) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', True) - @pytest.mark.run(order=9) + @pytest.mark.run(order=17) def test_server_started_again(self): - '''Test we can start the service again''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.share_mode.server_status.status, 2) + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + CommonTests.test_server_status_indicator_says_starting(self, 'share') + CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=10) + @pytest.mark.run(order=18) def test_have_same_slug(self): '''Test that we have the same slug''' self.assertEqual(self.gui.share_mode.server_status.web.slug, slug) - @pytest.mark.run(order=11) + @pytest.mark.run(order=19) def test_server_is_stopped_again(self): - '''Test that we can stop the server''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - QtTest.QTest.qWait(1000) - self.assertEqual(self.gui.share_mode.server_status.status, 0) + CommonTests.test_server_is_stopped(self, 'share', True) + CommonTests.test_web_service_is_stopped(self) + if __name__ == "__main__": unittest.main() diff --git a/unit_tests/onionshare_timer_test.py b/unit_tests/onionshare_timer_test.py index ed20c1c0..34c77f12 100644 --- a/unit_tests/onionshare_timer_test.py +++ b/unit_tests/onionshare_timer_test.py @@ -2,19 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -81,62 +80,63 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=1) def test_gui_loaded(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) - def test_set_timeout(self): - '''Test that the timeout can be set''' - timer = QtCore.QDateTime.currentDateTime().addSecs(120) - self.gui.share_mode.server_status.shutdown_timeout.setDateTime(timer) - self.assertTrue(self.gui.share_mode.server_status.shutdown_timeout.dateTime(), timer) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) - def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.share_mode.server_status.status, 1) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) - def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) - def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.share_mode.server_status.status, 2) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) - def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') @pytest.mark.run(order=7) - def test_timeout_widget_hidden(self): - '''Test that the timeout widget is hidden when share has started''' - self.assertFalse(self.gui.share_mode.server_status.shutdown_timeout_container.isVisible()) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') @pytest.mark.run(order=8) - def test_server_timed_out(self): - '''Test that the server has timed out after the timer ran out''' - QtTest.QTest.qWait(100000) - # We should have timed out now - self.assertEqual(self.gui.share_mode.server_status.status, 0) + def test_set_timeout(self): + CommonTests.test_set_timeout(self, 'share') @pytest.mark.run(order=9) - def test_web_service_is_stopped(self): - '''Test that the web server also stopped''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + @pytest.mark.run(order=10) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=11) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=12) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=13) + def test_timeout_widget_hidden(self): + CommonTests.test_timeout_widget_hidden(self, 'share') + + @pytest.mark.run(order=14) + def test_timeout(self): + CommonTests.test_server_timed_out(self, 'share', 120000) + + @pytest.mark.run(order=15) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) if __name__ == "__main__": unittest.main() From d34364530da881ccc2629d7e05f2a28cc57585f2 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 25 Sep 2018 14:40:10 +1000 Subject: [PATCH 060/123] Analyse the right file size to determine if the download has finished in the UI (in order to decide whether to stop server yet) --- onionshare/web/share_mode.py | 16 ++++++++-------- onionshare_gui/share_mode/__init__.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index d4d6aed7..a57d0a39 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -71,9 +71,9 @@ class ShareModeWeb(object): # If download is allowed to continue, serve download page if self.should_use_gzip(): - filesize = self.gzip_filesize + self.filesize = self.gzip_filesize else: - filesize = self.download_filesize + self.filesize = self.download_filesize if self.web.slug: r = make_response(render_template( @@ -81,7 +81,7 @@ class ShareModeWeb(object): slug=self.web.slug, file_info=self.file_info, filename=os.path.basename(self.download_filename), - filesize=filesize, + filesize=self.filesize, filesize_human=self.common.human_readable_filesize(self.download_filesize), is_zipped=self.is_zipped)) else: @@ -90,7 +90,7 @@ class ShareModeWeb(object): 'send.html', file_info=self.file_info, filename=os.path.basename(self.download_filename), - filesize=filesize, + filesize=self.filesize, filesize_human=self.common.human_readable_filesize(self.download_filesize), is_zipped=self.is_zipped)) return self.web.add_security_headers(r) @@ -132,10 +132,10 @@ class ShareModeWeb(object): use_gzip = self.should_use_gzip() if use_gzip: file_to_download = self.gzip_filename - filesize = self.gzip_filesize + self.filesize = self.gzip_filesize else: file_to_download = self.download_filename - filesize = self.download_filesize + self.filesize = self.download_filesize # Tell GUI the download started self.web.add_request(self.web.REQUEST_STARTED, path, { @@ -175,7 +175,7 @@ class ShareModeWeb(object): # tell GUI the progress downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / filesize) * 100 + percent = (1.0 * downloaded_bytes / self.filesize) * 100 # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) if not self.web.is_gui or self.common.platform == 'Linux' or self.common.platform == 'BSD': @@ -221,7 +221,7 @@ class ShareModeWeb(object): r = Response(generate()) if use_gzip: r.headers.set('Content-Encoding', 'gzip') - r.headers.set('Content-Length', filesize) + r.headers.set('Content-Length', self.filesize) r.headers.set('Content-Disposition', 'attachment', filename=basename) r = self.web.add_security_headers(r) # guess content type diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index ac6a1373..90fce49a 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -246,7 +246,7 @@ class ShareMode(Mode): self.downloads.update(event["data"]["id"], event["data"]["bytes"]) # Is the download complete? - if event["data"]["bytes"] == self.web.share_mode.download_filesize: + if event["data"]["bytes"] == self.web.share_mode.filesize: self.system_tray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True)) # Update the total 'completed downloads' info From 3ed04bf5ec9e6ed13d0d1c676e7da62cfd75f8d5 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 25 Sep 2018 15:26:19 +1000 Subject: [PATCH 061/123] Show whether Tor version supports next-gen onion support --- onionshare/onion.py | 2 ++ onionshare_gui/settings_dialog.py | 2 +- share/locale/en.json | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 81b82923..c45ae72e 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -402,6 +402,8 @@ class Onion(object): # ephemeral stealth onion services are not supported self.supports_stealth = False + # Does this version of Tor support next-gen ('v3') onions? + self.supports_next_gen_onions = self.tor_version > Version('0.3.3.1') def is_authenticated(self): """ diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index c31d4630..3cd25d31 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -746,7 +746,7 @@ class SettingsDialog(QtWidgets.QDialog): onion.connect(custom_settings=settings, config=self.config, tor_status_update_func=tor_status_update_func) # If an exception hasn't been raised yet, the Tor settings work - Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth)) + Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth, onion.supports_next_gen_onions)) # Clean up onion.cleanup() diff --git a/share/locale/en.json b/share/locale/en.json index 608fbfbc..0f0f0cf4 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -117,7 +117,7 @@ "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_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports client authentication: {}.", + "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", "error_invalid_private_key": "This private key type is unsupported", From 201f351279ee58166458717415045cfe62c2ac69 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 27 Sep 2018 15:43:59 +1000 Subject: [PATCH 062/123] Pass --local-only down to the ServerStatus and Mode so that we can set shorter timeouts for local GUI tests. Update the tests to use a very short timeout --- onionshare_gui/mode.py | 7 +++++-- onionshare_gui/onionshare_gui.py | 4 ++-- onionshare_gui/server_status.py | 26 ++++++++++++++++++-------- unit_tests/commontests.py | 4 ++-- unit_tests/onionshare_timer_test.py | 4 ++-- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index 4c21de76..6b156f7e 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -37,7 +37,7 @@ class Mode(QtWidgets.QWidget): starting_server_error = QtCore.pyqtSignal(str) set_server_active = QtCore.pyqtSignal(bool) - def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None): + def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None, local_only=False): super(Mode, self).__init__() self.common = common self.qtapp = qtapp @@ -54,12 +54,15 @@ class Mode(QtWidgets.QWidget): # 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 # Server status - self.server_status = ServerStatus(self.common, self.qtapp, self.app) + self.server_status = ServerStatus(self.common, self.qtapp, self.app, None, self.local_only) self.server_status.server_started.connect(self.start_server) self.server_status.server_stopped.connect(self.stop_server) self.server_status.server_canceled.connect(self.cancel_server) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 8b61a18e..83f3a7e0 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -121,7 +121,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setStatusBar(self.status_bar) # Share mode - self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames) + self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames, self.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) @@ -135,7 +135,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.share_mode.set_server_active.connect(self.set_server_active) # Receive mode - self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray) + self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, None, self.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) diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 9afceb38..32135ca4 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -44,7 +44,7 @@ class ServerStatus(QtWidgets.QWidget): STATUS_WORKING = 1 STATUS_STARTED = 2 - def __init__(self, common, qtapp, app, file_selection=None): + def __init__(self, common, qtapp, app, file_selection=None, local_only=False): super(ServerStatus, self).__init__() self.common = common @@ -56,17 +56,23 @@ class ServerStatus(QtWidgets.QWidget): self.app = app self.web = None + self.local_only = local_only self.resizeEvent(None) # Shutdown timeout layout self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True)) self.shutdown_timeout = QtWidgets.QDateTimeEdit() - # Set proposed timeout to be 5 minutes into the future self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy") - self.shutdown_timeout.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 2 min from now - self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) + if self.local_only: + # For testing + self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15)) + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime()) + else: + # Set proposed timeout to be 5 minutes into the future + self.shutdown_timeout.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.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection) shutdown_timeout_layout = QtWidgets.QHBoxLayout() shutdown_timeout_layout.addWidget(self.shutdown_timeout_label) @@ -154,7 +160,8 @@ class ServerStatus(QtWidgets.QWidget): Reset the timeout in the UI after stopping a share """ self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) - self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) + if not self.local_only: + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) def update(self): """ @@ -255,8 +262,11 @@ class ServerStatus(QtWidgets.QWidget): """ if self.status == self.STATUS_STOPPED: if self.common.settings.get('shutdown_timeout'): - # Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen - self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) + if self.local_only: + self.timeout = self.shutdown_timeout.dateTime().toPyDateTime() + else: + # Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen + self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) # If the timeout has actually passed already before the user hit Start, refuse to start the server. if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout: Alert(self.common, strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning)) diff --git a/unit_tests/commontests.py b/unit_tests/commontests.py index 1f6f5896..de1ad9ab 100644 --- a/unit_tests/commontests.py +++ b/unit_tests/commontests.py @@ -203,9 +203,9 @@ class CommonTests(object): self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) # Auto-stop timer tests - def test_set_timeout(self, mode): + def test_set_timeout(self, mode, timeout): '''Test that the timeout can be set''' - timer = QtCore.QDateTime.currentDateTime().addSecs(120) + timer = QtCore.QDateTime.currentDateTime().addSecs(timeout) if mode == 'receive': self.gui.receive_mode.server_status.shutdown_timeout.setDateTime(timer) self.assertTrue(self.gui.receive_mode.server_status.shutdown_timeout.dateTime(), timer) diff --git a/unit_tests/onionshare_timer_test.py b/unit_tests/onionshare_timer_test.py index 34c77f12..f36331b8 100644 --- a/unit_tests/onionshare_timer_test.py +++ b/unit_tests/onionshare_timer_test.py @@ -108,7 +108,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=8) def test_set_timeout(self): - CommonTests.test_set_timeout(self, 'share') + CommonTests.test_set_timeout(self, 'share', 5) @pytest.mark.run(order=9) def test_server_working_on_start_button_pressed(self): @@ -132,7 +132,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=14) def test_timeout(self): - CommonTests.test_server_timed_out(self, 'share', 120000) + CommonTests.test_server_timed_out(self, 'share', 10000) @pytest.mark.run(order=15) def test_web_service_is_stopped(self): From 8fc8e0765c73530136b9cd203bc059be09bd8475 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 27 Sep 2018 15:54:46 +1000 Subject: [PATCH 063/123] Rename test dir to tests. Rename unit_tests to tests_gui_local. Add test dependencies. Update various paths. Add GUI unit tests docs to BUILD.md --- .travis.yml | 2 +- BUILD.md | 17 +++++++++++++++-- MANIFEST.in | 2 +- install/check_lacked_trans.py | 2 +- install/requirements-tests.txt | 11 +++++++++++ {test => tests}/__init__.py | 0 {test => tests}/conftest.py | 0 {test => tests}/test_helpers.py | 0 {test => tests}/test_onionshare.py | 0 {test => tests}/test_onionshare_common.py | 0 {test => tests}/test_onionshare_settings.py | 0 {test => tests}/test_onionshare_strings.py | 0 {test => tests}/test_onionshare_web.py | 0 {unit_tests => tests_gui_local}/__init__.py | 0 {unit_tests => tests_gui_local}/commontests.py | 0 {unit_tests => tests_gui_local}/conftest.py | 0 .../onionshare_receive_mode_upload_test.py | 0 ...hare_receive_mode_upload_test_public_mode.py | 0 .../onionshare_share_mode_download_test.py | 0 ...hare_share_mode_download_test_public_mode.py | 0 ...nshare_share_mode_download_test_stay_open.py | 0 .../onionshare_slug_persistent_test.py | 0 .../onionshare_timer_test.py | 0 .../run_unit_tests.sh | 0 24 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 install/requirements-tests.txt rename {test => tests}/__init__.py (100%) rename {test => tests}/conftest.py (100%) rename {test => tests}/test_helpers.py (100%) rename {test => tests}/test_onionshare.py (100%) rename {test => tests}/test_onionshare_common.py (100%) rename {test => tests}/test_onionshare_settings.py (100%) rename {test => tests}/test_onionshare_strings.py (100%) rename {test => tests}/test_onionshare_web.py (100%) rename {unit_tests => tests_gui_local}/__init__.py (100%) rename {unit_tests => tests_gui_local}/commontests.py (100%) rename {unit_tests => tests_gui_local}/conftest.py (100%) rename {unit_tests => tests_gui_local}/onionshare_receive_mode_upload_test.py (100%) rename {unit_tests => tests_gui_local}/onionshare_receive_mode_upload_test_public_mode.py (100%) rename {unit_tests => tests_gui_local}/onionshare_share_mode_download_test.py (100%) rename {unit_tests => tests_gui_local}/onionshare_share_mode_download_test_public_mode.py (100%) rename {unit_tests => tests_gui_local}/onionshare_share_mode_download_test_stay_open.py (100%) rename {unit_tests => tests_gui_local}/onionshare_slug_persistent_test.py (100%) rename {unit_tests => tests_gui_local}/onionshare_timer_test.py (100%) rename {unit_tests => tests_gui_local}/run_unit_tests.sh (100%) diff --git a/.travis.yml b/.travis.yml index afbaa887..a41339cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,6 @@ before_script: # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests -script: pytest --cov=onionshare test/ +script: pytest --cov=onionshare tests/ after_success: - coveralls diff --git a/BUILD.md b/BUILD.md index 1feedf49..308a186c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -137,8 +137,21 @@ This will prompt you to codesign three binaries and execute one unsigned binary. ## Tests -OnionShare includes PyTest unit tests. To run the tests: +OnionShare includes PyTest unit tests. To run the tests, first install some dependencies: ```sh -pytest test/ +pip3 install -r install/requirements-tests.txt +``` + +If you'd like to run the CLI-based tests that Travis runs: + +```sh +pytest tests/ +``` + +If you would like to run the GUI unit tests in 'local only mode': + +```sh +cd tests_gui_local/ +./run_unit_tests.sh ``` diff --git a/MANIFEST.in b/MANIFEST.in index c8a4d87c..71af3740 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,4 +10,4 @@ include install/onionshare.desktop include install/onionshare.appdata.xml include install/onionshare80.xpm include install/scripts/onionshare-nautilus.py -include test/*.py +include tests/*.py diff --git a/install/check_lacked_trans.py b/install/check_lacked_trans.py index 027edab1..1caa6b27 100644 --- a/install/check_lacked_trans.py +++ b/install/check_lacked_trans.py @@ -59,7 +59,7 @@ def main(): files_in(dir, 'onionshare_gui/share_mode') + \ files_in(dir, 'onionshare_gui/receive_mode') + \ files_in(dir, 'install/scripts') + \ - files_in(dir, 'test') + files_in(dir, 'tests') pysrc = [p for p in src if p.endswith('.py')] lang_code = args.lang_code diff --git a/install/requirements-tests.txt b/install/requirements-tests.txt new file mode 100644 index 00000000..0d9c1581 --- /dev/null +++ b/install/requirements-tests.txt @@ -0,0 +1,11 @@ +atomicwrites==1.2.1 +attrs==18.2.0 +more-itertools==4.3.0 +pluggy==0.6.0 +py==1.6.0 +pytest==3.4.2 +pytest-faulthandler==1.5.0 +pytest-ordering==0.5 +pytest-qt==3.1.0 +six==1.11.0 +urllib3==1.23 diff --git a/test/__init__.py b/tests/__init__.py similarity index 100% rename from test/__init__.py rename to tests/__init__.py diff --git a/test/conftest.py b/tests/conftest.py similarity index 100% rename from test/conftest.py rename to tests/conftest.py diff --git a/test/test_helpers.py b/tests/test_helpers.py similarity index 100% rename from test/test_helpers.py rename to tests/test_helpers.py diff --git a/test/test_onionshare.py b/tests/test_onionshare.py similarity index 100% rename from test/test_onionshare.py rename to tests/test_onionshare.py diff --git a/test/test_onionshare_common.py b/tests/test_onionshare_common.py similarity index 100% rename from test/test_onionshare_common.py rename to tests/test_onionshare_common.py diff --git a/test/test_onionshare_settings.py b/tests/test_onionshare_settings.py similarity index 100% rename from test/test_onionshare_settings.py rename to tests/test_onionshare_settings.py diff --git a/test/test_onionshare_strings.py b/tests/test_onionshare_strings.py similarity index 100% rename from test/test_onionshare_strings.py rename to tests/test_onionshare_strings.py diff --git a/test/test_onionshare_web.py b/tests/test_onionshare_web.py similarity index 100% rename from test/test_onionshare_web.py rename to tests/test_onionshare_web.py diff --git a/unit_tests/__init__.py b/tests_gui_local/__init__.py similarity index 100% rename from unit_tests/__init__.py rename to tests_gui_local/__init__.py diff --git a/unit_tests/commontests.py b/tests_gui_local/commontests.py similarity index 100% rename from unit_tests/commontests.py rename to tests_gui_local/commontests.py diff --git a/unit_tests/conftest.py b/tests_gui_local/conftest.py similarity index 100% rename from unit_tests/conftest.py rename to tests_gui_local/conftest.py diff --git a/unit_tests/onionshare_receive_mode_upload_test.py b/tests_gui_local/onionshare_receive_mode_upload_test.py similarity index 100% rename from unit_tests/onionshare_receive_mode_upload_test.py rename to tests_gui_local/onionshare_receive_mode_upload_test.py diff --git a/unit_tests/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py similarity index 100% rename from unit_tests/onionshare_receive_mode_upload_test_public_mode.py rename to tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py diff --git a/unit_tests/onionshare_share_mode_download_test.py b/tests_gui_local/onionshare_share_mode_download_test.py similarity index 100% rename from unit_tests/onionshare_share_mode_download_test.py rename to tests_gui_local/onionshare_share_mode_download_test.py diff --git a/unit_tests/onionshare_share_mode_download_test_public_mode.py b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py similarity index 100% rename from unit_tests/onionshare_share_mode_download_test_public_mode.py rename to tests_gui_local/onionshare_share_mode_download_test_public_mode.py diff --git a/unit_tests/onionshare_share_mode_download_test_stay_open.py b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py similarity index 100% rename from unit_tests/onionshare_share_mode_download_test_stay_open.py rename to tests_gui_local/onionshare_share_mode_download_test_stay_open.py diff --git a/unit_tests/onionshare_slug_persistent_test.py b/tests_gui_local/onionshare_slug_persistent_test.py similarity index 100% rename from unit_tests/onionshare_slug_persistent_test.py rename to tests_gui_local/onionshare_slug_persistent_test.py diff --git a/unit_tests/onionshare_timer_test.py b/tests_gui_local/onionshare_timer_test.py similarity index 100% rename from unit_tests/onionshare_timer_test.py rename to tests_gui_local/onionshare_timer_test.py diff --git a/unit_tests/run_unit_tests.sh b/tests_gui_local/run_unit_tests.sh similarity index 100% rename from unit_tests/run_unit_tests.sh rename to tests_gui_local/run_unit_tests.sh From 8212da2d3df36c17162225f83be12a9db09cbccb Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 27 Sep 2018 17:33:15 +1000 Subject: [PATCH 064/123] Add Tor GUI unit tests --- BUILD.md | 9 + tests_gui_tor/__init__.py | 0 tests_gui_tor/commontests.py | 359 ++++++++++++++++++ tests_gui_tor/conftest.py | 160 ++++++++ .../onionshare_receive_mode_upload_test.py | 188 +++++++++ ...re_receive_mode_upload_test_public_mode.py | 188 +++++++++ ...onionshare_share_mode_cancel_share_test.py | 159 ++++++++ .../onionshare_share_mode_download_test.py | 195 ++++++++++ ...re_share_mode_download_test_public_mode.py | 195 ++++++++++ ...hare_share_mode_download_test_stay_open.py | 207 ++++++++++ .../onionshare_share_mode_persistent_test.py | 179 +++++++++ .../onionshare_share_mode_stealth_test.py | 174 +++++++++ ...e_share_mode_tor_connection_killed_test.py | 179 +++++++++ tests_gui_tor/onionshare_timer_test.py | 142 +++++++ .../onionshare_tor_connection_killed_test.py | 179 +++++++++ tests_gui_tor/run_unit_tests.sh | 5 + 16 files changed, 2518 insertions(+) create mode 100644 tests_gui_tor/__init__.py create mode 100644 tests_gui_tor/commontests.py create mode 100644 tests_gui_tor/conftest.py create mode 100644 tests_gui_tor/onionshare_receive_mode_upload_test.py create mode 100644 tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py create mode 100644 tests_gui_tor/onionshare_share_mode_cancel_share_test.py create mode 100644 tests_gui_tor/onionshare_share_mode_download_test.py create mode 100644 tests_gui_tor/onionshare_share_mode_download_test_public_mode.py create mode 100644 tests_gui_tor/onionshare_share_mode_download_test_stay_open.py create mode 100644 tests_gui_tor/onionshare_share_mode_persistent_test.py create mode 100644 tests_gui_tor/onionshare_share_mode_stealth_test.py create mode 100644 tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py create mode 100644 tests_gui_tor/onionshare_timer_test.py create mode 100644 tests_gui_tor/onionshare_tor_connection_killed_test.py create mode 100755 tests_gui_tor/run_unit_tests.sh diff --git a/BUILD.md b/BUILD.md index 308a186c..00d24cd2 100644 --- a/BUILD.md +++ b/BUILD.md @@ -155,3 +155,12 @@ If you would like to run the GUI unit tests in 'local only mode': cd tests_gui_local/ ./run_unit_tests.sh ``` + +If you would like to run the GUI unit tests in 'tor' (bundled) mode: + +```sh +cd tests_gui_tor/ +./run_unit_tests.sh +``` + +Keep in mind that the Tor tests take a lot longer to run than local mode, but they are also more comprehensive. diff --git a/tests_gui_tor/__init__.py b/tests_gui_tor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests_gui_tor/commontests.py b/tests_gui_tor/commontests.py new file mode 100644 index 00000000..a0d9bf5f --- /dev/null +++ b/tests_gui_tor/commontests.py @@ -0,0 +1,359 @@ +import os +import requests +import socket +import socks +import zipfile + +from PyQt5 import QtCore, QtTest +from onionshare import strings + +class CommonTests(object): + def test_gui_loaded(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + def test_info_widget_is_not_visible(self, mode): + '''Test that the info widget along top of screen is not shown''' + if mode == 'receive': + self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) + if mode == 'share': + self.assertFalse(self.gui.share_mode.info_widget.isVisible()) + + def test_info_widget_is_visible(self, mode): + '''Test that the info widget along top of screen is shown''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.info_widget.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + + def test_click_mode(self, mode): + '''Test that we can switch Mode by clicking the button''' + if mode == 'receive': + QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) + if mode == 'share': + QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_SHARE) + + def test_history_is_visible(self, mode): + '''Test that the History section is visible and that the relevant widget is present''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.uploads.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.downloads.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + + def test_server_working_on_start_button_pressed(self, mode): + '''Test we can start the service''' + # Should be in SERVER_WORKING state + if mode == 'receive': + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.receive_mode.server_status.status, 1) + if mode == 'share': + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + def test_server_status_indicator_says_starting(self, mode): + '''Test that the Server Status indicator shows we are Starting''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + if mode == 'share': + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + def test_a_server_is_started(self, mode): + '''Test that the server has started''' + QtTest.QTest.qWait(45000) + # Should now be in SERVER_STARTED state + if mode == 'receive': + self.assertEqual(self.gui.receive_mode.server_status.status, 2) + if mode == 'share': + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + def test_have_a_slug(self, mode, public_mode): + '''Test that we have a valid slug''' + if mode == 'receive': + if not public_mode: + self.assertRegex(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') + else: + self.assertIsNone(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') + if mode == 'share': + if not public_mode: + self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + else: + self.assertIsNone(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + + def test_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 test_url_description_shown(self, mode): + '''Test that the URL label is showing''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + + def test_have_copy_url_button(self, mode): + '''Test that the Copy URL button is shown''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + + def test_server_status_indicator_says_started(self, mode): + '''Test that the Server Status indicator shows we are started''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + if mode == 'share': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + + def test_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: + if mode == 'receive': + path = '/{}'.format(self.gui.receive_mode.server_status.web.slug) + if mode == 'share': + path = '/{}'.format(self.gui.share_mode.server_status.web.slug) + else: + path = '/' + + http_request = 'GET {} HTTP/1.0\r\n'.format(path) + http_request += 'Host: {}\r\n'.format(self.gui.app.onion_host) + 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 test_history_widgets_present(self, mode): + '''Test that the relevant widgets are present in the history view after activity has taken place''' + if mode == 'receive': + self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) + if mode == 'share': + self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + + def test_counter_incremented(self, mode, count): + '''Test that the counter has incremented''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.uploads_completed, count) + if mode == 'share': + self.assertEquals(self.gui.share_mode.downloads_completed, count) + + def test_server_is_stopped(self, mode, stay_open): + '''Test that the server stops when we click Stop''' + if mode == 'receive': + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.receive_mode.server_status.status, 0) + if mode == 'share': + if stay_open: + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.share_mode.server_status.status, 0) + + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + QtTest.QTest.qWait(2000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + def test_server_status_indicator_says_closed(self, mode, stay_open): + '''Test that the Server Status indicator shows we closed''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + if mode == 'share': + if stay_open: + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped', True)) + else: + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) + + def test_cancel_the_share(self, mode): + '''Test that we can cancel this share before it's started up ''' + if mode == 'share': + QtTest.QTest.mousePress(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + QtTest.QTest.qWait(1000) + QtTest.QTest.mouseRelease(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.share_mode.server_status.status, 0) + + if mode == 'receive': + QtTest.QTest.mousePress(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + QtTest.QTest.qWait(1000) + QtTest.QTest.mouseRelease(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.receive_mode.server_status.status, 0) + + + # Auto-stop timer tests + def test_set_timeout(self, mode, timeout): + '''Test that the timeout can be set''' + timer = QtCore.QDateTime.currentDateTime().addSecs(timeout) + if mode == 'receive': + self.gui.receive_mode.server_status.shutdown_timeout.setDateTime(timer) + self.assertTrue(self.gui.receive_mode.server_status.shutdown_timeout.dateTime(), timer) + if mode == 'share': + self.gui.share_mode.server_status.shutdown_timeout.setDateTime(timer) + self.assertTrue(self.gui.share_mode.server_status.shutdown_timeout.dateTime(), timer) + + def test_timeout_widget_hidden(self, mode): + '''Test that the timeout widget is hidden when share has started''' + if mode == 'receive': + self.assertFalse(self.gui.receive_mode.server_status.shutdown_timeout_container.isVisible()) + if mode == 'share': + self.assertFalse(self.gui.share_mode.server_status.shutdown_timeout_container.isVisible()) + + def test_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 + if mode == 'receive': + self.assertEqual(self.gui.receive_mode.server_status.status, 0) + if mode == 'share': + self.assertEqual(self.gui.share_mode.server_status.status, 0) + + # Receive-specific tests + def test_upload_file(self, public_mode, 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'] = 'socks5h://{}:{}'.format(socks_address, socks_port) + + files = {'file[]': open('/tmp/test.txt', 'rb')} + if not public_mode: + path = 'http://{}/{}/upload'.format(self.gui.app.onion_host, self.gui.receive_mode.web.slug) + else: + path = 'http://{}/upload'.format(self.gui.app.onion_host) + response = session.post(path, files=files) + QtTest.QTest.qWait(4000) + self.assertTrue(os.path.isfile(expected_file)) + + # Share-specific tests + def test_file_selection_widget_has_a_file(self): + '''Test that the number of files in the list is 1''' + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + + def test_deleting_only_file_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, and since there's no more files, the delete button should be hidden + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + def test_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.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + + def test_file_selection_widget_readd_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.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + + def test_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 test_download_share(self, public_mode): + '''Test that we can download the share''' + (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 public_mode: + path = '/download' + else: + path = '{}/download'.format(self.gui.share_mode.web.slug) + + http_request = 'GET {} HTTP/1.0\r\n'.format(path) + http_request += 'Host: {}\r\n'.format(self.gui.app.onion_host) + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/download.zip', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + zip = zipfile.ZipFile('/tmp/download.zip') + QtTest.QTest.qWait(4000) + self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + + def test_add_button_visible(self): + '''Test that the add button should be visible''' + self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + + + # Stealth tests + def test_copy_have_hidserv_auth_button(self, mode): + '''Test that the Copy HidservAuth button is shown''' + if mode == 'share': + self.assertTrue(self.gui.share_mode.server_status.copy_hidservauth_button.isVisible()) + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.server_status.copy_hidservauth_button.isVisible()) + + def test_hidserv_auth_string(self): + '''Test the validity of the HidservAuth string''' + self.assertRegex(self.gui.app.auth_string, r'HidServAuth %s [a-zA-Z1-9]' % self.gui.app.onion_host) + + + # Miscellaneous tests + def test_tor_killed_statusbar_message_shown(self, mode): + '''Test that the status bar message shows Tor was disconnected''' + self.gui.app.onion.cleanup(stop_tor=True) + QtTest.QTest.qWait(2500) + if mode == 'share': + self.assertTrue(self.gui.share_mode.status_bar.currentMessage(), strings._('gui_tor_connection_lost', True)) + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.status_bar.currentMessage(), strings._('gui_tor_connection_lost', True)) diff --git a/tests_gui_tor/conftest.py b/tests_gui_tor/conftest.py new file mode 100644 index 00000000..8ac7efb8 --- /dev/null +++ b/tests_gui_tor/conftest.py @@ -0,0 +1,160 @@ +import sys +# Force tests to look for resources in the source code tree +sys.onionshare_dev_mode = True + +import os +import shutil +import tempfile + +import pytest + +from onionshare import common, web, settings + +@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' + return settings.Settings(_common) diff --git a/tests_gui_tor/onionshare_receive_mode_upload_test.py b/tests_gui_tor/onionshare_receive_mode_upload_test.py new file mode 100644 index 00000000..5c2945f1 --- /dev/null +++ b/tests_gui_tor/onionshare_receive_mode_upload_test.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + os.remove('/tmp/OnionShare/test.txt') + os.remove('/tmp/OnionShare/test-2.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_info_widget_is_not_visible(self): + CommonTests.test_info_widget_is_not_visible(self, 'receive') + + @pytest.mark.run(order=6) + def test_click_mode(self): + CommonTests.test_click_mode(self, 'receive') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'receive') + + @pytest.mark.run(order=8) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'receive') + + @pytest.mark.run(order=9) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'receive') + + @pytest.mark.run(order=10) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=11) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'receive') + + @pytest.mark.run(order=12) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=14) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'receive', False) + + @pytest.mark.run(order=15) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=16) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'receive') + + @pytest.mark.run(order=17) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'receive') + + @pytest.mark.run(order=18) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'receive') + + @pytest.mark.run(order=19) + def test_web_page(self): + CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', False) + + @pytest.mark.run(order=20) + def test_upload_file(self): + CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test.txt') + + @pytest.mark.run(order=21) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'receive') + + @pytest.mark.run(order=22) + def test_counter_incremented(self): + CommonTests.test_counter_incremented(self, 'receive', 1) + + @pytest.mark.run(order=23) + def test_upload_same_file_is_renamed(self): + CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test-2.txt') + + @pytest.mark.run(order=24) + def test_upload_count_incremented_again(self): + CommonTests.test_counter_incremented(self, 'receive', 2) + + @pytest.mark.run(order=25) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'receive', False) + + @pytest.mark.run(order=26) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=27) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py new file mode 100644 index 00000000..86cde0d9 --- /dev/null +++ b/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": True, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + os.remove('/tmp/OnionShare/test.txt') + os.remove('/tmp/OnionShare/test-2.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_info_widget_is_not_visible(self): + CommonTests.test_info_widget_is_not_visible(self, 'receive') + + @pytest.mark.run(order=6) + def test_click_mode(self): + CommonTests.test_click_mode(self, 'receive') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'receive') + + @pytest.mark.run(order=8) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'receive') + + @pytest.mark.run(order=9) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'receive') + + @pytest.mark.run(order=10) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=11) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'receive') + + @pytest.mark.run(order=12) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=14) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'receive', True) + + @pytest.mark.run(order=15) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=16) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'receive') + + @pytest.mark.run(order=17) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'receive') + + @pytest.mark.run(order=18) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'receive') + + @pytest.mark.run(order=19) + def test_web_page(self): + CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', True) + + @pytest.mark.run(order=20) + def test_upload_file(self): + CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test.txt') + + @pytest.mark.run(order=21) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'receive') + + @pytest.mark.run(order=22) + def test_counter_incremented(self): + CommonTests.test_counter_incremented(self, 'receive', 1) + + @pytest.mark.run(order=23) + def test_upload_same_file_is_renamed(self): + CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test-2.txt') + + @pytest.mark.run(order=24) + def test_upload_count_incremented_again(self): + CommonTests.test_counter_incremented(self, 'receive', 2) + + @pytest.mark.run(order=25) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'receive', False) + + @pytest.mark.run(order=26) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=27) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_cancel_share_test.py b/tests_gui_tor/onionshare_share_mode_cancel_share_test.py new file mode 100644 index 00000000..a2d1a06a --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_cancel_share_test.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_cancel_the_share(self): + CommonTests.test_cancel_the_share(self, 'share') + + @pytest.mark.run(order=18) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) + + @pytest.mark.run(order=19) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=20) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_download_test.py b/tests_gui_tor/onionshare_share_mode_download_test.py new file mode 100644 index 00000000..d1eb5b54 --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_download_test.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', False) + + @pytest.mark.run(order=18) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=22) + def test_web_page(self): + CommonTests.test_web_page(self, 'share', 'Total size', False) + + @pytest.mark.run(order=23) + def test_download_share(self): + CommonTests.test_download_share(self, False) + + @pytest.mark.run(order=24) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'share') + + @pytest.mark.run(order=25) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) + + @pytest.mark.run(order=26) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=27) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', False) + + @pytest.mark.run(order=28) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py b/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py new file mode 100644 index 00000000..4e5f1114 --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": True, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', True) + + @pytest.mark.run(order=18) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=22) + def test_web_page(self): + CommonTests.test_web_page(self, 'share', 'Total size', True) + + @pytest.mark.run(order=23) + def test_download_share(self): + CommonTests.test_download_share(self, True) + + @pytest.mark.run(order=24) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'share') + + @pytest.mark.run(order=25) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) + + @pytest.mark.run(order=26) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=27) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', False) + + @pytest.mark.run(order=28) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py b/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py new file mode 100644 index 00000000..78cd1578 --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": False, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": True, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', True) + + @pytest.mark.run(order=18) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=22) + def test_web_page(self): + CommonTests.test_web_page(self, 'share', 'Total size', True) + + @pytest.mark.run(order=23) + def test_download_share(self): + CommonTests.test_download_share(self, True) + + @pytest.mark.run(order=24) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'share') + + @pytest.mark.run(order=25) + def test_counter_incremented(self): + CommonTests.test_counter_incremented(self, 'share', 1) + + @pytest.mark.run(order=26) + def test_download_share_again(self): + CommonTests.test_download_share(self, True) + + @pytest.mark.run(order=27) + def test_counter_incremented_again(self): + CommonTests.test_counter_incremented(self, 'share', 2) + + @pytest.mark.run(order=28) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', True) + + @pytest.mark.run(order=29) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=30) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', True) + + @pytest.mark.run(order=31) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_persistent_test.py b/tests_gui_tor/onionshare_share_mode_persistent_test.py new file mode 100644 index 00000000..a2d429b2 --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_persistent_test.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + slug = '' + onion_host = '' + + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": True, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=6) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=8) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=9) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=10) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=11) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=12) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', False) + global slug + slug = self.gui.share_mode.server_status.web.slug + + @pytest.mark.run(order=13) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + global onion_host + onion_host = self.gui.app.onion_host + + @pytest.mark.run(order=14) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=15) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', True) + + @pytest.mark.run(order=16) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=17) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', True) + + @pytest.mark.run(order=18) + def test_server_started_again(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + CommonTests.test_server_status_indicator_says_starting(self, 'share') + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=19) + def test_have_same_slug(self): + '''Test that we have the same slug''' + self.assertEqual(self.gui.share_mode.server_status.web.slug, slug) + + @pytest.mark.run(order=20) + def test_have_same_onion(self): + '''Test that we have the same onion''' + self.assertEqual(self.gui.app.onion_host, onion_host) + + @pytest.mark.run(order=21) + def test_server_is_stopped_again(self): + CommonTests.test_server_is_stopped(self, 'share', True) + CommonTests.test_web_service_is_stopped(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_stealth_test.py b/tests_gui_tor/onionshare_share_mode_stealth_test.py new file mode 100644 index 00000000..948e834a --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_stealth_test.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": True, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', False) + + @pytest.mark.run(order=18) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=22) + def test_copy_have_hidserv_auth_button(self): + CommonTests.test_copy_have_hidserv_auth_button(self, 'share') + + @pytest.mark.run(order=23) + def test_hidserv_auth_string(self): + CommonTests.test_hidserv_auth_string(self) + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py b/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py new file mode 100644 index 00000000..3eeea9bc --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', False) + + @pytest.mark.run(order=18) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=22) + def test_tor_killed_statusbar_message_shown(self): + CommonTests.test_tor_killed_statusbar_message_shown(self, 'share') + + @pytest.mark.run(order=23) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) + + @pytest.mark.run(order=24) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_timer_test.py b/tests_gui_tor/onionshare_timer_test.py new file mode 100644 index 00000000..865b3a8b --- /dev/null +++ b/tests_gui_tor/onionshare_timer_test.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": True, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_set_timeout(self): + CommonTests.test_set_timeout(self, 'share', 120) + + @pytest.mark.run(order=9) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=10) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=11) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=12) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=13) + def test_timeout_widget_hidden(self): + CommonTests.test_timeout_widget_hidden(self, 'share') + + @pytest.mark.run(order=14) + def test_timeout(self): + CommonTests.test_server_timed_out(self, 'share', 125000) + + @pytest.mark.run(order=15) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_tor_connection_killed_test.py b/tests_gui_tor/onionshare_tor_connection_killed_test.py new file mode 100644 index 00000000..3eeea9bc --- /dev/null +++ b/tests_gui_tor/onionshare_tor_connection_killed_test.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', False) + + @pytest.mark.run(order=18) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=22) + def test_tor_killed_statusbar_message_shown(self): + CommonTests.test_tor_killed_statusbar_message_shown(self, 'share') + + @pytest.mark.run(order=23) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) + + @pytest.mark.run(order=24) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/run_unit_tests.sh b/tests_gui_tor/run_unit_tests.sh new file mode 100755 index 00000000..d15f8a6e --- /dev/null +++ b/tests_gui_tor/run_unit_tests.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +for test in `ls -1 | egrep ^onionshare_`; do + py.test-3 $test -vvv || exit 1 +done From f0dd76f681cce41a40e8c821c2b401e07a15f09b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 27 Sep 2018 17:34:46 -0700 Subject: [PATCH 065/123] Remove all the extra QApplications --- tests_gui_local/onionshare_receive_mode_upload_test.py | 6 ++---- .../onionshare_receive_mode_upload_test_public_mode.py | 6 ++---- tests_gui_local/onionshare_share_mode_download_test.py | 4 +--- .../onionshare_share_mode_download_test_public_mode.py | 4 +--- .../onionshare_share_mode_download_test_stay_open.py | 4 +--- tests_gui_local/onionshare_slug_persistent_test.py | 4 +--- tests_gui_local/onionshare_timer_test.py | 4 +--- tests_gui_tor/onionshare_receive_mode_upload_test.py | 6 ++---- .../onionshare_receive_mode_upload_test_public_mode.py | 6 ++---- tests_gui_tor/onionshare_share_mode_cancel_share_test.py | 4 +--- tests_gui_tor/onionshare_share_mode_download_test.py | 4 +--- .../onionshare_share_mode_download_test_public_mode.py | 4 +--- .../onionshare_share_mode_download_test_stay_open.py | 4 +--- tests_gui_tor/onionshare_share_mode_persistent_test.py | 4 +--- tests_gui_tor/onionshare_share_mode_stealth_test.py | 4 +--- .../onionshare_share_mode_tor_connection_killed_test.py | 4 +--- tests_gui_tor/onionshare_timer_test.py | 4 +--- tests_gui_tor/onionshare_tor_connection_killed_test.py | 4 +--- 18 files changed, 22 insertions(+), 58 deletions(-) diff --git a/tests_gui_local/onionshare_receive_mode_upload_test.py b/tests_gui_local/onionshare_receive_mode_upload_test.py index bac622fb..2aa2ed94 100644 --- a/tests_gui_local/onionshare_receive_mode_upload_test.py +++ b/tests_gui_local/onionshare_receive_mode_upload_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, @@ -72,7 +70,7 @@ class OnionShareGuiTest(unittest.TestCase): open(testsettings, 'w').write(json.dumps(test_settings)) cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) - + @classmethod def tearDownClass(cls): '''Clean up after tests''' diff --git a/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py index 8ed385f5..30a290e7 100644 --- a/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py +++ b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": True, "receive_allow_receiver_shutdown": True, @@ -72,7 +70,7 @@ class OnionShareGuiTest(unittest.TestCase): open(testsettings, 'w').write(json.dumps(test_settings)) cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) - + @classmethod def tearDownClass(cls): '''Clean up after tests''' diff --git a/tests_gui_local/onionshare_share_mode_download_test.py b/tests_gui_local/onionshare_share_mode_download_test.py index 0ef03e97..c546fb61 100644 --- a/tests_gui_local/onionshare_share_mode_download_test.py +++ b/tests_gui_local/onionshare_share_mode_download_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_local/onionshare_share_mode_download_test_public_mode.py b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py index 417eb8b9..764b5885 100644 --- a/tests_gui_local/onionshare_share_mode_download_test_public_mode.py +++ b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": True, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_local/onionshare_share_mode_download_test_stay_open.py b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py index c3177a38..b92ff097 100644 --- a/tests_gui_local/onionshare_share_mode_download_test_stay_open.py +++ b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": True, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_local/onionshare_slug_persistent_test.py b/tests_gui_local/onionshare_slug_persistent_test.py index 05c9719a..1e5614dc 100644 --- a/tests_gui_local/onionshare_slug_persistent_test.py +++ b/tests_gui_local/onionshare_slug_persistent_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' slug = '' @@ -50,7 +48,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_local/onionshare_timer_test.py b/tests_gui_local/onionshare_timer_test.py index f36331b8..1a5134e2 100644 --- a/tests_gui_local/onionshare_timer_test.py +++ b/tests_gui_local/onionshare_timer_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_receive_mode_upload_test.py b/tests_gui_tor/onionshare_receive_mode_upload_test.py index 5c2945f1..5be400e2 100644 --- a/tests_gui_tor/onionshare_receive_mode_upload_test.py +++ b/tests_gui_tor/onionshare_receive_mode_upload_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, @@ -72,7 +70,7 @@ class OnionShareGuiTest(unittest.TestCase): open(testsettings, 'w').write(json.dumps(test_settings)) cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) - + @classmethod def tearDownClass(cls): '''Clean up after tests''' diff --git a/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py index 86cde0d9..9c9553a4 100644 --- a/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py +++ b/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": True, "receive_allow_receiver_shutdown": True, @@ -72,7 +70,7 @@ class OnionShareGuiTest(unittest.TestCase): open(testsettings, 'w').write(json.dumps(test_settings)) cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) - + @classmethod def tearDownClass(cls): '''Clean up after tests''' diff --git a/tests_gui_tor/onionshare_share_mode_cancel_share_test.py b/tests_gui_tor/onionshare_share_mode_cancel_share_test.py index a2d1a06a..466109d7 100644 --- a/tests_gui_tor/onionshare_share_mode_cancel_share_test.py +++ b/tests_gui_tor/onionshare_share_mode_cancel_share_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_share_mode_download_test.py b/tests_gui_tor/onionshare_share_mode_download_test.py index d1eb5b54..1c8e1b6c 100644 --- a/tests_gui_tor/onionshare_share_mode_download_test.py +++ b/tests_gui_tor/onionshare_share_mode_download_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py b/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py index 4e5f1114..c292e729 100644 --- a/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py +++ b/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": True, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py b/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py index 78cd1578..7838316f 100644 --- a/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py +++ b/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": True, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_share_mode_persistent_test.py b/tests_gui_tor/onionshare_share_mode_persistent_test.py index a2d429b2..3cffaab6 100644 --- a/tests_gui_tor/onionshare_share_mode_persistent_test.py +++ b/tests_gui_tor/onionshare_share_mode_persistent_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' slug = '' @@ -51,7 +49,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_share_mode_stealth_test.py b/tests_gui_tor/onionshare_share_mode_stealth_test.py index 948e834a..aaf6fbc6 100644 --- a/tests_gui_tor/onionshare_share_mode_stealth_test.py +++ b/tests_gui_tor/onionshare_share_mode_stealth_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py b/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py index 3eeea9bc..861b7ccc 100644 --- a/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py +++ b/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_timer_test.py b/tests_gui_tor/onionshare_timer_test.py index 865b3a8b..b76106d9 100644 --- a/tests_gui_tor/onionshare_timer_test.py +++ b/tests_gui_tor/onionshare_timer_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_tor_connection_killed_test.py b/tests_gui_tor/onionshare_tor_connection_killed_test.py index 3eeea9bc..861b7ccc 100644 --- a/tests_gui_tor/onionshare_tor_connection_killed_test.py +++ b/tests_gui_tor/onionshare_tor_connection_killed_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, From 8261b4868d5c05ad8f52c9f0efb4f038c3062879 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 27 Sep 2018 17:51:16 -0700 Subject: [PATCH 066/123] Add @mig5 as a code owner for all tests, and add @emmapeel2 as a code owner for locales --- .github/CODEOWNERS | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1cd5a1f6..42d1840f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,9 @@ * @micahflee + +# localization +/share/locale/ @emmapeel2 + +# tests +/tests/ @mig5 +/tests_gui_local/ @mig5 +/tests_gui_tor/ @mig5 From 6f57f7eae640b08e5be3a76da1663f97a4d15cdd Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 27 Sep 2018 21:19:42 -0700 Subject: [PATCH 067/123] Update Travis CI to run GUI tests --- .travis.yml | 12 ++++++++---- tests_gui_local/run_unit_tests.sh | 2 +- tests_gui_tor/run_unit_tests.sh | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index a41339cc..aa1ff102 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python -# sudo: required -dist: bionic +dist: trusty +sudo: required python: - "3.6" - "3.6-dev" @@ -8,14 +8,18 @@ python: - "nightly" # command to install dependencies install: + - sudo apt-get update && sudo apt-get install python3-pyqt5 - pip install -r install/requirements.txt + - pip install -r install/requirements-tests.txt - pip install pytest-cov coveralls flake8 before_script: # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics -# command to run tests -script: pytest --cov=onionshare tests/ +# run CLI tests and local GUI tests +script: + - pytest --cov=onionshare tests/ + - cd tests_gui_local/ && xvfb-run ./run_unit_tests.sh after_success: - coveralls diff --git a/tests_gui_local/run_unit_tests.sh b/tests_gui_local/run_unit_tests.sh index d15f8a6e..7d207a57 100755 --- a/tests_gui_local/run_unit_tests.sh +++ b/tests_gui_local/run_unit_tests.sh @@ -1,5 +1,5 @@ #!/bin/bash for test in `ls -1 | egrep ^onionshare_`; do - py.test-3 $test -vvv || exit 1 + pytest $test -vvv || exit 1 done diff --git a/tests_gui_tor/run_unit_tests.sh b/tests_gui_tor/run_unit_tests.sh index d15f8a6e..7d207a57 100755 --- a/tests_gui_tor/run_unit_tests.sh +++ b/tests_gui_tor/run_unit_tests.sh @@ -1,5 +1,5 @@ #!/bin/bash for test in `ls -1 | egrep ^onionshare_`; do - py.test-3 $test -vvv || exit 1 + pytest $test -vvv || exit 1 done From 75153db11e81972621c920788c3ad432e7b4a152 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 27 Sep 2018 21:22:10 -0700 Subject: [PATCH 068/123] Keep trying ports until it finds a free one --- onionshare/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare/common.py b/onionshare/common.py index 0ce411e8..28b282c2 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -433,7 +433,7 @@ class Common(object): tmpsock.bind(("127.0.0.1", random.randint(min_port, max_port))) break except OSError as e: - raise OSError(e) + pass _, port = tmpsock.getsockname() return port From aa7919abfd59629b985ed0ec60754ba6b709467c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 27 Sep 2018 21:27:29 -0700 Subject: [PATCH 069/123] Remove submitting to coveralls --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index aa1ff102..e0b5b822 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ install: - sudo apt-get update && sudo apt-get install python3-pyqt5 - pip install -r install/requirements.txt - pip install -r install/requirements-tests.txt - - pip install pytest-cov coveralls flake8 + - pip install pytest-cov flake8 before_script: # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics @@ -21,5 +21,3 @@ before_script: script: - pytest --cov=onionshare tests/ - cd tests_gui_local/ && xvfb-run ./run_unit_tests.sh -after_success: - - coveralls From 2ffcdbb1083dece7664792f7bef9dbf2245e549e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 27 Sep 2018 21:34:39 -0700 Subject: [PATCH 070/123] One script to run all tests --- dev_scripts/run_all_tests.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100755 dev_scripts/run_all_tests.sh diff --git a/dev_scripts/run_all_tests.sh b/dev_scripts/run_all_tests.sh new file mode 100755 index 00000000..90ef1dc0 --- /dev/null +++ b/dev_scripts/run_all_tests.sh @@ -0,0 +1,14 @@ +#!/bin/bash +ROOT="$( dirname $(cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd ))" + +# CLI tests +cd $ROOT +pytest tests/ + +# Local GUI tests +cd $ROOT/tests_gui_local +./run_unit_tests.sh + +# Tor GUI tests +cd $ROOT/tests_gui_tor +./run_unit_tests.sh From fc1902c1ee768e6573324d1ad0cfb8d086c432cd Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 12:51:30 -0700 Subject: [PATCH 071/123] Refactor how Mode layouts work, so the downstream Mode has more control over the UI --- onionshare_gui/mode.py | 19 ++++++------------- onionshare_gui/receive_mode/__init__.py | 16 ++++++++++++---- onionshare_gui/share_mode/__init__.py | 14 +++++++++++--- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index 6b156f7e..d91b2e64 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -72,24 +72,17 @@ class Mode(QtWidgets.QWidget): self.starting_server_step3.connect(self.start_server_step3) self.starting_server_error.connect(self.start_server_error) - # Primary action layout + # 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.server_status) self.primary_action = QtWidgets.QWidget() self.primary_action.setLayout(self.primary_action_layout) - # Layout - self.layout = QtWidgets.QVBoxLayout() - self.layout.addWidget(self.primary_action) - # Hack to allow a minimum width on self.layout - min_width_widget = QtWidgets.QWidget() - min_width_widget.setMinimumWidth(450) - self.layout.addWidget(min_width_widget) - - self.horizontal_layout_wrapper = QtWidgets.QHBoxLayout() - self.horizontal_layout_wrapper.addLayout(self.layout) - - self.setLayout(self.horizontal_layout_wrapper) + # 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(450) def init(self): """ diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index 590dec65..f2a82e54 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -76,11 +76,19 @@ class ReceiveMode(Mode): self.receive_info.setMinimumHeight(80) self.receive_info.setWordWrap(True) + # Main layout + self.main_layout = QtWidgets.QVBoxLayout() + self.main_layout.addWidget(self.info_widget) + self.main_layout.addWidget(self.receive_info) + self.main_layout.addWidget(self.primary_action) + self.main_layout.addStretch() + self.main_layout.addWidget(self.min_width_widget) + # Layout - self.layout.insertWidget(0, self.receive_info) - self.layout.insertWidget(0, self.info_widget) - self.layout.addStretch() - self.horizontal_layout_wrapper.addWidget(self.uploads) + self.layout = QtWidgets.QHBoxLayout() + self.layout.addLayout(self.main_layout) + self.layout.addWidget(self.uploads) + self.setLayout(self.layout) def get_stop_server_shutdown_timeout_text(self): """ diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 90fce49a..8a937f1d 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -106,10 +106,18 @@ class ShareMode(Mode): # Status bar, zip progress bar self._zip_progress_bar = None + # Main layout + self.main_layout = QtWidgets.QVBoxLayout() + self.main_layout.addWidget(self.info_widget) + self.main_layout.addLayout(self.file_selection) + self.main_layout.addWidget(self.primary_action) + self.main_layout.addWidget(self.min_width_widget) + # Layout - self.layout.insertLayout(0, self.file_selection) - self.layout.insertWidget(0, self.info_widget) - self.horizontal_layout_wrapper.addWidget(self.downloads) + self.layout = QtWidgets.QHBoxLayout() + self.layout.addLayout(self.main_layout) + self.layout.addWidget(self.downloads) + self.setLayout(self.layout) # Always start with focus on file selection self.file_selection.setFocus() From ddcbed451c2697e8b39ad7baf47c1518dd32b5d5 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 13:18:18 -0700 Subject: [PATCH 072/123] Hide the uploads and downloads by default, and make the mode switcher hide before showing, to prevent weird window resizing --- onionshare_gui/onionshare_gui.py | 3 +-- onionshare_gui/receive_mode/__init__.py | 1 + onionshare_gui/share_mode/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 83f3a7e0..79565edd 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -55,7 +55,6 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setWindowTitle('OnionShare') self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) - self.setMinimumWidth(850) # Load settings self.config = config @@ -194,8 +193,8 @@ class OnionShareGui(QtWidgets.QMainWindow): self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style']) self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) - self.share_mode.show() self.receive_mode.hide() + self.share_mode.show() else: self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style']) diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index f2a82e54..80bd9cad 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -48,6 +48,7 @@ class ReceiveMode(Mode): # Uploads self.uploads = Uploads(self.common) + self.uploads.hide() self.uploads_in_progress = 0 self.uploads_completed = 0 self.new_upload = False # For scrolling to the bottom of the uploads list diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 8a937f1d..c097d75a 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -72,6 +72,7 @@ class ShareMode(Mode): # Downloads self.downloads = Downloads(self.common) + self.downloads.hide() self.downloads_in_progress = 0 self.downloads_completed = 0 @@ -96,7 +97,6 @@ class ShareMode(Mode): self.info_widget = QtWidgets.QWidget() self.info_widget.setLayout(self.info_layout) - self.info_widget.hide() # Primary action layout self.primary_action_layout.addWidget(self.filesize_warning) From edbbe9377cac09d1c6063e23a09f68d925fa6acd Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 13:24:44 -0700 Subject: [PATCH 073/123] Add a toggle downloads button to share mode, and add new toggle upload and download images --- onionshare_gui/share_mode/__init__.py | 15 +++++++++++++++ share/images/downloads_toggle.png | Bin 0 -> 380 bytes share/images/downloads_toggle_selected.png | Bin 0 -> 468 bytes share/images/uploads_toggle.png | Bin 0 -> 389 bytes share/images/uploads_toggle_selected.png | Bin 0 -> 473 bytes 5 files changed, 15 insertions(+) create mode 100644 share/images/downloads_toggle.png create mode 100644 share/images/downloads_toggle_selected.png create mode 100644 share/images/uploads_toggle.png create mode 100644 share/images/uploads_toggle_selected.png diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index c097d75a..7e444137 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -89,11 +89,20 @@ class ShareMode(Mode): self.update_downloads_completed() self.update_downloads_in_progress() + self.info_toggle_button = QtWidgets.QPushButton() + self.info_toggle_button.setDefault(False) + self.info_toggle_button.setFixedWidth(30) + self.info_toggle_button.setFixedHeight(30) + self.info_toggle_button.setFlat(True) + self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')) ) + self.info_toggle_button.clicked.connect(self.toggle_downloads) + self.info_layout = QtWidgets.QHBoxLayout() self.info_layout.addWidget(self.info_label) self.info_layout.addStretch() self.info_layout.addWidget(self.info_in_progress_downloads_count) self.info_layout.addWidget(self.info_completed_downloads_count) + self.info_layout.addWidget(self.info_toggle_button) self.info_widget = QtWidgets.QWidget() self.info_widget.setLayout(self.info_layout) @@ -353,6 +362,12 @@ class ShareMode(Mode): self.info_in_progress_downloads_count.setText(' {1:d}'.format(image, self.downloads_in_progress)) self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(self.downloads_in_progress)) + def toggle_downloads(self): + """ + Toggle showing and hiding the Downloads widget + """ + pass + @staticmethod def _compute_total_size(filenames): total_size = 0 diff --git a/share/images/downloads_toggle.png b/share/images/downloads_toggle.png new file mode 100644 index 0000000000000000000000000000000000000000..846ececb367e96250c0f48218be55aba4a76b1a1 GIT binary patch literal 380 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPdr;G@g6&%$AH35qVFC(D|_}@I;7oC^gF*}4f}GxzNVDIkBl-}8P}UM9tkP>Ccm={ zp7egs;qUqZ4NE323GCb)BE)Q3@L*2#$9w(7d-4wc`8)r!%rtJc`3L+>IMv zCQrGYDRDJ;$Ghi8irFUlHEvh=cfC1N%VcwV3D@<+$NQq*T@Je(pV4a3{Mm=I_Xlga XzWd9M4j1nN!->Ju)z4*}Q$iB}!flz# literal 0 HcmV?d00001 diff --git a/share/images/downloads_toggle_selected.png b/share/images/downloads_toggle_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..127ce20806c715eaecbfce44f6a5907f5c769abc GIT binary patch literal 468 zcmV;_0W1EAP)!ax{?-(K5mfWe8FsFPvnLZ}hLPatt~Xq(QCCj1c=V)QQnmVwRxU?{|B zNE{548X^G(Bcyj}&p{ywrSx6y%Xi=V*D08|`Z`>#Ma%Yg7 zl4HzXXI;Sr0RV|gVLF#SZ9Fs{6DQFrImUc2_W&SuoGM0{{{d{nzLsQZuzD#1arl?S z_jO~l+pGbAvS|2k{uKy95Qv0%s>Me+p_VVuj4O6IuqBWH0000< KMNUMnLSTX_O~}&# literal 0 HcmV?d00001 diff --git a/share/images/uploads_toggle.png b/share/images/uploads_toggle.png new file mode 100644 index 0000000000000000000000000000000000000000..87303c9ffb4b08be932a0f38109ab8559b8ed668 GIT binary patch literal 389 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPdr;G@%@Twi_92giFg*;sxLo81BPEz!CaTGXS|K7&p zf=X6^-m1WsgGrlyFdj+TZ5Fxl#M0G=I@Jzu`OUzdcG=Z&Tf`Pg(+YV9maRRT_H6%s z@Ave(bAO)CFHhgDpd1>@E?2uVJuP1|J}~$X%bKbe%r`t8#e|L?KcKsT^G`(ur-i`8 zN{)w?+yPgEA28iuh-*xJAYH-u>%hK_5@!y6(S}uVjP3`-3)s&zIQz$2ZQy^?b)t;p zq1*yt*#rh1CShZ1{rI!V!ryuRJu*z-;yL?9?Yq%D>0R5IBxc^6!nQL#gz?EeOXi>7 z%d%_sW;Dm0yRwpTP3p6^OIIa_%3iPRI#I*4XZj~?yK};|Gp1G-@Yeit&sw~8ncSA3 gnx@T1M5VrR%L@9=OnY-s8W>;zopr0Bru3NdN!< literal 0 HcmV?d00001 diff --git a/share/images/uploads_toggle_selected.png b/share/images/uploads_toggle_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..0ba52cffb17cc54bd2870e13b946544be6326818 GIT binary patch literal 473 zcmV;~0Ve*5P)u`GL1Cu-RzAm`=b`fGgw0I|c^3t{Aa^|rbNfcRP^Fp1dWicnVScd6-ffTEC z$K5)1pgZo?S&G$s0%3a&f}2Z2Sq`MY|E@dO?%w9vp#Q%h=jDN%mj_WC;<~^V5y;fz z;s^kUKt8;Mol&O=0I^NLm>GAVa6rNLG}w$D;-1(#kdCE$Zwrz5{C$1^GdYWv3kMYo P00000NkvXXu0mjf=2^q% literal 0 HcmV?d00001 From 4bec79f4944e1222d2f79c7a11668a80da497872 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 13:43:10 -0700 Subject: [PATCH 074/123] Toggle showing uploads and downloads for both share and receive modes --- onionshare_gui/mode.py | 1 + onionshare_gui/onionshare_gui.py | 7 +++++++ onionshare_gui/receive_mode/__init__.py | 28 ++++++++++++++++++++++++- onionshare_gui/share_mode/__init__.py | 19 ++++++++++++++--- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index d91b2e64..6da83318 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -36,6 +36,7 @@ class Mode(QtWidgets.QWidget): starting_server_step3 = QtCore.pyqtSignal() starting_server_error = QtCore.pyqtSignal(str) set_server_active = QtCore.pyqtSignal(bool) + adjust_size = QtCore.pyqtSignal() def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None, local_only=False): super(Mode, self).__init__() diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 79565edd..a95b75c9 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -132,6 +132,7 @@ class OnionShareGui(QtWidgets.QMainWindow): 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.share_mode.adjust_size.connect(self.adjust_size) # Receive mode self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, None, self.local_only) @@ -146,6 +147,7 @@ class OnionShareGui(QtWidgets.QMainWindow): 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) + self.receive_mode.adjust_size.connect(self.adjust_size) self.update_mode_switcher() self.update_server_status_indicator() @@ -442,6 +444,11 @@ class OnionShareGui(QtWidgets.QMainWindow): # Disable settings menu action when server is active self.settings_action.setEnabled(not active) + def adjust_size(self): + self.share_mode.adjustSize() + self.receive_mode.adjustSize() + self.adjustSize() + def closeEvent(self, e): self.common.log('OnionShareGui', 'closeEvent') try: diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index 80bd9cad..efad618a 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -63,10 +63,19 @@ class ReceiveMode(Mode): self.update_uploads_completed() self.update_uploads_in_progress() + self.info_toggle_button = QtWidgets.QPushButton() + self.info_toggle_button.setDefault(False) + self.info_toggle_button.setFixedWidth(30) + self.info_toggle_button.setFixedHeight(30) + self.info_toggle_button.setFlat(True) + self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')) ) + self.info_toggle_button.clicked.connect(self.toggle_uploads) + self.info_layout = QtWidgets.QHBoxLayout() self.info_layout.addStretch() self.info_layout.addWidget(self.info_in_progress_uploads_count) self.info_layout.addWidget(self.info_completed_uploads_count) + self.info_layout.addWidget(self.info_toggle_button) self.info_widget = QtWidgets.QWidget() self.info_widget.setLayout(self.info_layout) @@ -226,4 +235,21 @@ class ReceiveMode(Mode): self.info_widget.hide() # Resize window - self.adjustSize() + self.adjust_size.emit() + + def toggle_uploads(self): + """ + Toggle showing and hiding the Uploads widget + """ + self.common.log('ReceiveMode', 'toggle_uploads') + + if self.uploads.isVisible(): + self.uploads.hide() + self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')) ) + self.info_toggle_button.setFlat(True) + else: + self.uploads.show() + self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle_selected.png')) ) + self.info_toggle_button.setFlat(False) + + self.adjust_size.emit() diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 7e444137..cf33e8b1 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -226,7 +226,7 @@ class ShareMode(Mode): Stop the compression thread on cancel """ if self.compress_thread: - self.common.log('OnionShareGui', 'cancel_server: quitting compress thread') + self.common.log('ShareMode', 'cancel_server: quitting compress thread') self.compress_thread.quit() def handle_tor_broke_custom(self): @@ -305,6 +305,8 @@ class ShareMode(Mode): self.info_widget.show() def update_primary_action(self): + self.common.log('ShareMode', 'update_primary_action') + # Show or hide primary action layout file_count = self.file_selection.file_list.count() if file_count > 0: @@ -328,7 +330,7 @@ class ShareMode(Mode): self.info_widget.hide() # Resize window - self.adjustSize() + self.adjust_size.emit() def reset_info_counters(self): """ @@ -366,7 +368,18 @@ class ShareMode(Mode): """ Toggle showing and hiding the Downloads widget """ - pass + self.common.log('ShareMode', 'toggle_downloads') + + if self.downloads.isVisible(): + self.downloads.hide() + self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')) ) + self.info_toggle_button.setFlat(True) + else: + self.downloads.show() + self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png')) ) + self.info_toggle_button.setFlat(False) + + self.adjust_size.emit() @staticmethod def _compute_total_size(filenames): From e29bb99f16f69be68dbee1667400a3a99451bd56 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 15:05:43 -0700 Subject: [PATCH 075/123] OnionShareGui.adjust_size now recursively runs adjustSize() on all widgets --- onionshare_gui/onionshare_gui.py | 31 +++++++++++++++++++++++-- onionshare_gui/receive_mode/__init__.py | 10 ++++---- onionshare_gui/share_mode/__init__.py | 10 ++++---- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index a95b75c9..5469c57c 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -445,8 +445,35 @@ class OnionShareGui(QtWidgets.QMainWindow): self.settings_action.setEnabled(not active) def adjust_size(self): - self.share_mode.adjustSize() - self.receive_mode.adjustSize() + """ + Recursively adjust size on all widgets + """ + # Recursively adjust sizes for the modes + def adjust_size_layout(layout): + count = layout.count() + for i in range(count): + item = layout.itemAt(i) + if item: + child_widget = item.widget() + if child_widget: + adjust_size_widget(child_widget) + child_layout = item.layout() + if child_layout: + adjust_size_layout(child_layout) + + def adjust_size_widget(widget): + layout = widget.layout() + if layout: + adjust_size_layout(layout) + # Processing Qt events before adjusting size makes each .adjustSize() actually count + self.qtapp.processEvents() + widget.adjustSize() + + # Adjust sizes of each mode + for mode in [self.share_mode, self.receive_mode]: + adjust_size_widget(mode) + + # Adjust window size self.adjustSize() def closeEvent(self, e): diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index efad618a..dab37ef2 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -94,11 +94,11 @@ class ReceiveMode(Mode): self.main_layout.addStretch() self.main_layout.addWidget(self.min_width_widget) - # Layout - self.layout = QtWidgets.QHBoxLayout() - self.layout.addLayout(self.main_layout) - self.layout.addWidget(self.uploads) - self.setLayout(self.layout) + # Wrapper layout + self.wrapper_layout = QtWidgets.QHBoxLayout() + self.wrapper_layout.addLayout(self.main_layout) + self.wrapper_layout.addWidget(self.uploads) + self.setLayout(self.wrapper_layout) def get_stop_server_shutdown_timeout_text(self): """ diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index cf33e8b1..c0cb6d39 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -122,11 +122,11 @@ class ShareMode(Mode): self.main_layout.addWidget(self.primary_action) self.main_layout.addWidget(self.min_width_widget) - # Layout - self.layout = QtWidgets.QHBoxLayout() - self.layout.addLayout(self.main_layout) - self.layout.addWidget(self.downloads) - self.setLayout(self.layout) + # Wrapper layout + self.wrapper_layout = QtWidgets.QHBoxLayout() + self.wrapper_layout.addLayout(self.main_layout) + self.wrapper_layout.addWidget(self.downloads) + self.setLayout(self.wrapper_layout) # Always start with focus on file selection self.file_selection.setFocus() From cddc8c06d4ebfbec16479f2945343b36c6386ed9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 15:30:37 -0700 Subject: [PATCH 076/123] Modes now get to choose a new minimum window width when resizing --- onionshare_gui/mode.py | 21 ++++++++++++++++++++- onionshare_gui/onionshare_gui.py | 14 ++++++++++---- onionshare_gui/receive_mode/__init__.py | 10 ++++++++-- onionshare_gui/share_mode/__init__.py | 10 ++++++++-- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index 6da83318..bd247568 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -36,7 +36,7 @@ class Mode(QtWidgets.QWidget): starting_server_step3 = QtCore.pyqtSignal() starting_server_error = QtCore.pyqtSignal(str) set_server_active = QtCore.pyqtSignal(bool) - adjust_size = QtCore.pyqtSignal() + adjust_size = QtCore.pyqtSignal(int) def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None, local_only=False): super(Mode, self).__init__() @@ -332,3 +332,22 @@ class Mode(QtWidgets.QWidget): Handle REQUEST_UPLOAD_FINISHED event. """ pass + + def resize_window(self): + """ + We call this to force the OnionShare window to resize itself to be smaller. + For this to do anything, the Mode needs to override it and call: + + self.adjust_size.emit(min_width) + + It can calculate min_width (the new minimum window width) based on what + widgets are visible. + """ + pass + + def show(self): + """ + Always resize the window after showing this Mode widget. + """ + super(Mode, self).show() + self.resize_window() diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 5469c57c..e2d95dab 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -168,6 +168,9 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setCentralWidget(central_widget) self.show() + # Adjust window size, to start with a minimum window width + self.adjust_size(450) + # The server isn't active yet self.set_server_active(False) @@ -444,10 +447,14 @@ class OnionShareGui(QtWidgets.QMainWindow): # Disable settings menu action when server is active self.settings_action.setEnabled(not active) - def adjust_size(self): + def adjust_size(self, min_width): """ - Recursively adjust size on all widgets + Recursively adjust size on all widgets. min_width is the new minimum width + of the window. """ + self.common.log("OnionShareGui", "adjust_size", "min_width={}".format(min_width)) + self.setMinimumWidth(min_width) + # Recursively adjust sizes for the modes def adjust_size_layout(layout): count = layout.count() @@ -465,8 +472,6 @@ class OnionShareGui(QtWidgets.QMainWindow): layout = widget.layout() if layout: adjust_size_layout(layout) - # Processing Qt events before adjusting size makes each .adjustSize() actually count - self.qtapp.processEvents() widget.adjustSize() # Adjust sizes of each mode @@ -474,6 +479,7 @@ class OnionShareGui(QtWidgets.QMainWindow): adjust_size_widget(mode) # Adjust window size + self.qtapp.processEvents() self.adjustSize() def closeEvent(self, e): diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index dab37ef2..e30ef519 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -235,7 +235,7 @@ class ReceiveMode(Mode): self.info_widget.hide() # Resize window - self.adjust_size.emit() + self.resize_window() def toggle_uploads(self): """ @@ -252,4 +252,10 @@ class ReceiveMode(Mode): self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle_selected.png')) ) self.info_toggle_button.setFlat(False) - self.adjust_size.emit() + self.resize_window() + + def resize_window(self): + min_width = 450 + if self.uploads.isVisible(): + min_width += 300 + self.adjust_size.emit(min_width) diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index c0cb6d39..cc0a9f32 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -330,7 +330,7 @@ class ShareMode(Mode): self.info_widget.hide() # Resize window - self.adjust_size.emit() + self.resize_window() def reset_info_counters(self): """ @@ -379,7 +379,13 @@ class ShareMode(Mode): self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png')) ) self.info_toggle_button.setFlat(False) - self.adjust_size.emit() + self.resize_window() + + def resize_window(self): + min_width = 450 + if self.downloads.isVisible(): + min_width += 300 + self.adjust_size.emit(min_width) @staticmethod def _compute_total_size(filenames): From f056ce576eee6ff5ea97b32a3642272052733a03 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 15:47:49 -0700 Subject: [PATCH 077/123] Refactor share mode info widget into its own file and custom class, and run .show_more() and .show_less() instead of .show() and .hide() --- onionshare_gui/share_mode/__init__.py | 100 ++++----------------- onionshare_gui/share_mode/info.py | 120 ++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 83 deletions(-) create mode 100644 onionshare_gui/share_mode/info.py diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index cc0a9f32..a8828497 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -28,6 +28,7 @@ from onionshare.web import Web from .file_selection import FileSelection from .downloads import Downloads from .threads import CompressThread +from .info import Info from ..mode import Mode from ..widgets import Alert @@ -77,35 +78,7 @@ class ShareMode(Mode): self.downloads_completed = 0 # Information about share, and show downloads button - self.info_label = QtWidgets.QLabel() - self.info_label.setStyleSheet(self.common.css['mode_info_label']) - - self.info_in_progress_downloads_count = QtWidgets.QLabel() - self.info_in_progress_downloads_count.setStyleSheet(self.common.css['mode_info_label']) - - self.info_completed_downloads_count = QtWidgets.QLabel() - self.info_completed_downloads_count.setStyleSheet(self.common.css['mode_info_label']) - - self.update_downloads_completed() - self.update_downloads_in_progress() - - self.info_toggle_button = QtWidgets.QPushButton() - self.info_toggle_button.setDefault(False) - self.info_toggle_button.setFixedWidth(30) - self.info_toggle_button.setFixedHeight(30) - self.info_toggle_button.setFlat(True) - self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')) ) - self.info_toggle_button.clicked.connect(self.toggle_downloads) - - self.info_layout = QtWidgets.QHBoxLayout() - self.info_layout.addWidget(self.info_label) - self.info_layout.addStretch() - self.info_layout.addWidget(self.info_in_progress_downloads_count) - self.info_layout.addWidget(self.info_completed_downloads_count) - self.info_layout.addWidget(self.info_toggle_button) - - self.info_widget = QtWidgets.QWidget() - self.info_widget.setLayout(self.info_layout) + self.info = Info(self.common, self) # Primary action layout self.primary_action_layout.addWidget(self.filesize_warning) @@ -117,7 +90,7 @@ class ShareMode(Mode): # Main layout self.main_layout = QtWidgets.QVBoxLayout() - self.main_layout.addWidget(self.info_widget) + self.main_layout.addWidget(self.info) self.main_layout.addLayout(self.file_selection) self.main_layout.addWidget(self.primary_action) self.main_layout.addWidget(self.min_width_widget) @@ -218,7 +191,7 @@ class ShareMode(Mode): self.filesize_warning.hide() self.downloads_in_progress = 0 self.downloads_completed = 0 - self.update_downloads_in_progress() + self.info.update_downloads_in_progress() self.file_selection.file_list.adjustSize() def cancel_server_custom(self): @@ -234,7 +207,7 @@ class ShareMode(Mode): Connection to Tor broke. """ self.primary_action.hide() - self.info_widget.hide() + self.info.show_less() def handle_request_load(self, event): """ @@ -252,7 +225,7 @@ class ShareMode(Mode): filesize = self.web.share_mode.download_filesize self.downloads.add(event["data"]["id"], filesize) self.downloads_in_progress += 1 - self.update_downloads_in_progress() + self.info.update_downloads_in_progress() self.system_tray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True)) @@ -268,10 +241,10 @@ class ShareMode(Mode): # Update the total 'completed downloads' info self.downloads_completed += 1 - self.update_downloads_completed() + self.info.update_downloads_completed() # Update the 'in progress downloads' info self.downloads_in_progress -= 1 - self.update_downloads_in_progress() + self.info.update_downloads_in_progress() # Close on finish? if self.common.settings.get('close_after_first_download'): @@ -282,7 +255,7 @@ class ShareMode(Mode): if self.server_status.status == self.server_status.STATUS_STOPPED: self.downloads.cancel(event["data"]["id"]) self.downloads_in_progress = 0 - self.update_downloads_in_progress() + self.info.update_downloads_in_progress() def handle_request_canceled(self, event): """ @@ -292,7 +265,7 @@ class ShareMode(Mode): # Update the 'in progress downloads' info self.downloads_in_progress -= 1 - self.update_downloads_in_progress() + self.info.update_downloads_in_progress() self.system_tray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True)) def on_reload_settings(self): @@ -302,7 +275,7 @@ class ShareMode(Mode): """ if self.server_status.file_selection.get_num_files() > 0: self.primary_action.show() - self.info_widget.show() + self.info.show_more() def update_primary_action(self): self.common.log('ShareMode', 'update_primary_action') @@ -311,7 +284,7 @@ class ShareMode(Mode): file_count = self.file_selection.file_list.count() if file_count > 0: self.primary_action.show() - self.info_widget.show() + self.info.show_more() # Update the file count in the info label total_size_bytes = 0 @@ -321,13 +294,13 @@ class ShareMode(Mode): total_size_readable = self.common.human_readable_filesize(total_size_bytes) if file_count > 1: - self.info_label.setText(strings._('gui_file_info', True).format(file_count, total_size_readable)) + self.info.update_label(strings._('gui_file_info', True).format(file_count, total_size_readable)) else: - self.info_label.setText(strings._('gui_file_info_single', True).format(file_count, total_size_readable)) + self.info.update_label(strings._('gui_file_info_single', True).format(file_count, total_size_readable)) else: self.primary_action.hide() - self.info_widget.hide() + self.info.show_less() # Resize window self.resize_window() @@ -338,49 +311,10 @@ class ShareMode(Mode): """ self.downloads_completed = 0 self.downloads_in_progress = 0 - self.update_downloads_completed() - self.update_downloads_in_progress() + self.info.update_downloads_completed() + self.info.update_downloads_in_progress() self.downloads.reset() - def update_downloads_completed(self): - """ - Update the 'Downloads completed' info widget. - """ - if self.downloads_completed == 0: - image = self.common.get_resource_path('images/share_completed_none.png') - else: - image = self.common.get_resource_path('images/share_completed.png') - self.info_completed_downloads_count.setText(' {1:d}'.format(image, self.downloads_completed)) - self.info_completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(self.downloads_completed)) - - def update_downloads_in_progress(self): - """ - Update the 'Downloads in progress' info widget. - """ - if self.downloads_in_progress == 0: - image = self.common.get_resource_path('images/share_in_progress_none.png') - else: - image = self.common.get_resource_path('images/share_in_progress.png') - self.info_in_progress_downloads_count.setText(' {1:d}'.format(image, self.downloads_in_progress)) - self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(self.downloads_in_progress)) - - def toggle_downloads(self): - """ - Toggle showing and hiding the Downloads widget - """ - self.common.log('ShareMode', 'toggle_downloads') - - if self.downloads.isVisible(): - self.downloads.hide() - self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')) ) - self.info_toggle_button.setFlat(True) - else: - self.downloads.show() - self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png')) ) - self.info_toggle_button.setFlat(False) - - self.resize_window() - def resize_window(self): min_width = 450 if self.downloads.isVisible(): diff --git a/onionshare_gui/share_mode/info.py b/onionshare_gui/share_mode/info.py new file mode 100644 index 00000000..548d70e3 --- /dev/null +++ b/onionshare_gui/share_mode/info.py @@ -0,0 +1,120 @@ +# -*- 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 Info(QtWidgets.QWidget): + """ + Share mode information widget + """ + def __init__(self, common, share_mode): + super(Info, self).__init__() + self.common = common + self.share_mode = share_mode + + # Label + self.label = QtWidgets.QLabel() + self.label.setStyleSheet(self.common.css['mode_info_label']) + + # In prorgess and completed labels + self.in_progress_downloads_count = QtWidgets.QLabel() + self.in_progress_downloads_count.setStyleSheet(self.common.css['mode_info_label']) + self.completed_downloads_count = QtWidgets.QLabel() + self.completed_downloads_count.setStyleSheet(self.common.css['mode_info_label']) + + # Toggle button + self.toggle_button = QtWidgets.QPushButton() + self.toggle_button.setDefault(False) + self.toggle_button.setFixedWidth(30) + self.toggle_button.setFixedHeight(30) + self.toggle_button.setFlat(True) + self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')) ) + self.toggle_button.clicked.connect(self.toggle_downloads) + + # Info layout + layout = QtWidgets.QHBoxLayout() + layout.addWidget(self.label) + layout.addStretch() + layout.addWidget(self.in_progress_downloads_count) + layout.addWidget(self.completed_downloads_count) + layout.addWidget(self.toggle_button) + self.setLayout(layout) + + self.update_downloads_completed() + self.update_downloads_in_progress() + + def update_label(self, s): + """ + Updates the text of the label. + """ + self.label.setText(s) + + def update_downloads_completed(self): + """ + Update the 'Downloads completed' info widget. + """ + if self.share_mode.downloads_completed == 0: + image = self.common.get_resource_path('images/share_completed_none.png') + else: + image = self.common.get_resource_path('images/share_completed.png') + self.completed_downloads_count.setText(' {1:d}'.format(image, self.share_mode.downloads_completed)) + self.completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(self.share_mode.downloads_completed)) + + def update_downloads_in_progress(self): + """ + Update the 'Downloads in progress' info widget. + """ + if self.share_mode.downloads_in_progress == 0: + image = self.common.get_resource_path('images/share_in_progress_none.png') + else: + image = self.common.get_resource_path('images/share_in_progress.png') + self.in_progress_downloads_count.setText(' {1:d}'.format(image, self.share_mode.downloads_in_progress)) + self.in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(self.share_mode.downloads_in_progress)) + + def toggle_downloads(self): + """ + Toggle showing and hiding the Downloads widget + """ + self.common.log('ShareMode', 'toggle_downloads') + + if self.share_mode.downloads.isVisible(): + self.share_mode.downloads.hide() + self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')) ) + self.toggle_button.setFlat(True) + else: + self.share_mode.downloads.show() + self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png')) ) + self.toggle_button.setFlat(False) + + self.share_mode.resize_window() + + def show_less(self): + """ + Remove clutter widgets that aren't necessary. + """ + self.label.hide() + + def show_more(self): + """ + Show all widgets. + """ + self.label.show() From 35065106efa934fac95991d1b593704e47bb12b5 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 16:00:22 -0700 Subject: [PATCH 078/123] Refactor receive mode into using an info widget too --- onionshare_gui/onionshare_gui.py | 1 - onionshare_gui/receive_mode/__init__.py | 88 +++---------------- onionshare_gui/receive_mode/info.py | 109 ++++++++++++++++++++++++ onionshare_gui/share_mode/__init__.py | 4 +- onionshare_gui/share_mode/info.py | 16 ++-- 5 files changed, 133 insertions(+), 85 deletions(-) create mode 100644 onionshare_gui/receive_mode/info.py diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index e2d95dab..7cc368b8 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -452,7 +452,6 @@ class OnionShareGui(QtWidgets.QMainWindow): Recursively adjust size on all widgets. min_width is the new minimum width of the window. """ - self.common.log("OnionShareGui", "adjust_size", "min_width={}".format(min_width)) self.setMinimumWidth(min_width) # Recursively adjust sizes for the modes diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index e30ef519..83113805 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -23,6 +23,7 @@ from onionshare import strings from onionshare.web import Web from .uploads import Uploads +from .info import ReceiveModeInfo from ..mode import Mode class ReceiveMode(Mode): @@ -54,32 +55,8 @@ class ReceiveMode(Mode): self.new_upload = False # For scrolling to the bottom of the uploads list # Information about share, and show uploads button - self.info_in_progress_uploads_count = QtWidgets.QLabel() - self.info_in_progress_uploads_count.setStyleSheet(self.common.css['mode_info_label']) - - self.info_completed_uploads_count = QtWidgets.QLabel() - self.info_completed_uploads_count.setStyleSheet(self.common.css['mode_info_label']) - - self.update_uploads_completed() - self.update_uploads_in_progress() - - self.info_toggle_button = QtWidgets.QPushButton() - self.info_toggle_button.setDefault(False) - self.info_toggle_button.setFixedWidth(30) - self.info_toggle_button.setFixedHeight(30) - self.info_toggle_button.setFlat(True) - self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')) ) - self.info_toggle_button.clicked.connect(self.toggle_uploads) - - self.info_layout = QtWidgets.QHBoxLayout() - self.info_layout.addStretch() - self.info_layout.addWidget(self.info_in_progress_uploads_count) - self.info_layout.addWidget(self.info_completed_uploads_count) - self.info_layout.addWidget(self.info_toggle_button) - - self.info_widget = QtWidgets.QWidget() - self.info_widget.setLayout(self.info_layout) - self.info_widget.hide() + self.info = ReceiveModeInfo(self.common, self) + self.info.show_less() # Receive mode info self.receive_info = QtWidgets.QLabel(strings._('gui_receive_mode_warning', True)) @@ -88,7 +65,7 @@ class ReceiveMode(Mode): # Main layout self.main_layout = QtWidgets.QVBoxLayout() - self.main_layout.addWidget(self.info_widget) + self.main_layout.addWidget(self.info) self.main_layout.addWidget(self.receive_info) self.main_layout.addWidget(self.primary_action) self.main_layout.addStretch() @@ -137,7 +114,7 @@ class ReceiveMode(Mode): Connection to Tor broke. """ self.primary_action.hide() - self.info_widget.hide() + self.info.show_less() def handle_request_load(self, event): """ @@ -151,7 +128,7 @@ class ReceiveMode(Mode): """ self.uploads.add(event["data"]["id"], event["data"]["content_length"]) self.uploads_in_progress += 1 - self.update_uploads_in_progress() + self.info.update_uploads_in_progress() self.system_tray.showMessage(strings._('systray_upload_started_title', True), strings._('systray_upload_started_message', True)) @@ -181,17 +158,17 @@ class ReceiveMode(Mode): self.uploads.finished(event["data"]["id"]) # Update the total 'completed uploads' info self.uploads_completed += 1 - self.update_uploads_completed() + self.info.update_uploads_completed() # Update the 'in progress uploads' info self.uploads_in_progress -= 1 - self.update_uploads_in_progress() + self.info.update_uploads_in_progress() def on_reload_settings(self): """ We should be ok to re-enable the 'Start Receive Mode' button now. """ self.primary_action.show() - self.info_widget.show() + self.info.show_more() def reset_info_counters(self): """ @@ -199,61 +176,22 @@ class ReceiveMode(Mode): """ self.uploads_completed = 0 self.uploads_in_progress = 0 - self.update_uploads_completed() - self.update_uploads_in_progress() + self.info.update_uploads_completed() + self.info.update_uploads_in_progress() self.uploads.reset() - def update_uploads_completed(self): - """ - Update the 'Uploads completed' info widget. - """ - if self.uploads_completed == 0: - image = self.common.get_resource_path('images/share_completed_none.png') - else: - image = self.common.get_resource_path('images/share_completed.png') - self.info_completed_uploads_count.setText(' {1:d}'.format(image, self.uploads_completed)) - self.info_completed_uploads_count.setToolTip(strings._('info_completed_uploads_tooltip', True).format(self.uploads_completed)) - - def update_uploads_in_progress(self): - """ - Update the 'Uploads in progress' info widget. - """ - if self.uploads_in_progress == 0: - image = self.common.get_resource_path('images/share_in_progress_none.png') - else: - image = self.common.get_resource_path('images/share_in_progress.png') - self.info_in_progress_uploads_count.setText(' {1:d}'.format(image, self.uploads_in_progress)) - self.info_in_progress_uploads_count.setToolTip(strings._('info_in_progress_uploads_tooltip', True).format(self.uploads_in_progress)) - def update_primary_action(self): self.common.log('ReceiveMode', 'update_primary_action') # Show the info widget when the server is active if self.server_status.status == self.server_status.STATUS_STARTED: - self.info_widget.show() + self.info.show_more() else: - self.info_widget.hide() + self.info.show_less() # Resize window self.resize_window() - def toggle_uploads(self): - """ - Toggle showing and hiding the Uploads widget - """ - self.common.log('ReceiveMode', 'toggle_uploads') - - if self.uploads.isVisible(): - self.uploads.hide() - self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')) ) - self.info_toggle_button.setFlat(True) - else: - self.uploads.show() - self.info_toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle_selected.png')) ) - self.info_toggle_button.setFlat(False) - - self.resize_window() - def resize_window(self): min_width = 450 if self.uploads.isVisible(): diff --git a/onionshare_gui/receive_mode/info.py b/onionshare_gui/receive_mode/info.py new file mode 100644 index 00000000..0f5bc298 --- /dev/null +++ b/onionshare_gui/receive_mode/info.py @@ -0,0 +1,109 @@ +# -*- 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 ReceiveModeInfo(QtWidgets.QWidget): + """ + Receive mode information widget + """ + def __init__(self, common, receive_mode): + super(ReceiveModeInfo, self).__init__() + self.common = common + self.receive_mode = receive_mode + + # In progress and completed labels + self.in_progress_uploads_count = QtWidgets.QLabel() + self.in_progress_uploads_count.setStyleSheet(self.common.css['mode_info_label']) + self.completed_uploads_count = QtWidgets.QLabel() + self.completed_uploads_count.setStyleSheet(self.common.css['mode_info_label']) + + # Toggle button + self.toggle_button = QtWidgets.QPushButton() + self.toggle_button.setDefault(False) + self.toggle_button.setFixedWidth(30) + self.toggle_button.setFixedHeight(30) + self.toggle_button.setFlat(True) + self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')) ) + self.toggle_button.clicked.connect(self.toggle_uploads) + + # Layout + layout = QtWidgets.QHBoxLayout() + layout.addStretch() + layout.addWidget(self.in_progress_uploads_count) + layout.addWidget(self.completed_uploads_count) + layout.addWidget(self.toggle_button) + self.setLayout(layout) + + self.update_uploads_completed() + self.update_uploads_in_progress() + + def update_uploads_completed(self): + """ + Update the 'Uploads completed' info widget. + """ + if self.receive_mode.uploads_completed == 0: + image = self.common.get_resource_path('images/share_completed_none.png') + else: + image = self.common.get_resource_path('images/share_completed.png') + self.completed_uploads_count.setText(' {1:d}'.format(image, self.receive_mode.uploads_completed)) + self.completed_uploads_count.setToolTip(strings._('info_completed_uploads_tooltip', True).format(self.receive_mode.uploads_completed)) + + def update_uploads_in_progress(self): + """ + Update the 'Uploads in progress' info widget. + """ + if self.receive_mode.uploads_in_progress == 0: + image = self.common.get_resource_path('images/share_in_progress_none.png') + else: + image = self.common.get_resource_path('images/share_in_progress.png') + self.in_progress_uploads_count.setText(' {1:d}'.format(image, self.receive_mode.uploads_in_progress)) + self.in_progress_uploads_count.setToolTip(strings._('info_in_progress_uploads_tooltip', True).format(self.receive_mode.uploads_in_progress)) + + def toggle_uploads(self): + """ + Toggle showing and hiding the Uploads widget + """ + self.common.log('ReceiveModeInfo', 'toggle_uploads') + + if self.receive_mode.uploads.isVisible(): + self.receive_mode.uploads.hide() + self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')) ) + self.toggle_button.setFlat(True) + else: + self.receive_mode.uploads.show() + self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle_selected.png')) ) + self.toggle_button.setFlat(False) + + self.receive_mode.resize_window() + + def show_less(self): + """ + Remove clutter widgets that aren't necessary. + """ + pass + + def show_more(self): + """ + Show all widgets. + """ + pass diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index a8828497..0504b529 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -28,7 +28,7 @@ from onionshare.web import Web from .file_selection import FileSelection from .downloads import Downloads from .threads import CompressThread -from .info import Info +from .info import ShareModeInfo from ..mode import Mode from ..widgets import Alert @@ -78,7 +78,7 @@ class ShareMode(Mode): self.downloads_completed = 0 # Information about share, and show downloads button - self.info = Info(self.common, self) + self.info = ShareModeInfo(self.common, self) # Primary action layout self.primary_action_layout.addWidget(self.filesize_warning) diff --git a/onionshare_gui/share_mode/info.py b/onionshare_gui/share_mode/info.py index 548d70e3..3ee12a95 100644 --- a/onionshare_gui/share_mode/info.py +++ b/onionshare_gui/share_mode/info.py @@ -22,20 +22,21 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings -class Info(QtWidgets.QWidget): +class ShareModeInfo(QtWidgets.QWidget): """ Share mode information widget """ def __init__(self, common, share_mode): - super(Info, self).__init__() + super(ShareModeInfo, self).__init__() self.common = common self.share_mode = share_mode # Label + self.label_text = "" self.label = QtWidgets.QLabel() self.label.setStyleSheet(self.common.css['mode_info_label']) - # In prorgess and completed labels + # In progress and completed labels self.in_progress_downloads_count = QtWidgets.QLabel() self.in_progress_downloads_count.setStyleSheet(self.common.css['mode_info_label']) self.completed_downloads_count = QtWidgets.QLabel() @@ -66,7 +67,8 @@ class Info(QtWidgets.QWidget): """ Updates the text of the label. """ - self.label.setText(s) + self.label_text = s + self.label.setText(self.label_text) def update_downloads_completed(self): """ @@ -94,7 +96,7 @@ class Info(QtWidgets.QWidget): """ Toggle showing and hiding the Downloads widget """ - self.common.log('ShareMode', 'toggle_downloads') + self.common.log('ShareModeInfo', 'toggle_downloads') if self.share_mode.downloads.isVisible(): self.share_mode.downloads.hide() @@ -111,10 +113,10 @@ class Info(QtWidgets.QWidget): """ Remove clutter widgets that aren't necessary. """ - self.label.hide() + self.label.setText("") def show_more(self): """ Show all widgets. """ - self.label.show() + self.label.setText(self.label_text) From 08ac4137c7b70e20bc28016eddbe3197bceb3a6b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 16:06:14 -0700 Subject: [PATCH 079/123] Process Qt events once more, to prevent weird size issues before adjusting size --- onionshare_gui/mode.py | 1 + onionshare_gui/onionshare_gui.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index bd247568..0fba029b 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -350,4 +350,5 @@ class Mode(QtWidgets.QWidget): Always resize the window after showing this Mode widget. """ super(Mode, self).show() + self.qtapp.processEvents() self.resize_window() diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 7cc368b8..51190ea3 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -454,7 +454,6 @@ class OnionShareGui(QtWidgets.QMainWindow): """ self.setMinimumWidth(min_width) - # Recursively adjust sizes for the modes def adjust_size_layout(layout): count = layout.count() for i in range(count): From c0e6968b2bcf8f7c490db375980fbef976d15e7d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 17:01:48 -0700 Subject: [PATCH 080/123] Attempting to redesign Downloads --- onionshare/common.py | 15 +++++ onionshare_gui/share_mode/downloads.py | 74 +++++++++++++++++-------- share/images/downloads.png | Bin 0 -> 2120 bytes share/images/downloads_transparent.png | Bin 0 -> 2138 bytes share/images/uploads.png | Bin 0 -> 2076 bytes share/images/uploads_transparent.png | Bin 0 -> 2096 bytes share/locale/en.json | 4 +- 7 files changed, 69 insertions(+), 24 deletions(-) create mode 100644 share/images/downloads.png create mode 100644 share/images/downloads_transparent.png create mode 100644 share/images/uploads.png create mode 100644 share/images/uploads_transparent.png diff --git a/onionshare/common.py b/onionshare/common.py index 28b282c2..78b6e2d7 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -248,6 +248,15 @@ class Common(object): border-radius: 5px; }""", + 'downloads_uploads': """ + background-color: #ffffff; + """, + + 'downloads_uploads_empty_text': """ + QLabel { + color: #999999; + }""", + 'downloads_uploads_label': """ QLabel { font-weight: bold; @@ -255,6 +264,12 @@ class Common(object): text-align: center; }""", + 'downloads_uploads_clear': """ + QPushButton { + color: #3f7fcf; + } + """, + 'downloads_uploads_progress_bar': """ QProgressBar { border: 1px solid #4e064f; diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index a34796f1..3da88bc4 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -89,33 +89,64 @@ class Downloads(QtWidgets.QScrollArea): self.downloads = {} - self.setWindowTitle(strings._('gui_downloads', True)) - self.setWidgetResizable(True) - self.setMinimumHeight(150) self.setMinimumWidth(350) - self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) - self.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint) + self.setStyleSheet(self.common.css['downloads_uploads']) + + # Scroll bar self.vbar = self.verticalScrollBar() self.vbar.rangeChanged.connect(self.resizeScroll) + # When there are no downloads + empty_image = QtWidgets.QLabel() + empty_image.setAlignment(QtCore.Qt.AlignCenter) + empty_image.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/downloads_transparent.png')))) + empty_text = QtWidgets.QLabel(strings._('gui_no_downloads', True)) + empty_text.setAlignment(QtCore.Qt.AlignCenter) + empty_text.setStyleSheet(self.common.css['downloads_uploads_empty_text']) + empty_layout = QtWidgets.QVBoxLayout() + empty_layout.addStretch() + empty_layout.addWidget(empty_image) + empty_layout.addWidget(empty_text) + empty_layout.addStretch() + self.empty = QtWidgets.QWidget() + self.empty.setLayout(empty_layout) + + # When there are downloads downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True)) downloads_label.setStyleSheet(self.common.css['downloads_uploads_label']) - self.no_downloads_label = QtWidgets.QLabel(strings._('gui_no_downloads', True)) - self.clear_history_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) - self.clear_history_button.clicked.connect(self.reset) - self.clear_history_button.hide() + clear_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) + clear_button.setStyleSheet(self.common.css['downloads_uploads_clear']) + clear_button.setFlat(True) + clear_button.clicked.connect(self.reset) + download_header = QtWidgets.QHBoxLayout() + download_header.addWidget(downloads_label) + download_header.addStretch() + download_header.addWidget(clear_button) + self.not_empty = QtWidgets.QWidget() + self.not_empty.setLayout(download_header) self.downloads_layout = QtWidgets.QVBoxLayout() - widget = QtWidgets.QWidget() + # Layout + self.widget = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout() - layout.addWidget(downloads_label) - layout.addWidget(self.no_downloads_label) - layout.addWidget(self.clear_history_button) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.empty) + layout.addWidget(self.not_empty) layout.addLayout(self.downloads_layout) layout.addStretch() - widget.setLayout(layout) - self.setWidget(widget) + self.widget.setLayout(layout) + self.setWidget(self.widget) + + # Reset once at the beginning + self.reset() + + def resizeEvent(self, event): + """ + When the widget resizes, resize the inner widget to match + """ + #self.empty.resize(self.width()-2, self.width()-2) + pass def resizeScroll(self, minimum, maximum): """ @@ -127,10 +158,9 @@ class Downloads(QtWidgets.QScrollArea): """ Add a new download progress bar. """ - # Hide the no_downloads_label - self.no_downloads_label.hide() - # Show the clear_history_button - self.clear_history_button.show() + # Hide empty, show not empty + self.empty.hide() + self.not_empty.show() # Add it to the list download = Download(self.common, download_id, total_bytes) @@ -158,6 +188,6 @@ class Downloads(QtWidgets.QScrollArea): download.progress_bar.close() self.downloads = {} - self.no_downloads_label.show() - self.clear_history_button.hide() - self.resize(self.sizeHint()) + # Hide not empty, show empty + self.not_empty.hide() + self.empty.show() diff --git a/share/images/downloads.png b/share/images/downloads.png new file mode 100644 index 0000000000000000000000000000000000000000..ad879b6e0f80dbfa3c82459e645d52f1412caf3a GIT binary patch literal 2120 zcmV-O2)Fl%P)T2h~YL zK~#9!?VVp}Th|@OKf01F)%Kl{W9HOJRQTP1#AYs39JVKz!snZXaxM?&=_zZ zm;n9(M1XPN0T5L+_1{iigUbPE+kqf(0B{3dgGJ5)kAe4qAyrd9S#tmtS!o4!0mp#~ zi_Oger+~1ksaIAlKt)!3qDkJg0a`YgJAjC)sWVm>kSv1tCU6(H;~+eQ?K{9-QK!=O zK+(U2fhtN<)4(n>`$?CH@a~Ecbu}oeYU;O30-z!*2Z7T-EvrjY!0W1}{@zF-ZiDcC z3-kj%R-b%;67UA?%4XiEQ1HP|n z>SQ4&d5SP}--OviDQbhbHlOJpC|ujWhT)}74I6=&s;NoSny?}(U)j}cTYuI{*Bf2b zt*--Ma(t4Dzq-hkKVGq|Ijfqw(X>%mbh6*Gqvi+SI!I?0UO3pkBtdgr}eWlgz5a*zgIPkQcwXNS+Z^|(y>vT~7#oX4`>6x$E zXUZ`qt5&ecAD~!Ov3+46nM|5;j0M^(nB?t&VJB*tm`|8;j719P1yC$JI*1z?)@8IdwBFK=J6YqRc0!Dg>()9=OX) zE1i9veE8oF7ym+oA^Z(~%dgJg;3qT~T4uYmuhVQZyV3zXZdU32m;34J>st0%TX!3S z*9Y19!dA=RZGB-YgVzUX>uy{2c~@T-{g?aAGFaA57#X0w zr`>dT?LF-bj0{j)pMLVRt$CZ7o>;LESTA^B(5#YEJug+B3J<}zgY=y0F%4eNsUCuF z2k}&R4Areb1(yOmU}v!EcDvbqXg9q>y@t!`9qMKGq20LMZfw>80FQXZY?*bJ%ldbY zBuS!WUkdXwJRz-E^sD?b7LluRZ~yOS4gU0}m^3VVEV|6li!>ty6QIqPxSq=q)B(fCYb zLFM23=RHEtn?6hZsf7M^sHx2$^n8eiUp_48oW^Gwsqxq3Jg&xHQ!u=TUp}+~9>97D zXtSc^;Il!-J|8RSoP2I#p*MQ$^D%>Bn9?#_g-O9h-CAzIvNuFCMef z#1;vVt+a5m<0Rqt!?v#DC6h_Q?}s_ranfp24IZGe8xs1)Jx8?=E9NVkV!V9x7YTjWqvJXgJd^i>b(>1KXzKI8ZEbMtg>>2@66l92C#1Vqu_3wB(wi5MO0I*5K2UGZJ?vnUjY_Pn;I*a0IWqf~$q;At)W^(0Bc)iehvNox4{^)te{yw&91L-sjG7mnv&K4W4*Ft)eVsuW` z)T4#~3W(Q$rvEc|qpGGh=bSI+Tbi9oCrUthC!Hv#Jy|Jh(w;1VIkSc#&Z?SP(3bdO zCasO=X_D4lvlZ8d)c|9ne;8e31I9-5Y{3P|&^v8wB*e)Xs$`8hjaZJvBOe94fv?0o zakb?ToC1!hnmS;lka7x8gEDR9Q&eaNrM(n34eT%ro~a&~;*D>jC$A`#{0m>BFNXG; yU2r)Occ}tQfmu!kZz9a{j}e#%bJhPG#{U3#k;&MKJ6Qq%0000Iz^0 literal 0 HcmV?d00001 diff --git a/share/images/downloads_transparent.png b/share/images/downloads_transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..9920709707a9dc6e3e9c59f5d5ab9d6a1fc21026 GIT binary patch literal 2138 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 a#{U3{;?ogoegf_Q0000 Date: Fri, 28 Sep 2018 18:30:32 -0700 Subject: [PATCH 081/123] Got empty Downloads looking good --- onionshare/common.py | 13 +++++++++-- onionshare_gui/share_mode/downloads.py | 31 +++++++++++++------------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/onionshare/common.py b/onionshare/common.py index 78b6e2d7..3489c3e6 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -248,8 +248,15 @@ class Common(object): border-radius: 5px; }""", - 'downloads_uploads': """ - background-color: #ffffff; + 'downloads_uploads_empty': """ + QWidget { + background-color: #ffffff; + border: 1px solid #999999; + } + QWidget QLabel { + background-color: none; + border: 0px; + } """, 'downloads_uploads_empty_text': """ @@ -262,6 +269,8 @@ class Common(object): font-weight: bold; font-size 14px; text-align: center; + background-color: none; + border: none; }""", 'downloads_uploads_clear': """ diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index 3da88bc4..fcf8bc8e 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -78,7 +78,7 @@ class Download(object): self.started) -class Downloads(QtWidgets.QScrollArea): +class Downloads(QtWidgets.QWidget): """ The downloads chunk of the GUI. This lists all of the active download progress bars. @@ -90,11 +90,6 @@ class Downloads(QtWidgets.QScrollArea): self.downloads = {} self.setMinimumWidth(350) - self.setStyleSheet(self.common.css['downloads_uploads']) - - # Scroll bar - self.vbar = self.verticalScrollBar() - self.vbar.rangeChanged.connect(self.resizeScroll) # When there are no downloads empty_image = QtWidgets.QLabel() @@ -109,6 +104,7 @@ class Downloads(QtWidgets.QScrollArea): empty_layout.addWidget(empty_text) empty_layout.addStretch() self.empty = QtWidgets.QWidget() + self.empty.setStyleSheet(self.common.css['downloads_uploads_empty']) self.empty.setLayout(empty_layout) # When there are downloads @@ -122,25 +118,29 @@ class Downloads(QtWidgets.QScrollArea): download_header.addWidget(downloads_label) download_header.addStretch() download_header.addWidget(clear_button) - self.not_empty = QtWidgets.QWidget() - self.not_empty.setLayout(download_header) - self.downloads_layout = QtWidgets.QVBoxLayout() + not_empty_layout = QtWidgets.QVBoxLayout() + not_empty_layout.addLayout(download_header) + not_empty_layout.addLayout(self.downloads_layout) + self.not_empty = QtWidgets.QWidget() + self.not_empty.setLayout(not_empty_layout) # Layout - self.widget = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.empty) layout.addWidget(self.not_empty) - layout.addLayout(self.downloads_layout) - layout.addStretch() - self.widget.setLayout(layout) - self.setWidget(self.widget) + self.setLayout(layout) # Reset once at the beginning self.reset() + """ + # Scroll bar + self.vbar = self.verticalScrollBar() + self.vbar.rangeChanged.connect(self.resizeScroll) + """ + def resizeEvent(self, event): """ When the widget resizes, resize the inner widget to match @@ -152,7 +152,8 @@ class Downloads(QtWidgets.QScrollArea): """ Scroll to the bottom of the window when the range changes. """ - self.vbar.setValue(maximum) + pass + #self.vbar.setValue(maximum) def add(self, download_id, total_bytes): """ From 71149c2937ba52429ef4360caaff28fc547e8380 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 18:48:12 -0700 Subject: [PATCH 082/123] Refactor Downloads to use an internal QListWidget to list the progess bars --- onionshare_gui/share_mode/downloads.py | 83 +++++++++++++++++++------- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index fcf8bc8e..855827b1 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -78,6 +78,59 @@ class Download(object): self.started) +class DownloadList(QtWidgets.QListWidget): + """ + List of download progess bars. + """ + def __init__(self, common): + super(DownloadList, self).__init__() + self.common = common + + self.downloads = {} + + self.setMinimumHeight(205) + self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + + def add(self, download_id, total_bytes): + """ + Add a new download progress bar. + """ + download = Download(self.common, download_id, total_bytes) + self.downloads[download_id] = download + + item = QtWidgets.QListWidgetItem() + self.addItem(item) + self.setItemWidget(item, download.progress_bar) + + def update(self, download_id, downloaded_bytes): + """ + Update the progress of a download progress bar. + """ + self.downloads[download_id].update(downloaded_bytes) + + def cancel(self, download_id): + """ + Update a download progress bar to show that it has been canceled. + """ + self.downloads[download_id].cancel() + + def reset(self): + """ + Reset the downloads back to zero + """ + # Remove all items from list + while True: + item = self.takeItem(0) + if not item: + break + + # Close all progress bars + for download in self.downloads.values(): + download.progress_bar.close() + self.downloads = {} + + class Downloads(QtWidgets.QWidget): """ The downloads chunk of the GUI. This lists all of the active download @@ -87,8 +140,6 @@ class Downloads(QtWidgets.QWidget): super(Downloads, self).__init__() self.common = common - self.downloads = {} - self.setMinimumWidth(350) # When there are no downloads @@ -108,6 +159,9 @@ class Downloads(QtWidgets.QWidget): self.empty.setLayout(empty_layout) # When there are downloads + self.download_list = DownloadList(self.common) + + # Download header downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True)) downloads_label.setStyleSheet(self.common.css['downloads_uploads_label']) clear_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) @@ -118,10 +172,11 @@ class Downloads(QtWidgets.QWidget): download_header.addWidget(downloads_label) download_header.addStretch() download_header.addWidget(clear_button) - self.downloads_layout = QtWidgets.QVBoxLayout() + + # Download layout not_empty_layout = QtWidgets.QVBoxLayout() not_empty_layout.addLayout(download_header) - not_empty_layout.addLayout(self.downloads_layout) + not_empty_layout.addWidget(self.download_list) self.not_empty = QtWidgets.QWidget() self.not_empty.setLayout(not_empty_layout) @@ -141,13 +196,6 @@ class Downloads(QtWidgets.QWidget): self.vbar.rangeChanged.connect(self.resizeScroll) """ - def resizeEvent(self, event): - """ - When the widget resizes, resize the inner widget to match - """ - #self.empty.resize(self.width()-2, self.width()-2) - pass - def resizeScroll(self, minimum, maximum): """ Scroll to the bottom of the window when the range changes. @@ -164,30 +212,25 @@ class Downloads(QtWidgets.QWidget): self.not_empty.show() # Add it to the list - download = Download(self.common, download_id, total_bytes) - self.downloads[download_id] = download - self.downloads_layout.addWidget(download.progress_bar) + self.download_list.add(download_id, total_bytes) def update(self, download_id, downloaded_bytes): """ Update the progress of a download progress bar. """ - self.downloads[download_id].update(downloaded_bytes) + self.download_list.update(download_id, downloaded_bytes) def cancel(self, download_id): """ Update a download progress bar to show that it has been canceled. """ - self.downloads[download_id].cancel() + self.download_list.cancel(download_id) def reset(self): """ Reset the downloads back to zero """ - for download in self.downloads.values(): - self.downloads_layout.removeWidget(download.progress_bar) - download.progress_bar.close() - self.downloads = {} + self.download_list.reset() # Hide not empty, show empty self.not_empty.hide() From 9a05eef49495f7100f2103a3fcbfae6b0928b748 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 19:05:26 -0700 Subject: [PATCH 083/123] Slightly improve Downloads progress bar style, but still needs spacing --- onionshare/common.py | 2 +- onionshare_gui/share_mode/downloads.py | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/onionshare/common.py b/onionshare/common.py index 3489c3e6..6662ca4e 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -285,7 +285,7 @@ class Common(object): background-color: #ffffff !important; text-align: center; color: #9b9b9b; - font-size: 12px; + font-size: 14px; } QProgressBar::chunk { background-color: #4e064f; diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index 855827b1..73e8faf5 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -190,19 +190,6 @@ class Downloads(QtWidgets.QWidget): # Reset once at the beginning self.reset() - """ - # Scroll bar - self.vbar = self.verticalScrollBar() - self.vbar.rangeChanged.connect(self.resizeScroll) - """ - - def resizeScroll(self, minimum, maximum): - """ - Scroll to the bottom of the window when the range changes. - """ - pass - #self.vbar.setValue(maximum) - def add(self, download_id, total_bytes): """ Add a new download progress bar. From e87263353f2cfae32eb965d2e5b8c1cdfc898e20 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 19:54:46 -0700 Subject: [PATCH 084/123] Added an indicator count for share mode --- onionshare/common.py | 10 ++++++++++ onionshare_gui/share_mode/__init__.py | 1 + onionshare_gui/share_mode/info.py | 26 ++++++++++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/onionshare/common.py b/onionshare/common.py index 6662ca4e..43a6ac43 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -279,6 +279,16 @@ class Common(object): } """, + 'download_uploads_indicator': """ + QLabel { + color: #ffffff; + background-color: #f44449; + font-weight: bold; + padding: 5px; + border-radius: 5px; + font-size: 10px; + }""", + 'downloads_uploads_progress_bar': """ QProgressBar { border: 1px solid #4e064f; diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 0504b529..58801c45 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -224,6 +224,7 @@ class ShareMode(Mode): else: filesize = self.web.share_mode.download_filesize self.downloads.add(event["data"]["id"], filesize) + self.info.update_indicator(True) self.downloads_in_progress += 1 self.info.update_downloads_in_progress() diff --git a/onionshare_gui/share_mode/info.py b/onionshare_gui/share_mode/info.py index 3ee12a95..a191933d 100644 --- a/onionshare_gui/share_mode/info.py +++ b/onionshare_gui/share_mode/info.py @@ -51,12 +51,19 @@ class ShareModeInfo(QtWidgets.QWidget): self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')) ) self.toggle_button.clicked.connect(self.toggle_downloads) + # Keep track of indicator + self.indicator_count = 0 + self.indicator_label = QtWidgets.QLabel() + self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) + self.update_indicator() + # Info layout layout = QtWidgets.QHBoxLayout() layout.addWidget(self.label) layout.addStretch() layout.addWidget(self.in_progress_downloads_count) layout.addWidget(self.completed_downloads_count) + layout.addWidget(self.indicator_label) layout.addWidget(self.toggle_button) self.setLayout(layout) @@ -70,6 +77,21 @@ class ShareModeInfo(QtWidgets.QWidget): self.label_text = s self.label.setText(self.label_text) + def update_indicator(self, increment=False): + """ + Update the display of the indicator count. If increment is True, then + only increment the counter if Downloads is hidden. + """ + if increment and not self.share_mode.downloads.isVisible(): + self.indicator_count += 1 + + self.indicator_label.setText("{}".format(self.indicator_count)) + + if self.indicator_count == 0: + self.indicator_label.hide() + else: + self.indicator_label.show() + def update_downloads_completed(self): """ Update the 'Downloads completed' info widget. @@ -107,6 +129,10 @@ class ShareModeInfo(QtWidgets.QWidget): self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png')) ) self.toggle_button.setFlat(False) + # Reset the indicator count + self.indicator_count = 0 + self.update_indicator() + self.share_mode.resize_window() def show_less(self): From 709eeeac5f5805e63279ec68eeee5fde31ef345d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 28 Sep 2018 22:03:48 -0700 Subject: [PATCH 085/123] Starting to implement the new Uploads UI, but not done --- onionshare_gui/receive_mode/__init__.py | 1 + onionshare_gui/receive_mode/info.py | 26 ++++ onionshare_gui/receive_mode/uploads.py | 191 ++++++++++++++++-------- onionshare_gui/share_mode/downloads.py | 2 + onionshare_gui/share_mode/info.py | 2 +- share/locale/en.json | 2 +- 6 files changed, 157 insertions(+), 67 deletions(-) diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index 83113805..2f61b2ca 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -127,6 +127,7 @@ class ReceiveMode(Mode): Handle REQUEST_STARTED event. """ self.uploads.add(event["data"]["id"], event["data"]["content_length"]) + self.info.update_indicator(True) self.uploads_in_progress += 1 self.info.update_uploads_in_progress() diff --git a/onionshare_gui/receive_mode/info.py b/onionshare_gui/receive_mode/info.py index 0f5bc298..bc4aada8 100644 --- a/onionshare_gui/receive_mode/info.py +++ b/onionshare_gui/receive_mode/info.py @@ -46,17 +46,39 @@ class ReceiveModeInfo(QtWidgets.QWidget): self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')) ) self.toggle_button.clicked.connect(self.toggle_uploads) + # Keep track of indicator + self.indicator_count = 0 + self.indicator_label = QtWidgets.QLabel() + self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) + self.update_indicator() + # Layout layout = QtWidgets.QHBoxLayout() layout.addStretch() layout.addWidget(self.in_progress_uploads_count) layout.addWidget(self.completed_uploads_count) + layout.addWidget(self.indicator_label) layout.addWidget(self.toggle_button) self.setLayout(layout) self.update_uploads_completed() self.update_uploads_in_progress() + def update_indicator(self, increment=False): + """ + Update the display of the indicator count. If increment is True, then + only increment the counter if Uploads is hidden. + """ + if increment and not self.receive_mode.uploads.isVisible(): + self.indicator_count += 1 + + self.indicator_label.setText("{}".format(self.indicator_count)) + + if self.indicator_count == 0: + self.indicator_label.hide() + else: + self.indicator_label.show() + def update_uploads_completed(self): """ Update the 'Uploads completed' info widget. @@ -94,6 +116,10 @@ class ReceiveModeInfo(QtWidgets.QWidget): self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle_selected.png')) ) self.toggle_button.setFlat(False) + # Reset the indicator count + self.indicator_count = 0 + self.update_indicator() + self.receive_mode.resize_window() def show_less(self): diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py index 48574cc7..8cda2e61 100644 --- a/onionshare_gui/receive_mode/uploads.py +++ b/onionshare_gui/receive_mode/uploads.py @@ -206,69 +206,30 @@ class Upload(QtWidgets.QWidget): self.label.setText(text) -class Uploads(QtWidgets.QScrollArea): +class UploadList(QtWidgets.QListWidget): """ - The uploads chunk of the GUI. This lists all of the active upload - progress bars, as well as information about each upload. + List of upload progess bars. """ def __init__(self, common): - super(Uploads, self).__init__() + super(UploadList, self).__init__() self.common = common - self.common.log('Uploads', '__init__') - - self.resizeEvent = None self.uploads = {} - self.setWindowTitle(strings._('gui_uploads', True)) - self.setWidgetResizable(True) - self.setMinimumHeight(150) - self.setMinimumWidth(350) - self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) - self.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint) - self.vbar = self.verticalScrollBar() - self.vbar.rangeChanged.connect(self.resizeScroll) - - uploads_label = QtWidgets.QLabel(strings._('gui_uploads', True)) - uploads_label.setStyleSheet(self.common.css['downloads_uploads_label']) - self.no_uploads_label = QtWidgets.QLabel(strings._('gui_no_uploads', True)) - self.clear_history_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) - self.clear_history_button.clicked.connect(self.reset) - self.clear_history_button.hide() - - - self.uploads_layout = QtWidgets.QVBoxLayout() - - widget = QtWidgets.QWidget() - layout = QtWidgets.QVBoxLayout() - layout.addWidget(uploads_label) - layout.addWidget(self.no_uploads_label) - layout.addWidget(self.clear_history_button) - layout.addLayout(self.uploads_layout) - layout.addStretch() - widget.setLayout(layout) - self.setWidget(widget) - - def resizeScroll(self, minimum, maximum): - """ - Scroll to the bottom of the window when the range changes. - """ - self.vbar.setValue(maximum) + self.setMinimumHeight(205) + self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) def add(self, upload_id, content_length): """ - Add a new upload. + Add a new upload progress bar. """ - self.common.log('Uploads', 'add', 'upload_id: {}, content_length: {}'.format(upload_id, content_length)) - # Hide the no_uploads_label - self.no_uploads_label.hide() - # Show the clear_history_button - self.clear_history_button.show() - - # Add it to the list upload = Upload(self.common, upload_id, content_length) self.uploads[upload_id] = upload - self.uploads_layout.addWidget(upload) + + item = QtWidgets.QListWidgetItem() + self.addItem(item) + self.setItemWidget(item, upload) def update(self, upload_id, progress): """ @@ -299,22 +260,122 @@ class Uploads(QtWidgets.QScrollArea): """ Reset the uploads back to zero """ - self.common.log('Uploads', 'reset') + # Remove all items from list + while True: + item = self.takeItem(0) + if not item: + break + + # Close all progress bars for upload in self.uploads.values(): - upload.close() - self.uploads_layout.removeWidget(upload) + upload.progress_bar.close() self.uploads = {} - self.no_uploads_label.show() - self.clear_history_button.hide() - self.resize(self.sizeHint()) - def resizeEvent(self, event): - width = self.frameGeometry().width() - try: - for upload in self.uploads.values(): - for item in upload.files.values(): - item.filename_label.setText(textwrap.fill(item.filename, 30)) - item.adjustSize() - except: - pass +class Uploads(QtWidgets.QWidget): + """ + The uploads chunk of the GUI. This lists all of the active upload + progress bars, as well as information about each upload. + """ + def __init__(self, common): + super(Uploads, self).__init__() + self.common = common + self.common.log('Uploads', '__init__') + + self.setMinimumWidth(350) + + # When there are no uploads + empty_image = QtWidgets.QLabel() + empty_image.setAlignment(QtCore.Qt.AlignCenter) + empty_image.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/uploads_transparent.png')))) + empty_text = QtWidgets.QLabel(strings._('gui_no_uploads', True)) + empty_text.setAlignment(QtCore.Qt.AlignCenter) + empty_text.setStyleSheet(self.common.css['downloads_uploads_empty_text']) + empty_layout = QtWidgets.QVBoxLayout() + empty_layout.addStretch() + empty_layout.addWidget(empty_image) + empty_layout.addWidget(empty_text) + empty_layout.addStretch() + self.empty = QtWidgets.QWidget() + self.empty.setStyleSheet(self.common.css['downloads_uploads_empty']) + self.empty.setLayout(empty_layout) + + # When there are uploads + self.upload_list = UploadList(self.common) + + # Upload header + uploads_label = QtWidgets.QLabel(strings._('gui_uploads', True)) + uploads_label.setStyleSheet(self.common.css['downloads_uploads_label']) + clear_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) + clear_button.setStyleSheet(self.common.css['downloads_uploads_clear']) + clear_button.setFlat(True) + clear_button.clicked.connect(self.reset) + upload_header = QtWidgets.QHBoxLayout() + upload_header.addWidget(uploads_label) + upload_header.addStretch() + upload_header.addWidget(clear_button) + + # Upload layout + not_empty_layout = QtWidgets.QVBoxLayout() + not_empty_layout.addLayout(upload_header) + not_empty_layout.addWidget(self.upload_list) + self.not_empty = QtWidgets.QWidget() + self.not_empty.setLayout(not_empty_layout) + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.empty) + layout.addWidget(self.not_empty) + self.setLayout(layout) + + # Reset once at the beginning + self.reset() + + def add(self, upload_id, content_length): + """ + Add a new upload. + """ + self.common.log('Uploads', 'add', 'upload_id: {}, content_length: {}'.format(upload_id, content_length)) + + # Hide empty, show not empty + self.empty.hide() + self.not_empty.show() + + # Add it to the list + self.upload_list.add(upload_id, content_length) + + def update(self, upload_id, progress): + """ + Update the progress of an upload. + """ + self.upload_list.update(upload_id, progress) + + def rename(self, upload_id, old_filename, new_filename): + """ + Rename a file, which happens if the filename already exists in downloads_dir. + """ + self.upload_list.rename(upload_id, old_filename, new_filename) + + def finished(self, upload_id): + """ + An upload has finished. + """ + self.upload_list.finished(upload_id) + + def cancel(self, upload_id): + """ + Update an upload progress bar to show that it has been canceled. + """ + self.common.log('Uploads', 'cancel', 'upload_id: {}'.format(upload_id)) + self.upload_list.cancel(upload_id) + + def reset(self): + """ + Reset the uploads back to zero + """ + self.upload_list.reset() + + # Hide not empty, show empty + self.not_empty.hide() + self.empty.show() diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index 73e8faf5..bb2376ef 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -194,6 +194,8 @@ class Downloads(QtWidgets.QWidget): """ Add a new download progress bar. """ + self.common.log('Downloads', 'add', 'download_id: {}, content_length: {}'.format(download_id, content_length)) + # Hide empty, show not empty self.empty.hide() self.not_empty.show() diff --git a/onionshare_gui/share_mode/info.py b/onionshare_gui/share_mode/info.py index a191933d..f8a68df7 100644 --- a/onionshare_gui/share_mode/info.py +++ b/onionshare_gui/share_mode/info.py @@ -57,7 +57,7 @@ class ShareModeInfo(QtWidgets.QWidget): self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) self.update_indicator() - # Info layout + # Layout layout = QtWidgets.QHBoxLayout() layout.addWidget(self.label) layout.addStretch() diff --git a/share/locale/en.json b/share/locale/en.json index 5821eea2..c7beb6ba 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -175,7 +175,7 @@ "systray_download_page_loaded_message": "A user loaded the download page", "systray_upload_page_loaded_message": "A user loaded the upload page", "gui_uploads": "Upload History", - "gui_no_uploads": "No uploads yet.", + "gui_no_uploads": "No Uploads Yet", "gui_clear_history": "Clear All", "gui_upload_in_progress": "Upload Started {}", "gui_upload_finished_range": "Uploaded {} to {}", From 4aed7c0f9d1f272b9bd408cea6752cc4a115dc5b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 29 Sep 2018 12:00:22 -0700 Subject: [PATCH 086/123] Use correct variable name for Downloads --- onionshare_gui/share_mode/downloads.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index bb2376ef..e567443a 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -92,11 +92,11 @@ class DownloadList(QtWidgets.QListWidget): self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) - def add(self, download_id, total_bytes): + def add(self, download_id, content_length): """ Add a new download progress bar. """ - download = Download(self.common, download_id, total_bytes) + download = Download(self.common, download_id, content_length) self.downloads[download_id] = download item = QtWidgets.QListWidgetItem() @@ -190,7 +190,7 @@ class Downloads(QtWidgets.QWidget): # Reset once at the beginning self.reset() - def add(self, download_id, total_bytes): + def add(self, download_id, content_length): """ Add a new download progress bar. """ @@ -201,7 +201,7 @@ class Downloads(QtWidgets.QWidget): self.not_empty.show() # Add it to the list - self.download_list.add(download_id, total_bytes) + self.download_list.add(download_id, content_length) def update(self, download_id, downloaded_bytes): """ From ffad77930f881de67381cbf9de12fadacdcc17df Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 29 Sep 2018 12:19:01 -0700 Subject: [PATCH 087/123] Switch Downloads from QListWidget to QScrollArea --- onionshare_gui/share_mode/downloads.py | 44 ++++++++++++++++---------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index e567443a..8eade23c 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -23,8 +23,9 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings -class Download(object): +class Download(QtWidgets.QWidget): def __init__(self, common, download_id, total_bytes): + super(Download, self).__init__() self.common = common self.download_id = download_id @@ -32,6 +33,8 @@ class Download(object): self.total_bytes = total_bytes self.downloaded_bytes = 0 + self.setStyleSheet('QWidget { border: 1px solid red; }') + # Progress bar self.progress_bar = QtWidgets.QProgressBar() self.progress_bar.setTextVisible(True) @@ -43,6 +46,11 @@ class Download(object): self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar']) self.progress_bar.total_bytes = total_bytes + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.progress_bar) + self.setLayout(layout) + # Start at 0 self.update(0) @@ -78,9 +86,9 @@ class Download(object): self.started) -class DownloadList(QtWidgets.QListWidget): +class DownloadList(QtWidgets.QScrollArea): """ - List of download progess bars. + List of download progress bars. """ def __init__(self, common): super(DownloadList, self).__init__() @@ -88,9 +96,20 @@ class DownloadList(QtWidgets.QListWidget): self.downloads = {} - self.setMinimumHeight(205) - self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.downloads_layout = QtWidgets.QVBoxLayout() + self.downloads_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) + widget = QtWidgets.QWidget() + widget.setLayout(self.downloads_layout) + self.setWidget(widget) + + self.setBackgroundRole(QtGui.QPalette.Light) + self.verticalScrollBar().rangeChanged.connect(self.resizeScroll) + + def resizeScroll(self, minimum, maximum): + """ + Scroll to the bottom of the window when the range changes. + """ + self.verticalScrollBar().setValue(maximum) def add(self, download_id, content_length): """ @@ -98,10 +117,7 @@ class DownloadList(QtWidgets.QListWidget): """ download = Download(self.common, download_id, content_length) self.downloads[download_id] = download - - item = QtWidgets.QListWidgetItem() - self.addItem(item) - self.setItemWidget(item, download.progress_bar) + self.downloads_layout.addWidget(download) def update(self, download_id, downloaded_bytes): """ @@ -119,14 +135,8 @@ class DownloadList(QtWidgets.QListWidget): """ Reset the downloads back to zero """ - # Remove all items from list - while True: - item = self.takeItem(0) - if not item: - break - - # Close all progress bars for download in self.downloads.values(): + self.downloads_layout.removeWidget(download.progress_bar) download.progress_bar.close() self.downloads = {} From d87115d21e8fc11979e416b9308019c9a7866bfe Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 29 Sep 2018 13:41:12 -0700 Subject: [PATCH 088/123] Fix Downloads scroll area so internal widget is always the right size --- onionshare_gui/share_mode/downloads.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index 8eade23c..50e7f0ef 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -96,12 +96,24 @@ class DownloadList(QtWidgets.QScrollArea): self.downloads = {} + # The layout that holds all of the downloads self.downloads_layout = QtWidgets.QVBoxLayout() + self.downloads_layout.setContentsMargins(0, 0, 0, 0) self.downloads_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) - widget = QtWidgets.QWidget() - widget.setLayout(self.downloads_layout) - self.setWidget(widget) + # Wrapper layout that also contains a stretch + wrapper_layout = QtWidgets.QVBoxLayout() + wrapper_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) + wrapper_layout.addLayout(self.downloads_layout) + wrapper_layout.addStretch() + + # The internal widget of the scroll area + widget = QtWidgets.QWidget() + widget.setLayout(wrapper_layout) + self.setWidget(widget) + self.setWidgetResizable(True) + + # Other scroll area settings self.setBackgroundRole(QtGui.QPalette.Light) self.verticalScrollBar().rangeChanged.connect(self.resizeScroll) From fa4ebbf263f02a07315972b805fd111e19463550 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 29 Sep 2018 13:47:00 -0700 Subject: [PATCH 089/123] Convert Uploads to a QScrollArea also --- onionshare_gui/receive_mode/uploads.py | 44 +++++++++++++++++--------- onionshare_gui/share_mode/downloads.py | 2 +- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py index 8cda2e61..f08b35cc 100644 --- a/onionshare_gui/receive_mode/uploads.py +++ b/onionshare_gui/receive_mode/uploads.py @@ -206,7 +206,7 @@ class Upload(QtWidgets.QWidget): self.label.setText(text) -class UploadList(QtWidgets.QListWidget): +class UploadList(QtWidgets.QScrollArea): """ List of upload progess bars. """ @@ -216,9 +216,32 @@ class UploadList(QtWidgets.QListWidget): self.uploads = {} - self.setMinimumHeight(205) - self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + # The layout that holds all of the uploads + self.uploads_layout = QtWidgets.QVBoxLayout() + self.uploads_layout.setContentsMargins(0, 0, 0, 0) + self.uploads_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) + + # Wrapper layout that also contains a stretch + wrapper_layout = QtWidgets.QVBoxLayout() + wrapper_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) + wrapper_layout.addLayout(self.uploads_layout) + wrapper_layout.addStretch() + + # The internal widget of the scroll area + widget = QtWidgets.QWidget() + widget.setLayout(wrapper_layout) + self.setWidget(widget) + self.setWidgetResizable(True) + + # Other scroll area settings + self.setBackgroundRole(QtGui.QPalette.Light) + self.verticalScrollBar().rangeChanged.connect(self.resizeScroll) + + def resizeScroll(self, minimum, maximum): + """ + Scroll to the bottom of the window when the range changes. + """ + self.verticalScrollBar().setValue(maximum) def add(self, upload_id, content_length): """ @@ -226,10 +249,7 @@ class UploadList(QtWidgets.QListWidget): """ upload = Upload(self.common, upload_id, content_length) self.uploads[upload_id] = upload - - item = QtWidgets.QListWidgetItem() - self.addItem(item) - self.setItemWidget(item, upload) + self.uploads_layout.addWidget(upload) def update(self, upload_id, progress): """ @@ -260,14 +280,8 @@ class UploadList(QtWidgets.QListWidget): """ Reset the uploads back to zero """ - # Remove all items from list - while True: - item = self.takeItem(0) - if not item: - break - - # Close all progress bars for upload in self.uploads.values(): + self.uploads_layout.removeWidget(upload) upload.progress_bar.close() self.uploads = {} diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index 50e7f0ef..e78231ad 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -148,7 +148,7 @@ class DownloadList(QtWidgets.QScrollArea): Reset the downloads back to zero """ for download in self.downloads.values(): - self.downloads_layout.removeWidget(download.progress_bar) + self.downloads_layout.removeWidget(download) download.progress_bar.close() self.downloads = {} From 4b3a68bb655c20ab2ccd6bfe8f9c492bcbd02ea9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 29 Sep 2018 14:40:55 -0700 Subject: [PATCH 090/123] Got the indicator label to display in the correct location for share mode --- onionshare/common.py | 5 +++-- onionshare_gui/share_mode/info.py | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/onionshare/common.py b/onionshare/common.py index 43a6ac43..fb3b1e7a 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -284,9 +284,10 @@ class Common(object): color: #ffffff; background-color: #f44449; font-weight: bold; - padding: 5px; - border-radius: 5px; font-size: 10px; + padding: 2px; + border-radius: 8px; + text-align: center; }""", 'downloads_uploads_progress_bar': """ diff --git a/onionshare_gui/share_mode/info.py b/onionshare_gui/share_mode/info.py index f8a68df7..b69820d6 100644 --- a/onionshare_gui/share_mode/info.py +++ b/onionshare_gui/share_mode/info.py @@ -45,7 +45,7 @@ class ShareModeInfo(QtWidgets.QWidget): # Toggle button self.toggle_button = QtWidgets.QPushButton() self.toggle_button.setDefault(False) - self.toggle_button.setFixedWidth(30) + self.toggle_button.setFixedWidth(35) self.toggle_button.setFixedHeight(30) self.toggle_button.setFlat(True) self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')) ) @@ -53,17 +53,24 @@ class ShareModeInfo(QtWidgets.QWidget): # Keep track of indicator self.indicator_count = 0 - self.indicator_label = QtWidgets.QLabel() + self.indicator_label = QtWidgets.QLabel(parent=self.toggle_button) self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) self.update_indicator() + """ + # Add it to the toggle button + toggle_button_layout = QtWidgets.QHBoxLayout() + toggle_button_layout.addSpacing(10) + toggle_button_layout.addWidget(self.indicator_label) + self.toggle_button.setLayout(toggle_button_layout) + """ + # Layout layout = QtWidgets.QHBoxLayout() layout.addWidget(self.label) layout.addStretch() layout.addWidget(self.in_progress_downloads_count) layout.addWidget(self.completed_downloads_count) - layout.addWidget(self.indicator_label) layout.addWidget(self.toggle_button) self.setLayout(layout) @@ -90,6 +97,8 @@ class ShareModeInfo(QtWidgets.QWidget): if self.indicator_count == 0: self.indicator_label.hide() else: + size = self.indicator_label.sizeHint() + self.indicator_label.setGeometry(35-size.width(), 0, size.width(), size.height()) self.indicator_label.show() def update_downloads_completed(self): From b4de634b7ac3a12450bc99a5e962dab16f486778 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 29 Sep 2018 14:43:13 -0700 Subject: [PATCH 091/123] Fix indicator label display for receive mode --- onionshare_gui/receive_mode/info.py | 7 ++++--- onionshare_gui/share_mode/info.py | 8 -------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/onionshare_gui/receive_mode/info.py b/onionshare_gui/receive_mode/info.py index bc4aada8..c23f8496 100644 --- a/onionshare_gui/receive_mode/info.py +++ b/onionshare_gui/receive_mode/info.py @@ -40,7 +40,7 @@ class ReceiveModeInfo(QtWidgets.QWidget): # Toggle button self.toggle_button = QtWidgets.QPushButton() self.toggle_button.setDefault(False) - self.toggle_button.setFixedWidth(30) + self.toggle_button.setFixedWidth(35) self.toggle_button.setFixedHeight(30) self.toggle_button.setFlat(True) self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')) ) @@ -48,7 +48,7 @@ class ReceiveModeInfo(QtWidgets.QWidget): # Keep track of indicator self.indicator_count = 0 - self.indicator_label = QtWidgets.QLabel() + self.indicator_label = QtWidgets.QLabel(parent=self.toggle_button) self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) self.update_indicator() @@ -57,7 +57,6 @@ class ReceiveModeInfo(QtWidgets.QWidget): layout.addStretch() layout.addWidget(self.in_progress_uploads_count) layout.addWidget(self.completed_uploads_count) - layout.addWidget(self.indicator_label) layout.addWidget(self.toggle_button) self.setLayout(layout) @@ -77,6 +76,8 @@ class ReceiveModeInfo(QtWidgets.QWidget): if self.indicator_count == 0: self.indicator_label.hide() else: + size = self.indicator_label.sizeHint() + self.indicator_label.setGeometry(35-size.width(), 0, size.width(), size.height()) self.indicator_label.show() def update_uploads_completed(self): diff --git a/onionshare_gui/share_mode/info.py b/onionshare_gui/share_mode/info.py index b69820d6..c692649c 100644 --- a/onionshare_gui/share_mode/info.py +++ b/onionshare_gui/share_mode/info.py @@ -57,14 +57,6 @@ class ShareModeInfo(QtWidgets.QWidget): self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) self.update_indicator() - """ - # Add it to the toggle button - toggle_button_layout = QtWidgets.QHBoxLayout() - toggle_button_layout.addSpacing(10) - toggle_button_layout.addWidget(self.indicator_label) - self.toggle_button.setLayout(toggle_button_layout) - """ - # Layout layout = QtWidgets.QHBoxLayout() layout.addWidget(self.label) From 8ca34fadd9895b7e2b16c13e5d315cc40e396c97 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 29 Sep 2018 14:49:06 -0700 Subject: [PATCH 092/123] Fix crash when clicking Help from the systray --- onionshare_gui/onionshare_gui.py | 2 +- onionshare_gui/settings_dialog.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 51190ea3..dd15fe12 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -65,7 +65,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.settings_action = menu.addAction(strings._('gui_settings_window_title', True)) self.settings_action.triggered.connect(self.open_settings) help_action = menu.addAction(strings._('gui_settings_button_help', True)) - help_action.triggered.connect(SettingsDialog.help_clicked) + help_action.triggered.connect(SettingsDialog.open_help) exit_action = menu.addAction(strings._('systray_menu_exit', True)) exit_action.triggered.connect(self.close) diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 3cd25d31..39f08128 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -883,8 +883,12 @@ class SettingsDialog(QtWidgets.QDialog): Help button clicked. """ self.common.log('SettingsDialog', 'help_clicked') - help_site = 'https://github.com/micahflee/onionshare/wiki' - QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_site)) + SettingsDialog.open_help() + + @staticmethod + def open_help(): + help_url = 'https://github.com/micahflee/onionshare/wiki' + QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_url)) def settings_from_fields(self): """ From 39dd0862d4c02391fa32bf5fd97b0ec721425abb Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 29 Sep 2018 15:12:05 -0700 Subject: [PATCH 093/123] Increase minimum window with to 460, and store it in a variable to stop repeating myself --- onionshare_gui/mode.py | 4 ++-- onionshare_gui/onionshare_gui.py | 4 +++- onionshare_gui/receive_mode/__init__.py | 2 +- onionshare_gui/share_mode/__init__.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index 0fba029b..1a961149 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -50,7 +50,7 @@ class Mode(QtWidgets.QWidget): self.filenames = filenames - self.setMinimumWidth(450) + self.setMinimumWidth(self.common.min_window_width) # The web object gets created in init() self.web = None @@ -83,7 +83,7 @@ class Mode(QtWidgets.QWidget): # 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(450) + self.min_width_widget.setMinimumWidth(self.common.min_window_width) def init(self): """ diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index dd15fe12..ced53ede 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -45,6 +45,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.common = common self.common.log('OnionShareGui', '__init__') + self.common.min_window_width = 460 self.onion = onion self.qtapp = qtapp @@ -169,7 +170,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.show() # Adjust window size, to start with a minimum window width - self.adjust_size(450) + self.adjust_size(self.common.min_window_width) # The server isn't active yet self.set_server_active(False) @@ -474,6 +475,7 @@ class OnionShareGui(QtWidgets.QMainWindow): # Adjust sizes of each mode for mode in [self.share_mode, self.receive_mode]: + self.qtapp.processEvents() adjust_size_widget(mode) # Adjust window size diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index 2f61b2ca..6430382b 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -194,7 +194,7 @@ class ReceiveMode(Mode): self.resize_window() def resize_window(self): - min_width = 450 + min_width = self.common.min_window_width if self.uploads.isVisible(): min_width += 300 self.adjust_size.emit(min_width) diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 58801c45..c44e8beb 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -317,7 +317,7 @@ class ShareMode(Mode): self.downloads.reset() def resize_window(self): - min_width = 450 + min_width = self.common.min_window_width if self.downloads.isVisible(): min_width += 300 self.adjust_size.emit(min_width) From 4710eaee4c35b46e4117021bf197a2f9aa07b666 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 29 Sep 2018 18:24:11 -0700 Subject: [PATCH 094/123] Fix local GUI tests so they pass --- tests_gui_local/commontests.py | 47 +++++++++------ .../onionshare_receive_mode_upload_test.py | 12 ++-- ...re_receive_mode_upload_test_public_mode.py | 22 ++++--- .../onionshare_share_mode_download_test.py | 52 ++++++++++------- ...re_share_mode_download_test_public_mode.py | 52 ++++++++++------- ...hare_share_mode_download_test_stay_open.py | 58 +++++++++++-------- .../onionshare_slug_persistent_test.py | 42 ++++++++------ tests_gui_local/onionshare_timer_test.py | 8 +-- 8 files changed, 171 insertions(+), 122 deletions(-) diff --git a/tests_gui_local/commontests.py b/tests_gui_local/commontests.py index de1ad9ab..870c2dbe 100644 --- a/tests_gui_local/commontests.py +++ b/tests_gui_local/commontests.py @@ -24,19 +24,13 @@ class CommonTests(object): '''Test that the status bar is visible''' self.assertTrue(self.gui.status_bar.isVisible()) - def test_info_widget_is_not_visible(self, mode): - '''Test that the info widget along top of screen is not shown''' - if mode == 'receive': - self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) + def test_info_widget_shows_less(self, mode): + '''Test that minimum information (no label) is displayed in the info bar''' if mode == 'share': - self.assertFalse(self.gui.share_mode.info_widget.isVisible()) - - def test_info_widget_is_visible(self, mode): - '''Test that the info widget along top of screen is shown''' + self.assertFalse(self.gui.share_mode.info.label.text() == "") if mode == 'receive': - self.assertTrue(self.gui.receive_mode.info_widget.isVisible()) - if mode == 'share': - self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + # There's no minimal display in receive mode + self.assertTrue(False) def test_click_mode(self, mode): '''Test that we can switch Mode by clicking the button''' @@ -47,14 +41,30 @@ class CommonTests(object): QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton) self.assertTrue(self.gui.mode, self.gui.MODE_SHARE) + def test_click_toggle_history(self, mode): + '''Test that we can toggle Download or Upload history by clicking the toggle button''' + if mode == 'receive': + currently_visible = self.gui.receive_mode.uploads.isVisible() + QtTest.QTest.mouseClick(self.gui.receive_mode.info.toggle_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.receive_mode.uploads.isVisible(), not currently_visible) + if mode == 'share': + currently_visible = self.gui.receive_mode.uploads.isVisible() + QtTest.QTest.mouseClick(self.gui.share_mode.info.toggle_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.share_mode.downloads.isVisible(), not currently_visible) + + def test_history_is_not_visible(self, mode): + '''Test that the History section is not visible''' + if mode == 'receive': + self.assertFalse(self.gui.receive_mode.uploads.isVisible()) + if mode == 'share': + self.assertFalse(self.gui.share_mode.downloads.isVisible()) + def test_history_is_visible(self, mode): - '''Test that the History section is visible and that the relevant widget is present''' + '''Test that the History section is visible''' if mode == 'receive': self.assertTrue(self.gui.receive_mode.uploads.isVisible()) - self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) if mode == 'share': self.assertTrue(self.gui.share_mode.downloads.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) def test_server_working_on_start_button_pressed(self, mode): '''Test we can start the service''' @@ -161,11 +171,11 @@ class CommonTests(object): def test_history_widgets_present(self, mode): '''Test that the relevant widgets are present in the history view after activity has taken place''' if mode == 'receive': - self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) - self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) + self.assertFalse(self.gui.receive_mode.uploads.empty.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.not_empty.isVisible()) if mode == 'share': - self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + self.assertFalse(self.gui.share_mode.downloads.empty.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.not_empty.isVisible()) def test_counter_incremented(self, mode, count): '''Test that the counter has incremented''' @@ -304,4 +314,3 @@ class CommonTests(object): def test_add_button_visible(self): '''Test that the add button should be visible''' self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) - diff --git a/tests_gui_local/onionshare_receive_mode_upload_test.py b/tests_gui_local/onionshare_receive_mode_upload_test.py index 2aa2ed94..b53d5c06 100644 --- a/tests_gui_local/onionshare_receive_mode_upload_test.py +++ b/tests_gui_local/onionshare_receive_mode_upload_test.py @@ -94,15 +94,19 @@ class OnionShareGuiTest(unittest.TestCase): def test_server_status_bar_is_visible(self): CommonTests.test_server_status_bar_is_visible(self) - @pytest.mark.run(order=5) - def test_info_widget_is_not_visible(self): - CommonTests.test_info_widget_is_not_visible(self, 'receive') - @pytest.mark.run(order=6) def test_click_mode(self): CommonTests.test_click_mode(self, 'receive') + @pytest.mark.run(order=6) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'receive') + @pytest.mark.run(order=7) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'receive') + + @pytest.mark.run(order=8) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'receive') diff --git a/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py index 30a290e7..5e5a6b77 100644 --- a/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py +++ b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py @@ -95,34 +95,38 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) - def test_info_widget_is_not_visible(self): - CommonTests.test_info_widget_is_not_visible(self, 'receive') - - @pytest.mark.run(order=6) def test_click_mode(self): CommonTests.test_click_mode(self, 'receive') + @pytest.mark.run(order=6) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'receive') + @pytest.mark.run(order=7) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'receive') + + @pytest.mark.run(order=8) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'receive') - @pytest.mark.run(order=8) + @pytest.mark.run(order=9) def test_server_working_on_start_button_pressed(self): CommonTests.test_server_working_on_start_button_pressed(self, 'receive') - @pytest.mark.run(order=9) + @pytest.mark.run(order=10) def test_server_status_indicator_says_starting(self): CommonTests.test_server_status_indicator_says_starting(self, 'receive') - @pytest.mark.run(order=10) + @pytest.mark.run(order=11) def test_settings_button_is_hidden(self): CommonTests.test_settings_button_is_hidden(self) - @pytest.mark.run(order=11) + @pytest.mark.run(order=12) def test_a_server_is_started(self): CommonTests.test_a_server_is_started(self, 'receive') - @pytest.mark.run(order=12) + @pytest.mark.run(order=13) def test_a_web_server_is_running(self): CommonTests.test_a_web_server_is_running(self) diff --git a/tests_gui_local/onionshare_share_mode_download_test.py b/tests_gui_local/onionshare_share_mode_download_test.py index c546fb61..40df6d98 100644 --- a/tests_gui_local/onionshare_share_mode_download_test.py +++ b/tests_gui_local/onionshare_share_mode_download_test.py @@ -97,90 +97,98 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') + def test_info_widget_shows_less(self): + CommonTests.test_info_widget_shows_less(self, 'share') @pytest.mark.run(order=7) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'share') + + @pytest.mark.run(order=9) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'share') - @pytest.mark.run(order=8) + @pytest.mark.run(order=10) def test_deleting_only_file_hides_delete_button(self): CommonTests.test_deleting_only_file_hides_delete_button(self) - @pytest.mark.run(order=9) + @pytest.mark.run(order=11) def test_add_a_file_and_delete_using_its_delete_widget(self): CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) - @pytest.mark.run(order=10) + @pytest.mark.run(order=12) def test_file_selection_widget_readd_files(self): CommonTests.test_file_selection_widget_readd_files(self) - @pytest.mark.run(order=11) + @pytest.mark.run(order=13) def test_server_working_on_start_button_pressed(self): CommonTests.test_server_working_on_start_button_pressed(self, 'share') - @pytest.mark.run(order=12) + @pytest.mark.run(order=14) def test_server_status_indicator_says_starting(self): CommonTests.test_server_status_indicator_says_starting(self, 'share') - @pytest.mark.run(order=13) + @pytest.mark.run(order=15) def test_add_delete_buttons_hidden(self): CommonTests.test_add_delete_buttons_hidden(self) - @pytest.mark.run(order=14) + @pytest.mark.run(order=16) def test_settings_button_is_hidden(self): CommonTests.test_settings_button_is_hidden(self) - @pytest.mark.run(order=15) + @pytest.mark.run(order=17) def test_a_server_is_started(self): CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=16) + @pytest.mark.run(order=18) def test_a_web_server_is_running(self): CommonTests.test_a_web_server_is_running(self) - @pytest.mark.run(order=17) + @pytest.mark.run(order=19) def test_have_a_slug(self): CommonTests.test_have_a_slug(self, 'share', False) - @pytest.mark.run(order=18) + @pytest.mark.run(order=20) def test_url_description_shown(self): CommonTests.test_url_description_shown(self, 'share') - @pytest.mark.run(order=19) + @pytest.mark.run(order=21) def test_have_copy_url_button(self): CommonTests.test_have_copy_url_button(self, 'share') - @pytest.mark.run(order=20) + @pytest.mark.run(order=22) def test_server_status_indicator_says_started(self): CommonTests.test_server_status_indicator_says_started(self, 'share') - @pytest.mark.run(order=21) + @pytest.mark.run(order=23) def test_web_page(self): CommonTests.test_web_page(self, 'share', 'Total size', False) - @pytest.mark.run(order=22) + @pytest.mark.run(order=24) def test_download_share(self): CommonTests.test_download_share(self, False) - @pytest.mark.run(order=23) + @pytest.mark.run(order=25) def test_history_widgets_present(self): CommonTests.test_history_widgets_present(self, 'share') - @pytest.mark.run(order=24) + @pytest.mark.run(order=26) def test_server_is_stopped(self): CommonTests.test_server_is_stopped(self, 'share', False) - @pytest.mark.run(order=25) + @pytest.mark.run(order=27) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) - @pytest.mark.run(order=26) + @pytest.mark.run(order=28) def test_server_status_indicator_says_closed(self): CommonTests.test_server_status_indicator_says_closed(self, 'share', False) - @pytest.mark.run(order=27) + @pytest.mark.run(order=29) def test_add_button_visible(self): CommonTests.test_add_button_visible(self) diff --git a/tests_gui_local/onionshare_share_mode_download_test_public_mode.py b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py index 764b5885..73d4c999 100644 --- a/tests_gui_local/onionshare_share_mode_download_test_public_mode.py +++ b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py @@ -97,90 +97,98 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') + def test_info_widget_shows_less(self): + CommonTests.test_info_widget_shows_less(self, 'share') @pytest.mark.run(order=7) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'share') + + @pytest.mark.run(order=9) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'share') - @pytest.mark.run(order=8) + @pytest.mark.run(order=10) def test_deleting_only_file_hides_delete_button(self): CommonTests.test_deleting_only_file_hides_delete_button(self) - @pytest.mark.run(order=9) + @pytest.mark.run(order=11) def test_add_a_file_and_delete_using_its_delete_widget(self): CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) - @pytest.mark.run(order=10) + @pytest.mark.run(order=12) def test_file_selection_widget_readd_files(self): CommonTests.test_file_selection_widget_readd_files(self) - @pytest.mark.run(order=11) + @pytest.mark.run(order=13) def test_server_working_on_start_button_pressed(self): CommonTests.test_server_working_on_start_button_pressed(self, 'share') - @pytest.mark.run(order=12) + @pytest.mark.run(order=14) def test_server_status_indicator_says_starting(self): CommonTests.test_server_status_indicator_says_starting(self, 'share') - @pytest.mark.run(order=13) + @pytest.mark.run(order=15) def test_add_delete_buttons_hidden(self): CommonTests.test_add_delete_buttons_hidden(self) - @pytest.mark.run(order=14) + @pytest.mark.run(order=16) def test_settings_button_is_hidden(self): CommonTests.test_settings_button_is_hidden(self) - @pytest.mark.run(order=15) + @pytest.mark.run(order=17) def test_a_server_is_started(self): CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=16) + @pytest.mark.run(order=18) def test_a_web_server_is_running(self): CommonTests.test_a_web_server_is_running(self) - @pytest.mark.run(order=17) + @pytest.mark.run(order=19) def test_have_a_slug(self): CommonTests.test_have_a_slug(self, 'share', True) - @pytest.mark.run(order=18) + @pytest.mark.run(order=20) def test_url_description_shown(self): CommonTests.test_url_description_shown(self, 'share') - @pytest.mark.run(order=19) + @pytest.mark.run(order=21) def test_have_copy_url_button(self): CommonTests.test_have_copy_url_button(self, 'share') - @pytest.mark.run(order=20) + @pytest.mark.run(order=22) def test_server_status_indicator_says_started(self): CommonTests.test_server_status_indicator_says_started(self, 'share') - @pytest.mark.run(order=21) + @pytest.mark.run(order=23) def test_web_page(self): CommonTests.test_web_page(self, 'share', 'Total size', True) - @pytest.mark.run(order=22) + @pytest.mark.run(order=24) def test_download_share(self): CommonTests.test_download_share(self, True) - @pytest.mark.run(order=23) + @pytest.mark.run(order=25) def test_history_widgets_present(self): CommonTests.test_history_widgets_present(self, 'share') - @pytest.mark.run(order=24) + @pytest.mark.run(order=26) def test_server_is_stopped(self): CommonTests.test_server_is_stopped(self, 'share', False) - @pytest.mark.run(order=25) + @pytest.mark.run(order=27) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) - @pytest.mark.run(order=26) + @pytest.mark.run(order=28) def test_server_status_indicator_says_closed(self): CommonTests.test_server_status_indicator_says_closed(self, 'share', False) - @pytest.mark.run(order=27) + @pytest.mark.run(order=29) def test_add_button_visible(self): CommonTests.test_add_button_visible(self) diff --git a/tests_gui_local/onionshare_share_mode_download_test_stay_open.py b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py index b92ff097..e849d224 100644 --- a/tests_gui_local/onionshare_share_mode_download_test_stay_open.py +++ b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py @@ -97,102 +97,110 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') + def test_info_widget_shows_less(self): + CommonTests.test_info_widget_shows_less(self, 'share') @pytest.mark.run(order=7) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'share') + + @pytest.mark.run(order=9) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'share') - @pytest.mark.run(order=8) + @pytest.mark.run(order=10) def test_deleting_only_file_hides_delete_button(self): CommonTests.test_deleting_only_file_hides_delete_button(self) - @pytest.mark.run(order=9) + @pytest.mark.run(order=11) def test_add_a_file_and_delete_using_its_delete_widget(self): CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) - @pytest.mark.run(order=10) + @pytest.mark.run(order=12) def test_file_selection_widget_readd_files(self): CommonTests.test_file_selection_widget_readd_files(self) - @pytest.mark.run(order=11) + @pytest.mark.run(order=13) def test_server_working_on_start_button_pressed(self): CommonTests.test_server_working_on_start_button_pressed(self, 'share') - @pytest.mark.run(order=12) + @pytest.mark.run(order=14) def test_server_status_indicator_says_starting(self): CommonTests.test_server_status_indicator_says_starting(self, 'share') - @pytest.mark.run(order=13) + @pytest.mark.run(order=15) def test_add_delete_buttons_hidden(self): CommonTests.test_add_delete_buttons_hidden(self) - @pytest.mark.run(order=14) + @pytest.mark.run(order=16) def test_settings_button_is_hidden(self): CommonTests.test_settings_button_is_hidden(self) - @pytest.mark.run(order=15) + @pytest.mark.run(order=17) def test_a_server_is_started(self): CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=16) + @pytest.mark.run(order=18) def test_a_web_server_is_running(self): CommonTests.test_a_web_server_is_running(self) - @pytest.mark.run(order=17) + @pytest.mark.run(order=19) def test_have_a_slug(self): CommonTests.test_have_a_slug(self, 'share', True) - @pytest.mark.run(order=18) + @pytest.mark.run(order=20) def test_url_description_shown(self): CommonTests.test_url_description_shown(self, 'share') - @pytest.mark.run(order=19) + @pytest.mark.run(order=21) def test_have_copy_url_button(self): CommonTests.test_have_copy_url_button(self, 'share') - @pytest.mark.run(order=20) + @pytest.mark.run(order=22) def test_server_status_indicator_says_started(self): CommonTests.test_server_status_indicator_says_started(self, 'share') - @pytest.mark.run(order=21) + @pytest.mark.run(order=23) def test_web_page(self): CommonTests.test_web_page(self, 'share', 'Total size', True) - @pytest.mark.run(order=22) + @pytest.mark.run(order=24) def test_download_share(self): CommonTests.test_download_share(self, True) - @pytest.mark.run(order=23) + @pytest.mark.run(order=25) def test_history_widgets_present(self): CommonTests.test_history_widgets_present(self, 'share') - @pytest.mark.run(order=24) + @pytest.mark.run(order=26) def test_counter_incremented(self): CommonTests.test_counter_incremented(self, 'share', 1) - @pytest.mark.run(order=25) + @pytest.mark.run(order=27) def test_download_share_again(self): CommonTests.test_download_share(self, True) - @pytest.mark.run(order=26) + @pytest.mark.run(order=28) def test_counter_incremented_again(self): CommonTests.test_counter_incremented(self, 'share', 2) - @pytest.mark.run(order=27) + @pytest.mark.run(order=29) def test_server_is_stopped(self): CommonTests.test_server_is_stopped(self, 'share', True) - @pytest.mark.run(order=28) + @pytest.mark.run(order=30) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) - @pytest.mark.run(order=29) + @pytest.mark.run(order=31) def test_server_status_indicator_says_closed(self): CommonTests.test_server_status_indicator_says_closed(self, 'share', True) - @pytest.mark.run(order=30) + @pytest.mark.run(order=32) def test_add_button_visible(self): CommonTests.test_add_button_visible(self) diff --git a/tests_gui_local/onionshare_slug_persistent_test.py b/tests_gui_local/onionshare_slug_persistent_test.py index 1e5614dc..5b53f7e0 100644 --- a/tests_gui_local/onionshare_slug_persistent_test.py +++ b/tests_gui_local/onionshare_slug_persistent_test.py @@ -94,68 +94,76 @@ class OnionShareGuiTest(unittest.TestCase): def test_server_status_bar_is_visible(self): CommonTests.test_server_status_bar_is_visible(self) - @pytest.mark.run(order=5) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') - @pytest.mark.run(order=6) + def test_info_widget_shows_less(self): + CommonTests.test_info_widget_shows_less(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'share') + + @pytest.mark.run(order=9) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'share') - @pytest.mark.run(order=7) + @pytest.mark.run(order=10) def test_server_working_on_start_button_pressed(self): CommonTests.test_server_working_on_start_button_pressed(self, 'share') - @pytest.mark.run(order=8) + @pytest.mark.run(order=11) def test_server_status_indicator_says_starting(self): CommonTests.test_server_status_indicator_says_starting(self, 'share') - @pytest.mark.run(order=9) + @pytest.mark.run(order=12) def test_settings_button_is_hidden(self): CommonTests.test_settings_button_is_hidden(self) - @pytest.mark.run(order=10) + @pytest.mark.run(order=13) def test_a_server_is_started(self): CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=11) + @pytest.mark.run(order=14) def test_a_web_server_is_running(self): CommonTests.test_a_web_server_is_running(self) - @pytest.mark.run(order=12) + @pytest.mark.run(order=15) def test_have_a_slug(self): CommonTests.test_have_a_slug(self, 'share', False) global slug slug = self.gui.share_mode.server_status.web.slug - @pytest.mark.run(order=13) + @pytest.mark.run(order=16) def test_server_status_indicator_says_started(self): CommonTests.test_server_status_indicator_says_started(self, 'share') - @pytest.mark.run(order=14) + @pytest.mark.run(order=17) def test_server_is_stopped(self): CommonTests.test_server_is_stopped(self, 'share', True) - @pytest.mark.run(order=15) + @pytest.mark.run(order=18) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) - @pytest.mark.run(order=16) + @pytest.mark.run(order=19) def test_server_status_indicator_says_closed(self): CommonTests.test_server_status_indicator_says_closed(self, 'share', True) - @pytest.mark.run(order=17) + @pytest.mark.run(order=20) def test_server_started_again(self): CommonTests.test_server_working_on_start_button_pressed(self, 'share') CommonTests.test_server_status_indicator_says_starting(self, 'share') CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=18) + @pytest.mark.run(order=21) def test_have_same_slug(self): '''Test that we have the same slug''' self.assertEqual(self.gui.share_mode.server_status.web.slug, slug) - @pytest.mark.run(order=19) + @pytest.mark.run(order=22) def test_server_is_stopped_again(self): CommonTests.test_server_is_stopped(self, 'share', True) CommonTests.test_web_service_is_stopped(self) diff --git a/tests_gui_local/onionshare_timer_test.py b/tests_gui_local/onionshare_timer_test.py index 1a5134e2..701d9a21 100644 --- a/tests_gui_local/onionshare_timer_test.py +++ b/tests_gui_local/onionshare_timer_test.py @@ -97,12 +97,12 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') + def test_info_widget_shows_less(self): + CommonTests.test_info_widget_shows_less(self, 'share') @pytest.mark.run(order=7) - def test_history_is_visible(self): - CommonTests.test_history_is_visible(self, 'share') + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'share') @pytest.mark.run(order=8) def test_set_timeout(self): From d8c225a9c627ffd45c56d96569faaa4f863acb37 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 29 Sep 2018 18:58:27 -0700 Subject: [PATCH 095/123] Begin refactoring Tor tests to inherit from local tests --- tests_gui_local/__init__.py | 8 + tests_gui_tor/commontests.py | 311 +---------------------------------- 2 files changed, 10 insertions(+), 309 deletions(-) diff --git a/tests_gui_local/__init__.py b/tests_gui_local/__init__.py index e69de29b..76e5b1b9 100644 --- a/tests_gui_local/__init__.py +++ b/tests_gui_local/__init__.py @@ -0,0 +1,8 @@ +from .onionshare_receive_mode_upload_test_public_mode import OnionShareGuiTest as ReceiveMoveUploadTestPublicMode +from .onionshare_receive_mode_upload_test import OnionShareGuiTest as ReceiveModeUploadTest +from .onionshare_share_mode_download_test_public_mode import OnionShareGuiTest as ShareModeDownloadTestPublicMode +from .onionshare_share_mode_download_test import OnionShareGuiTest as ShareModeDownloadTest +from .onionshare_share_mode_download_test_stay_open import OnionShareGuiTest as ShareModeDownloadTestStayOpen +from .onionshare_slug_persistent_test import OnionShareGuiTest as SlugPersistentTest +from .onionshare_timer_test import OnionShareGuiTest as TimerTest +from .commontests import CommonTests diff --git a/tests_gui_tor/commontests.py b/tests_gui_tor/commontests.py index a0d9bf5f..ea37279f 100644 --- a/tests_gui_tor/commontests.py +++ b/tests_gui_tor/commontests.py @@ -7,207 +7,13 @@ import zipfile from PyQt5 import QtCore, QtTest from onionshare import strings -class CommonTests(object): - def test_gui_loaded(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) - - def test_windowTitle_seen(self): - '''Test that the window title is OnionShare''' - self.assertEqual(self.gui.windowTitle(), 'OnionShare') - - def test_settings_button_is_visible(self): - '''Test that the settings button is visible''' - self.assertTrue(self.gui.settings_button.isVisible()) - - def test_server_status_bar_is_visible(self): - '''Test that the status bar is visible''' - self.assertTrue(self.gui.status_bar.isVisible()) - - def test_info_widget_is_not_visible(self, mode): - '''Test that the info widget along top of screen is not shown''' - if mode == 'receive': - self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) - if mode == 'share': - self.assertFalse(self.gui.share_mode.info_widget.isVisible()) - - def test_info_widget_is_visible(self, mode): - '''Test that the info widget along top of screen is shown''' - if mode == 'receive': - self.assertTrue(self.gui.receive_mode.info_widget.isVisible()) - if mode == 'share': - self.assertTrue(self.gui.share_mode.info_widget.isVisible()) - - def test_click_mode(self, mode): - '''Test that we can switch Mode by clicking the button''' - if mode == 'receive': - QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) - self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) - if mode == 'share': - QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton) - self.assertTrue(self.gui.mode, self.gui.MODE_SHARE) - - def test_history_is_visible(self, mode): - '''Test that the History section is visible and that the relevant widget is present''' - if mode == 'receive': - self.assertTrue(self.gui.receive_mode.uploads.isVisible()) - self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) - if mode == 'share': - self.assertTrue(self.gui.share_mode.downloads.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) - - def test_server_working_on_start_button_pressed(self, mode): - '''Test we can start the service''' - # Should be in SERVER_WORKING state - if mode == 'receive': - QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEqual(self.gui.receive_mode.server_status.status, 1) - if mode == 'share': - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEqual(self.gui.share_mode.server_status.status, 1) - - def test_server_status_indicator_says_starting(self, mode): - '''Test that the Server Status indicator shows we are Starting''' - if mode == 'receive': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) - if mode == 'share': - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) - - def test_settings_button_is_hidden(self): - '''Test that the settings button is hidden when the server starts''' - self.assertFalse(self.gui.settings_button.isVisible()) - - def test_a_server_is_started(self, mode): - '''Test that the server has started''' - QtTest.QTest.qWait(45000) - # Should now be in SERVER_STARTED state - if mode == 'receive': - self.assertEqual(self.gui.receive_mode.server_status.status, 2) - if mode == 'share': - self.assertEqual(self.gui.share_mode.server_status.status, 2) - - def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) - - def test_have_a_slug(self, mode, public_mode): - '''Test that we have a valid slug''' - if mode == 'receive': - if not public_mode: - self.assertRegex(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') - else: - self.assertIsNone(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') - if mode == 'share': - if not public_mode: - self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') - else: - self.assertIsNone(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') +from tests_gui_local import CommonTests as LocalCommonTests +class CommonTests(LocalCommonTests): def test_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 test_url_description_shown(self, mode): - '''Test that the URL label is showing''' - if mode == 'receive': - self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) - if mode == 'share': - self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) - - def test_have_copy_url_button(self, mode): - '''Test that the Copy URL button is shown''' - if mode == 'receive': - self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) - if mode == 'share': - self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) - - def test_server_status_indicator_says_started(self, mode): - '''Test that the Server Status indicator shows we are started''' - if mode == 'receive': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) - if mode == 'share': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) - - def test_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: - if mode == 'receive': - path = '/{}'.format(self.gui.receive_mode.server_status.web.slug) - if mode == 'share': - path = '/{}'.format(self.gui.share_mode.server_status.web.slug) - else: - path = '/' - - http_request = 'GET {} HTTP/1.0\r\n'.format(path) - http_request += 'Host: {}\r\n'.format(self.gui.app.onion_host) - 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 test_history_widgets_present(self, mode): - '''Test that the relevant widgets are present in the history view after activity has taken place''' - if mode == 'receive': - self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) - self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) - if mode == 'share': - self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) - - def test_counter_incremented(self, mode, count): - '''Test that the counter has incremented''' - if mode == 'receive': - self.assertEquals(self.gui.receive_mode.uploads_completed, count) - if mode == 'share': - self.assertEquals(self.gui.share_mode.downloads_completed, count) - - def test_server_is_stopped(self, mode, stay_open): - '''Test that the server stops when we click Stop''' - if mode == 'receive': - QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.receive_mode.server_status.status, 0) - if mode == 'share': - if stay_open: - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.share_mode.server_status.status, 0) - - def test_web_service_is_stopped(self): - '''Test that the web server also stopped''' - QtTest.QTest.qWait(2000) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) - - def test_server_status_indicator_says_closed(self, mode, stay_open): - '''Test that the Server Status indicator shows we closed''' - if mode == 'receive': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) - if mode == 'share': - if stay_open: - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped', True)) - else: - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) - def test_cancel_the_share(self, mode): '''Test that we can cancel this share before it's started up ''' if mode == 'share': @@ -222,119 +28,6 @@ class CommonTests(object): QtTest.QTest.mouseRelease(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) self.assertEqual(self.gui.receive_mode.server_status.status, 0) - - # Auto-stop timer tests - def test_set_timeout(self, mode, timeout): - '''Test that the timeout can be set''' - timer = QtCore.QDateTime.currentDateTime().addSecs(timeout) - if mode == 'receive': - self.gui.receive_mode.server_status.shutdown_timeout.setDateTime(timer) - self.assertTrue(self.gui.receive_mode.server_status.shutdown_timeout.dateTime(), timer) - if mode == 'share': - self.gui.share_mode.server_status.shutdown_timeout.setDateTime(timer) - self.assertTrue(self.gui.share_mode.server_status.shutdown_timeout.dateTime(), timer) - - def test_timeout_widget_hidden(self, mode): - '''Test that the timeout widget is hidden when share has started''' - if mode == 'receive': - self.assertFalse(self.gui.receive_mode.server_status.shutdown_timeout_container.isVisible()) - if mode == 'share': - self.assertFalse(self.gui.share_mode.server_status.shutdown_timeout_container.isVisible()) - - def test_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 - if mode == 'receive': - self.assertEqual(self.gui.receive_mode.server_status.status, 0) - if mode == 'share': - self.assertEqual(self.gui.share_mode.server_status.status, 0) - - # Receive-specific tests - def test_upload_file(self, public_mode, 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'] = 'socks5h://{}:{}'.format(socks_address, socks_port) - - files = {'file[]': open('/tmp/test.txt', 'rb')} - if not public_mode: - path = 'http://{}/{}/upload'.format(self.gui.app.onion_host, self.gui.receive_mode.web.slug) - else: - path = 'http://{}/upload'.format(self.gui.app.onion_host) - response = session.post(path, files=files) - QtTest.QTest.qWait(4000) - self.assertTrue(os.path.isfile(expected_file)) - - # Share-specific tests - def test_file_selection_widget_has_a_file(self): - '''Test that the number of files in the list is 1''' - self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) - - def test_deleting_only_file_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, and since there's no more files, the delete button should be hidden - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) - self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) - - def test_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.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) - - def test_file_selection_widget_readd_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.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) - - def test_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 test_download_share(self, public_mode): - '''Test that we can download the share''' - (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 public_mode: - path = '/download' - else: - path = '{}/download'.format(self.gui.share_mode.web.slug) - - http_request = 'GET {} HTTP/1.0\r\n'.format(path) - http_request += 'Host: {}\r\n'.format(self.gui.app.onion_host) - http_request += '\r\n' - s.sendall(http_request.encode('utf-8')) - - with open('/tmp/download.zip', 'wb') as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - - zip = zipfile.ZipFile('/tmp/download.zip') - QtTest.QTest.qWait(4000) - self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) - - def test_add_button_visible(self): - '''Test that the add button should be visible''' - self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) - - # Stealth tests def test_copy_have_hidserv_auth_button(self, mode): '''Test that the Copy HidservAuth button is shown''' From 4ffc0ddb82a4232d2bbb3da7dbc5b12632cd2798 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 30 Sep 2018 16:52:48 +1000 Subject: [PATCH 096/123] Ignore attribute error when optimistically trying to cancel compression (we may have no ZipWriter object yet) --- onionshare_gui/share_mode/threads.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py index d6022746..24e2c242 100644 --- a/onionshare_gui/share_mode/threads.py +++ b/onionshare_gui/share_mode/threads.py @@ -56,5 +56,8 @@ class CompressThread(QtCore.QThread): # Let the Web and ZipWriter objects know that we're canceling compression early self.mode.web.cancel_compression = True - if self.mode.web.zip_writer: + try: self.mode.web.zip_writer.cancel_compression = True + except AttributeError: + # we never made it as far as creating a ZipWriter object + pass From 10ca75fc91d24810a3d0685feacec63320080586 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 30 Sep 2018 17:16:37 +1000 Subject: [PATCH 097/123] Add a test for #790 --- ...onshare_790_cancel_on_second_share_test.py | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 tests_gui_tor/onionshare_790_cancel_on_second_share_test.py diff --git a/tests_gui_tor/onionshare_790_cancel_on_second_share_test.py b/tests_gui_tor/onionshare_790_cancel_on_second_share_test.py new file mode 100644 index 00000000..731de4fd --- /dev/null +++ b/tests_gui_tor/onionshare_790_cancel_on_second_share_test.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', False) + + @pytest.mark.run(order=18) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=22) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', True) + + @pytest.mark.run(order=23) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=24) + def test_server_working_on_start_button_pressed_round2(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=25) + def test_server_status_indicator_says_starting_round2(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=26) + def test_cancel_the_share(self): + CommonTests.test_cancel_the_share(self, 'share') + + @pytest.mark.run(order=27) + def test_server_is_stopped_round2(self): + CommonTests.test_server_is_stopped(self, 'share', False) + + @pytest.mark.run(order=28) + def test_web_service_is_stopped_round2(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=29) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) + + +if __name__ == "__main__": + unittest.main() From 84de1b51a0d777fb5fe3aa28bc05c1e79a92d4d8 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 30 Sep 2018 17:43:45 +1000 Subject: [PATCH 098/123] Replace deprecated assertEquals with assertEqual in tests --- tests_gui_local/commontests.py | 26 +++++++++++++------------- tests_gui_tor/commontests.py | 26 +++++++++++++------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/tests_gui_local/commontests.py b/tests_gui_local/commontests.py index de1ad9ab..e67a91f2 100644 --- a/tests_gui_local/commontests.py +++ b/tests_gui_local/commontests.py @@ -69,9 +69,9 @@ class CommonTests(object): def test_server_status_indicator_says_starting(self, mode): '''Test that the Server Status indicator shows we are Starting''' if mode == 'receive': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + self.assertEqual(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) if mode == 'share': - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) def test_settings_button_is_hidden(self): '''Test that the settings button is hidden when the server starts''' @@ -123,9 +123,9 @@ class CommonTests(object): def test_server_status_indicator_says_started(self, mode): '''Test that the Server Status indicator shows we are started''' if mode == 'receive': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + self.assertEqual(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) if mode == 'share': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + self.assertEqual(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) def test_web_page(self, mode, string, public_mode): '''Test that the web page contains a string''' @@ -170,19 +170,19 @@ class CommonTests(object): def test_counter_incremented(self, mode, count): '''Test that the counter has incremented''' if mode == 'receive': - self.assertEquals(self.gui.receive_mode.uploads_completed, count) + self.assertEqual(self.gui.receive_mode.uploads_completed, count) if mode == 'share': - self.assertEquals(self.gui.share_mode.downloads_completed, count) + self.assertEqual(self.gui.share_mode.downloads_completed, count) def test_server_is_stopped(self, mode, stay_open): '''Test that the server stops when we click Stop''' if mode == 'receive': QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.receive_mode.server_status.status, 0) + self.assertEqual(self.gui.receive_mode.server_status.status, 0) if mode == 'share': if stay_open: QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.share_mode.server_status.status, 0) + self.assertEqual(self.gui.share_mode.server_status.status, 0) def test_web_service_is_stopped(self): '''Test that the web server also stopped''' @@ -195,12 +195,12 @@ class CommonTests(object): def test_server_status_indicator_says_closed(self, mode, stay_open): '''Test that the Server Status indicator shows we closed''' if mode == 'receive': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + self.assertEqual(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) if mode == 'share': if stay_open: - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped', True)) + self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped', True)) else: - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) + self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) # Auto-stop timer tests def test_set_timeout(self, mode, timeout): @@ -260,7 +260,7 @@ class CommonTests(object): '''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.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) def test_file_selection_widget_readd_files(self): '''Re-add some files to the list so we can share''' @@ -299,7 +299,7 @@ class CommonTests(object): zip = zipfile.ZipFile('/tmp/download.zip') QtTest.QTest.qWait(2000) - self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8')) def test_add_button_visible(self): '''Test that the add button should be visible''' diff --git a/tests_gui_tor/commontests.py b/tests_gui_tor/commontests.py index a0d9bf5f..f58f0504 100644 --- a/tests_gui_tor/commontests.py +++ b/tests_gui_tor/commontests.py @@ -69,9 +69,9 @@ class CommonTests(object): def test_server_status_indicator_says_starting(self, mode): '''Test that the Server Status indicator shows we are Starting''' if mode == 'receive': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + self.assertEqual(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) if mode == 'share': - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) def test_settings_button_is_hidden(self): '''Test that the settings button is hidden when the server starts''' @@ -126,9 +126,9 @@ class CommonTests(object): def test_server_status_indicator_says_started(self, mode): '''Test that the Server Status indicator shows we are started''' if mode == 'receive': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + self.assertEqual(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) if mode == 'share': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + self.assertEqual(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) def test_web_page(self, mode, string, public_mode): '''Test that the web page contains a string''' @@ -176,19 +176,19 @@ class CommonTests(object): def test_counter_incremented(self, mode, count): '''Test that the counter has incremented''' if mode == 'receive': - self.assertEquals(self.gui.receive_mode.uploads_completed, count) + self.assertEqual(self.gui.receive_mode.uploads_completed, count) if mode == 'share': - self.assertEquals(self.gui.share_mode.downloads_completed, count) + self.assertEqual(self.gui.share_mode.downloads_completed, count) def test_server_is_stopped(self, mode, stay_open): '''Test that the server stops when we click Stop''' if mode == 'receive': QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.receive_mode.server_status.status, 0) + self.assertEqual(self.gui.receive_mode.server_status.status, 0) if mode == 'share': if stay_open: QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.share_mode.server_status.status, 0) + self.assertEqual(self.gui.share_mode.server_status.status, 0) def test_web_service_is_stopped(self): '''Test that the web server also stopped''' @@ -201,12 +201,12 @@ class CommonTests(object): def test_server_status_indicator_says_closed(self, mode, stay_open): '''Test that the Server Status indicator shows we closed''' if mode == 'receive': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + self.assertEqual(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) if mode == 'share': if stay_open: - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped', True)) + self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped', True)) else: - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) + self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) def test_cancel_the_share(self, mode): '''Test that we can cancel this share before it's started up ''' @@ -286,7 +286,7 @@ class CommonTests(object): '''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.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) def test_file_selection_widget_readd_files(self): '''Re-add some files to the list so we can share''' @@ -328,7 +328,7 @@ class CommonTests(object): zip = zipfile.ZipFile('/tmp/download.zip') QtTest.QTest.qWait(4000) - self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8')) def test_add_button_visible(self): '''Test that the add button should be visible''' From f5c7acf8f260679a9fec4b9772e8d8bb8d8d41e4 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 30 Sep 2018 10:57:13 -0700 Subject: [PATCH 099/123] Fix Tor tests so they pass, too --- tests_gui_local/__init__.py | 7 --- tests_gui_tor/commontests.py | 9 +++ .../onionshare_receive_mode_upload_test.py | 36 ++++++----- ...re_receive_mode_upload_test_public_mode.py | 46 +++++++------- ...onionshare_share_mode_cancel_share_test.py | 8 --- .../onionshare_share_mode_download_test.py | 16 ++--- ...re_share_mode_download_test_public_mode.py | 54 ++++++++++------- ...hare_share_mode_download_test_stay_open.py | 60 +++++++++++-------- .../onionshare_share_mode_persistent_test.py | 46 ++++++++------ .../onionshare_share_mode_stealth_test.py | 44 ++++++++------ ...e_share_mode_tor_connection_killed_test.py | 46 ++++++++------ tests_gui_tor/onionshare_timer_test.py | 28 +++++---- .../onionshare_tor_connection_killed_test.py | 46 ++++++++------ 13 files changed, 252 insertions(+), 194 deletions(-) diff --git a/tests_gui_local/__init__.py b/tests_gui_local/__init__.py index 76e5b1b9..bb2b2182 100644 --- a/tests_gui_local/__init__.py +++ b/tests_gui_local/__init__.py @@ -1,8 +1 @@ -from .onionshare_receive_mode_upload_test_public_mode import OnionShareGuiTest as ReceiveMoveUploadTestPublicMode -from .onionshare_receive_mode_upload_test import OnionShareGuiTest as ReceiveModeUploadTest -from .onionshare_share_mode_download_test_public_mode import OnionShareGuiTest as ShareModeDownloadTestPublicMode -from .onionshare_share_mode_download_test import OnionShareGuiTest as ShareModeDownloadTest -from .onionshare_share_mode_download_test_stay_open import OnionShareGuiTest as ShareModeDownloadTestStayOpen -from .onionshare_slug_persistent_test import OnionShareGuiTest as SlugPersistentTest -from .onionshare_timer_test import OnionShareGuiTest as TimerTest from .commontests import CommonTests diff --git a/tests_gui_tor/commontests.py b/tests_gui_tor/commontests.py index ea37279f..a1e420fd 100644 --- a/tests_gui_tor/commontests.py +++ b/tests_gui_tor/commontests.py @@ -10,6 +10,15 @@ from onionshare import strings from tests_gui_local import CommonTests as LocalCommonTests class CommonTests(LocalCommonTests): + def test_a_server_is_started(self, mode): + '''Test that the server has started (overriding from local tests to wait for longer)''' + QtTest.QTest.qWait(45000) + # Should now be in SERVER_STARTED state + if mode == 'receive': + self.assertEqual(self.gui.receive_mode.server_status.status, 2) + if mode == 'share': + self.assertEqual(self.gui.share_mode.server_status.status, 2) + def test_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') diff --git a/tests_gui_tor/onionshare_receive_mode_upload_test.py b/tests_gui_tor/onionshare_receive_mode_upload_test.py index 5be400e2..7c340037 100644 --- a/tests_gui_tor/onionshare_receive_mode_upload_test.py +++ b/tests_gui_tor/onionshare_receive_mode_upload_test.py @@ -94,15 +94,19 @@ class OnionShareGuiTest(unittest.TestCase): def test_server_status_bar_is_visible(self): CommonTests.test_server_status_bar_is_visible(self) - @pytest.mark.run(order=5) - def test_info_widget_is_not_visible(self): - CommonTests.test_info_widget_is_not_visible(self, 'receive') - @pytest.mark.run(order=6) def test_click_mode(self): CommonTests.test_click_mode(self, 'receive') + @pytest.mark.run(order=6) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'receive') + @pytest.mark.run(order=7) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'receive') + + @pytest.mark.run(order=8) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'receive') @@ -134,51 +138,51 @@ class OnionShareGuiTest(unittest.TestCase): def test_have_an_onion(self): CommonTests.test_have_an_onion_service(self) - @pytest.mark.run(order=16) + @pytest.mark.run(order=20) def test_url_description_shown(self): CommonTests.test_url_description_shown(self, 'receive') - @pytest.mark.run(order=17) + @pytest.mark.run(order=21) def test_have_copy_url_button(self): CommonTests.test_have_copy_url_button(self, 'receive') - @pytest.mark.run(order=18) + @pytest.mark.run(order=22) def test_server_status_indicator_says_started(self): CommonTests.test_server_status_indicator_says_started(self, 'receive') - @pytest.mark.run(order=19) + @pytest.mark.run(order=23) def test_web_page(self): CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', False) - @pytest.mark.run(order=20) + @pytest.mark.run(order=24) def test_upload_file(self): CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test.txt') - @pytest.mark.run(order=21) + @pytest.mark.run(order=25) def test_history_widgets_present(self): CommonTests.test_history_widgets_present(self, 'receive') - @pytest.mark.run(order=22) + @pytest.mark.run(order=26) def test_counter_incremented(self): CommonTests.test_counter_incremented(self, 'receive', 1) - @pytest.mark.run(order=23) + @pytest.mark.run(order=27) def test_upload_same_file_is_renamed(self): CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test-2.txt') - @pytest.mark.run(order=24) + @pytest.mark.run(order=28) def test_upload_count_incremented_again(self): CommonTests.test_counter_incremented(self, 'receive', 2) - @pytest.mark.run(order=25) + @pytest.mark.run(order=29) def test_server_is_stopped(self): CommonTests.test_server_is_stopped(self, 'receive', False) - @pytest.mark.run(order=26) + @pytest.mark.run(order=30) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) - @pytest.mark.run(order=27) + @pytest.mark.run(order=31) def test_server_status_indicator_says_closed(self): CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) diff --git a/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py index 9c9553a4..65bf5c89 100644 --- a/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py +++ b/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py @@ -95,34 +95,38 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) - def test_info_widget_is_not_visible(self): - CommonTests.test_info_widget_is_not_visible(self, 'receive') - - @pytest.mark.run(order=6) def test_click_mode(self): CommonTests.test_click_mode(self, 'receive') + @pytest.mark.run(order=6) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'receive') + @pytest.mark.run(order=7) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'receive') + + @pytest.mark.run(order=8) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'receive') - @pytest.mark.run(order=8) + @pytest.mark.run(order=9) def test_server_working_on_start_button_pressed(self): CommonTests.test_server_working_on_start_button_pressed(self, 'receive') - @pytest.mark.run(order=9) + @pytest.mark.run(order=10) def test_server_status_indicator_says_starting(self): CommonTests.test_server_status_indicator_says_starting(self, 'receive') - @pytest.mark.run(order=10) + @pytest.mark.run(order=11) def test_settings_button_is_hidden(self): CommonTests.test_settings_button_is_hidden(self) - @pytest.mark.run(order=11) + @pytest.mark.run(order=12) def test_a_server_is_started(self): CommonTests.test_a_server_is_started(self, 'receive') - @pytest.mark.run(order=12) + @pytest.mark.run(order=13) def test_a_web_server_is_running(self): CommonTests.test_a_web_server_is_running(self) @@ -134,51 +138,51 @@ class OnionShareGuiTest(unittest.TestCase): def test_have_an_onion(self): CommonTests.test_have_an_onion_service(self) - @pytest.mark.run(order=16) + @pytest.mark.run(order=20) def test_url_description_shown(self): CommonTests.test_url_description_shown(self, 'receive') - @pytest.mark.run(order=17) + @pytest.mark.run(order=21) def test_have_copy_url_button(self): CommonTests.test_have_copy_url_button(self, 'receive') - @pytest.mark.run(order=18) + @pytest.mark.run(order=22) def test_server_status_indicator_says_started(self): CommonTests.test_server_status_indicator_says_started(self, 'receive') - @pytest.mark.run(order=19) + @pytest.mark.run(order=23) def test_web_page(self): CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', True) - @pytest.mark.run(order=20) + @pytest.mark.run(order=24) def test_upload_file(self): CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test.txt') - @pytest.mark.run(order=21) + @pytest.mark.run(order=25) def test_history_widgets_present(self): CommonTests.test_history_widgets_present(self, 'receive') - @pytest.mark.run(order=22) + @pytest.mark.run(order=26) def test_counter_incremented(self): CommonTests.test_counter_incremented(self, 'receive', 1) - @pytest.mark.run(order=23) + @pytest.mark.run(order=27) def test_upload_same_file_is_renamed(self): CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test-2.txt') - @pytest.mark.run(order=24) + @pytest.mark.run(order=28) def test_upload_count_incremented_again(self): CommonTests.test_counter_incremented(self, 'receive', 2) - @pytest.mark.run(order=25) + @pytest.mark.run(order=29) def test_server_is_stopped(self): CommonTests.test_server_is_stopped(self, 'receive', False) - @pytest.mark.run(order=26) + @pytest.mark.run(order=30) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) - @pytest.mark.run(order=27) + @pytest.mark.run(order=31) def test_server_status_indicator_says_closed(self): CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) diff --git a/tests_gui_tor/onionshare_share_mode_cancel_share_test.py b/tests_gui_tor/onionshare_share_mode_cancel_share_test.py index 466109d7..cdab8f85 100644 --- a/tests_gui_tor/onionshare_share_mode_cancel_share_test.py +++ b/tests_gui_tor/onionshare_share_mode_cancel_share_test.py @@ -96,14 +96,6 @@ class OnionShareGuiTest(unittest.TestCase): def test_file_selection_widget_has_a_file(self): CommonTests.test_file_selection_widget_has_a_file(self) - @pytest.mark.run(order=6) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') - - @pytest.mark.run(order=7) - def test_history_is_visible(self): - CommonTests.test_history_is_visible(self, 'share') - @pytest.mark.run(order=8) def test_deleting_only_file_hides_delete_button(self): CommonTests.test_deleting_only_file_hides_delete_button(self) diff --git a/tests_gui_tor/onionshare_share_mode_download_test.py b/tests_gui_tor/onionshare_share_mode_download_test.py index 1c8e1b6c..2bf26690 100644 --- a/tests_gui_tor/onionshare_share_mode_download_test.py +++ b/tests_gui_tor/onionshare_share_mode_download_test.py @@ -97,20 +97,20 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') + def test_info_widget_shows_less(self): + CommonTests.test_info_widget_shows_less(self, 'share') @pytest.mark.run(order=7) - def test_history_is_visible(self): - CommonTests.test_history_is_visible(self, 'share') + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'share') @pytest.mark.run(order=8) - def test_deleting_only_file_hides_delete_button(self): - CommonTests.test_deleting_only_file_hides_delete_button(self) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'share') @pytest.mark.run(order=9) - def test_add_a_file_and_delete_using_its_delete_widget(self): - CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') @pytest.mark.run(order=10) def test_file_selection_widget_readd_files(self): diff --git a/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py b/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py index c292e729..4792994d 100644 --- a/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py +++ b/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py @@ -97,94 +97,102 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') + def test_info_widget_shows_less(self): + CommonTests.test_info_widget_shows_less(self, 'share') @pytest.mark.run(order=7) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'share') + + @pytest.mark.run(order=9) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'share') - @pytest.mark.run(order=8) + @pytest.mark.run(order=10) def test_deleting_only_file_hides_delete_button(self): CommonTests.test_deleting_only_file_hides_delete_button(self) - @pytest.mark.run(order=9) + @pytest.mark.run(order=11) def test_add_a_file_and_delete_using_its_delete_widget(self): CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) - @pytest.mark.run(order=10) + @pytest.mark.run(order=12) def test_file_selection_widget_readd_files(self): CommonTests.test_file_selection_widget_readd_files(self) - @pytest.mark.run(order=11) + @pytest.mark.run(order=13) def test_server_working_on_start_button_pressed(self): CommonTests.test_server_working_on_start_button_pressed(self, 'share') - @pytest.mark.run(order=12) + @pytest.mark.run(order=14) def test_server_status_indicator_says_starting(self): CommonTests.test_server_status_indicator_says_starting(self, 'share') - @pytest.mark.run(order=13) + @pytest.mark.run(order=15) def test_add_delete_buttons_hidden(self): CommonTests.test_add_delete_buttons_hidden(self) - @pytest.mark.run(order=14) + @pytest.mark.run(order=16) def test_settings_button_is_hidden(self): CommonTests.test_settings_button_is_hidden(self) - @pytest.mark.run(order=15) + @pytest.mark.run(order=17) def test_a_server_is_started(self): CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=16) + @pytest.mark.run(order=18) def test_a_web_server_is_running(self): CommonTests.test_a_web_server_is_running(self) - @pytest.mark.run(order=17) + @pytest.mark.run(order=19) def test_have_a_slug(self): CommonTests.test_have_a_slug(self, 'share', True) - @pytest.mark.run(order=18) + @pytest.mark.run(order=20) def test_have_an_onion(self): CommonTests.test_have_an_onion_service(self) - @pytest.mark.run(order=19) + @pytest.mark.run(order=21) def test_url_description_shown(self): CommonTests.test_url_description_shown(self, 'share') - @pytest.mark.run(order=20) + @pytest.mark.run(order=22) def test_have_copy_url_button(self): CommonTests.test_have_copy_url_button(self, 'share') - @pytest.mark.run(order=21) + @pytest.mark.run(order=23) def test_server_status_indicator_says_started(self): CommonTests.test_server_status_indicator_says_started(self, 'share') - @pytest.mark.run(order=22) + @pytest.mark.run(order=24) def test_web_page(self): CommonTests.test_web_page(self, 'share', 'Total size', True) - @pytest.mark.run(order=23) + @pytest.mark.run(order=25) def test_download_share(self): CommonTests.test_download_share(self, True) - @pytest.mark.run(order=24) + @pytest.mark.run(order=26) def test_history_widgets_present(self): CommonTests.test_history_widgets_present(self, 'share') - @pytest.mark.run(order=25) + @pytest.mark.run(order=27) def test_server_is_stopped(self): CommonTests.test_server_is_stopped(self, 'share', False) - @pytest.mark.run(order=26) + @pytest.mark.run(order=28) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) - @pytest.mark.run(order=27) + @pytest.mark.run(order=29) def test_server_status_indicator_says_closed(self): CommonTests.test_server_status_indicator_says_closed(self, 'share', False) - @pytest.mark.run(order=28) + @pytest.mark.run(order=30) def test_add_button_visible(self): CommonTests.test_add_button_visible(self) diff --git a/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py b/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py index 7838316f..92d52169 100644 --- a/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py +++ b/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py @@ -97,106 +97,114 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') + def test_info_widget_shows_less(self): + CommonTests.test_info_widget_shows_less(self, 'share') @pytest.mark.run(order=7) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'share') + + @pytest.mark.run(order=9) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'share') - @pytest.mark.run(order=8) + @pytest.mark.run(order=10) def test_deleting_only_file_hides_delete_button(self): CommonTests.test_deleting_only_file_hides_delete_button(self) - @pytest.mark.run(order=9) + @pytest.mark.run(order=11) def test_add_a_file_and_delete_using_its_delete_widget(self): CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) - @pytest.mark.run(order=10) + @pytest.mark.run(order=12) def test_file_selection_widget_readd_files(self): CommonTests.test_file_selection_widget_readd_files(self) - @pytest.mark.run(order=11) + @pytest.mark.run(order=13) def test_server_working_on_start_button_pressed(self): CommonTests.test_server_working_on_start_button_pressed(self, 'share') - @pytest.mark.run(order=12) + @pytest.mark.run(order=14) def test_server_status_indicator_says_starting(self): CommonTests.test_server_status_indicator_says_starting(self, 'share') - @pytest.mark.run(order=13) + @pytest.mark.run(order=15) def test_add_delete_buttons_hidden(self): CommonTests.test_add_delete_buttons_hidden(self) - @pytest.mark.run(order=14) + @pytest.mark.run(order=16) def test_settings_button_is_hidden(self): CommonTests.test_settings_button_is_hidden(self) - @pytest.mark.run(order=15) + @pytest.mark.run(order=17) def test_a_server_is_started(self): CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=16) + @pytest.mark.run(order=18) def test_a_web_server_is_running(self): CommonTests.test_a_web_server_is_running(self) - @pytest.mark.run(order=17) + @pytest.mark.run(order=19) def test_have_a_slug(self): CommonTests.test_have_a_slug(self, 'share', True) - @pytest.mark.run(order=18) + @pytest.mark.run(order=20) def test_have_an_onion(self): CommonTests.test_have_an_onion_service(self) - @pytest.mark.run(order=19) + @pytest.mark.run(order=21) def test_url_description_shown(self): CommonTests.test_url_description_shown(self, 'share') - @pytest.mark.run(order=20) + @pytest.mark.run(order=22) def test_have_copy_url_button(self): CommonTests.test_have_copy_url_button(self, 'share') - @pytest.mark.run(order=21) + @pytest.mark.run(order=23) def test_server_status_indicator_says_started(self): CommonTests.test_server_status_indicator_says_started(self, 'share') - @pytest.mark.run(order=22) + @pytest.mark.run(order=24) def test_web_page(self): CommonTests.test_web_page(self, 'share', 'Total size', True) - @pytest.mark.run(order=23) + @pytest.mark.run(order=25) def test_download_share(self): CommonTests.test_download_share(self, True) - @pytest.mark.run(order=24) + @pytest.mark.run(order=26) def test_history_widgets_present(self): CommonTests.test_history_widgets_present(self, 'share') - @pytest.mark.run(order=25) + @pytest.mark.run(order=27) def test_counter_incremented(self): CommonTests.test_counter_incremented(self, 'share', 1) - @pytest.mark.run(order=26) + @pytest.mark.run(order=28) def test_download_share_again(self): CommonTests.test_download_share(self, True) - @pytest.mark.run(order=27) + @pytest.mark.run(order=29) def test_counter_incremented_again(self): CommonTests.test_counter_incremented(self, 'share', 2) - @pytest.mark.run(order=28) + @pytest.mark.run(order=30) def test_server_is_stopped(self): CommonTests.test_server_is_stopped(self, 'share', True) - @pytest.mark.run(order=29) + @pytest.mark.run(order=31) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) - @pytest.mark.run(order=30) + @pytest.mark.run(order=32) def test_server_status_indicator_says_closed(self): CommonTests.test_server_status_indicator_says_closed(self, 'share', True) - @pytest.mark.run(order=31) + @pytest.mark.run(order=33) def test_add_button_visible(self): CommonTests.test_add_button_visible(self) diff --git a/tests_gui_tor/onionshare_share_mode_persistent_test.py b/tests_gui_tor/onionshare_share_mode_persistent_test.py index 3cffaab6..6b9fbe16 100644 --- a/tests_gui_tor/onionshare_share_mode_persistent_test.py +++ b/tests_gui_tor/onionshare_share_mode_persistent_test.py @@ -95,79 +95,87 @@ class OnionShareGuiTest(unittest.TestCase): def test_server_status_bar_is_visible(self): CommonTests.test_server_status_bar_is_visible(self) - @pytest.mark.run(order=5) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') - @pytest.mark.run(order=6) + def test_info_widget_shows_less(self): + CommonTests.test_info_widget_shows_less(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'share') + + @pytest.mark.run(order=9) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'share') - @pytest.mark.run(order=7) + @pytest.mark.run(order=10) def test_server_working_on_start_button_pressed(self): CommonTests.test_server_working_on_start_button_pressed(self, 'share') - @pytest.mark.run(order=8) + @pytest.mark.run(order=11) def test_server_status_indicator_says_starting(self): CommonTests.test_server_status_indicator_says_starting(self, 'share') - @pytest.mark.run(order=9) + @pytest.mark.run(order=12) def test_settings_button_is_hidden(self): CommonTests.test_settings_button_is_hidden(self) - @pytest.mark.run(order=10) + @pytest.mark.run(order=13) def test_a_server_is_started(self): CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=11) + @pytest.mark.run(order=14) def test_a_web_server_is_running(self): CommonTests.test_a_web_server_is_running(self) - @pytest.mark.run(order=12) + @pytest.mark.run(order=15) def test_have_a_slug(self): CommonTests.test_have_a_slug(self, 'share', False) global slug slug = self.gui.share_mode.server_status.web.slug - @pytest.mark.run(order=13) + @pytest.mark.run(order=16) def test_have_an_onion(self): CommonTests.test_have_an_onion_service(self) global onion_host onion_host = self.gui.app.onion_host - @pytest.mark.run(order=14) + @pytest.mark.run(order=17) def test_server_status_indicator_says_started(self): CommonTests.test_server_status_indicator_says_started(self, 'share') - @pytest.mark.run(order=15) + @pytest.mark.run(order=18) def test_server_is_stopped(self): CommonTests.test_server_is_stopped(self, 'share', True) - @pytest.mark.run(order=16) + @pytest.mark.run(order=19) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) - @pytest.mark.run(order=17) + @pytest.mark.run(order=20) def test_server_status_indicator_says_closed(self): CommonTests.test_server_status_indicator_says_closed(self, 'share', True) - @pytest.mark.run(order=18) + @pytest.mark.run(order=21) def test_server_started_again(self): CommonTests.test_server_working_on_start_button_pressed(self, 'share') CommonTests.test_server_status_indicator_says_starting(self, 'share') CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=19) + @pytest.mark.run(order=22) def test_have_same_slug(self): '''Test that we have the same slug''' self.assertEqual(self.gui.share_mode.server_status.web.slug, slug) - @pytest.mark.run(order=20) + @pytest.mark.run(order=23) def test_have_same_onion(self): '''Test that we have the same onion''' self.assertEqual(self.gui.app.onion_host, onion_host) - @pytest.mark.run(order=21) + @pytest.mark.run(order=24) def test_server_is_stopped_again(self): CommonTests.test_server_is_stopped(self, 'share', True) CommonTests.test_web_service_is_stopped(self) diff --git a/tests_gui_tor/onionshare_share_mode_stealth_test.py b/tests_gui_tor/onionshare_share_mode_stealth_test.py index aaf6fbc6..876efde2 100644 --- a/tests_gui_tor/onionshare_share_mode_stealth_test.py +++ b/tests_gui_tor/onionshare_share_mode_stealth_test.py @@ -97,74 +97,82 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') + def test_info_widget_shows_less(self): + CommonTests.test_info_widget_shows_less(self, 'share') @pytest.mark.run(order=7) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'share') + + @pytest.mark.run(order=9) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'share') - @pytest.mark.run(order=8) + @pytest.mark.run(order=10) def test_deleting_only_file_hides_delete_button(self): CommonTests.test_deleting_only_file_hides_delete_button(self) - @pytest.mark.run(order=9) + @pytest.mark.run(order=11) def test_add_a_file_and_delete_using_its_delete_widget(self): CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) - @pytest.mark.run(order=10) + @pytest.mark.run(order=12) def test_file_selection_widget_readd_files(self): CommonTests.test_file_selection_widget_readd_files(self) - @pytest.mark.run(order=11) + @pytest.mark.run(order=13) def test_server_working_on_start_button_pressed(self): CommonTests.test_server_working_on_start_button_pressed(self, 'share') - @pytest.mark.run(order=12) + @pytest.mark.run(order=14) def test_server_status_indicator_says_starting(self): CommonTests.test_server_status_indicator_says_starting(self, 'share') - @pytest.mark.run(order=13) + @pytest.mark.run(order=15) def test_add_delete_buttons_hidden(self): CommonTests.test_add_delete_buttons_hidden(self) - @pytest.mark.run(order=14) + @pytest.mark.run(order=16) def test_settings_button_is_hidden(self): CommonTests.test_settings_button_is_hidden(self) - @pytest.mark.run(order=15) + @pytest.mark.run(order=17) def test_a_server_is_started(self): CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=16) + @pytest.mark.run(order=18) def test_a_web_server_is_running(self): CommonTests.test_a_web_server_is_running(self) - @pytest.mark.run(order=17) + @pytest.mark.run(order=19) def test_have_a_slug(self): CommonTests.test_have_a_slug(self, 'share', False) - @pytest.mark.run(order=18) + @pytest.mark.run(order=20) def test_have_an_onion(self): CommonTests.test_have_an_onion_service(self) - @pytest.mark.run(order=19) + @pytest.mark.run(order=21) def test_url_description_shown(self): CommonTests.test_url_description_shown(self, 'share') - @pytest.mark.run(order=20) + @pytest.mark.run(order=22) def test_have_copy_url_button(self): CommonTests.test_have_copy_url_button(self, 'share') - @pytest.mark.run(order=21) + @pytest.mark.run(order=23) def test_server_status_indicator_says_started(self): CommonTests.test_server_status_indicator_says_started(self, 'share') - @pytest.mark.run(order=22) + @pytest.mark.run(order=24) def test_copy_have_hidserv_auth_button(self): CommonTests.test_copy_have_hidserv_auth_button(self, 'share') - @pytest.mark.run(order=23) + @pytest.mark.run(order=25) def test_hidserv_auth_string(self): CommonTests.test_hidserv_auth_string(self) diff --git a/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py b/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py index 861b7ccc..37abc825 100644 --- a/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py +++ b/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py @@ -97,78 +97,86 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') + def test_info_widget_shows_less(self): + CommonTests.test_info_widget_shows_less(self, 'share') @pytest.mark.run(order=7) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'share') + + @pytest.mark.run(order=9) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'share') - @pytest.mark.run(order=8) + @pytest.mark.run(order=10) def test_deleting_only_file_hides_delete_button(self): CommonTests.test_deleting_only_file_hides_delete_button(self) - @pytest.mark.run(order=9) + @pytest.mark.run(order=11) def test_add_a_file_and_delete_using_its_delete_widget(self): CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) - @pytest.mark.run(order=10) + @pytest.mark.run(order=12) def test_file_selection_widget_readd_files(self): CommonTests.test_file_selection_widget_readd_files(self) - @pytest.mark.run(order=11) + @pytest.mark.run(order=13) def test_server_working_on_start_button_pressed(self): CommonTests.test_server_working_on_start_button_pressed(self, 'share') - @pytest.mark.run(order=12) + @pytest.mark.run(order=14) def test_server_status_indicator_says_starting(self): CommonTests.test_server_status_indicator_says_starting(self, 'share') - @pytest.mark.run(order=13) + @pytest.mark.run(order=15) def test_add_delete_buttons_hidden(self): CommonTests.test_add_delete_buttons_hidden(self) - @pytest.mark.run(order=14) + @pytest.mark.run(order=16) def test_settings_button_is_hidden(self): CommonTests.test_settings_button_is_hidden(self) - @pytest.mark.run(order=15) + @pytest.mark.run(order=17) def test_a_server_is_started(self): CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=16) + @pytest.mark.run(order=18) def test_a_web_server_is_running(self): CommonTests.test_a_web_server_is_running(self) - @pytest.mark.run(order=17) + @pytest.mark.run(order=19) def test_have_a_slug(self): CommonTests.test_have_a_slug(self, 'share', False) - @pytest.mark.run(order=18) + @pytest.mark.run(order=20) def test_have_an_onion(self): CommonTests.test_have_an_onion_service(self) - @pytest.mark.run(order=19) + @pytest.mark.run(order=21) def test_url_description_shown(self): CommonTests.test_url_description_shown(self, 'share') - @pytest.mark.run(order=20) + @pytest.mark.run(order=22) def test_have_copy_url_button(self): CommonTests.test_have_copy_url_button(self, 'share') - @pytest.mark.run(order=21) + @pytest.mark.run(order=23) def test_server_status_indicator_says_started(self): CommonTests.test_server_status_indicator_says_started(self, 'share') - @pytest.mark.run(order=22) + @pytest.mark.run(order=24) def test_tor_killed_statusbar_message_shown(self): CommonTests.test_tor_killed_statusbar_message_shown(self, 'share') - @pytest.mark.run(order=23) + @pytest.mark.run(order=25) def test_server_is_stopped(self): CommonTests.test_server_is_stopped(self, 'share', False) - @pytest.mark.run(order=24) + @pytest.mark.run(order=26) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) diff --git a/tests_gui_tor/onionshare_timer_test.py b/tests_gui_tor/onionshare_timer_test.py index b76106d9..2b64b998 100644 --- a/tests_gui_tor/onionshare_timer_test.py +++ b/tests_gui_tor/onionshare_timer_test.py @@ -97,42 +97,50 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') + def test_info_widget_shows_less(self): + CommonTests.test_info_widget_shows_less(self, 'share') @pytest.mark.run(order=7) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'share') + + @pytest.mark.run(order=9) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'share') - @pytest.mark.run(order=8) + @pytest.mark.run(order=10) def test_set_timeout(self): CommonTests.test_set_timeout(self, 'share', 120) - @pytest.mark.run(order=9) + @pytest.mark.run(order=11) def test_server_working_on_start_button_pressed(self): CommonTests.test_server_working_on_start_button_pressed(self, 'share') - @pytest.mark.run(order=10) + @pytest.mark.run(order=12) def test_server_status_indicator_says_starting(self): CommonTests.test_server_status_indicator_says_starting(self, 'share') - @pytest.mark.run(order=11) + @pytest.mark.run(order=13) def test_a_server_is_started(self): CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=12) + @pytest.mark.run(order=14) def test_a_web_server_is_running(self): CommonTests.test_a_web_server_is_running(self) - @pytest.mark.run(order=13) + @pytest.mark.run(order=15) def test_timeout_widget_hidden(self): CommonTests.test_timeout_widget_hidden(self, 'share') - @pytest.mark.run(order=14) + @pytest.mark.run(order=16) def test_timeout(self): CommonTests.test_server_timed_out(self, 'share', 125000) - @pytest.mark.run(order=15) + @pytest.mark.run(order=17) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) diff --git a/tests_gui_tor/onionshare_tor_connection_killed_test.py b/tests_gui_tor/onionshare_tor_connection_killed_test.py index 861b7ccc..37abc825 100644 --- a/tests_gui_tor/onionshare_tor_connection_killed_test.py +++ b/tests_gui_tor/onionshare_tor_connection_killed_test.py @@ -97,78 +97,86 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) - def test_info_widget_is_visible(self): - CommonTests.test_info_widget_is_visible(self, 'share') + def test_info_widget_shows_less(self): + CommonTests.test_info_widget_shows_less(self, 'share') @pytest.mark.run(order=7) + def test_history_is_not_visible(self): + CommonTests.test_history_is_not_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_click_toggle_history(self): + CommonTests.test_click_toggle_history(self, 'share') + + @pytest.mark.run(order=9) def test_history_is_visible(self): CommonTests.test_history_is_visible(self, 'share') - @pytest.mark.run(order=8) + @pytest.mark.run(order=10) def test_deleting_only_file_hides_delete_button(self): CommonTests.test_deleting_only_file_hides_delete_button(self) - @pytest.mark.run(order=9) + @pytest.mark.run(order=11) def test_add_a_file_and_delete_using_its_delete_widget(self): CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) - @pytest.mark.run(order=10) + @pytest.mark.run(order=12) def test_file_selection_widget_readd_files(self): CommonTests.test_file_selection_widget_readd_files(self) - @pytest.mark.run(order=11) + @pytest.mark.run(order=13) def test_server_working_on_start_button_pressed(self): CommonTests.test_server_working_on_start_button_pressed(self, 'share') - @pytest.mark.run(order=12) + @pytest.mark.run(order=14) def test_server_status_indicator_says_starting(self): CommonTests.test_server_status_indicator_says_starting(self, 'share') - @pytest.mark.run(order=13) + @pytest.mark.run(order=15) def test_add_delete_buttons_hidden(self): CommonTests.test_add_delete_buttons_hidden(self) - @pytest.mark.run(order=14) + @pytest.mark.run(order=16) def test_settings_button_is_hidden(self): CommonTests.test_settings_button_is_hidden(self) - @pytest.mark.run(order=15) + @pytest.mark.run(order=17) def test_a_server_is_started(self): CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=16) + @pytest.mark.run(order=18) def test_a_web_server_is_running(self): CommonTests.test_a_web_server_is_running(self) - @pytest.mark.run(order=17) + @pytest.mark.run(order=19) def test_have_a_slug(self): CommonTests.test_have_a_slug(self, 'share', False) - @pytest.mark.run(order=18) + @pytest.mark.run(order=20) def test_have_an_onion(self): CommonTests.test_have_an_onion_service(self) - @pytest.mark.run(order=19) + @pytest.mark.run(order=21) def test_url_description_shown(self): CommonTests.test_url_description_shown(self, 'share') - @pytest.mark.run(order=20) + @pytest.mark.run(order=22) def test_have_copy_url_button(self): CommonTests.test_have_copy_url_button(self, 'share') - @pytest.mark.run(order=21) + @pytest.mark.run(order=23) def test_server_status_indicator_says_started(self): CommonTests.test_server_status_indicator_says_started(self, 'share') - @pytest.mark.run(order=22) + @pytest.mark.run(order=24) def test_tor_killed_statusbar_message_shown(self): CommonTests.test_tor_killed_statusbar_message_shown(self, 'share') - @pytest.mark.run(order=23) + @pytest.mark.run(order=25) def test_server_is_stopped(self): CommonTests.test_server_is_stopped(self, 'share', False) - @pytest.mark.run(order=24) + @pytest.mark.run(order=26) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) From 3fd75819953e18a652f0746b68e7ec99eeb9c4ba Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 30 Sep 2018 11:41:07 -0700 Subject: [PATCH 100/123] Test the history indicator widget, in local GUI tests --- tests_gui_local/commontests.py | 53 +++++++++++++++++++ .../onionshare_receive_mode_upload_test.py | 8 ++- ...re_receive_mode_upload_test_public_mode.py | 8 ++- .../onionshare_share_mode_download_test.py | 6 +++ ...re_share_mode_download_test_public_mode.py | 6 +++ ...hare_share_mode_download_test_stay_open.py | 6 +++ .../onionshare_slug_persistent_test.py | 6 +++ 7 files changed, 89 insertions(+), 4 deletions(-) diff --git a/tests_gui_local/commontests.py b/tests_gui_local/commontests.py index 870c2dbe..21e8cfad 100644 --- a/tests_gui_local/commontests.py +++ b/tests_gui_local/commontests.py @@ -52,6 +52,59 @@ class CommonTests(object): QtTest.QTest.mouseClick(self.gui.share_mode.info.toggle_button, QtCore.Qt.LeftButton) self.assertEqual(self.gui.share_mode.downloads.isVisible(), not currently_visible) + def test_history_indicator(self, mode, public_mode): + '''Test that we can make sure the history is toggled off, do an action, and the indiciator works''' + if mode == 'receive': + # Make sure history is toggled off + if self.gui.receive_mode.uploads.isVisible(): + QtTest.QTest.mouseClick(self.gui.receive_mode.info.toggle_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.receive_mode.uploads.isVisible()) + + # Indicator should not be visible yet + self.assertFalse(self.gui.receive_mode.info.indicator_label.isVisible()) + + # Upload a file + files = {'file[]': open('/tmp/test.txt', 'rb')} + if not public_mode: + path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug) + else: + path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) + response = requests.post(path, files=files) + QtTest.QTest.qWait(2000) + + # Indicator should be visible, have a value of "1" + self.assertTrue(self.gui.receive_mode.info.indicator_label.isVisible()) + self.assertEqual(self.gui.receive_mode.info.indicator_label.text(), "1") + + # Toggle history back on, indicator should be hidden again + QtTest.QTest.mouseClick(self.gui.receive_mode.info.toggle_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.receive_mode.info.indicator_label.isVisible()) + + if mode == 'share': + # Make sure history is toggled off + if self.gui.share_mode.downloads.isVisible(): + QtTest.QTest.mouseClick(self.gui.share_mode.info.toggle_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.share_mode.downloads.isVisible()) + + # Indicator should not be visible yet + self.assertFalse(self.gui.share_mode.info.indicator_label.isVisible()) + + # Download files + if public_mode: + url = "http://127.0.0.1:{}/download".format(self.gui.app.port) + else: + url = "http://127.0.0.1:{}/{}/download".format(self.gui.app.port, self.gui.share_mode.web.slug) + r = requests.get(url) + QtTest.QTest.qWait(2000) + + # Indicator should be visible, have a value of "1" + self.assertTrue(self.gui.share_mode.info.indicator_label.isVisible()) + self.assertEqual(self.gui.share_mode.info.indicator_label.text(), "1") + + # Toggle history back on, indicator should be hidden again + QtTest.QTest.mouseClick(self.gui.share_mode.info.toggle_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.share_mode.info.indicator_label.isVisible()) + def test_history_is_not_visible(self, mode): '''Test that the History section is not visible''' if mode == 'receive': diff --git a/tests_gui_local/onionshare_receive_mode_upload_test.py b/tests_gui_local/onionshare_receive_mode_upload_test.py index b53d5c06..19674aa3 100644 --- a/tests_gui_local/onionshare_receive_mode_upload_test.py +++ b/tests_gui_local/onionshare_receive_mode_upload_test.py @@ -171,14 +171,18 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_counter_incremented(self, 'receive', 2) @pytest.mark.run(order=24) + def test_history_indicator(self): + CommonTests.test_history_indicator(self, 'receive', False) + + @pytest.mark.run(order=25) def test_server_is_stopped(self): CommonTests.test_server_is_stopped(self, 'receive', False) - @pytest.mark.run(order=25) + @pytest.mark.run(order=26) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) - @pytest.mark.run(order=26) + @pytest.mark.run(order=27) def test_server_status_indicator_says_closed(self): CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) diff --git a/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py index 5e5a6b77..e3f85731 100644 --- a/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py +++ b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py @@ -171,14 +171,18 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_counter_incremented(self, 'receive', 2) @pytest.mark.run(order=24) + def test_history_indicator(self): + CommonTests.test_history_indicator(self, 'receive', True) + + @pytest.mark.run(order=25) def test_server_is_stopped(self): CommonTests.test_server_is_stopped(self, 'receive', False) - @pytest.mark.run(order=25) + @pytest.mark.run(order=26) def test_web_service_is_stopped(self): CommonTests.test_web_service_is_stopped(self) - @pytest.mark.run(order=26) + @pytest.mark.run(order=27) def test_server_status_indicator_says_closed(self): CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) diff --git a/tests_gui_local/onionshare_share_mode_download_test.py b/tests_gui_local/onionshare_share_mode_download_test.py index 40df6d98..c4a60101 100644 --- a/tests_gui_local/onionshare_share_mode_download_test.py +++ b/tests_gui_local/onionshare_share_mode_download_test.py @@ -192,6 +192,12 @@ class OnionShareGuiTest(unittest.TestCase): def test_add_button_visible(self): CommonTests.test_add_button_visible(self) + @pytest.mark.run(order=30) + def test_history_indicator(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + CommonTests.test_a_server_is_started(self, 'share') + CommonTests.test_history_indicator(self, 'share', False) + if __name__ == "__main__": unittest.main() diff --git a/tests_gui_local/onionshare_share_mode_download_test_public_mode.py b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py index 73d4c999..a10ee4c2 100644 --- a/tests_gui_local/onionshare_share_mode_download_test_public_mode.py +++ b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py @@ -192,6 +192,12 @@ class OnionShareGuiTest(unittest.TestCase): def test_add_button_visible(self): CommonTests.test_add_button_visible(self) + @pytest.mark.run(order=30) + def test_history_indicator(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + CommonTests.test_a_server_is_started(self, 'share') + CommonTests.test_history_indicator(self, 'share', True) + if __name__ == "__main__": unittest.main() diff --git a/tests_gui_local/onionshare_share_mode_download_test_stay_open.py b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py index e849d224..8426c264 100644 --- a/tests_gui_local/onionshare_share_mode_download_test_stay_open.py +++ b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py @@ -204,6 +204,12 @@ class OnionShareGuiTest(unittest.TestCase): def test_add_button_visible(self): CommonTests.test_add_button_visible(self) + @pytest.mark.run(order=33) + def test_history_indicator(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + CommonTests.test_a_server_is_started(self, 'share') + CommonTests.test_history_indicator(self, 'share', True) + if __name__ == "__main__": unittest.main() diff --git a/tests_gui_local/onionshare_slug_persistent_test.py b/tests_gui_local/onionshare_slug_persistent_test.py index 5b53f7e0..9fb623dd 100644 --- a/tests_gui_local/onionshare_slug_persistent_test.py +++ b/tests_gui_local/onionshare_slug_persistent_test.py @@ -168,6 +168,12 @@ class OnionShareGuiTest(unittest.TestCase): CommonTests.test_server_is_stopped(self, 'share', True) CommonTests.test_web_service_is_stopped(self) + @pytest.mark.run(order=23) + def test_history_indicator(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + CommonTests.test_a_server_is_started(self, 'share') + CommonTests.test_history_indicator(self, 'share', False) + if __name__ == "__main__": unittest.main() From 7c61483ae9adea7e5b78bc45249bba8afb9efc92 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 14:48:15 -0700 Subject: [PATCH 101/123] Move Mode module into its own folder --- onionshare_gui/{mode.py => mode/__init__.py} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename onionshare_gui/{mode.py => mode/__init__.py} (99%) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode/__init__.py similarity index 99% rename from onionshare_gui/mode.py rename to onionshare_gui/mode/__init__.py index 1a961149..cfbb235b 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode/__init__.py @@ -22,9 +22,9 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.common import ShutdownTimer -from .server_status import ServerStatus -from .threads import OnionThread -from .widgets import Alert +from ..server_status import ServerStatus +from ..threads import OnionThread +from ..widgets import Alert class Mode(QtWidgets.QWidget): """ From 801d8b965c693b310a8c4ec5e419564577041b69 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 14:54:51 -0700 Subject: [PATCH 102/123] Move ShareMode and ReceiveMode into Mode module --- onionshare_gui/{ => mode}/receive_mode/__init__.py | 2 +- onionshare_gui/{ => mode}/receive_mode/info.py | 0 onionshare_gui/{ => mode}/receive_mode/uploads.py | 2 +- onionshare_gui/{ => mode}/share_mode/__init__.py | 4 ++-- onionshare_gui/{ => mode}/share_mode/downloads.py | 0 onionshare_gui/{ => mode}/share_mode/file_selection.py | 2 +- onionshare_gui/{ => mode}/share_mode/info.py | 0 onionshare_gui/{ => mode}/share_mode/threads.py | 0 onionshare_gui/onionshare_gui.py | 4 ++-- setup.py | 5 +++-- 10 files changed, 10 insertions(+), 9 deletions(-) rename onionshare_gui/{ => mode}/receive_mode/__init__.py (99%) rename onionshare_gui/{ => mode}/receive_mode/info.py (100%) rename onionshare_gui/{ => mode}/receive_mode/uploads.py (99%) rename onionshare_gui/{ => mode}/share_mode/__init__.py (99%) rename onionshare_gui/{ => mode}/share_mode/downloads.py (100%) rename onionshare_gui/{ => mode}/share_mode/file_selection.py (99%) rename onionshare_gui/{ => mode}/share_mode/info.py (100%) rename onionshare_gui/{ => mode}/share_mode/threads.py (100%) diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py similarity index 99% rename from onionshare_gui/receive_mode/__init__.py rename to onionshare_gui/mode/receive_mode/__init__.py index 6430382b..96c76dbf 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/mode/receive_mode/__init__.py @@ -24,7 +24,7 @@ from onionshare.web import Web from .uploads import Uploads from .info import ReceiveModeInfo -from ..mode import Mode +from .. import Mode class ReceiveMode(Mode): """ diff --git a/onionshare_gui/receive_mode/info.py b/onionshare_gui/mode/receive_mode/info.py similarity index 100% rename from onionshare_gui/receive_mode/info.py rename to onionshare_gui/mode/receive_mode/info.py diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/mode/receive_mode/uploads.py similarity index 99% rename from onionshare_gui/receive_mode/uploads.py rename to onionshare_gui/mode/receive_mode/uploads.py index f08b35cc..c445be47 100644 --- a/onionshare_gui/receive_mode/uploads.py +++ b/onionshare_gui/mode/receive_mode/uploads.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 File(QtWidgets.QWidget): diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py similarity index 99% rename from onionshare_gui/share_mode/__init__.py rename to onionshare_gui/mode/share_mode/__init__.py index c44e8beb..c301037b 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -29,8 +29,8 @@ from .file_selection import FileSelection from .downloads import Downloads from .threads import CompressThread from .info import ShareModeInfo -from ..mode import Mode -from ..widgets import Alert +from .. import Mode +from ...widgets import Alert class ShareMode(Mode): """ diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/mode/share_mode/downloads.py similarity index 100% rename from onionshare_gui/share_mode/downloads.py rename to onionshare_gui/mode/share_mode/downloads.py diff --git a/onionshare_gui/share_mode/file_selection.py b/onionshare_gui/mode/share_mode/file_selection.py similarity index 99% rename from onionshare_gui/share_mode/file_selection.py rename to onionshare_gui/mode/share_mode/file_selection.py index 628ad5ef..d59df234 100644 --- a/onionshare_gui/share_mode/file_selection.py +++ b/onionshare_gui/mode/share_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/share_mode/info.py b/onionshare_gui/mode/share_mode/info.py similarity index 100% rename from onionshare_gui/share_mode/info.py rename to onionshare_gui/mode/share_mode/info.py diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/mode/share_mode/threads.py similarity index 100% rename from onionshare_gui/share_mode/threads.py rename to onionshare_gui/mode/share_mode/threads.py diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index ced53ede..6a7eb63a 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -23,8 +23,8 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.web import Web -from .share_mode import ShareMode -from .receive_mode import ReceiveMode +from .mode.share_mode import ShareMode +from .mode.receive_mode import ReceiveMode from .tor_connection_dialog import TorConnectionDialog from .settings_dialog import SettingsDialog diff --git a/setup.py b/setup.py index 94213f7c..86b71f82 100644 --- a/setup.py +++ b/setup.py @@ -69,8 +69,9 @@ setup( 'onionshare', 'onionshare.web', 'onionshare_gui', - 'onionshare_gui.share_mode', - 'onionshare_gui.receive_mode' + 'onionshare_gui.mode', + 'onionshare_gui.mode.share_mode', + 'onionshare_gui.mode.receive_mode' ], include_package_data=True, scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'], From 5a8cb2ac9dd3eadc5540f42766c779789304f3d1 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 15:20:22 -0700 Subject: [PATCH 103/123] In ShareMode, remove the ShareModeInfo widget and replace with a customized ToggleHistory widget --- onionshare_gui/mode/share_mode/__init__.py | 54 ++++++++----- onionshare_gui/mode/toggle_history.py | 88 ++++++++++++++++++++++ 2 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 onionshare_gui/mode/toggle_history.py diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index c301037b..f7ba2760 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -28,8 +28,9 @@ from onionshare.web import Web from .file_selection import FileSelection from .downloads import Downloads from .threads import CompressThread -from .info import ShareModeInfo +#from .info import ShareModeInfo from .. import Mode +from ..toggle_history import ToggleHistory from ...widgets import Alert class ShareMode(Mode): @@ -78,7 +79,24 @@ class ShareMode(Mode): self.downloads_completed = 0 # Information about share, and show downloads button - self.info = ShareModeInfo(self.common, self) + #self.info = ShareModeInfo(self.common, self) + + # Info label + self.info_label = QtWidgets.QLabel() + self.info_label.hide() + + # Toggle history + self.toggle_history = ToggleHistory( + self.common, self, self.downloads, + QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')), + QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png')) + ) + + # Top bar + top_bar_layout = QtWidgets.QHBoxLayout() + top_bar_layout.addWidget(self.info_label) + top_bar_layout.addStretch() + top_bar_layout.addWidget(self.toggle_history) # Primary action layout self.primary_action_layout.addWidget(self.filesize_warning) @@ -90,7 +108,7 @@ class ShareMode(Mode): # Main layout self.main_layout = QtWidgets.QVBoxLayout() - self.main_layout.addWidget(self.info) + 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) @@ -191,7 +209,7 @@ class ShareMode(Mode): self.filesize_warning.hide() self.downloads_in_progress = 0 self.downloads_completed = 0 - self.info.update_downloads_in_progress() + #self.info.update_downloads_in_progress() self.file_selection.file_list.adjustSize() def cancel_server_custom(self): @@ -207,7 +225,7 @@ class ShareMode(Mode): Connection to Tor broke. """ self.primary_action.hide() - self.info.show_less() + self.info_label.hide() def handle_request_load(self, event): """ @@ -224,9 +242,9 @@ class ShareMode(Mode): else: filesize = self.web.share_mode.download_filesize self.downloads.add(event["data"]["id"], filesize) - self.info.update_indicator(True) + self.toggle_history.update_indicator(True) self.downloads_in_progress += 1 - self.info.update_downloads_in_progress() + #self.info.update_downloads_in_progress() self.system_tray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True)) @@ -242,10 +260,10 @@ class ShareMode(Mode): # Update the total 'completed downloads' info self.downloads_completed += 1 - self.info.update_downloads_completed() + #self.info.update_downloads_completed() # Update the 'in progress downloads' info self.downloads_in_progress -= 1 - self.info.update_downloads_in_progress() + #self.info.update_downloads_in_progress() # Close on finish? if self.common.settings.get('close_after_first_download'): @@ -256,7 +274,7 @@ class ShareMode(Mode): if self.server_status.status == self.server_status.STATUS_STOPPED: self.downloads.cancel(event["data"]["id"]) self.downloads_in_progress = 0 - self.info.update_downloads_in_progress() + #self.info.update_downloads_in_progress() def handle_request_canceled(self, event): """ @@ -266,7 +284,7 @@ class ShareMode(Mode): # Update the 'in progress downloads' info self.downloads_in_progress -= 1 - self.info.update_downloads_in_progress() + #self.info.update_downloads_in_progress() self.system_tray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True)) def on_reload_settings(self): @@ -276,7 +294,7 @@ class ShareMode(Mode): """ if self.server_status.file_selection.get_num_files() > 0: self.primary_action.show() - self.info.show_more() + self.info_label.show() def update_primary_action(self): self.common.log('ShareMode', 'update_primary_action') @@ -285,7 +303,7 @@ class ShareMode(Mode): file_count = self.file_selection.file_list.count() if file_count > 0: self.primary_action.show() - self.info.show_more() + self.info_label.show() # Update the file count in the info label total_size_bytes = 0 @@ -295,13 +313,13 @@ class ShareMode(Mode): total_size_readable = self.common.human_readable_filesize(total_size_bytes) if file_count > 1: - self.info.update_label(strings._('gui_file_info', True).format(file_count, total_size_readable)) + self.info_label.setText(strings._('gui_file_info', True).format(file_count, total_size_readable)) else: - self.info.update_label(strings._('gui_file_info_single', True).format(file_count, total_size_readable)) + self.info_label.setText(strings._('gui_file_info_single', True).format(file_count, total_size_readable)) else: self.primary_action.hide() - self.info.show_less() + self.info_label.hide() # Resize window self.resize_window() @@ -312,8 +330,8 @@ class ShareMode(Mode): """ self.downloads_completed = 0 self.downloads_in_progress = 0 - self.info.update_downloads_completed() - self.info.update_downloads_in_progress() + #self.info.update_downloads_completed() + #self.info.update_downloads_in_progress() self.downloads.reset() def resize_window(self): diff --git a/onionshare_gui/mode/toggle_history.py b/onionshare_gui/mode/toggle_history.py new file mode 100644 index 00000000..81ecde86 --- /dev/null +++ b/onionshare_gui/mode/toggle_history.py @@ -0,0 +1,88 @@ +# -*- 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 ToggleHistory(QtWidgets.QPushButton): + """ + Widget for toggling download/upload history on or off, as well as keeping track + of the indicator counter + """ + def __init__(self, common, current_mode, history_widget, icon, selected_icon): + super(ToggleHistory, self).__init__() + self.common = common + self.current_mode = current_mode + self.history_widget = history_widget + self.icon = icon + self.selected_icon = selected_icon + + # Toggle button + self.setDefault(False) + self.setFixedWidth(35) + self.setFixedHeight(30) + self.setFlat(True) + self.setIcon(icon) + self.clicked.connect(self.toggle_clicked) + + # Keep track of indicator + self.indicator_count = 0 + self.indicator_label = QtWidgets.QLabel(parent=self) + self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) + self.update_indicator() + + def update_indicator(self, increment=False): + """ + Update the display of the indicator count. If increment is True, then + only increment the counter if Downloads is hidden. + """ + if increment and not self.history_widget.isVisible(): + self.indicator_count += 1 + + self.indicator_label.setText("{}".format(self.indicator_count)) + + if self.indicator_count == 0: + self.indicator_label.hide() + else: + size = self.indicator_label.sizeHint() + self.indicator_label.setGeometry(35-size.width(), 0, size.width(), size.height()) + self.indicator_label.show() + + def toggle_clicked(self): + """ + Toggle showing and hiding the history widget + """ + self.common.log('ToggleHistory', 'toggle_clicked') + + if self.history_widget.isVisible(): + self.history_widget.hide() + self.setIcon(self.icon) + self.setFlat(True) + else: + self.history_widget.show() + self.setIcon(self.selected_icon) + self.setFlat(False) + + # Reset the indicator count + self.indicator_count = 0 + self.update_indicator() + + self.current_mode.resize_window() From 484c33902fb6ddc6a5262e0e025b8b86c2854283 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 17:35:15 -0700 Subject: [PATCH 104/123] Make ShareMode just use a History object directly, instead of defining its own Downloads class --- onionshare_gui/mode/history.py | 328 ++++++++++++++++++++ onionshare_gui/mode/share_mode/__init__.py | 34 +- onionshare_gui/mode/share_mode/downloads.py | 248 --------------- onionshare_gui/mode/toggle_history.py | 88 ------ 4 files changed, 348 insertions(+), 350 deletions(-) create mode 100644 onionshare_gui/mode/history.py delete mode 100644 onionshare_gui/mode/share_mode/downloads.py delete mode 100644 onionshare_gui/mode/toggle_history.py diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py new file mode 100644 index 00000000..31b4a646 --- /dev/null +++ b/onionshare_gui/mode/history.py @@ -0,0 +1,328 @@ +# -*- 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 time +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare import strings + + +class HistoryItem(QtWidgets.QWidget): + """ + The base history item + """ + def __init__(self): + super(HistoryItem, self).__init__() + + def update(self): + pass + + def cancel(self): + pass + + +class DownloadHistoryItem(HistoryItem): + """ + Download history item, for share mode + """ + def __init__(self, common, id, total_bytes): + super(DownloadHistoryItem, self).__init__() + self.common = common + + self.id = id + self.started = time.time() + self.total_bytes = total_bytes + self.downloaded_bytes = 0 + + self.setStyleSheet('QWidget { border: 1px solid red; }') + + # Progress bar + self.progress_bar = QtWidgets.QProgressBar() + self.progress_bar.setTextVisible(True) + self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter) + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(total_bytes) + self.progress_bar.setValue(0) + self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar']) + self.progress_bar.total_bytes = total_bytes + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.progress_bar) + self.setLayout(layout) + + # Start at 0 + self.update(0) + + def update(self, downloaded_bytes): + self.downloaded_bytes = downloaded_bytes + + self.progress_bar.setValue(downloaded_bytes) + if downloaded_bytes == self.progress_bar.total_bytes: + pb_fmt = strings._('gui_download_upload_progress_complete').format( + self.common.format_seconds(time.time() - self.started)) + else: + elapsed = time.time() - self.started + if elapsed < 10: + # Wait a couple of seconds for the download rate to stabilize. + # This prevents a "Windows copy dialog"-esque experience at + # the beginning of the download. + pb_fmt = strings._('gui_download_upload_progress_starting').format( + self.common.human_readable_filesize(downloaded_bytes)) + else: + pb_fmt = strings._('gui_download_upload_progress_eta').format( + self.common.human_readable_filesize(downloaded_bytes), + self.estimated_time_remaining) + + self.progress_bar.setFormat(pb_fmt) + + def cancel(self): + self.progress_bar.setFormat(strings._('gui_canceled')) + + @property + def estimated_time_remaining(self): + return self.common.estimated_time_remaining(self.downloaded_bytes, + self.total_bytes, + self.started) + + +class HistoryItemList(QtWidgets.QScrollArea): + """ + List of items + """ + def __init__(self, common): + super(HistoryItemList, self).__init__() + self.common = common + + self.items = {} + + # The layout that holds all of the items + self.items_layout = QtWidgets.QVBoxLayout() + self.items_layout.setContentsMargins(0, 0, 0, 0) + self.items_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) + + # Wrapper layout that also contains a stretch + wrapper_layout = QtWidgets.QVBoxLayout() + wrapper_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) + wrapper_layout.addLayout(self.items_layout) + wrapper_layout.addStretch() + + # The internal widget of the scroll area + widget = QtWidgets.QWidget() + widget.setLayout(wrapper_layout) + self.setWidget(widget) + self.setWidgetResizable(True) + + # Other scroll area settings + self.setBackgroundRole(QtGui.QPalette.Light) + self.verticalScrollBar().rangeChanged.connect(self.resizeScroll) + + def resizeScroll(self, minimum, maximum): + """ + Scroll to the bottom of the window when the range changes. + """ + self.verticalScrollBar().setValue(maximum) + + def add(self, id, item): + """ + Add a new item. Override this method. + """ + self.items[id] = item + self.items_layout.addWidget(item) + + def update(self, id, data): + """ + Update an item. Override this method. + """ + self.items[id].update(data) + + def cancel(self, id): + """ + Cancel an item. Override this method. + """ + self.items[id].cancel() + + def reset(self): + """ + Reset all items, emptying the list. Override this method. + """ + for item in self.items.values(): + self.items_layout.removeWidget(item) + self.items = {} + + +class History(QtWidgets.QWidget): + """ + A history of what's happened so far in this mode. This contains an internal + object full of a scrollable list of items. + """ + def __init__(self, common, empty_image, empty_text, header_text): + super(History, self).__init__() + self.common = common + + self.setMinimumWidth(350) + + # When there are no items + self.empty_image = QtWidgets.QLabel() + self.empty_image.setAlignment(QtCore.Qt.AlignCenter) + 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']) + 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.setLayout(empty_layout) + + # Header + self.header_label = QtWidgets.QLabel(header_text) + self.header_label.setStyleSheet(self.common.css['downloads_uploads_label']) + clear_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) + clear_button.setStyleSheet(self.common.css['downloads_uploads_clear']) + clear_button.setFlat(True) + clear_button.clicked.connect(self.reset) + header_layout = QtWidgets.QHBoxLayout() + header_layout.addWidget(self.header_label) + header_layout.addStretch() + header_layout.addWidget(clear_button) + + # When there are items + self.item_list = HistoryItemList(self.common) + self.not_empty_layout = QtWidgets.QVBoxLayout() + self.not_empty_layout.addLayout(header_layout) + self.not_empty_layout.addWidget(self.item_list) + self.not_empty = QtWidgets.QWidget() + self.not_empty.setLayout(self.not_empty_layout) + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.empty) + layout.addWidget(self.not_empty) + self.setLayout(layout) + + # Reset once at the beginning + self.reset() + + def add(self, id, item): + """ + Add a new item. + """ + self.common.log('History', 'add', 'id: {}, item: {}'.format(id, item)) + + # Hide empty, show not empty + self.empty.hide() + self.not_empty.show() + + # Add it to the list + self.item_list.add(id, item) + + + def update(self, id, data): + """ + Update an item. + """ + self.item_list.update(id, data) + + def cancel(self, id): + """ + Cancel an item. + """ + self.item_list.cancel(id) + + def reset(self): + """ + Reset all items. + """ + self.item_list.reset() + + # Hide not empty, show empty + self.not_empty.hide() + self.empty.show() + + +class ToggleHistory(QtWidgets.QPushButton): + """ + Widget for toggling showing or hiding the history, as well as keeping track + of the indicator counter if it's hidden + """ + def __init__(self, common, current_mode, history_widget, icon, selected_icon): + super(ToggleHistory, self).__init__() + self.common = common + self.current_mode = current_mode + self.history_widget = history_widget + self.icon = icon + self.selected_icon = selected_icon + + # Toggle button + self.setDefault(False) + self.setFixedWidth(35) + self.setFixedHeight(30) + self.setFlat(True) + self.setIcon(icon) + self.clicked.connect(self.toggle_clicked) + + # Keep track of indicator + self.indicator_count = 0 + self.indicator_label = QtWidgets.QLabel(parent=self) + self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) + self.update_indicator() + + def update_indicator(self, increment=False): + """ + Update the display of the indicator count. If increment is True, then + only increment the counter if Downloads is hidden. + """ + if increment and not self.history_widget.isVisible(): + self.indicator_count += 1 + + self.indicator_label.setText("{}".format(self.indicator_count)) + + if self.indicator_count == 0: + self.indicator_label.hide() + else: + size = self.indicator_label.sizeHint() + self.indicator_label.setGeometry(35-size.width(), 0, size.width(), size.height()) + self.indicator_label.show() + + def toggle_clicked(self): + """ + Toggle showing and hiding the history widget + """ + self.common.log('ToggleHistory', 'toggle_clicked') + + if self.history_widget.isVisible(): + self.history_widget.hide() + self.setIcon(self.icon) + self.setFlat(True) + else: + self.history_widget.show() + self.setIcon(self.selected_icon) + self.setFlat(False) + + # Reset the indicator count + self.indicator_count = 0 + self.update_indicator() + + self.current_mode.resize_window() diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index f7ba2760..bae4bec8 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -26,13 +26,12 @@ from onionshare.common import Common from onionshare.web import Web from .file_selection import FileSelection -from .downloads import Downloads from .threads import CompressThread -#from .info import ShareModeInfo from .. import Mode -from ..toggle_history import ToggleHistory +from ..history import History, ToggleHistory, DownloadHistoryItem from ...widgets import Alert + class ShareMode(Mode): """ Parts of the main window UI for sharing files. @@ -72,9 +71,14 @@ class ShareMode(Mode): self.filesize_warning.setStyleSheet(self.common.css['share_filesize_warning']) self.filesize_warning.hide() - # Downloads - self.downloads = Downloads(self.common) - self.downloads.hide() + # Download history + self.history = History( + self.common, + QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/downloads_transparent.png'))), + strings._('gui_no_downloads'), + strings._('gui_downloads') + ) + self.history.hide() self.downloads_in_progress = 0 self.downloads_completed = 0 @@ -87,7 +91,7 @@ class ShareMode(Mode): # Toggle history self.toggle_history = ToggleHistory( - self.common, self, self.downloads, + self.common, self, self.history, QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')), QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png')) ) @@ -116,7 +120,7 @@ class ShareMode(Mode): # Wrapper layout self.wrapper_layout = QtWidgets.QHBoxLayout() self.wrapper_layout.addLayout(self.main_layout) - self.wrapper_layout.addWidget(self.downloads) + self.wrapper_layout.addWidget(self.history) self.setLayout(self.wrapper_layout) # Always start with focus on file selection @@ -241,7 +245,9 @@ class ShareMode(Mode): filesize = self.web.share_mode.gzip_filesize else: filesize = self.web.share_mode.download_filesize - self.downloads.add(event["data"]["id"], filesize) + + item = DownloadHistoryItem(self.common, event["data"]["id"], filesize) + self.history.add(event["data"]["id"], item) self.toggle_history.update_indicator(True) self.downloads_in_progress += 1 #self.info.update_downloads_in_progress() @@ -252,7 +258,7 @@ class ShareMode(Mode): """ Handle REQUEST_PROGRESS event. """ - self.downloads.update(event["data"]["id"], event["data"]["bytes"]) + self.history.update(event["data"]["id"], event["data"]["bytes"]) # Is the download complete? if event["data"]["bytes"] == self.web.share_mode.filesize: @@ -272,7 +278,7 @@ class ShareMode(Mode): self.server_status_label.setText(strings._('closing_automatically', True)) else: if self.server_status.status == self.server_status.STATUS_STOPPED: - self.downloads.cancel(event["data"]["id"]) + self.history.cancel(event["data"]["id"]) self.downloads_in_progress = 0 #self.info.update_downloads_in_progress() @@ -280,7 +286,7 @@ class ShareMode(Mode): """ Handle REQUEST_CANCELED event. """ - self.downloads.cancel(event["data"]["id"]) + self.history.cancel(event["data"]["id"]) # Update the 'in progress downloads' info self.downloads_in_progress -= 1 @@ -332,11 +338,11 @@ class ShareMode(Mode): self.downloads_in_progress = 0 #self.info.update_downloads_completed() #self.info.update_downloads_in_progress() - self.downloads.reset() + self.history.reset() def resize_window(self): min_width = self.common.min_window_width - if self.downloads.isVisible(): + if self.history.isVisible(): min_width += 300 self.adjust_size.emit(min_width) diff --git a/onionshare_gui/mode/share_mode/downloads.py b/onionshare_gui/mode/share_mode/downloads.py deleted file mode 100644 index e78231ad..00000000 --- a/onionshare_gui/mode/share_mode/downloads.py +++ /dev/null @@ -1,248 +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 time -from PyQt5 import QtCore, QtWidgets, QtGui - -from onionshare import strings - - -class Download(QtWidgets.QWidget): - def __init__(self, common, download_id, total_bytes): - super(Download, self).__init__() - self.common = common - - self.download_id = download_id - self.started = time.time() - self.total_bytes = total_bytes - self.downloaded_bytes = 0 - - self.setStyleSheet('QWidget { border: 1px solid red; }') - - # Progress bar - self.progress_bar = QtWidgets.QProgressBar() - self.progress_bar.setTextVisible(True) - self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter) - self.progress_bar.setMinimum(0) - self.progress_bar.setMaximum(total_bytes) - self.progress_bar.setValue(0) - self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar']) - self.progress_bar.total_bytes = total_bytes - - # Layout - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.progress_bar) - self.setLayout(layout) - - # Start at 0 - self.update(0) - - def update(self, downloaded_bytes): - self.downloaded_bytes = downloaded_bytes - - self.progress_bar.setValue(downloaded_bytes) - if downloaded_bytes == self.progress_bar.total_bytes: - pb_fmt = strings._('gui_download_upload_progress_complete').format( - self.common.format_seconds(time.time() - self.started)) - else: - elapsed = time.time() - self.started - if elapsed < 10: - # Wait a couple of seconds for the download rate to stabilize. - # This prevents a "Windows copy dialog"-esque experience at - # the beginning of the download. - pb_fmt = strings._('gui_download_upload_progress_starting').format( - self.common.human_readable_filesize(downloaded_bytes)) - else: - pb_fmt = strings._('gui_download_upload_progress_eta').format( - self.common.human_readable_filesize(downloaded_bytes), - self.estimated_time_remaining) - - self.progress_bar.setFormat(pb_fmt) - - def cancel(self): - self.progress_bar.setFormat(strings._('gui_canceled')) - - @property - def estimated_time_remaining(self): - return self.common.estimated_time_remaining(self.downloaded_bytes, - self.total_bytes, - self.started) - - -class DownloadList(QtWidgets.QScrollArea): - """ - List of download progress bars. - """ - def __init__(self, common): - super(DownloadList, self).__init__() - self.common = common - - self.downloads = {} - - # The layout that holds all of the downloads - self.downloads_layout = QtWidgets.QVBoxLayout() - self.downloads_layout.setContentsMargins(0, 0, 0, 0) - self.downloads_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) - - # Wrapper layout that also contains a stretch - wrapper_layout = QtWidgets.QVBoxLayout() - wrapper_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) - wrapper_layout.addLayout(self.downloads_layout) - wrapper_layout.addStretch() - - # The internal widget of the scroll area - widget = QtWidgets.QWidget() - widget.setLayout(wrapper_layout) - self.setWidget(widget) - self.setWidgetResizable(True) - - # Other scroll area settings - self.setBackgroundRole(QtGui.QPalette.Light) - self.verticalScrollBar().rangeChanged.connect(self.resizeScroll) - - def resizeScroll(self, minimum, maximum): - """ - Scroll to the bottom of the window when the range changes. - """ - self.verticalScrollBar().setValue(maximum) - - def add(self, download_id, content_length): - """ - Add a new download progress bar. - """ - download = Download(self.common, download_id, content_length) - self.downloads[download_id] = download - self.downloads_layout.addWidget(download) - - def update(self, download_id, downloaded_bytes): - """ - Update the progress of a download progress bar. - """ - self.downloads[download_id].update(downloaded_bytes) - - def cancel(self, download_id): - """ - Update a download progress bar to show that it has been canceled. - """ - self.downloads[download_id].cancel() - - def reset(self): - """ - Reset the downloads back to zero - """ - for download in self.downloads.values(): - self.downloads_layout.removeWidget(download) - download.progress_bar.close() - self.downloads = {} - - -class Downloads(QtWidgets.QWidget): - """ - The downloads chunk of the GUI. This lists all of the active download - progress bars. - """ - def __init__(self, common): - super(Downloads, self).__init__() - self.common = common - - self.setMinimumWidth(350) - - # When there are no downloads - empty_image = QtWidgets.QLabel() - empty_image.setAlignment(QtCore.Qt.AlignCenter) - empty_image.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/downloads_transparent.png')))) - empty_text = QtWidgets.QLabel(strings._('gui_no_downloads', True)) - empty_text.setAlignment(QtCore.Qt.AlignCenter) - empty_text.setStyleSheet(self.common.css['downloads_uploads_empty_text']) - empty_layout = QtWidgets.QVBoxLayout() - empty_layout.addStretch() - empty_layout.addWidget(empty_image) - empty_layout.addWidget(empty_text) - empty_layout.addStretch() - self.empty = QtWidgets.QWidget() - self.empty.setStyleSheet(self.common.css['downloads_uploads_empty']) - self.empty.setLayout(empty_layout) - - # When there are downloads - self.download_list = DownloadList(self.common) - - # Download header - downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True)) - downloads_label.setStyleSheet(self.common.css['downloads_uploads_label']) - clear_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) - clear_button.setStyleSheet(self.common.css['downloads_uploads_clear']) - clear_button.setFlat(True) - clear_button.clicked.connect(self.reset) - download_header = QtWidgets.QHBoxLayout() - download_header.addWidget(downloads_label) - download_header.addStretch() - download_header.addWidget(clear_button) - - # Download layout - not_empty_layout = QtWidgets.QVBoxLayout() - not_empty_layout.addLayout(download_header) - not_empty_layout.addWidget(self.download_list) - self.not_empty = QtWidgets.QWidget() - self.not_empty.setLayout(not_empty_layout) - - # Layout - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.empty) - layout.addWidget(self.not_empty) - self.setLayout(layout) - - # Reset once at the beginning - self.reset() - - def add(self, download_id, content_length): - """ - Add a new download progress bar. - """ - self.common.log('Downloads', 'add', 'download_id: {}, content_length: {}'.format(download_id, content_length)) - - # Hide empty, show not empty - self.empty.hide() - self.not_empty.show() - - # Add it to the list - self.download_list.add(download_id, content_length) - - def update(self, download_id, downloaded_bytes): - """ - Update the progress of a download progress bar. - """ - self.download_list.update(download_id, downloaded_bytes) - - def cancel(self, download_id): - """ - Update a download progress bar to show that it has been canceled. - """ - self.download_list.cancel(download_id) - - def reset(self): - """ - Reset the downloads back to zero - """ - self.download_list.reset() - - # Hide not empty, show empty - self.not_empty.hide() - self.empty.show() diff --git a/onionshare_gui/mode/toggle_history.py b/onionshare_gui/mode/toggle_history.py deleted file mode 100644 index 81ecde86..00000000 --- a/onionshare_gui/mode/toggle_history.py +++ /dev/null @@ -1,88 +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 . -""" -from PyQt5 import QtCore, QtWidgets, QtGui - -from onionshare import strings - - -class ToggleHistory(QtWidgets.QPushButton): - """ - Widget for toggling download/upload history on or off, as well as keeping track - of the indicator counter - """ - def __init__(self, common, current_mode, history_widget, icon, selected_icon): - super(ToggleHistory, self).__init__() - self.common = common - self.current_mode = current_mode - self.history_widget = history_widget - self.icon = icon - self.selected_icon = selected_icon - - # Toggle button - self.setDefault(False) - self.setFixedWidth(35) - self.setFixedHeight(30) - self.setFlat(True) - self.setIcon(icon) - self.clicked.connect(self.toggle_clicked) - - # Keep track of indicator - self.indicator_count = 0 - self.indicator_label = QtWidgets.QLabel(parent=self) - self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) - self.update_indicator() - - def update_indicator(self, increment=False): - """ - Update the display of the indicator count. If increment is True, then - only increment the counter if Downloads is hidden. - """ - if increment and not self.history_widget.isVisible(): - self.indicator_count += 1 - - self.indicator_label.setText("{}".format(self.indicator_count)) - - if self.indicator_count == 0: - self.indicator_label.hide() - else: - size = self.indicator_label.sizeHint() - self.indicator_label.setGeometry(35-size.width(), 0, size.width(), size.height()) - self.indicator_label.show() - - def toggle_clicked(self): - """ - Toggle showing and hiding the history widget - """ - self.common.log('ToggleHistory', 'toggle_clicked') - - if self.history_widget.isVisible(): - self.history_widget.hide() - self.setIcon(self.icon) - self.setFlat(True) - else: - self.history_widget.show() - self.setIcon(self.selected_icon) - self.setFlat(False) - - # Reset the indicator count - self.indicator_count = 0 - self.update_indicator() - - self.current_mode.resize_window() From bc573209d9be256822bee00dec6c1376ee197a98 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 18:09:02 -0700 Subject: [PATCH 105/123] Delete Info widget, and move completed and in progress widgets into the header of history --- onionshare_gui/mode/history.py | 68 ++++++++-- onionshare_gui/mode/share_mode/__init__.py | 40 +++--- onionshare_gui/mode/share_mode/info.py | 149 --------------------- share/locale/en.json | 4 +- 4 files changed, 72 insertions(+), 189 deletions(-) delete mode 100644 onionshare_gui/mode/share_mode/info.py diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index 31b4a646..a28340a4 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -179,6 +179,30 @@ class History(QtWidgets.QWidget): self.setMinimumWidth(350) + # In progress and completed counters + self.in_progress_count = 0 + self.completed_count = 0 + + # In progress and completed labels + self.in_progress_label = QtWidgets.QLabel() + self.in_progress_label.setStyleSheet(self.common.css['mode_info_label']) + self.completed_label = QtWidgets.QLabel() + self.completed_label.setStyleSheet(self.common.css['mode_info_label']) + + # Header + self.header_label = QtWidgets.QLabel(header_text) + self.header_label.setStyleSheet(self.common.css['downloads_uploads_label']) + clear_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) + clear_button.setStyleSheet(self.common.css['downloads_uploads_clear']) + clear_button.setFlat(True) + clear_button.clicked.connect(self.reset) + header_layout = QtWidgets.QHBoxLayout() + header_layout.addWidget(self.header_label) + header_layout.addStretch() + header_layout.addWidget(self.in_progress_label) + header_layout.addWidget(self.completed_label) + header_layout.addWidget(clear_button) + # When there are no items self.empty_image = QtWidgets.QLabel() self.empty_image.setAlignment(QtCore.Qt.AlignCenter) @@ -195,22 +219,9 @@ class History(QtWidgets.QWidget): self.empty.setStyleSheet(self.common.css['downloads_uploads_empty']) self.empty.setLayout(empty_layout) - # Header - self.header_label = QtWidgets.QLabel(header_text) - self.header_label.setStyleSheet(self.common.css['downloads_uploads_label']) - clear_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) - clear_button.setStyleSheet(self.common.css['downloads_uploads_clear']) - clear_button.setFlat(True) - clear_button.clicked.connect(self.reset) - header_layout = QtWidgets.QHBoxLayout() - header_layout.addWidget(self.header_label) - header_layout.addStretch() - header_layout.addWidget(clear_button) - # When there are items self.item_list = HistoryItemList(self.common) self.not_empty_layout = QtWidgets.QVBoxLayout() - self.not_empty_layout.addLayout(header_layout) self.not_empty_layout.addWidget(self.item_list) self.not_empty = QtWidgets.QWidget() self.not_empty.setLayout(self.not_empty_layout) @@ -218,12 +229,15 @@ class History(QtWidgets.QWidget): # Layout layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) + layout.addLayout(header_layout) layout.addWidget(self.empty) layout.addWidget(self.not_empty) self.setLayout(layout) # Reset once at the beginning self.reset() + self.update_completed() + self.update_in_progress() def add(self, id, item): """ @@ -261,6 +275,34 @@ class History(QtWidgets.QWidget): self.not_empty.hide() self.empty.show() + # Reset counters + self.completed_count = 0 + self.in_progress_count = 0 + self.update_completed() + self.update_in_progress() + + def update_completed(self): + """ + Update the 'completed' widget. + """ + if self.completed_count == 0: + image = self.common.get_resource_path('images/share_completed_none.png') + else: + image = self.common.get_resource_path('images/share_completed.png') + self.completed_label.setText(' {1:d}'.format(image, self.completed_count)) + self.completed_label.setToolTip(strings._('history_completed_tooltip').format(self.completed_count)) + + def update_in_progress(self): + """ + Update the 'in progress' widget. + """ + if self.in_progress_count == 0: + image = self.common.get_resource_path('images/share_in_progress_none.png') + else: + image = self.common.get_resource_path('images/share_in_progress.png') + self.in_progress_label.setText(' {1:d}'.format(image, self.in_progress_count)) + self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip', True).format(self.in_progress_count)) + class ToggleHistory(QtWidgets.QPushButton): """ diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index bae4bec8..0bf094c0 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -79,11 +79,6 @@ class ShareMode(Mode): strings._('gui_downloads') ) self.history.hide() - self.downloads_in_progress = 0 - self.downloads_completed = 0 - - # Information about share, and show downloads button - #self.info = ShareModeInfo(self.common, self) # Info label self.info_label = QtWidgets.QLabel() @@ -211,9 +206,9 @@ class ShareMode(Mode): self._zip_progress_bar = None self.filesize_warning.hide() - self.downloads_in_progress = 0 - self.downloads_completed = 0 - #self.info.update_downloads_in_progress() + self.history.in_progress_count = 0 + self.history.completed_count = 0 + self.history.update_in_progress() self.file_selection.file_list.adjustSize() def cancel_server_custom(self): @@ -249,8 +244,8 @@ class ShareMode(Mode): item = DownloadHistoryItem(self.common, event["data"]["id"], filesize) self.history.add(event["data"]["id"], item) self.toggle_history.update_indicator(True) - self.downloads_in_progress += 1 - #self.info.update_downloads_in_progress() + self.history.in_progress_count += 1 + self.history.update_in_progress() self.system_tray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True)) @@ -264,12 +259,11 @@ class ShareMode(Mode): if event["data"]["bytes"] == self.web.share_mode.filesize: self.system_tray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True)) - # Update the total 'completed downloads' info - self.downloads_completed += 1 - #self.info.update_downloads_completed() - # Update the 'in progress downloads' info - self.downloads_in_progress -= 1 - #self.info.update_downloads_in_progress() + # Update completed and in progress labels + self.history.completed_count += 1 + self.history.in_progress_count -= 1 + self.history.update_completed() + self.history.update_in_progress() # Close on finish? if self.common.settings.get('close_after_first_download'): @@ -279,8 +273,8 @@ class ShareMode(Mode): else: if self.server_status.status == self.server_status.STATUS_STOPPED: self.history.cancel(event["data"]["id"]) - self.downloads_in_progress = 0 - #self.info.update_downloads_in_progress() + self.history.in_progress_count = 0 + self.history.update_in_progress() def handle_request_canceled(self, event): """ @@ -288,9 +282,9 @@ class ShareMode(Mode): """ self.history.cancel(event["data"]["id"]) - # Update the 'in progress downloads' info - self.downloads_in_progress -= 1 - #self.info.update_downloads_in_progress() + # Update in progress count + self.history.in_progress_count -= 1 + self.history.update_in_progress() self.system_tray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True)) def on_reload_settings(self): @@ -334,10 +328,6 @@ class ShareMode(Mode): """ Set the info counters back to zero. """ - self.downloads_completed = 0 - self.downloads_in_progress = 0 - #self.info.update_downloads_completed() - #self.info.update_downloads_in_progress() self.history.reset() def resize_window(self): diff --git a/onionshare_gui/mode/share_mode/info.py b/onionshare_gui/mode/share_mode/info.py deleted file mode 100644 index c692649c..00000000 --- a/onionshare_gui/mode/share_mode/info.py +++ /dev/null @@ -1,149 +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 . -""" -from PyQt5 import QtCore, QtWidgets, QtGui - -from onionshare import strings - - -class ShareModeInfo(QtWidgets.QWidget): - """ - Share mode information widget - """ - def __init__(self, common, share_mode): - super(ShareModeInfo, self).__init__() - self.common = common - self.share_mode = share_mode - - # Label - self.label_text = "" - self.label = QtWidgets.QLabel() - self.label.setStyleSheet(self.common.css['mode_info_label']) - - # In progress and completed labels - self.in_progress_downloads_count = QtWidgets.QLabel() - self.in_progress_downloads_count.setStyleSheet(self.common.css['mode_info_label']) - self.completed_downloads_count = QtWidgets.QLabel() - self.completed_downloads_count.setStyleSheet(self.common.css['mode_info_label']) - - # Toggle button - self.toggle_button = QtWidgets.QPushButton() - self.toggle_button.setDefault(False) - self.toggle_button.setFixedWidth(35) - self.toggle_button.setFixedHeight(30) - self.toggle_button.setFlat(True) - self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')) ) - self.toggle_button.clicked.connect(self.toggle_downloads) - - # Keep track of indicator - self.indicator_count = 0 - self.indicator_label = QtWidgets.QLabel(parent=self.toggle_button) - self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) - self.update_indicator() - - # Layout - layout = QtWidgets.QHBoxLayout() - layout.addWidget(self.label) - layout.addStretch() - layout.addWidget(self.in_progress_downloads_count) - layout.addWidget(self.completed_downloads_count) - layout.addWidget(self.toggle_button) - self.setLayout(layout) - - self.update_downloads_completed() - self.update_downloads_in_progress() - - def update_label(self, s): - """ - Updates the text of the label. - """ - self.label_text = s - self.label.setText(self.label_text) - - def update_indicator(self, increment=False): - """ - Update the display of the indicator count. If increment is True, then - only increment the counter if Downloads is hidden. - """ - if increment and not self.share_mode.downloads.isVisible(): - self.indicator_count += 1 - - self.indicator_label.setText("{}".format(self.indicator_count)) - - if self.indicator_count == 0: - self.indicator_label.hide() - else: - size = self.indicator_label.sizeHint() - self.indicator_label.setGeometry(35-size.width(), 0, size.width(), size.height()) - self.indicator_label.show() - - def update_downloads_completed(self): - """ - Update the 'Downloads completed' info widget. - """ - if self.share_mode.downloads_completed == 0: - image = self.common.get_resource_path('images/share_completed_none.png') - else: - image = self.common.get_resource_path('images/share_completed.png') - self.completed_downloads_count.setText(' {1:d}'.format(image, self.share_mode.downloads_completed)) - self.completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(self.share_mode.downloads_completed)) - - def update_downloads_in_progress(self): - """ - Update the 'Downloads in progress' info widget. - """ - if self.share_mode.downloads_in_progress == 0: - image = self.common.get_resource_path('images/share_in_progress_none.png') - else: - image = self.common.get_resource_path('images/share_in_progress.png') - self.in_progress_downloads_count.setText(' {1:d}'.format(image, self.share_mode.downloads_in_progress)) - self.in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(self.share_mode.downloads_in_progress)) - - def toggle_downloads(self): - """ - Toggle showing and hiding the Downloads widget - """ - self.common.log('ShareModeInfo', 'toggle_downloads') - - if self.share_mode.downloads.isVisible(): - self.share_mode.downloads.hide() - self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')) ) - self.toggle_button.setFlat(True) - else: - self.share_mode.downloads.show() - self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png')) ) - self.toggle_button.setFlat(False) - - # Reset the indicator count - self.indicator_count = 0 - self.update_indicator() - - self.share_mode.resize_window() - - def show_less(self): - """ - Remove clutter widgets that aren't necessary. - """ - self.label.setText("") - - def show_more(self): - """ - Show all widgets. - """ - self.label.setText(self.label_text) diff --git a/share/locale/en.json b/share/locale/en.json index c7beb6ba..3537b0a2 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -151,8 +151,8 @@ "gui_status_indicator_receive_started": "Receiving", "gui_file_info": "{} files, {}", "gui_file_info_single": "{} file, {}", - "info_in_progress_downloads_tooltip": "{} download(s) in progress", - "info_completed_downloads_tooltip": "{} download(s) completed", + "history_in_progress_tooltip": "{} in progress", + "history_completed_tooltip": "{} completed", "info_in_progress_uploads_tooltip": "{} upload(s) in progress", "info_completed_uploads_tooltip": "{} upload(s) completed", "error_cannot_create_downloads_dir": "Could not create receive mode folder: {}", From 38e62d85288c9947f089b6ba80dc3146178d2a3a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 18:20:13 -0700 Subject: [PATCH 106/123] The History header is now only shown if there are items again, and the clear history button resets everything. Also, reset hides individual items because, for some reason, they still show up otherwise. --- onionshare_gui/mode/history.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index a28340a4..0f8ccdca 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -165,6 +165,7 @@ class HistoryItemList(QtWidgets.QScrollArea): """ for item in self.items.values(): self.items_layout.removeWidget(item) + item.hide() self.items = {} @@ -222,6 +223,7 @@ class History(QtWidgets.QWidget): # When there are items self.item_list = HistoryItemList(self.common) self.not_empty_layout = QtWidgets.QVBoxLayout() + self.not_empty_layout.addLayout(header_layout) self.not_empty_layout.addWidget(self.item_list) self.not_empty = QtWidgets.QWidget() self.not_empty.setLayout(self.not_empty_layout) @@ -229,15 +231,12 @@ class History(QtWidgets.QWidget): # Layout layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) - layout.addLayout(header_layout) layout.addWidget(self.empty) layout.addWidget(self.not_empty) self.setLayout(layout) # Reset once at the beginning self.reset() - self.update_completed() - self.update_in_progress() def add(self, id, item): """ @@ -252,7 +251,6 @@ class History(QtWidgets.QWidget): # Add it to the list self.item_list.add(id, item) - def update(self, id, data): """ Update an item. From 1b1ade63daf937b5de9e40f61fb5f523ca56c81a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 18:49:09 -0700 Subject: [PATCH 107/123] Start fixing the GUI tests. Also, refactor CommonTests to pass in a Mode object instead of the string "share" or "receive" --- tests_gui_local/commontests.py | 182 +++++------------- ...re_share_mode_download_test_public_mode.py | 38 ++-- 2 files changed, 69 insertions(+), 151 deletions(-) diff --git a/tests_gui_local/commontests.py b/tests_gui_local/commontests.py index 21e8cfad..5ceee668 100644 --- a/tests_gui_local/commontests.py +++ b/tests_gui_local/commontests.py @@ -6,6 +6,9 @@ import zipfile from PyQt5 import QtCore, QtTest from onionshare import strings +from onionshare_gui.mode.receive_mode import ReceiveMode +from onionshare_gui.mode.share_mode import ShareMode + class CommonTests(object): def test_gui_loaded(self): @@ -24,71 +27,42 @@ class CommonTests(object): '''Test that the status bar is visible''' self.assertTrue(self.gui.status_bar.isVisible()) - def test_info_widget_shows_less(self, mode): - '''Test that minimum information (no label) is displayed in the info bar''' - if mode == 'share': - self.assertFalse(self.gui.share_mode.info.label.text() == "") - if mode == 'receive': - # There's no minimal display in receive mode - self.assertTrue(False) - def test_click_mode(self, mode): '''Test that we can switch Mode by clicking the button''' - if mode == 'receive': + 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 mode == 'share': + if type(mode) == ShareMode: QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton) self.assertTrue(self.gui.mode, self.gui.MODE_SHARE) def test_click_toggle_history(self, mode): '''Test that we can toggle Download or Upload history by clicking the toggle button''' - if mode == 'receive': - currently_visible = self.gui.receive_mode.uploads.isVisible() - QtTest.QTest.mouseClick(self.gui.receive_mode.info.toggle_button, QtCore.Qt.LeftButton) - self.assertEqual(self.gui.receive_mode.uploads.isVisible(), not currently_visible) - if mode == 'share': - currently_visible = self.gui.receive_mode.uploads.isVisible() - QtTest.QTest.mouseClick(self.gui.share_mode.info.toggle_button, QtCore.Qt.LeftButton) - self.assertEqual(self.gui.share_mode.downloads.isVisible(), not currently_visible) + currently_visible = mode.history.isVisible() + QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton) + self.assertEqual(mode.history.isVisible(), not currently_visible) def test_history_indicator(self, mode, public_mode): '''Test that we can make sure the history is toggled off, do an action, and the indiciator works''' - if mode == 'receive': - # Make sure history is toggled off - if self.gui.receive_mode.uploads.isVisible(): - QtTest.QTest.mouseClick(self.gui.receive_mode.info.toggle_button, QtCore.Qt.LeftButton) - self.assertFalse(self.gui.receive_mode.uploads.isVisible()) + # 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(self.gui.receive_mode.info.indicator_label.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')} if not public_mode: - path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug) + path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, mode.web.slug) else: path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) response = requests.post(path, files=files) QtTest.QTest.qWait(2000) - # Indicator should be visible, have a value of "1" - self.assertTrue(self.gui.receive_mode.info.indicator_label.isVisible()) - self.assertEqual(self.gui.receive_mode.info.indicator_label.text(), "1") - - # Toggle history back on, indicator should be hidden again - QtTest.QTest.mouseClick(self.gui.receive_mode.info.toggle_button, QtCore.Qt.LeftButton) - self.assertFalse(self.gui.receive_mode.info.indicator_label.isVisible()) - - if mode == 'share': - # Make sure history is toggled off - if self.gui.share_mode.downloads.isVisible(): - QtTest.QTest.mouseClick(self.gui.share_mode.info.toggle_button, QtCore.Qt.LeftButton) - self.assertFalse(self.gui.share_mode.downloads.isVisible()) - - # Indicator should not be visible yet - self.assertFalse(self.gui.share_mode.info.indicator_label.isVisible()) - + if type(mode) == ShareMode: # Download files if public_mode: url = "http://127.0.0.1:{}/download".format(self.gui.app.port) @@ -97,44 +71,31 @@ class CommonTests(object): r = requests.get(url) QtTest.QTest.qWait(2000) - # Indicator should be visible, have a value of "1" - self.assertTrue(self.gui.share_mode.info.indicator_label.isVisible()) - self.assertEqual(self.gui.share_mode.info.indicator_label.text(), "1") + # 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(self.gui.share_mode.info.toggle_button, QtCore.Qt.LeftButton) - self.assertFalse(self.gui.share_mode.info.indicator_label.isVisible()) + # 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 test_history_is_not_visible(self, mode): '''Test that the History section is not visible''' - if mode == 'receive': - self.assertFalse(self.gui.receive_mode.uploads.isVisible()) - if mode == 'share': - self.assertFalse(self.gui.share_mode.downloads.isVisible()) + self.assertFalse(mode.history.isVisible()) def test_history_is_visible(self, mode): '''Test that the History section is visible''' - if mode == 'receive': - self.assertTrue(self.gui.receive_mode.uploads.isVisible()) - if mode == 'share': - self.assertTrue(self.gui.share_mode.downloads.isVisible()) + self.assertTrue(mode.history.isVisible()) def test_server_working_on_start_button_pressed(self, mode): '''Test we can start the service''' # Should be in SERVER_WORKING state - if mode == 'receive': - QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEqual(self.gui.receive_mode.server_status.status, 1) - if mode == 'share': - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEqual(self.gui.share_mode.server_status.status, 1) + QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(mode.server_status.status, 1) def test_server_status_indicator_says_starting(self, mode): '''Test that the Server Status indicator shows we are Starting''' - if mode == 'receive': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) - if mode == 'share': - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + self.assertEquals(mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) def test_settings_button_is_hidden(self): '''Test that the settings button is hidden when the server starts''' @@ -144,10 +105,7 @@ class CommonTests(object): '''Test that the server has started''' QtTest.QTest.qWait(2000) # Should now be in SERVER_STARTED state - if mode == 'receive': - self.assertEqual(self.gui.receive_mode.server_status.status, 2) - if mode == 'share': - self.assertEqual(self.gui.share_mode.server_status.status, 2) + self.assertEqual(mode.server_status.status, 2) def test_a_web_server_is_running(self): '''Test that the web server has started''' @@ -157,38 +115,26 @@ class CommonTests(object): def test_have_a_slug(self, mode, public_mode): '''Test that we have a valid slug''' - if mode == 'receive': - if not public_mode: - self.assertRegex(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') - else: - self.assertIsNone(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') - if mode == 'share': - if not public_mode: - self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') - else: - self.assertIsNone(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + if not public_mode: + self.assertRegex(mode.server_status.web.slug, r'(\w+)-(\w+)') + else: + self.assertIsNone(mode.server_status.web.slug, r'(\w+)-(\w+)') def test_url_description_shown(self, mode): '''Test that the URL label is showing''' - if mode == 'receive': - self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) - if mode == 'share': - self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + self.assertTrue(mode.server_status.url_description.isVisible()) def test_have_copy_url_button(self, mode): '''Test that the Copy URL button is shown''' - if mode == 'receive': - self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) - if mode == 'share': - self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + self.assertTrue(mode.server_status.copy_url_button.isVisible()) def test_server_status_indicator_says_started(self, mode): '''Test that the Server Status indicator shows we are started''' - if mode == 'receive': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) - if mode == 'share': - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + if type(mode) == ReceiveMode: + self.assertEquals(mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + if type(mode) == ShareMode: + self.assertEquals(mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) def test_web_page(self, mode, string, public_mode): '''Test that the web page contains a string''' @@ -197,10 +143,7 @@ class CommonTests(object): s.connect(('127.0.0.1', self.gui.app.port)) if not public_mode: - if mode == 'receive': - path = '/{}'.format(self.gui.receive_mode.server_status.web.slug) - if mode == 'share': - path = '/{}'.format(self.gui.share_mode.server_status.web.slug) + path = '/{}'.format(mode.server_status.web.slug) else: path = '/' @@ -223,29 +166,18 @@ class CommonTests(object): def test_history_widgets_present(self, mode): '''Test that the relevant widgets are present in the history view after activity has taken place''' - if mode == 'receive': - self.assertFalse(self.gui.receive_mode.uploads.empty.isVisible()) - self.assertTrue(self.gui.receive_mode.uploads.not_empty.isVisible()) - if mode == 'share': - self.assertFalse(self.gui.share_mode.downloads.empty.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.not_empty.isVisible()) + self.assertFalse(mode.history.empty.isVisible()) + self.assertTrue(mode.history.not_empty.isVisible()) def test_counter_incremented(self, mode, count): '''Test that the counter has incremented''' - if mode == 'receive': - self.assertEquals(self.gui.receive_mode.uploads_completed, count) - if mode == 'share': - self.assertEquals(self.gui.share_mode.downloads_completed, count) + self.assertEquals(mode.uploads_completed, count) def test_server_is_stopped(self, mode, stay_open): '''Test that the server stops when we click Stop''' - if mode == 'receive': - QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.receive_mode.server_status.status, 0) - if mode == 'share': - if stay_open: - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.share_mode.server_status.status, 0) + if type(mode) == ReceiveMode or (type(mode) == ShareMode and stay_open): + QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEquals(mode.server_status.status, 0) def test_web_service_is_stopped(self): '''Test that the web server also stopped''' @@ -257,9 +189,9 @@ class CommonTests(object): def test_server_status_indicator_says_closed(self, mode, stay_open): '''Test that the Server Status indicator shows we closed''' - if mode == 'receive': + if type(mode) == ReceiveMode: self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) - if mode == 'share': + if type(mode) == ShareMode: if stay_open: self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped', True)) else: @@ -269,28 +201,18 @@ class CommonTests(object): def test_set_timeout(self, mode, timeout): '''Test that the timeout can be set''' timer = QtCore.QDateTime.currentDateTime().addSecs(timeout) - if mode == 'receive': - self.gui.receive_mode.server_status.shutdown_timeout.setDateTime(timer) - self.assertTrue(self.gui.receive_mode.server_status.shutdown_timeout.dateTime(), timer) - if mode == 'share': - self.gui.share_mode.server_status.shutdown_timeout.setDateTime(timer) - self.assertTrue(self.gui.share_mode.server_status.shutdown_timeout.dateTime(), timer) + mode.server_status.shutdown_timeout.setDateTime(timer) + self.assertTrue(mode.server_status.shutdown_timeout.dateTime(), timer) def test_timeout_widget_hidden(self, mode): '''Test that the timeout widget is hidden when share has started''' - if mode == 'receive': - self.assertFalse(self.gui.receive_mode.server_status.shutdown_timeout_container.isVisible()) - if mode == 'share': - self.assertFalse(self.gui.share_mode.server_status.shutdown_timeout_container.isVisible()) + self.assertFalse(mode.server_status.shutdown_timeout_container.isVisible()) def test_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 - if mode == 'receive': - self.assertEqual(self.gui.receive_mode.server_status.status, 0) - if mode == 'share': - self.assertEqual(self.gui.share_mode.server_status.status, 0) + self.assertEqual(mode.server_status.status, 0) # Receive-specific tests def test_upload_file(self, public_mode, expected_file): diff --git a/tests_gui_local/onionshare_share_mode_download_test_public_mode.py b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py index a10ee4c2..82f1989c 100644 --- a/tests_gui_local/onionshare_share_mode_download_test_public_mode.py +++ b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py @@ -96,21 +96,17 @@ class OnionShareGuiTest(unittest.TestCase): def test_file_selection_widget_has_a_file(self): CommonTests.test_file_selection_widget_has_a_file(self) - @pytest.mark.run(order=6) - def test_info_widget_shows_less(self): - CommonTests.test_info_widget_shows_less(self, 'share') - @pytest.mark.run(order=7) def test_history_is_not_visible(self): - CommonTests.test_history_is_not_visible(self, 'share') + CommonTests.test_history_is_not_visible(self, self.gui.share_mode) @pytest.mark.run(order=8) def test_click_toggle_history(self): - CommonTests.test_click_toggle_history(self, 'share') + CommonTests.test_click_toggle_history(self, self.gui.share_mode) @pytest.mark.run(order=9) def test_history_is_visible(self): - CommonTests.test_history_is_visible(self, 'share') + CommonTests.test_history_is_visible(self, self.gui.share_mode) @pytest.mark.run(order=10) def test_deleting_only_file_hides_delete_button(self): @@ -126,11 +122,11 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=13) def test_server_working_on_start_button_pressed(self): - CommonTests.test_server_working_on_start_button_pressed(self, 'share') + CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode) @pytest.mark.run(order=14) def test_server_status_indicator_says_starting(self): - CommonTests.test_server_status_indicator_says_starting(self, 'share') + CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode) @pytest.mark.run(order=15) def test_add_delete_buttons_hidden(self): @@ -142,7 +138,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=17) def test_a_server_is_started(self): - CommonTests.test_a_server_is_started(self, 'share') + CommonTests.test_a_server_is_started(self, self.gui.share_mode) @pytest.mark.run(order=18) def test_a_web_server_is_running(self): @@ -150,23 +146,23 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=19) def test_have_a_slug(self): - CommonTests.test_have_a_slug(self, 'share', True) + CommonTests.test_have_a_slug(self, self.gui.share_mode, True) @pytest.mark.run(order=20) def test_url_description_shown(self): - CommonTests.test_url_description_shown(self, 'share') + CommonTests.test_url_description_shown(self, self.gui.share_mode) @pytest.mark.run(order=21) def test_have_copy_url_button(self): - CommonTests.test_have_copy_url_button(self, 'share') + CommonTests.test_have_copy_url_button(self, self.gui.share_mode) @pytest.mark.run(order=22) def test_server_status_indicator_says_started(self): - CommonTests.test_server_status_indicator_says_started(self, 'share') + CommonTests.test_server_status_indicator_says_started(self, self.gui.share_mode) @pytest.mark.run(order=23) def test_web_page(self): - CommonTests.test_web_page(self, 'share', 'Total size', True) + CommonTests.test_web_page(self, self.gui.share_mode, 'Total size', True) @pytest.mark.run(order=24) def test_download_share(self): @@ -174,11 +170,11 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=25) def test_history_widgets_present(self): - CommonTests.test_history_widgets_present(self, 'share') + CommonTests.test_history_widgets_present(self, self.gui.share_mode) @pytest.mark.run(order=26) def test_server_is_stopped(self): - CommonTests.test_server_is_stopped(self, 'share', False) + CommonTests.test_server_is_stopped(self, self.gui.share_mode, False) @pytest.mark.run(order=27) def test_web_service_is_stopped(self): @@ -186,7 +182,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=28) def test_server_status_indicator_says_closed(self): - CommonTests.test_server_status_indicator_says_closed(self, 'share', False) + CommonTests.test_server_status_indicator_says_closed(self, self.gui.share_mode, False) @pytest.mark.run(order=29) def test_add_button_visible(self): @@ -194,9 +190,9 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=30) def test_history_indicator(self): - CommonTests.test_server_working_on_start_button_pressed(self, 'share') - CommonTests.test_a_server_is_started(self, 'share') - CommonTests.test_history_indicator(self, 'share', True) + CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode) + CommonTests.test_a_server_is_started(self, self.gui.share_mode) + CommonTests.test_history_indicator(self, self.gui.share_mode, True) if __name__ == "__main__": From 4d217e84032cd9af64d1a1371ce75a08a5de47e9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 20:37:54 -0700 Subject: [PATCH 108/123] Refactor CommonTests to pass in actual Mode objects, and fix all tests. Now all ShareMode tests pass --- tests_gui_local/commontests.py | 2 +- .../onionshare_receive_mode_upload_test.py | 36 ++++++++-------- ...re_receive_mode_upload_test_public_mode.py | 36 ++++++++-------- .../onionshare_share_mode_download_test.py | 38 ++++++++--------- ...hare_share_mode_download_test_stay_open.py | 42 +++++++++---------- .../onionshare_slug_persistent_test.py | 38 ++++++++--------- tests_gui_local/onionshare_timer_test.py | 18 ++++---- 7 files changed, 97 insertions(+), 113 deletions(-) diff --git a/tests_gui_local/commontests.py b/tests_gui_local/commontests.py index 5ceee668..d311c7bb 100644 --- a/tests_gui_local/commontests.py +++ b/tests_gui_local/commontests.py @@ -171,7 +171,7 @@ class CommonTests(object): def test_counter_incremented(self, mode, count): '''Test that the counter has incremented''' - self.assertEquals(mode.uploads_completed, count) + self.assertEquals(mode.history.completed_count, count) def test_server_is_stopped(self, mode, stay_open): '''Test that the server stops when we click Stop''' diff --git a/tests_gui_local/onionshare_receive_mode_upload_test.py b/tests_gui_local/onionshare_receive_mode_upload_test.py index 19674aa3..1ce91ba2 100644 --- a/tests_gui_local/onionshare_receive_mode_upload_test.py +++ b/tests_gui_local/onionshare_receive_mode_upload_test.py @@ -96,27 +96,27 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=6) def test_click_mode(self): - CommonTests.test_click_mode(self, 'receive') + CommonTests.test_click_mode(self, self.gui.receive_mode) @pytest.mark.run(order=6) def test_history_is_not_visible(self): - CommonTests.test_history_is_not_visible(self, 'receive') + CommonTests.test_history_is_not_visible(self, self.gui.receive_mode) @pytest.mark.run(order=7) def test_click_toggle_history(self): - CommonTests.test_click_toggle_history(self, 'receive') + CommonTests.test_click_toggle_history(self, self.gui.receive_mode) @pytest.mark.run(order=8) def test_history_is_visible(self): - CommonTests.test_history_is_visible(self, 'receive') + CommonTests.test_history_is_visible(self, self.gui.receive_mode) @pytest.mark.run(order=8) def test_server_working_on_start_button_pressed(self): - CommonTests.test_server_working_on_start_button_pressed(self, 'receive') + CommonTests.test_server_working_on_start_button_pressed(self, self.gui.receive_mode) @pytest.mark.run(order=9) def test_server_status_indicator_says_starting(self): - CommonTests.test_server_status_indicator_says_starting(self, 'receive') + CommonTests.test_server_status_indicator_says_starting(self, self.gui.receive_mode) @pytest.mark.run(order=10) def test_settings_button_is_hidden(self): @@ -124,7 +124,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=11) def test_a_server_is_started(self): - CommonTests.test_a_server_is_started(self, 'receive') + CommonTests.test_a_server_is_started(self, self.gui.receive_mode) @pytest.mark.run(order=12) def test_a_web_server_is_running(self): @@ -132,23 +132,23 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=14) def test_have_a_slug(self): - CommonTests.test_have_a_slug(self, 'receive', False) + CommonTests.test_have_a_slug(self, self.gui.receive_mode, False) @pytest.mark.run(order=15) def test_url_description_shown(self): - CommonTests.test_url_description_shown(self, 'receive') + CommonTests.test_url_description_shown(self, self.gui.receive_mode) @pytest.mark.run(order=16) def test_have_copy_url_button(self): - CommonTests.test_have_copy_url_button(self, 'receive') + CommonTests.test_have_copy_url_button(self, self.gui.receive_mode) @pytest.mark.run(order=17) def test_server_status_indicator_says_started(self): - CommonTests.test_server_status_indicator_says_started(self, 'receive') + CommonTests.test_server_status_indicator_says_started(self, self.gui.receive_mode) @pytest.mark.run(order=18) def test_web_page(self): - CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', False) + CommonTests.test_web_page(self, self.gui.receive_mode, 'Select the files you want to send, then click', False) @pytest.mark.run(order=19) def test_upload_file(self): @@ -156,11 +156,11 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=20) def test_history_widgets_present(self): - CommonTests.test_history_widgets_present(self, 'receive') + CommonTests.test_history_widgets_present(self, self.gui.receive_mode) @pytest.mark.run(order=21) def test_counter_incremented(self): - CommonTests.test_counter_incremented(self, 'receive', 1) + CommonTests.test_counter_incremented(self, self.gui.receive_mode, 1) @pytest.mark.run(order=22) def test_upload_same_file_is_renamed(self): @@ -168,15 +168,15 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=23) def test_upload_count_incremented_again(self): - CommonTests.test_counter_incremented(self, 'receive', 2) + CommonTests.test_counter_incremented(self, self.gui.receive_mode, 2) @pytest.mark.run(order=24) def test_history_indicator(self): - CommonTests.test_history_indicator(self, 'receive', False) + CommonTests.test_history_indicator(self, self.gui.receive_mode, False) @pytest.mark.run(order=25) def test_server_is_stopped(self): - CommonTests.test_server_is_stopped(self, 'receive', False) + CommonTests.test_server_is_stopped(self, self.gui.receive_mode, False) @pytest.mark.run(order=26) def test_web_service_is_stopped(self): @@ -184,7 +184,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=27) def test_server_status_indicator_says_closed(self): - CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) + CommonTests.test_server_status_indicator_says_closed(self, self.gui.receive_mode, False) if __name__ == "__main__": unittest.main() diff --git a/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py index e3f85731..6591a884 100644 --- a/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py +++ b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py @@ -96,27 +96,27 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=5) def test_click_mode(self): - CommonTests.test_click_mode(self, 'receive') + CommonTests.test_click_mode(self, self.gui.receive_mode) @pytest.mark.run(order=6) def test_history_is_not_visible(self): - CommonTests.test_history_is_not_visible(self, 'receive') + CommonTests.test_history_is_not_visible(self, self.gui.receive_mode) @pytest.mark.run(order=7) def test_click_toggle_history(self): - CommonTests.test_click_toggle_history(self, 'receive') + CommonTests.test_click_toggle_history(self, self.gui.receive_mode) @pytest.mark.run(order=8) def test_history_is_visible(self): - CommonTests.test_history_is_visible(self, 'receive') + CommonTests.test_history_is_visible(self, self.gui.receive_mode) @pytest.mark.run(order=9) def test_server_working_on_start_button_pressed(self): - CommonTests.test_server_working_on_start_button_pressed(self, 'receive') + CommonTests.test_server_working_on_start_button_pressed(self, self.gui.receive_mode) @pytest.mark.run(order=10) def test_server_status_indicator_says_starting(self): - CommonTests.test_server_status_indicator_says_starting(self, 'receive') + CommonTests.test_server_status_indicator_says_starting(self, self.gui.receive_mode) @pytest.mark.run(order=11) def test_settings_button_is_hidden(self): @@ -124,7 +124,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=12) def test_a_server_is_started(self): - CommonTests.test_a_server_is_started(self, 'receive') + CommonTests.test_a_server_is_started(self, self.gui.receive_mode) @pytest.mark.run(order=13) def test_a_web_server_is_running(self): @@ -132,23 +132,23 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=14) def test_have_a_slug(self): - CommonTests.test_have_a_slug(self, 'receive', True) + CommonTests.test_have_a_slug(self, self.gui.receive_mode, True) @pytest.mark.run(order=15) def test_url_description_shown(self): - CommonTests.test_url_description_shown(self, 'receive') + CommonTests.test_url_description_shown(self, self.gui.receive_mode) @pytest.mark.run(order=16) def test_have_copy_url_button(self): - CommonTests.test_have_copy_url_button(self, 'receive') + CommonTests.test_have_copy_url_button(self, self.gui.receive_mode) @pytest.mark.run(order=17) def test_server_status_indicator_says_started(self): - CommonTests.test_server_status_indicator_says_started(self, 'receive') + CommonTests.test_server_status_indicator_says_started(self, self.gui.receive_mode) @pytest.mark.run(order=18) def test_web_page(self): - CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', True) + CommonTests.test_web_page(self, self.gui.receive_mode, 'Select the files you want to send, then click', True) @pytest.mark.run(order=19) def test_upload_file(self): @@ -156,11 +156,11 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=20) def test_history_widgets_present(self): - CommonTests.test_history_widgets_present(self, 'receive') + CommonTests.test_history_widgets_present(self, self.gui.receive_mode) @pytest.mark.run(order=21) def test_counter_incremented(self): - CommonTests.test_counter_incremented(self, 'receive', 1) + CommonTests.test_counter_incremented(self, self.gui.receive_mode, 1) @pytest.mark.run(order=22) def test_upload_same_file_is_renamed(self): @@ -168,15 +168,15 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=23) def test_upload_count_incremented_again(self): - CommonTests.test_counter_incremented(self, 'receive', 2) + CommonTests.test_counter_incremented(self, self.gui.receive_mode, 2) @pytest.mark.run(order=24) def test_history_indicator(self): - CommonTests.test_history_indicator(self, 'receive', True) + CommonTests.test_history_indicator(self, self.gui.receive_mode, True) @pytest.mark.run(order=25) def test_server_is_stopped(self): - CommonTests.test_server_is_stopped(self, 'receive', False) + CommonTests.test_server_is_stopped(self, self.gui.receive_mode, False) @pytest.mark.run(order=26) def test_web_service_is_stopped(self): @@ -184,7 +184,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=27) def test_server_status_indicator_says_closed(self): - CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) + CommonTests.test_server_status_indicator_says_closed(self, self.gui.receive_mode, False) if __name__ == "__main__": unittest.main() diff --git a/tests_gui_local/onionshare_share_mode_download_test.py b/tests_gui_local/onionshare_share_mode_download_test.py index c4a60101..6842f1a6 100644 --- a/tests_gui_local/onionshare_share_mode_download_test.py +++ b/tests_gui_local/onionshare_share_mode_download_test.py @@ -96,21 +96,17 @@ class OnionShareGuiTest(unittest.TestCase): def test_file_selection_widget_has_a_file(self): CommonTests.test_file_selection_widget_has_a_file(self) - @pytest.mark.run(order=6) - def test_info_widget_shows_less(self): - CommonTests.test_info_widget_shows_less(self, 'share') - @pytest.mark.run(order=7) def test_history_is_not_visible(self): - CommonTests.test_history_is_not_visible(self, 'share') + CommonTests.test_history_is_not_visible(self, self.gui.share_mode) @pytest.mark.run(order=8) def test_click_toggle_history(self): - CommonTests.test_click_toggle_history(self, 'share') + CommonTests.test_click_toggle_history(self, self.gui.share_mode) @pytest.mark.run(order=9) def test_history_is_visible(self): - CommonTests.test_history_is_visible(self, 'share') + CommonTests.test_history_is_visible(self, self.gui.share_mode) @pytest.mark.run(order=10) def test_deleting_only_file_hides_delete_button(self): @@ -126,11 +122,11 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=13) def test_server_working_on_start_button_pressed(self): - CommonTests.test_server_working_on_start_button_pressed(self, 'share') + CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode) @pytest.mark.run(order=14) def test_server_status_indicator_says_starting(self): - CommonTests.test_server_status_indicator_says_starting(self, 'share') + CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode) @pytest.mark.run(order=15) def test_add_delete_buttons_hidden(self): @@ -142,7 +138,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=17) def test_a_server_is_started(self): - CommonTests.test_a_server_is_started(self, 'share') + CommonTests.test_a_server_is_started(self, self.gui.share_mode) @pytest.mark.run(order=18) def test_a_web_server_is_running(self): @@ -150,23 +146,23 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=19) def test_have_a_slug(self): - CommonTests.test_have_a_slug(self, 'share', False) + CommonTests.test_have_a_slug(self, self.gui.share_mode, False) @pytest.mark.run(order=20) def test_url_description_shown(self): - CommonTests.test_url_description_shown(self, 'share') + CommonTests.test_url_description_shown(self, self.gui.share_mode) @pytest.mark.run(order=21) def test_have_copy_url_button(self): - CommonTests.test_have_copy_url_button(self, 'share') + CommonTests.test_have_copy_url_button(self, self.gui.share_mode) @pytest.mark.run(order=22) def test_server_status_indicator_says_started(self): - CommonTests.test_server_status_indicator_says_started(self, 'share') + CommonTests.test_server_status_indicator_says_started(self, self.gui.share_mode) @pytest.mark.run(order=23) def test_web_page(self): - CommonTests.test_web_page(self, 'share', 'Total size', False) + CommonTests.test_web_page(self, self.gui.share_mode, 'Total size', False) @pytest.mark.run(order=24) def test_download_share(self): @@ -174,11 +170,11 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=25) def test_history_widgets_present(self): - CommonTests.test_history_widgets_present(self, 'share') + CommonTests.test_history_widgets_present(self, self.gui.share_mode) @pytest.mark.run(order=26) def test_server_is_stopped(self): - CommonTests.test_server_is_stopped(self, 'share', False) + CommonTests.test_server_is_stopped(self, self.gui.share_mode, False) @pytest.mark.run(order=27) def test_web_service_is_stopped(self): @@ -186,7 +182,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=28) def test_server_status_indicator_says_closed(self): - CommonTests.test_server_status_indicator_says_closed(self, 'share', False) + CommonTests.test_server_status_indicator_says_closed(self, self.gui.share_mode, False) @pytest.mark.run(order=29) def test_add_button_visible(self): @@ -194,9 +190,9 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=30) def test_history_indicator(self): - CommonTests.test_server_working_on_start_button_pressed(self, 'share') - CommonTests.test_a_server_is_started(self, 'share') - CommonTests.test_history_indicator(self, 'share', False) + CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode) + CommonTests.test_a_server_is_started(self, self.gui.share_mode) + CommonTests.test_history_indicator(self, self.gui.share_mode, False) if __name__ == "__main__": diff --git a/tests_gui_local/onionshare_share_mode_download_test_stay_open.py b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py index 8426c264..df9bc857 100644 --- a/tests_gui_local/onionshare_share_mode_download_test_stay_open.py +++ b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py @@ -96,21 +96,17 @@ class OnionShareGuiTest(unittest.TestCase): def test_file_selection_widget_has_a_file(self): CommonTests.test_file_selection_widget_has_a_file(self) - @pytest.mark.run(order=6) - def test_info_widget_shows_less(self): - CommonTests.test_info_widget_shows_less(self, 'share') - @pytest.mark.run(order=7) def test_history_is_not_visible(self): - CommonTests.test_history_is_not_visible(self, 'share') + CommonTests.test_history_is_not_visible(self, self.gui.share_mode) @pytest.mark.run(order=8) def test_click_toggle_history(self): - CommonTests.test_click_toggle_history(self, 'share') + CommonTests.test_click_toggle_history(self, self.gui.share_mode) @pytest.mark.run(order=9) def test_history_is_visible(self): - CommonTests.test_history_is_visible(self, 'share') + CommonTests.test_history_is_visible(self, self.gui.share_mode) @pytest.mark.run(order=10) def test_deleting_only_file_hides_delete_button(self): @@ -126,11 +122,11 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=13) def test_server_working_on_start_button_pressed(self): - CommonTests.test_server_working_on_start_button_pressed(self, 'share') + CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode) @pytest.mark.run(order=14) def test_server_status_indicator_says_starting(self): - CommonTests.test_server_status_indicator_says_starting(self, 'share') + CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode) @pytest.mark.run(order=15) def test_add_delete_buttons_hidden(self): @@ -142,7 +138,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=17) def test_a_server_is_started(self): - CommonTests.test_a_server_is_started(self, 'share') + CommonTests.test_a_server_is_started(self, self.gui.share_mode) @pytest.mark.run(order=18) def test_a_web_server_is_running(self): @@ -150,23 +146,23 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=19) def test_have_a_slug(self): - CommonTests.test_have_a_slug(self, 'share', True) + CommonTests.test_have_a_slug(self, self.gui.share_mode, True) @pytest.mark.run(order=20) def test_url_description_shown(self): - CommonTests.test_url_description_shown(self, 'share') + CommonTests.test_url_description_shown(self, self.gui.share_mode) @pytest.mark.run(order=21) def test_have_copy_url_button(self): - CommonTests.test_have_copy_url_button(self, 'share') + CommonTests.test_have_copy_url_button(self, self.gui.share_mode) @pytest.mark.run(order=22) def test_server_status_indicator_says_started(self): - CommonTests.test_server_status_indicator_says_started(self, 'share') + CommonTests.test_server_status_indicator_says_started(self, self.gui.share_mode) @pytest.mark.run(order=23) def test_web_page(self): - CommonTests.test_web_page(self, 'share', 'Total size', True) + CommonTests.test_web_page(self, self.gui.share_mode, 'Total size', True) @pytest.mark.run(order=24) def test_download_share(self): @@ -174,11 +170,11 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=25) def test_history_widgets_present(self): - CommonTests.test_history_widgets_present(self, 'share') + CommonTests.test_history_widgets_present(self, self.gui.share_mode) @pytest.mark.run(order=26) def test_counter_incremented(self): - CommonTests.test_counter_incremented(self, 'share', 1) + CommonTests.test_counter_incremented(self, self.gui.share_mode, 1) @pytest.mark.run(order=27) def test_download_share_again(self): @@ -186,11 +182,11 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=28) def test_counter_incremented_again(self): - CommonTests.test_counter_incremented(self, 'share', 2) + CommonTests.test_counter_incremented(self, self.gui.share_mode, 2) @pytest.mark.run(order=29) def test_server_is_stopped(self): - CommonTests.test_server_is_stopped(self, 'share', True) + CommonTests.test_server_is_stopped(self, self.gui.share_mode, True) @pytest.mark.run(order=30) def test_web_service_is_stopped(self): @@ -198,7 +194,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=31) def test_server_status_indicator_says_closed(self): - CommonTests.test_server_status_indicator_says_closed(self, 'share', True) + CommonTests.test_server_status_indicator_says_closed(self, self.gui.share_mode, True) @pytest.mark.run(order=32) def test_add_button_visible(self): @@ -206,9 +202,9 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=33) def test_history_indicator(self): - CommonTests.test_server_working_on_start_button_pressed(self, 'share') - CommonTests.test_a_server_is_started(self, 'share') - CommonTests.test_history_indicator(self, 'share', True) + CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode) + CommonTests.test_a_server_is_started(self, self.gui.share_mode) + CommonTests.test_history_indicator(self, self.gui.share_mode, True) if __name__ == "__main__": diff --git a/tests_gui_local/onionshare_slug_persistent_test.py b/tests_gui_local/onionshare_slug_persistent_test.py index 9fb623dd..5b825dad 100644 --- a/tests_gui_local/onionshare_slug_persistent_test.py +++ b/tests_gui_local/onionshare_slug_persistent_test.py @@ -94,29 +94,25 @@ class OnionShareGuiTest(unittest.TestCase): def test_server_status_bar_is_visible(self): CommonTests.test_server_status_bar_is_visible(self) - @pytest.mark.run(order=6) - def test_info_widget_shows_less(self): - CommonTests.test_info_widget_shows_less(self, 'share') - @pytest.mark.run(order=7) def test_history_is_not_visible(self): - CommonTests.test_history_is_not_visible(self, 'share') + CommonTests.test_history_is_not_visible(self, self.gui.share_mode) @pytest.mark.run(order=8) def test_click_toggle_history(self): - CommonTests.test_click_toggle_history(self, 'share') + CommonTests.test_click_toggle_history(self, self.gui.share_mode) @pytest.mark.run(order=9) def test_history_is_visible(self): - CommonTests.test_history_is_visible(self, 'share') + CommonTests.test_history_is_visible(self, self.gui.share_mode) @pytest.mark.run(order=10) def test_server_working_on_start_button_pressed(self): - CommonTests.test_server_working_on_start_button_pressed(self, 'share') + CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode) @pytest.mark.run(order=11) def test_server_status_indicator_says_starting(self): - CommonTests.test_server_status_indicator_says_starting(self, 'share') + CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode) @pytest.mark.run(order=12) def test_settings_button_is_hidden(self): @@ -124,7 +120,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=13) def test_a_server_is_started(self): - CommonTests.test_a_server_is_started(self, 'share') + CommonTests.test_a_server_is_started(self, self.gui.share_mode) @pytest.mark.run(order=14) def test_a_web_server_is_running(self): @@ -132,17 +128,17 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=15) def test_have_a_slug(self): - CommonTests.test_have_a_slug(self, 'share', False) + CommonTests.test_have_a_slug(self, self.gui.share_mode, False) global slug slug = self.gui.share_mode.server_status.web.slug @pytest.mark.run(order=16) def test_server_status_indicator_says_started(self): - CommonTests.test_server_status_indicator_says_started(self, 'share') + CommonTests.test_server_status_indicator_says_started(self, self.gui.share_mode) @pytest.mark.run(order=17) def test_server_is_stopped(self): - CommonTests.test_server_is_stopped(self, 'share', True) + CommonTests.test_server_is_stopped(self, self.gui.share_mode, True) @pytest.mark.run(order=18) def test_web_service_is_stopped(self): @@ -150,13 +146,13 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=19) def test_server_status_indicator_says_closed(self): - CommonTests.test_server_status_indicator_says_closed(self, 'share', True) + CommonTests.test_server_status_indicator_says_closed(self, self.gui.share_mode, True) @pytest.mark.run(order=20) def test_server_started_again(self): - CommonTests.test_server_working_on_start_button_pressed(self, 'share') - CommonTests.test_server_status_indicator_says_starting(self, 'share') - CommonTests.test_a_server_is_started(self, 'share') + CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode) + CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode) + CommonTests.test_a_server_is_started(self, self.gui.share_mode) @pytest.mark.run(order=21) def test_have_same_slug(self): @@ -165,14 +161,14 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=22) def test_server_is_stopped_again(self): - CommonTests.test_server_is_stopped(self, 'share', True) + CommonTests.test_server_is_stopped(self, self.gui.share_mode, True) CommonTests.test_web_service_is_stopped(self) @pytest.mark.run(order=23) def test_history_indicator(self): - CommonTests.test_server_working_on_start_button_pressed(self, 'share') - CommonTests.test_a_server_is_started(self, 'share') - CommonTests.test_history_indicator(self, 'share', False) + CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode) + CommonTests.test_a_server_is_started(self, self.gui.share_mode) + CommonTests.test_history_indicator(self, self.gui.share_mode, False) if __name__ == "__main__": diff --git a/tests_gui_local/onionshare_timer_test.py b/tests_gui_local/onionshare_timer_test.py index 701d9a21..4aaaf364 100644 --- a/tests_gui_local/onionshare_timer_test.py +++ b/tests_gui_local/onionshare_timer_test.py @@ -96,29 +96,25 @@ class OnionShareGuiTest(unittest.TestCase): def test_file_selection_widget_has_a_file(self): CommonTests.test_file_selection_widget_has_a_file(self) - @pytest.mark.run(order=6) - def test_info_widget_shows_less(self): - CommonTests.test_info_widget_shows_less(self, 'share') - @pytest.mark.run(order=7) def test_history_is_not_visible(self): - CommonTests.test_history_is_not_visible(self, 'share') + CommonTests.test_history_is_not_visible(self, self.gui.share_mode) @pytest.mark.run(order=8) def test_set_timeout(self): - CommonTests.test_set_timeout(self, 'share', 5) + CommonTests.test_set_timeout(self, self.gui.share_mode, 5) @pytest.mark.run(order=9) def test_server_working_on_start_button_pressed(self): - CommonTests.test_server_working_on_start_button_pressed(self, 'share') + CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode) @pytest.mark.run(order=10) def test_server_status_indicator_says_starting(self): - CommonTests.test_server_status_indicator_says_starting(self, 'share') + CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode) @pytest.mark.run(order=11) def test_a_server_is_started(self): - CommonTests.test_a_server_is_started(self, 'share') + CommonTests.test_a_server_is_started(self, self.gui.share_mode) @pytest.mark.run(order=12) def test_a_web_server_is_running(self): @@ -126,11 +122,11 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=13) def test_timeout_widget_hidden(self): - CommonTests.test_timeout_widget_hidden(self, 'share') + CommonTests.test_timeout_widget_hidden(self, self.gui.share_mode) @pytest.mark.run(order=14) def test_timeout(self): - CommonTests.test_server_timed_out(self, 'share', 10000) + CommonTests.test_server_timed_out(self, self.gui.share_mode, 10000) @pytest.mark.run(order=15) def test_web_service_is_stopped(self): From c9beb694f2aaf3d9afcc7272f73eaf4eabe6a603 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 21:14:20 -0700 Subject: [PATCH 109/123] Update ReceiveMode to use History directly, and now all GUI tests pass --- onionshare_gui/mode/history.py | 182 +++++++++ onionshare_gui/mode/receive_mode/__init__.py | 97 ++--- onionshare_gui/mode/receive_mode/uploads.py | 395 ------------------- 3 files changed, 234 insertions(+), 440 deletions(-) delete mode 100644 onionshare_gui/mode/receive_mode/uploads.py diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index 0f8ccdca..4f5b2cef 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -18,6 +18,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ import time +import subprocess +from datetime import datetime from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings @@ -103,6 +105,186 @@ class DownloadHistoryItem(HistoryItem): self.started) +class UploadHistoryItemFile(QtWidgets.QWidget): + def __init__(self, common, filename): + super(UploadHistoryItemFile, self).__init__() + self.common = common + + self.common.log('UploadHistoryItemFile', '__init__', 'filename: {}'.format(filename)) + + self.filename = filename + self.started = datetime.now() + + # Filename label + self.filename_label = QtWidgets.QLabel(self.filename) + self.filename_label_width = self.filename_label.width() + + # File size label + self.filesize_label = QtWidgets.QLabel() + self.filesize_label.setStyleSheet(self.common.css['receive_file_size']) + self.filesize_label.hide() + + # Folder button + folder_pixmap = QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/open_folder.png'))) + folder_icon = QtGui.QIcon(folder_pixmap) + self.folder_button = QtWidgets.QPushButton() + self.folder_button.clicked.connect(self.open_folder) + self.folder_button.setIcon(folder_icon) + self.folder_button.setIconSize(folder_pixmap.rect().size()) + self.folder_button.setFlat(True) + self.folder_button.hide() + + # Layouts + layout = QtWidgets.QHBoxLayout() + layout.addWidget(self.filename_label) + layout.addWidget(self.filesize_label) + layout.addStretch() + layout.addWidget(self.folder_button) + self.setLayout(layout) + + def update(self, uploaded_bytes, complete): + self.filesize_label.setText(self.common.human_readable_filesize(uploaded_bytes)) + self.filesize_label.show() + + if complete: + self.folder_button.show() + + def rename(self, new_filename): + self.filename = new_filename + self.filename_label.setText(self.filename) + + def open_folder(self): + """ + Open the downloads folder, with the file selected, in a cross-platform manner + """ + self.common.log('UploadHistoryItemFile', 'open_folder') + + abs_filename = os.path.join(self.common.settings.get('downloads_dir'), self.filename) + + # Linux + if self.common.platform == 'Linux' or self.common.platform == 'BSD': + try: + # If nautilus is available, open it + subprocess.Popen(['nautilus', abs_filename]) + except: + Alert(self.common, strings._('gui_open_folder_error_nautilus').format(abs_filename)) + + # macOS + elif self.common.platform == 'Darwin': + # TODO: Implement opening folder with file selected in macOS + # This seems helpful: https://stackoverflow.com/questions/3520493/python-show-in-finder + self.common.log('UploadHistoryItemFile', 'open_folder', 'not implemented for Darwin yet') + + # Windows + elif self.common.platform == 'Windows': + # TODO: Implement opening folder with file selected in Windows + # This seems helpful: https://stackoverflow.com/questions/6631299/python-opening-a-folder-in-explorer-nautilus-mac-thingie + self.common.log('UploadHistoryItemFile', 'open_folder', 'not implemented for Windows yet') + + +class UploadHistoryItem(HistoryItem): + def __init__(self, common, id, content_length): + super(UploadHistoryItem, self).__init__() + self.common = common + self.id = id + self.content_length = content_length + self.started = datetime.now() + + # Label + self.label = QtWidgets.QLabel(strings._('gui_upload_in_progress', True).format(self.started.strftime("%b %d, %I:%M%p"))) + + # Progress bar + self.progress_bar = QtWidgets.QProgressBar() + self.progress_bar.setTextVisible(True) + self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter) + self.progress_bar.setMinimum(0) + self.progress_bar.setValue(0) + self.progress_bar.setStyleSheet(self.common.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.setLayout(self.files_layout) + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.label) + layout.addWidget(self.progress_bar) + layout.addWidget(files_widget) + layout.addStretch() + self.setLayout(layout) + + # We're also making a dictionary of file widgets, to make them easier to access + self.files = {} + + def update(self, data): + """ + Using the progress from Web, update the progress bar and file size labels + for each file + """ + if data['action'] == 'progress': + total_uploaded_bytes = 0 + for filename in data['progress']: + total_uploaded_bytes += data['progress'][filename]['uploaded_bytes'] + + # Update the progress bar + self.progress_bar.setMaximum(self.content_length) + self.progress_bar.setValue(total_uploaded_bytes) + + elapsed = datetime.now() - self.started + if elapsed.seconds < 10: + pb_fmt = strings._('gui_download_upload_progress_starting').format( + self.common.human_readable_filesize(total_uploaded_bytes)) + else: + estimated_time_remaining = self.common.estimated_time_remaining( + total_uploaded_bytes, + self.content_length, + self.started.timestamp()) + pb_fmt = strings._('gui_download_upload_progress_eta').format( + self.common.human_readable_filesize(total_uploaded_bytes), + estimated_time_remaining) + + # Using list(progress) to avoid "RuntimeError: dictionary changed size during iteration" + for filename in list(data['progress']): + # Add a new file if needed + if filename not in self.files: + self.files[filename] = UploadHistoryItemFile(self.common, filename) + self.files_layout.addWidget(self.files[filename]) + + # Update the file + self.files[filename].update(data['progress'][filename]['uploaded_bytes'], data['progress'][filename]['complete']) + + elif data['action'] == 'rename': + self.files[data['old_filename']].rename(data['new_filename']) + self.files[data['new_filename']] = self.files.pop(data['old_filename']) + + elif data['action'] == 'finished': + # Hide the progress bar + self.progress_bar.hide() + + # Change the label + self.ended = self.started = datetime.now() + if self.started.year == self.ended.year and self.started.month == self.ended.month and self.started.day == self.ended.day: + if self.started.hour == self.ended.hour and self.started.minute == self.ended.minute: + text = strings._('gui_upload_finished', True).format( + self.started.strftime("%b %d, %I:%M%p") + ) + else: + text = strings._('gui_upload_finished_range', True).format( + self.started.strftime("%b %d, %I:%M%p"), + self.ended.strftime("%I:%M%p") + ) + else: + text = strings._('gui_upload_finished_range', True).format( + self.started.strftime("%b %d, %I:%M%p"), + self.ended.strftime("%b %d, %I:%M%p") + ) + self.label.setText(text) + + class HistoryItemList(QtWidgets.QScrollArea): """ List of items diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py index 96c76dbf..ffa259e7 100644 --- a/onionshare_gui/mode/receive_mode/__init__.py +++ b/onionshare_gui/mode/receive_mode/__init__.py @@ -22,8 +22,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.web import Web -from .uploads import Uploads -from .info import ReceiveModeInfo +from ..history import History, ToggleHistory, UploadHistoryItem from .. import Mode class ReceiveMode(Mode): @@ -47,26 +46,36 @@ class ReceiveMode(Mode): self.server_status.web = self.web self.server_status.update() - # Uploads - self.uploads = Uploads(self.common) - self.uploads.hide() - self.uploads_in_progress = 0 - self.uploads_completed = 0 - self.new_upload = False # For scrolling to the bottom of the uploads list + # Upload history + self.history = History( + self.common, + QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/uploads_transparent.png'))), + strings._('gui_no_uploads'), + strings._('gui_uploads') + ) + self.history.hide() - # Information about share, and show uploads button - self.info = ReceiveModeInfo(self.common, self) - self.info.show_less() + # Toggle history + self.toggle_history = ToggleHistory( + self.common, self, self.history, + QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')), + QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle_selected.png')) + ) - # Receive mode info - self.receive_info = QtWidgets.QLabel(strings._('gui_receive_mode_warning', True)) - self.receive_info.setMinimumHeight(80) - self.receive_info.setWordWrap(True) + # Receive mode warning + receive_warning = QtWidgets.QLabel(strings._('gui_receive_mode_warning', True)) + receive_warning.setMinimumHeight(80) + receive_warning.setWordWrap(True) + + # Top bar + top_bar_layout = QtWidgets.QHBoxLayout() + top_bar_layout.addStretch() + top_bar_layout.addWidget(self.toggle_history) # Main layout self.main_layout = QtWidgets.QVBoxLayout() - self.main_layout.addWidget(self.info) - self.main_layout.addWidget(self.receive_info) + 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) @@ -74,7 +83,7 @@ class ReceiveMode(Mode): # Wrapper layout self.wrapper_layout = QtWidgets.QHBoxLayout() self.wrapper_layout.addLayout(self.main_layout) - self.wrapper_layout.addWidget(self.uploads) + self.wrapper_layout.addWidget(self.history) self.setLayout(self.wrapper_layout) def get_stop_server_shutdown_timeout_text(self): @@ -114,7 +123,7 @@ class ReceiveMode(Mode): Connection to Tor broke. """ self.primary_action.hide() - self.info.show_less() + #self.info.show_less() def handle_request_load(self, event): """ @@ -126,10 +135,11 @@ class ReceiveMode(Mode): """ Handle REQUEST_STARTED event. """ - self.uploads.add(event["data"]["id"], event["data"]["content_length"]) - self.info.update_indicator(True) - self.uploads_in_progress += 1 - self.info.update_uploads_in_progress() + item = UploadHistoryItem(self.common, event["data"]["id"], event["data"]["content_length"]) + self.history.add(event["data"]["id"], item) + self.toggle_history.update_indicator(True) + self.history.in_progress_count += 1 + self.history.update_in_progress() self.system_tray.showMessage(strings._('systray_upload_started_title', True), strings._('systray_upload_started_message', True)) @@ -137,7 +147,10 @@ class ReceiveMode(Mode): """ Handle REQUEST_PROGRESS event. """ - self.uploads.update(event["data"]["id"], event["data"]["progress"]) + self.history.update(event["data"]["id"], { + 'action': 'progress', + 'progress': event["data"]["progress"] + }) def handle_request_close_server(self, event): """ @@ -150,51 +163,45 @@ class ReceiveMode(Mode): """ Handle REQUEST_UPLOAD_FILE_RENAMED event. """ - self.uploads.rename(event["data"]["id"], event["data"]["old_filename"], event["data"]["new_filename"]) + self.history.update(event["data"]["id"], { + 'action': 'rename', + 'old_filename': event["data"]["old_filename"], + 'new_filename': event["data"]["new_filename"] + }) def handle_request_upload_finished(self, event): """ Handle REQUEST_UPLOAD_FINISHED event. """ - self.uploads.finished(event["data"]["id"]) - # Update the total 'completed uploads' info - self.uploads_completed += 1 - self.info.update_uploads_completed() - # Update the 'in progress uploads' info - self.uploads_in_progress -= 1 - self.info.update_uploads_in_progress() + self.history.update(event["data"]["id"], { + 'action': 'finished' + }) + self.history.completed_count += 1 + self.history.in_progress_count -= 1 + self.history.update_completed() + self.history.update_in_progress() def on_reload_settings(self): """ We should be ok to re-enable the 'Start Receive Mode' button now. """ self.primary_action.show() - self.info.show_more() + #self.info.show_more() def reset_info_counters(self): """ Set the info counters back to zero. """ - self.uploads_completed = 0 - self.uploads_in_progress = 0 - self.info.update_uploads_completed() - self.info.update_uploads_in_progress() - self.uploads.reset() + self.history.reset() def update_primary_action(self): self.common.log('ReceiveMode', 'update_primary_action') - # Show the info widget when the server is active - if self.server_status.status == self.server_status.STATUS_STARTED: - self.info.show_more() - else: - self.info.show_less() - # Resize window self.resize_window() def resize_window(self): min_width = self.common.min_window_width - if self.uploads.isVisible(): + if self.history.isVisible(): min_width += 300 self.adjust_size.emit(min_width) diff --git a/onionshare_gui/mode/receive_mode/uploads.py b/onionshare_gui/mode/receive_mode/uploads.py deleted file mode 100644 index c445be47..00000000 --- a/onionshare_gui/mode/receive_mode/uploads.py +++ /dev/null @@ -1,395 +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 os -import subprocess -import textwrap -from datetime import datetime -from PyQt5 import QtCore, QtWidgets, QtGui - -from onionshare import strings -from ...widgets import Alert - - -class File(QtWidgets.QWidget): - def __init__(self, common, filename): - super(File, self).__init__() - self.common = common - - self.common.log('File', '__init__', 'filename: {}'.format(filename)) - - self.filename = filename - self.started = datetime.now() - - # Filename label - self.filename_label = QtWidgets.QLabel(self.filename) - self.filename_label_width = self.filename_label.width() - - # File size label - self.filesize_label = QtWidgets.QLabel() - self.filesize_label.setStyleSheet(self.common.css['receive_file_size']) - self.filesize_label.hide() - - # Folder button - folder_pixmap = QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/open_folder.png'))) - folder_icon = QtGui.QIcon(folder_pixmap) - self.folder_button = QtWidgets.QPushButton() - self.folder_button.clicked.connect(self.open_folder) - self.folder_button.setIcon(folder_icon) - self.folder_button.setIconSize(folder_pixmap.rect().size()) - self.folder_button.setFlat(True) - self.folder_button.hide() - - # Layouts - layout = QtWidgets.QHBoxLayout() - layout.addWidget(self.filename_label) - layout.addWidget(self.filesize_label) - layout.addStretch() - layout.addWidget(self.folder_button) - self.setLayout(layout) - - def update(self, uploaded_bytes, complete): - self.filesize_label.setText(self.common.human_readable_filesize(uploaded_bytes)) - self.filesize_label.show() - - if complete: - self.folder_button.show() - - def rename(self, new_filename): - self.filename = new_filename - self.filename_label.setText(self.filename) - - def open_folder(self): - """ - Open the downloads folder, with the file selected, in a cross-platform manner - """ - self.common.log('File', 'open_folder') - - abs_filename = os.path.join(self.common.settings.get('downloads_dir'), self.filename) - - # Linux - if self.common.platform == 'Linux' or self.common.platform == 'BSD': - try: - # If nautilus is available, open it - subprocess.Popen(['nautilus', abs_filename]) - except: - Alert(self.common, strings._('gui_open_folder_error_nautilus').format(abs_filename)) - - # macOS - elif self.common.platform == 'Darwin': - # TODO: Implement opening folder with file selected in macOS - # This seems helpful: https://stackoverflow.com/questions/3520493/python-show-in-finder - self.common.log('File', 'open_folder', 'not implemented for Darwin yet') - - # Windows - elif self.common.platform == 'Windows': - # TODO: Implement opening folder with file selected in Windows - # This seems helpful: https://stackoverflow.com/questions/6631299/python-opening-a-folder-in-explorer-nautilus-mac-thingie - self.common.log('File', 'open_folder', 'not implemented for Windows yet') - - -class Upload(QtWidgets.QWidget): - def __init__(self, common, upload_id, content_length): - super(Upload, self).__init__() - self.common = common - self.upload_id = upload_id - self.content_length = content_length - self.started = datetime.now() - - # Label - self.label = QtWidgets.QLabel(strings._('gui_upload_in_progress', True).format(self.started.strftime("%b %d, %I:%M%p"))) - - # Progress bar - self.progress_bar = QtWidgets.QProgressBar() - self.progress_bar.setTextVisible(True) - self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter) - self.progress_bar.setMinimum(0) - self.progress_bar.setValue(0) - self.progress_bar.setStyleSheet(self.common.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.setLayout(self.files_layout) - - # Layout - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.label) - layout.addWidget(self.progress_bar) - layout.addWidget(files_widget) - layout.addStretch() - self.setLayout(layout) - - # We're also making a dictionary of file widgets, to make them easier to access - self.files = {} - - def update(self, progress): - """ - Using the progress from Web, update the progress bar and file size labels - for each file - """ - total_uploaded_bytes = 0 - for filename in progress: - total_uploaded_bytes += progress[filename]['uploaded_bytes'] - - # Update the progress bar - self.progress_bar.setMaximum(self.content_length) - self.progress_bar.setValue(total_uploaded_bytes) - - elapsed = datetime.now() - self.started - if elapsed.seconds < 10: - pb_fmt = strings._('gui_download_upload_progress_starting').format( - self.common.human_readable_filesize(total_uploaded_bytes)) - else: - estimated_time_remaining = self.common.estimated_time_remaining( - total_uploaded_bytes, - self.content_length, - self.started.timestamp()) - pb_fmt = strings._('gui_download_upload_progress_eta').format( - self.common.human_readable_filesize(total_uploaded_bytes), - estimated_time_remaining) - - # Using list(progress) to avoid "RuntimeError: dictionary changed size during iteration" - for filename in list(progress): - # Add a new file if needed - if filename not in self.files: - self.files[filename] = File(self.common, filename) - self.files_layout.addWidget(self.files[filename]) - - # Update the file - self.files[filename].update(progress[filename]['uploaded_bytes'], progress[filename]['complete']) - - def rename(self, old_filename, new_filename): - self.files[old_filename].rename(new_filename) - self.files[new_filename] = self.files.pop(old_filename) - - def finished(self): - # Hide the progress bar - self.progress_bar.hide() - - # Change the label - self.ended = self.started = datetime.now() - if self.started.year == self.ended.year and self.started.month == self.ended.month and self.started.day == self.ended.day: - if self.started.hour == self.ended.hour and self.started.minute == self.ended.minute: - text = strings._('gui_upload_finished', True).format( - self.started.strftime("%b %d, %I:%M%p") - ) - else: - text = strings._('gui_upload_finished_range', True).format( - self.started.strftime("%b %d, %I:%M%p"), - self.ended.strftime("%I:%M%p") - ) - else: - text = strings._('gui_upload_finished_range', True).format( - self.started.strftime("%b %d, %I:%M%p"), - self.ended.strftime("%b %d, %I:%M%p") - ) - self.label.setText(text) - - -class UploadList(QtWidgets.QScrollArea): - """ - List of upload progess bars. - """ - def __init__(self, common): - super(UploadList, self).__init__() - self.common = common - - self.uploads = {} - - # The layout that holds all of the uploads - self.uploads_layout = QtWidgets.QVBoxLayout() - self.uploads_layout.setContentsMargins(0, 0, 0, 0) - self.uploads_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) - - # Wrapper layout that also contains a stretch - wrapper_layout = QtWidgets.QVBoxLayout() - wrapper_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) - wrapper_layout.addLayout(self.uploads_layout) - wrapper_layout.addStretch() - - # The internal widget of the scroll area - widget = QtWidgets.QWidget() - widget.setLayout(wrapper_layout) - self.setWidget(widget) - self.setWidgetResizable(True) - - # Other scroll area settings - self.setBackgroundRole(QtGui.QPalette.Light) - self.verticalScrollBar().rangeChanged.connect(self.resizeScroll) - - def resizeScroll(self, minimum, maximum): - """ - Scroll to the bottom of the window when the range changes. - """ - self.verticalScrollBar().setValue(maximum) - - def add(self, upload_id, content_length): - """ - Add a new upload progress bar. - """ - upload = Upload(self.common, upload_id, content_length) - self.uploads[upload_id] = upload - self.uploads_layout.addWidget(upload) - - def update(self, upload_id, progress): - """ - Update the progress of an upload. - """ - self.uploads[upload_id].update(progress) - - def rename(self, upload_id, old_filename, new_filename): - """ - Rename a file, which happens if the filename already exists in downloads_dir. - """ - self.uploads[upload_id].rename(old_filename, new_filename) - - def finished(self, upload_id): - """ - An upload has finished. - """ - self.uploads[upload_id].finished() - - def cancel(self, upload_id): - """ - Update an upload progress bar to show that it has been canceled. - """ - self.common.log('Uploads', 'cancel', 'upload_id: {}'.format(upload_id)) - self.uploads[upload_id].cancel() - - def reset(self): - """ - Reset the uploads back to zero - """ - for upload in self.uploads.values(): - self.uploads_layout.removeWidget(upload) - upload.progress_bar.close() - self.uploads = {} - - -class Uploads(QtWidgets.QWidget): - """ - The uploads chunk of the GUI. This lists all of the active upload - progress bars, as well as information about each upload. - """ - def __init__(self, common): - super(Uploads, self).__init__() - self.common = common - self.common.log('Uploads', '__init__') - - self.setMinimumWidth(350) - - # When there are no uploads - empty_image = QtWidgets.QLabel() - empty_image.setAlignment(QtCore.Qt.AlignCenter) - empty_image.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/uploads_transparent.png')))) - empty_text = QtWidgets.QLabel(strings._('gui_no_uploads', True)) - empty_text.setAlignment(QtCore.Qt.AlignCenter) - empty_text.setStyleSheet(self.common.css['downloads_uploads_empty_text']) - empty_layout = QtWidgets.QVBoxLayout() - empty_layout.addStretch() - empty_layout.addWidget(empty_image) - empty_layout.addWidget(empty_text) - empty_layout.addStretch() - self.empty = QtWidgets.QWidget() - self.empty.setStyleSheet(self.common.css['downloads_uploads_empty']) - self.empty.setLayout(empty_layout) - - # When there are uploads - self.upload_list = UploadList(self.common) - - # Upload header - uploads_label = QtWidgets.QLabel(strings._('gui_uploads', True)) - uploads_label.setStyleSheet(self.common.css['downloads_uploads_label']) - clear_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) - clear_button.setStyleSheet(self.common.css['downloads_uploads_clear']) - clear_button.setFlat(True) - clear_button.clicked.connect(self.reset) - upload_header = QtWidgets.QHBoxLayout() - upload_header.addWidget(uploads_label) - upload_header.addStretch() - upload_header.addWidget(clear_button) - - # Upload layout - not_empty_layout = QtWidgets.QVBoxLayout() - not_empty_layout.addLayout(upload_header) - not_empty_layout.addWidget(self.upload_list) - self.not_empty = QtWidgets.QWidget() - self.not_empty.setLayout(not_empty_layout) - - # Layout - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.empty) - layout.addWidget(self.not_empty) - self.setLayout(layout) - - # Reset once at the beginning - self.reset() - - def add(self, upload_id, content_length): - """ - Add a new upload. - """ - self.common.log('Uploads', 'add', 'upload_id: {}, content_length: {}'.format(upload_id, content_length)) - - # Hide empty, show not empty - self.empty.hide() - self.not_empty.show() - - # Add it to the list - self.upload_list.add(upload_id, content_length) - - def update(self, upload_id, progress): - """ - Update the progress of an upload. - """ - self.upload_list.update(upload_id, progress) - - def rename(self, upload_id, old_filename, new_filename): - """ - Rename a file, which happens if the filename already exists in downloads_dir. - """ - self.upload_list.rename(upload_id, old_filename, new_filename) - - def finished(self, upload_id): - """ - An upload has finished. - """ - self.upload_list.finished(upload_id) - - def cancel(self, upload_id): - """ - Update an upload progress bar to show that it has been canceled. - """ - self.common.log('Uploads', 'cancel', 'upload_id: {}'.format(upload_id)) - self.upload_list.cancel(upload_id) - - def reset(self): - """ - Reset the uploads back to zero - """ - self.upload_list.reset() - - # Hide not empty, show empty - self.not_empty.hide() - self.empty.show() From 656784dfa94627121c8b75c588f555a61a223ae3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 21:28:10 -0700 Subject: [PATCH 110/123] Remove obsolete ReceiveModeInfo file --- onionshare_gui/mode/receive_mode/info.py | 136 ----------------------- 1 file changed, 136 deletions(-) delete mode 100644 onionshare_gui/mode/receive_mode/info.py diff --git a/onionshare_gui/mode/receive_mode/info.py b/onionshare_gui/mode/receive_mode/info.py deleted file mode 100644 index c23f8496..00000000 --- a/onionshare_gui/mode/receive_mode/info.py +++ /dev/null @@ -1,136 +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 . -""" -from PyQt5 import QtCore, QtWidgets, QtGui - -from onionshare import strings - - -class ReceiveModeInfo(QtWidgets.QWidget): - """ - Receive mode information widget - """ - def __init__(self, common, receive_mode): - super(ReceiveModeInfo, self).__init__() - self.common = common - self.receive_mode = receive_mode - - # In progress and completed labels - self.in_progress_uploads_count = QtWidgets.QLabel() - self.in_progress_uploads_count.setStyleSheet(self.common.css['mode_info_label']) - self.completed_uploads_count = QtWidgets.QLabel() - self.completed_uploads_count.setStyleSheet(self.common.css['mode_info_label']) - - # Toggle button - self.toggle_button = QtWidgets.QPushButton() - self.toggle_button.setDefault(False) - self.toggle_button.setFixedWidth(35) - self.toggle_button.setFixedHeight(30) - self.toggle_button.setFlat(True) - self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')) ) - self.toggle_button.clicked.connect(self.toggle_uploads) - - # Keep track of indicator - self.indicator_count = 0 - self.indicator_label = QtWidgets.QLabel(parent=self.toggle_button) - self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) - self.update_indicator() - - # Layout - layout = QtWidgets.QHBoxLayout() - layout.addStretch() - layout.addWidget(self.in_progress_uploads_count) - layout.addWidget(self.completed_uploads_count) - layout.addWidget(self.toggle_button) - self.setLayout(layout) - - self.update_uploads_completed() - self.update_uploads_in_progress() - - def update_indicator(self, increment=False): - """ - Update the display of the indicator count. If increment is True, then - only increment the counter if Uploads is hidden. - """ - if increment and not self.receive_mode.uploads.isVisible(): - self.indicator_count += 1 - - self.indicator_label.setText("{}".format(self.indicator_count)) - - if self.indicator_count == 0: - self.indicator_label.hide() - else: - size = self.indicator_label.sizeHint() - self.indicator_label.setGeometry(35-size.width(), 0, size.width(), size.height()) - self.indicator_label.show() - - def update_uploads_completed(self): - """ - Update the 'Uploads completed' info widget. - """ - if self.receive_mode.uploads_completed == 0: - image = self.common.get_resource_path('images/share_completed_none.png') - else: - image = self.common.get_resource_path('images/share_completed.png') - self.completed_uploads_count.setText(' {1:d}'.format(image, self.receive_mode.uploads_completed)) - self.completed_uploads_count.setToolTip(strings._('info_completed_uploads_tooltip', True).format(self.receive_mode.uploads_completed)) - - def update_uploads_in_progress(self): - """ - Update the 'Uploads in progress' info widget. - """ - if self.receive_mode.uploads_in_progress == 0: - image = self.common.get_resource_path('images/share_in_progress_none.png') - else: - image = self.common.get_resource_path('images/share_in_progress.png') - self.in_progress_uploads_count.setText(' {1:d}'.format(image, self.receive_mode.uploads_in_progress)) - self.in_progress_uploads_count.setToolTip(strings._('info_in_progress_uploads_tooltip', True).format(self.receive_mode.uploads_in_progress)) - - def toggle_uploads(self): - """ - Toggle showing and hiding the Uploads widget - """ - self.common.log('ReceiveModeInfo', 'toggle_uploads') - - if self.receive_mode.uploads.isVisible(): - self.receive_mode.uploads.hide() - self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')) ) - self.toggle_button.setFlat(True) - else: - self.receive_mode.uploads.show() - self.toggle_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle_selected.png')) ) - self.toggle_button.setFlat(False) - - # Reset the indicator count - self.indicator_count = 0 - self.update_indicator() - - self.receive_mode.resize_window() - - def show_less(self): - """ - Remove clutter widgets that aren't necessary. - """ - pass - - def show_more(self): - """ - Show all widgets. - """ - pass From bc8759bc77864fceeab22e0d4038ce0750c3cc07 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 21:46:16 -0700 Subject: [PATCH 111/123] Properly close items inside the item list, instead of just hiding them --- onionshare_gui/mode/history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index 4f5b2cef..ff31e3a9 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -347,7 +347,7 @@ class HistoryItemList(QtWidgets.QScrollArea): """ for item in self.items.values(): self.items_layout.removeWidget(item) - item.hide() + item.close() self.items = {} From 49e371d503ff05f95aeef358118fd74ba39a58fe Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 8 Oct 2018 10:59:11 +1100 Subject: [PATCH 112/123] adjust widget sizes when switching mode --- onionshare_gui/onionshare_gui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 6a7eb63a..35088ebe 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -200,12 +200,14 @@ class OnionShareGui(QtWidgets.QMainWindow): self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) self.receive_mode.hide() + self.adjust_size(self.common.min_window_width) self.share_mode.show() else: self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style']) self.share_mode.hide() + self.adjust_size(self.common.min_window_width) self.receive_mode.show() self.update_server_status_indicator() From 50c0d91c5751c96d1dfc51055ed37ff166abe416 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 21:57:45 -0700 Subject: [PATCH 113/123] Missing imports --- onionshare_gui/mode/history.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index ff31e3a9..07121363 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -19,10 +19,12 @@ along with this program. If not, see . """ import time import subprocess +import os from datetime import datetime from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings +from ..widgets import Alert class HistoryItem(QtWidgets.QWidget): From 5a2ca669a111604f46201627ea420a59ec9a9899 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 22:07:19 -0700 Subject: [PATCH 114/123] Rip out all of the adjust size logic and let Qt just handle it --- onionshare_gui/mode/__init__.py | 25 +----------- onionshare_gui/mode/receive_mode/__init__.py | 9 ---- onionshare_gui/mode/share_mode/__init__.py | 9 ---- onionshare_gui/onionshare_gui.py | 43 +------------------- 4 files changed, 2 insertions(+), 84 deletions(-) diff --git a/onionshare_gui/mode/__init__.py b/onionshare_gui/mode/__init__.py index cfbb235b..0971ff32 100644 --- a/onionshare_gui/mode/__init__.py +++ b/onionshare_gui/mode/__init__.py @@ -36,7 +36,6 @@ class Mode(QtWidgets.QWidget): starting_server_step3 = QtCore.pyqtSignal() starting_server_error = QtCore.pyqtSignal(str) set_server_active = QtCore.pyqtSignal(bool) - adjust_size = QtCore.pyqtSignal(int) def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None, local_only=False): super(Mode, self).__init__() @@ -50,8 +49,6 @@ class Mode(QtWidgets.QWidget): self.filenames = filenames - self.setMinimumWidth(self.common.min_window_width) - # The web object gets created in init() self.web = None @@ -83,7 +80,7 @@ class Mode(QtWidgets.QWidget): # 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(self.common.min_window_width) + self.min_width_widget.setMinimumWidth(600) def init(self): """ @@ -332,23 +329,3 @@ class Mode(QtWidgets.QWidget): Handle REQUEST_UPLOAD_FINISHED event. """ pass - - def resize_window(self): - """ - We call this to force the OnionShare window to resize itself to be smaller. - For this to do anything, the Mode needs to override it and call: - - self.adjust_size.emit(min_width) - - It can calculate min_width (the new minimum window width) based on what - widgets are visible. - """ - pass - - def show(self): - """ - Always resize the window after showing this Mode widget. - """ - super(Mode, self).show() - self.qtapp.processEvents() - self.resize_window() diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py index ffa259e7..66e0bbe7 100644 --- a/onionshare_gui/mode/receive_mode/__init__.py +++ b/onionshare_gui/mode/receive_mode/__init__.py @@ -196,12 +196,3 @@ class ReceiveMode(Mode): def update_primary_action(self): self.common.log('ReceiveMode', 'update_primary_action') - - # Resize window - self.resize_window() - - def resize_window(self): - min_width = self.common.min_window_width - if self.history.isVisible(): - min_width += 300 - self.adjust_size.emit(min_width) diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index 0bf094c0..b3d7c549 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -321,21 +321,12 @@ class ShareMode(Mode): self.primary_action.hide() self.info_label.hide() - # Resize window - self.resize_window() - def reset_info_counters(self): """ Set the info counters back to zero. """ self.history.reset() - def resize_window(self): - min_width = self.common.min_window_width - if self.history.isVisible(): - min_width += 300 - self.adjust_size.emit(min_width) - @staticmethod def _compute_total_size(filenames): total_size = 0 diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 35088ebe..9a71ae28 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -45,7 +45,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.common = common self.common.log('OnionShareGui', '__init__') - self.common.min_window_width = 460 + self.setMinimumWidth(700) self.onion = onion self.qtapp = qtapp @@ -133,7 +133,6 @@ class OnionShareGui(QtWidgets.QMainWindow): 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.share_mode.adjust_size.connect(self.adjust_size) # Receive mode self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, None, self.local_only) @@ -148,7 +147,6 @@ class OnionShareGui(QtWidgets.QMainWindow): 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) - self.receive_mode.adjust_size.connect(self.adjust_size) self.update_mode_switcher() self.update_server_status_indicator() @@ -169,9 +167,6 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setCentralWidget(central_widget) self.show() - # Adjust window size, to start with a minimum window width - self.adjust_size(self.common.min_window_width) - # The server isn't active yet self.set_server_active(False) @@ -200,14 +195,12 @@ class OnionShareGui(QtWidgets.QMainWindow): self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) self.receive_mode.hide() - self.adjust_size(self.common.min_window_width) self.share_mode.show() else: self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style']) self.share_mode.hide() - self.adjust_size(self.common.min_window_width) self.receive_mode.show() self.update_server_status_indicator() @@ -450,40 +443,6 @@ class OnionShareGui(QtWidgets.QMainWindow): # Disable settings menu action when server is active self.settings_action.setEnabled(not active) - def adjust_size(self, min_width): - """ - Recursively adjust size on all widgets. min_width is the new minimum width - of the window. - """ - self.setMinimumWidth(min_width) - - def adjust_size_layout(layout): - count = layout.count() - for i in range(count): - item = layout.itemAt(i) - if item: - child_widget = item.widget() - if child_widget: - adjust_size_widget(child_widget) - child_layout = item.layout() - if child_layout: - adjust_size_layout(child_layout) - - def adjust_size_widget(widget): - layout = widget.layout() - if layout: - adjust_size_layout(layout) - widget.adjustSize() - - # Adjust sizes of each mode - for mode in [self.share_mode, self.receive_mode]: - self.qtapp.processEvents() - adjust_size_widget(mode) - - # Adjust window size - self.qtapp.processEvents() - self.adjustSize() - def closeEvent(self, e): self.common.log('OnionShareGui', 'closeEvent') try: From 9aa982563ba2bfe71d56221d5137e857d1425fe2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Oct 2018 22:09:57 -0700 Subject: [PATCH 115/123] Remove one more reference to resize_window --- onionshare_gui/mode/history.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index 07121363..cf944aa0 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -548,5 +548,3 @@ class ToggleHistory(QtWidgets.QPushButton): # Reset the indicator count self.indicator_count = 0 self.update_indicator() - - self.current_mode.resize_window() From 6227c6cbc540078deaff627a1c70e907009dff00 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 9 Oct 2018 20:51:10 -0700 Subject: [PATCH 116/123] Set min width and height so everything always looks good, and change onion address to monospace font --- onionshare/common.py | 1 + onionshare_gui/onionshare_gui.py | 3 ++- onionshare_gui/server_status.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/onionshare/common.py b/onionshare/common.py index fb3b1e7a..96f9d2ad 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -211,6 +211,7 @@ class Common(object): color: #000000; padding: 10px; border: 1px solid #666666; + font-size: 12px; } """, diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 9a71ae28..3175f0f9 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -45,7 +45,8 @@ class OnionShareGui(QtWidgets.QMainWindow): self.common = common self.common.log('OnionShareGui', '__init__') - self.setMinimumWidth(700) + self.setMinimumWidth(820) + self.setMinimumHeight(530) self.onion = onion self.qtapp = qtapp diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 32135ca4..99aaa9f1 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -90,20 +90,20 @@ class ServerStatus(QtWidgets.QWidget): self.server_button.clicked.connect(self.server_button_clicked) # URL layout - url_font = QtGui.QFont() + url_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) self.url_description = QtWidgets.QLabel() self.url_description.setWordWrap(True) self.url_description.setMinimumHeight(50) self.url = QtWidgets.QLabel() self.url.setFont(url_font) self.url.setWordWrap(True) - self.url.setMinimumHeight(65) self.url.setMinimumSize(self.url.sizeHint()) self.url.setStyleSheet(self.common.css['server_status_url']) self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True)) self.copy_url_button.setFlat(True) self.copy_url_button.setStyleSheet(self.common.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(strings._('gui_copy_hidservauth', True)) self.copy_hidservauth_button.setFlat(True) From b982a9a24895f14dcb92289f7010e1a80d453557 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 9 Oct 2018 21:15:42 -0700 Subject: [PATCH 117/123] Actually, the window needs to be taller --- onionshare_gui/onionshare_gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 3175f0f9..1e03bc3e 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -46,7 +46,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.common = common self.common.log('OnionShareGui', '__init__') self.setMinimumWidth(820) - self.setMinimumHeight(530) + self.setMinimumHeight(620) self.onion = onion self.qtapp = qtapp @@ -154,7 +154,7 @@ class OnionShareGui(QtWidgets.QMainWindow): # Layouts contents_layout = QtWidgets.QVBoxLayout() - contents_layout.setContentsMargins(10, 10, 10, 10) + contents_layout.setContentsMargins(10, 0, 10, 0) contents_layout.addWidget(self.receive_mode) contents_layout.addWidget(self.share_mode) From 90172c913b025d011e05794902a55e4f478ffc76 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 9 Oct 2018 21:18:26 -0700 Subject: [PATCH 118/123] Stop hiding the share mode info label when tor breaks --- onionshare_gui/mode/receive_mode/__init__.py | 1 - onionshare_gui/mode/share_mode/__init__.py | 1 - 2 files changed, 2 deletions(-) diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py index 66e0bbe7..e312a55b 100644 --- a/onionshare_gui/mode/receive_mode/__init__.py +++ b/onionshare_gui/mode/receive_mode/__init__.py @@ -123,7 +123,6 @@ class ReceiveMode(Mode): Connection to Tor broke. """ self.primary_action.hide() - #self.info.show_less() def handle_request_load(self, event): """ diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index b3d7c549..1c1f33ae 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -224,7 +224,6 @@ class ShareMode(Mode): Connection to Tor broke. """ self.primary_action.hide() - self.info_label.hide() def handle_request_load(self, event): """ From 5616a6a965b8662e20b5608788e3e17a1eb22af0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 9 Oct 2018 21:49:05 -0700 Subject: [PATCH 119/123] Make the history indicator label circular again --- onionshare/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare/common.py b/onionshare/common.py index 96f9d2ad..cab1e747 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -287,7 +287,7 @@ class Common(object): font-weight: bold; font-size: 10px; padding: 2px; - border-radius: 8px; + border-radius: 7px; text-align: center; }""", From 56e5c8b90879d7c7053e506fe9d913736717539a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 9 Oct 2018 22:21:03 -0700 Subject: [PATCH 120/123] Add "download started" date/time to download history progress bars --- onionshare_gui/mode/history.py | 7 +++++-- share/locale/en.json | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index cf944aa0..8cfa0ed5 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -50,11 +50,13 @@ class DownloadHistoryItem(HistoryItem): self.common = common self.id = id - self.started = time.time() self.total_bytes = total_bytes self.downloaded_bytes = 0 + self.started = time.time() + self.started_dt = datetime.fromtimestamp(self.started) - self.setStyleSheet('QWidget { border: 1px solid red; }') + # Label + self.label = QtWidgets.QLabel(strings._('gui_download_in_progress').format(self.started_dt.strftime("%b %d, %I:%M%p"))) # Progress bar self.progress_bar = QtWidgets.QProgressBar() @@ -69,6 +71,7 @@ class DownloadHistoryItem(HistoryItem): # Layout layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.label) layout.addWidget(self.progress_bar) self.setLayout(layout) diff --git a/share/locale/en.json b/share/locale/en.json index 3537b0a2..e5d9a3be 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -180,5 +180,6 @@ "gui_upload_in_progress": "Upload Started {}", "gui_upload_finished_range": "Uploaded {} to {}", "gui_upload_finished": "Uploaded {}", + "gui_download_in_progress": "Download Started {}", "gui_open_folder_error_nautilus": "Cannot open folder because nautilus is not available. The file is here: {}" } From 85de803fda9a94e8721c94b2448043c6a30a1a03 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 10 Oct 2018 16:49:42 +1100 Subject: [PATCH 121/123] Raise minimumHeight again to account for overlap issues on MacOS caused by Mac's Qt widget padding --- onionshare_gui/onionshare_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 1e03bc3e..e672d74e 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -46,7 +46,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.common = common self.common.log('OnionShareGui', '__init__') self.setMinimumWidth(820) - self.setMinimumHeight(620) + self.setMinimumHeight(650) self.onion = onion self.qtapp = qtapp From beda37df06722cfa6163897a9f2ad3388084886b Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 10 Oct 2018 18:09:43 +1100 Subject: [PATCH 122/123] Remove commented out obsolete code --- onionshare_gui/mode/receive_mode/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py index e312a55b..b73acca2 100644 --- a/onionshare_gui/mode/receive_mode/__init__.py +++ b/onionshare_gui/mode/receive_mode/__init__.py @@ -185,7 +185,6 @@ class ReceiveMode(Mode): We should be ok to re-enable the 'Start Receive Mode' button now. """ self.primary_action.show() - #self.info.show_more() def reset_info_counters(self): """ From 753380663b5344e0b653ac8279143764b17ed465 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 10 Oct 2018 18:16:08 -0700 Subject: [PATCH 123/123] Final few tweaks to make this look perfect in macOS --- onionshare_gui/mode/share_mode/file_selection.py | 3 ++- onionshare_gui/onionshare_gui.py | 2 +- onionshare_gui/server_status.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/onionshare_gui/mode/share_mode/file_selection.py b/onionshare_gui/mode/share_mode/file_selection.py index d59df234..6bfa7dbf 100644 --- a/onionshare_gui/mode/share_mode/file_selection.py +++ b/onionshare_gui/mode/share_mode/file_selection.py @@ -89,7 +89,7 @@ class FileList(QtWidgets.QListWidget): self.setAcceptDrops(True) self.setIconSize(QtCore.QSize(32, 32)) self.setSortingEnabled(True) - self.setMinimumHeight(205) + self.setMinimumHeight(160) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.drop_here_image = DropHereLabel(self.common, self, True) self.drop_here_text = DropHereLabel(self.common, self, False) @@ -261,6 +261,7 @@ class FileList(QtWidgets.QListWidget): # Item info widget, with a white background item_info_layout = QtWidgets.QHBoxLayout() + item_info_layout.setContentsMargins(0, 0, 0, 0) item_info_layout.addWidget(item_size) item_info_layout.addWidget(item.item_button) item_info = QtWidgets.QWidget() diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index e672d74e..c2e6657b 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -46,7 +46,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.common = common self.common.log('OnionShareGui', '__init__') self.setMinimumWidth(820) - self.setMinimumHeight(650) + self.setMinimumHeight(660) self.onion = onion self.qtapp = qtapp diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 99aaa9f1..0267d826 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -142,12 +142,12 @@ class ServerStatus(QtWidgets.QWidget): When the widget is resized, try and adjust the display of a v3 onion URL. """ try: - self.get_url() + # Wrap the URL label url_length=len(self.get_url()) if url_length > 60: width = self.frameGeometry().width() if width < 530: - wrapped_onion_url = textwrap.fill(self.get_url(), 50) + wrapped_onion_url = textwrap.fill(self.get_url(), 46) self.url.setText(wrapped_onion_url) else: self.url.setText(self.get_url())