From 95216693f32107a61e6a0b51f20e19c912435024 Mon Sep 17 00:00:00 2001 From: marigalicer Date: Tue, 19 Feb 2019 16:45:17 -0500 Subject: [PATCH 01/10] Resolve #878 UX: Enhance URL copying - Style Copy Address as high contrast button - Make .onion URL text selectable co-authored-by: aguestuser --- desktop/src/onionshare/gui_common.py | 7 +- onionshare_gui/server_status.py | 354 +++++++++++++++++++++++++++ 2 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 onionshare_gui/server_status.py diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py index 7a32f8ec..6db8151b 100644 --- a/desktop/src/onionshare/gui_common.py +++ b/desktop/src/onionshare/gui_common.py @@ -128,7 +128,12 @@ class GuiCommon: """, "server_status_url_buttons": """ QPushButton { - color: #3f7fcf; + color: #ffffff; + background-color: #4e064f; + padding: 10px; + border: 0; + border-radius: 5px; + text-decoration: none; } """, "server_status_button_stopped": """ diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py new file mode 100644 index 00000000..251aa024 --- /dev/null +++ b/onionshare_gui/server_status.py @@ -0,0 +1,354 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +import platform +import textwrap +from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt5.QtCore import Qt + +from onionshare import strings + +from .widgets import Alert + +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() + + MODE_SHARE = 'share' + MODE_RECEIVE = 'receive' + + STATUS_STOPPED = 0 + STATUS_WORKING = 1 + STATUS_STARTED = 2 + + def __init__(self, common, qtapp, app, 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.web = None + self.local_only = local_only + + self.resizeEvent(None) + + # Shutdown timeout layout + self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout')) + self.shutdown_timeout = QtWidgets.QDateTimeEdit() + self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy") + if self.local_only: + # For testing + self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15)) + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime()) + else: + # Set proposed timeout to be 5 minutes into the future + self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) + # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 60s from now + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) + self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection) + shutdown_timeout_layout = QtWidgets.QHBoxLayout() + shutdown_timeout_layout.addWidget(self.shutdown_timeout_label) + shutdown_timeout_layout.addWidget(self.shutdown_timeout) + + # Shutdown timeout container, so it can all be hidden and shown as a group + shutdown_timeout_container_layout = QtWidgets.QVBoxLayout() + shutdown_timeout_container_layout.addLayout(shutdown_timeout_layout) + self.shutdown_timeout_container = QtWidgets.QWidget() + self.shutdown_timeout_container.setLayout(shutdown_timeout_container_layout) + self.shutdown_timeout_container.hide() + + # 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.css['server_status_url']) + self.url.setTextInteractionFlags(Qt.TextSelectableByMouse) + + self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url')) + self.copy_url_button.setFlat(True) + self.copy_url_button.setStyleSheet(self.common.css['server_status_url_buttons']) + self.copy_url_button.setMinimumHeight(20) + self.copy_url_button.clicked.connect(self.copy_url) + self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth')) + self.copy_hidservauth_button.setFlat(True) + self.copy_hidservauth_button.setStyleSheet(self.common.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.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 + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.server_button) + layout.addLayout(url_layout) + layout.addWidget(self.shutdown_timeout_container) + 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 == ServerStatus.MODE_SHARE: + 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 shutdown_timeout_reset(self): + """ + Reset the timeout in the UI after stopping a share + """ + self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) + if not self.local_only: + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) + + def update(self): + """ + Update the GUI elements based on the current state. + """ + # Set the URL fields + if self.status == self.STATUS_STARTED: + self.url_description.show() + + info_image = self.common.get_resource_path('images/info.png') + + if self.mode == ServerStatus.MODE_SHARE: + self.url_description.setText(strings._('gui_share_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.common.settings.get('save_private_key'): + if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'): + 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 == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'): + 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() + + if self.common.settings.get('save_private_key'): + if not self.common.settings.get('slug'): + self.common.settings.set('slug', self.web.slug) + self.common.settings.save() + + if self.common.settings.get('shutdown_timeout'): + self.shutdown_timeout_container.hide() + + if self.app.stealth: + self.copy_hidservauth_button.show() + else: + self.copy_hidservauth_button.hide() + else: + self.url_description.hide() + self.url.hide() + self.copy_url_button.hide() + self.copy_hidservauth_button.hide() + + # Button + if self.mode == ServerStatus.MODE_SHARE 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.css['server_status_button_stopped']) + self.server_button.setEnabled(True) + if self.mode == ServerStatus.MODE_SHARE: + self.server_button.setText(strings._('gui_share_start_server')) + else: + self.server_button.setText(strings._('gui_receive_start_server')) + self.server_button.setToolTip('') + if self.common.settings.get('shutdown_timeout'): + self.shutdown_timeout_container.show() + elif self.status == self.STATUS_STARTED: + self.server_button.setStyleSheet(self.common.css['server_status_button_started']) + self.server_button.setEnabled(True) + if self.mode == ServerStatus.MODE_SHARE: + self.server_button.setText(strings._('gui_share_stop_server')) + else: + self.server_button.setText(strings._('gui_receive_stop_server')) + if self.common.settings.get('shutdown_timeout'): + self.shutdown_timeout_container.hide() + if self.mode == ServerStatus.MODE_SHARE: + self.server_button.setToolTip(strings._('gui_share_stop_server_shutdown_timeout_tooltip').format(self.timeout)) + else: + self.server_button.setToolTip(strings._('gui_receive_stop_server_shutdown_timeout_tooltip').format(self.timeout)) + + elif self.status == self.STATUS_WORKING: + self.server_button.setStyleSheet(self.common.css['server_status_button_working']) + self.server_button.setEnabled(True) + self.server_button.setText(strings._('gui_please_wait')) + if self.common.settings.get('shutdown_timeout'): + self.shutdown_timeout_container.hide() + else: + self.server_button.setStyleSheet(self.common.css['server_status_button_working']) + self.server_button.setEnabled(False) + self.server_button.setText(strings._('gui_please_wait')) + if self.common.settings.get('shutdown_timeout'): + self.shutdown_timeout_container.hide() + + def server_button_clicked(self): + """ + Toggle starting or stopping the server. + """ + if self.status == self.STATUS_STOPPED: + if self.common.settings.get('shutdown_timeout'): + if self.local_only: + self.timeout = self.shutdown_timeout.dateTime().toPyDateTime() + else: + # Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen + self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) + # If the timeout has actually passed already before the user hit Start, refuse to start the server. + if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout: + Alert(self.common, strings._('gui_server_timeout_expired'), QtWidgets.QMessageBox.Warning) + else: + self.start_server() + else: + 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 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.shutdown_timeout_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.shutdown_timeout_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.common.settings.get('public_mode'): + url = 'http://{0:s}'.format(self.app.onion_host) + else: + url = 'http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug) + return url From 2e7fd4e7939db11df7b7dbd64fd583538530cd31 Mon Sep 17 00:00:00 2001 From: Saptak S Date: Mon, 7 Sep 2020 20:42:07 +0530 Subject: [PATCH 02/10] Makes button style similar to main page buttons --- desktop/src/onionshare/gui_common.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py index 6db8151b..f488a740 100644 --- a/desktop/src/onionshare/gui_common.py +++ b/desktop/src/onionshare/gui_common.py @@ -128,12 +128,15 @@ class GuiCommon: """, "server_status_url_buttons": """ QPushButton { - color: #ffffff; - background-color: #4e064f; - padding: 10px; - border: 0; - border-radius: 5px; - text-decoration: none; + border: 1px solid #d3d3d3; + border-radius: 4px; + background-color: #ffffff; + padding: 8px 16px; + text-align: center; + color: #4e0d4e; + } + QPushButton:pressed { + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 255, 255, 255), stop:1 rgba(239, 239, 240, 255)) } """, "server_status_button_stopped": """ From 9a93a6e52efa53601769fdd57d8a170a439159cd Mon Sep 17 00:00:00 2001 From: Saptak S Date: Wed, 11 Nov 2020 15:11:25 +0530 Subject: [PATCH 03/10] Fixes rebasing errors --- onionshare_gui/server_status.py | 354 -------------------------------- 1 file changed, 354 deletions(-) delete mode 100644 onionshare_gui/server_status.py diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py deleted file mode 100644 index 251aa024..00000000 --- a/onionshare_gui/server_status.py +++ /dev/null @@ -1,354 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" -import platform -import textwrap -from PyQt5 import QtCore, QtWidgets, QtGui -from PyQt5.QtCore import Qt - -from onionshare import strings - -from .widgets import Alert - -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() - - MODE_SHARE = 'share' - MODE_RECEIVE = 'receive' - - STATUS_STOPPED = 0 - STATUS_WORKING = 1 - STATUS_STARTED = 2 - - def __init__(self, common, qtapp, app, 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.web = None - self.local_only = local_only - - self.resizeEvent(None) - - # Shutdown timeout layout - self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout')) - self.shutdown_timeout = QtWidgets.QDateTimeEdit() - self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy") - if self.local_only: - # For testing - self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15)) - self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime()) - else: - # Set proposed timeout to be 5 minutes into the future - self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) - # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 60s from now - self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) - self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection) - shutdown_timeout_layout = QtWidgets.QHBoxLayout() - shutdown_timeout_layout.addWidget(self.shutdown_timeout_label) - shutdown_timeout_layout.addWidget(self.shutdown_timeout) - - # Shutdown timeout container, so it can all be hidden and shown as a group - shutdown_timeout_container_layout = QtWidgets.QVBoxLayout() - shutdown_timeout_container_layout.addLayout(shutdown_timeout_layout) - self.shutdown_timeout_container = QtWidgets.QWidget() - self.shutdown_timeout_container.setLayout(shutdown_timeout_container_layout) - self.shutdown_timeout_container.hide() - - # 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.css['server_status_url']) - self.url.setTextInteractionFlags(Qt.TextSelectableByMouse) - - self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url')) - self.copy_url_button.setFlat(True) - self.copy_url_button.setStyleSheet(self.common.css['server_status_url_buttons']) - self.copy_url_button.setMinimumHeight(20) - self.copy_url_button.clicked.connect(self.copy_url) - self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth')) - self.copy_hidservauth_button.setFlat(True) - self.copy_hidservauth_button.setStyleSheet(self.common.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.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 - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.server_button) - layout.addLayout(url_layout) - layout.addWidget(self.shutdown_timeout_container) - 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 == ServerStatus.MODE_SHARE: - 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 shutdown_timeout_reset(self): - """ - Reset the timeout in the UI after stopping a share - """ - self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) - if not self.local_only: - self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) - - def update(self): - """ - Update the GUI elements based on the current state. - """ - # Set the URL fields - if self.status == self.STATUS_STARTED: - self.url_description.show() - - info_image = self.common.get_resource_path('images/info.png') - - if self.mode == ServerStatus.MODE_SHARE: - self.url_description.setText(strings._('gui_share_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.common.settings.get('save_private_key'): - if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'): - 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 == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'): - 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() - - if self.common.settings.get('save_private_key'): - if not self.common.settings.get('slug'): - self.common.settings.set('slug', self.web.slug) - self.common.settings.save() - - if self.common.settings.get('shutdown_timeout'): - self.shutdown_timeout_container.hide() - - if self.app.stealth: - self.copy_hidservauth_button.show() - else: - self.copy_hidservauth_button.hide() - else: - self.url_description.hide() - self.url.hide() - self.copy_url_button.hide() - self.copy_hidservauth_button.hide() - - # Button - if self.mode == ServerStatus.MODE_SHARE 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.css['server_status_button_stopped']) - self.server_button.setEnabled(True) - if self.mode == ServerStatus.MODE_SHARE: - self.server_button.setText(strings._('gui_share_start_server')) - else: - self.server_button.setText(strings._('gui_receive_start_server')) - self.server_button.setToolTip('') - if self.common.settings.get('shutdown_timeout'): - self.shutdown_timeout_container.show() - elif self.status == self.STATUS_STARTED: - self.server_button.setStyleSheet(self.common.css['server_status_button_started']) - self.server_button.setEnabled(True) - if self.mode == ServerStatus.MODE_SHARE: - self.server_button.setText(strings._('gui_share_stop_server')) - else: - self.server_button.setText(strings._('gui_receive_stop_server')) - if self.common.settings.get('shutdown_timeout'): - self.shutdown_timeout_container.hide() - if self.mode == ServerStatus.MODE_SHARE: - self.server_button.setToolTip(strings._('gui_share_stop_server_shutdown_timeout_tooltip').format(self.timeout)) - else: - self.server_button.setToolTip(strings._('gui_receive_stop_server_shutdown_timeout_tooltip').format(self.timeout)) - - elif self.status == self.STATUS_WORKING: - self.server_button.setStyleSheet(self.common.css['server_status_button_working']) - self.server_button.setEnabled(True) - self.server_button.setText(strings._('gui_please_wait')) - if self.common.settings.get('shutdown_timeout'): - self.shutdown_timeout_container.hide() - else: - self.server_button.setStyleSheet(self.common.css['server_status_button_working']) - self.server_button.setEnabled(False) - self.server_button.setText(strings._('gui_please_wait')) - if self.common.settings.get('shutdown_timeout'): - self.shutdown_timeout_container.hide() - - def server_button_clicked(self): - """ - Toggle starting or stopping the server. - """ - if self.status == self.STATUS_STOPPED: - if self.common.settings.get('shutdown_timeout'): - if self.local_only: - self.timeout = self.shutdown_timeout.dateTime().toPyDateTime() - else: - # Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen - self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) - # If the timeout has actually passed already before the user hit Start, refuse to start the server. - if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout: - Alert(self.common, strings._('gui_server_timeout_expired'), QtWidgets.QMessageBox.Warning) - else: - self.start_server() - else: - 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 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.shutdown_timeout_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.shutdown_timeout_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.common.settings.get('public_mode'): - url = 'http://{0:s}'.format(self.app.onion_host) - else: - url = 'http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug) - return url From be63f6098c0a78ed9052af2dd19f4c23c0692d7e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 17 Nov 2020 17:45:04 -0800 Subject: [PATCH 04/10] Move psutil dependency from desktop to CLI --- cli/pyproject.toml | 1 + desktop/pyproject.toml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/pyproject.toml b/cli/pyproject.toml index 08206844..84c9656a 100644 --- a/cli/pyproject.toml +++ b/cli/pyproject.toml @@ -21,6 +21,7 @@ click = "*" flask = "*" flask-httpauth = "*" flask-socketio = "*" +psutil = "*" pycryptodome = "*" pysocks = "*" requests = "*" diff --git a/desktop/pyproject.toml b/desktop/pyproject.toml index d1be9c45..028f1ec1 100644 --- a/desktop/pyproject.toml +++ b/desktop/pyproject.toml @@ -14,7 +14,6 @@ icon = "src/onionshare/resources/onionshare" sources = ['src/onionshare'] requires = [ "./onionshare_cli-2.3.dev2-py3-none-any.whl", - "psutil", "pyside2==5.15.1", "qrcode" ] From c94f6eea262d2ad7bc6a0273e896dd260d073cb5 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 17 Nov 2020 17:45:31 -0800 Subject: [PATCH 05/10] Remove Onion.bundle_tor_supported because it is now always supported --- cli/onionshare_cli/onion.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index 6a42638a..0f712d5d 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -22,7 +22,13 @@ from stem.control import Controller from stem import ProtocolError, SocketClosed from stem.connection import MissingPassword, UnreadableCookieFile, AuthenticationFailure from Crypto.PublicKey import RSA -import base64, os, sys, tempfile, shutil, urllib, platform, subprocess, time, shlex +import base64 +import os +import tempfile +import subprocess +import time +import shlex +import psutil from distutils.version import LooseVersion as Version from . import common @@ -158,14 +164,6 @@ class Onion(object): self.use_tmp_dir = use_tmp_dir - # Is bundled tor supported? - if ( - self.common.platform == "Windows" or self.common.platform == "Darwin" - ) and getattr(sys, "onionshare_dev_mode", False): - self.bundle_tor_supported = False - else: - self.bundle_tor_supported = True - # Set the path of the tor binary, for bundled tor if not get_tor_paths: get_tor_paths = self.common.get_tor_paths @@ -218,12 +216,6 @@ class Onion(object): self.c = None if self.settings.get("connection_type") == "bundled": - if not self.bundle_tor_supported: - raise BundledTorNotSupported( - # strings._("settings_error_bundled_tor_not_supported") - "Using the Tor version that comes with OnionShare does not work in developer mode on Windows or macOS." - ) - # Create a torrc for this session if self.use_tmp_dir: self.tor_data_directory = tempfile.TemporaryDirectory( From 46647feffdf5a859e7b3cebbb3d78c3966c59ec0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 17 Nov 2020 18:06:36 -0800 Subject: [PATCH 06/10] Kill stale tor process --- cli/onionshare_cli/onion.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index 0f712d5d..95734b1a 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -28,6 +28,7 @@ import tempfile import subprocess import time import shlex +import getpass import psutil from distutils.version import LooseVersion as Version @@ -115,13 +116,6 @@ class TorTooOld(Exception): pass -class BundledTorNotSupported(Exception): - """ - This exception is raised if onionshare is set to use the bundled Tor binary, - but it's not supported on that platform, or in dev mode. - """ - - class BundledTorTimeout(Exception): """ This exception is raised if onionshare is set to use the bundled Tor binary, @@ -242,6 +236,23 @@ class Onion(object): raise OSError("OnionShare port not available") self.tor_torrc = os.path.join(self.tor_data_directory_name, "torrc") + # If there is an existing OnionShare tor process, kill it + for proc in psutil.process_iter(["pid", "name", "username"]): + if proc.username() == getpass.getuser(): + cmdline = proc.cmdline() + if ( + cmdline[0] == self.tor_path + and cmdline[1] == "-f" + and cmdline[2] == self.torrc + ): + self.common.log( + "Onion", + "connect", + "found a stale tor process, killing it", + ) + proc.terminate() + proc.wait() + if self.common.platform == "Windows" or self.common.platform == "Darwin": # Windows doesn't support unix sockets, so it must use a network port. # macOS can't use unix sockets either because socket filenames are limited to From 8a809b85545d2c57983fcc4b8b76cc8a6a8ed718 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 17 Nov 2020 18:08:59 -0800 Subject: [PATCH 07/10] Fix torrc filename --- cli/onionshare_cli/onion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index 95734b1a..138a6673 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -243,7 +243,7 @@ class Onion(object): if ( cmdline[0] == self.tor_path and cmdline[1] == "-f" - and cmdline[2] == self.torrc + and cmdline[2] == self.tor_torrc ): self.common.log( "Onion", From e2709c1d192732671f42fa2fcc11b48cdb513842 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 17 Nov 2020 18:46:04 -0800 Subject: [PATCH 08/10] Update CLI deps --- cli/poetry.lock | 46 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/cli/poetry.lock b/cli/poetry.lock index 00331cc4..cdac8565 100644 --- a/cli/poetry.lock +++ b/cli/poetry.lock @@ -98,7 +98,7 @@ dotenv = ["python-dotenv"] [[package]] name = "flask-httpauth" -version = "4.1.0" +version = "4.2.0" description = "Basic and Digest HTTP authentication for Flask routes" category = "main" optional = false @@ -214,6 +214,17 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] +[[package]] +name = "psutil" +version = "5.7.3" +description = "Cross-platform lib for process and system monitoring in Python." +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"] + [[package]] name = "py" version = "1.9.0" @@ -302,7 +313,7 @@ client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] [[package]] name = "requests" -version = "2.24.0" +version = "2.25.0" description = "Python HTTP for Humans." category = "main" optional = false @@ -312,7 +323,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" certifi = ">=2017.4.17" chardet = ">=3.0.2,<4" idna = ">=2.5,<3" -urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" +urllib3 = ">=1.21.1,<1.27" [package.extras] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] @@ -344,7 +355,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "urllib3" -version = "1.25.11" +version = "1.26.2" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -382,7 +393,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "3947b230139f4b699f40c97e0b90d8c8ab6d3d7ef9093d16d2acb507131e14da" +content-hash = "38f69a7cfa72b1da17d995e8c33dcceb0568ebfb065439927a5a007f3c8bd873" [metadata.files] atomicwrites = [ @@ -422,8 +433,8 @@ flask = [ {file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"}, ] flask-httpauth = [ - {file = "Flask-HTTPAuth-4.1.0.tar.gz", hash = "sha256:9e028e4375039a49031eb9ecc40be4761f0540476040f6eff329a31dabd4d000"}, - {file = "Flask_HTTPAuth-4.1.0-py2.py3-none-any.whl", hash = "sha256:29e0288869a213c7387f0323b6bf2c7191584fb1da8aa024d9af118e5cd70de7"}, + {file = "Flask-HTTPAuth-4.2.0.tar.gz", hash = "sha256:8c7e49e53ce7dc14e66fe39b9334e4b7ceb8d0b99a6ba1c3562bb528ef9da84a"}, + {file = "Flask_HTTPAuth-4.2.0-py2.py3-none-any.whl", hash = "sha256:3fcedb99a03985915335a38c35bfee6765cbd66d7f46440fa3b42ae94a90fac7"}, ] flask-socketio = [ {file = "Flask-SocketIO-4.3.1.tar.gz", hash = "sha256:36c1d5765010d1f4e4f05b4cc9c20c289d9dc70698c88d1addd0afcfedc5b062"}, @@ -512,6 +523,19 @@ pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] +psutil = [ + {file = "psutil-5.7.3-cp27-none-win32.whl", hash = "sha256:1cd6a0c9fb35ece2ccf2d1dd733c1e165b342604c67454fd56a4c12e0a106787"}, + {file = "psutil-5.7.3-cp27-none-win_amd64.whl", hash = "sha256:e02c31b2990dcd2431f4524b93491941df39f99619b0d312dfe1d4d530b08b4b"}, + {file = "psutil-5.7.3-cp35-cp35m-win32.whl", hash = "sha256:56c85120fa173a5d2ad1d15a0c6e0ae62b388bfb956bb036ac231fbdaf9e4c22"}, + {file = "psutil-5.7.3-cp35-cp35m-win_amd64.whl", hash = "sha256:fa38ac15dbf161ab1e941ff4ce39abd64b53fec5ddf60c23290daed2bc7d1157"}, + {file = "psutil-5.7.3-cp36-cp36m-win32.whl", hash = "sha256:01bc82813fbc3ea304914581954979e637bcc7084e59ac904d870d6eb8bb2bc7"}, + {file = "psutil-5.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:6a3e1fd2800ca45083d976b5478a2402dd62afdfb719b30ca46cd28bb25a2eb4"}, + {file = "psutil-5.7.3-cp37-cp37m-win32.whl", hash = "sha256:fbcac492cb082fa38d88587d75feb90785d05d7e12d4565cbf1ecc727aff71b7"}, + {file = "psutil-5.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:5d9106ff5ec2712e2f659ebbd112967f44e7d33f40ba40530c485cc5904360b8"}, + {file = "psutil-5.7.3-cp38-cp38-win32.whl", hash = "sha256:ade6af32eb80a536eff162d799e31b7ef92ddcda707c27bbd077238065018df4"}, + {file = "psutil-5.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:2cb55ef9591b03ef0104bedf67cc4edb38a3edf015cf8cf24007b99cb8497542"}, + {file = "psutil-5.7.3.tar.gz", hash = "sha256:af73f7bcebdc538eda9cc81d19db1db7bf26f103f91081d780bbacfcb620dee2"}, +] py = [ {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, @@ -575,8 +599,8 @@ python-socketio = [ {file = "python_socketio-4.6.0-py2.py3-none-any.whl", hash = "sha256:d437f797c44b6efba2f201867cf02b8c96b97dff26d4e4281ac08b45817cd522"}, ] requests = [ - {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, - {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, + {file = "requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"}, + {file = "requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8"}, ] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, @@ -590,8 +614,8 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] urllib3 = [ - {file = "urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"}, - {file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"}, + {file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"}, + {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, ] werkzeug = [ {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, From 717c8ec2f37682a4c8cbbd651f706dee7b3c1e1d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 17 Nov 2020 18:46:22 -0800 Subject: [PATCH 09/10] Make onionshare tor process detection work in Windows --- cli/onionshare_cli/onion.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index 138a6673..1b025bf8 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -238,7 +238,7 @@ class Onion(object): # If there is an existing OnionShare tor process, kill it for proc in psutil.process_iter(["pid", "name", "username"]): - if proc.username() == getpass.getuser(): + try: cmdline = proc.cmdline() if ( cmdline[0] == self.tor_path @@ -252,6 +252,9 @@ class Onion(object): ) proc.terminate() proc.wait() + break + except: + pass if self.common.platform == "Windows" or self.common.platform == "Darwin": # Windows doesn't support unix sockets, so it must use a network port. From e950cc5fe8bb066506295a48ef874bc28ed1d05f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 23 Nov 2020 14:52:52 -0800 Subject: [PATCH 10/10] Allow directory listing work with or without trailing slash (removing trailing slash by default), and make directory listing links absolute instead of relative --- .../resources/templates/listing.html | 100 +++++++++--------- cli/onionshare_cli/web/send_base_mode.py | 27 +++-- cli/onionshare_cli/web/website_mode.py | 8 +- 3 files changed, 75 insertions(+), 60 deletions(-) diff --git a/cli/onionshare_cli/resources/templates/listing.html b/cli/onionshare_cli/resources/templates/listing.html index ea050710..e1dd6d9f 100644 --- a/cli/onionshare_cli/resources/templates/listing.html +++ b/cli/onionshare_cli/resources/templates/listing.html @@ -1,55 +1,59 @@ - - OnionShare - - - - - - -
- -

OnionShare

-
+ + OnionShare + + + + + - {% if breadcrumbs %} - - {% endif %} + - - - - - - +
+ +

OnionShare

+
- {% for info in dirs %} - - - - - {% endfor %} + {% if breadcrumbs %} + + {% endif %} - {% for info in files %} - - - - - {% endfor %} -
FilenameSize
- - - {{ info.basename }} - -
- - - {{ info.basename }} - - {{ info.size_human }}
- - + + + + + + + + {% for info in dirs %} + + + + + {% endfor %} + + {% for info in files %} + + + + + {% endfor %} +
FilenameSize
+ + + {{ info.basename }} + +
+ + + {{ info.basename }} + + {{ info.size_human }}
+ + + \ No newline at end of file diff --git a/cli/onionshare_cli/web/send_base_mode.py b/cli/onionshare_cli/web/send_base_mode.py index f9db28c6..9913996e 100644 --- a/cli/onionshare_cli/web/send_base_mode.py +++ b/cli/onionshare_cli/web/send_base_mode.py @@ -85,7 +85,7 @@ class SendBaseModeWeb: # If it's a directory, add it recursively elif os.path.isdir(filename): - self.root_files[basename + "/"] = filename + self.root_files[basename] = filename for root, _, nested_filenames in os.walk(filename): # Normalize the root path. So if the directory name is "/home/user/Documents/some_folder", @@ -96,7 +96,7 @@ class SendBaseModeWeb: ).rstrip("/") # Add the dir itself - self.files[normalized_root + "/"] = root + self.files[normalized_root] = root # Add the files in this dir for nested_filename in nested_filenames: @@ -117,19 +117,21 @@ class SendBaseModeWeb: ) breadcrumbs = [("☗", "/")] - parts = path.split("/")[:-1] + parts = path.split("/") + if parts[-1] == "": + parts = parts[:-1] for i in range(len(parts)): - breadcrumbs.append((parts[i], f"/{'/'.join(parts[0 : i + 1])}/")) + breadcrumbs.append((parts[i], f"/{'/'.join(parts[0 : i + 1])}")) breadcrumbs_leaf = breadcrumbs.pop()[0] # If filesystem_path is None, this is the root directory listing - files, dirs = self.build_directory_listing(filenames, filesystem_path) + files, dirs = self.build_directory_listing(path, filenames, filesystem_path) r = self.directory_listing_template( path, files, dirs, breadcrumbs, breadcrumbs_leaf ) return self.web.add_security_headers(r) - def build_directory_listing(self, filenames, filesystem_path): + def build_directory_listing(self, path, filenames, filesystem_path): files = [] dirs = [] @@ -142,11 +144,20 @@ class SendBaseModeWeb: is_dir = os.path.isdir(this_filesystem_path) if is_dir: - dirs.append({"basename": filename}) + dirs.append( + {"link": os.path.join(f"/{path}", filename), "basename": filename} + ) else: size = os.path.getsize(this_filesystem_path) size_human = self.common.human_readable_filesize(size) - files.append({"basename": filename, "size_human": size_human}) + files.append( + { + "link": os.path.join(f"/{path}", filename), + "basename": filename, + "size_human": size_human, + } + ) + return files, dirs def stream_individual_file(self, filesystem_path): diff --git a/cli/onionshare_cli/web/website_mode.py b/cli/onionshare_cli/web/website_mode.py index ef37ab12..352f4f3c 100644 --- a/cli/onionshare_cli/web/website_mode.py +++ b/cli/onionshare_cli/web/website_mode.py @@ -71,6 +71,9 @@ class WebsiteModeWeb(SendBaseModeWeb): self.web.cancel_compression = True def render_logic(self, path=""): + # Strip trailing slash + path = path.rstrip("/") + if path in self.files: filesystem_path = self.files[path] @@ -86,10 +89,7 @@ class WebsiteModeWeb(SendBaseModeWeb): # Otherwise, render directory listing filenames = [] for filename in os.listdir(filesystem_path): - if os.path.isdir(os.path.join(filesystem_path, filename)): - filenames.append(filename + "/") - else: - filenames.append(filename) + filenames.append(filename) filenames.sort() return self.directory_listing(filenames, path, filesystem_path)