diff --git a/.gitignore b/.gitignore
index 712484d6..b202993b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,7 @@ pip-log.txt
.tox
nosetests.xml
.cache
+.pytest_cache
# Translations
*.mo
diff --git a/.travis.yml b/.travis.yml
index 6d324010..9010e77a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,7 +10,12 @@ python:
- "nightly"
# command to install dependencies
install:
- - pip install Flask==0.12 stem==1.5.4 pytest-cov coveralls
+ - pip install Flask==0.12 stem==1.5.4 pytest-cov coveralls flake8
+before_script:
+ # stop the build if there are Python syntax errors or undefined names
+ - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
+ # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+ - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
# command to run tests
script: pytest --cov=onionshare test/
after_success:
diff --git a/BUILD.md b/BUILD.md
index 55355aad..e6e54951 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -38,6 +38,8 @@ Install Xcode from the Mac App Store. Once it's installed, run it for the first
Download and install Python 3.6.4 from https://www.python.org/downloads/release/python-364/. I downloaded `python-3.6.4-macosx10.6.pkg`.
+You may also need to run the command `/Applications/Python\ 3.6/Install\ Certificates.command` to update Python 3.6's internal certificate store. Otherwise, you may find that fetching the Tor Browser .dmg file fails later due to a certificate validation error.
+
Download and install Qt5 from https://www.qt.io/download-open-source/. I downloaded `qt-unified-mac-x64-3.0.2-online.dmg`. There's no need to login to a Qt account during installation. Make sure you install the latest Qt 5.x. I installed Qt 5.10.0 -- all you need is to check `Qt > Qt 5.10.0 > macOS`.
Now install some python dependencies with pip (note, there's issues building a .app if you install this in a virtualenv):
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ecd0549d..6169840b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,20 @@
# OnionShare Changelog
+## 1.3
+
+* Major UI redesign, introducing many UX improvements
+* Client-side web interfact redesigned
+* New feature: Support for meek_lite pluggable transports (Amazon and Azure)
+* New feature: Support for custom obfs4 and meek-lite bridges
+* New feature: ability to cancel share before it starts
+* Bug fix: the UpdateChecker no longer blocks the UI when checking
+* Bug fix: simultaneous downloads (broken in 1.2)
+* Update Tor to 0.2.3.9
+* Improved support for BSD
+* Updated French and Danish translations
+* Minor build script and build documentation fixes
+* Add flake8 tests
+
## 1.2
* New feature: Support for Tor bridges, including obfs4proxy
diff --git a/README.md b/README.md
index c83636b0..55487045 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ You can download OnionShare for Windows and macOS from the [OnionShare website](
## Developing OnionShare
-You can set up your development environment to build OnionShare yourself by following [these instructions](/BUILD.md).
+You can set up your development environment to build OnionShare yourself by following [these instructions](/BUILD.md). You may also subscribe to our developers mailing list [here](https://lists.riseup.net/www/info/onionshare-dev).
# Screenshots
diff --git a/install/get-tor-osx.py b/install/get-tor-osx.py
index f6b04aa4..f6cac62f 100644
--- a/install/get-tor-osx.py
+++ b/install/get-tor-osx.py
@@ -28,9 +28,9 @@ import inspect, os, sys, hashlib, zipfile, io, shutil, subprocess
import urllib.request
def main():
- dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.0.11/TorBrowser-7.0.11-osx64_en-US.dmg'
- dmg_filename = 'TorBrowser-7.0.11-osx64_en-US.dmg'
- expected_dmg_sha256 = '5143e4a2141a69f66869be13eef4bcaac4e6c27c78383fc8a4c38b334759f3a2'
+ dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.5/TorBrowser-7.5-osx64_en-US.dmg'
+ dmg_filename = 'TorBrowser-7.5-osx64_en-US.dmg'
+ expected_dmg_sha256 = '43a8dc0afd0a77e42766311eb54ad9fc8714f67fcd2d3582a3bcb98b22c2e629'
# Build paths
root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))
diff --git a/install/get-tor-windows.py b/install/get-tor-windows.py
index b1a6665d..f5aeb3f7 100644
--- a/install/get-tor-windows.py
+++ b/install/get-tor-windows.py
@@ -28,9 +28,9 @@ import inspect, os, sys, hashlib, shutil, subprocess
import urllib.request
def main():
- exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.0.11/torbrowser-install-7.0.11_en-US.exe'
- exe_filename = 'torbrowser-install-7.0.11_en-US.exe'
- expected_exe_sha256 = 'a033eb9b9ed2ad389169b36a90946a8af8f05bd0c7bbd3e37678041331096624'
+ exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.5/torbrowser-install-7.5_en-US.exe'
+ exe_filename = 'torbrowser-install-7.5_en-US.exe'
+ expected_exe_sha256 = '81ccb9456118cf8fa755a3eafb5c514665fc69599cdd41e9eb36baa335ebe233'
# Build paths
root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))
working_path = os.path.join(os.path.join(root_path, 'build'), 'tor')
diff --git a/install/onionshare.desktop b/install/onionshare.desktop
index 256a7b50..fbac3660 100644
--- a/install/onionshare.desktop
+++ b/install/onionshare.desktop
@@ -1,9 +1,11 @@
[Desktop Entry]
Name=OnionShare
Comment=Share a file securely and anonymously over Tor
+Comment[da]=Del en fil sikkert og anonymt over Tor
Exec=/usr/bin/onionshare-gui
Terminal=false
Type=Application
Icon=/usr/share/pixmaps/onionshare80.xpm
Categories=Network;
Keywords=tor;anonymity;privacy;onion service;file sharing;file hosting;
+Keywords[da]=tor;anonymitet;privatliv;onion-tjeneste;fildeling;filhosting;
diff --git a/install/onionshare.nsi b/install/onionshare.nsi
index 9c15de47..4030636a 100644
--- a/install/onionshare.nsi
+++ b/install/onionshare.nsi
@@ -3,10 +3,10 @@
!define ABOUTURL "https:\\onionshare.org\"
# change these with each release
-!define INSTALLSIZE 66525
+!define INSTALLSIZE 66537
!define VERSIONMAJOR 1
-!define VERSIONMINOR 2
-!define VERSIONSTRING "1.2"
+!define VERSIONMINOR 3
+!define VERSIONSTRING "1.3"
RequestExecutionLevel admin
@@ -192,6 +192,8 @@ Section "install"
File "${BINPATH}\share\torrc_template"
File "${BINPATH}\share\torrc_template-windows"
File "${BINPATH}\share\torrc_template-obfs4"
+ File "${BINPATH}\share\torrc_template-meek_lite_amazon"
+ File "${BINPATH}\share\torrc_template-meek_lite_azure"
File "${BINPATH}\share\version.txt"
File "${BINPATH}\share\wordlist.txt"
@@ -201,14 +203,22 @@ Section "install"
File "${BINPATH}\share\html\index.html"
SetOutPath "$INSTDIR\share\images"
- File "${BINPATH}\share\images\drop_files.png"
+ File "${BINPATH}\share\images\download_completed.png"
+ File "${BINPATH}\share\images\download_completed_none.png"
+ File "${BINPATH}\share\images\download_in_progress.png"
+ File "${BINPATH}\share\images\download_in_progress_none.png"
+ File "${BINPATH}\share\images\favicon.ico"
+ File "${BINPATH}\share\images\file_delete.png"
+ File "${BINPATH}\share\images\info.png"
File "${BINPATH}\share\images\logo.png"
+ File "${BINPATH}\share\images\logo_transparent.png"
File "${BINPATH}\share\images\logo_grayscale.png"
File "${BINPATH}\share\images\server_started.png"
File "${BINPATH}\share\images\server_stopped.png"
File "${BINPATH}\share\images\server_working.png"
File "${BINPATH}\share\images\settings.png"
- File "${BINPATH}\share\images\settings_inactive.png"
+ File "${BINPATH}\share\images\web_file.png"
+ File "${BINPATH}\share\images\web_folder.png"
SetOutPath "$INSTDIR\share\locale"
File "${BINPATH}\share\locale\cs.json"
@@ -379,14 +389,22 @@ FunctionEnd
Delete "$INSTDIR\share\html\404.html"
Delete "$INSTDIR\share\html\denied.html"
Delete "$INSTDIR\share\html\index.html"
- Delete "$INSTDIR\share\images\drop_files.png"
+ Delete "$INSTDIR\share\images\download_completed.png"
+ Delete "$INSTDIR\share\images\download_completed_none.png"
+ Delete "$INSTDIR\share\images\download_in_progress.png"
+ Delete "$INSTDIR\share\images\download_in_progress_none.png"
+ Delete "$INSTDIR\share\images\favicon.ico"
+ Delete "$INSTDIR\share\images\file_delete.png"
+ Delete "$INSTDIR\share\images\info.png"
Delete "$INSTDIR\share\images\logo.png"
+ Delete "$INSTDIR\share\images\logo_transparent.png"
Delete "$INSTDIR\share\images\logo_grayscale.png"
Delete "$INSTDIR\share\images\server_started.png"
Delete "$INSTDIR\share\images\server_stopped.png"
Delete "$INSTDIR\share\images\server_working.png"
Delete "$INSTDIR\share\images\settings.png"
- Delete "$INSTDIR\share\images\settings_inactive.png"
+ Delete "$INSTDIR\share\images\web_file.png"
+ Delete "$INSTDIR\share\images\web_folder.png"
Delete "$INSTDIR\share\license.txt"
Delete "$INSTDIR\share\locale\cs.json"
Delete "$INSTDIR\share\locale\de.json"
@@ -404,6 +422,8 @@ FunctionEnd
Delete "$INSTDIR\share\torrc_template"
Delete "$INSTDIR\share\torrc_template-windows"
Delete "$INSTDIR\share\torrc_template-obfs4"
+ Delete "$INSTDIR\share\torrc_template-meek_lite_amazon"
+ Delete "$INSTDIR\share\torrc_template-meek_lite_azure"
Delete "$INSTDIR\share\version.txt"
Delete "$INSTDIR\share\wordlist.txt"
Delete "$INSTDIR\sip.pyd"
diff --git a/install/pyinstaller.spec b/install/pyinstaller.spec
index a38aaf6e..6ca2fdbe 100644
--- a/install/pyinstaller.spec
+++ b/install/pyinstaller.spec
@@ -15,6 +15,8 @@ a = Analysis(
('../share/wordlist.txt', 'share'),
('../share/torrc_template', 'share'),
('../share/torrc_template-obfs4', 'share'),
+ ('../share/torrc_template-meek_lite_amazon', 'share'),
+ ('../share/torrc_template-meek_lite_azure', 'share'),
('../share/torrc_template-windows', 'share'),
('../share/images/*', 'share/images'),
('../share/locale/*', 'share/locale'),
diff --git a/install/scripts/onionshare-nautilus.py b/install/scripts/onionshare-nautilus.py
index 6674dd18..ed50fb23 100644
--- a/install/scripts/onionshare-nautilus.py
+++ b/install/scripts/onionshare-nautilus.py
@@ -64,7 +64,7 @@ class OnionShareExtension(GObject.GObject, Nautilus.MenuProvider):
"""
def url2path(self,url):
- file_uri = url.get_activation_uri()
+ file_uri = url.get_activation_uri()
arg_uri = file_uri[7:]
path = urllib.url2pathname(arg_uri)
return path
diff --git a/onionshare/common.py b/onionshare/common.py
index 25b901ee..0d00c7b1 100644
--- a/onionshare/common.py
+++ b/onionshare/common.py
@@ -56,7 +56,10 @@ def get_platform():
"""
Returns the platform OnionShare is running on.
"""
- return platform.system()
+ plat = platform.system()
+ if plat.endswith('BSD'):
+ plat = 'BSD'
+ return plat
def get_resource_path(filename):
@@ -66,6 +69,10 @@ def get_resource_path(filename):
"""
p = get_platform()
+ # On Windows, and in Windows dev mode, switch slashes in incoming filename to backslackes
+ if p == 'Windows':
+ filename = filename.replace('/', '\\')
+
if getattr(sys, 'onionshare_dev_mode', False):
# Look for resources directory relative to python file
prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))), 'share')
@@ -73,7 +80,7 @@ def get_resource_path(filename):
# While running tests during stdeb bdist_deb, look 3 directories up for the share folder
prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(prefix)))), 'share')
- elif p == 'Linux':
+ elif p == 'BSD' or p == 'Linux':
# Assume OnionShare is installed systemwide in Linux, since we're not running in dev mode
prefix = os.path.join(sys.prefix, 'share/onionshare')
@@ -107,7 +114,7 @@ def get_tor_paths():
tor_geo_ip_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip')
tor_geo_ipv6_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip6')
obfs4proxy_file_path = os.path.join(base_path, 'Resources', 'Tor', 'obfs4proxy')
- elif p == 'OpenBSD' or p == 'FreeBSD':
+ elif p == 'BSD':
tor_path = '/usr/local/bin/tor'
tor_geo_ip_file_path = '/usr/local/share/tor/geoip'
tor_geo_ipv6_file_path = '/usr/local/share/tor/geoip6'
diff --git a/onionshare/onion.py b/onionshare/onion.py
index bae90f4c..068648ba 100644
--- a/onionshare/onion.py
+++ b/onionshare/onion.py
@@ -131,7 +131,7 @@ class Onion(object):
self.stealth = False
self.service_id = None
- self.system = platform.system()
+ self.system = common.get_platform()
# Is bundled tor supported?
if (self.system == 'Windows' or self.system == 'Darwin') and getattr(sys, 'onionshare_dev_mode', False):
@@ -183,7 +183,7 @@ class Onion(object):
raise OSError(strings._('no_available_port'))
self.tor_torrc = os.path.join(self.tor_data_directory.name, 'torrc')
else:
- # Linux and Mac can use unix sockets
+ # Linux, Mac and BSD can use unix sockets
with open(common.get_resource_path('torrc_template')) as f:
torrc_template = f.read()
self.tor_control_port = None
@@ -211,7 +211,22 @@ class Onion(object):
with open(common.get_resource_path('torrc_template-obfs4')) as o:
for line in o:
f.write(line)
+ elif self.settings.get('tor_bridges_use_meek_lite_amazon'):
+ f.write('ClientTransportPlugin meek_lite exec {}\n'.format(self.obfs4proxy_file_path))
+ with open(common.get_resource_path('torrc_template-meek_lite_amazon')) as o:
+ for line in o:
+ f.write(line)
+ elif self.settings.get('tor_bridges_use_meek_lite_azure'):
+ f.write('ClientTransportPlugin meek_lite exec {}\n'.format(self.obfs4proxy_file_path))
+ with open(common.get_resource_path('torrc_template-meek_lite_azure')) as o:
+ for line in o:
+ f.write(line)
+
if self.settings.get('tor_bridges_use_custom_bridges'):
+ if 'obfs4' in self.settings.get('tor_bridges_use_custom_bridges'):
+ f.write('ClientTransportPlugin obfs4 exec {}\n'.format(self.obfs4proxy_file_path))
+ elif 'meek_lite' in self.settings.get('tor_bridges_use_custom_bridges'):
+ f.write('ClientTransportPlugin meek_lite exec {}\n'.format(self.obfs4proxy_file_path))
f.write(self.settings.get('tor_bridges_use_custom_bridges'))
f.write('\nUseBridges 1')
@@ -265,7 +280,10 @@ class Onion(object):
time.sleep(0.2)
# If using bridges, it might take a bit longer to connect to Tor
- if self.settings.get('tor_bridges_use_custom_bridges') or self.settings.get('tor_bridges_use_obfs4'):
+ if self.settings.get('tor_bridges_use_custom_bridges') or \
+ self.settings.get('tor_bridges_use_obfs4') or \
+ self.settings.get('tor_bridges_use_meek_lite_amazon') or \
+ self.settings.get('tor_bridges_use_meek_lite_azure'):
connect_timeout = 150
else:
# Timeout after 120 seconds
@@ -316,7 +334,7 @@ class Onion(object):
# guessing the socket file name next
if not found_tor:
try:
- if self.system == 'Linux':
+ if self.system == 'Linux' or self.system == 'BSD':
socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
elif self.system == 'Darwin':
socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
@@ -470,8 +488,8 @@ class Onion(object):
auth_cookie = list(res.client_auth.values())[0]
self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie)
- self.settings.save()
if onion_host is not None:
+ self.settings.save()
return onion_host
else:
raise TorErrorProtocolError(strings._('error_tor_protocol_error'))
@@ -482,13 +500,19 @@ class Onion(object):
"""
common.log('Onion', 'cleanup')
- # Cleanup the ephemeral onion service
- if self.service_id:
- try:
- self.c.remove_ephemeral_hidden_service(self.service_id)
- except:
- pass
- self.service_id = None
+ # Cleanup the ephemeral onion services, if we have any
+ try:
+ onions = self.c.list_ephemeral_hidden_services()
+ for onion in onions:
+ try:
+ common.log('Onion', 'cleanup', 'trying to remove onion {}'.format(onion))
+ self.c.remove_ephemeral_hidden_service(onion)
+ except:
+ common.log('Onion', 'cleanup', 'could not remove onion {}.. moving on anyway'.format(onion))
+ pass
+ except:
+ pass
+ self.service_id = None
if stop_tor:
# Stop tor process
diff --git a/onionshare/settings.py b/onionshare/settings.py
index 94b5dde5..545915e8 100644
--- a/onionshare/settings.py
+++ b/onionshare/settings.py
@@ -58,11 +58,14 @@ class Settings(object):
'auth_password': '',
'close_after_first_download': True,
'systray_notifications': True,
+ 'shutdown_timeout': False,
'use_stealth': False,
'use_autoupdate': True,
'autoupdate_timestamp': None,
'no_bridges': True,
'tor_bridges_use_obfs4': False,
+ 'tor_bridges_use_meek_lite_amazon': False,
+ 'tor_bridges_use_meek_lite_azure': False,
'tor_bridges_use_custom_bridges': '',
'save_private_key': False,
'private_key': '',
diff --git a/onionshare/web.py b/onionshare/web.py
index 3eef67c7..d16ca251 100644
--- a/onionshare/web.py
+++ b/onionshare/web.py
@@ -26,6 +26,7 @@ import queue
import socket
import sys
import tempfile
+import base64
from distutils.version import LooseVersion as Version
from urllib.request import urlopen
@@ -58,7 +59,7 @@ zip_filename = None
zip_filesize = None
security_headers = [
- ('Content-Security-Policy', 'default-src \'self\'; style-src \'unsafe-inline\'; img-src \'self\' data:;'),
+ ('Content-Security-Policy', 'default-src \'self\'; style-src \'unsafe-inline\'; script-src \'unsafe-inline\'; img-src \'self\' data:;'),
('X-Frame-Options', 'DENY'),
('X-Xss-Protection', '1; mode=block'),
('X-Content-Type-Options', 'nosniff'),
@@ -125,6 +126,12 @@ def add_request(request_type, path, data=None):
})
+# Load and base64 encode images to pass into templates
+favicon_b64 = base64.b64encode(open(common.get_resource_path('images/favicon.ico'), 'rb').read()).decode()
+logo_b64 = base64.b64encode(open(common.get_resource_path('images/logo.png'), 'rb').read()).decode()
+folder_b64 = base64.b64encode(open(common.get_resource_path('images/web_folder.png'), 'rb').read()).decode()
+file_b64 = base64.b64encode(open(common.get_resource_path('images/web_file.png'), 'rb').read()).decode()
+
slug = None
@@ -206,7 +213,10 @@ def index(slug_candidate):
global stay_open, download_in_progress
deny_download = not stay_open and download_in_progress
if deny_download:
- r = make_response(render_template_string(open(common.get_resource_path('html/denied.html')).read()))
+ r = make_response(render_template_string(
+ open(common.get_resource_path('html/denied.html')).read(),
+ favicon_b64=favicon_b64
+ ))
for header, value in security_headers:
r.headers.set(header, value)
return r
@@ -215,6 +225,10 @@ def index(slug_candidate):
r = make_response(render_template_string(
open(common.get_resource_path('html/index.html')).read(),
+ favicon_b64=favicon_b64,
+ logo_b64=logo_b64,
+ folder_b64=folder_b64,
+ file_b64=file_b64,
slug=slug,
file_info=file_info,
filename=os.path.basename(zip_filename),
@@ -243,7 +257,10 @@ def download(slug_candidate):
global stay_open, download_in_progress, done
deny_download = not stay_open and download_in_progress
if deny_download:
- r = make_response(render_template_string(open(common.get_resource_path('html/denied.html')).read()))
+ r = make_response(render_template_string(
+ open(common.get_resource_path('html/denied.html')).read(),
+ favicon_b64=favicon_b64
+ ))
for header,value in security_headers:
r.headers.set(header, value)
return r
@@ -298,12 +315,14 @@ def download(slug_candidate):
percent = (1.0 * downloaded_bytes / zip_filesize) * 100
# only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304)
- if not gui_mode or common.get_platform() == 'Linux':
+ plat = common.get_platform()
+ if not gui_mode or plat == 'Linux' or plat == 'BSD':
sys.stdout.write(
"\r{0:s}, {1:.2f}% ".format(common.human_readable_filesize(downloaded_bytes), percent))
sys.stdout.flush()
add_request(REQUEST_PROGRESS, path, {'id': download_id, 'bytes': downloaded_bytes})
+ done = False
except:
# looks like the download was canceled
done = True
@@ -355,7 +374,10 @@ def page_not_found(e):
force_shutdown()
print(strings._('error_rate_limit'))
- r = make_response(render_template_string(open(common.get_resource_path('html/404.html')).read()), 404)
+ r = make_response(render_template_string(
+ open(common.get_resource_path('html/404.html')).read(),
+ favicon_b64=favicon_b64
+ ), 404)
for header, value in security_headers:
r.headers.set(header, value)
return r
diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py
index 14c76617..24e627bb 100644
--- a/onionshare_gui/__init__.py
+++ b/onionshare_gui/__init__.py
@@ -35,8 +35,8 @@ class Application(QtWidgets.QApplication):
and the quick keyboard shortcut.
"""
def __init__(self):
- system = platform.system()
- if system == 'Linux':
+ system = common.get_platform()
+ if system == 'Linux' or system == 'BSD':
self.setAttribute(QtCore.Qt.AA_X11InitThreads, True)
QtWidgets.QApplication.__init__(self, sys.argv)
self.installEventFilter(self)
diff --git a/onionshare_gui/downloads.py b/onionshare_gui/downloads.py
index 60bd59ac..166f14a4 100644
--- a/onionshare_gui/downloads.py
+++ b/onionshare_gui/downloads.py
@@ -34,13 +34,15 @@ class Download(object):
# make a new progress bar
cssStyleData ="""
QProgressBar {
- border: 2px solid grey;
- border-radius: 5px;
+ border: 1px solid #4e064f;
+ background-color: #ffffff !important;
text-align: center;
+ color: #9b9b9b;
+ font-size: 12px;
}
QProgressBar::chunk {
- background: qlineargradient(x1: 0.5, y1: 0, x2: 0.5, y2: 1, stop: 0 #b366ff, stop: 1 #d9b3ff);
+ background-color: #4e064f;
width: 10px;
}"""
self.progress_bar = QtWidgets.QProgressBar()
diff --git a/onionshare_gui/file_selection.py b/onionshare_gui/file_selection.py
index da03d24d..29bcc592 100644
--- a/onionshare_gui/file_selection.py
+++ b/onionshare_gui/file_selection.py
@@ -23,6 +23,50 @@ from .alert import Alert
from onionshare import strings, common
+class DropHereLabel(QtWidgets.QLabel):
+ """
+ When there are no files or folders in the FileList yet, display the
+ 'drop files here' message and graphic.
+ """
+ def __init__(self, parent, image=False):
+ self.parent = parent
+ super(DropHereLabel, self).__init__(parent=parent)
+ self.setAcceptDrops(True)
+ self.setAlignment(QtCore.Qt.AlignCenter)
+
+ if image:
+ self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(common.get_resource_path('images/logo_transparent.png'))))
+ else:
+ self.setText(strings._('gui_drag_and_drop', True))
+ self.setStyleSheet('color: #999999;')
+
+ self.hide()
+
+ def dragEnterEvent(self, event):
+ self.parent.drop_here_image.hide()
+ self.parent.drop_here_text.hide()
+ event.accept()
+
+
+class DropCountLabel(QtWidgets.QLabel):
+ """
+ While dragging files over the FileList, this counter displays the
+ number of files you're dragging.
+ """
+ def __init__(self, parent):
+ self.parent = parent
+ super(DropCountLabel, self).__init__(parent=parent)
+ self.setAcceptDrops(True)
+ self.setAlignment(QtCore.Qt.AlignCenter)
+ self.setText(strings._('gui_drag_and_drop', True))
+ self.setStyleSheet('color: #ffffff; background-color: #f44449; font-weight: bold; padding: 5px 10px; border-radius: 10px;')
+ self.hide()
+
+ def dragEnterEvent(self, event):
+ self.hide()
+ event.accept()
+
+
class FileList(QtWidgets.QListWidget):
"""
The list of files and folders in the GUI.
@@ -35,63 +79,82 @@ class FileList(QtWidgets.QListWidget):
self.setAcceptDrops(True)
self.setIconSize(QtCore.QSize(32, 32))
self.setSortingEnabled(True)
- self.setMinimumHeight(200)
+ self.setMinimumHeight(205)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
-
- class DropHereLabel(QtWidgets.QLabel):
- """
- When there are no files or folders in the FileList yet, display the
- 'drop files here' message and graphic.
- """
- def __init__(self, parent, image=False):
- self.parent = parent
- super(DropHereLabel, self).__init__(parent=parent)
- self.setAcceptDrops(True)
- self.setAlignment(QtCore.Qt.AlignCenter)
-
- if image:
- self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(common.get_resource_path('images/drop_files.png'))))
- else:
- self.setText(strings._('gui_drag_and_drop', True))
- self.setStyleSheet('color: #999999;')
-
- self.hide()
-
- def dragEnterEvent(self, event):
- self.parent.drop_here_image.hide()
- self.parent.drop_here_text.hide()
- event.ignore()
-
self.drop_here_image = DropHereLabel(self, True)
self.drop_here_text = DropHereLabel(self, False)
-
- self.filenames = []
- self.update()
+ self.drop_count = DropCountLabel(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 len(self.filenames) == 0:
+ if self.count() == 0:
self.drop_here_image.show()
self.drop_here_text.show()
else:
self.drop_here_image.hide()
self.drop_here_text.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_image.setGeometry(0, 0, self.width(), self.height())
- self.drop_here_text.setGeometry(0, 0, self.width(), self.height())
+ offset = 70
+ self.drop_here_image.setGeometry(0, 0, self.width(), self.height() - offset)
+ self.drop_here_text.setGeometry(0, offset, self.width(), self.height() - offset)
+
+ 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('FileList { border: 3px solid #538ad0; }')
+ count = len(event.mimeData().urls())
+ self.drop_count.setText('+{}'.format(count))
+
+ size_hint = self.drop_count.sizeHint()
+ self.drop_count.setGeometry(self.width() - size_hint.width() - 10, self.height() - size_hint.height() - 10, size_hint.width(), size_hint.height())
+ self.drop_count.show()
event.accept()
else:
event.ignore()
@@ -100,6 +163,8 @@ class FileList(QtWidgets.QListWidget):
"""
dragLeaveEvent for dragging files and directories into the widget.
"""
+ self.setStyleSheet('FileList { border: none; }')
+ self.drop_count.hide()
event.accept()
self.update()
@@ -125,36 +190,84 @@ class FileList(QtWidgets.QListWidget):
self.add_file(filename)
else:
event.ignore()
+
+ self.setStyleSheet('border: none;')
+ self.drop_count.hide()
+
self.files_dropped.emit()
def add_file(self, filename):
"""
Add a file or directory to this widget.
"""
- if filename not in self.filenames:
+ 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(strings._("not_a_readable_file", True).format(filename))
return
- self.filenames.append(filename)
- # Re-sort the list internally
- self.filenames.sort()
-
fileinfo = QtCore.QFileInfo(filename)
- basename = os.path.basename(filename.rstrip('/'))
ip = QtWidgets.QFileIconProvider()
icon = ip.icon(fileinfo)
if os.path.isfile(filename):
- size = common.human_readable_filesize(fileinfo.size())
+ size_bytes = fileinfo.size()
+ size_readable = common.human_readable_filesize(size_bytes)
else:
- size = common.human_readable_filesize(common.dir_size(filename))
- item_name = '{0:s} ({1:s})'.format(basename, size)
- item = QtWidgets.QListWidgetItem(item_name)
- item.setToolTip(size)
+ size_bytes = common.dir_size(filename)
+ size_readable = 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('QLabel { color: #666666; font-size: 11px; }')
+
+ 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(common.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.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()
@@ -168,21 +281,23 @@ class FileSelection(QtWidgets.QVBoxLayout):
super(FileSelection, self).__init__()
self.server_on = False
- # file list
+ # File list
self.file_list = FileList()
- self.file_list.currentItemChanged.connect(self.update)
+ self.file_list.itemSelectionChanged.connect(self.update)
self.file_list.files_dropped.connect(self.update)
+ self.file_list.files_updated.connect(self.update)
- # buttons
+ # Buttons
self.add_button = QtWidgets.QPushButton(strings._('gui_add', True))
self.add_button.clicked.connect(self.add)
self.delete_button = QtWidgets.QPushButton(strings._('gui_delete', True))
self.delete_button.clicked.connect(self.delete)
button_layout = QtWidgets.QHBoxLayout()
+ button_layout.addStretch()
button_layout.addWidget(self.add_button)
button_layout.addWidget(self.delete_button)
- # add the widgets
+ # Add the widgets
self.addWidget(self.file_list)
self.addLayout(button_layout)
@@ -192,21 +307,20 @@ class FileSelection(QtWidgets.QVBoxLayout):
"""
Update the GUI elements based on the current state.
"""
- # all buttons should be disabled if the server is on
+ # All buttons should be hidden if the server is on
if self.server_on:
- self.add_button.setEnabled(False)
- self.delete_button.setEnabled(False)
+ self.add_button.hide()
+ self.delete_button.hide()
else:
- self.add_button.setEnabled(True)
+ self.add_button.show()
- # delete button should be disabled if item isn't selected
- current_item = self.file_list.currentItem()
- if not current_item:
- self.delete_button.setEnabled(False)
+ # Delete button should be hidden if item isn't selected
+ if len(self.file_list.selectedItems()) == 0:
+ self.delete_button.hide()
else:
- self.delete_button.setEnabled(True)
+ self.delete_button.show()
- # update the file list
+ # Update the file list
self.file_list.update()
def add(self):
@@ -218,6 +332,7 @@ class FileSelection(QtWidgets.QVBoxLayout):
for filename in file_dialog.selectedFiles():
self.file_list.add_file(filename)
+ self.file_list.setCurrentItem(None)
self.update()
def delete(self):
@@ -227,9 +342,10 @@ class FileSelection(QtWidgets.QVBoxLayout):
selected = self.file_list.selectedItems()
for item in selected:
itemrow = self.file_list.row(item)
- self.file_list.filenames.pop(itemrow)
self.file_list.takeItem(itemrow)
self.file_list.files_updated.emit()
+
+ self.file_list.setCurrentItem(None)
self.update()
def server_started(self):
@@ -237,7 +353,7 @@ class FileSelection(QtWidgets.QVBoxLayout):
Gets called when the server starts.
"""
self.server_on = True
- self.file_list.setAcceptDrops(False)
+ self.file_list.server_started()
self.update()
def server_stopped(self):
@@ -245,14 +361,14 @@ class FileSelection(QtWidgets.QVBoxLayout):
Gets called when the server stops.
"""
self.server_on = False
- self.file_list.setAcceptDrops(True)
+ 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(self.file_list.filenames)
+ return len(range(self.file_list.count()))
def setFocus(self):
"""
diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py
index 582ebdb3..f38dd727 100644
--- a/onionshare_gui/onionshare_gui.py
+++ b/onionshare_gui/onionshare_gui.py
@@ -56,6 +56,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.setWindowTitle('OnionShare')
self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
+ self.setMinimumWidth(430)
# Load settings
self.config = config
@@ -72,20 +73,31 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection, self.settings)
self.server_status.server_started.connect(self.file_selection.server_started)
self.server_status.server_started.connect(self.start_server)
+ self.server_status.server_started.connect(self.update_server_status_indicator)
self.server_status.server_stopped.connect(self.file_selection.server_stopped)
self.server_status.server_stopped.connect(self.stop_server)
+ self.server_status.server_stopped.connect(self.update_server_status_indicator)
+ self.server_status.server_stopped.connect(self.update_primary_action)
+ self.server_status.server_canceled.connect(self.cancel_server)
+ self.server_status.server_canceled.connect(self.file_selection.server_stopped)
+ self.server_status.server_canceled.connect(self.update_primary_action)
self.start_server_finished.connect(self.clear_message)
self.start_server_finished.connect(self.server_status.start_server_finished)
+ self.start_server_finished.connect(self.update_server_status_indicator)
self.stop_server_finished.connect(self.server_status.stop_server_finished)
+ self.stop_server_finished.connect(self.update_server_status_indicator)
self.file_selection.file_list.files_updated.connect(self.server_status.update)
+ self.file_selection.file_list.files_updated.connect(self.update_primary_action)
self.server_status.url_copied.connect(self.copy_url)
self.server_status.hidservauth_copied.connect(self.copy_hidservauth)
self.starting_server_step2.connect(self.start_server_step2)
self.starting_server_step3.connect(self.start_server_step3)
self.starting_server_error.connect(self.start_server_error)
+ self.server_status.button_clicked.connect(self.clear_message)
# Filesize warning
self.filesize_warning = QtWidgets.QLabel()
+ self.filesize_warning.setWordWrap(True)
self.filesize_warning.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;')
self.filesize_warning.hide()
@@ -99,38 +111,95 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.vbar = self.downloads_container.verticalScrollBar()
self.downloads_container.hide() # downloads start out hidden
self.new_download = False
+ self.downloads_in_progress = 0
+ self.downloads_completed = 0
+
+ # Info label along top of screen
+ self.info_layout = QtWidgets.QHBoxLayout()
+ self.info_label = QtWidgets.QLabel()
+ self.info_label.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
+
+ self.info_in_progress_downloads_count = QtWidgets.QLabel()
+ self.info_in_progress_downloads_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
+
+ self.info_completed_downloads_count = QtWidgets.QLabel()
+ self.info_completed_downloads_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
+
+ self.update_downloads_completed(self.downloads_in_progress)
+ self.update_downloads_in_progress(self.downloads_in_progress)
+
+ self.info_layout.addWidget(self.info_label)
+ self.info_layout.addStretch()
+ self.info_layout.addWidget(self.info_in_progress_downloads_count)
+ self.info_layout.addWidget(self.info_completed_downloads_count)
+
+ self.info_widget = QtWidgets.QWidget()
+ self.info_widget.setLayout(self.info_layout)
+ self.info_widget.hide()
+
+ # Settings button on the status bar
+ self.settings_button = QtWidgets.QPushButton()
+ self.settings_button.setDefault(False)
+ self.settings_button.setFlat(True)
+ self.settings_button.setFixedWidth(40)
+ self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) )
+ self.settings_button.clicked.connect(self.open_settings)
+
+ # Server status indicator on the status bar
+ self.server_status_image_stopped = QtGui.QImage(common.get_resource_path('images/server_stopped.png'))
+ self.server_status_image_working = QtGui.QImage(common.get_resource_path('images/server_working.png'))
+ self.server_status_image_started = QtGui.QImage(common.get_resource_path('images/server_started.png'))
+ self.server_status_image_label = QtWidgets.QLabel()
+ self.server_status_image_label.setFixedWidth(20)
+ self.server_status_label = QtWidgets.QLabel()
+ self.server_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; }')
+ server_status_indicator_layout = QtWidgets.QHBoxLayout()
+ server_status_indicator_layout.addWidget(self.server_status_image_label)
+ server_status_indicator_layout.addWidget(self.server_status_label)
+ self.server_status_indicator = QtWidgets.QWidget()
+ self.server_status_indicator.setLayout(server_status_indicator_layout)
+ self.update_server_status_indicator()
# Status bar
self.status_bar = QtWidgets.QStatusBar()
self.status_bar.setSizeGripEnabled(False)
- self.status_bar.setStyleSheet(
- "QStatusBar::item { border: 0px; }")
- version_label = QtWidgets.QLabel('v{0:s}'.format(common.get_version()))
- version_label.setStyleSheet('color: #666666')
- self.settings_button = QtWidgets.QPushButton()
- self.settings_button.setDefault(False)
- self.settings_button.setFlat(True)
- self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) )
- self.settings_button.clicked.connect(self.open_settings)
- self.status_bar.addPermanentWidget(version_label)
+ statusBar_cssStyleData ="""
+ QStatusBar {
+ font-style: italic;
+ color: #666666;
+ }
+
+ QStatusBar::item {
+ border: 0px;
+ }"""
+
+ self.status_bar.setStyleSheet(statusBar_cssStyleData)
+ self.status_bar.addPermanentWidget(self.server_status_indicator)
self.status_bar.addPermanentWidget(self.settings_button)
self.setStatusBar(self.status_bar)
# Status bar, zip progress bar
self._zip_progress_bar = None
+ # Status bar, sharing messages
+ self.server_share_status_label = QtWidgets.QLabel('')
+ self.server_share_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; padding: 2px; }')
+ self.status_bar.insertWidget(0, self.server_share_status_label)
- # Persistent URL notification
- self.persistent_url_label = QtWidgets.QLabel(strings._('persistent_url_in_use', True))
- self.persistent_url_label.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;')
- self.persistent_url_label.hide()
+ # Primary action layout
+ primary_action_layout = QtWidgets.QVBoxLayout()
+ primary_action_layout.addWidget(self.server_status)
+ primary_action_layout.addWidget(self.filesize_warning)
+ primary_action_layout.addWidget(self.downloads_container)
+ self.primary_action = QtWidgets.QWidget()
+ self.primary_action.setLayout(primary_action_layout)
+ self.primary_action.hide()
+ self.update_primary_action()
# Main layout
self.layout = QtWidgets.QVBoxLayout()
+ self.layout.addWidget(self.info_widget)
self.layout.addLayout(self.file_selection)
- self.layout.addLayout(self.server_status)
- self.layout.addWidget(self.filesize_warning)
- self.layout.addWidget(self.persistent_url_label)
- self.layout.addWidget(self.downloads_container)
+ self.layout.addWidget(self.primary_action)
central_widget = QtWidgets.QWidget()
central_widget.setLayout(self.layout)
self.setCentralWidget(central_widget)
@@ -158,6 +227,46 @@ class OnionShareGui(QtWidgets.QMainWindow):
# After connecting to Tor, check for updates
self.check_for_updates()
+ def update_primary_action(self):
+ # Show or hide primary action layout
+ file_count = self.file_selection.file_list.count()
+ if file_count > 0:
+ self.primary_action.show()
+ self.info_widget.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 = common.human_readable_filesize(total_size_bytes)
+
+ if file_count > 1:
+ self.info_label.setText(strings._('gui_file_info', True).format(file_count, total_size_readable))
+ else:
+ self.info_label.setText(strings._('gui_file_info_single', True).format(file_count, total_size_readable))
+
+ else:
+ self.primary_action.hide()
+ self.info_widget.hide()
+
+ # Resize window
+ self.adjustSize()
+
+ def update_server_status_indicator(self):
+ common.log('OnionShareGui', 'update_server_status_indicator')
+
+ # Set the status image
+ if self.server_status.status == self.server_status.STATUS_STOPPED:
+ self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped))
+ self.server_status_label.setText(strings._('gui_status_indicator_stopped', True))
+ elif self.server_status.status == self.server_status.STATUS_WORKING:
+ self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working))
+ self.server_status_label.setText(strings._('gui_status_indicator_working', True))
+ elif self.server_status.status == self.server_status.STATUS_STARTED:
+ self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started))
+ self.server_status_label.setText(strings._('gui_status_indicator_started', True))
+
def _initSystemTray(self):
system = common.get_platform()
@@ -238,11 +347,17 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self.server_status.file_selection.get_num_files() > 0:
self.server_status.server_button.setEnabled(True)
self.status_bar.clearMessage()
+ # If we switched off the shutdown timeout setting, ensure the widget is hidden.
+ if not self.settings.get('shutdown_timeout'):
+ self.server_status.shutdown_timeout_container.hide()
d = SettingsDialog(self.onion, self.qtapp, self.config)
d.settings_saved.connect(reload_settings)
d.exec_()
+ # When settings close, refresh the server status UI
+ self.server_status.update()
+
def start_server(self):
"""
Start the onionshare server. This uses multiple threads to start the Tor onion
@@ -257,7 +372,9 @@ class OnionShareGui(QtWidgets.QMainWindow):
# Hide and reset the downloads if we have previously shared
self.downloads_container.hide()
self.downloads.reset_downloads()
+ self.reset_info_counters()
self.status_bar.clearMessage()
+ self.server_share_status_label.setText('')
# Reset web counters
web.download_count = 0
@@ -284,9 +401,10 @@ class OnionShareGui(QtWidgets.QMainWindow):
# wait for modules in thread to load, preventing a thread-related cx_Freeze crash
time.sleep(0.2)
- t = threading.Thread(target=start_onion_service, kwargs={'self': self})
- t.daemon = True
- t.start()
+ common.log('OnionshareGui', 'start_server', 'Starting an onion thread')
+ self.t = OnionThread(function=start_onion_service, kwargs={'self': self})
+ self.t.daemon = True
+ self.t.start()
def start_server_step2(self):
"""
@@ -296,8 +414,11 @@ class OnionShareGui(QtWidgets.QMainWindow):
# add progress bar to the status bar, indicating the crunching of files.
self._zip_progress_bar = ZipProgressBar(0)
- self._zip_progress_bar.total_files_size = OnionShareGui._compute_total_size(
- self.file_selection.file_list.filenames)
+ self.filenames = []
+ for index in range(self.file_selection.file_list.count()):
+ self.filenames.append(self.file_selection.file_list.item(index).filename)
+
+ self._zip_progress_bar.total_files_size = OnionShareGui._compute_total_size(self.filenames)
self.status_bar.insertWidget(0, self._zip_progress_bar)
# prepare the files for sending in a new thread
@@ -307,7 +428,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self._zip_progress_bar != None:
self._zip_progress_bar.update_processed_size_signal.emit(x)
try:
- web.set_file_info(self.file_selection.file_list.filenames, processed_size_callback=_set_processed_size)
+ web.set_file_info(self.filenames, processed_size_callback=_set_processed_size)
self.app.cleanup_filenames.append(web.zip_filename)
self.starting_server_step3.emit()
@@ -317,7 +438,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.starting_server_error.emit(e.strerror)
return
- #self.status_bar.showMessage(strings._('gui_starting_server2', True))
t = threading.Thread(target=finish_starting_server, kwargs={'self': self})
t.daemon = True
t.start()
@@ -339,7 +459,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.filesize_warning.setText(strings._("large_filesize", True))
self.filesize_warning.show()
- if self.server_status.timer_enabled:
+ if self.settings.get('shutdown_timeout'):
# Convert the date value to seconds between now and then
now = QtCore.QDateTime.currentDateTime()
self.timeout = now.secsTo(self.server_status.timeout)
@@ -352,9 +472,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.stop_server()
self.start_server_error(strings._('gui_server_started_after_timeout'))
- if self.settings.get('save_private_key'):
- self.persistent_url_label.show()
-
def start_server_error(self, error):
"""
If there's an error when trying to start the onion service
@@ -370,6 +487,14 @@ class OnionShareGui(QtWidgets.QMainWindow):
self._zip_progress_bar = None
self.status_bar.clearMessage()
+ def cancel_server(self):
+ """
+ Cancel the server while it is preparing to start
+ """
+ if self.t:
+ self.t.quit()
+ self.stop_server()
+
def stop_server(self):
"""
Stop the onionshare server.
@@ -386,10 +511,13 @@ class OnionShareGui(QtWidgets.QMainWindow):
# Remove ephemeral service, but don't disconnect from Tor
self.onion.cleanup(stop_tor=False)
self.filesize_warning.hide()
- self.persistent_url_label.hide()
- self.stop_server_finished.emit()
+ self.downloads_in_progress = 0
+ self.downloads_completed = 0
+ self.update_downloads_in_progress(0)
+ self.file_selection.file_list.adjustSize()
self.set_server_active(False)
+ self.stop_server_finished.emit()
def check_for_updates(self):
"""
@@ -455,6 +583,8 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.downloads_container.show() # show the downloads layout
self.downloads.add_download(event["data"]["id"], web.zip_filesize)
self.new_download = True
+ self.downloads_in_progress += 1
+ self.update_downloads_in_progress(self.downloads_in_progress)
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True))
@@ -469,16 +599,30 @@ class OnionShareGui(QtWidgets.QMainWindow):
if event["data"]["bytes"] == web.zip_filesize:
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True))
+ # Update the total 'completed downloads' info
+ self.downloads_completed += 1
+ self.update_downloads_completed(self.downloads_completed)
+ # Update the 'in progress downloads' info
+ self.downloads_in_progress -= 1
+ self.update_downloads_in_progress(self.downloads_in_progress)
+
# close on finish?
if not web.get_stay_open():
self.server_status.stop_server()
- self.status_bar.showMessage(strings._('closing_automatically', True))
+ self.status_bar.clearMessage()
+ self.server_share_status_label.setText(strings._('closing_automatically', True))
else:
if self.server_status.status == self.server_status.STATUS_STOPPED:
self.downloads.cancel_download(event["data"]["id"])
+ self.downloads_in_progress = 0
+ self.update_downloads_in_progress(self.downloads_in_progress)
+
elif event["type"] == web.REQUEST_CANCELED:
self.downloads.cancel_download(event["data"]["id"])
+ # Update the 'in progress downloads' info
+ self.downloads_in_progress -= 1
+ self.update_downloads_in_progress(self.downloads_in_progress)
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True))
@@ -487,30 +631,37 @@ class OnionShareGui(QtWidgets.QMainWindow):
# If the auto-shutdown timer has stopped, stop the server
if self.server_status.status == self.server_status.STATUS_STARTED:
- if self.app.shutdown_timer and self.server_status.timer_enabled:
+ if self.app.shutdown_timer and self.settings.get('shutdown_timeout'):
if self.timeout > 0:
+ now = QtCore.QDateTime.currentDateTime()
+ seconds_remaining = now.secsTo(self.server_status.timeout)
+ self.server_status.server_button.setText(strings._('gui_stop_server_shutdown_timeout', True).format(seconds_remaining))
if not self.app.shutdown_timer.is_alive():
# If there were no attempts to download the share, or all downloads are done, we can stop
if web.download_count == 0 or web.done:
self.server_status.stop_server()
- self.status_bar.showMessage(strings._('close_on_timeout', True))
+ self.status_bar.clearMessage()
+ self.server_share_status_label.setText(strings._('close_on_timeout', True))
# A download is probably still running - hold off on stopping the share
else:
- self.status_bar.showMessage(strings._('timeout_download_still_running', True))
+ self.status_bar.clearMessage()
+ self.server_share_status_label.setText(strings._('timeout_download_still_running', True))
def copy_url(self):
"""
When the URL gets copied to the clipboard, display this in the status bar.
"""
common.log('OnionShareGui', 'copy_url')
- self.status_bar.showMessage(strings._('gui_copied_url', True), 2000)
+ if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
+ self.systemTray.showMessage(strings._('gui_copied_url_title', True), strings._('gui_copied_url', True))
def copy_hidservauth(self):
"""
When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar.
"""
common.log('OnionShareGui', 'copy_hidservauth')
- self.status_bar.showMessage(strings._('gui_copied_hidservauth', True), 2000)
+ if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
+ self.systemTray.showMessage(strings._('gui_copied_hidservauth_title', True), strings._('gui_copied_hidservauth', True))
def clear_message(self):
"""
@@ -522,22 +673,52 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
Disable the Settings button while an OnionShare server is active.
"""
- self.settings_button.setEnabled(not active)
if active:
- self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings_inactive.png')) )
+ self.settings_button.hide()
else:
- self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) )
+ self.settings_button.show()
# Disable settings menu action when server is active
self.settingsAction.setEnabled(not active)
+ def reset_info_counters(self):
+ """
+ Set the info counters back to zero.
+ """
+ self.update_downloads_completed(0)
+ self.update_downloads_in_progress(0)
+
+ def update_downloads_completed(self, count):
+ """
+ Update the 'Downloads completed' info widget.
+ """
+ if count == 0:
+ self.info_completed_downloads_image = common.get_resource_path('images/download_completed_none.png')
+ else:
+ self.info_completed_downloads_image = common.get_resource_path('images/download_completed.png')
+ self.info_completed_downloads_count.setText(' {1:d}'.format(self.info_completed_downloads_image, count))
+ self.info_completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(count))
+
+ def update_downloads_in_progress(self, count):
+ """
+ Update the 'Downloads in progress' info widget.
+ """
+ if count == 0:
+ self.info_in_progress_downloads_image = common.get_resource_path('images/download_in_progress_none.png')
+ else:
+ self.info_in_progress_downloads_image = common.get_resource_path('images/download_in_progress.png')
+ self.info_in_progress_downloads_count.setText('
{1:d}'.format(self.info_in_progress_downloads_image, count))
+ self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(count))
+
def closeEvent(self, e):
common.log('OnionShareGui', 'closeEvent')
try:
if self.server_status.status != self.server_status.STATUS_STOPPED:
+ common.log('OnionShareGui', 'closeEvent, opening warning dialog')
dialog = QtWidgets.QMessageBox()
- dialog.setWindowTitle("OnionShare")
+ dialog.setWindowTitle(strings._('gui_quit_title', True))
dialog.setText(strings._('gui_quit_warning', True))
+ dialog.setIcon(QtWidgets.QMessageBox.Critical)
quit_button = dialog.addButton(strings._('gui_quit_warning_quit', True), QtWidgets.QMessageBox.YesRole)
dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit', True), QtWidgets.QMessageBox.NoRole)
dialog.setDefaultButton(dont_quit_button)
@@ -566,14 +747,15 @@ class ZipProgressBar(QtWidgets.QProgressBar):
self.setFormat(strings._('zip_progress_bar_format'))
cssStyleData ="""
QProgressBar {
- background-color: rgba(255, 255, 255, 0.0) !important;
- border: 0px;
+ border: 1px solid #4e064f;
+ background-color: #ffffff !important;
text-align: center;
+ color: #9b9b9b;
}
QProgressBar::chunk {
border: 0px;
- background: qlineargradient(x1: 0.5, y1: 0, x2: 0.5, y2: 1, stop: 0 #b366ff, stop: 1 #d9b3ff);
+ background-color: #4e064f;
width: 10px;
}"""
self.setStyleSheet(cssStyleData)
@@ -607,3 +789,26 @@ class ZipProgressBar(QtWidgets.QProgressBar):
self.setValue(100)
else:
self.setValue(0)
+
+
+class OnionThread(QtCore.QThread):
+ """
+ A QThread for starting our Onion Service.
+ By using QThread rather than threading.Thread, we are able
+ to call quit() or terminate() on the startup if the user
+ decided to cancel (in which case do not proceed with obtaining
+ the Onion address and starting the web server).
+ """
+ def __init__(self, function, kwargs=None):
+ super(OnionThread, self).__init__()
+ common.log('OnionThread', '__init__')
+ self.function = function
+ if not kwargs:
+ self.kwargs = {}
+ else:
+ self.kwargs = kwargs
+
+ def run(self):
+ common.log('OnionThread', 'run')
+
+ self.function(**self.kwargs)
diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py
index 442ae440..03540415 100644
--- a/onionshare_gui/server_status.py
+++ b/onionshare_gui/server_status.py
@@ -23,12 +23,14 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings, common, settings
-class ServerStatus(QtWidgets.QVBoxLayout):
+class ServerStatus(QtWidgets.QWidget):
"""
The server status chunk of the GUI.
"""
server_started = QtCore.pyqtSignal()
server_stopped = QtCore.pyqtSignal()
+ server_canceled = QtCore.pyqtSignal()
+ button_clicked = QtCore.pyqtSignal()
url_copied = QtCore.pyqtSignal()
hidservauth_copied = QtCore.pyqtSignal()
@@ -47,100 +49,103 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.settings = settings
- # Helper boolean as this is used in a few places
- self.timer_enabled = False
# Shutdown timeout layout
- self.server_shutdown_timeout_checkbox = QtWidgets.QCheckBox()
- self.server_shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
- self.server_shutdown_timeout_checkbox.toggled.connect(self.shutdown_timeout_toggled)
- self.server_shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_choice", True))
- self.server_shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True))
- self.server_shutdown_timeout = QtWidgets.QDateTimeEdit()
+ self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True))
+ self.shutdown_timeout = QtWidgets.QDateTimeEdit()
# Set proposed timeout to be 5 minutes into the future
- self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
+ self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy")
+ self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
# Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 2 min from now
- self.server_shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
- self.server_shutdown_timeout.setCurrentSectionIndex(4)
- self.server_shutdown_timeout_label.hide()
- self.server_shutdown_timeout.hide()
- shutdown_timeout_layout_group = QtWidgets.QHBoxLayout()
- shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout_checkbox)
- shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout_label)
- shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout)
- # server layout
- self.status_image_stopped = QtGui.QImage(common.get_resource_path('images/server_stopped.png'))
- self.status_image_working = QtGui.QImage(common.get_resource_path('images/server_working.png'))
- self.status_image_started = QtGui.QImage(common.get_resource_path('images/server_started.png'))
- self.status_image_label = QtWidgets.QLabel()
- self.status_image_label.setFixedWidth(30)
+ self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
+ 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)
- server_layout = QtWidgets.QHBoxLayout()
- server_layout.addWidget(self.status_image_label)
- server_layout.addWidget(self.server_button)
- # url layout
+ # URL layout
url_font = QtGui.QFont()
- self.url_label = QtWidgets.QLabel()
- self.url_label.setFont(url_font)
- self.url_label.setWordWrap(False)
- self.url_label.setAlignment(QtCore.Qt.AlignCenter)
+ self.url_description = QtWidgets.QLabel(strings._('gui_url_description', True))
+ self.url_description.setWordWrap(True)
+ self.url_description.setMinimumHeight(50)
+ self.url = QtWidgets.QLabel()
+ self.url.setFont(url_font)
+ self.url.setWordWrap(True)
+ self.url.setMinimumHeight(60)
+ self.url.setMinimumSize(self.url.sizeHint())
+ self.url.setStyleSheet('QLabel { background-color: #ffffff; color: #000000; padding: 10px; border: 1px solid #666666; }')
+
+ url_buttons_style = 'QPushButton { color: #3f7fcf; }'
self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True))
+ self.copy_url_button.setFlat(True)
+ self.copy_url_button.setStyleSheet(url_buttons_style)
self.copy_url_button.clicked.connect(self.copy_url)
self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True))
+ self.copy_hidservauth_button.setFlat(True)
+ self.copy_hidservauth_button.setStyleSheet(url_buttons_style)
self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth)
- url_layout = QtWidgets.QHBoxLayout()
- url_layout.addWidget(self.url_label)
- url_layout.addWidget(self.copy_url_button)
- url_layout.addWidget(self.copy_hidservauth_button)
+ 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()
- # add the widgets
- self.addLayout(shutdown_timeout_layout_group)
- self.addLayout(server_layout)
- self.addLayout(url_layout)
+ 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)
self.update()
- def shutdown_timeout_toggled(self, checked):
- """
- Shutdown timer option was toggled. If checked, show the timer settings.
- """
- if checked:
- self.timer_enabled = True
- # Hide the checkbox, show the options
- self.server_shutdown_timeout_label.show()
- # Reset the default timer to 5 minutes into the future after toggling the option on
- self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
- self.server_shutdown_timeout.show()
- else:
- self.timer_enabled = False
- self.server_shutdown_timeout_label.hide()
- self.server_shutdown_timeout.hide()
-
def shutdown_timeout_reset(self):
"""
Reset the timeout in the UI after stopping a share
"""
- self.server_shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
- self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
- self.server_shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
+ self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
+ self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
def update(self):
"""
Update the GUI elements based on the current state.
"""
- # set the status image
- if self.status == self.STATUS_STOPPED:
- self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_stopped))
- elif self.status == self.STATUS_WORKING:
- self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_working))
- elif self.status == self.STATUS_STARTED:
- self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_started))
-
- # set the URL fields
+ # Set the URL fields
if self.status == self.STATUS_STARTED:
- self.url_label.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug))
- self.url_label.show()
+ self.url_description.show()
+
+ info_image = common.get_resource_path('images/info.png')
+ self.url_description.setText(strings._('gui_url_description', True).format(info_image))
+ # Show a Tool Tip explaining the lifecycle of this URL
+ if self.settings.get('save_private_key'):
+ if self.settings.get('close_after_first_download'):
+ self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent', True))
+ else:
+ self.url_description.setToolTip(strings._('gui_url_label_persistent', True))
+ else:
+ if self.settings.get('close_after_first_download'):
+ self.url_description.setToolTip(strings._('gui_url_label_onetime', True))
+ else:
+ self.url_description.setToolTip(strings._('gui_url_label_stay_open', True))
+
+ self.url.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug))
+ self.url.show()
+
self.copy_url_button.show()
if self.settings.get('save_private_key'):
@@ -148,53 +153,63 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.settings.set('slug', self.web.slug)
self.settings.save()
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.hide()
+
if self.app.stealth:
self.copy_hidservauth_button.show()
else:
self.copy_hidservauth_button.hide()
-
- # resize parent widget
- p = self.parentWidget()
- p.resize(p.sizeHint())
else:
- self.url_label.hide()
+ self.url_description.hide()
+ self.url.hide()
self.copy_url_button.hide()
self.copy_hidservauth_button.hide()
- # button
+ # Button
+ button_stopped_style = 'QPushButton { background-color: #5fa416; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; }'
+ button_working_style = 'QPushButton { background-color: #4c8211; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; font-style: italic; }'
+ button_started_style = 'QPushButton { background-color: #d0011b; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; }'
if self.file_selection.get_num_files() == 0:
- self.server_button.setEnabled(False)
- self.server_button.setText(strings._('gui_start_server', True))
+ self.server_button.hide()
else:
+ self.server_button.show()
+
if self.status == self.STATUS_STOPPED:
+ self.server_button.setStyleSheet(button_stopped_style)
self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_start_server', True))
- self.server_shutdown_timeout.setEnabled(True)
- self.server_shutdown_timeout_checkbox.setEnabled(True)
+ self.server_button.setToolTip('')
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.show()
elif self.status == self.STATUS_STARTED:
+ self.server_button.setStyleSheet(button_started_style)
self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_stop_server', True))
- self.server_shutdown_timeout.setEnabled(False)
- self.server_shutdown_timeout_checkbox.setEnabled(False)
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.hide()
+ self.server_button.setToolTip(strings._('gui_stop_server_shutdown_timeout_tooltip', True).format(self.timeout))
elif self.status == self.STATUS_WORKING:
- self.server_button.setEnabled(False)
+ self.server_button.setStyleSheet(button_working_style)
+ self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_please_wait'))
- self.server_shutdown_timeout.setEnabled(False)
- self.server_shutdown_timeout_checkbox.setEnabled(False)
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.hide()
else:
+ self.server_button.setStyleSheet(button_working_style)
self.server_button.setEnabled(False)
self.server_button.setText(strings._('gui_please_wait'))
- self.server_shutdown_timeout.setEnabled(False)
- self.server_shutdown_timeout_checkbox.setEnabled(False)
+ if self.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.timer_enabled:
+ if self.settings.get('shutdown_timeout'):
# Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen
- self.timeout = self.server_shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0)
+ 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(strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning))
@@ -204,6 +219,9 @@ class ServerStatus(QtWidgets.QVBoxLayout):
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):
"""
@@ -230,6 +248,16 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.update()
self.server_stopped.emit()
+ def cancel_server(self):
+ """
+ Cancel the server.
+ """
+ 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.
diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py
index 18372a47..5666400c 100644
--- a/onionshare_gui/settings_dialog.py
+++ b/onionshare_gui/settings_dialog.py
@@ -60,6 +60,11 @@ class SettingsDialog(QtWidgets.QDialog):
self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Checked)
self.systray_notifications_checkbox.setText(strings._("gui_settings_systray_notifications", True))
+ # Whether or not to use a shutdown timer
+ self.shutdown_timeout_checkbox = QtWidgets.QCheckBox()
+ self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
+ self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox", True))
+
# Whether or not to save the Onion private key for reuse
self.save_private_key_checkbox = QtWidgets.QCheckBox()
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked)
@@ -69,6 +74,7 @@ class SettingsDialog(QtWidgets.QDialog):
sharing_group_layout = QtWidgets.QVBoxLayout()
sharing_group_layout.addWidget(self.close_after_first_download_checkbox)
sharing_group_layout.addWidget(self.systray_notifications_checkbox)
+ sharing_group_layout.addWidget(self.shutdown_timeout_checkbox)
sharing_group_layout.addWidget(self.save_private_key_checkbox)
sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label", True))
sharing_group.setLayout(sharing_group_layout)
@@ -80,12 +86,14 @@ class SettingsDialog(QtWidgets.QDialog):
stealth_details.setWordWrap(True)
stealth_details.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
stealth_details.setOpenExternalLinks(True)
+ stealth_details.setMinimumSize(stealth_details.sizeHint())
self.stealth_checkbox = QtWidgets.QCheckBox()
self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.stealth_checkbox.setText(strings._("gui_settings_stealth_option", True))
hidservauth_details = QtWidgets.QLabel(strings._('gui_settings_stealth_hidservauth_string', True))
hidservauth_details.setWordWrap(True)
+ hidservauth_details.setMinimumSize(hidservauth_details.sizeHint())
hidservauth_details.hide()
self.hidservauth_copy_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True))
@@ -156,6 +164,26 @@ class SettingsDialog(QtWidgets.QDialog):
self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_obfs4_radio_option', True))
self.tor_bridges_use_obfs4_radio.toggled.connect(self.tor_bridges_use_obfs4_radio_toggled)
+ # meek_lite-amazon option radio
+ # if the obfs4proxy binary is missing, we can't use meek_lite-amazon transports
+ (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths()
+ if not os.path.isfile(self.obfs4proxy_file_path):
+ self.tor_bridges_use_meek_lite_amazon_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_amazon_radio_option_no_obfs4proxy', True))
+ self.tor_bridges_use_meek_lite_amazon_radio.setEnabled(False)
+ else:
+ self.tor_bridges_use_meek_lite_amazon_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_amazon_radio_option', True))
+ self.tor_bridges_use_meek_lite_amazon_radio.toggled.connect(self.tor_bridges_use_meek_lite_amazon_radio_toggled)
+
+ # meek_lite-azure option radio
+ # if the obfs4proxy binary is missing, we can't use meek_lite-azure transports
+ (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths()
+ if not os.path.isfile(self.obfs4proxy_file_path):
+ self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy', True))
+ self.tor_bridges_use_meek_lite_azure_radio.setEnabled(False)
+ else:
+ self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_azure_radio_option', True))
+ self.tor_bridges_use_meek_lite_azure_radio.toggled.connect(self.tor_bridges_use_meek_lite_azure_radio_toggled)
+
# Custom bridges radio and textbox
self.tor_bridges_use_custom_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_custom_radio_option', True))
self.tor_bridges_use_custom_radio.toggled.connect(self.tor_bridges_use_custom_radio_toggled)
@@ -179,6 +207,8 @@ class SettingsDialog(QtWidgets.QDialog):
bridges_layout = QtWidgets.QVBoxLayout()
bridges_layout.addWidget(self.tor_bridges_no_bridges_radio)
bridges_layout.addWidget(self.tor_bridges_use_obfs4_radio)
+ bridges_layout.addWidget(self.tor_bridges_use_meek_lite_amazon_radio)
+ bridges_layout.addWidget(self.tor_bridges_use_meek_lite_azure_radio)
bridges_layout.addWidget(self.tor_bridges_use_custom_radio)
bridges_layout.addWidget(self.tor_bridges_use_custom_textbox_options)
@@ -295,9 +325,12 @@ class SettingsDialog(QtWidgets.QDialog):
self.save_button.clicked.connect(self.save_clicked)
self.cancel_button = QtWidgets.QPushButton(strings._('gui_settings_button_cancel', True))
self.cancel_button.clicked.connect(self.cancel_clicked)
+ version_label = QtWidgets.QLabel('OnionShare {0:s}'.format(common.get_version()))
+ version_label.setStyleSheet('color: #666666')
self.help_button = QtWidgets.QPushButton(strings._('gui_settings_button_help', True))
self.help_button.clicked.connect(self.help_clicked)
buttons_layout = QtWidgets.QHBoxLayout()
+ buttons_layout.addWidget(version_label)
buttons_layout.addWidget(self.help_button)
buttons_layout.addStretch()
buttons_layout.addWidget(self.save_button)
@@ -349,6 +382,12 @@ class SettingsDialog(QtWidgets.QDialog):
else:
self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Unchecked)
+ shutdown_timeout = self.old_settings.get('shutdown_timeout')
+ if shutdown_timeout:
+ self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
+
save_private_key = self.old_settings.get('save_private_key')
if save_private_key:
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Checked)
@@ -401,10 +440,15 @@ class SettingsDialog(QtWidgets.QDialog):
if self.old_settings.get('no_bridges'):
self.tor_bridges_no_bridges_radio.setChecked(True)
self.tor_bridges_use_obfs4_radio.setChecked(False)
+ self.tor_bridges_use_meek_lite_amazon_radio.setChecked(False)
+ self.tor_bridges_use_meek_lite_azure_radio.setChecked(False)
self.tor_bridges_use_custom_radio.setChecked(False)
else:
self.tor_bridges_no_bridges_radio.setChecked(False)
self.tor_bridges_use_obfs4_radio.setChecked(self.old_settings.get('tor_bridges_use_obfs4'))
+ self.tor_bridges_use_meek_lite_amazon_radio.setChecked(self.old_settings.get('tor_bridges_use_meek_lite_amazon'))
+ self.tor_bridges_use_meek_lite_azure_radio.setChecked(self.old_settings.get('tor_bridges_use_meek_lite_azure'))
+
if self.old_settings.get('tor_bridges_use_custom_bridges'):
self.tor_bridges_use_custom_radio.setChecked(True)
# Remove the 'Bridge' lines at the start of each bridge.
@@ -441,6 +485,20 @@ class SettingsDialog(QtWidgets.QDialog):
if checked:
self.tor_bridges_use_custom_textbox_options.hide()
+ def tor_bridges_use_meek_lite_amazon_radio_toggled(self, checked):
+ """
+ meek_lite-amazon bridges option was toggled. If checked, disable custom bridge options.
+ """
+ if checked:
+ self.tor_bridges_use_custom_textbox_options.hide()
+
+ def tor_bridges_use_meek_lite_azure_radio_toggled(self, checked):
+ """
+ meek_lite_azure bridges option was toggled. If checked, disable custom bridge options.
+ """
+ if checked:
+ self.tor_bridges_use_custom_textbox_options.hide()
+
def tor_bridges_use_custom_radio_toggled(self, checked):
"""
Custom bridges option was toggled. If checked, show custom bridge options.
@@ -557,31 +615,43 @@ class SettingsDialog(QtWidgets.QDialog):
self._disable_buttons()
self.qtapp.processEvents()
+ def update_timestamp():
+ # Update the last checked label
+ settings = Settings(self.config)
+ settings.load()
+ autoupdate_timestamp = settings.get('autoupdate_timestamp')
+ self._update_autoupdate_timestamp(autoupdate_timestamp)
+
+ def close_forced_update_thread():
+ forced_update_thread.quit()
+ # Enable buttons
+ self._enable_buttons()
+ # Update timestamp
+ update_timestamp()
+
# Check for updates
def update_available(update_url, installed_version, latest_version):
Alert(strings._("update_available", True).format(update_url, installed_version, latest_version))
+ close_forced_update_thread()
+
def update_not_available():
Alert(strings._('update_not_available', True))
+ close_forced_update_thread()
- u = UpdateChecker(self.onion)
- u.update_available.connect(update_available)
- u.update_not_available.connect(update_not_available)
-
- try:
- u.check(force=True)
- except UpdateCheckerCheckError:
+ def update_error():
Alert(strings._('update_error_check_error', True), QtWidgets.QMessageBox.Warning)
- except UpdateCheckerInvalidLatestVersion as e:
+ close_forced_update_thread()
+
+ def update_invalid_version():
Alert(strings._('update_error_invalid_latest_version', True).format(e.latest_version), QtWidgets.QMessageBox.Warning)
+ close_forced_update_thread()
- # Enable buttons
- self._enable_buttons()
-
- # Update the last checked label
- settings = Settings(self.config)
- settings.load()
- autoupdate_timestamp = settings.get('autoupdate_timestamp')
- self._update_autoupdate_timestamp(autoupdate_timestamp)
+ forced_update_thread = UpdateThread(self.onion, self.config, force=True)
+ forced_update_thread.update_available.connect(update_available)
+ forced_update_thread.update_not_available.connect(update_not_available)
+ forced_update_thread.update_error.connect(update_error)
+ forced_update_thread.update_invalid_version.connect(update_invalid_version)
+ forced_update_thread.start()
def save_clicked(self):
"""
@@ -590,56 +660,58 @@ class SettingsDialog(QtWidgets.QDialog):
common.log('SettingsDialog', 'save_clicked')
settings = self.settings_from_fields()
- settings.save()
+ if settings:
+ settings.save()
- # If Tor isn't connected, or if Tor settings have changed, Reinitialize
- # the Onion object
- reboot_onion = False
- if self.onion.is_authenticated():
- common.log('SettingsDialog', 'save_clicked', 'Connected to Tor')
- def changed(s1, s2, keys):
- """
- Compare the Settings objects s1 and s2 and return true if any values
- have changed for the given keys.
- """
- for key in keys:
- if s1.get(key) != s2.get(key):
- return True
- return False
+ # If Tor isn't connected, or if Tor settings have changed, Reinitialize
+ # the Onion object
+ reboot_onion = False
+ if self.onion.is_authenticated():
+ common.log('SettingsDialog', 'save_clicked', 'Connected to Tor')
+ def changed(s1, s2, keys):
+ """
+ Compare the Settings objects s1 and s2 and return true if any values
+ have changed for the given keys.
+ """
+ for key in keys:
+ if s1.get(key) != s2.get(key):
+ return True
+ return False
- if changed(settings, self.old_settings, [
- 'connection_type', 'control_port_address',
- 'control_port_port', 'socks_address', 'socks_port',
- 'socket_file_path', 'auth_type', 'auth_password',
- 'no_bridges', 'tor_bridges_use_obfs4',
- 'tor_bridges_use_custom_bridges']):
+ if changed(settings, self.old_settings, [
+ 'connection_type', 'control_port_address',
+ 'control_port_port', 'socks_address', 'socks_port',
+ 'socket_file_path', 'auth_type', 'auth_password',
+ 'no_bridges', 'tor_bridges_use_obfs4',
+ 'tor_bridges_use_meek_lite_amazon', 'tor_bridges_use_meek_lite_azure',
+ 'tor_bridges_use_custom_bridges']):
+ reboot_onion = True
+
+ else:
+ common.log('SettingsDialog', 'save_clicked', 'Not connected to Tor')
+ # Tor isn't connected, so try connecting
reboot_onion = True
- else:
- common.log('SettingsDialog', 'save_clicked', 'Not connected to Tor')
- # Tor isn't connected, so try connecting
- reboot_onion = True
+ # Do we need to reinitialize Tor?
+ if reboot_onion:
+ # Reinitialize the Onion object
+ common.log('SettingsDialog', 'save_clicked', 'rebooting the Onion')
+ self.onion.cleanup()
- # Do we need to reinitialize Tor?
- if reboot_onion:
- # Reinitialize the Onion object
- common.log('SettingsDialog', 'save_clicked', 'rebooting the Onion')
- self.onion.cleanup()
+ tor_con = TorConnectionDialog(self.qtapp, settings, self.onion)
+ tor_con.start()
- tor_con = TorConnectionDialog(self.qtapp, settings, self.onion)
- tor_con.start()
+ common.log('SettingsDialog', 'save_clicked', 'Onion done rebooting, connected to Tor: {}'.format(self.onion.connected_to_tor))
- common.log('SettingsDialog', 'save_clicked', 'Onion done rebooting, connected to Tor: {}'.format(self.onion.connected_to_tor))
+ if self.onion.is_authenticated() and not tor_con.wasCanceled():
+ self.settings_saved.emit()
+ self.close()
- if self.onion.is_authenticated() and not tor_con.wasCanceled():
+ else:
self.settings_saved.emit()
self.close()
- else:
- self.settings_saved.emit()
- self.close()
-
def cancel_clicked(self):
"""
Cancel button clicked.
@@ -669,6 +741,7 @@ class SettingsDialog(QtWidgets.QDialog):
settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked())
settings.set('systray_notifications', self.systray_notifications_checkbox.isChecked())
+ settings.set('shutdown_timeout', self.shutdown_timeout_checkbox.isChecked())
if self.save_private_key_checkbox.isChecked():
settings.set('save_private_key', True)
settings.set('private_key', self.old_settings.get('private_key'))
@@ -717,14 +790,33 @@ class SettingsDialog(QtWidgets.QDialog):
if self.tor_bridges_no_bridges_radio.isChecked():
settings.set('no_bridges', True)
settings.set('tor_bridges_use_obfs4', False)
+ settings.set('tor_bridges_use_meek_lite_amazon', False)
+ settings.set('tor_bridges_use_meek_lite_azure', False)
settings.set('tor_bridges_use_custom_bridges', '')
if self.tor_bridges_use_obfs4_radio.isChecked():
settings.set('no_bridges', False)
settings.set('tor_bridges_use_obfs4', True)
+ settings.set('tor_bridges_use_meek_lite_amazon', False)
+ settings.set('tor_bridges_use_meek_lite_azure', False)
+ settings.set('tor_bridges_use_custom_bridges', '')
+ if self.tor_bridges_use_meek_lite_amazon_radio.isChecked():
+ settings.set('no_bridges', False)
+ settings.set('tor_bridges_use_obfs4', False)
+ settings.set('tor_bridges_use_meek_lite_amazon', True)
+ settings.set('tor_bridges_use_meek_lite_azure', False)
+ settings.set('tor_bridges_use_custom_bridges', '')
+ if self.tor_bridges_use_meek_lite_azure_radio.isChecked():
+ settings.set('no_bridges', False)
+ settings.set('tor_bridges_use_obfs4', False)
+ settings.set('tor_bridges_use_meek_lite_amazon', False)
+ settings.set('tor_bridges_use_meek_lite_azure', True)
settings.set('tor_bridges_use_custom_bridges', '')
if self.tor_bridges_use_custom_radio.isChecked():
settings.set('no_bridges', False)
settings.set('tor_bridges_use_obfs4', False)
+ settings.set('tor_bridges_use_meek_lite_amazon', False)
+ settings.set('tor_bridges_use_meek_lite_azure', False)
+
# Insert a 'Bridge' line at the start of each bridge.
# This makes it easier to copy/paste a set of bridges
# provided from https://bridges.torproject.org
@@ -734,8 +826,12 @@ class SettingsDialog(QtWidgets.QDialog):
for bridge in bridges:
if bridge != '':
# Check the syntax of the custom bridge to make sure it looks legitimate
- pattern = re.compile("[0-9.]+:[0-9]+\s[A-Z0-9]+$")
- if pattern.match(bridge):
+ ipv4_pattern = re.compile("(obfs4\s+)?(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):([0-9]+)(\s+)([A-Z0-9]+)(.+)$")
+ ipv6_pattern = re.compile("(obfs4\s+)?\[(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\]:[0-9]+\s+[A-Z0-9]+(.+)$")
+ meek_lite_pattern = re.compile("(meek_lite)(\s)+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+)(\s)+([0-9A-Z]+)(\s)+url=(.+)(\s)+front=(.+)")
+ if ipv4_pattern.match(bridge) or \
+ ipv6_pattern.match(bridge) or \
+ meek_lite_pattern.match(bridge):
new_bridges.append(''.join(['Bridge ', bridge, '\n']))
bridges_valid = True
if bridges_valid:
@@ -744,6 +840,7 @@ class SettingsDialog(QtWidgets.QDialog):
else:
Alert(strings._('gui_settings_tor_bridges_invalid', True))
settings.set('no_bridges', True)
+ return False
return settings
diff --git a/onionshare_gui/update_checker.py b/onionshare_gui/update_checker.py
index ca2eb48a..8b4884a2 100644
--- a/onionshare_gui/update_checker.py
+++ b/onionshare_gui/update_checker.py
@@ -19,6 +19,7 @@ along with this program. If not, see
{{ filesize_human }} (compressed)
-This zip file contains the following contents:
-Type | -Name | -Size | +Filename | +Size | +|
---|---|---|---|---|---|
{{ info.basename }} | +
+ |
{{ info.size_human }} | +|||
{{ info.basename }} | +
+ |
{{ info.size_human }} | +