Merge branch 'httpupload' into 'gtk3'

httpupload 0.6.0

See merge request !25
This commit is contained in:
Philipp Hörist
2017-02-27 23:11:21 +01:00
3 changed files with 296 additions and 437 deletions

View File

@@ -14,267 +14,176 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from gi.repository import GObject, Gtk, GLib
import os import os
import sys import threading
import time import ssl
import urllib
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
import mimetypes # better use the magic packet, but that's not a standard lib import mimetypes
import gtkgui_helpers
import logging import logging
from queue import Queue from binascii import hexlify
import binascii import certifi
import nbxmpp
from gi.repository import Gtk, GLib
from common import gajim from common import gajim
from common import ged from common import ged
import chat_control
from plugins import GajimPlugin from plugins import GajimPlugin
from plugins.helpers import log_calls from dialogs import FileChooserDialog, ErrorDialog
from dialogs import FileChooserDialog, ImageChooserDialog, ErrorDialog
import nbxmpp
from .thumbnail import thumbnail
log = logging.getLogger('gajim.plugin_system.httpupload') log = logging.getLogger('gajim.plugin_system.httpupload')
try: try:
if os.name == 'nt': from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.backends.openssl import backend
else:
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers.modes import GCM from cryptography.hazmat.primitives.ciphers.modes import GCM
encryption_available = True ENCRYPTION_AVAILABLE = True
except Exception as e: except Exception as exc:
DEP_MSG = 'For encryption of files, ' \ DEP_MSG = 'For encryption of files, ' \
'please install python-cryptography!' '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') 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' NS_HTTPUPLOAD = 'urn:xmpp:http:upload'
TAGSIZE = 16 TAGSIZE = 16
jid_to_servers = {}
iq_ids_to_callbacks = {}
last_info_query = {}
class HttpuploadPlugin(GajimPlugin): class HttpuploadPlugin(GajimPlugin):
@log_calls('HttpuploadPlugin')
def init(self): def init(self):
if not encryption_available: if not ENCRYPTION_AVAILABLE:
self.available_text = DEP_MSG self.available_text = DEP_MSG
self.config_dialog = None # HttpuploadPluginConfigDialog(self) self.config_dialog = None
self.controls = []
self.events_handlers = {} self.events_handlers = {}
self.events_handlers['agent-info-received'] = (ged.PRECORE, self.events_handlers['agent-info-received'] = (
self.handle_agent_info_received) ged.PRECORE, self.handle_agent_info_received)
self.events_handlers['raw-iq-received'] = (ged.PRECORE, self.events_handlers['raw-iq-received'] = (
self.handle_iq_received) ged.PRECORE, self.handle_iq_received)
self.gui_extension_points = { self.gui_extension_points = {
'chat_control_base': (self.connect_with_chat_control, 'chat_control_base': (self.connect_with_chat_control,
self.disconnect_from_chat_control), self.disconnect_from_chat_control),
'chat_control_base_update_toolbar': (self.update_button_state, 'chat_control_base_update_toolbar': (self.update_chat_control,
None)} None)}
self.first_run = True self.gui_interfaces = {}
def handle_iq_received(self, event): @staticmethod
global iq_ids_to_callbacks def handle_iq_received(event):
id_ = event.stanza.getAttr("id") id_ = event.stanza.getAttr("id")
if str(id_) in iq_ids_to_callbacks: if id_ in IQ_CALLBACK:
try: try:
iq_ids_to_callbacks[str(id_)](event.stanza) IQ_CALLBACK[id_](event.stanza)
except: except:
raise raise
finally: finally:
del iq_ids_to_callbacks[str(id_)] del IQ_CALLBACK[id_]
def handle_agent_info_received(self, event): def handle_agent_info_received(self, event):
global jid_to_servers if (NS_HTTPUPLOAD in event.features and
if NS_HTTPUPLOAD in event.features and gajim.jid_is_transport(event.jid): gajim.jid_is_transport(event.jid)):
own_jid = gajim.get_jid_without_resource(str(event.stanza.getTo())) account = event.conn.name
jid_to_servers[own_jid] = event.jid # map own jid to upload component's jid interface = self.get_interface(account)
log.info(own_jid + " can do http uploads via component " + event.jid) interface.enabled = True
# update all buttons interface.component = event.jid
for base in self.controls: interface.update_button_states(True)
self.update_button_state(base.chat_control)
@log_calls('HttpuploadPlugin') def connect_with_chat_control(self, chat_control):
def connect_with_chat_control(self, control): account = chat_control.contact.account.name
self.chat_control = control self.get_interface(account).add_button(chat_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)
@log_calls('HttpuploadPlugin')
def disconnect_from_chat_control(self, chat_control): def disconnect_from_chat_control(self, chat_control):
for control in self.controls: jid = chat_control.contact.jid
control.disconnect_from_chat_control() account = chat_control.account
self.controls = [] 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_chat_control(self, chat_control):
def update_button_state(self, chat_control): account = chat_control.account
global jid_to_servers if gajim.connections[account].connection is None:
global iq_ids_to_callbacks self.get_interface(account).update_button_states(False)
global last_info_query
if gajim.connections[chat_control.account].connection == None and \ def get_interface(self, account):
gajim.get_jid_from_account(chat_control.account) in jid_to_servers: try:
# maybe don't delete this and detect vanished upload components when actually trying to upload something return self.gui_interfaces[account]
log.info("Deleting %s from jid_to_servers (disconnected)" % gajim.get_jid_from_account(chat_control.account)) except KeyError:
del jid_to_servers[gajim.get_jid_from_account(chat_control.account)] self.gui_interfaces[account] = Base(self, account)
return self.gui_interfaces[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)
class Base(object): class Base(object):
def __init__(self, plugin, chat_control): def __init__(self, plugin, account):
self.dlg = None
self.dialog_type = 'file'
self.plugin = plugin self.plugin = plugin
self.account = account
self.encrypted_upload = False self.encrypted_upload = False
self.chat_control = chat_control self.enabled = False
actions_hbox = chat_control.xml.get_object('actions_hbox') self.component = None
self.button = Gtk.Button(label=None, stock=None, use_underline=True) self.controls = {}
self.button.set_property('can-focus', False) self.conn = gajim.connections[account].connection
self.button.set_sensitive(False)
def add_button(self, chat_control):
jid = chat_control.contact.jid
img = Gtk.Image() img = Gtk.Image()
img.set_from_file(self.plugin.local_file_path('httpupload.png')) img.set_from_file(self.plugin.local_file_path('httpupload.png'))
self.button.set_image(img) actions_hbox = chat_control.xml.get_object('actions_hbox')
self.button.set_tooltip_text(_('Your server does not support http uploads')) button = Gtk.Button(label=None, stock=None, use_underline=True)
self.button.set_relief(Gtk.ReliefStyle.NONE) button.set_property('can-focus', False)
self.image_button = Gtk.Button(label=None, stock=None, use_underline=True) button.set_image(img)
self.image_button.set_property('can-focus', False) button.set_relief(Gtk.ReliefStyle.NONE)
self.image_button.set_relief(Gtk.ReliefStyle.NONE)
self.image_button.set_sensitive(False) actions_hbox.add(button)
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'))
send_button = chat_control.xml.get_object('send_button') send_button = chat_control.xml.get_object('send_button')
actions_hbox.add(self.button) button_pos = actions_hbox.child_get_property(send_button, 'position')
actions_hbox.add(self.image_button) actions_hbox.child_set_property(button, 'position', button_pos - 1)
send_button_pos = actions_hbox.child_get_property(send_button, 'position') self.controls[jid] = button
actions_hbox.child_set_property(self.image_button, 'position', send_button_pos - 1) id_ = button.connect(
actions_hbox.child_set_property(self.button, 'position', send_button_pos - 1) '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) @staticmethod
image_id = self.image_button.connect('clicked', self.on_image_button_clicked) def set_button_state(state, button):
chat_control.handlers[file_id] = self.button if state:
chat_control.handlers[image_id] = self.image_button button.set_sensitive(state)
self.button.show() button.set_tooltip_text(_('Send file via http upload'))
self.image_button.show() 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): def encryption_activated(self, jid):
actions_hbox = self.chat_control.xml.get_object('actions_hbox') if not ENCRYPTION_AVAILABLE:
actions_hbox.remove(self.button)
actions_hbox.remove(self.image_button)
def encryption_activated(self):
if not encryption_available:
return False return False
jid = self.chat_control.contact.jid
account = self.chat_control.account
for plugin in gajim.plugin_manager.active_plugins: for plugin in gajim.plugin_manager.active_plugins:
if type(plugin).__name__ == 'OmemoPlugin': if type(plugin).__name__ == 'OmemoPlugin':
omemo = plugin state = plugin.get_omemo_state(self.account)
break encryption = state.encryption.is_active(jid)
if omemo: log.info('Encryption is: %s', bool(encryption))
state = omemo.get_omemo_state(account) return bool(encryption)
log.info('Encryption is: ' + log.info('OMEMO not found, encryption disabled')
str(state.encryption.is_active(jid)))
return state.encryption.is_active(jid)
log.info('Encryption is: False / OMEMO not found')
return False return False
def on_file_dialog_ok(self, widget, path_to_file=None): def on_file_dialog_ok(self, widget, jid, chat_control):
global jid_to_servers path = widget.get_filename()
widget.destroy()
try: if not path or not os.path.exists(path):
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):
return 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 invalid_file = False
msg = '' if os.path.isfile(path):
if os.path.isfile(path_to_file): stat = os.stat(path)
stat = os.stat(path_to_file)
if stat[6] == 0: if stat[6] == 0:
invalid_file = True invalid_file = True
msg = _('File is empty') msg = _('File is empty')
@@ -282,207 +191,177 @@ class Base(object):
invalid_file = True invalid_file = True
msg = _('File does not exist') msg = _('File does not exist')
if invalid_file: 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 return
mime_type = mimetypes.MimeTypes().guess_type(path_to_file)[0] encrypted = self.encryption_activated(jid)
if not mime_type: size = os.path.getsize(path)
mime_type = 'application/octet-stream' # fallback mime type key, iv = None, None
log.info("Detected MIME Type of file: " + str(mime_type)) if encrypted:
progress_messages = Queue(8) key = os.urandom(32)
progress_window = ProgressWindow(_('HTTP Upload'), _('Requesting HTTP Upload Slot...'), iv = os.urandom(16)
progress_messages, self.plugin, parent=self.chat_control.parent_win.window) size += TAGSIZE
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
try: mime = mimetypes.MimeTypes().guess_type(path)[0]
if self.encrypted_upload: if not mime:
key = os.urandom(32) mime = 'application/octet-stream' # fallback mime type
iv = os.urandom(16) log.info("Detected MIME type of file: %s", mime)
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
put = slot.getTag("put") event = threading.Event()
get = slot.getTag("get") progress = ProgressWindow(
if not put or not get: self.plugin, chat_control.parent_win.window, event)
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
def upload_complete(response_code): file = File(path=path, size=size, mime=mime, encrypted=encrypted,
if response_code == 0: key=key, iv=iv, control=chat_control,
return # Upload was aborted progress=progress, event=event)
if 200 <= response_code < 300: self.request_slot(file)
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 = '<body><br/><a href="%s"><img alt="%s" src="data:image/jpeg;base64,%s"/></a></body>' % \
(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)
def on_upload_error(): def on_file_button_clicked(self, widget, jid, chat_control):
progress_window.close_dialog() FileChooserDialog(
ErrorDialog(_('Could not upload file'), on_response_ok=lambda widget: self.on_file_dialog_ok(widget, jid,
_('Got unexpected exception while uploading file' chat_control),
' (see error log for more information)'), title_text=_('Choose file to send'),
transient_for=self.chat_control.parent_win.window) action=Gtk.FileChooserAction.OPEN,
return 0 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(): def request_slot(self, file):
progress_messages.put(_('Uploading file via HTTP...')) iq = nbxmpp.Iq(typ='get', to=self.component)
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
id_ = gajim.get_an_id() 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_) iq.setID(id_)
request = iq.addChild( request = iq.setTag(name="request", namespace=NS_HTTPUPLOAD)
name="request", request.addChild('filename', payload=os.path.basename(file.path))
namespace=NS_HTTPUPLOAD request.addChild('size', payload=file.size)
) request.addChild('content-type', payload=file.mime)
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)
# send slot request and register callback log.info("Sending request for slot")
log.debug("sending httpupload slot request iq...") IQ_CALLBACK[id_] = lambda stanza: self.received_slot(stanza, file)
iq_ids_to_callbacks[str(id_)] = upload_file self.conn.send(iq)
gajim.connections[self.chat_control.account].connection.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): try:
self.dialog_type = 'file' file.put = stanza.getTag("slot").getTag("put").getData()
self.dlg = FileChooserDialog(on_response_ok=self.on_file_dialog_ok, on_response_cancel=None, file.get = stanza.getTag("slot").getTag("get").getData()
title_text = _('Choose file to send'), action = Gtk.FileChooserAction.OPEN, except Exception:
buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK), file.progress.close_dialog()
default_response = Gtk.ResponseType.OK,) log.error("Got unexpected stanza: %s", stanza)
self.dlg.set_transient_for(self.chat_control.parent_win.window) 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): try:
self.dialog_type = 'image' file.stream = StreamFileWithProgress(file, "rb")
self.dlg = ImageChooserDialog(on_response_ok=self.on_file_dialog_ok, on_response_cancel=None) except Exception as exc:
self.dlg.set_transient_for(self.chat_control.parent_win.window) 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: class StreamFileWithProgress:
def __init__(self, path, mode, callback=None, def __init__(self, file, mode, *args):
encrypted_upload=False, key=None, iv=None, *args): self.event = file.event
self.backing = open(path, mode) self.backing = open(file.path, mode)
self.encrypted_upload = encrypted_upload self.encrypted = file.encrypted
self.backing.seek(0, os.SEEK_END) self.backing.seek(0, os.SEEK_END)
if self.encrypted_upload: if self.encrypted:
if os.name == 'nt':
self.backend = backend
else:
self.backend = default_backend()
self.encryptor = Cipher( self.encryptor = Cipher(
algorithms.AES(key), algorithms.AES(file.key),
GCM(iv), GCM(file.iv),
backend=self.backend).encryptor() backend=default_backend()).encryptor()
self._total = self.backing.tell() + TAGSIZE self._total = self.backing.tell() + TAGSIZE
else: else:
self._total = self.backing.tell() self._total = self.backing.tell()
self.backing.seek(0) self.backing.seek(0)
self._callback = callback self._callback = file.progress.update_progress
self._args = args self._args = args
self._seen = 0 self._seen = 0
@@ -490,7 +369,9 @@ class StreamFileWithProgress:
return self._total return self._total
def read(self, size): def read(self, size):
if self.encrypted_upload: if self.event.isSet():
raise UploadAbortedException
if self.encrypted:
data = self.backing.read(size) data = self.backing.read(size)
if len(data) > 0: if len(data) > 0:
data = self.encryptor.update(data) data = self.encryptor.update(data)
@@ -500,13 +381,15 @@ class StreamFileWithProgress:
data += self.encryptor.tag data += self.encryptor.tag
self._seen += TAGSIZE self._seen += TAGSIZE
if self._callback: if self._callback:
self._callback(self._seen, self._total, *self._args) GLib.idle_add(
self._callback, self._seen, self._total, *self._args)
return data return data
else: else:
data = self.backing.read(size) data = self.backing.read(size)
self._seen += len(data) self._seen += len(data)
if self._callback: if self._callback:
self._callback(self._seen, self._total, *self._args) GLib.idle_add(
self._callback, self._seen, self._total, *self._args)
return data return data
def close(self): def close(self):
@@ -514,67 +397,47 @@ class StreamFileWithProgress:
class ProgressWindow: class ProgressWindow:
def __init__(self, title_text, during_text, messages_queue, plugin, parent): def __init__(self, plugin, parent, event):
self.plugin = plugin self.plugin = plugin
self.xml = gtkgui_helpers.get_gtk_builder(self.plugin.local_file_path('upload_progress_dialog.ui')) self.event = event
self.messages_queue = messages_queue 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 = self.xml.get_object('progress_dialog')
self.dialog.set_transient_for(parent) self.dialog.set_transient_for(parent)
self.dialog.set_title('HTTP Upload')
self.label = self.xml.get_object('label') self.label = self.xml.get_object('label')
self.cancel_button = self.xml.get_object('close_button') self.label.set_text(_('Requesting HTTP Upload Slot...'))
self.label.set_markup('<big>' + during_text + '</big>')
self.progressbar = self.xml.get_object('progressbar') 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.dialog.show_all()
self.xml.connect_signals(self) self.xml.connect_signals(self)
self.stopped = False self.pulse = GLib.timeout_add(100, self.pulse_progressbar)
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)
def pulse_progressbar(self): def pulse_progressbar(self):
if self.dialog: if self.dialog:
self.progressbar.pulse() self.progressbar.pulse()
return True # loop forever return True
return False return False
def process_messages_queue(self): def on_destroy(self, *args):
if not self.messages_queue.empty(): self.event.set()
self.label.set_markup('<big>' + self.messages_queue.get() + '</big>') if self.pulse:
if self.dialog: GLib.source_remove(self.pulse)
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 update_progress(self, seen, total): def update_progress(self, seen, total):
if self.stopped == True: if self.event.isSet():
raise UploadAbortedException return
if self.pulse_progressbar_timeout_id: if self.pulse:
GLib.source_remove(self.pulse_progressbar_timeout_id) GLib.source_remove(self.pulse)
self.pulse_progressbar_timeout_id = None self.pulse = None
pct = (float(seen) / total) * 100.0 pct = (float(seen) / total) * 100.0
self.progressbar.set_fraction(float(seen) / total) self.progressbar.set_fraction(float(seen) / total)
self.progressbar.set_text(str(int(pct)) + "%") self.progressbar.set_text(str(int(pct)) + "%")
log.debug('upload progress: %.2f%% (%d of %d bytes)' % (pct, seen, total))
def close_dialog(self): def close_dialog(self, *args):
self.on_cancel(None) self.dialog.destroy()
class UploadAbortedException(Exception): class UploadAbortedException(Exception):
def __str__(self): def __str__(self):

View File

@@ -1,16 +1,11 @@
[info] [info]
name: HttpUpload name: HttpUpload
short_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.<br/> description: This plugin is designed to send a file to a contact or muc by using httpupload.<br/>
Your server must support <a href="http://xmpp.org/extensions/xep-0363.html">XEP-0363: HTTP Upload</a>.<br/> Your server must support <a href="http://xmpp.org/extensions/xep-0363.html">XEP-0363: HTTP Upload</a>.<br/>
Conversations supported this.<br/>
If the receiving side supports <a href="http://xmpp.org/extensions/xep-0071.html">XEP-0071: XHTML-IM</a>
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 <thilo@eightysoft.de> authors: Thilo Molitor <thilo@eightysoft.de>
Philipp Hörist <philipp@hoerist.com> Philipp Hörist <philipp@hoerist.com>
Linus Heckemann <linus@sphalerite.org> Linus Heckemann <linus@sphalerite.org>
homepage: https://trac-plugins.gajim.org/wiki/HttpUploadPlugin homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/HttpUploadPlugin
min_gajim_version: 0.16.10 min_gajim_version: 0.16.10
max_gajim_version: 0.16.10.1

View File

@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.18.3 --> <!-- Generated with glade 3.20.0 -->
<interface> <interface>
<requires lib="gtk+" version="2.16"/> <requires lib="gtk+" version="3.14"/>
<object class="GtkDialog" id="progress_dialog"> <object class="GtkDialog" id="progress_dialog">
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="resizable">False</property> <property name="resizable">False</property>
<property name="icon_name">upload-media</property> <property name="icon_name">upload-media</property>
<property name="type_hint">dialog</property> <property name="type_hint">dialog</property>
<signal name="delete-event" handler="on_progress_dialog_delete_event" swapped="no"/> <signal name="destroy" handler="on_destroy" swapped="no"/>
<child internal-child="vbox"> <child internal-child="vbox">
<object class="GtkBox" id="dialog-vbox"> <object class="GtkBox" id="dialog-vbox">
<property name="visible">True</property> <property name="visible">True</property>
@@ -25,7 +25,6 @@
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="top_padding">2</property> <property name="top_padding">2</property>
<property name="bottom_padding">4</property> <property name="bottom_padding">4</property>
<property name="left_padding">0</property>
<property name="right_padding">3</property> <property name="right_padding">3</property>
<child> <child>
<object class="GtkButton" id="close_button"> <object class="GtkButton" id="close_button">
@@ -35,7 +34,7 @@
<property name="can_default">True</property> <property name="can_default">True</property>
<property name="receives_default">False</property> <property name="receives_default">False</property>
<property name="use_stock">True</property> <property name="use_stock">True</property>
<signal name="clicked" handler="on_cancel" swapped="no"/> <signal name="clicked" handler="close_dialog" swapped="no"/>
</object> </object>
</child> </child>
</object> </object>
@@ -65,8 +64,10 @@
<object class="GtkLabel" id="label"> <object class="GtkLabel" id="label">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="xalign">0</property> <attributes>
<property name="use_markup">True</property> <attribute name="weight" value="bold"/>
<attribute name="variant" value="normal"/>
</attributes>
</object> </object>
</child> </child>
</object> </object>