about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/command/command.c82
-rw-r--r--src/command/commands.c333
-rw-r--r--src/command/commands.h1
-rw-r--r--src/main.c1
-rw-r--r--src/server_events.c30
-rw-r--r--src/server_events.h3
-rw-r--r--src/tools/autocomplete.c10
-rw-r--r--src/ui/core.c319
-rw-r--r--src/ui/statusbar.c9
-rw-r--r--src/ui/titlebar.c10
-rw-r--r--src/ui/ui.h9
-rw-r--r--src/ui/window.c2
-rw-r--r--src/ui/window.h3
-rw-r--r--src/ui/windows.c21
-rw-r--r--src/xmpp/capabilities.c13
-rw-r--r--src/xmpp/form.c628
-rw-r--r--src/xmpp/form.h43
-rw-r--r--src/xmpp/iq.c125
-rw-r--r--src/xmpp/stanza.c161
-rw-r--r--src/xmpp/stanza.h17
-rw-r--r--src/xmpp/xmpp.h59
21 files changed, 1718 insertions, 161 deletions
diff --git a/src/command/command.c b/src/command/command.c
index 6d707afe..6e233238 100644
--- a/src/command/command.c
+++ b/src/command/command.c
@@ -65,6 +65,7 @@
 #include "xmpp/xmpp.h"
 #include "xmpp/bookmark.h"
 #include "ui/ui.h"
+#include "ui/windows.h"
 
 typedef char*(*autocompleter)(char*, int*);
 
@@ -86,7 +87,7 @@ static char * _statuses_autocomplete(char *input, int *size);
 static char * _alias_autocomplete(char *input, int *size);
 static char * _join_autocomplete(char *input, int *size);
 static char * _log_autocomplete(char *input, int *size);
-static char * _room_autocomplete(char *input, int *size);
+static char * _form_autocomplete(char *input, int *size);
 
 GHashTable *commands = NULL;
 
@@ -305,11 +306,27 @@ static struct cmd_t command_defs[] =
           NULL } } },
 
     { "/room",
-        cmd_room, parse_args, 2, 2, NULL,
-        { "/room config accept|cancel", "Room configuration.",
-        { "/room config accept|cncel",
-          "-------------------------",
-          "Accept or cancel default room configuration.",
+        cmd_room, parse_args, 1, 1, NULL,
+        { "/room accept|destroy|config", "Room configuration.",
+        { "/room accept|destroy|config",
+          "---------------------------",
+          "accept  - Accept default room configuration.",
+          "destroy - Reject default room configuration.",
+          "config  - Edit room configuration.",
+          NULL } } },
+
+    { "/form",
+        cmd_form, parse_args, 1, 3, NULL,
+        { "/form show|submit|cancel|set|add|remove|help [tag] [value]", "Form manipulation.",
+        { "/form show|submit|cancel|set|add|remove|help [tag] [value]",
+          "----------------------------------------------------------",
+          "set tag value    - Set tagged form field to value.",
+          "add tag value    - Add value to tagged form field.",
+          "remove tag value - Remove value from tagged form field.",
+          "show             - Show the current form.",
+          "submit           - Submit the current form.",
+          "cancel           - Cancel changes to the current form.",
+          "help [tag]       - Display help for form, or a specific field.",
           NULL } } },
 
     { "/rooms",
@@ -953,7 +970,7 @@ static Autocomplete alias_ac;
 static Autocomplete aliases_ac;
 static Autocomplete join_property_ac;
 static Autocomplete room_ac;
-static Autocomplete room_config_ac;
+static Autocomplete form_ac;
 
 /*
  * Initialise command autocompleter and history
@@ -1206,11 +1223,18 @@ cmd_init(void)
     autocomplete_add(alias_ac, "list");
 
     room_ac = autocomplete_new();
+    autocomplete_add(room_ac, "accept");
+    autocomplete_add(room_ac, "destroy");
     autocomplete_add(room_ac, "config");
 
-    room_config_ac = autocomplete_new();
-    autocomplete_add(room_config_ac, "accept");
-    autocomplete_add(room_config_ac, "cancel");
+    form_ac = autocomplete_new();
+    autocomplete_add(form_ac, "submit");
+    autocomplete_add(form_ac, "cancel");
+    autocomplete_add(form_ac, "show");
+    autocomplete_add(form_ac, "set");
+    autocomplete_add(form_ac, "add");
+    autocomplete_add(form_ac, "remove");
+    autocomplete_add(form_ac, "help");
 
     cmd_history_init();
 }
@@ -1256,7 +1280,7 @@ cmd_uninit(void)
     autocomplete_free(aliases_ac);
     autocomplete_free(join_property_ac);
     autocomplete_free(room_ac);
-    autocomplete_free(room_config_ac);
+    autocomplete_free(form_ac);
 }
 
 gboolean
@@ -1381,7 +1405,7 @@ cmd_reset_autocomplete()
     autocomplete_reset(aliases_ac);
     autocomplete_reset(join_property_ac);
     autocomplete_reset(room_ac);
-    autocomplete_reset(room_config_ac);
+    autocomplete_reset(form_ac);
     bookmark_autocomplete_reset();
 }
 
@@ -1631,8 +1655,8 @@ _cmd_complete_parameters(char *input, int *size)
         }
     }
 
-    gchar *cmds[] = { "/help", "/prefs", "/disco", "/close", "/wins" };
-    Autocomplete completers[] = { help_ac, prefs_ac, disco_ac, close_ac, wins_ac };
+    gchar *cmds[] = { "/help", "/prefs", "/disco", "/close", "/wins", "/room" };
+    Autocomplete completers[] = { help_ac, prefs_ac, disco_ac, close_ac, wins_ac, room_ac };
 
     for (i = 0; i < ARRAY_SIZE(cmds); i++) {
         result = autocomplete_param_with_ac(input, size, cmds[i], completers[i], TRUE);
@@ -1660,7 +1684,7 @@ _cmd_complete_parameters(char *input, int *size)
     g_hash_table_insert(ac_funcs, "/statuses",      _statuses_autocomplete);
     g_hash_table_insert(ac_funcs, "/alias",         _alias_autocomplete);
     g_hash_table_insert(ac_funcs, "/join",          _join_autocomplete);
-    g_hash_table_insert(ac_funcs, "/room",          _room_autocomplete);
+    g_hash_table_insert(ac_funcs, "/form",          _form_autocomplete);
 
     char parsed[*size+1];
     i = 0;
@@ -2071,16 +2095,34 @@ _theme_autocomplete(char *input, int *size)
 }
 
 static char *
-_room_autocomplete(char *input, int *size)
+_form_autocomplete(char *input, int *size)
 {
     char *result = NULL;
 
-    result = autocomplete_param_with_ac(input, size, "/room config", room_config_ac, TRUE);
-    if (result != NULL) {
-        return result;
+    ProfWin *current = wins_get_current();
+    if (current != NULL) {
+        DataForm *form = current->form;
+        if (form != NULL) {
+            result = autocomplete_param_with_ac(input, size, "/form set", form->tag_ac, TRUE);
+            if (result != NULL) {
+                return result;
+            }
+            result = autocomplete_param_with_ac(input, size, "/form add", form->tag_ac, TRUE);
+            if (result != NULL) {
+                return result;
+            }
+            result = autocomplete_param_with_ac(input, size, "/form remove", form->tag_ac, TRUE);
+            if (result != NULL) {
+                return result;
+            }
+            result = autocomplete_param_with_ac(input, size, "/form help", form->tag_ac, TRUE);
+            if (result != NULL) {
+                return result;
+            }
+        }
     }
 
-    result = autocomplete_param_with_ac(input, size, "/room", room_ac, TRUE);
+    result = autocomplete_param_with_ac(input, size, "/form", form_ac, TRUE);
     if (result != NULL) {
         return result;
     }
diff --git a/src/command/commands.c b/src/command/commands.c
index 7b49ad61..fa439955 100644
--- a/src/command/commands.c
+++ b/src/command/commands.c
@@ -634,12 +634,9 @@ cmd_help(gchar **args, struct cmd_help_t help)
         }
 
         cons_show("");
-
         if (help_text != NULL) {
-            int i;
-            for (i = 0; help_text[i] != NULL; i++) {
-                cons_show(help_text[i]);
-            }
+            ProfWin *console = wins_get_console();
+            ui_show_lines(console, help_text);
         } else {
             cons_show("No such command.");
         }
@@ -1788,7 +1785,7 @@ cmd_decline(gchar **args, struct cmd_help_t help)
 }
 
 gboolean
-cmd_room(gchar **args, struct cmd_help_t help)
+cmd_form(gchar **args, struct cmd_help_t help)
 {
     jabber_conn_status_t conn_status = jabber_get_connection_status();
 
@@ -1798,18 +1795,283 @@ cmd_room(gchar **args, struct cmd_help_t help)
     }
 
     win_type_t win_type = ui_current_win_type();
-    if (win_type != WIN_MUC) {
-        cons_show("Command /room only usable in chat rooms.");
+    if (win_type != WIN_MUC_CONFIG) {
+        cons_show("Command '/form' does not apply to this window.");
         return TRUE;
     }
 
-    if (g_strcmp0(args[0], "config") != 0) {
+    if ((g_strcmp0(args[0], "submit") != 0) &&
+            (g_strcmp0(args[0], "cancel") != 0) &&
+            (g_strcmp0(args[0], "show") != 0) &&
+            (g_strcmp0(args[0], "help") != 0) &&
+            (g_strcmp0(args[0], "set") != 0) &&
+            (g_strcmp0(args[0], "add") != 0) &&
+            (g_strcmp0(args[0], "remove") != 0)) {
         cons_show("Usage: %s", help.usage);
         return TRUE;
     }
 
-    if ((g_strcmp0(args[1], "accept") != 0) &&
-            (g_strcmp0(args[1], "cancel") != 0)) {
+    char *recipient = ui_current_recipient();
+    ProfWin *current = wins_get_current();
+    gchar **split_recipient = g_strsplit(recipient, " ", 2);
+    char *room = split_recipient[0];
+
+    if (g_strcmp0(args[0], "show") == 0) {
+        ui_show_form(current, room, current->form);
+        g_strfreev(split_recipient);
+        return TRUE;
+    }
+
+    if (g_strcmp0(args[0], "help") == 0) {
+        char *tag = args[1];
+        if (tag != NULL) {
+            ui_show_form_field_help(current, current->form, tag);
+        } else {
+            ui_show_form_help(current, current->form);
+
+            const gchar **help_text = NULL;
+            Command *command = g_hash_table_lookup(commands, "/form");
+
+            if (command != NULL) {
+                help_text = command->help.long_help;
+            }
+
+            ui_show_lines(current, help_text);
+        }
+        ui_current_print_line("");
+        g_strfreev(split_recipient);
+        return TRUE;
+    }
+
+    if (g_strcmp0(args[0], "submit") == 0) {
+        iq_submit_room_config(room, current->form);
+
+    }
+    if (g_strcmp0(args[0], "cancel") == 0) {
+        iq_room_config_cancel(room);
+    }
+    if (g_strcmp0(args[0], "set") == 0) {
+        char *tag = NULL;
+        char *value = NULL;
+        if (args[1] != NULL) {
+            tag = args[1];
+        } else {
+            ui_current_print_line("/room set command requires a field tag and value");
+            g_strfreev(split_recipient);
+            return TRUE;
+        }
+        if (args[2] != NULL) {
+            value = args[2];
+        } else {
+            ui_current_print_line("/room set command requires a field tag and value");
+            g_strfreev(split_recipient);
+            return TRUE;
+        }
+        if (!form_tag_exists(current->form, tag)) {
+            ui_current_print_line("Form does not contain a field with tag %s", tag);
+        } else {
+            form_field_type_t field_type = form_get_field_type(current->form, tag);
+            gboolean valid = FALSE;
+            switch (field_type) {
+            case FIELD_TEXT_SINGLE:
+            case FIELD_TEXT_PRIVATE:
+            case FIELD_JID_SINGLE:
+                form_set_value(current->form, tag, value);
+                ui_show_form_field(current, current->form, tag);
+                break;
+            case FIELD_BOOLEAN:
+                if (g_strcmp0(value, "on") == 0) {
+                    form_set_value(current->form, tag, "1");
+                    ui_show_form_field(current, current->form, tag);
+                } else if (g_strcmp0(value, "off") == 0) {
+                    form_set_value(current->form, tag, "0");
+                    ui_show_form_field(current, current->form, tag);
+                } else {
+                    ui_current_print_line("Value %s not valid for boolean field: %s", value, tag);
+                }
+                break;
+            case FIELD_LIST_SINGLE:
+                valid = form_field_contains_option(current->form, tag, value);
+                if (valid == TRUE) {
+                    form_set_value(current->form, tag, value);
+                    ui_show_form_field(current, current->form, tag);
+                } else {
+                    ui_current_print_line("Value %s not a valid option for field: %s", value, tag);
+                }
+                break;
+            default:
+                ui_current_print_line("Set command not valid for field: %s", tag);
+                break;
+            }
+        }
+    }
+
+    if (g_strcmp0(args[0], "add") == 0) {
+        char *tag = NULL;
+        char *value = NULL;
+        if (args[1] != NULL) {
+            tag = args[1];
+        } else {
+            ui_current_print_line("/room add command requires a field tag and value");
+            g_strfreev(split_recipient);
+            return TRUE;
+        }
+        if (args[2] != NULL) {
+            value = args[2];
+        } else {
+            ui_current_print_line("/room add command requires a field tag and value");
+            g_strfreev(split_recipient);
+            return TRUE;
+        }
+        if (!form_tag_exists(current->form, tag)) {
+            ui_current_print_line("Form does not contain a field with tag %s", tag);
+        } else {
+            form_field_type_t field_type = form_get_field_type(current->form, tag);
+            gboolean valid = FALSE;
+            gboolean added = FALSE;
+            switch (field_type) {
+            case FIELD_LIST_MULTI:
+                valid = form_field_contains_option(current->form, tag, value);
+                if (valid) {
+                    added = form_add_unique_value(current->form, tag, value);
+                    if (added) {
+                        ui_show_form_field(current, current->form, tag);
+                    } else {
+                        ui_current_print_line("Value %s already selected for %s", value, tag);
+                    }
+                } else {
+                    ui_current_print_line("Value %s not a valid option for field: %s", value, tag);
+                }
+                break;
+            case FIELD_TEXT_MULTI:
+                form_add_value(current->form, tag, value);
+                ui_show_form_field(current, current->form, tag);
+                break;
+            case FIELD_JID_MULTI:
+                added = form_add_unique_value(current->form, tag, value);
+                if (added) {
+                    ui_show_form_field(current, current->form, tag);
+                } else {
+                    ui_current_print_line("JID %s already exists in %s", value, tag);
+                }
+                break;
+            default:
+                ui_current_print_line("Add command not valid for field: %s", tag);
+                break;
+            }
+        }
+    }
+
+    if (g_strcmp0(args[0], "remove") == 0) {
+        char *tag = NULL;
+        char *value = NULL;
+        if (args[1] != NULL) {
+            tag = args[1];
+        } else {
+            ui_current_print_line("/room remove command requires a field tag and value");
+            g_strfreev(split_recipient);
+            return TRUE;
+        }
+        if (args[2] != NULL) {
+            value = args[2];
+        } else {
+            ui_current_print_line("/room remove command requires a field tag and value");
+            g_strfreev(split_recipient);
+            return TRUE;
+        }
+        if (!form_tag_exists(current->form, tag)) {
+            ui_current_print_line("Form does not contain a field with tag %s", tag);
+        } else {
+            form_field_type_t field_type = form_get_field_type(current->form, tag);
+            gboolean valid = FALSE;
+            gboolean removed = FALSE;
+            switch (field_type) {
+            case FIELD_LIST_MULTI:
+                valid = form_field_contains_option(current->form, tag, value);
+                if (valid == TRUE) {
+                    removed = form_remove_value(current->form, tag, value);
+                    if (removed) {
+                        ui_show_form_field(current, current->form, tag);
+                    } else {
+                        ui_current_print_line("Value %s is not currently set for %s", value, tag);
+                    }
+                } else {
+                    ui_current_print_line("Value %s not a valid option for field: %s", value, tag);
+                }
+                break;
+            case FIELD_TEXT_MULTI:
+                if (!g_str_has_prefix(value, "val")) {
+                    ui_current_print_line("No such value %s for %s", value, tag);
+                    break;
+                }
+                if (strlen(value) < 4) {
+                    ui_current_print_line("No such value %s for %s", value, tag);
+                    break;
+                }
+
+                int index = strtol(&value[3], NULL, 10);
+                if ((index < 1) || (index > form_get_value_count(current->form, tag))) {
+                    ui_current_print_line("No such value %s for %s", value, tag);
+                    break;
+                }
+
+                removed = form_remove_text_multi_value(current->form, tag, index);
+                if (removed) {
+                    ui_show_form_field(current, current->form, tag);
+                } else {
+                    ui_current_print_line("Could not remove %s from %s", value, tag);
+                }
+                break;
+            case FIELD_JID_MULTI:
+                removed = form_remove_value(current->form, tag, value);
+                if (removed) {
+                    ui_show_form_field(current, current->form, tag);
+                } else {
+                    ui_current_print_line("Field %s does not contain %s", tag, value);
+                }
+                break;
+            default:
+                ui_current_print_line("Remove command not valid for field: %s", tag);
+                break;
+            }
+        }
+    }
+
+    if ((g_strcmp0(args[0], "submit") == 0) ||
+            (g_strcmp0(args[0], "cancel") == 0)) {
+        wins_close_current();
+        current = wins_get_by_recipient(room);
+        if (current == NULL) {
+            current = wins_get_console();
+        }
+        int num = wins_get_num(current);
+        ui_switch_win(num);
+    }
+
+    g_strfreev(split_recipient);
+
+    return TRUE;
+}
+
+gboolean
+cmd_room(gchar **args, struct cmd_help_t help)
+{
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+
+    if (conn_status != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    win_type_t win_type = ui_current_win_type();
+    if (win_type != WIN_MUC) {
+        cons_show("Command '/room' does not apply to this window.");
+        return TRUE;
+    }
+
+    if ((g_strcmp0(args[0], "accept") != 0) &&
+            (g_strcmp0(args[0], "destroy") != 0) &&
+            (g_strcmp0(args[0], "config") != 0)) {
         cons_show("Usage: %s", help.usage);
         return TRUE;
     }
@@ -1817,26 +2079,42 @@ cmd_room(gchar **args, struct cmd_help_t help)
     char *room = ui_current_recipient();
     ProfWin *window = wins_get_by_recipient(room);
     int num = wins_get_num(window);
+
     int ui_index = num;
     if (ui_index == 10) {
         ui_index = 0;
     }
-    gboolean requires_config = muc_requires_config(room);
-    if (!requires_config) {
-        win_save_vprint(window, '!', NULL, 0, COLOUR_ROOMINFO, "", "Current room does not require configuration.");
-        return TRUE;
+
+    if (g_strcmp0(args[0], "accept") == 0) {
+        gboolean requires_config = muc_requires_config(room);
+        if (!requires_config) {
+            win_save_vprint(window, '!', NULL, 0, COLOUR_ROOMINFO, "", "Current room does not require configuration.");
+            return TRUE;
+        } else {
+            iq_confirm_instant_room(room);
+            muc_set_requires_config(room, FALSE);
+            win_save_vprint(window, '!', NULL, 0, COLOUR_ROOMINFO, "", "Room unlocked.");
+            cons_show("Room unlocked: %s (%d)", room, ui_index);
+            return TRUE;
+        }
     }
 
-    if (g_strcmp0(args[1], "accept") == 0) {
-        iq_confirm_instant_room(room);
-        muc_set_requires_config(room, FALSE);
-        win_save_vprint(window, '!', NULL, 0, COLOUR_ROOMINFO, "", "Room unlocked.");
-        cons_show("Room unlocked: %s (%d)", room, ui_index);
+    if (g_strcmp0(args[0], "destroy") == 0) {
+        iq_destroy_instant_room(room);
         return TRUE;
     }
 
-    if (g_strcmp0(args[1], "cancel") == 0) {
-        iq_destroy_instant_room(room);
+    if (g_strcmp0(args[0], "config") == 0) {
+        GString *win_title = g_string_new(room);
+        g_string_append(win_title, " config");
+        ProfWin *window = wins_get_by_recipient(win_title->str);
+        g_string_free(win_title, TRUE);
+        if (window != NULL) {
+            num = wins_get_num(window);
+            ui_switch_win(num);
+        } else {
+            iq_request_room_config_form(room);
+        }
         return TRUE;
     }
 
@@ -2238,6 +2516,17 @@ cmd_close(gchar **args, struct cmd_help_t help)
         return TRUE;
     }
 
+    // check for unsaved form
+    if (ui_win_has_unsaved_form(index)) {
+        ProfWin *window = wins_get_current();
+        if (wins_is_current(window)) {
+            ui_current_print_line("You have unsaved changes, use /form submit or /form cancel");
+        } else {
+            cons_show("Cannot close form window with unsaved changes, use /form submit or /form cancel");
+        }
+        return TRUE;
+    }
+
     // handle leaving rooms, or chat
     if (conn_status == JABBER_CONNECTED) {
         ui_close_connected_win(index);
diff --git a/src/command/commands.h b/src/command/commands.h
index 7eddc127..528c78aa 100644
--- a/src/command/commands.h
+++ b/src/command/commands.h
@@ -125,5 +125,6 @@ gboolean cmd_xa(gchar **args, struct cmd_help_t help);
 gboolean cmd_alias(gchar **args, struct cmd_help_t help);
 gboolean cmd_xmlconsole(gchar **args, struct cmd_help_t help);
 gboolean cmd_ping(gchar **args, struct cmd_help_t help);
+gboolean cmd_form(gchar **args, struct cmd_help_t help);
 
 #endif
diff --git a/src/main.c b/src/main.c
index 898cdaf3..e4643d2e 100644
--- a/src/main.c
+++ b/src/main.c
@@ -63,6 +63,7 @@ _init_modules(void)
     message_init_module();
     presence_init_module();
     roster_init_module();
+    form_init_module();
 
     ui_init_module();
     console_init_module();
diff --git a/src/server_events.c b/src/server_events.c
index 408e4e3e..4e48aecb 100644
--- a/src/server_events.c
+++ b/src/server_events.c
@@ -464,6 +464,36 @@ handle_room_destroy(const char * const room)
 }
 
 void
+handle_room_configure(const char * const room, DataForm *form)
+{
+    ui_handle_room_configuration(room, form);
+}
+
+void
+handle_room_config_submit_result(void)
+{
+    ui_handle_room_config_submit_result();
+}
+
+void
+handle_room_configuration_form_error(const char * const room, const char * const message)
+{
+    if (room != NULL) {
+        if (message != NULL) {
+            cons_show_error("Room config error for %s: %s.", room, message);
+        } else {
+            cons_show_error("Room config error for %s.", room);
+        }
+    } else {
+        if (message != NULL) {
+            cons_show_error("Room config error: %s.", message);
+        } else {
+            cons_show_error("Room config error.");
+        }
+    }
+}
+
+void
 handle_room_roster_complete(const char * const room)
 {
     if (muc_room_is_autojoin(room)) {
diff --git a/src/server_events.h b/src/server_events.h
index aaad9923..06e851f0 100644
--- a/src/server_events.h
+++ b/src/server_events.h
@@ -96,5 +96,8 @@ void handle_presence_error(const char *from, const char * const type,
 void handle_xmpp_stanza(const char * const msg);
 void handle_ping_result(const char * const from, int millis);
 void handle_ping_error_result(const char * const from, const char * const error);
+void handle_room_configure(const char * const room, DataForm *form);
+void handle_room_configuration_form_error(const char * const from, const char * const message);
+void handle_room_config_submit_result(void);
 
 #endif
diff --git a/src/tools/autocomplete.c b/src/tools/autocomplete.c
index 3361c10b..efff6ee0 100644
--- a/src/tools/autocomplete.c
+++ b/src/tools/autocomplete.c
@@ -62,10 +62,12 @@ autocomplete_new(void)
 void
 autocomplete_clear(Autocomplete ac)
 {
-    g_slist_free_full(ac->items, free);
-    ac->items = NULL;
+    if (ac != NULL) {
+        g_slist_free_full(ac->items, free);
+        ac->items = NULL;
 
-    autocomplete_reset(ac);
+        autocomplete_reset(ac);
+    }
 }
 
 void
@@ -335,4 +337,4 @@ _search_from(Autocomplete ac, GSList *curr, gboolean quote)
     }
 
     return NULL;
-}
\ No newline at end of file
+}
diff --git a/src/ui/core.c b/src/ui/core.c
index 83e5c1a2..53d3a2ad 100644
--- a/src/ui/core.c
+++ b/src/ui/core.c
@@ -633,7 +633,7 @@ _ui_close_all_wins(void)
 
     while (curr != NULL) {
         int num = GPOINTER_TO_INT(curr->data);
-        if (num != 1) {
+        if ((num != 1) && (!ui_win_has_unsaved_form(num))) {
             if (conn_status == JABBER_CONNECTED) {
                 ui_close_connected_win(num);
             }
@@ -660,7 +660,7 @@ _ui_close_read_wins(void)
 
     while (curr != NULL) {
         int num = GPOINTER_TO_INT(curr->data);
-        if ((num != 1) && (ui_win_unread(num) == 0)) {
+        if ((num != 1) && (ui_win_unread(num) == 0) && (!ui_win_has_unsaved_form(num))) {
             if (conn_status == JABBER_CONNECTED) {
                 ui_close_connected_win(num);
             }
@@ -676,6 +676,20 @@ _ui_close_read_wins(void)
     return count;
 }
 
+static gboolean
+_ui_win_has_unsaved_form(int num)
+{
+    ProfWin *window = wins_get_by_num(num);
+
+    if (window->type != WIN_MUC_CONFIG) {
+        return FALSE;
+    }
+    if (window->form == NULL) {
+        return FALSE;
+    }
+    return window->form->modified;
+}
+
 GString *
 _get_recipient_string(ProfWin *window)
 {
@@ -1602,7 +1616,8 @@ _ui_room_requires_config(const char * const room_jid)
             ui_index = 0;
         }
 
-        win_save_vprint(window, '!', NULL, 0, COLOUR_ROOMINFO, "", "Room requires configuration, use '/room config accept' or '/room config cancel'");
+        win_save_vprint(window, '!', NULL, 0, COLOUR_ROOMINFO, "",
+            "Room requires configuration, use '/room config accept' or '/room config destroy'");
 
         // currently in groupchat window
         if (wins_is_current(window)) {
@@ -1871,6 +1886,296 @@ _ui_draw_term_title(void)
 }
 
 static void
+_ui_handle_form_field(ProfWin *window, char *tag, FormField *field)
+{
+    win_save_vprint(window, '-', NULL, NO_EOL, COLOUR_AWAY, "", "[%s] ", tag);
+    win_save_vprint(window, '-', NULL, NO_EOL | NO_DATE, 0, "", "%s", field->label);
+    if (field->required) {
+        win_save_print(window, '-', NULL, NO_DATE | NO_EOL, 0, "", " (required): ");
+    } else {
+        win_save_print(window, '-', NULL, NO_DATE | NO_EOL, 0, "", ": ");
+    }
+
+    GSList *values = field->values;
+    GSList *curr_value = values;
+
+    switch (field->type_t) {
+    case FIELD_HIDDEN:
+        break;
+    case FIELD_TEXT_SINGLE:
+        if (curr_value != NULL) {
+            char *value = curr_value->data;
+            if (value != NULL) {
+                if (g_strcmp0(field->var, "muc#roomconfig_roomsecret") == 0) {
+                    win_save_print(window, '-', NULL, NO_DATE | NO_EOL, COLOUR_ONLINE, "", "[hidden]");
+                } else {
+                    win_save_print(window, '-', NULL, NO_DATE | NO_EOL, COLOUR_ONLINE, "", value);
+                }
+            }
+        }
+        win_save_newline(window);
+        break;
+    case FIELD_TEXT_PRIVATE:
+        if (curr_value != NULL) {
+            char *value = curr_value->data;
+            if (value != NULL) {
+                win_save_print(window, '-', NULL, NO_DATE | NO_EOL, COLOUR_ONLINE, "", "[hidden]");
+            }
+        }
+        win_save_newline(window);
+        break;
+    case FIELD_TEXT_MULTI:
+        win_save_newline(window);
+        int index = 1;
+        while (curr_value != NULL) {
+            char *value = curr_value->data;
+            GString *val_tag = g_string_new("");
+            g_string_printf(val_tag, "val%d", index++);
+            win_save_vprint(window, '-', NULL, 0, COLOUR_ONLINE, "", "  [%s] %s", val_tag->str, value);
+            g_string_free(val_tag, TRUE);
+            curr_value = g_slist_next(curr_value);
+        }
+        break;
+    case FIELD_BOOLEAN:
+        if (curr_value == NULL) {
+            win_save_print(window, '-', NULL, NO_DATE, COLOUR_OFFLINE, "", "FALSE");
+        } else {
+            char *value = curr_value->data;
+            if (value == NULL) {
+                win_save_print(window, '-', NULL, NO_DATE, COLOUR_OFFLINE, "", "FALSE");
+            } else {
+                if (g_strcmp0(value, "0") == 0) {
+                    win_save_print(window, '-', NULL, NO_DATE, COLOUR_OFFLINE, "", "FALSE");
+                } else {
+                    win_save_print(window, '-', NULL, NO_DATE, COLOUR_ONLINE, "", "TRUE");
+                }
+            }
+        }
+        break;
+    case FIELD_LIST_SINGLE:
+        if (curr_value != NULL) {
+            win_save_newline(window);
+            char *value = curr_value->data;
+            GSList *options = field->options;
+            GSList *curr_option = options;
+            while (curr_option != NULL) {
+                FormOption *option = curr_option->data;
+                if (g_strcmp0(option->value, value) == 0) {
+                    win_save_vprint(window, '-', NULL, 0, COLOUR_ONLINE, "", "  [%s] %s", option->value, option->label);
+                } else {
+                    win_save_vprint(window, '-', NULL, 0, COLOUR_OFFLINE, "", "  [%s] %s", option->value, option->label);
+                }
+                curr_option = g_slist_next(curr_option);
+            }
+        }
+        break;
+    case FIELD_LIST_MULTI:
+        if (curr_value != NULL) {
+            win_save_newline(window);
+            GSList *options = field->options;
+            GSList *curr_option = options;
+            while (curr_option != NULL) {
+                FormOption *option = curr_option->data;
+                if (g_slist_find_custom(curr_value, option->value, (GCompareFunc)g_strcmp0) != NULL) {
+                    win_save_vprint(window, '-', NULL, 0, COLOUR_ONLINE, "", "  [%s] %s", option->value, option->label);
+                } else {
+                    win_save_vprint(window, '-', NULL, 0, COLOUR_OFFLINE, "", "  [%s] %s", option->value, option->label);
+                }
+                curr_option = g_slist_next(curr_option);
+            }
+        }
+        break;
+    case FIELD_JID_SINGLE:
+        if (curr_value != NULL) {
+            char *value = curr_value->data;
+            if (value != NULL) {
+                win_save_print(window, '-', NULL, NO_DATE | NO_EOL, COLOUR_ONLINE, "", value);
+            }
+        }
+        win_save_newline(window);
+        break;
+    case FIELD_JID_MULTI:
+        win_save_newline(window);
+        while (curr_value != NULL) {
+            char *value = curr_value->data;
+            win_save_vprint(window, '-', NULL, 0, COLOUR_ONLINE, "", "  %s", value);
+            curr_value = g_slist_next(curr_value);
+        }
+        break;
+    case FIELD_FIXED:
+        if (curr_value != NULL) {
+            char *value = curr_value->data;
+            if (value != NULL) {
+                win_save_print(window, '-', NULL, NO_DATE | NO_EOL, 0, "", value);
+            }
+        }
+        win_save_newline(window);
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+_ui_show_form(ProfWin *window, const char * const room, DataForm *form)
+{
+    if (form->title != NULL) {
+        win_save_print(window, '-', NULL, 0, 0, "", form->title);
+    } else {
+        win_save_vprint(window, '-', NULL, 0, 0, "", "Configuration for room %s.", room);
+    }
+    win_save_print(window, '-', NULL, 0, 0, "", "");
+
+    ui_show_form_help(window, form);
+
+    GSList *fields = form->fields;
+    GSList *curr_field = fields;
+    while (curr_field != NULL) {
+        FormField *field = curr_field->data;
+
+        if (g_strcmp0(field->type, "hidden") != 0) {
+            char *tag = g_hash_table_lookup(form->var_to_tag, field->var);
+            _ui_handle_form_field(window, tag, field);
+        }
+
+        curr_field = g_slist_next(curr_field);
+    }
+
+    win_save_println(window, "");
+}
+
+static void
+_ui_show_form_field(ProfWin *window, DataForm *form, char *tag)
+{
+    FormField *field = form_get_field_by_tag(form, tag);
+    _ui_handle_form_field(window, tag, field);
+    win_save_println(window, "");
+}
+
+static void
+_ui_handle_room_configuration(const char * const room, DataForm *form)
+{
+    GString *title = g_string_new(room);
+    g_string_append(title, " config");
+    ProfWin *window = wins_new(title->str, WIN_MUC_CONFIG);
+    g_string_free(title, TRUE);
+
+    window->form = form;
+
+    int num = wins_get_num(window);
+    ui_switch_win(num);
+
+    ui_show_form(window, room, form);
+}
+
+static void
+_ui_handle_room_config_submit_result(void)
+{
+    cons_show("GOT ROOM CONFIG SUBMIT RESULT!!!!");
+}
+
+static void
+_ui_show_form_field_help(ProfWin *window, DataForm *form, char *tag)
+{
+    FormField *field = form_get_field_by_tag(form, tag);
+    if (field != NULL) {
+        win_save_print(window, '-', NULL, NO_EOL, 0, "", field->label);
+        if (field->required) {
+            win_save_print(window, '-', NULL, NO_DATE, 0, "", " (Required):");
+        } else {
+            win_save_print(window, '-', NULL, NO_DATE, 0, "", ":");
+        }
+        if (field->description != NULL) {
+            win_save_vprint(window, '-', NULL, 0, 0, "", "  Description : %s", field->description);
+        }
+        win_save_vprint(window, '-', NULL, 0, 0, "", "  Type        : %s", field->type);
+
+        int num_values = 0;
+        GSList *curr_option = NULL;
+        FormOption *option = NULL;
+
+        switch (field->type_t) {
+        case FIELD_TEXT_SINGLE:
+        case FIELD_TEXT_PRIVATE:
+            win_save_vprint(window, '-', NULL, 0, 0, "", "  Set         : /form set %s <value>", tag);
+            win_save_print(window, '-', NULL, 0, 0, "", "  Where       : <value> is any text");
+            break;
+        case FIELD_TEXT_MULTI:
+            num_values = form_get_value_count(form, tag);
+            win_save_vprint(window, '-', NULL, 0, 0, "", "  Add         : /form add %s <value>", tag);
+            win_save_print(window, '-', NULL, 0, 0, "", "  Where       : <value> is any text");
+            if (num_values > 0) {
+                win_save_vprint(window, '-', NULL, 0, 0, "", "  Remove      : /form remove %s <value>", tag);
+                win_save_vprint(window, '-', NULL, 0, 0, "", "  Where       : <value> between 'val1' and 'val%d'", num_values);
+            }
+            break;
+        case FIELD_BOOLEAN:
+            win_save_vprint(window, '-', NULL, 0, 0, "", "  Set         : /form set %s <value>", tag);
+            win_save_print(window, '-', NULL, 0, 0, "", "  Where       : <value> is either 'on' or 'off'");
+            break;
+        case FIELD_LIST_SINGLE:
+            win_save_vprint(window, '-', NULL, 0, 0, "", "  Set         : /form set %s <value>", tag);
+            win_save_print(window, '-', NULL, 0, 0, "", "  Where       : <value> is one of");
+            curr_option = field->options;
+            while (curr_option != NULL) {
+                option = curr_option->data;
+                win_save_vprint(window, '-', NULL, 0, 0, "", "                  %s", option->value);
+                curr_option = g_slist_next(curr_option);
+            }
+            break;
+        case FIELD_LIST_MULTI:
+            win_save_vprint(window, '-', NULL, 0, 0, "", "  Add         : /form add %s <value>", tag);
+            win_save_vprint(window, '-', NULL, 0, 0, "", "  Remove      : /form remove %s <value>", tag);
+            win_save_print(window, '-', NULL, 0, 0, "", "  Where       : <value> is one of");
+            curr_option = field->options;
+            while (curr_option != NULL) {
+                option = curr_option->data;
+                win_save_vprint(window, '-', NULL, 0, 0, "", "                  %s", option->value);
+                curr_option = g_slist_next(curr_option);
+            }
+            break;
+        case FIELD_JID_SINGLE:
+            win_save_vprint(window, '-', NULL, 0, 0, "", "  Set         : /form set %s <value>", tag);
+            win_save_print(window, '-', NULL, 0, 0, "", "  Where       : <value> is a valid Jabber ID");
+            break;
+        case FIELD_JID_MULTI:
+            win_save_vprint(window, '-', NULL, 0, 0, "", "  Add         : /form set %s <value>", tag);
+            win_save_vprint(window, '-', NULL, 0, 0, "", "  Remove      : /form set %s <value>", tag);
+            win_save_print(window, '-', NULL, 0, 0, "", "  Where       : <value> is a valid Jabber ID");
+            break;
+        case FIELD_FIXED:
+        case FIELD_UNKNOWN:
+        case FIELD_HIDDEN:
+        default:
+            break;
+        }
+    } else {
+        win_save_vprint(window, '-', NULL, 0, 0, "", "No such field %s", tag);
+    }
+}
+
+static void
+_ui_show_form_help(ProfWin *window, DataForm *form)
+{
+    if (form->instructions != NULL) {
+        win_save_print(window, '-', NULL, 0, 0, "", "Instructions:");
+        win_save_print(window, '-', NULL, 0, 0, "", form->instructions);
+        win_save_print(window, '-', NULL, 0, 0, "", "");
+    }
+}
+
+static void
+_ui_show_lines(ProfWin *window, const gchar** lines)
+{
+    if (lines != NULL) {
+        int i;
+        for (i = 0; lines[i] != NULL; i++) {
+            win_save_print(window, '-', NULL, 0, 0, "", lines[i]);
+        }
+    }
+}
+
+static void
 _win_handle_switch(const wint_t * const ch)
 {
     if (*ch == KEY_F(1)) {
@@ -2107,4 +2412,12 @@ ui_init_module(void)
     ui_update = _ui_update;
     ui_room_requires_config = _ui_room_requires_config;
     ui_room_destroyed = _ui_room_destroyed;
+    ui_handle_room_configuration = _ui_handle_room_configuration;
+    ui_handle_room_config_submit_result = _ui_handle_room_config_submit_result;
+    ui_win_has_unsaved_form = _ui_win_has_unsaved_form;
+    ui_show_form = _ui_show_form;
+    ui_show_form_field = _ui_show_form_field;
+    ui_show_form_help = _ui_show_form_help;
+    ui_show_form_field_help = _ui_show_form_field_help;
+    ui_show_lines = _ui_show_lines;
 }
diff --git a/src/ui/statusbar.c b/src/ui/statusbar.c
index 2d9ea533..0f3267b3 100644
--- a/src/ui/statusbar.c
+++ b/src/ui/statusbar.c
@@ -103,14 +103,7 @@ create_status_bar(void)
 void
 status_bar_update_virtual(void)
 {
-    GDateTime *now_time = g_date_time_new_now_local();
-    GTimeSpan elapsed = g_date_time_difference(now_time, last_time);
-
-    if (elapsed >= TIME_CHECK) {
-        _status_bar_draw();
-    }
-
-    g_date_time_unref(now_time);
+    _status_bar_draw();
 }
 
 void
diff --git a/src/ui/titlebar.c b/src/ui/titlebar.c
index ad374786..811ac2c2 100644
--- a/src/ui/titlebar.c
+++ b/src/ui/titlebar.c
@@ -86,10 +86,10 @@ title_bar_update_virtual(void)
                 g_timer_destroy(typing_elapsed);
                 typing_elapsed = NULL;
 
-                _title_bar_draw();
             }
         }
     }
+    _title_bar_draw();
 }
 
 void
@@ -241,6 +241,14 @@ _title_bar_draw(void)
     }
 #endif
 
+    // show indicator for unsaved forms
+    ProfWin *current = wins_get_current();
+    if ((current != NULL ) && (current->type == WIN_MUC_CONFIG)) {
+        if ((current->form != NULL) && (current->form->modified)) {
+            wprintw(win, " *");
+        }
+    }
+
     // show contact typing
     if (typing) {
         wprintw(win, " (typing...)");
diff --git a/src/ui/ui.h b/src/ui/ui.h
index 7e8967c0..52a93521 100644
--- a/src/ui/ui.h
+++ b/src/ui/ui.h
@@ -160,6 +160,13 @@ void (*ui_handle_recipient_error)(const char * const recipient, const char * con
 void (*ui_handle_error)(const char * const err_msg);
 void (*ui_clear_win_title)(void);
 void (*ui_handle_room_join_error)(const char * const room, const char * const err);
+void (*ui_handle_room_configuration)(const char * const room, DataForm *form);
+void (*ui_handle_room_config_submit_result)(void);
+void (*ui_show_form)(ProfWin *window, const char * const room, DataForm *form);
+void (*ui_show_form_field)(ProfWin *window, DataForm *form, char *tag);
+void (*ui_show_form_help)(ProfWin *window, DataForm *form);
+void (*ui_show_form_field_help)(ProfWin *window, DataForm *form, char *tag);
+void (*ui_show_lines)(ProfWin *window, const gchar** lines);
 
 // contact status functions
 void (*ui_status_room)(const char * const contact);
@@ -199,6 +206,8 @@ void (*ui_create_xmlconsole_win)(void);
 gboolean (*ui_xmlconsole_exists)(void);
 void (*ui_open_xmlconsole_win)(void);
 
+gboolean (*ui_win_has_unsaved_form)(int num);
+
 // console window actions
 void (*cons_show)(const char * const msg, ...);
 void (*cons_about)(void);
diff --git a/src/ui/window.c b/src/ui/window.c
index 6a1240e5..21591bae 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -68,6 +68,7 @@ win_create(const char * const title, int cols, win_type_t type)
     new_win->type = type;
     new_win->is_otr = FALSE;
     new_win->is_trusted = FALSE;
+    new_win->form = NULL;
     scrollok(new_win->win, TRUE);
 
     return new_win;
@@ -79,6 +80,7 @@ win_free(ProfWin* window)
     buffer_free(window->buffer);
     delwin(window->win);
     free(window->from);
+    form_destroy(window->form);
     free(window);
 }
 
diff --git a/src/ui/window.h b/src/ui/window.h
index ed0c64b4..8150fac9 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -45,6 +45,7 @@
 
 #include "contact.h"
 #include "ui/buffer.h"
+#include "xmpp/xmpp.h"
 
 #define NO_ME   1
 #define NO_DATE 2
@@ -58,6 +59,7 @@ typedef enum {
     WIN_CONSOLE,
     WIN_CHAT,
     WIN_MUC,
+    WIN_MUC_CONFIG,
     WIN_PRIVATE,
     WIN_DUCK,
     WIN_XML
@@ -74,6 +76,7 @@ typedef struct prof_win_t {
     int paged;
     int unread;
     int history_shown;
+    DataForm *form;
 } ProfWin;
 
 ProfWin* win_create(const char * const title, int cols, win_type_t type);
diff --git a/src/ui/windows.c b/src/ui/windows.c
index 3d835a0e..ff7fc3e2 100644
--- a/src/ui/windows.c
+++ b/src/ui/windows.c
@@ -81,7 +81,11 @@ wins_get_console(void)
 ProfWin *
 wins_get_current(void)
 {
-    return g_hash_table_lookup(windows, GINT_TO_POINTER(current));
+    if (windows != NULL) {
+        return g_hash_table_lookup(windows, GINT_TO_POINTER(current));
+    } else {
+        return NULL;
+    }
 }
 
 GList *
@@ -385,7 +389,11 @@ wins_get_prune_recipients(void)
 
     while (curr != NULL) {
         ProfWin *window = curr->data;
-        if (window->unread == 0 && window->type != WIN_MUC && window->type != WIN_CONSOLE) {
+        if (window->unread == 0 &&
+                window->type != WIN_MUC &&
+                window->type != WIN_MUC_CONFIG &&
+                window->type != WIN_XML &&
+                window->type != WIN_CONSOLE) {
             result = g_slist_append(result, window->from);
         }
         curr = g_list_next(curr);
@@ -539,6 +547,7 @@ wins_create_summary(void)
         GString *chat_string;
         GString *priv_string;
         GString *muc_string;
+        GString *muc_config_string;
         GString *duck_string;
         GString *xml_string;
 
@@ -606,6 +615,14 @@ wins_create_summary(void)
 
                 break;
 
+            case WIN_MUC_CONFIG:
+                muc_config_string = g_string_new("");
+                g_string_printf(muc_config_string, "%d: %s", ui_index, window->from);
+                result = g_slist_append(result, strdup(muc_config_string->str));
+                g_string_free(muc_config_string, TRUE);
+
+                break;
+
             case WIN_DUCK:
                 duck_string = g_string_new("");
                 g_string_printf(duck_string, "%d: DuckDuckGo search", ui_index);
diff --git a/src/xmpp/capabilities.c b/src/xmpp/capabilities.c
index 2fb9f065..606bd64b 100644
--- a/src/xmpp/capabilities.c
+++ b/src/xmpp/capabilities.c
@@ -47,6 +47,7 @@
 #include "common.h"
 #include "xmpp/xmpp.h"
 #include "xmpp/stanza.h"
+#include "xmpp/form.h"
 
 static GHashTable *capabilities;
 
@@ -137,7 +138,7 @@ caps_create_sha1_str(xmpp_stanza_t * const query)
     GSList *form_names = NULL;
     DataForm *form = NULL;
     FormField *field = NULL;
-    GHashTable *forms = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)stanza_destroy_form);
+    GHashTable *forms = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)form_destroy);
 
     GString *s = g_string_new("");
 
@@ -170,9 +171,10 @@ caps_create_sha1_str(xmpp_stanza_t * const query)
             features = g_slist_insert_sorted(features, g_strdup(feature_str), (GCompareFunc)strcmp);
         } else if (g_strcmp0(xmpp_stanza_get_name(child), STANZA_NAME_X) == 0) {
             if (strcmp(xmpp_stanza_get_ns(child), STANZA_NS_DATA) == 0) {
-                form = stanza_create_form(child);
-                form_names = g_slist_insert_sorted(form_names, g_strdup(form->form_type), (GCompareFunc)strcmp);
-                g_hash_table_insert(forms, g_strdup(form->form_type), form);
+                form = form_create(child);
+                char *form_type = form_get_form_type_field(form);
+                form_names = g_slist_insert_sorted(form_names, g_strdup(form_type), (GCompareFunc)strcmp);
+                g_hash_table_insert(forms, g_strdup(form_type), form);
             }
         }
         child = xmpp_stanza_get_next(child);
@@ -194,7 +196,8 @@ caps_create_sha1_str(xmpp_stanza_t * const query)
     curr = form_names;
     while (curr != NULL) {
         form = g_hash_table_lookup(forms, curr->data);
-        g_string_append(s, form->form_type);
+        char *form_type = form_get_form_type_field(form);
+        g_string_append(s, form_type);
         g_string_append(s, "<");
 
         GSList *curr_field = form->fields;
diff --git a/src/xmpp/form.c b/src/xmpp/form.c
new file mode 100644
index 00000000..a7b9fe0a
--- /dev/null
+++ b/src/xmpp/form.c
@@ -0,0 +1,628 @@
+/*
+ * form.c
+ *
+ * Copyright (C) 2012 - 2014 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.
+ *
+ */
+
+#include <string.h>
+#include <stdlib.h>
+
+#include <strophe.h>
+#include <glib.h>
+
+#include "log.h"
+#include "xmpp/xmpp.h"
+#include "xmpp/stanza.h"
+#include "xmpp/connection.h"
+
+static gboolean
+_is_valid_form_element(xmpp_stanza_t *stanza)
+{
+    char *name = xmpp_stanza_get_name(stanza);
+    if (g_strcmp0(name, STANZA_NAME_X) != 0) {
+        log_error("Error parsing form, root element not <x/>.");
+        return FALSE;
+    }
+
+    char *ns = xmpp_stanza_get_ns(stanza);
+    if (g_strcmp0(ns, STANZA_NS_DATA) != 0) {
+        log_error("Error parsing form, namespace not %s.", STANZA_NS_DATA);
+        return FALSE;
+    }
+
+    char *type = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TYPE);
+    if ((g_strcmp0(type, "form") != 0) &&
+            (g_strcmp0(type, "submit") != 0) &&
+            (g_strcmp0(type, "cancel") != 0) &&
+            (g_strcmp0(type, "result") != 0)) {
+        log_error("Error parsing form, unknown type.");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static DataForm *
+_form_new(void)
+{
+    DataForm *form = malloc(sizeof(DataForm));
+    form->type = NULL;
+    form->title = NULL;
+    form->instructions = NULL;
+    form->fields = NULL;
+    form->var_to_tag = NULL;
+    form->tag_to_var = NULL;
+    form->tag_ac = NULL;
+
+    return form;
+}
+
+static FormField *
+_field_new(void)
+{
+    FormField *field = malloc(sizeof(FormField));
+    field->label = NULL;
+    field->type = NULL;
+    field->var = NULL;
+    field->description = NULL;
+    field->required = FALSE;
+    field->values = NULL;
+    field->options = NULL;
+
+    return field;
+}
+
+static char *
+_get_property(xmpp_stanza_t * const stanza, const char * const property)
+{
+    char *result = NULL;
+    xmpp_ctx_t *ctx = connection_get_ctx();
+
+    xmpp_stanza_t *child = xmpp_stanza_get_child_by_name(stanza, property);
+    if (child != NULL) {
+        char *child_text = xmpp_stanza_get_text(child);
+        if (child_text != NULL) {
+            result = strdup(child_text);
+            xmpp_free(ctx, child_text);
+        }
+    }
+
+    return result;
+}
+
+static char *
+_get_attr(xmpp_stanza_t * const stanza, const char * const attr)
+{
+    char *result = xmpp_stanza_get_attribute(stanza, attr);
+    if (result != NULL) {
+        return strdup(result);
+    } else {
+        return NULL;
+    }
+}
+
+static gboolean
+_is_required(xmpp_stanza_t * const stanza)
+{
+    xmpp_stanza_t *child = xmpp_stanza_get_child_by_name(stanza, "required");
+    if (child != NULL) {
+        return TRUE;
+    } else {
+        return FALSE;
+    }
+}
+
+static form_field_type_t
+_get_field_type(const char * const type)
+{
+    if (g_strcmp0(type, "hidden") == 0) {
+        return FIELD_HIDDEN;
+    }
+    if (g_strcmp0(type, "text-single") == 0) {
+        return FIELD_TEXT_SINGLE;
+    }
+    if (g_strcmp0(type, "text-private") == 0) {
+        return FIELD_TEXT_PRIVATE;
+    }
+    if (g_strcmp0(type, "text-multi") == 0) {
+        return FIELD_TEXT_MULTI;
+    }
+    if (g_strcmp0(type, "boolean") == 0) {
+        return FIELD_BOOLEAN;
+    }
+    if (g_strcmp0(type, "list-single") == 0) {
+        return FIELD_LIST_SINGLE;
+    }
+    if (g_strcmp0(type, "list-multi") == 0) {
+        return FIELD_LIST_MULTI;
+    }
+    if (g_strcmp0(type, "jid-single") == 0) {
+        return FIELD_JID_SINGLE;
+    }
+    if (g_strcmp0(type, "jid-multi") == 0) {
+        return FIELD_JID_MULTI;
+    }
+    if (g_strcmp0(type, "fixed") == 0) {
+        return FIELD_FIXED;
+    }
+    return FIELD_UNKNOWN;
+}
+
+DataForm *
+form_create(xmpp_stanza_t * const form_stanza)
+{
+    xmpp_ctx_t *ctx = connection_get_ctx();
+
+    if (!_is_valid_form_element(form_stanza)) {
+        return NULL;
+    }
+
+    DataForm *form = _form_new();
+    form->type = _get_attr(form_stanza, "type");
+    form->title = _get_property(form_stanza, "title");
+    form->instructions = _get_property(form_stanza, "instructions");
+    form->var_to_tag = g_hash_table_new_full(g_str_hash, g_str_equal, free, free);
+    form->tag_to_var = g_hash_table_new_full(g_str_hash, g_str_equal, free, free);
+    form->tag_ac = autocomplete_new();
+    form->modified = FALSE;
+
+    int tag_num = 1;
+
+    // get fields
+    xmpp_stanza_t *form_child = xmpp_stanza_get_children(form_stanza);
+    while (form_child != NULL) {
+        char *child_name = xmpp_stanza_get_name(form_child);
+        if (g_strcmp0(child_name, "field") == 0) {
+            xmpp_stanza_t *field_stanza = form_child;
+
+            FormField *field = _field_new();
+            field->label = _get_attr(field_stanza, "label");
+            field->type = _get_attr(field_stanza, "type");
+            field->type_t = _get_field_type(field->type);
+
+            field->var = _get_attr(field_stanza, "var");
+
+            if (field->type_t != FIELD_HIDDEN) {
+                GString *tag = g_string_new("");
+                g_string_printf(tag, "field%d", tag_num++);
+                g_hash_table_insert(form->var_to_tag, strdup(field->var), strdup(tag->str));
+                g_hash_table_insert(form->tag_to_var, strdup(tag->str), strdup(field->var));
+                autocomplete_add(form->tag_ac, tag->str);
+                g_string_free(tag, TRUE);
+            }
+
+            field->description = _get_property(field_stanza, "desc");
+            field->required = _is_required(field_stanza);
+
+            // handle repeated field children
+            xmpp_stanza_t *field_child = xmpp_stanza_get_children(field_stanza);
+            while (field_child != NULL) {
+                child_name = xmpp_stanza_get_name(field_child);
+
+                // handle values
+                if (g_strcmp0(child_name, "value") == 0) {
+                    char *value = xmpp_stanza_get_text(field_child);
+                    if (value != NULL) {
+                        field->values = g_slist_append(field->values, strdup(value));
+                        xmpp_free(ctx, value);
+                    }
+
+                // handle options
+                } else if (g_strcmp0(child_name, "option") == 0) {
+                    FormOption *option = malloc(sizeof(FormOption));
+                    option->label = _get_attr(field_child, "label");
+                    option->value = _get_property(field_child, "value");
+
+                    field->options = g_slist_append(field->options, option);
+                }
+
+                field_child = xmpp_stanza_get_next(field_child);
+            }
+
+            form->fields = g_slist_append(form->fields, field);
+        }
+
+        form_child = xmpp_stanza_get_next(form_child);
+    }
+
+    return form;
+}
+
+xmpp_stanza_t *
+form_create_submission(DataForm *form)
+{
+    xmpp_ctx_t *ctx = connection_get_ctx();
+
+    xmpp_stanza_t *x = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(x, STANZA_NAME_X);
+    xmpp_stanza_set_ns(x, STANZA_NS_DATA);
+    xmpp_stanza_set_type(x, "submit");
+
+    GSList *curr_field = form->fields;
+    while (curr_field != NULL) {
+        FormField *field = curr_field->data;
+
+        xmpp_stanza_t *field_stanza = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_name(field_stanza, "field");
+        xmpp_stanza_set_attribute(field_stanza, "var", field->var);
+
+        xmpp_stanza_t *value_stanza = NULL;
+        GSList *curr_value = NULL;
+
+        switch (field->type_t) {
+            case FIELD_HIDDEN:
+            case FIELD_FIXED:
+            case FIELD_TEXT_SINGLE:
+            case FIELD_TEXT_PRIVATE:
+            case FIELD_BOOLEAN:
+            case FIELD_LIST_SINGLE:
+            case FIELD_JID_SINGLE:
+                value_stanza = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(value_stanza, "value");
+                if (field->values != NULL) {
+                    if (field->values->data != NULL) {
+                        xmpp_stanza_t *text_stanza = xmpp_stanza_new(ctx);
+                        xmpp_stanza_set_text(text_stanza, field->values->data);
+                        xmpp_stanza_add_child(value_stanza, text_stanza);
+                        xmpp_stanza_release(text_stanza);
+                    }
+                }
+                xmpp_stanza_add_child(field_stanza, value_stanza);
+                xmpp_stanza_release(value_stanza);
+
+                break;
+
+            case FIELD_TEXT_MULTI:
+            case FIELD_LIST_MULTI:
+            case FIELD_JID_MULTI:
+                curr_value = field->values;
+                while (curr_value != NULL) {
+                    char *value = curr_value->data;
+
+                    value_stanza = xmpp_stanza_new(ctx);
+                    xmpp_stanza_set_name(value_stanza, "value");
+                    if (value != NULL) {
+                        xmpp_stanza_t *text_stanza = xmpp_stanza_new(ctx);
+                        xmpp_stanza_set_text(text_stanza, value);
+                        xmpp_stanza_add_child(value_stanza, text_stanza);
+                        xmpp_stanza_release(text_stanza);
+                    }
+
+                    xmpp_stanza_add_child(field_stanza, value_stanza);
+                    xmpp_stanza_release(value_stanza);
+
+                    curr_value = g_slist_next(curr_value);
+                }
+                break;
+            default:
+                break;
+        }
+
+        xmpp_stanza_add_child(x, field_stanza);
+        xmpp_stanza_release(field_stanza);
+
+        curr_field = g_slist_next(curr_field);
+    }
+
+    return x;
+}
+
+static void
+_free_option(FormOption *option)
+{
+    if (option != NULL) {
+        free(option->label);
+        free(option->value);
+        free(option);
+    }
+}
+
+static void
+_free_field(FormField *field)
+{
+    if (field != NULL) {
+        free(field->label);
+        free(field->type);
+        free(field->var);
+        free(field->description);
+        g_slist_free_full(field->values, free);
+        g_slist_free_full(field->options, (GDestroyNotify)_free_option);
+        free(field);
+    }
+}
+
+static void
+_form_destroy(DataForm *form)
+{
+    if (form != NULL) {
+        free(form->type);
+        free(form->title);
+        free(form->instructions);
+        g_slist_free_full(form->fields, (GDestroyNotify)_free_field);
+        g_hash_table_destroy(form->var_to_tag);
+        g_hash_table_destroy(form->tag_to_var);
+        autocomplete_free(form->tag_ac);
+        free(form);
+    }
+}
+
+static char *
+_form_get_form_type_field(DataForm *form)
+{
+    GSList *curr = form->fields;
+    while (curr != NULL) {
+        FormField *field = curr->data;
+        if (g_strcmp0(field->var, "FORM_TYPE") == 0) {
+            return field->values->data;
+        }
+        curr = g_slist_next(curr);
+    }
+
+    return NULL;
+}
+
+static gboolean
+_form_tag_exists(DataForm *form, const char * const tag)
+{
+    GList *tags = g_hash_table_get_keys(form->tag_to_var);
+    GList *curr = tags;
+    while (curr != NULL) {
+        if (g_strcmp0(curr->data, tag) == 0) {
+            return TRUE;
+        }
+        curr = g_list_next(curr);
+    }
+    return FALSE;
+}
+
+static form_field_type_t
+_form_get_field_type(DataForm *form, const char * const tag)
+{
+    char *var = g_hash_table_lookup(form->tag_to_var, tag);
+    if (var != NULL) {
+        GSList *curr = form->fields;
+        while (curr != NULL) {
+            FormField *field = curr->data;
+            if (g_strcmp0(field->var, var) == 0) {
+                return field->type_t;
+            }
+            curr = g_slist_next(curr);
+        }
+    }
+    return FIELD_UNKNOWN;
+}
+
+static void
+_form_set_value(DataForm *form, const char * const tag, char *value)
+{
+    char *var = g_hash_table_lookup(form->tag_to_var, tag);
+    if (var != NULL) {
+        GSList *curr = form->fields;
+        while (curr != NULL) {
+            FormField *field = curr->data;
+            if (g_strcmp0(field->var, var) == 0) {
+                if (g_slist_length(field->values) == 0) {
+                    field->values = g_slist_append(field->values, strdup(value));
+                    form->modified = TRUE;
+                    return;
+                } else if (g_slist_length(field->values) == 1) {
+                    free(field->values->data);
+                    field->values->data = strdup(value);
+                    form->modified = TRUE;
+                    return;
+                }
+            }
+            curr = g_slist_next(curr);
+        }
+    }
+}
+
+static void
+_form_add_value(DataForm *form, const char * const tag, char *value)
+{
+    char *var = g_hash_table_lookup(form->tag_to_var, tag);
+    if (var != NULL) {
+        GSList *curr = form->fields;
+        while (curr != NULL) {
+            FormField *field = curr->data;
+            if (g_strcmp0(field->var, var) == 0) {
+                field->values = g_slist_append(field->values, strdup(value));
+                form->modified = TRUE;
+                return;
+            }
+            curr = g_slist_next(curr);
+        }
+    }
+}
+
+static gboolean
+_form_add_unique_value(DataForm *form, const char * const tag, char *value)
+{
+    char *var = g_hash_table_lookup(form->tag_to_var, tag);
+    if (var != NULL) {
+        GSList *curr = form->fields;
+        while (curr != NULL) {
+            FormField *field = curr->data;
+            if (g_strcmp0(field->var, var) == 0) {
+                GSList *curr_value = field->values;
+                while (curr_value != NULL) {
+                    if (g_strcmp0(curr_value->data, value) == 0) {
+                        return FALSE;
+                    }
+                    curr_value = g_slist_next(curr_value);
+                }
+
+                field->values = g_slist_append(field->values, strdup(value));
+                form->modified = TRUE;
+                return TRUE;
+            }
+            curr = g_slist_next(curr);
+        }
+    }
+
+    return FALSE;
+}
+
+static gboolean
+_form_remove_value(DataForm *form, const char * const tag, char *value)
+{
+    char *var = g_hash_table_lookup(form->tag_to_var, tag);
+    if (var != NULL) {
+        GSList *curr = form->fields;
+        while (curr != NULL) {
+            FormField *field = curr->data;
+            if (g_strcmp0(field->var, var) == 0) {
+                GSList *found = g_slist_find_custom(field->values, value, (GCompareFunc)g_strcmp0);
+                if (found != NULL) {
+                    free(found->data);
+                    found->data = NULL;
+                    field->values = g_slist_delete_link(field->values, found);
+                    form->modified = TRUE;
+                    return TRUE;
+                } else {
+                    return FALSE;
+                }
+            }
+            curr = g_slist_next(curr);
+        }
+    }
+
+    return FALSE;
+}
+
+static gboolean
+_form_remove_text_multi_value(DataForm *form, const char * const tag, int index)
+{
+    index--;
+    char *var = g_hash_table_lookup(form->tag_to_var, tag);
+    if (var != NULL) {
+        GSList *curr = form->fields;
+        while (curr != NULL) {
+            FormField *field = curr->data;
+            if (g_strcmp0(field->var, var) == 0) {
+                GSList *item = g_slist_nth(field->values, index);
+                if (item != NULL) {
+                    free(item->data);
+                    item->data = NULL;
+                    field->values = g_slist_delete_link(field->values, item);
+                    form->modified = TRUE;
+                    return TRUE;
+                } else {
+                    return FALSE;
+                }
+            }
+            curr = g_slist_next(curr);
+        }
+    }
+
+    return FALSE;
+}
+
+static int
+_form_get_value_count(DataForm *form, const char * const tag)
+{
+    char *var = g_hash_table_lookup(form->tag_to_var, tag);
+    if (var != NULL) {
+        GSList *curr = form->fields;
+        while (curr != NULL) {
+            FormField *field = curr->data;
+            if (g_strcmp0(field->var, var) == 0) {
+                if ((g_slist_length(field->values) == 1) && (field->values->data == NULL)) {
+                    return 0;
+                } else {
+                    return g_slist_length(field->values);
+                }
+            }
+            curr = g_slist_next(curr);
+        }
+    }
+
+    return 0;
+}
+
+static gboolean
+_form_field_contains_option(DataForm *form, const char * const tag, char *value)
+{
+    char *var = g_hash_table_lookup(form->tag_to_var, tag);
+    if (var != NULL) {
+        GSList *curr = form->fields;
+        while (curr != NULL) {
+            FormField *field = curr->data;
+            if (g_strcmp0(field->var, var) == 0) {
+                GSList *curr_option = field->options;
+                while (curr_option != NULL) {
+                    FormOption *option = curr_option->data;
+                    if (g_strcmp0(option->value, value) == 0) {
+                        return TRUE;
+                    }
+                    curr_option = g_slist_next(curr_option);
+                }
+            }
+            curr = g_slist_next(curr);
+        }
+    }
+
+    return FALSE;
+}
+
+static FormField *
+_form_get_field_by_tag(DataForm *form, const char * const tag)
+{
+    char *var = g_hash_table_lookup(form->tag_to_var, tag);
+    if (var != NULL) {
+        GSList *curr = form->fields;
+        while (curr != NULL) {
+            FormField *field = curr->data;
+            if (g_strcmp0(field->var, var) == 0) {
+                return field;
+            }
+            curr = g_slist_next(curr);
+        }
+    }
+    return NULL;
+}
+
+void
+form_init_module(void)
+{
+    form_destroy = _form_destroy;
+    form_get_form_type_field = _form_get_form_type_field;
+    form_get_field_type = _form_get_field_type;
+    form_set_value = _form_set_value;
+    form_add_unique_value = _form_add_unique_value;
+    form_add_value = _form_add_value;
+    form_remove_value = _form_remove_value;
+    form_remove_text_multi_value = _form_remove_text_multi_value;
+    form_field_contains_option = _form_field_contains_option;
+    form_tag_exists = _form_tag_exists;
+    form_get_value_count = _form_get_value_count;
+    form_get_field_by_tag = _form_get_field_by_tag;
+}
diff --git a/src/xmpp/form.h b/src/xmpp/form.h
new file mode 100644
index 00000000..816f5df0
--- /dev/null
+++ b/src/xmpp/form.h
@@ -0,0 +1,43 @@
+/*
+ * form.h
+ *
+ * Copyright (C) 2012 - 2014 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 FORM_H
+#define FROM_H
+
+#include "xmpp/xmpp.h"
+
+DataForm* form_create(xmpp_stanza_t * const stanza);
+xmpp_stanza_t* form_create_submission(DataForm *form);
+
+#endif
diff --git a/src/xmpp/iq.c b/src/xmpp/iq.c
index 8dc86080..62dfd64a 100644
--- a/src/xmpp/iq.c
+++ b/src/xmpp/iq.c
@@ -52,6 +52,7 @@
 #include "xmpp/capabilities.h"
 #include "xmpp/connection.h"
 #include "xmpp/stanza.h"
+#include "xmpp/form.h"
 #include "roster_list.h"
 #include "xmpp/xmpp.h"
 
@@ -75,6 +76,10 @@ static int _disco_items_get_handler(xmpp_conn_t * const conn,
     xmpp_stanza_t * const stanza, void * const userdata);
 static int _destroy_room_result_handler(xmpp_conn_t * const conn,
     xmpp_stanza_t * const stanza, void * const userdata);
+static int _room_config_handler(xmpp_conn_t * const conn,
+    xmpp_stanza_t * const stanza, void * const userdata);
+static int _room_config_submit_handler(xmpp_conn_t * const conn,
+    xmpp_stanza_t * const stanza, void * const userdata);
 static int _manual_pong_handler(xmpp_conn_t *const conn,
     xmpp_stanza_t * const stanza, void * const userdata);
 static int _ping_timed_handler(xmpp_conn_t * const conn,
@@ -187,6 +192,44 @@ _iq_destroy_instant_room(const char * const room_jid)
 }
 
 static void
+_iq_request_room_config_form(const char * const room_jid)
+{
+    xmpp_conn_t * const conn = connection_get_conn();
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    xmpp_stanza_t *iq = stanza_create_room_config_request_iq(ctx, room_jid);
+
+    char *id = xmpp_stanza_get_id(iq);
+    xmpp_id_handler_add(conn, _room_config_handler, id, NULL);
+
+    xmpp_send(conn, iq);
+    xmpp_stanza_release(iq);
+}
+
+static void
+_iq_submit_room_config(const char * const room, DataForm *form)
+{
+    xmpp_conn_t * const conn = connection_get_conn();
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    xmpp_stanza_t *iq = stanza_create_room_config_submit_iq(ctx, room, form);
+
+    char *id = xmpp_stanza_get_id(iq);
+    xmpp_id_handler_add(conn, _room_config_submit_handler, id, NULL);
+
+    xmpp_send(conn, iq);
+    xmpp_stanza_release(iq);
+}
+
+static void
+_iq_room_config_cancel(const char * const room_jid)
+{
+    xmpp_conn_t * const conn = connection_get_conn();
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    xmpp_stanza_t *iq = stanza_create_room_config_cancel_iq(ctx, room_jid);
+    xmpp_send(conn, iq);
+    xmpp_stanza_release(iq);
+}
+
+static void
 _iq_send_ping(const char * const target)
 {
     xmpp_conn_t * const conn = connection_get_conn();
@@ -563,6 +606,78 @@ _destroy_room_result_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const sta
     return 0;
 }
 
+static int
+_room_config_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
+    void * const userdata)
+{
+    const char *id = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_ID);
+    const char *type = xmpp_stanza_get_type(stanza);
+    const char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
+
+    if (id != NULL) {
+        log_debug("IQ room config handler fired, id: %s.", id);
+    } else {
+        log_debug("IQ room config handler fired.");
+    }
+
+    // handle error responses
+    if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) {
+        char *error_message = stanza_get_error_message(stanza);
+        handle_room_configuration_form_error(from, error_message);
+        free(error_message);
+        return 0;
+    }
+
+    if (from == NULL) {
+        log_error("No from attribute for IQ config request result");
+        handle_room_configuration_form_error(from, "No from attribute for room cofig response.");
+        return 0;
+    }
+
+    xmpp_stanza_t *query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY);
+    if (query == NULL) {
+        log_error("No query element found parsing room config response");
+        handle_room_configuration_form_error(from, "No query element found parsing room config response");
+        return 0;
+    }
+
+    xmpp_stanza_t *x = xmpp_stanza_get_child_by_ns(query, STANZA_NS_DATA);
+    if (x == NULL) {
+        log_error("No x element found with %s namespace parsing room config response", STANZA_NS_DATA);
+        handle_room_configuration_form_error(from, "No form data element found parsing room config response");
+        return 0;
+    }
+
+    char *form_type = xmpp_stanza_get_attribute(x, STANZA_ATTR_TYPE);
+    if (g_strcmp0(form_type, "form") != 0) {
+        log_error("x element not of type 'form' parsing room config response");
+        handle_room_configuration_form_error(from, "Form not of type 'form' parsing room config response.");
+        return 0;
+    }
+
+    DataForm *form = form_create(x);
+    handle_room_configure(from, form);
+
+    return 0;
+}
+
+static int
+_room_config_submit_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
+    void * const userdata)
+{
+    const char *id = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_ID);
+
+    if (id != NULL) {
+        log_debug("IQ room config handler fired, id: %s.", id);
+    } else {
+        log_debug("IQ room config handler fired.");
+    }
+
+    handle_room_config_submit_result();
+
+    return 0;
+}
+
 static void
 _identity_destroy(DiscoIdentity *identity)
 {
@@ -709,10 +824,11 @@ _disco_info_result_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanz
 
         xmpp_stanza_t *softwareinfo = xmpp_stanza_get_child_by_ns(query, STANZA_NS_DATA);
         if (softwareinfo != NULL) {
-            DataForm *form = stanza_create_form(softwareinfo);
+            DataForm *form = form_create(softwareinfo);
             FormField *formField = NULL;
 
-            if (g_strcmp0(form->form_type, STANZA_DATAFORM_SOFTWARE) == 0) {
+            char *form_type = form_get_form_type_field(form);
+            if (g_strcmp0(form_type, STANZA_DATAFORM_SOFTWARE) == 0) {
                 GSList *field = form->fields;
                 while (field != NULL) {
                     formField = field->data;
@@ -731,7 +847,7 @@ _disco_info_result_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanz
                 }
             }
 
-            stanza_destroy_form(form);
+            form_destroy(form);
         }
 
         xmpp_stanza_t *child = xmpp_stanza_get_children(query);
@@ -812,4 +928,7 @@ iq_init_module(void)
     iq_confirm_instant_room = _iq_confirm_instant_room;
     iq_destroy_instant_room = _iq_destroy_instant_room;
     iq_send_ping = _iq_send_ping;
+    iq_request_room_config_form = _iq_request_room_config_form;
+    iq_room_config_cancel = _iq_room_config_cancel;
+    iq_submit_room_config = _iq_submit_room_config;
 }
diff --git a/src/xmpp/stanza.c b/src/xmpp/stanza.c
index 654ba121..118bffb7 100644
--- a/src/xmpp/stanza.c
+++ b/src/xmpp/stanza.c
@@ -43,11 +43,10 @@
 #include "xmpp/connection.h"
 #include "xmpp/stanza.h"
 #include "xmpp/capabilities.h"
+#include "xmpp/form.h"
 
 #include "muc.h"
 
-static int _field_compare(FormField *f1, FormField *f2);
-
 #if 0
 xmpp_stanza_t *
 stanza_create_bookmarks_pubsub_request(xmpp_ctx_t *ctx)
@@ -430,7 +429,7 @@ stanza_create_instant_room_request_iq(xmpp_ctx_t *ctx, const char * const room_j
     xmpp_stanza_set_name(iq, STANZA_NAME_IQ);
     xmpp_stanza_set_type(iq, STANZA_TYPE_SET);
     xmpp_stanza_set_attribute(iq, STANZA_ATTR_TO, room_jid);
-    char *id = create_unique_id("leave");
+    char *id = create_unique_id("room");
     xmpp_stanza_set_id(iq, id);
     free(id);
 
@@ -459,7 +458,7 @@ stanza_create_instant_room_destroy_iq(xmpp_ctx_t *ctx, const char * const room_j
     xmpp_stanza_set_name(iq, STANZA_NAME_IQ);
     xmpp_stanza_set_type(iq, STANZA_TYPE_SET);
     xmpp_stanza_set_attribute(iq, STANZA_ATTR_TO, room_jid);
-    char *id = create_unique_id("leave");
+    char *id = create_unique_id("room");
     xmpp_stanza_set_id(iq, id);
     free(id);
 
@@ -480,6 +479,56 @@ stanza_create_instant_room_destroy_iq(xmpp_ctx_t *ctx, const char * const room_j
 }
 
 xmpp_stanza_t *
+stanza_create_room_config_request_iq(xmpp_ctx_t *ctx, const char * const room_jid)
+{
+    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, room_jid);
+    char *id = create_unique_id("room");
+    xmpp_stanza_set_id(iq, id);
+    free(id);
+
+    xmpp_stanza_t *query = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(query, STANZA_NAME_QUERY);
+    xmpp_stanza_set_ns(query, STANZA_NS_MUC_OWNER);
+
+    xmpp_stanza_add_child(iq, query);
+    xmpp_stanza_release(query);
+
+    return iq;
+}
+
+xmpp_stanza_t *
+stanza_create_room_config_cancel_iq(xmpp_ctx_t *ctx, const char * const room_jid)
+{
+    xmpp_stanza_t *iq = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(iq, STANZA_NAME_IQ);
+    xmpp_stanza_set_type(iq, STANZA_TYPE_SET);
+    xmpp_stanza_set_attribute(iq, STANZA_ATTR_TO, room_jid);
+    char *id = create_unique_id("room");
+    xmpp_stanza_set_id(iq, id);
+    free(id);
+
+    xmpp_stanza_t *query = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(query, STANZA_NAME_QUERY);
+    xmpp_stanza_set_ns(query, STANZA_NS_MUC_OWNER);
+
+    xmpp_stanza_t *x = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(x, STANZA_NAME_X);
+    xmpp_stanza_set_type(x, "cancel");
+    xmpp_stanza_set_ns(x, STANZA_NS_DATA);
+
+    xmpp_stanza_add_child(query, x);
+    xmpp_stanza_release(x);
+
+    xmpp_stanza_add_child(iq, query);
+    xmpp_stanza_release(query);
+
+    return iq;
+}
+
+xmpp_stanza_t *
 stanza_create_presence(xmpp_ctx_t * const ctx)
 {
     xmpp_stanza_t *presence = xmpp_stanza_new(ctx);
@@ -568,6 +617,31 @@ stanza_create_disco_items_iq(xmpp_ctx_t *ctx, const char * const id,
     return iq;
 }
 
+xmpp_stanza_t *
+stanza_create_room_config_submit_iq(xmpp_ctx_t *ctx, const char * const room, DataForm *form)
+{
+    xmpp_stanza_t *iq = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(iq, STANZA_NAME_IQ);
+    xmpp_stanza_set_type(iq, STANZA_TYPE_SET);
+    xmpp_stanza_set_attribute(iq, STANZA_ATTR_TO, room);
+    char *id = create_unique_id("roomconf_submit");
+    xmpp_stanza_set_id(iq, id);
+    free(id);
+
+    xmpp_stanza_t *query = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(query, STANZA_NAME_QUERY);
+    xmpp_stanza_set_ns(query, STANZA_NS_MUC_OWNER);
+
+    xmpp_stanza_t *x = form_create_submission(form);
+    xmpp_stanza_add_child(query, x);
+    xmpp_stanza_release(x);
+
+    xmpp_stanza_add_child(iq, query);
+    xmpp_stanza_release(query);
+
+    return iq;
+}
+
 gboolean
 stanza_contains_chat_state(xmpp_stanza_t *stanza)
 {
@@ -1023,79 +1097,6 @@ stanza_get_error_message(xmpp_stanza_t *stanza)
     return strdup("unknown");
 }
 
-DataForm *
-stanza_create_form(xmpp_stanza_t * const stanza)
-{
-    DataForm *result = NULL;
-    xmpp_ctx_t *ctx = connection_get_ctx();
-
-    xmpp_stanza_t *child = xmpp_stanza_get_children(stanza);
-
-    if (child != NULL) {
-        result = malloc(sizeof(DataForm));
-        result->form_type = NULL;
-        result->fields = NULL;
-    }
-
-    //handle fields
-    while (child != NULL) {
-        char *var = xmpp_stanza_get_attribute(child, "var");
-
-        // handle FORM_TYPE
-        if (g_strcmp0(var, "FORM_TYPE") == 0) {
-            xmpp_stanza_t *value = xmpp_stanza_get_child_by_name(child, "value");
-            char *value_text = xmpp_stanza_get_text(value);
-            result->form_type = strdup(value_text);
-            xmpp_free(ctx, value_text);
-
-        // handle regular fields
-        } else {
-            FormField *field = malloc(sizeof(FormField));
-            field->var = strdup(var);
-            field->values = NULL;
-            xmpp_stanza_t *value = xmpp_stanza_get_children(child);
-
-            // handle values
-            while (value != NULL) {
-                char *text = xmpp_stanza_get_text(value);
-                if (text != NULL) {
-                    field->values = g_slist_insert_sorted(field->values, strdup(text), (GCompareFunc)strcmp);
-                    xmpp_free(ctx, text);
-                }
-                value = xmpp_stanza_get_next(value);
-            }
-
-            result->fields = g_slist_insert_sorted(result->fields, field, (GCompareFunc)_field_compare);
-        }
-
-        child = xmpp_stanza_get_next(child);
-    }
-
-    return result;
-}
-
-void
-stanza_destroy_form(DataForm *form)
-{
-    if (form != NULL) {
-        if (form->fields != NULL) {
-            GSList *curr_field = form->fields;
-            while (curr_field != NULL) {
-                FormField *field = curr_field->data;
-                free(field->var);
-                if ((field->values) != NULL) {
-                    g_slist_free_full(field->values, free);
-                }
-                curr_field = curr_field->next;
-            }
-            g_slist_free_full(form->fields, free);
-        }
-
-        free(form->form_type);
-        free(form);
-    }
-}
-
 void
 stanza_attach_priority(xmpp_ctx_t * const ctx, xmpp_stanza_t * const presence,
     const int pri)
@@ -1199,9 +1200,3 @@ stanza_get_presence_string_from_type(resource_presence_t presence_type)
             return NULL;
     }
 }
-
-static int
-_field_compare(FormField *f1, FormField *f2)
-{
-    return strcmp(f1->var, f2->var);
-}
diff --git a/src/xmpp/stanza.h b/src/xmpp/stanza.h
index 3d925787..155044f2 100644
--- a/src/xmpp/stanza.h
+++ b/src/xmpp/stanza.h
@@ -36,6 +36,7 @@
 #define XMPP_STANZA_H
 
 #include <strophe.h>
+#include <xmpp/xmpp.h>
 
 #define STANZA_NAME_ACTIVE "active"
 #define STANZA_NAME_INACTIVE "inactive"
@@ -154,16 +155,6 @@
 
 #define STANZA_DATAFORM_SOFTWARE "urn:xmpp:dataforms:softwareinfo"
 
-typedef struct form_field_t {
-    char *var;
-    GSList *values;
-} FormField;
-
-typedef struct data_form_t {
-    char *form_type;
-    GSList *fields;
-} DataForm;
-
 xmpp_stanza_t* stanza_create_bookmarks_storage_request(xmpp_ctx_t *ctx);
 
 xmpp_stanza_t* stanza_create_chat_state(xmpp_ctx_t *ctx,
@@ -207,6 +198,12 @@ xmpp_stanza_t* stanza_create_instant_room_request_iq(xmpp_ctx_t *ctx,
     const char * const room_jid);
 xmpp_stanza_t* stanza_create_instant_room_destroy_iq(xmpp_ctx_t *ctx,
     const char * const room_jid);
+xmpp_stanza_t* stanza_create_room_config_request_iq(xmpp_ctx_t *ctx,
+    const char * const room_jid);
+xmpp_stanza_t* stanza_create_room_config_cancel_iq(xmpp_ctx_t *ctx,
+    const char * const room_jid);
+xmpp_stanza_t* stanza_create_room_config_submit_iq(xmpp_ctx_t *ctx,
+    const char * const room, DataForm *form);
 
 int stanza_get_idle_time(xmpp_stanza_t * const stanza);
 char * stanza_get_caps_str(xmpp_stanza_t * const stanza);
diff --git a/src/xmpp/xmpp.h b/src/xmpp/xmpp.h
index 78703230..85704640 100644
--- a/src/xmpp/xmpp.h
+++ b/src/xmpp/xmpp.h
@@ -40,6 +40,7 @@
 #include "config/accounts.h"
 #include "contact.h"
 #include "jid.h"
+#include "tools/autocomplete.h"
 
 #define JABBER_PRIORITY_MIN -128
 #define JABBER_PRIORITY_MAX 127
@@ -86,6 +87,47 @@ 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;
+} 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;
+
 void jabber_init_module(void);
 void bookmark_init_module(void);
 void capabilities_init_module(void);
@@ -93,6 +135,7 @@ void iq_init_module(void);
 void message_init_module(void);
 void presence_init_module(void);
 void roster_init_module(void);
+void form_init_module(void);
 
 // connection functions
 void (*jabber_init)(const int disable_tls);
@@ -141,6 +184,9 @@ void (*iq_disco_items_request)(gchar *jid);
 void (*iq_set_autoping)(int seconds);
 void (*iq_confirm_instant_room)(const char * const room_jid);
 void (*iq_destroy_instant_room)(const char * const room_jid);
+void (*iq_request_room_config_form)(const char * const room_jid);
+void (*iq_submit_room_config)(const char * const room, DataForm *form);
+void (*iq_room_config_cancel)(const char * const room_jid);
 void (*iq_send_ping)(const char * const target);
 
 // caps functions
@@ -161,4 +207,17 @@ void (*roster_send_remove_from_group)(const char * const group, PContact contact
 void (*roster_send_add_new)(const char * const barejid, const char * const name);
 void (*roster_send_remove)(const char * const barejid);
 
+void (*form_destroy)(DataForm *form);
+char * (*form_get_form_type_field)(DataForm *form);
+void (*form_set_value)(DataForm *form, const char * const tag, char *value);
+gboolean (*form_add_unique_value)(DataForm *form, const char * const tag, char *value);
+void (*form_add_value)(DataForm *form, const char * const tag, char *value);
+gboolean (*form_remove_value)(DataForm *form, const char * const tag, char *value);
+gboolean (*form_remove_text_multi_value)(DataForm *form, const char * const tag, int index);
+gboolean (*form_tag_exists)(DataForm *form, const char * const tag);
+form_field_type_t (*form_get_field_type)(DataForm *form, const char * const tag);
+gboolean (*form_field_contains_option)(DataForm *form, const char * const tag, char *value);
+int (*form_get_value_count)(DataForm *form, const char * const tag);
+FormField* (*form_get_field_by_tag)(DataForm *form, const char * const tag);
+
 #endif