/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* soup-xmlrpc.c: XML-RPC parser/generator
*
* Copyright 2007 Red Hat, Inc.
* Copyright 2007 OpenedHand Ltd.
* Copyright 2015 Collabora ltd.
*
* Author:
* Eduardo Lima Mitev <elima@igalia.com>
* Xavier Claessens <xavier.claessens@collabora.com>
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
#include <errno.h>
#include <libxml/tree.h>
#include "soup-xmlrpc.h"
#include "soup.h"
static gboolean insert_value (xmlNode *parent, GVariant *value, GError **error);
static gboolean
insert_array (xmlNode *parent, GVariant *value, GError **error)
{
xmlNode *node;
GVariantIter iter;
GVariant *child;
node = xmlNewChild (parent, NULL,
(const xmlChar *)"array", NULL);
node = xmlNewChild (node, NULL,
(const xmlChar *)"data", NULL);
g_variant_iter_init (&iter, value);
while ((child = g_variant_iter_next_value (&iter))) {
if (!insert_value (node, child, error)) {
g_variant_unref (child);
return FALSE;
}
g_variant_unref (child);
}
return TRUE;
}
static gboolean
insert_struct_member (xmlNode *parent, GVariant *value, GError **error)
{
xmlNode *member;
GVariant *mname;
GVariant *mvalue;
gboolean ret = FALSE;
mname = g_variant_get_child_value (value, 0);
mvalue = g_variant_get_child_value (value, 1);
if (g_variant_classify (mname) != G_VARIANT_CLASS_STRING) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Only string keys are supported in dictionaries, got %s",
g_variant_get_type_string (mname));
goto fail;
}
member = xmlNewChild (parent, NULL,
(const xmlChar *)"member", NULL);
xmlNewTextChild (member, NULL,
(const xmlChar *)"name",
(const xmlChar *)g_variant_get_string (mname, NULL));
ret = insert_value (member, mvalue, error);
fail:
g_variant_unref (mname);
g_variant_unref (mvalue);
return ret;
}
static gboolean
insert_struct (xmlNode *parent, GVariant *value, GError **error)
{
xmlNode *struct_node;
GVariantIter iter;
GVariant *child;
struct_node = xmlNewChild (parent, NULL,
(const xmlChar *)"struct", NULL);
g_variant_iter_init (&iter, value);
while ((child = g_variant_iter_next_value (&iter))) {
if (!insert_struct_member (struct_node, child, error)) {
g_variant_unref (child);
return FALSE;
}
g_variant_unref (child);
}
return TRUE;
}
static gboolean
insert_value (xmlNode *parent, GVariant *value, GError **error)
{
xmlNode *xvalue;
const char *type_str = NULL;
char buf[128];
xvalue = xmlNewChild (parent, NULL, (const xmlChar *)"value", NULL);
switch (g_variant_classify (value)) {
case G_VARIANT_CLASS_BOOLEAN:
g_snprintf (buf, sizeof (buf), "%d", g_variant_get_boolean (value));
type_str = "boolean";
break;
case G_VARIANT_CLASS_BYTE:
g_snprintf (buf, sizeof (buf), "%u", g_variant_get_byte (value));
type_str = "int";
break;
case G_VARIANT_CLASS_INT16:
g_snprintf (buf, sizeof (buf), "%d", g_variant_get_int16 (value));
type_str = "int";
break;
case G_VARIANT_CLASS_UINT16:
g_snprintf (buf, sizeof (buf), "%u", g_variant_get_uint16 (value));
type_str = "int";
break;
case G_VARIANT_CLASS_INT32:
g_snprintf (buf, sizeof (buf), "%d", g_variant_get_int32 (value));
type_str = "int";
break;
case G_VARIANT_CLASS_UINT32:
g_snprintf (buf, sizeof (buf), "%u", g_variant_get_uint32 (value));
type_str = "i8";
break;
case G_VARIANT_CLASS_INT64:
g_snprintf (buf, sizeof (buf), "%"G_GINT64_FORMAT, g_variant_get_int64 (value));
type_str = "i8";
break;
case G_VARIANT_CLASS_DOUBLE:
g_ascii_dtostr (buf, sizeof (buf), g_variant_get_double (value));
type_str = "double";
break;
case G_VARIANT_CLASS_STRING:
xmlNewTextChild (xvalue, NULL,
(const xmlChar *)"string",
(const xmlChar *)g_variant_get_string (value, NULL));
break;
case G_VARIANT_CLASS_VARIANT: {
GVariant *child;
xmlUnlinkNode (xvalue);
xmlFreeNode (xvalue);
child = g_variant_get_variant (value);
if (!insert_value (parent, child, error)) {
g_variant_unref (child);
return FALSE;
}
g_variant_unref (child);
break;
}
case G_VARIANT_CLASS_ARRAY: {
if (g_variant_is_of_type (value, G_VARIANT_TYPE_BYTESTRING)) {
char *encoded;
encoded = g_base64_encode (g_variant_get_data (value),
g_variant_get_size (value));
xmlNewChild (xvalue, NULL,
(const xmlChar *)"base64",
(const xmlChar *)encoded);
g_free (encoded);
} else if (g_variant_is_of_type (value, G_VARIANT_TYPE_DICTIONARY)) {
if (!insert_struct (xvalue, value, error))
return FALSE;
} else {
if (!insert_array (xvalue, value, error))
return FALSE;
}
break;
}
case G_VARIANT_CLASS_TUPLE: {
/* Special case for custom types */
if (g_variant_is_of_type (value, G_VARIANT_TYPE ("(oss)"))) {
const char *path;
const char *type;
const char *v;
g_variant_get (value, "(&o&s&s)", &path, &type, &v);
if (g_str_equal (path, "/org/gnome/libsoup/ExtensionType")) {
xmlNewTextChild (xvalue, NULL,
(const xmlChar *)type,
(const xmlChar *)v);
break;
}
}
if (!insert_array (xvalue, value, error))
return FALSE;
break;
}
case G_VARIANT_CLASS_DICT_ENTRY: {
xmlNode *node;
node = xmlNewChild (xvalue, NULL,
(const xmlChar *)"struct", NULL);
if (!insert_struct_member (node, value, error))
return FALSE;
break;
}
case G_VARIANT_CLASS_HANDLE:
case G_VARIANT_CLASS_MAYBE:
case G_VARIANT_CLASS_UINT64:
case G_VARIANT_CLASS_OBJECT_PATH:
case G_VARIANT_CLASS_SIGNATURE:
default:
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Unsupported type: %s", g_variant_get_type_string (value));
goto fail;
}
if (type_str != NULL) {
xmlNewTextChild (xvalue, NULL,
(const xmlChar *)type_str,
(const xmlChar *)buf);
}
return TRUE;
fail:
return FALSE;
}
/**
* soup_xmlrpc_build_request:
* @method_name: the name of the XML-RPC method
* @params: a #GVariant tuple
* @error: a #GError, or %NULL
*
* This creates an XML-RPC methodCall and returns it as a string.
* This is the low-level method that soup_xmlrpc_message_new() is
* built on.
*
* @params is a #GVariant tuple representing the method parameters.
*
* Serialization details:
* - "a{s*}" and "{s*}" are serialized as <struct>
* - "ay" is serialized as <base64>
* - Other arrays and tuples are serialized as <array>
* - booleans are serialized as <boolean>
* - byte, int16, uint16 and int32 are serialized as <int>
* - uint32 and int64 are serialized as the nonstandard <i8> type
* - doubles are serialized as <double>
* - Strings are serialized as <string>
* - Variants (i.e. "v" type) are unwrapped and their child is serialized.
* - #GVariants created by soup_xmlrpc_variant_new_datetime() are serialized as
* <dateTime.iso8601>
* - Other types are not supported and will return %NULL and set @error.
* This notably includes: object-paths, signatures, uint64, handles, maybes
* and dictionaries with non-string keys.
*
* If @params is floating, it is consumed.
*
* Return value: the text of the methodCall, or %NULL on error.
* Since: 2.52
**/
char *
soup_xmlrpc_build_request (const char *method_name,
GVariant *params,
GError **error)
{
xmlDoc *doc;
xmlNode *node, *param;
xmlChar *xmlbody;
GVariantIter iter;
GVariant *child;
int len;
char *body = NULL;
g_return_val_if_fail (g_variant_is_of_type (params, G_VARIANT_TYPE_TUPLE), NULL);
g_variant_ref_sink (params);
doc = xmlNewDoc ((const xmlChar *)"1.0");
doc->standalone = FALSE;
doc->encoding = xmlCharStrdup ("UTF-8");
node = xmlNewDocNode (doc, NULL, (const xmlChar *)"methodCall", NULL);
xmlDocSetRootElement (doc, node);
xmlNewChild (node, NULL, (const xmlChar *)"methodName",
(const xmlChar *)method_name);
node = xmlNewChild (node, NULL, (const xmlChar *)"params", NULL);
g_variant_iter_init (&iter, params);
while ((child = g_variant_iter_next_value (&iter))) {
param = xmlNewChild (node, NULL,
(const xmlChar *)"param", NULL);
if (!insert_value (param, child, error)) {
xmlFreeDoc (doc);
g_variant_unref (child);
g_variant_unref (params);
return NULL;
}
g_variant_unref (child);
}
xmlDocDumpMemory (doc, &xmlbody, &len);
body = g_strndup ((char *)xmlbody, len);
xmlFree (xmlbody);
xmlFreeDoc (doc);
g_variant_unref (params);
return body;
}
/**
* soup_xmlrpc_message_new:
* @uri: URI of the XML-RPC service
* @method_name: the name of the XML-RPC method to invoke at @uri
* @params: a #GVariant tuple
* @error: a #GError, or %NULL
*
* Creates an XML-RPC methodCall and returns a #SoupMessage, ready
* to send, for that method call.
*
* See soup_xmlrpc_build_request() for serialization details.
*
* If @params is floating, it is consumed.
*
* Returns: (transfer full): a #SoupMessage encoding the
* indicated XML-RPC request, or %NULL on error.
*
* Since: 2.52
**/
SoupMessage *
soup_xmlrpc_message_new (const char *uri,
const char *method_name,
GVariant *params,
GError **error)
{
SoupMessage *msg;
char *body;
body = soup_xmlrpc_build_request (method_name, params, error);
if (!body)
return NULL;
msg = soup_message_new ("POST", uri);
soup_message_set_request (msg, "text/xml", SOUP_MEMORY_TAKE,
body, strlen (body));
return msg;
}
/**
* soup_xmlrpc_build_response:
* @value: the return value
* @error: a #GError, or %NULL
*
* This creates a (successful) XML-RPC methodResponse and returns it
* as a string. To create a fault response, use soup_xmlrpc_build_fault(). This
* is the low-level method that soup_xmlrpc_message_set_response() is built on.
*
* See soup_xmlrpc_build_request() for serialization details, but note
* that since a method can only have a single return value, @value
* should not be a tuple here (unless the return value is an array).
*
* If @value is floating, it is consumed.
*
* Returns: the text of the methodResponse, or %NULL on error.
*
* Since: 2.52
**/
char *
soup_xmlrpc_build_response (GVariant *value, GError **error)
{
xmlDoc *doc;
xmlNode *node;
xmlChar *xmlbody;
char *body;
int len;
g_variant_ref_sink (value);
doc = xmlNewDoc ((const xmlChar *)"1.0");
doc->standalone = FALSE;
doc->encoding = xmlCharStrdup ("UTF-8");
node = xmlNewDocNode (doc, NULL,
(const xmlChar *)"methodResponse", NULL);
xmlDocSetRootElement (doc, node);
node = xmlNewChild (node, NULL, (const xmlChar *)"params", NULL);
node = xmlNewChild (node, NULL, (const xmlChar *)"param", NULL);
if (!insert_value (node, value, error)) {
xmlFreeDoc (doc);
g_variant_unref (value);
return NULL;
}
xmlDocDumpMemory (doc, &xmlbody, &len);
body = g_strndup ((char *)xmlbody, len);
xmlFree (xmlbody);
xmlFreeDoc (doc);
g_variant_unref (value);
return body;
}
char *
soup_xmlrpc_build_faultv (int fault_code,
const char *fault_format,
va_list args) G_GNUC_PRINTF (2, 0);
char *
soup_xmlrpc_build_faultv (int fault_code, const char *fault_format, va_list args)
{
xmlDoc *doc;
xmlNode *node, *member;
GVariant *value;
xmlChar *xmlbody;
char *fault_string, *body;
int len;
fault_string = g_strdup_vprintf (fault_format, args);
doc = xmlNewDoc ((const xmlChar *)"1.0");
doc->standalone = FALSE;
doc->encoding = xmlCharStrdup ("UTF-8");
node = xmlNewDocNode (doc, NULL,
(const xmlChar *)"methodResponse", NULL);
xmlDocSetRootElement (doc, node);
node = xmlNewChild (node, NULL, (const xmlChar *)"fault", NULL);
node = xmlNewChild (node, NULL, (const xmlChar *)"value", NULL);
node = xmlNewChild (node, NULL, (const xmlChar *)"struct", NULL);
member = xmlNewChild (node, NULL, (const xmlChar *)"member", NULL);
xmlNewChild (member, NULL,
(const xmlChar *)"name", (const xmlChar *)"faultCode");
value = g_variant_new_int32 (fault_code);
insert_value (member, value, NULL);
g_variant_unref (value);
member = xmlNewChild (node, NULL, (const xmlChar *)"member", NULL);
xmlNewChild (member, NULL,
(const xmlChar *)"name", (const xmlChar *)"faultString");
value = g_variant_new_take_string (fault_string);
insert_value (member, value, NULL);
g_variant_unref (value);
xmlDocDumpMemory (doc, &xmlbody, &len);
body = g_strndup ((char *)xmlbody, len);
xmlFree (xmlbody);
xmlFreeDoc (doc);
return body;
}
/**
* soup_xmlrpc_build_fault:
* @fault_code: the fault code
* @fault_format: a printf()-style format string
* @...: the parameters to @fault_format
*
* This creates an XML-RPC fault response and returns it as a string.
* (To create a successful response, use
* soup_xmlrpc_build_method_response().)
*
* Return value: the text of the fault
**/
char *
soup_xmlrpc_build_fault (int fault_code, const char *fault_format, ...)
{
va_list args;
char *body;
va_start (args, fault_format);
body = soup_xmlrpc_build_faultv (fault_code, fault_format, args);
va_end (args);
return body;
}
/**
* soup_xmlrpc_message_set_fault:
* @msg: an XML-RPC request
* @fault_code: the fault code
* @fault_format: a printf()-style format string
* @...: the parameters to @fault_format
*
* Sets the status code and response body of @msg to indicate an
* unsuccessful XML-RPC call, with the error described by @fault_code
* and @fault_format.
*
* Since: 2.52
**/
void
soup_xmlrpc_message_set_fault (SoupMessage *msg, int fault_code,
const char *fault_format, ...)
{
va_list args;
char *body;
va_start (args, fault_format);
body = soup_xmlrpc_build_faultv (fault_code, fault_format, args);
va_end (args);
soup_message_set_status (msg, SOUP_STATUS_OK);
soup_message_set_response (msg, "text/xml", SOUP_MEMORY_TAKE,
body, strlen (body));
}
/**
* soup_xmlrpc_message_set_response:
* @msg: an XML-RPC request
* @value: a #GVariant
* @error: a #GError, or %NULL
*
* Sets the status code and response body of @msg to indicate a
* successful XML-RPC call, with a return value given by @value. To set a
* fault response, use soup_xmlrpc_message_set_fault().
*
* See soup_xmlrpc_build_request() for serialization details.
*
* If @value is floating, it is consumed.
*
* Returns: %TRUE on success, %FALSE otherwise.
*
* Since: 2.52
**/
gboolean
soup_xmlrpc_message_set_response (SoupMessage *msg, GVariant *value, GError **error)
{
char *body;
body = soup_xmlrpc_build_response (value, error);
if (!body)
return FALSE;
soup_message_set_status (msg, SOUP_STATUS_OK);
soup_message_set_response (msg, "text/xml", SOUP_MEMORY_TAKE,
body, strlen (body));
return TRUE;
}
static GVariant *parse_value (xmlNode *node, const char **signature, GError **error);
static xmlNode *
find_real_node (xmlNode *node)
{
while (node && (node->type == XML_COMMENT_NODE ||
xmlIsBlankNode (node)))
node = node->next;
return node;
}
static char *
signature_get_next_complete_type (const char **signature)
{
GVariantClass class;
const char *initial_signature;
char *result;
/* here it is assumed that 'signature' is a valid type string */
initial_signature = *signature;
class = (*signature)[0];
if (class == G_VARIANT_CLASS_TUPLE || class == G_VARIANT_CLASS_DICT_ENTRY) {
char stack[256] = {0};
guint stack_len = 0;
do {
if ((*signature)[0] == G_VARIANT_CLASS_TUPLE) {
stack[stack_len] = ')';
stack_len++;
}
else if ( (*signature)[0] == G_VARIANT_CLASS_DICT_ENTRY) {
stack[stack_len] = '}';
stack_len++;
}
(*signature)++;
if ( (*signature)[0] == stack[stack_len - 1])
stack_len--;
} while (stack_len > 0);
(*signature)++;
} else if (class == G_VARIANT_CLASS_ARRAY || class == G_VARIANT_CLASS_MAYBE) {
char *tmp_sig;
(*signature)++;
tmp_sig = signature_get_next_complete_type (signature);
g_free (tmp_sig);
} else {
(*signature)++;
}
result = g_strndup (initial_signature, (*signature) - initial_signature);
return result;
}
static GVariant *
parse_array (xmlNode *node, const char **signature, GError **error)
{
GVariant *variant = NULL;
char *child_signature = NULL;
char *array_signature = NULL;
const char *tmp_signature;
gboolean is_tuple = FALSE;
xmlNode *member;
GVariantBuilder builder;
gboolean is_params = FALSE;
if (signature && *signature[0] == G_VARIANT_CLASS_VARIANT)
signature = NULL;
if (g_str_equal (node->name, "array")) {
node = find_real_node (node->children);
if (!g_str_equal (node->name, "data")) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<data> expected but got '%s'", node->name);
goto fail;
}
} else if (g_str_equal (node->name, "params")) {
is_params = TRUE;
} else {
g_assert_not_reached ();
}
if (signature != NULL) {
if ((*signature)[0] == G_VARIANT_CLASS_TUPLE) {
tmp_signature = *signature;
array_signature = signature_get_next_complete_type (&tmp_signature);
is_tuple = TRUE;
}
(*signature)++;
child_signature = signature_get_next_complete_type (signature);
} else {
child_signature = g_strdup ("v");
}
if (!array_signature)
array_signature = g_strdup_printf ("a%s", child_signature);
g_variant_builder_init (&builder, G_VARIANT_TYPE (array_signature));
for (member = find_real_node (node->children);
member;
member = find_real_node (member->next)) {
GVariant *child;
xmlNode *xval = member;
if (is_params) {
if (!g_str_equal (member->name, "param")) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<param> expected but got '%s'", member->name);
goto fail;
}
xval = find_real_node (member->children);
}
if (strcmp ((const char *)xval->name, "value") != 0) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<value> expected but got '%s'", xval->name);
goto fail;
}
if (is_tuple && child_signature[0] == ')') {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Too many values for tuple");
goto fail;
}
tmp_signature = child_signature;
child = parse_value (xval, &tmp_signature, error);
if (child == NULL)
goto fail;
if (is_tuple) {
g_free (child_signature),
child_signature = signature_get_next_complete_type (signature);
}
g_variant_builder_add_value (&builder, child);
}
if (is_tuple && child_signature[0] != ')') {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Too few values for tuple");
goto fail;
}
variant = g_variant_builder_end (&builder);
fail:
g_variant_builder_clear (&builder);
g_free (child_signature);
g_free (array_signature);
/* compensate the (*signature)++ call at the end of 'recurse()' */
if (signature)
(*signature)--;
return variant;
}
static void
parse_dict_entry_signature (const char **signature,
char **entry_signature,
char **key_signature,
char **value_signature)
{
const char *tmp_sig;
if (signature)
*entry_signature = signature_get_next_complete_type (signature);
else
*entry_signature = g_strdup ("{sv}");
tmp_sig = (*entry_signature) + 1;
*key_signature = signature_get_next_complete_type (&tmp_sig);
*value_signature = signature_get_next_complete_type (&tmp_sig);
}
static GVariant *
parse_dictionary (xmlNode *node, const char **signature, GError **error)
{
GVariant *variant = NULL;
char *dict_signature;
char *entry_signature;
char *key_signature;
char *value_signature;
GVariantBuilder builder;
xmlNode *member;
if (signature && *signature[0] == G_VARIANT_CLASS_VARIANT)
signature = NULL;
if (signature)
(*signature)++;
parse_dict_entry_signature (signature,
&entry_signature,
&key_signature,
&value_signature);
dict_signature = g_strdup_printf ("a%s", entry_signature);
g_variant_builder_init (&builder, G_VARIANT_TYPE (dict_signature));
if (!g_str_equal (key_signature, "s")) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Dictionary key must be string but got '%s'", key_signature);
goto fail;
}
for (member = find_real_node (node->children);
member;
member = find_real_node (member->next)) {
xmlNode *child, *mname, *mxval;
const char *tmp_signature;
GVariant *value;
xmlChar *content;
if (strcmp ((const char *)member->name, "member") != 0) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<member> expected but got '%s'", member->name);
goto fail;
}
mname = mxval = NULL;
for (child = find_real_node (member->children);
child;
child = find_real_node (child->next)) {
if (!strcmp ((const char *)child->name, "name"))
mname = child;
else if (!strcmp ((const char *)child->name, "value"))
mxval = child;
else {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<name> or <value> expected but got '%s'", child->name);
goto fail;
}
}
if (!mname || !mxval) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Missing name or value in <member>");
goto fail;
}
tmp_signature = value_signature;
value = parse_value (mxval, &tmp_signature, error);
if (!value)
goto fail;
content = xmlNodeGetContent (mname);
g_variant_builder_open (&builder, G_VARIANT_TYPE (entry_signature));
g_variant_builder_add (&builder, "s", content);
g_variant_builder_add_value (&builder, value);
g_variant_builder_close (&builder);
xmlFree (content);
}
variant = g_variant_builder_end (&builder);
fail:
g_variant_builder_clear (&builder);
g_free (value_signature);
g_free (key_signature);
g_free (entry_signature);
g_free (dict_signature);
/* compensate the (*signature)++ call at the end of 'recurse()' */
if (signature != NULL)
(*signature)--;
return variant;
}
static GVariant *
parse_number (xmlNode *typenode, GVariantClass class, GError **error)
{
xmlChar *content;
const char *str;
char *endptr;
gint64 num = 0;
guint64 unum = 0;
GVariant *variant = NULL;
content = xmlNodeGetContent (typenode);
str = (const char *) content;
errno = 0;
if (class == G_VARIANT_CLASS_UINT64)
unum = g_ascii_strtoull (str, &endptr, 10);
else
num = g_ascii_strtoll (str, &endptr, 10);
if (errno || endptr == str) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Couldn't parse number '%s'", str);
goto fail;
}
#define RANGE(v, min, max) \
G_STMT_START{ \
if (v < min || v > max) { \
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS, \
"Number out of range '%s'", str); \
goto fail; \
} \
} G_STMT_END
switch (class) {
case G_VARIANT_CLASS_BOOLEAN:
RANGE (num, 0, 1);
variant = g_variant_new_boolean (num);
break;
case G_VARIANT_CLASS_BYTE:
RANGE (num, 0, G_MAXUINT8);
variant = g_variant_new_byte (num);
break;
case G_VARIANT_CLASS_INT16:
RANGE (num, G_MININT16, G_MAXINT16);
variant = g_variant_new_int16 (num);
break;
case G_VARIANT_CLASS_UINT16:
RANGE (num, 0, G_MAXUINT16);
variant = g_variant_new_uint16 (num);
break;
case G_VARIANT_CLASS_INT32:
RANGE (num, G_MININT32, G_MAXINT32);
variant = g_variant_new_int32 (num);
break;
case G_VARIANT_CLASS_UINT32:
RANGE (num, 0, G_MAXUINT32);
variant = g_variant_new_uint32 (num);
break;
case G_VARIANT_CLASS_INT64:
RANGE (num, G_MININT64, G_MAXINT64);
variant = g_variant_new_int64 (num);
break;
case G_VARIANT_CLASS_UINT64:
RANGE (unum, 0, G_MAXUINT64);
variant = g_variant_new_uint64 (unum);
break;
default:
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<%s> node does not match signature",
(const char *)typenode->name);
goto fail;
}
fail:
xmlFree (content);
return variant;
}
static GVariant *
parse_double (xmlNode *typenode, GError **error)
{
GVariant *variant = NULL;
xmlChar *content;
const char *str;
char *endptr;
gdouble d;
content = xmlNodeGetContent (typenode);
str = (const char *) content;
errno = 0;
d = g_ascii_strtod (str, &endptr);
if (errno || endptr == str) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Couldn't parse double '%s'", str);
goto fail;
}
variant = g_variant_new_double (d);
fail:
xmlFree (content);
return variant;
}
static GVariant *
parse_base64 (xmlNode *typenode, GError **error)
{
GVariant *variant;
xmlChar *content;
guchar *decoded;
gsize len;
content = xmlNodeGetContent (typenode);
decoded = g_base64_decode ((char *)content, &len);
variant = g_variant_new_from_data (G_VARIANT_TYPE ("ay"),
decoded, len,
TRUE,
g_free, decoded);
xmlFree (content);
return variant;
}
static GVariant *
soup_xmlrpc_variant_new_custom (const char *type, const char *v)
{
return g_variant_new ("(oss)", "/org/gnome/libsoup/ExtensionType",
type, v);
}
static GVariant *
parse_value (xmlNode *node, const char **signature, GError **error)
{
xmlNode *typenode;
const char *typename;
xmlChar *content = NULL;
GVariant *variant = NULL;
GVariantClass class = G_VARIANT_CLASS_VARIANT;
if (signature)
class = *signature[0];
if (g_str_equal ((const char *)node->name, "value")) {
typenode = find_real_node (node->children);
if (!typenode) {
/* If no typenode, assume value's content is string */
typename = "string";
typenode = node;
} else {
typename = (const char *)typenode->name;
}
} else if (g_str_equal ((const char *)node->name, "params")) {
typenode = node;
typename = "params";
} else {
g_assert_not_reached ();
}
if (g_str_equal (typename, "boolean")) {
if (class != G_VARIANT_CLASS_VARIANT && class != G_VARIANT_CLASS_BOOLEAN) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<boolean> node does not match signature");
goto fail;
}
variant = parse_number (typenode, G_VARIANT_CLASS_BOOLEAN, error);
} else if (g_str_equal (typename, "int") || g_str_equal (typename, "i4")) {
if (class == G_VARIANT_CLASS_VARIANT)
variant = parse_number (typenode, G_VARIANT_CLASS_INT32, error);
else
variant = parse_number (typenode, class, error);
} else if (g_str_equal (typename, "i8")) {
if (class == G_VARIANT_CLASS_VARIANT)
variant = parse_number (typenode, G_VARIANT_CLASS_INT64, error);
else
variant = parse_number (typenode, class, error);
} else if (g_str_equal (typename, "double")) {
if (class != G_VARIANT_CLASS_VARIANT && class != G_VARIANT_CLASS_DOUBLE) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<double> node does not match signature");
goto fail;
}
variant = parse_double (typenode, error);
} else if (g_str_equal (typename, "string")) {
if (class != G_VARIANT_CLASS_VARIANT && class != G_VARIANT_CLASS_STRING) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<string> node does not match signature");
goto fail;
}
content = xmlNodeGetContent (typenode);
variant = g_variant_new_string ((const char *)content);
} else if (g_str_equal (typename, "base64")) {
if (class != G_VARIANT_CLASS_VARIANT) {
if (!g_str_has_prefix (*signature, "ay")) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<base64> node does not match signature");
goto fail;
}
(*signature)++;
}
variant = parse_base64 (typenode, error);
} else if (g_str_equal (typename, "struct")) {
if (class != G_VARIANT_CLASS_VARIANT &&
!g_str_has_prefix (*signature, "a{")) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<struct> node does not match signature");
goto fail;
}
variant = parse_dictionary (typenode, signature, error);
} else if (g_str_equal (typename, "array") || g_str_equal (typename, "params")) {
if (class != G_VARIANT_CLASS_VARIANT &&
class != G_VARIANT_CLASS_ARRAY &&
class != G_VARIANT_CLASS_TUPLE) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<%s> node does not match signature", typename);
goto fail;
}
variant = parse_array (typenode, signature, error);
} else if (g_str_equal (typename, "dateTime.iso8601")) {
if (class != G_VARIANT_CLASS_VARIANT) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<dateTime.iso8601> node does not match signature");
goto fail;
}
content = xmlNodeGetContent (typenode);
variant = soup_xmlrpc_variant_new_custom ("dateTime.iso8601",
(const char *)content);
} else {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Unknown node name %s", typename);
goto fail;
}
if (variant && signature) {
if (class == G_VARIANT_CLASS_VARIANT)
variant = g_variant_new_variant (variant);
(*signature)++;
}
fail:
if (content)
xmlFree (content);
return variant;
}
/**
* SoupXMLRPCParams:
*
* Opaque structure containing XML-RPC methodCall parameter values.
* Can be parsed using soup_xmlrpc_params_parse() and freed with
* soup_xmlrpc_params_free().
*
* Since: 2.52
*/
struct _SoupXMLRPCParams
{
xmlNode *node;
};
/**
* soup_xmlrpc_params_free:
* @self: a SoupXMLRPCParams
*
* Free a #SoupXMLRPCParams returned by soup_xmlrpc_parse_request().
*
* Since: 2.52
*/
void
soup_xmlrpc_params_free (SoupXMLRPCParams *self)
{
g_return_if_fail (self != NULL);
if (self->node)
xmlFreeDoc (self->node->doc);
g_slice_free (SoupXMLRPCParams, self);
}
static SoupXMLRPCParams *
soup_xmlrpc_params_new (xmlNode *node)
{
SoupXMLRPCParams *self;
self = g_slice_new (SoupXMLRPCParams);
self->node = node;
return self;
}
/**
* soup_xmlrpc_params_parse:
* @self: A #SoupXMLRPCParams
* @signature: (allow-none): A valid #GVariant type string, or %NULL
* @error: a #GError, or %NULL
*
* Parse method parameters returned by soup_xmlrpc_parse_request().
*
* Deserialization details:
* - If @signature is provided, <int> and <i4> can be deserialized
* to byte, int16, uint16, int32, uint32, int64 or uint64. Otherwise
* it will be deserialized to int32. If the value is out of range
* for the target type it will return an error.
* - <struct> will be deserialized to "a{sv}". @signature could define
* another value type (e.g. "a{ss}").
* - <array> will be deserialized to "av". @signature could define
* another element type (e.g. "as") or could be a tuple (e.g. "(ss)").
* - <base64> will be deserialized to "ay".
* - <string> will be deserialized to "s".
* - <dateTime.iso8601> will be deserialized to an unspecified variant
* type. If @signature is provided it must have the generic "v" type, which
* means there is no guarantee that it's actually a datetime that has been
* received. soup_xmlrpc_variant_get_datetime() must be used to parse and
* type check this special variant.
* - @signature must not have maybes, otherwise an error is returned.
* - Dictionaries must have string keys, otherwise an error is returned.
*
* Returns: (transfer full): a new (non-floating) #GVariant, or %NULL
*
* Since: 2.52
*/
GVariant *
soup_xmlrpc_params_parse (SoupXMLRPCParams *self,
const char *signature,
GError **error)
{
GVariant *value = NULL;
g_return_val_if_fail (self, NULL);
g_return_val_if_fail (!signature || g_variant_type_string_is_valid (signature), NULL);
if (!self->node) {
if (!signature || g_variant_type_equal (G_VARIANT_TYPE (signature), G_VARIANT_TYPE ("av")))
value = g_variant_new_array (G_VARIANT_TYPE_VARIANT, NULL, 0);
else if (g_variant_type_equal (G_VARIANT_TYPE (signature), G_VARIANT_TYPE_UNIT))
value = g_variant_new_tuple (NULL, 0);
else {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Invalid signature '%s', was expecting '()'", signature);
goto fail;
}
} else {
value = parse_value (self->node, signature ? &signature : NULL, error);
}
fail:
return value ? g_variant_ref_sink (value) : NULL;
}
/**
* soup_xmlrpc_parse_request:
* @method_call: the XML-RPC methodCall string
* @length: the length of @method_call, or -1 if it is NUL-terminated
* @params: (out): on success, a new #SoupXMLRPCParams
* @error: a #GError, or %NULL
*
* Parses @method_call and return the method name. Method parameters can be
* parsed later using soup_xmlrpc_params_parse().
*
* Returns: (transfer full): method's name, or %NULL on error.
* Since: 2.52
**/
char *
soup_xmlrpc_parse_request (const char *method_call,
int length,
SoupXMLRPCParams **params,
GError **error)
{
xmlDoc *doc = NULL;
xmlNode *node;
xmlChar *xmlMethodName = NULL;
char *method_name = NULL;
doc = xmlParseMemory (method_call, length == -1 ? strlen (method_call) : length);
if (!doc) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Could not parse XML document");
goto fail;
}
node = xmlDocGetRootElement (doc);
if (!node || strcmp ((const char *)node->name, "methodCall") != 0) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<methodCall> node expected");
goto fail;
}
node = find_real_node (node->children);
if (!node || strcmp ((const char *)node->name, "methodName") != 0) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<methodName> node expected");
goto fail;
}
xmlMethodName = xmlNodeGetContent (node);
if (params) {
node = find_real_node (node->next);
if (node) {
if (strcmp ((const char *)node->name, "params") != 0) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"<params> node expected");
goto fail;
}
*params = soup_xmlrpc_params_new (node);
doc = NULL;
} else {
*params = soup_xmlrpc_params_new (NULL);
}
}
method_name = g_strdup ((char *)xmlMethodName);
fail:
if (doc)
xmlFreeDoc (doc);
if (xmlMethodName)
xmlFree (xmlMethodName);
return method_name;
}
/**
* soup_xmlrpc_parse_response:
* @method_response: the XML-RPC methodResponse string
* @length: the length of @method_response, or -1 if it is NUL-terminated
* @signature: (allow-none): A valid #GVariant type string, or %NULL
* @error: a #GError, or %NULL
*
* Parses @method_response and returns the return value. If
* @method_response is a fault, %NULL is returned, and @error
* will be set to an error in the %SOUP_XMLRPC_FAULT domain, with the error
* code containing the fault code, and the error message containing
* the fault string. If @method_response cannot be parsed, %NULL is returned,
* and @error will be set to an error in the %SOUP_XMLRPC_ERROR domain.
*
* See soup_xmlrpc_params_parse() for deserialization details.
*
* Returns: (transfer full): a new (non-floating) #GVariant, or %NULL
*
* Since: 2.52
**/
GVariant *
soup_xmlrpc_parse_response (const char *method_response,
int length,
const char *signature,
GError **error)
{
xmlDoc *doc = NULL;
xmlNode *node;
GVariant *value = NULL;
g_return_val_if_fail (!signature || g_variant_type_string_is_valid (signature), NULL);
doc = xmlParseMemory (method_response,
length == -1 ? strlen (method_response) : length);
if (!doc) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Failed to parse response XML");
goto fail;
}
node = xmlDocGetRootElement (doc);
if (!node || strcmp ((const char *)node->name, "methodResponse") != 0) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Missing 'methodResponse' node");
goto fail;
}
node = find_real_node (node->children);
if (!node) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"'methodResponse' has no child");
goto fail;
}
if (!strcmp ((const char *)node->name, "fault")) {
int fault_code;
const char *fault_string;
const char *fault_sig = "a{sv}";
GVariant *fault_val;
node = find_real_node (node->children);
if (!node || strcmp ((const char *)node->name, "value") != 0) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"'fault' has no 'value' child");
goto fail;
}
fault_val = parse_value (node, &fault_sig, error);
if (!fault_val)
goto fail;
if (!g_variant_lookup (fault_val, "faultCode", "i", &fault_code) ||
!g_variant_lookup (fault_val, "faultString", "&s", &fault_string)) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"'fault' missing 'faultCode' or 'faultString'");
goto fail;
}
g_set_error (error, SOUP_XMLRPC_FAULT,
fault_code, "%s", fault_string);
g_variant_unref (fault_val);
} else if (!strcmp ((const char *)node->name, "params")) {
node = find_real_node (node->children);
if (!node || strcmp ((const char *)node->name, "param") != 0) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"'params' has no 'param' child");
goto fail;
}
node = find_real_node (node->children);
if (!node || strcmp ((const char *)node->name, "value") != 0) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"'param' has no 'value' child");
goto fail;
}
value = parse_value (node, signature ? &signature : NULL, error);
}
fail:
if (doc)
xmlFreeDoc (doc);
return value ? g_variant_ref_sink (value) : NULL;
}
/**
* soup_xmlrpc_variant_new_datetime:
* @date: a #SoupDate
*
* Construct a special #GVariant used to serialize a <dateTime.iso8601>
* node. See soup_xmlrpc_build_request().
*
* The actual type of the returned #GVariant is unspecified and "v" or "*"
* should be used in variant format strings. For example:
* <informalexample><programlisting>
* args = g_variant_new ("(v)", soup_xmlrpc_variant_new_datetime (date));
* </programlisting></informalexample>
*
* Returns: a floating #GVariant.
*
* Since: 2.52
*/
GVariant *
soup_xmlrpc_variant_new_datetime (SoupDate *date)
{
GVariant *variant;
char *str;
str = soup_date_to_string (date, SOUP_DATE_ISO8601_XMLRPC);
variant = soup_xmlrpc_variant_new_custom ("dateTime.iso8601", str);
g_free (str);
return variant;
}
/**
* soup_xmlrpc_variant_get_datetime:
* @variant: a #GVariant
* @error: a #GError, or %NULL
*
* Get the #SoupDate from special #GVariant created by
* soup_xmlrpc_variant_new_datetime() or by parsing a <dateTime.iso8601>
* node. See soup_xmlrpc_params_parse().
*
* If @variant does not contain a datetime it will return an error but it is not
* considered a programmer error because it generally means parameters received
* are not in the expected type.
*
* Returns: a new #SoupDate, or %NULL on error.
*
* Since: 2.52
*/
SoupDate *
soup_xmlrpc_variant_get_datetime (GVariant *variant, GError **error)
{
SoupDate *date = NULL;
const char *path;
const char *type;
const char *v;
if (!g_variant_is_of_type (variant, G_VARIANT_TYPE ("(oss)"))) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Variant is of type '%s' which is not expected for a datetime",
g_variant_get_type_string (variant));
return NULL;
}
g_variant_get (variant, "(&o&s&s)", &path, &type, &v);
if (!g_str_equal (path, "/org/gnome/libsoup/ExtensionType") ||
!g_str_equal (type, "dateTime.iso8601")) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Variant doesn't represent a datetime: %s",
g_variant_get_type_string (variant));
return NULL;
}
date = soup_date_new_from_string (v);
if (date == NULL) {
g_set_error (error, SOUP_XMLRPC_ERROR, SOUP_XMLRPC_ERROR_ARGUMENTS,
"Can't parse datetime string: %s", v);
return NULL;
}
return date;
}
/**
* SOUP_XMLRPC_FAULT:
*
* A #GError domain representing an XML-RPC fault code. Used with
* #SoupXMLRPCFault (although servers may also return fault codes not
* in that enumeration).
*/
/**
* SoupXMLRPCFault:
* @SOUP_XMLRPC_FAULT_PARSE_ERROR_NOT_WELL_FORMED: request was not
* well-formed
* @SOUP_XMLRPC_FAULT_PARSE_ERROR_UNSUPPORTED_ENCODING: request was in
* an unsupported encoding
* @SOUP_XMLRPC_FAULT_PARSE_ERROR_INVALID_CHARACTER_FOR_ENCODING:
* request contained an invalid character
* @SOUP_XMLRPC_FAULT_SERVER_ERROR_INVALID_XML_RPC: request was not
* valid XML-RPC
* @SOUP_XMLRPC_FAULT_SERVER_ERROR_REQUESTED_METHOD_NOT_FOUND: method
* not found
* @SOUP_XMLRPC_FAULT_SERVER_ERROR_INVALID_METHOD_PARAMETERS: invalid
* parameters
* @SOUP_XMLRPC_FAULT_SERVER_ERROR_INTERNAL_XML_RPC_ERROR: internal
* error
* @SOUP_XMLRPC_FAULT_APPLICATION_ERROR: start of reserved range for
* application error codes
* @SOUP_XMLRPC_FAULT_SYSTEM_ERROR: start of reserved range for
* system error codes
* @SOUP_XMLRPC_FAULT_TRANSPORT_ERROR: start of reserved range for
* transport error codes
*
* Pre-defined XML-RPC fault codes from <ulink
* url="http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php">http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php</ulink>.
* These are an extension, not part of the XML-RPC spec; you can't
* assume servers will use them.
*/
G_DEFINE_QUARK (soup_xmlrpc_fault_quark, soup_xmlrpc_fault);
G_DEFINE_QUARK (soup_xmlrpc_error_quark, soup_xmlrpc_error);