diary

Text-based journaling program
git clone https://git.in0rdr.ch/diary.git
Log | Files | Refs | Pull requests | README | LICENSE

commit 49549e3ca1b9a96820e06d63f6f7932cf51e920b
parent 099b3614353dc95326e9637409dc08e5bf1878d7
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Sat, 14 Sep 2024 12:37:21 +0200

feat(libxml): extract_xml_content

Adds the extract_xml_content utility function.The function extracts XML
content using XPath expressions.

Replaces previous custom parsing logic (string token manipulation).

Diffstat:
Msrc/caldav.c | 185++++++++++++++++++++++++-------------------------------------------------------
Msrc/utils.c | 43+++++++++++++++++++++++++++++++++++++++++++
Msrc/utils.h | 4++++
3 files changed, 103 insertions(+), 129 deletions(-)

diff --git a/src/caldav.c b/src/caldav.c @@ -7,6 +7,25 @@ char access_token[sizeof(char)*2048] = ""; // Reserve 2 chars for the ipv6 square brackets. char ip[INET6_ADDRSTRLEN], ipstr[INET6_ADDRSTRLEN + 2]; +// Static XML data for CalDav post requests: +// https://write.in0rdr.ch/caldav-calendar-discovery +const char* principal_postfields = "<d:propfind xmlns:d='DAV:' xmlns:cs='http://calendarserver.org/ns/'>" + "<d:prop><d:current-user-principal/></d:prop>" + "</d:propfind>"; +const char* homeset_request = "<d:propfind xmlns:d='DAV:' xmlns:c='urn:ietf:params:xml:ns:caldav'>" + " <d:prop>" + " <c:calendar-home-set />" + " </d:prop>" + "</d:propfind>"; +const char* calendar_request = "<d:propfind xmlns:d='DAV:' xmlns:cs='http://calendarserver.org/ns/' xmlns:c='urn:ietf:params:xml:ns:caldav'>" + " <d:prop>" + " <d:resourcetype />" + " <d:displayname />" + " <cs:getctag />" + " <c:supported-calendar-component-set />" + " </d:prop>" + "</d:propfind>"; + static size_t curl_write_mem_callback(void* contents, size_t size, size_t nmemb, void* userp) { size_t realsize = size * nmemb; struct curl_mem_chunk* mem = (struct curl_mem_chunk*)userp; @@ -57,7 +76,7 @@ void get_access_token() { } /* Make a CalDAV request */ -char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields, int depth, bool basicauth, char* content_type, long num_redirects) { +char* caldav_req(struct tm* date, char* url, char* http_method, const char* postfields, int depth, bool basicauth, char* content_type, long num_redirects) { // only support depths 0 or 1 if (depth < 0 || depth > 1) { return NULL; @@ -144,102 +163,6 @@ char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields return caldav_resp.memory; } -/* Return current user principal from CalDAV XML response */ -char* parse_caldav_current_user_principal(char* xml) { - char* xml_key_pos = strstr(xml, "current-user-principal"); - // this XML does not contain a user principal at all - if (xml_key_pos == NULL) { - return NULL; - } - - //<D:current-user-principal> - //<D:href>/caldav/v2/diary.in0rdr%40gmail.com/user</D:href> - char* tok = strtok(xml_key_pos, "<"); // D:current-user-principal> - if (tok != NULL) { - tok = strtok(NULL, "<"); // D:href>/caldav/v2/test%40gmail.com/user - tok = strstr(tok, ">"); // >/caldav/v2/test%40gmail.com/user - tok++; // cut > - //char* tok_end = strrchr(tok, '/'); - //*tok_end = '\0'; // cut /user - } - - return tok; -} - -/* Return home set uri from CalDAV home set XML response */ -char* parse_home_set(const char* xml) { - char* homeset_needle = "calendar-home-set"; - - char* homeset_pos = strstr(xml, homeset_needle); - if (homeset_pos == NULL) { - tracepoint(diary, debug, "parse_home_set() homeset_pos was null"); - return NULL; - } - tracepoint(diary, debug_string, "parse_home_set() homeset_pos", homeset_pos); - - char* href = strrstr(homeset_pos, "href>/"); - if (href != NULL) { - tracepoint(diary, debug_string, "parse_home_set() href", href); - href = strtok(href, "<"); - tracepoint(diary, debug_string, "parse_home_set() href strtok", href); - if (href != NULL) { - href = strchr(href, '>'); - href++; // cut > - } - - // return a copy of the href - char* res = (char*) malloc(strlen(href) * sizeof(char) + 1); - if (res == NULL) { - perror("malloc failed in parse_home_set()"); - return NULL; - } - strcpy(res, href); - - return res; - } - return NULL; -} - -/* Return calendar uri from CalDAV home set XML response */ -char* parse_caldav_calendar(char* xml, char* calendar) { - char displayname_needle[strlen(calendar) + strlen("displayname></") + 1]; - sprintf(displayname_needle, "displayname>%s</", calendar); - - char* displayname_pos = strstr(xml, displayname_needle); - // this XML multistatus response does not contain the users calendar - if (displayname_pos == NULL) { - tracepoint(diary, debug, "parse_caldav_calendar() displayname_pos was null"); - return NULL; - } - tracepoint(diary, debug_string, "parse_caldav_calendar() displayname_pos", displayname_pos); - - // shorten multistatus response and find last hyperlink - *displayname_pos = '\0'; - - tracepoint(diary, debug_string, "parse_caldav_calendar() shortened multistatus response", xml); - char* href = strrstr(xml, "href>/"); - if (href != NULL) { - tracepoint(diary, debug_string, "parse_caldav_calendar() last href", href); - href = strtok(href, "<"); // :href>/caldav/v2/aaa%40group.calendar.google.com/events/ - tracepoint(diary, debug_string, "parse_caldav_calendar() last href strtok", href); - if (href != NULL) { - href = strchr(href, '>'); - href++; // cut > - } - - // return a copy of the href - char* res = (char*) malloc(strlen(href) * sizeof(char) + 1); - if (res == NULL) { - perror("malloc failed in parse_caldav_calendar"); - return NULL; - } - strcpy(res, href); - - return res; - } - return NULL; -} - /* Upload event to CalDAV server */ void put_event(struct tm* date, const char* dir, size_t dir_size, char* calendar_uri, bool basicauth) { // get entry path @@ -362,13 +285,8 @@ int caldav_sync(struct tm* date, pthread_create(&progress_tid, NULL, show_progress, (void*)header); pthread_detach(progress_tid); - char* principal_postfields = "<d:propfind xmlns:d='DAV:' xmlns:cs='http://calendarserver.org/ns/'>" - "<d:prop><d:current-user-principal/></d:prop>" - "</d:propfind>"; - - char* user_principal = caldav_req(date, CONFIG.caldav_server, "PROPFIND", principal_postfields, 0, basicauth_enabled, "application/xml", 1); - tracepoint(diary, debug_string, "User principal", user_principal); + tracepoint(diary, debug_string, "User principal return XML", user_principal); if (user_principal == NULL) { // The principal could not be fetched @@ -377,8 +295,20 @@ int caldav_sync(struct tm* date, return -1; } - char* current_user_principal = parse_caldav_current_user_principal(user_principal); - tracepoint(diary, debug_string, "Current user principal", current_user_principal); + char* current_user_principal = extract_xml_content( + user_principal, + "//*[local-name()='current-user-principal']/*[local-name()='href']", + header, + &progress_tid); + + // free memory allocated by curl request + free(user_principal); + + if (current_user_principal == NULL) { + tracepoint(diary, debug, "Unable to parse current user principal"); + show_info(header, "Unable to parse current user principal, press any key to continue.", &progress_tid); + return -1; + } // extract host without path as basis for further api paths char *caldav_host; char* caldav_host_scheme; @@ -395,46 +325,41 @@ int caldav_sync(struct tm* date, // get the home-set of the user char uri[300]; sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, current_user_principal); - // free memory allocated by curl request - free(user_principal); - - char* homeset_request = "<d:propfind xmlns:d='DAV:' xmlns:c='urn:ietf:params:xml:ns:caldav'>" - " <d:prop>" - " <c:calendar-home-set />" - " </d:prop>" - "</d:propfind>"; char* home_set = caldav_req(date, uri, "PROPFIND", homeset_request, 0, basicauth_enabled, "application/xml", 0); tracepoint(diary, debug_string, "Home set xml", home_set); // parse home set from xml response - char* home_set_parsed = parse_home_set(home_set); - tracepoint(diary, debug_string, "home_set_parsed", home_set_parsed); - free(home_set); + char* home_set_parsed = extract_xml_content( + home_set, + "//*[local-name()='calendar-home-set']/*[local-name()='href']", + header, + &progress_tid); + if (home_set_parsed == NULL) { - show_info(header, "Error while fetching home set in caldav_sync, press any key to continue", &progress_tid); + tracepoint(diary, debug, "Unable to parse home set"); + show_info(header, "Unable to parse home set, press any key to continue.", &progress_tid); return -1; } + sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, home_set_parsed); - free(home_set_parsed); - char* calendar_request = "<d:propfind xmlns:d='DAV:' xmlns:cs='http://calendarserver.org/ns/' xmlns:c='urn:ietf:params:xml:ns:caldav'>" - " <d:prop>" - " <d:resourcetype />" - " <d:displayname />" - " <cs:getctag />" - " <c:supported-calendar-component-set />" - " </d:prop>" - "</d:propfind>"; + free(home_set); + free(home_set_parsed); // get calendar from home-set char* calendar_xml = caldav_req(date, uri, "PROPFIND", calendar_request, 1, basicauth_enabled, "application/xml", 0); tracepoint(diary, debug_string, "Calendar set xml", calendar_xml); // get calendar URI from the home-set - char* calendar_href = parse_caldav_calendar(calendar_xml, CONFIG.caldav_calendar); - free(calendar_xml); - tracepoint(diary, debug_string, "calendar_href", calendar_href); + char calendar_xpath_query[300]; + sprintf(calendar_xpath_query, "//*[local-name()='displayname'][text()='%s']/../../../*[local-name()='href']", CONFIG.caldav_calendar); + tracepoint(diary, debug_string, "Calendar xpath query", calendar_xpath_query); + char* calendar_href = extract_xml_content( + calendar_xml, + calendar_xpath_query, + header, + &progress_tid); if (calendar_href == NULL) { tracepoint(diary, debug_string, "Could not find CalDAV calendar", CONFIG.caldav_calendar); @@ -445,6 +370,8 @@ int caldav_sync(struct tm* date, return -1; } + free(calendar_xml); + char* xml_filter = "<c:calendar-query xmlns:d='DAV:' xmlns:c='urn:ietf:params:xml:ns:caldav'>" "<d:prop><d:getetag/><c:calendar-data/></d:prop>" "<c:filter><c:comp-filter name='VCALENDAR'>" diff --git a/src/utils.c b/src/utils.c @@ -284,6 +284,49 @@ char* extract_ical_field(const char* ics, char* key, long* start_pos, bool multi return buf; } +/* Extract xml node content using XPath expression, xmlsoft.org/examples */ +char* extract_xml_content(const char* xml, const char* xpathExpression, WINDOW* w, pthread_t* p) { + xmlDocPtr doc = NULL; + xmlXPathContextPtr xpathCtx = NULL; + xmlXPathObjectPtr xpathObj = NULL; + + // Macro to check that the libxml version in use is compatible with the + // version the software has been compiled against + LIBXML_TEST_VERSION; + + doc = xmlReadMemory(xml, strlen(xml), NULL, NULL, 0); + if (doc == NULL) { + tracepoint(diary, debug, "Failed to parse xml"); + show_info(w, "Failed to parse xml", p); + return NULL; + } + + xpathCtx = xmlXPathNewContext(doc); + if (xpathCtx == NULL) { + tracepoint(diary, debug, "Failed to create xpath context"); + show_info(w, "Failed to create xpath context", p); + xmlFreeDoc(doc); + return NULL; + } + + xmlChar* xpath = (unsigned char *) xpathExpression; + xpathObj = xmlXPathEvalExpression(xpath, xpathCtx); + if(xpathObj == NULL) { + tracepoint(diary, debug, "Failed to evaluate xpath expression"); + show_info(w, "Failed to evaluate xpath expression", p); + xmlXPathFreeContext(xpathCtx); + xmlFreeDoc(doc); + return NULL; + } + + char* nodeContent = (char *) xmlNodeGetContent(xpathObj->nodesetval->nodeTab[0]); + tracepoint(diary, debug_string, "XML node contents", nodeContent); + + xmlFreeDoc(doc); + + return nodeContent; +} + // Return expanded file path char* expand_path(const char* str) { char* res = ""; diff --git a/src/utils.h b/src/utils.h @@ -11,6 +11,9 @@ #include <wordexp.h> #include <stdbool.h> #include <ncurses.h> +#include <libxml/parser.h> +#include <libxml/xpath.h> +#include <libxml/tree.h> #include "diary-tp.h" @@ -24,6 +27,7 @@ char* extract_json_value(const char* json, char* key, bool quoted); char* fold(const char* str); char* unfold(const char* str); char* extract_ical_field(const char* ical, char* key, long* start_pos, bool multline); +char* extract_xml_content(const char* xml, const char* xpathExpression, WINDOW* w, pthread_t* p); char* expand_path(const char* str); char* strrstr(char *haystack, char *needle); void fpath(const char* dir, size_t dir_size, const struct tm* date, char** rpath, size_t rpath_size);