mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-26 07:05:57 -05:00
[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:
parent
83ac327d7f
commit
65c72c484b
@ -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) \
|
||||
|
285
src/httpd_rsp.c
285
src/httpd_rsp.c
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
88
src/lastfm.c
88
src/lastfm.c
@ -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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -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
400
src/misc_xml.c
Normal 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
56
src/misc_xml.h
Normal 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_ */
|
@ -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__ */
|
Loading…
Reference in New Issue
Block a user