From 3a3be94aa85a635dbc496f136603ae79e427b5e4 Mon Sep 17 00:00:00 2001
From: Bronko <c0de_gitlab_gajim@teamchris.info>
Date: Wed, 27 Dec 2017 13:17:13 +0100
Subject: [PATCH] integrate httpupload plugin into gajim core

add new config option 'filetransfer_preference'
add drag and drop support for file upload
---
 gajim/chat_control.py                        | 102 +++--
 gajim/chat_control_base.py                   |  87 ++++-
 gajim/common/config.py                       |   2 +
 gajim/common/connection.py                   |   7 +-
 gajim/common/connection_handlers.py          |   6 +-
 gajim/common/connection_handlers_events.py   |  14 +
 gajim/common/httpupload.py                   | 384 +++++++++++++++++++
 gajim/data/gui/chat_control.ui               |  35 +-
 gajim/data/gui/groupchat_control.ui          |  27 +-
 gajim/data/gui/httpupload_progress_dialog.ui |  58 +++
 gajim/dialog_messages.py                     |  40 ++
 gajim/dialogs.py                             |  62 +++
 gajim/groupchat_control.py                   |  74 ++--
 gajim/gui_interface.py                       |  43 ++-
 gajim/gui_menu_builder.py                    |  19 +-
 15 files changed, 880 insertions(+), 80 deletions(-)
 create mode 100644 gajim/common/httpupload.py
 create mode 100644 gajim/data/gui/httpupload_progress_dialog.ui

diff --git a/gajim/chat_control.py b/gajim/chat_control.py
index 83b63c8673..17e96fe8f4 100644
--- a/gajim/chat_control.py
+++ b/gajim/chat_control.py
@@ -97,6 +97,10 @@ class ChatControl(ChatControlBase):
             self._on_authentication_button_clicked)
         self.handlers[id_] = self.authentication_button
 
+        self.sendfile_button = self.xml.get_object('sendfile_button')
+        self.sendfile_button.set_action_name('win.send-file-' + \
+                                             self.control_id)
+
         # Add lock image to show chat encryption
         self.lock_image = self.xml.get_object('lock_image')
 
@@ -242,7 +246,6 @@ class ChatControl(ChatControlBase):
 
     def add_actions(self):
         actions = [
-            ('send-file-', self._on_send_file),
             ('invite-contacts-', self._on_invite_contacts),
             ('add-to-roster-', self._on_add_to_roster),
             ('information-', self._on_information),
@@ -288,16 +291,42 @@ class ChatControl(ChatControlBase):
         win.lookup_action('toggle-video-' + self.control_id).set_enabled(
             online and self.video_available)
 
+        # Send file (HTTP File Upload)
+        httpupload = win.lookup_action(
+            'send-file-httpupload-' + self.control_id)
+        httpupload.set_enabled(
+            online and app.connections[self.account].httpupload)
+
+        # Send file (Jingle)
+        jingle_conditions = (
+            (self.contact.supports(NS_FILE) or
+             self.contact.supports(NS_JINGLE_FILE_TRANSFER_5)) and
+             self.contact.show != 'offline')
+        jingle = win.lookup_action('send-file-jingle-' + self.control_id)
+        jingle.set_enabled(online and jingle_conditions)
+
         # Send file
-        if ((self.contact.supports(NS_FILE) or \
-        self.contact.supports(NS_JINGLE_FILE_TRANSFER_5)) and \
-        (self.type_id == 'chat' or self.gc_contact.resource)) and \
-        self.contact.show != 'offline' and online:
-            win.lookup_action('send-file-' + self.control_id).set_enabled(
-                True)
-        else:
-            win.lookup_action('send-file-' + self.control_id).set_enabled(
-                False)
+        win.lookup_action(
+            'send-file-' + self.control_id).set_enabled(
+            jingle.get_enabled() or httpupload.get_enabled())
+
+        # Set File Transfer Button tooltip
+        ft_pref = app.config.get_per('accounts', self.account,
+                                     'filetransfer_preference')
+
+        tooltip_text = None
+        if httpupload.get_enabled() and jingle.get_enabled():
+            if ft_pref == 'httpupload':
+                tooltip_text = _('HTTP File Upload')
+            else:
+                tooltip_text = _('Jingle File Transfer')
+        elif httpupload.get_enabled():
+            tooltip_text = _('HTTP File Upload')
+        elif jingle.get_enabled():
+            tooltip_text = _('Jingle File Transfer')
+        elif online:
+            tooltip_text = _('No File Transfer available')
+        self.sendfile_button.set_tooltip_text(tooltip_text)
 
         # Convert to GC
         if app.config.get_per('accounts', self.account, 'is_zeroconf'):
@@ -315,9 +344,6 @@ class ChatControl(ChatControlBase):
         win.lookup_action(
             'information-' + self.control_id).set_enabled(online)
 
-    def _on_send_file(self, action, param):
-        super()._on_send_file()
-
     def _on_add_to_roster(self, action, param):
         dialogs.AddNewContactWindow(self.account, self.contact.jid)
 
@@ -1265,43 +1291,37 @@ class ChatControl(ChatControlBase):
         self.show_avatar()
 
     def _on_drag_data_received(self, widget, context, x, y, selection,
-            target_type, timestamp):
+                               target_type, timestamp):
         if not selection.get_data():
             return
+
+        # get contact info (check for PM = private chat)
         if self.TYPE_ID == message_control.TYPE_PM:
-            c = self.gc_contact
+            c = self.gc_contact.as_contact()
         else:
             c = self.contact
+
         if target_type == self.TARGET_TYPE_URI_LIST:
-            if not c.resource: # If no resource is known, we can't send a file
+            # file drag and drop (handled in chat_control_base)
+            self.drag_data_file_transfer(c, selection, self)
+        else:
+            # chat2muc
+            treeview = app.interface.roster.tree
+            model = treeview.get_model()
+            data = selection.get_data()
+            path = treeview.get_selection().get_selected_rows()[1][0]
+            iter_ = model.get_iter(path)
+            type_ = model[iter_][2]
+            if type_ != 'contact': # source is not a contact
                 return
+            dropped_jid = data
 
-            # we may have more than one file dropped
-            uri_splitted = selection.get_uris()
-            for uri in uri_splitted:
-                path = helpers.get_file_path_from_dnd_dropped_uri(uri)
-                if os.path.isfile(path): # is it file?
-                    ft = app.interface.instances['file_transfers']
-                    ft.send_file(self.account, c, path)
-            return
-
-        # chat2muc
-        treeview = app.interface.roster.tree
-        model = treeview.get_model()
-        data = selection.get_data()
-        path = treeview.get_selection().get_selected_rows()[1][0]
-        iter_ = model.get_iter(path)
-        type_ = model[iter_][2]
-        if type_ != 'contact': # source is not a contact
-            return
-        dropped_jid = data
-
-        dropped_transport = app.get_transport_name_from_jid(dropped_jid)
-        c_transport = app.get_transport_name_from_jid(c.jid)
-        if dropped_transport or c_transport:
-            return # transport contacts cannot be invited
+            dropped_transport = app.get_transport_name_from_jid(dropped_jid)
+            c_transport = app.get_transport_name_from_jid(c.jid)
+            if dropped_transport or c_transport:
+                return # transport contacts cannot be invited
 
-        dialogs.TransformChatToMUC(self.account, [c.jid], [dropped_jid])
+            dialogs.TransformChatToMUC(self.account, [c.jid], [dropped_jid])
 
     def _on_message_tv_buffer_changed(self, textbuffer):
         super()._on_message_tv_buffer_changed(textbuffer)
diff --git a/gajim/chat_control_base.py b/gajim/chat_control_base.py
index 1b4c18ba23..325835f763 100644
--- a/gajim/chat_control_base.py
+++ b/gajim/chat_control_base.py
@@ -27,6 +27,7 @@
 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
 ##
 
+import os
 import time
 from gi.repository import Gtk
 from gi.repository import Gdk
@@ -403,6 +404,24 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
         action.connect('activate', self._on_history)
         self.parent_win.window.add_action(action)
 
+        action = Gio.SimpleAction.new(
+            'send-file-%s' % self.control_id, None)
+        action.connect('activate', self._on_send_file)
+        action.set_enabled(False)
+        self.parent_win.window.add_action(action)
+
+        action = Gio.SimpleAction.new(
+            'send-file-httpupload-%s' % self.control_id, None)
+        action.connect('activate', self._on_send_httpupload)
+        action.set_enabled(False)
+        self.parent_win.window.add_action(action)
+
+        action = Gio.SimpleAction.new(
+            'send-file-jingle-%s' % self.control_id, None)
+        action.connect('activate', self._on_send_jingle)
+        action.set_enabled(False)
+        self.parent_win.window.add_action(action)
+
     # Actions
 
     def _on_history(self, action, param):
@@ -730,6 +749,44 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
             self.drag_entered_conv = True
             self.conv_textview.tv.set_editable(True)
 
+    def drag_data_file_transfer(self, contact, selection, widget):
+        # get file transfer preference
+        ft_pref = app.config.get_per('accounts', self.account,
+                                     'filetransfer_preference')
+        win = self.parent_win.window
+        con = app.connections[self.account]
+        httpupload = win.lookup_action(
+            'send-file-httpupload-%s' % self.control_id)
+        jingle = win.lookup_action('send-file-jingle-%s' % self.control_id)
+
+        # we may have more than one file dropped
+        uri_splitted = selection.get_uris()
+        for uri in uri_splitted:
+            path = helpers.get_file_path_from_dnd_dropped_uri(uri)
+            if not os.path.isfile(path):  # is it a file?
+                continue
+            if self.type_id == message_control.TYPE_GC:
+                # groupchat only supports httpupload on drag and drop
+                if httpupload.get_enabled():
+                    # use httpupload
+                    con.check_file_before_transfer(
+                        path, self.encryption, contact,
+                        self.session, groupchat=True)
+            else:
+                if httpupload.get_enabled() and jingle.get_enabled():
+                    if ft_pref == 'httpupload':
+                        con.check_file_before_transfer(
+                            path, self.encryption, contact, self.session)
+                    else:
+                        ft = app.interface.instances['file_transfers']
+                        ft.send_file(self.account, contact, path)
+                elif httpupload.get_enabled():
+                    con.check_file_before_transfer(
+                        path, self.encryption, contact, self.session)
+                elif jingle.get_enabled():
+                    ft = app.interface.instances['file_transfers']
+                    ft.send_file(self.account, contact, path)
+
     def get_seclabel(self):
         label = None
         if self.seclabel_combo is not None:
@@ -1065,14 +1122,40 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
             app.interface.instances['logs'] = \
                     history_window.HistoryWindow(jid, self.account)
 
-    def _on_send_file(self, gc_contact=None):
+    def _on_send_file(self, action, param):
+        # get file transfer preference
+        ft_pref = app.config.get_per('accounts', self.account,
+                                     'filetransfer_preference')
+
+        win = self.parent_win.window
+        httpupload = win.lookup_action(
+            'send-file-httpupload-%s' % self.control_id)
+        jingle = win.lookup_action('send-file-jingle-%s' % self.control_id)
+
+        if httpupload.get_enabled() and jingle.get_enabled():
+            if ft_pref == 'httpupload':
+                httpupload.activate()
+            else:
+                jingle.activate()
+        elif httpupload.get_enabled():
+            httpupload.activate()
+        elif jingle.get_enabled():
+            jingle.activate()
+
+    def _on_send_httpupload(self, action, param):
+        app.interface.send_httpupload(self)
+
+    def _on_send_jingle(self, action, param):
+        self._on_send_file_jingle()
+
+    def _on_send_file_jingle(self, gc_contact=None):
         """
         gc_contact can be set when we are in a groupchat control
         """
         def _on_ok(c):
             app.interface.instances['file_transfers'].show_file_send_request(
                     self.account, c)
-        if self.TYPE_ID == message_control.TYPE_PM:
+        if self.type_id == message_control.TYPE_PM:
             gc_contact = self.gc_contact
         if gc_contact:
             # gc or pm
diff --git a/gajim/common/config.py b/gajim/common/config.py
index 224faabd8b..e9d590ebb8 100644
--- a/gajim/common/config.py
+++ b/gajim/common/config.py
@@ -408,6 +408,8 @@ class Config:
                     'oauth2_redirect_url': [ opt_str, 'https%3A%2F%2Fgajim.org%2Fmsnauth%2Findex.cgi', _('redirect_url for OAuth 2.0 authentication.')],
                     'opened_chat_controls': [opt_str, '', _('Space separated list of JIDs for which we want to re-open a chat window on next startup.')],
                     'recent_groupchats': [ opt_str, '' ],
+                    'httpupload_verify': [ opt_bool, True, _('HTTP Upload: Enable HTTPS Verification')],
+                    'filetransfer_preference' : [ opt_str, 'httpupload', _('Preferred file transfer mechanism for file drag&drop on chat window. Can be \'httpupload\' (default) or \'jingle\'')],
             }, {}),
             'statusmsg': ({
                     'message': [ opt_str, '' ],
diff --git a/gajim/common/connection.py b/gajim/common/connection.py
index ddeafad62e..20da1165e6 100644
--- a/gajim/common/connection.py
+++ b/gajim/common/connection.py
@@ -422,7 +422,10 @@ class CommonConnection:
         if not obj.is_loggable:
             return
 
-        if obj.forward_from or not obj.session or not obj.session.is_loggable():
+        if obj.forward_from:
+            return
+
+        if obj.session and not obj.session.is_loggable():
             return
 
         if not app.config.should_log(self.name, jid):
@@ -2662,7 +2665,7 @@ class Connection(CommonConnection, ConnectionHandlers):
         if not obj.xhtml and app.config.get('rst_formatting_outgoing_messages'):
             from gajim.common.rst_xhtml_generator import create_xhtml
             obj.xhtml = create_xhtml(obj.message)
-        
+
         msg_iq = nbxmpp.Message(obj.jid, obj.message, typ='groupchat',
                                 xhtml=obj.xhtml)
 
diff --git a/gajim/common/connection_handlers.py b/gajim/common/connection_handlers.py
index 7a40be15d1..3d3c2d1af6 100644
--- a/gajim/common/connection_handlers.py
+++ b/gajim/common/connection_handlers.py
@@ -54,6 +54,7 @@ from gajim.common.protocol.caps import ConnectionCaps
 from gajim.common.protocol.bytestream import ConnectionSocks5Bytestream
 from gajim.common.protocol.bytestream import ConnectionIBBytestream
 from gajim.common.message_archiving import ConnectionArchive313
+from gajim.common.httpupload import ConnectionHTTPUpload
 from gajim.common.connection_handlers_events import *
 
 from gajim.common import ged
@@ -1249,7 +1250,8 @@ class ConnectionHandlersBase:
 class ConnectionHandlers(ConnectionArchive313,
 ConnectionVcard, ConnectionSocks5Bytestream, ConnectionDisco,
 ConnectionCommands, ConnectionPubSub, ConnectionPEP, ConnectionCaps,
-ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
+ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream,
+ConnectionHTTPUpload):
     def __init__(self):
         ConnectionArchive313.__init__(self)
         ConnectionVcard.__init__(self)
@@ -1259,6 +1261,7 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
         ConnectionPubSub.__init__(self)
         ConnectionPEP.__init__(self, account=self.name, dispatcher=self,
             pubsub_connection=self)
+        ConnectionHTTPUpload.__init__(self, account=self.name)
 
         # Handle presences BEFORE caps
         app.nec.register_incoming_event(PresenceReceivedEvent)
@@ -1343,6 +1346,7 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
         ConnectionCaps.cleanup(self)
         ConnectionArchive313.cleanup(self)
         ConnectionPubSub.cleanup(self)
+        ConnectionHTTPUpload.cleanup(self)
         app.ged.remove_event_handler('http-auth-received', ged.CORE,
             self._nec_http_auth_received)
         app.ged.remove_event_handler('version-request-received', ged.CORE,
diff --git a/gajim/common/connection_handlers_events.py b/gajim/common/connection_handlers_events.py
index 2ad7b78bff..98d03a64de 100644
--- a/gajim/common/connection_handlers_events.py
+++ b/gajim/common/connection_handlers_events.py
@@ -2958,3 +2958,17 @@ class BlockingEvent(nec.NetworkIncomingEvent):
             app.log('blocking').info(
                 'Blocking Push - unblocked JIDs: %s', self.unblocked_jids)
         return True
+
+class HTTPUploadStartEvent(nec.NetworkIncomingEvent):
+    name = 'httpupload-start'
+    base_network_events = []
+
+    def generate(self):
+        return True
+
+class HTTPUploadProgressEvent(nec.NetworkIncomingEvent):
+    name = 'httpupload-progress'
+    base_network_events = []
+
+    def generate(self):
+        return True
\ No newline at end of file
diff --git a/gajim/common/httpupload.py b/gajim/common/httpupload.py
new file mode 100644
index 0000000000..49ea37b587
--- /dev/null
+++ b/gajim/common/httpupload.py
@@ -0,0 +1,384 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of Gajim.
+#
+# Gajim is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published
+# by the Free Software Foundation; version 3 only.
+#
+# Gajim is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import threading
+import ssl
+import urllib
+from urllib.request import Request, urlopen
+from urllib.parse import urlparse
+import io
+import mimetypes
+import logging
+
+import nbxmpp
+from nbxmpp import NS_HTTPUPLOAD
+from gi.repository import GLib
+
+from gajim.common import app
+from gajim.common import ged
+from gajim.common.connection_handlers_events import InformationEvent
+from gajim.common.connection_handlers_events import HTTPUploadProgressEvent
+from gajim.common.connection_handlers_events import MessageOutgoingEvent
+from gajim.common.connection_handlers_events import GcMessageOutgoingEvent
+
+if os.name == 'nt':
+    import certifi
+
+log = logging.getLogger('gajim.c.httpupload')
+
+
+class ConnectionHTTPUpload:
+    """
+    Implement HTTP File Upload
+    (XEP-0363, https://xmpp.org/extensions/xep-0363.html)
+    """
+    def __init__(self, account):
+        self.name = account
+        self.encrypted_upload = False
+        self.component = None
+        self.max_file_size = None  # maximum file size in bytes
+
+        app.ged.register_event_handler('agent-info-received',
+                                       ged.GUI1,
+                                       self.handle_agent_info_received)
+        app.ged.register_event_handler('stanza-message-outgoing',
+                                       ged.OUT_PREGUI,
+                                       self.handle_outgoing_stanza)
+        app.ged.register_event_handler('gc-stanza-message-outgoing',
+                                       ged.OUT_PREGUI,
+                                       self.handle_outgoing_stanza)
+
+        self.messages = []
+
+    def cleanup(self):
+        app.ged.remove_event_handler('agent-info-received',
+                                     ged.GUI1,
+                                     self.handle_agent_info_received)
+        app.ged.remove_event_handler('stanza-message-outgoing',
+                                     ged.OUT_PREGUI,
+                                     self.handle_outgoing_stanza)
+        app.ged.remove_event_handler('gc-stanza-message-outgoing',
+                                     ged.OUT_PREGUI,
+                                     self.handle_outgoing_stanza)
+
+    def handle_agent_info_received(self, event):
+        if (NS_HTTPUPLOAD not in event.features or not
+                app.jid_is_transport(event.jid)):
+            return
+
+        if not event.id_.startswith('Gajim_'):
+            return
+
+        account = event.conn.name
+        self.component = event.jid
+
+        for form in event.data:
+            form_dict = form.asDict()
+            if form_dict.get('FORM_TYPE', None) != NS_HTTPUPLOAD:
+                continue
+            size = form_dict.get('max-file-size', None)
+            if size is not None:
+                self.max_file_size = int(size)
+                break
+
+        if self.max_file_size is None:
+            log.warning('%s does not provide maximum file size', account)
+        else:
+            log.info('%s has a maximum file size of: %s MiB',
+                     account, self.max_file_size/(1024*1024))
+
+    def handle_outgoing_stanza(self, event):
+        message = event.msg_iq.getTagData('body')
+        if message and message in self.messages:
+            self.messages.remove(message)
+            # Add oob information before sending message to recipient,
+            #  to distinguish HTTP File Upload Link from pasted URL
+            oob = event.msg_iq.addChild('x', namespace=nbxmpp.NS_X_OOB)
+            oob.addChild('url').setData(message)
+            if 'gajim' in event.additional_data:
+                event.additional_data['gajim']['oob_url'] = message
+            else:
+                event.additional_data['gajim'] = {'oob_url': message}
+
+    def check_file_before_transfer(self, path, encryption, contact, session,
+                                   groupchat=False):
+        if not path or not os.path.exists(path):
+            return
+
+        invalid_file = False
+        stat = os.stat(path)
+
+        if os.path.isfile(path):
+            if stat[6] == 0:
+                invalid_file = True
+                msg = _('File is empty')
+        else:
+            invalid_file = True
+            msg = _('File does not exist')
+
+        if self.max_file_size is not None and \
+                stat.st_size > self.max_file_size:
+            invalid_file = True
+            size = GLib.format_size_full(self.max_file_size,
+                                         GLib.FormatSizeFlags.IEC_UNITS)
+            msg = _('File is too large, '
+                    'maximum allowed file size is: %s') % size
+
+        if invalid_file:
+            self.raise_information_event('open-file-error2', msg)
+            return
+
+        mime = mimetypes.MimeTypes().guess_type(path)[0]
+        if not mime:
+            mime = 'application/octet-stream'  # fallback mime type
+        log.info("Detected MIME type of file: %s", mime)
+
+        try:
+            file = File(path, contact, mime=mime, encryption=encryption,
+                        update_progress=self.raise_progress_event,
+                        session=session, groupchat=groupchat)
+            app.interface.show_httpupload_progress(file)
+        except Exception as error:
+            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(file, self.request_slot)
+        else:
+            self.request_slot(file)
+
+    @staticmethod
+    def raise_progress_event(status, file, seen=None, total=None):
+        app.nec.push_incoming_event(HTTPUploadProgressEvent(
+            None, status=status, file=file, seen=seen, total=total))
+
+    @staticmethod
+    def raise_information_event(dialog_name, args=None):
+        app.nec.push_incoming_event(InformationEvent(
+            None, dialog_name=dialog_name, args=args))
+
+    def request_slot(self, file):
+        GLib.idle_add(self.raise_progress_event, 'request', file)
+        iq = nbxmpp.Iq(typ='get', to=self.component)
+        id_ = app.get_an_id()
+        iq.setID(id_)
+        request = iq.setTag(name="request", namespace=NS_HTTPUPLOAD)
+        request.addChild('filename', payload=os.path.basename(file.path))
+        request.addChild('size', payload=file.size)
+        request.addChild('content-type', payload=file.mime)
+
+        log.info("Sending request for slot")
+        app.connections[self.name].connection.SendAndCallForResponse(
+            iq, self.received_slot, {'file': file})
+
+    @staticmethod
+    def get_slot_error_message(stanza):
+        tmp = stanza.getTag('error').getTag('file-too-large')
+
+        if tmp is not None:
+            max_file_size = int(tmp.getTag('max-file-size').getData())
+            return _('File is too large, maximum allowed file size is: %s') % \
+                GLib.format_size_full(max_file_size,
+                                      GLib.FormatSizeFlags.IEC_UNITS)
+
+        return stanza.getErrorMsg()
+
+    def received_slot(self, conn, stanza, file):
+        log.info("Received slot")
+        if stanza.getType() == 'error':
+            self.raise_progress_event('close', file)
+            self.raise_information_event('request-upload-slot-error',
+                                         self.get_slot_error_message(stanza))
+            log.error(stanza)
+            return
+
+        try:
+            file.put = stanza.getTag("slot").getTag("put").getData()
+            file.get = stanza.getTag("slot").getTag("get").getData()
+        except Exception:
+            log.error("Got unexpected stanza: %s", stanza)
+            log.exception('Error')
+            self.raise_progress_event('close', file)
+            self.raise_information_event('request-upload-slot-error2')
+            return
+
+        if (urlparse(file.put).scheme != 'https' or
+                urlparse(file.get).scheme != 'https'):
+            self.raise_progress_event('close', file)
+            self.raise_information_event('unsecure-error')
+            return
+
+        try:
+            file.stream = StreamFileWithProgress(file)
+        except Exception:
+            log.exception('Error')
+            self.raise_progress_event('close', file)
+            self.raise_information_event('open-file-error')
+            return
+
+        log.info('Uploading file to %s', file.put)
+        log.info('Please download from %s', file.get)
+
+        thread = threading.Thread(target=self.upload_file, args=(file,))
+        thread.daemon = True
+        thread.start()
+
+    def upload_file(self, file):
+        GLib.idle_add(self.raise_progress_event, 'upload', file)
+        try:
+            headers = {'User-Agent': 'Gajim %s' % app.version,
+                       'Content-Type': file.mime,
+                       'Content-Length': file.size}
+
+            request = Request(
+                file.put, data=file.stream, headers=headers, method='PUT')
+            log.info("Opening Urllib upload request...")
+
+            if not app.config.get_per('accounts', self.name, 'httpupload_verify'):
+                context = ssl.create_default_context()
+                context.check_hostname = False
+                context.verify_mode = ssl.CERT_NONE
+                log.warning('CERT Verification disabled')
+                transfer = urlopen(request, timeout=30, context=context)
+            else:
+                if os.name == 'nt':
+                    transfer = urlopen(
+                        request, cafile=certifi.where(), timeout=30)
+                else:
+                    transfer = urlopen(request, timeout=30)
+            file.stream.close()
+            log.info('Urllib upload request done, response code: %s',
+                     transfer.getcode())
+            GLib.idle_add(self.upload_complete, transfer.getcode(), file)
+            return
+        except UploadAbortedException as exc:
+            log.info(exc)
+            error_msg = exc
+        except urllib.error.URLError as exc:
+            if isinstance(exc.reason, ssl.SSLError):
+                error_msg = exc.reason.reason
+                if error_msg == 'CERTIFICATE_VERIFY_FAILED':
+                    log.exception('Certificate verify failed')
+            else:
+                log.exception('URLError')
+                error_msg = exc.reason
+        except Exception as exc:
+            log.exception("Exception during upload")
+            error_msg = exc
+        GLib.idle_add(self.raise_progress_event, 'close', file)
+        GLib.idle_add(self.on_upload_error, file, error_msg)
+
+    def upload_complete(self, response_code, file):
+        self.raise_progress_event('close', file)
+        if 200 <= response_code < 300:
+            log.info("Upload completed successfully")
+            message = file.get
+            if file.user_data:
+                message += '#' + file.user_data
+                message = self.convert_to_aegscm(message)
+            else:
+                self.messages.append(message)
+
+            if file.groupchat:
+                app.nec.push_outgoing_event(GcMessageOutgoingEvent(
+                    None, account=self.name, jid=file.contact.jid,
+                    message=message, automatic_message=False,
+                    session=file.session))
+            else:
+                app.nec.push_outgoing_event(MessageOutgoingEvent(
+                    None, account=self.name, jid=file.contact.jid,
+                    message=message, keyID=file.keyID, type_='chat',
+                    automatic_message=False, session=file.session))
+
+        else:
+            log.error('Got unexpected http upload response code: %s',
+                      response_code)
+            self.raise_information_event('httpupload-response-error',
+                                         response_code)
+
+    def on_upload_error(self, file, reason):
+        self.raise_progress_event('close', file)
+        self.raise_information_event('httpupload-error', str(reason))
+
+    @staticmethod
+    def convert_to_aegscm(url):
+        return 'aesgcm' + url[5:]
+
+
+class File:
+    def __init__(self, path, contact, **kwargs):
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+        self.encrypted = False
+        self.contact = contact
+        self.keyID = None
+        if hasattr(contact, 'keyID'):
+            self.keyID = contact.keyID
+        self.stream = None
+        self.path = path
+        self.put = None
+        self.get = None
+        self.data = None
+        self.user_data = None
+        self.size = None
+        self.event = threading.Event()
+        self.load_data()
+
+    def load_data(self):
+        with open(self.path, 'rb') as content:
+            self.data = content.read()
+        self.size = len(self.data)
+
+    def get_data(self, full=False):
+        if full:
+            return io.BytesIO(self.data).getvalue()
+        return io.BytesIO(self.data)
+
+
+class StreamFileWithProgress:
+    def __init__(self, file):
+        self.file = file
+        self.event = file.event
+        self.backing = file.get_data()
+        self.backing.seek(0, os.SEEK_END)
+        self._total = self.backing.tell()
+        self.backing.seek(0)
+        self._callback = file.update_progress
+        self._seen = 0
+
+    def __len__(self):
+        return self._total
+
+    def read(self, size):
+        if self.event.isSet():
+            raise UploadAbortedException
+
+        data = self.backing.read(size)
+        self._seen += len(data)
+        if self._callback:
+            GLib.idle_add(self._callback, 'update',
+                          self.file, self._seen, self._total)
+        return data
+
+    def close(self):
+        return self.backing.close()
+
+
+class UploadAbortedException(Exception):
+    def __str__(self):
+        return "Upload Aborted"
diff --git a/gajim/data/gui/chat_control.ui b/gajim/data/gui/chat_control.ui
index 5bf3e23030..74f560baeb 100644
--- a/gajim/data/gui/chat_control.ui
+++ b/gajim/data/gui/chat_control.ui
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.20.0 -->
+<!-- Generated with glade 3.20.1 -->
 <interface>
   <requires lib="gtk+" version="3.20"/>
   <object class="GtkAdjustment" id="adjustment1">
@@ -821,9 +821,6 @@ audio-mic-volume-low</property>
                     <property name="position">4</property>
                   </packing>
                 </child>
-                <child>
-                  <placeholder/>
-                </child>
                 <child>
                   <object class="GtkMenuButton" id="encryption_menu">
                     <property name="visible">True</property>
@@ -846,9 +843,39 @@ audio-mic-volume-low</property>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
                     <property name="pack_type">end</property>
+                    <property name="position">5</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton" id="sendfile_button">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="focus_on_click">False</property>
+                    <property name="receives_default">True</property>
+                    <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                    <property name="relief">none</property>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="icon_name">mail-attachment-symbolic</property>
+                        <property name="icon_size">1</property>
+                      </object>
+                    </child>
+                    <style>
+                      <class name="chatcontrol-actionbar-button"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">False</property>
+                    <property name="pack_type">end</property>
                     <property name="position">6</property>
                   </packing>
                 </child>
+                <child>
+                  <placeholder/>
+                </child>
               </object>
               <packing>
                 <property name="expand">False</property>
diff --git a/gajim/data/gui/groupchat_control.ui b/gajim/data/gui/groupchat_control.ui
index 6115478e93..8e0169d092 100644
--- a/gajim/data/gui/groupchat_control.ui
+++ b/gajim/data/gui/groupchat_control.ui
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.20.0 -->
+<!-- Generated with glade 3.20.1 -->
 <interface>
   <requires lib="gtk+" version="3.20"/>
   <object class="GtkMenu" id="formattings_menu">
@@ -340,6 +340,31 @@
                         <property name="position">5</property>
                       </packing>
                     </child>
+                    <child>
+                      <object class="GtkButton" id="sendfile_button">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="focus_on_click">False</property>
+                        <property name="receives_default">True</property>
+                        <property name="relief">none</property>
+                        <child>
+                          <object class="GtkImage">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="icon_name">mail-attachment-symbolic</property>
+                          </object>
+                        </child>
+                        <style>
+                          <class name="chatcontrol-actionbar-button"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">False</property>
+                        <property name="pack_type">end</property>
+                        <property name="position">6</property>
+                      </packing>
+                    </child>
                   </object>
                   <packing>
                     <property name="expand">False</property>
diff --git a/gajim/data/gui/httpupload_progress_dialog.ui b/gajim/data/gui/httpupload_progress_dialog.ui
new file mode 100644
index 0000000000..2ee9d43b85
--- /dev/null
+++ b/gajim/data/gui/httpupload_progress_dialog.ui
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.1 -->
+<interface>
+  <requires lib="gtk+" version="3.14"/>
+  <object class="GtkBox" id="box">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="orientation">vertical</property>
+    <child>
+      <object class="GtkAlignment" id="alignment1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="top_padding">8</property>
+        <property name="bottom_padding">4</property>
+        <property name="left_padding">8</property>
+        <property name="right_padding">8</property>
+        <child>
+          <object class="GtkLabel" id="label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <attributes>
+              <attribute name="weight" value="bold"/>
+              <attribute name="variant" value="normal"/>
+            </attributes>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkAlignment" id="alignment2">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="top_padding">4</property>
+        <property name="bottom_padding">4</property>
+        <property name="left_padding">8</property>
+        <property name="right_padding">8</property>
+        <child>
+          <object class="GtkProgressBar" id="progressbar">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="pulse_step">0.10000000149</property>
+            <property name="show_text">True</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/gajim/dialog_messages.py b/gajim/dialog_messages.py
index a58c91b647..4e2dd29d9d 100644
--- a/gajim/dialog_messages.py
+++ b/gajim/dialog_messages.py
@@ -133,6 +133,46 @@ messages = {
         _('%s\nLink-local messaging might not work properly.'),
         ErrorDialog),
 
+    'request-upload-slot-error': Message(
+        _('Could not request upload slot'),
+        '%s',
+        ErrorDialog),
+
+    'request-upload-slot-error2': Message(
+        _('Could not request upload slot'),
+        _('Got unexpected response from server (see log)'),
+        ErrorDialog),
+
+    'open-file-error': Message(
+        _('Could not open file'),
+        _('Exception raised while opening file (see log)'),
+        ErrorDialog),
+
+    'open-file-error2': Message(
+        _('Could not open file'),
+        '%s',
+        ErrorDialog),
+
+    'unsecure-error': Message(
+        _('Unsecure'),
+        _('Server returned unsecure 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'),
+        _('For the choosen encryption is no encryption method available'),
+        ErrorDialog),
+
     }
 
 
diff --git a/gajim/dialogs.py b/gajim/dialogs.py
index 56b6f4bfdb..1e8ea8c836 100644
--- a/gajim/dialogs.py
+++ b/gajim/dialogs.py
@@ -5417,3 +5417,65 @@ class SSLErrorDialog(ConfirmationDialogDoubleCheck):
 
     def on_cert_clicked(self, button):
         d = CertificatDialog(self, self.account, self.cert)
+
+class ProgressWindow(Gtk.ApplicationWindow):
+    def __init__(self, file):
+        Gtk.ApplicationWindow.__init__(self)
+        self.set_name('HTTPUploadProgressWindow')
+        self.set_application(app.app)
+        self.set_position(Gtk.WindowPosition.CENTER)
+        self.set_show_menubar(False)
+        self.set_title(_('File Transfer'))
+        self.set_default_size(250, -1)
+
+        self.event = file.event
+        self.file = file
+        self.xml = gtkgui_helpers.get_gtk_builder(
+            'httpupload_progress_dialog.ui')
+
+        self.label = self.xml.get_object('label')
+        self.progressbar = self.xml.get_object('progressbar')
+
+        self.add(self.xml.get_object('box'))
+
+        self.pulse = GLib.timeout_add(100, self._pulse_progressbar)
+        self.show_all()
+
+        self.connect('destroy', self._on_destroy)
+        app.ged.register_event_handler('httpupload-progress', ged.CORE,
+                                       self._on_httpupload_progress)
+
+    def _on_httpupload_progress(self, obj):
+        if self.file != obj.file:
+            return
+        if obj.status == 'request':
+            self.label.set_text(_('Requesting HTTP Upload Slot...'))
+        elif obj.status == 'close':
+            self.destroy()
+        elif obj.status == 'upload':
+            self.label.set_text(_('Uploading file via HTTP File Upload...'))
+        elif obj.status == 'update':
+            self.update_progress(obj.seen, obj.total)
+        elif obj.status == 'encrypt':
+            self.label.set_text(_('Encrypting file...'))
+
+    def _pulse_progressbar(self):
+        self.progressbar.pulse()
+        return True
+
+    def _on_destroy(self, *args):
+        self.event.set()
+        if self.pulse:
+            GLib.source_remove(self.pulse)
+        app.ged.remove_event_handler('httpupload-progress', ged.CORE,
+                                     self._on_httpupload_progress)
+
+    def update_progress(self, seen, total):
+        if self.event.isSet():
+            return
+        if self.pulse:
+            GLib.source_remove(self.pulse)
+            self.pulse = None
+        pct = (float(seen) / total) * 100.0
+        self.progressbar.set_fraction(float(seen) / total)
+        self.progressbar.set_text(str(int(pct)) + "%")
diff --git a/gajim/groupchat_control.py b/gajim/groupchat_control.py
index ca210c5dbb..aa91b616a2 100644
--- a/gajim/groupchat_control.py
+++ b/gajim/groupchat_control.py
@@ -58,6 +58,7 @@ from gajim.common import helpers
 from gajim.common import dataforms
 from gajim.common import ged
 from gajim.common import i18n
+from gajim.common import contacts
 
 from gajim.chat_control import ChatControl
 from gajim.chat_control_base import ChatControlBase
@@ -444,6 +445,11 @@ class GroupchatControl(ChatControlBase):
 
         self.form_widget = None
 
+        # Send file
+        self.sendfile_button = self.xml.get_object('sendfile_button')
+        self.sendfile_button.set_action_name('win.send-file-' + \
+                                             self.control_id)
+
         # Encryption
         self.lock_image = self.xml.get_object('lock_image')
         self.authentication_button = self.xml.get_object(
@@ -589,6 +595,24 @@ class GroupchatControl(ChatControlBase):
         win.lookup_action('execute-command-' + self.control_id).set_enabled(
             online)
 
+        # Send file (HTTP File Upload)
+        httpupload = win.lookup_action(
+            'send-file-httpupload-' + self.control_id)
+        httpupload.set_enabled(
+            online and app.connections[self.account].httpupload)
+
+        win.lookup_action('send-file-' + self.control_id).set_enabled(
+            httpupload.get_enabled())
+
+        tooltip_text = None
+        if online:
+            if httpupload.get_enabled():
+                tooltip_text = _('HTTP File Upload')
+            else:
+                tooltip_text = _('HTTP File Upload not supported '
+                                 'by your server')
+        self.sendfile_button.set_tooltip_text(tooltip_text)
+
     # Actions
 
     def _on_change_subject(self, action, param):
@@ -1565,12 +1589,6 @@ class GroupchatControl(ChatControlBase):
         if ctrl and msg:
             ctrl.send_message(msg)
 
-    def on_send_file(self, widget, gc_contact):
-        """
-        Send a file to a contact in the room
-        """
-        self._on_send_file(gc_contact)
-
     def draw_contact(self, nick, selected=False, focus=False):
         iter_ = self.get_contact_iter(nick)
         if not iter_:
@@ -2294,23 +2312,31 @@ class GroupchatControl(ChatControlBase):
             ok_handler=on_ok, transient_for=self.parent_win.window)
 
     def _on_drag_data_received(self, widget, context, x, y, selection,
-    target_type, timestamp):
-        # Invite contact to groupchat
-        treeview = app.interface.roster.tree
-        model = treeview.get_model()
-        if not selection.get_data() or target_type == 80:
-            #  target_type = 80 means a file is dropped
+                               target_type, timestamp):
+        if not selection.get_data():
             return
-        data = selection.get_data()
-        path = treeview.get_selection().get_selected_rows()[1][0]
-        iter_ = model.get_iter(path)
-        type_ = model[iter_][2]
-        if type_ != 'contact': # source is not a contact
-            return
-        contact_jid = data
-        app.connections[self.account].send_invite(self.room_jid, contact_jid)
-        self.print_conversation(_('%(jid)s has been invited in this room') % {
-            'jid': contact_jid}, graphics=False)
+
+        # get contact info
+        contact = contacts.Contact(jid=self.room_jid, account=self.account)
+
+        if target_type == self.TARGET_TYPE_URI_LIST:
+            # file drag and drop (handled in chat_control_base)
+            self.drag_data_file_transfer(contact, selection, self)
+        else:
+            # Invite contact to groupchat
+            treeview = app.interface.roster.tree
+            model = treeview.get_model()
+            data = selection.get_data()
+            path = treeview.get_selection().get_selected_rows()[1][0]
+            iter_ = model.get_iter(path)
+            type_ = model[iter_][2]
+            if type_ != 'contact': # source is not a contact
+                return
+            contact_jid = data
+
+            app.connections[self.account].send_invite(self.room_jid, contact_jid)
+            self.print_conversation(_('%(jid)s has been invited in this room') %
+                                    {'jid': contact_jid}, graphics=False)
 
     def _on_message_textview_key_press_event(self, widget, event):
         res = ChatControlBase._on_message_textview_key_press_event(self, widget,
@@ -2605,7 +2631,9 @@ class GroupchatControl(ChatControlBase):
         if not c.resource:
             item.set_sensitive(False)
         else:
-            id_ = item.connect('activate', self.on_send_file, c)
+            item.set_sensitive(False)
+            # ToDo: integrate HTTP File Upload
+            id_ = item.connect('activate', self._on_send_file_jingle, c)
             self.handlers[id_] = item
 
         # show the popup now!
diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py
index 9d52c03e66..8bb11fdd5c 100644
--- a/gajim/gui_interface.py
+++ b/gajim/gui_interface.py
@@ -64,6 +64,8 @@ from gajim import dialogs
 from gajim import notify
 from gajim import message_control
 from gajim.dialog_messages import get_dialog
+from gajim.dialogs import ProgressWindow
+from gajim.dialogs import FileChooserDialog
 
 from gajim.chat_control_base import ChatControlBase
 from gajim.chat_control import ChatControl
@@ -88,7 +90,7 @@ from gajim.common import passwords
 from gajim.common import logging_helpers
 from gajim.common.connection_handlers_events import (
     OurShowEvent, FileRequestErrorEvent, FileTransferCompletedEvent,
-    UpdateRosterAvatarEvent, UpdateGCAvatarEvent)
+    UpdateRosterAvatarEvent, UpdateGCAvatarEvent, HTTPUploadProgressEvent)
 from gajim.common.connection import Connection
 from gajim.common.file_props import FilesProp
 from gajim.common import pep
@@ -1139,6 +1141,45 @@ class Interface:
                 app.config.get_per('accounts', account, 'publish_location')):
             location_listener.enable()
 
+    @staticmethod
+    def show_httpupload_progress(file):
+        ProgressWindow(file)
+
+    def send_httpupload(self, chat_control):
+        FileChooserDialog(
+            on_response_ok=lambda widget: self.on_file_dialog_ok(widget,
+                                                                 chat_control),
+            title_text=_('Choose file to send'),
+            action=Gtk.FileChooserAction.OPEN,
+            buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+                     Gtk.STOCK_OPEN, Gtk.ResponseType.OK),
+            default_response=Gtk.ResponseType.OK,
+            transient_for=chat_control.parent_win.window)
+
+    @staticmethod
+    def on_file_dialog_ok(widget, chat_control):
+        path = widget.get_filename()
+        widget.destroy()
+        con = app.connections[chat_control.account]
+        groupchat = chat_control.type_id == message_control.TYPE_GC
+        con.check_file_before_transfer(path,
+                                       chat_control.encryption,
+                                       chat_control.contact,
+                                       chat_control.session,
+                                       groupchat)
+
+    def encrypt_file(self, file, callback):
+        app.nec.push_incoming_event(HTTPUploadProgressEvent(
+            None, status='encrypt', file=file))
+        encryption = file.encryption
+        plugin = app.plugin_manager.encryption_plugins[encryption]
+        if hasattr(plugin, 'encrypt_file'):
+            plugin.encrypt_file(file, None, callback)
+        else:
+            app.nec.push_incoming_event(HTTPUploadProgressEvent(
+                None, status='close', file=file))
+            self.raise_dialog('httpupload-encryption-not-available')
+
     @staticmethod
     def handle_event_metacontacts(obj):
         app.contacts.define_metacontacts(obj.conn.name, obj.meta_list)
diff --git a/gajim/gui_menu_builder.py b/gajim/gui_menu_builder.py
index 011da1ec36..ce1980fa55 100644
--- a/gajim/gui_menu_builder.py
+++ b/gajim/gui_menu_builder.py
@@ -608,7 +608,10 @@ Build dynamic Application Menus
 
 def get_singlechat_menu(control_id):
     singlechat_menu = [
-        ('win.send-file-', _('Send File...')),
+        (_('Send File...'), [
+            ('win.send-file-httpupload-', 'HTTP Upload'),
+            ('win.send-file-jingle-', 'Jingle'),
+            ]),
         ('win.invite-contacts-', _('Invite Contacts')),
         ('win.add-to-roster-', _('Add to Roster')),
         ('win.toggle-audio-', _('Audio Session')),
@@ -620,11 +623,17 @@ def get_singlechat_menu(control_id):
     def build_menu(preset):
         menu = Gio.Menu()
         for item in preset:
-            action_name, label = item
-            if action_name == 'win.browse-history-':
-                menu.append(label, action_name + control_id + '::none')
+            if isinstance(item[1], str):
+                action_name, label = item
+                if action_name == 'win.browse-history-':
+                    menu.append(label, action_name + control_id + '::none')
+                else:
+                    menu.append(label, action_name + control_id)
             else:
-                menu.append(label, action_name + control_id)
+                label, sub_menu = item
+                # This is a submenu
+                submenu = build_menu(sub_menu)
+                menu.append_submenu(label, submenu)
         return menu
 
     return build_menu(singlechat_menu)
-- 
GitLab