/* * 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; }