summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMicah Lee <micah@micahflee.com>2019-09-15 14:52:57 -0700
committerGitHub <noreply@github.com>2019-09-15 14:52:57 -0700
commit51a1f92d9975f76a0e4bfc36078715baeec203ce (patch)
tree4063a36658eeed59ce48295a0ae789f87cbcdc38
parent414e7f9a38a4762ea845de278f24a8cff35bdbd3 (diff)
parent16fedabf400a696c452c644bcd7031af8b0e2b9e (diff)
downloadonionshare-51a1f92d9975f76a0e4bfc36078715baeec203ce.tar.gz
onionshare-51a1f92d9975f76a0e4bfc36078715baeec203ce.zip
Merge pull request #1020 from micahflee/991_sharing_code
[WIP] Share code between share mode and website mode
-rw-r--r--onionshare/__init__.py6
-rw-r--r--onionshare/common.py17
-rw-r--r--onionshare/web/receive_mode.py41
-rw-r--r--onionshare/web/send_base_mode.py270
-rw-r--r--onionshare/web/share_mode.py163
-rw-r--r--onionshare/web/web.py91
-rw-r--r--onionshare/web/website_mode.py207
-rw-r--r--onionshare_gui/mode/__init__.py33
-rw-r--r--onionshare_gui/mode/history.py149
-rw-r--r--onionshare_gui/mode/receive_mode/__init__.py6
-rw-r--r--onionshare_gui/mode/share_mode/__init__.py12
-rw-r--r--onionshare_gui/mode/share_mode/threads.py8
-rw-r--r--onionshare_gui/mode/website_mode/__init__.py29
-rw-r--r--onionshare_gui/onionshare_gui.py11
-rw-r--r--onionshare_gui/settings_dialog.py3
-rw-r--r--share/images/history_completed.png (renamed from share/images/share_completed.png)bin646 -> 646 bytes
-rw-r--r--share/images/history_completed_none.png (renamed from share/images/share_completed_none.png)bin437 -> 437 bytes
-rw-r--r--share/images/history_in_progress.png (renamed from share/images/share_in_progress.png)bin638 -> 638 bytes
-rw-r--r--share/images/history_in_progress_none.png (renamed from share/images/share_in_progress_none.png)bin412 -> 412 bytes
-rw-r--r--share/images/history_requests.pngbin0 -> 738 bytes
-rw-r--r--share/images/history_requests_none.pngbin0 -> 754 bytes
-rw-r--r--share/locale/en.json4
-rw-r--r--share/static/css/style.css13
-rw-r--r--share/templates/405.html19
-rw-r--r--share/templates/send.html19
-rw-r--r--tests/GuiBaseTest.py20
-rw-r--r--tests/GuiReceiveTest.py39
-rw-r--r--tests/GuiShareTest.py73
-rw-r--r--tests/GuiWebsiteTest.py100
-rw-r--r--tests/TorGuiShareTest.py2
-rw-r--r--tests/local_onionshare_receive_mode_clear_all_button_test.py25
-rw-r--r--tests/local_onionshare_share_mode_clear_all_button_test.py26
-rw-r--r--tests/local_onionshare_share_mode_individual_file_view_stay_open_test.py26
-rw-r--r--tests/local_onionshare_share_mode_individual_file_view_test.py26
-rw-r--r--tests/local_onionshare_website_mode_test.py25
35 files changed, 1065 insertions, 398 deletions
diff --git a/onionshare/__init__.py b/onionshare/__init__.py
index 7a1bf170..7e7798f8 100644
--- a/onionshare/__init__.py
+++ b/onionshare/__init__.py
@@ -109,6 +109,8 @@ def main(cwd=None):
# Re-load settings, if a custom config was passed in
if config:
common.load_settings(config)
+ else:
+ common.load_settings()
# Verbose mode?
common.verbose = verbose
@@ -260,12 +262,12 @@ def main(cwd=None):
if not app.autostop_timer_thread.is_alive():
if mode == 'share' or (mode == 'website'):
# 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:
+ if web.share_mode.cur_history_id == 0 or web.done:
print("Stopped because auto-stop timer ran out")
web.stop(app.port)
break
if mode == 'receive':
- if web.receive_mode.upload_count == 0 or not web.receive_mode.uploads_in_progress:
+ if web.receive_mode.cur_history_id == 0 or not web.receive_mode.uploads_in_progress:
print("Stopped because auto-stop timer ran out")
web.stop(app.port)
break
diff --git a/onionshare/common.py b/onionshare/common.py
index 27e8efc2..ab503fdc 100644
--- a/onionshare/common.py
+++ b/onionshare/common.py
@@ -203,7 +203,7 @@ class Common(object):
border: 0px;
}""",
- # Common styles between ShareMode and ReceiveMode and their child widgets
+ # Common styles between modes and their child widgets
'mode_info_label': """
QLabel {
font-size: 12px;
@@ -310,6 +310,21 @@ class Common(object):
width: 10px;
}""",
+ 'history_individual_file_timestamp_label': """
+ QLabel {
+ color: #666666;
+ }""",
+
+ 'history_individual_file_status_code_label_2xx': """
+ QLabel {
+ color: #008800;
+ }""",
+
+ 'history_individual_file_status_code_label_4xx': """
+ QLabel {
+ color: #cc0000;
+ }""",
+
# Share mode and child widget styles
'share_zip_progess_bar': """
QProgressBar {
diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py
index 3f848d2f..83040683 100644
--- a/onionshare/web/receive_mode.py
+++ b/onionshare/web/receive_mode.py
@@ -8,7 +8,7 @@ from werkzeug.utils import secure_filename
from .. import strings
-class ReceiveModeWeb(object):
+class ReceiveModeWeb:
"""
All of the web logic for receive mode
"""
@@ -18,13 +18,12 @@ class ReceiveModeWeb(object):
self.web = web
- # Reset assets path
- self.web.app.static_folder=self.common.get_resource_path('static')
-
self.can_upload = True
- self.upload_count = 0
self.uploads_in_progress = []
+ # This tracks the history id
+ self.cur_history_id = 0
+
self.define_routes()
def define_routes(self):
@@ -33,8 +32,15 @@ class ReceiveModeWeb(object):
"""
@self.web.app.route("/")
def index():
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_STARTED, '{}'.format(request.path), {
+ 'id': history_id,
+ 'status_code': 200
+ })
+
self.web.add_request(self.web.REQUEST_LOAD, request.path)
- r = make_response(render_template('receive.html',
+ r = make_response(render_template('receive.html',
static_url_path=self.web.static_url_path))
return self.web.add_security_headers(r)
@@ -55,7 +61,7 @@ class ReceiveModeWeb(object):
# Tell the GUI the receive mode directory for this file
self.web.add_request(self.web.REQUEST_UPLOAD_SET_DIR, request.path, {
- 'id': request.upload_id,
+ 'id': request.history_id,
'filename': basename,
'dir': request.receive_mode_dir
})
@@ -275,10 +281,9 @@ class ReceiveModeRequest(Request):
# Prevent new uploads if we've said so (timer expired)
if self.web.receive_mode.can_upload:
- # 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
+ # Create an history_id, attach it to the request
+ self.history_id = self.web.receive_mode.cur_history_id
+ self.web.receive_mode.cur_history_id += 1
# Figure out the content length
try:
@@ -305,10 +310,10 @@ class ReceiveModeRequest(Request):
if not self.told_gui_about_request:
# Tell the GUI about the request
self.web.add_request(self.web.REQUEST_STARTED, self.path, {
- 'id': self.upload_id,
+ 'id': self.history_id,
'content_length': self.content_length
})
- self.web.receive_mode.uploads_in_progress.append(self.upload_id)
+ self.web.receive_mode.uploads_in_progress.append(self.history_id)
self.told_gui_about_request = True
@@ -340,19 +345,19 @@ class ReceiveModeRequest(Request):
try:
if self.told_gui_about_request:
- upload_id = self.upload_id
+ history_id = self.history_id
if not self.web.stop_q.empty() or not self.progress[self.filename]['complete']:
# Inform the GUI that the upload has canceled
self.web.add_request(self.web.REQUEST_UPLOAD_CANCELED, self.path, {
- 'id': upload_id
+ 'id': history_id
})
else:
# Inform the GUI that the upload has finished
self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, {
- 'id': upload_id
+ 'id': history_id
})
- self.web.receive_mode.uploads_in_progress.remove(upload_id)
+ self.web.receive_mode.uploads_in_progress.remove(history_id)
except AttributeError:
pass
@@ -378,7 +383,7 @@ class ReceiveModeRequest(Request):
# Update the GUI on the upload progress
if self.told_gui_about_request:
self.web.add_request(self.web.REQUEST_PROGRESS, self.path, {
- 'id': self.upload_id,
+ 'id': self.history_id,
'progress': self.progress
})
diff --git a/onionshare/web/send_base_mode.py b/onionshare/web/send_base_mode.py
new file mode 100644
index 00000000..67fb26d0
--- /dev/null
+++ b/onionshare/web/send_base_mode.py
@@ -0,0 +1,270 @@
+import os
+import sys
+import tempfile
+import mimetypes
+import gzip
+from flask import Response, request, render_template, make_response
+
+from .. import strings
+
+
+class SendBaseModeWeb:
+ """
+ All of the web logic shared between share and website mode (modes where the user sends files)
+ """
+ def __init__(self, common, web):
+ super(SendBaseModeWeb, self).__init__()
+ self.common = common
+ self.web = web
+
+ # Information about the file to be shared
+ self.is_zipped = False
+ self.download_filename = None
+ self.download_filesize = None
+ self.gzip_filename = None
+ self.gzip_filesize = None
+ self.zip_writer = None
+
+ # If "Stop After First Download" is checked (stay_open == False), only allow
+ # one download at a time.
+ self.download_in_progress = False
+
+ # This tracks the history id
+ self.cur_history_id = 0
+
+ self.define_routes()
+ self.init()
+
+ def set_file_info(self, filenames, processed_size_callback=None):
+ """
+ Build a data structure that describes the list of files
+ """
+ # If there's just one folder, replace filenames with a list of files inside that folder
+ if len(filenames) == 1 and os.path.isdir(filenames[0]):
+ filenames = [os.path.join(filenames[0], x) for x in os.listdir(filenames[0])]
+
+ # Re-initialize
+ self.files = {} # Dictionary mapping file paths to filenames on disk
+ self.root_files = {} # This is only the root files and dirs, as opposed to all of them
+ self.cleanup_filenames = []
+ self.cur_history_id = 0
+ self.file_info = {'files': [], 'dirs': []}
+ self.gzip_individual_files = {}
+ self.init()
+
+ # Build the file list
+ for filename in filenames:
+ basename = os.path.basename(filename.rstrip('/'))
+
+ # If it's a filename, add it
+ if os.path.isfile(filename):
+ self.files[basename] = filename
+ self.root_files[basename] = filename
+
+ # If it's a directory, add it recursively
+ elif os.path.isdir(filename):
+ self.root_files[basename + '/'] = filename
+
+ for root, _, nested_filenames in os.walk(filename):
+ # Normalize the root path. So if the directory name is "/home/user/Documents/some_folder",
+ # and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar".
+ # The normalized_root should be "some_folder/foobar"
+ normalized_root = os.path.join(basename, root[len(filename):].lstrip('/')).rstrip('/')
+
+ # Add the dir itself
+ self.files[normalized_root + '/'] = root
+
+ # Add the files in this dir
+ for nested_filename in nested_filenames:
+ self.files[os.path.join(normalized_root, nested_filename)] = os.path.join(root, nested_filename)
+
+ self.set_file_info_custom(filenames, processed_size_callback)
+
+ def directory_listing(self, filenames, path='', filesystem_path=None):
+ # Tell the GUI about the directory listing
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_STARTED, '/{}'.format(path), {
+ 'id': history_id,
+ 'method': request.method,
+ 'status_code': 200
+ })
+
+ # If filesystem_path is None, this is the root directory listing
+ files, dirs = self.build_directory_listing(filenames, filesystem_path)
+ r = self.directory_listing_template(path, files, dirs)
+ return self.web.add_security_headers(r)
+
+ def build_directory_listing(self, filenames, filesystem_path):
+ files = []
+ dirs = []
+
+ for filename in filenames:
+ if filesystem_path:
+ this_filesystem_path = os.path.join(filesystem_path, filename)
+ else:
+ this_filesystem_path = self.files[filename]
+
+ is_dir = os.path.isdir(this_filesystem_path)
+
+ if is_dir:
+ dirs.append({
+ 'basename': filename
+ })
+ else:
+ size = os.path.getsize(this_filesystem_path)
+ size_human = self.common.human_readable_filesize(size)
+ files.append({
+ 'basename': filename,
+ 'size_human': size_human
+ })
+ return files, dirs
+
+ def stream_individual_file(self, filesystem_path):
+ """
+ Return a flask response that's streaming the download of an individual file, and gzip
+ compressing it if the browser supports it.
+ """
+ use_gzip = self.should_use_gzip()
+
+ # gzip compress the individual file, if it hasn't already been compressed
+ if use_gzip:
+ if filesystem_path not in self.gzip_individual_files:
+ gzip_filename = tempfile.mkstemp('wb+')[1]
+ self._gzip_compress(filesystem_path, gzip_filename, 6, None)
+ self.gzip_individual_files[filesystem_path] = gzip_filename
+
+ # Make sure the gzip file gets cleaned up when onionshare stops
+ self.cleanup_filenames.append(gzip_filename)
+
+ file_to_download = self.gzip_individual_files[filesystem_path]
+ filesize = os.path.getsize(self.gzip_individual_files[filesystem_path])
+ else:
+ file_to_download = filesystem_path
+ filesize = os.path.getsize(filesystem_path)
+
+ path = request.path
+
+ # Tell GUI the individual file started
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_STARTED, path, {
+ 'id': history_id,
+ 'filesize': filesize
+ })
+
+ # Only GET requests are allowed, any other method should fail
+ if request.method != "GET":
+ return self.web.error405()
+
+ def generate():
+ chunk_size = 102400 # 100kb
+
+ fp = open(file_to_download, 'rb')
+ done = False
+ while not done:
+ chunk = fp.read(chunk_size)
+ if chunk == b'':
+ done = True
+ else:
+ try:
+ yield chunk
+
+ # Tell GUI the progress
+ downloaded_bytes = fp.tell()
+ percent = (1.0 * downloaded_bytes / filesize) * 100
+ 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_INDIVIDUAL_FILE_PROGRESS, path, {
+ 'id': history_id,
+ 'bytes': downloaded_bytes,
+ 'filesize': filesize
+ })
+ done = False
+ except:
+ # Looks like the download was canceled
+ done = True
+
+ # Tell the GUI the individual file was canceled
+ self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_CANCELED, path, {
+ 'id': history_id
+ })
+
+ fp.close()
+
+ if self.common.platform != 'Darwin':
+ sys.stdout.write("\n")
+
+ basename = os.path.basename(filesystem_path)
+
+ r = Response(generate())
+ if use_gzip:
+ r.headers.set('Content-Encoding', 'gzip')
+ r.headers.set('Content-Length', filesize)
+ r.headers.set('Content-Disposition', 'inline', filename=basename)
+ r = self.web.add_security_headers(r)
+ (content_type, _) = mimetypes.guess_type(basename, strict=False)
+ if content_type is not None:
+ r.headers.set('Content-Type', content_type)
+ return r
+
+ 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()
+
+ def init(self):
+ """
+ Inherited class will implement this
+ """
+ pass
+
+ def define_routes(self):
+ """
+ Inherited class will implement this
+ """
+ pass
+
+ def directory_listing_template(self):
+ """
+ Inherited class will implement this. It should call render_template and return
+ the response.
+ """
+ pass
+
+ def set_file_info_custom(self, filenames, processed_size_callback):
+ """
+ Inherited class will implement this.
+ """
+ pass
+
+ def render_logic(self, path=''):
+ """
+ Inherited class will implement this.
+ """
+ pass
diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py
index 0dfa7e0a..f52bc2c7 100644
--- a/onionshare/web/share_mode.py
+++ b/onionshare/web/share_mode.py
@@ -3,55 +3,35 @@ import sys
import tempfile
import zipfile
import mimetypes
-import gzip
from flask import Response, request, render_template, make_response
+from .send_base_mode import SendBaseModeWeb
from .. import strings
-class ShareModeWeb(object):
+class ShareModeWeb(SendBaseModeWeb):
"""
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
-
- # Reset assets path
- self.web.app.static_folder=self.common.get_resource_path('static')
+ def init(self):
+ self.common.log('ShareModeWeb', 'init')
-
- self.define_routes()
+ # Allow downloading individual files if "Stop sharing after files have been sent" is unchecked
+ self.download_individual_files = not self.common.settings.get('close_after_first_download')
def define_routes(self):
"""
The web app routes for sharing files
"""
- @self.web.app.route("/")
- def index():
+ @self.web.app.route('/', defaults={'path': ''})
+ @self.web.app.route('/<path:path>')
+ def index(path):
"""
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
+ # Deny new downloads if "Stop sharing after files have been sent" is checked and there is
# currently a download
deny_download = not self.web.stay_open and self.download_in_progress
if deny_download:
@@ -65,15 +45,7 @@ class ShareModeWeb(object):
else:
self.filesize = self.download_filesize
- 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,
- static_url_path=self.web.static_url_path))
- return self.web.add_security_headers(r)
+ return self.render_logic(path)
@self.web.app.route("/download")
def download():
@@ -88,10 +60,6 @@ class ShareModeWeb(object):
static_url_path=self.web.static_url_path))
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')
@@ -109,8 +77,10 @@ class ShareModeWeb(object):
self.filesize = self.download_filesize
# Tell GUI the download started
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
self.web.add_request(self.web.REQUEST_STARTED, path, {
- 'id': download_id,
+ 'id': history_id,
'use_gzip': use_gzip
})
@@ -130,7 +100,7 @@ class ShareModeWeb(object):
# The user has canceled the download, so stop serving the file
if not self.web.stop_q.empty():
self.web.add_request(self.web.REQUEST_CANCELED, path, {
- 'id': download_id
+ 'id': history_id
})
break
@@ -152,7 +122,7 @@ class ShareModeWeb(object):
sys.stdout.flush()
self.web.add_request(self.web.REQUEST_PROGRESS, path, {
- 'id': download_id,
+ 'id': history_id,
'bytes': downloaded_bytes
})
self.web.done = False
@@ -163,7 +133,7 @@ class ShareModeWeb(object):
# tell the GUI the download has canceled
self.web.add_request(self.web.REQUEST_CANCELED, path, {
- 'id': download_id
+ 'id': history_id
})
fp.close()
@@ -198,19 +168,71 @@ class ShareModeWeb(object):
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")
+ def directory_listing_template(self, path, files, dirs):
+ return make_response(render_template(
+ 'send.html',
+ file_info=self.file_info,
+ files=files,
+ dirs=dirs,
+ 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,
+ static_url_path=self.web.static_url_path,
+ download_individual_files=self.download_individual_files))
+
+ def set_file_info_custom(self, filenames, processed_size_callback):
+ self.common.log("ShareModeWeb", "set_file_info_custom")
self.web.cancel_compression = False
+ self.build_zipfile_list(filenames, processed_size_callback)
+
+ def render_logic(self, path=''):
+ if path in self.files:
+ filesystem_path = self.files[path]
+
+ # If it's a directory
+ if os.path.isdir(filesystem_path):
+ # Render directory listing
+ filenames = []
+ for filename in os.listdir(filesystem_path):
+ if os.path.isdir(os.path.join(filesystem_path, filename)):
+ filenames.append(filename + '/')
+ else:
+ filenames.append(filename)
+ filenames.sort()
+ return self.directory_listing(filenames, path, filesystem_path)
+
+ # If it's a file
+ elif os.path.isfile(filesystem_path):
+ if self.download_individual_files:
+ return self.stream_individual_file(filesystem_path)
+ else:
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ return self.web.error404(history_id)
+
+ # If it's not a directory or file, throw a 404
+ else:
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ return self.web.error404(history_id)
+ else:
+ # Special case loading /
- self.cleanup_filenames = []
+ if path == '':
+ # Root directory listing
+ filenames = list(self.root_files)
+ filenames.sort()
+ return self.directory_listing(filenames, path)
- # build file info list
- self.file_info = {'files': [], 'dirs': []}
+ else:
+ # If the path isn't found, throw a 404
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ return self.web.error404(history_id)
+
+ def build_zipfile_list(self, filenames, processed_size_callback=None):
+ self.common.log("ShareModeWeb", "build_zipfile_list")
for filename in filenames:
info = {
'filename': filename,
@@ -267,33 +289,6 @@ class ShareModeWeb(object):
return True
- def should_use_gzip(self):
- """
- Should we use gzip for this browser?
- """
- return (not self.is_zipped) and ('gzip' in request.headers.get('Accept-Encoding', '').lower())
-
- def _gzip_compress(self, input_filename, output_filename, level, processed_size_callback=None):
- """
- Compress a file with gzip, without loading the whole thing into memory
- Thanks: https://stackoverflow.com/questions/27035296/python-how-to-gzip-a-large-text-file-without-memoryerror
- """
- bytes_processed = 0
- blocksize = 1 << 16 # 64kB
- with open(input_filename, 'rb') as input_file:
- output_file = gzip.open(output_filename, 'wb', level)
- while True:
- if processed_size_callback is not None:
- processed_size_callback(bytes_processed)
-
- block = input_file.read(blocksize)
- if len(block) == 0:
- break
- output_file.write(block)
- bytes_processed += blocksize
-
- output_file.close()
-
class ZipWriter(object):
"""
diff --git a/onionshare/web/web.py b/onionshare/web/web.py
index 1d2a3fec..ecd9edc2 100644
--- a/onionshare/web/web.py
+++ b/onionshare/web/web.py
@@ -10,7 +10,7 @@ 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 flask import Flask, request, render_template, abort, make_response, send_file, __version__ as flask_version
from flask_httpauth import HTTPBasicAuth
from .. import strings
@@ -30,22 +30,25 @@ except:
pass
-class Web(object):
+class Web:
"""
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_UPLOAD_FILE_RENAMED = 6
- REQUEST_UPLOAD_SET_DIR = 7
- REQUEST_UPLOAD_FINISHED = 8
- REQUEST_UPLOAD_CANCELED = 9
- REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 10
- REQUEST_INVALID_PASSWORD = 11
+ REQUEST_CANCELED = 3
+ REQUEST_RATE_LIMIT = 4
+ REQUEST_UPLOAD_FILE_RENAMED = 5
+ REQUEST_UPLOAD_SET_DIR = 6
+ REQUEST_UPLOAD_FINISHED = 7
+ REQUEST_UPLOAD_CANCELED = 8
+ REQUEST_INDIVIDUAL_FILE_STARTED = 9
+ REQUEST_INDIVIDUAL_FILE_PROGRESS = 10
+ REQUEST_INDIVIDUAL_FILE_CANCELED = 11
+ REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 12
+ REQUEST_OTHER = 13
+ REQUEST_INVALID_PASSWORD = 14
def __init__(self, common, is_gui, mode='share'):
self.common = common
@@ -116,13 +119,35 @@ class Web(object):
# Create the mode web object, which defines its own routes
self.share_mode = None
self.receive_mode = None
- if self.mode == 'receive':
+ self.website_mode = None
+ if self.mode == 'share':
+ self.share_mode = ShareModeWeb(self.common, self)
+ elif self.mode == 'receive':
self.receive_mode = ReceiveModeWeb(self.common, self)
elif self.mode == 'website':
self.website_mode = WebsiteModeWeb(self.common, self)
- elif self.mode == 'share':
- self.share_mode = ShareModeWeb(self.common, self)
+ def get_mode(self):
+ if self.mode == 'share':
+ return self.share_mode
+ elif self.mode == 'receive':
+ return self.receive_mode
+ elif self.mode == 'website':
+ return self.website_mode
+ else:
+ return None
+
+ def generate_static_url_path(self):
+ # The static URL path has a 128-bit random number in it to avoid having name
+ # collisions with files that might be getting shared
+ self.static_url_path = '/static_{}'.format(self.common.random_string(16))
+ self.common.log('Web', 'generate_static_url_path', 'new static_url_path is {}'.format(self.static_url_path))
+
+ # Update the flask route to handle the new static URL path
+ self.app.static_url_path = self.static_url_path
+ self.app.add_url_rule(
+ self.static_url_path + '/<path:filename>',
+ endpoint='static', view_func=self.app.send_static_file)
def define_common_routes(self):
"""
@@ -152,7 +177,10 @@ class Web(object):
@self.app.errorhandler(404)
def not_found(e):
- return self.error404()
+ mode = self.get_mode()
+ history_id = mode.cur_history_id
+ mode.cur_history_id += 1
+ return self.error404(history_id)
@self.app.route("/<password_candidate>/shutdown")
def shutdown(password_candidate):
@@ -164,6 +192,11 @@ class Web(object):
return ""
abort(404)
+ if self.mode != 'website':
+ @self.app.route("/favicon.ico")
+ def favicon():
+ return send_file('{}/img/favicon.ico'.format(self.common.get_resource_path('static')))
+
def error401(self):
auth = request.authorization
if auth:
@@ -182,15 +215,23 @@ class Web(object):
r = make_response(render_template('401.html', static_url_path=self.static_url_path), 401)
return self.add_security_headers(r)
- def error404(self):
+ def error403(self):
self.add_request(Web.REQUEST_OTHER, request.path)
- r = make_response(render_template('404.html', static_url_path=self.static_url_path), 404)
+ r = make_response(render_template('403.html', static_url_path=self.static_url_path), 403)
return self.add_security_headers(r)
- def error403(self):
+ def error404(self, history_id):
+ self.add_request(self.REQUEST_INDIVIDUAL_FILE_STARTED, '{}'.format(request.path), {
+ 'id': history_id,
+ 'status_code': 404
+ })
+
self.add_request(Web.REQUEST_OTHER, request.path)
+ r = make_response(render_template('404.html', static_url_path=self.static_url_path), 404)
+ return self.add_security_headers(r)
- r = make_response(render_template('403.html', static_url_path=self.static_url_path), 403)
+ def error405(self):
+ r = make_response(render_template('405.html', static_url_path=self.static_url_path), 405)
return self.add_security_headers(r)
def add_security_headers(self, r):
@@ -225,18 +266,6 @@ class Web(object):
self.password = self.common.build_password()
self.common.log('Web', 'generate_password', 'built random password: "{}"'.format(self.password))
- def generate_static_url_path(self):
- # The static URL path has a 128-bit random number in it to avoid having name
- # collisions with files that might be getting shared
- self.static_url_path = '/static_{}'.format(self.common.random_string(16))
- self.common.log('Web', 'generate_static_url_path', 'new static_url_path is {}'.format(self.static_url_path))
-
- # Update the flask route to handle the new static URL path
- self.app.static_url_path = self.static_url_path
- self.app.add_url_rule(
- self.static_url_path + '/<path:filename>',
- endpoint='static', view_func=self.app.send_static_file)
-
def verbose_mode(self):
"""
Turn on verbose mode, which will log flask errors to a file.
diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py
index d2cd6db9..0b7602ea 100644
--- a/onionshare/web/website_mode.py
+++ b/onionshare/web/website_mode.py
@@ -2,35 +2,23 @@ import os
import sys
import tempfile
import mimetypes
-from flask import Response, request, render_template, make_response, send_from_directory
+from flask import Response, request, render_template, make_response
+from .send_base_mode import SendBaseModeWeb
from .. import strings
-class WebsiteModeWeb(object):
+class WebsiteModeWeb(SendBaseModeWeb):
"""
- All of the web logic for share mode
+ All of the web logic for website mode
"""
- def __init__(self, common, web):
- self.common = common
- self.common.log('WebsiteModeWeb', '__init__')
-
- self.web = web
-
- # Dictionary mapping file paths to filenames on disk
- self.files = {}
- self.visit_count = 0
-
- # Reset assets path
- self.web.app.static_folder=self.common.get_resource_path('static')
-
- self.define_routes()
+ def init(self):
+ pass
def define_routes(self):
"""
The web app routes for sharing a website
"""
-
@self.web.app.route('/', defaults={'path': ''})
@self.web.app.route('/<path:path>')
def path_public(path):
@@ -40,142 +28,67 @@ class WebsiteModeWeb(object):
"""
Render the onionshare website.
"""
+ return self.render_logic(path)
- # Each download has a unique id
- visit_id = self.visit_count
- self.visit_count += 1
-
- # Tell GUI the page has been visited
- self.web.add_request(self.web.REQUEST_STARTED, path, {
- 'id': visit_id,
- 'action': 'visit'
- })
-
- if path in self.files:
- filesystem_path = self.files[path]
-
- # If it's a directory
- if os.path.isdir(filesystem_path):
- # Is there an index.html?
- index_path = os.path.join(path, 'index.html')
- if index_path in self.files:
- # Render it
- dirname = os.path.dirname(self.files[index_path])
- basename = os.path.basename(self.files[index_path])
- return send_from_directory(dirname, basename)
-
- else:
- # Otherwise, render directory listing
- filenames = []
- for filename in os.listdir(filesystem_path):
- if os.path.isdir(os.path.join(filesystem_path, filename)):
- filenames.append(filename + '/')
- else:
- filenames.append(filename)
- filenames.sort()
- return self.directory_listing(path, filenames, filesystem_path)
-
- # If it's a file
- elif os.path.isfile(filesystem_path):
- dirname = os.path.dirname(filesystem_path)
- basename = os.path.basename(filesystem_path)
- return send_from_directory(dirname, basename)
-
- # If it's not a directory or file, throw a 404
- else:
- return self.web.error404()
- else:
- # Special case loading /
- if path == '':
- index_path = 'index.html'
- if index_path in self.files:
- # Render it
- dirname = os.path.dirname(self.files[index_path])
- basename = os.path.basename(self.files[index_path])
- return send_from_directory(dirname, basename)
- else:
- # Root directory listing
- filenames = list(self.root_files)
- filenames.sort()
- return self.directory_listing(path, filenames)
-
- else:
- # If the path isn't found, throw a 404
- return self.web.error404()
-
- def directory_listing(self, path, filenames, filesystem_path=None):
- # If filesystem_path is None, this is the root directory listing
- files = []
- dirs = []
-
- for filename in filenames:
- if filesystem_path:
- this_filesystem_path = os.path.join(filesystem_path, filename)
- else:
- this_filesystem_path = self.files[filename]
-
- is_dir = os.path.isdir(this_filesystem_path)
-
- if is_dir:
- dirs.append({
- 'basename': filename
- })
- else:
- size = os.path.getsize(this_filesystem_path)
- size_human = self.common.human_readable_filesize(size)
- files.append({
- 'basename': filename,
- 'size_human': size_human
- })
-
- r = make_response(render_template('listing.html',
+ def directory_listing_template(self, path, files, dirs):
+ return make_response(render_template('listing.html',
path=path,
files=files,
dirs=dirs,
static_url_path=self.web.static_url_path))
- return self.web.add_security_headers(r)
-
- def set_file_info(self, filenames):
- """
- Build a data structure that describes the list of files that make up
- the static website.
- """
- self.common.log("WebsiteModeWeb", "set_file_info")
-
- # This is a dictionary that maps HTTP routes to filenames on disk
- self.files = {}
- # This is only the root files and dirs, as opposed to all of them
- self.root_files = {}
+ def set_file_info_custom(self, filenames, processed_size_callback):
+ self.common.log("WebsiteModeWeb", "set_file_info_custom")
+ self.web.cancel_compression = True
- # If there's just one folder, replace filenames with a list of files inside that folder
- if len(filenames) == 1 and os.path.isdir(filenames[0]):
- filenames = [os.path.join(filenames[0], x) for x in os.listdir(filenames[0])]
+ def render_logic(self, path=''):
+ if path in self.files:
+ filesystem_path = self.files[path]
- # Loop through the files
- for filename in filenames:
- basename = os.path.basename(filename.rstrip('/'))
+ # If it's a directory
+ if os.path.isdir(filesystem_path):
+ # Is there an index.html?
+ index_path = os.path.join(path, 'index.html')
+ if index_path in self.files:
+ # Render it
+ return self.stream_individual_file(filesystem_path)
- # If it's a filename, add it
- if os.path.isfile(filename):
- self.files[basename] = filename
- self.root_files[basename] = filename
-
- # If it's a directory, add it recursively
- elif os.path.isdir(filename):
- self.root_files[basename + '/'] = filename
-
- for root, _, nested_filenames in os.walk(filename):
- # Normalize the root path. So if the directory name is "/home/user/Documents/some_folder",
- # and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar".
- # The normalized_root should be "some_folder/foobar"
- normalized_root = os.path.join(basename, root[len(filename):].lstrip('/')).rstrip('/')
-
- # Add the dir itself
- self.files[normalized_root + '/'] = root
-
- # Add the files in this dir
- for nested_filename in nested_filenames:
- self.files[os.path.join(normalized_root, nested_filename)] = os.path.join(root, nested_filename)
+ else:
+ # Otherwise, render directory listing
+ filenames = []
+ for filename in os.listdir(filesystem_path):
+ if os.path.isdir(os.path.join(filesystem_path, filename)):
+ filenames.append(filename + '/')
+ else:
+ filenames.append(filename)
+ filenames.sort()
+ return self.directory_listing(filenames, path, filesystem_path)
+
+ # If it's a file
+ elif os.path.isfile(filesystem_path):
+ return self.stream_individual_file(filesystem_path)
+
+ # If it's not a directory or file, throw a 404
+ else:
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ return self.web.error404(history_id)
+ else:
+ # Special case loading /
+
+ if path == '':
+ index_path = 'index.html'
+ if index_path in self.files:
+ # Render it
+ return self.stream_individual_file(self.files[index_path])
+ else:
+ # Root directory listing
+ filenames = list(self.root_files)
+ filenames.sort()
+ return self.directory_listing(filenames, path)
- return True
+ else:
+ # If the path isn't found, throw a 404
+ history_id = self.cur_history_id
+ self.cur_history_id += 1
+ return self.web.error404(history_id)
diff --git a/onionshare_gui/mode/__init__.py b/onionshare_gui/mode/__init__.py
index e92e36f8..3ef285c4 100644
--- a/onionshare_gui/mode/__init__.py
+++ b/onionshare_gui/mode/__init__.py
@@ -22,6 +22,8 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from onionshare.common import AutoStopTimer
+from .history import IndividualFileHistoryItem
+
from ..server_status import ServerStatus
from ..threads import OnionThread
from ..threads import AutoStartTimer
@@ -29,7 +31,7 @@ from ..widgets import Alert
class Mode(QtWidgets.QWidget):
"""
- The class that ShareMode and ReceiveMode inherit from.
+ The class that all modes inherit from
"""
start_server_finished = QtCore.pyqtSignal()
stop_server_finished = QtCore.pyqtSignal()
@@ -417,3 +419,32 @@ class Mode(QtWidgets.QWidget):
Handle REQUEST_UPLOAD_CANCELED event.
"""
pass
+
+ def handle_request_individual_file_started(self, event):
+ """
+ Handle REQUEST_INDVIDIDUAL_FILES_STARTED event.
+ Used in both Share and Website modes, so implemented here.
+ """
+ self.toggle_history.update_indicator(True)
+ self.history.requests_count += 1
+ self.history.update_requests()
+
+ item = IndividualFileHistoryItem(self.common, event["data"], event["path"])
+ self.history.add(event["data"]["id"], item)
+
+ def handle_request_individual_file_progress(self, event):
+ """
+ Handle REQUEST_INDVIDIDUAL_FILES_PROGRESS event.
+ Used in both Share and Website modes, so implemented here.
+ """
+ self.history.update(event["data"]["id"], event["data"]["bytes"])
+
+ if self.server_status.status == self.server_status.STATUS_STOPPED:
+ self.history.cancel(event["data"]["id"])
+
+ def handle_request_individual_file_canceled(self, event):
+ """
+ Handle REQUEST_INDVIDIDUAL_FILES_CANCELED event.
+ Used in both Share and Website modes, so implemented here.
+ """
+ self.history.cancel(event["data"]["id"])
diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py
index 51b36f9a..b8baebd1 100644
--- a/onionshare_gui/mode/history.py
+++ b/onionshare_gui/mode/history.py
@@ -237,6 +237,7 @@ class ReceiveHistoryItemFile(QtWidgets.QWidget):
elif self.common.platform == 'Windows':
subprocess.Popen(['explorer', '/select,{}'.format(abs_filename)])
+
class ReceiveHistoryItem(HistoryItem):
def __init__(self, common, id, content_length):
super(ReceiveHistoryItem, self).__init__()
@@ -341,35 +342,108 @@ class ReceiveHistoryItem(HistoryItem):
self.label.setText(self.get_canceled_label_text(self.started))
-class VisitHistoryItem(HistoryItem):
+class IndividualFileHistoryItem(HistoryItem):
"""
- Download history item, for share mode
+ Individual file history item, for share mode viewing of individual files
"""
- def __init__(self, common, id, total_bytes):
- super(VisitHistoryItem, self).__init__()
+ def __init__(self, common, data, path):
+ super(IndividualFileHistoryItem, self).__init__()
self.status = HistoryItem.STATUS_STARTED
self.common = common
self.id = id
- self.visited = time.time()
- self.visited_dt = datetime.fromtimestamp(self.visited)
+ self.path = path
+ self.total_bytes = 0
+ self.downloaded_bytes = 0
+ self.started = time.time()
+ self.started_dt = datetime.fromtimestamp(self.started)
+ self.status = HistoryItem.STATUS_STARTED
- # Label
- self.label = QtWidgets.QLabel(strings._('gui_visit_started').format(self.visited_dt.strftime("%b %d, %I:%M%p")))
+ self.directory_listing = 'directory_listing' in data
+
+ # Labels
+ self.timestamp_label = QtWidgets.QLabel(self.started_dt.strftime("%b %d, %I:%M%p"))
+ self.timestamp_label.setStyleSheet(self.common.css['history_individual_file_timestamp_label'])
+ self.path_label = QtWidgets.QLabel("{}".format(self.path))
+ self.status_code_label = QtWidgets.QLabel()
+
+ # Progress bar
+ self.progress_bar = QtWidgets.QProgressBar()
+ self.progress_bar.setTextVisible(True)
+ self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
+ self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
+ self.progress_bar.setValue(0)
+ self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
+
+ # Text layout
+ labels_layout = QtWidgets.QHBoxLayout()
+ labels_layout.addWidget(self.timestamp_label)
+ labels_layout.addWidget(self.path_label)
+ labels_layout.addWidget(self.status_code_label)
+ labels_layout.addStretch()
# Layout
layout = QtWidgets.QVBoxLayout()
- layout.addWidget(self.label)
+ layout.addLayout(labels_layout)
+ layout.addWidget(self.progress_bar)
self.setLayout(layout)
- def update(self):
- self.label.setText(self.get_finished_label_text(self.started_dt))
- self.status = HistoryItem.STATUS_FINISHED
+ # Is a status code already sent?
+ if 'status_code' in data:
+ self.status_code_label.setText("{}".format(data['status_code']))
+ if data['status_code'] >= 200 and data['status_code'] < 300:
+ self.status_code_label.setStyleSheet(self.common.css['history_individual_file_status_code_label_2xx'])
+ if data['status_code'] >= 400 and data['status_code'] < 500:
+ self.status_code_label.setStyleSheet(self.common.css['history_individual_file_status_code_label_4xx'])
+ self.status = HistoryItem.STATUS_FINISHED
+ self.progress_bar.hide()
+ return
+
+ else:
+ self.total_bytes = data['filesize']
+ self.progress_bar.setMinimum(0)
+ self.progress_bar.setMaximum(data['filesize'])
+ self.progress_bar.total_bytes = data['filesize']
+
+ # Start at 0
+ self.update(0)
+
+ def update(self, downloaded_bytes):
+ self.downloaded_bytes = downloaded_bytes
+
+ self.progress_bar.setValue(downloaded_bytes)
+ if downloaded_bytes == self.progress_bar.total_bytes:
+ self.status_code_label.setText("200")
+ self.status_code_label.setStyleSheet(self.common.css['history_individual_file_status_code_label_2xx'])
+ self.progress_bar.hide()
+ self.status = HistoryItem.STATUS_FINISHED
+
+ else:
+ elapsed = time.time() - self.started
+ if elapsed < 10:
+ # Wait a couple of seconds for the download rate to stabilize.
+ # This prevents a "Windows copy dialog"-esque experience at
+ # the beginning of the download.
+ pb_fmt = strings._('gui_all_modes_progress_starting').format(
+ self.common.human_readable_filesize(downloaded_bytes))
+ else:
+ pb_fmt = strings._('gui_all_modes_progress_eta').format(
+ self.common.human_readable_filesize(downloaded_bytes),
+ self.estimated_time_remaining)
+
+ self.progress_bar.setFormat(pb_fmt)
def cancel(self):
self.progress_bar.setFormat(strings._('gui_canceled'))
self.status = HistoryItem.STATUS_CANCELED
+ @property
+ def estimated_time_remaining(self):
+ return self.common.estimated_time_remaining(self.downloaded_bytes,
+ self.total_bytes,
+ self.started)
+
+
class HistoryItemList(QtWidgets.QScrollArea):
"""
List of items
@@ -452,26 +526,30 @@ class History(QtWidgets.QWidget):
# In progress and completed counters
self.in_progress_count = 0
self.completed_count = 0
+ self.requests_count = 0
- # In progress and completed labels
+ # In progress, completed, and requests 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'])
+ self.requests_label = QtWidgets.QLabel()
+ self.requests_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_all_modes_clear_history'))
- clear_button.setStyleSheet(self.common.css['downloads_uploads_clear'])
- clear_button.setFlat(True)
- clear_button.clicked.connect(self.reset)
+ self.clear_button = QtWidgets.QPushButton(strings._('gui_all_modes_clear_history'))
+ self.clear_button.setStyleSheet(self.common.css['downloads_uploads_clear'])
+ self.clear_button.setFlat(True)
+ self.clear_button.clicked.connect(self.reset)
header_layout = QtWidgets.QHBoxLayout()
header_layout.addWidget(self.header_label)
header_layout.addStretch()
header_layout.addWidget(self.in_progress_label)
header_layout.addWidget(self.completed_label)
- header_layout.addWidget(clear_button)
+ header_layout.addWidget(self.requests_label)
+ header_layout.addWidget(self.clear_button)
# When there are no items
self.empty_image = QtWidgets.QLabel()
@@ -549,14 +627,18 @@ class History(QtWidgets.QWidget):
self.completed_count = 0
self.update_completed()
+ # Reset web requests counter
+ self.requests_count = 0
+ self.update_requests()
+
def update_completed(self):
"""
Update the 'completed' widget.
"""
if self.completed_count == 0:
- image = self.common.get_resource_path('images/share_completed_none.png')
+ image = self.common.get_resource_path('images/history_completed_none.png')
else:
- image = self.common.get_resource_path('images/share_completed.png')
+ image = self.common.get_resource_path('images/history_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))
@@ -564,14 +646,25 @@ class History(QtWidgets.QWidget):
"""
Update the 'in progress' widget.
"""
- if self.mode != 'website':
- 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')
+ if self.in_progress_count == 0:
+ image = self.common.get_resource_path('images/history_in_progress_none.png')
+ else:
+ image = self.common.get_resource_path('images/history_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').format(self.in_progress_count))
+
+ def update_requests(self):
+ """
+ Update the 'web requests' widget.
+ """
+ if self.requests_count == 0:
+ image = self.common.get_resource_path('images/history_requests_none.png')
+ else:
+ image = self.common.get_resource_path('images/history_requests.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').format(self.in_progress_count))
+ self.requests_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.requests_count))
+ self.requests_label.setToolTip(strings._('history_requests_tooltip').format(self.requests_count))
class ToggleHistory(QtWidgets.QPushButton):
@@ -604,7 +697,7 @@ class ToggleHistory(QtWidgets.QPushButton):
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.
+ only increment the counter if History is hidden.
"""
if increment and not self.history_widget.isVisible():
self.indicator_count += 1
diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py
index dbc0bc73..ecbfa54a 100644
--- a/onionshare_gui/mode/receive_mode/__init__.py
+++ b/onionshare_gui/mode/receive_mode/__init__.py
@@ -97,7 +97,7 @@ class ReceiveMode(Mode):
The auto-stop timer expired, should we stop the server? Returns a bool
"""
# If there were no attempts to upload files, or all uploads are done, we can stop
- if self.web.receive_mode.upload_count == 0 or not self.web.receive_mode.uploads_in_progress:
+ if self.web.receive_mode.cur_history_id == 0 or not self.web.receive_mode.uploads_in_progress:
self.server_status.stop_server()
self.server_status_label.setText(strings._('close_on_autostop_timer'))
return True
@@ -112,7 +112,7 @@ class ReceiveMode(Mode):
Starting the server.
"""
# Reset web counters
- self.web.receive_mode.upload_count = 0
+ self.web.receive_mode.cur_history_id = 0
self.web.reset_invalid_passwords()
# Hide and reset the uploads if we have previously shared
@@ -212,6 +212,8 @@ class ReceiveMode(Mode):
Set the info counters back to zero.
"""
self.history.reset()
+ self.toggle_history.indicator_count = 0
+ self.toggle_history.update_indicator()
def update_primary_action(self):
self.common.log('ReceiveMode', 'update_primary_action')
diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py
index 143fd577..28b439af 100644
--- a/onionshare_gui/mode/share_mode/__init__.py
+++ b/onionshare_gui/mode/share_mode/__init__.py
@@ -132,7 +132,7 @@ class ShareMode(Mode):
The auto-stop timer expired, should we stop the server? Returns a bool
"""
# If there were no attempts to download the share, or all downloads are done, we can stop
- if self.web.share_mode.download_count == 0 or self.web.done:
+ if self.web.share_mode.cur_history_id == 0 or self.web.done:
self.server_status.stop_server()
self.server_status_label.setText(strings._('close_on_autostop_timer'))
return True
@@ -146,7 +146,7 @@ class ShareMode(Mode):
Starting the server.
"""
# Reset web counters
- self.web.share_mode.download_count = 0
+ self.web.share_mode.cur_history_id = 0
self.web.reset_invalid_passwords()
# Hide and reset the downloads if we have previously shared
@@ -225,12 +225,6 @@ class ShareMode(Mode):
"""
self.primary_action.hide()
- def handle_request_load(self, event):
- """
- Handle REQUEST_LOAD event.
- """
- self.system_tray.showMessage(strings._('systray_page_loaded_title'), strings._('systray_page_loaded_message'))
-
def handle_request_started(self, event):
"""
Handle REQUEST_STARTED event.
@@ -325,6 +319,8 @@ class ShareMode(Mode):
Set the info counters back to zero.
"""
self.history.reset()
+ self.toggle_history.indicator_count = 0
+ self.toggle_history.update_indicator()
@staticmethod
def _compute_total_size(filenames):
diff --git a/onionshare_gui/mode/share_mode/threads.py b/onionshare_gui/mode/share_mode/threads.py
index 24e2c242..fed362eb 100644
--- a/onionshare_gui/mode/share_mode/threads.py
+++ b/onionshare_gui/mode/share_mode/threads.py
@@ -41,12 +41,8 @@ class CompressThread(QtCore.QThread):
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.web.share_mode.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size)
+ self.success.emit()
self.mode.app.cleanup_filenames += self.mode.web.share_mode.cleanup_filenames
except OSError as e:
self.error.emit(e.strerror)
diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/mode/website_mode/__init__.py
index 50af4725..b277b6c3 100644
--- a/onionshare_gui/mode/website_mode/__init__.py
+++ b/onionshare_gui/mode/website_mode/__init__.py
@@ -30,7 +30,7 @@ from onionshare.web import Web
from ..file_selection import FileSelection
from .. import Mode
-from ..history import History, ToggleHistory, VisitHistoryItem
+from ..history import History, ToggleHistory
from ...widgets import Alert
class WebsiteMode(Mode):
@@ -80,6 +80,8 @@ class WebsiteMode(Mode):
strings._('gui_all_modes_history'),
'website'
)
+ self.history.in_progress_label.hide()
+ self.history.completed_label.hide()
self.history.hide()
# Info label
@@ -165,12 +167,8 @@ class WebsiteMode(Mode):
Step 3 in starting the server. Display large filesize
warning, if applicable.
"""
-
- if self.web.website_mode.set_file_info(self.filenames):
- self.success.emit()
- else:
- # Cancelled
- pass
+ self.web.website_mode.set_file_info(self.filenames)
+ self.success.emit()
def start_server_error_custom(self):
"""
@@ -208,21 +206,6 @@ class WebsiteMode(Mode):
"""
self.system_tray.showMessage(strings._('systray_site_loaded_title'), strings._('systray_site_loaded_message'))
- def handle_request_started(self, event):
- """
- Handle REQUEST_STARTED event.
- """
- if ( (event["path"] == '') or (event["path"].find(".htm") != -1 ) ):
- item = VisitHistoryItem(self.common, event["data"]["id"], 0)
-
- self.history.add(event["data"]["id"], item)
- self.toggle_history.update_indicator(True)
- self.history.completed_count += 1
- self.history.update_completed()
-
- self.system_tray.showMessage(strings._('systray_website_started_title'), strings._('systray_website_started_message'))
-
-
def on_reload_settings(self):
"""
If there were some files listed for sharing, we should be ok to re-enable
@@ -262,6 +245,8 @@ class WebsiteMode(Mode):
Set the info counters back to zero.
"""
self.history.reset()
+ self.toggle_history.indicator_count = 0
+ self.toggle_history.update_indicator()
@staticmethod
def _compute_total_size(filenames):
diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py
index bed86895..20873bc8 100644
--- a/onionshare_gui/onionshare_gui.py
+++ b/onionshare_gui/onionshare_gui.py
@@ -383,7 +383,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.share_mode.server_status.autostart_timer_container.hide()
self.receive_mode.server_status.autostart_timer_container.hide()
self.website_mode.server_status.autostart_timer_container.hide()
-
+
d = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only)
d.settings_saved.connect(reload_settings)
d.exec_()
@@ -470,6 +470,15 @@ class OnionShareGui(QtWidgets.QMainWindow):
elif event["type"] == Web.REQUEST_UPLOAD_CANCELED:
mode.handle_request_upload_canceled(event)
+ elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_STARTED:
+ mode.handle_request_individual_file_started(event)
+
+ elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_PROGRESS:
+ mode.handle_request_individual_file_progress(event)
+
+ elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_CANCELED:
+ mode.handle_request_individual_file_canceled(event)
+
if event["type"] == Web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE:
Alert(self.common, strings._('error_cannot_create_data_dir').format(event["data"]["receive_mode_dir"]))
diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py
index cb732aa2..25165688 100644
--- a/onionshare_gui/settings_dialog.py
+++ b/onionshare_gui/settings_dialog.py
@@ -212,10 +212,12 @@ class SettingsDialog(QtWidgets.QDialog):
self.close_after_first_download_checkbox = QtWidgets.QCheckBox()
self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Checked)
self.close_after_first_download_checkbox.setText(strings._("gui_settings_close_after_first_download_option"))
+ individual_downloads_label = QtWidgets.QLabel(strings._("gui_settings_individual_downloads_label"))
# Sharing options layout
sharing_group_layout = QtWidgets.QVBoxLayout()
sharing_group_layout.addWidget(self.close_after_first_download_checkbox)
+ sharing_group_layout.addWidget(individual_downloads_label)
sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label"))
sharing_group.setLayout(sharing_group_layout)
@@ -638,7 +640,6 @@ class SettingsDialog(QtWidgets.QDialog):
self.connect_to_tor_label.show()
self.onion_settings_widget.hide()
-
def connection_type_bundled_toggled(self, checked):
"""
Connection type bundled was toggled. If checked, hide authentication fields.
diff --git a/share/images/share_completed.png b/share/images/history_completed.png
index e68fe5a2..e68fe5a2 100644
--- a/share/images/share_completed.png
+++ b/share/images/history_completed.png
Binary files differ
diff --git a/share/images/share_completed_none.png b/share/images/history_completed_none.png
index 8dbd6939..8dbd6939 100644
--- a/share/images/share_completed_none.png
+++ b/share/images/history_completed_none.png
Binary files differ
diff --git a/share/images/share_in_progress.png b/share/images/history_in_progress.png
index 19694659..19694659 100644
--- a/share/images/share_in_progress.png
+++ b/share/images/history_in_progress.png
Binary files differ
diff --git a/share/images/share_in_progress_none.png b/share/images/history_in_progress_none.png
index 2d61dba4..2d61dba4 100644
--- a/share/images/share_in_progress_none.png
+++ b/share/images/history_in_progress_none.png
Binary files differ
diff --git a/share/images/history_requests.png b/share/images/history_requests.png
new file mode 100644
index 00000000..4965744d
--- /dev/null
+++ b/share/images/history_requests.png
Binary files differ
diff --git a/share/images/history_requests_none.png b/share/images/history_requests_none.png
new file mode 100644
index 00000000..93a71ef3
--- /dev/null
+++ b/share/images/history_requests_none.png
Binary files differ
diff --git a/share/locale/en.json b/share/locale/en.json
index 2063a415..aab6153d 100644
--- a/share/locale/en.json
+++ b/share/locale/en.json
@@ -52,6 +52,7 @@
"gui_settings_onion_label": "Onion settings",
"gui_settings_sharing_label": "Sharing settings",
"gui_settings_close_after_first_download_option": "Stop sharing after files have been sent",
+ "gui_settings_individual_downloads_label": "Uncheck to allow downloading individual files",
"gui_settings_connection_type_label": "How should OnionShare connect to Tor?",
"gui_settings_connection_type_bundled_option": "Use the Tor version built into OnionShare",
"gui_settings_connection_type_automatic_option": "Attempt auto-configuration with Tor Browser",
@@ -133,6 +134,7 @@
"gui_file_info_single": "{} file, {}",
"history_in_progress_tooltip": "{} in progress",
"history_completed_tooltip": "{} completed",
+ "history_requests_tooltip": "{} web requests",
"error_cannot_create_data_dir": "Could not create OnionShare data folder: {}",
"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>",
"gui_mode_share_button": "Share Files",
@@ -160,6 +162,8 @@
"systray_receive_started_message": "Someone is sending files to you",
"systray_website_started_title": "Starting sharing website",
"systray_website_started_message": "Someone is visiting your website",
+ "systray_individual_file_downloaded_title": "Individual file loaded",
+ "systray_individual_file_downloaded_message": "Individual file {} viewed",
"gui_all_modes_history": "History",
"gui_all_modes_clear_history": "Clear All",
"gui_all_modes_transfer_started": "Started {}",
diff --git a/share/static/css/style.css b/share/static/css/style.css
index f2ded524..bc986e57 100644
--- a/share/static/css/style.css
+++ b/share/static/css/style.css
@@ -56,6 +56,10 @@ header .right ul li {
cursor: pointer;
}
+a.button:visited {
+ color: #ffffff;
+}
+
.close-button {
color: #ffffff;
background-color: #c90c0c;
@@ -222,3 +226,12 @@ li.info {
color: #666666;
margin: 0 0 20px 0;
}
+
+a {
+ text-decoration: none;
+ color: #1c1ca0;
+}
+
+a:visited {
+ color: #601ca0;
+} \ No newline at end of file
diff --git a/share/templates/405.html b/share/templates/405.html
new file mode 100644
index 00000000..55493ae7
--- /dev/null
+++ b/share/templates/405.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>OnionShare: 405 Method Not Allowed</title>
+ <link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon">
+ <link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all">
+</head>
+
+<body>
+ <div class="info-wrapper">
+ <div class="info">
+ <p><img class="logo" src="{{ static_url_path }}/img/logo_large.png" title="OnionShare"></p>
+ <p class="info-header">405 Method Not Allowed</p>
+ </div>
+ </div>
+</body>
+
+</html>
diff --git a/share/templates/send.html b/share/templates/send.html
index e0076c0f..916b3bfe 100644
--- a/share/templates/send.html
+++ b/share/templates/send.html
@@ -28,24 +28,31 @@
<th id="size-header">Size</th>
<th></th>
</tr>
- {% for info in file_info.dirs %}
+ {% for info in dirs %}
<tr>
<td>
<img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_folder.png" />
- {{ info.basename }}
+ <a href="{{ info.basename }}">
+ {{ info.basename }}
+ </a>
</td>
- <td>{{ info.size_human }}</td>
- <td></td>
+ <td>&mdash;</td>
</tr>
{% endfor %}
- {% for info in file_info.files %}
+
+ {% for info in files %}
<tr>
<td>
<img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_file.png" />
+ {% if download_individual_files %}
+ <a href="{{ info.basename }}">
+ {{ info.basename }}
+ </a>
+ {% else %}
{{ info.basename }}
+ {% endif %}
</td>
<td>{{ info.size_human }}</td>
- <td></td>
</tr>
{% endfor %}
</table>
diff --git a/tests/GuiBaseTest.py b/tests/GuiBaseTest.py
index 2f340396..3e82769a 100644
--- a/tests/GuiBaseTest.py
+++ b/tests/GuiBaseTest.py
@@ -14,6 +14,7 @@ from onionshare.web import Web
from onionshare_gui import Application, OnionShare, OnionShareGui
from onionshare_gui.mode.share_mode import ShareMode
from onionshare_gui.mode.receive_mode import ReceiveMode
+from onionshare_gui.mode.website_mode import WebsiteMode
class GuiBaseTest(object):
@@ -103,6 +104,9 @@ class GuiBaseTest(object):
if type(mode) == ShareMode:
QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton)
self.assertTrue(self.gui.mode, self.gui.MODE_SHARE)
+ if type(mode) == WebsiteMode:
+ QtTest.QTest.mouseClick(self.gui.website_mode_button, QtCore.Qt.LeftButton)
+ self.assertTrue(self.gui.mode, self.gui.MODE_WEBSITE)
def click_toggle_history(self, mode):
@@ -112,7 +116,7 @@ class GuiBaseTest(object):
self.assertEqual(mode.history.isVisible(), not currently_visible)
- def history_indicator(self, mode, public_mode):
+ def history_indicator(self, mode, public_mode, indicator_count="1"):
'''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():
@@ -143,7 +147,7 @@ class GuiBaseTest(object):
# 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")
+ self.assertEqual(mode.toggle_history.indicator_label.text(), indicator_count)
# Toggle history back on, indicator should be hidden again
QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton)
@@ -166,6 +170,9 @@ class GuiBaseTest(object):
QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton)
self.assertEqual(mode.server_status.status, 1)
+ def toggle_indicator_is_reset(self, mode):
+ self.assertEqual(mode.toggle_history.indicator_count, 0)
+ self.assertFalse(mode.toggle_history.indicator_label.isVisible())
def server_status_indicator_says_starting(self, mode):
'''Test that the Server Status indicator shows we are Starting'''
@@ -198,6 +205,9 @@ class GuiBaseTest(object):
else:
self.assertIsNone(mode.server_status.web.password, r'(\w+)-(\w+)')
+ def add_button_visible(self, mode):
+ '''Test that the add button should be visible'''
+ self.assertTrue(mode.server_status.file_selection.add_button.isVisible())
def url_description_shown(self, mode):
'''Test that the URL label is showing'''
@@ -249,7 +259,7 @@ class GuiBaseTest(object):
def 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):
+ if type(mode) == ReceiveMode or (type(mode) == ShareMode and stay_open) or (type(mode) == WebsiteMode):
QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton)
self.assertEqual(mode.server_status.status, 0)
@@ -275,6 +285,10 @@ class GuiBaseTest(object):
else:
self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically'))
+ def clear_all_history_items(self, mode, count):
+ if count == 0:
+ QtTest.QTest.mouseClick(mode.history.clear_button, QtCore.Qt.LeftButton)
+ self.assertEquals(len(mode.history.item_list.items.keys()), count)
# Auto-stop timer tests
def set_timeout(self, mode, timeout):
diff --git a/tests/GuiReceiveTest.py b/tests/GuiReceiveTest.py
index c4bfa884..80e05250 100644
--- a/tests/GuiReceiveTest.py
+++ b/tests/GuiReceiveTest.py
@@ -66,31 +66,6 @@ class GuiReceiveTest(GuiBaseTest):
r = requests.get('http://127.0.0.1:{}/close'.format(self.gui.app.port))
self.assertEqual(r.status_code, 401)
- def uploading_zero_files_shouldnt_change_ui(self, mode, public_mode):
- '''If you submit the receive mode form without selecting any files, the UI shouldn't get updated'''
- url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port)
-
- # What were the counts before submitting the form?
- before_in_progress_count = mode.history.in_progress_count
- before_completed_count = mode.history.completed_count
- before_number_of_history_items = len(mode.history.item_list.items)
-
- # Click submit without including any files a few times
- if public_mode:
- r = requests.post(url, files={})
- r = requests.post(url, files={})
- r = requests.post(url, files={})
- else:
- auth = requests.auth.HTTPBasicAuth('onionshare', mode.web.password)
- r = requests.post(url, files={}, auth=auth)
- r = requests.post(url, files={}, auth=auth)
- r = requests.post(url, files={}, auth=auth)
-
- # The counts shouldn't change
- self.assertEqual(mode.history.in_progress_count, before_in_progress_count)
- self.assertEqual(mode.history.completed_count, before_completed_count)
- self.assertEqual(len(mode.history.item_list.items), before_number_of_history_items)
-
# 'Grouped' tests follow from here
def run_all_receive_mode_setup_tests(self, public_mode):
@@ -127,14 +102,13 @@ class GuiReceiveTest(GuiBaseTest):
# Test uploading the same file twice at the same time, and make sure no collisions
self.upload_file(public_mode, '/tmp/test.txt', 'test.txt', True)
self.counter_incremented(self.gui.receive_mode, 6)
- self.uploading_zero_files_shouldnt_change_ui(self.gui.receive_mode, public_mode)
- self.history_indicator(self.gui.receive_mode, public_mode)
+ self.history_indicator(self.gui.receive_mode, public_mode, "2")
self.server_is_stopped(self.gui.receive_mode, False)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.receive_mode, False)
self.server_working_on_start_button_pressed(self.gui.receive_mode)
self.server_is_started(self.gui.receive_mode)
- self.history_indicator(self.gui.receive_mode, public_mode)
+ self.history_indicator(self.gui.receive_mode, public_mode, "2")
def run_all_receive_mode_unwritable_dir_tests(self, public_mode):
'''Attempt to upload (unwritable) files in receive mode and stop the share'''
@@ -153,3 +127,12 @@ class GuiReceiveTest(GuiBaseTest):
self.autostop_timer_widget_hidden(self.gui.receive_mode)
self.server_timed_out(self.gui.receive_mode, 15000)
self.web_server_is_stopped()
+
+ def run_all_clear_all_button_tests(self, public_mode):
+ """Test the Clear All history button"""
+ self.run_all_receive_mode_setup_tests(public_mode)
+ self.upload_file(public_mode, '/tmp/test.txt', 'test.txt')
+ self.history_widgets_present(self.gui.receive_mode)
+ self.clear_all_history_items(self.gui.receive_mode, 0)
+ self.upload_file(public_mode, '/tmp/test.txt', 'test.txt')
+ self.clear_all_history_items(self.gui.receive_mode, 2)
diff --git a/tests/GuiShareTest.py b/tests/GuiShareTest.py
index 64e57b9f..6925defa 100644
--- a/tests/GuiShareTest.py
+++ b/tests/GuiShareTest.py
@@ -44,7 +44,7 @@ class GuiShareTest(GuiBaseTest):
self.file_selection_widget_has_files(0)
- def file_selection_widget_readd_files(self):
+ def file_selection_widget_read_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')
@@ -81,6 +81,40 @@ class GuiShareTest(GuiBaseTest):
QtTest.QTest.qWait(2000)
self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8'))
+ def individual_file_is_viewable_or_not(self, public_mode, stay_open):
+ '''Test whether an individual file is viewable (when in stay_open mode) and that it isn't (when not in stay_open mode)'''
+ url = "http://127.0.0.1:{}".format(self.gui.app.port)
+ download_file_url = "http://127.0.0.1:{}/test.txt".format(self.gui.app.port)
+ if public_mode:
+ r = requests.get(url)
+ else:
+ r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password))
+
+ if stay_open:
+ self.assertTrue('a href="test.txt"' in r.text)
+
+ if public_mode:
+ r = requests.get(download_file_url)
+ else:
+ r = requests.get(download_file_url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password))
+
+ tmp_file = tempfile.NamedTemporaryFile()
+ with open(tmp_file.name, 'wb') as f:
+ f.write(r.content)
+
+ with open(tmp_file.name, 'r') as f:
+ self.assertEqual('onionshare', f.read())
+ else:
+ self.assertFalse('a href="/test.txt"' in r.text)
+ if public_mode:
+ r = requests.get(download_file_url)
+ else:
+ r = requests.get(download_file_url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password))
+ self.assertEqual(r.status_code, 404)
+ self.download_share(public_mode)
+
+ QtTest.QTest.qWait(2000)
+
def hit_401(self, public_mode):
'''Test that the server stops after too many 401s, or doesn't when in public_mode'''
url = "http://127.0.0.1:{}/".format(self.gui.app.port)
@@ -101,11 +135,6 @@ class GuiShareTest(GuiBaseTest):
self.web_server_is_stopped()
- def 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())
-
-
# 'Grouped' tests follow from here
def run_all_share_mode_setup_tests(self):
@@ -117,7 +146,7 @@ class GuiShareTest(GuiBaseTest):
self.history_is_visible(self.gui.share_mode)
self.deleting_all_files_hides_delete_button()
self.add_a_file_and_delete_using_its_delete_widget()
- self.file_selection_widget_readd_files()
+ self.file_selection_widget_read_files()
def run_all_share_mode_started_tests(self, public_mode, startup_time=2000):
@@ -142,11 +171,24 @@ class GuiShareTest(GuiBaseTest):
self.server_is_stopped(self.gui.share_mode, stay_open)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
- self.add_button_visible()
+ self.add_button_visible(self.gui.share_mode)
self.server_working_on_start_button_pressed(self.gui.share_mode)
+ self.toggle_indicator_is_reset(self.gui.share_mode)
self.server_is_started(self.gui.share_mode)
self.history_indicator(self.gui.share_mode, public_mode)
+ def run_all_share_mode_individual_file_download_tests(self, public_mode, stay_open):
+ """Tests in share mode after downloading a share"""
+ self.web_page(self.gui.share_mode, 'Total size', public_mode)
+ self.individual_file_is_viewable_or_not(public_mode, stay_open)
+ self.history_widgets_present(self.gui.share_mode)
+ self.server_is_stopped(self.gui.share_mode, stay_open)
+ self.web_server_is_stopped()
+ self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
+ self.add_button_visible(self.gui.share_mode)
+ self.server_working_on_start_button_pressed(self.gui.share_mode)
+ self.server_is_started(self.gui.share_mode)
+ self.history_indicator(self.gui.share_mode, public_mode)
def run_all_share_mode_tests(self, public_mode, stay_open):
"""End-to-end share tests"""
@@ -154,6 +196,21 @@ class GuiShareTest(GuiBaseTest):
self.run_all_share_mode_started_tests(public_mode)
self.run_all_share_mode_download_tests(public_mode, stay_open)
+ def run_all_clear_all_button_tests(self, public_mode, stay_open):
+ """Test the Clear All history button"""
+ self.run_all_share_mode_setup_tests()
+ self.run_all_share_mode_started_tests(public_mode)
+ self.individual_file_is_viewable_or_not(public_mode, stay_open)
+ self.history_widgets_present(self.gui.share_mode)
+ self.clear_all_history_items(self.gui.share_mode, 0)
+ self.individual_file_is_viewable_or_not(public_mode, stay_open)
+ self.clear_all_history_items(self.gui.share_mode, 2)
+
+ def run_all_share_mode_individual_file_tests(self, public_mode, stay_open):
+ """Tests in share mode when viewing an individual file"""
+ self.run_all_share_mode_setup_tests()
+ self.run_all_share_mode_started_tests(public_mode)
+ self.run_all_share_mode_individual_file_download_tests(public_mode, stay_open)
def run_all_large_file_tests(self, public_mode, stay_open):
"""Same as above but with a larger file"""
diff --git a/tests/GuiWebsiteTest.py b/tests/GuiWebsiteTest.py
new file mode 100644
index 00000000..7b88bfdf
--- /dev/null
+++ b/tests/GuiWebsiteTest.py
@@ -0,0 +1,100 @@
+import json
+import os
+import requests
+import socks
+import zipfile
+import tempfile
+from PyQt5 import QtCore, QtTest
+from onionshare import strings
+from onionshare.common import Common
+from onionshare.settings import Settings
+from onionshare.onion import Onion
+from onionshare.web import Web
+from onionshare_gui import Application, OnionShare, OnionShareGui
+from .GuiShareTest import GuiShareTest
+
+class GuiWebsiteTest(GuiShareTest):
+ @staticmethod
+ def set_up(test_settings):
+ '''Create GUI with given settings'''
+ # Create our test file
+ testfile = open('/tmp/index.html', 'w')
+ testfile.write('<html><body><p>This is a test website hosted by OnionShare</p></body></html>')
+ testfile.close()
+
+ common = Common()
+ common.settings = Settings(common)
+ common.define_css()
+ strings.load_strings(common)
+
+ # Get all of the settings in test_settings
+ test_settings['data_dir'] = '/tmp/OnionShare'
+ for key, val in common.settings.default_settings.items():
+ if key not in test_settings:
+ test_settings[key] = val
+
+ # Start the Onion
+ testonion = Onion(common)
+ global qtapp
+ qtapp = Application(common)
+ app = OnionShare(common, testonion, True, 0)
+
+ web = Web(common, False, True)
+ open('/tmp/settings.json', 'w').write(json.dumps(test_settings))
+
+ gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/index.html'], '/tmp/settings.json', True)
+ return gui
+
+ @staticmethod
+ def tear_down():
+ '''Clean up after tests'''
+ try:
+ os.remove('/tmp/index.html')
+ os.remove('/tmp/settings.json')
+ except:
+ pass
+
+ def view_website(self, public_mode):
+ '''Test that we can download the share'''
+ url = "http://127.0.0.1:{}/".format(self.gui.app.port)
+ if public_mode:
+ r = requests.get(url)
+ else:
+ r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.website_mode.server_status.web.password))
+
+ QtTest.QTest.qWait(2000)
+ self.assertTrue('This is a test website hosted by OnionShare' in r.text)
+
+ def run_all_website_mode_setup_tests(self):
+ """Tests in website mode prior to starting a share"""
+ self.click_mode(self.gui.website_mode)
+ self.file_selection_widget_has_files(1)
+ self.history_is_not_visible(self.gui.website_mode)
+ self.click_toggle_history(self.gui.website_mode)
+ self.history_is_visible(self.gui.website_mode)
+
+ def run_all_website_mode_started_tests(self, public_mode, startup_time=2000):
+ """Tests in website mode after starting a share"""
+ self.server_working_on_start_button_pressed(self.gui.website_mode)
+ self.server_status_indicator_says_starting(self.gui.website_mode)
+ self.add_delete_buttons_hidden()
+ self.settings_button_is_hidden()
+ self.server_is_started(self.gui.website_mode, startup_time)
+ self.web_server_is_running()
+ self.have_a_password(self.gui.website_mode, public_mode)
+ self.url_description_shown(self.gui.website_mode)
+ self.have_copy_url_button(self.gui.website_mode, public_mode)
+ self.server_status_indicator_says_started(self.gui.website_mode)
+
+
+ def run_all_website_mode_download_tests(self, public_mode):
+ """Tests in website mode after viewing the site"""
+ self.run_all_website_mode_setup_tests()
+ self.run_all_website_mode_started_tests(public_mode, startup_time=2000)
+ self.view_website(public_mode)
+ self.history_widgets_present(self.gui.website_mode)
+ self.server_is_stopped(self.gui.website_mode, False)
+ self.web_server_is_stopped()
+ self.server_status_indicator_says_closed(self.gui.website_mode, False)
+ self.add_button_visible(self.gui.website_mode)
+
diff --git a/tests/TorGuiShareTest.py b/tests/TorGuiShareTest.py
index 352707eb..cfce9d4e 100644
--- a/tests/TorGuiShareTest.py
+++ b/tests/TorGuiShareTest.py
@@ -67,7 +67,7 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest):
self.server_is_stopped(self.gui.share_mode, stay_open)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
- self.add_button_visible()
+ self.add_button_visible(self.gui.share_mode)
self.server_working_on_start_button_pressed(self.gui.share_mode)
self.server_is_started(self.gui.share_mode, startup_time=45000)
self.history_indicator(self.gui.share_mode, public_mode)
diff --git a/tests/local_onionshare_receive_mode_clear_all_button_test.py b/tests/local_onionshare_receive_mode_clear_all_button_test.py
new file mode 100644
index 00000000..f93d4fe1
--- /dev/null
+++ b/tests/local_onionshare_receive_mode_clear_all_button_test.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+import pytest
+import unittest
+
+from .GuiReceiveTest import GuiReceiveTest
+
+class LocalReceiveModeClearAllButtonTest(unittest.TestCase, GuiReceiveTest):
+ @classmethod
+ def setUpClass(cls):
+ test_settings = {
+ }
+ cls.gui = GuiReceiveTest.set_up(test_settings)
+
+ @classmethod
+ def tearDownClass(cls):
+ GuiReceiveTest.tear_down()
+
+ @pytest.mark.gui
+ @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
+ def test_gui(self):
+ self.run_all_common_setup_tests()
+ self.run_all_clear_all_button_tests(False)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/local_onionshare_share_mode_clear_all_button_test.py b/tests/local_onionshare_share_mode_clear_all_button_test.py
new file mode 100644
index 00000000..caed342d
--- /dev/null
+++ b/tests/local_onionshare_share_mode_clear_all_button_test.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+import pytest
+import unittest
+
+from .GuiShareTest import GuiShareTest
+
+class LocalShareModeClearAllButtonTest(unittest.TestCase, GuiShareTest):
+ @classmethod
+ def setUpClass(cls):
+ test_settings = {
+ "close_after_first_download": False,
+ }
+ cls.gui = GuiShareTest.set_up(test_settings)
+
+ @classmethod
+ def tearDownClass(cls):
+ GuiShareTest.tear_down()
+
+ @pytest.mark.gui
+ @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
+ def test_gui(self):
+ self.run_all_common_setup_tests()
+ self.run_all_clear_all_button_tests(False, True)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/local_onionshare_share_mode_individual_file_view_stay_open_test.py b/tests/local_onionshare_share_mode_individual_file_view_stay_open_test.py
new file mode 100644
index 00000000..4e026e16
--- /dev/null
+++ b/tests/local_onionshare_share_mode_individual_file_view_stay_open_test.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+import pytest
+import unittest
+
+from .GuiShareTest import GuiShareTest
+
+class LocalShareModeIndividualFileViewStayOpenTest(unittest.TestCase, GuiShareTest):
+ @classmethod
+ def setUpClass(cls):
+ test_settings = {
+ "close_after_first_download": False,
+ }
+ cls.gui = GuiShareTest.set_up(test_settings)
+
+ @classmethod
+ def tearDownClass(cls):
+ GuiShareTest.tear_down()
+
+ @pytest.mark.gui
+ @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
+ def test_gui(self):
+ self.run_all_common_setup_tests()
+ self.run_all_share_mode_individual_file_tests(False, True)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/local_onionshare_share_mode_individual_file_view_test.py b/tests/local_onionshare_share_mode_individual_file_view_test.py
new file mode 100644
index 00000000..2bdccaec
--- /dev/null
+++ b/tests/local_onionshare_share_mode_individual_file_view_test.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+import pytest
+import unittest
+
+from .GuiShareTest import GuiShareTest
+
+class LocalShareModeIndividualFileViewTest(unittest.TestCase, GuiShareTest):
+ @classmethod
+ def setUpClass(cls):
+ test_settings = {
+ "close_after_first_download": True,
+ }
+ cls.gui = GuiShareTest.set_up(test_settings)
+
+ @classmethod
+ def tearDownClass(cls):
+ GuiShareTest.tear_down()
+
+ @pytest.mark.gui
+ @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
+ def test_gui(self):
+ self.run_all_common_setup_tests()
+ self.run_all_share_mode_individual_file_tests(False, False)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/local_onionshare_website_mode_test.py b/tests/local_onionshare_website_mode_test.py
new file mode 100644
index 00000000..051adb3c
--- /dev/null
+++ b/tests/local_onionshare_website_mode_test.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+import pytest
+import unittest
+
+from .GuiWebsiteTest import GuiWebsiteTest
+
+class LocalWebsiteModeTest(unittest.TestCase, GuiWebsiteTest):
+ @classmethod
+ def setUpClass(cls):
+ test_settings = {
+ }
+ cls.gui = GuiWebsiteTest.set_up(test_settings)
+
+ @classmethod
+ def tearDownClass(cls):
+ GuiWebsiteTest.tear_down()
+
+ @pytest.mark.gui
+ @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
+ def test_gui(self):
+ #self.run_all_common_setup_tests()
+ self.run_all_website_mode_download_tests(False)
+
+if __name__ == "__main__":
+ unittest.main()