changeset 1999:51f032d5ca22

Add support for XEP-0115 Entity Capabilities, with offline cache
author Hermitifier
date Mon, 03 Oct 2011 16:00:34 +0200
parents 41667bc02883
children 1fe1e8d1d1ea
files mcabber/mcabber/api.h mcabber/mcabber/caps.c mcabber/mcabber/caps.h mcabber/mcabber/xmpp.c mcabber/mcabber/xmpp_helper.c mcabber/mcabber/xmpp_iq.c
diffstat 6 files changed, 580 insertions(+), 53 deletions(-) [+]
line wrap: on
line diff
--- a/mcabber/mcabber/api.h	Sun Jul 24 13:30:47 2011 +0200
+++ b/mcabber/mcabber/api.h	Mon Oct 03 16:00:34 2011 +0200
@@ -3,8 +3,8 @@
 
 #include <mcabber/config.h> // For MCABBER_BRANCH
 
-#define MCABBER_API_VERSION 20
-#define MCABBER_API_MIN     19
+#define MCABBER_API_VERSION 21
+#define MCABBER_API_MIN     21
 
 #define MCABBER_BRANCH_DEV  1
 
--- a/mcabber/mcabber/caps.c	Sun Jul 24 13:30:47 2011 +0200
+++ b/mcabber/mcabber/caps.c	Mon Oct 03 16:00:34 2011 +0200
@@ -20,12 +20,28 @@
  */
 
 #include <glib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include "settings.h"
+#include "utils.h"
 
 typedef struct {
   char *category;
+  char *type;
   char *name;
-  char *type;
+} identity;
+
+typedef struct {
+  GHashTable *fields;
+} dataform;
+
+typedef struct {
+  GHashTable *identities;
   GHashTable *features;
+  GHashTable *forms;
 } caps;
 
 static GHashTable *caps_cache = NULL;
@@ -33,13 +49,34 @@
 void caps_destroy(gpointer data)
 {
   caps *c = data;
-  g_free(c->category);
-  g_free(c->name);
-  g_free(c->type);
+  g_hash_table_destroy(c->identities);
   g_hash_table_destroy(c->features);
+  g_hash_table_destroy(c->forms);
   g_free(c);
 }
 
+void identity_destroy(gpointer data)
+{
+  identity *i = data;
+  g_free(i->category);
+  g_free(i->type);
+  g_free(i->name);
+  g_free(i);
+}
+
+void form_destroy(gpointer data)
+{
+  dataform *f = data;
+  g_hash_table_destroy(f->fields);
+  g_free(f);
+}
+
+void field_destroy(gpointer data)
+{
+  GList *v = data;
+  g_list_free_full(v, g_free);
+}
+
 void caps_init(void)
 {
   if (!caps_cache)
@@ -55,18 +92,76 @@
   }
 }
 
-void caps_add(char *hash)
+void caps_add(const char *hash)
 {
   if (!hash)
     return;
   caps *c = g_new0(caps, 1);
   c->features = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+  c->identities = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, identity_destroy);
+  c->forms = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, form_destroy);
   g_hash_table_insert(caps_cache, g_strdup(hash), c);
 }
 
-int caps_has_hash(const char *hash)
+void caps_remove(const char *hash)
+{
+  if (!hash)
+    return;
+  g_hash_table_remove(caps_cache, hash);
+}
+
+void caps_move_to_local(char *hash, char *bjid)
+{
+  char *orig_hash;
+  caps *c = NULL;
+  if (!hash || !bjid)
+    return;
+  g_hash_table_lookup_extended(caps_cache, hash, (gpointer*)&orig_hash, (gpointer*)&c);
+  if (c) {
+    g_hash_table_steal(caps_cache, hash);
+    g_free(orig_hash);
+    g_hash_table_replace(caps_cache, g_strdup_printf("%s/#%s", bjid, hash), c);
+    // solidus is guaranteed to never appear in bare jid
+    // hash will not appear in base64 encoded hash
+    // sequence "/#" is deterministic separator, and allows to identify local cache entry
+  }
+}
+
+int caps_has_hash(const char *hash, const char *bjid)
 {
-  return (hash != NULL && (g_hash_table_lookup(caps_cache, hash) != NULL));
+  caps *c = NULL;
+  if (!hash)
+    return 0;
+  c = g_hash_table_lookup(caps_cache, hash);
+  if (!c && bjid) {
+    char *key = g_strdup_printf("%s/#%s", bjid, hash);
+    c = g_hash_table_lookup(caps_cache, key);
+    g_free(key);
+  }
+  return (c != NULL);
+}
+
+void caps_add_identity(const char *hash,
+                       const char *category,
+                       const char *name,
+                       const char *type,
+                       const char *lang)
+{
+  caps *c;
+  if (!hash || !category || !type)
+    return;
+  if (!lang)
+    lang = "";
+
+  c = g_hash_table_lookup(caps_cache, hash);
+  if (c) {
+    identity *i = g_new0(identity, 1);
+
+    i->category = g_strdup(category);
+    i->name = g_strdup(name);
+    i->type = g_strdup(type);
+    g_hash_table_replace(c->identities, g_strdup(lang), i);
+  }
 }
 
 void caps_set_identity(char *hash,
@@ -74,19 +169,56 @@
                        const char *name,
                        const char *type)
 {
+  caps_add_identity(hash, category, name, type, NULL);
+}
+
+void caps_add_dataform(const char *hash, const char *formtype)
+{
   caps *c;
-  if (!hash || !category || !type)
+  if (!formtype)
     return;
-
   c = g_hash_table_lookup(caps_cache, hash);
   if (c) {
-    c->category = g_strdup(category);
-    c->name = g_strdup(name);
-    c->type = g_strdup(type);
+    dataform *d = g_new0(dataform, 1);
+    char *f = g_strdup(formtype);
+
+    d->fields = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, field_destroy);
+    g_hash_table_replace(c->forms, f, d);
   }
 }
 
-void caps_add_feature(char *hash, const char *feature)
+gint _strcmp_sort(gconstpointer a, gconstpointer b)
+{
+  return g_strcmp0(a, b);
+}
+
+void caps_add_dataform_field(const char *hash, const char *formtype,
+                             const char *field, const char *value)
+{
+  caps *c;
+  if (!formtype || !field || !value)
+    return;
+  c = g_hash_table_lookup(caps_cache, hash);
+  if (c) {
+    dataform *d;
+    d = g_hash_table_lookup(c->forms, formtype);
+    if (d) {
+      gpointer key, val;
+      char *f;
+      GList *v = NULL;
+      if (g_hash_table_lookup_extended(d->fields, field, &key, &val)) {
+        g_hash_table_steal(d->fields, field);
+        g_free(key);
+        v = val;
+      }
+      f = g_strdup(field);
+      v = g_list_insert_sorted(v, g_strdup(value), _strcmp_sort);
+      g_hash_table_replace(d->fields, f, v);
+    }
+  }
+}
+
+void caps_add_feature(const char *hash, const char *feature)
 {
   caps *c;
   if (!hash || !feature)
@@ -98,12 +230,17 @@
   }
 }
 
-int caps_has_feature(char *hash, char *feature)
+int caps_has_feature(const char *hash, char *feature, char *bjid)
 {
-  caps *c;
+  caps *c = NULL;
   if (!hash || !feature)
     return 0;
   c = g_hash_table_lookup(caps_cache, hash);
+  if (!c && bjid) {
+    char *key = g_strdup_printf("%s/#%s", bjid, hash);
+    c = g_hash_table_lookup(caps_cache, key);
+    g_free(key);
+  }
   if (c)
     return (g_hash_table_lookup(c->features, feature) != NULL);
   return 0;
@@ -129,16 +266,10 @@
   g_hash_table_foreach(c->features, _caps_foreach_helper, user_data);
 }
 
-gint _strcmp_sort(gconstpointer a, gconstpointer b)
-{
-  return g_strcmp0(a, b);
-}
-
 // Generates the sha1 hash for the special capability "" and returns it
 const char *caps_generate(void)
 {
-  char *identity;
-  GList *features;
+  GList *features, *langs;
   GChecksum *sha1;
   guint8 digest[20];
   gsize digest_size = 20;
@@ -148,10 +279,22 @@
 
   g_hash_table_steal(caps_cache, "");
   sha1 = g_checksum_new(G_CHECKSUM_SHA1);
-  identity = g_strdup_printf("%s/%s//%s<", c->category, c->type,
-                             c->name ? c->name : "");
-  g_checksum_update(sha1, (guchar*)identity, -1);
-  g_free(identity);
+
+  langs = g_hash_table_get_keys(c->identities);
+  langs = g_list_sort(langs, _strcmp_sort);
+  {
+    identity *i;
+    GList *lang;
+    char *identity_S;
+    for (lang=langs; lang; lang=lang->next) {
+      i = g_hash_table_lookup(c->identities, lang->data);
+      identity_S = g_strdup_printf("%s/%s/%s/%s<", i->category, i->type,
+                                   (char *)lang->data, i->name ? i->name : "");
+      g_checksum_update(sha1, (guchar *)identity_S, -1);
+      g_free(identity_S);
+    }
+  }
+  g_list_free(langs);
 
   features = g_hash_table_get_values(c->features);
   features = g_list_sort(features, _strcmp_sort);
@@ -176,4 +319,301 @@
     return hash;
 }
 
+gboolean caps_verify(const char *hash, char *function)
+{
+  GList *features, *langs, *forms;
+  GChecksum *checksum;
+  guint8 digest[20];
+  gsize digest_size = 20;
+  gchar *local_hash;
+  gboolean match = FALSE;
+  caps *c = g_hash_table_lookup(caps_cache, hash);
+
+  if (!g_strcmp0(function, "sha-1")) {
+    checksum = g_checksum_new(G_CHECKSUM_SHA1);
+  } else if (!g_strcmp0(function, "md5")) {
+    checksum = g_checksum_new(G_CHECKSUM_MD5);
+    digest_size = 16;
+  } else
+    return FALSE;
+
+  langs = g_hash_table_get_keys(c->identities);
+  langs = g_list_sort(langs, _strcmp_sort);
+  {
+    identity *i;
+    GList *lang;
+    char *identity_S;
+    for (lang=langs; lang; lang=lang->next) {
+      i = g_hash_table_lookup(c->identities, lang->data);
+      identity_S = g_strdup_printf("%s/%s/%s/%s<", i->category, i->type,
+                                   (char *)lang->data, i->name ? i->name : "");
+      g_checksum_update(checksum, (guchar *)identity_S, -1);
+      g_free(identity_S);
+    }
+  }
+  g_list_free(langs);
+
+  features = g_hash_table_get_values(c->features);
+  features = g_list_sort(features, _strcmp_sort);
+  {
+    GList *feature;
+    for (feature=features; feature; feature=feature->next) {
+      g_checksum_update(checksum, feature->data, -1);
+      g_checksum_update(checksum, (guchar *)"<", -1);
+    }
+  }
+  g_list_free(features);
+
+  forms = g_hash_table_get_keys(c->forms);
+  forms = g_list_sort(forms, _strcmp_sort);
+  {
+    dataform *d;
+    GList *form, *fields;
+    for (form=forms; form; form=form->next) {
+      d = g_hash_table_lookup(c->forms, form->data);
+      g_checksum_update(checksum, form->data, -1);
+      g_checksum_update(checksum, (guchar *)"<", -1);
+      fields = g_hash_table_get_keys(d->fields);
+      fields = g_list_sort(fields, _strcmp_sort);
+      {
+        GList *field;
+        GList *values;
+        for (field=fields; field; field=field->next) {
+          g_checksum_update(checksum, field->data, -1);
+          g_checksum_update(checksum, (guchar *)"<", -1);
+          values = g_hash_table_lookup(d->fields, field->data);
+          {
+            GList *value;
+            for (value=values; value; value=value->next) {
+              g_checksum_update(checksum, value->data, -1);
+              g_checksum_update(checksum, (guchar *)"<", -1);
+            }
+          }
+        }
+      }
+      g_list_free(fields);
+    }
+  }
+  g_list_free(forms);
+
+  g_checksum_get_digest(checksum, digest, &digest_size);
+  local_hash = g_base64_encode(digest, digest_size);
+  g_checksum_free(checksum);
+
+  match = !g_strcmp0(hash, local_hash);
+
+  g_free(local_hash);
+  return match;
+}
+
+static gchar* caps_get_filename(const char* hash)
+{
+  gchar *hash_fs = g_strdup (hash);
+  gchar *dir = (gchar *) settings_opt_get ("caps_directory");
+  gchar *file = NULL;
+
+  if (!dir)
+    goto caps_filename_return;
+
+  {
+    const gchar *valid_fs =
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=";
+    g_strcanon(hash_fs, valid_fs, '-');
+  }
+
+  dir = expand_filename (dir);
+  file = g_strdup_printf ("%s/%s.ini", dir, hash_fs);
+  g_free(dir);
+
+caps_filename_return:
+  g_free(hash_fs);
+  return file;
+}
+
+void caps_copy_to_persistent(const char* hash, char* xml)
+{
+  gchar *file;
+  GList *features, *langs, *forms;
+  GKeyFile *key_file;
+  caps *c;
+  int fd;
+
+  g_free (xml);
+  
+  c = g_hash_table_lookup (caps_cache, hash);
+  if (!c) 
+    goto caps_copy_return;
+
+  file = caps_get_filename (hash);
+  if (!file) 
+    goto caps_copy_return;
+
+  fd = open (file, O_WRONLY|O_CREAT|O_EXCL, S_IRUSR|S_IWUSR);
+  if (fd == -1)
+    goto caps_copy_exists;
+
+  key_file = g_key_file_new ();
+
+  langs = g_hash_table_get_keys (c->identities);
+  {
+    identity *i;
+    GList *lang;
+    gchar *group;
+    for (lang=langs; lang; lang=lang->next) {
+      i = g_hash_table_lookup (c->identities, lang->data);
+      group = g_strdup_printf("identity_%s", (gchar *)lang->data);
+      g_key_file_set_string (key_file, group, "category", i->category);
+      g_key_file_set_string (key_file, group, "type", i->type);
+      g_key_file_set_string (key_file, group, "name", i->name);
+      g_free (group);
+    }
+  }
+  g_list_free (langs);
+
+  features = g_hash_table_get_values (c->features);
+  {
+    GList *feature;
+    gchar **string_list;
+    gint i;
+
+    i = g_list_length (features);
+    string_list = g_new (gchar*, i + 1);
+    i = 0;
+    for (feature=features; feature; feature=feature->next) {
+      string_list[i] = g_strdup(feature->data);
+      ++i;
+    }
+    string_list[i] = NULL;
+
+    g_key_file_set_string_list (key_file, "features", "features",
+                                (const gchar**)string_list, i);
+    g_strfreev (string_list);
+  }
+  g_list_free (features);
+
+  forms = g_hash_table_get_keys(c->forms);
+  {
+    dataform *d;
+    GList *form, *fields;
+    gchar *group;
+    for (form=forms; form; form=form->next) {
+      d = g_hash_table_lookup (c->forms, form->data);
+      group = g_strdup_printf ("form_%s", (gchar *)form->data);
+      fields = g_hash_table_get_keys(d->fields);
+      {
+        GList *field;
+        GList *values;
+        for (field=fields; field; field=field->next) {
+          values = g_hash_table_lookup (d->fields, field->data);
+	  {
+	    GList *value;
+	    gchar **string_list;
+	    gint i;
+	    i = g_list_length (values);
+	    string_list = g_new (gchar*, i + 1);
+	    i = 0;
+	    for (value=values; value; value=value->next) {
+	      string_list[i] = g_strdup(value->data);
+	      ++i;
+	    }
+	    string_list[i] = NULL;
+
+	    g_key_file_set_string_list (key_file, group, field->data,
+					(const gchar**)string_list, i);
+	    
+	    g_strfreev (string_list);
+	  }
+        }
+      }
+      g_list_free(fields);
+      g_free (group);
+    }
+  }
+  g_list_free (forms);
+
+  {
+    gchar *data;
+    gsize length;
+    data = g_key_file_to_data (key_file, &length, NULL);
+    write (fd, data, length);
+    g_free(data);
+    close (fd);
+  }
+
+  g_key_file_free(key_file);
+caps_copy_exists:
+  g_free(file);
+caps_copy_return:
+  return;
+}
+
+gboolean caps_restore_from_persistent (const char* hash)
+{
+  gchar *file;
+  GKeyFile *key_file;
+  gchar **groups, **group;
+  gboolean restored = FALSE;
+
+  file = caps_get_filename (hash);
+  if (!file)
+    goto caps_restore_no_file;
+
+  key_file = g_key_file_new ();
+  if (!g_key_file_load_from_file (key_file, file, G_KEY_FILE_NONE, NULL))
+    goto caps_restore_bad_file;
+
+  caps_add(hash);
+
+  groups = g_key_file_get_groups (key_file, NULL);
+  for (group = groups; *group; ++group) {
+    if (!g_strcmp0(*group, "features")) {
+      gchar **features, **feature;
+      features = g_key_file_get_string_list (key_file, *group, "features",
+                                             NULL, NULL);
+      for (feature = features; *feature; ++feature) {
+        caps_add_feature(hash, *feature);
+      }
+
+      g_strfreev (features);
+    } else if (g_str_has_prefix (*group, "identity_")) {
+      gchar *category, *type, *name, *lang;
+
+      category = g_key_file_get_string(key_file, *group, "category", NULL);
+      type = g_key_file_get_string(key_file, *group, "type", NULL);
+      name = g_key_file_get_string(key_file, *group, "name", NULL);
+      lang = *group + 9; /* "identity_" */
+
+      caps_add_identity(hash, category, name, type, lang);
+      g_free(category);
+      g_free(type);
+      g_free(name);
+    } else if (g_str_has_prefix (*group, "form_")) {
+      gchar *formtype;
+      gchar **fields, **field;
+      formtype = *group + 5; /* "form_" */
+      caps_add_dataform (hash, formtype);
+
+      fields = g_key_file_get_keys(key_file, *group, NULL, NULL);
+      for (field = fields; *field; ++field) {
+        gchar **values, **value;
+        values = g_key_file_get_string_list (key_file, *group, *field,
+	                                     NULL, NULL);
+	for (value = values; *value; ++value) {
+	  caps_add_dataform_field (hash, formtype, *field, *value);
+	}
+	g_strfreev (values);
+      }
+      g_strfreev (fields);
+    }
+  }
+  g_strfreev(groups);
+  restored = TRUE;
+
+caps_restore_bad_file:
+  g_key_file_free (key_file);
+  g_free (file);
+caps_restore_no_file:
+  return restored;
+}
+
 /* vim: set expandtab cindent cinoptions=>2\:2(0 sw=2 ts=2:  For Vim users... */
--- a/mcabber/mcabber/caps.h	Sun Jul 24 13:30:47 2011 +0200
+++ b/mcabber/mcabber/caps.h	Mon Oct 03 16:00:34 2011 +0200
@@ -5,17 +5,30 @@
 
 void  caps_init(void);
 void  caps_free(void);
-void  caps_add(char *hash);
-int   caps_has_hash(const char *hash);
+void  caps_add(const char *hash);
+void  caps_remove(const char *hash);
+void  caps_move_to_local(const char *hash, char *bjid);
+int   caps_has_hash(const char *hash, const char *bjid);
+void  caps_add_identity(const char *hash,
+                        const char *category,
+                        const char *name,
+                        const char *type,
+                        const char *lang);
 void  caps_set_identity(char *hash,
                         const char *category,
                         const char *name,
                         const char *type);
+void  caps_add_dataform(const char *hash, const char *formtype);
+void  caps_add_dataform_field(const char *hash, const char *formtype,
+                              const char *field, const char *value);
 void  caps_add_feature(char *hash, const char *feature);
-int   caps_has_feature(char *hash, char *feature);
+int   caps_has_feature(char *hash, char *feature, char *bjid);
 void  caps_foreach_feature(const char *hash, GFunc func, gpointer user_data);
 
 char *caps_generate(void);
+gboolean caps_verify(const char *hash, char *function);
+void  caps_copy_to_persistent(const char *hash, char *xml);
+gboolean caps_restore_from_persistent(const char *hash);
 
 #endif /* __MCABBER_CAPS_H__ */
 
--- a/mcabber/mcabber/xmpp.c	Sun Jul 24 13:30:47 2011 +0200
+++ b/mcabber/mcabber/xmpp.c	Mon Oct 03 16:00:34 2011 +0200
@@ -318,8 +318,9 @@
 #ifdef HAVE_LIBOTR
   int otr_msg = 0;
 #endif
+  char *barejid;
 #if defined HAVE_GPGME || defined XEP0022 || defined XEP0085
-  char *rname, *barejid;
+  char *rname;
   GSList *sl_buddy;
 #endif
 #if defined XEP0022 || defined XEP0085
@@ -349,10 +350,10 @@
       subtype = LM_MESSAGE_SUB_TYPE_CHAT;
   }
 
+  barejid = jidtodisp(fjid);
 #if defined HAVE_GPGME || defined HAVE_LIBOTR || \
     defined XEP0022 || defined XEP0085
   rname = strchr(fjid, JID_RESOURCE_SEPARATOR);
-  barejid = jidtodisp(fjid);
   sl_buddy = roster_find(barejid, jidsearch, ROSTER_TYPE_USER);
 
   // If we can get a resource name, we use it.  Else we use NULL,
@@ -406,7 +407,6 @@
   }
 #endif // HAVE_GPGME
 
-  g_free(barejid);
 #endif // HAVE_GPGME || defined XEP0022 || defined XEP0085
 
   x = lm_message_new_with_sub_type(fjid, LM_MESSAGE_TYPE_MESSAGE, subtype);
@@ -428,12 +428,13 @@
   // XEP-0184: Message Receipts
   if (sl_buddy && xep184 &&
       caps_has_feature(buddy_resource_getcaps(sl_buddy->data, rname),
-                       NS_RECEIPTS)) {
+                       NS_RECEIPTS, barejid)) {
     lm_message_node_set_attribute
             (lm_message_node_add_child(x->node, "request", NULL),
              "xmlns", NS_RECEIPTS);
     *xep184 = lm_message_handler_new(cb_xep184, NULL, NULL);
   }
+  g_free(barejid);
 
 #if defined XEP0022 || defined XEP0085
   // If typing notifications are disabled, we can skip all this stuff...
@@ -1326,28 +1327,99 @@
                                LmMessage *m, gpointer user_data)
 {
   char *ver = user_data;
+  char *hash;
+  const char *from = lm_message_get_from(m);
+  char *bjid = jidtodisp(from);
   LmMessageSubType mstype = lm_message_get_sub_type(m);
 
-  caps_add(ver);
-  if (mstype == LM_MESSAGE_SUB_TYPE_ERROR) {
-    display_server_error(lm_message_node_get_child(m->node, "error"),
-                         lm_message_get_from(m));
-  } else if (mstype == LM_MESSAGE_SUB_TYPE_RESULT) {
+  hash = strchr(ver, ',');
+  if (hash)
+    *hash++ = '\0';
+
+  if (mstype == LM_MESSAGE_SUB_TYPE_RESULT) {
     LmMessageNode *info;
     LmMessageNode *query = lm_message_node_get_child(m->node, "query");
 
+    if (caps_has_hash(ver, bjid))
+      goto caps_callback_return;
+
+    caps_add(ver);
+
     info = lm_message_node_get_child(query, "identity");
-    if (info)
-      caps_set_identity(ver, lm_message_node_get_attribute(info, "category"),
-                        lm_message_node_get_attribute(info, "name"),
-                        lm_message_node_get_attribute(info, "type"));
+    while (info) {
+      if (!g_strcmp0(info->name, "identity"))
+        caps_add_identity(ver, lm_message_node_get_attribute(info, "category"),
+                          lm_message_node_get_attribute(info, "name"),
+                          lm_message_node_get_attribute(info, "type"),
+                          lm_message_node_get_attribute(info, "xml:lang"));
+        info = info->next;
+    }
+    
     info = lm_message_node_get_child(query, "feature");
     while (info) {
       if (!g_strcmp0(info->name, "feature"))
         caps_add_feature(ver, lm_message_node_get_attribute(info, "var"));
       info = info->next;
     }
+
+    info = lm_message_node_get_child(query, "x");
+    {
+      LmMessageNode *field;
+      LmMessageNode *value;
+      const char *formtype, *var;
+      while (info) {
+        if (!g_strcmp0(info->name, "x")
+            && !g_strcmp0(lm_message_node_get_attribute(info, "type"),
+                          "result")
+            && !g_strcmp0(lm_message_node_get_attribute(info, "xmlns"),
+                          "jabber:x:data")) {
+          field = lm_message_node_get_child(info, "field");
+          formtype = NULL;
+          while (field) {
+            if (!g_strcmp0(field->name, "field")
+                && !g_strcmp0(lm_message_node_get_attribute(field, "var"),
+                              "FORM_TYPE")
+                && !g_strcmp0(lm_message_node_get_attribute(field, "type"),
+                              "hidden")) {
+              value = lm_message_node_get_child(field, "value");
+              if (value)
+                formtype = lm_message_node_get_value(value);
+            }
+            field = field->next;
+          }
+          if (formtype) {
+            caps_add_dataform(ver, formtype);
+            field = lm_message_node_get_child(info, "field");
+            while (field) {
+              var = lm_message_node_get_attribute(field, "var");
+              if (!g_strcmp0(field->name, "field")
+                  && (g_strcmp0(var, "FORM_TYPE")
+                  || g_strcmp0(lm_message_node_get_attribute(field, "type"),
+                               "hidden"))) {
+                value = lm_message_node_get_child(field, "value");
+                while (value) {
+                  if (!g_strcmp0(value->name, "value"))
+                    caps_add_dataform_field(ver, formtype, var,
+                      lm_message_node_get_value(value));
+                  value = value->next;
+                }
+              }
+              field = field->next;
+            }
+          }
+        }
+        info = info->next;
+      }
+    }
+
+    if (caps_verify(ver, hash))
+      caps_copy_to_persistent(ver, lm_message_node_to_string(query));
+    else 
+      caps_move_to_local(ver, bjid);
   }
+
+caps_callback_return:
+  g_free(bjid);
   g_free(ver);
   return LM_HANDLER_RESULT_REMOVE_MESSAGE;
 }
@@ -1462,12 +1534,15 @@
   caps = lm_message_node_find_xmlns(m->node, NS_CAPS);
   if (caps && ust != offline) {
     const char *ver = lm_message_node_get_attribute(caps, "ver");
+    const char *hash = lm_message_node_get_attribute(caps, "hash");
     GSList *sl_buddy = NULL;
 
-    if (!ver) {
-      scr_LogPrint(LPRINT_LOGNORM, "Error: malformed caps version (%s)", bjid);
+    if (!hash) {
+      // No support for legacy format
       goto handle_presence_return;
     }
+    if (!ver || !g_strcmp0(ver, "") || !g_strcmp0(hash, ""))
+      goto handle_presence_return;
 
     if (rname)
       sl_buddy = roster_find(bjid, jidsearch, ROSTER_TYPE_USER);
@@ -1475,7 +1550,7 @@
     if (sl_buddy && buddy_getonserverflag(sl_buddy->data)) {
       buddy_resource_setcaps(sl_buddy->data, rname, ver);
 
-      if (!caps_has_hash(ver)) {
+      if (!caps_has_hash(ver, bjid) && !caps_restore_from_persistent(ver)) {
         char *node;
         LmMessageHandler *handler;
         LmMessage *iq = lm_message_new_with_sub_type(from, LM_MESSAGE_TYPE_IQ,
@@ -1489,7 +1564,9 @@
                  "node", node,
                  NULL);
         g_free(node);
-        handler = lm_message_handler_new(cb_caps, g_strdup(ver), NULL);
+        handler = lm_message_handler_new(cb_caps,
+                                         g_strdup_printf("%s,%s",ver,hash),
+                                         NULL);
         lm_connection_send_with_reply(connection, iq, handler, NULL);
         lm_message_unref(iq);
         lm_message_handler_unref(handler);
--- a/mcabber/mcabber/xmpp_helper.c	Sun Jul 24 13:30:47 2011 +0200
+++ b/mcabber/mcabber/xmpp_helper.c	Mon Oct 03 16:00:34 2011 +0200
@@ -224,6 +224,7 @@
   caps_add("");
   caps_set_identity("", "client", PACKAGE_STRING, "pc");
   caps_add_feature("", NS_DISCO_INFO);
+  caps_add_feature("", NS_CAPS);
   caps_add_feature("", NS_MUC);
   // advertise ChatStates only if they aren't disabled
   if (!settings_opt_get_int("disable_chatstates"))
--- a/mcabber/mcabber/xmpp_iq.c	Sun Jul 24 13:30:47 2011 +0200
+++ b/mcabber/mcabber/xmpp_iq.c	Mon Oct 03 16:00:34 2011 +0200
@@ -521,14 +521,10 @@
 
   if (entitycaps)
     caps_foreach_feature(entitycaps, _disco_add_feature_helper, ansquery);
-  else {
+  else
     caps_foreach_feature(entity_version(xmpp_getstatus()),
                          _disco_add_feature_helper,
                          ansquery);
-    lm_message_node_set_attribute
-            (lm_message_node_add_child(ansquery, "feature", NULL),
-             "var", NS_CAPS);
-  }
 }
 
 LmHandlerResult handle_iq_disco_info(LmMessageHandler *h,