[misc] Add misc_xml.c to wrap mxml, fixes XML reading of CDATA (et al)

The change removes all direct calls to mxml from the modules that need an XML
parser (lastfm.c, pipe.c, rssscanner.c and httpd_rsp.c).

Even with the help of mxml, reading XML is hard, so a layer is added which
helps deal with stuff like whitespace and CDATA. This should make OwnTone more
resilient to any XML variations it might receive.

The changes fixes issue #1677.
This commit is contained in:
ejurgensen 2023-11-10 17:40:17 +01:00
parent 83ac327d7f
commit 65c72c484b
8 changed files with 648 additions and 497 deletions

View File

@ -107,6 +107,7 @@ owntone_SOURCES = main.c \
artwork.c artwork.h \
misc.c misc.h \
misc_json.c misc_json.h \
misc_xml.c misc_xml.h \
rng.c rng.h \
smartpl_query.c smartpl_query.h \
player.c player.h \
@ -128,7 +129,6 @@ owntone_SOURCES = main.c \
$(MPD_SRC) \
listener.c listener.h \
commands.c commands.h \
mxml-compat.h \
outputs/plist_wrap.h \
$(LIBWEBSOCKETS_SRC) \
$(GPERF_SRC) \

View File

@ -30,13 +30,12 @@
#include <sys/types.h>
#include <limits.h>
#include "mxml-compat.h"
#include "httpd_internal.h"
#include "logger.h"
#include "db.h"
#include "conffile.h"
#include "misc.h"
#include "misc_xml.h"
#include "transcode.h"
#include "parsers/rsp_parser.h"
@ -120,12 +119,12 @@ static const struct field_map rsp_fields[] =
/* -------------------------------- HELPERS --------------------------------- */
static int
mxml_to_evbuf(struct evbuffer *evbuf, mxml_node_t *tree)
xml_to_evbuf(struct evbuffer *evbuf, xml_node *tree)
{
char *xml;
int ret;
xml = mxmlSaveAllocString(tree, MXML_NO_CALLBACK);
xml = xml_to_string(tree);
if (!xml)
{
DPRINTF(E_LOG, L_RSP, "Could not finalize RSP reply\n");
@ -143,37 +142,33 @@ mxml_to_evbuf(struct evbuffer *evbuf, mxml_node_t *tree)
return 0;
}
static void
rsp_xml_response_new(xml_node **xml_ptr, xml_node **response_ptr, int errorcode, const char *errorstring, int records, int totalrecords)
{
xml_node *xml = xml_new_node(NULL, RSP_XML_ROOT, NULL);
xml_node *response = xml_new_node(xml, "response", NULL);
xml_node *status = xml_new_node(response, "status", NULL);
xml_new_node_textf(status, "errorcode", "%d", errorcode);
xml_new_node(status, "errorstring", errorstring);
xml_new_node_textf(status, "records", "%d", records);
xml_new_node_textf(status, "totalrecords", "%d", totalrecords);
if (response_ptr)
*response_ptr = response;
if (xml_ptr)
*xml_ptr = xml;
}
static void
rsp_send_error(struct httpd_request *hreq, char *errmsg)
{
mxml_node_t *reply;
mxml_node_t *status;
mxml_node_t *node;
xml_node *xml;
int ret;
/* We'd use mxmlNewXML(), but then we can't put any attributes
* on the root node and we need some.
*/
reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT);
node = mxmlNewElement(reply, "response");
status = mxmlNewElement(node, "status");
/* Status block */
node = mxmlNewElement(status, "errorcode");
mxmlNewText(node, 0, "1");
node = mxmlNewElement(status, "errorstring");
mxmlNewText(node, 0, errmsg);
node = mxmlNewElement(status, "records");
mxmlNewText(node, 0, "0");
node = mxmlNewElement(status, "totalrecords");
mxmlNewText(node, 0, "0");
ret = mxml_to_evbuf(hreq->out_body, reply);
mxmlDelete(reply);
rsp_xml_response_new(&xml, NULL, 1, errmsg, 0, 0);
ret = xml_to_evbuf(hreq->out_body, xml);
xml_free(xml);
if (ret < 0)
{
@ -259,12 +254,12 @@ query_params_set(struct query_params *qp, struct httpd_request *hreq)
}
static void
rsp_send_reply(struct httpd_request *hreq, mxml_node_t *reply)
rsp_send_reply(struct httpd_request *hreq, xml_node *reply)
{
int ret;
ret = mxml_to_evbuf(hreq->out_body, reply);
mxmlDelete(reply);
ret = xml_to_evbuf(hreq->out_body, reply);
xml_free(reply);
if (ret < 0)
{
@ -310,10 +305,9 @@ rsp_request_authorize(struct httpd_request *hreq)
static int
rsp_reply_info(struct httpd_request *hreq)
{
mxml_node_t *reply;
mxml_node_t *status;
mxml_node_t *info;
mxml_node_t *node;
xml_node *xml;
xml_node *response;
xml_node *info;
cfg_t *lib;
char *library;
uint32_t songcount;
@ -323,43 +317,16 @@ rsp_reply_info(struct httpd_request *hreq)
lib = cfg_getsec(cfg, "library");
library = cfg_getstr(lib, "name");
/* We'd use mxmlNewXML(), but then we can't put any attributes
* on the root node and we need some.
*/
reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT);
rsp_xml_response_new(&xml, &response, 0, "", 0, 0);
node = mxmlNewElement(reply, "response");
status = mxmlNewElement(node, "status");
info = mxmlNewElement(node, "info");
info = xml_new_node(response, "info", NULL);
/* Status block */
node = mxmlNewElement(status, "errorcode");
mxmlNewText(node, 0, "0");
node = mxmlNewElement(status, "errorstring");
mxmlNewText(node, 0, "");
node = mxmlNewElement(status, "records");
mxmlNewText(node, 0, "0");
node = mxmlNewElement(status, "totalrecords");
mxmlNewText(node, 0, "0");
/* Info block */
node = mxmlNewElement(info, "count");
mxmlNewTextf(node, 0, "%d", (int)songcount);
node = mxmlNewElement(info, "rsp-version");
mxmlNewText(node, 0, RSP_VERSION);
node = mxmlNewElement(info, "server-version");
mxmlNewText(node, 0, VERSION);
node = mxmlNewElement(info, "name");
mxmlNewText(node, 0, library);
rsp_send_reply(hreq, reply);
xml_new_node_textf(info, "count", "%d", (int)songcount);
xml_new_node(info, "rsp-version", RSP_VERSION);
xml_new_node(info, "server-version", VERSION);
xml_new_node(info, "name", library);
rsp_send_reply(hreq, xml);
return 0;
}
@ -369,11 +336,10 @@ rsp_reply_db(struct httpd_request *hreq)
struct query_params qp;
struct db_playlist_info dbpli;
char **strval;
mxml_node_t *reply;
mxml_node_t *status;
mxml_node_t *pls;
mxml_node_t *pl;
mxml_node_t *node;
xml_node *xml;
xml_node *response;
xml_node *pls;
xml_node *pl;
int i;
int ret;
@ -391,27 +357,9 @@ rsp_reply_db(struct httpd_request *hreq)
return -1;
}
/* We'd use mxmlNewXML(), but then we can't put any attributes
* on the root node and we need some.
*/
reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT);
rsp_xml_response_new(&xml, &response, 0, "", qp.results, qp.results);
node = mxmlNewElement(reply, "response");
status = mxmlNewElement(node, "status");
pls = mxmlNewElement(node, "playlists");
/* Status block */
node = mxmlNewElement(status, "errorcode");
mxmlNewText(node, 0, "0");
node = mxmlNewElement(status, "errorstring");
mxmlNewText(node, 0, "");
node = mxmlNewElement(status, "records");
mxmlNewTextf(node, 0, "%d", qp.results);
node = mxmlNewElement(status, "totalrecords");
mxmlNewTextf(node, 0, "%d", qp.results);
pls = xml_new_node(response, "playlists", NULL);
/* Playlists block (all playlists) */
while (((ret = db_query_fetch_pl(&dbpli, &qp)) == 0) && (dbpli.id))
@ -421,7 +369,7 @@ rsp_reply_db(struct httpd_request *hreq)
continue;
/* Playlist block (one playlist) */
pl = mxmlNewElement(pls, "playlist");
pl = xml_new_node(pls, "playlist", NULL);
for (i = 0; pl_fields[i].field; i++)
{
@ -429,8 +377,7 @@ rsp_reply_db(struct httpd_request *hreq)
{
strval = (char **) ((char *)&dbpli + pl_fields[i].offset);
node = mxmlNewElement(pl, pl_fields[i].field);
mxmlNewText(node, 0, *strval);
xml_new_node(pl, pl_fields[i].field, *strval);
}
}
}
@ -439,7 +386,7 @@ rsp_reply_db(struct httpd_request *hreq)
{
DPRINTF(E_LOG, L_RSP, "Error fetching results\n");
mxmlDelete(reply);
xml_free(xml);
db_query_end(&qp);
rsp_send_error(hreq, "Error fetching query results");
return -1;
@ -451,11 +398,11 @@ rsp_reply_db(struct httpd_request *hreq)
* tag that the SoundBridge does not handle. It's hackish, but it works.
*/
if (qp.results == 0)
mxmlNewText(pls, 0, "");
xml_new_text(pls, "");
db_query_end(&qp);
rsp_send_reply(hreq, reply);
rsp_send_reply(hreq, xml);
return 0;
}
@ -469,11 +416,10 @@ rsp_reply_playlist(struct httpd_request *hreq)
const char *ua;
const char *client_codecs;
char **strval;
mxml_node_t *reply;
mxml_node_t *status;
mxml_node_t *items;
mxml_node_t *item;
mxml_node_t *node;
xml_node *xml;
xml_node *response;
xml_node *items;
xml_node *item;
int mode;
int records;
int transcode;
@ -537,27 +483,9 @@ rsp_reply_playlist(struct httpd_request *hreq)
if (qp.limit && (records > qp.limit))
records = qp.limit;
/* We'd use mxmlNewXML(), but then we can't put any attributes
* on the root node and we need some.
*/
reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT);
rsp_xml_response_new(&xml, &response, 0, "", records, qp.results);
node = mxmlNewElement(reply, "response");
status = mxmlNewElement(node, "status");
items = mxmlNewElement(node, "items");
/* Status block */
node = mxmlNewElement(status, "errorcode");
mxmlNewText(node, 0, "0");
node = mxmlNewElement(status, "errorstring");
mxmlNewText(node, 0, "");
node = mxmlNewElement(status, "records");
mxmlNewTextf(node, 0, "%d", records);
node = mxmlNewElement(status, "totalrecords");
mxmlNewTextf(node, 0, "%d", qp.results);
items = xml_new_node(response, "items", NULL);
/* Items block (all items) */
while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0)
@ -568,7 +496,7 @@ rsp_reply_playlist(struct httpd_request *hreq)
transcode = transcode_needed(ua, client_codecs, dbmfi.codectype);
/* Item block (one item) */
item = mxmlNewElement(items, "item");
item = xml_new_node(items, "item", NULL);
for (i = 0; rsp_fields[i].field; i++)
{
@ -580,44 +508,41 @@ rsp_reply_playlist(struct httpd_request *hreq)
if (!(*strval) || (strlen(*strval) == 0))
continue;
node = mxmlNewElement(item, rsp_fields[i].field);
if (!transcode)
mxmlNewText(node, 0, *strval);
else
{
switch (rsp_fields[i].offset)
{
case dbmfi_offsetof(type):
mxmlNewText(node, 0, "wav");
break;
xml_new_node(item, rsp_fields[i].field, *strval);
continue;
}
case dbmfi_offsetof(bitrate):
bitrate = 0;
ret = safe_atoi32(dbmfi.samplerate, &bitrate);
if ((ret < 0) || (bitrate == 0))
bitrate = 1411;
else
bitrate = (bitrate * 8) / 250;
switch (rsp_fields[i].offset)
{
case dbmfi_offsetof(type):
xml_new_node(item, rsp_fields[i].field, "wav");
break;
mxmlNewTextf(node, 0, "%d", bitrate);
break;
case dbmfi_offsetof(bitrate):
bitrate = 0;
ret = safe_atoi32(dbmfi.samplerate, &bitrate);
if ((ret < 0) || (bitrate == 0))
bitrate = 1411;
else
bitrate = (bitrate * 8) / 250;
case dbmfi_offsetof(description):
mxmlNewText(node, 0, "wav audio file");
break;
xml_new_node_textf(item, rsp_fields[i].field, "%d", bitrate);
break;
case dbmfi_offsetof(codectype):
mxmlNewText(node, 0, "wav");
case dbmfi_offsetof(description):
xml_new_node(item, rsp_fields[i].field, "wav audio file");
break;
node = mxmlNewElement(item, "original_codec");
mxmlNewText(node, 0, *strval);
break;
case dbmfi_offsetof(codectype):
xml_new_node(item, rsp_fields[i].field, "wav");
xml_new_node(item, "original_codec", *strval);
break;
default:
mxmlNewText(node, 0, *strval);
break;
}
default:
xml_new_node(item, rsp_fields[i].field, *strval);
break;
}
}
}
@ -629,7 +554,7 @@ rsp_reply_playlist(struct httpd_request *hreq)
{
DPRINTF(E_LOG, L_RSP, "Error fetching results\n");
mxmlDelete(reply);
xml_free(xml);
db_query_end(&qp);
rsp_send_error(hreq, "Error fetching query results");
return -1;
@ -641,11 +566,11 @@ rsp_reply_playlist(struct httpd_request *hreq)
* tag that the SoundBridge does not handle. It's hackish, but it works.
*/
if (qp.results == 0)
mxmlNewText(items, 0, "");
xml_new_text(items, "");
db_query_end(&qp);
rsp_send_reply(hreq, reply);
rsp_send_reply(hreq, xml);
return 0;
}
@ -655,10 +580,9 @@ rsp_reply_browse(struct httpd_request *hreq)
{
struct query_params qp;
char *browse_item;
mxml_node_t *reply;
mxml_node_t *status;
mxml_node_t *items;
mxml_node_t *node;
xml_node *xml;
xml_node *response;
xml_node *items;
int records;
int ret;
@ -719,33 +643,14 @@ rsp_reply_browse(struct httpd_request *hreq)
if (qp.limit && (records > qp.limit))
records = qp.limit;
/* We'd use mxmlNewXML(), but then we can't put any attributes
* on the root node and we need some.
*/
reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT);
rsp_xml_response_new(&xml, &response, 0, "", records, qp.results);
node = mxmlNewElement(reply, "response");
status = mxmlNewElement(node, "status");
items = mxmlNewElement(node, "items");
/* Status block */
node = mxmlNewElement(status, "errorcode");
mxmlNewText(node, 0, "0");
node = mxmlNewElement(status, "errorstring");
mxmlNewText(node, 0, "");
node = mxmlNewElement(status, "records");
mxmlNewTextf(node, 0, "%d", records);
node = mxmlNewElement(status, "totalrecords");
mxmlNewTextf(node, 0, "%d", qp.results);
items = xml_new_node(response, "items", NULL);
/* Items block (all items) */
while (((ret = db_query_fetch_string(&browse_item, &qp)) == 0) && (browse_item))
{
node = mxmlNewElement(items, "item");
mxmlNewText(node, 0, browse_item);
xml_new_node(items, "item", browse_item);
}
if (qp.filter)
@ -755,7 +660,7 @@ rsp_reply_browse(struct httpd_request *hreq)
{
DPRINTF(E_LOG, L_RSP, "Error fetching results\n");
mxmlDelete(reply);
xml_free(xml);
db_query_end(&qp);
rsp_send_error(hreq, "Error fetching query results");
return -1;
@ -767,11 +672,11 @@ rsp_reply_browse(struct httpd_request *hreq)
* tag that the SoundBridge does not handle. It's hackish, but it works.
*/
if (qp.results == 0)
mxmlNewText(items, 0, "");
xml_new_text(items, "");
db_query_end(&qp);
rsp_send_reply(hreq, reply);
rsp_send_reply(hreq, xml);
return 0;
}

View File

@ -52,10 +52,9 @@
#include <event2/event.h>
#include <event2/buffer.h>
#include "mxml-compat.h"
#include "input.h"
#include "misc.h"
#include "misc_xml.h"
#include "logger.h"
#include "db.h"
#include "conffile.h"
@ -531,35 +530,33 @@ log_incoming(int severity, const char *msg, uint32_t type, uint32_t code, int da
DPRINTF(severity, L_PLAYER, "%s (type=%s, code=%s, len=%d)\n", msg, typestr, codestr, data_len);
}
/* Example of xml item:
<item><type>73736e63</type><code>6d647374</code><length>9</length>
<data encoding="base64">
NDE5OTg3OTU0</data></item>
*/
static int
parse_item_xml(uint32_t *type, uint32_t *code, uint8_t **data, int *data_len, const char *item)
{
mxml_node_t *xml;
mxml_node_t *haystack;
mxml_node_t *needle;
xml_node *xml;
const char *s;
xml = mxmlNewXML("1.0");
// DPRINTF(E_DBG, L_PLAYER, "Got pipe metadata item: '%s'\n", item);
xml = xml_from_string(item);
if (!xml)
return -1;
// DPRINTF(E_DBG, L_PLAYER, "Parsing %s\n", item);
haystack = mxmlLoadString(xml, item, MXML_NO_CALLBACK);
if (!haystack)
{
DPRINTF(E_LOG, L_PLAYER, "Could not parse pipe metadata: %s\n", item);
DPRINTF(E_LOG, L_PLAYER, "Could not parse pipe metadata item: %s\n", item);
goto error;
}
*type = 0;
if ( (needle = mxmlFindElement(haystack, haystack, "type", NULL, NULL, MXML_DESCEND)) &&
(s = mxmlGetText(needle, NULL)) )
if ((s = xml_get_val(xml, "item/type")))
sscanf(s, "%8x", type);
*code = 0;
if ( (needle = mxmlFindElement(haystack, haystack, "code", NULL, NULL, MXML_DESCEND)) &&
(s = mxmlGetText(needle, NULL)) )
if ((s = xml_get_val(xml, "item/code")))
sscanf(s, "%8x", code);
if (*type == 0 || *code == 0)
@ -570,8 +567,7 @@ parse_item_xml(uint32_t *type, uint32_t *code, uint8_t **data, int *data_len, co
*data = NULL;
*data_len = 0;
if ( (needle = mxmlFindElement(haystack, haystack, "data", NULL, NULL, MXML_DESCEND)) &&
(s = mxmlGetText(needle, NULL)) )
if ((s = xml_get_val(xml, "item/data")))
{
*data = b64_decode(data_len, s);
if (*data == NULL)
@ -583,11 +579,11 @@ parse_item_xml(uint32_t *type, uint32_t *code, uint8_t **data, int *data_len, co
log_incoming(E_SPAM, "Read Shairport metadata", *type, *code, *data_len);
mxmlDelete(xml);
xml_free(xml);
return 0;
error:
mxmlDelete(xml);
xml_free(xml);
return -1;
}

View File

@ -34,14 +34,13 @@
#include <event2/buffer.h>
#include <event2/http.h>
#include "mxml-compat.h"
#include "db.h"
#include "conffile.h"
#include "lastfm.h"
#include "listener.h"
#include "logger.h"
#include "misc.h"
#include "misc_xml.h"
#include "http.h"
// LastFM becomes disabled if we get a scrobble, try initialising session,
@ -119,15 +118,44 @@ param_sign(struct keyval *kv)
/* --------------------------------- MAIN --------------------------------- */
/* Example responses
<?xml version="1.0" encoding="UTF-8"?>
<lfm status="ok">
<session>
<name>myname</name>
<key>dsfjDFDS22</key>
<subscriber>0</subscriber>
</session>
</lfm>
<?xml version="1.0" encoding="UTF-8"?>
<lfm status="failed">
<error code="4">Authentication Failed - You do not have permissions to access the service</error>
</lfm>
<?xml version="1.0" encoding="UTF-8"?>
<lfm status="ok">
<scrobbles ignored="0" accepted="1">
<scrobble>
<track corrected="0">Hard Place</track>
<artist corrected="0">My Artist</artist>
<album corrected="0">My Album</album>
<albumArtist corrected="0">My Album Artist</albumArtist>
<timestamp>1699649816</timestamp>
<ignoredMessage code="0"></ignoredMessage>
</scrobble>
</scrobbles>
</lfm>
*/
static int
response_process(struct http_client_ctx *ctx, char **errmsg)
{
mxml_node_t *tree;
mxml_node_t *s_node;
mxml_node_t *e_node;
xml_node *tree;
const char *error;
char *body;
char *sk;
int ret;
// NULL-terminate the buffer
evbuffer_add(ctx->input_body, "", 1);
@ -139,67 +167,55 @@ response_process(struct http_client_ctx *ctx, char **errmsg)
return -1;
}
tree = mxmlLoadString(NULL, body, MXML_OPAQUE_CALLBACK);
tree = xml_from_string(body);
if (!tree)
{
DPRINTF(E_LOG, L_LASTFM, "Failed to parse LastFM response:\n%s\n", body);
return -1;
}
// Look for errors
e_node = mxmlFindElement(tree, tree, "error", NULL, NULL, MXML_DESCEND);
if (e_node)
error = xml_get_val(tree, "lfm/error");
if (error)
{
DPRINTF(E_LOG, L_LASTFM, "Request to LastFM failed: %s\n", mxmlGetOpaque(e_node));
DPRINTF(E_LOG, L_LASTFM, "Request to LastFM failed: %s\n", error);
DPRINTF(E_DBG, L_LASTFM, "LastFM response:\n%s\n", body);
if (errmsg)
*errmsg = atrim(mxmlGetOpaque(e_node));
*errmsg = atrim(error);
mxmlDelete(tree);
xml_free(tree);
return -1;
}
DPRINTF(E_SPAM, L_LASTFM, "LastFM response:\n%s\n", body);
// Was it a scrobble request? Then do nothing. TODO: Check for error messages
s_node = mxmlFindElement(tree, tree, "scrobbles", NULL, NULL, MXML_DESCEND);
if (s_node)
if (xml_get_node(tree, "lfm/scrobbles/scrobble"))
{
DPRINTF(E_DBG, L_LASTFM, "Scrobble callback\n");
mxmlDelete(tree);
xml_free(tree);
return 0;
}
// Otherwise an auth request, so get the session key
s_node = mxmlFindElement(tree, tree, "key", NULL, NULL, MXML_DESCEND);
if (!s_node)
sk = atrim(xml_get_val(tree, "lfm/session/key"));
if (!sk)
{
DPRINTF(E_LOG, L_LASTFM, "Session key not found\n");
mxmlDelete(tree);
xml_free(tree);
return -1;
}
sk = atrim(mxmlGetOpaque(s_node));
if (sk)
{
DPRINTF(E_LOG, L_LASTFM, "Got session key from LastFM: %s\n", sk);
db_admin_set(DB_ADMIN_LASTFM_SESSION_KEY, sk);
DPRINTF(E_INFO, L_LASTFM, "Got session key from LastFM: %s\n", sk);
db_admin_set(DB_ADMIN_LASTFM_SESSION_KEY, sk);
if (lastfm_session_key)
free(lastfm_session_key);
free(lastfm_session_key);
lastfm_session_key = sk;
lastfm_disabled = false;
ret = 0;
}
else
{
ret = -1;
}
lastfm_session_key = sk;
lastfm_disabled = false;
mxmlDelete(tree);
return ret;
xml_free(tree);
return 0;
}
/*

View File

@ -40,14 +40,13 @@
#include <event2/buffer.h>
#include "mxml-compat.h"
#include "conffile.h"
#include "logger.h"
#include "db.h"
#include "http.h"
#include "misc.h"
#include "misc_json.h"
#include "misc_xml.h"
#include "library.h"
#include "library/filescanner.h"
@ -233,12 +232,12 @@ playlist_fetch(bool *is_new, const char *path)
return NULL;
}
static mxml_node_t *
static xml_node *
rss_xml_get(const char *url)
{
struct http_client_ctx ctx = { 0 };
const char *raw = NULL;
mxml_node_t *xml = NULL;
xml_node *xml = NULL;
char *feedurl;
int ret;
@ -267,7 +266,7 @@ rss_xml_get(const char *url)
raw = (const char*)evbuffer_pullup(ctx.input_body, -1);
xml = mxmlLoadString(NULL, raw, MXML_OPAQUE_CALLBACK);
xml = xml_from_string(raw);
if (!xml)
{
DPRINTF(E_LOG, L_LIB, "Failed to parse RSS XML from '%s'\n", ctx.url);
@ -281,85 +280,39 @@ rss_xml_get(const char *url)
}
static int
rss_xml_parse_feed(const char **feed_title, const char **feed_author, const char **feed_artwork, mxml_node_t *xml)
feed_metadata_from_xml(const char **feed_title, const char **feed_author, const char **feed_artwork, xml_node *xml)
{
mxml_node_t *channel;
mxml_node_t *node;
channel = mxmlFindElement(xml, xml, "channel", NULL, NULL, MXML_DESCEND);
xml_node *channel = xml_get_node(xml, "rss/channel");
if (!channel)
{
DPRINTF(E_LOG, L_LIB, "Invalid RSS/xml, missing 'channel' node\n");
return -1;
}
node = mxmlFindElement(channel, channel, "title", NULL, NULL, MXML_DESCEND_FIRST);
if (!node)
*feed_title = xml_get_val(channel, "title");
if (!*feed_title)
{
DPRINTF(E_LOG, L_LIB, "Invalid RSS/xml, missing 'title' node\n");
return -1;
}
*feed_title = mxmlGetOpaque(node);
node = mxmlFindElement(channel, channel, "itunes:author", NULL, NULL, MXML_DESCEND_FIRST);
*feed_author = node ? mxmlGetOpaque(node) : NULL;
*feed_artwork = NULL;
node = mxmlFindElement(channel, channel, "image", NULL, NULL, MXML_DESCEND_FIRST);
if (node)
{
node = mxmlFindElement(node, node, "url", NULL, NULL, MXML_DESCEND_FIRST);
*feed_artwork = node ? mxmlGetOpaque(node) : NULL;
}
*feed_author = xml_get_val(channel, "itunes:author");
*feed_artwork = xml_get_val(channel, "image/url");
return 0;
}
static int
rss_xml_parse_item(struct rss_item_info *ri, mxml_node_t *xml, void **saveptr)
static void
ri_from_item(struct rss_item_info *ri, xml_node *item)
{
mxml_node_t *item;
mxml_node_t *node;
const char *s;
if (*saveptr)
{
item = (mxml_node_t *)(*saveptr);
while ( (item = mxmlGetNextSibling(item)) )
{
s = mxmlGetElement(item);
if (s && strcmp(s, "item") == 0)
break;
}
*saveptr = item;
}
else
{
item = mxmlFindElement(xml, xml, "item", NULL, NULL, MXML_DESCEND);
*saveptr = item;
}
if (!item)
return -1; // No more items
memset(ri, 0, sizeof(struct rss_item_info));
node = mxmlFindElement(item, item, "title", NULL, NULL, MXML_DESCEND_FIRST);
ri->title = mxmlGetOpaque(node);
ri->title = xml_get_val(item, "title");
ri->pubdate = xml_get_val(item, "pubDate");
ri->link = xml_get_val(item, "link");
node = mxmlFindElement(item, item, "pubDate", NULL, NULL, MXML_DESCEND_FIRST);
ri->pubdate = mxmlGetOpaque(node);
node = mxmlFindElement(item, item, "link", NULL, NULL, MXML_DESCEND_FIRST);
ri->link = mxmlGetOpaque(node);
node = mxmlFindElement(item, item, "enclosure", NULL, NULL, MXML_DESCEND_FIRST);
ri->url = mxmlElementGetAttr(node, "url");
ri->type = mxmlElementGetAttr(node, "type");
DPRINTF(E_DBG, L_LIB, "RSS/xml item: title '%s' pubdate: '%s' link: '%s' url: '%s' type: '%s'\n", ri->title, ri->pubdate, ri->link, ri->url, ri->type);
return 0;
ri->url = xml_get_attr(item, "enclosure", "url");
ri->type = xml_get_attr(item, "enclosure", "type");
}
// The RSS spec states:
@ -411,14 +364,14 @@ mfi_metadata_fixup(struct media_file_info *mfi, struct rss_item_info *ri, const
static int
rss_save(struct playlist_info *pli, int *count, enum rss_scan_type scan_type)
{
mxml_node_t *xml;
xml_node *xml;
xml_node *item;
const char *feed_title;
const char *feed_author;
const char *feed_artwork;
struct media_file_info mfi = { 0 };
struct rss_item_info ri;
uint32_t time_added;
void *ptr = NULL;
int ret;
xml = rss_xml_get(pli->path);
@ -428,11 +381,11 @@ rss_save(struct playlist_info *pli, int *count, enum rss_scan_type scan_type)
return -1;
}
ret = rss_xml_parse_feed(&feed_title, &feed_author, &feed_artwork, xml);
ret = feed_metadata_from_xml(&feed_title, &feed_author, &feed_artwork, xml);
if (ret < 0)
{
DPRINTF(E_LOG, L_LIB, "Invalid RSS/xml received from '%s' (id %d)\n", pli->path, pli->id);
mxmlDelete(xml);
xml_free(xml);
return -1;
}
@ -455,21 +408,24 @@ rss_save(struct playlist_info *pli, int *count, enum rss_scan_type scan_type)
*count = 0;
db_transaction_begin();
db_pl_clear_items(pli->id);
while ((ret = rss_xml_parse_item(&ri, xml, &ptr)) == 0 && (*count < pli->query_limit))
for (item = xml_get_node(xml, "rss/channel/item"); item && (*count < pli->query_limit); item = xml_get_next(xml, item))
{
if (library_is_exiting())
{
db_transaction_rollback();
mxmlDelete(xml);
xml_free(xml);
return -1;
}
ri_from_item(&ri, item);
if (!ri.url)
{
DPRINTF(E_WARN, L_LIB, "Missing URL for item '%s' (date %s) in RSS feed '%s'\n", ri.title, ri.pubdate, feed_title);
continue;
}
DPRINTF(E_DBG, L_LIB, "RSS/xml item: title '%s' pubdate: '%s' link: '%s' url: '%s' type: '%s'\n", ri.title, ri.pubdate, ri.link, ri.url, ri.type);
db_pl_add_item_bypath(pli->id, ri.url);
(*count)++;
@ -499,7 +455,7 @@ rss_save(struct playlist_info *pli, int *count, enum rss_scan_type scan_type)
}
db_transaction_end();
mxmlDelete(xml);
xml_free(xml);
return 0;
}

400
src/misc_xml.c Normal file
View File

@ -0,0 +1,400 @@
/*
* Copyright (C) 2023 Espen Jurgensen
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*
* About pipe.c
* --------------
* This module will read a PCM16 stream from a named pipe and write it to the
* input buffer. The user may start/stop playback from a pipe by selecting it
* through a client. If the user has configured pipe_autostart, then pipes in
* the library will also be watched for data, and playback will start/stop
* automatically.
*
* The module will also look for pipes with a .metadata suffix, and if found,
* the metadata will be parsed and fed to the player. The metadata must be in
* the format Shairport uses for this purpose.
*
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdio.h> // fopen
#include <stdarg.h> // va_*
#include <ctype.h>
#include <mxml.h>
typedef mxml_node_t xml_node;
/* ---------------- Compability with older versions of mxml ----------------- */
// mxml 2.10 has a memory leak in mxmlDelete, see https://github.com/michaelrsweet/mxml/issues/183
// - and since this is the version in Ubuntu 18.04 LTS and Raspian Stretch, we
// fix it by including a fixed mxmlDelete here. It should be removed once the
// major distros no longer have 2.10. The below code is msweet's fixed mxml.
#if (MXML_MAJOR_VERSION == 2) && (MXML_MINOR_VERSION <= 10)
#define mxmlDelete compat_mxmlDelete
static void
compat_mxml_free(mxml_node_t *node)
{
int i;
switch (node->type)
{
case MXML_ELEMENT :
if (node->value.element.name)
free(node->value.element.name);
if (node->value.element.num_attrs)
{
for (i = 0; i < node->value.element.num_attrs; i ++)
{
if (node->value.element.attrs[i].name)
free(node->value.element.attrs[i].name);
if (node->value.element.attrs[i].value)
free(node->value.element.attrs[i].value);
}
free(node->value.element.attrs);
}
break;
case MXML_INTEGER :
break;
case MXML_OPAQUE :
if (node->value.opaque)
free(node->value.opaque);
break;
case MXML_REAL :
break;
case MXML_TEXT :
if (node->value.text.string)
free(node->value.text.string);
break;
case MXML_CUSTOM :
if (node->value.custom.data &&
node->value.custom.destroy)
(*(node->value.custom.destroy))(node->value.custom.data);
break;
default :
break;
}
free(node);
}
__attribute__((unused)) static void
compat_mxmlDelete(mxml_node_t *node)
{
mxml_node_t *current,
*next;
if (!node)
return;
mxmlRemove(node);
for (current = node->child; current; current = next)
{
if ((next = current->child) != NULL)
{
current->child = NULL;
continue;
}
if ((next = current->next) == NULL)
{
if ((next = current->parent) == node)
next = NULL;
}
compat_mxml_free(current);
}
compat_mxml_free(node);
}
#endif
/* For compability with mxml 2.6 */
#ifndef HAVE_MXMLGETTEXT
__attribute__((unused)) static const char * /* O - Text string or NULL */
mxmlGetText(mxml_node_t *node, /* I - Node to get */
int *whitespace) /* O - 1 if string is preceded by whitespace, 0 otherwise */
{
if (node->type == MXML_TEXT)
return (node->value.text.string);
else if (node->type == MXML_ELEMENT &&
node->child &&
node->child->type == MXML_TEXT)
return (node->child->value.text.string);
else
return (NULL);
}
#endif
#ifndef HAVE_MXMLGETOPAQUE
__attribute__((unused)) static const char * /* O - Opaque string or NULL */
mxmlGetOpaque(mxml_node_t *node) /* I - Node to get */
{
if (!node)
return (NULL);
if (node->type == MXML_OPAQUE)
return (node->value.opaque);
else if (node->type == MXML_ELEMENT &&
node->child &&
node->child->type == MXML_OPAQUE)
return (node->child->value.opaque);
else
return (NULL);
}
#endif
#ifndef HAVE_MXMLGETFIRSTCHILD
__attribute__((unused)) static mxml_node_t * /* O - First child or NULL */
mxmlGetFirstChild(mxml_node_t *node) /* I - Node to get */
{
if (!node || node->type != MXML_ELEMENT)
return (NULL);
return (node->child);
}
#endif
#ifndef HAVE_MXMLGETTYPE
__attribute__((unused)) static mxml_type_t /* O - Type of node */
mxmlGetType(mxml_node_t *node) /* I - Node to get */
{
return (node->type);
}
#endif
/* --------------------------------- Helpers -------------------------------- */
// We get values from mxml via GetOpaque, but that means they can whitespace,
// thus we trim them. A bit dirty, since the values are in principle const.
static const char *
trim(const char *str)
{
char *term;
if (!str)
return NULL;
while (isspace(*str))
str++;
term = (char *)str + strlen(str);
while (term != str && isspace(*(term - 1)))
term--;
// Dirty write to the const string from mxml
*term = '\0';
return str;
}
/* -------------------------- Wrapper implementation ------------------------ */
char *
xml_to_string(xml_node *top)
{
return mxmlSaveAllocString(top, MXML_NO_CALLBACK);
}
// This works both for well-formed xml strings (beginning with <?xml..) and for
// those that get straight down to business (<foo...)
xml_node *
xml_from_string(const char *string)
{
mxml_node_t *top;
mxml_node_t *node;
top = mxmlNewXML("1.0");
if (!top)
goto error;
node = mxmlLoadString(top, string, MXML_OPAQUE_CALLBACK);
if (!node)
goto error;
return top;
error:
mxmlDelete(top);
return NULL;
}
xml_node *
xml_from_file(const char *path)
{
FILE *fp;
mxml_node_t *top;
mxml_node_t *node;
top = mxmlNewXML("1.0");
if (!top)
goto error;
fp = fopen(path, "r");
node = mxmlLoadFile(top, fp, MXML_OPAQUE_CALLBACK);
fclose(fp);
if (!node)
goto error;
return top;
error:
mxmlDelete(top);
return NULL;
}
void
xml_free(xml_node *top)
{
mxmlDelete(top);
}
xml_node *
xml_get_node(xml_node *top, const char *path)
{
mxml_node_t *node;
mxml_type_t type;
// This example shows why we can't just return the result of mxmlFindPath:
// <?xml version="1.0""?><rss>
// <channel>
// <title><![CDATA[Tissages]]></title>
// mxmlFindPath(top, "rss/channel") will return an OPAQUE node where the
// opaque value is just the whitespace. What we want is the ELEMENT parent,
// because that's the one we can use to search for children nodes ("title").
node = mxmlFindPath(top, path);
type = mxmlGetType(node);
if (type == MXML_ELEMENT)
return node;
return mxmlGetParent(node);
}
xml_node *
xml_get_next(xml_node *top, xml_node *node)
{
const char *name;
const char *s;
name = mxmlGetElement(node);
if (!name)
return NULL;
while ( (node = mxmlGetNextSibling(node)) )
{
s = mxmlGetElement(node);
if (s && strcmp(s, name) == 0)
return node;
}
return NULL;
}
// Walks through the children of the "path" node until it finds one that is
// not just whitespace and returns a trimmed value (except for CDATA). Means
// that these variations will all give the same result:
//
// <foo>FOO FOO</foo><bar>\nBAR BAR \n</bar>
// <foo>FOO FOO</foo><bar><![CDATA[BAR BAR]]></bar>
// <foo>\nFOO FOO\n</foo><bar>\n<![CDATA[BAR BAR]]></bar>
const char *
xml_get_val(xml_node *top, const char *path)
{
mxml_node_t *parent;
mxml_node_t *node;
mxml_type_t type;
const char *s = "";
parent = xml_get_node(top, path);
if (!parent)
return NULL;
for (node = mxmlGetFirstChild(parent); node; node = mxmlGetNextSibling(node))
{
type = mxmlGetType(node);
if (type == MXML_OPAQUE)
s = trim(mxmlGetOpaque(node));
else if (type == MXML_ELEMENT)
s = mxmlGetCDATA(node);
if (s && *s != '\0')
break;
}
return s;
}
const char *
xml_get_attr(xml_node *top, const char *path, const char *name)
{
mxml_node_t *node = mxmlFindPath(top, path);
return mxmlElementGetAttr(node, name);
}
xml_node *
xml_new_node(xml_node *parent, const char *name, const char *val)
{
if (!parent)
parent = MXML_NO_PARENT;
mxml_node_t *node = mxmlNewElement(parent, name);
if (!val)
return node; // We're done, caller gets an ELEMENT to use as parent
mxmlNewText(node, 0, val);
return node;
}
xml_node *
xml_new_node_textf(xml_node *parent, const char *name, const char *format, ...)
{
char *s = NULL;
va_list va;
mxml_node_t *node;
int ret;
va_start(va, format);
ret = vasprintf(&s, format, va);
va_end(va);
if (ret < 0)
return NULL;
node = xml_new_node(parent, name, s);
free(s);
return node;
}
void
xml_new_text(xml_node *parent, const char *val)
{
mxmlNewText(parent, 0, val);
}

56
src/misc_xml.h Normal file
View File

@ -0,0 +1,56 @@
#ifndef SRC_MISC_XML_H_
#define SRC_MISC_XML_H_
// This wraps mxml and adds some convenience functions. This also means that
// callers don't need to concern themselves with changes and bugs in various
// versions of mxml.
typedef void xml_node;
// Wraps mxmlSaveAllocString. Returns NULL on error.
char *
xml_to_string(xml_node *top);
// Wraps mxmlNewXML and mxmlLoadString, so creates an xml struct with the parsed
// content of string. Returns NULL on error.
xml_node *
xml_from_string(const char *string);
// Wraps mxmlNewXML and mxmlLoadFile, so creates an xml struct with the parsed
// content of string. Returns NULL on error.
xml_node *
xml_from_file(const char *path);
// Wraps mxmlDelete, which will free node + underlying nodes
void
xml_free(xml_node *top);
// Wraps mxmlFindPath.
xml_node *
xml_get_node(xml_node *top, const char *path);
// Wraps mxmlGetNextSibling, but only returns sibling nodes that have the same
// name as input node.
xml_node *
xml_get_next(xml_node *top, xml_node *node);
// Wraps mxmlFindPath and mxmlGetOpaque + mxmlGetCDATA. Returns NULL if nothing
// can be found.
const char *
xml_get_val(xml_node *top, const char *path);
// Wraps mxmlFindPath and mxmlElementGetAttr. Returns NULL if nothing can be
// found.
const char *
xml_get_attr(xml_node *top, const char *path, const char *name);
xml_node *
xml_new_node(xml_node *parent, const char *name, const char *val);
xml_node *
xml_new_node_textf(xml_node *parent, const char *name, const char *format, ...);
void
xml_new_text(xml_node *parent, const char *val);
#endif /* SRC_MISC_XML_H_ */

View File

@ -1,178 +0,0 @@
#ifndef __MXML_COMPAT_H__
#define __MXML_COMPAT_H__
#include <mxml.h>
// mxml 2.10 has a memory leak in mxmlDelete, see https://github.com/michaelrsweet/mxml/issues/183
// - and since this is the version in Ubuntu 18.04 LTS and Raspian Stretch, we
// fix it by including a fixed mxmlDelete here. It should be removed once the
// major distros no longer have 2.10. The below code is msweet's fixed mxml.
#if (MXML_MAJOR_VERSION == 2) && (MXML_MINOR_VERSION <= 10)
#define mxmlDelete compat_mxmlDelete
static void
compat_mxml_free(mxml_node_t *node)
{
int i;
switch (node->type)
{
case MXML_ELEMENT :
if (node->value.element.name)
free(node->value.element.name);
if (node->value.element.num_attrs)
{
for (i = 0; i < node->value.element.num_attrs; i ++)
{
if (node->value.element.attrs[i].name)
free(node->value.element.attrs[i].name);
if (node->value.element.attrs[i].value)
free(node->value.element.attrs[i].value);
}
free(node->value.element.attrs);
}
break;
case MXML_INTEGER :
break;
case MXML_OPAQUE :
if (node->value.opaque)
free(node->value.opaque);
break;
case MXML_REAL :
break;
case MXML_TEXT :
if (node->value.text.string)
free(node->value.text.string);
break;
case MXML_CUSTOM :
if (node->value.custom.data &&
node->value.custom.destroy)
(*(node->value.custom.destroy))(node->value.custom.data);
break;
default :
break;
}
free(node);
}
__attribute__((unused)) static void
compat_mxmlDelete(mxml_node_t *node)
{
mxml_node_t *current,
*next;
if (!node)
return;
mxmlRemove(node);
for (current = node->child; current; current = next)
{
if ((next = current->child) != NULL)
{
current->child = NULL;
continue;
}
if ((next = current->next) == NULL)
{
if ((next = current->parent) == node)
next = NULL;
}
compat_mxml_free(current);
}
compat_mxml_free(node);
}
#endif
// Debian 10.x amd64 w/mxml 2.12 has a mxmlNewTextf that causes segfault when
// mxmlSaveString or mxmlSaveAllocString is called,
// ref https://github.com/owntone/owntone-server/issues/938
#if (MXML_MAJOR_VERSION == 2) && (MXML_MINOR_VERSION == 12)
#include <stdarg.h>
#define mxmlNewTextf compat_mxmlNewTextf
__attribute__((unused)) static mxml_node_t *
compat_mxmlNewTextf(mxml_node_t *parent, int whitespace, const char *format, ...)
{
char *s = NULL;
va_list va;
mxml_node_t *node;
int ret;
va_start(va, format);
ret = vasprintf(&s, format, va);
va_end(va);
if (ret < 0)
return NULL;
node = mxmlNewText(parent, whitespace, s);
free(s);
return node;
}
#endif
/* For compability with mxml 2.6 */
#ifndef HAVE_MXMLGETTEXT
__attribute__((unused)) static const char * /* O - Text string or NULL */
mxmlGetText(mxml_node_t *node, /* I - Node to get */
int *whitespace) /* O - 1 if string is preceded by whitespace, 0 otherwise */
{
if (node->type == MXML_TEXT)
return (node->value.text.string);
else if (node->type == MXML_ELEMENT &&
node->child &&
node->child->type == MXML_TEXT)
return (node->child->value.text.string);
else
return (NULL);
}
#endif
#ifndef HAVE_MXMLGETOPAQUE
__attribute__((unused)) static const char * /* O - Opaque string or NULL */
mxmlGetOpaque(mxml_node_t *node) /* I - Node to get */
{
if (!node)
return (NULL);
if (node->type == MXML_OPAQUE)
return (node->value.opaque);
else if (node->type == MXML_ELEMENT &&
node->child &&
node->child->type == MXML_OPAQUE)
return (node->child->value.opaque);
else
return (NULL);
}
#endif
#ifndef HAVE_MXMLGETFIRSTCHILD
__attribute__((unused)) static mxml_node_t * /* O - First child or NULL */
mxmlGetFirstChild(mxml_node_t *node) /* I - Node to get */
{
if (!node || node->type != MXML_ELEMENT)
return (NULL);
return (node->child);
}
#endif
#ifndef HAVE_MXMLGETTYPE
__attribute__((unused)) static mxml_type_t /* O - Type of node */
mxmlGetType(mxml_node_t *node) /* I - Node to get */
{
return (node->type);
}
#endif
#endif /* !__MXML_COMPAT_H__ */