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
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
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 = '<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)
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('<big>' + during_text + '</big>')
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('<big>' + self.messages_queue.get() + '</big>')
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):

View File

@@ -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.<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>
Philipp Hörist <philipp@hoerist.com>
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
max_gajim_version: 0.16.10.1

View File

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