Commit a290db02 authored by Philipp Hörist's avatar Philipp Hörist

HTTPUpload: Separate transfer from sending message

parent d54f28f9
......@@ -814,14 +814,8 @@ def _start_filetransfer(self, path):
if method is None:
return
con = app.connections[self.account]
if method == 'httpupload':
con.get_module('HTTPUpload').check_file_before_transfer(
path,
self.encryption,
self.contact,
groupchat=self._type.is_groupchat)
app.interface.send_httpupload(self, path)
else:
ft = app.interface.instances['file_transfers']
......
......@@ -908,6 +908,7 @@ class FTState(Enum):
IN_PROGRESS = 'progress'
FINISHED = 'finished'
ERROR = 'error'
CANCELLED = 'cancelled'
@property
def is_preparing(self):
......@@ -937,6 +938,16 @@ def is_finished(self):
def is_error(self):
return self == FTState.ERROR
@property
def is_cancelled(self):
return self == FTState.CANCELLED
@property
def is_active(self):
return not (self.is_error or
self.is_cancelled or
self.is_finished)
SASL_ERRORS = {
'aborted': _('Authentication aborted'),
......
......@@ -151,3 +151,7 @@ def __str__(self):
class SendMessageError(Exception):
pass
class FileError(Exception):
pass
......@@ -21,22 +21,26 @@ class FileTransfer(Observable):
_state_descriptions = {} # type: Dict[FTState, str]
def __init__(self, account, cancel_func=None):
def __init__(self, account):
Observable.__init__(self)
self._account = account
self._cancel_func = cancel_func
self._seen = 0
self.size = 0
self._state = None
self._error_text = ''
self._error_domain = None
@property
def account(self):
return self._account
@property
def state(self):
return self._state
@property
def seen(self):
return self._seen
......@@ -51,9 +55,13 @@ def is_complete(self):
def filename(self):
raise NotImplementedError
def cancel(self):
if self._cancel_func is not None:
self._cancel_func(self)
@property
def error_text(self):
return self._error_text
@property
def error_domain(self):
return self._error_domain
def get_state_description(self):
return self._state_descriptions.get(self._state, '')
......@@ -74,12 +82,18 @@ def set_started(self):
self._state = FTState.STARTED
self.notify('state-changed', FTState.STARTED)
def set_error(self, text=''):
def set_error(self, domain, text=''):
self._error_text = text
self._error_domain = domain
self._state = FTState.ERROR
self.notify('state-changed', FTState.ERROR)
self.disconnect_signals()
def set_cancelled(self):
self._state = FTState.CANCELLED
self.notify('state-changed', FTState.CANCELLED)
self.disconnect_signals()
def set_in_progress(self):
self._state = FTState.IN_PROGRESS
self.notify('state-changed', FTState.IN_PROGRESS)
......
......@@ -45,6 +45,7 @@
from collections import defaultdict
import random
import weakref
import inspect
import string
from string import Template
import urllib
......@@ -1256,7 +1257,11 @@ def disconnect(self, object_):
self._callbacks[signal_name].remove(handler)
def connect(self, signal_name, func):
weak_func = weakref.WeakMethod(func)
if inspect.ismethod(func):
weak_func = weakref.WeakMethod(func)
elif inspect.isfunction(func):
weak_func = weakref.ref(func)
self._callbacks[signal_name].append(weak_func)
def notify(self, signal_name, *args, **kwargs):
......
......@@ -35,8 +35,7 @@
from gajim.common.const import FTState
from gajim.common.filetransfer import FileTransfer
from gajim.common.modules.base import BaseModule
from gajim.common.structs import OutgoingMessage
from gajim.common.connection_handlers_events import InformationEvent
from gajim.common.exceptions import FileError
class HTTPUpload(BaseModule):
......@@ -87,10 +86,9 @@ def pass_disco(self, info):
for ctrl in app.interface.msg_win_mgr.get_controls(acct=self._account):
ctrl.update_actions()
def check_file_before_transfer(self, path, encryption, contact,
groupchat=False):
def make_transfer(self, path, encryption, contact, groupchat=False):
if not path or not os.path.exists(path):
return
return None
invalid_file = False
stat = os.stat(path)
......@@ -112,47 +110,41 @@ def check_file_before_transfer(self, path, encryption, contact,
'maximum allowed file size is: %s') % size
if invalid_file:
self._raise_information_event('open-file-error2', msg)
return
raise FileError('file-error', msg)
mime = mimetypes.MimeTypes().guess_type(path)[0]
if not mime:
mime = 'application/octet-stream' # fallback mime type
self._log.info("Detected MIME type of file: %s", mime)
try:
transfer = HTTPFileTransfer(self._account,
self._cancel_upload,
path,
contact,
mime,
encryption,
groupchat)
app.interface.show_httpupload_progress(transfer)
except Exception as error:
self._log.exception('Error while loading file')
self._raise_information_event('open-file-error2', str(error))
return
if encryption is not None:
app.interface.encrypt_file(transfer,
self._account,
self._request_slot)
else:
self._request_slot(transfer)
return HTTPFileTransfer(self._account,
path,
contact,
mime,
encryption,
groupchat)
def _cancel_upload(self, transfer):
def cancel_transfer(self, transfer):
transfer.set_cancelled()
message = self._queued_messages.get(id(transfer))
if message is None:
return
self._session.cancel_message(message, Soup.Status.CANCELLED)
@staticmethod
def _raise_information_event(dialog_name, args=None):
app.nec.push_incoming_event(InformationEvent(
None, dialog_name=dialog_name, args=args))
def start_transfer(self, transfer):
if transfer.encryption is not None and not transfer.is_encrypted:
transfer.set_encrypting()
plugin = app.plugin_manager.encryption_plugins[transfer.encryption]
if hasattr(plugin, 'encrypt_file'):
plugin.encrypt_file(transfer,
self._account,
self.start_transfer)
else:
transfer.set_error('encryption-not-available')
return
def _request_slot(self, transfer):
transfer.set_preparing()
self._log.info('Sending request for slot')
self._nbxmpp('HTTPUpload').request_slot(
......@@ -172,8 +164,6 @@ def _received_slot(self, task):
HTTPUploadStanzaError,
MalformedStanzaError) as error:
transfer.set_error()
if error.app_condition == 'file-too-large':
size_text = GLib.format_size_full(
error.get_max_file_size(),
......@@ -181,20 +171,18 @@ def _received_slot(self, task):
error_text = _('File is too large, '
'maximum allowed file size is: %s' % size_text)
transfer.set_error('file-too-large', error_text)
else:
error_text = str(error)
self._log.warning(error)
transfer.set_error('misc', str(error))
self._raise_information_event('request-upload-slot-error',
error_text)
return
transfer.process_result(result)
if (urlparse(transfer.put_uri).scheme != 'https' or
urlparse(transfer.get_uri).scheme != 'https'):
transfer.set_error()
self._raise_information_event('unsecure-error')
transfer.set_error('unsecure')
return
self._log.info('Uploading file to %s', transfer.put_uri)
......@@ -206,7 +194,7 @@ def _upload_file(self, transfer):
transfer.set_started()
message = Soup.Message.new('PUT', transfer.put_uri)
message.connect('starting', self._check_certificate)
message.connect('starting', self._check_certificate, transfer)
# Set CAN_REBUILD so chunks get discarded after they have been
# written to the network
......@@ -227,10 +215,11 @@ def _upload_file(self, transfer):
self._set_proxy_if_available()
self._session.queue_message(message, self._on_finish, transfer)
def _check_certificate(self, message):
def _check_certificate(self, message, transfer):
https_used, tls_certificate, tls_errors = message.get_https_status()
if not https_used:
self._log.warning('HTTPS was not used for upload')
transfer.set_error('unsecure')
self._session.cancel_message(message, Soup.Status.CANCELLED)
return
......@@ -241,12 +230,12 @@ def _check_certificate(self, message):
for error in tls_errors:
phrase = get_tls_error_phrase(error)
self._log.warning('TLS verification failed: %s', phrase)
transfer.set_error('tls-verification-failed', phrase)
self._session.cancel_message(message, Soup.Status.CANCELLED)
self._raise_information_event('httpupload-error', phrase)
def _on_finish(self, _session, message, transfer):
self._queued_messages.pop(id(transfer), None)
transfer.set_finished()
if message.props.status_code == Soup.Status.CANCELLED:
self._log.info('Upload cancelled')
......@@ -254,25 +243,14 @@ def _on_finish(self, _session, message, transfer):
if message.props.status_code in (Soup.Status.OK, Soup.Status.CREATED):
self._log.info('Upload completed successfully')
uri = transfer.get_transformed_uri()
transfer.set_finished()
type_ = 'chat'
if transfer.is_groupchat:
type_ = 'groupchat'
message = OutgoingMessage(account=self._account,
contact=transfer.contact,
message=uri,
type_=type_,
oob_url=uri)
self._con.send_message(message)
else:
phrase = Soup.Status.get_phrase(message.props.status_code)
self._log.error('Got unexpected http upload response code: %s',
phrase)
self._raise_information_event('httpupload-response-error', phrase)
transfer.set_error('http-response', phrase)
def _on_wrote_chunk(self, message, transfer):
transfer.update_progress()
......@@ -303,15 +281,21 @@ class HTTPFileTransfer(FileTransfer):
FTState.STARTED: _('Uploading via HTTP File Upload…'),
}
_errors = {
'unsecure': _('The server returned an insecure transport (HTTP).'),
'encryption-not-available': _('There is no encryption method available '
'for the chosen encryption.')
}
def __init__(self,
account,
cancel_func,
path,
contact,
mime,
encryption,
groupchat):
FileTransfer.__init__(self, account, cancel_func=cancel_func)
FileTransfer.__init__(self, account)
self._path = path
self._encryption = encryption
......@@ -328,6 +312,8 @@ def __init__(self,
self._data = None
self._headers = {}
self._is_encrypted = False
@property
def mime(self):
return self._mime
......@@ -352,6 +338,10 @@ def headers(self):
def path(self):
return self._path
@property
def is_encrypted(self):
return self._is_encrypted
def get_transformed_uri(self):
if self._uri_transform_func is not None:
return self._uri_transform_func(self.get_uri)
......@@ -364,9 +354,12 @@ def set_uri_transform_func(self, func):
def filename(self):
return os.path.basename(self._path)
def set_error(self, text=''):
def set_error(self, domain, text=''):
if not text:
text = self._errors[domain]
self._close()
super().set_error(text)
super().set_error(domain, text)
def set_finished(self):
self._close()
......@@ -374,6 +367,7 @@ def set_finished(self):
def set_encrypted_data(self, data):
self._data = data
self._is_encrypted = True
def _close(self):
if self._stream is not None:
......
......@@ -97,42 +97,11 @@
_('%s\nLink-local messaging might not work properly.'),
ErrorDialog),
'request-upload-slot-error': Message(
_('Could not request upload slot for HTTP File Upload'),
'%s',
ErrorDialog),
'open-file-error': Message(
_('Could not Open File'),
_('Exception raised while trying to open file (see log).'),
ErrorDialog),
'open-file-error2': Message(
_('Could not Open File'),
'%s',
ErrorDialog),
'unsecure-error': Message(
_('Not Secure'),
_('The server returned an insecure transport (HTTP).'),
ErrorDialog),
'httpupload-response-error': Message(
_('Could not Upload File'),
_('HTTP response code from server: %s'),
ErrorDialog),
'httpupload-error': Message(
_('Upload Error'),
'%s',
ErrorDialog),
'httpupload-encryption-not-available': Message(
_('Encryption Error'),
_('There is no encryption method available '
'for the chosen encryption.'),
ErrorDialog),
}
......
......@@ -22,6 +22,7 @@
from .util import get_builder
from .util import EventHelper
from .dialogs import ErrorDialog
class FileTransferProgress(Gtk.ApplicationWindow, EventHelper):
......@@ -57,7 +58,11 @@ def __init__(self, transfer):
self._ui.connect_signals(self)
def _on_transfer_state_change(self, transfer, _signal_name, state):
if state.is_finished or state.is_error:
if state.is_error:
ErrorDialog(_('Upload Failed'), transfer.error_text)
self.destroy()
if state.is_finished or state.is_cancelled:
self.destroy()
return
......@@ -73,8 +78,10 @@ def _on_cancel_button_clicked(self, _widget):
self.destroy()
def _on_destroy(self, *args):
self._transfer.cancel()
self._transfer.disconnect(self)
if self._transfer.state.is_active:
client = app.get_client(self._transfer.account)
client.get_module('HTTPUpload').cancel_transfer(self._transfer)
self._transfer = None
self._destroyed = True
if self._pulse is not None:
......
......@@ -77,6 +77,7 @@
from gajim.common.helpers import ask_for_status_message
from gajim.common.helpers import get_group_chat_nick
from gajim.common.structs import MUCData
from gajim.common.structs import OutgoingMessage
from gajim.common.nec import NetworkEvent
from gajim.common.i18n import _
from gajim.common.client import Client
......@@ -84,9 +85,11 @@
from gajim.common.const import JingleState
from gajim.common.file_props import FilesProp
from gajim.common.connection_handlers_events import InformationEvent
from gajim import roster_window
from gajim.common import ged
from gajim.common.exceptions import FileError
from gajim.gui.avatar import AvatarStorage
from gajim.gui.notification import Notification
......@@ -849,34 +852,55 @@ def handle_event_signed_in(self, obj):
if ask_for_status_message(obj.conn.status, signin=True):
open_window('StatusChange', status=obj.conn.status)
@staticmethod
def show_httpupload_progress(transfer):
FileTransferProgress(transfer)
def send_httpupload(self, chat_control, path=None):
if path is not None:
self._send_httpupload(chat_control, path)
return
def send_httpupload(self, chat_control):
accept_cb = partial(self.on_file_dialog_ok, chat_control)
FileChooserDialog(accept_cb,
select_multiple=True,
transient_for=chat_control.parent_win.window)
@staticmethod
def on_file_dialog_ok(chat_control, paths):
con = app.connections[chat_control.account]
def on_file_dialog_ok(self, chat_control, paths):
for path in paths:
con.get_module('HTTPUpload').check_file_before_transfer(
self._send_httpupload(chat_control, path)
def _send_httpupload(self, chat_control, path):
con = app.connections[chat_control.account]
try:
transfer = con.get_module('HTTPUpload').make_transfer(
path,
chat_control.encryption,
chat_control.contact,
chat_control.is_groupchat)
except FileError as error:
app.nec.push_incoming_event(InformationEvent(
None, dialog_name='open-file-error2', args=error))
return
def encrypt_file(self, transfer, account, callback):
transfer.set_encrypting()
plugin = app.plugin_manager.encryption_plugins[transfer.encryption]
if hasattr(plugin, 'encrypt_file'):
plugin.encrypt_file(transfer, account, callback)
else:
transfer.set_error()
self.raise_dialog('httpupload-encryption-not-available')
transfer.connect('state-changed',
self._on_http_upload_state_changed)
FileTransferProgress(transfer)
con.get_module('HTTPUpload').start_transfer(transfer)
@staticmethod
def _on_http_upload_state_changed(transfer, _signal_name, state):
if state.is_finished:
uri = transfer.get_transformed_uri()
type_ = 'chat'
if transfer.is_groupchat:
type_ = 'groupchat'
message = OutgoingMessage(account=transfer.account,
contact=transfer.contact,
message=uri,
type_=type_,
oob_url=uri)
client = app.get_client(transfer.account)
client.send_message(message)
@staticmethod
def handle_event_metacontacts(obj):
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment