--- /dev/null
+/*
+ * caldav.c
+ *
+ * Copyright 2016 Michael Rasmussen <mir@datanom.net>
+ *
+ * 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 <stdio.h>
+#include <unistd.h>
+#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(
+ "<c:time-range start=\"", start, "\"",
+ " end=\"", finish, "\"/>",
+ 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(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>",
+ "<d:propfind xmlns:d=\"DAV:\" xmlns:cs=\"http://calendarserver.org/ns/\">",
+ "<d:prop><d:displayname /><cs:getctag /></d:prop></d:propfind>",
+ 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("<d:href>", h->href, "</d:href>", NULL);
+ href = g_string_append(href, elem);
+ g_free(elem);
+ }
+ }
+ rq->data = g_strconcat(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>",
+ "<c:calendar-multiget xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">",
+ "<d:prop><d:getetag /><c:calendar-data /></d:prop>",
+ g_string_free(href, FALSE), "</c:calendar-multiget>",
+ 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(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>",
+ "<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\">",
+ "<c:comp-filter name=\"VEVENT\">",timerange ? timerange : "",
+ "</c:comp-filter></c:comp-filter>",
+ "</c:filter></c:calendar-query>",
+ 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(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>",
+ "<c:calendar-query xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">",
+ "<d:prop><d:getetag /></d:prop><c:filter><c:comp-filter name=\"VCALENDAR\">",
+ "<c:comp-filter name=\"VEVENT\">",timerange ? timerange : "",
+ "</c:comp-filter></c:comp-filter>",
+ "</c:filter></c:calendar-query>",
+ 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(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>",
+ "<D:lockinfo xmlns:D=\"DAV:\"><D:lockscope><D:exclusive/></D:lockscope>",
+ " <D:locktype><D:write/></D:locktype></D:lockinfo>",
+ 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(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>",
+ "<c:free-busy-query xmlns:D=\"DAV:\"",
+ " xmlns:c=\"urn:ietf:params:xml:ns:caldav\">",
+ timerange ? timerange : "", "</c:free-busy-query>",
+ 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(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>",
+ "<d:propfind xmlns:d=\"DAV:\">",
+ "<d:prop><d:current-user-principal /></d:prop></d:propfind>",
+ 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(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>",
+ "<d:propfind xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">",
+ "<d:prop><c:calendar-home-set /></d:prop></d:propfind>",
+ 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(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>",
+ "<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>",
+ 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;
+}
--- /dev/null
+/*
+ * caldav.h
+ *
+ * Copyright 2016 Michael Rasmussen <mir@datanom.net>
+ *
+ * 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 <glib.h>
+
+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
+
--- /dev/null
+/*
+ * curllib.c
+ *
+ * Copyright 2016 Michael Rasmussen <mir@datanom.net>
+ *
+ * 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 <string.h>
+#include <stdlib.h>
+#include <curl/curl.h>
+#include <curl/easy.h>
+#include <glib.h>
+#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<size; i+= width) {
+ fprintf(stream, "%04zx: ", i);
+ if(!nohex) {
+ /* hex not disabled, show it */
+ for(c = 0; c < width; c++) {
+ if(i+c < size)
+ fprintf(stream, "%02x ", ptr[i+c]);
+ else
+ fputs(" ", stream);
+ }
+ }
+ for(c = 0; (c < width) && (i+c < size); c++) {
+ /* check for 0D0A; if found, skip past and start a new line of output */
+ if (nohex && (i+c+1 < size) && ptr[i+c]==0x0D && ptr[i+c+1]==0x0A) {
+ i+=(c+2-width);
+ break;
+ }
+ fprintf(stream, "%c",(ptr[i+c]>=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;
+ }
+}
--- /dev/null
+/*
+ * curllib.h
+ *
+ * Copyright 2016 Michael Rasmussen <mir@datanom.net>
+ *
+ * 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 <glib.h>
+
+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
+
--- /dev/null
+/*
+ * xml.c
+ *
+ * Copyright 2017 Michael Rasmussen <mir@datanom.net>
+ *
+ * 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 <libxml/parser.h>
+#include <libxml/xpath.h>
+#include <libxml/xpathInternals.h>
+#include <string.h>
+#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;
+}
--- /dev/null
+/*
+ * xml.h
+ *
+ * Copyright 2017 Michael Rasmussen <mir@datanom.net>
+ *
+ * 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 <glib.h>
+#include <libxml/parser.h>
+#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
--- /dev/null
+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
--- /dev/null
+/*
+ * cmdline-parser.c
+ *
+ * Copyright 2017 Michael Rasmussen <mir@datanom.net>
+ *
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <getopt.h>
+#include <glib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#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;
+}
+
--- /dev/null
+/*
+ * cmdline-parser.h
+ *
+ * Copyright 2017 Michael Rasmussen <mir@datanom.net>
+ *
+ * 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 <glib.h>
+#include "caldav.h"
+
+G_BEGIN_DECLS
+
+Runtime* parse_cmdline(int argc, char **argv);
+
+G_END_DECLS
+
+#endif
--- /dev/null
+/*
+ * test-app.c
+ *
+ * Copyright 2017 Michael Rasmussen <mir@datanom.net>
+ *
+ * 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 <stdio.h>
+#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;
+}
+