From e1b22e2b5b944589477889b759029f8fe104a731 Mon Sep 17 00:00:00 2001 From: Michael Rasmussen Date: Fri, 12 May 2017 13:44:20 +0200 Subject: [PATCH 1/1] First complete version --- src/caldav.c | 1022 +++++++++++++++++++++++++++++++++++++ src/caldav.h | 223 ++++++++ src/curllib.c | 360 +++++++++++++ src/curllib.h | 63 +++ src/xml.c | 242 +++++++++ src/xml.h | 42 ++ test-app/Makefile.am | 23 + test-app/cmdline-parser.c | 305 +++++++++++ test-app/cmdline-parser.h | 34 ++ test-app/test-app.c | 42 ++ 10 files changed, 2356 insertions(+) create mode 100644 src/caldav.c create mode 100644 src/caldav.h create mode 100644 src/curllib.c create mode 100644 src/curllib.h create mode 100644 src/xml.c create mode 100644 src/xml.h create mode 100644 test-app/Makefile.am create mode 100644 test-app/cmdline-parser.c create mode 100644 test-app/cmdline-parser.h create mode 100644 test-app/test-app.c diff --git a/src/caldav.c b/src/caldav.c new file mode 100644 index 0000000..c3328e1 --- /dev/null +++ b/src/caldav.c @@ -0,0 +1,1022 @@ +/* + * caldav.c + * + * Copyright 2016 Michael Rasmussen + * + * 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include "caldav.h" +#include "curllib.h" +#include "xml.h" + +static gchar* get_timerange(Runtime* runtime) { + gchar* timerange = NULL; + + if (runtime->start && runtime->finish) { + GDateTime* s_utc = g_date_time_to_utc(runtime->start); + GDateTime* f_utc = g_date_time_to_utc(runtime->finish); + gchar* start = g_date_time_format(s_utc, "%Y%m%dT%H%M%SZ"); + gchar* finish = g_date_time_format(f_utc, "%Y%m%dT%H%M%SZ"); + g_date_time_unref(s_utc); + g_date_time_unref(f_utc); + + timerange = g_strconcat( + "", + NULL); + g_free(start); + g_free(finish); + } + + return timerange; +} + +/* + * Operation Depth RequestType + * CALENDARINFO 0 PROPFIND + * GETOBJECTS 1 REPORT + * CHANGEINFO 1 REPORT + * UPDATEOBJECTS - PUT + * ADDOBJECTS - PUT + * DELETEOBJECTS - DELETE + * DISCOVER 0 PROPFIND + * OPTIONSINFO 0 OPTIONS + * LOCKING 0 +*/ +static Request* request_init(Runtime* r) { + Request* rq = request_new(); + GString* href = g_string_new(""); + gchar* timerange = NULL; + + switch (*r->operation) { + case CALENDARINFO: + rq->request = PROPFIND; + rq->headers = g_slist_append(rq->headers, g_strdup("Depth: 0")); + rq->headers = g_slist_append(rq->headers, g_strdup("Prefer: return-minimal")); + rq->data = g_strconcat( + "", + "", + "", + NULL); + break; + case GETOBJECTS: + rq->request = REPORT; + rq->headers = g_slist_append(rq->headers, g_strdup("Depth: 1")); + rq->headers = g_slist_append(rq->headers, g_strdup("Prefer: return-minimal")); + if (r->hrefs) { + for (GSList* l = r->hrefs; l; l = g_slist_next(l)) { + HrefData* h = (HrefData *) l->data; + gchar* elem = g_strconcat("", h->href, "", NULL); + href = g_string_append(href, elem); + g_free(elem); + } + } + rq->data = g_strconcat( + "", + "", + "", + g_string_free(href, FALSE), "", + NULL); + break; + case GETALLOBJECTS: + rq->request = REPORT; + rq->headers = g_slist_append(rq->headers, g_strdup("Depth: 1")); + rq->headers = g_slist_append(rq->headers, g_strdup("Prefer: return-minimal")); + timerange = get_timerange(r); + rq->data = g_strconcat( + "", + "", + "", + "", + "",timerange ? timerange : "", + "", + "", + NULL); + if (timerange) + g_free(timerange); + break; + case CHANGEINFO: + rq->request = REPORT; + rq->headers = g_slist_append(rq->headers, g_strdup("Depth: 1")); + rq->headers = g_slist_append(rq->headers, g_strdup("Prefer: return-minimal")); + timerange = get_timerange(r); + rq->data = g_strconcat( + "", + "", + "", + "",timerange ? timerange : "", + "", + "", + NULL); + if (timerange) + g_free(timerange); + break; + case UPDATEOBJECTS: + rq->request = PUT; + rq->headers = g_slist_append(rq->headers, g_strconcat("If-Match: \"", r->etag ? r->etag : "", "\"", NULL)); + rq->data = g_strdup(r->component ? r->component : ""); + break; + case ADDOBJECTS: + rq->request = PUT; + rq->headers = g_slist_append(rq->headers, g_strdup("If-None-Match: *")); + rq->data = g_strdup(r->component ? r->component : ""); + break; + case DELETEOBJECTS: + rq->request = DELETE; + rq->headers = g_slist_append(rq->headers, g_strconcat("If-Match: \"", r->etag ? r->etag : "", "\"", NULL)); + break; + case DISCOVER: + rq->request = PROPFIND; + rq->headers = g_slist_append(rq->headers, g_strdup("Depth: 0")); + rq->headers = g_slist_append(rq->headers, g_strdup("Prefer: return-minimal")); + break; + case SIMPLEGET: + rq->request = GET; + rq->headers = g_slist_append(rq->headers, g_strdup("Depth: 0")); + rq->headers = g_slist_append(rq->headers, g_strdup("Prefer: return-minimal")); + break; + case OPTIONSINFO: + rq->request = OPTIONS; + rq->headers = g_slist_append(rq->headers, g_strdup("Depth: 0")); + rq->headers = g_slist_append(rq->headers, g_strdup("Prefer: return-minimal")); + break; + case LOCKING: + rq->request = LOCK; + rq->headers = g_slist_append(rq->headers, g_strdup("Depth: 0")); + rq->headers = g_slist_append(rq->headers, g_strdup("Prefer: return-minimal")); + rq->headers = g_slist_append(rq->headers, g_strdup("Timeout: Second-60")); + rq->data = g_strconcat( + "", + "", + " ", + NULL); + break; + case UNLOCKING: + rq->request = UNLOCK; + rq->headers = g_slist_append(rq->headers, g_strdup("Depth: 0")); + rq->headers = g_slist_append(rq->headers, g_strdup("Prefer: return-minimal")); + break; + case FREEBUSY: + rq->request = REPORT; + rq->headers = g_slist_append(rq->headers, g_strdup("Depth: 1")); + rq->headers = g_slist_append(rq->headers, g_strdup("Prefer: return-minimal")); + timerange = get_timerange(r); + rq->data = g_strconcat( + "", + "", + timerange ? timerange : "", "", + NULL); + if (timerange) + g_free(timerange); + break; + default: + request_free(rq); + rq = NULL; + } + + if (rq) { + rq->headers = g_slist_append(rq->headers, g_strdup("Content-Type: application/xml; charset=utf-8")); + rq->username = g_strdup(r->username); + rq->password = g_strdup(r->password); + rq->url = g_strdup(r->url); + } + + return rq; +} + +static gchar* get_host_part(const gchar* url) { + gchar* host_part = NULL; + + gchar* s = g_strstr_len(url, -1, "://"); + if (!s) { + s = (gchar *) url; + } else { + s += 3; + } + + gchar* s1 = g_strstr_len(s, -1, "/"); + + if (s1) { + host_part = g_strndup(url, s1 - url); + } else { + host_part = g_strdup(url); + } + + return host_part; +} + +static GSList* gchar_list_copy(GSList* list) { + GSList* headers = NULL; + for (GSList* l = list; l; l = g_slist_next(l)) { + gchar* h = g_strdup((gchar *) l->data); + headers = g_slist_append(headers, h); + } + + return headers; +} + +static void debug_request(Request* rq) { + printf("HTTP Headers:\n"); + for (GSList* l = rq->headers; l; l = g_slist_next(l)) { + printf("%s\n", (gchar *) l->data); + } + switch (rq->request) { + case DELETE: printf("DELETE "); break; + case PROPFIND: printf("PROPFIND "); break; + case PUT: printf("PUT "); break; + case REPORT: printf("REPORT "); break; + case GET: printf("GET "); break; + case OPTIONS: printf("OPTIONS "); break; + case LOCK: printf("LOCK "); break; + case UNLOCK: printf("UNLOCK "); break; + } + printf("%s\n", rq->url); + printf("DATA:\n%s\n", rq->data ? rq->data : "None"); +} + +static void debug_response(Response* r) { + printf("HTTP Status: %i\nHTTP headers:\n", r->status); + for (GSList* l = r->headers; l; l = g_slist_next(l)) { + printf("%s\n", (gchar *) l->data); + } +} + +static guint execute_options(gpointer ptr); +static void caldav_response_free(Operation op, CaldavResponse* cr); + +static gchar* execute_locking(Runtime* runtime, const gchar* lock_token, guint* status) { + gchar* token = NULL; + Request* rq = NULL; + Response* r = NULL; + + Runtime* tmp = runtime_copy(runtime); + if (!tmp->options) { + *tmp->operation = OPTIONSINFO; + guint status = execute_options((gpointer) tmp); + if (status != 200) + return NULL; + caldav_response_free(*tmp->operation, tmp->output); + runtime->options = slist_copy_gchar(tmp->options); + } + + if (tmp->options && has_option(tmp, "lock")) { + if (lock_token) { + *tmp->operation = UNLOCKING; + rq = request_init(tmp); + rq->headers = g_slist_append(rq->headers, + g_strconcat("Lock-Token: ", lock_token, NULL)); + if (rq) { + if (runtime->debug) { + debug_request(rq); + } + r = request_send(rq, runtime->debug); + + if (runtime->debug) { + debug_response(r); + } + } + } else { + *tmp->operation = LOCKING; + rq = request_init(tmp); + if (rq) { + if (runtime->debug) { + debug_request(rq); + } + r = request_send(rq, runtime->debug); + + if (runtime->debug) { + debug_response(r); + } + + if (r->status != 423) + token = find_header(r->headers, "Lock-Token"); + else + token = g_strdup(r->data); + } + } + } + + if (status) + *status = r->status; + + response_free(r); + request_free(rq); + runtime_free(tmp); + + return token; +} + +static guint execute_default(gpointer ptr) { + Runtime* runtime = (Runtime *) ptr; + Request* rq = request_init(runtime); + guint status = 500; + gchar* lock_token = NULL; + + if (rq) { + if (runtime->debug) { + debug_request(rq); + } + + if (*runtime->operation == DELETEOBJECTS) { + lock_token = execute_locking(runtime, NULL, &status); + if (status == 423) { + runtime->output = g_new0(CaldavResponse, 1); + runtime->output->data = (gpointer) g_strdup(lock_token); + runtime->output->status = status; + g_free(lock_token); + request_free(rq); + return status; + } + } + if (lock_token) { + rq->headers = g_slist_append(rq->headers, + g_strconcat("Lock-Token: ", lock_token, NULL)); + } + Response* r = request_send(rq, runtime->debug); + if (lock_token) { + execute_locking(runtime, lock_token, &status); + g_free(lock_token); + lock_token = NULL; + } + request_free(rq); + + if (runtime->debug) { + debug_response(r); + } + + runtime->output = g_new0(CaldavResponse, 1); + runtime->output->data = (gpointer) g_strdup(r->data); + + if (runtime->file) + fprintf(runtime->file, r->data); + + runtime->output->status = status = r->status; + + runtime->output->headers = gchar_list_copy(r->headers); + + response_free(r); + } + + return status; +} + +static guint execute_options(gpointer ptr) { + Runtime* runtime = (Runtime *) ptr; + Request* rq = request_init(runtime); + guint status = 500; + + if (rq) { + if (runtime->debug) { + debug_request(rq); + } + Response* r = request_send(rq, runtime->debug); + request_free(rq); + + if (runtime->debug) { + debug_response(r); + } + + gchar* headers = find_header(r->headers, "Allow"); + if (headers) { + gchar** options = g_strsplit(headers, ",", 0); + for (int i = 0; options[i]; i++) { + gchar* tmp = g_strdup(options[i]); + tmp = g_strstrip(tmp); + runtime->options = g_slist_append(runtime->options, g_strdup(tmp)); + g_free(tmp); + } + g_strfreev(options); + g_free(headers); + } + + if (runtime->file) { + for (GSList* l = runtime->options; l; l = g_slist_next(l)) { + fprintf(runtime->file, "%s\n", (gchar *) l->data); + } + } + + runtime->output = g_new0(CaldavResponse, 1); + runtime->output->status = status = r->status; + + runtime->output->headers = gchar_list_copy(r->headers); + + response_free(r); + } + + return status; +} + +static guint execute_freebusy(gpointer ptr) { + Runtime* runtime = (Runtime *) ptr; + + Request* rq = request_init(runtime); + guint status = 500; + + if (rq) { + if (runtime->debug) { + debug_request(rq); + } + Response* r = request_send(rq, runtime->debug); + request_free(rq); + + if (runtime->debug) { + debug_response(r); + } + + runtime->output = g_new0(CaldavResponse, 1); + runtime->output->data = (gpointer) g_strdup(r->data); + + if (runtime->file) + fprintf(runtime->file, r->data); + + runtime->output->status = status = r->status; + + runtime->output->headers = gchar_list_copy(r->headers); + + response_free(r); + } + + return status; +} + +static guint execute_discover(gpointer ptr) { + Runtime* runtime = (Runtime *) ptr; + Response* r = NULL; + Request* rq = NULL; + GSList* list = NULL; + guint status = 500; + + rq = request_init(runtime); + if (rq) { + rq->data = g_strconcat( + "", + "", + "", + NULL); + if (runtime->debug) { + debug_request(rq); + } + r = request_send(rq, runtime->debug); + + if (runtime->debug) { + debug_response(r); + } + list = find_element("//d:current-user-principal/*", r->data, "d", "DAV:"); + response_free(r); + if (list) { + /* Change URL to where "current-user-principal" is located */ + gchar* s = get_host_part(runtime->url); + g_free(runtime->url); + runtime->url = g_strconcat(s, list->data, NULL); + g_free(s); + slist_free_gchar(list); + list = NULL; + } + g_free(rq->data); + rq->data = g_strconcat( + "", + "", + "", + NULL); + r = request_send(rq, runtime->debug); + list = find_element("//c:calendar-home-set/*", r->data, + "c", "urn:ietf:params:xml:ns:caldav"); + response_free(r); + if (list) { + /* Change URL to where "calendar-home-set" is located */ + gchar* s = get_host_part(runtime->url); + g_free(runtime->url); + runtime->url = g_strconcat(s, list->data, NULL); + g_free(s); + slist_free_gchar(list); + list = NULL; + } + g_free(rq->data); + rq->data = g_strconcat( + "", + "", + "", + "", + NULL); + for (GSList* l = rq->headers; l; l = g_slist_next(l)) { + if (g_strcmp0(l->data, "Depth: 0") == 0) { + g_free(l->data); + l->data = g_strdup("Depth: 1"); + } + + } + r = request_send(rq, runtime->debug); + gchar* s = get_host_part(runtime->url); + list = find_calendars(s, r->data); + g_free(s); + + runtime->output = g_new0(CaldavResponse, 1); + GSList* data = NULL; + for (GSList* l = list; l; l = g_slist_next(l)) { + Calendar* c = calendar_copy((Calendar *) l->data); + data = g_slist_append(data, c); + } + runtime->output->data = (gpointer) data; + + if (runtime->file) { + for (GSList* l = list; l; l = g_slist_next(l)) { + calendar_dump(runtime->file, (Calendar *) l->data); + } + } + g_slist_foreach(list, (GFunc)calendar_free, NULL); + g_slist_free(list); + list = NULL; + + runtime->output->headers = gchar_list_copy(r->headers); + + runtime->output->status = status = r->status; + + response_free(r); + request_free(rq); + } + + return status; +} + +static guint execute_add_update(gpointer ptr) { + Runtime* runtime = (Runtime *) ptr; + Request* rq = request_init(runtime); + guint status = 500; + gchar* lock_token = NULL; + + if (rq) { + if (runtime->debug) { + debug_request(rq); + } + + if (*runtime->operation == UPDATEOBJECTS) { + lock_token = execute_locking(runtime, NULL, &status); + if (status == 423) { + runtime->output = g_new0(CaldavResponse, 1); + runtime->output->data = (gpointer) g_strdup(lock_token); + runtime->output->status = status; + g_free(lock_token); + request_free(rq); + return status; + } + } + if (lock_token) { + rq->headers = g_slist_append(rq->headers, + g_strconcat("Lock-Token: ", lock_token, NULL)); + } + Response* r = request_send(rq, runtime->debug); + if (lock_token) { + execute_locking(runtime, lock_token, &status); + g_free(lock_token); + lock_token = NULL; + } + request_free(rq); + + if (runtime->debug) { + debug_response(r); + } + + runtime->output = g_new0(CaldavResponse, 1); + if (r->status == 201 || r->status == 204) { + gchar* etag = find_header(r->headers, "ETag"); + if (etag == NULL) { + Runtime* r_new = runtime_copy(runtime); + *r_new->operation = SIMPLEGET; + guint s = execute(r_new); + if (s == 200 && r_new->output && r_new->output->headers) { + etag = find_header(r_new->output->headers, "ETag"); + } + runtime_free(r_new); + } + runtime->output->data = (gpointer) g_strdup(etag); + if (runtime->file) { + if (etag) + fprintf(runtime->file, "ETag: %s\n", etag); + else + fprintf(runtime->file, "%s\n", "No ETag returned"); + } + g_free(etag); + } else { + runtime->output->data = (gpointer) g_strdup(r->data); + if (runtime->file) + fprintf(runtime->file, "%s\n", r->data); + } + + runtime->output->status = status = r->status; + + runtime->output->headers = gchar_list_copy(r->headers); + + response_free(r); + } + + return status; +} + +static void caldav_response_free(Operation op, CaldavResponse* cr) { + GSList* list = NULL; + + if (cr) { + if (cr->headers) { + slist_free_gchar(cr->headers); + cr->headers = NULL; + } + if (cr->data) { + switch (op) { + case DISCOVER: + list = (GSList *) cr->data; + g_slist_foreach(list, (GFunc)calendar_free, NULL); + g_slist_free(list); + break; + case OPTIONSINFO: + list = (GSList *) cr->data; + slist_free_gchar(list); + break; + default: + g_free((gchar *)cr->data); + } + cr->data = NULL; + } + } +} + +gchar* find_header(GSList* list, const gchar* header) { + g_return_val_if_fail(header != NULL, NULL); + + GString* hit = NULL; + + for (GSList* l = list; l; l = g_slist_next(l)) { + gchar* s = (gchar *) l->data; + gchar** t = g_strsplit(s, ":", 2); + for (int i = 0; t[i]; i++) { + gchar* s1 = g_utf8_casefold(g_strstrip(t[i]), -1); + gchar* s2 = g_utf8_casefold(header, -1); + if (g_strcmp0(s1, s2) == 0) { + if (hit) { + hit = g_string_append(hit, ", "); + hit = g_string_append(hit, g_strstrip(t[i+1])); + } else { + hit = g_string_new(g_strstrip(t[i+1])); + } + } + g_free(s1); + g_free(s2); + } + } + + return (hit) ? g_string_free(hit, FALSE) : NULL; +} + +void slist_free_gchar(GSList* list) { + if (list) { + g_slist_foreach(list, (GFunc)g_free, NULL); + g_slist_free(list); + list = NULL; + } +} + +Runtime* runtime_new() { + Runtime* r = g_new0(Runtime, 1); + r->default_executor = TRUE; + + return r; +} + +Calendar* calendar_copy(Calendar* cal) { + Calendar* new_cal = NULL; + + if (!cal) + return NULL; + + new_cal = g_new0(Calendar, 1); + new_cal->displayname = g_strdup(cal->displayname); + new_cal->url = g_strdup(cal->url); + new_cal->ctag = g_strdup(cal->ctag); + new_cal->components = g_slist_copy(cal->components); + + return new_cal; +} + +void calendar_dump(FILE* file, Calendar* cal) { + g_return_if_fail(file && cal); + + fprintf(file, "------- Display Name: %s -------\n", cal->displayname); + fprintf(file, "URL: %s\n", cal->url); + fprintf(file, "CTAG: %s\n", cal->ctag); + fprintf(file, "Supported components\n"); + for (GSList* l = cal->components; l; l = g_slist_next(l)) { + Component c = GPOINTER_TO_UINT(l->data); + gchar* value = component_to_string(c); + fprintf(file, "\t%s\n", value); + g_free(value); + } +} + +void calendar_free(Calendar* cal) { + if (cal) { + if (cal->url) { + g_free(cal->url); + cal->url = NULL; + } + if (cal->ctag) { + g_free(cal->ctag); + cal->ctag = NULL; + } + if (cal->displayname) { + g_free(cal->displayname); + cal->displayname = NULL; + } + if (cal->components) { + g_slist_free(cal->components); + cal->components = NULL; + } + g_free(cal); + cal = NULL; + } +} + +void runtime_free(Runtime* runtime) { + if (runtime) { + if (runtime->username) { + g_free(runtime->username); + runtime->username = NULL; + } + if (runtime->password) { + g_free(runtime->password); + runtime->password = NULL; + } + if (runtime->url) { + g_free(runtime->url); + runtime->url = NULL; + } + if (runtime->etag) { + g_free(runtime->etag); + runtime->etag = NULL; + } + if (runtime->component) { + g_free(runtime->component); + runtime->component = NULL; + } + if (runtime->hrefs) { + slist_free_gchar(runtime->hrefs); + runtime->hrefs = NULL; + } + if (runtime->options) { + slist_free_gchar(runtime->options); + runtime->options = NULL; + } + if (runtime->output && runtime->default_executor) { + caldav_response_free(*runtime->operation, runtime->output); + runtime->output = NULL; + } + if (runtime->operation) { + g_free(runtime->operation); + runtime->operation = NULL; + } + if (runtime->executor) { + runtime->executor = NULL; + } + if (runtime->start) { + g_date_time_unref(runtime->start); + runtime->start = NULL; + } + if (runtime->finish) { + g_date_time_unref(runtime->finish); + runtime->finish = NULL; + } + g_free(runtime); + runtime = NULL; + } +} + +guint execute(Runtime* runtime) { + g_return_val_if_fail(runtime && runtime->operation, 500); + + switch (*runtime->operation) { + case DISCOVER: + runtime->executor = execute_discover; + break; + case ADDOBJECTS: + case UPDATEOBJECTS: + runtime->executor = execute_add_update; + break; + case OPTIONSINFO: + runtime->executor = execute_options; + break; + case FREEBUSY: + runtime->executor = execute_freebusy; + break; + default: + runtime->executor = execute_default; + } + + return runtime->executor(runtime); +} + +gboolean is_component(const gchar* value, Component component) { + g_return_val_if_fail(value != NULL, FALSE); + + if (g_strcmp0(value, "VEVENT") == 0 && component == VEVENT) + return TRUE; + else if (g_strcmp0(value, "VTODO") == 0 && component == VTODO) + return TRUE; + else if (g_strcmp0(value, "VFREEBUSY") == 0 && component == VFREEBUSY) + return TRUE; + else if (g_strcmp0(value, "VJOURNAL") == 0 && component == VJOURNAL) + return TRUE; + else + return FALSE; +} + +gchar* component_to_string(Component component) { + if (component == VEVENT) + return g_strdup("VEVENT"); + else if (component == VTODO) + return g_strdup("VTODO"); + else if (component == VFREEBUSY) + return g_strdup("VFREEBUSY"); + else if (component == VJOURNAL) + return g_strdup("VJOURNAL"); + else + return NULL; +} + +Component string_to_component(const gchar* value) { + g_return_val_if_fail(value != NULL, VUNKNOWN); + + if (g_strcmp0(value, "VEVENT") == 0) + return VEVENT; + else if (g_strcmp0(value, "VTODO") == 0) + return VTODO; + else if (g_strcmp0(value, "VFREEBUSY") == 0) + return VFREEBUSY; + else if (g_strcmp0(value, "VJOURNAL") == 0) + return VJOURNAL; + else + return VUNKNOWN; +} + +gchar* status_str(Runtime* runtime) { + gchar* str = NULL; + + g_return_val_if_fail(runtime && runtime->output, NULL); + + switch (runtime->output->status) { + case 200: str = g_strdup("OK"); break; + case 201: str = g_strdup("Created"); break; + case 204: str = g_strdup("No Content"); break; + case 207: str = g_strdup("Multi-Status"); break; + case 400: str = g_strdup("Bad Request"); break; + case 401: str = g_strdup("Unauthorized"); break; + case 403: str = g_strdup("Forbidden"); break; + case 404: str = g_strdup("Not Found"); break; + case 405: str = g_strdup("Method Not Allowed"); break; + case 406: str = g_strdup("Not Acceptable"); break; + case 409: str = g_strdup("Conflict"); break; + case 410: str = g_strdup("Gone"); break; + case 412: str = g_strdup("Precondition Failed"); break; + case 415: str = g_strdup("Unsupported Media Type"); break; + case 423: str = g_strdup("Locked"); break; + case 424: str = g_strdup("Failed Dependency"); break; + case 500: str = g_strdup("Internal Server Error"); break; + case 501: str = g_strdup("Not Implemented"); break; + case 503: str = g_strdup("Service Unavailable"); break; + case 507: str = g_strdup("Insufficient Storage"); break; + default: str = g_strdup_printf("%d: Unexpected status", runtime->output->status); + } + + return str; +} + +HrefData* href_data_new(const gchar* href, const gchar* data) { + g_return_val_if_fail(href != NULL, NULL); + + HrefData* h = g_new0(HrefData, 1); + h->href = g_strdup(href); + h->data = data ? g_strdup(data) : NULL; + + return h; +} + +void href_data_free(HrefData* href_data) { + if (href_data) { + if (href_data->href) { + g_free(href_data->href); + href_data->href = NULL; + } + if (href_data->data) { + g_free(href_data->data); + href_data->data = NULL; + } + g_free(href_data); + href_data = NULL; + } +} + +gboolean has_option(Runtime* runtime, const gchar* option) { + g_return_val_if_fail(runtime && option, FALSE); + gboolean has = FALSE; + + gchar* s2 = g_utf8_casefold(option, -1); + if (runtime->options) { + for (GSList* l = runtime->options; l && ! has; l = g_slist_next(l)) { + gchar* s1 = g_utf8_casefold((gchar *) l->data, -1); + if (g_strcmp0(s1, s2) == 0) { + has = TRUE; + } + g_free(s1); + } + } + g_free(s2); + + return has; +} + +Runtime* runtime_copy(Runtime* runtime) { + Runtime* new = NULL; + + g_return_val_if_fail(runtime != NULL, NULL); + + new = runtime_new(); + new->username = g_strdup(runtime->username); + new->password = g_strdup(runtime->password); + new->url = g_strdup(runtime->url); + new->debug = runtime->debug; + new->operation = g_new0(Operation, 1); + *new->operation = *runtime->operation; + new->executor = runtime->executor; + new->default_executor = runtime->default_executor; + new->etag = g_strdup(runtime->etag); + new->component = g_strdup(runtime->component); + new->hrefs = slist_copy_gchar(runtime->hrefs); + new->options = slist_copy_gchar(runtime->options); + + return new; +} + +GSList* slist_copy_gchar(GSList* list) { + GSList* new = NULL; + + for (GSList* l = list; l; l = g_slist_next(l)) { + new = g_slist_append(new, g_strdup(l->data)); + } + + return new; +} + +GDateTime* get_date_time_from_string(const gchar* datetime) { + GDateTime* dt = NULL; + g_return_val_if_fail(datetime != NULL, NULL); + + gchar** dt_part = g_strsplit(datetime, "T", 0); + if (!dt_part || dt_part[2] != NULL) { + g_strfreev(dt_part); + return NULL; + } + + gchar** d_part = g_strsplit(dt_part[0], "-", 0); + if (!d_part || d_part[3] != NULL) { + g_strfreev(dt_part); + g_strfreev(d_part); + return NULL; + } + + gchar** t_part = g_strsplit(dt_part[1], ":", 0); + if (!t_part || t_part[3] != NULL) { + g_strfreev(dt_part); + g_strfreev(t_part); + return NULL; + } + + g_strfreev(dt_part); + char* endptr; + dt = g_date_time_new_local( + atoi(d_part[0]), atoi(d_part[1]), atoi(d_part[2]), + atoi(t_part[0]), atoi(t_part[1]), strtod(t_part[2], &endptr)); + + g_strfreev(d_part); + g_strfreev(t_part); + + return dt; +} diff --git a/src/caldav.h b/src/caldav.h new file mode 100644 index 0000000..e6af5cd --- /dev/null +++ b/src/caldav.h @@ -0,0 +1,223 @@ +/* + * caldav.h + * + * Copyright 2016 Michael Rasmussen + * + * 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 3 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#ifndef __CALDAV_H__ +#define __CALDAV_H__ + +#include + +G_BEGIN_DECLS + +typedef enum { + VUNKNOWN, + VEVENT, + VTODO, + VFREEBUSY, + VJOURNAL, +} Component; + +typedef struct { + guint status; /* Status code from the request */ + GSList* headers; /* List of headers in the response */ + gpointer data; /* Either gchar* or GSList* (Calendar) or GSList* (options). */ + /* If adding or updating a component a successful */ + /* request will return the ETag in data */ +} CaldavResponse; + +typedef enum { + UNKNOWN, + ADDOBJECTS, + CALENDARINFO, + CHANGEINFO, + DELETEOBJECTS, + DISCOVER, + GETALLOBJECTS, + GETOBJECTS, + LOCKING, + UNLOCKING, + OPTIONSINFO, + SIMPLEGET, + UPDATEOBJECTS, + FREEBUSY, +} Operation; + +typedef struct { + gchar* href; + gchar* data; +} HrefData; + +typedef guint (*Executor)(gpointer runtime); +typedef struct { + FILE* file; /* Where to write output. If file is NULL only output will be in 'output' */ + gchar* username; /* Account to use */ + gchar* password; /* Password for the account */ + gchar* url; /* URL to caldav server */ + gboolean debug; /* Show debug */ + Operation* operation; /* Which operation to execute */ + CaldavResponse* output; /* Structure containing the response */ + Executor executor; /* Function which executes the request */ + gboolean default_executor; /* Using default executors or custom executors */ + gchar* etag; /* For If-Match */ + gchar* component; /* The data for UPDATEOBJECTS or ADDOBJECTS */ + GSList* hrefs; /* For GETOBJECTS: list of HrefData */ + GSList* options; /* List of supported options by this caldav server */ + GDateTime* start; /* Start time interval in ISO 8601 local time. */ + /* 2017-01-13T13:13:13. For freebusy or get operations */ + GDateTime* finish; /* End time interval in ISO 8601 local time. */ + /* 2017-01-13T13:13:13. For freebusy or get operations */ +} Runtime; + +typedef struct { + gchar* url; + gchar* displayname; + gchar* ctag; + GSList* components; /* list of Components */ +} Calendar; + +/** + * Return a new Runtime structure all zeroed out. + * @return NULL or a new Runtime + */ +Runtime* runtime_new(); + +/** + * Free a previously created Runtime structure. + * @param runtime + */ +void runtime_free(Runtime* runtime); + +/** + * Make a deep copy of a Runtime. + * @param runtime + * @return Runtime + */ +Runtime* runtime_copy(Runtime* runtime); + +/** + * Execute the Runtime + * @param runtime + * @return guint + */ +guint execute(Runtime* runtime); + +/** + * Make a deep copy of a Calendar structure + * @param cal + * @return a deep copy of the Calendar + */ +Calendar* calendar_copy(Calendar* cal); + +/** + * Free previously created Calendar. + * @param cal + */ +void calendar_free(Calendar* cal); + +/** + * Dump Calendar to FILE. + * @param file + * @param cal + */ +void calendar_dump(FILE* file, Calendar* cal); + +/** + * Test whether Component is equal to Component + * @param value + * @param component + * @return TRUE or FALSE + */ +gboolean is_component(const gchar* value, Component component); + +/** + * Convert Component to its string representation + * @param component + * @return String + */ +gchar* component_to_string(Component component); + +/** + * Convert a String representation of a component to Component. + * @param value + * @return Component + */ +Component string_to_component(const gchar* value); + +/** + * Get error as String from Runtime. + * @param runtime + * @return String + */ +gchar* status_str(Runtime* runtime); + +/** + * Deep copy of a GSList containing Strings to another GSList. + * @param list + * @return GSList + */ +GSList* slist_copy_gchar(GSList* list); + +/** + * Free a GSList of Strings. + * @param list + */ +void slist_free_gchar(GSList* list); + +/** + * Create a new HrefData structure. + * @param href + * @param data + * @return HrefData + */ +HrefData* href_data_new(const gchar* href, const gchar* data); + +/** + * Free a HrefData structure. + * @param href_data + */ +void href_data_free(HrefData* href_data); + +/** + * Find HTTP header in GSList of HTTP headers. + * @param list + * @param header + * @return String + */ +gchar* find_header(GSList* list, const gchar* header); + +/** + * Test whether caldav server supports option. + * @param runtime + * @param option + * @return TRUE or FALSE + */ +gboolean has_option(Runtime* runtime, const gchar* option); + +/** + * Convert a string representation of an ISO 8601 date to GDateTime. + * @param datetime + * @return GDateTime + */ +GDateTime* get_date_time_from_string(const gchar* datetime); + +G_END_DECLS + +#endif + diff --git a/src/curllib.c b/src/curllib.c new file mode 100644 index 0000000..985bf53 --- /dev/null +++ b/src/curllib.c @@ -0,0 +1,360 @@ +/* + * curllib.c + * + * Copyright 2016 Michael Rasmussen + * + * 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include +#include +#include +#include "config.h" +#include "curllib.h" +#include "caldav.h" +#include "xml.h" + +static gboolean curl_not_initialized = TRUE; +static struct Data config; + +struct MemoryStruct { + char * memory; + size_t size; +}; + +struct Data { + char trace_ascii; /* 1 or 0 */ +}; + +/** + * This function is burrowed from the libcurl documentation + * @param text + * @param stream + * @param ptr + * @param size + * @param nohex + */ +static void dump(const char* text, FILE* stream, char* ptr, size_t size, char nohex) { + size_t i; + size_t c; + + unsigned int width=0x10; + + if(nohex) + /* without the hex output, we can fit more on screen */ + width = 0x40; + fprintf(stream, "%s, %zd bytes (0x%zx)\n", text, size, size); + for(i=0; i=0x20) && (ptr[i+c]<0x80)?ptr[i+c]:'.'); + /* check again for 0D0A, to avoid an extra \n if it's at width */ + if (nohex && (i+c+2 < size) && ptr[i+c+1]==0x0D && ptr[i+c+2]==0x0A) { + i+=(c+3-width); + break; + } + } + fputc('\n', stream); /* newline */ + } + fflush(stream); +} + +/** + * This function is burrowed from the libcurl documentation + * @param handle + * @param type + * @param data + * @param size + * @param userp + * @return + */ +static int my_trace(CURL* handle, curl_infotype type, char* data, size_t size, void* userp) { + struct Data* config = (struct Data *)userp; + const char* text; + (void)handle; /* prevent compiler warning */ + + switch (type) { + case CURLINFO_TEXT: + fprintf(stderr, "== Info: %s", data); + default: /* in case a new one is introduced to shock us */ + return 0; + case CURLINFO_HEADER_OUT: + text = "=> Send header"; + break; + case CURLINFO_DATA_OUT: + text = "=> Send data"; + break; + case CURLINFO_SSL_DATA_OUT: + text = "=> Send SSL data"; + break; + case CURLINFO_HEADER_IN: + text = "<= Recv header"; + break; + case CURLINFO_DATA_IN: + text = "<= Recv data"; + break; + case CURLINFO_SSL_DATA_IN: + text = "<= Recv SSL data"; + break; + } + dump(text, stderr, data, size, config->trace_ascii); + return 0; +} + +/** + * This function is burrowed from the libcurl documentation + * @param ptr + * @param size + * @return void* to memory region + */ +static void* myrealloc(void* ptr, size_t size) { +/* There might be a realloc() out there that doesn't like reallocing + * NULL pointers, so we take care of it here + * */ + if(ptr) + return realloc(ptr, size); + else + return malloc(size); +} + +/** + * This function is burrowed from the libcurl documentation + * @param ptr + * @param size + * @param nmemb + * @param data + * @return number of written bytes + */ +static size_t WriteMemoryCallback(void* ptr, size_t size, size_t nmemb, void* data) { + size_t realsize = size * nmemb; + struct MemoryStruct* mem = (struct MemoryStruct *)data; + mem->memory = (char *)myrealloc(mem->memory, mem->size + realsize + 1); + + if (mem->memory) { + memcpy(&(mem->memory[mem->size]), ptr, realsize); + mem->size += realsize; + mem->memory[mem->size] = 0; + } + return realsize; +} + +static struct curl_slist* gslist_to_slist(GSList* list) { + struct curl_slist* s = NULL; + + for (GSList* elem = list; elem; elem = g_slist_next(elem)) { + s = curl_slist_append(s, elem->data); + } + + return s; +} + +static void curl_set_request(CURL* curl, RequestType type) { + gchar* req = NULL; + + switch (type) { + case DELETE: req = g_strdup("DELETE"); break; + case PROPFIND: req = g_strdup("PROPFIND"); break; + case PUT: req = g_strdup("PUT"); break; + case REPORT: req = g_strdup("REPORT"); break; + case GET: req = g_strdup("GET"); break; + case OPTIONS: req = g_strdup("OPTIONS"); break; + case LOCK: req = g_strdup("LOCK"); break; + case UNLOCK: req = g_strdup("UNLOCK"); break; + } + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req); + g_free(req); +} + +static CURL* get_curl(Request* request, gboolean debug) { + CURL* curl; + gchar* userpwd; + + curl = curl_easy_init(); + if (curl) { + if (request->password) { + userpwd = g_strconcat(request->username, ":", request->password, NULL); + curl_easy_setopt(curl, CURLOPT_USERPWD, userpwd); + } else { + userpwd = g_strconcat(request->username, NULL); + curl_easy_setopt(curl, CURLOPT_USERPWD, userpwd); + } + g_free(userpwd); + curl_set_request(curl, request->request); + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + curl_easy_setopt(curl, CURLOPT_USERAGENT, PACKAGE_STRING); + curl_easy_setopt(curl, CURLOPT_URL, request->url); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, WriteMemoryCallback); + if (debug) { + config.trace_ascii = 1; + curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, my_trace); + curl_easy_setopt(curl, CURLOPT_DEBUGDATA, &config); + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); + } + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(curl, CURLOPT_UNRESTRICTED_AUTH, 1); + curl_easy_setopt(curl, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); + if (request->request != DELETE && request->request != GET && request->request != OPTIONS) { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request->data); + curl_easy_setopt (curl, CURLOPT_POSTFIELDSIZE, + (request->data) ? strlen(request->data) : 0); + } + } + + return curl; +} + +static GSList* memory_struct_to_gslist(struct MemoryStruct buffer) { + GSList* list = NULL; + gchar **elems, **e; + + if (buffer.size > 0) { + elems = g_strsplit(buffer.memory, "\n", 0); + for (e = elems; *e != NULL; ++e) { + if (g_strcmp0(*e, "\r") != 0 || g_strcmp0(*e, "") != 0) + list = g_slist_prepend(list, g_strdup(*e)); + } + g_strfreev(elems); + } + + return list; +} + +Request* request_new() { + Request* r = g_new0(Request, 1); + + return r; +} + + +void request_free(Request* request) { + if (request) { + if (request->headers) { + slist_free_gchar(request->headers); + request->headers = NULL; + } + if (request->data) { + g_free(request->data); + request->data = NULL; + } + if (request->username) { + g_free(request->username); + request->username = NULL; + } + if (request->password) { + g_free(request->password); + request->password = NULL; + } + if (request->url) { + g_free(request->url); + request->url = NULL; + } + g_free(request); + request = NULL; + } +} + +Response* request_send(Request* request, gboolean debug) { + g_return_val_if_fail(request != NULL, NULL); + + Response* r = g_new0(Response, 1); + CURLcode curlres = 0; + struct MemoryStruct data; + struct MemoryStruct headers; + char error_buf[CURL_ERROR_SIZE]; + struct curl_slist* http_headers; + + data.memory = NULL; + data.size = 0; + headers.memory = NULL; + headers.size = 0; + + if (curl_not_initialized) { + curl_global_init(CURL_GLOBAL_ALL); + curl_not_initialized = FALSE; + } + + CURL* curl = get_curl(request, debug); + if (curl) { + http_headers = gslist_to_slist(request->headers); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, http_headers); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&data); + curl_easy_setopt(curl, CURLOPT_WRITEHEADER, (void *)&headers); + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, (char *) &error_buf); + curlres = curl_easy_perform(curl); + if (curlres != CURLE_OK) { + request->data = g_strdup(error_buf); + } + else { + curlres = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &r->status); + r->headers = memory_struct_to_gslist(headers); + r->data = g_strndup(data.memory, data.size); + if (r->status == 207 && (request->request == DELETE || request->request == PUT)) { + /* Error code is returnded inside XML response */ + GSList* response = find_element("//d:status", r->data, "d", "DAV:"); + if (response) { + if (g_strstr_len((gchar *) response->data, -1, "423")) { + r->status = 423; + } + } + slist_free_gchar(response); + } + } + if (data.memory) + free(data.memory); + if (headers.memory) + free(headers.memory); + curl_slist_free_all(http_headers); + curl_easy_cleanup(curl); + } + + return r; +} + +void response_free(Response* response) { + if (response) { + if (response->headers) { + slist_free_gchar(response->headers); + response->headers = NULL; + } + if (response->data) { + g_free(response->data); + response->data = NULL; + } + g_free(response); + response = NULL; + } +} diff --git a/src/curllib.h b/src/curllib.h new file mode 100644 index 0000000..7066187 --- /dev/null +++ b/src/curllib.h @@ -0,0 +1,63 @@ +/* + * curllib.h + * + * Copyright 2016 Michael Rasmussen + * + * 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 3 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#ifndef __CURLLIB_H__ +#define __CURLLIB_H__ + +#include + +G_BEGIN_DECLS + +typedef enum { + DELETE, + GET, + PROPFIND, + PUT, + REPORT, + OPTIONS, + LOCK, + UNLOCK, +} RequestType; + +typedef struct { + RequestType request; + GSList* headers; + gchar* data; + gchar* username; + gchar* password; + gchar* url; +} Request; + +typedef struct { + gint status; + GSList* headers; + gchar* data; +} Response; + +Request* request_new(); +void request_free(Request* request); +Response* request_send(Request* request, gboolean debug); +void response_free(Response* response); + +G_END_DECLS + +#endif + diff --git a/src/xml.c b/src/xml.c new file mode 100644 index 0000000..fe7928b --- /dev/null +++ b/src/xml.c @@ -0,0 +1,242 @@ +/* + * xml.c + * + * Copyright 2017 Michael Rasmussen + * + * 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include +#include +#include "xml.h" + +static gboolean debug = FALSE; + +static xmlDocPtr getdoc(const gchar* xml) { + xmlDocPtr doc; + doc = xmlParseMemory(xml, strlen(xml)); + + if (doc == NULL ) { + g_warning("Document not parsed successfully."); + return NULL; + } + + return doc; +} + +static xmlXPathObjectPtr getnodeset(xmlDocPtr doc, xmlChar *xpath, const gchar* prefix, const gchar* ns_url) { + + xmlXPathContextPtr context; + xmlXPathObjectPtr result = NULL; + + context = xmlXPathNewContext(doc); + if (context == NULL) { + g_warning("Error in xmlXPathNewContext"); + return NULL; + } + + if (prefix && ns_url) { + xmlXPathRegisterNs(context, (xmlChar *)prefix, (xmlChar *)ns_url); + } + + result = xmlXPathEvalExpression(xpath, context); + xmlXPathFreeContext(context); + xmlFree(xpath); + if (result == NULL) { + g_warning("Error in xmlXPathEvalExpression"); + return NULL; + } + + if(xmlXPathNodeSetIsEmpty(result->nodesetval)){ + xmlXPathFreeObject(result); + result = NULL; + } + + return result; +} + +xmlNodePtr find_node_by_name(xmlNodePtr rootnode, const xmlChar* nodename, GSList** nodes) { + xmlNodePtr node = rootnode; + + if (!node) { + return NULL; + } + + while (node != NULL) { + + if (!xmlStrcmp(node->name, nodename)) { + if (nodes) { + *nodes = g_slist_append(*nodes, (gpointer) node); + } else { + return node; + } + } else if (node->children != NULL) { + xmlNodePtr intNode = find_node_by_name(node->children, nodename, nodes); + if (intNode != NULL) { + if (nodes) { + *nodes = g_slist_append(*nodes, (gpointer) node); + } else { + return intNode; + } + } + } + node = node->next; + } + + return NULL; +} + +static GSList* find_all_nodes_by_name(xmlDocPtr doc, const gchar* nodename) { + GSList* nodes = NULL; + + find_node_by_name(doc->children, (const xmlChar *) nodename, &nodes); + + return nodes; +} + +void dump_node(FILE* f, xmlDocPtr doc, xmlNodePtr node) { + g_return_if_fail(f && doc && node); + + xmlElemDump(f, doc, node); + fprintf(f, "\n"); +} + +void dump_nodes(gpointer data, gpointer user_data) { + g_return_if_fail(data && user_data); + + xmlNodePtr node = (xmlNodePtr) data; + xmlDocPtr doc = (xmlDocPtr) user_data; + + dump_node(stderr, doc, node); +} + +void get_props(gpointer data, gpointer user_data) { + g_return_if_fail(data && user_data); + + xmlNodePtr node = (xmlNodePtr) data; + Calendar* cal = (Calendar *) user_data; + + xmlChar* comp = xmlGetProp(node, (const xmlChar *)"name"); + if (comp) { + Component c = string_to_component((const gchar *) comp); + cal->components = g_slist_append(cal->components, GUINT_TO_POINTER(c)); + xmlFree(comp); + } +} + +GSList* find_element(const gchar* element, const gchar* xml, const gchar* prefix, const gchar* ns_url) { + GSList* elements = NULL; + xmlChar* keyword = NULL; + + g_return_val_if_fail(element && xml, NULL); + + xmlDocPtr doc = getdoc(xml); + if (doc) { + xmlXPathObjectPtr result = getnodeset(doc, xmlCharStrdup(element), prefix, ns_url); + + if (result) { + xmlNodeSetPtr nodeset = result->nodesetval; + for (int i = 0; i < nodeset->nodeNr; i++) { + keyword = xmlNodeListGetString(doc, nodeset->nodeTab[i]->xmlChildrenNode, 1); + elements = g_slist_append(elements, (gpointer) g_strdup((gchar *) keyword)); + xmlFree(keyword); + } + xmlXPathFreeObject(result); + } + + xmlFreeDoc(doc); + } + xmlCleanupParser(); + + return elements; +} + +GSList* find_node(const gchar* node, const gchar* xml) { + GSList* nodes = NULL; + xmlDocPtr doc = getdoc(xml); + + if (doc) { + GSList* n = find_all_nodes_by_name(doc, node); + + for (GSList* l = n; l; l = g_slist_next(l)) { + xmlNodePtr p = (xmlNodePtr) l->data; + nodes = g_slist_append(nodes, g_strdup((gchar *) p->name)); + } + + g_slist_free(n); + xmlFreeDoc(doc); + } + xmlCleanupParser(); + + return nodes; +} + +GSList* find_calendars(const gchar* host_part, const gchar* xml) { + GSList* list = NULL; + xmlNodePtr ptr; + xmlNodePtr found; + xmlDocPtr doc = getdoc(xml); + + if (doc) { + GSList* n = find_all_nodes_by_name(doc, "response"); + for (GSList* l = n; l; l = g_slist_next(l)) { + xmlNodePtr p = (xmlNodePtr) l->data; + if (debug) + dump_node(stderr, doc, p); + found = find_node_by_name(p, (const xmlChar*)"resourcetype", NULL); + if (found) { + found = find_node_by_name(found, (const xmlChar*)"calendar", NULL); + if (found) { + if (debug) + dump_node(stderr, doc, p); + Calendar* cal = g_new0(Calendar, 1); + ptr = find_node_by_name(p, (const xmlChar*)"href", NULL); + if (ptr) + cal->url = g_strconcat(host_part, ptr->children->content, NULL); + ptr = find_node_by_name(p, (const xmlChar*)"displayname", NULL); + if (ptr) + cal->displayname = g_strdup((gchar *)ptr->children->content); + ptr = find_node_by_name(p, (const xmlChar*)"getctag", NULL); + if (ptr) + cal->ctag = g_strdup((gchar *)ptr->children->content); + ptr = find_node_by_name(p, (const xmlChar*)"supported-calendar-component-set", NULL); + if (ptr) { + GSList* comps = NULL; + find_node_by_name(ptr, (const xmlChar *)"comp", &comps); + g_slist_foreach(comps, (GFunc)get_props, (gpointer) cal); + g_slist_free(comps); + } + list = g_slist_append(list, (gpointer) cal); + if (debug) + calendar_dump(stderr, cal); + } + } + } + g_slist_free(n); + } + + return list; +} + +void set_debug_mode(gboolean mode) { + debug = mode; +} diff --git a/src/xml.h b/src/xml.h new file mode 100644 index 0000000..87e2bbc --- /dev/null +++ b/src/xml.h @@ -0,0 +1,42 @@ +/* + * xml.h + * + * Copyright 2017 Michael Rasmussen + * + * 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#ifndef __XML_H__ +#define __XML_H__ + +#include +#include +#include "caldav.h" + +G_BEGIN_DECLS + +GSList* find_element(const gchar* element, const gchar* xml, const gchar* prefix, const gchar* ns_url); +xmlNodePtr find_node_by_name(xmlNodePtr rootnode, const xmlChar* nodename, GSList** nodes); +GSList* find_node(const gchar* node, const gchar* xml); +GSList* find_calendars(const gchar* host_part, const gchar* xml); +void dump_node(FILE* f, xmlDocPtr doc, xmlNodePtr node); +void dump_nodes(gpointer data, gpointer user_data); +void get_props(gpointer data, gpointer user_data); +void set_debug_mode(gboolean mode); + +G_END_DECLS + +#endif diff --git a/test-app/Makefile.am b/test-app/Makefile.am new file mode 100644 index 0000000..5f6fa96 --- /dev/null +++ b/test-app/Makefile.am @@ -0,0 +1,23 @@ +AUTOMAKE_OPTIONS = gnu + +AM_CPPFLAGS = \ + @GLIB_CFLAGS@ \ + @CURL_CFLAGS@ \ + -I$(top_srcdir) -I$(top_builddir) \ + -I$(top_srcdir)/src -I$(top_builddir)/src \ + -I$(top_srcdir)/test-app -I$(top_builddir)/test-app + +bin_PROGRAMS = test-app + +test_app_SOURCES = \ + test-app.c \ + cmdline-parser.h \ + cmdline-parser.c + +test_app_LDFLAGS = \ + -L$(top_srcdir)/src/ + +test_app_LDADD = \ + $(GLIB_LIBS) \ + @CURL_LIBS@ \ + -lcaldav diff --git a/test-app/cmdline-parser.c b/test-app/cmdline-parser.c new file mode 100644 index 0000000..747e3d7 --- /dev/null +++ b/test-app/cmdline-parser.c @@ -0,0 +1,305 @@ +/* + * cmdline-parser.c + * + * Copyright 2017 Michael Rasmussen + * + * 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cmdline-parser.h" +#include "caldav.h" + +static const gchar* help[] = { + "Usage: test-app [Options] -o operation -p password -u username URL", + "", + "a|data Use this option to insert calendar data from file.", + " As an alternative use redirect IO with <.", + "d|debug Print a lot of debug information to STDOUT", + "e|etag ETag to use for update and delete", + "f|finish End time interval in ISO 8601 local time. 2017-01-13T13:13:13", + "h|help Show this help info.", + "r|href HREFs to use for getobjects. Each href on a separate line.", + " As an alternative use redirect IO with <.", + "s|start Start time interval in ISO 8601 local time. 2017-01-13T13:13:13", + NULL +}; + +static void show_help() { + for (int i = 0; help[i]; i++) { + printf("%s\n", help[i]); + } +} + +static void get_operation(const gchar* op, Operation* o) { + if (o) { + if (g_strcmp0("CALENDARINFO", op) == 0) { + *o = CALENDARINFO; + } else if (g_strcmp0("GETOBJECTS", op) == 0) { + *o = GETOBJECTS; + } else if (g_strcmp0("GETALLOBJECTS", op) == 0) { + *o = GETALLOBJECTS; + } else if (g_strcmp0("SIMPLEGET", op) == 0) { + *o = SIMPLEGET; + } else if (g_strcmp0("CHANGEINFO", op) == 0) { + *o = CHANGEINFO; + } else if (g_strcmp0("UPDATEOBJECTS", op) == 0) { + *o = UPDATEOBJECTS; + } else if (g_strcmp0("ADDOBJECTS", op) == 0) { + *o = ADDOBJECTS; + } else if (g_strcmp0("DELETEOBJECTS", op) == 0) { + *o = DELETEOBJECTS; + } else if (g_strcmp0("DISCOVER", op) == 0) { + *o = DISCOVER; + } else if (g_strcmp0("OPTIONSINFO", op) == 0) { + *o = OPTIONSINFO; + } else if (g_strcmp0("FREEBUSY", op) == 0) { + *o = FREEBUSY; + } else { + *o = UNKNOWN; + } + } +} + +static gboolean invalid_test_case(Runtime* test) { + gboolean invalid = TRUE; + gboolean error = FALSE; + + if (test) { + if (!test->username) { + printf("Missing username\n"); + error = TRUE; + } + if (!test->password) { + printf("Missing password\n"); + error = TRUE; + } + if (!test->url) { + printf("Missing URL\n"); + error = TRUE; + } + if (!test->operation) { + printf("Missing operation\n"); + error = TRUE; + } + invalid = (error) ? TRUE : FALSE; + } + + return invalid; +} + +#define BUF_SIZE 1024 +static gchar* read_stream(FILE* stream) { + gchar buffer[BUF_SIZE]; + size_t size = 1; + gchar* content = g_malloc0(sizeof(gchar) * BUF_SIZE); + + while (fgets(buffer, BUF_SIZE, stream) != NULL) { + size += strlen(buffer); + content = g_realloc(content, size); + g_strlcat(content, buffer, size); + } + + return content; +} + +static GSList* parse_input(const gchar* input) { + GSList* list = NULL; + + gchar** strings = g_strsplit(input, "\n", 0); + for (gchar** h = strings; *h; ++h) { + if (g_strcmp0(*h, "") != 0) { + HrefData* hd = href_data_new(*h, NULL); + list = g_slist_append(list, hd); + } + } + g_strfreev(strings); + + return list; +} + +Runtime* parse_cmdline(int argc, char **argv) { + int opt = 0; + int debug_flag = 0; + Runtime* test = runtime_new(); + Operation* op = g_new(Operation, 1); + FILE* stream = NULL; + + struct option long_options[] = { + {"data", required_argument, 0, 'a'}, + {"etag", required_argument, 0, 'e'}, + {"finish", required_argument, 0, 'f'}, + {"debug", no_argument, &debug_flag, 1 }, + {"help", no_argument, 0, 'h'}, + {"operation", required_argument, 0, 'o'}, + {"password", required_argument, 0, 'p'}, + {"href", required_argument, 0, 'r'}, + {"start", required_argument, 0, 's'}, + {"username", required_argument, 0, 'u'}, + {0, 0, 0, 0 } + }; + + int option_index = 0; + while ((opt = getopt_long(argc, argv, "a:de:f:h:o:p:rs:u:", + long_options, &option_index)) != -1) { + switch (opt) { + case 'a': { + stream = fopen(optarg, "r"); + if (!stream) { + perror("File"); + runtime_free(test); + exit(EXIT_FAILURE); + } + break; + } + + case 'd': + debug_flag = 1; + break; + + case 'e': + test->etag = g_strdup(optarg); + break; + + case 'f': + test->finish = get_date_time_from_string(optarg); + break; + + case 'h': + runtime_free(test); + show_help(); + exit(EXIT_SUCCESS); + + case 'o': + get_operation(optarg, op); + if (op && *op != UNKNOWN) { + test->operation = op; + } else { + runtime_free(test); + fprintf(stderr, "%s: Invalid operation\n", optarg); + show_help(); + exit(EXIT_FAILURE); + } + break; + + case 'p': + test->password = g_strdup(optarg); + break; + + case 'r': { + stream = fopen(optarg, "r"); + if (!stream) { + perror("File"); + runtime_free(test); + exit(EXIT_FAILURE); + } + break; + } + + case 's': + test->start = get_date_time_from_string(optarg); + break; + + case 'u': + test->username = g_strdup(optarg); + break; + + case ':': + /* missing option argument */ + runtime_free(test); + printf("%s: option '-%c' requires an argument\n", argv[0], optopt); + show_help(); + exit(EXIT_FAILURE); + + case '?': + default: + /* invalid option */ + runtime_free(test); + fprintf(stderr, "%s: option '-%c' is invalid: ignored\n", argv[0], optopt); + show_help(); + exit(EXIT_FAILURE); + } + } + + if (optind + 1 < argc) { + runtime_free(test); + printf("Only one URL allowed\n"); + show_help(); + exit(EXIT_FAILURE); + } + + test->url= g_strdup(argv[optind]); + + if (invalid_test_case(test)) { + runtime_free(test); + show_help(); + exit(EXIT_FAILURE); + } + + if (*test->operation == ADDOBJECTS || *test->operation == UPDATEOBJECTS) { + struct stat sb; + if (fstat(fileno(stdin), &sb) == -1) { + if (!stream) { + perror("Stat"); + runtime_free(test); + exit(EXIT_FAILURE); + } + } else { + if (stream && sb.st_size > 0) { + fprintf(stderr, "Error: Option --data or --href active. Cannot redirect STDIN\n"); + runtime_free(test); + exit(EXIT_FAILURE); + } else { + stream = (stream) ? stream : stdin; + } + } + + gchar* text = read_stream(stream); + + switch (*test->operation) { + case GETOBJECTS: + test->hrefs = parse_input(text); + g_free(text); + break; + case UPDATEOBJECTS: + case ADDOBJECTS: + test->component = text; + break; + default: + fprintf(stderr, "%i: Input not allowed\n", *test->operation); + runtime_free(test); + g_free(text); + exit(EXIT_FAILURE); + } + } + + test->debug = (debug_flag) ? TRUE : FALSE; + + return test; +} + diff --git a/test-app/cmdline-parser.h b/test-app/cmdline-parser.h new file mode 100644 index 0000000..65c590e --- /dev/null +++ b/test-app/cmdline-parser.h @@ -0,0 +1,34 @@ +/* + * cmdline-parser.h + * + * Copyright 2017 Michael Rasmussen + * + * 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#ifndef __CMDLINE_PARSER_H__ +#define __CMDLINE_PARSER_H__ + +#include +#include "caldav.h" + +G_BEGIN_DECLS + +Runtime* parse_cmdline(int argc, char **argv); + +G_END_DECLS + +#endif diff --git a/test-app/test-app.c b/test-app/test-app.c new file mode 100644 index 0000000..aec68cb --- /dev/null +++ b/test-app/test-app.c @@ -0,0 +1,42 @@ +/* + * test-app.c + * + * Copyright 2017 Michael Rasmussen + * + * 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include "caldav.h" +#include "cmdline-parser.h" + +int main(int argc, char **argv) { + Runtime* test = parse_cmdline(argc, argv); + test->file = stderr; + guint status = (guint) execute(test); + + gchar* err = status_str(test); + fprintf(stdout, "\n%u: %s\n", test->output->status, err); + g_free(err); + runtime_free(test); + + return (int) status; +} + -- 2.39.5