about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorJames Booth <boothj5@gmail.com>2016-04-26 23:56:12 +0100
committerJames Booth <boothj5@gmail.com>2016-04-26 23:56:12 +0100
commitd4aa73159e7b50b18815920b29d68b38a03d58a8 (patch)
tree69a653058eeb3041096b186f492fb3b0658bf1dd
parent0ff29b3d2edafbc74df5e4d0e6be61952b20f2c2 (diff)
parent043a673fe15075d06776a460ff35f03b0225c04c (diff)
downloadprofani-tty-d4aa73159e7b50b18815920b29d68b38a03d58a8.tar.gz
Merge branch 'master' into subcmds
-rw-r--r--Makefile.am3
-rw-r--r--configure.ac10
-rw-r--r--src/command/command.c133
-rw-r--r--src/command/commands.c71
-rw-r--r--src/command/commands.h1
-rw-r--r--src/event/client_events.c18
-rw-r--r--src/event/client_events.h6
-rw-r--r--src/otr/otr.c1
-rw-r--r--src/profanity.c5
-rw-r--r--src/profanity.h4
-rw-r--r--src/tools/http_upload.c338
-rw-r--r--src/tools/http_upload.h66
-rw-r--r--src/ui/buffer.c15
-rw-r--r--src/ui/buffer.h1
-rw-r--r--src/ui/inputwin.c3
-rw-r--r--src/ui/notifier.c1
-rw-r--r--src/ui/ui.h1
-rw-r--r--src/ui/win_types.h44
-rw-r--r--src/ui/window.c21
-rw-r--r--src/ui/window.h2
-rw-r--r--src/window_list.c11
-rw-r--r--src/xmpp/connection.c36
-rw-r--r--src/xmpp/iq.c194
-rw-r--r--src/xmpp/message.c18
-rw-r--r--src/xmpp/stanza.c84
-rw-r--r--src/xmpp/stanza.h13
-rw-r--r--src/xmpp/xmpp.h57
-rw-r--r--tests/functionaltests/test_muc.c32
-rw-r--r--tests/functionaltests/test_ping.c10
-rw-r--r--tests/functionaltests/test_presence.c26
-rw-r--r--tests/unittests/test_cmd_pgp.c1
-rw-r--r--tests/unittests/tools/stub_http_upload.c63
-rw-r--r--tests/unittests/xmpp/stub_xmpp.c7
33 files changed, 1189 insertions, 107 deletions
diff --git a/Makefile.am b/Makefile.am
index 9dfb400c..ff08149c 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -32,6 +32,8 @@ core_sources = \
 	src/command/commands.h src/command/commands.c \
 	src/tools/parser.c \
 	src/tools/parser.h \
+	src/tools/http_upload.c \
+	src/tools/http_upload.h \
 	src/tools/p_sha1.h src/tools/p_sha1.c \
 	src/tools/autocomplete.c src/tools/autocomplete.h \
 	src/tools/tinyurl.c src/tools/tinyurl.h \
@@ -90,6 +92,7 @@ unittest_sources = \
 	tests/unittests/ui/stub_ui.c \
 	tests/unittests/log/stub_log.c \
 	tests/unittests/config/stub_accounts.c \
+	tests/unittests/tools/stub_http_upload.c \
 	tests/unittests/helpers.c tests/unittests/helpers.h \
 	tests/unittests/test_form.c tests/unittests/test_form.h \
 	tests/unittests/test_common.c tests/unittests/test_common.h \
diff --git a/configure.ac b/configure.ac
index 427c0f89..0057c3e5 100644
--- a/configure.ac
+++ b/configure.ac
@@ -116,6 +116,12 @@ else
     AM_CONDITIONAL([BUILD_C_API], [false])
 fi
 
+# threading
+ACX_PTHREAD
+LIBS="$PTHREAD_LIBS $LIBS"
+CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+CC="$PTHREAD_CC"
+
 ### Check for libmesode, fall back to libstrophe
 PKG_CHECK_MODULES([libmesode], [libmesode],
     [LIBS="$libmesode_LIBS $LIBS" CFLAGS="$CFLAGS $libmesode_CFLAGS" AC_DEFINE([HAVE_LIBMESODE], [1], [libmesode])],
@@ -282,10 +288,12 @@ AC_CHECK_HEADERS([ncurses.h], [], [])
 AM_CFLAGS="-Wall -Wno-deprecated-declarations"
 AS_IF([test "x$PACKAGE_STATUS" = xdevelopment],
     [AM_CFLAGS="$AM_CFLAGS -Wunused -Werror"])
+AS_IF([test "x$PLATFORM" = xosx],
+    [AM_CFLAGS="$AM_CFLAGS -Qunused-arguments"])
 AM_LDFLAGS="$AM_LDFLAGS -export-dynamic"
 AM_CPPFLAGS="$AM_CPPFLAGS $glib_CFLAGS $curl_CFLAGS $libnotify_CFLAGS $PYTHON_CPPFLAGS ${GTK_CFLAGS}"
 AM_CPPFLAGS="$AM_CPPFLAGS -DTHEMES_PATH=\"\\\"$THEMES_PATH\\\"\" -DICONS_PATH=\"\\\"$ICONS_PATH\\\"\""
-LIBS="$glib_LIBS $curl_LIBS $libnotify_LIBS $PYTHON_LIBS $PYTHON_LDFLAGS ${GTK_LIBS} $LIBS"
+LIBS="$glib_LIBS $curl_LIBS $libnotify_LIBS $PYTHON_LIBS $PYTHON_LDFLAGS ${GTK_LIBS} -lgio-2.0 $LIBS"
 
 AC_SUBST(AM_LDFLAGS)
 AC_SUBST(AM_CFLAGS)
diff --git a/src/command/command.c b/src/command/command.c
index f109bd7b..906f2708 100644
--- a/src/command/command.c
+++ b/src/command/command.c
@@ -42,6 +42,10 @@
 #include <stdlib.h>
 #include <stdio.h>
 #include <string.h>
+#include <libgen.h>
+
+#include <dirent.h>
+#include <sys/types.h>
 
 #include <glib.h>
 
@@ -120,6 +124,7 @@ static char* _console_autocomplete(ProfWin *window, const char *const input);
 static char* _win_autocomplete(ProfWin *window, const char *const input);
 static char* _close_autocomplete(ProfWin *window, const char *const input);
 static char* _plugins_autocomplete(ProfWin *window, const char *const input);
+static char* _sendfile_autocomplete(ProfWin *window, const char *const input);
 
 GHashTable *commands = NULL;
 
@@ -864,6 +869,24 @@ static struct cmd_t command_defs[] =
             "/disco info myfriend@server.com/laptop")
     },
 
+    { "/sendfile",
+        parse_args_with_freetext, 1, 1, NULL,
+        CMD_NOSUBFUNCS
+        CMD_MAINFUNC(cmd_sendfile)
+        CMD_TAGS(
+            CMD_TAG_CHAT,
+            CMD_TAG_GROUPCHAT)
+        CMD_SYN(
+            "/sendfile <file>")
+        CMD_DESC(
+            "Send a file using XEP-0363 HTTP file transfer.")
+        CMD_ARGS(
+            { "<file>", "Path to the file." })
+        CMD_EXAMPLES(
+            "/sendfile /etc/hosts",
+            "/sendfile ~/images/sweet_cat.jpg")
+    },
+
     { "/lastactivity",
         parse_args, 0, 1, NULL,
         CMD_NOSUBFUNCS
@@ -2255,6 +2278,7 @@ static Autocomplete console_msg_ac;
 static Autocomplete autoping_ac;
 static Autocomplete plugins_ac;
 static Autocomplete plugins_load_ac;
+static Autocomplete sendfile_ac;
 
 /*
  * Initialise command autocompleter and history
@@ -2809,6 +2833,8 @@ cmd_init(void)
 
     plugins_ac = autocomplete_new();
     autocomplete_add(plugins_ac, "load");
+
+    sendfile_ac = autocomplete_new();
 }
 
 void
@@ -2898,6 +2924,7 @@ cmd_uninit(void)
     autocomplete_free(autoping_ac);
     autocomplete_free(plugins_ac);
     autocomplete_free(plugins_load_ac);
+    autocomplete_free(sendfile_ac);
 }
 
 gboolean
@@ -3046,6 +3073,7 @@ cmd_reset_autocomplete(ProfWin *window)
     autocomplete_reset(notify_mention_ac);
     autocomplete_reset(notify_trigger_ac);
     autocomplete_reset(sub_ac);
+    autocomplete_reset(sendfile_ac);
 
     autocomplete_reset(who_room_ac);
     autocomplete_reset(who_roster_ac);
@@ -3418,6 +3446,7 @@ _cmd_complete_parameters(ProfWin *window, const char *const input)
     g_hash_table_insert(ac_funcs, "/win",           _win_autocomplete);
     g_hash_table_insert(ac_funcs, "/close",         _close_autocomplete);
     g_hash_table_insert(ac_funcs, "/plugins",         _plugins_autocomplete);
+    g_hash_table_insert(ac_funcs, "/sendfile",      _sendfile_autocomplete);
 
     int len = strlen(input);
     char parsed[len+1];
@@ -4817,6 +4846,110 @@ _close_autocomplete(ProfWin *window, const char *const input)
 }
 
 static char*
+_sendfile_autocomplete(ProfWin *window, const char *const input)
+{
+    static char* last_directory = NULL;
+
+    unsigned int output_off = 0;
+
+    char *result = NULL;
+    char *tmp;
+
+    // strip command
+    char *inpcp = (char*)input + 9;
+    while (*inpcp == ' ') {
+        inpcp++;
+    }
+
+    inpcp = strdup(inpcp);
+
+    // strip quotes
+    if (*inpcp == '"') {
+        tmp = strchr(inpcp+1, '"');
+        if (tmp) {
+            *tmp = '\0';
+        }
+        tmp = strdup(inpcp+1);
+        free(inpcp);
+        inpcp = tmp;
+    }
+
+    // expand ~ to $HOME
+    if (inpcp[0] == '~' && inpcp[1] == '/') {
+        if (asprintf(&tmp, "%s/%sfoo", getenv("HOME"), inpcp+2) == -1) {
+            return NULL;
+        }
+        output_off = strlen(getenv("HOME"))+1;
+    } else {
+        if (asprintf(&tmp, "%sfoo", inpcp) == -1) {
+            return NULL;
+        }
+    }
+    free(inpcp);
+    inpcp = tmp;
+
+    char* inpcp2 = strdup(inpcp);
+    char* foofile = strdup(basename(inpcp2));
+    char* directory = strdup(dirname(inpcp));
+    free(inpcp);
+    free(inpcp2);
+
+    if (!last_directory || strcmp(last_directory, directory) != 0) {
+        free(last_directory);
+        last_directory = directory;
+        autocomplete_reset(sendfile_ac);
+
+        struct dirent *dir;
+
+        DIR *d = opendir(directory);
+        if (d) {
+            while ((dir = readdir(d)) != NULL) {
+                if (strcmp(dir->d_name, ".") == 0) {
+                    continue;
+                } else if (strcmp(dir->d_name, "..") == 0) {
+                    continue;
+                } else if (*(dir->d_name) == '.' && *foofile != '.') {
+                    // only show hidden files on explicit request
+                    continue;
+                }
+                char * acstring;
+                if (output_off) {
+                    if (asprintf(&tmp, "%s/%s", directory, dir->d_name) == -1) {
+                        return NULL;
+                    }
+                    if (asprintf(&acstring, "~/%s", tmp+output_off) == -1) {
+                        return NULL;
+                    }
+                    free(tmp);
+                } else if (strcmp(directory, "/") == 0) {
+                    if (asprintf(&acstring, "/%s", dir->d_name) == -1) {
+                        return NULL;
+                    }
+                } else {
+                    if (asprintf(&acstring, "%s/%s", directory, dir->d_name) == -1) {
+                        return NULL;
+                    }
+                }
+                autocomplete_add(sendfile_ac, acstring);
+                free(acstring);
+            }
+            closedir(d);
+        }
+    } else {
+        free(foofile);
+        free(directory);
+    }
+
+    result = autocomplete_param_with_ac(input, "/sendfile", sendfile_ac, TRUE);
+    if (result) {
+        return result;
+    }
+
+    return NULL;
+}
+
+
+static char*
 _subject_autocomplete(ProfWin *window, const char *const input)
 {
     char *result = NULL;
diff --git a/src/command/commands.c b/src/command/commands.c
index f74fc96a..7e2825e8 100644
--- a/src/command/commands.c
+++ b/src/command/commands.c
@@ -32,10 +32,13 @@
  *
  */
 
+#define _GNU_SOURCE 1
+
 #include "config.h"
 
 #include <string.h>
 #include <stdlib.h>
+#include <stdio.h>
 #include <errno.h>
 #include <assert.h>
 #include <glib.h>
@@ -80,6 +83,7 @@
 #ifdef HAVE_GTK
 #include "tray.h"
 #endif
+#include "tools/http_upload.h"
 
 static void _update_presence(const resource_presence_t presence,
     const char *const show, gchar **args);
@@ -128,21 +132,21 @@ cmd_execute_default(ProfWin *window, const char *inp)
     {
         ProfChatWin *chatwin = (ProfChatWin*)window;
         assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
-        cl_ev_send_msg(chatwin, inp);
+        cl_ev_send_msg(chatwin, inp, NULL);
         break;
     }
     case WIN_PRIVATE:
     {
         ProfPrivateWin *privatewin = (ProfPrivateWin*)window;
         assert(privatewin->memcheck == PROFPRIVATEWIN_MEMCHECK);
-        cl_ev_send_priv_msg(privatewin, inp);
+        cl_ev_send_priv_msg(privatewin, inp, NULL);
         break;
     }
     case WIN_MUC:
     {
         ProfMucWin *mucwin = (ProfMucWin*)window;
         assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
-        cl_ev_send_muc_msg(mucwin, inp);
+        cl_ev_send_muc_msg(mucwin, inp, NULL);
         break;
     }
     case WIN_XML:
@@ -2013,7 +2017,7 @@ cmd_msg(ProfWin *window, const char *const command, gchar **args)
             ui_focus_win((ProfWin*)privwin);
 
             if (msg) {
-                cl_ev_send_priv_msg(privwin, msg);
+                cl_ev_send_priv_msg(privwin, msg, NULL);
             }
 
             g_string_free(full_jid, TRUE);
@@ -2038,7 +2042,7 @@ cmd_msg(ProfWin *window, const char *const command, gchar **args)
         ui_focus_win((ProfWin*)chatwin);
 
         if (msg) {
-            cl_ev_send_msg(chatwin, msg);
+            cl_ev_send_msg(chatwin, msg, NULL);
         } else {
 #ifdef HAVE_LIBOTR
             if (otr_is_secure(barejid)) {
@@ -4335,6 +4339,57 @@ cmd_disco(ProfWin *window, const char *const command, gchar **args)
 }
 
 gboolean
+cmd_sendfile(ProfWin *window, const char *const command, gchar **args)
+{
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+    char *filename = args[0];
+
+    // expand ~ to $HOME
+    if (filename[0] == '~' && filename[1] == '/') {
+        if (asprintf(&filename, "%s/%s", getenv("HOME"), filename+2) == -1) {
+            return TRUE;
+        }
+    } else {
+        filename = strdup(filename);
+    }
+
+    if (conn_status != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        free(filename);
+        return TRUE;
+    }
+
+    if (window->type != WIN_CHAT && window->type != WIN_PRIVATE && window->type != WIN_MUC) {
+        cons_show_error("Unsupported window for file transmission.");
+        free(filename);
+        return TRUE;
+    }
+
+    if (access(filename, R_OK) != 0) {
+        cons_show_error("Uploading '%s' failed: File not found!", filename);
+        free(filename);
+        return TRUE;
+    }
+
+    if (!is_regular_file(filename)) {
+        cons_show_error("Uploading '%s' failed: Not a file!", filename);
+        free(filename);
+        return TRUE;
+    }
+
+    HTTPUpload *upload = malloc(sizeof(HTTPUpload));
+    upload->window = window;
+
+    upload->filename = filename;
+    upload->filesize = file_size(filename);
+    upload->mime_type = file_mime_type(filename);
+
+    iq_http_upload_request(upload);
+
+    return TRUE;
+}
+
+gboolean
 cmd_lastactivity(ProfWin *window, const char *const command, gchar **args)
 {
     if ((g_strcmp0(args[0], "on") == 0) || (g_strcmp0(args[0], "off") == 0)) {
@@ -4485,21 +4540,21 @@ cmd_tiny(ProfWin *window, const char *const command, gchar **args)
     {
         ProfChatWin *chatwin = (ProfChatWin*)window;
         assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
-        cl_ev_send_msg(chatwin, tiny);
+        cl_ev_send_msg(chatwin, tiny, NULL);
         break;
     }
     case WIN_PRIVATE:
     {
         ProfPrivateWin *privatewin = (ProfPrivateWin*)window;
         assert(privatewin->memcheck == PROFPRIVATEWIN_MEMCHECK);
-        cl_ev_send_priv_msg(privatewin, tiny);
+        cl_ev_send_priv_msg(privatewin, tiny, NULL);
         break;
     }
     case WIN_MUC:
     {
         ProfMucWin *mucwin = (ProfMucWin*)window;
         assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
-        cl_ev_send_muc_msg(mucwin, tiny);
+        cl_ev_send_muc_msg(mucwin, tiny, NULL);
         break;
     }
     default:
diff --git a/src/command/commands.h b/src/command/commands.h
index 27e753a0..68d3947f 100644
--- a/src/command/commands.h
+++ b/src/command/commands.h
@@ -104,6 +104,7 @@ gboolean cmd_tls_cert(ProfWin *window, const char *const command, gchar **args);
 
 gboolean cmd_decline(ProfWin *window, const char *const command, gchar **args);
 gboolean cmd_disco(ProfWin *window, const char *const command, gchar **args);
+gboolean cmd_sendfile(ProfWin *window, const char *const command, gchar **args);
 gboolean cmd_lastactivity(ProfWin *window, const char *const command, gchar **args);
 gboolean cmd_disconnect(ProfWin *window, const char *const command, gchar **args);
 gboolean cmd_dnd(ProfWin *window, const char *const command, gchar **args);
diff --git a/src/event/client_events.c b/src/event/client_events.c
index 20a1a861..1c715f03 100644
--- a/src/event/client_events.c
+++ b/src/event/client_events.c
@@ -106,7 +106,7 @@ cl_ev_presence_send(const resource_presence_t presence_type, const char *const m
 }
 
 void
-cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg)
+cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg, const char *const oob_url)
 {
     chat_state_active(chatwin->state);
     char *plugin_msg = plugins_pre_chat_message_send(chatwin->barejid, msg);
@@ -122,7 +122,7 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg)
     } else {
         gboolean handled = otr_on_message_send(chatwin, plugin_msg);
         if (!handled) {
-            char *id = message_send_chat(chatwin->barejid, plugin_msg);
+            char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url);
             chat_log_msg_out(chatwin->barejid, plugin_msg);
             chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN);
             free(id);
@@ -140,7 +140,7 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg)
 #ifndef HAVE_LIBGPGME
     gboolean handled = otr_on_message_send(chatwin, plugin_msg);
     if (!handled) {
-        char *id = message_send_chat(chatwin->barejid, plugin_msg);
+        char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url);
         chat_log_msg_out(chatwin->barejid, plugin_msg);
         chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN);
         free(id);
@@ -161,7 +161,7 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg)
         chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PGP);
         free(id);
     } else {
-        char *id = message_send_chat(chatwin->barejid, plugin_msg);
+        char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url);
         chat_log_msg_out(chatwin->barejid, plugin_msg);
         chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN);
         free(id);
@@ -176,7 +176,7 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg)
 // OTR unsupported, PGP unsupported
 #ifndef HAVE_LIBOTR
 #ifndef HAVE_LIBGPGME
-    char *id = message_send_chat(chatwin->barejid, plugin_msg);
+    char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url);
     chat_log_msg_out(chatwin->barejid, plugin_msg);
     chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN);
     free(id);
@@ -189,18 +189,18 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg)
 }
 
 void
-cl_ev_send_muc_msg(ProfMucWin *mucwin, const char *const msg)
+cl_ev_send_muc_msg(ProfMucWin *mucwin, const char *const msg, const char *const oob_url)
 {
     char *plugin_msg = plugins_pre_room_message_send(mucwin->roomjid, msg);
 
-    message_send_groupchat(mucwin->roomjid, plugin_msg);
+    message_send_groupchat(mucwin->roomjid, plugin_msg, oob_url);
 
     plugins_post_room_message_send(mucwin->roomjid, plugin_msg);
     free(plugin_msg);
 }
 
 void
-cl_ev_send_priv_msg(ProfPrivateWin *privwin, const char *const msg)
+cl_ev_send_priv_msg(ProfPrivateWin *privwin, const char *const msg, const char *const oob_url)
 {
     if (privwin->occupant_offline) {
         privwin_message_occupant_offline(privwin);
@@ -209,7 +209,7 @@ cl_ev_send_priv_msg(ProfPrivateWin *privwin, const char *const msg)
     } else {
         char *plugin_msg = plugins_pre_priv_message_send(privwin->fulljid, msg);
 
-        message_send_private(privwin->fulljid, plugin_msg);
+        message_send_private(privwin->fulljid, plugin_msg, oob_url);
         privwin_outgoing_msg(privwin, plugin_msg);
 
         plugins_post_priv_message_send(privwin->fulljid, plugin_msg);
diff --git a/src/event/client_events.h b/src/event/client_events.h
index 7b7ec13a..3231ade9 100644
--- a/src/event/client_events.h
+++ b/src/event/client_events.h
@@ -42,8 +42,8 @@ void cl_ev_disconnect(void);
 
 void cl_ev_presence_send(const resource_presence_t presence_type, const char *const msg, const int idle_secs);
 
-void cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg);
-void cl_ev_send_muc_msg(ProfMucWin *mucwin, const char *const msg);
-void cl_ev_send_priv_msg(ProfPrivateWin *privwin, const char *const msg);
+void cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg, const char *const oob_url);
+void cl_ev_send_muc_msg(ProfMucWin *mucwin, const char *const msg, const char *const oob_url);
+void cl_ev_send_priv_msg(ProfPrivateWin *privwin, const char *const msg, const char *const oob_url);
 
 #endif
diff --git a/src/otr/otr.c b/src/otr/otr.c
index c119e896..b1cf1e16 100644
--- a/src/otr/otr.c
+++ b/src/otr/otr.c
@@ -45,6 +45,7 @@
 #include "window_list.h"
 #include "contact.h"
 #include "ui/ui.h"
+#include "xmpp/xmpp.h"
 #include "config/preferences.h"
 #include "chat_session.h"
 
diff --git a/src/profanity.c b/src/profanity.c
index 1140594f..9e27b6e8 100644
--- a/src/profanity.c
+++ b/src/profanity.c
@@ -323,6 +323,11 @@ _init(char *log_level)
     signal(SIGINT, SIG_IGN);
     signal(SIGTSTP, SIG_IGN);
     signal(SIGWINCH, ui_sigwinch_handler);
+    if (pthread_mutex_init(&lock, NULL) != 0) {
+        log_error("Mutex init failed");
+        exit(1);
+    }
+    pthread_mutex_lock(&lock);
     _create_directories();
     log_level_t prof_log_level = log_level_from_string(log_level);
     prefs_load();
diff --git a/src/profanity.h b/src/profanity.h
index 7e128dc8..28a71467 100644
--- a/src/profanity.h
+++ b/src/profanity.h
@@ -35,6 +35,8 @@
 #ifndef PROFANITY_H
 #define PROFANITY_H
 
+#include <pthread.h>
+
 #include "resource.h"
 #include "xmpp/xmpp.h"
 
@@ -46,4 +48,6 @@ gboolean prof_process_input(char *inp);
 
 void prof_set_quit(void);
 
+pthread_mutex_t lock;
+
 #endif
diff --git a/src/tools/http_upload.c b/src/tools/http_upload.c
new file mode 100644
index 00000000..2169a382
--- /dev/null
+++ b/src/tools/http_upload.c
@@ -0,0 +1,338 @@
+/*
+ * http_upload.c
+ *
+ * Copyright (C) 2012 - 2016 James Booth <boothj5@gmail.com>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity 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 Profanity.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * In addition, as a special exception, the copyright holders give permission to
+ * link the code of portions of this program with the OpenSSL library under
+ * certain conditions as described in each individual source file, and
+ * distribute linked combinations including the two.
+ *
+ * You must obey the GNU General Public License in all respects for all of the
+ * code used other than OpenSSL. If you modify file(s) with this exception, you
+ * may extend this exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this exception
+ * statement from your version. If you delete this exception statement from all
+ * source files in the program, then also delete it here.
+ *
+ */
+
+#define _GNU_SOURCE 1
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <curl/curl.h>
+#include <gio/gio.h>
+#include <pthread.h>
+#include <assert.h>
+
+#include "profanity.h"
+#include "ui/ui.h"
+#include "ui/window.h"
+#include "tools/http_upload.h"
+#include "event/client_events.h"
+#include "config/preferences.h"
+
+#define FALLBACK_MIMETYPE "application/octet-stream"
+#define FALLBACK_CONTENTTYPE_HEADER "Content-Type: application/octet-stream"
+#define FALLBACK_MSG ""
+#define FILE_HEADER_BYTES 512
+
+struct curl_data_t {
+    char *buffer;
+    size_t size;
+};
+
+
+static int
+_xferinfo(void *userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
+{
+    HTTPUpload *upload = (HTTPUpload *)userdata;
+
+    pthread_mutex_lock(&lock);
+
+    if (upload->cancel) {
+        pthread_mutex_unlock(&lock);
+        return 1;
+    }
+
+    if (upload->bytes_sent == ulnow) {
+        pthread_mutex_unlock(&lock);
+        return 0;
+    } else {
+        upload->bytes_sent = ulnow;
+    }
+
+    unsigned int ulperc = 0;
+    if (ultotal != 0) {
+        ulperc = (100 * ulnow) / ultotal;
+    }
+
+    char *msg;
+    if (asprintf(&msg, "Uploading '%s': %d%%", upload->filename, ulperc) == -1) {
+        msg = strdup(FALLBACK_MSG);
+    }
+    win_update_entry_message(upload->window, upload->put_url, msg);
+    free(msg);
+
+    pthread_mutex_unlock(&lock);
+
+    return 0;
+}
+
+#if LIBCURL_VERSION_NUM < 0x072000
+static int
+_older_progress(void *p, double dltotal, double dlnow, double ultotal, double ulnow)
+{
+    return _xferinfo(p, (curl_off_t)dltotal, (curl_off_t)dlnow, (curl_off_t)ultotal, (curl_off_t)ulnow);
+}
+#endif
+
+static size_t
+_data_callback(void *ptr, size_t size, size_t nmemb, void *data)
+{
+    size_t realsize = size * nmemb;
+    struct curl_data_t *mem = (struct curl_data_t *) data;
+    mem->buffer = realloc(mem->buffer, mem->size + realsize + 1);
+
+    if (mem->buffer)
+    {
+        memcpy( &( mem->buffer[ mem->size ] ), ptr, realsize );
+        mem->size += realsize;
+        mem->buffer[ mem->size ] = 0;
+    }
+
+    return realsize;
+}
+
+void *
+http_file_put(void *userdata)
+{
+    HTTPUpload *upload = (HTTPUpload *)userdata;
+
+    FILE *fd = NULL;
+
+    char *err = NULL;
+    char *content_type_header;
+
+    CURL *curl;
+    CURLcode res;
+
+    upload->cancel = 0;
+    upload->bytes_sent = 0;
+
+    pthread_mutex_lock(&lock);
+    char* msg;
+    if (asprintf(&msg, "Uploading '%s': 0%%", upload->filename) == -1) {
+        msg = strdup(FALLBACK_MSG);
+    }
+    win_print_with_receipt(upload->window, '!', 0, NULL, 0, THEME_TEXT_ME, NULL, msg, upload->put_url);
+    free(msg);
+
+    char *cert_path = prefs_get_string(PREF_TLS_CERTPATH);
+    pthread_mutex_unlock(&lock);
+
+    curl_global_init(CURL_GLOBAL_ALL);
+    curl = curl_easy_init();
+
+    curl_easy_setopt(curl, CURLOPT_URL, upload->put_url);
+    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
+
+    struct curl_slist *headers = NULL;
+    if (asprintf(&content_type_header, "Content-Type: %s", upload->mime_type) == -1) {
+        content_type_header = strdup(FALLBACK_CONTENTTYPE_HEADER);
+    }
+    headers = curl_slist_append(headers, content_type_header);
+    headers = curl_slist_append(headers, "Expect:");
+    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+    #if LIBCURL_VERSION_NUM >= 0x072000
+    curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, _xferinfo);
+    curl_easy_setopt(curl, CURLOPT_XFERINFODATA, upload);
+    #else
+    curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, _older_progress);
+    curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, upload);
+    #endif
+    curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
+
+    struct curl_data_t output;
+    output.buffer = NULL;
+    output.size = 0;
+    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _data_callback);
+    curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&output);
+
+    curl_easy_setopt(curl, CURLOPT_USERAGENT, "profanity");
+
+    if (!(fd = fopen(upload->filename, "rb"))) {
+        if (asprintf(&err, "failed to open '%s'", upload->filename) == -1) {
+            err = NULL;
+        }
+        goto end;
+    }
+
+    if (cert_path) {
+        curl_easy_setopt(curl, CURLOPT_CAPATH, cert_path);
+    }
+
+    curl_easy_setopt(curl, CURLOPT_READDATA, fd);
+    curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)(upload->filesize));
+    curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
+
+    if ((res = curl_easy_perform(curl)) != CURLE_OK) {
+        err = strdup(curl_easy_strerror(res));
+    } else {
+        long http_code = 0;
+        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
+
+        if (output.buffer) {
+            output.buffer[output.size++] = '\0';
+        }
+
+        // XEP-0363 specifies 201 but prosody returns 200
+        if (http_code != 200 && http_code != 201) {
+            if (asprintf(&err, "Server returned %lu", http_code) == -1) {
+                err = NULL;
+            }
+        }
+
+        #if 0
+        printf("HTTP Status: %lu\n", http_code);
+        printf("%s\n", output.buffer);
+        printf("%lu bytes retrieved\n", (long)output.size);
+        #endif
+    }
+
+end:
+    curl_easy_cleanup(curl);
+    curl_global_cleanup();
+    curl_slist_free_all(headers);
+    if (fd) {
+        fclose(fd);
+    }
+    free(content_type_header);
+    free(output.buffer);
+
+    pthread_mutex_lock(&lock);
+    prefs_free_string(cert_path);
+
+    if (err) {
+        char *msg;
+        if (upload->cancel) {
+            if (asprintf(&msg, "Uploading '%s' failed: Upload was canceled", upload->filename) == -1) {
+                msg = strdup(FALLBACK_MSG);
+            }
+        } else {
+            if (asprintf(&msg, "Uploading '%s' failed: %s", upload->filename, err) == -1) {
+                msg = strdup(FALLBACK_MSG);
+            }
+            win_update_entry_message(upload->window, upload->put_url, msg);
+        }
+        cons_show_error(msg);
+        free(msg);
+        free(err);
+    } else {
+        if (!upload->cancel) {
+            if (asprintf(&msg, "Uploading '%s': 100%%", upload->filename) == -1) {
+                msg = strdup(FALLBACK_MSG);
+            }
+            win_update_entry_message(upload->window, upload->put_url, msg);
+            //win_update_entry_theme(upload->window, upload->put_url, THEME_THEM);
+            win_mark_received(upload->window, upload->put_url);
+            free(msg);
+
+            switch (upload->window->type) {
+            case WIN_CHAT:
+            {
+                ProfChatWin *chatwin = (ProfChatWin*)(upload->window);
+                assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
+                cl_ev_send_msg(chatwin, upload->get_url, upload->get_url);
+                break;
+            }
+            case WIN_PRIVATE:
+            {
+                ProfPrivateWin *privatewin = (ProfPrivateWin*)(upload->window);
+                assert(privatewin->memcheck == PROFPRIVATEWIN_MEMCHECK);
+                cl_ev_send_priv_msg(privatewin, upload->get_url, upload->get_url);
+                break;
+            }
+            case WIN_MUC:
+            {
+                ProfMucWin *mucwin = (ProfMucWin*)(upload->window);
+                assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
+                cl_ev_send_muc_msg(mucwin, upload->get_url, upload->get_url);
+                break;
+            }
+            default:
+                break;
+            }
+        }
+    }
+
+    upload_processes = g_slist_remove(upload_processes, upload);
+    pthread_mutex_unlock(&lock);
+
+    free(upload->filename);
+    free(upload->mime_type);
+    free(upload->get_url);
+    free(upload->put_url);
+    free(upload);
+
+    return NULL;
+}
+
+char*
+file_mime_type(const char* const file_name)
+{
+	char *out_mime_type;
+    char file_header[FILE_HEADER_BYTES];
+    FILE *fd;
+    if (!(fd = fopen(file_name, "rb"))) {
+        return strdup(FALLBACK_MIMETYPE);
+    }
+    size_t file_header_size = fread(file_header, 1, FILE_HEADER_BYTES, fd);
+    fclose(fd);
+
+    char *content_type = g_content_type_guess(file_name, (unsigned char*)file_header, file_header_size, NULL);
+    if (content_type != NULL) {
+        char *mime_type = g_content_type_get_mime_type(content_type);
+        out_mime_type = strdup(mime_type);
+        g_free(mime_type);
+    }
+    else {
+        return strdup(FALLBACK_MIMETYPE);
+    }
+    g_free(content_type);
+    return out_mime_type;
+}
+
+off_t file_size(const char* const filename)
+{
+    struct stat st;
+    stat(filename, &st);
+    return st.st_size;
+}
+
+int is_regular_file(const char *filename)
+{
+    struct stat st;
+    stat(filename, &st);
+    return S_ISREG(st.st_mode);
+}
diff --git a/src/tools/http_upload.h b/src/tools/http_upload.h
new file mode 100644
index 00000000..546eea7f
--- /dev/null
+++ b/src/tools/http_upload.h
@@ -0,0 +1,66 @@
+/*
+ * http_upload.h
+ *
+ * Copyright (C) 2012 - 2016 James Booth <boothj5@gmail.com>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity 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 Profanity.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * In addition, as a special exception, the copyright holders give permission to
+ * link the code of portions of this program with the OpenSSL library under
+ * certain conditions as described in each individual source file, and
+ * distribute linked combinations including the two.
+ *
+ * You must obey the GNU General Public License in all respects for all of the
+ * code used other than OpenSSL. If you modify file(s) with this exception, you
+ * may extend this exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this exception
+ * statement from your version. If you delete this exception statement from all
+ * source files in the program, then also delete it here.
+ *
+ */
+
+#ifndef TOOLS_HTTP_UPLOAD_H
+#define TOOLS_HTTP_UPLOAD_H
+
+#ifdef PLATFORM_CYGWIN
+#define SOCKET int
+#endif
+
+#include <sys/select.h>
+#include <curl/curl.h>
+#include "ui/win_types.h"
+
+typedef struct http_upload_t {
+    char *filename;
+    off_t filesize;
+    curl_off_t bytes_sent;
+    char *mime_type;
+    char *get_url;
+    char *put_url;
+    ProfWin *window;
+    pthread_t worker;
+    int cancel;
+} HTTPUpload;
+
+GSList *upload_processes;
+
+void* http_file_put(void *userdata);
+
+char* file_mime_type(const char* const file_name);
+off_t file_size(const char* const file_name);
+int is_regular_file(const char *filename);
+
+#endif
diff --git a/src/ui/buffer.c b/src/ui/buffer.c
index 29eddd89..a86e85ae 100644
--- a/src/ui/buffer.c
+++ b/src/ui/buffer.c
@@ -125,6 +125,21 @@ buffer_yield_entry(ProfBuff buffer, int entry)
     return node->data;
 }
 
+ProfBuffEntry*
+buffer_yield_entry_by_id(ProfBuff buffer, const char *const id)
+{
+    GSList *entries = buffer->entries;
+    while (entries) {
+        ProfBuffEntry *entry = entries->data;
+        if (entry->receipt && g_strcmp0(entry->receipt->id, id) == 0) {
+            return entry;
+        }
+        entries = g_slist_next(entries);
+    }
+
+    return NULL;
+}
+
 static void
 _free_entry(ProfBuffEntry *entry)
 {
diff --git a/src/ui/buffer.h b/src/ui/buffer.h
index ce9763ed..50ce0634 100644
--- a/src/ui/buffer.h
+++ b/src/ui/buffer.h
@@ -64,6 +64,7 @@ void buffer_push(ProfBuff buffer, const char show_char, int pad_indent, GDateTim
     const char *const from, const char *const message, DeliveryReceipt *receipt);
 int buffer_size(ProfBuff buffer);
 ProfBuffEntry* buffer_yield_entry(ProfBuff buffer, int entry);
+ProfBuffEntry* buffer_yield_entry_by_id(ProfBuff buffer, const char *const id);
 gboolean buffer_mark_received(ProfBuff buffer, const char *const id);
 
 #endif
diff --git a/src/ui/inputwin.c b/src/ui/inputwin.c
index fc1c204d..c94c0a8c 100644
--- a/src/ui/inputwin.c
+++ b/src/ui/inputwin.c
@@ -42,6 +42,7 @@
 #include <wchar.h>
 #include <sys/time.h>
 #include <errno.h>
+#include <pthread.h>
 
 #include <readline/readline.h>
 #include <readline/history.h>
@@ -145,7 +146,9 @@ inp_readline(void)
     FD_ZERO(&fds);
     FD_SET(fileno(rl_instream), &fds);
     errno = 0;
+    pthread_mutex_unlock(&lock);
     r = select(FD_SETSIZE, &fds, NULL, NULL, &p_rl_timeout);
+    pthread_mutex_lock(&lock);
     if (r < 0) {
         if (errno != EINTR) {
             char *err_msg = strerror(errno);
diff --git a/src/ui/notifier.c b/src/ui/notifier.c
index 80ede2dc..1a6c0132 100644
--- a/src/ui/notifier.c
+++ b/src/ui/notifier.c
@@ -50,6 +50,7 @@
 #include "ui/ui.h"
 #include "window_list.h"
 #include "config/preferences.h"
+#include "xmpp/xmpp.h"
 
 static GTimer *remind_timer;
 
diff --git a/src/ui/ui.h b/src/ui/ui.h
index 01ae3953..36a70618 100644
--- a/src/ui/ui.h
+++ b/src/ui/ui.h
@@ -40,6 +40,7 @@
 #include "command/commands.h"
 #include "ui/win_types.h"
 #include "muc.h"
+#include "config/tlscerts.h"
 #ifdef HAVE_LIBOTR
 #include "otr/otr.h"
 #endif
diff --git a/src/ui/win_types.h b/src/ui/win_types.h
index e241a852..f05f237e 100644
--- a/src/ui/win_types.h
+++ b/src/ui/win_types.h
@@ -45,9 +45,9 @@
 #include <ncurses.h>
 #endif
 
-#include "xmpp/xmpp.h"
 #include "ui/buffer.h"
 #include "chat_state.h"
+#include "tools/autocomplete.h"
 
 #define LAYOUT_SPLIT_MEMCHECK       12345671
 #define PROFCHATWIN_MEMCHECK        22374522
@@ -58,6 +58,48 @@
 #define PROFPLUGINWIN_MEMCHECK      43434777
 
 typedef enum {
+    FIELD_HIDDEN,
+    FIELD_TEXT_SINGLE,
+    FIELD_TEXT_PRIVATE,
+    FIELD_TEXT_MULTI,
+    FIELD_BOOLEAN,
+    FIELD_LIST_SINGLE,
+    FIELD_LIST_MULTI,
+    FIELD_JID_SINGLE,
+    FIELD_JID_MULTI,
+    FIELD_FIXED,
+    FIELD_UNKNOWN
+} form_field_type_t;
+
+typedef struct form_option_t {
+    char *label;
+    char *value;
+} FormOption;
+
+typedef struct form_field_t {
+    char *label;
+    char *type;
+    form_field_type_t type_t;
+    char *var;
+    char *description;
+    gboolean required;
+    GSList *values;
+    GSList *options;
+    Autocomplete value_ac;
+} FormField;
+
+typedef struct data_form_t {
+    char *type;
+    char *title;
+    char *instructions;
+    GSList *fields;
+    GHashTable *var_to_tag;
+    GHashTable *tag_to_var;
+    Autocomplete tag_ac;
+    gboolean modified;
+} DataForm;
+
+typedef enum {
     LAYOUT_SIMPLE,
     LAYOUT_SPLIT
 } layout_type_t;
diff --git a/src/ui/window.c b/src/ui/window.c
index 1d95d4cb..3a96ca08 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -1055,6 +1055,27 @@ win_mark_received(ProfWin *window, const char *const id)
 }
 
 void
+win_update_entry_message(ProfWin *window, const char *const id, const char *const message)
+{
+    ProfBuffEntry *entry = buffer_yield_entry_by_id(window->layout->buffer, id);
+    if (entry) {
+        free(entry->message);
+        entry->message = strdup(message);
+        win_redraw(window);
+    }
+}
+
+void
+win_update_entry_theme(ProfWin *window, const char *const id, theme_item_t theme_item)
+{
+    ProfBuffEntry *entry = buffer_yield_entry_by_id(window->layout->buffer, id);
+    if (entry) {
+        entry->theme_item = theme_item;
+        win_redraw(window);
+    }
+}
+
+void
 win_println(ProfWin *window, int pad, const char *const message)
 {
     win_print(window, '-', pad, NULL, 0, 0, "", message);
diff --git a/src/ui/window.h b/src/ui/window.h
index 4923f4ec..f1f740c5 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -70,6 +70,8 @@ int win_occpuants_cols(void);
 void win_sub_print(WINDOW *win, char *msg, gboolean newline, gboolean wrap, int indent);
 void win_sub_newline_lazy(WINDOW *win);
 void win_mark_received(ProfWin *window, const char *const id);
+void win_update_entry_message(ProfWin *window, const char *const id, const char *const message);
+void win_update_entry_theme(ProfWin *window, const char *const id, theme_item_t theme_item);
 
 gboolean win_has_active_subwin(ProfWin *window);
 
diff --git a/src/window_list.c b/src/window_list.c
index 7fcc0172..6b9f2411 100644
--- a/src/window_list.c
+++ b/src/window_list.c
@@ -508,6 +508,17 @@ wins_close_by_num(int i)
 
         ProfWin *window = wins_get_by_num(i);
         if (window) {
+            // cancel upload proccesses of this window
+            GSList *upload_process = upload_processes;
+            while (upload_process) {
+                HTTPUpload *upload = upload_process->data;
+                if (upload->window == window) {
+                    upload->cancel = 1;
+                    break;
+                }
+                upload_process = g_slist_next(upload_process);
+            }
+
             switch (window->type) {
             case WIN_CHAT:
             {
diff --git a/src/xmpp/connection.c b/src/xmpp/connection.c
index e760a0b7..710e71b2 100644
--- a/src/xmpp/connection.c
+++ b/src/xmpp/connection.c
@@ -75,6 +75,7 @@ static struct _jabber_conn_t {
 } jabber_conn;
 
 static GHashTable *available_resources;
+static GSList *disco_items;
 
 // for auto reconnect
 static struct {
@@ -113,6 +114,18 @@ void _connection_free_saved_account(void);
 void _connection_free_saved_details(void);
 void _connection_free_session_data(void);
 
+static void
+_info_destroy(DiscoInfo *info)
+{
+    if (info) {
+        free(info->item);
+        if (info->features) {
+            g_hash_table_remove_all(info->features);
+        }
+        free(info);
+    }
+}
+
 void
 jabber_init(void)
 {
@@ -125,6 +138,7 @@ jabber_init(void)
     presence_sub_requests_init();
     caps_init();
     available_resources = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)resource_destroy);
+    disco_items = NULL;
     xmpp_initialize();
 }
 
@@ -323,6 +337,18 @@ jabber_set_connection_status(jabber_conn_status_t status)
     jabber_conn.conn_status = status;
 }
 
+GSList*
+jabber_get_disco_items(void)
+{
+    return (disco_items);
+}
+
+void
+jabber_set_disco_items(GSList *_disco_items)
+{
+    disco_items = _disco_items;
+}
+
 xmpp_conn_t*
 connection_get_conn(void)
 {
@@ -420,6 +446,8 @@ _connection_free_saved_details(void)
 void
 _connection_free_session_data(void)
 {
+    g_slist_free_full(disco_items, (GDestroyNotify)_info_destroy);
+    disco_items = NULL;
     g_hash_table_remove_all(available_resources);
     chat_sessions_clear();
     presence_clear_sub_requests();
@@ -651,6 +679,14 @@ _connection_handler(xmpp_conn_t *const conn, const xmpp_conn_event_t status, con
         roster_request();
         bookmark_request();
 
+        // items discovery
+        DiscoInfo *info = malloc(sizeof(struct disco_info_t));
+        info->item = strdup(jabber_conn.domain);
+        info->features = g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL);
+        disco_items = g_slist_append(disco_items, info);
+        iq_disco_info_request_onconnect(info->item);
+        iq_disco_items_request_onconnect(jabber_conn.domain);
+
         if (prefs_get_boolean(PREF_CARBONS)){
             iq_enable_carbons();
         }
diff --git a/src/xmpp/iq.c b/src/xmpp/iq.c
index 1242c6cf..ef1a45b5 100644
--- a/src/xmpp/iq.c
+++ b/src/xmpp/iq.c
@@ -64,6 +64,7 @@
 #include "roster_list.h"
 #include "xmpp/xmpp.h"
 #include "plugins/plugins.h"
+#include "tools/http_upload.h"
 
 typedef struct p_room_info_data_t {
     char *room;
@@ -87,6 +88,8 @@ static void _ping_get_handler(xmpp_stanza_t *const stanza);
 
 static int _version_result_id_handler(xmpp_stanza_t *const stanza, void *const userdata);
 static int _disco_info_response_id_handler(xmpp_stanza_t *const stanza, void *const userdata);
+static int _disco_info_response_id_handler_onconnect(xmpp_stanza_t *const stanza, void *const userdata);
+static int _http_upload_response_id_handler(xmpp_stanza_t *const stanza, void *const upload_ctx);
 static int _last_activity_response_id_handler(xmpp_stanza_t *const stanza, void *const userdata);
 static int _room_info_response_id_handler(xmpp_stanza_t *const stanza, void *const userdata);
 static int _destroy_room_result_id_handler(xmpp_stanza_t *const stanza, void *const userdata);
@@ -293,6 +296,42 @@ iq_disable_carbons(void)
 }
 
 void
+iq_http_upload_request(HTTPUpload *upload)
+{
+    GSList *disco_items = jabber_get_disco_items();
+    DiscoInfo *disco_info;
+    if (disco_items && (g_slist_length(disco_items) > 0)) {
+        while (disco_items) {
+            disco_info = disco_items->data;
+            if (g_hash_table_lookup_extended(disco_info->features, STANZA_NS_HTTP_UPLOAD, NULL, NULL)) {
+                break;
+            }
+            disco_items = g_slist_next(disco_items);
+            if (!disco_items) {
+                cons_show_error("XEP-0363 HTTP File Upload is not supported by the server");
+                return;
+            }
+        }
+    } else {
+        cons_show_error("No disco items");
+        return;
+    }
+
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    char *id = create_unique_id("http_upload_request");
+
+    xmpp_stanza_t *iq = stanza_create_http_upload_request(ctx, id, disco_info->item, upload);
+
+    id_handler_add(id, _http_upload_response_id_handler, upload);
+
+    free(id);
+
+    send_iq_stanza(iq);
+    xmpp_stanza_release(iq);
+    return;
+}
+
+void
 iq_disco_info_request(gchar *jid)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
@@ -308,6 +347,21 @@ iq_disco_info_request(gchar *jid)
 }
 
 void
+iq_disco_info_request_onconnect(gchar *jid)
+{
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    char *id = create_unique_id("disco_info_onconnect");
+    xmpp_stanza_t *iq = stanza_create_disco_info_iq(ctx, id, jid, NULL);
+
+    id_handler_add(id, _disco_info_response_id_handler_onconnect, NULL);
+
+    free(id);
+
+    send_iq_stanza(iq);
+    xmpp_stanza_release(iq);
+}
+
+void
 iq_last_activity_request(gchar *jid)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
@@ -429,6 +483,15 @@ iq_disco_items_request(gchar *jid)
 }
 
 void
+iq_disco_items_request_onconnect(gchar *jid)
+{
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    xmpp_stanza_t *iq = stanza_create_disco_items_iq(ctx, "discoitemsreq_onconnect", jid);
+    send_iq_stanza(iq);
+    xmpp_stanza_release(iq);
+}
+
+void
 iq_send_software_version(const char *const fulljid)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
@@ -1817,6 +1880,122 @@ _disco_info_response_id_handler(xmpp_stanza_t *const stanza, void *const userdat
     return 0;
 }
 
+static int
+_disco_info_response_id_handler_onconnect(xmpp_stanza_t *const stanza, void *const userdata)
+{
+    const char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
+    const char *type = xmpp_stanza_get_type(stanza);
+
+    if (from) {
+        log_info("Received disco#info response from: %s", from);
+    } else {
+        log_info("Received disco#info response");
+    }
+
+    // handle error responses
+    if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) {
+        char *error_message = stanza_get_error_message(stanza);
+        if (from) {
+            log_error("Service discovery failed for %s: %s", from, error_message);
+        } else {
+            log_error("Service discovery failed: %s", error_message);
+        }
+        free(error_message);
+        return 0;
+    }
+
+    xmpp_stanza_t *query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY);
+
+    if (query) {
+        xmpp_stanza_t *child = xmpp_stanza_get_children(query);
+
+        GSList *disco_items = jabber_get_disco_items();
+        DiscoInfo *disco_info;
+        if (disco_items && (g_slist_length(disco_items) > 0)) {
+            while (disco_items) {
+                disco_info = disco_items->data;
+                if (g_strcmp0(disco_info->item, from) == 0) {
+                    break;
+                }
+                disco_items = g_slist_next(disco_items);
+                if (!disco_items) {
+                    log_error("No matching disco item found for %s", from);
+                    return 1;
+                }
+            }
+        } else {
+            return 1;
+        }
+
+        while (child) {
+            const char *stanza_name = xmpp_stanza_get_name(child);
+            if (g_strcmp0(stanza_name, STANZA_NAME_FEATURE) == 0) {
+                const char *var = xmpp_stanza_get_attribute(child, STANZA_ATTR_VAR);
+                if (var) {
+                    g_hash_table_add(disco_info->features, strdup(var));
+                }
+            }
+            child = xmpp_stanza_get_next(child);
+        }
+    }
+
+    return 0;
+}
+
+static int
+_http_upload_response_id_handler(xmpp_stanza_t *const stanza, void *const userdata)
+{
+    HTTPUpload *upload = (HTTPUpload *)userdata;
+    const char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
+    const char *type = xmpp_stanza_get_type(stanza);
+
+    if (from) {
+        log_info("Received http_upload response from: %s", from);
+    } else {
+        log_info("Received http_upload response");
+    }
+
+    // handle error responses
+    if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) {
+        char *error_message = stanza_get_error_message(stanza);
+        if (from) {
+            cons_show_error("Uploading '%s' failed for %s: %s", upload->filename, from, error_message);
+        } else {
+            cons_show_error("Uploading '%s' failed: %s", upload->filename, error_message);
+        }
+        free(error_message);
+        return 0;
+    }
+
+    xmpp_stanza_t *slot = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_SLOT);
+
+    if (slot && g_strcmp0(xmpp_stanza_get_ns(slot), STANZA_NS_HTTP_UPLOAD) == 0) {
+        xmpp_stanza_t *put = xmpp_stanza_get_child_by_name(slot, STANZA_NAME_PUT);
+        xmpp_stanza_t *get = xmpp_stanza_get_child_by_name(slot, STANZA_NAME_GET);
+
+        if (put && get) {
+            char *put_url = xmpp_stanza_get_text(put);
+            char *get_url = xmpp_stanza_get_text(get);
+
+            upload->put_url = strdup(put_url);
+            upload->get_url = strdup(get_url);
+
+            xmpp_conn_t *conn = connection_get_conn();
+            xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
+            if (put_url) xmpp_free(ctx, put_url);
+            if (get_url) xmpp_free(ctx, get_url);
+
+            pthread_create(&(upload->worker), NULL, &http_file_put, upload);
+            upload_processes = g_slist_append(upload_processes, upload);
+        } else {
+            log_error("Invalid XML in HTTP Upload slot");
+            return 1;
+        }
+    }
+
+    return 0;
+}
+
 static void
 _disco_items_result_handler(xmpp_stanza_t *const stanza)
 {
@@ -1825,7 +2004,7 @@ _disco_items_result_handler(xmpp_stanza_t *const stanza)
     const char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
     GSList *items = NULL;
 
-    if ((g_strcmp0(id, "confreq") == 0) || (g_strcmp0(id, "discoitemsreq") == 0)) {
+    if ((g_strcmp0(id, "confreq") == 0) || (g_strcmp0(id, "discoitemsreq") == 0) || (g_strcmp0(id, "discoitemsreq_onconnect") == 0)) {
         log_debug("Response to query: %s", id);
         xmpp_stanza_t *query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY);
 
@@ -1857,6 +2036,19 @@ _disco_items_result_handler(xmpp_stanza_t *const stanza)
         cons_show_room_list(items, from);
     } else if (g_strcmp0(id, "discoitemsreq") == 0) {
         cons_show_disco_items(items, from);
+    } else if (g_strcmp0(id, "discoitemsreq_onconnect") == 0) {
+        GSList *res_items = items;
+        if (res_items && (g_slist_length(res_items) > 0)) {
+            while (res_items) {
+                DiscoItem *item = res_items->data;
+                DiscoInfo *info = malloc(sizeof(struct disco_info_t));
+                info->item = strdup(item->jid);
+                info->features = g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL);
+                jabber_set_disco_items(g_slist_append(jabber_get_disco_items(), info));
+                iq_disco_info_request_onconnect(info->item);
+                res_items = g_slist_next(res_items);
+            }
+        }
     }
 
     g_slist_free_full(items, (GDestroyNotify)_item_destroy);
diff --git a/src/xmpp/message.c b/src/xmpp/message.c
index bdcceb5e..0d06408b 100644
--- a/src/xmpp/message.c
+++ b/src/xmpp/message.c
@@ -163,7 +163,7 @@ _session_state(const char *const barejid)
 }
 
 char*
-message_send_chat(const char *const barejid, const char *const msg)
+message_send_chat(const char *const barejid, const char *const msg, const char *const oob_url)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
 
@@ -178,6 +178,10 @@ message_send_chat(const char *const barejid, const char *const msg)
         stanza_attach_state(ctx, message, state);
     }
 
+    if (oob_url) {
+        stanza_attach_x_oob_url(ctx, message, oob_url);
+    }
+
     if (prefs_get_boolean(PREF_RECEIPTS_REQUEST)) {
         stanza_attach_receipt_request(ctx, message);
     }
@@ -274,25 +278,33 @@ message_send_chat_otr(const char *const barejid, const char *const msg)
 }
 
 void
-message_send_private(const char *const fulljid, const char *const msg)
+message_send_private(const char *const fulljid, const char *const msg, const char *const oob_url)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
     char *id = create_unique_id("prv");
     xmpp_stanza_t *message = stanza_create_message(ctx, id, fulljid, STANZA_TYPE_CHAT, msg);
     free(id);
 
+    if (oob_url) {
+        stanza_attach_x_oob_url(ctx, message, oob_url);
+    }
+
     _send_message_stanza(message);
     xmpp_stanza_release(message);
 }
 
 void
-message_send_groupchat(const char *const roomjid, const char *const msg)
+message_send_groupchat(const char *const roomjid, const char *const msg, const char *const oob_url)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
     char *id = create_unique_id("muc");
     xmpp_stanza_t *message = stanza_create_message(ctx, id, roomjid, STANZA_TYPE_GROUPCHAT, msg);
     free(id);
 
+    if (oob_url) {
+        stanza_attach_x_oob_url(ctx, message, oob_url);
+    }
+
     _send_message_stanza(message);
     xmpp_stanza_release(message);
 }
diff --git a/src/xmpp/stanza.c b/src/xmpp/stanza.c
index 618c455f..69f0b174 100644
--- a/src/xmpp/stanza.c
+++ b/src/xmpp/stanza.c
@@ -32,10 +32,15 @@
  *
  */
 
+#define _GNU_SOURCE 1
+
 #include "config.h"
 
 #include <stdlib.h>
 #include <string.h>
+#include <stdio.h>
+#include <libgen.h>
+#include <inttypes.h>
 
 #include <glib.h>
 
@@ -207,6 +212,61 @@ stanza_create_bookmarks_pubsub_add(xmpp_ctx_t *ctx, const char *const jid,
 #endif
 
 xmpp_stanza_t*
+stanza_create_http_upload_request(xmpp_ctx_t *ctx, const char *const id,
+    const char *const jid, HTTPUpload *upload)
+{
+    int i;
+
+    xmpp_stanza_t *iq = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(iq, STANZA_NAME_IQ);
+    xmpp_stanza_set_type(iq, STANZA_TYPE_GET);
+    xmpp_stanza_set_attribute(iq, STANZA_ATTR_TO, jid);
+    xmpp_stanza_set_id(iq, id);
+
+    xmpp_stanza_t *request = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(request, STANZA_NAME_REQUEST);
+    xmpp_stanza_set_ns(request, STANZA_NS_HTTP_UPLOAD);
+
+    xmpp_stanza_t *filename = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(filename, STANZA_NAME_FILENAME);
+    xmpp_stanza_t *filename_txt = xmpp_stanza_new(ctx);
+    char* filename_cpy = strdup(upload->filename);
+    // strip spaces from filename (servers don't spaces)
+    for (i=0; i<strlen(filename_cpy); i++) {
+        if (filename_cpy[i] == ' ') {
+            filename_cpy[i] = '_';
+        }
+    }
+    xmpp_stanza_set_text(filename_txt, basename(filename_cpy));
+    free(filename_cpy);
+    xmpp_stanza_add_child(filename, filename_txt);
+    xmpp_stanza_add_child(request, filename);
+
+    xmpp_stanza_t *size = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(size, STANZA_NAME_SIZE);
+    xmpp_stanza_t *size_txt = xmpp_stanza_new(ctx);
+    char* filesize = NULL;
+    if (asprintf(&filesize, "%jd", (intmax_t)(upload->filesize)) != -1) {
+        xmpp_stanza_set_text(size_txt, filesize);
+        free(filesize);
+    }
+    xmpp_stanza_add_child(size, size_txt);
+    xmpp_stanza_add_child(request, size);
+
+    xmpp_stanza_t *content_type = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(content_type, STANZA_NAME_CONTENT_TYPE);
+    xmpp_stanza_t *content_type_txt = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_text(content_type_txt, upload->mime_type);
+    xmpp_stanza_add_child(content_type, content_type_txt);
+    xmpp_stanza_add_child(request, content_type);
+
+    xmpp_stanza_add_child(iq, request);
+    xmpp_stanza_release(request);
+
+    return iq;
+}
+
+xmpp_stanza_t*
 stanza_enable_carbons(xmpp_ctx_t *ctx)
 {
     xmpp_stanza_t *iq = xmpp_stanza_new(ctx);
@@ -354,6 +414,30 @@ stanza_attach_receipt_request(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza)
 }
 
 xmpp_stanza_t*
+stanza_attach_x_oob_url(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza, const char *const url)
+{
+    xmpp_stanza_t *x_oob = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(x_oob, STANZA_NAME_X);
+    xmpp_stanza_set_ns(x_oob, STANZA_NS_X_OOB);
+
+    xmpp_stanza_t *surl = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(surl, STANZA_NAME_URL);
+
+    xmpp_stanza_t *surl_txt = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_text(surl_txt, url);
+    xmpp_stanza_add_child(surl, surl_txt);
+    xmpp_stanza_release(surl_txt);
+
+    xmpp_stanza_add_child(x_oob, surl);
+    xmpp_stanza_release(surl);
+
+    xmpp_stanza_add_child(stanza, x_oob);
+    xmpp_stanza_release(x_oob);
+
+    return stanza;
+}
+
+xmpp_stanza_t*
 stanza_create_message(xmpp_ctx_t *ctx, char *id, const char *const recipient,
     const char *const type, const char *const message)
 {
diff --git a/src/xmpp/stanza.h b/src/xmpp/stanza.h
index b4a51580..83f69405 100644
--- a/src/xmpp/stanza.h
+++ b/src/xmpp/stanza.h
@@ -61,6 +61,7 @@
 #define STANZA_NAME_STATUS "status"
 #define STANZA_NAME_IQ "iq"
 #define STANZA_NAME_QUERY "query"
+#define STANZA_NAME_REQUEST "request"
 #define STANZA_NAME_DELAY "delay"
 #define STANZA_NAME_ERROR "error"
 #define STANZA_NAME_PING "ping"
@@ -87,6 +88,13 @@
 #define STANZA_NAME_ACTOR "actor"
 #define STANZA_NAME_ENABLE "enable"
 #define STANZA_NAME_DISABLE "disable"
+#define STANZA_NAME_FILENAME "filename"
+#define STANZA_NAME_SIZE "size"
+#define STANZA_NAME_CONTENT_TYPE "content-type"
+#define STANZA_NAME_SLOT "slot"
+#define STANZA_NAME_PUT "put"
+#define STANZA_NAME_GET "get"
+#define STANZA_NAME_URL "url"
 
 // error conditions
 #define STANZA_NAME_BAD_REQUEST "bad-request"
@@ -171,6 +179,8 @@
 #define STANZA_NS_RECEIPTS "urn:xmpp:receipts"
 #define STANZA_NS_SIGNED "jabber:x:signed"
 #define STANZA_NS_ENCRYPTED "jabber:x:encrypted"
+#define STANZA_NS_HTTP_UPLOAD "urn:xmpp:http:upload"
+#define STANZA_NS_X_OOB "jabber:x:oob"
 
 #define STANZA_DATAFORM_SOFTWARE "urn:xmpp:dataforms:softwareinfo"
 
@@ -195,6 +205,8 @@ typedef enum {
 
 xmpp_stanza_t* stanza_create_bookmarks_storage_request(xmpp_ctx_t *ctx);
 
+xmpp_stanza_t* stanza_create_http_upload_request(xmpp_ctx_t *ctx, const char *const id, const char *const jid, HTTPUpload *upload);
+
 xmpp_stanza_t* stanza_enable_carbons(xmpp_ctx_t *ctx);
 
 xmpp_stanza_t* stanza_disable_carbons(xmpp_ctx_t *ctx);
@@ -207,6 +219,7 @@ xmpp_stanza_t* stanza_attach_carbons_private(xmpp_ctx_t *ctx, xmpp_stanza_t *sta
 xmpp_stanza_t* stanza_attach_hints_no_copy(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza);
 xmpp_stanza_t* stanza_attach_hints_no_store(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza);
 xmpp_stanza_t* stanza_attach_receipt_request(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza);
+xmpp_stanza_t* stanza_attach_x_oob_url(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza, const char *const url);
 
 xmpp_stanza_t* stanza_create_message(xmpp_ctx_t *ctx, char *id,
     const char *const recipient, const char *const type, const char *const message);
diff --git a/src/xmpp/xmpp.h b/src/xmpp/xmpp.h
index 9dcc1798..ea8a6e1d 100644
--- a/src/xmpp/xmpp.h
+++ b/src/xmpp/xmpp.h
@@ -49,6 +49,7 @@
 #include "contact.h"
 #include "jid.h"
 #include "tools/autocomplete.h"
+#include "tools/http_upload.h"
 
 #define JABBER_PRIORITY_MIN -128
 #define JABBER_PRIORITY_MAX 127
@@ -95,47 +96,10 @@ typedef struct disco_identity_t {
     char *category;
 } DiscoIdentity;
 
-typedef enum {
-    FIELD_HIDDEN,
-    FIELD_TEXT_SINGLE,
-    FIELD_TEXT_PRIVATE,
-    FIELD_TEXT_MULTI,
-    FIELD_BOOLEAN,
-    FIELD_LIST_SINGLE,
-    FIELD_LIST_MULTI,
-    FIELD_JID_SINGLE,
-    FIELD_JID_MULTI,
-    FIELD_FIXED,
-    FIELD_UNKNOWN
-} form_field_type_t;
-
-typedef struct form_option_t {
-    char *label;
-    char *value;
-} FormOption;
-
-typedef struct form_field_t {
-    char *label;
-    char *type;
-    form_field_type_t type_t;
-    char *var;
-    char *description;
-    gboolean required;
-    GSList *values;
-    GSList *options;
-    Autocomplete value_ac;
-} FormField;
-
-typedef struct data_form_t {
-    char *type;
-    char *title;
-    char *instructions;
-    GSList *fields;
-    GHashTable *var_to_tag;
-    GHashTable *tag_to_var;
-    Autocomplete tag_ac;
-    gboolean modified;
-} DataForm;
+typedef struct disco_info_t {
+    char *item;
+    GHashTable *features;
+} DiscoInfo;
 
 // connection functions
 void jabber_init(void);
@@ -150,6 +114,8 @@ const char* jabber_get_fulljid(void);
 const char* jabber_get_domain(void);
 jabber_conn_status_t jabber_get_connection_status(void);
 void jabber_set_connection_status(jabber_conn_status_t status);
+GSList* jabber_get_disco_items(void);
+void jabber_set_disco_items(GSList *disco_items);
 char* jabber_get_presence_message(void);
 char* jabber_get_account_name(void);
 GList* jabber_get_available_resources(void);
@@ -162,11 +128,11 @@ gboolean jabber_conn_is_secured(void);
 gboolean jabber_send_stanza(const char *const stanza);
 
 // message functions
-char* message_send_chat(const char *const barejid, const char *const msg);
+char* message_send_chat(const char *const barejid, const char *const msg, const char *const oob_url);
 char* message_send_chat_otr(const char *const barejid, const char *const msg);
 char* message_send_chat_pgp(const char *const barejid, const char *const msg);
-void message_send_private(const char *const fulljid, const char *const msg);
-void message_send_groupchat(const char *const roomjid, const char *const msg);
+void message_send_private(const char *const fulljid, const char *const msg, const char *const oob_url);
+void message_send_groupchat(const char *const roomjid, const char *const msg, const char *const oob_url);
 void message_send_groupchat_subject(const char *const roomjid, const char *const subject);
 
 void message_send_inactive(const char *const jid);
@@ -194,7 +160,9 @@ void iq_disable_carbons(void);
 void iq_send_software_version(const char *const fulljid);
 void iq_room_list_request(gchar *conferencejid);
 void iq_disco_info_request(gchar *jid);
+void iq_disco_info_request_onconnect(gchar *jid);
 void iq_disco_items_request(gchar *jid);
+void iq_disco_items_request_onconnect(gchar *jid);
 void iq_last_activity_request(gchar *jid);
 void iq_set_autoping(int seconds);
 void iq_confirm_instant_room(const char *const room_jid);
@@ -216,6 +184,7 @@ void iq_room_kick_occupant(const char *const room, const char *const nick, const
 void iq_room_role_set(const char *const room, const char *const nick, char *role, const char *const reason);
 void iq_room_role_list(const char * const room, char *role);
 void iq_autoping_check(void);
+void iq_http_upload_request(HTTPUpload *upload);
 
 // caps functions
 Capabilities* caps_lookup(const char *const jid);
diff --git a/tests/functionaltests/test_muc.c b/tests/functionaltests/test_muc.c
index 002b052d..4ee0e698 100644
--- a/tests/functionaltests/test_muc.c
+++ b/tests/functionaltests/test_muc.c
@@ -95,8 +95,8 @@ shows_role_and_affiliation_on_join(void **state)
 {
     prof_connect();
 
-    stbbr_for_id("prof_join_2",
-        "<presence id='prof_join_2' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
+    stbbr_for_id("prof_join_3",
+        "<presence id='prof_join_3' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://www.profanity.im' ver='*'/>"
             "<x xmlns='http://jabber.org/protocol/muc#user'>"
                 "<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -115,8 +115,8 @@ shows_subject_on_join(void **state)
 {
     prof_connect();
 
-    stbbr_for_id("prof_join_2",
-        "<presence id='prof_join_2' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
+    stbbr_for_id("prof_join_3",
+        "<presence id='prof_join_3' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://www.profanity.im' ver='*'/>"
             "<x xmlns='http://jabber.org/protocol/muc#user'>"
                 "<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -143,8 +143,8 @@ shows_history_message(void **state)
 {
     prof_connect();
 
-    stbbr_for_id("prof_join_2",
-        "<presence id='prof_join_2' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
+    stbbr_for_id("prof_join_3",
+        "<presence id='prof_join_3' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://www.profanity.im' ver='*'/>"
             "<x xmlns='http://jabber.org/protocol/muc#user'>"
                 "<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -172,8 +172,8 @@ shows_occupant_join(void **state)
 {
     prof_connect();
 
-    stbbr_for_id("prof_join_2",
-        "<presence id='prof_join_2' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
+    stbbr_for_id("prof_join_3",
+        "<presence id='prof_join_3' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://www.profanity.im' ver='*'/>"
             "<x xmlns='http://jabber.org/protocol/muc#user'>"
                 "<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -201,8 +201,8 @@ shows_message(void **state)
 {
     prof_connect();
 
-    stbbr_for_id("prof_join_2",
-        "<presence id='prof_join_2' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
+    stbbr_for_id("prof_join_3",
+        "<presence id='prof_join_3' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://www.profanity.im' ver='*'/>"
             "<x xmlns='http://jabber.org/protocol/muc#user'>"
                 "<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -228,8 +228,8 @@ shows_all_messages_in_console_when_window_not_focussed(void **state)
 {
     prof_connect();
 
-    stbbr_for_id("prof_join_2",
-        "<presence id='prof_join_2' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
+    stbbr_for_id("prof_join_3",
+        "<presence id='prof_join_3' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://www.profanity.im' ver='*'/>"
             "<x xmlns='http://jabber.org/protocol/muc#user'>"
                 "<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -269,8 +269,8 @@ shows_first_message_in_console_when_window_not_focussed(void **state)
     prof_input("/console muc first");
     assert_true(prof_output_exact("Console MUC messages set: first"));
 
-    stbbr_for_id("prof_join_2",
-        "<presence id='prof_join_2' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
+    stbbr_for_id("prof_join_3",
+        "<presence id='prof_join_3' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://www.profanity.im' ver='*'/>"
             "<x xmlns='http://jabber.org/protocol/muc#user'>"
                 "<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -315,8 +315,8 @@ shows_no_message_in_console_when_window_not_focussed(void **state)
     prof_input("/console muc none");
     assert_true(prof_output_exact("Console MUC messages set: none"));
 
-    stbbr_for_id("prof_join_2",
-        "<presence id='prof_join_2' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
+    stbbr_for_id("prof_join_3",
+        "<presence id='prof_join_3' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://www.profanity.im' ver='*'/>"
             "<x xmlns='http://jabber.org/protocol/muc#user'>"
                 "<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
diff --git a/tests/functionaltests/test_ping.c b/tests/functionaltests/test_ping.c
index a3f3458c..8a065f15 100644
--- a/tests/functionaltests/test_ping.c
+++ b/tests/functionaltests/test_ping.c
@@ -14,18 +14,18 @@
 void
 ping_multiple(void **state)
 {
-    stbbr_for_id("prof_ping_2",
-        "<iq id='prof_ping_2' type='result' to='stabber@localhost/profanity'/>"
-    );
     stbbr_for_id("prof_ping_3",
         "<iq id='prof_ping_3' type='result' to='stabber@localhost/profanity'/>"
     );
+    stbbr_for_id("prof_ping_4",
+        "<iq id='prof_ping_4' type='result' to='stabber@localhost/profanity'/>"
+    );
 
     prof_connect();
 
     prof_input("/ping");
     assert_true(stbbr_received(
-        "<iq id='prof_ping_2' type='get'>"
+        "<iq id='prof_ping_3' type='get'>"
             "<ping xmlns='urn:xmpp:ping'/>"
         "</iq>"
     ));
@@ -33,7 +33,7 @@ ping_multiple(void **state)
 
     prof_input("/ping");
     assert_true(stbbr_received(
-        "<iq id='prof_ping_3' type='get'>"
+        "<iq id='prof_ping_4' type='get'>"
             "<ping xmlns='urn:xmpp:ping'/>"
         "</iq>"
     ));
diff --git a/tests/functionaltests/test_presence.c b/tests/functionaltests/test_presence.c
index 5b51113c..1a933134 100644
--- a/tests/functionaltests/test_presence.c
+++ b/tests/functionaltests/test_presence.c
@@ -19,7 +19,7 @@ presence_online(void **state)
     prof_input("/online");
 
     assert_true(stbbr_received(
-        "<presence id='prof_presence_2'>"
+        "<presence id='prof_presence_3'>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://www.profanity.im'/>"
         "</presence>"
     ));
@@ -35,7 +35,7 @@ presence_online_with_message(void **state)
     prof_input("/online \"Hi there\"");
 
     assert_true(stbbr_received(
-        "<presence id='prof_presence_2'>"
+        "<presence id='prof_presence_3'>"
             "<status>Hi there</status>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://www.profanity.im'/>"
         "</presence>"
@@ -52,7 +52,7 @@ presence_away(void **state)
     prof_input("/away");
 
     assert_true(stbbr_received(
-        "<presence id='prof_presence_2'>"
+        "<presence id='prof_presence_3'>"
             "<show>away</show>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://www.profanity.im'/>"
         "</presence>"
@@ -69,7 +69,7 @@ presence_away_with_message(void **state)
     prof_input("/away \"I'm not here for a bit\"");
 
     assert_true(stbbr_received(
-        "<presence id='prof_presence_2'>"
+        "<presence id='prof_presence_3'>"
             "<show>away</show>"
             "<status>I'm not here for a bit</status>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://www.profanity.im'/>"
@@ -87,7 +87,7 @@ presence_xa(void **state)
     prof_input("/xa");
 
     assert_true(stbbr_received(
-        "<presence id='prof_presence_2'>"
+        "<presence id='prof_presence_3'>"
             "<show>xa</show>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://www.profanity.im'/>"
         "</presence>"
@@ -104,7 +104,7 @@ presence_xa_with_message(void **state)
     prof_input("/xa \"Gone to the shops\"");
 
     assert_true(stbbr_received(
-        "<presence id='prof_presence_2'>"
+        "<presence id='prof_presence_3'>"
             "<show>xa</show>"
             "<status>Gone to the shops</status>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://www.profanity.im'/>"
@@ -122,7 +122,7 @@ presence_dnd(void **state)
     prof_input("/dnd");
 
     assert_true(stbbr_received(
-        "<presence id='prof_presence_2'>"
+        "<presence id='prof_presence_3'>"
             "<show>dnd</show>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://www.profanity.im'/>"
         "</presence>"
@@ -139,7 +139,7 @@ presence_dnd_with_message(void **state)
     prof_input("/dnd \"Working\"");
 
     assert_true(stbbr_received(
-        "<presence id='prof_presence_2'>"
+        "<presence id='prof_presence_3'>"
             "<show>dnd</show>"
             "<status>Working</status>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://www.profanity.im'/>"
@@ -157,7 +157,7 @@ presence_chat(void **state)
     prof_input("/chat");
 
     assert_true(stbbr_received(
-        "<presence id='prof_presence_2'>"
+        "<presence id='prof_presence_3'>"
             "<show>chat</show>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://www.profanity.im'/>"
         "</presence>"
@@ -174,7 +174,7 @@ presence_chat_with_message(void **state)
     prof_input("/chat \"Free to talk\"");
 
     assert_true(stbbr_received(
-        "<presence id='prof_presence_2'>"
+        "<presence id='prof_presence_3'>"
             "<show>chat</show>"
             "<status>Free to talk</status>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://www.profanity.im'/>"
@@ -192,7 +192,7 @@ presence_set_priority(void **state)
     prof_input("/priority 25");
 
     assert_true(stbbr_received(
-        "<presence id='prof_presence_2'>"
+        "<presence id='prof_presence_3'>"
             "<priority>25</priority>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://www.profanity.im'/>"
         "</presence>"
@@ -208,7 +208,7 @@ presence_includes_priority(void **state)
 
     prof_input("/priority 25");
     assert_true(stbbr_received(
-        "<presence id='prof_presence_2'>"
+        "<presence id='prof_presence_3'>"
             "<priority>25</priority>"
             "<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://www.profanity.im'/>"
         "</presence>"
@@ -217,7 +217,7 @@ presence_includes_priority(void **state)
 
     prof_input("/chat \"Free to talk\"");
     assert_true(stbbr_received(
-        "<presence id='prof_presence_3'>"
+        "<presence id='prof_presence_4'>"
             "<priority>25</priority>"
             "<show>chat</show>"
             "<status>Free to talk</status>"
diff --git a/tests/unittests/test_cmd_pgp.c b/tests/unittests/test_cmd_pgp.c
index b1d0ab52..68d48b0c 100644
--- a/tests/unittests/test_cmd_pgp.c
+++ b/tests/unittests/test_cmd_pgp.c
@@ -9,6 +9,7 @@
 #include "config.h"
 
 #include "command/commands.h"
+#include "xmpp/xmpp.h"
 
 #include "ui/stub_ui.h"
 
diff --git a/tests/unittests/tools/stub_http_upload.c b/tests/unittests/tools/stub_http_upload.c
new file mode 100644
index 00000000..cb7688cf
--- /dev/null
+++ b/tests/unittests/tools/stub_http_upload.c
@@ -0,0 +1,63 @@
+/*
+ * http_upload.h
+ *
+ * Copyright (C) 2012 - 2016 James Booth <boothj5@gmail.com>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity 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 Profanity.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * In addition, as a special exception, the copyright holders give permission to
+ * link the code of portions of this program with the OpenSSL library under
+ * certain conditions as described in each individual source file, and
+ * distribute linked combinations including the two.
+ *
+ * You must obey the GNU General Public License in all respects for all of the
+ * code used other than OpenSSL. If you modify file(s) with this exception, you
+ * may extend this exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this exception
+ * statement from your version. If you delete this exception statement from all
+ * source files in the program, then also delete it here.
+ *
+ */
+
+#ifndef TOOLS_HTTP_UPLOAD_H
+#define TOOLS_HTTP_UPLOAD_H
+
+#include <curl/curl.h>
+
+// forward -> ui/win_types.h
+typedef struct prof_win_t ProfWin;
+
+typedef struct http_upload_t {
+    char *filename;
+    off_t filesize;
+    curl_off_t bytes_sent;
+    char *mime_type;
+    char *get_url;
+    char *put_url;
+    ProfWin *window;
+    pthread_t worker;
+    int cancel;
+} HTTPUpload;
+
+//GSList *upload_processes;
+
+void* http_file_put(void *userdata) {}
+
+char* file_mime_type(const char* const file_name) {}
+off_t file_size(const char* const file_name) {}
+int is_regular_file(const char *filename) {}
+
+#endif
diff --git a/tests/unittests/xmpp/stub_xmpp.c b/tests/unittests/xmpp/stub_xmpp.c
index 3ba7f2b3..6437de86 100644
--- a/tests/unittests/xmpp/stub_xmpp.c
+++ b/tests/unittests/xmpp/stub_xmpp.c
@@ -71,7 +71,7 @@ jabber_send_stanza(const char *const stanza)
 }
 
 // message functions
-char* message_send_chat(const char * const barejid, const char * const msg)
+char* message_send_chat(const char * const barejid, const char * const msg, const char *const oob_url)
 {
     check_expected(barejid);
     check_expected(msg);
@@ -90,8 +90,8 @@ char* message_send_chat_pgp(const char * const barejid, const char * const msg)
     return NULL;
 }
 
-void message_send_private(const char * const fulljid, const char * const msg) {}
-void message_send_groupchat(const char * const roomjid, const char * const msg) {}
+void message_send_private(const char * const fulljid, const char * const msg, const char *const oob_url) {}
+void message_send_groupchat(const char * const roomjid, const char * const msg, const char *const oob_url) {}
 void message_send_groupchat_subject(const char * const roomjid, const char * const subject) {}
 
 void message_send_inactive(const char * const barejid) {}
@@ -158,6 +158,7 @@ void iq_room_list_request(gchar *conferencejid)
 void iq_disco_info_request(gchar *jid) {}
 void iq_disco_items_request(gchar *jid) {}
 void iq_set_autoping(int seconds) {}
+void iq_http_upload_request(HTTPUpload *upload) {}
 void iq_confirm_instant_room(const char * const room_jid) {}
 void iq_destroy_room(const char * const room_jid) {}
 void iq_request_room_config_form(const char * const room_jid) {}