diff options
author | Micah Lee <micah@micahflee.com> | 2020-10-13 17:28:54 -0700 |
---|---|---|
committer | Micah Lee <micah@micahflee.com> | 2020-10-13 17:28:54 -0700 |
commit | b42f92d714145dcc6282773e61f68c00b4b79a28 (patch) | |
tree | ee4443ab3c5300db279a3cf0686380074d98c973 /desktop/src/onionshare/tab | |
parent | f4abcf1be9122a28005dc3e0949bf5952192e982 (diff) | |
download | onionshare-b42f92d714145dcc6282773e61f68c00b4b79a28.tar.gz onionshare-b42f92d714145dcc6282773e61f68c00b4b79a28.zip |
Move docs back to root, move onionshare_gui into briefcase app, and make modifications so briefcase app will work
Diffstat (limited to 'desktop/src/onionshare/tab')
-rw-r--r-- | desktop/src/onionshare/tab/__init__.py | 21 | ||||
-rw-r--r-- | desktop/src/onionshare/tab/mode/__init__.py | 494 | ||||
-rw-r--r-- | desktop/src/onionshare/tab/mode/chat_mode/__init__.py | 154 | ||||
-rw-r--r-- | desktop/src/onionshare/tab/mode/file_selection.py | 496 | ||||
-rw-r--r-- | desktop/src/onionshare/tab/mode/history.py | 808 | ||||
-rw-r--r-- | desktop/src/onionshare/tab/mode/mode_settings_widget.py | 296 | ||||
-rw-r--r-- | desktop/src/onionshare/tab/mode/receive_mode/__init__.py | 321 | ||||
-rw-r--r-- | desktop/src/onionshare/tab/mode/share_mode/__init__.py | 471 | ||||
-rw-r--r-- | desktop/src/onionshare/tab/mode/share_mode/threads.py | 65 | ||||
-rw-r--r-- | desktop/src/onionshare/tab/mode/website_mode/__init__.py | 331 | ||||
-rw-r--r-- | desktop/src/onionshare/tab/server_status.py | 466 | ||||
-rw-r--r-- | desktop/src/onionshare/tab/tab.py | 662 |
12 files changed, 4585 insertions, 0 deletions
diff --git a/desktop/src/onionshare/tab/__init__.py b/desktop/src/onionshare/tab/__init__.py new file mode 100644 index 00000000..162d13aa --- /dev/null +++ b/desktop/src/onionshare/tab/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +from .tab import Tab diff --git a/desktop/src/onionshare/tab/mode/__init__.py b/desktop/src/onionshare/tab/mode/__init__.py new file mode 100644 index 00000000..06500aea --- /dev/null +++ b/desktop/src/onionshare/tab/mode/__init__.py @@ -0,0 +1,494 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare_cli.common import AutoStopTimer + +from .history import IndividualFileHistoryItem +from .mode_settings_widget import ModeSettingsWidget + +from ..server_status import ServerStatus +from ... import strings +from ...threads import OnionThread, AutoStartTimer +from ...widgets import Alert + + +class Mode(QtWidgets.QWidget): + """ + The class that all modes inherit from + """ + + start_server_finished = QtCore.pyqtSignal() + stop_server_finished = QtCore.pyqtSignal() + starting_server_step2 = QtCore.pyqtSignal() + starting_server_step3 = QtCore.pyqtSignal() + starting_server_error = QtCore.pyqtSignal(str) + starting_server_early = QtCore.pyqtSignal() + set_server_active = QtCore.pyqtSignal(bool) + change_persistent = QtCore.pyqtSignal(int, bool) + + def __init__(self, tab): + super(Mode, self).__init__() + self.tab = tab + self.settings = tab.settings + + self.common = tab.common + self.qtapp = self.common.gui.qtapp + self.app = tab.app + + self.status_bar = tab.status_bar + self.server_status_label = tab.status_bar.server_status_label + self.system_tray = tab.system_tray + + self.filenames = tab.filenames + + # The web object gets created in init() + self.web = None + + # Threads start out as None + self.onion_thread = None + self.web_thread = None + self.startup_thread = None + + # Mode settings widget + self.mode_settings_widget = ModeSettingsWidget( + self.common, self.tab, self.settings + ) + self.mode_settings_widget.change_persistent.connect(self.change_persistent) + + # Server status + self.server_status = ServerStatus( + self.common, + self.qtapp, + self.app, + self.settings, + self.mode_settings_widget, + None, + self.common.gui.local_only, + ) + 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) + self.start_server_finished.connect(self.server_status.start_server_finished) + self.stop_server_finished.connect(self.server_status.stop_server_finished) + self.starting_server_step2.connect(self.start_server_step2) + self.starting_server_step3.connect(self.start_server_step3) + self.starting_server_early.connect(self.start_server_early) + self.starting_server_error.connect(self.start_server_error) + + # Primary action + # Note: It's up to the downstream Mode to add this to its layout + self.primary_action_layout = QtWidgets.QVBoxLayout() + self.primary_action_layout.addWidget(self.mode_settings_widget) + self.primary_action = QtWidgets.QWidget() + self.primary_action.setLayout(self.primary_action_layout) + + def init(self): + """ + Add custom initialization here. + """ + pass + + def human_friendly_time(self, secs): + """ + Returns a human-friendly time delta from given seconds. + """ + days = secs // 86400 + hours = (secs - days * 86400) // 3600 + minutes = (secs - days * 86400 - hours * 3600) // 60 + seconds = secs - days * 86400 - hours * 3600 - minutes * 60 + if not seconds: + seconds = "0" + result = ( + (f"{days}{strings._('days_first_letter')}, " if days else "") + + (f"{hours}{strings._('hours_first_letter')}, " if hours else "") + + (f"{minutes}{strings._('minutes_first_letter')}, " if minutes else "") + + f"{seconds}{strings._('seconds_first_letter')}" + ) + + return result + + def timer_callback(self): + """ + This method is called regularly on a timer. + """ + # If this is a scheduled share, display the countdown til the share starts + if self.server_status.status == ServerStatus.STATUS_WORKING: + if self.settings.get("general", "autostart_timer"): + now = QtCore.QDateTime.currentDateTime() + if self.server_status.local_only: + seconds_remaining = now.secsTo( + self.mode_settings_widget.autostart_timer_widget.dateTime() + ) + else: + seconds_remaining = now.secsTo( + self.server_status.autostart_timer_datetime.replace( + second=0, microsecond=0 + ) + ) + # Update the server button + if seconds_remaining > 0: + self.server_status.server_button.setText( + strings._("gui_waiting_to_start").format( + self.human_friendly_time(seconds_remaining) + ) + ) + else: + self.server_status.server_button.setText( + strings._("gui_please_wait") + ) + + # If the auto-stop timer has stopped, stop the server + if self.server_status.status == ServerStatus.STATUS_STARTED: + if self.app.autostop_timer_thread and self.settings.get( + "general", "autostop_timer" + ): + if self.autostop_timer_datetime_delta > 0: + now = QtCore.QDateTime.currentDateTime() + seconds_remaining = now.secsTo( + self.server_status.autostop_timer_datetime + ) + + # Update the server button + server_button_text = self.get_stop_server_autostop_timer_text() + self.server_status.server_button.setText( + server_button_text.format( + self.human_friendly_time(seconds_remaining) + ) + ) + + self.status_bar.clearMessage() + if not self.app.autostop_timer_thread.is_alive(): + if self.autostop_timer_finished_should_stop_server(): + self.server_status.stop_server() + + def timer_callback_custom(self): + """ + Add custom timer code. + """ + pass + + def get_stop_server_autostop_timer_text(self): + """ + Return the string to put on the stop server button, if there's an auto-stop timer + """ + pass + + def autostop_timer_finished_should_stop_server(self): + """ + The auto-stop timer expired, should we stop the server? Returns a bool + """ + pass + + def start_server(self): + """ + Start the onionshare server. This uses multiple threads to start the Tor onion + server and the web app. + """ + self.common.log("Mode", "start_server") + + self.start_server_custom() + self.set_server_active.emit(True) + + # Clear the status bar + self.status_bar.clearMessage() + self.server_status_label.setText("") + + # Hide the mode settings + self.mode_settings_widget.hide() + + # Ensure we always get a new random port each time we might launch an OnionThread + self.app.port = None + + # Start the onion thread. If this share was scheduled for a future date, + # the OnionThread will start and exit 'early' to obtain the port, password + # and onion address, but it will not start the WebThread yet. + if self.settings.get("general", "autostart_timer"): + self.start_onion_thread(obtain_onion_early=True) + self.common.log("Mode", "start_server", "Starting auto-start timer") + self.startup_thread = AutoStartTimer(self) + # Once the timer has finished, start the real share, with a WebThread + self.startup_thread.success.connect(self.start_scheduled_service) + self.startup_thread.error.connect(self.start_server_error) + self.startup_thread.canceled = False + self.startup_thread.start() + else: + self.start_onion_thread() + + def start_onion_thread(self, obtain_onion_early=False): + self.common.log("Mode", "start_server", "Starting an onion thread") + self.obtain_onion_early = obtain_onion_early + self.onion_thread = OnionThread(self) + self.onion_thread.success.connect(self.starting_server_step2.emit) + self.onion_thread.success_early.connect(self.starting_server_early.emit) + self.onion_thread.error.connect(self.starting_server_error.emit) + self.onion_thread.start() + + def start_scheduled_service(self, obtain_onion_early=False): + # We start a new OnionThread with the saved scheduled key from settings + self.common.settings.load() + self.obtain_onion_early = obtain_onion_early + self.common.log("Mode", "start_server", "Starting a scheduled onion thread") + 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): + """ + Add custom initialization here. + """ + pass + + def start_server_early(self): + """ + An 'early' start of an onion service in order to obtain the onion + address for a scheduled start. Shows the onion address in the UI + in advance of actually starting the share. + """ + self.server_status.show_url() + + def start_server_step2(self): + """ + Step 2 in starting the onionshare server. + """ + self.common.log("Mode", "start_server_step2") + + self.start_server_step2_custom() + + # Nothing to do here. + + # start_server_step2_custom has call these to move on: + # self.starting_server_step3.emit() + # self.start_server_finished.emit() + + def start_server_step2_custom(self): + """ + Add custom initialization here. + """ + pass + + def start_server_step3(self): + """ + Step 3 in starting the onionshare server. + """ + self.common.log("Mode", "start_server_step3") + + self.start_server_step3_custom() + + if self.settings.get("general", "autostop_timer"): + # Convert the date value to seconds between now and then + now = QtCore.QDateTime.currentDateTime() + self.autostop_timer_datetime_delta = now.secsTo( + self.server_status.autostop_timer_datetime + ) + # Start the auto-stop timer + if self.autostop_timer_datetime_delta > 0: + self.app.autostop_timer_thread = AutoStopTimer( + self.common, self.autostop_timer_datetime_delta + ) + self.app.autostop_timer_thread.start() + # The auto-stop timer has actually already passed since the user clicked Start. Probably the Onion service took too long to start. + else: + self.stop_server() + self.start_server_error( + strings._("gui_server_started_after_autostop_timer") + ) + + def start_server_step3_custom(self): + """ + Add custom initialization here. + """ + pass + + def start_server_error(self, error): + """ + If there's an error when trying to start the onion service + """ + self.common.log("Mode", "start_server_error") + + Alert(self.common, error, QtWidgets.QMessageBox.Warning) + self.set_server_active.emit(False) + self.server_status.stop_server() + self.status_bar.clearMessage() + + self.start_server_error_custom() + + def start_server_error_custom(self): + """ + Add custom initialization here. + """ + pass + + def cancel_server(self): + """ + Cancel the server while it is preparing to start + """ + self.cancel_server_custom() + if self.startup_thread: + self.common.log("Mode", "cancel_server: quitting startup thread") + self.startup_thread.canceled = True + self.app.onion.scheduled_key = None + self.app.onion.scheduled_auth_cookie = None + self.startup_thread.quit() + 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. + """ + self.common.log("Mode", "stop_server") + + if self.server_status.status != ServerStatus.STATUS_STOPPED: + try: + self.web.stop(self.app.port) + except: + # Probably we had no port to begin with (Onion service didn't start) + pass + self.app.cleanup() + + self.stop_server_custom() + + self.set_server_active.emit(False) + self.stop_server_finished.emit() + + # Show the mode settings + self.mode_settings_widget.show() + + def stop_server_custom(self): + """ + Add custom initialization here. + """ + pass + + def handle_tor_broke(self): + """ + Handle connection from Tor breaking. + """ + if self.server_status.status != ServerStatus.STATUS_STOPPED: + self.server_status.stop_server() + self.handle_tor_broke_custom() + + def handle_tor_broke_custom(self): + """ + Add custom initialization here. + """ + pass + + # Handle web server events + + def handle_request_load(self, event): + """ + Handle REQUEST_LOAD event. + """ + pass + + def handle_request_started(self, event): + """ + Handle REQUEST_STARTED event. + """ + pass + + def handle_request_rate_limit(self, event): + """ + Handle REQUEST_RATE_LIMIT event. + """ + self.stop_server() + Alert( + self.common, strings._("error_rate_limit"), QtWidgets.QMessageBox.Critical + ) + + def handle_request_progress(self, event): + """ + Handle REQUEST_PROGRESS event. + """ + pass + + def handle_request_canceled(self, event): + """ + Handle REQUEST_CANCELED event. + """ + pass + + def handle_request_upload_file_renamed(self, event): + """ + Handle REQUEST_UPLOAD_FILE_RENAMED event. + """ + pass + + def handle_request_upload_set_dir(self, event): + """ + Handle REQUEST_UPLOAD_SET_DIR event. + """ + pass + + def handle_request_upload_finished(self, event): + """ + Handle REQUEST_UPLOAD_FINISHED event. + """ + pass + + def handle_request_upload_canceled(self, event): + """ + Handle REQUEST_UPLOAD_CANCELED event. + """ + pass + + def handle_request_individual_file_started(self, event): + """ + Handle REQUEST_INDVIDIDUAL_FILES_STARTED event. + Used in both Share and Website modes, so implemented here. + """ + self.toggle_history.update_indicator(True) + self.history.requests_count += 1 + self.history.update_requests() + + item = IndividualFileHistoryItem(self.common, event["data"], event["path"]) + self.history.add(event["data"]["id"], item) + + def handle_request_individual_file_progress(self, event): + """ + Handle REQUEST_INDVIDIDUAL_FILES_PROGRESS event. + Used in both Share and Website modes, so implemented here. + """ + self.history.update(event["data"]["id"], event["data"]["bytes"]) + + if self.server_status.status == self.server_status.STATUS_STOPPED: + self.history.cancel(event["data"]["id"]) + + def handle_request_individual_file_canceled(self, event): + """ + Handle REQUEST_INDVIDIDUAL_FILES_CANCELED event. + Used in both Share and Website modes, so implemented here. + """ + self.history.cancel(event["data"]["id"]) diff --git a/desktop/src/onionshare/tab/mode/chat_mode/__init__.py b/desktop/src/onionshare/tab/mode/chat_mode/__init__.py new file mode 100644 index 00000000..f96e2e77 --- /dev/null +++ b/desktop/src/onionshare/tab/mode/chat_mode/__init__.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +import os +import random +import string + +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare_cli.onion import * +from onionshare_cli.common import Common +from onionshare_cli.web import Web + +from .. import Mode +from .... import strings +from ....widgets import MinimumWidthWidget +from ....gui_common import GuiCommon + + +class ChatMode(Mode): + """ + Parts of the main window UI for sharing files. + """ + + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def init(self): + """ + Custom initialization for ChatMode. + """ + # Create the Web object + self.web = Web(self.common, True, self.settings, "chat") + + # Chat image + self.image_label = QtWidgets.QLabel() + self.image_label.setPixmap( + QtGui.QPixmap.fromImage( + QtGui.QImage(GuiCommon.get_resource_path("images/mode_chat.png")) + ) + ) + self.image_label.setFixedSize(300, 300) + image_layout = QtWidgets.QVBoxLayout() + image_layout.addStretch() + image_layout.addWidget(self.image_label) + image_layout.addStretch() + self.image = QtWidgets.QWidget() + self.image.setLayout(image_layout) + + # Server status + self.server_status.set_mode("chat") + self.server_status.server_started_finished.connect(self.update_primary_action) + self.server_status.server_stopped.connect(self.update_primary_action) + self.server_status.server_canceled.connect(self.update_primary_action) + # Tell server_status about web, then update + self.server_status.web = self.web + self.server_status.update() + + # Header + header_label = QtWidgets.QLabel(strings._("gui_new_tab_chat_button")) + header_label.setStyleSheet(self.common.gui.css["mode_header_label"]) + + # Top bar + top_bar_layout = QtWidgets.QHBoxLayout() + top_bar_layout.addStretch() + + # Main layout + self.main_layout = QtWidgets.QVBoxLayout() + self.main_layout.addLayout(top_bar_layout) + self.main_layout.addStretch() + self.main_layout.addWidget(header_label) + self.main_layout.addWidget(self.primary_action) + self.main_layout.addWidget(self.server_status) + self.main_layout.addStretch() + self.main_layout.addWidget(MinimumWidthWidget(700)) + + # Column layout + self.column_layout = QtWidgets.QHBoxLayout() + self.column_layout.addWidget(self.image) + self.column_layout.addLayout(self.main_layout) + + # Wrapper layout + self.wrapper_layout = QtWidgets.QVBoxLayout() + self.wrapper_layout.addLayout(self.column_layout) + self.setLayout(self.wrapper_layout) + + def get_stop_server_autostop_timer_text(self): + """ + Return the string to put on the stop server button, if there's an auto-stop timer + """ + return strings._("gui_share_stop_server_autostop_timer") + + def autostop_timer_finished_should_stop_server(self): + """ + The auto-stop timer expired, should we stop the server? Returns a bool + """ + + self.server_status.stop_server() + self.server_status_label.setText(strings._("close_on_autostop_timer")) + return True + + def start_server_custom(self): + """ + Starting the server. + """ + # Reset web counters + self.web.chat_mode.cur_history_id = 0 + self.web.reset_invalid_passwords() + + def start_server_step2_custom(self): + """ + Step 2 in starting the server. Zipping up files. + """ + # Continue + self.starting_server_step3.emit() + self.start_server_finished.emit() + + def cancel_server_custom(self): + """ + Log that the server has been cancelled + """ + self.common.log("ChatMode", "cancel_server") + + def handle_tor_broke_custom(self): + """ + Connection to Tor broke. + """ + self.primary_action.hide() + + def on_reload_settings(self): + """ + We should be ok to re-enable the 'Start Receive Mode' button now. + """ + self.primary_action.show() + + def update_primary_action(self): + self.common.log("ChatMode", "update_primary_action") diff --git a/desktop/src/onionshare/tab/mode/file_selection.py b/desktop/src/onionshare/tab/mode/file_selection.py new file mode 100644 index 00000000..faefda0e --- /dev/null +++ b/desktop/src/onionshare/tab/mode/file_selection.py @@ -0,0 +1,496 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +import os +from PyQt5 import QtCore, QtWidgets, QtGui + +from ... import strings +from ...widgets import Alert, AddFileDialog +from ...gui_common import GuiCommon + + +class DropHereWidget(QtWidgets.QWidget): + """ + When there are no files or folders in the FileList yet, display the + 'drop files here' message and graphic. + """ + + def __init__(self, common, image_filename, header_text, w, h, parent): + super(DropHereWidget, self).__init__(parent) + self.common = common + self.setAcceptDrops(True) + + self.image_label = QtWidgets.QLabel(parent=self) + self.image_label.setPixmap( + QtGui.QPixmap.fromImage( + QtGui.QImage(GuiCommon.get_resource_path(image_filename)) + ) + ) + self.image_label.setAlignment(QtCore.Qt.AlignCenter) + self.image_label.show() + + self.header_label = QtWidgets.QLabel(parent=self) + self.header_label.setText(header_text) + self.header_label.setStyleSheet( + self.common.gui.css["share_file_selection_drop_here_header_label"] + ) + self.header_label.setAlignment(QtCore.Qt.AlignCenter) + self.header_label.show() + + self.text_label = QtWidgets.QLabel(parent=self) + self.text_label.setText(strings._("gui_drag_and_drop")) + self.text_label.setStyleSheet( + self.common.gui.css["share_file_selection_drop_here_label"] + ) + self.text_label.setAlignment(QtCore.Qt.AlignCenter) + self.text_label.show() + + self.resize(w, h) + self.hide() + + def dragEnterEvent(self, event): + self.hide() + event.accept() + + def resize(self, w, h): + self.setGeometry(0, 0, w, h) + self.image_label.setGeometry(0, 0, w, h - 100) + self.header_label.setGeometry(0, 340, w, h - 340) + self.text_label.setGeometry(0, 410, w, h - 410) + + +class DropCountLabel(QtWidgets.QLabel): + """ + While dragging files over the FileList, this counter displays the + number of files you're dragging. + """ + + def __init__(self, common, parent): + self.parent = parent + super(DropCountLabel, self).__init__(parent=parent) + + self.common = common + + self.setAcceptDrops(True) + self.setAlignment(QtCore.Qt.AlignCenter) + self.setText(strings._("gui_drag_and_drop")) + self.setStyleSheet(self.common.gui.css["share_file_selection_drop_count_label"]) + self.hide() + + def dragEnterEvent(self, event): + self.hide() + event.accept() + + +class FileList(QtWidgets.QListWidget): + """ + The list of files and folders in the GUI. + """ + + files_dropped = QtCore.pyqtSignal() + files_updated = QtCore.pyqtSignal() + + def __init__(self, common, background_image_filename, header_text, parent=None): + super(FileList, self).__init__(parent) + self.common = common + self.setAcceptDrops(True) + + self.setIconSize(QtCore.QSize(32, 32)) + self.setSortingEnabled(True) + self.setMinimumHeight(160) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.drop_here = DropHereWidget( + self.common, + background_image_filename, + header_text, + self.width(), + self.height(), + self, + ) + self.drop_count = DropCountLabel(self.common, self) + self.resizeEvent(None) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + + def update(self): + """ + Update the GUI elements based on the current state. + """ + # file list should have a background image if empty + if self.count() == 0: + self.drop_here.show() + else: + self.drop_here.hide() + + def server_started(self): + """ + Update the GUI when the server starts, by hiding delete buttons. + """ + self.setAcceptDrops(False) + self.setCurrentItem(None) + self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + for index in range(self.count()): + self.item(index).item_button.hide() + + def server_stopped(self): + """ + Update the GUI when the server stops, by showing delete buttons. + """ + self.setAcceptDrops(True) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + for index in range(self.count()): + self.item(index).item_button.show() + + def resizeEvent(self, event): + """ + When the widget is resized, resize the drop files image and text. + """ + self.drop_here.resize(self.width(), self.height()) + + if self.count() > 0: + # Add and delete an empty item, to force all items to get redrawn + # This is ugly, but the only way I could figure out how to proceed + item = QtWidgets.QListWidgetItem("fake item") + self.addItem(item) + self.takeItem(self.row(item)) + self.update() + + # Extend any filenames that were truncated to fit the window + # We use 200 as a rough guess at how wide the 'file size + delete button' widget is + # and extend based on the overall width minus that amount. + for index in range(self.count()): + metrics = QtGui.QFontMetrics(self.item(index).font()) + elided = metrics.elidedText( + self.item(index).basename, QtCore.Qt.ElideRight, self.width() - 200 + ) + self.item(index).setText(elided) + + def dragEnterEvent(self, event): + """ + dragEnterEvent for dragging files and directories into the widget. + """ + if event.mimeData().hasUrls: + self.setStyleSheet(self.common.gui.css["share_file_list_drag_enter"]) + count = len(event.mimeData().urls()) + self.drop_count.setText(f"+{count}") + + size_hint = self.drop_count.sizeHint() + self.drop_count.setGeometry( + self.width() - size_hint.width() - 30, + self.height() - size_hint.height() - 10, + size_hint.width(), + size_hint.height(), + ) + self.drop_count.show() + event.accept() + else: + event.ignore() + + def dragLeaveEvent(self, event): + """ + dragLeaveEvent for dragging files and directories into the widget. + """ + self.setStyleSheet(self.common.gui.css["share_file_list_drag_leave"]) + self.drop_count.hide() + event.accept() + self.update() + + def dragMoveEvent(self, event): + """ + dragMoveEvent for dragging files and directories into the widget. + """ + if event.mimeData().hasUrls: + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + else: + event.ignore() + + def dropEvent(self, event): + """ + dropEvent for dragging files and directories into the widget. + """ + if event.mimeData().hasUrls: + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + for url in event.mimeData().urls(): + filename = str(url.toLocalFile()) + self.add_file(filename) + else: + event.ignore() + + self.setStyleSheet(self.common.gui.css["share_file_list_drag_leave"]) + self.drop_count.hide() + + self.files_dropped.emit() + + def add_file(self, filename): + """ + Add a file or directory to this widget. + """ + filenames = [] + for index in range(self.count()): + filenames.append(self.item(index).filename) + + if filename not in filenames: + if not os.access(filename, os.R_OK): + Alert(self.common, strings._("not_a_readable_file").format(filename)) + return + + fileinfo = QtCore.QFileInfo(filename) + ip = QtWidgets.QFileIconProvider() + icon = ip.icon(fileinfo) + + if os.path.isfile(filename): + size_bytes = fileinfo.size() + size_readable = self.common.human_readable_filesize(size_bytes) + else: + size_bytes = self.common.dir_size(filename) + size_readable = self.common.human_readable_filesize(size_bytes) + + # Create a new item + item = QtWidgets.QListWidgetItem() + item.setIcon(icon) + item.size_bytes = size_bytes + + # Item's filename attribute and size labels + item.filename = filename + item_size = QtWidgets.QLabel(size_readable) + item_size.setStyleSheet(self.common.gui.css["share_file_list_item_size"]) + + item.basename = os.path.basename(filename.rstrip("/")) + # Use the basename as the method with which to sort the list + metrics = QtGui.QFontMetrics(item.font()) + elided = metrics.elidedText( + item.basename, QtCore.Qt.ElideRight, self.sizeHint().width() + ) + item.setData(QtCore.Qt.DisplayRole, elided) + + # Item's delete button + def delete_item(): + itemrow = self.row(item) + self.takeItem(itemrow) + self.files_updated.emit() + + item.item_button = QtWidgets.QPushButton() + item.item_button.setDefault(False) + item.item_button.setFlat(True) + item.item_button.setIcon( + QtGui.QIcon(GuiCommon.get_resource_path("images/file_delete.png")) + ) + item.item_button.clicked.connect(delete_item) + item.item_button.setSizePolicy( + QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed + ) + + # 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() + item_info.setObjectName("item-info") + item_info.setLayout(item_info_layout) + + # Create the item's widget and layouts + item_hlayout = QtWidgets.QHBoxLayout() + item_hlayout.addStretch() + item_hlayout.addWidget(item_info) + widget = QtWidgets.QWidget() + widget.setLayout(item_hlayout) + + item.setSizeHint(widget.sizeHint()) + + self.addItem(item) + self.setItemWidget(item, widget) + + self.files_updated.emit() + + +class FileSelection(QtWidgets.QVBoxLayout): + """ + The list of files and folders in the GUI, as well as buttons to add and + delete the files and folders. + """ + + def __init__(self, common, background_image_filename, header_text, parent): + super(FileSelection, self).__init__() + + self.common = common + self.parent = parent + + self.server_on = False + + # File list + self.file_list = FileList(self.common, background_image_filename, header_text) + self.file_list.itemSelectionChanged.connect(self.update) + self.file_list.files_dropped.connect(self.update) + self.file_list.files_updated.connect(self.update) + + # Buttons + if self.common.platform == "Darwin": + # The macOS sandbox makes it so the Mac version needs separate add files + # and folders buttons, in order to use native file selection dialogs + self.add_files_button = QtWidgets.QPushButton(strings._("gui_add_files")) + self.add_files_button.clicked.connect(self.add_files) + self.add_folder_button = QtWidgets.QPushButton(strings._("gui_add_folder")) + self.add_folder_button.clicked.connect(self.add_folder) + else: + self.add_button = QtWidgets.QPushButton(strings._("gui_add")) + self.add_button.clicked.connect(self.add) + self.remove_button = QtWidgets.QPushButton(strings._("gui_remove")) + self.remove_button.clicked.connect(self.delete) + button_layout = QtWidgets.QHBoxLayout() + button_layout.addStretch() + if self.common.platform == "Darwin": + button_layout.addWidget(self.add_files_button) + button_layout.addWidget(self.add_folder_button) + else: + button_layout.addWidget(self.add_button) + button_layout.addWidget(self.remove_button) + + # Add the widgets + self.addWidget(self.file_list) + self.addLayout(button_layout) + + self.update() + + def update(self): + """ + Update the GUI elements based on the current state. + """ + # All buttons should be hidden if the server is on + if self.server_on: + if self.common.platform == "Darwin": + self.add_files_button.hide() + self.add_folder_button.hide() + else: + self.add_button.hide() + self.remove_button.hide() + else: + if self.common.platform == "Darwin": + self.add_files_button.show() + self.add_folder_button.show() + else: + self.add_button.show() + + # Delete button should be hidden if item isn't selected + if len(self.file_list.selectedItems()) == 0: + self.remove_button.hide() + else: + self.remove_button.show() + + # Update the file list + self.file_list.update() + + # Save the latest file list to mode settings + self.save_filenames() + + def add(self): + """ + Add button clicked. + """ + file_dialog = AddFileDialog(self.common, caption=strings._("gui_choose_items")) + if file_dialog.exec_() == QtWidgets.QDialog.Accepted: + for filename in file_dialog.selectedFiles(): + self.file_list.add_file(filename) + + self.file_list.setCurrentItem(None) + self.update() + + def add_files(self): + """ + Add files button clicked. + """ + files = QtWidgets.QFileDialog.getOpenFileNames( + self.parent, caption=strings._("gui_choose_items") + ) + filenames = files[0] + for filename in filenames: + self.file_list.add_file(filename) + + def add_folder(self): + """ + Add folder button clicked. + """ + filename = QtWidgets.QFileDialog.getExistingDirectory( + self.parent, + caption=strings._("gui_choose_items"), + options=QtWidgets.QFileDialog.ShowDirsOnly, + ) + self.file_list.add_file(filename) + + def delete(self): + """ + Delete button clicked + """ + selected = self.file_list.selectedItems() + for item in selected: + itemrow = self.file_list.row(item) + self.file_list.takeItem(itemrow) + self.file_list.files_updated.emit() + + self.file_list.setCurrentItem(None) + self.update() + + def server_started(self): + """ + Gets called when the server starts. + """ + self.server_on = True + self.file_list.server_started() + self.update() + + def server_stopped(self): + """ + Gets called when the server stops. + """ + self.server_on = False + self.file_list.server_stopped() + self.update() + + def get_num_files(self): + """ + Returns the total number of files and folders in the list. + """ + return len(range(self.file_list.count())) + + def get_filenames(self): + """ + Return the list of file and folder names + """ + filenames = [] + for index in range(self.file_list.count()): + filenames.append(self.file_list.item(index).filename) + return filenames + + def save_filenames(self): + """ + Save the filenames to mode settings + """ + filenames = self.get_filenames() + if self.parent.tab.mode == self.common.gui.MODE_SHARE: + self.parent.settings.set("share", "filenames", filenames) + elif self.parent.tab.mode == self.common.gui.MODE_WEBSITE: + self.parent.settings.set("website", "filenames", filenames) + + def setFocus(self): + """ + Set the Qt app focus on the file selection box. + """ + self.file_list.setFocus() diff --git a/desktop/src/onionshare/tab/mode/history.py b/desktop/src/onionshare/tab/mode/history.py new file mode 100644 index 00000000..caa36387 --- /dev/null +++ b/desktop/src/onionshare/tab/mode/history.py @@ -0,0 +1,808 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +import time +import subprocess +import os +from datetime import datetime +from PyQt5 import QtCore, QtWidgets, QtGui + +from ... import strings +from ...widgets import Alert +from ...gui_common import GuiCommon + + +class HistoryItem(QtWidgets.QWidget): + """ + The base history item + """ + + STATUS_STARTED = 0 + STATUS_FINISHED = 1 + STATUS_CANCELED = 2 + + def __init__(self): + super(HistoryItem, self).__init__() + + def update(self): + pass + + def cancel(self): + pass + + def get_finished_label_text(self, started): + """ + When an item finishes, returns a string displaying the start/end datetime range. + started is a datetime object. + """ + return self._get_label_text( + "gui_all_modes_transfer_finished", + "gui_all_modes_transfer_finished_range", + started, + ) + + def get_canceled_label_text(self, started): + """ + When an item is canceled, returns a string displaying the start/end datetime range. + started is a datetime object. + """ + return self._get_label_text( + "gui_all_modes_transfer_canceled", + "gui_all_modes_transfer_canceled_range", + started, + ) + + def _get_label_text(self, string_name, string_range_name, started): + """ + Return a string that contains a date, or date range. + """ + ended = datetime.now() + if ( + started.year == ended.year + and started.month == ended.month + and started.day == ended.day + ): + if started.hour == ended.hour and started.minute == ended.minute: + text = strings._(string_name).format(started.strftime("%b %d, %I:%M%p")) + else: + text = strings._(string_range_name).format( + started.strftime("%b %d, %I:%M%p"), ended.strftime("%I:%M%p") + ) + else: + text = strings._(string_range_name).format( + started.strftime("%b %d, %I:%M%p"), ended.strftime("%b %d, %I:%M%p") + ) + return text + + +class ShareHistoryItem(HistoryItem): + """ + Download history item, for share mode + """ + + def __init__(self, common, id, total_bytes): + super(ShareHistoryItem, self).__init__() + self.common = common + + self.id = id + self.total_bytes = total_bytes + self.downloaded_bytes = 0 + self.started = time.time() + self.started_dt = datetime.fromtimestamp(self.started) + self.status = HistoryItem.STATUS_STARTED + + # Label + self.label = QtWidgets.QLabel( + strings._("gui_all_modes_transfer_started").format( + self.started_dt.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.setMaximum(total_bytes / 1024) + self.progress_bar.setValue(0) + self.progress_bar.setStyleSheet( + self.common.gui.css["downloads_uploads_progress_bar"] + ) + self.progress_bar.total_bytes = total_bytes + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.label) + 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 / 1024) + if (downloaded_bytes / 1024) == (self.progress_bar.total_bytes / 1024): + pb_fmt = strings._("gui_all_modes_progress_complete").format( + self.common.format_seconds(time.time() - self.started) + ) + + # Change the label + self.label.setText(self.get_finished_label_text(self.started_dt)) + self.status = HistoryItem.STATUS_FINISHED + + 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_all_modes_progress_starting").format( + self.common.human_readable_filesize(downloaded_bytes) + ) + else: + pb_fmt = strings._("gui_all_modes_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")) + self.status = HistoryItem.STATUS_CANCELED + + @property + def estimated_time_remaining(self): + return self.common.estimated_time_remaining( + self.downloaded_bytes, self.total_bytes, self.started + ) + + +class ReceiveHistoryItemFile(QtWidgets.QWidget): + def __init__(self, common, filename): + super(ReceiveHistoryItemFile, self).__init__() + self.common = common + + self.common.log("ReceiveHistoryItemFile", "__init__", f"filename: {filename}") + + self.filename = filename + self.dir = None + 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.gui.css["receive_file_size"]) + self.filesize_label.hide() + + # Folder button + folder_pixmap = QtGui.QPixmap.fromImage( + QtGui.QImage(GuiCommon.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 set_dir(self, dir): + self.dir = dir + + def open_folder(self): + """ + Open the downloads folder, with the file selected, in a cross-platform manner + """ + self.common.log("ReceiveHistoryItemFile", "open_folder") + + if not self.dir: + self.common.log( + "ReceiveHistoryItemFile", + "open_folder", + "dir has not been set yet, can't open folder", + ) + return + + abs_filename = os.path.join(self.dir, self.filename) + + # Linux + if self.common.platform == "Linux" or self.common.platform == "BSD": + try: + # If nautilus is available, open it + subprocess.Popen(["xdg-open", self.dir]) + except: + Alert( + self.common, + strings._("gui_open_folder_error").format(abs_filename), + ) + + # macOS + elif self.common.platform == "Darwin": + subprocess.call(["open", "-R", abs_filename]) + + # Windows + elif self.common.platform == "Windows": + subprocess.Popen(["explorer", f"/select,{abs_filename}"]) + + +class ReceiveHistoryItem(HistoryItem): + def __init__(self, common, id, content_length): + super(ReceiveHistoryItem, self).__init__() + self.common = common + self.id = id + self.content_length = content_length + self.started = datetime.now() + self.status = HistoryItem.STATUS_STARTED + + # Label + self.label = QtWidgets.QLabel( + strings._("gui_all_modes_transfer_started").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.gui.css["downloads_uploads_progress_bar"] + ) + + # This layout contains file widgets + self.files_layout = QtWidgets.QVBoxLayout() + self.files_layout.setContentsMargins(0, 0, 0, 0) + files_widget = QtWidgets.QWidget() + files_widget.setStyleSheet(self.common.gui.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 / 1024) + self.progress_bar.setValue(total_uploaded_bytes / 1024) + + elapsed = datetime.now() - self.started + if elapsed.seconds < 10: + pb_fmt = strings._("gui_all_modes_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_all_modes_progress_eta").format( + self.common.human_readable_filesize(total_uploaded_bytes), + estimated_time_remaining, + ) + + self.progress_bar.setFormat(pb_fmt) + + # 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] = ReceiveHistoryItemFile(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"] == "set_dir": + self.files[data["filename"]].set_dir(data["dir"]) + + elif data["action"] == "finished": + # Change the status + self.status = HistoryItem.STATUS_FINISHED + + # Hide the progress bar + self.progress_bar.hide() + + # Change the label + self.label.setText(self.get_finished_label_text(self.started)) + + elif data["action"] == "canceled": + # Change the status + self.status = HistoryItem.STATUS_CANCELED + + # Hide the progress bar + self.progress_bar.hide() + + # Change the label + self.label.setText(self.get_canceled_label_text(self.started)) + + +class IndividualFileHistoryItem(HistoryItem): + """ + Individual file history item, for share mode viewing of individual files + """ + + def __init__(self, common, data, path): + super(IndividualFileHistoryItem, self).__init__() + self.status = HistoryItem.STATUS_STARTED + self.common = common + + self.id = id + self.path = path + self.total_bytes = 0 + self.downloaded_bytes = 0 + self.started = time.time() + self.started_dt = datetime.fromtimestamp(self.started) + self.status = HistoryItem.STATUS_STARTED + + self.directory_listing = "directory_listing" in data + + # Labels + self.timestamp_label = QtWidgets.QLabel( + self.started_dt.strftime("%b %d, %I:%M%p") + ) + self.timestamp_label.setStyleSheet( + self.common.gui.css["history_individual_file_timestamp_label"] + ) + self.path_label = QtWidgets.QLabel(self.path) + self.status_code_label = QtWidgets.QLabel() + + # 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.setValue(0) + self.progress_bar.setStyleSheet( + self.common.gui.css["downloads_uploads_progress_bar"] + ) + + # Text layout + labels_layout = QtWidgets.QHBoxLayout() + labels_layout.addWidget(self.timestamp_label) + labels_layout.addWidget(self.path_label) + labels_layout.addWidget(self.status_code_label) + labels_layout.addStretch() + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addLayout(labels_layout) + layout.addWidget(self.progress_bar) + self.setLayout(layout) + + # Is a status code already sent? + if "status_code" in data: + self.status_code_label.setText(str(data["status_code"])) + if data["status_code"] >= 200 and data["status_code"] < 300: + self.status_code_label.setStyleSheet( + self.common.gui.css["history_individual_file_status_code_label_2xx"] + ) + if data["status_code"] >= 400 and data["status_code"] < 500: + self.status_code_label.setStyleSheet( + self.common.gui.css["history_individual_file_status_code_label_4xx"] + ) + self.status = HistoryItem.STATUS_FINISHED + self.progress_bar.hide() + return + + else: + self.total_bytes = data["filesize"] + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(data["filesize"] / 1024) + self.progress_bar.total_bytes = data["filesize"] + + # Start at 0 + self.update(0) + + def update(self, downloaded_bytes): + self.downloaded_bytes = downloaded_bytes + + self.progress_bar.setValue(downloaded_bytes / 1024) + if (downloaded_bytes / 1024) == (self.progress_bar.total_bytes / 1024): + self.status_code_label.setText("200") + self.status_code_label.setStyleSheet( + self.common.gui.css["history_individual_file_status_code_label_2xx"] + ) + self.progress_bar.hide() + self.status = HistoryItem.STATUS_FINISHED + + 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_all_modes_progress_starting").format( + self.common.human_readable_filesize(downloaded_bytes) + ) + else: + pb_fmt = strings._("gui_all_modes_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")) + self.status = HistoryItem.STATUS_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. + """ + if id in self.items: + self.items[id].update(data) + + def cancel(self, id): + """ + Cancel an item. Override this method. + """ + if id in self.items: + self.items[id].cancel() + + def reset(self): + """ + Reset all items, emptying the list. Override this method. + """ + for key, item in self.items.copy().items(): + self.items_layout.removeWidget(item) + item.close() + del self.items[key] + + +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, mode=""): + super(History, self).__init__() + self.common = common + self.mode = mode + + self.setMinimumWidth(350) + + # In progress and completed counters + self.in_progress_count = 0 + self.completed_count = 0 + self.requests_count = 0 + + # In progress, completed, and requests labels + self.in_progress_label = QtWidgets.QLabel() + self.in_progress_label.setStyleSheet(self.common.gui.css["mode_info_label"]) + self.completed_label = QtWidgets.QLabel() + self.completed_label.setStyleSheet(self.common.gui.css["mode_info_label"]) + self.requests_label = QtWidgets.QLabel() + self.requests_label.setStyleSheet(self.common.gui.css["mode_info_label"]) + + # Header + self.header_label = QtWidgets.QLabel(header_text) + self.header_label.setStyleSheet(self.common.gui.css["downloads_uploads_label"]) + self.clear_button = QtWidgets.QPushButton( + strings._("gui_all_modes_clear_history") + ) + self.clear_button.setStyleSheet(self.common.gui.css["downloads_uploads_clear"]) + self.clear_button.setFlat(True) + self.clear_button.clicked.connect(self.reset) + header_layout = QtWidgets.QHBoxLayout() + 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(self.requests_label) + header_layout.addWidget(self.clear_button) + + # 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.gui.css["downloads_uploads_empty_text"] + ) + empty_layout = QtWidgets.QVBoxLayout() + empty_layout.addStretch() + empty_layout.addWidget(self.empty_image) + empty_layout.addWidget(self.empty_text) + empty_layout.addStretch() + self.empty = QtWidgets.QWidget() + self.empty.setStyleSheet(self.common.gui.css["downloads_uploads_empty"]) + self.empty.setLayout(empty_layout) + + # 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", f"id: {id}, item: {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() + if len(self.item_list.items) == 0: + # Hide not empty, show empty + self.not_empty.hide() + self.empty.show() + # Reset in-progress counter + self.in_progress_count = 0 + self.update_in_progress() + + # Reset completed counter + self.completed_count = 0 + self.update_completed() + + # Reset web requests counter + self.requests_count = 0 + self.update_requests() + + def update_completed(self): + """ + Update the 'completed' widget. + """ + if self.completed_count == 0: + image = GuiCommon.get_resource_path("images/history_completed_none.png") + else: + image = GuiCommon.get_resource_path("images/history_completed.png") + self.completed_label.setText(f'<img src="{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 = GuiCommon.get_resource_path("images/history_in_progress_none.png") + else: + image = GuiCommon.get_resource_path("images/history_in_progress.png") + + self.in_progress_label.setText( + f'<img src="{image}" /> {self.in_progress_count}' + ) + self.in_progress_label.setToolTip( + strings._("history_in_progress_tooltip").format(self.in_progress_count) + ) + + def update_requests(self): + """ + Update the 'web requests' widget. + """ + if self.requests_count == 0: + image = GuiCommon.get_resource_path("images/history_requests_none.png") + else: + image = GuiCommon.get_resource_path("images/history_requests.png") + + self.requests_label.setText(f'<img src="{image}" /> {self.requests_count}') + self.requests_label.setToolTip( + strings._("history_requests_tooltip").format(self.requests_count) + ) + + +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.gui.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 History is hidden. + """ + if increment and not self.history_widget.isVisible(): + self.indicator_count += 1 + + self.indicator_label.setText(str(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() diff --git a/desktop/src/onionshare/tab/mode/mode_settings_widget.py b/desktop/src/onionshare/tab/mode/mode_settings_widget.py new file mode 100644 index 00000000..8c070897 --- /dev/null +++ b/desktop/src/onionshare/tab/mode/mode_settings_widget.py @@ -0,0 +1,296 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +from PyQt5 import QtCore, QtWidgets + +from ... import strings + + +class ModeSettingsWidget(QtWidgets.QWidget): + """ + All of the common settings for each mode are in this widget + """ + + change_persistent = QtCore.pyqtSignal(int, bool) + + def __init__(self, common, tab, mode_settings): + super(ModeSettingsWidget, self).__init__() + self.common = common + self.tab = tab + self.settings = mode_settings + + # Downstream Mode need to fill in this layout with its settings + self.mode_specific_layout = QtWidgets.QVBoxLayout() + + # Persistent + self.persistent_checkbox = QtWidgets.QCheckBox() + self.persistent_checkbox.clicked.connect(self.persistent_checkbox_clicked) + self.persistent_checkbox.setText(strings._("mode_settings_persistent_checkbox")) + if self.settings.get("persistent", "enabled"): + self.persistent_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.persistent_checkbox.setCheckState(QtCore.Qt.Unchecked) + + # Public + self.public_checkbox = QtWidgets.QCheckBox() + self.public_checkbox.clicked.connect(self.public_checkbox_clicked) + self.public_checkbox.setText(strings._("mode_settings_public_checkbox")) + if self.settings.get("general", "public"): + self.public_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.public_checkbox.setCheckState(QtCore.Qt.Unchecked) + + # Whether or not to use an auto-start timer + self.autostart_timer_checkbox = QtWidgets.QCheckBox() + self.autostart_timer_checkbox.clicked.connect( + self.autostart_timer_checkbox_clicked + ) + self.autostart_timer_checkbox.setText( + strings._("mode_settings_autostart_timer_checkbox") + ) + if self.settings.get("general", "autostart_timer"): + self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) + + # The autostart timer widget + self.autostart_timer_widget = QtWidgets.QDateTimeEdit() + self.autostart_timer_widget.setDisplayFormat("hh:mm A MMM d, yy") + self.autostart_timer_reset() + self.autostart_timer_widget.setCurrentSection( + QtWidgets.QDateTimeEdit.MinuteSection + ) + if self.settings.get("general", "autostart_timer"): + self.autostart_timer_widget.show() + else: + self.autostart_timer_widget.hide() + + # Autostart timer layout + autostart_timer_layout = QtWidgets.QHBoxLayout() + autostart_timer_layout.setContentsMargins(0, 0, 0, 0) + autostart_timer_layout.addWidget(self.autostart_timer_checkbox) + autostart_timer_layout.addWidget(self.autostart_timer_widget) + + # Whether or not to use an auto-stop timer + self.autostop_timer_checkbox = QtWidgets.QCheckBox() + self.autostop_timer_checkbox.clicked.connect( + self.autostop_timer_checkbox_clicked + ) + self.autostop_timer_checkbox.setText( + strings._("mode_settings_autostop_timer_checkbox") + ) + if self.settings.get("general", "autostop_timer"): + self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Unchecked) + + # The autostop timer widget + self.autostop_timer_widget = QtWidgets.QDateTimeEdit() + self.autostop_timer_widget.setDisplayFormat("hh:mm A MMM d, yy") + self.autostop_timer_reset() + self.autostop_timer_widget.setCurrentSection( + QtWidgets.QDateTimeEdit.MinuteSection + ) + if self.settings.get("general", "autostop_timer"): + self.autostop_timer_widget.show() + else: + self.autostop_timer_widget.hide() + + # Autostop timer layout + autostop_timer_layout = QtWidgets.QHBoxLayout() + autostop_timer_layout.setContentsMargins(0, 0, 0, 0) + autostop_timer_layout.addWidget(self.autostop_timer_checkbox) + autostop_timer_layout.addWidget(self.autostop_timer_widget) + + # Legacy address + self.legacy_checkbox = QtWidgets.QCheckBox() + self.legacy_checkbox.clicked.connect(self.legacy_checkbox_clicked) + self.legacy_checkbox.clicked.connect(self.update_ui) + self.legacy_checkbox.setText(strings._("mode_settings_legacy_checkbox")) + if self.settings.get("general", "legacy"): + self.legacy_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.legacy_checkbox.setCheckState(QtCore.Qt.Unchecked) + + # Client auth + self.client_auth_checkbox = QtWidgets.QCheckBox() + self.client_auth_checkbox.clicked.connect(self.client_auth_checkbox_clicked) + self.client_auth_checkbox.clicked.connect(self.update_ui) + self.client_auth_checkbox.setText( + strings._("mode_settings_client_auth_checkbox") + ) + if self.settings.get("general", "client_auth"): + self.client_auth_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.client_auth_checkbox.setCheckState(QtCore.Qt.Unchecked) + + # Toggle advanced settings + self.toggle_advanced_button = QtWidgets.QPushButton() + self.toggle_advanced_button.clicked.connect(self.toggle_advanced_clicked) + self.toggle_advanced_button.setFlat(True) + self.toggle_advanced_button.setStyleSheet( + self.common.gui.css["mode_settings_toggle_advanced"] + ) + + # Advanced group itself + advanced_layout = QtWidgets.QVBoxLayout() + advanced_layout.setContentsMargins(0, 0, 0, 0) + advanced_layout.addLayout(autostart_timer_layout) + advanced_layout.addLayout(autostop_timer_layout) + advanced_layout.addWidget(self.legacy_checkbox) + advanced_layout.addWidget(self.client_auth_checkbox) + self.advanced_widget = QtWidgets.QWidget() + self.advanced_widget.setLayout(advanced_layout) + self.advanced_widget.hide() + + layout = QtWidgets.QVBoxLayout() + layout.addLayout(self.mode_specific_layout) + layout.addWidget(self.persistent_checkbox) + layout.addWidget(self.public_checkbox) + layout.addWidget(self.advanced_widget) + layout.addWidget(self.toggle_advanced_button) + self.setLayout(layout) + + self.update_ui() + + def update_ui(self): + # Update text on advanced group toggle button + if self.advanced_widget.isVisible(): + self.toggle_advanced_button.setText( + strings._("mode_settings_advanced_toggle_hide") + ) + else: + self.toggle_advanced_button.setText( + strings._("mode_settings_advanced_toggle_show") + ) + + # Client auth is only a legacy option + if self.client_auth_checkbox.isChecked(): + self.legacy_checkbox.setChecked(True) + self.legacy_checkbox.setEnabled(False) + else: + self.legacy_checkbox.setEnabled(True) + if self.legacy_checkbox.isChecked(): + self.client_auth_checkbox.show() + else: + self.client_auth_checkbox.hide() + + # If the server has been started in the past, prevent changing legacy option + if self.settings.get("onion", "private_key"): + if self.legacy_checkbox.isChecked(): + # If using legacy, disable legacy and client auth options + self.legacy_checkbox.setEnabled(False) + self.client_auth_checkbox.setEnabled(False) + else: + # If using v3, hide legacy and client auth options + self.legacy_checkbox.hide() + self.client_auth_checkbox.hide() + + def persistent_checkbox_clicked(self): + self.settings.set("persistent", "enabled", self.persistent_checkbox.isChecked()) + self.settings.set("persistent", "mode", self.tab.mode) + self.change_persistent.emit( + self.tab.tab_id, self.persistent_checkbox.isChecked() + ) + + # If disabling persistence, delete the file from disk + if not self.persistent_checkbox.isChecked(): + self.settings.delete() + + def public_checkbox_clicked(self): + self.settings.set("general", "public", self.public_checkbox.isChecked()) + + def autostart_timer_checkbox_clicked(self): + self.settings.set( + "general", "autostart_timer", self.autostart_timer_checkbox.isChecked() + ) + + if self.autostart_timer_checkbox.isChecked(): + self.autostart_timer_widget.show() + else: + self.autostart_timer_widget.hide() + + def autostop_timer_checkbox_clicked(self): + self.settings.set( + "general", "autostop_timer", self.autostop_timer_checkbox.isChecked() + ) + + if self.autostop_timer_checkbox.isChecked(): + self.autostop_timer_widget.show() + else: + self.autostop_timer_widget.hide() + + def legacy_checkbox_clicked(self): + self.settings.set("general", "legacy", self.legacy_checkbox.isChecked()) + + def client_auth_checkbox_clicked(self): + self.settings.set( + "general", "client_auth", self.client_auth_checkbox.isChecked() + ) + + def toggle_advanced_clicked(self): + if self.advanced_widget.isVisible(): + self.advanced_widget.hide() + else: + self.advanced_widget.show() + + self.update_ui() + + def autostart_timer_reset(self): + """ + Reset the auto-start timer in the UI after stopping a share + """ + if self.common.gui.local_only: + # For testing + self.autostart_timer_widget.setDateTime( + QtCore.QDateTime.currentDateTime().addSecs(15) + ) + self.autostart_timer_widget.setMinimumDateTime( + QtCore.QDateTime.currentDateTime() + ) + else: + self.autostart_timer_widget.setDateTime( + QtCore.QDateTime.currentDateTime().addSecs( + 300 + ) # 5 minutes in the future + ) + self.autostart_timer_widget.setMinimumDateTime( + QtCore.QDateTime.currentDateTime().addSecs(60) + ) + + def autostop_timer_reset(self): + """ + Reset the auto-stop timer in the UI after stopping a share + """ + if self.common.gui.local_only: + # For testing + self.autostop_timer_widget.setDateTime( + QtCore.QDateTime.currentDateTime().addSecs(15) + ) + self.autostop_timer_widget.setMinimumDateTime( + QtCore.QDateTime.currentDateTime() + ) + else: + self.autostop_timer_widget.setDateTime( + QtCore.QDateTime.currentDateTime().addSecs(300) + ) + self.autostop_timer_widget.setMinimumDateTime( + QtCore.QDateTime.currentDateTime().addSecs(60) + ) diff --git a/desktop/src/onionshare/tab/mode/receive_mode/__init__.py b/desktop/src/onionshare/tab/mode/receive_mode/__init__.py new file mode 100644 index 00000000..35b4b7e9 --- /dev/null +++ b/desktop/src/onionshare/tab/mode/receive_mode/__init__.py @@ -0,0 +1,321 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +import os +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare_cli.web import Web + +from ..history import History, ToggleHistory, ReceiveHistoryItem +from .. import Mode +from .... import strings +from ....widgets import MinimumWidthWidget, Alert +from ....gui_common import GuiCommon + + +class ReceiveMode(Mode): + """ + Parts of the main window UI for receiving files. + """ + + def init(self): + """ + Custom initialization for ReceiveMode. + """ + # Create the Web object + self.web = Web(self.common, True, self.settings, "receive") + + # Receive image + self.image_label = QtWidgets.QLabel() + self.image_label.setPixmap( + QtGui.QPixmap.fromImage( + QtGui.QImage(GuiCommon.get_resource_path("images/mode_receive.png")) + ) + ) + self.image_label.setFixedSize(250, 250) + image_layout = QtWidgets.QVBoxLayout() + image_layout.addWidget(self.image_label) + self.image = QtWidgets.QWidget() + self.image.setLayout(image_layout) + + # Settings + data_dir_label = QtWidgets.QLabel( + strings._("mode_settings_receive_data_dir_label") + ) + self.data_dir_lineedit = QtWidgets.QLineEdit() + self.data_dir_lineedit.setReadOnly(True) + self.data_dir_lineedit.setText(self.settings.get("receive", "data_dir")) + data_dir_button = QtWidgets.QPushButton( + strings._("mode_settings_receive_data_dir_browse_button") + ) + data_dir_button.clicked.connect(self.data_dir_button_clicked) + data_dir_layout = QtWidgets.QHBoxLayout() + data_dir_layout.addWidget(data_dir_label) + data_dir_layout.addWidget(self.data_dir_lineedit) + data_dir_layout.addWidget(data_dir_button) + + self.mode_settings_widget.mode_specific_layout.addLayout(data_dir_layout) + + # Server status + self.server_status.set_mode("receive") + self.server_status.server_started_finished.connect(self.update_primary_action) + self.server_status.server_stopped.connect(self.update_primary_action) + self.server_status.server_canceled.connect(self.update_primary_action) + + # Tell server_status about web, then update + self.server_status.web = self.web + self.server_status.update() + + # Upload history + self.history = History( + self.common, + QtGui.QPixmap.fromImage( + QtGui.QImage( + GuiCommon.get_resource_path("images/receive_icon_transparent.png") + ) + ), + strings._("gui_receive_mode_no_files"), + strings._("gui_all_modes_history"), + ) + self.history.hide() + + # Toggle history + self.toggle_history = ToggleHistory( + self.common, + self, + self.history, + QtGui.QIcon(GuiCommon.get_resource_path("images/receive_icon_toggle.png")), + QtGui.QIcon( + GuiCommon.get_resource_path("images/receive_icon_toggle_selected.png") + ), + ) + + # Header + header_label = QtWidgets.QLabel(strings._("gui_new_tab_receive_button")) + header_label.setStyleSheet(self.common.gui.css["mode_header_label"]) + + # Receive mode warning + receive_warning = QtWidgets.QLabel(strings._("gui_receive_mode_warning")) + 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(header_label) + self.main_layout.addWidget(receive_warning) + self.main_layout.addWidget(self.primary_action) + self.main_layout.addWidget(MinimumWidthWidget(525)) + + # Row layout + content_row = QtWidgets.QHBoxLayout() + content_row.addLayout(self.main_layout) + content_row.addWidget(self.image) + row_layout = QtWidgets.QVBoxLayout() + row_layout.addLayout(top_bar_layout) + row_layout.addStretch() + row_layout.addLayout(content_row) + row_layout.addWidget(self.server_status) + row_layout.addStretch() + + # Column layout + self.column_layout = QtWidgets.QHBoxLayout() + self.column_layout.addLayout(row_layout) + self.column_layout.addWidget(self.history, stretch=1) + + # Wrapper layout + self.wrapper_layout = QtWidgets.QVBoxLayout() + self.wrapper_layout.addLayout(self.column_layout) + self.setLayout(self.wrapper_layout) + + def data_dir_button_clicked(self): + """ + Browse for a new OnionShare data directory, and save to tab settings + """ + data_dir = self.data_dir_lineedit.text() + selected_dir = QtWidgets.QFileDialog.getExistingDirectory( + self, strings._("mode_settings_receive_data_dir_label"), data_dir + ) + + if selected_dir: + # If we're running inside a flatpak package, the data dir must be inside ~/OnionShare + if self.common.gui.is_flatpak: + if not selected_dir.startswith(os.path.expanduser("~/OnionShare")): + Alert(self.common, strings._("gui_receive_flatpak_data_dir")) + return + + self.common.log( + "ReceiveMode", + "data_dir_button_clicked", + f"selected dir: {selected_dir}", + ) + self.data_dir_lineedit.setText(selected_dir) + self.settings.set("receive", "data_dir", selected_dir) + + def get_stop_server_autostop_timer_text(self): + """ + Return the string to put on the stop server button, if there's an auto-stop timer + """ + return strings._("gui_receive_stop_server_autostop_timer") + + def autostop_timer_finished_should_stop_server(self): + """ + The auto-stop timer expired, should we stop the server? Returns a bool + """ + # If there were no attempts to upload files, or all uploads are done, we can stop + if ( + self.web.receive_mode.cur_history_id == 0 + or not self.web.receive_mode.uploads_in_progress + ): + self.server_status.stop_server() + self.server_status_label.setText(strings._("close_on_autostop_timer")) + return True + # An upload is probably still running - hold off on stopping the share, but block new shares. + else: + self.server_status_label.setText( + strings._("gui_receive_mode_autostop_timer_waiting") + ) + self.web.receive_mode.can_upload = False + return False + + def start_server_custom(self): + """ + Starting the server. + """ + # Reset web counters + self.web.receive_mode.cur_history_id = 0 + self.web.reset_invalid_passwords() + + # Hide and reset the uploads if we have previously shared + self.reset_info_counters() + + def start_server_step2_custom(self): + """ + Step 2 in starting the server. + """ + # Continue + self.starting_server_step3.emit() + self.start_server_finished.emit() + + def handle_tor_broke_custom(self): + """ + Connection to Tor broke. + """ + self.primary_action.hide() + + def handle_request_load(self, event): + """ + Handle REQUEST_LOAD event. + """ + self.system_tray.showMessage( + strings._("systray_page_loaded_title"), + strings._("systray_page_loaded_message"), + ) + + def handle_request_started(self, event): + """ + Handle REQUEST_STARTED event. + """ + item = ReceiveHistoryItem( + 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_receive_started_title"), + strings._("systray_receive_started_message"), + ) + + def handle_request_progress(self, event): + """ + Handle REQUEST_PROGRESS event. + """ + self.history.update( + event["data"]["id"], + {"action": "progress", "progress": event["data"]["progress"]}, + ) + + def handle_request_upload_file_renamed(self, event): + """ + Handle REQUEST_UPLOAD_FILE_RENAMED event. + """ + self.history.update( + event["data"]["id"], + { + "action": "rename", + "old_filename": event["data"]["old_filename"], + "new_filename": event["data"]["new_filename"], + }, + ) + + def handle_request_upload_set_dir(self, event): + """ + Handle REQUEST_UPLOAD_SET_DIR event. + """ + self.history.update( + event["data"]["id"], + { + "action": "set_dir", + "filename": event["data"]["filename"], + "dir": event["data"]["dir"], + }, + ) + + def handle_request_upload_finished(self, event): + """ + Handle REQUEST_UPLOAD_FINISHED event. + """ + 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 handle_request_upload_canceled(self, event): + """ + Handle REQUEST_UPLOAD_CANCELED event. + """ + self.history.update(event["data"]["id"], {"action": "canceled"}) + self.history.in_progress_count -= 1 + 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() + + def reset_info_counters(self): + """ + Set the info counters back to zero. + """ + self.history.reset() + self.toggle_history.indicator_count = 0 + self.toggle_history.update_indicator() + + def update_primary_action(self): + self.common.log("ReceiveMode", "update_primary_action") diff --git a/desktop/src/onionshare/tab/mode/share_mode/__init__.py b/desktop/src/onionshare/tab/mode/share_mode/__init__.py new file mode 100644 index 00000000..3c34ab46 --- /dev/null +++ b/desktop/src/onionshare/tab/mode/share_mode/__init__.py @@ -0,0 +1,471 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +import os +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare_cli.onion import * +from onionshare_cli.common import Common +from onionshare_cli.web import Web + +from .threads import CompressThread +from .. import Mode +from ..file_selection import FileSelection +from ..history import History, ToggleHistory, ShareHistoryItem +from .... import strings +from ....widgets import Alert, MinimumWidthWidget +from ....gui_common import GuiCommon + + +class ShareMode(Mode): + """ + Parts of the main window UI for sharing files. + """ + + def init(self): + """ + Custom initialization for ReceiveMode. + """ + # Threads start out as None + self.compress_thread = None + + # Create the Web object + self.web = Web(self.common, True, self.settings, "share") + + # Settings + self.autostop_sharing_checkbox = QtWidgets.QCheckBox() + self.autostop_sharing_checkbox.clicked.connect( + self.autostop_sharing_checkbox_clicked + ) + self.autostop_sharing_checkbox.setText( + strings._("mode_settings_share_autostop_sharing_checkbox") + ) + if self.settings.get("share", "autostop_sharing"): + self.autostop_sharing_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.autostop_sharing_checkbox.setCheckState(QtCore.Qt.Unchecked) + + self.mode_settings_widget.mode_specific_layout.addWidget( + self.autostop_sharing_checkbox + ) + + # File selection + self.file_selection = FileSelection( + self.common, + "images/mode_share.png", + strings._("gui_new_tab_share_button"), + self, + ) + if self.filenames: + for filename in self.filenames: + self.file_selection.file_list.add_file(filename) + + # Server status + self.server_status.set_mode("share", self.file_selection) + self.server_status.server_started.connect(self.file_selection.server_started) + self.server_status.server_stopped.connect(self.file_selection.server_stopped) + self.server_status.server_stopped.connect(self.update_primary_action) + self.server_status.server_canceled.connect(self.file_selection.server_stopped) + self.server_status.server_canceled.connect(self.update_primary_action) + self.file_selection.file_list.files_updated.connect(self.server_status.update) + self.file_selection.file_list.files_updated.connect(self.update_primary_action) + # Tell server_status about web, then update + self.server_status.web = self.web + self.server_status.update() + + # Filesize warning + self.filesize_warning = QtWidgets.QLabel() + self.filesize_warning.setWordWrap(True) + self.filesize_warning.setStyleSheet( + self.common.gui.css["share_filesize_warning"] + ) + self.filesize_warning.hide() + + # Download history + self.history = History( + self.common, + QtGui.QPixmap.fromImage( + QtGui.QImage( + GuiCommon.get_resource_path("images/share_icon_transparent.png") + ) + ), + strings._("gui_share_mode_no_files"), + strings._("gui_all_modes_history"), + ) + self.history.hide() + + # Info label + self.info_label = QtWidgets.QLabel() + self.info_label.hide() + + # Delete all files button + self.remove_all_button = QtWidgets.QPushButton( + strings._("gui_file_selection_remove_all") + ) + self.remove_all_button.setFlat(True) + self.remove_all_button.setStyleSheet( + self.common.gui.css["share_delete_all_files_button"] + ) + self.remove_all_button.clicked.connect(self.delete_all) + self.remove_all_button.hide() + + # Toggle history + self.toggle_history = ToggleHistory( + self.common, + self, + self.history, + QtGui.QIcon(GuiCommon.get_resource_path("images/share_icon_toggle.png")), + QtGui.QIcon( + GuiCommon.get_resource_path("images/share_icon_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.remove_all_button) + top_bar_layout.addWidget(self.toggle_history) + + # Primary action layout + self.primary_action_layout.addWidget(self.filesize_warning) + self.primary_action.hide() + self.update_primary_action() + + # Status bar, zip progress bar + self._zip_progress_bar = None + + # Main layout + self.main_layout = QtWidgets.QVBoxLayout() + 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.server_status) + self.main_layout.addWidget(MinimumWidthWidget(700)) + + # Column layout + self.column_layout = QtWidgets.QHBoxLayout() + self.column_layout.addLayout(self.main_layout) + self.column_layout.addWidget(self.history, stretch=1) + + # Wrapper layout + self.wrapper_layout = QtWidgets.QVBoxLayout() + self.wrapper_layout.addLayout(self.column_layout) + self.setLayout(self.wrapper_layout) + + # Always start with focus on file selection + self.file_selection.setFocus() + + def autostop_sharing_checkbox_clicked(self): + """ + Save autostop sharing setting to the tab settings + """ + self.settings.set( + "share", "autostop_sharing", self.autostop_sharing_checkbox.isChecked() + ) + + def get_stop_server_autostop_timer_text(self): + """ + Return the string to put on the stop server button, if there's an auto-stop timer + """ + return strings._("gui_share_stop_server_autostop_timer") + + def autostop_timer_finished_should_stop_server(self): + """ + The auto-stop timer expired, should we stop the server? Returns a bool + """ + # If there were no attempts to download the share, or all downloads are done, we can stop + if self.history.in_progress_count == 0 or self.web.done: + self.server_status.stop_server() + self.server_status_label.setText(strings._("close_on_autostop_timer")) + return True + # A download is probably still running - hold off on stopping the share + else: + self.server_status_label.setText( + strings._("gui_share_mode_autostop_timer_waiting") + ) + return False + + def start_server_custom(self): + """ + Starting the server. + """ + # Reset web counters + self.web.share_mode.cur_history_id = 0 + self.web.reset_invalid_passwords() + + # Hide and reset the downloads if we have previously shared + self.reset_info_counters() + + self.remove_all_button.hide() + + def start_server_step2_custom(self): + """ + Step 2 in starting the server. Zipping up files. + """ + # Add progress bar to the status bar, indicating the compressing of files. + self._zip_progress_bar = ZipProgressBar(self.common, 0) + self.filenames = self.file_selection.get_filenames() + + 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 + 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): + """ + Step 3 in starting the server. Remove zip progess bar, and display large filesize + warning, if applicable. + """ + # Remove zip progress bar + if self._zip_progress_bar is not None: + self.status_bar.removeWidget(self._zip_progress_bar) + self._zip_progress_bar = None + + # Warn about sending large files over Tor + if self.web.share_mode.download_filesize >= 157286400: # 150mb + self.filesize_warning.setText(strings._("large_filesize")) + self.filesize_warning.show() + + def start_server_error_custom(self): + """ + Start server error. + """ + if self._zip_progress_bar is not None: + self.status_bar.removeWidget(self._zip_progress_bar) + self._zip_progress_bar = None + + def stop_server_custom(self): + """ + Stop server. + """ + # Remove the progress bar + if self._zip_progress_bar is not None: + self.status_bar.removeWidget(self._zip_progress_bar) + self._zip_progress_bar = None + + self.filesize_warning.hide() + self.history.in_progress_count = 0 + self.history.completed_count = 0 + self.history.update_in_progress() + self.file_selection.file_list.adjustSize() + + self.remove_all_button.show() + + def cancel_server_custom(self): + """ + Stop the compression thread on cancel + """ + if self.compress_thread: + self.common.log("ShareMode", "cancel_server: quitting compress thread") + self.compress_thread.quit() + + def handle_tor_broke_custom(self): + """ + Connection to Tor broke. + """ + self.primary_action.hide() + + def handle_request_started(self, event): + """ + Handle REQUEST_STARTED event. + """ + if event["data"]["use_gzip"]: + filesize = self.web.share_mode.gzip_filesize + else: + filesize = self.web.share_mode.download_filesize + + item = ShareHistoryItem(self.common, event["data"]["id"], filesize) + 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_share_started_title"), + strings._("systray_share_started_message"), + ) + + def handle_request_progress(self, event): + """ + Handle REQUEST_PROGRESS event. + """ + self.history.update(event["data"]["id"], event["data"]["bytes"]) + + # Is the download complete? + if event["data"]["bytes"] == self.web.share_mode.filesize: + self.system_tray.showMessage( + strings._("systray_share_completed_title"), + strings._("systray_share_completed_message"), + ) + + # 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.settings.get("share", "autostop_sharing"): + self.server_status.stop_server() + self.status_bar.clearMessage() + self.server_status_label.setText(strings._("closing_automatically")) + else: + if self.server_status.status == self.server_status.STATUS_STOPPED: + self.history.cancel(event["data"]["id"]) + self.history.in_progress_count = 0 + self.history.update_in_progress() + + def handle_request_canceled(self, event): + """ + Handle REQUEST_CANCELED event. + """ + self.history.cancel(event["data"]["id"]) + + # Update in progress count + self.history.in_progress_count -= 1 + self.history.update_in_progress() + self.system_tray.showMessage( + strings._("systray_share_canceled_title"), + strings._("systray_share_canceled_message"), + ) + + def on_reload_settings(self): + """ + If there were some files listed for sharing, we should be ok to re-enable + the 'Start Sharing' button now. + """ + if self.server_status.file_selection.get_num_files() > 0: + self.primary_action.show() + self.info_label.show() + self.remove_all_button.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: + self.primary_action.show() + self.info_label.show() + self.remove_all_button.show() + + # Update the file count in the info label + total_size_bytes = 0 + for index in range(self.file_selection.file_list.count()): + item = self.file_selection.file_list.item(index) + total_size_bytes += item.size_bytes + total_size_readable = self.common.human_readable_filesize(total_size_bytes) + + if file_count > 1: + self.info_label.setText( + strings._("gui_file_info").format(file_count, total_size_readable) + ) + else: + self.info_label.setText( + strings._("gui_file_info_single").format( + file_count, total_size_readable + ) + ) + + else: + self.primary_action.hide() + self.info_label.hide() + self.remove_all_button.hide() + + def reset_info_counters(self): + """ + Set the info counters back to zero. + """ + self.history.reset() + self.toggle_history.indicator_count = 0 + self.toggle_history.update_indicator() + + def delete_all(self): + """ + Delete All button clicked + """ + self.file_selection.file_list.clear() + self.file_selection.file_list.files_updated.emit() + + self.file_selection.file_list.setCurrentItem(None) + + @staticmethod + def _compute_total_size(filenames): + total_size = 0 + for filename in filenames: + if os.path.isfile(filename): + total_size += os.path.getsize(filename) + if os.path.isdir(filename): + total_size += Common.dir_size(filename) + return total_size + + +class ZipProgressBar(QtWidgets.QProgressBar): + update_processed_size_signal = QtCore.pyqtSignal(int) + + def __init__(self, common, total_files_size): + super(ZipProgressBar, self).__init__() + self.common = common + + self.setMaximumHeight(20) + self.setMinimumWidth(200) + self.setValue(0) + self.setFormat(strings._("zip_progress_bar_format")) + self.setStyleSheet(self.common.gui.css["share_zip_progess_bar"]) + + self._total_files_size = total_files_size + self._processed_size = 0 + + self.update_processed_size_signal.connect(self.update_processed_size) + + @property + def total_files_size(self): + return self._total_files_size + + @total_files_size.setter + def total_files_size(self, val): + self._total_files_size = val + + @property + def processed_size(self): + return self._processed_size + + @processed_size.setter + def processed_size(self, val): + self.update_processed_size(val) + + 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: + self.setValue(100) + else: + self.setValue(0) diff --git a/desktop/src/onionshare/tab/mode/share_mode/threads.py b/desktop/src/onionshare/tab/mode/share_mode/threads.py new file mode 100644 index 00000000..500b6525 --- /dev/null +++ b/desktop/src/onionshare/tab/mode/share_mode/threads.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +from 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: + self.mode.web.share_mode.set_file_info( + self.mode.filenames, processed_size_callback=self.set_processed_size + ) + self.success.emit() + self.mode.app.cleanup_filenames += ( + self.mode.web.share_mode.cleanup_filenames + ) + 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 + try: + self.mode.web.zip_writer.cancel_compression = True + except AttributeError: + # we never made it as far as creating a ZipWriter object + pass diff --git a/desktop/src/onionshare/tab/mode/website_mode/__init__.py b/desktop/src/onionshare/tab/mode/website_mode/__init__.py new file mode 100644 index 00000000..6d3d62a7 --- /dev/null +++ b/desktop/src/onionshare/tab/mode/website_mode/__init__.py @@ -0,0 +1,331 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +import os +import random +import string + +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare_cli.onion import * +from onionshare_cli.common import Common +from onionshare_cli.web import Web + +from .. import Mode +from ..file_selection import FileSelection +from ..history import History, ToggleHistory +from .... import strings +from ....widgets import Alert, MinimumWidthWidget +from ....gui_common import GuiCommon + + +class WebsiteMode(Mode): + """ + Parts of the main window UI for sharing files. + """ + + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def init(self): + """ + Custom initialization for ReceiveMode. + """ + # Create the Web object + self.web = Web(self.common, True, self.settings, "website") + + # Settings + self.disable_csp_checkbox = QtWidgets.QCheckBox() + self.disable_csp_checkbox.clicked.connect(self.disable_csp_checkbox_clicked) + self.disable_csp_checkbox.setText( + strings._("mode_settings_website_disable_csp_checkbox") + ) + if self.settings.get("website", "disable_csp"): + self.disable_csp_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.disable_csp_checkbox.setCheckState(QtCore.Qt.Unchecked) + + self.mode_settings_widget.mode_specific_layout.addWidget( + self.disable_csp_checkbox + ) + + # File selection + self.file_selection = FileSelection( + self.common, + "images/mode_website.png", + strings._("gui_new_tab_website_button"), + self, + ) + if self.filenames: + for filename in self.filenames: + self.file_selection.file_list.add_file(filename) + + # Server status + self.server_status.set_mode("website", self.file_selection) + self.server_status.server_started.connect(self.file_selection.server_started) + self.server_status.server_stopped.connect(self.file_selection.server_stopped) + self.server_status.server_stopped.connect(self.update_primary_action) + self.server_status.server_canceled.connect(self.file_selection.server_stopped) + self.server_status.server_canceled.connect(self.update_primary_action) + self.file_selection.file_list.files_updated.connect(self.server_status.update) + self.file_selection.file_list.files_updated.connect(self.update_primary_action) + # Tell server_status about web, then update + self.server_status.web = self.web + self.server_status.update() + + # Filesize warning + self.filesize_warning = QtWidgets.QLabel() + self.filesize_warning.setWordWrap(True) + self.filesize_warning.setStyleSheet( + self.common.gui.css["share_filesize_warning"] + ) + self.filesize_warning.hide() + + # Download history + self.history = History( + self.common, + QtGui.QPixmap.fromImage( + QtGui.QImage( + GuiCommon.get_resource_path("images/share_icon_transparent.png") + ) + ), + strings._("gui_website_mode_no_files"), + strings._("gui_all_modes_history"), + "website", + ) + self.history.in_progress_label.hide() + self.history.completed_label.hide() + self.history.hide() + + # Info label + self.info_label = QtWidgets.QLabel() + self.info_label.hide() + + # Delete all files button + self.remove_all_button = QtWidgets.QPushButton( + strings._("gui_file_selection_remove_all") + ) + self.remove_all_button.setFlat(True) + self.remove_all_button.setStyleSheet( + self.common.gui.css["share_delete_all_files_button"] + ) + self.remove_all_button.clicked.connect(self.delete_all) + self.remove_all_button.hide() + + # Toggle history + self.toggle_history = ToggleHistory( + self.common, + self, + self.history, + QtGui.QIcon(GuiCommon.get_resource_path("images/share_icon_toggle.png")), + QtGui.QIcon( + GuiCommon.get_resource_path("images/share_icon_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.remove_all_button) + top_bar_layout.addWidget(self.toggle_history) + + # Primary action layout + self.primary_action_layout.addWidget(self.filesize_warning) + self.primary_action.hide() + self.update_primary_action() + + # Main layout + self.main_layout = QtWidgets.QVBoxLayout() + 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.server_status) + self.main_layout.addWidget(MinimumWidthWidget(700)) + + # Column layout + self.column_layout = QtWidgets.QHBoxLayout() + self.column_layout.addLayout(self.main_layout) + self.column_layout.addWidget(self.history, stretch=1) + + # Wrapper layout + self.wrapper_layout = QtWidgets.QVBoxLayout() + self.wrapper_layout.addLayout(self.column_layout) + self.setLayout(self.wrapper_layout) + + # Always start with focus on file selection + self.file_selection.setFocus() + + def disable_csp_checkbox_clicked(self): + """ + Save disable CSP setting to the tab settings + """ + self.settings.set( + "website", "disable_csp", self.disable_csp_checkbox.isChecked() + ) + + def get_stop_server_autostop_timer_text(self): + """ + Return the string to put on the stop server button, if there's an auto-stop timer + """ + return strings._("gui_share_stop_server_autostop_timer") + + def autostop_timer_finished_should_stop_server(self): + """ + The auto-stop timer expired, should we stop the server? Returns a bool + """ + + self.server_status.stop_server() + self.server_status_label.setText(strings._("close_on_autostop_timer")) + return True + + def start_server_custom(self): + """ + Starting the server. + """ + # Reset web counters + self.web.website_mode.visit_count = 0 + self.web.reset_invalid_passwords() + + # Hide and reset the downloads if we have previously shared + self.reset_info_counters() + + self.remove_all_button.hide() + + def start_server_step2_custom(self): + """ + Step 2 in starting the server. Zipping up files. + """ + self.filenames = [] + for index in range(self.file_selection.file_list.count()): + self.filenames.append(self.file_selection.file_list.item(index).filename) + + # Continue + self.starting_server_step3.emit() + self.start_server_finished.emit() + + def start_server_step3_custom(self): + """ + Step 3 in starting the server. Display large filesize + warning, if applicable. + """ + self.web.website_mode.set_file_info(self.filenames) + self.success.emit() + + def start_server_error_custom(self): + """ + Start server error. + """ + if self._zip_progress_bar is not None: + self.status_bar.removeWidget(self._zip_progress_bar) + self._zip_progress_bar = None + + def stop_server_custom(self): + """ + Stop server. + """ + + self.filesize_warning.hide() + self.history.completed_count = 0 + self.file_selection.file_list.adjustSize() + + self.remove_all_button.show() + + def cancel_server_custom(self): + """ + Log that the server has been cancelled + """ + self.common.log("WebsiteMode", "cancel_server") + + def handle_tor_broke_custom(self): + """ + Connection to Tor broke. + """ + self.primary_action.hide() + + def on_reload_settings(self): + """ + If there were some files listed for sharing, we should be ok to re-enable + the 'Start Sharing' button now. + """ + if self.server_status.file_selection.get_num_files() > 0: + self.primary_action.show() + self.info_label.show() + self.remove_all_button.show() + + def update_primary_action(self): + self.common.log("WebsiteMode", "update_primary_action") + + # Show or hide primary action layout + file_count = self.file_selection.file_list.count() + if file_count > 0: + self.primary_action.show() + self.info_label.show() + self.remove_all_button.show() + + # Update the file count in the info label + total_size_bytes = 0 + for index in range(self.file_selection.file_list.count()): + item = self.file_selection.file_list.item(index) + total_size_bytes += item.size_bytes + total_size_readable = self.common.human_readable_filesize(total_size_bytes) + + if file_count > 1: + self.info_label.setText( + strings._("gui_file_info").format(file_count, total_size_readable) + ) + else: + self.info_label.setText( + strings._("gui_file_info_single").format( + file_count, total_size_readable + ) + ) + + else: + self.primary_action.hide() + self.info_label.hide() + self.remove_all_button.hide() + + def reset_info_counters(self): + """ + Set the info counters back to zero. + """ + self.history.reset() + self.toggle_history.indicator_count = 0 + self.toggle_history.update_indicator() + + def delete_all(self): + """ + Delete All button clicked + """ + self.file_selection.file_list.clear() + self.file_selection.file_list.files_updated.emit() + + self.file_selection.file_list.setCurrentItem(None) + + @staticmethod + def _compute_total_size(filenames): + total_size = 0 + for filename in filenames: + if os.path.isfile(filename): + total_size += os.path.getsize(filename) + if os.path.isdir(filename): + total_size += Common.dir_size(filename) + return total_size diff --git a/desktop/src/onionshare/tab/server_status.py b/desktop/src/onionshare/tab/server_status.py new file mode 100644 index 00000000..d14aa41c --- /dev/null +++ b/desktop/src/onionshare/tab/server_status.py @@ -0,0 +1,466 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +import platform +import textwrap +from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt5.QtCore import Qt + +from .. import strings +from ..widgets import Alert +from ..widgets import QRCodeDialog +from ..gui_common import GuiCommon + + +class ServerStatus(QtWidgets.QWidget): + """ + The server status chunk of the GUI. + """ + + server_started = QtCore.pyqtSignal() + server_started_finished = QtCore.pyqtSignal() + server_stopped = QtCore.pyqtSignal() + server_canceled = QtCore.pyqtSignal() + button_clicked = QtCore.pyqtSignal() + url_copied = QtCore.pyqtSignal() + hidservauth_copied = QtCore.pyqtSignal() + + STATUS_STOPPED = 0 + STATUS_WORKING = 1 + STATUS_STARTED = 2 + + def __init__( + self, + common, + qtapp, + app, + mode_settings, + mode_settings_widget, + file_selection=None, + local_only=False, + ): + super(ServerStatus, self).__init__() + + self.common = common + + self.status = self.STATUS_STOPPED + self.mode = None # Gets set in self.set_mode + + self.qtapp = qtapp + self.app = app + self.settings = mode_settings + self.mode_settings_widget = mode_settings_widget + + self.web = None + self.autostart_timer_datetime = None + self.local_only = local_only + + self.resizeEvent(None) + + # Server layout + self.server_button = QtWidgets.QPushButton() + self.server_button.clicked.connect(self.server_button_clicked) + + # URL layout + 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.setMinimumSize(self.url.sizeHint()) + self.url.setStyleSheet(self.common.gui.css["server_status_url"]) + self.url.setTextInteractionFlags( + Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard + ) + + self.copy_url_button = QtWidgets.QPushButton(strings._("gui_copy_url")) + self.copy_url_button.setFlat(True) + self.copy_url_button.setStyleSheet( + self.common.gui.css["server_status_url_buttons"] + ) + self.copy_url_button.clicked.connect(self.copy_url) + self.copy_hidservauth_button = QtWidgets.QPushButton( + strings._("gui_copy_hidservauth") + ) + self.show_url_qr_code_button = QtWidgets.QPushButton( + strings._("gui_show_url_qr_code") + ) + self.show_url_qr_code_button.hide() + self.show_url_qr_code_button.clicked.connect( + self.show_url_qr_code_button_clicked + ) + self.show_url_qr_code_button.setFlat(True) + self.show_url_qr_code_button.setStyleSheet( + self.common.gui.css["server_status_url_buttons"] + ) + + self.copy_hidservauth_button.setFlat(True) + self.copy_hidservauth_button.setStyleSheet( + self.common.gui.css["server_status_url_buttons"] + ) + self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth) + url_buttons_layout = QtWidgets.QHBoxLayout() + url_buttons_layout.addWidget(self.copy_url_button) + url_buttons_layout.addWidget(self.show_url_qr_code_button) + url_buttons_layout.addWidget(self.copy_hidservauth_button) + url_buttons_layout.addStretch() + + url_layout = QtWidgets.QVBoxLayout() + url_layout.addWidget(self.url_description) + url_layout.addWidget(self.url) + url_layout.addLayout(url_buttons_layout) + + # Add the widgets + button_layout = QtWidgets.QHBoxLayout() + button_layout.addWidget(self.server_button) + button_layout.addStretch() + + layout = QtWidgets.QVBoxLayout() + layout.addLayout(button_layout) + layout.addLayout(url_layout) + self.setLayout(layout) + + def set_mode(self, share_mode, file_selection=None): + """ + The server status is in share mode. + """ + self.mode = share_mode + + if (self.mode == self.common.gui.MODE_SHARE) or ( + self.mode == self.common.gui.MODE_WEBSITE + ): + self.file_selection = file_selection + + self.update() + + def resizeEvent(self, event): + """ + When the widget is resized, try and adjust the display of a v3 onion URL. + """ + try: + # 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(), 46) + self.url.setText(wrapped_onion_url) + else: + self.url.setText(self.get_url()) + except: + pass + + def show_url(self): + """ + Show the URL in the UI. + """ + self.url_description.show() + + info_image = GuiCommon.get_resource_path("images/info.png") + + if self.mode == self.common.gui.MODE_SHARE: + self.url_description.setText( + strings._("gui_share_url_description").format(info_image) + ) + elif self.mode == self.common.gui.MODE_WEBSITE: + self.url_description.setText( + strings._("gui_website_url_description").format(info_image) + ) + else: + self.url_description.setText( + strings._("gui_receive_url_description").format(info_image) + ) + + # Show a Tool Tip explaining the lifecycle of this URL + if self.settings.get("persistent", "enabled"): + if self.mode == self.common.gui.MODE_SHARE and self.settings.get( + "share", "autostop_sharing" + ): + self.url_description.setToolTip( + strings._("gui_url_label_onetime_and_persistent") + ) + else: + self.url_description.setToolTip(strings._("gui_url_label_persistent")) + else: + if self.mode == self.common.gui.MODE_SHARE and self.settings.get( + "share", "autostop_sharing" + ): + self.url_description.setToolTip(strings._("gui_url_label_onetime")) + else: + self.url_description.setToolTip(strings._("gui_url_label_stay_open")) + + self.url.setText(self.get_url()) + self.url.show() + self.copy_url_button.show() + + self.show_url_qr_code_button.show() + + if self.settings.get("general", "client_auth"): + self.copy_hidservauth_button.show() + else: + self.copy_hidservauth_button.hide() + + def update(self): + """ + Update the GUI elements based on the current state. + """ + self.common.log("ServerStatus", "update") + # Set the URL fields + if self.status == self.STATUS_STARTED: + # The backend Onion may have saved new settings, such as the private key. + # Reload the settings before saving new ones. + self.common.settings.load() + self.show_url() + + if not self.settings.get("onion", "password"): + self.settings.set("onion", "password", self.web.password) + self.settings.save() + + if self.settings.get("general", "autostop_timer"): + self.server_button.setToolTip( + strings._("gui_stop_server_autostop_timer_tooltip").format( + self.mode_settings_widget.autostop_timer_widget.dateTime().toString( + "h:mm AP, MMMM dd, yyyy" + ) + ) + ) + else: + self.url_description.hide() + self.url.hide() + self.copy_url_button.hide() + self.copy_hidservauth_button.hide() + self.show_url_qr_code_button.hide() + + self.mode_settings_widget.update_ui() + + # Button + if ( + self.mode == self.common.gui.MODE_SHARE + and self.file_selection.get_num_files() == 0 + ): + self.server_button.hide() + elif ( + self.mode == self.common.gui.MODE_WEBSITE + and self.file_selection.get_num_files() == 0 + ): + self.server_button.hide() + else: + self.server_button.show() + + if self.status == self.STATUS_STOPPED: + self.server_button.setStyleSheet( + self.common.gui.css["server_status_button_stopped"] + ) + self.server_button.setEnabled(True) + if self.mode == self.common.gui.MODE_SHARE: + self.server_button.setText(strings._("gui_share_start_server")) + elif self.mode == self.common.gui.MODE_WEBSITE: + self.server_button.setText(strings._("gui_share_start_server")) + elif self.mode == self.common.gui.MODE_CHAT: + self.server_button.setText(strings._("gui_chat_start_server")) + else: + self.server_button.setText(strings._("gui_receive_start_server")) + self.server_button.setToolTip("") + elif self.status == self.STATUS_STARTED: + self.server_button.setStyleSheet( + self.common.gui.css["server_status_button_started"] + ) + self.server_button.setEnabled(True) + if self.mode == self.common.gui.MODE_SHARE: + self.server_button.setText(strings._("gui_share_stop_server")) + elif self.mode == self.common.gui.MODE_WEBSITE: + self.server_button.setText(strings._("gui_share_stop_server")) + elif self.mode == self.common.gui.MODE_CHAT: + self.server_button.setText(strings._("gui_chat_stop_server")) + else: + self.server_button.setText(strings._("gui_receive_stop_server")) + elif self.status == self.STATUS_WORKING: + self.server_button.setStyleSheet( + self.common.gui.css["server_status_button_working"] + ) + self.server_button.setEnabled(True) + if self.settings.get("general", "autostart_timer"): + self.server_button.setToolTip( + strings._("gui_start_server_autostart_timer_tooltip").format( + self.mode_settings_widget.autostart_timer_widget.dateTime().toString( + "h:mm AP, MMMM dd, yyyy" + ) + ) + ) + else: + self.server_button.setText(strings._("gui_please_wait")) + else: + self.server_button.setStyleSheet( + self.common.gui.css["server_status_button_working"] + ) + self.server_button.setEnabled(False) + self.server_button.setText(strings._("gui_please_wait")) + + def server_button_clicked(self): + """ + Toggle starting or stopping the server. + """ + if self.status == self.STATUS_STOPPED: + can_start = True + if self.settings.get("general", "autostart_timer"): + if self.local_only: + self.autostart_timer_datetime = ( + self.mode_settings_widget.autostart_timer_widget.dateTime().toPyDateTime() + ) + else: + self.autostart_timer_datetime = ( + self.mode_settings_widget.autostart_timer_widget.dateTime() + .toPyDateTime() + .replace(second=0, microsecond=0) + ) + # If the timer has actually passed already before the user hit Start, refuse to start the server. + if ( + QtCore.QDateTime.currentDateTime().toPyDateTime() + > self.autostart_timer_datetime + ): + can_start = False + Alert( + self.common, + strings._("gui_server_autostart_timer_expired"), + QtWidgets.QMessageBox.Warning, + ) + if self.settings.get("general", "autostop_timer"): + if self.local_only: + self.autostop_timer_datetime = ( + self.mode_settings_widget.autostop_timer_widget.dateTime().toPyDateTime() + ) + else: + # Get the timer chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen + self.autostop_timer_datetime = ( + self.mode_settings_widget.autostop_timer_widget.dateTime() + .toPyDateTime() + .replace(second=0, microsecond=0) + ) + # If the timer has actually passed already before the user hit Start, refuse to start the server. + if ( + QtCore.QDateTime.currentDateTime().toPyDateTime() + > self.autostop_timer_datetime + ): + can_start = False + Alert( + self.common, + strings._("gui_server_autostop_timer_expired"), + QtWidgets.QMessageBox.Warning, + ) + if self.settings.get("general", "autostart_timer"): + if self.autostop_timer_datetime <= self.autostart_timer_datetime: + Alert( + self.common, + strings._( + "gui_autostop_timer_cant_be_earlier_than_autostart_timer" + ), + QtWidgets.QMessageBox.Warning, + ) + can_start = False + if can_start: + self.start_server() + elif self.status == self.STATUS_STARTED: + self.stop_server() + elif self.status == self.STATUS_WORKING: + self.cancel_server() + self.button_clicked.emit() + + def show_url_qr_code_button_clicked(self): + """ + Show a QR code of the onion URL. + """ + self.qr_code_dialog = QRCodeDialog(self.common, self.get_url()) + + def start_server(self): + """ + Start the server. + """ + self.status = self.STATUS_WORKING + self.update() + self.server_started.emit() + + def start_server_finished(self): + """ + The server has finished starting. + """ + self.status = self.STATUS_STARTED + # self.copy_url() + self.update() + self.server_started_finished.emit() + + def stop_server(self): + """ + Stop the server. + """ + self.status = self.STATUS_WORKING + self.mode_settings_widget.autostart_timer_reset() + self.mode_settings_widget.autostop_timer_reset() + self.update() + self.server_stopped.emit() + + def cancel_server(self): + """ + Cancel the server. + """ + self.common.log( + "ServerStatus", "cancel_server", "Canceling the server mid-startup" + ) + self.status = self.STATUS_WORKING + self.mode_settings_widget.autostart_timer_reset() + self.mode_settings_widget.autostop_timer_reset() + self.update() + self.server_canceled.emit() + + def stop_server_finished(self): + """ + The server has finished stopping. + """ + self.status = self.STATUS_STOPPED + self.update() + + def copy_url(self): + """ + Copy the onionshare URL to the clipboard. + """ + clipboard = self.qtapp.clipboard() + clipboard.setText(self.get_url()) + + self.url_copied.emit() + + def copy_hidservauth(self): + """ + Copy the HidServAuth line to the clipboard. + """ + clipboard = self.qtapp.clipboard() + clipboard.setText(self.app.auth_string) + + self.hidservauth_copied.emit() + + def get_url(self): + """ + Returns the OnionShare URL. + """ + if self.settings.get("general", "public"): + url = f"http://{self.app.onion_host}" + else: + url = f"http://onionshare:{self.web.password}@{self.app.onion_host}" + return url diff --git a/desktop/src/onionshare/tab/tab.py b/desktop/src/onionshare/tab/tab.py new file mode 100644 index 00000000..b3f9533d --- /dev/null +++ b/desktop/src/onionshare/tab/tab.py @@ -0,0 +1,662 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +import queue +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare_cli.onionshare import OnionShare +from onionshare_cli.web import Web +from onionshare_cli.mode_settings import ModeSettings + +from .mode.share_mode import ShareMode +from .mode.receive_mode import ReceiveMode +from .mode.website_mode import WebsiteMode +from .mode.chat_mode import ChatMode + +from .server_status import ServerStatus + +from .. import strings +from ..gui_common import GuiCommon +from ..widgets import Alert + + +class NewTabButton(QtWidgets.QPushButton): + def __init__(self, common, image_filename, title, text): + super(NewTabButton, self).__init__() + self.common = common + + self.setFixedSize(280, 280) + + # Image + self.image_label = QtWidgets.QLabel(parent=self) + self.image_label.setPixmap( + QtGui.QPixmap.fromImage( + QtGui.QImage(GuiCommon.get_resource_path(image_filename)) + ) + ) + self.image_label.setAlignment(QtCore.Qt.AlignCenter) + self.image_label.setStyleSheet(self.common.gui.css["new_tab_button_image"]) + self.image_label.setGeometry(0, 0, self.width(), 200) + self.image_label.show() + + # Title + self.title_label = QtWidgets.QLabel(title, parent=self) + self.title_label.setAlignment(QtCore.Qt.AlignCenter) + self.title_label.setStyleSheet(self.common.gui.css["new_tab_title_text"]) + self.title_label.setGeometry( + (self.width() - 250) / 2, self.height() - 100, 250, 30 + ) + self.title_label.show() + + # Text + self.text_label = QtWidgets.QLabel(text, parent=self) + self.text_label.setAlignment(QtCore.Qt.AlignCenter) + self.text_label.setStyleSheet(self.common.gui.css["new_tab_button_text"]) + self.text_label.setGeometry( + (self.width() - 200) / 2, self.height() - 50, 200, 30 + ) + self.text_label.show() + + +class Tab(QtWidgets.QWidget): + """ + A GUI tab, you know, sort of like in a web browser + """ + + change_title = QtCore.pyqtSignal(int, str) + change_icon = QtCore.pyqtSignal(int, str) + change_persistent = QtCore.pyqtSignal(int, bool) + + def __init__( + self, + common, + tab_id, + system_tray, + status_bar, + mode_settings=None, + filenames=None, + ): + super(Tab, self).__init__() + self.common = common + self.common.log("Tab", "__init__") + + self.tab_id = tab_id + self.system_tray = system_tray + self.status_bar = status_bar + self.filenames = filenames + + self.mode = None + + # Start the OnionShare app + self.app = OnionShare(common, self.common.gui.onion, self.common.gui.local_only) + + # Onionshare logo + self.image_label = QtWidgets.QLabel() + self.image_label.setPixmap( + QtGui.QPixmap.fromImage( + QtGui.QImage(GuiCommon.get_resource_path("images/logo_text.png")) + ) + ) + self.image_label.setFixedSize(160, 40) + image_layout = QtWidgets.QVBoxLayout() + image_layout.addWidget(self.image_label) + image_layout.addStretch() + self.image = QtWidgets.QWidget() + self.image.setLayout(image_layout) + + # New tab buttons + self.share_button = NewTabButton( + self.common, + "images/mode_new_tab_share.png", + strings._("gui_new_tab_share_button"), + strings._("gui_main_page_share_button"), + ) + self.share_button.clicked.connect(self.share_mode_clicked) + + self.receive_button = NewTabButton( + self.common, + "images/mode_new_tab_receive.png", + strings._("gui_new_tab_receive_button"), + strings._("gui_main_page_receive_button"), + ) + self.receive_button.clicked.connect(self.receive_mode_clicked) + + self.website_button = NewTabButton( + self.common, + "images/mode_new_tab_website.png", + strings._("gui_new_tab_website_button"), + strings._("gui_main_page_website_button"), + ) + self.website_button.clicked.connect(self.website_mode_clicked) + + self.chat_button = NewTabButton( + self.common, + "images/mode_new_tab_chat.png", + strings._("gui_new_tab_chat_button"), + strings._("gui_main_page_chat_button"), + ) + self.chat_button.clicked.connect(self.chat_mode_clicked) + + new_tab_top_layout = QtWidgets.QHBoxLayout() + new_tab_top_layout.addStretch() + new_tab_top_layout.addWidget(self.share_button) + new_tab_top_layout.addWidget(self.receive_button) + new_tab_top_layout.addStretch() + + new_tab_bottom_layout = QtWidgets.QHBoxLayout() + new_tab_bottom_layout.addStretch() + new_tab_bottom_layout.addWidget(self.website_button) + new_tab_bottom_layout.addWidget(self.chat_button) + new_tab_bottom_layout.addStretch() + + new_tab_layout = QtWidgets.QVBoxLayout() + new_tab_layout.addStretch() + new_tab_layout.addLayout(new_tab_top_layout) + new_tab_layout.addLayout(new_tab_bottom_layout) + new_tab_layout.addStretch() + + new_tab_img_layout = QtWidgets.QHBoxLayout() + new_tab_img_layout.addWidget(self.image) + new_tab_img_layout.addStretch(1) + new_tab_img_layout.addLayout(new_tab_layout) + new_tab_img_layout.addStretch(2) + + self.new_tab = QtWidgets.QWidget() + self.new_tab.setLayout(new_tab_img_layout) + self.new_tab.show() + + # Layout + self.layout = QtWidgets.QVBoxLayout() + self.layout.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.new_tab) + self.setLayout(self.layout) + + # Create the timer + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.timer_callback) + + # Persistent image + self.persistent_image_label = QtWidgets.QLabel() + self.persistent_image_label.setPixmap( + QtGui.QPixmap.fromImage( + QtGui.QImage( + GuiCommon.get_resource_path("images/persistent_enabled.png") + ) + ) + ) + self.persistent_image_label.setFixedSize(20, 20) + + # Create the close warning dialog -- the dialog widget needs to be in the constructor + # in order to test it + self.close_dialog = QtWidgets.QMessageBox() + self.close_dialog.setWindowTitle(strings._("gui_close_tab_warning_title")) + self.close_dialog.setIcon(QtWidgets.QMessageBox.Critical) + self.close_dialog.accept_button = self.close_dialog.addButton( + strings._("gui_close_tab_warning_close"), QtWidgets.QMessageBox.AcceptRole + ) + self.close_dialog.reject_button = self.close_dialog.addButton( + strings._("gui_close_tab_warning_cancel"), QtWidgets.QMessageBox.RejectRole + ) + self.close_dialog.setDefaultButton(self.close_dialog.reject_button) + + def init(self, mode_settings=None): + if mode_settings: + # Load this tab + self.settings = mode_settings + mode = self.settings.get("persistent", "mode") + if mode == "share": + self.filenames = self.settings.get("share", "filenames") + self.share_mode_clicked() + elif mode == "receive": + self.receive_mode_clicked() + elif mode == "website": + self.filenames = self.settings.get("website", "filenames") + self.website_mode_clicked() + else: + # This is a new tab + self.settings = ModeSettings(self.common) + + def share_mode_clicked(self): + self.common.log("Tab", "share_mode_clicked") + self.mode = self.common.gui.MODE_SHARE + self.new_tab.hide() + + self.share_mode = ShareMode(self) + self.share_mode.change_persistent.connect(self.change_persistent) + + self.layout.addWidget(self.share_mode) + self.share_mode.show() + + self.share_mode.init() + self.share_mode.server_status.server_started.connect( + self.update_server_status_indicator + ) + self.share_mode.server_status.server_stopped.connect( + self.update_server_status_indicator + ) + self.share_mode.start_server_finished.connect( + self.update_server_status_indicator + ) + self.share_mode.stop_server_finished.connect( + self.update_server_status_indicator + ) + self.share_mode.stop_server_finished.connect(self.stop_server_finished) + self.share_mode.start_server_finished.connect(self.clear_message) + self.share_mode.server_status.button_clicked.connect(self.clear_message) + self.share_mode.server_status.url_copied.connect(self.copy_url) + self.share_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth) + + self.change_title.emit(self.tab_id, strings._("gui_tab_name_share")) + + self.update_server_status_indicator() + self.timer.start(500) + + def receive_mode_clicked(self): + self.common.log("Tab", "receive_mode_clicked") + self.mode = self.common.gui.MODE_RECEIVE + self.new_tab.hide() + + self.receive_mode = ReceiveMode(self) + self.receive_mode.change_persistent.connect(self.change_persistent) + + self.layout.addWidget(self.receive_mode) + self.receive_mode.show() + + self.receive_mode.init() + self.receive_mode.server_status.server_started.connect( + self.update_server_status_indicator + ) + self.receive_mode.server_status.server_stopped.connect( + self.update_server_status_indicator + ) + self.receive_mode.start_server_finished.connect( + self.update_server_status_indicator + ) + self.receive_mode.stop_server_finished.connect( + self.update_server_status_indicator + ) + self.receive_mode.stop_server_finished.connect(self.stop_server_finished) + self.receive_mode.start_server_finished.connect(self.clear_message) + self.receive_mode.server_status.button_clicked.connect(self.clear_message) + self.receive_mode.server_status.url_copied.connect(self.copy_url) + self.receive_mode.server_status.hidservauth_copied.connect( + self.copy_hidservauth + ) + + self.change_title.emit(self.tab_id, strings._("gui_tab_name_receive")) + + self.update_server_status_indicator() + self.timer.start(500) + + def website_mode_clicked(self): + self.common.log("Tab", "website_mode_clicked") + self.mode = self.common.gui.MODE_WEBSITE + self.new_tab.hide() + + self.website_mode = WebsiteMode(self) + self.website_mode.change_persistent.connect(self.change_persistent) + + self.layout.addWidget(self.website_mode) + self.website_mode.show() + + self.website_mode.init() + self.website_mode.server_status.server_started.connect( + self.update_server_status_indicator + ) + self.website_mode.server_status.server_stopped.connect( + self.update_server_status_indicator + ) + self.website_mode.start_server_finished.connect( + self.update_server_status_indicator + ) + self.website_mode.stop_server_finished.connect( + self.update_server_status_indicator + ) + self.website_mode.stop_server_finished.connect(self.stop_server_finished) + self.website_mode.start_server_finished.connect(self.clear_message) + self.website_mode.server_status.button_clicked.connect(self.clear_message) + self.website_mode.server_status.url_copied.connect(self.copy_url) + self.website_mode.server_status.hidservauth_copied.connect( + self.copy_hidservauth + ) + + self.change_title.emit(self.tab_id, strings._("gui_tab_name_website")) + + self.update_server_status_indicator() + self.timer.start(500) + + def chat_mode_clicked(self): + self.common.log("Tab", "chat_mode_clicked") + self.mode = self.common.gui.MODE_CHAT + self.new_tab.hide() + + self.chat_mode = ChatMode(self) + self.chat_mode.change_persistent.connect(self.change_persistent) + + self.layout.addWidget(self.chat_mode) + self.chat_mode.show() + + self.chat_mode.init() + self.chat_mode.server_status.server_started.connect( + self.update_server_status_indicator + ) + self.chat_mode.server_status.server_stopped.connect( + self.update_server_status_indicator + ) + self.chat_mode.start_server_finished.connect( + self.update_server_status_indicator + ) + self.chat_mode.stop_server_finished.connect(self.update_server_status_indicator) + self.chat_mode.stop_server_finished.connect(self.stop_server_finished) + self.chat_mode.start_server_finished.connect(self.clear_message) + self.chat_mode.server_status.button_clicked.connect(self.clear_message) + self.chat_mode.server_status.url_copied.connect(self.copy_url) + self.chat_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth) + + self.change_title.emit(self.tab_id, strings._("gui_tab_name_chat")) + + self.update_server_status_indicator() + self.timer.start(500) + + def update_server_status_indicator(self): + # Set the status image + if self.mode == self.common.gui.MODE_SHARE: + # Share mode + if self.share_mode.server_status.status == ServerStatus.STATUS_STOPPED: + self.set_server_status_indicator_stopped( + strings._("gui_status_indicator_share_stopped") + ) + elif self.share_mode.server_status.status == ServerStatus.STATUS_WORKING: + if self.settings.get("general", "autostart_timer"): + self.set_server_status_indicator_working( + strings._("gui_status_indicator_share_scheduled") + ) + else: + self.set_server_status_indicator_working( + strings._("gui_status_indicator_share_working") + ) + elif self.share_mode.server_status.status == ServerStatus.STATUS_STARTED: + self.set_server_status_indicator_started( + strings._("gui_status_indicator_share_started") + ) + elif self.mode == self.common.gui.MODE_WEBSITE: + # Website mode + if self.website_mode.server_status.status == ServerStatus.STATUS_STOPPED: + self.set_server_status_indicator_stopped( + strings._("gui_status_indicator_share_stopped") + ) + elif self.website_mode.server_status.status == ServerStatus.STATUS_WORKING: + if self.website_mode.server_status.autostart_timer_datetime: + self.set_server_status_indicator_working( + strings._("gui_status_indicator_share_scheduled") + ) + else: + self.set_server_status_indicator_working( + strings._("gui_status_indicator_share_working") + ) + elif self.website_mode.server_status.status == ServerStatus.STATUS_STARTED: + self.set_server_status_indicator_started( + strings._("gui_status_indicator_share_started") + ) + elif self.mode == self.common.gui.MODE_RECEIVE: + # Receive mode + if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED: + self.set_server_status_indicator_stopped( + strings._("gui_status_indicator_receive_stopped") + ) + elif self.receive_mode.server_status.status == ServerStatus.STATUS_WORKING: + if self.settings.get("general", "autostart_timer"): + self.set_server_status_indicator_working( + strings._("gui_status_indicator_receive_scheduled") + ) + else: + self.set_server_status_indicator_working( + strings._("gui_status_indicator_receive_working") + ) + elif self.receive_mode.server_status.status == ServerStatus.STATUS_STARTED: + self.set_server_status_indicator_started( + strings._("gui_status_indicator_receive_started") + ) + elif self.mode == self.common.gui.MODE_CHAT: + # Chat mode + if self.chat_mode.server_status.status == ServerStatus.STATUS_STOPPED: + self.set_server_status_indicator_stopped( + strings._("gui_status_indicator_receive_stopped") + ) + elif self.chat_mode.server_status.status == ServerStatus.STATUS_WORKING: + if self.settings.get("general", "autostart_timer"): + self.set_server_status_indicator_working( + strings._("gui_status_indicator_receive_scheduled") + ) + else: + self.set_server_status_indicator_working( + strings._("gui_status_indicator_receive_working") + ) + elif self.chat_mode.server_status.status == ServerStatus.STATUS_STARTED: + self.set_server_status_indicator_started( + strings._("gui_status_indicator_receive_started") + ) + + def set_server_status_indicator_stopped(self, label_text): + self.change_icon.emit(self.tab_id, "images/server_stopped.png") + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_stopped) + ) + self.status_bar.server_status_label.setText(label_text) + + def set_server_status_indicator_working(self, label_text): + self.change_icon.emit(self.tab_id, "images/server_working.png") + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_working) + ) + self.status_bar.server_status_label.setText(label_text) + + def set_server_status_indicator_started(self, label_text): + self.change_icon.emit(self.tab_id, "images/server_started.png") + self.status_bar.server_status_image_label.setPixmap( + QtGui.QPixmap.fromImage(self.status_bar.server_status_image_started) + ) + self.status_bar.server_status_label.setText(label_text) + + def stop_server_finished(self): + # When the server stopped, cleanup the ephemeral onion service + self.get_mode().app.stop_onion_service(self.settings) + + def timer_callback(self): + """ + Check for messages communicated from the web app, and update the GUI accordingly. Also, + call ShareMode and ReceiveMode's timer_callbacks. + """ + self.update() + + if not self.common.gui.local_only: + # Have we lost connection to Tor somehow? + if not self.common.gui.onion.is_authenticated(): + self.timer.stop() + self.status_bar.showMessage(strings._("gui_tor_connection_lost")) + self.system_tray.showMessage( + strings._("gui_tor_connection_lost"), + strings._("gui_tor_connection_error_settings"), + ) + self.get_mode().handle_tor_broke() + + # Process events from the web object + mode = self.get_mode() + + events = [] + + done = False + while not done: + try: + r = mode.web.q.get(False) + events.append(r) + except queue.Empty: + done = True + + for event in events: + if event["type"] == Web.REQUEST_LOAD: + mode.handle_request_load(event) + + elif event["type"] == Web.REQUEST_STARTED: + mode.handle_request_started(event) + + elif event["type"] == Web.REQUEST_RATE_LIMIT: + mode.handle_request_rate_limit(event) + + elif event["type"] == Web.REQUEST_PROGRESS: + mode.handle_request_progress(event) + + elif event["type"] == Web.REQUEST_CANCELED: + mode.handle_request_canceled(event) + + elif event["type"] == Web.REQUEST_UPLOAD_FILE_RENAMED: + mode.handle_request_upload_file_renamed(event) + + elif event["type"] == Web.REQUEST_UPLOAD_SET_DIR: + mode.handle_request_upload_set_dir(event) + + elif event["type"] == Web.REQUEST_UPLOAD_FINISHED: + mode.handle_request_upload_finished(event) + + elif event["type"] == Web.REQUEST_UPLOAD_CANCELED: + mode.handle_request_upload_canceled(event) + + elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_STARTED: + mode.handle_request_individual_file_started(event) + + elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_PROGRESS: + mode.handle_request_individual_file_progress(event) + + elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_CANCELED: + mode.handle_request_individual_file_canceled(event) + + if event["type"] == Web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE: + Alert( + self.common, + strings._("error_cannot_create_data_dir").format( + event["data"]["receive_mode_dir"] + ), + ) + + if event["type"] == Web.REQUEST_OTHER: + if ( + event["path"] != "/favicon.ico" + and event["path"] != f"/{mode.web.shutdown_password}/shutdown" + ): + self.status_bar.showMessage( + f"{strings._('other_page_loaded')}: {event['path']}" + ) + + if event["type"] == Web.REQUEST_INVALID_PASSWORD: + self.status_bar.showMessage( + f"[#{mode.web.invalid_passwords_count}] {strings._('incorrect_password')}: {event['data']}" + ) + + mode.timer_callback() + + def copy_url(self): + """ + When the URL gets copied to the clipboard, display this in the status bar. + """ + self.common.log("Tab", "copy_url") + self.system_tray.showMessage( + strings._("gui_copied_url_title"), strings._("gui_copied_url") + ) + + def copy_hidservauth(self): + """ + When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar. + """ + self.common.log("Tab", "copy_hidservauth") + self.system_tray.showMessage( + strings._("gui_copied_hidservauth_title"), + strings._("gui_copied_hidservauth"), + ) + + def clear_message(self): + """ + Clear messages from the status bar. + """ + self.status_bar.clearMessage() + + def get_mode(self): + if self.mode: + if self.mode == self.common.gui.MODE_SHARE: + return self.share_mode + elif self.mode == self.common.gui.MODE_RECEIVE: + return self.receive_mode + elif self.mode == self.common.gui.MODE_CHAT: + return self.chat_mode + else: + return self.website_mode + else: + return None + + def settings_have_changed(self): + # Global settings have changed + self.common.log("Tab", "settings_have_changed") + + # We might've stopped the main requests timer if a Tor connection failed. If we've reloaded + # settings, we probably succeeded in obtaining a new connection. If so, restart the timer. + if not self.common.gui.local_only: + if self.common.gui.onion.is_authenticated(): + mode = self.get_mode() + if mode: + if not self.timer.isActive(): + self.timer.start(500) + mode.on_reload_settings() + + def close_tab(self): + self.common.log("Tab", "close_tab") + if self.mode is None: + return True + + if self.settings.get("persistent", "enabled"): + dialog_text = strings._("gui_close_tab_warning_persistent_description") + else: + server_status = self.get_mode().server_status + if server_status.status == server_status.STATUS_STOPPED: + return True + else: + if self.mode == self.common.gui.MODE_SHARE: + dialog_text = strings._("gui_close_tab_warning_share_description") + elif self.mode == self.common.gui.MODE_RECEIVE: + dialog_text = strings._("gui_close_tab_warning_receive_description") + else: + dialog_text = strings._("gui_close_tab_warning_website_description") + + # Open the warning dialog + self.common.log("Tab", "close_tab, opening warning dialog") + self.close_dialog.setText(dialog_text) + self.close_dialog.exec_() + + # Close + if self.close_dialog.clickedButton() == self.close_dialog.accept_button: + self.common.log("Tab", "close_tab", "close, closing tab") + self.get_mode().stop_server() + self.app.cleanup() + return True + # Cancel + else: + self.common.log("Tab", "close_tab", "cancel, keeping tab open") + return False + + def cleanup(self): + self.app.cleanup() |