Mercurial > pidgin
diff libpurple/protocols/jabber/caps.c @ 25985:c4fd9222dda1
propagate from branch 'im.pidgin.pidgin' (head 303af74a38e7b313d4fb0be4d4054a16cb13d819)
to branch 'im.pidgin.cpw.darkrain42.xmpp.bosh' (head 650d82b8a5f0c8860804dd8004cd54badea48e1e)
author | Paul Aurich <paul@darkrain42.org> |
---|---|
date | Sat, 07 Mar 2009 01:59:40 +0000 |
parents | 5f9a24d1c25e 5ad14a53e266 |
children | 882748e27f9d |
line wrap: on
line diff
--- a/libpurple/protocols/jabber/caps.c Thu Mar 05 23:54:50 2009 +0000 +++ b/libpurple/protocols/jabber/caps.c Sat Mar 07 01:59:40 2009 +0000 @@ -21,99 +21,222 @@ #include "internal.h" +#include "debug.h" #include "caps.h" -#include <string.h> -#include "internal.h" +#include "cipher.h" +#include "iq.h" +#include "presence.h" #include "util.h" -#include "iq.h" #define JABBER_CAPS_FILENAME "xmpp-caps.xml" -static GHashTable *capstable = NULL; /* JabberCapsKey -> JabberCapsValue */ +typedef struct _JabberDataFormField { + gchar *var; + GList *values; +} JabberDataFormField; typedef struct _JabberCapsKey { char *node; char *ver; + char *hash; } JabberCapsKey; -typedef struct _JabberCapsValueExt { - GList *identities; /* JabberCapsIdentity */ - GList *features; /* char * */ -} JabberCapsValueExt; +static GHashTable *capstable = NULL; /* JabberCapsKey -> JabberCapsClientInfo */ +static GHashTable *nodetable = NULL; /* char *node -> JabberCapsNodeExts */ +static guint save_timer = 0; + +/** + * Processes a query-node and returns a JabberCapsClientInfo object with all relevant info. + * + * @param query A query object. + * @return A JabberCapsClientInfo object. + */ +static JabberCapsClientInfo *jabber_caps_parse_client_info(xmlnode *query); + +/* Free a GList of allocated char* */ +static void +free_string_glist(GList *list) +{ + g_list_foreach(list, (GFunc)g_free, NULL); + g_list_free(list); +} + +static JabberCapsNodeExts* +jabber_caps_node_exts_ref(JabberCapsNodeExts *exts) +{ + g_return_val_if_fail(exts != NULL, NULL); -typedef struct _JabberCapsValue { - GList *identities; /* JabberCapsIdentity */ - GList *features; /* char * */ - GHashTable *ext; /* char * -> JabberCapsValueExt */ -} JabberCapsValue; + ++exts->ref; + return exts; +} + +static void +jabber_caps_node_exts_unref(JabberCapsNodeExts *exts) +{ + if (exts == NULL) + return; + + g_return_if_fail(exts->ref != 0); -static guint jabber_caps_hash(gconstpointer key) { - const JabberCapsKey *name = key; - guint nodehash = g_str_hash(name->node); - guint verhash = g_str_hash(name->ver); + if (--exts->ref != 0) + return; + + g_hash_table_destroy(exts->exts); + g_free(exts); +} - return nodehash ^ verhash; +static guint jabber_caps_hash(gconstpointer data) { + const JabberCapsKey *key = data; + guint nodehash = g_str_hash(key->node); + guint verhash = g_str_hash(key->ver); + /* 'hash' was optional in XEP-0115 v1.4 and I think g_str_hash crashes on + * NULL >:O. Okay, maybe I've played too much Zelda, but that looks like + * a Deku Shrub... */ + guint hashhash = (key->hash ? g_str_hash(key->hash) : 0); + return nodehash ^ verhash ^ hashhash; } static gboolean jabber_caps_compare(gconstpointer v1, gconstpointer v2) { const JabberCapsKey *name1 = v1; const JabberCapsKey *name2 = v2; + /* Again, hash might be NULL and I *know* strcmp will crash on NULL. */ + gboolean hasheq = ((!name1->hash && !name2->hash) || + (name1->hash && name2->hash && !strcmp(name1->hash, name2->hash))); - return strcmp(name1->node,name2->node) == 0 && strcmp(name1->ver,name2->ver) == 0; + return strcmp(name1->node, name2->node) == 0 && + strcmp(name1->ver, name2->ver) == 0 && + hasheq; } -static void jabber_caps_destroy_key(gpointer key) { - JabberCapsKey *keystruct = key; - g_free(keystruct->node); - g_free(keystruct->ver); - g_free(keystruct); +void jabber_caps_destroy_key(gpointer data) { + JabberCapsKey *key = data; + g_free(key->node); + g_free(key->ver); + g_free(key->hash); + g_free(key); } -static void jabber_caps_destroy_value(gpointer value) { - JabberCapsValue *valuestruct = value; - while(valuestruct->identities) { - JabberCapsIdentity *id = valuestruct->identities->data; +static void +jabber_caps_client_info_destroy(JabberCapsClientInfo *info) +{ + if (info == NULL) + return; + + while(info->identities) { + JabberIdentity *id = info->identities->data; g_free(id->category); g_free(id->type); g_free(id->name); + g_free(id->lang); g_free(id); - - valuestruct->identities = g_list_delete_link(valuestruct->identities,valuestruct->identities); + info->identities = g_list_delete_link(info->identities, info->identities); } - while(valuestruct->features) { - g_free(valuestruct->features->data); - valuestruct->features = g_list_delete_link(valuestruct->features,valuestruct->features); + + free_string_glist(info->features); + free_string_glist(info->forms); + + jabber_caps_node_exts_unref(info->exts); + + g_free(info); +} + +/* NOTE: Takes a reference to the exts, unref it if you don't really want to + * keep it around. */ +static JabberCapsNodeExts* +jabber_caps_find_exts_by_node(const char *node) +{ + JabberCapsNodeExts *exts; + if (NULL == (exts = g_hash_table_lookup(nodetable, node))) { + exts = g_new0(JabberCapsNodeExts, 1); + exts->exts = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, + (GDestroyNotify)free_string_glist); + g_hash_table_insert(nodetable, g_strdup(node), jabber_caps_node_exts_ref(exts)); } - g_hash_table_destroy(valuestruct->ext); - g_free(valuestruct); + + return jabber_caps_node_exts_ref(exts); +} + +static void +exts_to_xmlnode(gconstpointer key, gconstpointer value, gpointer user_data) +{ + const char *identifier = key; + const GList *features = value, *node; + xmlnode *client = user_data, *ext, *feature; + + ext = xmlnode_new_child(client, "ext"); + xmlnode_set_attrib(ext, "identifier", identifier); + + for (node = features; node; node = node->next) { + feature = xmlnode_new_child(ext, "feature"); + xmlnode_set_attrib(feature, "var", (const gchar *)node->data); + } } -static void jabber_caps_ext_destroy_value(gpointer value) { - JabberCapsValueExt *valuestruct = value; - while(valuestruct->identities) { - JabberCapsIdentity *id = valuestruct->identities->data; - g_free(id->category); - g_free(id->type); - g_free(id->name); - g_free(id); +static void jabber_caps_store_client(gpointer key, gpointer value, gpointer user_data) { + JabberCapsKey *clientinfo = key; + JabberCapsClientInfo *props = value; + xmlnode *root = user_data; + xmlnode *client = xmlnode_new_child(root, "client"); + GList *iter; - valuestruct->identities = g_list_delete_link(valuestruct->identities,valuestruct->identities); + xmlnode_set_attrib(client, "node", clientinfo->node); + xmlnode_set_attrib(client, "ver", clientinfo->ver); + if (clientinfo->hash) + xmlnode_set_attrib(client, "hash", clientinfo->hash); + for(iter = props->identities; iter; iter = g_list_next(iter)) { + JabberIdentity *id = iter->data; + xmlnode *identity = xmlnode_new_child(client, "identity"); + xmlnode_set_attrib(identity, "category", id->category); + xmlnode_set_attrib(identity, "type", id->type); + if (id->name) + xmlnode_set_attrib(identity, "name", id->name); + if (id->lang) + xmlnode_set_attrib(identity, "lang", id->lang); } - while(valuestruct->features) { - g_free(valuestruct->features->data); - valuestruct->features = g_list_delete_link(valuestruct->features,valuestruct->features); + + for(iter = props->features; iter; iter = g_list_next(iter)) { + const char *feat = iter->data; + xmlnode *feature = xmlnode_new_child(client, "feature"); + xmlnode_set_attrib(feature, "var", feat); } - g_free(valuestruct); + + for(iter = props->forms; iter; iter = g_list_next(iter)) { + /* FIXME: See #7814 */ + xmlnode *xdata = iter->data; + xmlnode_insert_child(client, xmlnode_copy(xdata)); + } + + /* TODO: Ideally, only save this once-per-node... */ + if (props->exts) + g_hash_table_foreach(props->exts->exts, (GHFunc)exts_to_xmlnode, client); } -static void jabber_caps_load(void); +static gboolean +do_jabber_caps_store(gpointer data) +{ + char *str; + int length = 0; + xmlnode *root = xmlnode_new("capabilities"); + g_hash_table_foreach(capstable, jabber_caps_store_client, root); + str = xmlnode_to_formatted_str(root, &length); + xmlnode_free(root); + purple_util_write_data_to_file(JABBER_CAPS_FILENAME, str, length); + g_free(str); -void jabber_caps_init(void) { - capstable = g_hash_table_new_full(jabber_caps_hash, jabber_caps_compare, jabber_caps_destroy_key, jabber_caps_destroy_value); - jabber_caps_load(); + save_timer = 0; + return FALSE; } -static void jabber_caps_load(void) { +static void +schedule_caps_save(void) +{ + if (save_timer == 0) + save_timer = purple_timeout_add_seconds(5, do_jabber_caps_store, NULL); +} + +static void +jabber_caps_load(void) +{ xmlnode *capsdata = purple_util_read_xml_from_file(JABBER_CAPS_FILENAME, "XMPP capabilities cache"); xmlnode *client; @@ -124,17 +247,23 @@ xmlnode_free(capsdata); return; } - + for(client = capsdata->child; client; client = client->next) { if(client->type != XMLNODE_TYPE_TAG) continue; if(!strcmp(client->name, "client")) { JabberCapsKey *key = g_new0(JabberCapsKey, 1); - JabberCapsValue *value = g_new0(JabberCapsValue, 1); + JabberCapsClientInfo *value = g_new0(JabberCapsClientInfo, 1); xmlnode *child; + JabberCapsNodeExts *exts = NULL; key->node = g_strdup(xmlnode_get_attrib(client,"node")); key->ver = g_strdup(xmlnode_get_attrib(client,"ver")); - value->ext = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, jabber_caps_ext_destroy_value); + key->hash = g_strdup(xmlnode_get_attrib(client,"hash")); + + /* v1.3 capabilities */ + if (key->hash == NULL) + exts = jabber_caps_find_exts_by_node(key->node); + for(child = client->child; child; child = child->next) { if(child->type != XMLNODE_TYPE_TAG) continue; @@ -147,432 +276,654 @@ const char *category = xmlnode_get_attrib(child, "category"); const char *type = xmlnode_get_attrib(child, "type"); const char *name = xmlnode_get_attrib(child, "name"); + const char *lang = xmlnode_get_attrib(child, "lang"); + JabberIdentity *id; - JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); + if (!category || !type) + continue; + + id = g_new0(JabberIdentity, 1); id->category = g_strdup(category); id->type = g_strdup(type); id->name = g_strdup(name); - + id->lang = g_strdup(lang); + value->identities = g_list_append(value->identities,id); - } else if(!strcmp(child->name,"ext")) { + } else if(!strcmp(child->name,"x")) { + /* FIXME: See #7814 -- this will cause problems if anyone + * ever actually specifies forms. In fact, for this to + * work properly, that bug needs to be fixed in + * xmlnode_from_str, not the output version... */ + value->forms = g_list_append(value->forms, xmlnode_copy(child)); + } else if (!strcmp(child->name, "ext") && key->hash != NULL) { + purple_debug_warning("jabber", "Ignoring exts when reading new-style caps\n"); + } else if (!strcmp(child->name, "ext")) { + /* TODO: Do we care about reading in the identities listed here? */ const char *identifier = xmlnode_get_attrib(child, "identifier"); - if(identifier) { - xmlnode *extchild; + xmlnode *node; + GList *features = NULL; + + if (!identifier) + continue; - JabberCapsValueExt *extvalue = g_new0(JabberCapsValueExt, 1); - - for(extchild = child->child; extchild; extchild = extchild->next) { - if(extchild->type != XMLNODE_TYPE_TAG) + for (node = child->child; node; node = node->next) { + if (node->type != XMLNODE_TYPE_TAG) + continue; + if (!strcmp(node->name, "feature")) { + const char *var = xmlnode_get_attrib(node, "var"); + if (!var) continue; - if(!strcmp(extchild->name,"feature")) { - const char *var = xmlnode_get_attrib(extchild, "var"); - if(!var) - continue; - extvalue->features = g_list_append(extvalue->features,g_strdup(var)); - } else if(!strcmp(extchild->name,"identity")) { - const char *category = xmlnode_get_attrib(extchild, "category"); - const char *type = xmlnode_get_attrib(extchild, "type"); - const char *name = xmlnode_get_attrib(extchild, "name"); + features = g_list_prepend(features, g_strdup(var)); + } + } - JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); - id->category = g_strdup(category); - id->type = g_strdup(type); - id->name = g_strdup(name); - - extvalue->identities = g_list_append(extvalue->identities,id); - } - } - g_hash_table_replace(value->ext, g_strdup(identifier), extvalue); - } + if (features) { + g_hash_table_insert(exts->exts, g_strdup(identifier), + features); + } else + purple_debug_warning("jabber", "Caps ext %s had no features.\n", + identifier); } } + + value->exts = exts; g_hash_table_replace(capstable, key, value); + } } xmlnode_free(capsdata); } -static void jabber_caps_store_ext(gpointer key, gpointer value, gpointer user_data) { - const char *extname = key; - JabberCapsValueExt *props = value; - xmlnode *root = user_data; - xmlnode *ext = xmlnode_new_child(root,"ext"); - GList *iter; - - xmlnode_set_attrib(ext,"identifier",extname); - - for(iter = props->identities; iter; iter = g_list_next(iter)) { - JabberCapsIdentity *id = iter->data; - xmlnode *identity = xmlnode_new_child(ext, "identity"); - xmlnode_set_attrib(identity, "category", id->category); - xmlnode_set_attrib(identity, "type", id->type); - if (id->name) - xmlnode_set_attrib(identity, "name", id->name); - } - - for(iter = props->features; iter; iter = g_list_next(iter)) { - const char *feat = iter->data; - xmlnode *feature = xmlnode_new_child(ext, "feature"); - xmlnode_set_attrib(feature, "var", feat); - } -} - -static void jabber_caps_store_client(gpointer key, gpointer value, gpointer user_data) { - JabberCapsKey *clientinfo = key; - JabberCapsValue *props = value; - xmlnode *root = user_data; - xmlnode *client = xmlnode_new_child(root,"client"); - GList *iter; - - xmlnode_set_attrib(client,"node",clientinfo->node); - xmlnode_set_attrib(client,"ver",clientinfo->ver); - - for(iter = props->identities; iter; iter = g_list_next(iter)) { - JabberCapsIdentity *id = iter->data; - xmlnode *identity = xmlnode_new_child(client, "identity"); - xmlnode_set_attrib(identity, "category", id->category); - xmlnode_set_attrib(identity, "type", id->type); - if (id->name) - xmlnode_set_attrib(identity, "name", id->name); - } - - for(iter = props->features; iter; iter = g_list_next(iter)) { - const char *feat = iter->data; - xmlnode *feature = xmlnode_new_child(client, "feature"); - xmlnode_set_attrib(feature, "var", feat); - } - - g_hash_table_foreach(props->ext,jabber_caps_store_ext,client); -} - -static void jabber_caps_store(void) { - char *str; - xmlnode *root = xmlnode_new("capabilities"); - g_hash_table_foreach(capstable, jabber_caps_store_client, root); - str = xmlnode_to_formatted_str(root, NULL); - xmlnode_free(root); - purple_util_write_data_to_file(JABBER_CAPS_FILENAME, str, -1); - g_free(str); +void jabber_caps_init(void) +{ + nodetable = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)jabber_caps_node_exts_unref); + capstable = g_hash_table_new_full(jabber_caps_hash, jabber_caps_compare, jabber_caps_destroy_key, (GDestroyNotify)jabber_caps_client_info_destroy); + jabber_caps_load(); } -/* this function assumes that all information is available locally */ -static JabberCapsClientInfo *jabber_caps_collect_info(const char *node, const char *ver, GList *ext) { - JabberCapsClientInfo *result; - JabberCapsKey *key = g_new0(JabberCapsKey, 1); - JabberCapsValue *caps; - GList *iter; - - key->node = (char *)node; - key->ver = (char *)ver; - - caps = g_hash_table_lookup(capstable,key); - - g_free(key); - - if (caps == NULL) - return NULL; - - result = g_new0(JabberCapsClientInfo, 1); - - /* join all information */ - for(iter = caps->identities; iter; iter = g_list_next(iter)) { - JabberCapsIdentity *id = iter->data; - JabberCapsIdentity *newid = g_new0(JabberCapsIdentity, 1); - newid->category = g_strdup(id->category); - newid->type = g_strdup(id->type); - newid->name = g_strdup(id->name); - - result->identities = g_list_append(result->identities,newid); - } - for(iter = caps->features; iter; iter = g_list_next(iter)) { - const char *feat = iter->data; - char *newfeat = g_strdup(feat); - - result->features = g_list_append(result->features,newfeat); +void jabber_caps_uninit(void) +{ + if (save_timer != 0) { + purple_timeout_remove(save_timer); + save_timer = 0; + do_jabber_caps_store(NULL); } - - for(iter = ext; iter; iter = g_list_next(iter)) { - const char *extname = iter->data; - JabberCapsValueExt *extinfo = g_hash_table_lookup(caps->ext,extname); - - if(extinfo) { - GList *iter2; - for(iter2 = extinfo->identities; iter2; iter2 = g_list_next(iter2)) { - JabberCapsIdentity *id = iter2->data; - JabberCapsIdentity *newid = g_new0(JabberCapsIdentity, 1); - newid->category = g_strdup(id->category); - newid->type = g_strdup(id->type); - newid->name = g_strdup(id->name); - - result->identities = g_list_append(result->identities,newid); - } - for(iter2 = extinfo->features; iter2; iter2 = g_list_next(iter2)) { - const char *feat = iter2->data; - char *newfeat = g_strdup(feat); - - result->features = g_list_append(result->features,newfeat); - } - } - } - return result; -} - -void jabber_caps_free_clientinfo(JabberCapsClientInfo *clientinfo) { - if(!clientinfo) - return; - while(clientinfo->identities) { - JabberCapsIdentity *id = clientinfo->identities->data; - g_free(id->category); - g_free(id->type); - g_free(id->name); - g_free(id); - - clientinfo->identities = g_list_delete_link(clientinfo->identities,clientinfo->identities); - } - while(clientinfo->features) { - char *feat = clientinfo->features->data; - g_free(feat); - - clientinfo->features = g_list_delete_link(clientinfo->features,clientinfo->features); - } - - g_free(clientinfo); + g_hash_table_destroy(capstable); + g_hash_table_destroy(nodetable); + capstable = NULL; } typedef struct _jabber_caps_cbplususerdata { + guint ref; + jabber_caps_get_info_cb cb; - gpointer user_data; + gpointer cb_data; char *who; char *node; char *ver; - GList *ext; - unsigned extOutstanding; + char *hash; + + JabberCapsClientInfo *info; + + GList *exts; + guint extOutstanding; + JabberCapsNodeExts *node_exts; } jabber_caps_cbplususerdata; -typedef struct jabber_ext_userdata { - jabber_caps_cbplususerdata *userdata; - char *node; -} jabber_ext_userdata; +static jabber_caps_cbplususerdata* +cbplususerdata_ref(jabber_caps_cbplususerdata *data) +{ + g_return_val_if_fail(data != NULL, NULL); -static void jabber_caps_get_info_check_completion(jabber_caps_cbplususerdata *userdata) { - if(userdata->extOutstanding == 0) { - userdata->cb(jabber_caps_collect_info(userdata->node, userdata->ver, userdata->ext), userdata->user_data); - g_free(userdata->who); - g_free(userdata->node); - g_free(userdata->ver); - while(userdata->ext) { - g_free(userdata->ext->data); - userdata->ext = g_list_delete_link(userdata->ext,userdata->ext); - } - g_free(userdata); - } + ++data->ref; + return data; } -static void jabber_caps_ext_iqcb(JabberStream *js, xmlnode *packet, gpointer data) { - /* collect data and fetch all exts */ - xmlnode *query = xmlnode_get_child_with_namespace(packet, "query", "http://jabber.org/protocol/disco#info"); - jabber_ext_userdata *extuserdata = data; - jabber_caps_cbplususerdata *userdata = extuserdata->userdata; - const char *node = extuserdata->node; - - --userdata->extOutstanding; - - /* TODO: Better error handling */ +static void +cbplususerdata_unref(jabber_caps_cbplususerdata *data) +{ + if (data == NULL) + return; - if(node && query) { - const char *key; - JabberCapsValue *client; - xmlnode *child; - JabberCapsValueExt *value = g_new0(JabberCapsValueExt, 1); - JabberCapsKey *clientkey = g_new0(JabberCapsKey, 1); + g_return_if_fail(data->ref != 0); - clientkey->node = userdata->node; - clientkey->ver = userdata->ver; - - client = g_hash_table_lookup(capstable, clientkey); - - g_free(clientkey); + if (--data->ref > 0) + return; - /* split node by #, key either points to \0 or the correct ext afterwards */ - for(key = node; key[0] != '\0'; ++key) { - if(key[0] == '#') { - ++key; - break; - } - } + g_free(data->who); + g_free(data->node); + g_free(data->ver); + g_free(data->hash); - for(child = query->child; child; child = child->next) { - if(child->type != XMLNODE_TYPE_TAG) - continue; - if(!strcmp(child->name,"feature")) { - const char *var = xmlnode_get_attrib(child, "var"); - if(!var) - continue; - value->features = g_list_append(value->features,g_strdup(var)); - } else if(!strcmp(child->name,"identity")) { - const char *category = xmlnode_get_attrib(child, "category"); - const char *type = xmlnode_get_attrib(child, "type"); - const char *name = xmlnode_get_attrib(child, "name"); - - JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); - id->category = g_strdup(category); - id->type = g_strdup(type); - id->name = g_strdup(name); - - value->identities = g_list_append(value->identities,id); - } - } - g_hash_table_replace(client->ext, g_strdup(key), value); - - jabber_caps_store(); - } - - g_free(extuserdata->node); - g_free(extuserdata); - jabber_caps_get_info_check_completion(userdata); + /* If we have info here, it's already in the capstable, so don't free it */ + if (data->exts) + free_string_glist(data->exts); + if (data->node_exts) + jabber_caps_node_exts_unref(data->node_exts); + g_free(data); } -static void jabber_caps_client_iqcb(JabberStream *js, xmlnode *packet, gpointer data) { - /* collect data and fetch all exts */ +static void +jabber_caps_get_info_complete(jabber_caps_cbplususerdata *userdata) +{ + userdata->cb(userdata->info, userdata->exts, userdata->cb_data); + userdata->info = NULL; + userdata->exts = NULL; + + if (userdata->ref != 1) + purple_debug_warning("jabber", "Lost a reference to caps cbdata: %d\n", + userdata->ref); +} + +static void +jabber_caps_client_iqcb(JabberStream *js, xmlnode *packet, gpointer data) +{ xmlnode *query = xmlnode_get_child_with_namespace(packet, "query", "http://jabber.org/protocol/disco#info"); jabber_caps_cbplususerdata *userdata = data; - - /* TODO: Better error checking! */ - - if (query) { - JabberCapsValue *value = g_new0(JabberCapsValue, 1); - JabberCapsKey *key = g_new0(JabberCapsKey, 1); - xmlnode *child; - GList *iter; - - value->ext = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, jabber_caps_ext_destroy_value); + JabberCapsClientInfo *info = NULL, *value; + const char *type = xmlnode_get_attrib(packet, "type"); + JabberCapsKey key; - key->node = g_strdup(userdata->node); - key->ver = g_strdup(userdata->ver); + if (!query || !strcmp(type, "error")) { + /* Any outstanding exts will be dealt with via ref-counting */ + userdata->cb(NULL, NULL, userdata->cb_data); + cbplususerdata_unref(userdata); + return; + } - for(child = query->child; child; child = child->next) { - if(child->type != XMLNODE_TYPE_TAG) - continue; - if(!strcmp(child->name,"feature")) { - const char *var = xmlnode_get_attrib(child, "var"); - if(!var) - continue; - value->features = g_list_append(value->features, g_strdup(var)); - } else if(!strcmp(child->name,"identity")) { - const char *category = xmlnode_get_attrib(child, "category"); - const char *type = xmlnode_get_attrib(child, "type"); - const char *name = xmlnode_get_attrib(child, "name"); + /* check hash */ + info = jabber_caps_parse_client_info(query); - JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); - id->category = g_strdup(category); - id->type = g_strdup(type); - id->name = g_strdup(name); - - value->identities = g_list_append(value->identities,id); - } + /* Only validate if these are v1.5 capabilities */ + if (userdata->hash) { + gchar *hash = NULL; + if (!strcmp(userdata->hash, "sha-1")) { + hash = jabber_caps_calculate_hash(info, "sha1"); + } else if (!strcmp(userdata->hash, "md5")) { + hash = jabber_caps_calculate_hash(info, "md5"); } - g_hash_table_replace(capstable, key, value); - jabber_caps_store(); - - /* fetch all exts */ - for(iter = userdata->ext; iter; iter = g_list_next(iter)) { - JabberIq *iq = jabber_iq_new_query(js, JABBER_IQ_GET, - "http://jabber.org/protocol/disco#info"); - xmlnode *query = xmlnode_get_child_with_namespace(iq->node, - "query", "http://jabber.org/protocol/disco#info"); - char *node = g_strdup_printf("%s#%s", userdata->node, (const char*)iter->data); - jabber_ext_userdata *ext_data = g_new0(jabber_ext_userdata, 1); - ext_data->node = node; - ext_data->userdata = userdata; + if (!hash || strcmp(hash, userdata->ver)) { + purple_debug_warning("jabber", "Could not validate caps info from %s\n", + xmlnode_get_attrib(packet, "from")); - xmlnode_set_attrib(query, "node", node); - xmlnode_set_attrib(iq->node, "to", userdata->who); - - jabber_iq_set_callback(iq, jabber_caps_ext_iqcb, ext_data); - jabber_iq_send(iq); + userdata->cb(NULL, NULL, userdata->cb_data); + jabber_caps_client_info_destroy(info); + cbplususerdata_unref(userdata); + g_free(hash); + return; } - } else - /* Don't wait for the ext discoveries; they aren't going to happen */ - userdata->extOutstanding = 0; + g_free(hash); + } + + if (!userdata->hash && userdata->node_exts) { + /* If the ClientInfo doesn't have information about the exts, give them + * ours (along with our ref) */ + info->exts = userdata->node_exts; + userdata->node_exts = NULL; + } + + key.node = userdata->node; + key.ver = userdata->ver; + key.hash = userdata->hash; - jabber_caps_get_info_check_completion(userdata); + /* Use the copy of this data already in the table if it exists or insert + * a new one if we need to */ + if ((value = g_hash_table_lookup(capstable, &key))) { + jabber_caps_client_info_destroy(info); + info = value; + } else { + JabberCapsKey *n_key = g_new(JabberCapsKey, 1); + n_key->node = userdata->node; + n_key->ver = userdata->ver; + n_key->hash = userdata->hash; + userdata->node = userdata->ver = userdata->hash = NULL; + + /* The capstable gets a reference */ + g_hash_table_insert(capstable, n_key, info); + schedule_caps_save(); + } + + userdata->info = info; + + if (userdata->extOutstanding == 0) + jabber_caps_get_info_complete(userdata); + + cbplususerdata_unref(userdata); } -void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, const char *ver, const char *ext, jabber_caps_get_info_cb cb, gpointer user_data) { - JabberCapsValue *client; - JabberCapsKey *key = g_new0(JabberCapsKey, 1); - char *originalext = g_strdup(ext); - jabber_caps_cbplususerdata *userdata = g_new0(jabber_caps_cbplususerdata, 1); +typedef struct { + const char *name; + jabber_caps_cbplususerdata *data; +} ext_iq_data; + +static void +jabber_caps_ext_iqcb(JabberStream *js, xmlnode *packet, gpointer data) +{ + xmlnode *query = xmlnode_get_child_with_namespace(packet, "query", + "http://jabber.org/protocol/disco#info"); + xmlnode *child; + ext_iq_data *userdata = data; + const char *type = xmlnode_get_attrib(packet, "type"); + GList *features = NULL; + JabberCapsNodeExts *node_exts; + + if (!query || !strcmp(type, "error")) { + cbplususerdata_unref(userdata->data); + g_free(userdata); + return; + } + + /* So, we decrement this after checking for an error, which means that + * if there *is* an error, we'll never call the callback passed to + * jabber_caps_get_info. We will still free all of our data, though. + */ + --userdata->data->extOutstanding; + + for (child = xmlnode_get_child(query, "feature"); child; + child = xmlnode_get_next_twin(child)) { + const char *var = xmlnode_get_attrib(child, "var"); + if (var) + features = g_list_prepend(features, g_strdup(var)); + } + + node_exts = (userdata->data->info ? userdata->data->info->exts : + userdata->data->node_exts); + g_hash_table_insert(node_exts->exts, g_strdup(userdata->name), features); + schedule_caps_save(); + + /* Are we done? */ + if (userdata->data->info && userdata->data->extOutstanding == 0) + jabber_caps_get_info_complete(userdata->data); + + cbplususerdata_unref(userdata->data); + g_free(userdata); +} + +void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, + const char *ver, const char *hash, const char *ext, + jabber_caps_get_info_cb cb, gpointer user_data) +{ + JabberCapsClientInfo *info; + JabberCapsKey key; + jabber_caps_cbplususerdata *userdata; + + if (ext && *ext && hash) + purple_debug_warning("jabber", "Ignoring exts in new-style caps from %s\n", + who); + + /* Using this in a read-only fashion, so the cast is OK */ + key.node = (char *)node; + key.ver = (char *)ver; + key.hash = (char *)hash; + + info = g_hash_table_lookup(capstable, &key); + if (info && hash) { + /* v1.5 - We already have all the information we care about */ + cb(info, NULL, user_data); + return; + } + + userdata = g_new0(jabber_caps_cbplususerdata, 1); + /* This ref is given to fetching the basic node#ver info if we need it + * or unrefed at the bottom of this function */ + cbplususerdata_ref(userdata); userdata->cb = cb; - userdata->user_data = user_data; + userdata->cb_data = user_data; userdata->who = g_strdup(who); userdata->node = g_strdup(node); userdata->ver = g_strdup(ver); - - if(originalext) { - int i; - gchar **splat = g_strsplit(originalext, " ", 0); - for(i =0; splat[i]; i++) { - userdata->ext = g_list_append(userdata->ext, splat[i]); - ++userdata->extOutstanding; - } - g_free(splat); - } - g_free(originalext); + userdata->hash = g_strdup(hash); - key->node = (char *)node; - key->ver = (char *)ver; - - client = g_hash_table_lookup(capstable, key); + if (info) { + userdata->info = info; + } else { + /* If we don't have the basic information about the client, we need + * to fetch it. */ + JabberIq *iq; + xmlnode *query; + char *nodever; - g_free(key); - - if(!client) { - JabberIq *iq = jabber_iq_new_query(js,JABBER_IQ_GET,"http://jabber.org/protocol/disco#info"); - xmlnode *query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#info"); - char *nodever = g_strdup_printf("%s#%s", node, ver); + iq = jabber_iq_new_query(js, JABBER_IQ_GET, + "http://jabber.org/protocol/disco#info"); + query = xmlnode_get_child_with_namespace(iq->node, "query", + "http://jabber.org/protocol/disco#info"); + nodever = g_strdup_printf("%s#%s", node, ver); xmlnode_set_attrib(query, "node", nodever); g_free(nodever); xmlnode_set_attrib(iq->node, "to", who); - jabber_iq_set_callback(iq,jabber_caps_client_iqcb,userdata); + jabber_iq_set_callback(iq, jabber_caps_client_iqcb, userdata); jabber_iq_send(iq); - } else { - GList *iter; - /* fetch unknown exts only */ - for(iter = userdata->ext; iter; iter = g_list_next(iter)) { - JabberCapsValueExt *extvalue = g_hash_table_lookup(client->ext, (const char*)iter->data); - JabberIq *iq; - xmlnode *query; - char *nodever; - jabber_ext_userdata *ext_data; + } + + /* Are there any exts that we don't recognize? */ + if (ext && *ext && !hash) { + JabberCapsNodeExts *node_exts; + gchar **splat = g_strsplit(ext, " ", 0); + int i; + + if (info) { + if (info->exts) + node_exts = info->exts; + else + node_exts = info->exts = jabber_caps_find_exts_by_node(node); + } else + /* We'll put it in later once we have the client info */ + node_exts = userdata->node_exts = jabber_caps_find_exts_by_node(node); - if(extvalue) { - /* we already have this ext, don't bother with it */ - --userdata->extOutstanding; - continue; + for (i = 0; splat[i]; ++i) { + userdata->exts = g_list_prepend(userdata->exts, splat[i]); + /* Look it up if we don't already know what it means */ + if (!g_hash_table_lookup(node_exts->exts, splat[i])) { + JabberIq *iq; + xmlnode *query; + char *nodeext; + ext_iq_data *cbdata = g_new(ext_iq_data, 1); + + cbdata->name = splat[i]; + cbdata->data = cbplususerdata_ref(userdata); + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, + "http://jabber.org/protocol/disco#info"); + query = xmlnode_get_child_with_namespace(iq->node, "query", + "http://jabber.org/protocol/disco#info"); + nodeext = g_strdup_printf("%s#%s", node, splat[i]); + xmlnode_set_attrib(query, "node", nodeext); + g_free(nodeext); + xmlnode_set_attrib(iq->node, "to", who); + + jabber_iq_set_callback(iq, jabber_caps_ext_iqcb, cbdata); + jabber_iq_send(iq); + + ++userdata->extOutstanding; } - - ext_data = g_new0(jabber_ext_userdata, 1); + splat[i] = NULL; + } + /* All the strings are now part of the GList, so don't need + * g_strfreev. */ + g_free(splat); + } - iq = jabber_iq_new_query(js,JABBER_IQ_GET,"http://jabber.org/protocol/disco#info"); - query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#info"); - nodever = g_strdup_printf("%s#%s", node, (const char*)iter->data); - xmlnode_set_attrib(query, "node", nodever); - xmlnode_set_attrib(iq->node, "to", who); + if (userdata->info && userdata->extOutstanding == 0) { + jabber_caps_get_info_complete(userdata); + cbplususerdata_unref(userdata); + } +} - ext_data->node = nodever; - ext_data->userdata = userdata; +static gint +jabber_identity_compare(gconstpointer a, gconstpointer b) +{ + const JabberIdentity *ac; + const JabberIdentity *bc; + gint cat_cmp; + gint typ_cmp; + + ac = a; + bc = b; - jabber_iq_set_callback(iq, jabber_caps_ext_iqcb, ext_data); - jabber_iq_send(iq); + if ((cat_cmp = strcmp(ac->category, bc->category)) == 0) { + if ((typ_cmp = strcmp(ac->type, bc->type)) == 0) { + if (!ac->lang && !bc->lang) { + return 0; + } else if (ac->lang && !bc->lang) { + return 1; + } else if (!ac->lang && bc->lang) { + return -1; + } else { + return strcmp(ac->lang, bc->lang); + } + } else { + return typ_cmp; } - /* maybe we have all data available anyways? This is the ideal case where no network traffic is necessary */ - jabber_caps_get_info_check_completion(userdata); + } else { + return cat_cmp; } } +static gchar *jabber_caps_get_formtype(const xmlnode *x) { + xmlnode *formtypefield; + formtypefield = xmlnode_get_child(x, "field"); + while (formtypefield && strcmp(xmlnode_get_attrib(formtypefield, "var"), "FORM_TYPE")) formtypefield = xmlnode_get_next_twin(formtypefield); + formtypefield = xmlnode_get_child(formtypefield, "value"); + return xmlnode_get_data(formtypefield);; +} + +static gint +jabber_xdata_compare(gconstpointer a, gconstpointer b) +{ + const xmlnode *aformtypefield = a; + const xmlnode *bformtypefield = b; + char *aformtype; + char *bformtype; + int result; + + aformtype = jabber_caps_get_formtype(aformtypefield); + bformtype = jabber_caps_get_formtype(bformtypefield); + + result = strcmp(aformtype, bformtype); + g_free(aformtype); + g_free(bformtype); + return result; +} + +static JabberCapsClientInfo *jabber_caps_parse_client_info(xmlnode *query) +{ + xmlnode *child; + JabberCapsClientInfo *info; + + if (!query || strcmp(query->xmlns, "http://jabber.org/protocol/disco#info")) + return 0; + + info = g_new0(JabberCapsClientInfo, 1); + + for(child = query->child; child; child = child->next) { + if (!strcmp(child->name,"identity")) { + /* parse identity */ + const char *category = xmlnode_get_attrib(child, "category"); + const char *type = xmlnode_get_attrib(child, "type"); + const char *name = xmlnode_get_attrib(child, "name"); + const char *lang = xmlnode_get_attrib(child, "lang"); + JabberIdentity *id; + + if (!category || !type) + continue; + + id = g_new0(JabberIdentity, 1); + id->category = g_strdup(category); + id->type = g_strdup(type); + id->name = g_strdup(name); + id->lang = g_strdup(lang); + + info->identities = g_list_append(info->identities, id); + } else if (!strcmp(child->name, "feature")) { + /* parse feature */ + const char *var = xmlnode_get_attrib(child, "var"); + if (var) + info->features = g_list_prepend(info->features, g_strdup(var)); + } else if (!strcmp(child->name, "x")) { + if (child->xmlns && !strcmp(child->xmlns, "jabber:x:data")) { + /* x-data form */ + xmlnode *dataform = xmlnode_copy(child); + info->forms = g_list_append(info->forms, dataform); + } + } + } + return info; +} + +static gint jabber_caps_xdata_field_compare(gconstpointer a, gconstpointer b) +{ + const JabberDataFormField *ac = a; + const JabberDataFormField *bc = b; + + return strcmp(ac->var, bc->var); +} + +static GList* jabber_caps_xdata_get_fields(const xmlnode *x) +{ + GList *fields = NULL; + xmlnode *field; + + if (!x) + return NULL; + + for (field = xmlnode_get_child(x, "field"); field; field = xmlnode_get_next_twin(field)) { + xmlnode *value; + JabberDataFormField *xdatafield = g_new0(JabberDataFormField, 1); + xdatafield->var = g_strdup(xmlnode_get_attrib(field, "var")); + + for (value = xmlnode_get_child(field, "value"); value; value = xmlnode_get_next_twin(value)) { + gchar *val = xmlnode_get_data(value); + xdatafield->values = g_list_append(xdatafield->values, val); + } + + xdatafield->values = g_list_sort(xdatafield->values, (GCompareFunc)strcmp); + fields = g_list_append(fields, xdatafield); + } + + fields = g_list_sort(fields, jabber_caps_xdata_field_compare); + return fields; +} + +static GString* +jabber_caps_verification_append(GString *verification, const gchar *string) +{ + verification = g_string_append(verification, string); + return g_string_append_c(verification, '<'); +} + +gchar *jabber_caps_calculate_hash(JabberCapsClientInfo *info, const char *hash) +{ + GList *node; + GString *verification; + PurpleCipherContext *context; + guint8 checksum[20]; + gsize checksum_size = 20; + gboolean success; + + if (!info || !(context = purple_cipher_context_new_by_name(hash, NULL))) + return NULL; + + /* sort identities, features and x-data forms */ + info->identities = g_list_sort(info->identities, jabber_identity_compare); + info->features = g_list_sort(info->features, (GCompareFunc)strcmp); + info->forms = g_list_sort(info->forms, jabber_xdata_compare); + + verification = g_string_new(""); + + /* concat identities to the verification string */ + for (node = info->identities; node; node = node->next) { + JabberIdentity *id = (JabberIdentity*)node->data; + + g_string_append_printf(verification, "%s/%s/%s/%s<", id->category, + id->type, id->lang ? id->lang : "", id->name); + } + + /* concat features to the verification string */ + for (node = info->features; node; node = node->next) { + verification = jabber_caps_verification_append(verification, node->data); + } + + /* concat x-data forms to the verification string */ + for(node = info->forms; node; node = node->next) { + xmlnode *data = (xmlnode *)node->data; + gchar *formtype = jabber_caps_get_formtype(data); + GList *fields = jabber_caps_xdata_get_fields(data); + + /* append FORM_TYPE's field value to the verification string */ + verification = jabber_caps_verification_append(verification, formtype); + g_free(formtype); + + while (fields) { + GList *value; + JabberDataFormField *field = (JabberDataFormField*)fields->data; + + if (strcmp(field->var, "FORM_TYPE")) { + /* Append the "var" attribute */ + verification = jabber_caps_verification_append(verification, field->var); + /* Append <value/> elements' cdata */ + for(value = field->values; value; value = value->next) { + verification = jabber_caps_verification_append(verification, value->data); + g_free(value->data); + } + } + + g_free(field->var); + g_list_free(field->values); + + fields = g_list_delete_link(fields, fields); + } + } + + /* generate hash */ + purple_cipher_context_append(context, (guchar*)verification->str, verification->len); + + success = purple_cipher_context_digest(context, verification->len, + checksum, &checksum_size); + + g_string_free(verification, TRUE); + purple_cipher_context_destroy(context); + + return (success ? purple_base64_encode(checksum, checksum_size) : NULL); +} + +void jabber_caps_calculate_own_hash(JabberStream *js) { + JabberCapsClientInfo info; + GList *iter = 0; + GList *features = 0; + + if (!jabber_identities && !jabber_features) { + /* This really shouldn't ever happen */ + purple_debug_warning("jabber", "No features or identities, cannot calculate own caps hash.\n"); + g_free(js->caps_hash); + js->caps_hash = NULL; + return; + } + + /* build the currently-supported list of features */ + if (jabber_features) { + for (iter = jabber_features; iter; iter = iter->next) { + JabberFeature *feat = iter->data; + if(!feat->is_enabled || feat->is_enabled(js, feat->namespace)) { + features = g_list_append(features, feat->namespace); + } + } + } + + info.features = features; + info.identities = jabber_identities; + info.forms = NULL; + + g_free(js->caps_hash); + js->caps_hash = jabber_caps_calculate_hash(&info, "sha1"); + g_list_free(features); +} + +const gchar* jabber_caps_get_own_hash(JabberStream *js) +{ + if (!js->caps_hash) + jabber_caps_calculate_own_hash(js); + + return js->caps_hash; +} + +void jabber_caps_broadcast_change() +{ + GList *node, *accounts = purple_accounts_get_all_active(); + + for (node = accounts; node; node = node->next) { + PurpleAccount *account = node->data; + const char *prpl_id = purple_account_get_protocol_id(account); + if (!strcmp("prpl-jabber", prpl_id) && purple_account_is_connected(account)) { + PurpleConnection *gc = purple_account_get_connection(account); + jabber_presence_send(gc->proto_data, TRUE); + } + } + + g_list_free(accounts); +} +