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);
}