From 7ce02c25b1a1e0ed915be4a9bc781744328d92ee Mon Sep 17 00:00:00 2001 From: steckbrief Date: Tue, 23 Aug 2016 10:07:29 +0200 Subject: Add content type header --- storage-backend/index.php | 1 + 1 file changed, 1 insertion(+) diff --git a/storage-backend/index.php b/storage-backend/index.php index a7c79d3..99d919c 100644 --- a/storage-backend/index.php +++ b/storage-backend/index.php @@ -244,6 +244,7 @@ function sendHttpReturnCodeAndJson($code, $data) { if (!is_array($data)) { $data = ['msg' => $data]; } + header('Content-Type: application/json'); sendHttpReturnCodeAndMessage($code, json_encode($data)); } -- cgit v1.2.3 From 509128be585b7108ef77f807d49ce5ffe1a57853 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Fri, 30 Sep 2016 16:03:18 +0200 Subject: Implements FS#236: Save receipient jid --- storage-backend/index.php | 101 ++------------------- storage-backend/lib/functions.common.inc.php | 18 ++++ storage-backend/lib/functions.filetransfer.inc.php | 23 +++++ storage-backend/lib/functions.http.inc.php | 64 +++++++++++++ 4 files changed, 114 insertions(+), 92 deletions(-) create mode 100644 storage-backend/lib/functions.common.inc.php create mode 100644 storage-backend/lib/functions.filetransfer.inc.php create mode 100644 storage-backend/lib/functions.http.inc.php diff --git a/storage-backend/index.php b/storage-backend/index.php index 99d919c..d153e63 100644 --- a/storage-backend/index.php +++ b/storage-backend/index.php @@ -11,6 +11,7 @@ * size * content_type * user_jid + * receipient_jid * 403: In case the XMPP Server Key is not valid * 406: * File is empty (error code: 1) @@ -46,7 +47,9 @@ * The slot's delete token does not match the header field "X-FILETRANSFER-HTTP-DELETE-TOKEN" * The slot's delete token is not valid any more */ - +include_once(__DIR__.'/lib/functions.common.inc.php'); +include_once(__DIR__.'/lib/functions.http.inc.php'); +include_once(__DIR__.'/lib/functions.filetransfer.inc.php'); $method = $_SERVER['REQUEST_METHOD']; // Load configuration @@ -101,6 +104,7 @@ switch ($method) { $filename = rawurlencode(getMandatoryPostParameter('filename')); $filesize = getMandatoryPostParameter('size'); $mimeType = getOptionalPostParameter('content_type'); + $receipientJid = getMandatoryPostParameter('receipient_jid'); // check file name - return 406 (not acceptable) if file contains invalid characters foreach ($config['invalid_characters_in_filename'] as $invalidCharacter) { @@ -118,7 +122,7 @@ switch ($method) { } // generate slot uuid, register slot uuid and expected file size and expected mime type $slotUUID = generate_uuid(); - registerSlot($slotUUID, $filename, $filesize, $mimeType, $userJid, $config); + registerSlot($slotUUID, $filename, $filesize, $mimeType, $userJid, $receipientJid, $config); if (!mkdir(getUploadFilePath($slotUUID, $config))) { sendHttpReturnCodeAndJson(500, "Could not create directory for upload."); } @@ -217,13 +221,6 @@ function checkFilenameParameter($filename, $slotParameters) { return $slotParameters['filename'] == $filename; } -function loadSlotParameters($slotUUID, $config) { - $slotParameters = require(getSlotFilePath($slotUUID, $config)); - $slotParameters['filename'] = $slotParameters['filename']; - - return $slotParameters; -} - function getMandatoryPostParameter($parameterName) { $parameter = $_POST[$parameterName]; if (!isset($parameter) || is_null($parameter) || empty($parameter)) { @@ -232,27 +229,6 @@ function getMandatoryPostParameter($parameterName) { return $parameter; } -function getOptionalPostParameter($parameterName, $default = NULL) { - $parameter = $_POST[$parameterName]; - if (!isset($parameter) || is_null($parameter) || empty($parameter)) { - $parameter = $default; - } - return $parameter; -} - -function sendHttpReturnCodeAndJson($code, $data) { - if (!is_array($data)) { - $data = ['msg' => $data]; - } - header('Content-Type: application/json'); - sendHttpReturnCodeAndMessage($code, json_encode($data)); -} - -function sendHttpReturnCodeAndMessage($code, $text = '') { - http_response_code($code); - exit($text); -} - function getUUIDFromUri($uri) { $pattern = "/[a-f0-9]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/"; preg_match($pattern, $uri, $matches); @@ -264,10 +240,11 @@ function getFilenameFromUri($uri) { return substr($uri, $lastSlash); } -function registerSlot($slotUUID, $filename, $filesize, $contentType, $userJid, $config) { +function registerSlot($slotUUID, $filename, $filesize, $contentType, $userJid, $receipientJid, $config) { $contents = " \''.$filename.'\', \'filesize\' => \''.$filesize.'\', '; - $contents .= '\'content_type\' => \''.$contentType.'\', \'user_jid\' => \''.$userJid.'\'];\n?>'; + $contents .= '\'content_type\' => \''.$contentType.'\', \'user_jid\' => \''.$userJid.'\', \'receipient_jid\' => \''.$receipientJid.'\'];'; + $contents .= "\n?>"; if (!file_put_contents(getSlotFilePath($slotUUID, $config), $contents)) { sendHttpReturnCodeAndMessage(500, "Could not create slot registry entry."); } @@ -286,64 +263,4 @@ function registerDeleteToken($slotUUID, $filename, $deleteToken, $config) { function slotExists($slotUUID, $config) { return file_exists(getSlotFilePath($slotUUID, $config)); } - -function getSlotFilePath($slotUUID, $config) { - return $config['slot_registry_dir'].$slotUUID; -} - -function getUploadFilePath($slotUUID, $config, $filename = NULL) { - $path = $config['storage_base_path'].$slotUUID; - if (!is_null($filename)) { - $path .= '/'.$filename; - } - return $path; -} - -/** - * Inspired by https://github.com/owncloud/core/blob/master/lib/private/appframework/http/request.php#L523 - */ -function getServerProtocol() { - if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { - if (strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], ',') !== false) { - $parts = explode(',', $_SERVER['HTTP_X_FORWARDED_PROTO']); - $proto = strtolower(trim($parts[0])); - } else { - $proto = strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']); - } - // Verify that the protocol is always HTTP or HTTPS - // default to http if an invalid value is provided - return $proto === 'https' ? 'https' : 'http'; - } - if (isset($_SERVER['HTTPS']) - && $_SERVER['HTTPS'] !== null - && $_SERVER['HTTPS'] !== 'off' - && $_SERVER['HTTPS'] !== '') { - return 'https'; - } - return 'http'; -} - -function getRequestHostname() { - if (isset($_SERVER['HTTP_X_FORWARDED_HOST'])) { - return strtolower($_SERVER['HTTP_X_FORWARDED_HOST']); - } - return strtolower($_SERVER['HTTP_HOST']); -} - -function getRequestUriWithoutFilename() { - return strtolower(substr($_SERVER['REQUEST_URI'], 0, strrpos($_SERVER['REQUEST_URI'], '/') + 1)); -} - -/** - * Copied from http://rogerstringer.com/2013/11/15/generate-uuids-php/ - */ -function generate_uuid() { - return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', - mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), - mt_rand( 0, 0xffff ), - mt_rand( 0, 0x0fff ) | 0x4000, - mt_rand( 0, 0x3fff ) | 0x8000, - mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) - ); -} ?> diff --git a/storage-backend/lib/functions.common.inc.php b/storage-backend/lib/functions.common.inc.php new file mode 100644 index 0000000..b47268e --- /dev/null +++ b/storage-backend/lib/functions.common.inc.php @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/storage-backend/lib/functions.filetransfer.inc.php b/storage-backend/lib/functions.filetransfer.inc.php new file mode 100644 index 0000000..679cef1 --- /dev/null +++ b/storage-backend/lib/functions.filetransfer.inc.php @@ -0,0 +1,23 @@ + $data]; + } + header('Content-Type: application/json'); + sendHttpReturnCodeAndMessage($code, json_encode($data)); +} + +function sendHttpReturnCodeAndMessage($code, $text = '') { + http_response_code($code); + exit($text); +} + +function getOptionalPostParameter($parameterName, $default = NULL) { + $parameter = $_POST[$parameterName]; + if (!isset($parameter) || is_null($parameter) || empty($parameter)) { + $parameter = $default; + } + return $parameter; +} +?> \ No newline at end of file -- cgit v1.2.3 From 6816cd2435c5389b7cf004aa8fd0e21888a6f52b Mon Sep 17 00:00:00 2001 From: steckbrief Date: Sun, 9 Oct 2016 11:45:54 +0200 Subject: Added gajim-plugin 'httpupload' version 0.4.1 from https://trac-plugins.gajim.org/browser/httpupload, revision: 952:19bcffb26247 --- gajim-plugin/__init__.py | 2 + gajim-plugin/httpupload.png | Bin 0 -> 780 bytes gajim-plugin/httpupload.py | 681 +++++++++++++++++++++++++++++++++ gajim-plugin/image.png | Bin 0 -> 666 bytes gajim-plugin/manifest.ini | 15 + gajim-plugin/upload_progress_dialog.ui | 105 +++++ 6 files changed, 803 insertions(+) create mode 100644 gajim-plugin/__init__.py create mode 100644 gajim-plugin/httpupload.png create mode 100644 gajim-plugin/httpupload.py create mode 100644 gajim-plugin/image.png create mode 100644 gajim-plugin/manifest.ini create mode 100644 gajim-plugin/upload_progress_dialog.ui diff --git a/gajim-plugin/__init__.py b/gajim-plugin/__init__.py new file mode 100644 index 0000000..23c0eff --- /dev/null +++ b/gajim-plugin/__init__.py @@ -0,0 +1,2 @@ +# simple redirect +from httpupload import HttpuploadPlugin diff --git a/gajim-plugin/httpupload.png b/gajim-plugin/httpupload.png new file mode 100644 index 0000000..c3b3733 Binary files /dev/null and b/gajim-plugin/httpupload.png differ diff --git a/gajim-plugin/httpupload.py b/gajim-plugin/httpupload.py new file mode 100644 index 0000000..e845e3d --- /dev/null +++ b/gajim-plugin/httpupload.py @@ -0,0 +1,681 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Gajim. +## +## Gajim is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## Gajim is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see . +## + +from common import demandimport +demandimport.enable() +demandimport.ignore += ['builtins', '__builtin__', 'PIL', '_imp'] + +import gtk +import gobject +import os +import time +import base64 +import tempfile +import urllib2 +import mimetypes # better use the magic packet, but that's not a standard lib +import gtkgui_helpers +from Queue import Queue +try: + from PIL import Image + pil_available = True +except: + pil_available = False +from io import BytesIO + +import binascii +from common import gajim +from common import ged +import chat_control +from plugins import GajimPlugin +from plugins.helpers import log_calls +import logging +from dialogs import FileChooserDialog, ImageChooserDialog, ErrorDialog +import nbxmpp + +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.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: + DEP_MSG = 'For encryption of files, ' \ + 'please install python-cryptography!' + log.debug('Cryptography Import Error: ' + str(e)) + log.info('Decryption/Encryption disabled due to errors') + encryption_available = False + +# XEP-0363 (http://xmpp.org/extensions/xep-0363.html) +NS_HTTPUPLOAD = 'urn:xmpp:http:upload' +TAGSIZE = 16 + +jid_to_servers = {} +iq_ids_to_callbacks = {} +last_info_query = {} +max_thumbnail_size = 2048 +max_thumbnail_dimension = 160 + + +class HttpuploadPlugin(GajimPlugin): + + @log_calls('HttpuploadPlugin') + def init(self): + if not encryption_available: + self.available_text = DEP_MSG + self.config_dialog = None # HttpuploadPluginConfigDialog(self) + self.controls = [] + 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.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 + + def handle_iq_received(self, event): + global iq_ids_to_callbacks + id_ = event.stanza.getAttr("id") + if str(id_) in iq_ids_to_callbacks: + try: + iq_ids_to_callbacks[str(id_)](event.stanza) + except: + raise + finally: + del iq_ids_to_callbacks[str(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) + + @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: + # ALT + U + gtk.binding_entry_add_signal(control.msg_textview, + gtk.keysyms.u, gtk.gdk.MOD1_MASK, 'mykeypress', + int, gtk.keysyms.u, gtk.gdk.ModifierType, gtk.gdk.MOD1_MASK) + self.first_run = False + self.update_button_state(self.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 = [] + + @log_calls('HttpuploadPlugin') + def update_button_state(self, chat_control): + global jid_to_servers + global iq_ids_to_callbacks + global last_info_query + + 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)] + #pass + + # 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): + def __init__(self, plugin, chat_control): + self.dlg = None + self.dialog_type = 'file' + self.keypress_id = chat_control.msg_textview.connect('mykeypress', + self.on_key_press) + self.plugin = plugin + 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('relief', gtk.RELIEF_NONE) + self.button.set_property('can-focus', False) + self.button.set_sensitive(False) + 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.image_button = gtk.Button(label=None, stock=None, use_underline=True) + self.image_button.set_property('relief', gtk.RELIEF_NONE) + self.image_button.set_property('can-focus', False) + 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')) + send_button = chat_control.xml.get_object('send_button') + send_button_pos = actions_hbox.child_get_property(send_button, + 'position') + actions_hbox.add_with_properties(self.button, 'position', + send_button_pos - 2, 'expand', False) + + actions_hbox.add_with_properties(self.image_button, 'position', + send_button_pos - 1, 'expand', False) + + 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 + chat_control.handlers[self.keypress_id] = chat_control.msg_textview + self.button.show() + self.image_button.show() + + def on_key_press(self, widget, event_keyval, event_keymod): + # construct event instance from binding + event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here + event.keyval = event_keyval + event.state = event_keymod + event.time = 0 # assign current time + + if event.keyval != gtk.keysyms.u: + return + if event.state != gtk.gdk.MOD1_MASK: # ALT+u + return + is_supported = gajim.get_jid_from_account(self.chat_control.account) in jid_to_servers and \ + gajim.connections[self.chat_control.account].connection != None + if not is_supported: + from dialogs import WarningDialog + WarningDialog('Warning', _('Your server does not support http uploads'), + transient_for=self.chat_control.parent_win.window) + return + self.on_file_button_clicked(widget) + + 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) + if self.keypress_id in self.chat_control.handlers and \ + self.chat_control.handlers[self.keypress_id].handler_is_connected(self.keypress_id): + self.chat_control.handlers[self.keypress_id].disconnect(self.keypress_id) + del self.chat_control.handlers[self.keypress_id] + + def encryption_activated(self): + 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') + return False + + def on_file_dialog_ok(self, widget, path_to_file=None): + global jid_to_servers + + 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 + path_to_file = gtkgui_helpers.decode_filechooser_file_paths( + (path_to_file,))[0] + self.dlg.destroy() + if not os.path.exists(path_to_file): + 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 stat[6] == 0: + invalid_file = True + msg = _('File is empty') + else: + 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) + 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) + def upload_file(stanza): + slot = stanza.getTag("slot") + if not slot: + progress_window.close_dialog() + log.error("got unexpected stanza: "+str(stanza)) + 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: + 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: + 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") + get = slot.getTag("get") + if not put or not get: + progress_window.close_dialog() + log.error("got unexpected stanza: " + str(stanza)) + 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): + if response_code == 0: + return # Upload was aborted + if response_code >= 200 and 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 = None + quality_steps = (100, 80, 60, 50, 40, 35, 30, 25, 23, 20, 18, 15, 13, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) + with open(path_to_file, 'rb') as content_file: + thumb = urllib2.quote(base64.standard_b64encode(content_file.read()), '') + if thumb and len(thumb) < max_thumbnail_size: + quality = 100 + log.info("Image small enough (%d bytes), not resampling" % len(thumb)) + elif pil_available: + log.info("PIL available, using it for image downsampling") + try: + for quality in quality_steps: + thumb = Image.open(path_to_file) + thumb.thumbnail((max_thumbnail_dimension, max_thumbnail_dimension), Image.ANTIALIAS) + output = BytesIO() + thumb.save(output, format='JPEG', quality=quality, optimize=True) + thumb = output.getvalue() + output.close() + thumb = urllib2.quote(base64.standard_b64encode(thumb), '') + log.debug("pil thumbnail jpeg quality %d produces an image of size %d..." % (quality, len(thumb))) + if len(thumb) < max_thumbnail_size: + break + except: + thumb = None + else: + thumb = None + if not thumb: + log.info("PIL not available, using GTK for image downsampling") + temp_file = None + try: + with open(path_to_file, 'rb') as content_file: + thumb = content_file.read() + loader = gtk.gdk.PixbufLoader() + loader.write(thumb) + loader.close() + pixbuf = loader.get_pixbuf() + scaled_pb = self.get_pixbuf_of_size(pixbuf, max_thumbnail_dimension) + handle, temp_file = tempfile.mkstemp(suffix='.jpeg', prefix='gajim_httpupload_scaled_tmp', dir=gajim.TMP) + log.debug("Saving temporary jpeg image to '%s'..." % temp_file) + os.close(handle) + for quality in quality_steps: + scaled_pb.save(temp_file, "jpeg", {"quality": str(quality)}) + with open(temp_file, 'rb') as content_file: + thumb = content_file.read() + thumb = urllib2.quote(base64.standard_b64encode(thumb), '') + log.debug("gtk thumbnail jpeg quality %d produces an image of size %d..." % (quality, len(thumb))) + if len(thumb) < max_thumbnail_size: + break + except: + thumb = None + finally: + if temp_file: + os.unlink(temp_file) + if thumb: + if len(thumb) > max_thumbnail_size: + log.info("Couldn't compress image enough, not sending any thumbnail") + else: + log.info("Using thumbnail jpeg quality %d (image size: %d bytes)" % (quality, len(thumb))) + xhtml = '
%s ' % \ + (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 uploader(): + progress_messages.put(_('Uploading file via HTTP...')) + try: + headers = {'User-Agent': 'Gajim %s' % gajim.version, + 'Content-Type': mime_type} + request = urllib2.Request(put.getData().encode("utf-8"), data=data, headers=headers) + request.get_method = lambda: 'PUT' + log.debug("opening urllib2 upload request...") + transfer = urllib2.urlopen(request, timeout=30) + log.debug("urllib2 upload request done, response code: " + str(transfer.getcode())) + return transfer.getcode() + except UploadAbortedException: + log.info("Upload aborted") + except: + 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) + raise # fill error log with useful information + 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() + 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) + + # 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) + + self.chat_control.msg_textview.grab_focus() + + 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.FILE_CHOOSER_ACTION_OPEN, + buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK), + default_response = gtk.RESPONSE_OK,) + + 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) + + def get_pixbuf_of_size(self, pixbuf, size): + # Creates a pixbuf that fits in the specified square of sizexsize + # while preserving the aspect ratio + # Returns scaled_pixbuf + image_width = pixbuf.get_width() + image_height = pixbuf.get_height() + + if image_width > image_height: + if image_width > size: + image_height = int(size / float(image_width) * image_height) + image_width = int(size) + else: + if image_height > size: + image_width = int(size / float(image_height) * image_width) + image_height = int(size) + + crop_pixbuf = pixbuf.scale_simple(image_width, image_height, + gtk.gdk.INTERP_BILINEAR) + return crop_pixbuf + + +class StreamFileWithProgress(file): + def __init__(self, path, mode, callback=None, + encrypted_upload=False, key=None, iv=None, *args): + file.__init__(self, path, mode) + self.encrypted_upload = encrypted_upload + self.seek(0, os.SEEK_END) + if self.encrypted_upload: + if os.name == 'nt': + self.backend = backend + else: + self.backend = default_backend() + self.encryptor = Cipher( + algorithms.AES(key), + GCM(iv), + backend=self.backend).encryptor() + self._total = self.tell() + TAGSIZE + else: + self._total = self.tell() + self.seek(0) + self._callback = callback + self._args = args + self._seen = 0 + + def __len__(self): + return self._total + + def read(self, size): + if self.encrypted_upload: + data = file.read(self, size) + if len(data) > 0: + data = self.encryptor.update(data) + self._seen += len(data) + if (self._seen + TAGSIZE) == self._total: + self.encryptor.finalize() + data += self.encryptor.tag + self._seen += TAGSIZE + if self._callback: + self._callback(self._seen, self._total, *self._args) + return data + else: + data = file.read(self, size) + self._seen += len(data) + if self._callback: + self._callback(self._seen, self._total, *self._args) + return data + + +class ProgressWindow: + def __init__(self, title_text, during_text, messages_queue, plugin): + 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.dialog = self.xml.get_object('progress_dialog') + self.label = self.xml.get_object('label') + self.cancel_button = self.xml.get_object('close_button') + self.label.set_markup('' + during_text + '') + 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 = gobject.timeout_add(100, self.pulse_progressbar) + self.process_messages_queue_timeout_id = gobject.timeout_add(100, self.process_messages_queue) + + + def pulse_progressbar(self): + if self.dialog: + self.progressbar.pulse() + return True # loop forever + 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: + gobject.source_remove(self.pulse_progressbar_timeout_id) + gobject.source_remove(self.process_messages_queue_timeout_id) + + def on_cancel(self, widget): + self.stopped = True + if self.pulse_progressbar_timeout_id: + gobject.source_remove(self.pulse_progressbar_timeout_id) + gobject.source_remove(self.process_messages_queue_timeout_id) + self.dialog.destroy() + + def update_progress(self, seen, total): + if self.stopped == True: + raise UploadAbortedException + if self.pulse_progressbar_timeout_id: + gobject.source_remove(self.pulse_progressbar_timeout_id) + self.pulse_progressbar_timeout_id = 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) + +class UploadAbortedException(Exception): + def __str__(self): + return "Upload Aborted" diff --git a/gajim-plugin/image.png b/gajim-plugin/image.png new file mode 100644 index 0000000..32dda52 Binary files /dev/null and b/gajim-plugin/image.png differ diff --git a/gajim-plugin/manifest.ini b/gajim-plugin/manifest.ini new file mode 100644 index 0000000..57f5d37 --- /dev/null +++ b/gajim-plugin/manifest.ini @@ -0,0 +1,15 @@ +[info] +name: HttpUpload +short_name: httpupload +version: 0.4.1 +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 +homepage: https://trac-plugins.gajim.org/wiki/HttpUploadPlugin +min_gajim_version: 0.16.5 +max_gajim_version: 0.16.9 \ No newline at end of file diff --git a/gajim-plugin/upload_progress_dialog.ui b/gajim-plugin/upload_progress_dialog.ui new file mode 100644 index 0000000..b50c3c2 --- /dev/null +++ b/gajim-plugin/upload_progress_dialog.ui @@ -0,0 +1,105 @@ + + + + + + True + False + upload-media + dialog + + + + True + False + vertical + 6 + + + True + False + end + + + True + False + 2 + 4 + 0 + 3 + + + gtk-cancel + True + True + True + False + True + + + + + + False + True + 2 + + + + + False + False + end + 0 + + + + + True + False + 8 + 4 + 8 + 8 + + + True + False + 0 + True + + + + + False + True + 1 + + + + + True + False + 4 + 4 + 8 + 8 + + + True + False + 0.10000000149 + True + + + + + False + True + 2 + + + + + + -- cgit v1.2.3 From 858e615f1d1dabd1960068296a9f96fb6d9b7ce6 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Sun, 9 Oct 2016 11:48:27 +0200 Subject: Add support for httpuploadim message hint --- gajim-plugin/httpupload.py | 23 ++++++++++++++++++++++- gajim-plugin/manifest.ini | 5 +++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/gajim-plugin/httpupload.py b/gajim-plugin/httpupload.py index e845e3d..b2bb3fc 100644 --- a/gajim-plugin/httpupload.py +++ b/gajim-plugin/httpupload.py @@ -64,6 +64,7 @@ except Exception as e: log.info('Decryption/Encryption disabled due to errors') encryption_available = False +NS_HINTS = 'urn:xmpp:hints' # XEP-0363 (http://xmpp.org/extensions/xep-0363.html) NS_HTTPUPLOAD = 'urn:xmpp:http:upload' TAGSIZE = 16 @@ -73,6 +74,7 @@ iq_ids_to_callbacks = {} last_info_query = {} max_thumbnail_size = 2048 max_thumbnail_dimension = 160 +httpuploadurls = {} class HttpuploadPlugin(GajimPlugin): @@ -88,6 +90,8 @@ class HttpuploadPlugin(GajimPlugin): self.handle_agent_info_received) self.events_handlers['raw-iq-received'] = (ged.PRECORE, self.handle_iq_received) + self.events_handlers['stanza-message-outgoing'] = (ged.PRECORE, + self.handle_message_stanza_out) self.gui_extension_points = { 'chat_control_base': (self.connect_with_chat_control, self.disconnect_from_chat_control), @@ -116,6 +120,20 @@ class HttpuploadPlugin(GajimPlugin): for base in self.controls: self.update_button_state(base.chat_control) + @log_calls('HttpuploadPlugin') + def handle_message_stanza_out(self, event): + try: + global httpuploadurls + if not event.msg_iq.getTag('body'): + return + url = event.msg_iq.getBody() + if url in httpuploadurls: + # httpupload Hint + event.msg_iq.addChild('httpupload', namespace=NS_HINTS) + del httpuploadurls[url] + except Exception as e: + log.error(e) + @log_calls('HttpuploadPlugin') def connect_with_chat_control(self, control): self.chat_control = control @@ -458,7 +476,10 @@ class Base(object): 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) + global httpuploadurls + url = get.getData() + httpuploadurls[url] = True + self.chat_control.send_message(message=url, xhtml=xhtml) self.chat_control.msg_textview.grab_focus() else: progress_window.close_dialog() diff --git a/gajim-plugin/manifest.ini b/gajim-plugin/manifest.ini index 57f5d37..fa21628 100644 --- a/gajim-plugin/manifest.ini +++ b/gajim-plugin/manifest.ini @@ -1,15 +1,16 @@ [info] name: HttpUpload short_name: httpupload -version: 0.4.1 +version: 0.4.1-thedevstack 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. + This plugin adds additionally to this a hint for processing: authors: Thilo Molitor Philipp Hörist homepage: https://trac-plugins.gajim.org/wiki/HttpUploadPlugin min_gajim_version: 0.16.5 -max_gajim_version: 0.16.9 \ No newline at end of file +max_gajim_version: 0.16.9 -- cgit v1.2.3 From 62e463bdd57e03c3e51c9db2160e87b39615c7c9 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Sun, 9 Oct 2016 12:09:49 +0200 Subject: moved gajim plugin to the subfolder 'gajim-plugin/httpupload' --- gajim-plugin/__init__.py | 2 - gajim-plugin/httpupload.png | Bin 780 -> 0 bytes gajim-plugin/httpupload.py | 702 ---------------------- gajim-plugin/httpupload/__init__.py | 2 + gajim-plugin/httpupload/httpupload.png | Bin 0 -> 780 bytes gajim-plugin/httpupload/httpupload.py | 702 ++++++++++++++++++++++ gajim-plugin/httpupload/image.png | Bin 0 -> 666 bytes gajim-plugin/httpupload/manifest.ini | 16 + gajim-plugin/httpupload/upload_progress_dialog.ui | 105 ++++ gajim-plugin/image.png | Bin 666 -> 0 bytes gajim-plugin/manifest.ini | 16 - gajim-plugin/upload_progress_dialog.ui | 105 ---- 12 files changed, 825 insertions(+), 825 deletions(-) delete mode 100644 gajim-plugin/__init__.py delete mode 100644 gajim-plugin/httpupload.png delete mode 100644 gajim-plugin/httpupload.py create mode 100644 gajim-plugin/httpupload/__init__.py create mode 100644 gajim-plugin/httpupload/httpupload.png create mode 100644 gajim-plugin/httpupload/httpupload.py create mode 100644 gajim-plugin/httpupload/image.png create mode 100644 gajim-plugin/httpupload/manifest.ini create mode 100644 gajim-plugin/httpupload/upload_progress_dialog.ui delete mode 100644 gajim-plugin/image.png delete mode 100644 gajim-plugin/manifest.ini delete mode 100644 gajim-plugin/upload_progress_dialog.ui diff --git a/gajim-plugin/__init__.py b/gajim-plugin/__init__.py deleted file mode 100644 index 23c0eff..0000000 --- a/gajim-plugin/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# simple redirect -from httpupload import HttpuploadPlugin diff --git a/gajim-plugin/httpupload.png b/gajim-plugin/httpupload.png deleted file mode 100644 index c3b3733..0000000 Binary files a/gajim-plugin/httpupload.png and /dev/null differ diff --git a/gajim-plugin/httpupload.py b/gajim-plugin/httpupload.py deleted file mode 100644 index b2bb3fc..0000000 --- a/gajim-plugin/httpupload.py +++ /dev/null @@ -1,702 +0,0 @@ -# -*- coding: utf-8 -*- -## -## This file is part of Gajim. -## -## Gajim is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published -## by the Free Software Foundation; version 3 only. -## -## Gajim is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with Gajim. If not, see . -## - -from common import demandimport -demandimport.enable() -demandimport.ignore += ['builtins', '__builtin__', 'PIL', '_imp'] - -import gtk -import gobject -import os -import time -import base64 -import tempfile -import urllib2 -import mimetypes # better use the magic packet, but that's not a standard lib -import gtkgui_helpers -from Queue import Queue -try: - from PIL import Image - pil_available = True -except: - pil_available = False -from io import BytesIO - -import binascii -from common import gajim -from common import ged -import chat_control -from plugins import GajimPlugin -from plugins.helpers import log_calls -import logging -from dialogs import FileChooserDialog, ImageChooserDialog, ErrorDialog -import nbxmpp - -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.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: - DEP_MSG = 'For encryption of files, ' \ - 'please install python-cryptography!' - log.debug('Cryptography Import Error: ' + str(e)) - log.info('Decryption/Encryption disabled due to errors') - encryption_available = False - -NS_HINTS = 'urn:xmpp:hints' -# XEP-0363 (http://xmpp.org/extensions/xep-0363.html) -NS_HTTPUPLOAD = 'urn:xmpp:http:upload' -TAGSIZE = 16 - -jid_to_servers = {} -iq_ids_to_callbacks = {} -last_info_query = {} -max_thumbnail_size = 2048 -max_thumbnail_dimension = 160 -httpuploadurls = {} - - -class HttpuploadPlugin(GajimPlugin): - - @log_calls('HttpuploadPlugin') - def init(self): - if not encryption_available: - self.available_text = DEP_MSG - self.config_dialog = None # HttpuploadPluginConfigDialog(self) - self.controls = [] - 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['stanza-message-outgoing'] = (ged.PRECORE, - self.handle_message_stanza_out) - 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 - - def handle_iq_received(self, event): - global iq_ids_to_callbacks - id_ = event.stanza.getAttr("id") - if str(id_) in iq_ids_to_callbacks: - try: - iq_ids_to_callbacks[str(id_)](event.stanza) - except: - raise - finally: - del iq_ids_to_callbacks[str(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) - - @log_calls('HttpuploadPlugin') - def handle_message_stanza_out(self, event): - try: - global httpuploadurls - if not event.msg_iq.getTag('body'): - return - url = event.msg_iq.getBody() - if url in httpuploadurls: - # httpupload Hint - event.msg_iq.addChild('httpupload', namespace=NS_HINTS) - del httpuploadurls[url] - except Exception as e: - log.error(e) - - @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: - # ALT + U - gtk.binding_entry_add_signal(control.msg_textview, - gtk.keysyms.u, gtk.gdk.MOD1_MASK, 'mykeypress', - int, gtk.keysyms.u, gtk.gdk.ModifierType, gtk.gdk.MOD1_MASK) - self.first_run = False - self.update_button_state(self.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 = [] - - @log_calls('HttpuploadPlugin') - def update_button_state(self, chat_control): - global jid_to_servers - global iq_ids_to_callbacks - global last_info_query - - 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)] - #pass - - # 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): - def __init__(self, plugin, chat_control): - self.dlg = None - self.dialog_type = 'file' - self.keypress_id = chat_control.msg_textview.connect('mykeypress', - self.on_key_press) - self.plugin = plugin - 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('relief', gtk.RELIEF_NONE) - self.button.set_property('can-focus', False) - self.button.set_sensitive(False) - 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.image_button = gtk.Button(label=None, stock=None, use_underline=True) - self.image_button.set_property('relief', gtk.RELIEF_NONE) - self.image_button.set_property('can-focus', False) - 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')) - send_button = chat_control.xml.get_object('send_button') - send_button_pos = actions_hbox.child_get_property(send_button, - 'position') - actions_hbox.add_with_properties(self.button, 'position', - send_button_pos - 2, 'expand', False) - - actions_hbox.add_with_properties(self.image_button, 'position', - send_button_pos - 1, 'expand', False) - - 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 - chat_control.handlers[self.keypress_id] = chat_control.msg_textview - self.button.show() - self.image_button.show() - - def on_key_press(self, widget, event_keyval, event_keymod): - # construct event instance from binding - event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here - event.keyval = event_keyval - event.state = event_keymod - event.time = 0 # assign current time - - if event.keyval != gtk.keysyms.u: - return - if event.state != gtk.gdk.MOD1_MASK: # ALT+u - return - is_supported = gajim.get_jid_from_account(self.chat_control.account) in jid_to_servers and \ - gajim.connections[self.chat_control.account].connection != None - if not is_supported: - from dialogs import WarningDialog - WarningDialog('Warning', _('Your server does not support http uploads'), - transient_for=self.chat_control.parent_win.window) - return - self.on_file_button_clicked(widget) - - 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) - if self.keypress_id in self.chat_control.handlers and \ - self.chat_control.handlers[self.keypress_id].handler_is_connected(self.keypress_id): - self.chat_control.handlers[self.keypress_id].disconnect(self.keypress_id) - del self.chat_control.handlers[self.keypress_id] - - def encryption_activated(self): - 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') - return False - - def on_file_dialog_ok(self, widget, path_to_file=None): - global jid_to_servers - - 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 - path_to_file = gtkgui_helpers.decode_filechooser_file_paths( - (path_to_file,))[0] - self.dlg.destroy() - if not os.path.exists(path_to_file): - 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 stat[6] == 0: - invalid_file = True - msg = _('File is empty') - else: - 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) - 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) - def upload_file(stanza): - slot = stanza.getTag("slot") - if not slot: - progress_window.close_dialog() - log.error("got unexpected stanza: "+str(stanza)) - 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: - 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: - 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") - get = slot.getTag("get") - if not put or not get: - progress_window.close_dialog() - log.error("got unexpected stanza: " + str(stanza)) - 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): - if response_code == 0: - return # Upload was aborted - if response_code >= 200 and 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 = None - quality_steps = (100, 80, 60, 50, 40, 35, 30, 25, 23, 20, 18, 15, 13, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) - with open(path_to_file, 'rb') as content_file: - thumb = urllib2.quote(base64.standard_b64encode(content_file.read()), '') - if thumb and len(thumb) < max_thumbnail_size: - quality = 100 - log.info("Image small enough (%d bytes), not resampling" % len(thumb)) - elif pil_available: - log.info("PIL available, using it for image downsampling") - try: - for quality in quality_steps: - thumb = Image.open(path_to_file) - thumb.thumbnail((max_thumbnail_dimension, max_thumbnail_dimension), Image.ANTIALIAS) - output = BytesIO() - thumb.save(output, format='JPEG', quality=quality, optimize=True) - thumb = output.getvalue() - output.close() - thumb = urllib2.quote(base64.standard_b64encode(thumb), '') - log.debug("pil thumbnail jpeg quality %d produces an image of size %d..." % (quality, len(thumb))) - if len(thumb) < max_thumbnail_size: - break - except: - thumb = None - else: - thumb = None - if not thumb: - log.info("PIL not available, using GTK for image downsampling") - temp_file = None - try: - with open(path_to_file, 'rb') as content_file: - thumb = content_file.read() - loader = gtk.gdk.PixbufLoader() - loader.write(thumb) - loader.close() - pixbuf = loader.get_pixbuf() - scaled_pb = self.get_pixbuf_of_size(pixbuf, max_thumbnail_dimension) - handle, temp_file = tempfile.mkstemp(suffix='.jpeg', prefix='gajim_httpupload_scaled_tmp', dir=gajim.TMP) - log.debug("Saving temporary jpeg image to '%s'..." % temp_file) - os.close(handle) - for quality in quality_steps: - scaled_pb.save(temp_file, "jpeg", {"quality": str(quality)}) - with open(temp_file, 'rb') as content_file: - thumb = content_file.read() - thumb = urllib2.quote(base64.standard_b64encode(thumb), '') - log.debug("gtk thumbnail jpeg quality %d produces an image of size %d..." % (quality, len(thumb))) - if len(thumb) < max_thumbnail_size: - break - except: - thumb = None - finally: - if temp_file: - os.unlink(temp_file) - if thumb: - if len(thumb) > max_thumbnail_size: - log.info("Couldn't compress image enough, not sending any thumbnail") - else: - log.info("Using thumbnail jpeg quality %d (image size: %d bytes)" % (quality, len(thumb))) - xhtml = '
%s ' % \ - (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: - global httpuploadurls - url = get.getData() - httpuploadurls[url] = True - self.chat_control.send_message(message=url, 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 uploader(): - progress_messages.put(_('Uploading file via HTTP...')) - try: - headers = {'User-Agent': 'Gajim %s' % gajim.version, - 'Content-Type': mime_type} - request = urllib2.Request(put.getData().encode("utf-8"), data=data, headers=headers) - request.get_method = lambda: 'PUT' - log.debug("opening urllib2 upload request...") - transfer = urllib2.urlopen(request, timeout=30) - log.debug("urllib2 upload request done, response code: " + str(transfer.getcode())) - return transfer.getcode() - except UploadAbortedException: - log.info("Upload aborted") - except: - 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) - raise # fill error log with useful information - 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() - 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) - - # 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) - - self.chat_control.msg_textview.grab_focus() - - 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.FILE_CHOOSER_ACTION_OPEN, - buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK), - default_response = gtk.RESPONSE_OK,) - - 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) - - def get_pixbuf_of_size(self, pixbuf, size): - # Creates a pixbuf that fits in the specified square of sizexsize - # while preserving the aspect ratio - # Returns scaled_pixbuf - image_width = pixbuf.get_width() - image_height = pixbuf.get_height() - - if image_width > image_height: - if image_width > size: - image_height = int(size / float(image_width) * image_height) - image_width = int(size) - else: - if image_height > size: - image_width = int(size / float(image_height) * image_width) - image_height = int(size) - - crop_pixbuf = pixbuf.scale_simple(image_width, image_height, - gtk.gdk.INTERP_BILINEAR) - return crop_pixbuf - - -class StreamFileWithProgress(file): - def __init__(self, path, mode, callback=None, - encrypted_upload=False, key=None, iv=None, *args): - file.__init__(self, path, mode) - self.encrypted_upload = encrypted_upload - self.seek(0, os.SEEK_END) - if self.encrypted_upload: - if os.name == 'nt': - self.backend = backend - else: - self.backend = default_backend() - self.encryptor = Cipher( - algorithms.AES(key), - GCM(iv), - backend=self.backend).encryptor() - self._total = self.tell() + TAGSIZE - else: - self._total = self.tell() - self.seek(0) - self._callback = callback - self._args = args - self._seen = 0 - - def __len__(self): - return self._total - - def read(self, size): - if self.encrypted_upload: - data = file.read(self, size) - if len(data) > 0: - data = self.encryptor.update(data) - self._seen += len(data) - if (self._seen + TAGSIZE) == self._total: - self.encryptor.finalize() - data += self.encryptor.tag - self._seen += TAGSIZE - if self._callback: - self._callback(self._seen, self._total, *self._args) - return data - else: - data = file.read(self, size) - self._seen += len(data) - if self._callback: - self._callback(self._seen, self._total, *self._args) - return data - - -class ProgressWindow: - def __init__(self, title_text, during_text, messages_queue, plugin): - 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.dialog = self.xml.get_object('progress_dialog') - self.label = self.xml.get_object('label') - self.cancel_button = self.xml.get_object('close_button') - self.label.set_markup('' + during_text + '') - 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 = gobject.timeout_add(100, self.pulse_progressbar) - self.process_messages_queue_timeout_id = gobject.timeout_add(100, self.process_messages_queue) - - - def pulse_progressbar(self): - if self.dialog: - self.progressbar.pulse() - return True # loop forever - 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: - gobject.source_remove(self.pulse_progressbar_timeout_id) - gobject.source_remove(self.process_messages_queue_timeout_id) - - def on_cancel(self, widget): - self.stopped = True - if self.pulse_progressbar_timeout_id: - gobject.source_remove(self.pulse_progressbar_timeout_id) - gobject.source_remove(self.process_messages_queue_timeout_id) - self.dialog.destroy() - - def update_progress(self, seen, total): - if self.stopped == True: - raise UploadAbortedException - if self.pulse_progressbar_timeout_id: - gobject.source_remove(self.pulse_progressbar_timeout_id) - self.pulse_progressbar_timeout_id = 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) - -class UploadAbortedException(Exception): - def __str__(self): - return "Upload Aborted" diff --git a/gajim-plugin/httpupload/__init__.py b/gajim-plugin/httpupload/__init__.py new file mode 100644 index 0000000..23c0eff --- /dev/null +++ b/gajim-plugin/httpupload/__init__.py @@ -0,0 +1,2 @@ +# simple redirect +from httpupload import HttpuploadPlugin diff --git a/gajim-plugin/httpupload/httpupload.png b/gajim-plugin/httpupload/httpupload.png new file mode 100644 index 0000000..c3b3733 Binary files /dev/null and b/gajim-plugin/httpupload/httpupload.png differ diff --git a/gajim-plugin/httpupload/httpupload.py b/gajim-plugin/httpupload/httpupload.py new file mode 100644 index 0000000..b2bb3fc --- /dev/null +++ b/gajim-plugin/httpupload/httpupload.py @@ -0,0 +1,702 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Gajim. +## +## Gajim is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## Gajim is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see . +## + +from common import demandimport +demandimport.enable() +demandimport.ignore += ['builtins', '__builtin__', 'PIL', '_imp'] + +import gtk +import gobject +import os +import time +import base64 +import tempfile +import urllib2 +import mimetypes # better use the magic packet, but that's not a standard lib +import gtkgui_helpers +from Queue import Queue +try: + from PIL import Image + pil_available = True +except: + pil_available = False +from io import BytesIO + +import binascii +from common import gajim +from common import ged +import chat_control +from plugins import GajimPlugin +from plugins.helpers import log_calls +import logging +from dialogs import FileChooserDialog, ImageChooserDialog, ErrorDialog +import nbxmpp + +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.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: + DEP_MSG = 'For encryption of files, ' \ + 'please install python-cryptography!' + log.debug('Cryptography Import Error: ' + str(e)) + log.info('Decryption/Encryption disabled due to errors') + encryption_available = False + +NS_HINTS = 'urn:xmpp:hints' +# XEP-0363 (http://xmpp.org/extensions/xep-0363.html) +NS_HTTPUPLOAD = 'urn:xmpp:http:upload' +TAGSIZE = 16 + +jid_to_servers = {} +iq_ids_to_callbacks = {} +last_info_query = {} +max_thumbnail_size = 2048 +max_thumbnail_dimension = 160 +httpuploadurls = {} + + +class HttpuploadPlugin(GajimPlugin): + + @log_calls('HttpuploadPlugin') + def init(self): + if not encryption_available: + self.available_text = DEP_MSG + self.config_dialog = None # HttpuploadPluginConfigDialog(self) + self.controls = [] + 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['stanza-message-outgoing'] = (ged.PRECORE, + self.handle_message_stanza_out) + 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 + + def handle_iq_received(self, event): + global iq_ids_to_callbacks + id_ = event.stanza.getAttr("id") + if str(id_) in iq_ids_to_callbacks: + try: + iq_ids_to_callbacks[str(id_)](event.stanza) + except: + raise + finally: + del iq_ids_to_callbacks[str(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) + + @log_calls('HttpuploadPlugin') + def handle_message_stanza_out(self, event): + try: + global httpuploadurls + if not event.msg_iq.getTag('body'): + return + url = event.msg_iq.getBody() + if url in httpuploadurls: + # httpupload Hint + event.msg_iq.addChild('httpupload', namespace=NS_HINTS) + del httpuploadurls[url] + except Exception as e: + log.error(e) + + @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: + # ALT + U + gtk.binding_entry_add_signal(control.msg_textview, + gtk.keysyms.u, gtk.gdk.MOD1_MASK, 'mykeypress', + int, gtk.keysyms.u, gtk.gdk.ModifierType, gtk.gdk.MOD1_MASK) + self.first_run = False + self.update_button_state(self.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 = [] + + @log_calls('HttpuploadPlugin') + def update_button_state(self, chat_control): + global jid_to_servers + global iq_ids_to_callbacks + global last_info_query + + 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)] + #pass + + # 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): + def __init__(self, plugin, chat_control): + self.dlg = None + self.dialog_type = 'file' + self.keypress_id = chat_control.msg_textview.connect('mykeypress', + self.on_key_press) + self.plugin = plugin + 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('relief', gtk.RELIEF_NONE) + self.button.set_property('can-focus', False) + self.button.set_sensitive(False) + 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.image_button = gtk.Button(label=None, stock=None, use_underline=True) + self.image_button.set_property('relief', gtk.RELIEF_NONE) + self.image_button.set_property('can-focus', False) + 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')) + send_button = chat_control.xml.get_object('send_button') + send_button_pos = actions_hbox.child_get_property(send_button, + 'position') + actions_hbox.add_with_properties(self.button, 'position', + send_button_pos - 2, 'expand', False) + + actions_hbox.add_with_properties(self.image_button, 'position', + send_button_pos - 1, 'expand', False) + + 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 + chat_control.handlers[self.keypress_id] = chat_control.msg_textview + self.button.show() + self.image_button.show() + + def on_key_press(self, widget, event_keyval, event_keymod): + # construct event instance from binding + event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here + event.keyval = event_keyval + event.state = event_keymod + event.time = 0 # assign current time + + if event.keyval != gtk.keysyms.u: + return + if event.state != gtk.gdk.MOD1_MASK: # ALT+u + return + is_supported = gajim.get_jid_from_account(self.chat_control.account) in jid_to_servers and \ + gajim.connections[self.chat_control.account].connection != None + if not is_supported: + from dialogs import WarningDialog + WarningDialog('Warning', _('Your server does not support http uploads'), + transient_for=self.chat_control.parent_win.window) + return + self.on_file_button_clicked(widget) + + 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) + if self.keypress_id in self.chat_control.handlers and \ + self.chat_control.handlers[self.keypress_id].handler_is_connected(self.keypress_id): + self.chat_control.handlers[self.keypress_id].disconnect(self.keypress_id) + del self.chat_control.handlers[self.keypress_id] + + def encryption_activated(self): + 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') + return False + + def on_file_dialog_ok(self, widget, path_to_file=None): + global jid_to_servers + + 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 + path_to_file = gtkgui_helpers.decode_filechooser_file_paths( + (path_to_file,))[0] + self.dlg.destroy() + if not os.path.exists(path_to_file): + 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 stat[6] == 0: + invalid_file = True + msg = _('File is empty') + else: + 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) + 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) + def upload_file(stanza): + slot = stanza.getTag("slot") + if not slot: + progress_window.close_dialog() + log.error("got unexpected stanza: "+str(stanza)) + 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: + 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: + 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") + get = slot.getTag("get") + if not put or not get: + progress_window.close_dialog() + log.error("got unexpected stanza: " + str(stanza)) + 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): + if response_code == 0: + return # Upload was aborted + if response_code >= 200 and 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 = None + quality_steps = (100, 80, 60, 50, 40, 35, 30, 25, 23, 20, 18, 15, 13, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) + with open(path_to_file, 'rb') as content_file: + thumb = urllib2.quote(base64.standard_b64encode(content_file.read()), '') + if thumb and len(thumb) < max_thumbnail_size: + quality = 100 + log.info("Image small enough (%d bytes), not resampling" % len(thumb)) + elif pil_available: + log.info("PIL available, using it for image downsampling") + try: + for quality in quality_steps: + thumb = Image.open(path_to_file) + thumb.thumbnail((max_thumbnail_dimension, max_thumbnail_dimension), Image.ANTIALIAS) + output = BytesIO() + thumb.save(output, format='JPEG', quality=quality, optimize=True) + thumb = output.getvalue() + output.close() + thumb = urllib2.quote(base64.standard_b64encode(thumb), '') + log.debug("pil thumbnail jpeg quality %d produces an image of size %d..." % (quality, len(thumb))) + if len(thumb) < max_thumbnail_size: + break + except: + thumb = None + else: + thumb = None + if not thumb: + log.info("PIL not available, using GTK for image downsampling") + temp_file = None + try: + with open(path_to_file, 'rb') as content_file: + thumb = content_file.read() + loader = gtk.gdk.PixbufLoader() + loader.write(thumb) + loader.close() + pixbuf = loader.get_pixbuf() + scaled_pb = self.get_pixbuf_of_size(pixbuf, max_thumbnail_dimension) + handle, temp_file = tempfile.mkstemp(suffix='.jpeg', prefix='gajim_httpupload_scaled_tmp', dir=gajim.TMP) + log.debug("Saving temporary jpeg image to '%s'..." % temp_file) + os.close(handle) + for quality in quality_steps: + scaled_pb.save(temp_file, "jpeg", {"quality": str(quality)}) + with open(temp_file, 'rb') as content_file: + thumb = content_file.read() + thumb = urllib2.quote(base64.standard_b64encode(thumb), '') + log.debug("gtk thumbnail jpeg quality %d produces an image of size %d..." % (quality, len(thumb))) + if len(thumb) < max_thumbnail_size: + break + except: + thumb = None + finally: + if temp_file: + os.unlink(temp_file) + if thumb: + if len(thumb) > max_thumbnail_size: + log.info("Couldn't compress image enough, not sending any thumbnail") + else: + log.info("Using thumbnail jpeg quality %d (image size: %d bytes)" % (quality, len(thumb))) + xhtml = '
%s ' % \ + (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: + global httpuploadurls + url = get.getData() + httpuploadurls[url] = True + self.chat_control.send_message(message=url, 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 uploader(): + progress_messages.put(_('Uploading file via HTTP...')) + try: + headers = {'User-Agent': 'Gajim %s' % gajim.version, + 'Content-Type': mime_type} + request = urllib2.Request(put.getData().encode("utf-8"), data=data, headers=headers) + request.get_method = lambda: 'PUT' + log.debug("opening urllib2 upload request...") + transfer = urllib2.urlopen(request, timeout=30) + log.debug("urllib2 upload request done, response code: " + str(transfer.getcode())) + return transfer.getcode() + except UploadAbortedException: + log.info("Upload aborted") + except: + 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) + raise # fill error log with useful information + 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() + 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) + + # 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) + + self.chat_control.msg_textview.grab_focus() + + 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.FILE_CHOOSER_ACTION_OPEN, + buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK), + default_response = gtk.RESPONSE_OK,) + + 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) + + def get_pixbuf_of_size(self, pixbuf, size): + # Creates a pixbuf that fits in the specified square of sizexsize + # while preserving the aspect ratio + # Returns scaled_pixbuf + image_width = pixbuf.get_width() + image_height = pixbuf.get_height() + + if image_width > image_height: + if image_width > size: + image_height = int(size / float(image_width) * image_height) + image_width = int(size) + else: + if image_height > size: + image_width = int(size / float(image_height) * image_width) + image_height = int(size) + + crop_pixbuf = pixbuf.scale_simple(image_width, image_height, + gtk.gdk.INTERP_BILINEAR) + return crop_pixbuf + + +class StreamFileWithProgress(file): + def __init__(self, path, mode, callback=None, + encrypted_upload=False, key=None, iv=None, *args): + file.__init__(self, path, mode) + self.encrypted_upload = encrypted_upload + self.seek(0, os.SEEK_END) + if self.encrypted_upload: + if os.name == 'nt': + self.backend = backend + else: + self.backend = default_backend() + self.encryptor = Cipher( + algorithms.AES(key), + GCM(iv), + backend=self.backend).encryptor() + self._total = self.tell() + TAGSIZE + else: + self._total = self.tell() + self.seek(0) + self._callback = callback + self._args = args + self._seen = 0 + + def __len__(self): + return self._total + + def read(self, size): + if self.encrypted_upload: + data = file.read(self, size) + if len(data) > 0: + data = self.encryptor.update(data) + self._seen += len(data) + if (self._seen + TAGSIZE) == self._total: + self.encryptor.finalize() + data += self.encryptor.tag + self._seen += TAGSIZE + if self._callback: + self._callback(self._seen, self._total, *self._args) + return data + else: + data = file.read(self, size) + self._seen += len(data) + if self._callback: + self._callback(self._seen, self._total, *self._args) + return data + + +class ProgressWindow: + def __init__(self, title_text, during_text, messages_queue, plugin): + 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.dialog = self.xml.get_object('progress_dialog') + self.label = self.xml.get_object('label') + self.cancel_button = self.xml.get_object('close_button') + self.label.set_markup('' + during_text + '') + 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 = gobject.timeout_add(100, self.pulse_progressbar) + self.process_messages_queue_timeout_id = gobject.timeout_add(100, self.process_messages_queue) + + + def pulse_progressbar(self): + if self.dialog: + self.progressbar.pulse() + return True # loop forever + 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: + gobject.source_remove(self.pulse_progressbar_timeout_id) + gobject.source_remove(self.process_messages_queue_timeout_id) + + def on_cancel(self, widget): + self.stopped = True + if self.pulse_progressbar_timeout_id: + gobject.source_remove(self.pulse_progressbar_timeout_id) + gobject.source_remove(self.process_messages_queue_timeout_id) + self.dialog.destroy() + + def update_progress(self, seen, total): + if self.stopped == True: + raise UploadAbortedException + if self.pulse_progressbar_timeout_id: + gobject.source_remove(self.pulse_progressbar_timeout_id) + self.pulse_progressbar_timeout_id = 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) + +class UploadAbortedException(Exception): + def __str__(self): + return "Upload Aborted" diff --git a/gajim-plugin/httpupload/image.png b/gajim-plugin/httpupload/image.png new file mode 100644 index 0000000..32dda52 Binary files /dev/null and b/gajim-plugin/httpupload/image.png differ diff --git a/gajim-plugin/httpupload/manifest.ini b/gajim-plugin/httpupload/manifest.ini new file mode 100644 index 0000000..fa21628 --- /dev/null +++ b/gajim-plugin/httpupload/manifest.ini @@ -0,0 +1,16 @@ +[info] +name: HttpUpload +short_name: httpupload +version: 0.4.1-thedevstack +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. + This plugin adds additionally to this a hint for processing: +authors: Thilo Molitor + Philipp Hörist +homepage: https://trac-plugins.gajim.org/wiki/HttpUploadPlugin +min_gajim_version: 0.16.5 +max_gajim_version: 0.16.9 diff --git a/gajim-plugin/httpupload/upload_progress_dialog.ui b/gajim-plugin/httpupload/upload_progress_dialog.ui new file mode 100644 index 0000000..b50c3c2 --- /dev/null +++ b/gajim-plugin/httpupload/upload_progress_dialog.ui @@ -0,0 +1,105 @@ + + + + + + True + False + upload-media + dialog + + + + True + False + vertical + 6 + + + True + False + end + + + True + False + 2 + 4 + 0 + 3 + + + gtk-cancel + True + True + True + False + True + + + + + + False + True + 2 + + + + + False + False + end + 0 + + + + + True + False + 8 + 4 + 8 + 8 + + + True + False + 0 + True + + + + + False + True + 1 + + + + + True + False + 4 + 4 + 8 + 8 + + + True + False + 0.10000000149 + True + + + + + False + True + 2 + + + + + + diff --git a/gajim-plugin/image.png b/gajim-plugin/image.png deleted file mode 100644 index 32dda52..0000000 Binary files a/gajim-plugin/image.png and /dev/null differ diff --git a/gajim-plugin/manifest.ini b/gajim-plugin/manifest.ini deleted file mode 100644 index fa21628..0000000 --- a/gajim-plugin/manifest.ini +++ /dev/null @@ -1,16 +0,0 @@ -[info] -name: HttpUpload -short_name: httpupload -version: 0.4.1-thedevstack -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. - This plugin adds additionally to this a hint for processing: -authors: Thilo Molitor - Philipp Hörist -homepage: https://trac-plugins.gajim.org/wiki/HttpUploadPlugin -min_gajim_version: 0.16.5 -max_gajim_version: 0.16.9 diff --git a/gajim-plugin/upload_progress_dialog.ui b/gajim-plugin/upload_progress_dialog.ui deleted file mode 100644 index b50c3c2..0000000 --- a/gajim-plugin/upload_progress_dialog.ui +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - True - False - upload-media - dialog - - - - True - False - vertical - 6 - - - True - False - end - - - True - False - 2 - 4 - 0 - 3 - - - gtk-cancel - True - True - True - False - True - - - - - - False - True - 2 - - - - - False - False - end - 0 - - - - - True - False - 8 - 4 - 8 - 8 - - - True - False - 0 - True - - - - - False - True - 1 - - - - - True - False - 4 - 4 - 8 - 8 - - - True - False - 0.10000000149 - True - - - - - False - True - 2 - - - - - - -- cgit v1.2.3 From 471b217ceb7bcf4a1db5eb3f825bb09b5f8d8345 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Tue, 2 May 2017 20:55:42 +0200 Subject: improved error handling in prosody module --- .../mod_http_upload_external.lua | 27 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/prosody-module/mod_http_upload_external/mod_http_upload_external.lua b/prosody-module/mod_http_upload_external/mod_http_upload_external.lua index 11175ee..23e356e 100644 --- a/prosody-module/mod_http_upload_external/mod_http_upload_external.lua +++ b/prosody-module/mod_http_upload_external/mod_http_upload_external.lua @@ -43,7 +43,12 @@ module:hook("iq/host/"..xmlns_http_upload..":request", function (event) return true; end local slot_type = request.attr.type; - module:log("debug", "incoming request is of type " .. slot_type); + if slot_type then + module:log("debug", "incoming request is of type " .. slot_type); + else + module:log("debug", "incoming request has no type - using default type 'upload'"); + end + if not slot_type or slot_type == "upload" then -- validate local filename = request:get_child_text("filename"); @@ -67,7 +72,10 @@ module:hook("iq/host/"..xmlns_http_upload..":request", function (event) -- the request local respbody, statuscode = http.request(external_url, reqbody); - respbody = string.gsub(respbody, "\\/", "/") + -- respbody is nil in case the server is not reachable + if respbody ~= nil then + respbody = string.gsub(respbody, "\\/", "/"); + end local get_url = nil; local put_url = nil; @@ -127,9 +135,12 @@ module:hook("iq/host/"..xmlns_http_upload..":request", function (event) return true; end end - else + elseif respbody ~= nil then origin.send(st.error_reply(stanza, "cancel", "undefined-condition", "status code: " .. statuscode .. " response: " ..respbody)); return true; + else + -- http file service not reachable + origin.send(st.error_reply(stanza, "cancel", "undefined-condition", "status code: " .. statuscode)); end local reply = st.reply(stanza); @@ -148,7 +159,10 @@ module:hook("iq/host/"..xmlns_http_upload..":request", function (event) local reqbody = "xmpp_server_key=" .. xmpp_server_key .. "&slot_type=delete&file_url=" .. fileurl .. "&user_jid=" .. orig_from; -- the request local respbody, statuscode = http.request(external_url, reqbody); - respbody = string.gsub(respbody, "\\/", "/") + -- respbody is nil in case the server is not reachable + if respbody ~= nil then + respbody = string.gsub(respbody, "\\/", "/"); + end local delete_token = nil; -- check the response @@ -192,9 +206,12 @@ module:hook("iq/host/"..xmlns_http_upload..":request", function (event) return true; end end - else + elseif respbody ~= nil then origin.send(st.error_reply(stanza, "cancel", "undefined-condition", "status code: " .. statuscode .. " response: " ..respbody)); return true; + else + -- http file service not reachable + origin.send(st.error_reply(stanza, "cancel", "undefined-condition", "status code: " .. statuscode)); end local reply = st.reply(stanza); -- cgit v1.2.3 From 9cd578978fcb4fbd8d656afa96280a6f53c5b035 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Tue, 2 May 2017 20:59:08 +0200 Subject: add additional disco info to advertise managing UI --- .../mod_http_upload_external/mod_http_upload_external.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/prosody-module/mod_http_upload_external/mod_http_upload_external.lua b/prosody-module/mod_http_upload_external/mod_http_upload_external.lua index 23e356e..f88b29c 100644 --- a/prosody-module/mod_http_upload_external/mod_http_upload_external.lua +++ b/prosody-module/mod_http_upload_external/mod_http_upload_external.lua @@ -12,11 +12,13 @@ -- configuration local external_url = module:get_option("http_upload_external_url"); local xmpp_server_key = module:get_option("http_upload_external_server_key"); +local filetransfer_manager_ui_url = module:get_option("filetransfer_manager_ui_url"); -- imports local st = require"util.stanza"; local http = require"socket.http"; local json = require"util.json"; +local dataform = require "util.dataforms".new; -- depends module:depends("disco"); @@ -26,6 +28,12 @@ local xmlns_http_upload = "urn:xmpp:filetransfer:http"; module:add_feature(xmlns_http_upload); +-- add additional disco info to advertise managing UI +module:add_extension(dataform { + { name = "FORM_TYPE", type = "hidden", value = xmlns_http_upload }, + { name = "filetransfer-manager-ui-url", type = "text-single" }, +}:form({ ["filetransfer-manager-ui-url"] = filetransfer_manager_ui_url }, "result")); + -- hooks module:hook("iq/host/"..xmlns_http_upload..":request", function (event) local stanza, origin = event.stanza, event.origin; -- cgit v1.2.3 From bcfe8b2d97520e699377b41a1e024185fc7813dc Mon Sep 17 00:00:00 2001 From: steckbrief Date: Wed, 3 May 2017 09:45:19 +0200 Subject: Fixed error stanzas to clients to include additional information as intended --- .../mod_http_upload_external.lua | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/prosody-module/mod_http_upload_external/mod_http_upload_external.lua b/prosody-module/mod_http_upload_external/mod_http_upload_external.lua index f88b29c..a9b4551 100644 --- a/prosody-module/mod_http_upload_external/mod_http_upload_external.lua +++ b/prosody-module/mod_http_upload_external/mod_http_upload_external.lua @@ -103,19 +103,19 @@ module:hook("iq/host/"..xmlns_http_upload..":request", function (event) origin.send(st.error_reply(stanza, "modify", "not-acceptable", errobj.msg)); return true; elseif errobj.err_code == 2 then - origin.send(st.error_reply(stanza, "modify", "not-acceptable", errobj.msg, - st.stanza("file-too-large", {xmlns=xmlns_http_upload}) - :tag("max-size"):text(errobj.parameters.max_file_size))); + origin.send(st.error_reply(stanza, "modify", "not-acceptable", errobj.msg) + :tag("file-too-large", {xmlns=xmlns_http_upload}) + :tag("max-size"):text(errobj.parameters.max_file_size)); return true; elseif errobj.err_code == 3 then - origin.send(st.error_reply(stanza, "modify", "not-acceptable", errobj.msg, - st.stanza("invalid-character", {xmlns=xmlns_http_upload}) - :text(errobj.parameters.invalid_character))); + origin.send(st.error_reply(stanza, "modify", "not-acceptable", errobj.msg) + :tag("invalid-character", {xmlns=xmlns_http_upload}) + :text(errobj.parameters.invalid_character)); return true; elseif errobj.err_code == 4 then - origin.send(st.error_reply(stanza, "cancel", "internal-server-error", errobj.msg, - st.stanza("missing-parameter", {xmlns=xmlns_http_upload}) - :text(errobj.parameters.missing_parameter))); + origin.send(st.error_reply(stanza, "cancel", "internal-server-error", errobj.msg) + :tag("missing-parameter", {xmlns=xmlns_http_upload}) + :text(errobj.parameters.missing_parameter)); return true; else origin.send(st.error_reply(stanza, "cancel", "undefined-condition", "unknown err_code")); @@ -185,9 +185,9 @@ module:hook("iq/host/"..xmlns_http_upload..":request", function (event) else if errobj["err_code"] ~= nil and errobj["msg"] ~= nil then if errobj.err_code == 4 then - origin.send(st.error_reply(stanza, "cancel", "internal-server-error", errobj.msg, - st.stanza("missing-parameter", {xmlns=xmlns_http_upload}) - :text(errobj.parameters.missing_parameter))); + origin.send(st.error_reply(stanza, "cancel", "internal-server-error", errobj.msg) + :tag("missing-parameter", {xmlns=xmlns_http_upload}) + :text(errobj.parameters.missing_parameter)); return true; else origin.send(st.error_reply(stanza, "cancel", "undefined-condition", "unknown err_code")); -- cgit v1.2.3