diff mcabber/mcabber/caps.c @ 1999:51f032d5ca22

Add support for XEP-0115 Entity Capabilities, with offline cache
author Hermitifier
date Mon, 03 Oct 2011 16:00:34 +0200
parents c30fa2baf387
children 76d7c5721210
line wrap: on
line diff
--- 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... */