diff --git a/httpupload/httpupload.py b/httpupload/httpupload.py
index 0c3446f..9133c60 100644
--- a/httpupload/httpupload.py
+++ b/httpupload/httpupload.py
@@ -14,267 +14,176 @@
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see .
-
-from gi.repository import GObject, Gtk, GLib
import os
-import sys
-import time
+import threading
+import ssl
+import urllib
from urllib.request import Request, urlopen
-import mimetypes # better use the magic packet, but that's not a standard lib
-import gtkgui_helpers
+import mimetypes
import logging
-from queue import Queue
-import binascii
+from binascii import hexlify
+import certifi
+
+import nbxmpp
+from gi.repository import Gtk, GLib
from common import gajim
from common import ged
-import chat_control
from plugins import GajimPlugin
-from plugins.helpers import log_calls
-from dialogs import FileChooserDialog, ImageChooserDialog, ErrorDialog
-import nbxmpp
-
-from .thumbnail import thumbnail
+from dialogs import FileChooserDialog, ErrorDialog
log = logging.getLogger('gajim.plugin_system.httpupload')
try:
- if os.name == 'nt':
- from cryptography.hazmat.backends.openssl import backend
- else:
- from cryptography.hazmat.backends import default_backend
+ from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers.modes import GCM
- encryption_available = True
-except Exception as e:
+ ENCRYPTION_AVAILABLE = True
+except Exception as exc:
DEP_MSG = 'For encryption of files, ' \
'please install python-cryptography!'
- log.debug('Cryptography Import Error: ' + str(e))
+ log.error('Cryptography Import Error: %s', exc)
log.info('Decryption/Encryption disabled due to errors')
- encryption_available = False
+ ENCRYPTION_AVAILABLE = False
-# XEP-0363 (http://xmpp.org/extensions/xep-0363.html)
+IQ_CALLBACK = {}
NS_HTTPUPLOAD = 'urn:xmpp:http:upload'
TAGSIZE = 16
-jid_to_servers = {}
-iq_ids_to_callbacks = {}
-last_info_query = {}
-
class HttpuploadPlugin(GajimPlugin):
-
- @log_calls('HttpuploadPlugin')
def init(self):
- if not encryption_available:
+ if not ENCRYPTION_AVAILABLE:
self.available_text = DEP_MSG
- self.config_dialog = None # HttpuploadPluginConfigDialog(self)
- self.controls = []
+ self.config_dialog = None
self.events_handlers = {}
- self.events_handlers['agent-info-received'] = (ged.PRECORE,
- self.handle_agent_info_received)
- self.events_handlers['raw-iq-received'] = (ged.PRECORE,
- self.handle_iq_received)
+ self.events_handlers['agent-info-received'] = (
+ ged.PRECORE, self.handle_agent_info_received)
+ self.events_handlers['raw-iq-received'] = (
+ ged.PRECORE, self.handle_iq_received)
self.gui_extension_points = {
'chat_control_base': (self.connect_with_chat_control,
- self.disconnect_from_chat_control),
- 'chat_control_base_update_toolbar': (self.update_button_state,
- None)}
- self.first_run = True
+ self.disconnect_from_chat_control),
+ 'chat_control_base_update_toolbar': (self.update_chat_control,
+ None)}
+ self.gui_interfaces = {}
- def handle_iq_received(self, event):
- global iq_ids_to_callbacks
+ @staticmethod
+ def handle_iq_received(event):
id_ = event.stanza.getAttr("id")
- if str(id_) in iq_ids_to_callbacks:
+ if id_ in IQ_CALLBACK:
try:
- iq_ids_to_callbacks[str(id_)](event.stanza)
+ IQ_CALLBACK[id_](event.stanza)
except:
raise
finally:
- del iq_ids_to_callbacks[str(id_)]
+ del IQ_CALLBACK[id_]
def handle_agent_info_received(self, event):
- global jid_to_servers
- if NS_HTTPUPLOAD in event.features and gajim.jid_is_transport(event.jid):
- own_jid = gajim.get_jid_without_resource(str(event.stanza.getTo()))
- jid_to_servers[own_jid] = event.jid # map own jid to upload component's jid
- log.info(own_jid + " can do http uploads via component " + event.jid)
- # update all buttons
- for base in self.controls:
- self.update_button_state(base.chat_control)
+ if (NS_HTTPUPLOAD in event.features and
+ gajim.jid_is_transport(event.jid)):
+ account = event.conn.name
+ interface = self.get_interface(account)
+ interface.enabled = True
+ interface.component = event.jid
+ interface.update_button_states(True)
- @log_calls('HttpuploadPlugin')
- def connect_with_chat_control(self, control):
- self.chat_control = control
- base = Base(self, self.chat_control)
- self.controls.append(base)
- if self.first_run:
- # TODO: Potentially add back keyboard shortcut
- self.first_run = False
- self.update_button_state(self.chat_control)
+ def connect_with_chat_control(self, chat_control):
+ account = chat_control.contact.account.name
+ self.get_interface(account).add_button(chat_control)
- @log_calls('HttpuploadPlugin')
def disconnect_from_chat_control(self, chat_control):
- for control in self.controls:
- control.disconnect_from_chat_control()
- self.controls = []
+ jid = chat_control.contact.jid
+ account = chat_control.account
+ interface = self.get_interface(account)
+ if jid not in interface.controls:
+ return
+ actions_hbox = chat_control.xml.get_object('actions_hbox')
+ actions_hbox.remove(interface.controls[jid])
- @log_calls('HttpuploadPlugin')
- def update_button_state(self, chat_control):
- global jid_to_servers
- global iq_ids_to_callbacks
- global last_info_query
+ def update_chat_control(self, chat_control):
+ account = chat_control.account
+ if gajim.connections[account].connection is None:
+ self.get_interface(account).update_button_states(False)
- if gajim.connections[chat_control.account].connection == None and \
- gajim.get_jid_from_account(chat_control.account) in jid_to_servers:
- # maybe don't delete this and detect vanished upload components when actually trying to upload something
- log.info("Deleting %s from jid_to_servers (disconnected)" % gajim.get_jid_from_account(chat_control.account))
- del jid_to_servers[gajim.get_jid_from_account(chat_control.account)]
-
- # query info at most every 60 seconds in case something goes wrong
- if ((not chat_control.account in last_info_query or
- last_info_query[chat_control.account] + 60 < time.time())
- and not gajim.get_jid_from_account(chat_control.account) in jid_to_servers
- and gajim.account_is_connected(chat_control.account)
- ):
- log.info("Account %s: Using dicovery to find jid of httpupload component" % chat_control.account)
- id_ = gajim.get_an_id()
- iq = nbxmpp.Iq(
- typ='get',
- to=gajim.get_server_from_jid(gajim.get_jid_from_account(chat_control.account)),
- queryNS="http://jabber.org/protocol/disco#items"
- )
- iq.setID(id_)
- def query_info(stanza):
- global last_info_query
- for item in stanza.getTag("query").getTags("item"):
- id_ = gajim.get_an_id()
- iq = nbxmpp.Iq(
- typ='get',
- to=item.getAttr("jid"),
- queryNS="http://jabber.org/protocol/disco#info"
- )
- iq.setID(id_)
- last_info_query[chat_control.account] = time.time()
- gajim.connections[chat_control.account].connection.send(iq)
- iq_ids_to_callbacks[str(id_)] = query_info
- gajim.connections[chat_control.account].connection.send(iq)
- #send disco query to main server jid
- id_ = gajim.get_an_id()
- iq = nbxmpp.Iq(
- typ='get',
- to=gajim.get_server_from_jid(gajim.get_jid_from_account(chat_control.account)),
- queryNS="http://jabber.org/protocol/disco#info"
- )
- iq.setID(id_)
- last_info_query[chat_control.account] = time.time()
- gajim.connections[chat_control.account].connection.send(iq)
-
- for base in self.controls:
- if base.chat_control == chat_control:
- is_supported = gajim.get_jid_from_account(chat_control.account) in jid_to_servers and \
- gajim.connections[chat_control.account].connection != None
- log.info("Account %s: httpupload is_supported: %s" % (str(chat_control.account), str(is_supported)))
- if not is_supported:
- text = _('Your server does not support http uploads')
- image_text = text
- else:
- text = _('Send file via http upload')
- image_text = _('Send image via http upload')
- base.button.set_sensitive(is_supported)
- base.button.set_tooltip_text(text)
- base.image_button.set_sensitive(is_supported)
- base.image_button.set_tooltip_text(image_text)
+ def get_interface(self, account):
+ try:
+ return self.gui_interfaces[account]
+ except KeyError:
+ self.gui_interfaces[account] = Base(self, account)
+ return self.gui_interfaces[account]
class Base(object):
- def __init__(self, plugin, chat_control):
- self.dlg = None
- self.dialog_type = 'file'
+ def __init__(self, plugin, account):
self.plugin = plugin
+ self.account = account
self.encrypted_upload = False
- self.chat_control = chat_control
- actions_hbox = chat_control.xml.get_object('actions_hbox')
- self.button = Gtk.Button(label=None, stock=None, use_underline=True)
- self.button.set_property('can-focus', False)
- self.button.set_sensitive(False)
+ self.enabled = False
+ self.component = None
+ self.controls = {}
+ self.conn = gajim.connections[account].connection
+
+ def add_button(self, chat_control):
+ jid = chat_control.contact.jid
+
img = Gtk.Image()
img.set_from_file(self.plugin.local_file_path('httpupload.png'))
- self.button.set_image(img)
- self.button.set_tooltip_text(_('Your server does not support http uploads'))
- self.button.set_relief(Gtk.ReliefStyle.NONE)
- self.image_button = Gtk.Button(label=None, stock=None, use_underline=True)
- self.image_button.set_property('can-focus', False)
- self.image_button.set_relief(Gtk.ReliefStyle.NONE)
- self.image_button.set_sensitive(False)
- img = Gtk.Image()
- img.set_from_file(self.plugin.local_file_path('image.png'))
- self.image_button.set_image(img)
- self.image_button.set_tooltip_text(_('Your server does not support http uploads'))
+ actions_hbox = chat_control.xml.get_object('actions_hbox')
+ button = Gtk.Button(label=None, stock=None, use_underline=True)
+ button.set_property('can-focus', False)
+ button.set_image(img)
+ button.set_relief(Gtk.ReliefStyle.NONE)
+
+ actions_hbox.add(button)
send_button = chat_control.xml.get_object('send_button')
- actions_hbox.add(self.button)
- actions_hbox.add(self.image_button)
+ button_pos = actions_hbox.child_get_property(send_button, 'position')
+ actions_hbox.child_set_property(button, 'position', button_pos - 1)
- send_button_pos = actions_hbox.child_get_property(send_button, 'position')
- actions_hbox.child_set_property(self.image_button, 'position', send_button_pos - 1)
- actions_hbox.child_set_property(self.button, 'position', send_button_pos - 1)
+ self.controls[jid] = button
+ id_ = button.connect(
+ 'clicked', self.on_file_button_clicked, jid, chat_control)
+ chat_control.handlers[id_] = button
+ self.set_button_state(self.enabled, button)
+ button.show()
- file_id = self.button.connect('clicked', self.on_file_button_clicked)
- image_id = self.image_button.connect('clicked', self.on_image_button_clicked)
- chat_control.handlers[file_id] = self.button
- chat_control.handlers[image_id] = self.image_button
- self.button.show()
- self.image_button.show()
+ @staticmethod
+ def set_button_state(state, button):
+ if state:
+ button.set_sensitive(state)
+ button.set_tooltip_text(_('Send file via http upload'))
+ else:
+ button.set_sensitive(state)
+ button.set_tooltip_text(
+ _('Your server does not support http uploads'))
+ def update_button_states(self, state):
+ for jid in self.controls:
+ self.set_button_state(state, self.controls[jid])
- def disconnect_from_chat_control(self):
- actions_hbox = self.chat_control.xml.get_object('actions_hbox')
- actions_hbox.remove(self.button)
- actions_hbox.remove(self.image_button)
-
- def encryption_activated(self):
- if not encryption_available:
+ def encryption_activated(self, jid):
+ if not ENCRYPTION_AVAILABLE:
return False
- jid = self.chat_control.contact.jid
- account = self.chat_control.account
for plugin in gajim.plugin_manager.active_plugins:
if type(plugin).__name__ == 'OmemoPlugin':
- omemo = plugin
- break
- if omemo:
- state = omemo.get_omemo_state(account)
- log.info('Encryption is: ' +
- str(state.encryption.is_active(jid)))
- return state.encryption.is_active(jid)
- log.info('Encryption is: False / OMEMO not found')
+ state = plugin.get_omemo_state(self.account)
+ encryption = state.encryption.is_active(jid)
+ log.info('Encryption is: %s', bool(encryption))
+ return bool(encryption)
+ log.info('OMEMO not found, encryption disabled')
return False
- def on_file_dialog_ok(self, widget, path_to_file=None):
- global jid_to_servers
+ def on_file_dialog_ok(self, widget, jid, chat_control):
+ path = widget.get_filename()
+ widget.destroy()
- try:
- self.encrypted_upload = self.encryption_activated()
- except Exception as e:
- log.debug(e)
- self.encrypted_upload = False
-
- if not path_to_file:
- path_to_file = self.dlg.get_filename()
- if not path_to_file:
- self.dlg.destroy()
- return
- self.dlg.destroy()
- if not os.path.exists(path_to_file):
+ if not path or not os.path.exists(path):
return
- if self.encrypted_upload:
- filesize = os.path.getsize(path_to_file) + TAGSIZE # in bytes
- else:
- filesize = os.path.getsize(path_to_file)
+
invalid_file = False
- msg = ''
- if os.path.isfile(path_to_file):
- stat = os.stat(path_to_file)
+ if os.path.isfile(path):
+ stat = os.stat(path)
if stat[6] == 0:
invalid_file = True
msg = _('File is empty')
@@ -282,207 +191,177 @@ class Base(object):
invalid_file = True
msg = _('File does not exist')
if invalid_file:
- ErrorDialog(_('Could not open file'), msg, transient_for=self.chat_control.parent_win.window)
+ ErrorDialog(_('Could not open file'), msg,
+ transient_for=chat_control.parent_win.window)
return
- mime_type = mimetypes.MimeTypes().guess_type(path_to_file)[0]
- if not mime_type:
- mime_type = 'application/octet-stream' # fallback mime type
- log.info("Detected MIME Type of file: " + str(mime_type))
- progress_messages = Queue(8)
- progress_window = ProgressWindow(_('HTTP Upload'), _('Requesting HTTP Upload Slot...'),
- progress_messages, self.plugin, parent=self.chat_control.parent_win.window)
- def upload_file(stanza):
- slot = stanza.getTag("slot")
- if not slot:
- log.error("got unexpected stanza: "+str(stanza))
- progress_window.close_dialog()
- error = stanza.getTag("error")
- if error and error.getTag("text"):
- ErrorDialog(_('Could not request upload slot'),
- _('Got unexpected response from server: %s') % str(error.getTagData("text")),
- transient_for=self.chat_control.parent_win.window)
- else:
- ErrorDialog(_('Could not request upload slot'),
- _('Got unexpected response from server (protocol mismatch??)'),
- transient_for=self.chat_control.parent_win.window)
- return
+ encrypted = self.encryption_activated(jid)
+ size = os.path.getsize(path)
+ key, iv = None, None
+ if encrypted:
+ key = os.urandom(32)
+ iv = os.urandom(16)
+ size += TAGSIZE
- try:
- if self.encrypted_upload:
- key = os.urandom(32)
- iv = os.urandom(16)
- data = StreamFileWithProgress(path_to_file,
- "rb",
- progress_window.update_progress,
- self.encrypted_upload, key, iv)
- else:
- data = StreamFileWithProgress(path_to_file,
- "rb",
- progress_window.update_progress)
- except:
- log.error("Could not open file")
- progress_window.close_dialog()
- ErrorDialog(_('Could not open file'),
- _('Exception raised while opening file (see error log for more information)'),
- transient_for=self.chat_control.parent_win.window)
- raise # fill error log with useful information
+ 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)
- put = slot.getTag("put")
- get = slot.getTag("get")
- if not put or not get:
- log.error("got unexpected stanza: " + str(stanza))
- progress_window.close_dialog()
- ErrorDialog(_('Could not request upload slot'),
- _('Got unexpected response from server (protocol mismatch??)'),
- transient_for=self.chat_control.parent_win.window)
- return
+ event = threading.Event()
+ progress = ProgressWindow(
+ self.plugin, chat_control.parent_win.window, event)
- def upload_complete(response_code):
- if response_code == 0:
- return # Upload was aborted
- if 200 <= response_code < 300:
- log.info("Upload completed successfully")
- xhtml = None
- is_image = mime_type.split('/', 1)[0] == 'image'
- if ((not isinstance(self.chat_control, chat_control.ChatControl)
- or not self.chat_control.gpg_is_active)
- and self.dialog_type == 'image'
- and is_image
- and not self.encrypted_upload
- ):
- progress_messages.put(_('Calculating (possible) image thumbnail...'))
- thumb = thumbnail(path_to_file)
- if thumb:
- xhtml = '
' % \
- (get.getData(), get.getData(), thumb)
- progress_window.close_dialog()
- id_ = gajim.get_an_id()
- def add_oob_tag():
- pass
- if self.encrypted_upload:
- keyAndIv = '#' + binascii.hexlify(iv) + binascii.hexlify(key)
- self.chat_control.send_message(message=get.getData() + keyAndIv, xhtml=None)
- else:
- self.chat_control.send_message(message=get.getData(), xhtml=xhtml)
- self.chat_control.msg_textview.grab_focus()
- else:
- progress_window.close_dialog()
- log.error("got unexpected http upload response code: " + str(response_code))
- ErrorDialog(_('Could not upload file'),
- _('Got unexpected http response code from server: ') + str(response_code),
- transient_for=self.chat_control.parent_win.window)
+ file = File(path=path, size=size, mime=mime, encrypted=encrypted,
+ key=key, iv=iv, control=chat_control,
+ progress=progress, event=event)
+ self.request_slot(file)
- def on_upload_error():
- progress_window.close_dialog()
- ErrorDialog(_('Could not upload file'),
- _('Got unexpected exception while uploading file'
- ' (see error log for more information)'),
- transient_for=self.chat_control.parent_win.window)
- return 0
+ def on_file_button_clicked(self, widget, jid, chat_control):
+ FileChooserDialog(
+ on_response_ok=lambda widget: self.on_file_dialog_ok(widget, jid,
+ 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)
- def uploader():
- progress_messages.put(_('Uploading file via HTTP...'))
- try:
- headers = {'User-Agent': 'Gajim %s' % gajim.version,
- 'Content-Type': mime_type}
- request = Request(put.getData(), data=data, headers=headers, method='PUT')
- log.debug("opening urllib upload request...")
- transfer = urlopen(request, timeout=30)
- data.close()
- log.debug("urllib upload request done, response code: " + str(transfer.getcode()))
- return transfer.getcode()
- except UploadAbortedException:
- log.info("Upload aborted")
- except:
- log.error("Exception during upload", exc_info=sys.exc_info())
- GLib.idle_add(on_upload_error)
- return 0
-
- log.info("Uploading file to '%s'..." % str(put.getData()))
- log.info("Please download from '%s' later..." % str(get.getData()))
-
- gajim.thread_interface(uploader, [], upload_complete)
-
- is_supported = gajim.get_jid_from_account(self.chat_control.account) in jid_to_servers and \
- gajim.connections[self.chat_control.account].connection != None
- log.info("jid_to_servers of %s: %s ; connection: %s",
- gajim.get_jid_from_account(self.chat_control.account),
- str(jid_to_servers[gajim.get_jid_from_account(self.chat_control.account)]),
- str(gajim.connections[self.chat_control.account].connection))
- if not is_supported:
- progress_window.close_dialog()
- log.error("upload component vanished, account got disconnected??")
- ErrorDialog(_('Your server does not support http uploads or you just got disconnected.\nPlease try to reconnect or reopen the chat window to fix this.'),
- transient_for=self.chat_control.parent_win.window)
- return
-
- # create iq for slot request
+ def request_slot(self, file):
+ iq = nbxmpp.Iq(typ='get', to=self.component)
id_ = gajim.get_an_id()
- iq = nbxmpp.Iq(
- typ='get',
- to=jid_to_servers[gajim.get_jid_from_account(self.chat_control.account)],
- queryNS=None
- )
iq.setID(id_)
- request = iq.addChild(
- name="request",
- namespace=NS_HTTPUPLOAD
- )
- filename = request.addChild(
- name="filename",
- )
- filename.addData(os.path.basename(path_to_file))
- size = request.addChild(
- name="size",
- )
- size.addData(filesize)
- content_type = request.addChild(
- name="content-type",
- )
- content_type.addData(mime_type)
+ 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)
- # send slot request and register callback
- log.debug("sending httpupload slot request iq...")
- iq_ids_to_callbacks[str(id_)] = upload_file
- gajim.connections[self.chat_control.account].connection.send(iq)
+ log.info("Sending request for slot")
+ IQ_CALLBACK[id_] = lambda stanza: self.received_slot(stanza, file)
+ self.conn.send(iq)
- self.chat_control.msg_textview.grab_focus()
+ def received_slot(self, stanza, file):
+ log.info("Received slot")
+ if stanza.getType() == 'error':
+ file.progress.close_dialog()
+ ErrorDialog(_('Could not request upload slot'),
+ stanza.getErrorMsg(),
+ transient_for=file.control.parent_win.window)
+ log.error(stanza)
+ return
- def on_file_button_clicked(self, widget):
- self.dialog_type = 'file'
- self.dlg = FileChooserDialog(on_response_ok=self.on_file_dialog_ok, on_response_cancel=None,
- 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,)
- self.dlg.set_transient_for(self.chat_control.parent_win.window)
+ try:
+ file.put = stanza.getTag("slot").getTag("put").getData()
+ file.get = stanza.getTag("slot").getTag("get").getData()
+ except Exception:
+ file.progress.close_dialog()
+ log.error("Got unexpected stanza: %s", stanza)
+ log.exception('Error')
+ ErrorDialog(_('Could not request upload slot'),
+ _('Got unexpected response from server (see log)'),
+ transient_for=file.control.parent_win.window)
+ return
- def on_image_button_clicked(self, widget):
- self.dialog_type = 'image'
- self.dlg = ImageChooserDialog(on_response_ok=self.on_file_dialog_ok, on_response_cancel=None)
- self.dlg.set_transient_for(self.chat_control.parent_win.window)
+ try:
+ file.stream = StreamFileWithProgress(file, "rb")
+ except Exception as exc:
+ file.progress.close_dialog()
+ log.exception("Could not open file")
+ ErrorDialog(_('Could not open file'),
+ _('Exception raised while opening file (see log)'),
+ transient_for=file.control.parent_win.window)
+ 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(file.progress.label.set_text,
+ _('Uploading file via HTTP...'))
+ try:
+ headers = {'User-Agent': 'Gajim %s' % gajim.version,
+ 'Content-Type': file.mime}
+ request = Request(
+ file.put, data=file.stream, headers=headers, method='PUT')
+ log.info("Opening Urllib upload request...")
+ 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')
+ except Exception as exc:
+ log.exception("Exception during upload")
+ error_msg = exc
+ GLib.idle_add(file.progress.close_dialog)
+ GLib.idle_add(self.on_upload_error, file, error_msg)
+
+ @staticmethod
+ def upload_complete(response_code, file):
+ file.progress.close_dialog()
+ if 200 <= response_code < 300:
+ log.info("Upload completed successfully")
+ message = file.get
+ if file.encrypted:
+ message += '#' + hexlify(file.iv + file.key).decode('utf-8')
+ file.control.send_message(message=message)
+ file.control.msg_textview.grab_focus()
+ else:
+ log.error('Got unexpected http upload response code: %s',
+ response_code)
+ ErrorDialog(
+ _('Could not upload file'),
+ _('HTTP response code from server: %s') % response_code,
+ transient_for=file.control.parent_win.window)
+
+ @staticmethod
+ def on_upload_error(file, reason):
+ file.progress.close_dialog()
+ ErrorDialog(_('Error'), str(reason),
+ transient_for=file.control.parent_win.window)
+
+
+class File:
+ def __init__(self, **kwargs):
+ for k, v in kwargs.items():
+ setattr(self, k, v)
+ self.stream = None
+ self.put = None
+ self.get = None
class StreamFileWithProgress:
- def __init__(self, path, mode, callback=None,
- encrypted_upload=False, key=None, iv=None, *args):
- self.backing = open(path, mode)
- self.encrypted_upload = encrypted_upload
+ def __init__(self, file, mode, *args):
+ self.event = file.event
+ self.backing = open(file.path, mode)
+ self.encrypted = file.encrypted
self.backing.seek(0, os.SEEK_END)
- if self.encrypted_upload:
- if os.name == 'nt':
- self.backend = backend
- else:
- self.backend = default_backend()
+ if self.encrypted:
self.encryptor = Cipher(
- algorithms.AES(key),
- GCM(iv),
- backend=self.backend).encryptor()
+ algorithms.AES(file.key),
+ GCM(file.iv),
+ backend=default_backend()).encryptor()
self._total = self.backing.tell() + TAGSIZE
else:
self._total = self.backing.tell()
self.backing.seek(0)
- self._callback = callback
+ self._callback = file.progress.update_progress
self._args = args
self._seen = 0
@@ -490,7 +369,9 @@ class StreamFileWithProgress:
return self._total
def read(self, size):
- if self.encrypted_upload:
+ if self.event.isSet():
+ raise UploadAbortedException
+ if self.encrypted:
data = self.backing.read(size)
if len(data) > 0:
data = self.encryptor.update(data)
@@ -500,13 +381,15 @@ class StreamFileWithProgress:
data += self.encryptor.tag
self._seen += TAGSIZE
if self._callback:
- self._callback(self._seen, self._total, *self._args)
+ GLib.idle_add(
+ self._callback, self._seen, self._total, *self._args)
return data
else:
data = self.backing.read(size)
self._seen += len(data)
if self._callback:
- self._callback(self._seen, self._total, *self._args)
+ GLib.idle_add(
+ self._callback, self._seen, self._total, *self._args)
return data
def close(self):
@@ -514,67 +397,47 @@ class StreamFileWithProgress:
class ProgressWindow:
- def __init__(self, title_text, during_text, messages_queue, plugin, parent):
+ def __init__(self, plugin, parent, event):
self.plugin = plugin
- self.xml = gtkgui_helpers.get_gtk_builder(self.plugin.local_file_path('upload_progress_dialog.ui'))
- self.messages_queue = messages_queue
+ self.event = event
+ glade_file = self.plugin.local_file_path('upload_progress_dialog.ui')
+ self.xml = Gtk.Builder()
+ self.xml.add_from_file(glade_file)
self.dialog = self.xml.get_object('progress_dialog')
self.dialog.set_transient_for(parent)
+ self.dialog.set_title('HTTP Upload')
self.label = self.xml.get_object('label')
- self.cancel_button = self.xml.get_object('close_button')
- self.label.set_markup('' + during_text + '')
+ self.label.set_text(_('Requesting HTTP Upload Slot...'))
self.progressbar = self.xml.get_object('progressbar')
- self.progressbar.set_text("")
- self.dialog.set_title(title_text)
- #self.dialog.set_geometry_hints(min_width=400, min_height=96)
- #self.dialog.set_position(Gtk.WIN_POS_CENTER_ON_PARENT)
self.dialog.show_all()
self.xml.connect_signals(self)
- self.stopped = False
- self.pulse_progressbar_timeout_id = GLib.timeout_add(100, self.pulse_progressbar)
- self.process_messages_queue_timeout_id = GLib.timeout_add(100, self.process_messages_queue)
-
+ self.pulse = GLib.timeout_add(100, self.pulse_progressbar)
def pulse_progressbar(self):
if self.dialog:
self.progressbar.pulse()
- return True # loop forever
+ return True
return False
- def process_messages_queue(self):
- if not self.messages_queue.empty():
- self.label.set_markup('' + self.messages_queue.get() + '')
- if self.dialog:
- return True # loop forever
- return False
-
- def on_progress_dialog_delete_event(self, widget, event):
- self.stopped = True
- if self.pulse_progressbar_timeout_id:
- GLib.source_remove(self.pulse_progressbar_timeout_id)
- GLib.source_remove(self.process_messages_queue_timeout_id)
-
- def on_cancel(self, widget):
- self.stopped = True
- if self.pulse_progressbar_timeout_id:
- GLib.source_remove(self.pulse_progressbar_timeout_id)
- GLib.source_remove(self.process_messages_queue_timeout_id)
- self.dialog.destroy()
+ def on_destroy(self, *args):
+ self.event.set()
+ if self.pulse:
+ GLib.source_remove(self.pulse)
def update_progress(self, seen, total):
- if self.stopped == True:
- raise UploadAbortedException
- if self.pulse_progressbar_timeout_id:
- GLib.source_remove(self.pulse_progressbar_timeout_id)
- self.pulse_progressbar_timeout_id = None
+ 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)) + "%")
- log.debug('upload progress: %.2f%% (%d of %d bytes)' % (pct, seen, total))
- def close_dialog(self):
- self.on_cancel(None)
+ def close_dialog(self, *args):
+ self.dialog.destroy()
+
class UploadAbortedException(Exception):
def __str__(self):
diff --git a/httpupload/manifest.ini b/httpupload/manifest.ini
index 0d2e319..140363d 100644
--- a/httpupload/manifest.ini
+++ b/httpupload/manifest.ini
@@ -1,16 +1,11 @@
[info]
name: HttpUpload
short_name: httpupload
-version: 0.5.2
+version: 0.6.0
description: This plugin is designed to send a file to a contact or muc by using httpupload.
Your server must support XEP-0363: HTTP Upload.
- Conversations supported this.
- If the receiving side supports XEP-0071: XHTML-IM
- and maintains the scheme data: URI, a thumbnail image is send along the link to the full size image.
- If the receiving side doesn't support this, only a text message containing the link to the image is send.
authors: Thilo Molitor
Philipp Hörist
Linus Heckemann
-homepage: https://trac-plugins.gajim.org/wiki/HttpUploadPlugin
+homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/HttpUploadPlugin
min_gajim_version: 0.16.10
-max_gajim_version: 0.16.10.1
diff --git a/httpupload/upload_progress_dialog.ui b/httpupload/upload_progress_dialog.ui
index b50c3c2..7c4f78c 100644
--- a/httpupload/upload_progress_dialog.ui
+++ b/httpupload/upload_progress_dialog.ui
@@ -1,13 +1,13 @@
-
+
-
+