summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiguel Jacq <mig@mig5.net>2018-10-11 13:04:48 +1100
committerMiguel Jacq <mig@mig5.net>2018-10-11 13:04:48 +1100
commit86b537a4a6376a4280f30912ab9b2e313083c058 (patch)
tree5d173265aeb7e2798ecb8f48ac2c1d23df9c3ef3
parentf45eae5768f75d2843f835a135708f1b521f0068 (diff)
parent93dd7a0effc8f5a450c84628ad2c640732d533ca (diff)
downloadonionshare-86b537a4a6376a4280f30912ab9b2e313083c058.tar.gz
onionshare-86b537a4a6376a4280f30912ab9b2e313083c058.zip
Fix conflicts
-rw-r--r--.github/CODEOWNERS8
-rw-r--r--.travis.yml16
-rw-r--r--BUILD.md30
-rw-r--r--MANIFEST.in2
-rwxr-xr-xdev_scripts/run_all_tests.sh14
-rwxr-xr-xinstall/build_rpm.sh2
-rw-r--r--install/check_lacked_trans.py2
-rw-r--r--install/get-tor-osx.py6
-rw-r--r--install/get-tor-windows.py6
-rw-r--r--install/requirements-tests.txt11
-rw-r--r--onionshare/__init__.py55
-rw-r--r--onionshare/common.py40
-rw-r--r--onionshare/onion.py3
-rw-r--r--onionshare/web.py846
-rw-r--r--onionshare/web/__init__.py21
-rw-r--r--onionshare/web/receive_mode.py325
-rw-r--r--onionshare/web/share_mode.py384
-rw-r--r--onionshare/web/web.py252
-rw-r--r--onionshare_gui/__init__.py10
-rw-r--r--onionshare_gui/mode/__init__.py (renamed from onionshare_gui/mode.py)79
-rw-r--r--onionshare_gui/mode/history.py548
-rw-r--r--onionshare_gui/mode/receive_mode/__init__.py196
-rw-r--r--onionshare_gui/mode/share_mode/__init__.py (renamed from onionshare_gui/share_mode/__init__.py)224
-rw-r--r--onionshare_gui/mode/share_mode/file_selection.py (renamed from onionshare_gui/share_mode/file_selection.py)5
-rw-r--r--onionshare_gui/mode/share_mode/threads.py63
-rw-r--r--onionshare_gui/onion_thread.py45
-rw-r--r--onionshare_gui/onionshare_gui.py20
-rw-r--r--onionshare_gui/receive_mode/__init__.py237
-rw-r--r--onionshare_gui/receive_mode/uploads.py310
-rw-r--r--onionshare_gui/server_status.py34
-rw-r--r--onionshare_gui/settings_dialog.py10
-rw-r--r--onionshare_gui/share_mode/downloads.py157
-rw-r--r--onionshare_gui/threads.py77
-rw-r--r--screenshots/server.pngbin45923 -> 0 bytes
-rw-r--r--setup.py9
-rw-r--r--share/images/download_window_gray.pngbin440 -> 0 bytes
-rw-r--r--share/images/download_window_green.pngbin761 -> 0 bytes
-rw-r--r--share/images/downloads.pngbin0 -> 2120 bytes
-rw-r--r--share/images/downloads_toggle.pngbin0 -> 380 bytes
-rw-r--r--share/images/downloads_toggle_selected.pngbin0 -> 468 bytes
-rw-r--r--share/images/downloads_transparent.pngbin0 -> 2138 bytes
-rw-r--r--share/images/upload_window_gray.pngbin298 -> 0 bytes
-rw-r--r--share/images/upload_window_green.pngbin483 -> 0 bytes
-rw-r--r--share/images/uploads.pngbin0 -> 2076 bytes
-rw-r--r--share/images/uploads_toggle.pngbin0 -> 389 bytes
-rw-r--r--share/images/uploads_toggle_selected.pngbin0 -> 473 bytes
-rw-r--r--share/images/uploads_transparent.pngbin0 -> 2096 bytes
-rw-r--r--share/locale/cs.json2
-rw-r--r--share/locale/da.json4
-rw-r--r--share/locale/de.json1
-rw-r--r--share/locale/en.json175
-rw-r--r--share/locale/eo.json2
-rw-r--r--share/locale/es.json1
-rw-r--r--share/locale/fi.json2
-rw-r--r--share/locale/fr.json1
-rw-r--r--share/locale/it.json2
-rw-r--r--share/locale/nl.json4
-rw-r--r--share/locale/tr.json2
-rw-r--r--share/templates/send.html24
-rw-r--r--stdeb.cfg4
-rw-r--r--tests/__init__.py (renamed from test/__init__.py)0
-rw-r--r--tests/conftest.py (renamed from test/conftest.py)4
-rw-r--r--tests/test_helpers.py (renamed from test/test_helpers.py)0
-rw-r--r--tests/test_onionshare.py (renamed from test/test_onionshare.py)0
-rw-r--r--tests/test_onionshare_common.py (renamed from test/test_onionshare_common.py)0
-rw-r--r--tests/test_onionshare_settings.py (renamed from test/test_onionshare_settings.py)0
-rw-r--r--tests/test_onionshare_strings.py (renamed from test/test_onionshare_strings.py)12
-rw-r--r--tests/test_onionshare_web.py (renamed from test/test_onionshare_web.py)32
-rw-r--r--tests_gui_local/__init__.py1
-rw-r--r--tests_gui_local/commontests.py291
-rw-r--r--tests_gui_local/conftest.py160
-rw-r--r--tests_gui_local/onionshare_receive_mode_upload_test.py190
-rw-r--r--tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py193
-rw-r--r--tests_gui_local/onionshare_share_mode_download_test.py199
-rw-r--r--tests_gui_local/onionshare_share_mode_download_test_public_mode.py199
-rw-r--r--tests_gui_local/onionshare_share_mode_download_test_stay_open.py211
-rw-r--r--tests_gui_local/onionshare_slug_persistent_test.py175
-rw-r--r--tests_gui_local/onionshare_timer_test.py136
-rwxr-xr-xtests_gui_local/run_unit_tests.sh5
-rw-r--r--tests_gui_tor/__init__.py0
-rw-r--r--tests_gui_tor/commontests.py61
-rw-r--r--tests_gui_tor/conftest.py160
-rw-r--r--tests_gui_tor/onionshare_790_cancel_on_second_share_test.py197
-rw-r--r--tests_gui_tor/onionshare_receive_mode_upload_test.py190
-rw-r--r--tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py190
-rw-r--r--tests_gui_tor/onionshare_share_mode_cancel_share_test.py149
-rw-r--r--tests_gui_tor/onionshare_share_mode_download_test.py193
-rw-r--r--tests_gui_tor/onionshare_share_mode_download_test_public_mode.py201
-rw-r--r--tests_gui_tor/onionshare_share_mode_download_test_stay_open.py213
-rw-r--r--tests_gui_tor/onionshare_share_mode_persistent_test.py185
-rw-r--r--tests_gui_tor/onionshare_share_mode_stealth_test.py180
-rw-r--r--tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py185
-rw-r--r--tests_gui_tor/onionshare_timer_test.py148
-rw-r--r--tests_gui_tor/onionshare_tor_connection_killed_test.py185
-rwxr-xr-xtests_gui_tor/run_unit_tests.sh5
95 files changed, 6524 insertions, 1997 deletions
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 1cd5a1f6..42d1840f 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1,9 @@
* @micahflee
+
+# localization
+/share/locale/ @emmapeel2
+
+# tests
+/tests/ @mig5
+/tests_gui_local/ @mig5
+/tests_gui_tor/ @mig5
diff --git a/.travis.yml b/.travis.yml
index afbaa887..e0b5b822 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,6 @@
language: python
-# sudo: required
-dist: bionic
+dist: trusty
+sudo: required
python:
- "3.6"
- "3.6-dev"
@@ -8,14 +8,16 @@ python:
- "nightly"
# command to install dependencies
install:
+ - sudo apt-get update && sudo apt-get install python3-pyqt5
- pip install -r install/requirements.txt
- - pip install pytest-cov coveralls flake8
+ - pip install -r install/requirements-tests.txt
+ - pip install pytest-cov flake8
before_script:
# stop the build if there are Python syntax errors or undefined names
- flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
# 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:
- - coveralls
+# run CLI tests and local GUI tests
+script:
+ - pytest --cov=onionshare tests/
+ - cd tests_gui_local/ && xvfb-run ./run_unit_tests.sh
diff --git a/BUILD.md b/BUILD.md
index 51f5cadd..00d24cd2 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -11,11 +11,11 @@ cd onionshare
Install the needed dependencies:
-For Debian-like distros: `apt install -y build-essential fakeroot python3-all python3-stdeb dh-python python3-flask python3-stem python3-pyqt5 python-nautilus python3-pytest tor obfs4proxy python3-cryptography python3-crypto python3-nacl python3-pip python3-socks python3-sha3`
+For Debian-like distros: `apt install -y python3-flask python3-stem python3-pyqt5 python3-cryptography python3-crypto python3-nacl python3-socks python-nautilus tor obfs4proxy python3-pytest build-essential fakeroot python3-all python3-stdeb dh-python`
On some older versions of Debian you may need to install pysha3 with `pip3 install pysha3` if python3-sha3 is not available.
-For Fedora-like distros: `dnf install -y rpm-build python3-flask python3-stem python3-qt5 python3-pytest nautilus-python tor obfs4 python3-pynacl python3-cryptography python3-crypto python3-pip python3-pysocks`
+For Fedora-like distros: `dnf install -y python3-flask python3-stem python3-qt5 python3-pynacl python3-cryptography python3-crypto python3-pysocks nautilus-python tor obfs4 python3-pytest rpm-build`
After that you can try both the CLI and the GUI version of OnionShare:
@@ -137,8 +137,30 @@ This will prompt you to codesign three binaries and execute one unsigned binary.
## Tests
-OnionShare includes PyTest unit tests. To run the tests:
+OnionShare includes PyTest unit tests. To run the tests, first install some dependencies:
```sh
-pytest test/
+pip3 install -r install/requirements-tests.txt
```
+
+If you'd like to run the CLI-based tests that Travis runs:
+
+```sh
+pytest tests/
+```
+
+If you would like to run the GUI unit tests in 'local only mode':
+
+```sh
+cd tests_gui_local/
+./run_unit_tests.sh
+```
+
+If you would like to run the GUI unit tests in 'tor' (bundled) mode:
+
+```sh
+cd tests_gui_tor/
+./run_unit_tests.sh
+```
+
+Keep in mind that the Tor tests take a lot longer to run than local mode, but they are also more comprehensive.
diff --git a/MANIFEST.in b/MANIFEST.in
index c8a4d87c..71af3740 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -10,4 +10,4 @@ include install/onionshare.desktop
include install/onionshare.appdata.xml
include install/onionshare80.xpm
include install/scripts/onionshare-nautilus.py
-include test/*.py
+include tests/*.py
diff --git a/dev_scripts/run_all_tests.sh b/dev_scripts/run_all_tests.sh
new file mode 100755
index 00000000..90ef1dc0
--- /dev/null
+++ b/dev_scripts/run_all_tests.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+ROOT="$( dirname $(cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd ))"
+
+# CLI tests
+cd $ROOT
+pytest tests/
+
+# Local GUI tests
+cd $ROOT/tests_gui_local
+./run_unit_tests.sh
+
+# Tor GUI tests
+cd $ROOT/tests_gui_tor
+./run_unit_tests.sh
diff --git a/install/build_rpm.sh b/install/build_rpm.sh
index c103262c..3f7a68ac 100755
--- a/install/build_rpm.sh
+++ b/install/build_rpm.sh
@@ -9,7 +9,7 @@ VERSION=`cat share/version.txt`
rm -r build dist >/dev/null 2>&1
# build binary package
-python3 setup.py bdist_rpm --requires="python3-flask, python3-stem, python3-qt5, nautilus-python, tor, obfs4"
+python3 setup.py bdist_rpm --requires="python3-flask, python3-stem, python3-qt5, python3-pynacl, python3-cryptography, python3-crypto, python3-pysocks, nautilus-python, tor, obfs4"
# install it
echo ""
diff --git a/install/check_lacked_trans.py b/install/check_lacked_trans.py
index 027edab1..1caa6b27 100644
--- a/install/check_lacked_trans.py
+++ b/install/check_lacked_trans.py
@@ -59,7 +59,7 @@ def main():
files_in(dir, 'onionshare_gui/share_mode') + \
files_in(dir, 'onionshare_gui/receive_mode') + \
files_in(dir, 'install/scripts') + \
- files_in(dir, 'test')
+ files_in(dir, 'tests')
pysrc = [p for p in src if p.endswith('.py')]
lang_code = args.lang_code
diff --git a/install/get-tor-osx.py b/install/get-tor-osx.py
index 1d2c6f56..ae20fd74 100644
--- a/install/get-tor-osx.py
+++ b/install/get-tor-osx.py
@@ -35,9 +35,9 @@ import subprocess
import requests
def main():
- dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0/TorBrowser-8.0-osx64_en-US.dmg'
- dmg_filename = 'TorBrowser-8.0-osx64_en-US.dmg'
- expected_dmg_sha256 = '15603ae7b3a1942863c98acc92f509e4409db48fe22c9acae6b15c9cb9bf3088'
+ dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0.1/TorBrowser-8.0.1-osx64_en-US.dmg'
+ dmg_filename = 'TorBrowser-8.0.1-osx64_en-US.dmg'
+ expected_dmg_sha256 = 'fb1be2a0f850a65bae38747c3abbf9061742c5d7799e1693405078aaf38d2b08'
# Build paths
root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))
diff --git a/install/get-tor-windows.py b/install/get-tor-windows.py
index 0d16dd29..e5a24be9 100644
--- a/install/get-tor-windows.py
+++ b/install/get-tor-windows.py
@@ -33,9 +33,9 @@ import subprocess
import requests
def main():
- exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0/torbrowser-install-8.0_en-US.exe'
- exe_filename = 'torbrowser-install-8.0_en-US.exe'
- expected_exe_sha256 = '0682b44eff5877dfc2fe2fdd5b46e678d47adad86d564e7cb6654c5f60eb1ed2'
+ exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0.1/torbrowser-install-8.0.1_en-US.exe'
+ exe_filename = 'torbrowser-install-8.0.1_en-US.exe'
+ expected_exe_sha256 = 'bdf81d4282b991a6425c213c7b03b3f5c1f17bb02986b7fe9a1891e577e51639'
# Build paths
root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))
working_path = os.path.join(os.path.join(root_path, 'build'), 'tor')
diff --git a/install/requirements-tests.txt b/install/requirements-tests.txt
new file mode 100644
index 00000000..0d9c1581
--- /dev/null
+++ b/install/requirements-tests.txt
@@ -0,0 +1,11 @@
+atomicwrites==1.2.1
+attrs==18.2.0
+more-itertools==4.3.0
+pluggy==0.6.0
+py==1.6.0
+pytest==3.4.2
+pytest-faulthandler==1.5.0
+pytest-ordering==0.5
+pytest-qt==3.1.0
+six==1.11.0
+urllib3==1.23
diff --git a/onionshare/__init__.py b/onionshare/__init__.py
index 51210b6b..715c5571 100644
--- a/onionshare/__init__.py
+++ b/onionshare/__init__.py
@@ -65,13 +65,18 @@ def main(cwd=None):
receive = bool(args.receive)
config = args.config
+ if receive:
+ mode = 'receive'
+ else:
+ mode = 'share'
+
# Make sure filenames given if not using receiver mode
- if not receive and len(filenames) == 0:
- print(strings._('no_filenames'))
+ if mode == 'share' and len(filenames) == 0:
+ parser.print_help()
sys.exit()
# Validate filenames
- if not receive:
+ if mode == 'share':
valid = True
for filename in filenames:
if not os.path.isfile(filename) and not os.path.isdir(filename):
@@ -90,7 +95,7 @@ def main(cwd=None):
common.debug = debug
# Create the Web object
- web = Web(common, False, receive)
+ web = Web(common, False, mode)
# Start the Onion object
onion = Onion(common)
@@ -116,20 +121,21 @@ def main(cwd=None):
print(e.args[0])
sys.exit()
- # Prepare files to share
- print(strings._("preparing_files"))
- try:
- web.set_file_info(filenames)
- app.cleanup_filenames.append(web.zip_filename)
- except OSError as e:
- print(e.strerror)
- sys.exit(1)
-
- # Warn about sending large files over Tor
- if web.zip_filesize >= 157286400: # 150mb
- print('')
- print(strings._("large_filesize"))
- print('')
+ if mode == 'share':
+ # Prepare files to share
+ print(strings._("preparing_files"))
+ try:
+ web.share_mode.set_file_info(filenames)
+ app.cleanup_filenames += web.share_mode.cleanup_filenames
+ except OSError as e:
+ print(e.strerror)
+ sys.exit(1)
+
+ # Warn about sending large files over Tor
+ if web.share_mode.download_filesize >= 157286400: # 150mb
+ print('')
+ print(strings._("large_filesize"))
+ print('')
# Start OnionShare http service in new thread
t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), common.settings.get('slug')))
@@ -157,7 +163,7 @@ def main(cwd=None):
url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug)
print('')
- if receive:
+ if mode == 'receive':
print(strings._('receive_mode_downloads_dir').format(common.settings.get('downloads_dir')))
print('')
print(strings._('receive_mode_warning'))
@@ -186,11 +192,12 @@ def main(cwd=None):
if app.shutdown_timeout > 0:
# if the shutdown timer was set and has run out, stop the server
if not app.shutdown_timer.is_alive():
- # If there were no attempts to download the share, or all downloads are done, we can stop
- if web.download_count == 0 or web.done:
- print(strings._("close_on_timeout"))
- web.stop(app.port)
- break
+ if mode == 'share':
+ # If there were no attempts to download the share, or all downloads are done, we can stop
+ if web.share_mode.download_count == 0 or web.done:
+ print(strings._("close_on_timeout"))
+ web.stop(app.port)
+ break
# Allow KeyboardInterrupt exception to be handled with threads
# https://stackoverflow.com/questions/3788208/python-threading-ignores-keyboardinterrupt-exception
time.sleep(0.2)
diff --git a/onionshare/common.py b/onionshare/common.py
index 0ce411e8..cab1e747 100644
--- a/onionshare/common.py
+++ b/onionshare/common.py
@@ -211,6 +211,7 @@ class Common(object):
color: #000000;
padding: 10px;
border: 1px solid #666666;
+ font-size: 12px;
}
""",
@@ -248,11 +249,46 @@ class Common(object):
border-radius: 5px;
}""",
+ 'downloads_uploads_empty': """
+ QWidget {
+ background-color: #ffffff;
+ border: 1px solid #999999;
+ }
+ QWidget QLabel {
+ background-color: none;
+ border: 0px;
+ }
+ """,
+
+ 'downloads_uploads_empty_text': """
+ QLabel {
+ color: #999999;
+ }""",
+
'downloads_uploads_label': """
QLabel {
font-weight: bold;
font-size 14px;
text-align: center;
+ background-color: none;
+ border: none;
+ }""",
+
+ 'downloads_uploads_clear': """
+ QPushButton {
+ color: #3f7fcf;
+ }
+ """,
+
+ 'download_uploads_indicator': """
+ QLabel {
+ color: #ffffff;
+ background-color: #f44449;
+ font-weight: bold;
+ font-size: 10px;
+ padding: 2px;
+ border-radius: 7px;
+ text-align: center;
}""",
'downloads_uploads_progress_bar': """
@@ -261,7 +297,7 @@ class Common(object):
background-color: #ffffff !important;
text-align: center;
color: #9b9b9b;
- font-size: 12px;
+ font-size: 14px;
}
QProgressBar::chunk {
background-color: #4e064f;
@@ -433,7 +469,7 @@ class Common(object):
tmpsock.bind(("127.0.0.1", random.randint(min_port, max_port)))
break
except OSError as e:
- raise OSError(e)
+ pass
_, port = tmpsock.getsockname()
return port
diff --git a/onionshare/onion.py b/onionshare/onion.py
index 7a111eff..c45ae72e 100644
--- a/onionshare/onion.py
+++ b/onionshare/onion.py
@@ -402,6 +402,8 @@ class Onion(object):
# ephemeral stealth onion services are not supported
self.supports_stealth = False
+ # Does this version of Tor support next-gen ('v3') onions?
+ self.supports_next_gen_onions = self.tor_version > Version('0.3.3.1')
def is_authenticated(self):
"""
@@ -427,7 +429,6 @@ class Onion(object):
raise TorTooOld(strings._('error_stealth_not_supported'))
print(strings._("config_onion_service").format(int(port)))
- print(strings._('using_ephemeral'))
if self.stealth:
if self.settings.get('hidservauth_string'):
diff --git a/onionshare/web.py b/onionshare/web.py
deleted file mode 100644
index 10c130cb..00000000
--- a/onionshare/web.py
+++ /dev/null
@@ -1,846 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-OnionShare | https://onionshare.org/
-
-Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""
-
-import hmac
-import logging
-import mimetypes
-import os
-import queue
-import socket
-import sys
-import tempfile
-import zipfile
-import re
-import io
-from distutils.version import LooseVersion as Version
-from urllib.request import urlopen
-from datetime import datetime
-
-import flask
-from flask import (
- Flask, Response, Request, request, render_template, abort, make_response,
- flash, redirect, __version__ as flask_version
-)
-from werkzeug.utils import secure_filename
-
-from . import strings
-from .common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable
-
-
-# Stub out flask's show_server_banner function, to avoiding showing warnings that
-# are not applicable to OnionShare
-def stubbed_show_server_banner(env, debug, app_import_path, eager_loading):
- pass
-
-flask.cli.show_server_banner = stubbed_show_server_banner
-
-
-class Web(object):
- """
- The Web object is the OnionShare web server, powered by flask
- """
- REQUEST_LOAD = 0
- REQUEST_STARTED = 1
- REQUEST_PROGRESS = 2
- REQUEST_OTHER = 3
- REQUEST_CANCELED = 4
- REQUEST_RATE_LIMIT = 5
- REQUEST_CLOSE_SERVER = 6
- REQUEST_UPLOAD_FILE_RENAMED = 7
- REQUEST_UPLOAD_FINISHED = 8
- REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE = 9
- REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE = 10
-
- def __init__(self, common, gui_mode, receive_mode=False):
- self.common = common
-
- # The flask app
- self.app = Flask(__name__,
- static_folder=self.common.get_resource_path('static'),
- template_folder=self.common.get_resource_path('templates'))
- self.app.secret_key = self.common.random_string(8)
-
- # Debug mode?
- if self.common.debug:
- self.debug_mode()
-
- # Are we running in GUI mode?
- self.gui_mode = gui_mode
-
- # Are we using receive mode?
- self.receive_mode = receive_mode
- if self.receive_mode:
- # Use custom WSGI middleware, to modify environ
- self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self)
- # Use a custom Request class to track upload progess
- self.app.request_class = ReceiveModeRequest
-
- # Starting in Flask 0.11, render_template_string autoescapes template variables
- # by default. To prevent content injection through template variables in
- # earlier versions of Flask, we force autoescaping in the Jinja2 template
- # engine if we detect a Flask version with insecure default behavior.
- if Version(flask_version) < Version('0.11'):
- # Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc
- Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape
-
- # Information about the file
- self.file_info = []
- self.zip_filename = None
- self.zip_filesize = None
-
- self.security_headers = [
- ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'),
- ('X-Frame-Options', 'DENY'),
- ('X-Xss-Protection', '1; mode=block'),
- ('X-Content-Type-Options', 'nosniff'),
- ('Referrer-Policy', 'no-referrer'),
- ('Server', 'OnionShare')
- ]
-
- self.q = queue.Queue()
-
- self.slug = None
-
- self.download_count = 0
- self.upload_count = 0
-
- self.error404_count = 0
-
- # If "Stop After First Download" is checked (stay_open == False), only allow
- # one download at a time.
- self.download_in_progress = False
-
- self.done = False
-
- # If the client closes the OnionShare window while a download is in progress,
- # it should immediately stop serving the file. The client_cancel global is
- # used to tell the download function that the client is canceling the download.
- self.client_cancel = False
-
- # shutting down the server only works within the context of flask, so the easiest way to do it is over http
- self.shutdown_slug = self.common.random_string(16)
-
- # Keep track if the server is running
- self.running = False
-
- # Define the ewb app routes
- self.common_routes()
- if self.receive_mode:
- self.receive_routes()
- else:
- self.send_routes()
-
- def send_routes(self):
- """
- The web app routes for sharing files
- """
- @self.app.route("/<slug_candidate>")
- def index(slug_candidate):
- self.check_slug_candidate(slug_candidate)
- return index_logic()
-
- @self.app.route("/")
- def index_public():
- if not self.common.settings.get('public_mode'):
- return self.error404()
- return index_logic()
-
- def index_logic(slug_candidate=''):
- """
- Render the template for the onionshare landing page.
- """
- self.add_request(Web.REQUEST_LOAD, request.path)
-
- # Deny new downloads if "Stop After First Download" is checked and there is
- # currently a download
- deny_download = not self.stay_open and self.download_in_progress
- if deny_download:
- r = make_response(render_template('denied.html'))
- return self.add_security_headers(r)
-
- # If download is allowed to continue, serve download page
- if self.slug:
- r = make_response(render_template(
- 'send.html',
- slug=self.slug,
- file_info=self.file_info,
- filename=os.path.basename(self.zip_filename),
- filesize=self.zip_filesize,
- filesize_human=self.common.human_readable_filesize(self.zip_filesize)))
- else:
- # If download is allowed to continue, serve download page
- r = make_response(render_template(
- 'send.html',
- file_info=self.file_info,
- filename=os.path.basename(self.zip_filename),
- filesize=self.zip_filesize,
- filesize_human=self.common.human_readable_filesize(self.zip_filesize)))
- return self.add_security_headers(r)
-
- @self.app.route("/<slug_candidate>/download")
- def download(slug_candidate):
- self.check_slug_candidate(slug_candidate)
- return download_logic()
-
- @self.app.route("/download")
- def download_public():
- if not self.common.settings.get('public_mode'):
- return self.error404()
- return download_logic()
-
- def download_logic(slug_candidate=''):
- """
- Download the zip file.
- """
- # Deny new downloads if "Stop After First Download" is checked and there is
- # currently a download
- deny_download = not self.stay_open and self.download_in_progress
- if deny_download:
- r = make_response(render_template('denied.html'))
- return self.add_security_headers(r)
-
- # Each download has a unique id
- download_id = self.download_count
- self.download_count += 1
-
- # Prepare some variables to use inside generate() function below
- # which is outside of the request context
- shutdown_func = request.environ.get('werkzeug.server.shutdown')
- path = request.path
-
- # Tell GUI the download started
- self.add_request(Web.REQUEST_STARTED, path, {
- 'id': download_id}
- )
-
- dirname = os.path.dirname(self.zip_filename)
- basename = os.path.basename(self.zip_filename)
-
- def generate():
- # The user hasn't canceled the download
- self.client_cancel = False
-
- # Starting a new download
- if not self.stay_open:
- self.download_in_progress = True
-
- chunk_size = 102400 # 100kb
-
- fp = open(self.zip_filename, 'rb')
- self.done = False
- canceled = False
- while not self.done:
- # The user has canceled the download, so stop serving the file
- if self.client_cancel:
- self.add_request(Web.REQUEST_CANCELED, path, {
- 'id': download_id
- })
- break
-
- chunk = fp.read(chunk_size)
- if chunk == b'':
- self.done = True
- else:
- try:
- yield chunk
-
- # tell GUI the progress
- downloaded_bytes = fp.tell()
- percent = (1.0 * downloaded_bytes / self.zip_filesize) * 100
-
- # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304)
- if not self.gui_mode or self.common.platform == 'Linux' or self.common.platform == 'BSD':
- sys.stdout.write(
- "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent))
- sys.stdout.flush()
-
- self.add_request(Web.REQUEST_PROGRESS, path, {
- 'id': download_id,
- 'bytes': downloaded_bytes
- })
- self.done = False
- except:
- # looks like the download was canceled
- self.done = True
- canceled = True
-
- # tell the GUI the download has canceled
- self.add_request(Web.REQUEST_CANCELED, path, {
- 'id': download_id
- })
-
- fp.close()
-
- if self.common.platform != 'Darwin':
- sys.stdout.write("\n")
-
- # Download is finished
- if not self.stay_open:
- self.download_in_progress = False
-
- # Close the server, if necessary
- if not self.stay_open and not canceled:
- print(strings._("closing_automatically"))
- self.running = False
- try:
- if shutdown_func is None:
- raise RuntimeError('Not running with the Werkzeug Server')
- shutdown_func()
- except:
- pass
-
- r = Response(generate())
- r.headers.set('Content-Length', self.zip_filesize)
- r.headers.set('Content-Disposition', 'attachment', filename=basename)
- r = self.add_security_headers(r)
- # guess content type
- (content_type, _) = mimetypes.guess_type(basename, strict=False)
- if content_type is not None:
- r.headers.set('Content-Type', content_type)
- return r
-
- def receive_routes(self):
- """
- The web app routes for receiving files
- """
- def index_logic():
- self.add_request(Web.REQUEST_LOAD, request.path)
-
- if self.common.settings.get('public_mode'):
- upload_action = '/upload'
- close_action = '/close'
- else:
- upload_action = '/{}/upload'.format(self.slug)
- close_action = '/{}/close'.format(self.slug)
-
- r = make_response(render_template(
- 'receive.html',
- upload_action=upload_action,
- close_action=close_action,
- receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown')))
- return self.add_security_headers(r)
-
- @self.app.route("/<slug_candidate>")
- def index(slug_candidate):
- self.check_slug_candidate(slug_candidate)
- return index_logic()
-
- @self.app.route("/")
- def index_public():
- if not self.common.settings.get('public_mode'):
- return self.error404()
- return index_logic()
-
-
- def upload_logic(slug_candidate=''):
- """
- Upload files.
- """
- # Make sure downloads_dir exists
- valid = True
- try:
- self.common.validate_downloads_dir()
- except DownloadsDirErrorCannotCreate:
- self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path)
- print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir')))
- valid = False
- except DownloadsDirErrorNotWritable:
- self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path)
- print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir')))
- valid = False
- if not valid:
- flash('Error uploading, please inform the OnionShare user', 'error')
- if self.common.settings.get('public_mode'):
- return redirect('/')
- else:
- return redirect('/{}'.format(slug_candidate))
-
- files = request.files.getlist('file[]')
- filenames = []
- print('')
- for f in files:
- if f.filename != '':
- # Automatically rename the file, if a file of the same name already exists
- filename = secure_filename(f.filename)
- filenames.append(filename)
- local_path = os.path.join(self.common.settings.get('downloads_dir'), filename)
- if os.path.exists(local_path):
- if '.' in filename:
- # Add "-i", e.g. change "foo.txt" to "foo-2.txt"
- parts = filename.split('.')
- name = parts[:-1]
- ext = parts[-1]
-
- i = 2
- valid = False
- while not valid:
- new_filename = '{}-{}.{}'.format('.'.join(name), i, ext)
- local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename)
- if os.path.exists(local_path):
- i += 1
- else:
- valid = True
- else:
- # If no extension, just add "-i", e.g. change "foo" to "foo-2"
- i = 2
- valid = False
- while not valid:
- new_filename = '{}-{}'.format(filename, i)
- local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename)
- if os.path.exists(local_path):
- i += 1
- else:
- valid = True
-
- basename = os.path.basename(local_path)
- if f.filename != basename:
- # Tell the GUI that the file has changed names
- self.add_request(Web.REQUEST_UPLOAD_FILE_RENAMED, request.path, {
- 'id': request.upload_id,
- 'old_filename': f.filename,
- 'new_filename': basename
- })
-
- self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path))
- print(strings._('receive_mode_received_file').format(local_path))
- f.save(local_path)
-
- # Note that flash strings are on English, and not translated, on purpose,
- # to avoid leaking the locale of the OnionShare user
- if len(filenames) == 0:
- flash('No files uploaded', 'info')
- else:
- for filename in filenames:
- flash('Sent {}'.format(filename), 'info')
-
- if self.common.settings.get('public_mode'):
- return redirect('/')
- else:
- return redirect('/{}'.format(slug_candidate))
-
- @self.app.route("/<slug_candidate>/upload", methods=['POST'])
- def upload(slug_candidate):
- self.check_slug_candidate(slug_candidate)
- return upload_logic(slug_candidate)
-
- @self.app.route("/upload", methods=['POST'])
- def upload_public():
- if not self.common.settings.get('public_mode'):
- return self.error404()
- return upload_logic()
-
-
- def close_logic(slug_candidate=''):
- if self.common.settings.get('receive_allow_receiver_shutdown'):
- self.force_shutdown()
- r = make_response(render_template('closed.html'))
- self.add_request(Web.REQUEST_CLOSE_SERVER, request.path)
- return self.add_security_headers(r)
- else:
- return redirect('/{}'.format(slug_candidate))
-
- @self.app.route("/<slug_candidate>/close", methods=['POST'])
- def close(slug_candidate):
- self.check_slug_candidate(slug_candidate)
- return close_logic(slug_candidate)
-
- @self.app.route("/close", methods=['POST'])
- def close_public():
- if not self.common.settings.get('public_mode'):
- return self.error404()
- return close_logic()
-
- def common_routes(self):
- """
- Common web app routes between sending and receiving
- """
- @self.app.errorhandler(404)
- def page_not_found(e):
- """
- 404 error page.
- """
- return self.error404()
-
- @self.app.route("/<slug_candidate>/shutdown")
- def shutdown(slug_candidate):
- """
- Stop the flask web server, from the context of an http request.
- """
- self.check_shutdown_slug_candidate(slug_candidate)
- self.force_shutdown()
- return ""
-
- def error404(self):
- self.add_request(Web.REQUEST_OTHER, request.path)
- if request.path != '/favicon.ico':
- self.error404_count += 1
-
- # In receive mode, with public mode enabled, skip rate limiting 404s
- if not self.common.settings.get('public_mode'):
- if self.error404_count == 20:
- self.add_request(Web.REQUEST_RATE_LIMIT, request.path)
- self.force_shutdown()
- print(strings._('error_rate_limit'))
-
- r = make_response(render_template('404.html'), 404)
- return self.add_security_headers(r)
-
- def add_security_headers(self, r):
- """
- Add security headers to a request
- """
- for header, value in self.security_headers:
- r.headers.set(header, value)
- return r
-
- def set_file_info(self, filenames, processed_size_callback=None):
- """
- Using the list of filenames being shared, fill in details that the web
- page will need to display. This includes zipping up the file in order to
- get the zip file's name and size.
- """
- # build file info list
- self.file_info = {'files': [], 'dirs': []}
- for filename in filenames:
- info = {
- 'filename': filename,
- 'basename': os.path.basename(filename.rstrip('/'))
- }
- if os.path.isfile(filename):
- info['size'] = os.path.getsize(filename)
- info['size_human'] = self.common.human_readable_filesize(info['size'])
- self.file_info['files'].append(info)
- if os.path.isdir(filename):
- info['size'] = self.common.dir_size(filename)
- info['size_human'] = self.common.human_readable_filesize(info['size'])
- self.file_info['dirs'].append(info)
- self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename'])
- self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename'])
-
- # zip up the files and folders
- z = ZipWriter(self.common, processed_size_callback=processed_size_callback)
- for info in self.file_info['files']:
- z.add_file(info['filename'])
- for info in self.file_info['dirs']:
- z.add_dir(info['filename'])
- z.close()
- self.zip_filename = z.zip_filename
- self.zip_filesize = os.path.getsize(self.zip_filename)
-
- def _safe_select_jinja_autoescape(self, filename):
- if filename is None:
- return True
- return filename.endswith(('.html', '.htm', '.xml', '.xhtml'))
-
- def add_request(self, request_type, path, data=None):
- """
- Add a request to the queue, to communicate with the GUI.
- """
- self.q.put({
- 'type': request_type,
- 'path': path,
- 'data': data
- })
-
- def generate_slug(self, persistent_slug=None):
- self.common.log('Web', 'generate_slug', 'persistent_slug={}'.format(persistent_slug))
- if persistent_slug != None and persistent_slug != '':
- self.slug = persistent_slug
- self.common.log('Web', 'generate_slug', 'persistent_slug sent, so slug is: "{}"'.format(self.slug))
- else:
- self.slug = self.common.build_slug()
- self.common.log('Web', 'generate_slug', 'built random slug: "{}"'.format(self.slug))
-
- def debug_mode(self):
- """
- Turn on debugging mode, which will log flask errors to a debug file.
- """
- temp_dir = tempfile.gettempdir()
- log_handler = logging.FileHandler(
- os.path.join(temp_dir, 'onionshare_server.log'))
- log_handler.setLevel(logging.WARNING)
- self.app.logger.addHandler(log_handler)
-
- def check_slug_candidate(self, slug_candidate):
- self.common.log('Web', 'check_slug_candidate: slug_candidate={}'.format(slug_candidate))
- if self.common.settings.get('public_mode'):
- abort(404)
- if not hmac.compare_digest(self.slug, slug_candidate):
- abort(404)
-
- def check_shutdown_slug_candidate(self, slug_candidate):
- self.common.log('Web', 'check_shutdown_slug_candidate: slug_candidate={}'.format(slug_candidate))
- if not hmac.compare_digest(self.shutdown_slug, slug_candidate):
- abort(404)
-
- def force_shutdown(self):
- """
- Stop the flask web server, from the context of the flask app.
- """
- # Shutdown the flask service
- try:
- func = request.environ.get('werkzeug.server.shutdown')
- if func is None:
- raise RuntimeError('Not running with the Werkzeug Server')
- func()
- except:
- pass
- self.running = False
-
- def start(self, port, stay_open=False, public_mode=False, persistent_slug=None):
- """
- Start the flask web server.
- """
- self.common.log('Web', 'start', 'port={}, stay_open={}, persistent_slug={}'.format(port, stay_open, persistent_slug))
- if not public_mode:
- self.generate_slug(persistent_slug)
-
- self.stay_open = stay_open
-
- # In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220)
- if os.path.exists('/usr/share/anon-ws-base-files/workstation'):
- host = '0.0.0.0'
- else:
- host = '127.0.0.1'
-
- self.running = True
- self.app.run(host=host, port=port, threaded=True)
-
- def stop(self, port):
- """
- Stop the flask web server by loading /shutdown.
- """
-
- # If the user cancels the download, let the download function know to stop
- # serving the file
- self.client_cancel = True
-
- # To stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
- if self.running:
- try:
- s = socket.socket()
- s.connect(('127.0.0.1', port))
- s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(self.shutdown_slug))
- except:
- try:
- urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, self.shutdown_slug)).read()
- except:
- pass
-
-
-class ZipWriter(object):
- """
- ZipWriter accepts files and directories and compresses them into a zip file
- with. If a zip_filename is not passed in, it will use the default onionshare
- filename.
- """
- def __init__(self, common, zip_filename=None, processed_size_callback=None):
- self.common = common
-
- if zip_filename:
- self.zip_filename = zip_filename
- else:
- self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), self.common.random_string(4, 6))
-
- self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True)
- self.processed_size_callback = processed_size_callback
- if self.processed_size_callback is None:
- self.processed_size_callback = lambda _: None
- self._size = 0
- self.processed_size_callback(self._size)
-
- def add_file(self, filename):
- """
- Add a file to the zip archive.
- """
- self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED)
- self._size += os.path.getsize(filename)
- self.processed_size_callback(self._size)
-
- def add_dir(self, filename):
- """
- Add a directory, and all of its children, to the zip archive.
- """
- dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/'
- for dirpath, dirnames, filenames in os.walk(filename):
- for f in filenames:
- full_filename = os.path.join(dirpath, f)
- if not os.path.islink(full_filename):
- arc_filename = full_filename[len(dir_to_strip):]
- self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
- self._size += os.path.getsize(full_filename)
- self.processed_size_callback(self._size)
-
- def close(self):
- """
- Close the zip archive.
- """
- self.z.close()
-
-
-class ReceiveModeWSGIMiddleware(object):
- """
- Custom WSGI middleware in order to attach the Web object to environ, so
- ReceiveModeRequest can access it.
- """
- def __init__(self, app, web):
- self.app = app
- self.web = web
-
- def __call__(self, environ, start_response):
- environ['web'] = self.web
- return self.app(environ, start_response)
-
-
-class ReceiveModeTemporaryFile(object):
- """
- A custom TemporaryFile that tells ReceiveModeRequest every time data gets
- written to it, in order to track the progress of uploads.
- """
- def __init__(self, filename, write_func, close_func):
- self.onionshare_filename = filename
- self.onionshare_write_func = write_func
- self.onionshare_close_func = close_func
-
- # Create a temporary file
- self.f = tempfile.TemporaryFile('wb+')
-
- # Make all the file-like methods and attributes actually access the
- # TemporaryFile, except for write
- attrs = ['closed', 'detach', 'fileno', 'flush', 'isatty', 'mode',
- 'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto',
- 'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell',
- 'truncate', 'writable', 'writelines']
- for attr in attrs:
- setattr(self, attr, getattr(self.f, attr))
-
- def write(self, b):
- """
- Custom write method that calls out to onionshare_write_func
- """
- bytes_written = self.f.write(b)
- self.onionshare_write_func(self.onionshare_filename, bytes_written)
-
- def close(self):
- """
- Custom close method that calls out to onionshare_close_func
- """
- self.f.close()
- self.onionshare_close_func(self.onionshare_filename)
-
-
-class ReceiveModeRequest(Request):
- """
- A custom flask Request object that keeps track of how much data has been
- uploaded for each file, for receive mode.
- """
- def __init__(self, environ, populate_request=True, shallow=False):
- super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow)
- self.web = environ['web']
-
- # Is this a valid upload request?
- self.upload_request = False
- if self.method == 'POST':
- if self.path == '/{}/upload'.format(self.web.slug):
- self.upload_request = True
- else:
- if self.web.common.settings.get('public_mode'):
- if self.path == '/upload':
- self.upload_request = True
-
- if self.upload_request:
- # A dictionary that maps filenames to the bytes uploaded so far
- self.progress = {}
-
- # Create an upload_id, attach it to the request
- self.upload_id = self.web.upload_count
- self.web.upload_count += 1
-
- # Figure out the content length
- try:
- self.content_length = int(self.headers['Content-Length'])
- except:
- self.content_length = 0
-
- print("{}: {}".format(
- datetime.now().strftime("%b %d, %I:%M%p"),
- strings._("receive_mode_upload_starting").format(self.web.common.human_readable_filesize(self.content_length))
- ))
-
- # Tell the GUI
- self.web.add_request(Web.REQUEST_STARTED, self.path, {
- 'id': self.upload_id,
- 'content_length': self.content_length
- })
-
- self.previous_file = None
-
- def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None):
- """
- This gets called for each file that gets uploaded, and returns an file-like
- writable stream.
- """
- if self.upload_request:
- self.progress[filename] = {
- 'uploaded_bytes': 0,
- 'complete': False
- }
-
- return ReceiveModeTemporaryFile(filename, self.file_write_func, self.file_close_func)
-
- def close(self):
- """
- Closing the request.
- """
- super(ReceiveModeRequest, self).close()
- if self.upload_request:
- # Inform the GUI that the upload has finished
- self.web.add_request(Web.REQUEST_UPLOAD_FINISHED, self.path, {
- 'id': self.upload_id
- })
-
- def file_write_func(self, filename, length):
- """
- This function gets called when a specific file is written to.
- """
- if self.upload_request:
- self.progress[filename]['uploaded_bytes'] += length
-
- if self.previous_file != filename:
- if self.previous_file is not None:
- print('')
- self.previous_file = filename
-
- print('\r=> {:15s} {}'.format(
- self.web.common.human_readable_filesize(self.progress[filename]['uploaded_bytes']),
- filename
- ), end='')
-
- # Update the GUI on the upload progress
- self.web.add_request(Web.REQUEST_PROGRESS, self.path, {
- 'id': self.upload_id,
- 'progress': self.progress
- })
-
- def file_close_func(self, filename):
- """
- This function gets called when a specific file is closed.
- """
- self.progress[filename]['complete'] = True
diff --git a/onionshare/web/__init__.py b/onionshare/web/__init__.py
new file mode 100644
index 00000000..d45b4983
--- /dev/null
+++ b/onionshare/web/__init__.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from .web import Web
diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py
new file mode 100644
index 00000000..4a6934a1
--- /dev/null
+++ b/onionshare/web/receive_mode.py
@@ -0,0 +1,325 @@
+import os
+import tempfile
+from datetime import datetime
+from flask import Request, request, render_template, make_response, flash, redirect
+from werkzeug.utils import secure_filename
+
+from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable
+from .. import strings
+
+
+class ReceiveModeWeb(object):
+ """
+ All of the web logic for receive mode
+ """
+ def __init__(self, common, web):
+ self.common = common
+ self.common.log('ReceiveModeWeb', '__init__')
+
+ self.web = web
+
+ self.upload_count = 0
+
+ self.define_routes()
+
+ def define_routes(self):
+ """
+ The web app routes for receiving files
+ """
+ def index_logic():
+ self.web.add_request(self.web.REQUEST_LOAD, request.path)
+
+ if self.common.settings.get('public_mode'):
+ upload_action = '/upload'
+ close_action = '/close'
+ else:
+ upload_action = '/{}/upload'.format(self.web.slug)
+ close_action = '/{}/close'.format(self.web.slug)
+
+ r = make_response(render_template(
+ 'receive.html',
+ upload_action=upload_action,
+ close_action=close_action,
+ receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown')))
+ return self.web.add_security_headers(r)
+
+ @self.web.app.route("/<slug_candidate>")
+ def index(slug_candidate):
+ self.web.check_slug_candidate(slug_candidate)
+ return index_logic()
+
+ @self.web.app.route("/")
+ def index_public():
+ if not self.common.settings.get('public_mode'):
+ return self.web.error404()
+ return index_logic()
+
+
+ def upload_logic(slug_candidate=''):
+ """
+ Upload files.
+ """
+ # Make sure downloads_dir exists
+ valid = True
+ try:
+ self.common.validate_downloads_dir()
+ except DownloadsDirErrorCannotCreate:
+ self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path)
+ print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir')))
+ valid = False
+ except DownloadsDirErrorNotWritable:
+ self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path)
+ print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir')))
+ valid = False
+ if not valid:
+ flash('Error uploading, please inform the OnionShare user', 'error')
+ if self.common.settings.get('public_mode'):
+ return redirect('/')
+ else:
+ return redirect('/{}'.format(slug_candidate))
+
+ files = request.files.getlist('file[]')
+ filenames = []
+ print('')
+ for f in files:
+ if f.filename != '':
+ # Automatically rename the file, if a file of the same name already exists
+ filename = secure_filename(f.filename)
+ filenames.append(filename)
+ local_path = os.path.join(self.common.settings.get('downloads_dir'), filename)
+ if os.path.exists(local_path):
+ if '.' in filename:
+ # Add "-i", e.g. change "foo.txt" to "foo-2.txt"
+ parts = filename.split('.')
+ name = parts[:-1]
+ ext = parts[-1]
+
+ i = 2
+ valid = False
+ while not valid:
+ new_filename = '{}-{}.{}'.format('.'.join(name), i, ext)
+ local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename)
+ if os.path.exists(local_path):
+ i += 1
+ else:
+ valid = True
+ else:
+ # If no extension, just add "-i", e.g. change "foo" to "foo-2"
+ i = 2
+ valid = False
+ while not valid:
+ new_filename = '{}-{}'.format(filename, i)
+ local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename)
+ if os.path.exists(local_path):
+ i += 1
+ else:
+ valid = True
+
+ basename = os.path.basename(local_path)
+ if f.filename != basename:
+ # Tell the GUI that the file has changed names
+ self.web.add_request(self.web.REQUEST_UPLOAD_FILE_RENAMED, request.path, {
+ 'id': request.upload_id,
+ 'old_filename': f.filename,
+ 'new_filename': basename
+ })
+
+ self.common.log('ReceiveModeWeb', 'define_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path))
+ print(strings._('receive_mode_received_file').format(local_path))
+ f.save(local_path)
+
+ # Note that flash strings are on English, and not translated, on purpose,
+ # to avoid leaking the locale of the OnionShare user
+ if len(filenames) == 0:
+ flash('No files uploaded', 'info')
+ else:
+ for filename in filenames:
+ flash('Sent {}'.format(filename), 'info')
+
+ if self.common.settings.get('public_mode'):
+ return redirect('/')
+ else:
+ return redirect('/{}'.format(slug_candidate))
+
+ @self.web.app.route("/<slug_candidate>/upload", methods=['POST'])
+ def upload(slug_candidate):
+ self.web.check_slug_candidate(slug_candidate)
+ return upload_logic(slug_candidate)
+
+ @self.web.app.route("/upload", methods=['POST'])
+ def upload_public():
+ if not self.common.settings.get('public_mode'):
+ return self.web.error404()
+ return upload_logic()
+
+
+ def close_logic(slug_candidate=''):
+ if self.common.settings.get('receive_allow_receiver_shutdown'):
+ self.web.force_shutdown()
+ r = make_response(render_template('closed.html'))
+ self.web.add_request(self.web.REQUEST_CLOSE_SERVER, request.path)
+ return self.web.add_security_headers(r)
+ else:
+ return redirect('/{}'.format(slug_candidate))
+
+ @self.web.app.route("/<slug_candidate>/close", methods=['POST'])
+ def close(slug_candidate):
+ self.web.check_slug_candidate(slug_candidate)
+ return close_logic(slug_candidate)
+
+ @self.web.app.route("/close", methods=['POST'])
+ def close_public():
+ if not self.common.settings.get('public_mode'):
+ return self.web.error404()
+ return close_logic()
+
+
+class ReceiveModeWSGIMiddleware(object):
+ """
+ Custom WSGI middleware in order to attach the Web object to environ, so
+ ReceiveModeRequest can access it.
+ """
+ def __init__(self, app, web):
+ self.app = app
+ self.web = web
+
+ def __call__(self, environ, start_response):
+ environ['web'] = self.web
+ return self.app(environ, start_response)
+
+
+class ReceiveModeTemporaryFile(object):
+ """
+ A custom TemporaryFile that tells ReceiveModeRequest every time data gets
+ written to it, in order to track the progress of uploads.
+ """
+ def __init__(self, filename, write_func, close_func):
+ self.onionshare_filename = filename
+ self.onionshare_write_func = write_func
+ self.onionshare_close_func = close_func
+
+ # Create a temporary file
+ self.f = tempfile.TemporaryFile('wb+')
+
+ # Make all the file-like methods and attributes actually access the
+ # TemporaryFile, except for write
+ attrs = ['closed', 'detach', 'fileno', 'flush', 'isatty', 'mode',
+ 'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto',
+ 'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell',
+ 'truncate', 'writable', 'writelines']
+ for attr in attrs:
+ setattr(self, attr, getattr(self.f, attr))
+
+ def write(self, b):
+ """
+ Custom write method that calls out to onionshare_write_func
+ """
+ bytes_written = self.f.write(b)
+ self.onionshare_write_func(self.onionshare_filename, bytes_written)
+
+ def close(self):
+ """
+ Custom close method that calls out to onionshare_close_func
+ """
+ self.f.close()
+ self.onionshare_close_func(self.onionshare_filename)
+
+
+class ReceiveModeRequest(Request):
+ """
+ A custom flask Request object that keeps track of how much data has been
+ uploaded for each file, for receive mode.
+ """
+ def __init__(self, environ, populate_request=True, shallow=False):
+ super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow)
+ self.web = environ['web']
+
+ # Is this a valid upload request?
+ self.upload_request = False
+ if self.method == 'POST':
+ if self.path == '/{}/upload'.format(self.web.slug):
+ self.upload_request = True
+ else:
+ if self.web.common.settings.get('public_mode'):
+ if self.path == '/upload':
+ self.upload_request = True
+
+ if self.upload_request:
+ # A dictionary that maps filenames to the bytes uploaded so far
+ self.progress = {}
+
+ # Create an upload_id, attach it to the request
+ self.upload_id = self.web.receive_mode.upload_count
+ self.web.receive_mode.upload_count += 1
+
+ # Figure out the content length
+ try:
+ self.content_length = int(self.headers['Content-Length'])
+ except:
+ self.content_length = 0
+
+ print("{}: {}".format(
+ datetime.now().strftime("%b %d, %I:%M%p"),
+ strings._("receive_mode_upload_starting").format(self.web.common.human_readable_filesize(self.content_length))
+ ))
+
+ # Tell the GUI
+ self.web.add_request(self.web.REQUEST_STARTED, self.path, {
+ 'id': self.upload_id,
+ 'content_length': self.content_length
+ })
+
+ self.previous_file = None
+
+ def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None):
+ """
+ This gets called for each file that gets uploaded, and returns an file-like
+ writable stream.
+ """
+ if self.upload_request:
+ self.progress[filename] = {
+ 'uploaded_bytes': 0,
+ 'complete': False
+ }
+
+ return ReceiveModeTemporaryFile(filename, self.file_write_func, self.file_close_func)
+
+ def close(self):
+ """
+ Closing the request.
+ """
+ super(ReceiveModeRequest, self).close()
+ if self.upload_request:
+ # Inform the GUI that the upload has finished
+ self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, {
+ 'id': self.upload_id
+ })
+
+ def file_write_func(self, filename, length):
+ """
+ This function gets called when a specific file is written to.
+ """
+ if self.upload_request:
+ self.progress[filename]['uploaded_bytes'] += length
+
+ if self.previous_file != filename:
+ if self.previous_file is not None:
+ print('')
+ self.previous_file = filename
+
+ print('\r=> {:15s} {}'.format(
+ self.web.common.human_readable_filesize(self.progress[filename]['uploaded_bytes']),
+ filename
+ ), end='')
+
+ # Update the GUI on the upload progress
+ self.web.add_request(self.web.REQUEST_PROGRESS, self.path, {
+ 'id': self.upload_id,
+ 'progress': self.progress
+ })
+
+ def file_close_func(self, filename):
+ """
+ This function gets called when a specific file is closed.
+ """
+ self.progress[filename]['complete'] = True
diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py
new file mode 100644
index 00000000..a57d0a39
--- /dev/null
+++ b/onionshare/web/share_mode.py
@@ -0,0 +1,384 @@
+import os
+import sys
+import tempfile
+import zipfile
+import mimetypes
+import gzip
+from flask import Response, request, render_template, make_response
+
+from .. import strings
+
+
+class ShareModeWeb(object):
+ """
+ All of the web logic for share mode
+ """
+ def __init__(self, common, web):
+ self.common = common
+ self.common.log('ShareModeWeb', '__init__')
+
+ self.web = web
+
+ # Information about the file to be shared
+ self.file_info = []
+ self.is_zipped = False
+ self.download_filename = None
+ self.download_filesize = None
+ self.gzip_filename = None
+ self.gzip_filesize = None
+ self.zip_writer = None
+
+ self.download_count = 0
+
+ # If "Stop After First Download" is checked (stay_open == False), only allow
+ # one download at a time.
+ self.download_in_progress = False
+
+ # If the client closes the OnionShare window while a download is in progress,
+ # it should immediately stop serving the file. The client_cancel global is
+ # used to tell the download function that the client is canceling the download.
+ self.client_cancel = False
+
+ self.define_routes()
+
+ def define_routes(self):
+ """
+ The web app routes for sharing files
+ """
+ @self.web.app.route("/<slug_candidate>")
+ def index(slug_candidate):
+ self.web.check_slug_candidate(slug_candidate)
+ return index_logic()
+
+ @self.web.app.route("/")
+ def index_public():
+ if not self.common.settings.get('public_mode'):
+ return self.web.error404()
+ return index_logic()
+
+ def index_logic(slug_candidate=''):
+ """
+ Render the template for the onionshare landing page.
+ """
+ self.web.add_request(self.web.REQUEST_LOAD, request.path)
+
+ # Deny new downloads if "Stop After First Download" is checked and there is
+ # currently a download
+ deny_download = not self.web.stay_open and self.download_in_progress
+ if deny_download:
+ r = make_response(render_template('denied.html'))
+ return self.web.add_security_headers(r)
+
+ # If download is allowed to continue, serve download page
+ if self.should_use_gzip():
+ self.filesize = self.gzip_filesize
+ else:
+ self.filesize = self.download_filesize
+
+ if self.web.slug:
+ r = make_response(render_template(
+ 'send.html',
+ slug=self.web.slug,
+ file_info=self.file_info,
+ filename=os.path.basename(self.download_filename),
+ filesize=self.filesize,
+ filesize_human=self.common.human_readable_filesize(self.download_filesize),
+ is_zipped=self.is_zipped))
+ else:
+ # If download is allowed to continue, serve download page
+ r = make_response(render_template(
+ 'send.html',
+ file_info=self.file_info,
+ filename=os.path.basename(self.download_filename),
+ filesize=self.filesize,
+ filesize_human=self.common.human_readable_filesize(self.download_filesize),
+ is_zipped=self.is_zipped))
+ return self.web.add_security_headers(r)
+
+ @self.web.app.route("/<slug_candidate>/download")
+ def download(slug_candidate):
+ self.web.check_slug_candidate(slug_candidate)
+ return download_logic()
+
+ @self.web.app.route("/download")
+ def download_public():
+ if not self.common.settings.get('public_mode'):
+ return self.web.error404()
+ return download_logic()
+
+ def download_logic(slug_candidate=''):
+ """
+ Download the zip file.
+ """
+ # Deny new downloads if "Stop After First Download" is checked and there is
+ # currently a download
+ deny_download = not self.web.stay_open and self.download_in_progress
+ if deny_download:
+ r = make_response(render_template('denied.html'))
+ return self.web.add_security_headers(r)
+
+ # Each download has a unique id
+ download_id = self.download_count
+ self.download_count += 1
+
+ # Prepare some variables to use inside generate() function below
+ # which is outside of the request context
+ shutdown_func = request.environ.get('werkzeug.server.shutdown')
+ path = request.path
+
+ # If this is a zipped file, then serve as-is. If it's not zipped, then,
+ # if the http client supports gzip compression, gzip the file first
+ # and serve that
+ use_gzip = self.should_use_gzip()
+ if use_gzip:
+ file_to_download = self.gzip_filename
+ self.filesize = self.gzip_filesize
+ else:
+ file_to_download = self.download_filename
+ self.filesize = self.download_filesize
+
+ # Tell GUI the download started
+ self.web.add_request(self.web.REQUEST_STARTED, path, {
+ 'id': download_id,
+ 'use_gzip': use_gzip
+ })
+
+ basename = os.path.basename(self.download_filename)
+
+ def generate():
+ # The user hasn't canceled the download
+ self.client_cancel = False
+
+ # Starting a new download
+ if not self.web.stay_open:
+ self.download_in_progress = True
+
+ chunk_size = 102400 # 100kb
+
+ fp = open(file_to_download, 'rb')
+ self.web.done = False
+ canceled = False
+ while not self.web.done:
+ # The user has canceled the download, so stop serving the file
+ if self.client_cancel:
+ self.web.add_request(self.web.REQUEST_CANCELED, path, {
+ 'id': download_id
+ })
+ break
+
+ chunk = fp.read(chunk_size)
+ if chunk == b'':
+ self.web.done = True
+ else:
+ try:
+ yield chunk
+
+ # tell GUI the progress
+ downloaded_bytes = fp.tell()
+ percent = (1.0 * downloaded_bytes / self.filesize) * 100
+
+ # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304)
+ if not self.web.is_gui or self.common.platform == 'Linux' or self.common.platform == 'BSD':
+ sys.stdout.write(
+ "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent))
+ sys.stdout.flush()
+
+ self.web.add_request(self.web.REQUEST_PROGRESS, path, {
+ 'id': download_id,
+ 'bytes': downloaded_bytes
+ })
+ self.web.done = False
+ except:
+ # looks like the download was canceled
+ self.web.done = True
+ canceled = True
+
+ # tell the GUI the download has canceled
+ self.web.add_request(self.web.REQUEST_CANCELED, path, {
+ 'id': download_id
+ })
+
+ fp.close()
+
+ if self.common.platform != 'Darwin':
+ sys.stdout.write("\n")
+
+ # Download is finished
+ if not self.web.stay_open:
+ self.download_in_progress = False
+
+ # Close the server, if necessary
+ if not self.web.stay_open and not canceled:
+ print(strings._("closing_automatically"))
+ self.web.running = False
+ try:
+ if shutdown_func is None:
+ raise RuntimeError('Not running with the Werkzeug Server')
+ shutdown_func()
+ except:
+ pass
+
+ r = Response(generate())
+ if use_gzip:
+ r.headers.set('Content-Encoding', 'gzip')
+ r.headers.set('Content-Length', self.filesize)
+ r.headers.set('Content-Disposition', 'attachment', filename=basename)
+ r = self.web.add_security_headers(r)
+ # guess content type
+ (content_type, _) = mimetypes.guess_type(basename, strict=False)
+ if content_type is not None:
+ r.headers.set('Content-Type', content_type)
+ return r
+
+ def set_file_info(self, filenames, processed_size_callback=None):
+ """
+ Using the list of filenames being shared, fill in details that the web
+ page will need to display. This includes zipping up the file in order to
+ get the zip file's name and size.
+ """
+ self.common.log("ShareModeWeb", "set_file_info")
+ self.web.cancel_compression = False
+
+ self.cleanup_filenames = []
+
+ # build file info list
+ self.file_info = {'files': [], 'dirs': []}
+ for filename in filenames:
+ info = {
+ 'filename': filename,
+ 'basename': os.path.basename(filename.rstrip('/'))
+ }
+ if os.path.isfile(filename):
+ info['size'] = os.path.getsize(filename)
+ info['size_human'] = self.common.human_readable_filesize(info['size'])
+ self.file_info['files'].append(info)
+ if os.path.isdir(filename):
+ info['size'] = self.common.dir_size(filename)
+ info['size_human'] = self.common.human_readable_filesize(info['size'])
+ self.file_info['dirs'].append(info)
+ self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename'])
+ self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename'])
+
+ # Check if there's only 1 file and no folders
+ if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0:
+ self.download_filename = self.file_info['files'][0]['filename']
+ self.download_filesize = self.file_info['files'][0]['size']
+
+ # Compress the file with gzip now, so we don't have to do it on each request
+ self.gzip_filename = tempfile.mkstemp('wb+')[1]
+ self._gzip_compress(self.download_filename, self.gzip_filename, 6, processed_size_callback)
+ self.gzip_filesize = os.path.getsize(self.gzip_filename)
+
+ # Make sure the gzip file gets cleaned up when onionshare stops
+ self.cleanup_filenames.append(self.gzip_filename)
+
+ self.is_zipped = False
+
+ else:
+ # Zip up the files and folders
+ self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback)
+ self.download_filename = self.zip_writer.zip_filename
+ for info in self.file_info['files']:
+ self.zip_writer.add_file(info['filename'])
+ # Canceling early?
+ if self.web.cancel_compression:
+ self.zip_writer.close()
+ return False
+
+ for info in self.file_info['dirs']:
+ if not self.zip_writer.add_dir(info['filename']):
+ return False
+
+ self.zip_writer.close()
+ self.download_filesize = os.path.getsize(self.download_filename)
+
+ # Make sure the zip file gets cleaned up when onionshare stops
+ self.cleanup_filenames.append(self.zip_writer.zip_filename)
+
+ self.is_zipped = True
+
+ return True
+
+ def should_use_gzip(self):
+ """
+ Should we use gzip for this browser?
+ """
+ return (not self.is_zipped) and ('gzip' in request.headers.get('Accept-Encoding', '').lower())
+
+ def _gzip_compress(self, input_filename, output_filename, level, processed_size_callback=None):
+ """
+ Compress a file with gzip, without loading the whole thing into memory
+ Thanks: https://stackoverflow.com/questions/27035296/python-how-to-gzip-a-large-text-file-without-memoryerror
+ """
+ bytes_processed = 0
+ blocksize = 1 << 16 # 64kB
+ with open(input_filename, 'rb') as input_file:
+ output_file = gzip.open(output_filename, 'wb', level)
+ while True:
+ if processed_size_callback is not None:
+ processed_size_callback(bytes_processed)
+
+ block = input_file.read(blocksize)
+ if len(block) == 0:
+ break
+ output_file.write(block)
+ bytes_processed += blocksize
+
+ output_file.close()
+
+
+class ZipWriter(object):
+ """
+ ZipWriter accepts files and directories and compresses them into a zip file
+ with. If a zip_filename is not passed in, it will use the default onionshare
+ filename.
+ """
+ def __init__(self, common, zip_filename=None, processed_size_callback=None):
+ self.common = common
+ self.cancel_compression = False
+
+ if zip_filename:
+ self.zip_filename = zip_filename
+ else:
+ self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), self.common.random_string(4, 6))
+
+ self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True)
+ self.processed_size_callback = processed_size_callback
+ if self.processed_size_callback is None:
+ self.processed_size_callback = lambda _: None
+ self._size = 0
+ self.processed_size_callback(self._size)
+
+ def add_file(self, filename):
+ """
+ Add a file to the zip archive.
+ """
+ self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED)
+ self._size += os.path.getsize(filename)
+ self.processed_size_callback(self._size)
+
+ def add_dir(self, filename):
+ """
+ Add a directory, and all of its children, to the zip archive.
+ """
+ dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/'
+ for dirpath, dirnames, filenames in os.walk(filename):
+ for f in filenames:
+ # Canceling early?
+ if self.cancel_compression:
+ return False
+
+ full_filename = os.path.join(dirpath, f)
+ if not os.path.islink(full_filename):
+ arc_filename = full_filename[len(dir_to_strip):]
+ self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
+ self._size += os.path.getsize(full_filename)
+ self.processed_size_callback(self._size)
+
+ return True
+
+ def close(self):
+ """
+ Close the zip archive.
+ """
+ self.z.close()
diff --git a/onionshare/web/web.py b/onionshare/web/web.py
new file mode 100644
index 00000000..52c4da16
--- /dev/null
+++ b/onionshare/web/web.py
@@ -0,0 +1,252 @@
+import hmac
+import logging
+import os
+import queue
+import socket
+import sys
+import tempfile
+from distutils.version import LooseVersion as Version
+from urllib.request import urlopen
+
+import flask
+from flask import Flask, request, render_template, abort, make_response, __version__ as flask_version
+
+from .. import strings
+
+from .share_mode import ShareModeWeb
+from .receive_mode import ReceiveModeWeb, ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest
+
+
+# Stub out flask's show_server_banner function, to avoiding showing warnings that
+# are not applicable to OnionShare
+def stubbed_show_server_banner(env, debug, app_import_path, eager_loading):
+ pass
+
+flask.cli.show_server_banner = stubbed_show_server_banner
+
+
+class Web(object):
+ """
+ The Web object is the OnionShare web server, powered by flask
+ """
+ REQUEST_LOAD = 0
+ REQUEST_STARTED = 1
+ REQUEST_PROGRESS = 2
+ REQUEST_OTHER = 3
+ REQUEST_CANCELED = 4
+ REQUEST_RATE_LIMIT = 5
+ REQUEST_CLOSE_SERVER = 6
+ REQUEST_UPLOAD_FILE_RENAMED = 7
+ REQUEST_UPLOAD_FINISHED = 8
+ REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE = 9
+ REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE = 10
+
+ def __init__(self, common, is_gui, mode='share'):
+ self.common = common
+ self.common.log('Web', '__init__', 'is_gui={}, mode={}'.format(is_gui, mode))
+
+ # The flask app
+ self.app = Flask(__name__,
+ static_folder=self.common.get_resource_path('static'),
+ template_folder=self.common.get_resource_path('templates'))
+ self.app.secret_key = self.common.random_string(8)
+
+ # Debug mode?
+ if self.common.debug:
+ self.debug_mode()
+
+ # Are we running in GUI mode?
+ self.is_gui = is_gui
+
+ # Are we using receive mode?
+ self.mode = mode
+ if self.mode == 'receive':
+ # Use custom WSGI middleware, to modify environ
+ self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self)
+ # Use a custom Request class to track upload progess
+ self.app.request_class = ReceiveModeRequest
+
+ # Starting in Flask 0.11, render_template_string autoescapes template variables
+ # by default. To prevent content injection through template variables in
+ # earlier versions of Flask, we force autoescaping in the Jinja2 template
+ # engine if we detect a Flask version with insecure default behavior.
+ if Version(flask_version) < Version('0.11'):
+ # Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc
+ Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape
+
+ self.security_headers = [
+ ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'),
+ ('X-Frame-Options', 'DENY'),
+ ('X-Xss-Protection', '1; mode=block'),
+ ('X-Content-Type-Options', 'nosniff'),
+ ('Referrer-Policy', 'no-referrer'),
+ ('Server', 'OnionShare')
+ ]
+
+ self.q = queue.Queue()
+ self.slug = None
+ self.error404_count = 0
+
+ self.done = False
+
+ # shutting down the server only works within the context of flask, so the easiest way to do it is over http
+ self.shutdown_slug = self.common.random_string(16)
+
+ # Keep track if the server is running
+ self.running = False
+
+ # Define the web app routes
+ self.define_common_routes()
+
+ # Create the mode web object, which defines its own routes
+ self.share_mode = None
+ self.receive_mode = None
+ if self.mode == 'receive':
+ self.receive_mode = ReceiveModeWeb(self.common, self)
+ elif self.mode == 'share':
+ self.share_mode = ShareModeWeb(self.common, self)
+
+
+ def define_common_routes(self):
+ """
+ Common web app routes between sending and receiving
+ """
+ @self.app.errorhandler(404)
+ def page_not_found(e):
+ """
+ 404 error page.
+ """
+ return self.error404()
+
+ @self.app.route("/<slug_candidate>/shutdown")
+ def shutdown(slug_candidate):
+ """
+ Stop the flask web server, from the context of an http request.
+ """
+ self.check_shutdown_slug_candidate(slug_candidate)
+ self.force_shutdown()
+ return ""
+
+ def error404(self):
+ self.add_request(Web.REQUEST_OTHER, request.path)
+ if request.path != '/favicon.ico':
+ self.error404_count += 1
+
+ # In receive mode, with public mode enabled, skip rate limiting 404s
+ if not self.common.settings.get('public_mode'):
+ if self.error404_count == 20:
+ self.add_request(Web.REQUEST_RATE_LIMIT, request.path)
+ self.force_shutdown()
+ print(strings._('error_rate_limit'))
+
+ r = make_response(render_template('404.html'), 404)
+ return self.add_security_headers(r)
+
+ def add_security_headers(self, r):
+ """
+ Add security headers to a request
+ """
+ for header, value in self.security_headers:
+ r.headers.set(header, value)
+ return r
+
+ def _safe_select_jinja_autoescape(self, filename):
+ if filename is None:
+ return True
+ return filename.endswith(('.html', '.htm', '.xml', '.xhtml'))
+
+ def add_request(self, request_type, path, data=None):
+ """
+ Add a request to the queue, to communicate with the GUI.
+ """
+ self.q.put({
+ 'type': request_type,
+ 'path': path,
+ 'data': data
+ })
+
+ def generate_slug(self, persistent_slug=None):
+ self.common.log('Web', 'generate_slug', 'persistent_slug={}'.format(persistent_slug))
+ if persistent_slug != None and persistent_slug != '':
+ self.slug = persistent_slug
+ self.common.log('Web', 'generate_slug', 'persistent_slug sent, so slug is: "{}"'.format(self.slug))
+ else:
+ self.slug = self.common.build_slug()
+ self.common.log('Web', 'generate_slug', 'built random slug: "{}"'.format(self.slug))
+
+ def debug_mode(self):
+ """
+ Turn on debugging mode, which will log flask errors to a debug file.
+ """
+ temp_dir = tempfile.gettempdir()
+ log_handler = logging.FileHandler(
+ os.path.join(temp_dir, 'onionshare_server.log'))
+ log_handler.setLevel(logging.WARNING)
+ self.app.logger.addHandler(log_handler)
+
+ def check_slug_candidate(self, slug_candidate):
+ self.common.log('Web', 'check_slug_candidate: slug_candidate={}'.format(slug_candidate))
+ if self.common.settings.get('public_mode'):
+ abort(404)
+ if not hmac.compare_digest(self.slug, slug_candidate):
+ abort(404)
+
+ def check_shutdown_slug_candidate(self, slug_candidate):
+ self.common.log('Web', 'check_shutdown_slug_candidate: slug_candidate={}'.format(slug_candidate))
+ if not hmac.compare_digest(self.shutdown_slug, slug_candidate):
+ abort(404)
+
+ def force_shutdown(self):
+ """
+ Stop the flask web server, from the context of the flask app.
+ """
+ # Shutdown the flask service
+ try:
+ func = request.environ.get('werkzeug.server.shutdown')
+ if func is None:
+ raise RuntimeError('Not running with the Werkzeug Server')
+ func()
+ except:
+ pass
+ self.running = False
+
+ def start(self, port, stay_open=False, public_mode=False, persistent_slug=None):
+ """
+ Start the flask web server.
+ """
+ self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, persistent_slug={}'.format(port, stay_open, public_mode, persistent_slug))
+ if not public_mode:
+ self.generate_slug(persistent_slug)
+
+ self.stay_open = stay_open
+
+ # In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220)
+ if os.path.exists('/usr/share/anon-ws-base-files/workstation'):
+ host = '0.0.0.0'
+ else:
+ host = '127.0.0.1'
+
+ self.running = True
+ self.app.run(host=host, port=port, threaded=True)
+
+ def stop(self, port):
+ """
+ Stop the flask web server by loading /shutdown.
+ """
+
+ if self.mode == 'share':
+ # If the user cancels the download, let the download function know to stop
+ # serving the file
+ self.share_mode.client_cancel = True
+
+ # To stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
+ if self.running:
+ try:
+ s = socket.socket()
+ s.connect(('127.0.0.1', port))
+ s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(self.shutdown_slug))
+ except:
+ try:
+ urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, self.shutdown_slug)).read()
+ except:
+ pass
diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py
index 13f0e8c7..99db635a 100644
--- a/onionshare_gui/__init__.py
+++ b/onionshare_gui/__init__.py
@@ -18,7 +18,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from __future__ import division
-import os, sys, platform, argparse
+import os
+import sys
+import platform
+import argparse
+import signal
from .widgets import Alert
from PyQt5 import QtCore, QtWidgets
@@ -58,6 +62,10 @@ def main():
strings.load_strings(common)
print(strings._('version_string').format(common.version))
+ # Allow Ctrl-C to smoothly quit the program instead of throwing an exception
+ # https://stackoverflow.com/questions/42814093/how-to-handle-ctrlc-in-python-app-with-pyqt
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+
# Start the Qt app
global qtapp
qtapp = Application(common)
diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode/__init__.py
index 418afffd..0971ff32 100644
--- a/onionshare_gui/mode.py
+++ b/onionshare_gui/mode/__init__.py
@@ -17,16 +17,14 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
-import time
-import threading
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from onionshare.common import ShutdownTimer
-from .server_status import ServerStatus
-from .onion_thread import OnionThread
-from .widgets import Alert
+from ..server_status import ServerStatus
+from ..threads import OnionThread
+from ..widgets import Alert
class Mode(QtWidgets.QWidget):
"""
@@ -39,7 +37,7 @@ class Mode(QtWidgets.QWidget):
starting_server_error = QtCore.pyqtSignal(str)
set_server_active = QtCore.pyqtSignal(bool)
- def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None):
+ def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None, local_only=False):
super(Mode, self).__init__()
self.common = common
self.qtapp = qtapp
@@ -51,13 +49,18 @@ class Mode(QtWidgets.QWidget):
self.filenames = filenames
- self.setMinimumWidth(450)
-
# The web object gets created in init()
self.web = None
+ # Local mode is passed from OnionShareGui
+ self.local_only = local_only
+
+ # Threads start out as None
+ self.onion_thread = None
+ self.web_thread = None
+
# Server status
- self.server_status = ServerStatus(self.common, self.qtapp, self.app)
+ self.server_status = ServerStatus(self.common, self.qtapp, self.app, None, self.local_only)
self.server_status.server_started.connect(self.start_server)
self.server_status.server_stopped.connect(self.stop_server)
self.server_status.server_canceled.connect(self.cancel_server)
@@ -67,16 +70,17 @@ class Mode(QtWidgets.QWidget):
self.starting_server_step3.connect(self.start_server_step3)
self.starting_server_error.connect(self.start_server_error)
- # Primary action layout
+ # Primary action
+ # Note: It's up to the downstream Mode to add this to its layout
self.primary_action_layout = QtWidgets.QVBoxLayout()
self.primary_action_layout.addWidget(self.server_status)
self.primary_action = QtWidgets.QWidget()
self.primary_action.setLayout(self.primary_action_layout)
- # Layout
- self.layout = QtWidgets.QVBoxLayout()
- self.layout.addWidget(self.primary_action)
- self.setLayout(self.layout)
+ # Hack to allow a minimum width on the main layout
+ # Note: It's up to the downstream Mode to add this to its layout
+ self.min_width_widget = QtWidgets.QWidget()
+ self.min_width_widget.setMinimumWidth(600)
def init(self):
"""
@@ -138,34 +142,11 @@ class Mode(QtWidgets.QWidget):
self.status_bar.clearMessage()
self.server_status_label.setText('')
- # Start the onion service in a new thread
- def start_onion_service(self):
- # Choose a port for the web app
- self.app.choose_port()
-
- # Start http service in new thread
- t = threading.Thread(target=self.web.start, args=(self.app.port, not self.common.settings.get('close_after_first_download'), self.common.settings.get('public_mode'), self.common.settings.get('slug')))
- t.daemon = True
- t.start()
-
- # Wait for the web app slug to generate before continuing
- if not self.common.settings.get('public_mode'):
- while self.web.slug == None:
- time.sleep(0.1)
-
- # Now start the onion service
- try:
- self.app.start_onion_service()
- self.starting_server_step2.emit()
-
- except Exception as e:
- self.starting_server_error.emit(e.args[0])
- return
-
self.common.log('Mode', 'start_server', 'Starting an onion thread')
- self.t = OnionThread(self.common, function=start_onion_service, kwargs={'self': self})
- self.t.daemon = True
- self.t.start()
+ self.onion_thread = OnionThread(self)
+ self.onion_thread.success.connect(self.starting_server_step2.emit)
+ self.onion_thread.error.connect(self.starting_server_error.emit)
+ self.onion_thread.start()
def start_server_custom(self):
"""
@@ -243,10 +224,22 @@ class Mode(QtWidgets.QWidget):
"""
Cancel the server while it is preparing to start
"""
- if self.t:
- self.t.quit()
+ self.cancel_server_custom()
+
+ if self.onion_thread:
+ self.common.log('Mode', 'cancel_server: quitting onion thread')
+ self.onion_thread.quit()
+ if self.web_thread:
+ self.common.log('Mode', 'cancel_server: quitting web thread')
+ self.web_thread.quit()
self.stop_server()
+ def cancel_server_custom(self):
+ """
+ Add custom initialization here.
+ """
+ pass
+
def stop_server(self):
"""
Stop the onionshare server.
diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py
new file mode 100644
index 00000000..9e821868
--- /dev/null
+++ b/onionshare_gui/mode/history.py
@@ -0,0 +1,548 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+import time
+import subprocess
+import os
+from datetime import datetime
+from PyQt5 import QtCore, QtWidgets, QtGui
+
+from onionshare import strings
+from ..widgets import Alert
+
+
+class HistoryItem(QtWidgets.QWidget):
+ """
+ The base history item
+ """
+ def __init__(self):
+ super(HistoryItem, self).__init__()
+
+ def update(self):
+ pass
+
+ def cancel(self):
+ pass
+
+
+class DownloadHistoryItem(HistoryItem):
+ """
+ Download history item, for share mode
+ """
+ def __init__(self, common, id, total_bytes):
+ super(DownloadHistoryItem, self).__init__()
+ self.common = common
+
+ self.id = id
+ self.total_bytes = total_bytes
+ self.downloaded_bytes = 0
+ self.started = time.time()
+ self.started_dt = datetime.fromtimestamp(self.started)
+
+ # Label
+ self.label = QtWidgets.QLabel(strings._('gui_download_in_progress').format(self.started_dt.strftime("%b %d, %I:%M%p")))
+
+ # Progress bar
+ self.progress_bar = QtWidgets.QProgressBar()
+ self.progress_bar.setTextVisible(True)
+ self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
+ self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
+ self.progress_bar.setMinimum(0)
+ self.progress_bar.setMaximum(total_bytes)
+ self.progress_bar.setValue(0)
+ self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
+ self.progress_bar.total_bytes = total_bytes
+
+ # Layout
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(self.label)
+ layout.addWidget(self.progress_bar)
+ self.setLayout(layout)
+
+ # Start at 0
+ self.update(0)
+
+ def update(self, downloaded_bytes):
+ self.downloaded_bytes = downloaded_bytes
+
+ self.progress_bar.setValue(downloaded_bytes)
+ if downloaded_bytes == self.progress_bar.total_bytes:
+ pb_fmt = strings._('gui_download_upload_progress_complete').format(
+ self.common.format_seconds(time.time() - self.started))
+ else:
+ elapsed = time.time() - self.started
+ if elapsed < 10:
+ # Wait a couple of seconds for the download rate to stabilize.
+ # This prevents a "Windows copy dialog"-esque experience at
+ # the beginning of the download.
+ pb_fmt = strings._('gui_download_upload_progress_starting').format(
+ self.common.human_readable_filesize(downloaded_bytes))
+ else:
+ pb_fmt = strings._('gui_download_upload_progress_eta').format(
+ self.common.human_readable_filesize(downloaded_bytes),
+ self.estimated_time_remaining)
+
+ self.progress_bar.setFormat(pb_fmt)
+
+ def cancel(self):
+ self.progress_bar.setFormat(strings._('gui_canceled'))
+
+ @property
+ def estimated_time_remaining(self):
+ return self.common.estimated_time_remaining(self.downloaded_bytes,
+ self.total_bytes,
+ self.started)
+
+
+class UploadHistoryItemFile(QtWidgets.QWidget):
+ def __init__(self, common, filename):
+ super(UploadHistoryItemFile, self).__init__()
+ self.common = common
+
+ self.common.log('UploadHistoryItemFile', '__init__', 'filename: {}'.format(filename))
+
+ self.filename = filename
+ self.started = datetime.now()
+
+ # Filename label
+ self.filename_label = QtWidgets.QLabel(self.filename)
+ self.filename_label_width = self.filename_label.width()
+
+ # File size label
+ self.filesize_label = QtWidgets.QLabel()
+ self.filesize_label.setStyleSheet(self.common.css['receive_file_size'])
+ self.filesize_label.hide()
+
+ # Folder button
+ folder_pixmap = QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/open_folder.png')))
+ folder_icon = QtGui.QIcon(folder_pixmap)
+ self.folder_button = QtWidgets.QPushButton()
+ self.folder_button.clicked.connect(self.open_folder)
+ self.folder_button.setIcon(folder_icon)
+ self.folder_button.setIconSize(folder_pixmap.rect().size())
+ self.folder_button.setFlat(True)
+ self.folder_button.hide()
+
+ # Layouts
+ layout = QtWidgets.QHBoxLayout()
+ layout.addWidget(self.filename_label)
+ layout.addWidget(self.filesize_label)
+ layout.addStretch()
+ layout.addWidget(self.folder_button)
+ self.setLayout(layout)
+
+ def update(self, uploaded_bytes, complete):
+ self.filesize_label.setText(self.common.human_readable_filesize(uploaded_bytes))
+ self.filesize_label.show()
+
+ if complete:
+ self.folder_button.show()
+
+ def rename(self, new_filename):
+ self.filename = new_filename
+ self.filename_label.setText(self.filename)
+
+ def open_folder(self):
+ """
+ Open the downloads folder, with the file selected, in a cross-platform manner
+ """
+ self.common.log('UploadHistoryItemFile', 'open_folder')
+
+ abs_filename = os.path.join(self.common.settings.get('downloads_dir'), self.filename)
+
+ # Linux
+ if self.common.platform == 'Linux' or self.common.platform == 'BSD':
+ try:
+ # If nautilus is available, open it
+ subprocess.Popen(['nautilus', abs_filename])
+ except:
+ Alert(self.common, strings._('gui_open_folder_error_nautilus').format(abs_filename))
+
+ # macOS
+ elif self.common.platform == 'Darwin':
+ subprocess.call(['open', '-R', abs_filename])
+
+ # Windows
+ elif self.common.platform == 'Windows':
+ subprocess.Popen(['explorer', '/select,{}'.format(abs_filename)])
+
+class UploadHistoryItem(HistoryItem):
+ def __init__(self, common, id, content_length):
+ super(UploadHistoryItem, self).__init__()
+ self.common = common
+ self.id = id
+ self.content_length = content_length
+ self.started = datetime.now()
+
+ # Label
+ self.label = QtWidgets.QLabel(strings._('gui_upload_in_progress', True).format(self.started.strftime("%b %d, %I:%M%p")))
+
+ # Progress bar
+ self.progress_bar = QtWidgets.QProgressBar()
+ self.progress_bar.setTextVisible(True)
+ self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
+ self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
+ self.progress_bar.setMinimum(0)
+ self.progress_bar.setValue(0)
+ self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
+
+ # This layout contains file widgets
+ self.files_layout = QtWidgets.QVBoxLayout()
+ self.files_layout.setContentsMargins(0, 0, 0, 0)
+ files_widget = QtWidgets.QWidget()
+ files_widget.setStyleSheet(self.common.css['receive_file'])
+ files_widget.setLayout(self.files_layout)
+
+ # Layout
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(self.label)
+ layout.addWidget(self.progress_bar)
+ layout.addWidget(files_widget)
+ layout.addStretch()
+ self.setLayout(layout)
+
+ # We're also making a dictionary of file widgets, to make them easier to access
+ self.files = {}
+
+ def update(self, data):
+ """
+ Using the progress from Web, update the progress bar and file size labels
+ for each file
+ """
+ if data['action'] == 'progress':
+ total_uploaded_bytes = 0
+ for filename in data['progress']:
+ total_uploaded_bytes += data['progress'][filename]['uploaded_bytes']
+
+ # Update the progress bar
+ self.progress_bar.setMaximum(self.content_length)
+ self.progress_bar.setValue(total_uploaded_bytes)
+
+ elapsed = datetime.now() - self.started
+ if elapsed.seconds < 10:
+ pb_fmt = strings._('gui_download_upload_progress_starting').format(
+ self.common.human_readable_filesize(total_uploaded_bytes))
+ else:
+ estimated_time_remaining = self.common.estimated_time_remaining(
+ total_uploaded_bytes,
+ self.content_length,
+ self.started.timestamp())
+ pb_fmt = strings._('gui_download_upload_progress_eta').format(
+ self.common.human_readable_filesize(total_uploaded_bytes),
+ estimated_time_remaining)
+
+ # Using list(progress) to avoid "RuntimeError: dictionary changed size during iteration"
+ for filename in list(data['progress']):
+ # Add a new file if needed
+ if filename not in self.files:
+ self.files[filename] = UploadHistoryItemFile(self.common, filename)
+ self.files_layout.addWidget(self.files[filename])
+
+ # Update the file
+ self.files[filename].update(data['progress'][filename]['uploaded_bytes'], data['progress'][filename]['complete'])
+
+ elif data['action'] == 'rename':
+ self.files[data['old_filename']].rename(data['new_filename'])
+ self.files[data['new_filename']] = self.files.pop(data['old_filename'])
+
+ elif data['action'] == 'finished':
+ # Hide the progress bar
+ self.progress_bar.hide()
+
+ # Change the label
+ self.ended = self.started = datetime.now()
+ if self.started.year == self.ended.year and self.started.month == self.ended.month and self.started.day == self.ended.day:
+ if self.started.hour == self.ended.hour and self.started.minute == self.ended.minute:
+ text = strings._('gui_upload_finished', True).format(
+ self.started.strftime("%b %d, %I:%M%p")
+ )
+ else:
+ text = strings._('gui_upload_finished_range', True).format(
+ self.started.strftime("%b %d, %I:%M%p"),
+ self.ended.strftime("%I:%M%p")
+ )
+ else:
+ text = strings._('gui_upload_finished_range', True).format(
+ self.started.strftime("%b %d, %I:%M%p"),
+ self.ended.strftime("%b %d, %I:%M%p")
+ )
+ self.label.setText(text)
+
+
+class HistoryItemList(QtWidgets.QScrollArea):
+ """
+ List of items
+ """
+ def __init__(self, common):
+ super(HistoryItemList, self).__init__()
+ self.common = common
+
+ self.items = {}
+
+ # The layout that holds all of the items
+ self.items_layout = QtWidgets.QVBoxLayout()
+ self.items_layout.setContentsMargins(0, 0, 0, 0)
+ self.items_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize)
+
+ # Wrapper layout that also contains a stretch
+ wrapper_layout = QtWidgets.QVBoxLayout()
+ wrapper_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize)
+ wrapper_layout.addLayout(self.items_layout)
+ wrapper_layout.addStretch()
+
+ # The internal widget of the scroll area
+ widget = QtWidgets.QWidget()
+ widget.setLayout(wrapper_layout)
+ self.setWidget(widget)
+ self.setWidgetResizable(True)
+
+ # Other scroll area settings
+ self.setBackgroundRole(QtGui.QPalette.Light)
+ self.verticalScrollBar().rangeChanged.connect(self.resizeScroll)
+
+ def resizeScroll(self, minimum, maximum):
+ """
+ Scroll to the bottom of the window when the range changes.
+ """
+ self.verticalScrollBar().setValue(maximum)
+
+ def add(self, id, item):
+ """
+ Add a new item. Override this method.
+ """
+ self.items[id] = item
+ self.items_layout.addWidget(item)
+
+ def update(self, id, data):
+ """
+ Update an item. Override this method.
+ """
+ self.items[id].update(data)
+
+ def cancel(self, id):
+ """
+ Cancel an item. Override this method.
+ """
+ self.items[id].cancel()
+
+ def reset(self):
+ """
+ Reset all items, emptying the list. Override this method.
+ """
+ for item in self.items.values():
+ self.items_layout.removeWidget(item)
+ item.close()
+ self.items = {}
+
+
+class History(QtWidgets.QWidget):
+ """
+ A history of what's happened so far in this mode. This contains an internal
+ object full of a scrollable list of items.
+ """
+ def __init__(self, common, empty_image, empty_text, header_text):
+ super(History, self).__init__()
+ self.common = common
+
+ self.setMinimumWidth(350)
+
+ # In progress and completed counters
+ self.in_progress_count = 0
+ self.completed_count = 0
+
+ # In progress and completed labels
+ self.in_progress_label = QtWidgets.QLabel()
+ self.in_progress_label.setStyleSheet(self.common.css['mode_info_label'])
+ self.completed_label = QtWidgets.QLabel()
+ self.completed_label.setStyleSheet(self.common.css['mode_info_label'])
+
+ # Header
+ self.header_label = QtWidgets.QLabel(header_text)
+ self.header_label.setStyleSheet(self.common.css['downloads_uploads_label'])
+ clear_button = QtWidgets.QPushButton(strings._('gui_clear_history', True))
+ clear_button.setStyleSheet(self.common.css['downloads_uploads_clear'])
+ clear_button.setFlat(True)
+ clear_button.clicked.connect(self.reset)
+ header_layout = QtWidgets.QHBoxLayout()
+ header_layout.addWidget(self.header_label)
+ header_layout.addStretch()
+ header_layout.addWidget(self.in_progress_label)
+ header_layout.addWidget(self.completed_label)
+ header_layout.addWidget(clear_button)
+
+ # When there are no items
+ self.empty_image = QtWidgets.QLabel()
+ self.empty_image.setAlignment(QtCore.Qt.AlignCenter)
+ self.empty_image.setPixmap(empty_image)
+ self.empty_text = QtWidgets.QLabel(empty_text)
+ self.empty_text.setAlignment(QtCore.Qt.AlignCenter)
+ self.empty_text.setStyleSheet(self.common.css['downloads_uploads_empty_text'])
+ empty_layout = QtWidgets.QVBoxLayout()
+ empty_layout.addStretch()
+ empty_layout.addWidget(self.empty_image)
+ empty_layout.addWidget(self.empty_text)
+ empty_layout.addStretch()
+ self.empty = QtWidgets.QWidget()
+ self.empty.setStyleSheet(self.common.css['downloads_uploads_empty'])
+ self.empty.setLayout(empty_layout)
+
+ # When there are items
+ self.item_list = HistoryItemList(self.common)
+ self.not_empty_layout = QtWidgets.QVBoxLayout()
+ self.not_empty_layout.addLayout(header_layout)
+ self.not_empty_layout.addWidget(self.item_list)
+ self.not_empty = QtWidgets.QWidget()
+ self.not_empty.setLayout(self.not_empty_layout)
+
+ # Layout
+ layout = QtWidgets.QVBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(self.empty)
+ layout.addWidget(self.not_empty)
+ self.setLayout(layout)
+
+ # Reset once at the beginning
+ self.reset()
+
+ def add(self, id, item):
+ """
+ Add a new item.
+ """
+ self.common.log('History', 'add', 'id: {}, item: {}'.format(id, item))
+
+ # Hide empty, show not empty
+ self.empty.hide()
+ self.not_empty.show()
+
+ # Add it to the list
+ self.item_list.add(id, item)
+
+ def update(self, id, data):
+ """
+ Update an item.
+ """
+ self.item_list.update(id, data)
+
+ def cancel(self, id):
+ """
+ Cancel an item.
+ """
+ self.item_list.cancel(id)
+
+ def reset(self):
+ """
+ Reset all items.
+ """
+ self.item_list.reset()
+
+ # Hide not empty, show empty
+ self.not_empty.hide()
+ self.empty.show()
+
+ # Reset counters
+ self.completed_count = 0
+ self.in_progress_count = 0
+ self.update_completed()
+ self.update_in_progress()
+
+ def update_completed(self):
+ """
+ Update the 'completed' widget.
+ """
+ if self.completed_count == 0:
+ image = self.common.get_resource_path('images/share_completed_none.png')
+ else:
+ image = self.common.get_resource_path('images/share_completed.png')
+ self.completed_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.completed_count))
+ self.completed_label.setToolTip(strings._('history_completed_tooltip').format(self.completed_count))
+
+ def update_in_progress(self):
+ """
+ Update the 'in progress' widget.
+ """
+ if self.in_progress_count == 0:
+ image = self.common.get_resource_path('images/share_in_progress_none.png')
+ else:
+ image = self.common.get_resource_path('images/share_in_progress.png')
+ self.in_progress_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.in_progress_count))
+ self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip', True).format(self.in_progress_count))
+
+
+class ToggleHistory(QtWidgets.QPushButton):
+ """
+ Widget for toggling showing or hiding the history, as well as keeping track
+ of the indicator counter if it's hidden
+ """
+ def __init__(self, common, current_mode, history_widget, icon, selected_icon):
+ super(ToggleHistory, self).__init__()
+ self.common = common
+ self.current_mode = current_mode
+ self.history_widget = history_widget
+ self.icon = icon
+ self.selected_icon = selected_icon
+
+ # Toggle button
+ self.setDefault(False)
+ self.setFixedWidth(35)
+ self.setFixedHeight(30)
+ self.setFlat(True)
+ self.setIcon(icon)
+ self.clicked.connect(self.toggle_clicked)
+
+ # Keep track of indicator
+ self.indicator_count = 0
+ self.indicator_label = QtWidgets.QLabel(parent=self)
+ self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator'])
+ self.update_indicator()
+
+ def update_indicator(self, increment=False):
+ """
+ Update the display of the indicator count. If increment is True, then
+ only increment the counter if Downloads is hidden.
+ """
+ if increment and not self.history_widget.isVisible():
+ self.indicator_count += 1
+
+ self.indicator_label.setText("{}".format(self.indicator_count))
+
+ if self.indicator_count == 0:
+ self.indicator_label.hide()
+ else:
+ size = self.indicator_label.sizeHint()
+ self.indicator_label.setGeometry(35-size.width(), 0, size.width(), size.height())
+ self.indicator_label.show()
+
+ def toggle_clicked(self):
+ """
+ Toggle showing and hiding the history widget
+ """
+ self.common.log('ToggleHistory', 'toggle_clicked')
+
+ if self.history_widget.isVisible():
+ self.history_widget.hide()
+ self.setIcon(self.icon)
+ self.setFlat(True)
+ else:
+ self.history_widget.show()
+ self.setIcon(self.selected_icon)
+ self.setFlat(False)
+
+ # Reset the indicator count
+ self.indicator_count = 0
+ self.update_indicator()
diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py
new file mode 100644
index 00000000..b73acca2
--- /dev/null
+++ b/onionshare_gui/mode/receive_mode/__init__.py
@@ -0,0 +1,196 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+from PyQt5 import QtCore, QtWidgets, QtGui
+
+from onionshare import strings
+from onionshare.web import Web
+
+from ..history import History, ToggleHistory, UploadHistoryItem
+from .. import Mode
+
+class ReceiveMode(Mode):
+ """
+ Parts of the main window UI for receiving files.
+ """
+ def init(self):
+ """
+ Custom initialization for ReceiveMode.
+ """
+ # Create the Web object
+ self.web = Web(self.common, True, 'receive')
+
+ # Server status
+ self.server_status.set_mode('receive')
+ self.server_status.server_started_finished.connect(self.update_primary_action)
+ self.server_status.server_stopped.connect(self.update_primary_action)
+ self.server_status.server_canceled.connect(self.update_primary_action)
+
+ # Tell server_status about web, then update
+ self.server_status.web = self.web
+ self.server_status.update()
+
+ # Upload history
+ self.history = History(
+ self.common,
+ QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/uploads_transparent.png'))),
+ strings._('gui_no_uploads'),
+ strings._('gui_uploads')
+ )
+ self.history.hide()
+
+ # Toggle history
+ self.toggle_history = ToggleHistory(
+ self.common, self, self.history,
+ QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')),
+ QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle_selected.png'))
+ )
+
+ # Receive mode warning
+ receive_warning = QtWidgets.QLabel(strings._('gui_receive_mode_warning', True))
+ receive_warning.setMinimumHeight(80)
+ receive_warning.setWordWrap(True)
+
+ # Top bar
+ top_bar_layout = QtWidgets.QHBoxLayout()
+ top_bar_layout.addStretch()
+ top_bar_layout.addWidget(self.toggle_history)
+
+ # Main layout
+ self.main_layout = QtWidgets.QVBoxLayout()
+ self.main_layout.addLayout(top_bar_layout)
+ self.main_layout.addWidget(receive_warning)
+ self.main_layout.addWidget(self.primary_action)
+ self.main_layout.addStretch()
+ self.main_layout.addWidget(self.min_width_widget)
+
+ # Wrapper layout
+ self.wrapper_layout = QtWidgets.QHBoxLayout()
+ self.wrapper_layout.addLayout(self.main_layout)
+ self.wrapper_layout.addWidget(self.history)
+ self.setLayout(self.wrapper_layout)
+
+ def get_stop_server_shutdown_timeout_text(self):
+ """
+ Return the string to put on the stop server button, if there's a shutdown timeout
+ """
+ return strings._('gui_receive_stop_server_shutdown_timeout', True)
+
+ def timeout_finished_should_stop_server(self):
+ """
+ The shutdown timer expired, should we stop the server? Returns a bool
+ """
+ # TODO: wait until the final upload is done before stoppign the server?
+ return True
+
+ def start_server_custom(self):
+ """
+ Starting the server.
+ """
+ # Reset web counters
+ self.web.receive_mode.upload_count = 0
+ self.web.error404_count = 0
+
+ # Hide and reset the uploads if we have previously shared
+ self.reset_info_counters()
+
+ def start_server_step2_custom(self):
+ """
+ Step 2 in starting the server.
+ """
+ # Continue
+ self.starting_server_step3.emit()
+ self.start_server_finished.emit()
+
+ def handle_tor_broke_custom(self):
+ """
+ Connection to Tor broke.
+ """
+ self.primary_action.hide()
+
+ def handle_request_load(self, event):
+ """
+ Handle REQUEST_LOAD event.
+ """
+ self.system_tray.showMessage(strings._('systray_page_loaded_title', True), strings._('systray_upload_page_loaded_message', True))
+
+ def handle_request_started(self, event):
+ """
+ Handle REQUEST_STARTED event.
+ """
+ item = UploadHistoryItem(self.common, event["data"]["id"], event["data"]["content_length"])
+ self.history.add(event["data"]["id"], item)
+ self.toggle_history.update_indicator(True)
+ self.history.in_progress_count += 1
+ self.history.update_in_progress()
+
+ self.system_tray.showMessage(strings._('systray_upload_started_title', True), strings._('systray_upload_started_message', True))
+
+ def handle_request_progress(self, event):
+ """
+ Handle REQUEST_PROGRESS event.
+ """
+ self.history.update(event["data"]["id"], {
+ 'action': 'progress',
+ 'progress': event["data"]["progress"]
+ })
+
+ def handle_request_close_server(self, event):
+ """
+ Handle REQUEST_CLOSE_SERVER event.
+ """
+ self.stop_server()
+ self.system_tray.showMessage(strings._('systray_close_server_title', True), strings._('systray_close_server_message', True))
+
+ def handle_request_upload_file_renamed(self, event):
+ """
+ Handle REQUEST_UPLOAD_FILE_RENAMED event.
+ """
+ self.history.update(event["data"]["id"], {
+ 'action': 'rename',
+ 'old_filename': event["data"]["old_filename"],
+ 'new_filename': event["data"]["new_filename"]
+ })
+
+ def handle_request_upload_finished(self, event):
+ """
+ Handle REQUEST_UPLOAD_FINISHED event.
+ """
+ self.history.update(event["data"]["id"], {
+ 'action': 'finished'
+ })
+ self.history.completed_count += 1
+ self.history.in_progress_count -= 1
+ self.history.update_completed()
+ self.history.update_in_progress()
+
+ def on_reload_settings(self):
+ """
+ We should be ok to re-enable the 'Start Receive Mode' button now.
+ """
+ self.primary_action.show()
+
+ def reset_info_counters(self):
+ """
+ Set the info counters back to zero.
+ """
+ self.history.reset()
+
+ def update_primary_action(self):
+ self.common.log('ReceiveMode', 'update_primary_action')
diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py
index 37315bbe..1c1f33ae 100644
--- a/onionshare_gui/share_mode/__init__.py
+++ b/onionshare_gui/mode/share_mode/__init__.py
@@ -17,7 +17,6 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
-import threading
import os
from PyQt5 import QtCore, QtWidgets, QtGui
@@ -27,9 +26,11 @@ from onionshare.common import Common
from onionshare.web import Web
from .file_selection import FileSelection
-from .downloads import Downloads
-from ..mode import Mode
-from ..widgets import Alert
+from .threads import CompressThread
+from .. import Mode
+from ..history import History, ToggleHistory, DownloadHistoryItem
+from ...widgets import Alert
+
class ShareMode(Mode):
"""
@@ -39,8 +40,11 @@ class ShareMode(Mode):
"""
Custom initialization for ReceiveMode.
"""
+ # Threads start out as None
+ self.compress_thread = None
+
# Create the Web object
- self.web = Web(self.common, True, False)
+ self.web = Web(self.common, True, 'share')
# File selection
self.file_selection = FileSelection(self.common)
@@ -67,40 +71,31 @@ class ShareMode(Mode):
self.filesize_warning.setStyleSheet(self.common.css['share_filesize_warning'])
self.filesize_warning.hide()
- # Downloads
- self.downloads = Downloads(self.common)
- self.downloads_in_progress = 0
- self.downloads_completed = 0
+ # Download history
+ self.history = History(
+ self.common,
+ QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/downloads_transparent.png'))),
+ strings._('gui_no_downloads'),
+ strings._('gui_downloads')
+ )
+ self.history.hide()
- # Information about share, and show downloads button
+ # Info label
self.info_label = QtWidgets.QLabel()
- self.info_label.setStyleSheet(self.common.css['mode_info_label'])
-
- self.info_show_downloads = QtWidgets.QToolButton()
- self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png')))
- self.info_show_downloads.setCheckable(True)
- self.info_show_downloads.toggled.connect(self.downloads_toggled)
- self.info_show_downloads.setToolTip(strings._('gui_downloads_window_tooltip', True))
-
- self.info_in_progress_downloads_count = QtWidgets.QLabel()
- self.info_in_progress_downloads_count.setStyleSheet(self.common.css['mode_info_label'])
+ self.info_label.hide()
- self.info_completed_downloads_count = QtWidgets.QLabel()
- self.info_completed_downloads_count.setStyleSheet(self.common.css['mode_info_label'])
+ # Toggle history
+ self.toggle_history = ToggleHistory(
+ self.common, self, self.history,
+ QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')),
+ QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png'))
+ )
- self.update_downloads_completed()
- self.update_downloads_in_progress()
-
- self.info_layout = QtWidgets.QHBoxLayout()
- self.info_layout.addWidget(self.info_label)
- self.info_layout.addStretch()
- self.info_layout.addWidget(self.info_in_progress_downloads_count)
- self.info_layout.addWidget(self.info_completed_downloads_count)
- self.info_layout.addWidget(self.info_show_downloads)
-
- self.info_widget = QtWidgets.QWidget()
- self.info_widget.setLayout(self.info_layout)
- self.info_widget.hide()
+ # Top bar
+ top_bar_layout = QtWidgets.QHBoxLayout()
+ top_bar_layout.addWidget(self.info_label)
+ top_bar_layout.addStretch()
+ top_bar_layout.addWidget(self.toggle_history)
# Primary action layout
self.primary_action_layout.addWidget(self.filesize_warning)
@@ -110,9 +105,18 @@ class ShareMode(Mode):
# Status bar, zip progress bar
self._zip_progress_bar = None
- # Layout
- self.layout.insertLayout(0, self.file_selection)
- self.layout.insertWidget(0, self.info_widget)
+ # Main layout
+ self.main_layout = QtWidgets.QVBoxLayout()
+ self.main_layout.addLayout(top_bar_layout)
+ self.main_layout.addLayout(self.file_selection)
+ self.main_layout.addWidget(self.primary_action)
+ self.main_layout.addWidget(self.min_width_widget)
+
+ # Wrapper layout
+ self.wrapper_layout = QtWidgets.QHBoxLayout()
+ self.wrapper_layout.addLayout(self.main_layout)
+ self.wrapper_layout.addWidget(self.history)
+ self.setLayout(self.wrapper_layout)
# Always start with focus on file selection
self.file_selection.setFocus()
@@ -128,7 +132,7 @@ class ShareMode(Mode):
The shutdown timer expired, should we stop the server? Returns a bool
"""
# If there were no attempts to download the share, or all downloads are done, we can stop
- if self.web.download_count == 0 or self.web.done:
+ if self.web.share_mode.download_count == 0 or self.web.done:
self.server_status.stop_server()
self.server_status_label.setText(strings._('close_on_timeout', True))
return True
@@ -142,7 +146,7 @@ class ShareMode(Mode):
Starting the server.
"""
# Reset web counters
- self.web.download_count = 0
+ self.web.share_mode.download_count = 0
self.web.error404_count = 0
# Hide and reset the downloads if we have previously shared
@@ -161,28 +165,13 @@ class ShareMode(Mode):
self._zip_progress_bar.total_files_size = ShareMode._compute_total_size(self.filenames)
self.status_bar.insertWidget(0, self._zip_progress_bar)
- # Prepare the files for sending in a new thread
- def finish_starting_server(self):
- # Prepare files to share
- def _set_processed_size(x):
- if self._zip_progress_bar != None:
- self._zip_progress_bar.update_processed_size_signal.emit(x)
-
- try:
- self.web.set_file_info(self.filenames, processed_size_callback=_set_processed_size)
- self.app.cleanup_filenames.append(self.web.zip_filename)
-
- # Only continue if the server hasn't been canceled
- if self.server_status.status != self.server_status.STATUS_STOPPED:
- self.starting_server_step3.emit()
- self.start_server_finished.emit()
- except OSError as e:
- self.starting_server_error.emit(e.strerror)
- return
-
- t = threading.Thread(target=finish_starting_server, kwargs={'self': self})
- t.daemon = True
- t.start()
+ # prepare the files for sending in a new thread
+ self.compress_thread = CompressThread(self)
+ self.compress_thread.success.connect(self.starting_server_step3.emit)
+ self.compress_thread.success.connect(self.start_server_finished.emit)
+ self.compress_thread.error.connect(self.starting_server_error.emit)
+ self.server_status.server_canceled.connect(self.compress_thread.cancel)
+ self.compress_thread.start()
def start_server_step3_custom(self):
"""
@@ -195,7 +184,7 @@ class ShareMode(Mode):
self._zip_progress_bar = None
# Warn about sending large files over Tor
- if self.web.zip_filesize >= 157286400: # 150mb
+ if self.web.share_mode.download_filesize >= 157286400: # 150mb
self.filesize_warning.setText(strings._("large_filesize", True))
self.filesize_warning.show()
@@ -217,17 +206,24 @@ class ShareMode(Mode):
self._zip_progress_bar = None
self.filesize_warning.hide()
- self.downloads_in_progress = 0
- self.downloads_completed = 0
- self.update_downloads_in_progress()
+ self.history.in_progress_count = 0
+ self.history.completed_count = 0
+ self.history.update_in_progress()
self.file_selection.file_list.adjustSize()
+ def cancel_server_custom(self):
+ """
+ Stop the compression thread on cancel
+ """
+ if self.compress_thread:
+ self.common.log('ShareMode', 'cancel_server: quitting compress thread')
+ self.compress_thread.quit()
+
def handle_tor_broke_custom(self):
"""
Connection to Tor broke.
"""
self.primary_action.hide()
- self.info_widget.hide()
def handle_request_load(self, event):
"""
@@ -239,9 +235,16 @@ class ShareMode(Mode):
"""
Handle REQUEST_STARTED event.
"""
- self.downloads.add(event["data"]["id"], self.web.zip_filesize)
- self.downloads_in_progress += 1
- self.update_downloads_in_progress()
+ if event["data"]["use_gzip"]:
+ filesize = self.web.share_mode.gzip_filesize
+ else:
+ filesize = self.web.share_mode.download_filesize
+
+ item = DownloadHistoryItem(self.common, event["data"]["id"], filesize)
+ self.history.add(event["data"]["id"], item)
+ self.toggle_history.update_indicator(True)
+ self.history.in_progress_count += 1
+ self.history.update_in_progress()
self.system_tray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True))
@@ -249,18 +252,17 @@ class ShareMode(Mode):
"""
Handle REQUEST_PROGRESS event.
"""
- self.downloads.update(event["data"]["id"], event["data"]["bytes"])
+ self.history.update(event["data"]["id"], event["data"]["bytes"])
# Is the download complete?
- if event["data"]["bytes"] == self.web.zip_filesize:
+ if event["data"]["bytes"] == self.web.share_mode.filesize:
self.system_tray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True))
- # Update the total 'completed downloads' info
- self.downloads_completed += 1
- self.update_downloads_completed()
- # Update the 'in progress downloads' info
- self.downloads_in_progress -= 1
- self.update_downloads_in_progress()
+ # Update completed and in progress labels
+ self.history.completed_count += 1
+ self.history.in_progress_count -= 1
+ self.history.update_completed()
+ self.history.update_in_progress()
# Close on finish?
if self.common.settings.get('close_after_first_download'):
@@ -269,19 +271,19 @@ class ShareMode(Mode):
self.server_status_label.setText(strings._('closing_automatically', True))
else:
if self.server_status.status == self.server_status.STATUS_STOPPED:
- self.downloads.cancel(event["data"]["id"])
- self.downloads_in_progress = 0
- self.update_downloads_in_progress()
+ self.history.cancel(event["data"]["id"])
+ self.history.in_progress_count = 0
+ self.history.update_in_progress()
def handle_request_canceled(self, event):
"""
Handle REQUEST_CANCELED event.
"""
- self.downloads.cancel(event["data"]["id"])
+ self.history.cancel(event["data"]["id"])
- # Update the 'in progress downloads' info
- self.downloads_in_progress -= 1
- self.update_downloads_in_progress()
+ # Update in progress count
+ self.history.in_progress_count -= 1
+ self.history.update_in_progress()
self.system_tray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True))
def on_reload_settings(self):
@@ -291,14 +293,16 @@ class ShareMode(Mode):
"""
if self.server_status.file_selection.get_num_files() > 0:
self.primary_action.show()
- self.info_widget.show()
+ self.info_label.show()
def update_primary_action(self):
+ self.common.log('ShareMode', 'update_primary_action')
+
# Show or hide primary action layout
file_count = self.file_selection.file_list.count()
if file_count > 0:
self.primary_action.show()
- self.info_widget.show()
+ self.info_label.show()
# Update the file count in the info label
total_size_bytes = 0
@@ -314,54 +318,13 @@ class ShareMode(Mode):
else:
self.primary_action.hide()
- self.info_widget.hide()
-
- # Resize window
- self.adjustSize()
-
- def downloads_toggled(self, checked):
- """
- When the 'Show/hide downloads' button is toggled, show or hide the downloads window.
- """
- self.common.log('ShareMode', 'toggle_downloads')
- if checked:
- self.downloads.show()
- else:
- self.downloads.hide()
+ self.info_label.hide()
def reset_info_counters(self):
"""
Set the info counters back to zero.
"""
- self.downloads_completed = 0
- self.downloads_in_progress = 0
- self.update_downloads_completed()
- self.update_downloads_in_progress()
- self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png')))
- self.downloads.reset()
-
- def update_downloads_completed(self):
- """
- Update the 'Downloads completed' info widget.
- """
- if self.downloads_completed == 0:
- image = self.common.get_resource_path('images/share_completed_none.png')
- else:
- image = self.common.get_resource_path('images/share_completed.png')
- self.info_completed_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.downloads_completed))
- self.info_completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(self.downloads_completed))
-
- def update_downloads_in_progress(self):
- """
- Update the 'Downloads in progress' info widget.
- """
- if self.downloads_in_progress == 0:
- image = self.common.get_resource_path('images/share_in_progress_none.png')
- else:
- image = self.common.get_resource_path('images/share_in_progress.png')
- self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_green.png')))
- self.info_in_progress_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.downloads_in_progress))
- self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(self.downloads_in_progress))
+ self.history.reset()
@staticmethod
def _compute_total_size(filenames):
@@ -410,6 +373,7 @@ class ZipProgressBar(QtWidgets.QProgressBar):
def update_processed_size(self, val):
self._processed_size = val
+
if self.processed_size < self.total_files_size:
self.setValue(int((self.processed_size * 100) / self.total_files_size))
elif self.total_files_size != 0:
diff --git a/onionshare_gui/share_mode/file_selection.py b/onionshare_gui/mode/share_mode/file_selection.py
index 628ad5ef..6bfa7dbf 100644
--- a/onionshare_gui/share_mode/file_selection.py
+++ b/onionshare_gui/mode/share_mode/file_selection.py
@@ -22,7 +22,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
-from ..widgets import Alert, AddFileDialog
+from ...widgets import Alert, AddFileDialog
class DropHereLabel(QtWidgets.QLabel):
"""
@@ -89,7 +89,7 @@ class FileList(QtWidgets.QListWidget):
self.setAcceptDrops(True)
self.setIconSize(QtCore.QSize(32, 32))
self.setSortingEnabled(True)
- self.setMinimumHeight(205)
+ self.setMinimumHeight(160)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.drop_here_image = DropHereLabel(self.common, self, True)
self.drop_here_text = DropHereLabel(self.common, self, False)
@@ -261,6 +261,7 @@ class FileList(QtWidgets.QListWidget):
# Item info widget, with a white background
item_info_layout = QtWidgets.QHBoxLayout()
+ item_info_layout.setContentsMargins(0, 0, 0, 0)
item_info_layout.addWidget(item_size)
item_info_layout.addWidget(item.item_button)
item_info = QtWidgets.QWidget()
diff --git a/onionshare_gui/mode/share_mode/threads.py b/onionshare_gui/mode/share_mode/threads.py
new file mode 100644
index 00000000..24e2c242
--- /dev/null
+++ b/onionshare_gui/mode/share_mode/threads.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+from PyQt5 import QtCore
+
+
+class CompressThread(QtCore.QThread):
+ """
+ Compresses files to be shared
+ """
+ success = QtCore.pyqtSignal()
+ error = QtCore.pyqtSignal(str)
+
+ def __init__(self, mode):
+ super(CompressThread, self).__init__()
+ self.mode = mode
+ self.mode.common.log('CompressThread', '__init__')
+
+ # prepare files to share
+ def set_processed_size(self, x):
+ if self.mode._zip_progress_bar != None:
+ self.mode._zip_progress_bar.update_processed_size_signal.emit(x)
+
+ def run(self):
+ self.mode.common.log('CompressThread', 'run')
+
+ try:
+ if self.mode.web.share_mode.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size):
+ self.success.emit()
+ else:
+ # Cancelled
+ pass
+
+ self.mode.app.cleanup_filenames += self.mode.web.share_mode.cleanup_filenames
+ except OSError as e:
+ self.error.emit(e.strerror)
+
+ def cancel(self):
+ self.mode.common.log('CompressThread', 'cancel')
+
+ # Let the Web and ZipWriter objects know that we're canceling compression early
+ self.mode.web.cancel_compression = True
+ try:
+ self.mode.web.zip_writer.cancel_compression = True
+ except AttributeError:
+ # we never made it as far as creating a ZipWriter object
+ pass
diff --git a/onionshare_gui/onion_thread.py b/onionshare_gui/onion_thread.py
deleted file mode 100644
index 0a25e891..00000000
--- a/onionshare_gui/onion_thread.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-OnionShare | https://onionshare.org/
-
-Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""
-from PyQt5 import QtCore
-
-class OnionThread(QtCore.QThread):
- """
- A QThread for starting our Onion Service.
- By using QThread rather than threading.Thread, we are able
- to call quit() or terminate() on the startup if the user
- decided to cancel (in which case do not proceed with obtaining
- the Onion address and starting the web server).
- """
- def __init__(self, common, function, kwargs=None):
- super(OnionThread, self).__init__()
-
- self.common = common
-
- self.common.log('OnionThread', '__init__')
- self.function = function
- if not kwargs:
- self.kwargs = {}
- else:
- self.kwargs = kwargs
-
- def run(self):
- self.common.log('OnionThread', 'run')
-
- self.function(**self.kwargs)
diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py
index b63119bb..c2e6657b 100644
--- a/onionshare_gui/onionshare_gui.py
+++ b/onionshare_gui/onionshare_gui.py
@@ -23,8 +23,8 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from onionshare.web import Web
-from .share_mode import ShareMode
-from .receive_mode import ReceiveMode
+from .mode.share_mode import ShareMode
+from .mode.receive_mode import ReceiveMode
from .tor_connection_dialog import TorConnectionDialog
from .settings_dialog import SettingsDialog
@@ -45,6 +45,8 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.common = common
self.common.log('OnionShareGui', '__init__')
+ self.setMinimumWidth(820)
+ self.setMinimumHeight(660)
self.onion = onion
self.qtapp = qtapp
@@ -55,7 +57,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.setWindowTitle('OnionShare')
self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
- self.setMinimumWidth(450)
# Load settings
self.config = config
@@ -66,7 +67,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.settings_action = menu.addAction(strings._('gui_settings_window_title', True))
self.settings_action.triggered.connect(self.open_settings)
help_action = menu.addAction(strings._('gui_settings_button_help', True))
- help_action.triggered.connect(SettingsDialog.help_clicked)
+ help_action.triggered.connect(SettingsDialog.open_help)
exit_action = menu.addAction(strings._('systray_menu_exit', True))
exit_action.triggered.connect(self.close)
@@ -121,7 +122,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.setStatusBar(self.status_bar)
# Share mode
- self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames)
+ self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames, self.local_only)
self.share_mode.init()
self.share_mode.server_status.server_started.connect(self.update_server_status_indicator)
self.share_mode.server_status.server_stopped.connect(self.update_server_status_indicator)
@@ -135,7 +136,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.share_mode.set_server_active.connect(self.set_server_active)
# Receive mode
- self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray)
+ self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, None, self.local_only)
self.receive_mode.init()
self.receive_mode.server_status.server_started.connect(self.update_server_status_indicator)
self.receive_mode.server_status.server_stopped.connect(self.update_server_status_indicator)
@@ -153,7 +154,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
# Layouts
contents_layout = QtWidgets.QVBoxLayout()
- contents_layout.setContentsMargins(10, 10, 10, 10)
+ contents_layout.setContentsMargins(10, 0, 10, 0)
contents_layout.addWidget(self.receive_mode)
contents_layout.addWidget(self.share_mode)
@@ -194,8 +195,8 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style'])
self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
- self.share_mode.show()
self.receive_mode.hide()
+ self.share_mode.show()
else:
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style'])
@@ -205,9 +206,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.update_server_status_indicator()
- # Wait 1ms for the event loop to finish, then adjust size
- QtCore.QTimer.singleShot(1, self.adjustSize)
-
def share_mode_clicked(self):
if self.mode != self.MODE_SHARE:
self.common.log('OnionShareGui', 'share_mode_clicked')
diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py
deleted file mode 100644
index d414f3b0..00000000
--- a/onionshare_gui/receive_mode/__init__.py
+++ /dev/null
@@ -1,237 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-OnionShare | https://onionshare.org/
-
-Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""
-from PyQt5 import QtCore, QtWidgets, QtGui
-
-from onionshare import strings
-from onionshare.web import Web
-
-from .uploads import Uploads
-from ..mode import Mode
-
-class ReceiveMode(Mode):
- """
- Parts of the main window UI for receiving files.
- """
- def init(self):
- """
- Custom initialization for ReceiveMode.
- """
- # Create the Web object
- self.web = Web(self.common, True, True)
-
- # Server status
- self.server_status.set_mode('receive')
- self.server_status.server_started_finished.connect(self.update_primary_action)
- self.server_status.server_stopped.connect(self.update_primary_action)
- self.server_status.server_canceled.connect(self.update_primary_action)
-
- # Tell server_status about web, then update
- self.server_status.web = self.web
- self.server_status.update()
-
- # Downloads
- self.uploads = Uploads(self.common)
- self.uploads_in_progress = 0
- self.uploads_completed = 0
- self.new_upload = False # For scrolling to the bottom of the uploads list
-
- # Information about share, and show uploads button
- self.info_show_uploads = QtWidgets.QToolButton()
- self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_gray.png')))
- self.info_show_uploads.setCheckable(True)
- self.info_show_uploads.toggled.connect(self.uploads_toggled)
- self.info_show_uploads.setToolTip(strings._('gui_uploads_window_tooltip', True))
-
- self.info_in_progress_uploads_count = QtWidgets.QLabel()
- self.info_in_progress_uploads_count.setStyleSheet(self.common.css['mode_info_label'])
-
- self.info_completed_uploads_count = QtWidgets.QLabel()
- self.info_completed_uploads_count.setStyleSheet(self.common.css['mode_info_label'])
-
- self.update_uploads_completed()
- self.update_uploads_in_progress()
-
- self.info_layout = QtWidgets.QHBoxLayout()
- self.info_layout.addStretch()
- self.info_layout.addWidget(self.info_in_progress_uploads_count)
- self.info_layout.addWidget(self.info_completed_uploads_count)
- self.info_layout.addWidget(self.info_show_uploads)
-
- self.info_widget = QtWidgets.QWidget()
- self.info_widget.setLayout(self.info_layout)
- self.info_widget.hide()
-
- # Receive mode info
- self.receive_info = QtWidgets.QLabel(strings._('gui_receive_mode_warning', True))
- self.receive_info.setMinimumHeight(80)
- self.receive_info.setWordWrap(True)
-
- # Layout
- self.layout.insertWidget(0, self.receive_info)
- self.layout.insertWidget(0, self.info_widget)
-
- def get_stop_server_shutdown_timeout_text(self):
- """
- Return the string to put on the stop server button, if there's a shutdown timeout
- """
- return strings._('gui_receive_stop_server_shutdown_timeout', True)
-
- def timeout_finished_should_stop_server(self):
- """
- The shutdown timer expired, should we stop the server? Returns a bool
- """
- # TODO: wait until the final upload is done before stoppign the server?
- return True
-
- def start_server_custom(self):
- """
- Starting the server.
- """
- # Reset web counters
- self.web.upload_count = 0
- self.web.error404_count = 0
-
- # Hide and reset the uploads if we have previously shared
- self.reset_info_counters()
-
- def start_server_step2_custom(self):
- """
- Step 2 in starting the server.
- """
- # Continue
- self.starting_server_step3.emit()
- self.start_server_finished.emit()
-
- def handle_tor_broke_custom(self):
- """
- Connection to Tor broke.
- """
- self.primary_action.hide()
- self.info_widget.hide()
-
- def handle_request_load(self, event):
- """
- Handle REQUEST_LOAD event.
- """
- self.system_tray.showMessage(strings._('systray_page_loaded_title', True), strings._('systray_upload_page_loaded_message', True))
-
- def handle_request_started(self, event):
- """
- Handle REQUEST_STARTED event.
- """
- self.uploads.add(event["data"]["id"], event["data"]["content_length"])
- self.uploads_in_progress += 1
- self.update_uploads_in_progress()
-
- self.system_tray.showMessage(strings._('systray_upload_started_title', True), strings._('systray_upload_started_message', True))
-
- def handle_request_progress(self, event):
- """
- Handle REQUEST_PROGRESS event.
- """
- self.uploads.update(event["data"]["id"], event["data"]["progress"])
-
- def handle_request_close_server(self, event):
- """
- Handle REQUEST_CLOSE_SERVER event.
- """
- self.stop_server()
- self.system_tray.showMessage(strings._('systray_close_server_title', True), strings._('systray_close_server_message', True))
-
- def handle_request_upload_file_renamed(self, event):
- """
- Handle REQUEST_UPLOAD_FILE_RENAMED event.
- """
- self.uploads.rename(event["data"]["id"], event["data"]["old_filename"], event["data"]["new_filename"])
-
- def handle_request_upload_finished(self, event):
- """
- Handle REQUEST_UPLOAD_FINISHED event.
- """
- self.uploads.finished(event["data"]["id"])
- # Update the total 'completed uploads' info
- self.uploads_completed += 1
- self.update_uploads_completed()
- # Update the 'in progress uploads' info
- self.uploads_in_progress -= 1
- self.update_uploads_in_progress()
-
- def on_reload_settings(self):
- """
- We should be ok to re-enable the 'Start Receive Mode' button now.
- """
- self.primary_action.show()
- self.info_widget.show()
-
- def reset_info_counters(self):
- """
- Set the info counters back to zero.
- """
- self.uploads_completed = 0
- self.uploads_in_progress = 0
- self.update_uploads_completed()
- self.update_uploads_in_progress()
- self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_gray.png')))
- self.uploads.reset()
-
- def update_uploads_completed(self):
- """
- Update the 'Uploads completed' info widget.
- """
- if self.uploads_completed == 0:
- image = self.common.get_resource_path('images/share_completed_none.png')
- else:
- image = self.common.get_resource_path('images/share_completed.png')
- self.info_completed_uploads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.uploads_completed))
- self.info_completed_uploads_count.setToolTip(strings._('info_completed_uploads_tooltip', True).format(self.uploads_completed))
-
- def update_uploads_in_progress(self):
- """
- Update the 'Uploads in progress' info widget.
- """
- if self.uploads_in_progress == 0:
- image = self.common.get_resource_path('images/share_in_progress_none.png')
- else:
- image = self.common.get_resource_path('images/share_in_progress.png')
- self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_green.png')))
- self.info_in_progress_uploads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.uploads_in_progress))
- self.info_in_progress_uploads_count.setToolTip(strings._('info_in_progress_uploads_tooltip', True).format(self.uploads_in_progress))
-
- def update_primary_action(self):
- self.common.log('ReceiveMode', 'update_primary_action')
-
- # Show the info widget when the server is active
- if self.server_status.status == self.server_status.STATUS_STARTED:
- self.info_widget.show()
- else:
- self.info_widget.hide()
-
- # Resize window
- self.adjustSize()
-
- def uploads_toggled(self, checked):
- """
- When the 'Show/hide uploads' button is toggled, show or hide the uploads window.
- """
- self.common.log('ReceiveMode', 'toggle_uploads')
- if checked:
- self.uploads.show()
- else:
- self.uploads.hide()
diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py
deleted file mode 100644
index 8d95d984..00000000
--- a/onionshare_gui/receive_mode/uploads.py
+++ /dev/null
@@ -1,310 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-OnionShare | https://onionshare.org/
-
-Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""
-import os
-import subprocess
-from datetime import datetime
-from PyQt5 import QtCore, QtWidgets, QtGui
-
-from onionshare import strings
-from ..widgets import Alert
-
-
-class File(QtWidgets.QWidget):
- def __init__(self, common, filename):
- super(File, self).__init__()
- self.common = common
-
- self.common.log('File', '__init__', 'filename: {}'.format(filename))
-
- self.filename = filename
- self.started = datetime.now()
-
- # Filename label
- self.filename_label = QtWidgets.QLabel(self.filename)
- self.filename_label_width = self.filename_label.width()
-
- # File size label
- self.filesize_label = QtWidgets.QLabel()
- self.filesize_label.setStyleSheet(self.common.css['receive_file_size'])
- self.filesize_label.hide()
-
- # Folder button
- folder_pixmap = QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/open_folder.png')))
- folder_icon = QtGui.QIcon(folder_pixmap)
- self.folder_button = QtWidgets.QPushButton()
- self.folder_button.clicked.connect(self.open_folder)
- self.folder_button.setIcon(folder_icon)
- self.folder_button.setIconSize(folder_pixmap.rect().size())
- self.folder_button.setFlat(True)
- self.folder_button.hide()
-
- # Layouts
- layout = QtWidgets.QHBoxLayout()
- layout.addWidget(self.filename_label)
- layout.addWidget(self.filesize_label)
- layout.addStretch()
- layout.addWidget(self.folder_button)
- self.setLayout(layout)
-
- def update(self, uploaded_bytes, complete):
- self.filesize_label.setText(self.common.human_readable_filesize(uploaded_bytes))
- self.filesize_label.show()
-
- if complete:
- self.folder_button.show()
-
- def rename(self, new_filename):
- self.filename = new_filename
- self.filename_label.setText(self.filename)
-
- def open_folder(self):
- """
- Open the downloads folder, with the file selected, in a cross-platform manner
- """
- self.common.log('File', 'open_folder')
-
- abs_filename = os.path.join(self.common.settings.get('downloads_dir'), self.filename)
-
- # Linux
- if self.common.platform == 'Linux' or self.common.platform == 'BSD':
- try:
- # If nautilus is available, open it
- subprocess.Popen(['nautilus', abs_filename])
- except:
- Alert(self.common, strings._('gui_open_folder_error_nautilus').format(abs_filename))
-
- # macOS
- elif self.common.platform == 'Darwin':
- subprocess.call(['open', '-R', abs_filename])
-
- # Windows
- elif self.common.platform == 'Windows':
- subprocess.Popen(['explorer', '/select,{}'.format(abs_filename)])
-
-
-class Upload(QtWidgets.QWidget):
- def __init__(self, common, upload_id, content_length):
- super(Upload, self).__init__()
- self.common = common
- self.upload_id = upload_id
- self.content_length = content_length
- self.started = datetime.now()
-
- # Label
- self.label = QtWidgets.QLabel(strings._('gui_upload_in_progress', True).format(self.started.strftime("%b %d, %I:%M%p")))
-
- # Progress bar
- self.progress_bar = QtWidgets.QProgressBar()
- self.progress_bar.setTextVisible(True)
- self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
- self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
- self.progress_bar.setMinimum(0)
- self.progress_bar.setValue(0)
- self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
-
- # This layout contains file widgets
- self.files_layout = QtWidgets.QVBoxLayout()
- self.files_layout.setContentsMargins(0, 0, 0, 0)
- files_widget = QtWidgets.QWidget()
- files_widget.setStyleSheet(self.common.css['receive_file'])
- files_widget.setLayout(self.files_layout)
-
- # Layout
- layout = QtWidgets.QVBoxLayout()
- layout.addWidget(self.label)
- layout.addWidget(self.progress_bar)
- layout.addWidget(files_widget)
- layout.addStretch()
- self.setLayout(layout)
-
- # We're also making a dictionary of file widgets, to make them easier to access
- self.files = {}
-
- def update(self, progress):
- """
- Using the progress from Web, update the progress bar and file size labels
- for each file
- """
- total_uploaded_bytes = 0
- for filename in progress:
- total_uploaded_bytes += progress[filename]['uploaded_bytes']
-
- # Update the progress bar
- self.progress_bar.setMaximum(self.content_length)
- self.progress_bar.setValue(total_uploaded_bytes)
-
- elapsed = datetime.now() - self.started
- if elapsed.seconds < 10:
- pb_fmt = strings._('gui_download_upload_progress_starting').format(
- self.common.human_readable_filesize(total_uploaded_bytes))
- else:
- estimated_time_remaining = self.common.estimated_time_remaining(
- total_uploaded_bytes,
- self.content_length,
- self.started.timestamp())
- pb_fmt = strings._('gui_download_upload_progress_eta').format(
- self.common.human_readable_filesize(total_uploaded_bytes),
- estimated_time_remaining)
-
- # Using list(progress) to avoid "RuntimeError: dictionary changed size during iteration"
- for filename in list(progress):
- # Add a new file if needed
- if filename not in self.files:
- self.files[filename] = File(self.common, filename)
- self.files_layout.addWidget(self.files[filename])
-
- # Update the file
- self.files[filename].update(progress[filename]['uploaded_bytes'], progress[filename]['complete'])
-
- def rename(self, old_filename, new_filename):
- self.files[old_filename].rename(new_filename)
- self.files[new_filename] = self.files.pop(old_filename)
-
- def finished(self):
- # Hide the progress bar
- self.progress_bar.hide()
-
- # Change the label
- self.ended = self.started = datetime.now()
- if self.started.year == self.ended.year and self.started.month == self.ended.month and self.started.day == self.ended.day:
- if self.started.hour == self.ended.hour and self.started.minute == self.ended.minute:
- text = strings._('gui_upload_finished', True).format(
- self.started.strftime("%b %d, %I:%M%p")
- )
- else:
- text = strings._('gui_upload_finished_range', True).format(
- self.started.strftime("%b %d, %I:%M%p"),
- self.ended.strftime("%I:%M%p")
- )
- else:
- text = strings._('gui_upload_finished_range', True).format(
- self.started.strftime("%b %d, %I:%M%p"),
- self.ended.strftime("%b %d, %I:%M%p")
- )
- self.label.setText(text)
-
-
-class Uploads(QtWidgets.QScrollArea):
- """
- The uploads chunk of the GUI. This lists all of the active upload
- progress bars, as well as information about each upload.
- """
- def __init__(self, common):
- super(Uploads, self).__init__()
- self.common = common
- self.common.log('Uploads', '__init__')
-
- self.resizeEvent = None
-
- self.uploads = {}
-
- self.setWindowTitle(strings._('gui_uploads', True))
- self.setWidgetResizable(True)
- self.setMaximumHeight(600)
- self.setMinimumHeight(150)
- self.setMinimumWidth(350)
- self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
- self.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint)
- self.vbar = self.verticalScrollBar()
- self.vbar.rangeChanged.connect(self.resizeScroll)
-
- uploads_label = QtWidgets.QLabel(strings._('gui_uploads', True))
- uploads_label.setStyleSheet(self.common.css['downloads_uploads_label'])
- self.no_uploads_label = QtWidgets.QLabel(strings._('gui_no_uploads', True))
-
- self.uploads_layout = QtWidgets.QVBoxLayout()
-
- widget = QtWidgets.QWidget()
- layout = QtWidgets.QVBoxLayout()
- layout.addWidget(uploads_label)
- layout.addWidget(self.no_uploads_label)
- layout.addLayout(self.uploads_layout)
- layout.addStretch()
- widget.setLayout(layout)
- self.setWidget(widget)
-
- def resizeScroll(self, minimum, maximum):
- """
- Scroll to the bottom of the window when the range changes.
- """
- self.vbar.setValue(maximum)
-
- def add(self, upload_id, content_length):
- """
- Add a new upload.
- """
- self.common.log('Uploads', 'add', 'upload_id: {}, content_length: {}'.format(upload_id, content_length))
- # Hide the no_uploads_label
- self.no_uploads_label.hide()
-
- # Add it to the list
- upload = Upload(self.common, upload_id, content_length)
- self.uploads[upload_id] = upload
- self.uploads_layout.addWidget(upload)
-
- def update(self, upload_id, progress):
- """
- Update the progress of an upload.
- """
- self.uploads[upload_id].update(progress)
-
- def rename(self, upload_id, old_filename, new_filename):
- """
- Rename a file, which happens if the filename already exists in downloads_dir.
- """
- self.uploads[upload_id].rename(old_filename, new_filename)
-
- def finished(self, upload_id):
- """
- An upload has finished.
- """
- self.uploads[upload_id].finished()
-
- def cancel(self, upload_id):
- """
- Update an upload progress bar to show that it has been canceled.
- """
- self.common.log('Uploads', 'cancel', 'upload_id: {}'.format(upload_id))
- self.uploads[upload_id].cancel()
-
- def reset(self):
- """
- Reset the uploads back to zero
- """
- self.common.log('Uploads', 'reset')
- for upload in self.uploads.values():
- self.uploads_layout.removeWidget(upload)
- self.uploads = {}
-
- self.no_uploads_label.show()
- self.resize(self.sizeHint())
-
- def resizeEvent(self, event):
- width = self.frameGeometry().width()
- try:
- for upload in self.uploads.values():
- for item in upload.files.values():
- if item.filename_label_width > width:
- item.filename_label.setText(item.filename[:25] + '[...]')
- item.adjustSize()
- if width > item.filename_label_width:
- item.filename_label.setText(item.filename)
- except:
- pass
diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py
index 9afceb38..0267d826 100644
--- a/onionshare_gui/server_status.py
+++ b/onionshare_gui/server_status.py
@@ -44,7 +44,7 @@ class ServerStatus(QtWidgets.QWidget):
STATUS_WORKING = 1
STATUS_STARTED = 2
- def __init__(self, common, qtapp, app, file_selection=None):
+ def __init__(self, common, qtapp, app, file_selection=None, local_only=False):
super(ServerStatus, self).__init__()
self.common = common
@@ -56,17 +56,23 @@ class ServerStatus(QtWidgets.QWidget):
self.app = app
self.web = None
+ self.local_only = local_only
self.resizeEvent(None)
# Shutdown timeout layout
self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True))
self.shutdown_timeout = QtWidgets.QDateTimeEdit()
- # Set proposed timeout to be 5 minutes into the future
self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy")
- self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
- # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 2 min from now
- self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
+ if self.local_only:
+ # For testing
+ self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15))
+ self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime())
+ else:
+ # Set proposed timeout to be 5 minutes into the future
+ self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
+ # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 60s from now
+ self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60))
self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection)
shutdown_timeout_layout = QtWidgets.QHBoxLayout()
shutdown_timeout_layout.addWidget(self.shutdown_timeout_label)
@@ -84,20 +90,20 @@ class ServerStatus(QtWidgets.QWidget):
self.server_button.clicked.connect(self.server_button_clicked)
# URL layout
- url_font = QtGui.QFont()
+ url_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)
self.url_description = QtWidgets.QLabel()
self.url_description.setWordWrap(True)
self.url_description.setMinimumHeight(50)
self.url = QtWidgets.QLabel()
self.url.setFont(url_font)
self.url.setWordWrap(True)
- self.url.setMinimumHeight(65)
self.url.setMinimumSize(self.url.sizeHint())
self.url.setStyleSheet(self.common.css['server_status_url'])
self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True))
self.copy_url_button.setFlat(True)
self.copy_url_button.setStyleSheet(self.common.css['server_status_url_buttons'])
+ self.copy_url_button.setMinimumHeight(65)
self.copy_url_button.clicked.connect(self.copy_url)
self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True))
self.copy_hidservauth_button.setFlat(True)
@@ -136,12 +142,12 @@ class ServerStatus(QtWidgets.QWidget):
When the widget is resized, try and adjust the display of a v3 onion URL.
"""
try:
- self.get_url()
+ # Wrap the URL label
url_length=len(self.get_url())
if url_length > 60:
width = self.frameGeometry().width()
if width < 530:
- wrapped_onion_url = textwrap.fill(self.get_url(), 50)
+ wrapped_onion_url = textwrap.fill(self.get_url(), 46)
self.url.setText(wrapped_onion_url)
else:
self.url.setText(self.get_url())
@@ -154,7 +160,8 @@ class ServerStatus(QtWidgets.QWidget):
Reset the timeout in the UI after stopping a share
"""
self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
- self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
+ if not self.local_only:
+ self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60))
def update(self):
"""
@@ -255,8 +262,11 @@ class ServerStatus(QtWidgets.QWidget):
"""
if self.status == self.STATUS_STOPPED:
if self.common.settings.get('shutdown_timeout'):
- # Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen
- self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0)
+ if self.local_only:
+ self.timeout = self.shutdown_timeout.dateTime().toPyDateTime()
+ else:
+ # Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen
+ self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0)
# If the timeout has actually passed already before the user hit Start, refuse to start the server.
if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout:
Alert(self.common, strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning))
diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py
index c31d4630..39f08128 100644
--- a/onionshare_gui/settings_dialog.py
+++ b/onionshare_gui/settings_dialog.py
@@ -746,7 +746,7 @@ class SettingsDialog(QtWidgets.QDialog):
onion.connect(custom_settings=settings, config=self.config, tor_status_update_func=tor_status_update_func)
# If an exception hasn't been raised yet, the Tor settings work
- Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth))
+ Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth, onion.supports_next_gen_onions))
# Clean up
onion.cleanup()
@@ -883,8 +883,12 @@ class SettingsDialog(QtWidgets.QDialog):
Help button clicked.
"""
self.common.log('SettingsDialog', 'help_clicked')
- help_site = 'https://github.com/micahflee/onionshare/wiki'
- QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_site))
+ SettingsDialog.open_help()
+
+ @staticmethod
+ def open_help():
+ help_url = 'https://github.com/micahflee/onionshare/wiki'
+ QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_url))
def settings_from_fields(self):
"""
diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py
deleted file mode 100644
index 538ddfd0..00000000
--- a/onionshare_gui/share_mode/downloads.py
+++ /dev/null
@@ -1,157 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-OnionShare | https://onionshare.org/
-
-Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""
-import time
-from PyQt5 import QtCore, QtWidgets, QtGui
-
-from onionshare import strings
-
-
-class Download(object):
- def __init__(self, common, download_id, total_bytes):
- self.common = common
-
- self.download_id = download_id
- self.started = time.time()
- self.total_bytes = total_bytes
- self.downloaded_bytes = 0
-
- # Progress bar
- self.progress_bar = QtWidgets.QProgressBar()
- self.progress_bar.setTextVisible(True)
- self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
- self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
- self.progress_bar.setMinimum(0)
- self.progress_bar.setMaximum(total_bytes)
- self.progress_bar.setValue(0)
- self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
- self.progress_bar.total_bytes = total_bytes
-
- # Start at 0
- self.update(0)
-
- def update(self, downloaded_bytes):
- self.downloaded_bytes = downloaded_bytes
-
- self.progress_bar.setValue(downloaded_bytes)
- if downloaded_bytes == self.progress_bar.total_bytes:
- pb_fmt = strings._('gui_download_upload_progress_complete').format(
- self.common.format_seconds(time.time() - self.started))
- else:
- elapsed = time.time() - self.started
- if elapsed < 10:
- # Wait a couple of seconds for the download rate to stabilize.
- # This prevents a "Windows copy dialog"-esque experience at
- # the beginning of the download.
- pb_fmt = strings._('gui_download_upload_progress_starting').format(
- self.common.human_readable_filesize(downloaded_bytes))
- else:
- pb_fmt = strings._('gui_download_upload_progress_eta').format(
- self.common.human_readable_filesize(downloaded_bytes),
- self.estimated_time_remaining)
-
- self.progress_bar.setFormat(pb_fmt)
-
- def cancel(self):
- self.progress_bar.setFormat(strings._('gui_canceled'))
-
- @property
- def estimated_time_remaining(self):
- return self.common.estimated_time_remaining(self.downloaded_bytes,
- self.total_bytes,
- self.started)
-
-
-class Downloads(QtWidgets.QScrollArea):
- """
- The downloads chunk of the GUI. This lists all of the active download
- progress bars.
- """
- def __init__(self, common):
- super(Downloads, self).__init__()
- self.common = common
-
- self.downloads = {}
-
- self.setWindowTitle(strings._('gui_downloads', True))
- self.setWidgetResizable(True)
- self.setMaximumHeight(600)
- self.setMinimumHeight(150)
- self.setMinimumWidth(350)
- self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
- self.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint)
- self.vbar = self.verticalScrollBar()
- self.vbar.rangeChanged.connect(self.resizeScroll)
-
- downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True))
- downloads_label.setStyleSheet(self.common.css['downloads_uploads_label'])
- self.no_downloads_label = QtWidgets.QLabel(strings._('gui_no_downloads', True))
-
- self.downloads_layout = QtWidgets.QVBoxLayout()
-
- widget = QtWidgets.QWidget()
- layout = QtWidgets.QVBoxLayout()
- layout.addWidget(downloads_label)
- layout.addWidget(self.no_downloads_label)
- layout.addLayout(self.downloads_layout)
- layout.addStretch()
- widget.setLayout(layout)
- self.setWidget(widget)
-
- def resizeScroll(self, minimum, maximum):
- """
- Scroll to the bottom of the window when the range changes.
- """
- self.vbar.setValue(maximum)
-
- def add(self, download_id, total_bytes):
- """
- Add a new download progress bar.
- """
- # Hide the no_downloads_label
- self.no_downloads_label.hide()
-
- # Add it to the list
- download = Download(self.common, download_id, total_bytes)
- self.downloads[download_id] = download
- self.downloads_layout.addWidget(download.progress_bar)
-
- def update(self, download_id, downloaded_bytes):
- """
- Update the progress of a download progress bar.
- """
- self.downloads[download_id].update(downloaded_bytes)
-
- def cancel(self, download_id):
- """
- Update a download progress bar to show that it has been canceled.
- """
- self.downloads[download_id].cancel()
-
- def reset(self):
- """
- Reset the downloads back to zero
- """
- for download in self.downloads.values():
- self.downloads_layout.removeWidget(download.progress_bar)
- download.progress_bar.close()
- self.downloads = {}
-
- self.no_downloads_label.show()
- self.resize(self.sizeHint())
diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py
new file mode 100644
index 00000000..3b05bebf
--- /dev/null
+++ b/onionshare_gui/threads.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+import time
+from PyQt5 import QtCore
+
+from onionshare.onion import *
+
+
+class OnionThread(QtCore.QThread):
+ """
+ Starts the onion service, and waits for it to finish
+ """
+ success = QtCore.pyqtSignal()
+ error = QtCore.pyqtSignal(str)
+
+ def __init__(self, mode):
+ super(OnionThread, self).__init__()
+ self.mode = mode
+ self.mode.common.log('OnionThread', '__init__')
+
+ # allow this thread to be terminated
+ self.setTerminationEnabled()
+
+ def run(self):
+ self.mode.common.log('OnionThread', 'run')
+
+ self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download')
+
+ # start onionshare http service in new thread
+ self.mode.web_thread = WebThread(self.mode)
+ self.mode.web_thread.start()
+
+ # wait for modules in thread to load, preventing a thread-related cx_Freeze crash
+ time.sleep(0.2)
+
+ try:
+ self.mode.app.start_onion_service()
+ self.success.emit()
+
+ except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e:
+ self.error.emit(e.args[0])
+ return
+
+
+class WebThread(QtCore.QThread):
+ """
+ Starts the web service
+ """
+ success = QtCore.pyqtSignal()
+ error = QtCore.pyqtSignal(str)
+
+ def __init__(self, mode):
+ super(WebThread, self).__init__()
+ self.mode = mode
+ self.mode.common.log('WebThread', '__init__')
+
+ def run(self):
+ self.mode.common.log('WebThread', 'run')
+ self.mode.app.choose_port()
+ self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.common.settings.get('slug'))
diff --git a/screenshots/server.png b/screenshots/server.png
deleted file mode 100644
index 8bdf2971..00000000
--- a/screenshots/server.png
+++ /dev/null
Binary files differ
diff --git a/setup.py b/setup.py
index 1a665085..86b71f82 100644
--- a/setup.py
+++ b/setup.py
@@ -65,7 +65,14 @@ setup(
description=description, long_description=long_description,
author=author, author_email=author_email,
url=url, license=license, keywords=keywords,
- packages=['onionshare', 'onionshare_gui'],
+ packages=[
+ 'onionshare',
+ 'onionshare.web',
+ 'onionshare_gui',
+ 'onionshare_gui.mode',
+ 'onionshare_gui.mode.share_mode',
+ 'onionshare_gui.mode.receive_mode'
+ ],
include_package_data=True,
scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'],
data_files=data_files
diff --git a/share/images/download_window_gray.png b/share/images/download_window_gray.png
deleted file mode 100644
index bf9c168e..00000000
--- a/share/images/download_window_gray.png
+++ /dev/null
Binary files differ
diff --git a/share/images/download_window_green.png b/share/images/download_window_green.png
deleted file mode 100644
index 8f9a899b..00000000
--- a/share/images/download_window_green.png
+++ /dev/null
Binary files differ
diff --git a/share/images/downloads.png b/share/images/downloads.png
new file mode 100644
index 00000000..ad879b6e
--- /dev/null
+++ b/share/images/downloads.png
Binary files differ
diff --git a/share/images/downloads_toggle.png b/share/images/downloads_toggle.png
new file mode 100644
index 00000000..846ececb
--- /dev/null
+++ b/share/images/downloads_toggle.png
Binary files differ
diff --git a/share/images/downloads_toggle_selected.png b/share/images/downloads_toggle_selected.png
new file mode 100644
index 00000000..127ce208
--- /dev/null
+++ b/share/images/downloads_toggle_selected.png
Binary files differ
diff --git a/share/images/downloads_transparent.png b/share/images/downloads_transparent.png
new file mode 100644
index 00000000..99207097
--- /dev/null
+++ b/share/images/downloads_transparent.png
Binary files differ
diff --git a/share/images/upload_window_gray.png b/share/images/upload_window_gray.png
deleted file mode 100644
index 80db4b8f..00000000
--- a/share/images/upload_window_gray.png
+++ /dev/null
Binary files differ
diff --git a/share/images/upload_window_green.png b/share/images/upload_window_green.png
deleted file mode 100644
index 652ddaff..00000000
--- a/share/images/upload_window_green.png
+++ /dev/null
Binary files differ
diff --git a/share/images/uploads.png b/share/images/uploads.png
new file mode 100644
index 00000000..cd9bd98e
--- /dev/null
+++ b/share/images/uploads.png
Binary files differ
diff --git a/share/images/uploads_toggle.png b/share/images/uploads_toggle.png
new file mode 100644
index 00000000..87303c9f
--- /dev/null
+++ b/share/images/uploads_toggle.png
Binary files differ
diff --git a/share/images/uploads_toggle_selected.png b/share/images/uploads_toggle_selected.png
new file mode 100644
index 00000000..0ba52cff
--- /dev/null
+++ b/share/images/uploads_toggle_selected.png
Binary files differ
diff --git a/share/images/uploads_transparent.png b/share/images/uploads_transparent.png
new file mode 100644
index 00000000..3648c3fb
--- /dev/null
+++ b/share/images/uploads_transparent.png
Binary files differ
diff --git a/share/locale/cs.json b/share/locale/cs.json
index 40e48f87..a595ce67 100644
--- a/share/locale/cs.json
+++ b/share/locale/cs.json
@@ -1,7 +1,6 @@
{
"config_onion_service": "Nastavuji onion service na portu {0:d}.",
"preparing_files": "Připravuji soubory ke sdílení.",
- "wait_for_hs": "Čekám na HS až bude připravena:",
"give_this_url": "Dejte tuto URL osobě, které dané soubory posíláte:",
"give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:",
"ctrlc_to_stop": "Stiskněte Ctrl-C pro zastavení serveru",
@@ -27,7 +26,6 @@
"gui_copied_url": "URL zkopírováno do schránky",
"gui_copied_hidservauth": "Copied HidServAuth line to clipboard",
"gui_please_wait": "Prosím čekejte...",
- "using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
"gui_download_upload_progress_complete": "%p%, Uplynulý čas: {0:s}",
"gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)",
"gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%",
diff --git a/share/locale/da.json b/share/locale/da.json
index 00539212..d414695b 100644
--- a/share/locale/da.json
+++ b/share/locale/da.json
@@ -1,7 +1,6 @@
{
"config_onion_service": "Konfigurerer onion-tjeneste på port {0:d}.",
"preparing_files": "Forbereder filer som skal deles.",
- "wait_for_hs": "Venter på at HS bliver klar:",
"give_this_url": "Giv denne URL til personen du sender filen til:",
"give_this_url_stealth": "Giv denne URL og HidServAuth-linje til personen du sender filen til:",
"ctrlc_to_stop": "Tryk på Ctrl-C for at stoppe serveren",
@@ -40,7 +39,6 @@
"gui_copied_url": "Kopierede URL til udklipsholder",
"gui_copied_hidservauth": "Kopierede HidServAuth-linje til udklipsholder",
"gui_please_wait": "Vent venligst...",
- "using_ephemeral": "Starter kortvarig Tor onion-tjeneste og afventer udgivelse",
"gui_download_upload_progress_complete": "%p%, tid forløbet: {0:s}",
"gui_download_upload_progress_starting": "{0:s}, %p% (udregner anslået ankomsttid)",
"gui_download_upload_progress_eta": "{0:s}, anslået ankomsttid: {1:s}, %p%",
@@ -53,9 +51,7 @@
"error_stealth_not_supported": "For at oprette usynlige onion-tjenester, skal du mindst have Tor 0.2.9.1-alpha (eller Tor Browser 6.5) og mindst python3-stem 1.5.0.",
"error_ephemeral_not_supported": "OnionShare kræver mindst Tor 0.2.7.1 og mindst python3-stem 1.4.0.",
"gui_settings_window_title": "Indstillinger",
- "gui_settings_stealth_label": "Usynlig (avanceret)",
"gui_settings_stealth_option": "Opret usynlige onion-tjenester",
- "gui_settings_stealth_option_details": "Det gør OnionShare mere sikker, men også mere besværlig for modtageren at oprette forbindelse til den.<br><a href=\"https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services\">Mere information</a>.",
"gui_settings_stealth_hidservauth_string": "Du har gemt den private nøgle til at blive brugt igen, så din HidServAuth-streng bruges også igen.\nKlik nedenfor, for at kopiere HidServAuth.",
"gui_settings_autoupdate_label": "Søg efter opdateringer",
"gui_settings_autoupdate_option": "Giv mig besked når der findes opdateringer",
diff --git a/share/locale/de.json b/share/locale/de.json
index 6c0fa861..1d0436a0 100644
--- a/share/locale/de.json
+++ b/share/locale/de.json
@@ -1,6 +1,5 @@
{
"preparing_files": "Dateien werden vorbereitet.",
- "wait_for_hs": "Warte auf HS:",
"give_this_url": "Geben Sie diese URL der Person, der Sie die Datei zusenden möchten:",
"ctrlc_to_stop": "Drücken Sie Strg+C um den Server anzuhalten",
"not_a_file": "{0:s} ist keine Datei.",
diff --git a/share/locale/en.json b/share/locale/en.json
index 7d3daba8..e5d9a3be 100644
--- a/share/locale/en.json
+++ b/share/locale/en.json
@@ -1,21 +1,19 @@
{
- "config_onion_service": "Configuring onion service on port {0:d}.",
- "preparing_files": "Preparing files to share.",
- "wait_for_hs": "Waiting for HS to be ready:",
- "give_this_url": "Give this address to the person you're sending the file to:",
- "give_this_url_stealth": "Give this address and HidServAuth line to the person you're sending the file to:",
- "give_this_url_receive": "Give this address to the people sending you files:",
- "give_this_url_receive_stealth": "Give this address and HidServAuth line to the people sending you files:",
+ "config_onion_service": "Setting up onion service on port {0:d}.",
+ "preparing_files": "Compressing files.",
+ "give_this_url": "Give this address to the recipient:",
+ "give_this_url_stealth": "Give this address and HidServAuth line to the recipient:",
+ "give_this_url_receive": "Give this address to the sender:",
+ "give_this_url_receive_stealth": "Give this address and HidServAuth to the sender:",
"ctrlc_to_stop": "Press Ctrl+C to stop the server",
"not_a_file": "{0:s} is not a valid file.",
"not_a_readable_file": "{0:s} is not a readable file.",
- "no_filenames": "You must specify a list of files to share.",
- "no_available_port": "Could not start the Onion service as there was no available port.",
+ "no_available_port": "Could not find an available port to start the onion service",
"other_page_loaded": "Address loaded",
- "close_on_timeout": "Stopped because timer expired",
+ "close_on_timeout": "Stopped because auto-stop timer ran out",
"closing_automatically": "Stopped because download finished",
"timeout_download_still_running": "Waiting for download to complete",
- "large_filesize": "Warning: Sending large files could take hours",
+ "large_filesize": "Warning: Sending a large share could take hours",
"systray_menu_exit": "Quit",
"systray_download_started_title": "OnionShare Download Started",
"systray_download_started_message": "A user started downloading your files",
@@ -25,69 +23,67 @@
"systray_download_canceled_message": "The user canceled the download",
"systray_upload_started_title": "OnionShare Upload Started",
"systray_upload_started_message": "A user started uploading files to your computer",
- "help_local_only": "Do not attempt to use Tor: For development only",
- "help_stay_open": "Keep onion service running after download has finished",
- "help_shutdown_timeout": "Shut down the onion service after N seconds",
- "help_stealth": "Create stealth onion service (advanced)",
- "help_receive": "Receive files instead of sending them",
- "help_debug": "Log application errors to stdout, and log web errors to disk",
+ "help_local_only": "Don't use Tor (only for development)",
+ "help_stay_open": "Keep sharing after first download",
+ "help_shutdown_timeout": "Stop sharing after a given amount of seconds",
+ "help_stealth": "Use client authorization (advanced)",
+ "help_receive": "Receive shares instead of sending them",
+ "help_debug": "Log OnionShare errors to stdout, and web errors to disk",
"help_filename": "List of files or folders to share",
- "help_config": "Path to a custom JSON config file (optional)",
+ "help_config": "Custom JSON config file location (optional)",
"gui_drag_and_drop": "Drag and drop files and folders\nto start sharing",
"gui_add": "Add",
"gui_delete": "Delete",
"gui_choose_items": "Choose",
- "gui_share_start_server": "Start Sharing",
- "gui_share_stop_server": "Stop Sharing",
+ "gui_share_start_server": "Start sharing",
+ "gui_share_stop_server": "Stop sharing",
"gui_share_stop_server_shutdown_timeout": "Stop Sharing ({}s remaining)",
- "gui_share_stop_server_shutdown_timeout_tooltip": "Share will expire automatically at {}",
+ "gui_share_stop_server_shutdown_timeout_tooltip": "Auto-stop timer ends at {}",
"gui_receive_start_server": "Start Receive Mode",
"gui_receive_stop_server": "Stop Receive Mode",
"gui_receive_stop_server_shutdown_timeout": "Stop Receive Mode ({}s remaining)",
- "gui_receive_stop_server_shutdown_timeout_tooltip": "Receive mode will expire automatically at {}",
+ "gui_receive_stop_server_shutdown_timeout_tooltip": "Auto-stop timer ends at {}",
"gui_copy_url": "Copy Address",
"gui_copy_hidservauth": "Copy HidServAuth",
"gui_downloads": "Download History",
- "gui_downloads_window_tooltip": "Show/hide downloads",
- "gui_no_downloads": "No downloads yet.",
+ "gui_no_downloads": "No Downloads Yet",
"gui_canceled": "Canceled",
- "gui_copied_url_title": "Copied OnionShare address",
- "gui_copied_url": "The OnionShare address has been copied to clipboard",
+ "gui_copied_url_title": "Copied OnionShare Address",
+ "gui_copied_url": "OnionShare address copied to clipboard",
"gui_copied_hidservauth_title": "Copied HidServAuth",
- "gui_copied_hidservauth": "The HidServAuth line has been copied to clipboard",
- "gui_please_wait": "Starting… Click to cancel",
- "using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
- "gui_download_upload_progress_complete": "%p%, Time Elapsed: {0:s}",
- "gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)",
+ "gui_copied_hidservauth": "HidServAuth line copied to clipboard",
+ "gui_please_wait": "Starting… Click to cancel.",
+ "gui_download_upload_progress_complete": "%p%, {0:s} elapsed.",
+ "gui_download_upload_progress_starting": "{0:s}, %p% (calculating)",
"gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%",
"version_string": "OnionShare {0:s} | https://onionshare.org/",
- "gui_quit_title": "Transfer in Progress",
+ "gui_quit_title": "Not so fast",
"gui_share_quit_warning": "You're in the process of sending files. Are you sure you want to quit OnionShare?",
"gui_receive_quit_warning": "You're in the process of receiving files. Are you sure you want to quit OnionShare?",
"gui_quit_warning_quit": "Quit",
"gui_quit_warning_dont_quit": "Cancel",
- "error_rate_limit": "An attacker might be trying to guess your address. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new address.",
- "zip_progress_bar_format": "Compressing files: %p%",
- "error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.",
- "error_ephemeral_not_supported": "OnionShare requires at least Tor 0.2.7.1 and at least python3-stem 1.4.0.",
+ "error_rate_limit": "Someone has made too many wrong attempts on your address, which means they could be trying to guess it, so OnionShare has stopped the server. Start sharing again and send the receipient a new address to share.",
+ "zip_progress_bar_format": "Compressing: %p%",
+ "error_stealth_not_supported": "To use client authorization, you need at least both Tor 0.2.9.1-alpha (or Tor Browser 6.5) and python3-stem 1.5.0.",
+ "error_ephemeral_not_supported": "OnionShare requires at least both Tor 0.2.7.1 and python3-stem 1.4.0.",
"gui_settings_window_title": "Settings",
- "gui_settings_whats_this": "<a href='{0:s}'>what's this?</a>",
- "gui_settings_stealth_option": "Create stealth onion services (legacy)",
- "gui_settings_stealth_hidservauth_string": "You have saved the private key for reuse, so your HidServAuth string is also reused.\nClick below to copy the HidServAuth.",
- "gui_settings_autoupdate_label": "Check for upgrades",
- "gui_settings_autoupdate_option": "Notify me when upgrades are available",
+ "gui_settings_whats_this": "<a href='{0:s}'>What's this?</a>",
+ "gui_settings_stealth_option": "Use client authorization (legacy)",
+ "gui_settings_stealth_hidservauth_string": "Having saved your private key for reuse, means you can now\nclick to copy your HidServAuth.",
+ "gui_settings_autoupdate_label": "Check for new version",
+ "gui_settings_autoupdate_option": "Notify me when a new version is available",
"gui_settings_autoupdate_timestamp": "Last checked: {}",
"gui_settings_autoupdate_timestamp_never": "Never",
- "gui_settings_autoupdate_check_button": "Check For Upgrades",
+ "gui_settings_autoupdate_check_button": "Check for New Version",
"gui_settings_general_label": "General settings",
"gui_settings_sharing_label": "Sharing settings",
"gui_settings_close_after_first_download_option": "Stop sharing after first download",
"gui_settings_connection_type_label": "How should OnionShare connect to Tor?",
- "gui_settings_connection_type_bundled_option": "Use the Tor version that is bundled with OnionShare",
- "gui_settings_connection_type_automatic_option": "Attempt automatic configuration with Tor Browser",
+ "gui_settings_connection_type_bundled_option": "Use the Tor version built into OnionShare",
+ "gui_settings_connection_type_automatic_option": "Attempt auto-configuration with Tor Browser",
"gui_settings_connection_type_control_port_option": "Connect using control port",
"gui_settings_connection_type_socket_file_option": "Connect using socket file",
- "gui_settings_connection_type_test_button": "Test Tor Settings",
+ "gui_settings_connection_type_test_button": "Test Connection to Tor",
"gui_settings_control_port_label": "Control port",
"gui_settings_socket_file_label": "Socket file",
"gui_settings_socks_label": "SOCKS port",
@@ -95,77 +91,77 @@
"gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication",
"gui_settings_authenticate_password_option": "Password",
"gui_settings_password_label": "Password",
- "gui_settings_tor_bridges": "Tor Bridge support",
+ "gui_settings_tor_bridges": "Tor bridge support",
"gui_settings_tor_bridges_no_bridges_radio_option": "Don't use bridges",
"gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 pluggable transports",
"gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Use built-in obfs4 pluggable transports (requires obfs4proxy)",
"gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek_lite (Azure) pluggable transports",
"gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy": "Use built-in meek_lite (Azure) pluggable transports (requires obfs4proxy)",
- "gui_settings_meek_lite_expensive_warning": "Warning: the meek_lite bridges are very costly for the Tor Project to run!<br><br>You should only use meek_lite bridges if you are having trouble connecting to Tor directly, via obfs4 transports or other normal bridges.",
+ "gui_settings_meek_lite_expensive_warning": "Warning: The meek_lite bridges are very costly for the Tor Project to run.<br><br>Only use them if unable to connect to Tor directly, via obfs4 transports, or other normal bridges.",
"gui_settings_tor_bridges_custom_radio_option": "Use custom bridges",
"gui_settings_tor_bridges_custom_label": "You can get bridges from <a href=\"https://bridges.torproject.org/options\">https://bridges.torproject.org</a>",
- "gui_settings_tor_bridges_invalid": "None of the bridges you supplied seem to work.\nPlease try again by double-checking them or adding other ones.",
+ "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.",
"gui_settings_button_save": "Save",
"gui_settings_button_cancel": "Cancel",
"gui_settings_button_help": "Help",
"gui_settings_shutdown_timeout_checkbox": "Use auto-stop timer",
"gui_settings_shutdown_timeout": "Stop the share at:",
- "settings_saved": "Settings saved to {}",
- "settings_error_unknown": "Can't connect to Tor controller because the settings don't make sense.",
- "settings_error_automatic": "Can't connect to Tor controller. Is Tor Browser running in the background? If you don't have it you can get it from:\nhttps://www.torproject.org/.",
- "settings_error_socket_port": "Can't connect to Tor controller on {}:{}.",
- "settings_error_socket_file": "Can't connect to Tor controller using socket file {}.",
+ "settings_saved": "Settings saved in {}",
+ "settings_error_unknown": "Can't connect to Tor controller because your settings don't make sense.",
+ "settings_error_automatic": "Could not connect to the Tor controller. Is Tor Browser (available from torproject.org) running in the background?",
+ "settings_error_socket_port": "Can't connect to the Tor controller at {}:{}.",
+ "settings_error_socket_file": "Can't connect to the Tor controller using socket file {}.",
"settings_error_auth": "Connected to {}:{}, but can't authenticate. Maybe this isn't a Tor controller?",
"settings_error_missing_password": "Connected to Tor controller, but it requires a password to authenticate.",
- "settings_error_unreadable_cookie_file": "Connected to Tor controller, but can't authenticate because your password may be wrong, and your user lacks permission to read the cookie file.",
- "settings_error_bundled_tor_not_supported": "Use of the Tor version bundled with OnionShare is not supported when using developer mode on Windows or macOS.",
- "settings_error_bundled_tor_timeout": "Connecting to Tor is taking too long. Maybe your computer is offline, or your system clock isn't accurate.",
+ "settings_error_unreadable_cookie_file": "Connected to the Tor controller, but password may be wrong, or your user is not permitted to read the cookie file.",
+ "settings_error_bundled_tor_not_supported": "Using the Tor version that comes with OnionShare does not work in developer mode on Windows or macOS.",
+ "settings_error_bundled_tor_timeout": "Taking too long to connect to Tor. Maybe you aren't connected to the Internet, or have an inaccurate system clock?",
"settings_error_bundled_tor_broken": "OnionShare could not connect to Tor in the background:\n{}",
- "settings_test_success": "Congratulations, OnionShare can connect to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}\nSupports stealth onion services: {}",
+ "settings_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports client authentication: {}.\nSupports next-gen .onion addresses: {}.",
"error_tor_protocol_error": "There was an error with Tor: {}",
"error_tor_protocol_error_unknown": "There was an unknown error with Tor",
"error_invalid_private_key": "This private key type is unsupported",
"connecting_to_tor": "Connecting to the Tor network",
- "update_available": "A new version of OnionShare is available. <a href='{}'>Click here</a> to download it.<br><br>Installed version: {}<br>Latest version: {}",
- "update_error_check_error": "Error checking for updates: Maybe you're not connected to Tor, or maybe the OnionShare website is down.",
- "update_error_invalid_latest_version": "Error checking for updates: The OnionShare website responded saying the latest version is '{}', but that doesn't appear to be a valid version string.",
- "update_not_available": "You are running the latest version of OnionShare.",
- "gui_tor_connection_ask": "Would you like to open OnionShare settings to troubleshoot connecting to Tor?",
- "gui_tor_connection_ask_open_settings": "Open Settings",
+ "update_available": "New OnionShare out. <a href='{}'>Click here</a> to get it.<br><br>You are using {} and the latest is {}.",
+ "update_error_check_error": "Could not check for new versions: The OnionShare website is saying the latest version is the unrecognizable '{}'…",
+ "update_error_invalid_latest_version": "Could not check for new version: Maybe you're not connected to Tor, or the OnionShare website is down?",
+ "update_not_available": "You are running the latest OnionShare.",
+ "gui_tor_connection_ask": "Open the settings to sort out connection to Tor?",
+ "gui_tor_connection_ask_open_settings": "Yes",
"gui_tor_connection_ask_quit": "Quit",
- "gui_tor_connection_error_settings": "Try adjusting how OnionShare connects to the Tor network in Settings.",
- "gui_tor_connection_canceled": "OnionShare could not connect to Tor.\n\nMake sure you're connected to the Internet, then re-open OnionShare to set up the Tor connection.",
+ "gui_tor_connection_error_settings": "Try changing how OnionShare connects to the Tor network in the settings.",
+ "gui_tor_connection_canceled": "Could not connect to Tor.\n\nEnsure you are connected to the Internet, then re-open OnionShare and set up its connection to Tor.",
"gui_tor_connection_lost": "Disconnected from Tor.",
- "gui_server_started_after_timeout": "The server started after your chosen auto-timeout.\nPlease start a new share.",
- "gui_server_timeout_expired": "The chosen timeout has already expired.\nPlease update the timeout and then you may start sharing.",
- "share_via_onionshare": "Share via OnionShare",
+ "gui_server_started_after_timeout": "The auto-stop timer ran out before the server started.\nPlease make a new share.",
+ "gui_server_timeout_expired": "The auto-stop timer already ran out.\nPlease update it to start sharing.",
+ "share_via_onionshare": "OnionShare it",
"gui_use_legacy_v2_onions_checkbox": "Use legacy addresses",
"gui_save_private_key_checkbox": "Use a persistent address (legacy)",
- "gui_share_url_description": "<b>Anyone</b> with this link can <b>download</b> your files using the <b>Tor Browser</b>: <img src='{}' />",
- "gui_receive_url_description": "<b>Anyone</b> with this link can <b>upload</b> files to your computer using the <b>Tor Browser</b>: <img src='{}' />",
- "gui_url_label_persistent": "This share will not expire automatically unless a timer is set.<br><br>Every share will have the same address (to use one-time addresses, disable persistence in Settings)",
- "gui_url_label_stay_open": "This share will not expire automatically unless a timer is set.",
- "gui_url_label_onetime": "This share will expire after the first download",
- "gui_url_label_onetime_and_persistent": "This share will expire after the first download<br><br>Every share will have the same address (to use one-time addresses, disable persistence in the Settings)",
- "gui_status_indicator_share_stopped": "Ready to Share",
+ "gui_share_url_description": "<b>Anyone</b> with this OnionShare address can <b>download</b> your files using the <b>Tor Browser</b>: <img src='{}' />",
+ "gui_receive_url_description": "<b>Anyone</b> with this OnionShare address can <b>upload</b> files to your computer using the <b>Tor Browser</b>: <img src='{}' />",
+ "gui_url_label_persistent": "This share will not auto-stop.<br><br>Every subsequent share reuses the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)",
+ "gui_url_label_stay_open": "This share will not auto-stop.",
+ "gui_url_label_onetime": "This share will stop after first completion.",
+ "gui_url_label_onetime_and_persistent": "This share will not auto-stop.<br><br>Every subsequent share will reuse the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)",
+ "gui_status_indicator_share_stopped": "Ready to share",
"gui_status_indicator_share_working": "Starting…",
"gui_status_indicator_share_started": "Sharing",
- "gui_status_indicator_receive_stopped": "Ready to Receive",
+ "gui_status_indicator_receive_stopped": "Ready to receive",
"gui_status_indicator_receive_working": "Starting…",
"gui_status_indicator_receive_started": "Receiving",
- "gui_file_info": "{} Files, {}",
- "gui_file_info_single": "{} File, {}",
- "info_in_progress_downloads_tooltip": "{} download(s) in progress",
- "info_completed_downloads_tooltip": "{} download(s) completed",
+ "gui_file_info": "{} files, {}",
+ "gui_file_info_single": "{} file, {}",
+ "history_in_progress_tooltip": "{} in progress",
+ "history_completed_tooltip": "{} completed",
"info_in_progress_uploads_tooltip": "{} upload(s) in progress",
"info_completed_uploads_tooltip": "{} upload(s) completed",
- "error_cannot_create_downloads_dir": "Error creating downloads folder: {}",
- "error_downloads_dir_not_writable": "The downloads folder isn't writable: {}",
- "receive_mode_downloads_dir": "Files people send you will appear in this folder: {}",
- "receive_mode_warning": "Warning: Receive mode lets someone else upload files to your computer. Some files can hack your computer if you open them! Only open files from people you trust, or if you know what you're doing.",
- "gui_receive_mode_warning": "<b>Some files can hack your computer if you open them!</b><br>Only open files from people you trust, or if you know what you're doing.",
+ "error_cannot_create_downloads_dir": "Could not create receive mode folder: {}",
+ "error_downloads_dir_not_writable": "The receive mode folder is write protected: {}",
+ "receive_mode_downloads_dir": "Files sent to you appear in this folder: {}",
+ "receive_mode_warning": "Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.",
+ "gui_receive_mode_warning": "Receive mode lets people upload files to your computer.<br><br><b>Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.</b>",
"receive_mode_upload_starting": "Upload of total size {} is starting",
- "receive_mode_received_file": "Received file: {}",
+ "receive_mode_received_file": "Received: {}",
"gui_mode_share_button": "Share Files",
"gui_mode_receive_button": "Receive Files",
"gui_settings_receiving_label": "Receiving settings",
@@ -179,10 +175,11 @@
"systray_download_page_loaded_message": "A user loaded the download page",
"systray_upload_page_loaded_message": "A user loaded the upload page",
"gui_uploads": "Upload History",
- "gui_uploads_window_tooltip": "Show/hide uploads",
- "gui_no_uploads": "No uploads yet.",
+ "gui_no_uploads": "No Uploads Yet",
+ "gui_clear_history": "Clear All",
"gui_upload_in_progress": "Upload Started {}",
"gui_upload_finished_range": "Uploaded {} to {}",
"gui_upload_finished": "Uploaded {}",
- "gui_open_folder_error_nautilus": "Cannot open folder the because nautilus is not available. You can find this file here: {}"
+ "gui_download_in_progress": "Download Started {}",
+ "gui_open_folder_error_nautilus": "Cannot open folder because nautilus is not available. The file is here: {}"
}
diff --git a/share/locale/eo.json b/share/locale/eo.json
index 18e73165..9902e4ae 100644
--- a/share/locale/eo.json
+++ b/share/locale/eo.json
@@ -1,7 +1,6 @@
{
"config_onion_service": "Agordas onion service je pordo {0:d}.",
"preparing_files": "Preparas dosierojn por kundivido.",
- "wait_for_hs": "Atendas al hidden sevice por esti preta:",
"give_this_url": "Donu ĉi tiun URL al la persono al kiu vi sendas la dosieron:",
"give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:",
"ctrlc_to_stop": "Presu Ctrl-C por halti la servilon",
@@ -27,7 +26,6 @@
"gui_copied_url": "URL kopiita en tondujon",
"gui_copied_hidservauth": "Copied HidServAuth line to clipboard",
"gui_please_wait": "Bonvolu atendi...",
- "using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
"gui_download_upload_progress_complete": "%p%, Tempo pasinta: {0:s}",
"gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)",
"gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%",
diff --git a/share/locale/es.json b/share/locale/es.json
index b829540a..8c9945a8 100644
--- a/share/locale/es.json
+++ b/share/locale/es.json
@@ -1,6 +1,5 @@
{
"preparing_files": "Preparando los archivos para compartir.",
- "wait_for_hs": "Esperando a que HS esté listo:",
"give_this_url": "Entregue esta URL a la persona a la que está enviando el archivo:",
"ctrlc_to_stop": "Pulse Ctrl-C para detener el servidor",
"not_a_file": "{0:s} no es un archivo.",
diff --git a/share/locale/fi.json b/share/locale/fi.json
index 09186be8..d0ee80ff 100644
--- a/share/locale/fi.json
+++ b/share/locale/fi.json
@@ -1,6 +1,5 @@
{
"preparing_files": "Valmistellaan tiedostoja jaettavaksi.",
- "wait_for_hs": "Odotetaan piilopalvelun valmistumista:",
"give_this_url": "Anna tämä URL-osoite henkilölle, jolle lähetät tiedostot:",
"ctrlc_to_stop": "Näppäin Ctrl-C pysäyttää palvelimen",
"not_a_file": "{0:s} Ei ole tiedosto.",
@@ -22,6 +21,5 @@
"gui_canceled": "Peruutettu",
"gui_copied_url": "URL-osoite kopioitu leikepöydälle",
"gui_please_wait": "Odota...",
- "using_ephemeral": "Käynnistetään lyhytaikainen Tor piilopalvelu ja odotetaan julkaisua",
"zip_progress_bar_format": "Tiivistän tiedostoja: %p%"
}
diff --git a/share/locale/fr.json b/share/locale/fr.json
index b6f6eaa7..967e456e 100644
--- a/share/locale/fr.json
+++ b/share/locale/fr.json
@@ -1,6 +1,5 @@
{
"preparing_files": "Préparation des fichiers à partager.",
- "wait_for_hs": "En attente du HS:",
"give_this_url": "Donnez cette URL à la personne qui doit recevoir le fichier :",
"ctrlc_to_stop": "Ctrl-C arrête le serveur",
"not_a_file": "{0:s} n'est pas un fichier.",
diff --git a/share/locale/it.json b/share/locale/it.json
index 304e0cb9..ebe2df4e 100644
--- a/share/locale/it.json
+++ b/share/locale/it.json
@@ -1,6 +1,5 @@
{
"preparing_files": "Preparazione dei files da condividere.",
- "wait_for_hs": "In attesa che l'HS sia pronto:",
"give_this_url": "Dai questo URL alla persona a cui vuoi inviare il file:",
"ctrlc_to_stop": "Premi Ctrl-C per fermare il server",
"not_a_file": "{0:s} non è un file.",
@@ -22,6 +21,5 @@
"gui_canceled": "Cancellati",
"gui_copied_url": "URL Copiato nella clipboard",
"gui_please_wait": "Attendere prego...",
- "using_ephemeral": "Avviamento del servizio nascosto Tor ephemeral e attesa della pubblicazione",
"zip_progress_bar_format": "Elaborazione files: %p%"
}
diff --git a/share/locale/nl.json b/share/locale/nl.json
index 67297ae0..abd14753 100644
--- a/share/locale/nl.json
+++ b/share/locale/nl.json
@@ -1,7 +1,6 @@
{
"config_onion_service": "Onion service configureren op poort {0:d}.",
"preparing_files": "Bestanden om te delen aan het voorbereiden.",
- "wait_for_hs": "Wachten op gereed zijn van HS:",
"give_this_url": "Geef deze URL aan de persoon aan wie je dit bestand verzend:",
"give_this_url_stealth": "Geef deze URL en de HidServAuth regel aan de persoon aan wie je dit bestand verzend:",
"ctrlc_to_stop": "Druk Ctrl-C om de server te stoppen",
@@ -38,7 +37,6 @@
"gui_copied_url": "URL gekopieerd naar klembord",
"gui_copied_hidservauth": "HidServAuth regel gekopieerd naar klembord",
"gui_please_wait": "Moment geduld...",
- "using_ephemeral": "Kortstondige Tor onion service gestart en in afwachting van publicatie",
"gui_download_upload_progress_complete": "%p%, Tijd verstreken: {0:s}",
"gui_download_upload_progress_starting": "{0:s}, %p% (ETA berekenen)",
"gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%",
@@ -51,9 +49,7 @@
"error_stealth_not_supported": "Om een geheime onion service te maken heb je minstens Tor 0.2.9.1-alpha (of Tor Browser 6.5) en minstens python3-stem 1.5.0 nodig.",
"error_ephemeral_not_supported": "OnionShare vereist minstens Tor 0.2.7.1 en minstens python3-stem 1.4.0.",
"gui_settings_window_title": "Instellingen",
- "gui_settings_stealth_label": "Stealth (geavanceerd)",
"gui_settings_stealth_option": "Maak stealth onion services",
- "gui_settings_stealth_option_details": "Dit maakt OnionShare veiliger, maar ook lastiger voor de ontvanger om te verbinden.<br><a href=\"https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services\">Meer informatie</a>.",
"gui_settings_autoupdate_label": "Controleer voor updates",
"gui_settings_autoupdate_option": "Notificeer me als er updates beschikbaar zijn",
"gui_settings_autoupdate_timestamp": "Laatste controle: {}",
diff --git a/share/locale/tr.json b/share/locale/tr.json
index 7b531bd6..68807410 100644
--- a/share/locale/tr.json
+++ b/share/locale/tr.json
@@ -1,6 +1,5 @@
{
"preparing_files": "Paylaşmak için dosyalar hazırlanıyor.",
- "wait_for_hs": "GH hazır olması bekleniyor:",
"give_this_url": "Dosyayı gönderdiğin kişiye bu URL'i verin:",
"ctrlc_to_stop": "Sunucuyu durdurmak için, Ctrl-C basın",
"not_a_file": "{0:s} dosya değil.",
@@ -22,6 +21,5 @@
"gui_canceled": "İptal edilen",
"gui_copied_url": "Panoya kopyalanan URL",
"gui_please_wait": "Lütfen bekleyin...",
- "using_ephemeral": "Geçici Tor gizli hizmetine bakılıyor ve yayımı bekleniyor",
"zip_progress_bar_format": "Dosyalar hazırlanıyor: %p%"
}
diff --git a/share/templates/send.html b/share/templates/send.html
index df1d3563..e7e1fde0 100644
--- a/share/templates/send.html
+++ b/share/templates/send.html
@@ -10,18 +10,18 @@
<body>
<header class="clearfix">
- <div class="right">
- <ul>
- <li>Total size: <strong>{{ filesize_human }}</strong> (compressed)</li>
- {% if slug %}
- <li><a class="button" href='/{{ slug }}/download'>Download Files</a></li>
- {% else %}
- <li><a class="button" href='/download'>Download Files</a></li>
- {% endif %}
- </ul>
- </div>
- <img class="logo" src="/static/img/logo.png" title="OnionShare">
- <h1>OnionShare</h1>
+ <div class="right">
+ <ul>
+ <li>Total size: <strong>{{ filesize_human }}</strong> {% if is_zipped %} (compressed){% endif %}</li>
+ {% if slug %}
+ <li><a class="button" href='/{{ slug }}/download'>Download Files</a></li>
+ {% else %}
+ <li><a class="button" href='/download'>Download Files</a></li>
+ {% endif %}
+ </ul>
+ </div>
+ <img class="logo" src="/static/img/logo.png" title="OnionShare">
+ <h1>OnionShare</h1>
</header>
<table class="file-list" id="file-list">
diff --git a/stdeb.cfg b/stdeb.cfg
index e190fe8b..2fc3d3bf 100644
--- a/stdeb.cfg
+++ b/stdeb.cfg
@@ -1,6 +1,6 @@
[DEFAULT]
Package3: onionshare
-Depends3: python3-flask, python3-stem, python3-pyqt5, python-nautilus, tor, obfs4proxy
-Build-Depends: python3-pytest, python3-flask, python3-stem, python3-pyqt5
+Depends3: python3-flask, python3-stem, python3-pyqt5, python3-cryptography, python3-crypto, python3-nacl, python3-socks, python-nautilus, tor, obfs4proxy
+Build-Depends: python3-pytest, python3-flask, python3-stem, python3-pyqt5, python3-cryptography, python3-crypto, python3-nacl, python3-socks, python-nautilus, tor, obfs4proxy
Suite: bionic
X-Python3-Version: >= 3.6
diff --git a/test/__init__.py b/tests/__init__.py
index e69de29b..e69de29b 100644
--- a/test/__init__.py
+++ b/tests/__init__.py
diff --git a/test/conftest.py b/tests/conftest.py
index 610a43ea..8ac7efb8 100644
--- a/test/conftest.py
+++ b/tests/conftest.py
@@ -64,7 +64,7 @@ def temp_file_1024_delete():
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture(scope='session')
def custom_zw():
- zw = web.ZipWriter(
+ zw = web.share_mode.ZipWriter(
common.Common(),
zip_filename=common.Common.random_string(4, 6),
processed_size_callback=lambda _: 'custom_callback'
@@ -77,7 +77,7 @@ def custom_zw():
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture(scope='session')
def default_zw():
- zw = web.ZipWriter(common.Common())
+ zw = web.share_mode.ZipWriter(common.Common())
yield zw
zw.close()
tmp_dir = os.path.dirname(zw.zip_filename)
diff --git a/test/test_helpers.py b/tests/test_helpers.py
index 321afbb7..321afbb7 100644
--- a/test/test_helpers.py
+++ b/tests/test_helpers.py
diff --git a/test/test_onionshare.py b/tests/test_onionshare.py
index 7592a777..7592a777 100644
--- a/test/test_onionshare.py
+++ b/tests/test_onionshare.py
diff --git a/test/test_onionshare_common.py b/tests/test_onionshare_common.py
index d70f2c0e..d70f2c0e 100644
--- a/test/test_onionshare_common.py
+++ b/tests/test_onionshare_common.py
diff --git a/test/test_onionshare_settings.py b/tests/test_onionshare_settings.py
index 1f1ef528..1f1ef528 100644
--- a/test/test_onionshare_settings.py
+++ b/tests/test_onionshare_settings.py
diff --git a/test/test_onionshare_strings.py b/tests/test_onionshare_strings.py
index d1daa1e5..d3d40c8f 100644
--- a/test/test_onionshare_strings.py
+++ b/tests/test_onionshare_strings.py
@@ -47,22 +47,14 @@ class TestLoadStrings:
self, common_obj, locale_en, sys_onionshare_dev_mode):
""" load_strings() loads English by default """
strings.load_strings(common_obj)
- assert strings._('wait_for_hs') == "Waiting for HS to be ready:"
+ assert strings._('preparing_files') == "Compressing files."
def test_load_strings_loads_other_languages(
self, common_obj, locale_fr, sys_onionshare_dev_mode):
""" load_strings() loads other languages in different locales """
strings.load_strings(common_obj, "fr")
- assert strings._('wait_for_hs') == "En attente du HS:"
-
- def test_load_partial_strings(
- self, common_obj, locale_ru, sys_onionshare_dev_mode):
- strings.load_strings(common_obj)
- assert strings._("give_this_url") == (
- "Отправьте эту ссылку тому человеку, "
- "которому вы хотите передать файл:")
- assert strings._('wait_for_hs') == "Waiting for HS to be ready:"
+ assert strings._('preparing_files') == "Préparation des fichiers à partager."
def test_load_invalid_locale(
self, common_obj, locale_invalid, sys_onionshare_dev_mode):
diff --git a/test/test_onionshare_web.py b/tests/test_onionshare_web.py
index 2209a0fd..24a0e163 100644
--- a/test/test_onionshare_web.py
+++ b/tests/test_onionshare_web.py
@@ -38,11 +38,11 @@ DEFAULT_ZW_FILENAME_REGEX = re.compile(r'^onionshare_[a-z2-7]{6}.zip$')
RANDOM_STR_REGEX = re.compile(r'^[a-z2-7]+$')
-def web_obj(common_obj, receive_mode, num_files=0):
+def web_obj(common_obj, mode, num_files=0):
""" Creates a Web object, in either share mode or receive mode, ready for testing """
common_obj.load_settings()
- web = Web(common_obj, False, receive_mode)
+ web = Web(common_obj, False, mode)
web.generate_slug()
web.stay_open = True
web.running = True
@@ -50,14 +50,14 @@ def web_obj(common_obj, receive_mode, num_files=0):
web.app.testing = True
# Share mode
- if not receive_mode:
+ if mode == 'share':
# Add files
files = []
for i in range(num_files):
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
tmp_file.write(b'*' * 1024)
files.append(tmp_file.name)
- web.set_file_info(files)
+ web.share_mode.set_file_info(files)
# Receive mode
else:
pass
@@ -67,8 +67,8 @@ def web_obj(common_obj, receive_mode, num_files=0):
class TestWeb:
def test_share_mode(self, common_obj):
- web = web_obj(common_obj, False, 3)
- assert web.receive_mode is False
+ web = web_obj(common_obj, 'share', 3)
+ assert web.mode is 'share'
with web.app.test_client() as c:
# Load 404 pages
res = c.get('/')
@@ -91,7 +91,7 @@ class TestWeb:
assert res.mimetype == 'application/zip'
def test_share_mode_close_after_first_download_on(self, common_obj, temp_file_1024):
- web = web_obj(common_obj, False, 3)
+ web = web_obj(common_obj, 'share', 3)
web.stay_open = False
assert web.running == True
@@ -106,7 +106,7 @@ class TestWeb:
assert web.running == False
def test_share_mode_close_after_first_download_off(self, common_obj, temp_file_1024):
- web = web_obj(common_obj, False, 3)
+ web = web_obj(common_obj, 'share', 3)
web.stay_open = True
assert web.running == True
@@ -120,8 +120,8 @@ class TestWeb:
assert web.running == True
def test_receive_mode(self, common_obj):
- web = web_obj(common_obj, True)
- assert web.receive_mode is True
+ web = web_obj(common_obj, 'receive')
+ assert web.mode is 'receive'
with web.app.test_client() as c:
# Load 404 pages
@@ -139,7 +139,7 @@ class TestWeb:
assert res.status_code == 200
def test_receive_mode_allow_receiver_shutdown_on(self, common_obj):
- web = web_obj(common_obj, True)
+ web = web_obj(common_obj, 'receive')
common_obj.settings.set('receive_allow_receiver_shutdown', True)
@@ -154,7 +154,7 @@ class TestWeb:
assert web.running == False
def test_receive_mode_allow_receiver_shutdown_off(self, common_obj):
- web = web_obj(common_obj, True)
+ web = web_obj(common_obj, 'receive')
common_obj.settings.set('receive_allow_receiver_shutdown', False)
@@ -167,9 +167,9 @@ class TestWeb:
# Should redirect to index, and server should still be running
assert res.status_code == 302
assert web.running == True
-
+
def test_public_mode_on(self, common_obj):
- web = web_obj(common_obj, True)
+ web = web_obj(common_obj, 'receive')
common_obj.settings.set('public_mode', True)
with web.app.test_client() as c:
@@ -182,9 +182,9 @@ class TestWeb:
res = c.get('/{}'.format(web.slug))
data2 = res.get_data()
assert res.status_code == 404
-
+
def test_public_mode_off(self, common_obj):
- web = web_obj(common_obj, True)
+ web = web_obj(common_obj, 'receive')
common_obj.settings.set('public_mode', False)
with web.app.test_client() as c:
diff --git a/tests_gui_local/__init__.py b/tests_gui_local/__init__.py
new file mode 100644
index 00000000..bb2b2182
--- /dev/null
+++ b/tests_gui_local/__init__.py
@@ -0,0 +1 @@
+from .commontests import CommonTests
diff --git a/tests_gui_local/commontests.py b/tests_gui_local/commontests.py
new file mode 100644
index 00000000..39011b4a
--- /dev/null
+++ b/tests_gui_local/commontests.py
@@ -0,0 +1,291 @@
+import os
+import requests
+import socket
+import socks
+import zipfile
+
+from PyQt5 import QtCore, QtTest
+from onionshare import strings
+from onionshare_gui.mode.receive_mode import ReceiveMode
+from onionshare_gui.mode.share_mode import ShareMode
+
+
+class CommonTests(object):
+ def test_gui_loaded(self):
+ '''Test that the GUI actually is shown'''
+ self.assertTrue(self.gui.show)
+
+ def test_windowTitle_seen(self):
+ '''Test that the window title is OnionShare'''
+ self.assertEqual(self.gui.windowTitle(), 'OnionShare')
+
+ def test_settings_button_is_visible(self):
+ '''Test that the settings button is visible'''
+ self.assertTrue(self.gui.settings_button.isVisible())
+
+ def test_server_status_bar_is_visible(self):
+ '''Test that the status bar is visible'''
+ self.assertTrue(self.gui.status_bar.isVisible())
+
+ def test_click_mode(self, mode):
+ '''Test that we can switch Mode by clicking the button'''
+ if type(mode) == ReceiveMode:
+ QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton)
+ self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE)
+ if type(mode) == ShareMode:
+ QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton)
+ self.assertTrue(self.gui.mode, self.gui.MODE_SHARE)
+
+ def test_click_toggle_history(self, mode):
+ '''Test that we can toggle Download or Upload history by clicking the toggle button'''
+ currently_visible = mode.history.isVisible()
+ QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton)
+ self.assertEqual(mode.history.isVisible(), not currently_visible)
+
+ def test_history_indicator(self, mode, public_mode):
+ '''Test that we can make sure the history is toggled off, do an action, and the indiciator works'''
+ # Make sure history is toggled off
+ if mode.history.isVisible():
+ QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton)
+ self.assertFalse(mode.history.isVisible())
+
+ # Indicator should not be visible yet
+ self.assertFalse(mode.toggle_history.indicator_label.isVisible())
+
+ if type(mode) == ReceiveMode:
+ # Upload a file
+ files = {'file[]': open('/tmp/test.txt', 'rb')}
+ if not public_mode:
+ path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, mode.web.slug)
+ else:
+ path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port)
+ response = requests.post(path, files=files)
+ QtTest.QTest.qWait(2000)
+
+ if type(mode) == ShareMode:
+ # Download files
+ if public_mode:
+ url = "http://127.0.0.1:{}/download".format(self.gui.app.port)
+ else:
+ url = "http://127.0.0.1:{}/{}/download".format(self.gui.app.port, self.gui.share_mode.web.slug)
+ r = requests.get(url)
+ QtTest.QTest.qWait(2000)
+
+ # Indicator should be visible, have a value of "1"
+ self.assertTrue(mode.toggle_history.indicator_label.isVisible())
+ self.assertEqual(mode.toggle_history.indicator_label.text(), "1")
+
+ # Toggle history back on, indicator should be hidden again
+ QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton)
+ self.assertFalse(mode.toggle_history.indicator_label.isVisible())
+
+ def test_history_is_not_visible(self, mode):
+ '''Test that the History section is not visible'''
+ self.assertFalse(mode.history.isVisible())
+
+ def test_history_is_visible(self, mode):
+ '''Test that the History section is visible'''
+ self.assertTrue(mode.history.isVisible())
+
+ def test_server_working_on_start_button_pressed(self, mode):
+ '''Test we can start the service'''
+ # Should be in SERVER_WORKING state
+ QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton)
+ self.assertEqual(mode.server_status.status, 1)
+
+ def test_server_status_indicator_says_starting(self, mode):
+ '''Test that the Server Status indicator shows we are Starting'''
+ self.assertEquals(mode.server_status_label.text(), strings._('gui_status_indicator_share_working'))
+
+ def test_settings_button_is_hidden(self):
+ '''Test that the settings button is hidden when the server starts'''
+ self.assertFalse(self.gui.settings_button.isVisible())
+
+ def test_a_server_is_started(self, mode):
+ '''Test that the server has started'''
+ QtTest.QTest.qWait(2000)
+ # Should now be in SERVER_STARTED state
+ self.assertEqual(mode.server_status.status, 2)
+
+ def test_a_web_server_is_running(self):
+ '''Test that the web server has started'''
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+
+ self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0)
+
+ def test_have_a_slug(self, mode, public_mode):
+ '''Test that we have a valid slug'''
+ if not public_mode:
+ self.assertRegex(mode.server_status.web.slug, r'(\w+)-(\w+)')
+ else:
+ self.assertIsNone(mode.server_status.web.slug, r'(\w+)-(\w+)')
+
+
+ def test_url_description_shown(self, mode):
+ '''Test that the URL label is showing'''
+ self.assertTrue(mode.server_status.url_description.isVisible())
+
+ def test_have_copy_url_button(self, mode):
+ '''Test that the Copy URL button is shown'''
+ self.assertTrue(mode.server_status.copy_url_button.isVisible())
+
+ def test_server_status_indicator_says_started(self, mode):
+ '''Test that the Server Status indicator shows we are started'''
+ if type(mode) == ReceiveMode:
+ self.assertEquals(mode.server_status_label.text(), strings._('gui_status_indicator_receive_started'))
+ if type(mode) == ShareMode:
+ self.assertEquals(mode.server_status_label.text(), strings._('gui_status_indicator_share_started'))
+
+ def test_web_page(self, mode, string, public_mode):
+ '''Test that the web page contains a string'''
+ s = socks.socksocket()
+ s.settimeout(60)
+ s.connect(('127.0.0.1', self.gui.app.port))
+
+ if not public_mode:
+ path = '/{}'.format(mode.server_status.web.slug)
+ else:
+ path = '/'
+
+ http_request = 'GET {} HTTP/1.0\r\n'.format(path)
+ http_request += 'Host: 127.0.0.1\r\n'
+ http_request += '\r\n'
+ s.sendall(http_request.encode('utf-8'))
+
+ with open('/tmp/webpage', 'wb') as file_to_write:
+ while True:
+ data = s.recv(1024)
+ if not data:
+ break
+ file_to_write.write(data)
+ file_to_write.close()
+
+ f = open('/tmp/webpage')
+ self.assertTrue(string in f.read())
+ f.close()
+
+ def test_history_widgets_present(self, mode):
+ '''Test that the relevant widgets are present in the history view after activity has taken place'''
+ self.assertFalse(mode.history.empty.isVisible())
+ self.assertTrue(mode.history.not_empty.isVisible())
+
+ def test_counter_incremented(self, mode, count):
+ '''Test that the counter has incremented'''
+ self.assertEquals(mode.history.completed_count, count)
+
+ def test_server_is_stopped(self, mode, stay_open):
+ '''Test that the server stops when we click Stop'''
+ if type(mode) == ReceiveMode or (type(mode) == ShareMode and stay_open):
+ QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton)
+ self.assertEquals(mode.server_status.status, 0)
+
+ def test_web_service_is_stopped(self):
+ '''Test that the web server also stopped'''
+ QtTest.QTest.qWait(2000)
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+
+ # We should be closed by now. Fail if not!
+ self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0)
+
+ def test_server_status_indicator_says_closed(self, mode, stay_open):
+ '''Test that the Server Status indicator shows we closed'''
+ if type(mode) == ReceiveMode:
+ self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True))
+ if type(mode) == ShareMode:
+ if stay_open:
+ self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped', True))
+ else:
+ self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True))
+
+ # Auto-stop timer tests
+ def test_set_timeout(self, mode, timeout):
+ '''Test that the timeout can be set'''
+ timer = QtCore.QDateTime.currentDateTime().addSecs(timeout)
+ mode.server_status.shutdown_timeout.setDateTime(timer)
+ self.assertTrue(mode.server_status.shutdown_timeout.dateTime(), timer)
+
+ def test_timeout_widget_hidden(self, mode):
+ '''Test that the timeout widget is hidden when share has started'''
+ self.assertFalse(mode.server_status.shutdown_timeout_container.isVisible())
+
+ def test_server_timed_out(self, mode, wait):
+ '''Test that the server has timed out after the timer ran out'''
+ QtTest.QTest.qWait(wait)
+ # We should have timed out now
+ self.assertEqual(mode.server_status.status, 0)
+
+ # Receive-specific tests
+ def test_upload_file(self, public_mode, expected_file):
+ '''Test that we can upload the file'''
+ files = {'file[]': open('/tmp/test.txt', 'rb')}
+ if not public_mode:
+ path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug)
+ else:
+ path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port)
+ response = requests.post(path, files=files)
+ QtTest.QTest.qWait(2000)
+ self.assertTrue(os.path.isfile(expected_file))
+
+ # Share-specific tests
+ def test_file_selection_widget_has_a_file(self):
+ '''Test that the number of files in the list is 1'''
+ self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1)
+
+ def test_deleting_only_file_hides_delete_button(self):
+ '''Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button'''
+ rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0))
+ QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center())
+ # Delete button should be visible
+ self.assertTrue(self.gui.share_mode.server_status.file_selection.delete_button.isVisible())
+ # Click delete, and since there's no more files, the delete button should be hidden
+ QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton)
+ self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible())
+
+ def test_add_a_file_and_delete_using_its_delete_widget(self):
+ '''Test that we can also delete a file by clicking on its [X] widget'''
+ self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts')
+ QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton)
+ self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 0)
+
+ def test_file_selection_widget_readd_files(self):
+ '''Re-add some files to the list so we can share'''
+ self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts')
+ self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt')
+ self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2)
+
+ def test_add_delete_buttons_hidden(self):
+ '''Test that the add and delete buttons are hidden when the server starts'''
+ self.assertFalse(self.gui.share_mode.server_status.file_selection.add_button.isVisible())
+ self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible())
+
+ def test_download_share(self, public_mode):
+ '''Test that we can download the share'''
+ s = socks.socksocket()
+ s.settimeout(60)
+ s.connect(('127.0.0.1', self.gui.app.port))
+
+ if public_mode:
+ path = '/download'
+ else:
+ path = '{}/download'.format(self.gui.share_mode.web.slug)
+
+ http_request = 'GET {} HTTP/1.0\r\n'.format(path)
+ http_request += 'Host: 127.0.0.1\r\n'
+ http_request += '\r\n'
+ s.sendall(http_request.encode('utf-8'))
+
+ with open('/tmp/download.zip', 'wb') as file_to_write:
+ while True:
+ data = s.recv(1024)
+ if not data:
+ break
+ file_to_write.write(data)
+ file_to_write.close()
+
+ zip = zipfile.ZipFile('/tmp/download.zip')
+ QtTest.QTest.qWait(2000)
+ self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8'))
+
+ def test_add_button_visible(self):
+ '''Test that the add button should be visible'''
+ self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible())
diff --git a/tests_gui_local/conftest.py b/tests_gui_local/conftest.py
new file mode 100644
index 00000000..8ac7efb8
--- /dev/null
+++ b/tests_gui_local/conftest.py
@@ -0,0 +1,160 @@
+import sys
+# Force tests to look for resources in the source code tree
+sys.onionshare_dev_mode = True
+
+import os
+import shutil
+import tempfile
+
+import pytest
+
+from onionshare import common, web, settings
+
+@pytest.fixture
+def temp_dir_1024():
+ """ Create a temporary directory that has a single file of a
+ particular size (1024 bytes).
+ """
+
+ tmp_dir = tempfile.mkdtemp()
+ tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir)
+ with open(tmp_file, 'wb') as f:
+ f.write(b'*' * 1024)
+ return tmp_dir
+
+
+# pytest > 2.9 only needs @pytest.fixture
+@pytest.yield_fixture
+def temp_dir_1024_delete():
+ """ Create a temporary directory that has a single file of a
+ particular size (1024 bytes). The temporary directory (including
+ the file inside) will be deleted after fixture usage.
+ """
+
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir)
+ with open(tmp_file, 'wb') as f:
+ f.write(b'*' * 1024)
+ yield tmp_dir
+
+
+@pytest.fixture
+def temp_file_1024():
+ """ Create a temporary file of a particular size (1024 bytes). """
+
+ with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
+ tmp_file.write(b'*' * 1024)
+ return tmp_file.name
+
+
+# pytest > 2.9 only needs @pytest.fixture
+@pytest.yield_fixture
+def temp_file_1024_delete():
+ """
+ Create a temporary file of a particular size (1024 bytes).
+ The temporary file will be deleted after fixture usage.
+ """
+
+ with tempfile.NamedTemporaryFile() as tmp_file:
+ tmp_file.write(b'*' * 1024)
+ tmp_file.flush()
+ yield tmp_file.name
+
+
+# pytest > 2.9 only needs @pytest.fixture
+@pytest.yield_fixture(scope='session')
+def custom_zw():
+ zw = web.share_mode.ZipWriter(
+ common.Common(),
+ zip_filename=common.Common.random_string(4, 6),
+ processed_size_callback=lambda _: 'custom_callback'
+ )
+ yield zw
+ zw.close()
+ os.remove(zw.zip_filename)
+
+
+# pytest > 2.9 only needs @pytest.fixture
+@pytest.yield_fixture(scope='session')
+def default_zw():
+ zw = web.share_mode.ZipWriter(common.Common())
+ yield zw
+ zw.close()
+ tmp_dir = os.path.dirname(zw.zip_filename)
+ shutil.rmtree(tmp_dir)
+
+
+@pytest.fixture
+def locale_en(monkeypatch):
+ monkeypatch.setattr('locale.getdefaultlocale', lambda: ('en_US', 'UTF-8'))
+
+
+@pytest.fixture
+def locale_fr(monkeypatch):
+ monkeypatch.setattr('locale.getdefaultlocale', lambda: ('fr_FR', 'UTF-8'))
+
+
+@pytest.fixture
+def locale_invalid(monkeypatch):
+ monkeypatch.setattr('locale.getdefaultlocale', lambda: ('xx_XX', 'UTF-8'))
+
+
+@pytest.fixture
+def locale_ru(monkeypatch):
+ monkeypatch.setattr('locale.getdefaultlocale', lambda: ('ru_RU', 'UTF-8'))
+
+
+@pytest.fixture
+def platform_darwin(monkeypatch):
+ monkeypatch.setattr('platform.system', lambda: 'Darwin')
+
+
+@pytest.fixture # (scope="session")
+def platform_linux(monkeypatch):
+ monkeypatch.setattr('platform.system', lambda: 'Linux')
+
+
+@pytest.fixture
+def platform_windows(monkeypatch):
+ monkeypatch.setattr('platform.system', lambda: 'Windows')
+
+
+@pytest.fixture
+def sys_argv_sys_prefix(monkeypatch):
+ monkeypatch.setattr('sys.argv', [sys.prefix])
+
+
+@pytest.fixture
+def sys_frozen(monkeypatch):
+ monkeypatch.setattr('sys.frozen', True, raising=False)
+
+
+@pytest.fixture
+def sys_meipass(monkeypatch):
+ monkeypatch.setattr(
+ 'sys._MEIPASS', os.path.expanduser('~'), raising=False)
+
+
+@pytest.fixture # (scope="session")
+def sys_onionshare_dev_mode(monkeypatch):
+ monkeypatch.setattr('sys.onionshare_dev_mode', True, raising=False)
+
+
+@pytest.fixture
+def time_time_100(monkeypatch):
+ monkeypatch.setattr('time.time', lambda: 100)
+
+
+@pytest.fixture
+def time_strftime(monkeypatch):
+ monkeypatch.setattr('time.strftime', lambda _: 'Jun 06 2013 11:05:00')
+
+@pytest.fixture
+def common_obj():
+ return common.Common()
+
+@pytest.fixture
+def settings_obj(sys_onionshare_dev_mode, platform_linux):
+ _common = common.Common()
+ _common.version = 'DUMMY_VERSION_1.2.3'
+ return settings.Settings(_common)
diff --git a/tests_gui_local/onionshare_receive_mode_upload_test.py b/tests_gui_local/onionshare_receive_mode_upload_test.py
new file mode 100644
index 00000000..1ce91ba2
--- /dev/null
+++ b/tests_gui_local/onionshare_receive_mode_upload_test.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, True, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": False,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+ os.remove('/tmp/OnionShare/test.txt')
+ os.remove('/tmp/OnionShare/test-2.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=6)
+ def test_click_mode(self):
+ CommonTests.test_click_mode(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=6)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=7)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=8)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=8)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=9)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=10)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=11)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=12)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=14)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, self.gui.receive_mode, False)
+
+ @pytest.mark.run(order=15)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=16)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=17)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=18)
+ def test_web_page(self):
+ CommonTests.test_web_page(self, self.gui.receive_mode, 'Select the files you want to send, then click', False)
+
+ @pytest.mark.run(order=19)
+ def test_upload_file(self):
+ CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test.txt')
+
+ @pytest.mark.run(order=20)
+ def test_history_widgets_present(self):
+ CommonTests.test_history_widgets_present(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=21)
+ def test_counter_incremented(self):
+ CommonTests.test_counter_incremented(self, self.gui.receive_mode, 1)
+
+ @pytest.mark.run(order=22)
+ def test_upload_same_file_is_renamed(self):
+ CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test-2.txt')
+
+ @pytest.mark.run(order=23)
+ def test_upload_count_incremented_again(self):
+ CommonTests.test_counter_incremented(self, self.gui.receive_mode, 2)
+
+ @pytest.mark.run(order=24)
+ def test_history_indicator(self):
+ CommonTests.test_history_indicator(self, self.gui.receive_mode, False)
+
+ @pytest.mark.run(order=25)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, self.gui.receive_mode, False)
+
+ @pytest.mark.run(order=26)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=27)
+ def test_server_status_indicator_says_closed(self):
+ CommonTests.test_server_status_indicator_says_closed(self, self.gui.receive_mode, False)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py
new file mode 100644
index 00000000..cd02e012
--- /dev/null
+++ b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, True, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": True,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ try:
+ os.remove('/tmp/test.txt')
+ os.remove('/tmp/OnionShare/test.txt')
+ os.remove('/tmp/OnionShare/test-2.txt')
+ except:
+ pass
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_click_mode(self):
+ CommonTests.test_click_mode(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=6)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=7)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=8)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=9)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=10)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=11)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=12)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=13)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=14)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, self.gui.receive_mode, True)
+
+ @pytest.mark.run(order=15)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=16)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=17)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=18)
+ def test_web_page(self):
+ CommonTests.test_web_page(self, self.gui.receive_mode, 'Select the files you want to send, then click', True)
+
+ @pytest.mark.run(order=19)
+ def test_upload_file(self):
+ CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test.txt')
+
+ @pytest.mark.run(order=20)
+ def test_history_widgets_present(self):
+ CommonTests.test_history_widgets_present(self, self.gui.receive_mode)
+
+ @pytest.mark.run(order=21)
+ def test_counter_incremented(self):
+ CommonTests.test_counter_incremented(self, self.gui.receive_mode, 1)
+
+ @pytest.mark.run(order=22)
+ def test_upload_same_file_is_renamed(self):
+ CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test-2.txt')
+
+ @pytest.mark.run(order=23)
+ def test_upload_count_incremented_again(self):
+ CommonTests.test_counter_incremented(self, self.gui.receive_mode, 2)
+
+ @pytest.mark.run(order=24)
+ def test_history_indicator(self):
+ CommonTests.test_history_indicator(self, self.gui.receive_mode, True)
+
+ @pytest.mark.run(order=25)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, self.gui.receive_mode, False)
+
+ @pytest.mark.run(order=26)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=27)
+ def test_server_status_indicator_says_closed(self):
+ CommonTests.test_server_status_indicator_says_closed(self, self.gui.receive_mode, False)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_local/onionshare_share_mode_download_test.py b/tests_gui_local/onionshare_share_mode_download_test.py
new file mode 100644
index 00000000..6842f1a6
--- /dev/null
+++ b/tests_gui_local/onionshare_share_mode_download_test.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, True, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": False,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_file_selection_widget_has_a_file(self):
+ CommonTests.test_file_selection_widget_has_a_file(self)
+
+ @pytest.mark.run(order=7)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=8)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=9)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=10)
+ def test_deleting_only_file_hides_delete_button(self):
+ CommonTests.test_deleting_only_file_hides_delete_button(self)
+
+ @pytest.mark.run(order=11)
+ def test_add_a_file_and_delete_using_its_delete_widget(self):
+ CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
+
+ @pytest.mark.run(order=12)
+ def test_file_selection_widget_readd_files(self):
+ CommonTests.test_file_selection_widget_readd_files(self)
+
+ @pytest.mark.run(order=13)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=14)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=15)
+ def test_add_delete_buttons_hidden(self):
+ CommonTests.test_add_delete_buttons_hidden(self)
+
+ @pytest.mark.run(order=16)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=17)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=18)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=19)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, self.gui.share_mode, False)
+
+ @pytest.mark.run(order=20)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=21)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=22)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=23)
+ def test_web_page(self):
+ CommonTests.test_web_page(self, self.gui.share_mode, 'Total size', False)
+
+ @pytest.mark.run(order=24)
+ def test_download_share(self):
+ CommonTests.test_download_share(self, False)
+
+ @pytest.mark.run(order=25)
+ def test_history_widgets_present(self):
+ CommonTests.test_history_widgets_present(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=26)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, self.gui.share_mode, False)
+
+ @pytest.mark.run(order=27)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=28)
+ def test_server_status_indicator_says_closed(self):
+ CommonTests.test_server_status_indicator_says_closed(self, self.gui.share_mode, False)
+
+ @pytest.mark.run(order=29)
+ def test_add_button_visible(self):
+ CommonTests.test_add_button_visible(self)
+
+ @pytest.mark.run(order=30)
+ def test_history_indicator(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
+ CommonTests.test_a_server_is_started(self, self.gui.share_mode)
+ CommonTests.test_history_indicator(self, self.gui.share_mode, False)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_local/onionshare_share_mode_download_test_public_mode.py b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py
new file mode 100644
index 00000000..82f1989c
--- /dev/null
+++ b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, True, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": True,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_file_selection_widget_has_a_file(self):
+ CommonTests.test_file_selection_widget_has_a_file(self)
+
+ @pytest.mark.run(order=7)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=8)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=9)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=10)
+ def test_deleting_only_file_hides_delete_button(self):
+ CommonTests.test_deleting_only_file_hides_delete_button(self)
+
+ @pytest.mark.run(order=11)
+ def test_add_a_file_and_delete_using_its_delete_widget(self):
+ CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
+
+ @pytest.mark.run(order=12)
+ def test_file_selection_widget_readd_files(self):
+ CommonTests.test_file_selection_widget_readd_files(self)
+
+ @pytest.mark.run(order=13)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=14)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=15)
+ def test_add_delete_buttons_hidden(self):
+ CommonTests.test_add_delete_buttons_hidden(self)
+
+ @pytest.mark.run(order=16)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=17)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=18)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=19)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, self.gui.share_mode, True)
+
+ @pytest.mark.run(order=20)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=21)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=22)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=23)
+ def test_web_page(self):
+ CommonTests.test_web_page(self, self.gui.share_mode, 'Total size', True)
+
+ @pytest.mark.run(order=24)
+ def test_download_share(self):
+ CommonTests.test_download_share(self, True)
+
+ @pytest.mark.run(order=25)
+ def test_history_widgets_present(self):
+ CommonTests.test_history_widgets_present(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=26)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, self.gui.share_mode, False)
+
+ @pytest.mark.run(order=27)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=28)
+ def test_server_status_indicator_says_closed(self):
+ CommonTests.test_server_status_indicator_says_closed(self, self.gui.share_mode, False)
+
+ @pytest.mark.run(order=29)
+ def test_add_button_visible(self):
+ CommonTests.test_add_button_visible(self)
+
+ @pytest.mark.run(order=30)
+ def test_history_indicator(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
+ CommonTests.test_a_server_is_started(self, self.gui.share_mode)
+ CommonTests.test_history_indicator(self, self.gui.share_mode, True)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_local/onionshare_share_mode_download_test_stay_open.py b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py
new file mode 100644
index 00000000..df9bc857
--- /dev/null
+++ b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py
@@ -0,0 +1,211 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, True, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": False,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": True,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_file_selection_widget_has_a_file(self):
+ CommonTests.test_file_selection_widget_has_a_file(self)
+
+ @pytest.mark.run(order=7)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=8)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=9)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=10)
+ def test_deleting_only_file_hides_delete_button(self):
+ CommonTests.test_deleting_only_file_hides_delete_button(self)
+
+ @pytest.mark.run(order=11)
+ def test_add_a_file_and_delete_using_its_delete_widget(self):
+ CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
+
+ @pytest.mark.run(order=12)
+ def test_file_selection_widget_readd_files(self):
+ CommonTests.test_file_selection_widget_readd_files(self)
+
+ @pytest.mark.run(order=13)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=14)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=15)
+ def test_add_delete_buttons_hidden(self):
+ CommonTests.test_add_delete_buttons_hidden(self)
+
+ @pytest.mark.run(order=16)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=17)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=18)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=19)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, self.gui.share_mode, True)
+
+ @pytest.mark.run(order=20)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=21)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=22)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=23)
+ def test_web_page(self):
+ CommonTests.test_web_page(self, self.gui.share_mode, 'Total size', True)
+
+ @pytest.mark.run(order=24)
+ def test_download_share(self):
+ CommonTests.test_download_share(self, True)
+
+ @pytest.mark.run(order=25)
+ def test_history_widgets_present(self):
+ CommonTests.test_history_widgets_present(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=26)
+ def test_counter_incremented(self):
+ CommonTests.test_counter_incremented(self, self.gui.share_mode, 1)
+
+ @pytest.mark.run(order=27)
+ def test_download_share_again(self):
+ CommonTests.test_download_share(self, True)
+
+ @pytest.mark.run(order=28)
+ def test_counter_incremented_again(self):
+ CommonTests.test_counter_incremented(self, self.gui.share_mode, 2)
+
+ @pytest.mark.run(order=29)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, self.gui.share_mode, True)
+
+ @pytest.mark.run(order=30)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=31)
+ def test_server_status_indicator_says_closed(self):
+ CommonTests.test_server_status_indicator_says_closed(self, self.gui.share_mode, True)
+
+ @pytest.mark.run(order=32)
+ def test_add_button_visible(self):
+ CommonTests.test_add_button_visible(self)
+
+ @pytest.mark.run(order=33)
+ def test_history_indicator(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
+ CommonTests.test_a_server_is_started(self, self.gui.share_mode)
+ CommonTests.test_history_indicator(self, self.gui.share_mode, True)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_local/onionshare_slug_persistent_test.py b/tests_gui_local/onionshare_slug_persistent_test.py
new file mode 100644
index 00000000..5b825dad
--- /dev/null
+++ b/tests_gui_local/onionshare_slug_persistent_test.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ slug = ''
+
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, True, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": False,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": True,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=7)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=8)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=9)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=10)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=11)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=12)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=13)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=14)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=15)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, self.gui.share_mode, False)
+ global slug
+ slug = self.gui.share_mode.server_status.web.slug
+
+ @pytest.mark.run(order=16)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=17)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, self.gui.share_mode, True)
+
+ @pytest.mark.run(order=18)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=19)
+ def test_server_status_indicator_says_closed(self):
+ CommonTests.test_server_status_indicator_says_closed(self, self.gui.share_mode, True)
+
+ @pytest.mark.run(order=20)
+ def test_server_started_again(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
+ CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode)
+ CommonTests.test_a_server_is_started(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=21)
+ def test_have_same_slug(self):
+ '''Test that we have the same slug'''
+ self.assertEqual(self.gui.share_mode.server_status.web.slug, slug)
+
+ @pytest.mark.run(order=22)
+ def test_server_is_stopped_again(self):
+ CommonTests.test_server_is_stopped(self, self.gui.share_mode, True)
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=23)
+ def test_history_indicator(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
+ CommonTests.test_a_server_is_started(self, self.gui.share_mode)
+ CommonTests.test_history_indicator(self, self.gui.share_mode, False)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_local/onionshare_timer_test.py b/tests_gui_local/onionshare_timer_test.py
new file mode 100644
index 00000000..4aaaf364
--- /dev/null
+++ b/tests_gui_local/onionshare_timer_test.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, True, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": False,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": True,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_file_selection_widget_has_a_file(self):
+ CommonTests.test_file_selection_widget_has_a_file(self)
+
+ @pytest.mark.run(order=7)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=8)
+ def test_set_timeout(self):
+ CommonTests.test_set_timeout(self, self.gui.share_mode, 5)
+
+ @pytest.mark.run(order=9)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=10)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=11)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=12)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=13)
+ def test_timeout_widget_hidden(self):
+ CommonTests.test_timeout_widget_hidden(self, self.gui.share_mode)
+
+ @pytest.mark.run(order=14)
+ def test_timeout(self):
+ CommonTests.test_server_timed_out(self, self.gui.share_mode, 10000)
+
+ @pytest.mark.run(order=15)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_local/run_unit_tests.sh b/tests_gui_local/run_unit_tests.sh
new file mode 100755
index 00000000..7d207a57
--- /dev/null
+++ b/tests_gui_local/run_unit_tests.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+for test in `ls -1 | egrep ^onionshare_`; do
+ pytest $test -vvv || exit 1
+done
diff --git a/tests_gui_tor/__init__.py b/tests_gui_tor/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests_gui_tor/__init__.py
diff --git a/tests_gui_tor/commontests.py b/tests_gui_tor/commontests.py
new file mode 100644
index 00000000..a1e420fd
--- /dev/null
+++ b/tests_gui_tor/commontests.py
@@ -0,0 +1,61 @@
+import os
+import requests
+import socket
+import socks
+import zipfile
+
+from PyQt5 import QtCore, QtTest
+from onionshare import strings
+
+from tests_gui_local import CommonTests as LocalCommonTests
+
+class CommonTests(LocalCommonTests):
+ def test_a_server_is_started(self, mode):
+ '''Test that the server has started (overriding from local tests to wait for longer)'''
+ QtTest.QTest.qWait(45000)
+ # Should now be in SERVER_STARTED state
+ if mode == 'receive':
+ self.assertEqual(self.gui.receive_mode.server_status.status, 2)
+ if mode == 'share':
+ self.assertEqual(self.gui.share_mode.server_status.status, 2)
+
+ def test_have_an_onion_service(self):
+ '''Test that we have a valid Onion URL'''
+ self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion')
+
+ def test_cancel_the_share(self, mode):
+ '''Test that we can cancel this share before it's started up '''
+ if mode == 'share':
+ QtTest.QTest.mousePress(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton)
+ QtTest.QTest.qWait(1000)
+ QtTest.QTest.mouseRelease(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton)
+ self.assertEqual(self.gui.share_mode.server_status.status, 0)
+
+ if mode == 'receive':
+ QtTest.QTest.mousePress(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton)
+ QtTest.QTest.qWait(1000)
+ QtTest.QTest.mouseRelease(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton)
+ self.assertEqual(self.gui.receive_mode.server_status.status, 0)
+
+ # Stealth tests
+ def test_copy_have_hidserv_auth_button(self, mode):
+ '''Test that the Copy HidservAuth button is shown'''
+ if mode == 'share':
+ self.assertTrue(self.gui.share_mode.server_status.copy_hidservauth_button.isVisible())
+ if mode == 'receive':
+ self.assertTrue(self.gui.receive_mode.server_status.copy_hidservauth_button.isVisible())
+
+ def test_hidserv_auth_string(self):
+ '''Test the validity of the HidservAuth string'''
+ self.assertRegex(self.gui.app.auth_string, r'HidServAuth %s [a-zA-Z1-9]' % self.gui.app.onion_host)
+
+
+ # Miscellaneous tests
+ def test_tor_killed_statusbar_message_shown(self, mode):
+ '''Test that the status bar message shows Tor was disconnected'''
+ self.gui.app.onion.cleanup(stop_tor=True)
+ QtTest.QTest.qWait(2500)
+ if mode == 'share':
+ self.assertTrue(self.gui.share_mode.status_bar.currentMessage(), strings._('gui_tor_connection_lost', True))
+ if mode == 'receive':
+ self.assertTrue(self.gui.receive_mode.status_bar.currentMessage(), strings._('gui_tor_connection_lost', True))
diff --git a/tests_gui_tor/conftest.py b/tests_gui_tor/conftest.py
new file mode 100644
index 00000000..8ac7efb8
--- /dev/null
+++ b/tests_gui_tor/conftest.py
@@ -0,0 +1,160 @@
+import sys
+# Force tests to look for resources in the source code tree
+sys.onionshare_dev_mode = True
+
+import os
+import shutil
+import tempfile
+
+import pytest
+
+from onionshare import common, web, settings
+
+@pytest.fixture
+def temp_dir_1024():
+ """ Create a temporary directory that has a single file of a
+ particular size (1024 bytes).
+ """
+
+ tmp_dir = tempfile.mkdtemp()
+ tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir)
+ with open(tmp_file, 'wb') as f:
+ f.write(b'*' * 1024)
+ return tmp_dir
+
+
+# pytest > 2.9 only needs @pytest.fixture
+@pytest.yield_fixture
+def temp_dir_1024_delete():
+ """ Create a temporary directory that has a single file of a
+ particular size (1024 bytes). The temporary directory (including
+ the file inside) will be deleted after fixture usage.
+ """
+
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir)
+ with open(tmp_file, 'wb') as f:
+ f.write(b'*' * 1024)
+ yield tmp_dir
+
+
+@pytest.fixture
+def temp_file_1024():
+ """ Create a temporary file of a particular size (1024 bytes). """
+
+ with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
+ tmp_file.write(b'*' * 1024)
+ return tmp_file.name
+
+
+# pytest > 2.9 only needs @pytest.fixture
+@pytest.yield_fixture
+def temp_file_1024_delete():
+ """
+ Create a temporary file of a particular size (1024 bytes).
+ The temporary file will be deleted after fixture usage.
+ """
+
+ with tempfile.NamedTemporaryFile() as tmp_file:
+ tmp_file.write(b'*' * 1024)
+ tmp_file.flush()
+ yield tmp_file.name
+
+
+# pytest > 2.9 only needs @pytest.fixture
+@pytest.yield_fixture(scope='session')
+def custom_zw():
+ zw = web.share_mode.ZipWriter(
+ common.Common(),
+ zip_filename=common.Common.random_string(4, 6),
+ processed_size_callback=lambda _: 'custom_callback'
+ )
+ yield zw
+ zw.close()
+ os.remove(zw.zip_filename)
+
+
+# pytest > 2.9 only needs @pytest.fixture
+@pytest.yield_fixture(scope='session')
+def default_zw():
+ zw = web.share_mode.ZipWriter(common.Common())
+ yield zw
+ zw.close()
+ tmp_dir = os.path.dirname(zw.zip_filename)
+ shutil.rmtree(tmp_dir)
+
+
+@pytest.fixture
+def locale_en(monkeypatch):
+ monkeypatch.setattr('locale.getdefaultlocale', lambda: ('en_US', 'UTF-8'))
+
+
+@pytest.fixture
+def locale_fr(monkeypatch):
+ monkeypatch.setattr('locale.getdefaultlocale', lambda: ('fr_FR', 'UTF-8'))
+
+
+@pytest.fixture
+def locale_invalid(monkeypatch):
+ monkeypatch.setattr('locale.getdefaultlocale', lambda: ('xx_XX', 'UTF-8'))
+
+
+@pytest.fixture
+def locale_ru(monkeypatch):
+ monkeypatch.setattr('locale.getdefaultlocale', lambda: ('ru_RU', 'UTF-8'))
+
+
+@pytest.fixture
+def platform_darwin(monkeypatch):
+ monkeypatch.setattr('platform.system', lambda: 'Darwin')
+
+
+@pytest.fixture # (scope="session")
+def platform_linux(monkeypatch):
+ monkeypatch.setattr('platform.system', lambda: 'Linux')
+
+
+@pytest.fixture
+def platform_windows(monkeypatch):
+ monkeypatch.setattr('platform.system', lambda: 'Windows')
+
+
+@pytest.fixture
+def sys_argv_sys_prefix(monkeypatch):
+ monkeypatch.setattr('sys.argv', [sys.prefix])
+
+
+@pytest.fixture
+def sys_frozen(monkeypatch):
+ monkeypatch.setattr('sys.frozen', True, raising=False)
+
+
+@pytest.fixture
+def sys_meipass(monkeypatch):
+ monkeypatch.setattr(
+ 'sys._MEIPASS', os.path.expanduser('~'), raising=False)
+
+
+@pytest.fixture # (scope="session")
+def sys_onionshare_dev_mode(monkeypatch):
+ monkeypatch.setattr('sys.onionshare_dev_mode', True, raising=False)
+
+
+@pytest.fixture
+def time_time_100(monkeypatch):
+ monkeypatch.setattr('time.time', lambda: 100)
+
+
+@pytest.fixture
+def time_strftime(monkeypatch):
+ monkeypatch.setattr('time.strftime', lambda _: 'Jun 06 2013 11:05:00')
+
+@pytest.fixture
+def common_obj():
+ return common.Common()
+
+@pytest.fixture
+def settings_obj(sys_onionshare_dev_mode, platform_linux):
+ _common = common.Common()
+ _common.version = 'DUMMY_VERSION_1.2.3'
+ return settings.Settings(_common)
diff --git a/tests_gui_tor/onionshare_790_cancel_on_second_share_test.py b/tests_gui_tor/onionshare_790_cancel_on_second_share_test.py
new file mode 100644
index 00000000..731de4fd
--- /dev/null
+++ b/tests_gui_tor/onionshare_790_cancel_on_second_share_test.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, False, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": False,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_file_selection_widget_has_a_file(self):
+ CommonTests.test_file_selection_widget_has_a_file(self)
+
+ @pytest.mark.run(order=6)
+ def test_info_widget_is_visible(self):
+ CommonTests.test_info_widget_is_visible(self, 'share')
+
+ @pytest.mark.run(order=7)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, 'share')
+
+ @pytest.mark.run(order=8)
+ def test_deleting_only_file_hides_delete_button(self):
+ CommonTests.test_deleting_only_file_hides_delete_button(self)
+
+ @pytest.mark.run(order=9)
+ def test_add_a_file_and_delete_using_its_delete_widget(self):
+ CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
+
+ @pytest.mark.run(order=10)
+ def test_file_selection_widget_readd_files(self):
+ CommonTests.test_file_selection_widget_readd_files(self)
+
+ @pytest.mark.run(order=11)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'share')
+
+ @pytest.mark.run(order=12)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, 'share')
+
+ @pytest.mark.run(order=13)
+ def test_add_delete_buttons_hidden(self):
+ CommonTests.test_add_delete_buttons_hidden(self)
+
+ @pytest.mark.run(order=14)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=15)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, 'share')
+
+ @pytest.mark.run(order=16)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=17)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, 'share', False)
+
+ @pytest.mark.run(order=18)
+ def test_have_an_onion(self):
+ CommonTests.test_have_an_onion_service(self)
+
+ @pytest.mark.run(order=19)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, 'share')
+
+ @pytest.mark.run(order=20)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, 'share')
+
+ @pytest.mark.run(order=21)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, 'share')
+
+ @pytest.mark.run(order=22)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, 'share', True)
+
+ @pytest.mark.run(order=23)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=24)
+ def test_server_working_on_start_button_pressed_round2(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'share')
+
+ @pytest.mark.run(order=25)
+ def test_server_status_indicator_says_starting_round2(self):
+ CommonTests.test_server_status_indicator_says_starting(self, 'share')
+
+ @pytest.mark.run(order=26)
+ def test_cancel_the_share(self):
+ CommonTests.test_cancel_the_share(self, 'share')
+
+ @pytest.mark.run(order=27)
+ def test_server_is_stopped_round2(self):
+ CommonTests.test_server_is_stopped(self, 'share', False)
+
+ @pytest.mark.run(order=28)
+ def test_web_service_is_stopped_round2(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=29)
+ def test_add_button_visible(self):
+ CommonTests.test_add_button_visible(self)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_tor/onionshare_receive_mode_upload_test.py b/tests_gui_tor/onionshare_receive_mode_upload_test.py
new file mode 100644
index 00000000..7c340037
--- /dev/null
+++ b/tests_gui_tor/onionshare_receive_mode_upload_test.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, False, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": False,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+ os.remove('/tmp/OnionShare/test.txt')
+ os.remove('/tmp/OnionShare/test-2.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=6)
+ def test_click_mode(self):
+ CommonTests.test_click_mode(self, 'receive')
+
+ @pytest.mark.run(order=6)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, 'receive')
+
+ @pytest.mark.run(order=7)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, 'receive')
+
+ @pytest.mark.run(order=8)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, 'receive')
+
+ @pytest.mark.run(order=8)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'receive')
+
+ @pytest.mark.run(order=9)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, 'receive')
+
+ @pytest.mark.run(order=10)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=11)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, 'receive')
+
+ @pytest.mark.run(order=12)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=14)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, 'receive', False)
+
+ @pytest.mark.run(order=15)
+ def test_have_an_onion(self):
+ CommonTests.test_have_an_onion_service(self)
+
+ @pytest.mark.run(order=20)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, 'receive')
+
+ @pytest.mark.run(order=21)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, 'receive')
+
+ @pytest.mark.run(order=22)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, 'receive')
+
+ @pytest.mark.run(order=23)
+ def test_web_page(self):
+ CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', False)
+
+ @pytest.mark.run(order=24)
+ def test_upload_file(self):
+ CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test.txt')
+
+ @pytest.mark.run(order=25)
+ def test_history_widgets_present(self):
+ CommonTests.test_history_widgets_present(self, 'receive')
+
+ @pytest.mark.run(order=26)
+ def test_counter_incremented(self):
+ CommonTests.test_counter_incremented(self, 'receive', 1)
+
+ @pytest.mark.run(order=27)
+ def test_upload_same_file_is_renamed(self):
+ CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test-2.txt')
+
+ @pytest.mark.run(order=28)
+ def test_upload_count_incremented_again(self):
+ CommonTests.test_counter_incremented(self, 'receive', 2)
+
+ @pytest.mark.run(order=29)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, 'receive', False)
+
+ @pytest.mark.run(order=30)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=31)
+ def test_server_status_indicator_says_closed(self):
+ CommonTests.test_server_status_indicator_says_closed(self, 'receive', False)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py
new file mode 100644
index 00000000..65bf5c89
--- /dev/null
+++ b/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, False, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": True,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+ os.remove('/tmp/OnionShare/test.txt')
+ os.remove('/tmp/OnionShare/test-2.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_click_mode(self):
+ CommonTests.test_click_mode(self, 'receive')
+
+ @pytest.mark.run(order=6)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, 'receive')
+
+ @pytest.mark.run(order=7)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, 'receive')
+
+ @pytest.mark.run(order=8)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, 'receive')
+
+ @pytest.mark.run(order=9)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'receive')
+
+ @pytest.mark.run(order=10)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, 'receive')
+
+ @pytest.mark.run(order=11)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=12)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, 'receive')
+
+ @pytest.mark.run(order=13)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=14)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, 'receive', True)
+
+ @pytest.mark.run(order=15)
+ def test_have_an_onion(self):
+ CommonTests.test_have_an_onion_service(self)
+
+ @pytest.mark.run(order=20)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, 'receive')
+
+ @pytest.mark.run(order=21)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, 'receive')
+
+ @pytest.mark.run(order=22)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, 'receive')
+
+ @pytest.mark.run(order=23)
+ def test_web_page(self):
+ CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', True)
+
+ @pytest.mark.run(order=24)
+ def test_upload_file(self):
+ CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test.txt')
+
+ @pytest.mark.run(order=25)
+ def test_history_widgets_present(self):
+ CommonTests.test_history_widgets_present(self, 'receive')
+
+ @pytest.mark.run(order=26)
+ def test_counter_incremented(self):
+ CommonTests.test_counter_incremented(self, 'receive', 1)
+
+ @pytest.mark.run(order=27)
+ def test_upload_same_file_is_renamed(self):
+ CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test-2.txt')
+
+ @pytest.mark.run(order=28)
+ def test_upload_count_incremented_again(self):
+ CommonTests.test_counter_incremented(self, 'receive', 2)
+
+ @pytest.mark.run(order=29)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, 'receive', False)
+
+ @pytest.mark.run(order=30)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=31)
+ def test_server_status_indicator_says_closed(self):
+ CommonTests.test_server_status_indicator_says_closed(self, 'receive', False)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_tor/onionshare_share_mode_cancel_share_test.py b/tests_gui_tor/onionshare_share_mode_cancel_share_test.py
new file mode 100644
index 00000000..cdab8f85
--- /dev/null
+++ b/tests_gui_tor/onionshare_share_mode_cancel_share_test.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, False, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": False,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_file_selection_widget_has_a_file(self):
+ CommonTests.test_file_selection_widget_has_a_file(self)
+
+ @pytest.mark.run(order=8)
+ def test_deleting_only_file_hides_delete_button(self):
+ CommonTests.test_deleting_only_file_hides_delete_button(self)
+
+ @pytest.mark.run(order=9)
+ def test_add_a_file_and_delete_using_its_delete_widget(self):
+ CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
+
+ @pytest.mark.run(order=10)
+ def test_file_selection_widget_readd_files(self):
+ CommonTests.test_file_selection_widget_readd_files(self)
+
+ @pytest.mark.run(order=11)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'share')
+
+ @pytest.mark.run(order=12)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, 'share')
+
+ @pytest.mark.run(order=13)
+ def test_add_delete_buttons_hidden(self):
+ CommonTests.test_add_delete_buttons_hidden(self)
+
+ @pytest.mark.run(order=14)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=16)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=17)
+ def test_cancel_the_share(self):
+ CommonTests.test_cancel_the_share(self, 'share')
+
+ @pytest.mark.run(order=18)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, 'share', False)
+
+ @pytest.mark.run(order=19)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=20)
+ def test_add_button_visible(self):
+ CommonTests.test_add_button_visible(self)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_tor/onionshare_share_mode_download_test.py b/tests_gui_tor/onionshare_share_mode_download_test.py
new file mode 100644
index 00000000..2bf26690
--- /dev/null
+++ b/tests_gui_tor/onionshare_share_mode_download_test.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, False, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": False,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_file_selection_widget_has_a_file(self):
+ CommonTests.test_file_selection_widget_has_a_file(self)
+
+ @pytest.mark.run(order=6)
+ def test_info_widget_shows_less(self):
+ CommonTests.test_info_widget_shows_less(self, 'share')
+
+ @pytest.mark.run(order=7)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, 'share')
+
+ @pytest.mark.run(order=8)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, 'share')
+
+ @pytest.mark.run(order=9)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, 'share')
+
+ @pytest.mark.run(order=10)
+ def test_file_selection_widget_readd_files(self):
+ CommonTests.test_file_selection_widget_readd_files(self)
+
+ @pytest.mark.run(order=11)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'share')
+
+ @pytest.mark.run(order=12)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, 'share')
+
+ @pytest.mark.run(order=13)
+ def test_add_delete_buttons_hidden(self):
+ CommonTests.test_add_delete_buttons_hidden(self)
+
+ @pytest.mark.run(order=14)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=15)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, 'share')
+
+ @pytest.mark.run(order=16)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=17)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, 'share', False)
+
+ @pytest.mark.run(order=18)
+ def test_have_an_onion(self):
+ CommonTests.test_have_an_onion_service(self)
+
+ @pytest.mark.run(order=19)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, 'share')
+
+ @pytest.mark.run(order=20)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, 'share')
+
+ @pytest.mark.run(order=21)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, 'share')
+
+ @pytest.mark.run(order=22)
+ def test_web_page(self):
+ CommonTests.test_web_page(self, 'share', 'Total size', False)
+
+ @pytest.mark.run(order=23)
+ def test_download_share(self):
+ CommonTests.test_download_share(self, False)
+
+ @pytest.mark.run(order=24)
+ def test_history_widgets_present(self):
+ CommonTests.test_history_widgets_present(self, 'share')
+
+ @pytest.mark.run(order=25)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, 'share', False)
+
+ @pytest.mark.run(order=26)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=27)
+ def test_server_status_indicator_says_closed(self):
+ CommonTests.test_server_status_indicator_says_closed(self, 'share', False)
+
+ @pytest.mark.run(order=28)
+ def test_add_button_visible(self):
+ CommonTests.test_add_button_visible(self)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py b/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py
new file mode 100644
index 00000000..4792994d
--- /dev/null
+++ b/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py
@@ -0,0 +1,201 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, False, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": True,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_file_selection_widget_has_a_file(self):
+ CommonTests.test_file_selection_widget_has_a_file(self)
+
+ @pytest.mark.run(order=6)
+ def test_info_widget_shows_less(self):
+ CommonTests.test_info_widget_shows_less(self, 'share')
+
+ @pytest.mark.run(order=7)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, 'share')
+
+ @pytest.mark.run(order=8)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, 'share')
+
+ @pytest.mark.run(order=9)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, 'share')
+
+ @pytest.mark.run(order=10)
+ def test_deleting_only_file_hides_delete_button(self):
+ CommonTests.test_deleting_only_file_hides_delete_button(self)
+
+ @pytest.mark.run(order=11)
+ def test_add_a_file_and_delete_using_its_delete_widget(self):
+ CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
+
+ @pytest.mark.run(order=12)
+ def test_file_selection_widget_readd_files(self):
+ CommonTests.test_file_selection_widget_readd_files(self)
+
+ @pytest.mark.run(order=13)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'share')
+
+ @pytest.mark.run(order=14)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, 'share')
+
+ @pytest.mark.run(order=15)
+ def test_add_delete_buttons_hidden(self):
+ CommonTests.test_add_delete_buttons_hidden(self)
+
+ @pytest.mark.run(order=16)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=17)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, 'share')
+
+ @pytest.mark.run(order=18)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=19)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, 'share', True)
+
+ @pytest.mark.run(order=20)
+ def test_have_an_onion(self):
+ CommonTests.test_have_an_onion_service(self)
+
+ @pytest.mark.run(order=21)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, 'share')
+
+ @pytest.mark.run(order=22)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, 'share')
+
+ @pytest.mark.run(order=23)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, 'share')
+
+ @pytest.mark.run(order=24)
+ def test_web_page(self):
+ CommonTests.test_web_page(self, 'share', 'Total size', True)
+
+ @pytest.mark.run(order=25)
+ def test_download_share(self):
+ CommonTests.test_download_share(self, True)
+
+ @pytest.mark.run(order=26)
+ def test_history_widgets_present(self):
+ CommonTests.test_history_widgets_present(self, 'share')
+
+ @pytest.mark.run(order=27)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, 'share', False)
+
+ @pytest.mark.run(order=28)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=29)
+ def test_server_status_indicator_says_closed(self):
+ CommonTests.test_server_status_indicator_says_closed(self, 'share', False)
+
+ @pytest.mark.run(order=30)
+ def test_add_button_visible(self):
+ CommonTests.test_add_button_visible(self)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py b/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py
new file mode 100644
index 00000000..92d52169
--- /dev/null
+++ b/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py
@@ -0,0 +1,213 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, False, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": False,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": True,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_file_selection_widget_has_a_file(self):
+ CommonTests.test_file_selection_widget_has_a_file(self)
+
+ @pytest.mark.run(order=6)
+ def test_info_widget_shows_less(self):
+ CommonTests.test_info_widget_shows_less(self, 'share')
+
+ @pytest.mark.run(order=7)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, 'share')
+
+ @pytest.mark.run(order=8)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, 'share')
+
+ @pytest.mark.run(order=9)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, 'share')
+
+ @pytest.mark.run(order=10)
+ def test_deleting_only_file_hides_delete_button(self):
+ CommonTests.test_deleting_only_file_hides_delete_button(self)
+
+ @pytest.mark.run(order=11)
+ def test_add_a_file_and_delete_using_its_delete_widget(self):
+ CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
+
+ @pytest.mark.run(order=12)
+ def test_file_selection_widget_readd_files(self):
+ CommonTests.test_file_selection_widget_readd_files(self)
+
+ @pytest.mark.run(order=13)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'share')
+
+ @pytest.mark.run(order=14)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, 'share')
+
+ @pytest.mark.run(order=15)
+ def test_add_delete_buttons_hidden(self):
+ CommonTests.test_add_delete_buttons_hidden(self)
+
+ @pytest.mark.run(order=16)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=17)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, 'share')
+
+ @pytest.mark.run(order=18)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=19)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, 'share', True)
+
+ @pytest.mark.run(order=20)
+ def test_have_an_onion(self):
+ CommonTests.test_have_an_onion_service(self)
+
+ @pytest.mark.run(order=21)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, 'share')
+
+ @pytest.mark.run(order=22)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, 'share')
+
+ @pytest.mark.run(order=23)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, 'share')
+
+ @pytest.mark.run(order=24)
+ def test_web_page(self):
+ CommonTests.test_web_page(self, 'share', 'Total size', True)
+
+ @pytest.mark.run(order=25)
+ def test_download_share(self):
+ CommonTests.test_download_share(self, True)
+
+ @pytest.mark.run(order=26)
+ def test_history_widgets_present(self):
+ CommonTests.test_history_widgets_present(self, 'share')
+
+ @pytest.mark.run(order=27)
+ def test_counter_incremented(self):
+ CommonTests.test_counter_incremented(self, 'share', 1)
+
+ @pytest.mark.run(order=28)
+ def test_download_share_again(self):
+ CommonTests.test_download_share(self, True)
+
+ @pytest.mark.run(order=29)
+ def test_counter_incremented_again(self):
+ CommonTests.test_counter_incremented(self, 'share', 2)
+
+ @pytest.mark.run(order=30)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, 'share', True)
+
+ @pytest.mark.run(order=31)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=32)
+ def test_server_status_indicator_says_closed(self):
+ CommonTests.test_server_status_indicator_says_closed(self, 'share', True)
+
+ @pytest.mark.run(order=33)
+ def test_add_button_visible(self):
+ CommonTests.test_add_button_visible(self)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_tor/onionshare_share_mode_persistent_test.py b/tests_gui_tor/onionshare_share_mode_persistent_test.py
new file mode 100644
index 00000000..6b9fbe16
--- /dev/null
+++ b/tests_gui_tor/onionshare_share_mode_persistent_test.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ slug = ''
+ onion_host = ''
+
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, False, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": False,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": True,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=6)
+ def test_info_widget_shows_less(self):
+ CommonTests.test_info_widget_shows_less(self, 'share')
+
+ @pytest.mark.run(order=7)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, 'share')
+
+ @pytest.mark.run(order=8)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, 'share')
+
+ @pytest.mark.run(order=9)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, 'share')
+
+ @pytest.mark.run(order=10)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'share')
+
+ @pytest.mark.run(order=11)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, 'share')
+
+ @pytest.mark.run(order=12)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=13)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, 'share')
+
+ @pytest.mark.run(order=14)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=15)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, 'share', False)
+ global slug
+ slug = self.gui.share_mode.server_status.web.slug
+
+ @pytest.mark.run(order=16)
+ def test_have_an_onion(self):
+ CommonTests.test_have_an_onion_service(self)
+ global onion_host
+ onion_host = self.gui.app.onion_host
+
+ @pytest.mark.run(order=17)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, 'share')
+
+ @pytest.mark.run(order=18)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, 'share', True)
+
+ @pytest.mark.run(order=19)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+ @pytest.mark.run(order=20)
+ def test_server_status_indicator_says_closed(self):
+ CommonTests.test_server_status_indicator_says_closed(self, 'share', True)
+
+ @pytest.mark.run(order=21)
+ def test_server_started_again(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'share')
+ CommonTests.test_server_status_indicator_says_starting(self, 'share')
+ CommonTests.test_a_server_is_started(self, 'share')
+
+ @pytest.mark.run(order=22)
+ def test_have_same_slug(self):
+ '''Test that we have the same slug'''
+ self.assertEqual(self.gui.share_mode.server_status.web.slug, slug)
+
+ @pytest.mark.run(order=23)
+ def test_have_same_onion(self):
+ '''Test that we have the same onion'''
+ self.assertEqual(self.gui.app.onion_host, onion_host)
+
+ @pytest.mark.run(order=24)
+ def test_server_is_stopped_again(self):
+ CommonTests.test_server_is_stopped(self, 'share', True)
+ CommonTests.test_web_service_is_stopped(self)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_tor/onionshare_share_mode_stealth_test.py b/tests_gui_tor/onionshare_share_mode_stealth_test.py
new file mode 100644
index 00000000..876efde2
--- /dev/null
+++ b/tests_gui_tor/onionshare_share_mode_stealth_test.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, False, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": False,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": True,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_file_selection_widget_has_a_file(self):
+ CommonTests.test_file_selection_widget_has_a_file(self)
+
+ @pytest.mark.run(order=6)
+ def test_info_widget_shows_less(self):
+ CommonTests.test_info_widget_shows_less(self, 'share')
+
+ @pytest.mark.run(order=7)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, 'share')
+
+ @pytest.mark.run(order=8)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, 'share')
+
+ @pytest.mark.run(order=9)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, 'share')
+
+ @pytest.mark.run(order=10)
+ def test_deleting_only_file_hides_delete_button(self):
+ CommonTests.test_deleting_only_file_hides_delete_button(self)
+
+ @pytest.mark.run(order=11)
+ def test_add_a_file_and_delete_using_its_delete_widget(self):
+ CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
+
+ @pytest.mark.run(order=12)
+ def test_file_selection_widget_readd_files(self):
+ CommonTests.test_file_selection_widget_readd_files(self)
+
+ @pytest.mark.run(order=13)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'share')
+
+ @pytest.mark.run(order=14)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, 'share')
+
+ @pytest.mark.run(order=15)
+ def test_add_delete_buttons_hidden(self):
+ CommonTests.test_add_delete_buttons_hidden(self)
+
+ @pytest.mark.run(order=16)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=17)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, 'share')
+
+ @pytest.mark.run(order=18)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=19)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, 'share', False)
+
+ @pytest.mark.run(order=20)
+ def test_have_an_onion(self):
+ CommonTests.test_have_an_onion_service(self)
+
+ @pytest.mark.run(order=21)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, 'share')
+
+ @pytest.mark.run(order=22)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, 'share')
+
+ @pytest.mark.run(order=23)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, 'share')
+
+ @pytest.mark.run(order=24)
+ def test_copy_have_hidserv_auth_button(self):
+ CommonTests.test_copy_have_hidserv_auth_button(self, 'share')
+
+ @pytest.mark.run(order=25)
+ def test_hidserv_auth_string(self):
+ CommonTests.test_hidserv_auth_string(self)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py b/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py
new file mode 100644
index 00000000..37abc825
--- /dev/null
+++ b/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, False, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": False,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_file_selection_widget_has_a_file(self):
+ CommonTests.test_file_selection_widget_has_a_file(self)
+
+ @pytest.mark.run(order=6)
+ def test_info_widget_shows_less(self):
+ CommonTests.test_info_widget_shows_less(self, 'share')
+
+ @pytest.mark.run(order=7)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, 'share')
+
+ @pytest.mark.run(order=8)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, 'share')
+
+ @pytest.mark.run(order=9)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, 'share')
+
+ @pytest.mark.run(order=10)
+ def test_deleting_only_file_hides_delete_button(self):
+ CommonTests.test_deleting_only_file_hides_delete_button(self)
+
+ @pytest.mark.run(order=11)
+ def test_add_a_file_and_delete_using_its_delete_widget(self):
+ CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
+
+ @pytest.mark.run(order=12)
+ def test_file_selection_widget_readd_files(self):
+ CommonTests.test_file_selection_widget_readd_files(self)
+
+ @pytest.mark.run(order=13)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'share')
+
+ @pytest.mark.run(order=14)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, 'share')
+
+ @pytest.mark.run(order=15)
+ def test_add_delete_buttons_hidden(self):
+ CommonTests.test_add_delete_buttons_hidden(self)
+
+ @pytest.mark.run(order=16)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=17)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, 'share')
+
+ @pytest.mark.run(order=18)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=19)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, 'share', False)
+
+ @pytest.mark.run(order=20)
+ def test_have_an_onion(self):
+ CommonTests.test_have_an_onion_service(self)
+
+ @pytest.mark.run(order=21)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, 'share')
+
+ @pytest.mark.run(order=22)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, 'share')
+
+ @pytest.mark.run(order=23)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, 'share')
+
+ @pytest.mark.run(order=24)
+ def test_tor_killed_statusbar_message_shown(self):
+ CommonTests.test_tor_killed_statusbar_message_shown(self, 'share')
+
+ @pytest.mark.run(order=25)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, 'share', False)
+
+ @pytest.mark.run(order=26)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_tor/onionshare_timer_test.py b/tests_gui_tor/onionshare_timer_test.py
new file mode 100644
index 00000000..2b64b998
--- /dev/null
+++ b/tests_gui_tor/onionshare_timer_test.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, False, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": False,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": True,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_file_selection_widget_has_a_file(self):
+ CommonTests.test_file_selection_widget_has_a_file(self)
+
+ @pytest.mark.run(order=6)
+ def test_info_widget_shows_less(self):
+ CommonTests.test_info_widget_shows_less(self, 'share')
+
+ @pytest.mark.run(order=7)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, 'share')
+
+ @pytest.mark.run(order=8)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, 'share')
+
+ @pytest.mark.run(order=9)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, 'share')
+
+ @pytest.mark.run(order=10)
+ def test_set_timeout(self):
+ CommonTests.test_set_timeout(self, 'share', 120)
+
+ @pytest.mark.run(order=11)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'share')
+
+ @pytest.mark.run(order=12)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, 'share')
+
+ @pytest.mark.run(order=13)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, 'share')
+
+ @pytest.mark.run(order=14)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=15)
+ def test_timeout_widget_hidden(self):
+ CommonTests.test_timeout_widget_hidden(self, 'share')
+
+ @pytest.mark.run(order=16)
+ def test_timeout(self):
+ CommonTests.test_server_timed_out(self, 'share', 125000)
+
+ @pytest.mark.run(order=17)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_tor/onionshare_tor_connection_killed_test.py b/tests_gui_tor/onionshare_tor_connection_killed_test.py
new file mode 100644
index 00000000..37abc825
--- /dev/null
+++ b/tests_gui_tor/onionshare_tor_connection_killed_test.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+import os
+import sys
+import unittest
+import pytest
+import json
+
+from PyQt5 import QtWidgets
+
+from onionshare.common import Common
+from onionshare.web import Web
+from onionshare import onion, strings
+from onionshare_gui import *
+
+from .commontests import CommonTests
+
+class OnionShareGuiTest(unittest.TestCase):
+ '''Test the OnionShare GUI'''
+ @classmethod
+ def setUpClass(cls):
+ '''Create the GUI'''
+ # Create our test file
+ testfile = open('/tmp/test.txt', 'w')
+ testfile.write('onionshare')
+ testfile.close()
+ common = Common()
+ common.define_css()
+
+ # Start the Onion
+ strings.load_strings(common)
+
+ testonion = onion.Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, False, 0)
+
+ web = Web(common, False, True)
+
+ test_settings = {
+ "auth_password": "",
+ "auth_type": "no_auth",
+ "autoupdate_timestamp": "",
+ "close_after_first_download": True,
+ "connection_type": "bundled",
+ "control_port_address": "127.0.0.1",
+ "control_port_port": 9051,
+ "downloads_dir": "/tmp/OnionShare",
+ "hidservauth_string": "",
+ "no_bridges": True,
+ "private_key": "",
+ "public_mode": False,
+ "receive_allow_receiver_shutdown": True,
+ "save_private_key": False,
+ "shutdown_timeout": False,
+ "slug": "",
+ "socks_address": "127.0.0.1",
+ "socks_port": 9050,
+ "socket_file_path": "/var/run/tor/control",
+ "systray_notifications": True,
+ "tor_bridges_use_meek_lite_azure": False,
+ "tor_bridges_use_meek_lite_amazon": False,
+ "tor_bridges_use_custom_bridges": "",
+ "tor_bridges_use_obfs4": False,
+ "use_stealth": False,
+ "use_legacy_v2_onions": False,
+ "use_autoupdate": True,
+ "version": "1.3.1"
+ }
+ testsettings = '/tmp/testsettings.json'
+ open(testsettings, 'w').write(json.dumps(test_settings))
+
+ cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
+
+ @classmethod
+ def tearDownClass(cls):
+ '''Clean up after tests'''
+ os.remove('/tmp/test.txt')
+
+ @pytest.mark.run(order=1)
+ def test_gui_loaded(self):
+ CommonTests.test_gui_loaded(self)
+
+ @pytest.mark.run(order=2)
+ def test_windowTitle_seen(self):
+ CommonTests.test_windowTitle_seen(self)
+
+ @pytest.mark.run(order=3)
+ def test_settings_button_is_visible(self):
+ CommonTests.test_settings_button_is_visible(self)
+
+ @pytest.mark.run(order=4)
+ def test_server_status_bar_is_visible(self):
+ CommonTests.test_server_status_bar_is_visible(self)
+
+ @pytest.mark.run(order=5)
+ def test_file_selection_widget_has_a_file(self):
+ CommonTests.test_file_selection_widget_has_a_file(self)
+
+ @pytest.mark.run(order=6)
+ def test_info_widget_shows_less(self):
+ CommonTests.test_info_widget_shows_less(self, 'share')
+
+ @pytest.mark.run(order=7)
+ def test_history_is_not_visible(self):
+ CommonTests.test_history_is_not_visible(self, 'share')
+
+ @pytest.mark.run(order=8)
+ def test_click_toggle_history(self):
+ CommonTests.test_click_toggle_history(self, 'share')
+
+ @pytest.mark.run(order=9)
+ def test_history_is_visible(self):
+ CommonTests.test_history_is_visible(self, 'share')
+
+ @pytest.mark.run(order=10)
+ def test_deleting_only_file_hides_delete_button(self):
+ CommonTests.test_deleting_only_file_hides_delete_button(self)
+
+ @pytest.mark.run(order=11)
+ def test_add_a_file_and_delete_using_its_delete_widget(self):
+ CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
+
+ @pytest.mark.run(order=12)
+ def test_file_selection_widget_readd_files(self):
+ CommonTests.test_file_selection_widget_readd_files(self)
+
+ @pytest.mark.run(order=13)
+ def test_server_working_on_start_button_pressed(self):
+ CommonTests.test_server_working_on_start_button_pressed(self, 'share')
+
+ @pytest.mark.run(order=14)
+ def test_server_status_indicator_says_starting(self):
+ CommonTests.test_server_status_indicator_says_starting(self, 'share')
+
+ @pytest.mark.run(order=15)
+ def test_add_delete_buttons_hidden(self):
+ CommonTests.test_add_delete_buttons_hidden(self)
+
+ @pytest.mark.run(order=16)
+ def test_settings_button_is_hidden(self):
+ CommonTests.test_settings_button_is_hidden(self)
+
+ @pytest.mark.run(order=17)
+ def test_a_server_is_started(self):
+ CommonTests.test_a_server_is_started(self, 'share')
+
+ @pytest.mark.run(order=18)
+ def test_a_web_server_is_running(self):
+ CommonTests.test_a_web_server_is_running(self)
+
+ @pytest.mark.run(order=19)
+ def test_have_a_slug(self):
+ CommonTests.test_have_a_slug(self, 'share', False)
+
+ @pytest.mark.run(order=20)
+ def test_have_an_onion(self):
+ CommonTests.test_have_an_onion_service(self)
+
+ @pytest.mark.run(order=21)
+ def test_url_description_shown(self):
+ CommonTests.test_url_description_shown(self, 'share')
+
+ @pytest.mark.run(order=22)
+ def test_have_copy_url_button(self):
+ CommonTests.test_have_copy_url_button(self, 'share')
+
+ @pytest.mark.run(order=23)
+ def test_server_status_indicator_says_started(self):
+ CommonTests.test_server_status_indicator_says_started(self, 'share')
+
+ @pytest.mark.run(order=24)
+ def test_tor_killed_statusbar_message_shown(self):
+ CommonTests.test_tor_killed_statusbar_message_shown(self, 'share')
+
+ @pytest.mark.run(order=25)
+ def test_server_is_stopped(self):
+ CommonTests.test_server_is_stopped(self, 'share', False)
+
+ @pytest.mark.run(order=26)
+ def test_web_service_is_stopped(self):
+ CommonTests.test_web_service_is_stopped(self)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests_gui_tor/run_unit_tests.sh b/tests_gui_tor/run_unit_tests.sh
new file mode 100755
index 00000000..7d207a57
--- /dev/null
+++ b/tests_gui_tor/run_unit_tests.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+for test in `ls -1 | egrep ^onionshare_`; do
+ pytest $test -vvv || exit 1
+done