diff --git a/ChangeLog b/ChangeLog index d807d32..7437e1b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,7 @@ 0.12.0 - Add code coverage support + - New API: + - xmpp_stanza_get_child_by_path() 0.11.0 - SASL EXTERNAL support (XEP-0178) diff --git a/src/stanza.c b/src/stanza.c index b95c25e..60df711 100644 --- a/src/stanza.c +++ b/src/stanza.c @@ -844,6 +844,82 @@ const char *xmpp_stanza_get_from(xmpp_stanza_t *stanza) return xmpp_stanza_get_attribute(stanza, "from"); } +/** Get the first child of stanza following a path-like list of names. + * This function searches the children and their children that match + * the given path. + * + * * "name" - Search 'name' + * + * * "name[@ns='foo']" - Search 'name' which is in the namespace 'foo' + * + * The Syntax to pass namespaces is inspired by the XPATH way of passing + * attributes. + * + * The namespace syntax only supports single quotes `'`. + * + * The \ref XMPP_STANZA_NAME_IN_NS macro is provided as a helper for names + * in namespaces. + * + * @param stanza a Strophe stanza object + * @param ... a var-args list that must be terminated by a NULL entry + * + * @return the matching child stanza object or NULL if no match was found + * + * @ingroup Stanza + */ +xmpp_stanza_t *xmpp_stanza_get_child_by_path(xmpp_stanza_t *stanza, ...) +{ + xmpp_stanza_t *child = NULL; + char *p, *tok, *attr, *saveattr, *ns = NULL; + const char *xmlns; + va_list ap; + + va_start(ap, stanza); + + while ((p = va_arg(ap, char *)) != NULL) { + tok = xmpp_strdup(stanza->ctx, p); + if (!tok) { + child = NULL; + break; + } + saveattr = ns = NULL; + attr = xmpp_strtok_r(tok, "[", &saveattr); + if (attr) { + attr = xmpp_strtok_r(NULL, "]", &saveattr); + if (attr) { + if (!strncmp(attr, "@ns='", 5)) { + ns = attr + 5; + xmpp_strtok_r(ns, "'", &saveattr); + } + } + } + if (!child) { + if (strcmp(xmpp_stanza_get_name(stanza), tok)) + goto error_out; + + if (ns) { + xmlns = xmpp_stanza_get_ns(stanza); + if (!xmlns || strcmp(xmlns, ns)) + goto error_out; + } + child = stanza; + } else { + if (!ns) + child = xmpp_stanza_get_child_by_name(child, tok); + else + child = xmpp_stanza_get_child_by_name_and_ns(child, tok, ns); + } +error_out: + xmpp_free(stanza->ctx, tok); + if (!child) + break; + } + + va_end(ap); + + return p == NULL ? child : NULL; +} + /** Get the first child of stanza with name. * This function searches all the immediate children of stanza for a child * stanza that matches the name. The first matching child is returned. diff --git a/strophe.h b/strophe.h index 254f96a..eed6d8d 100644 --- a/strophe.h +++ b/strophe.h @@ -406,6 +406,9 @@ xmpp_stanza_t *xmpp_stanza_get_child_by_ns(xmpp_stanza_t *stanza, xmpp_stanza_t *xmpp_stanza_get_child_by_name_and_ns(xmpp_stanza_t *stanza, const char *name, const char *ns); +/* helper macro for names with a namespace */ +#define XMPP_STANZA_NAME_IN_NS(name, ns) name "[@ns='" ns "']" +xmpp_stanza_t *xmpp_stanza_get_child_by_path(xmpp_stanza_t *stanza, ...); xmpp_stanza_t *xmpp_stanza_get_next(xmpp_stanza_t *stanza); int xmpp_stanza_add_child(xmpp_stanza_t *stanza, xmpp_stanza_t *child); int xmpp_stanza_add_child_ex(xmpp_stanza_t *stanza, diff --git a/tests/test_stanza.c b/tests/test_stanza.c index 8643f2e..445a24a 100644 --- a/tests/test_stanza.c +++ b/tests/test_stanza.c @@ -123,6 +123,7 @@ static void test_stanza_error(xmpp_ctx_t *ctx) xmpp_stanza_t *stanza; xmpp_stanza_t *error; xmpp_stanza_t *item; + xmpp_stanza_t *mood; char *buf; size_t buflen; const char *attr[10]; @@ -135,12 +136,29 @@ static void test_stanza_error(xmpp_ctx_t *ctx) static const char *str_error = ""; + // clang-format off + static const char *str_mood = + "" + "" + "" + "" + "" + "" + "curse my nurse!" + "" + "" + "" + "" + ""; + // clang-format on stanza = xmpp_stanza_new_from_string(ctx, str); assert(stanza != NULL); error = xmpp_stanza_reply_error(stanza, "cancel", "service-unavailable", NULL); assert(error != NULL); + mood = xmpp_stanza_new_from_string(ctx, str_mood); + assert(stanza != NULL); assert(xmpp_stanza_get_to(error) != NULL); COMPARE("romeo@montague.lit/home", xmpp_stanza_get_to(error)); @@ -151,6 +169,46 @@ static void test_stanza_error(xmpp_ctx_t *ctx) assert(xmpp_stanza_get_type(error) != NULL); COMPARE("error", xmpp_stanza_get_type(error)); + /* FAIL - no list given */ + item = xmpp_stanza_get_child_by_path(mood, NULL); + assert(item == NULL); + + /* FAIL - first entry doesn't match */ + item = xmpp_stanza_get_child_by_path(mood, "foo", NULL); + assert(item == NULL); + + /* FAIL - 'iq' has no namespace */ + item = xmpp_stanza_get_child_by_path( + mood, XMPP_STANZA_NAME_IN_NS("iq", "foobar"), + XMPP_STANZA_NAME_IN_NS("pubsub", "http://jabber.org/protocol/pubsub"), + "publish", "item", "mood", NULL); + assert(item == NULL); + + /* FAIL - 'pubsub' is in another namespace */ + item = xmpp_stanza_get_child_by_path( + mood, "iq", + XMPP_STANZA_NAME_IN_NS("pubsub", "http://jabber.org/protocol/foobar"), + "publish", "item", "mood", NULL); + assert(item == NULL); + + item = xmpp_stanza_get_child_by_path(mood, "iq", "pubsub", "publish", + "item", "mood", NULL); + assert(item != NULL); + assert(xmpp_stanza_get_children(item) != NULL); + assert(xmpp_stanza_get_name(xmpp_stanza_get_children(item)) != NULL); + COMPARE("annoyed", xmpp_stanza_get_name(xmpp_stanza_get_children(item))); + + item = xmpp_stanza_get_child_by_path( + mood, "iq", + XMPP_STANZA_NAME_IN_NS("pubsub", "http://jabber.org/protocol/pubsub"), + "publish", "item", + XMPP_STANZA_NAME_IN_NS("mood", "http://jabber.org/protocol/mood"), + NULL); + assert(item != NULL); + assert(xmpp_stanza_get_children(item) != NULL); + assert(xmpp_stanza_get_name(xmpp_stanza_get_children(item)) != NULL); + COMPARE("annoyed", xmpp_stanza_get_name(xmpp_stanza_get_children(item))); + ret = xmpp_stanza_get_attributes(error, attr, attrlen); /* attr contains both attribute name and value. */ assert(ret == 8); @@ -163,6 +221,7 @@ static void test_stanza_error(xmpp_ctx_t *ctx) COMPARE(str_error, buf); xmpp_free(ctx, buf); + xmpp_stanza_release(mood); xmpp_stanza_release(stanza); xmpp_stanza_release(error); }