res_stasis: Add ability to switch applications.
[asterisk/asterisk.git] / res / res_calendar_caldav.c
index 64f8f2a..425a1e3 100644 (file)
  */
 
 /*! \file
- * \brief Resource for handling iCalnedar calendars
+ * \brief Resource for handling CalDAV calendars
  */
 
 /*** MODULEINFO
+       <depend>res_calendar</depend>
        <depend>neon</depend>
        <depend>ical</depend>
        <depend>libxml2</depend>
+       <support_level>extended</support_level>
 ***/
-#include "asterisk.h"
 
-ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+#include "asterisk.h"
 
 #include <libical/ical.h>
-#include <neon/ne_session.h>
-#include <neon/ne_uri.h>
-#include <neon/ne_request.h>
-#include <neon/ne_auth.h>
+#include <ne_session.h>
+#include <ne_uri.h>
+#include <ne_request.h>
+#include <ne_auth.h>
+#include <ne_redirect.h>
 #include <libxml/xmlmemory.h>
 #include <libxml/parser.h>
 
 #include "asterisk/module.h"
+#include "asterisk/channel.h"
 #include "asterisk/calendar.h"
 #include "asterisk/lock.h"
 #include "asterisk/config.h"
@@ -68,11 +71,6 @@ struct caldav_pvt {
        struct ao2_container *events;
 };
 
-static int cb_true(void *user_data, void *arg, int flags)
-{
-       return CMP_MATCH;
-}
-
 static void caldav_destructor(void *obj)
 {
        struct caldav_pvt *pvt = obj;
@@ -81,9 +79,10 @@ static void caldav_destructor(void *obj)
        if (pvt->session) {
                ne_session_destroy(pvt->session);
        }
+       ne_uri_free(&pvt->uri);
        ast_string_field_free_memory(pvt);
 
-       ao2_callback(pvt->events, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, cb_true, NULL);
+       ao2_callback(pvt->events, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, NULL, NULL);
 
        ao2_ref(pvt->events, -1);
 }
@@ -127,6 +126,15 @@ static int auth_credentials(void *userdata, const char *realm, int attempts, cha
        return 0;
 }
 
+static int debug_response_handler(void *userdata, ne_request *req, const ne_status *st)
+{
+       if (st->code < 200 || st->code > 299) {
+               ast_debug(1, "Unexpected response from server, %d: %s\n", st->code, st->reason_phrase);
+               return 0;
+       }
+       return 1;
+}
+
 static struct ast_str *caldav_request(struct caldav_pvt *pvt, const char *method, struct ast_str *req_body, struct ast_str *subdir, const char *content_type)
 {
        struct ast_str *response;
@@ -147,15 +155,16 @@ static struct ast_str *caldav_request(struct caldav_pvt *pvt, const char *method
        snprintf(buf, sizeof(buf), "%s%s", pvt->uri.path, subdir ? ast_str_buffer(subdir) : "");
 
        req = ne_request_create(pvt->session, method, buf);
-       ne_add_response_body_reader(req, ne_accept_2xx, fetch_response_reader, &response);
+       ne_add_response_body_reader(req, debug_response_handler, fetch_response_reader, &response);
        ne_set_request_body_buffer(req, ast_str_buffer(req_body), ast_str_strlen(req_body));
        ne_add_request_header(req, "Content-type", ast_strlen_zero(content_type) ? "text/xml" : content_type);
+       ne_add_request_header(req, "Depth", "1");
 
        ret = ne_request_dispatch(req);
        ne_request_destroy(req);
 
-       if (ret != NE_OK || !ast_str_strlen(response)) {
-               ast_log(LOG_WARNING, "Unknown response to CalDAV calendar %s, request %s to %s: %s\n", pvt->owner->name, method, pvt->url, ne_get_error(pvt->session));
+       if (ret != NE_OK) {
+               ast_log(LOG_WARNING, "Unknown response to CalDAV calendar %s, request %s to %s: %s\n", pvt->owner->name, method, buf, ne_get_error(pvt->session));
                ast_free(response);
                return NULL;
        }
@@ -165,6 +174,7 @@ static struct ast_str *caldav_request(struct caldav_pvt *pvt, const char *method
 
 static int caldav_write_event(struct ast_calendar_event *event)
 {
+       struct caldav_pvt *pvt;
        struct ast_str *body = NULL, *response = NULL, *subdir = NULL;
        icalcomponent *calendar, *icalevent;
        icaltimezone *utc = icaltimezone_get_utc_timezone();
@@ -180,19 +190,23 @@ static int caldav_write_event(struct ast_calendar_event *event)
                return -1;
        }
        if (!(body = ast_str_create(512)) ||
-               !(subdir = ast_str_create(32)) ||
-               !(response = ast_str_create(512))) {
-               ast_log(LOG_ERROR, "Could not allocate memory for request and response!\n");
+               !(subdir = ast_str_create(32))) {
+               ast_log(LOG_ERROR, "Could not allocate memory for request!\n");
                goto write_cleanup;
        }
 
+       pvt = event->owner->tech_pvt;
+
        if (ast_strlen_zero(event->uid)) {
                unsigned short val[8];
                int x;
                for (x = 0; x < 8; x++) {
                        val[x] = ast_random();
                }
-               ast_string_field_build(event, uid, "%04x%04x-%04x-%04x-%04x-%04x%04x%04x", val[0], val[1], val[2], val[3], val[4], val[5], val[6], val[7]);
+               ast_string_field_build(event, uid, "%04x%04x-%04x-%04x-%04x-%04x%04x%04x",
+                       (unsigned)val[0], (unsigned)val[1], (unsigned)val[2],
+                       (unsigned)val[3], (unsigned)val[4], (unsigned)val[5],
+                       (unsigned)val[6], (unsigned)val[7]);
        }
 
        calendar = icalcomponent_new(ICAL_VCALENDAR_COMPONENT);
@@ -216,6 +230,12 @@ static int caldav_write_event(struct ast_calendar_event *event)
        if (!ast_strlen_zero(event->location)) {
                icalcomponent_add_property(icalevent, icalproperty_new_location(event->location));
        }
+       if (!ast_strlen_zero(event->categories)) {
+               icalcomponent_add_property(icalevent, icalproperty_new_categories(event->categories));
+       }
+       if (event->priority > 0) {
+               icalcomponent_add_property(icalevent, icalproperty_new_priority(event->priority));
+       }
 
        switch (event->busy_state) {
        case AST_CALENDAR_BS_BUSY:
@@ -233,11 +253,11 @@ static int caldav_write_event(struct ast_calendar_event *event)
        icalcomponent_add_component(calendar, icalevent);
 
        ast_str_append(&body, 0, "%s", icalcomponent_as_ical_string(calendar));
-       ast_str_set(&subdir, 0, "%s%s.ics", ast_str_buffer(body)[ast_str_strlen(body)] == '/' ? "" : "/", event->uid);
+       ast_str_set(&subdir, 0, "%s%s.ics", pvt->url[strlen(pvt->url) - 1] == '/' ? "" : "/", event->uid);
 
-       response = caldav_request(event->owner->tech_pvt, "PUT", body, subdir, "text/calendar");
-
-       ret = 0;
+       if ((response = caldav_request(pvt, "PUT", body, subdir, "text/calendar"))) {
+               ret = 0;
+       }
 
 write_cleanup:
        if (body) {
@@ -293,10 +313,39 @@ static struct ast_str *caldav_get_events_between(struct caldav_pvt *pvt, time_t
 
        response = caldav_request(pvt, "REPORT", body, NULL, NULL);
        ast_free(body);
+       if (response && !ast_str_strlen(response)) {
+               ast_free(response);
+               return NULL;
+       }
 
        return response;
 }
 
+static time_t icalfloat_to_timet(icaltimetype time)
+{
+       struct ast_tm tm = {0,};
+       struct timeval tv;
+
+       tm.tm_mday = time.day;
+       tm.tm_mon = time.month - 1;
+       tm.tm_year = time.year - 1900;
+       tm.tm_hour = time.hour;
+       tm.tm_min = time.minute;
+       tm.tm_sec = time.second;
+       tm.tm_isdst = -1;
+       tv = ast_mktime(&tm, NULL);
+
+       return tv.tv_sec;
+}
+
+/* span->start & span->end may be dates or floating times which have no timezone,
+ * which would mean that they should apply to the local timezone for all recepients.
+ * For example, if a meeting was set for 1PM-2PM floating time, people in different time
+ * zones would not be scheduled at the same local times.  Dates are often treated as
+ * floating times, so all day events will need to be converted--so we can trust the
+ * span here, and instead will grab the start and end from the component, which will
+ * allow us to test for floating times or dates.
+ */
 static void caldav_add_event(icalcomponent *comp, struct icaltime_span *span, void *data)
 {
        struct caldav_pvt *pvt = data;
@@ -317,23 +366,12 @@ static void caldav_add_event(icalcomponent *comp, struct icaltime_span *span, vo
                return;
        }
 
-       start = icaltime_from_timet_with_zone(span->start, 0, utc);
-       end = icaltime_from_timet_with_zone(span->end, 0, utc);
-       event->start = span->start;
-       event->end = span->end;
-
-       switch(icalcomponent_get_status(comp)) {
-       case ICAL_STATUS_CONFIRMED:
-               event->busy_state = AST_CALENDAR_BS_BUSY;
-               break;
-
-       case ICAL_STATUS_TENTATIVE:
-               event->busy_state = AST_CALENDAR_BS_BUSY_TENTATIVE;
-               break;
+       start = icalcomponent_get_dtstart(comp);
+       end = icalcomponent_get_dtend(comp);
 
-       default:
-               event->busy_state = AST_CALENDAR_BS_FREE;
-       }
+       event->start = icaltime_get_tzid(start) ? span->start : icalfloat_to_timet(start);
+       event->end = icaltime_get_tzid(end) ? span->end : icalfloat_to_timet(end);
+       event->busy_state = span->is_busy ? AST_CALENDAR_BS_BUSY : AST_CALENDAR_BS_FREE;
 
        if ((prop = icalcomponent_get_first_property(comp, ICAL_SUMMARY_PROPERTY))) {
                ast_string_field_set(event, summary, icalproperty_get_value_as_string(prop));
@@ -351,6 +389,14 @@ static void caldav_add_event(icalcomponent *comp, struct icaltime_span *span, vo
                ast_string_field_set(event, location, icalproperty_get_value_as_string(prop));
        }
 
+       if ((prop = icalcomponent_get_first_property(comp, ICAL_CATEGORIES_PROPERTY))) {
+               ast_string_field_set(event, categories, icalproperty_get_value_as_string(prop));
+       }
+
+       if ((prop = icalcomponent_get_first_property(comp, ICAL_PRIORITY_PROPERTY))) {
+               event->priority = icalvalue_get_integer(icalproperty_get_value(prop));
+       }
+
        if ((prop = icalcomponent_get_first_property(comp, ICAL_UID_PROPERTY))) {
                ast_string_field_set(event, uid, icalproperty_get_value_as_string(prop));
        } else {
@@ -359,7 +405,7 @@ static void caldav_add_event(icalcomponent *comp, struct icaltime_span *span, vo
                        ast_string_field_set(event, uid, event->summary);
                } else {
                        char tmp[100];
-                       snprintf(tmp, sizeof(tmp), "%lu", event->start);
+                       snprintf(tmp, sizeof(tmp), "%ld", event->start);
                        ast_string_field_set(event, uid, tmp);
                }
        }
@@ -375,10 +421,12 @@ static void caldav_add_event(icalcomponent *comp, struct icaltime_span *span, vo
                        return;
                }
                data = icalproperty_get_attendee(prop);
-               if (!ast_strlen_zero(data)) {
-                       attendee->data = ast_strdup(data);;
-                       AST_LIST_INSERT_TAIL(&event->attendees, attendee, next);
+               if (ast_strlen_zero(data)) {
+                       ast_free(attendee);
+                       continue;
                }
+               attendee->data = ast_strdup(data);
+               AST_LIST_INSERT_TAIL(&event->attendees, attendee, next);
        }
 
 
@@ -414,7 +462,7 @@ static void caldav_add_event(icalcomponent *comp, struct icaltime_span *span, vo
                /* XXX Technically you can check RELATED to see if the event fires from the END of the event
                 * But, I'm not sure I've ever seen anyone implement it in calendaring software, so I'm ignoring for now */
                tmp = icaltime_add(start, trigger.duration);
-               event->alarm = icaltime_as_timet_with_zone(tmp, utc);
+               event->alarm = icaltime_as_timet_with_zone(tmp, icaltime_get_timezone(start));
        }
 
        ao2_link(pvt->events, event);
@@ -431,17 +479,26 @@ struct xmlstate {
        time_t end;
 };
 
-static void handle_start_element(void *data, const xmlChar *fullname, const xmlChar **atts)
+static const xmlChar *caldav_node_localname = BAD_CAST "calendar-data";
+static const xmlChar *caldav_node_nsuri     = BAD_CAST "urn:ietf:params:xml:ns:caldav";
+
+static void handle_start_element(void *data,
+                                                                const xmlChar *localname, const xmlChar *prefix, const xmlChar *uri,
+                                                                int nb_namespaces, const xmlChar **namespaces,
+                                                                int nb_attributes, int nb_defaulted, const xmlChar **attributes)
 {
        struct xmlstate *state = data;
 
-       if (!xmlStrcasecmp(fullname, BAD_CAST "C:calendar-data")) {
-               state->in_caldata = 1;
-               ast_str_reset(state->cdata);
+       if (xmlStrcmp(localname, caldav_node_localname) || xmlStrcmp(uri, caldav_node_nsuri)) {
+               return;
        }
+
+       state->in_caldata = 1;
+       ast_str_reset(state->cdata);
 }
 
-static void handle_end_element(void *data, const xmlChar *name)
+static void handle_end_element(void *data,
+                                                          const xmlChar *localname, const xmlChar *prefix, const xmlChar *uri)
 {
        struct xmlstate *state = data;
        struct icaltimetype start, end;
@@ -449,7 +506,7 @@ static void handle_end_element(void *data, const xmlChar *name)
        icalcomponent *iter;
        icalcomponent *comp;
 
-       if (xmlStrcasecmp(name, BAD_CAST "C:calendar-data")) {
+       if (xmlStrcmp(localname, caldav_node_localname) || xmlStrcmp(uri, caldav_node_nsuri)) {
                return;
        }
 
@@ -512,9 +569,23 @@ static int update_caldav(struct caldav_pvt *pvt)
        state.start = start;
        state.end = end;
 
+       /*
+        * We want SAX2, so you assume that we want to call xmlSAXVersion() here, and
+        * that certainly seems like the right thing to do, but the default SAX
+        * handling functions assume that the 'data' pointer is going to be a
+        * xmlParserCtxtPtr, not a user data pointer, so we have to make sure that we
+        * are only calling the handlers that we control.
+        *
+        * So instead we hack things up a bit, clearing the struct and then assigning
+        * the magic number manually.
+        *
+        * There may be a cleaner way to do this, but frankly the libxml2 docs are
+        * pretty sparse.
+        */
        memset(&saxHandler, 0, sizeof(saxHandler));
-       saxHandler.startElement = handle_start_element;
-       saxHandler.endElement = handle_end_element;
+       saxHandler.initialized = XML_SAX2_MAGIC;
+       saxHandler.startElementNs = handle_start_element;
+       saxHandler.endElementNs = handle_end_element;
        saxHandler.characters = handle_characters;
 
        xmlSAXUserParseMemory(&saxHandler, &state, ast_str_buffer(response), ast_str_strlen(response));
@@ -536,11 +607,12 @@ static int verify_cert(void *userdata, int failures, const ne_ssl_certificate *c
 static void *caldav_load_calendar(void *void_data)
 {
        struct caldav_pvt *pvt;
+       const struct ast_config *cfg;
        struct ast_variable *v;
        struct ast_calendar *cal = void_data;
        ast_mutex_t refreshlock;
 
-       if (!(cal && ast_calendar_config)) {
+       if (!(cal && (cfg = ast_calendar_config_acquire()))) {
                ast_log(LOG_ERROR, "You must enable calendar support for res_caldav to load\n");
                return NULL;
        }
@@ -551,11 +623,13 @@ static void *caldav_load_calendar(void *void_data)
                } else {
                        ast_log(LOG_WARNING, "Could not lock calendar, aborting!\n");
                }
+               ast_calendar_config_release();
                return NULL;
        }
 
        if (!(pvt = ao2_alloc(sizeof(*pvt), caldav_destructor))) {
                ast_log(LOG_ERROR, "Could not allocate caldav_pvt structure for calendar: %s\n", cal->name);
+               ast_calendar_config_release();
                return NULL;
        }
 
@@ -565,6 +639,7 @@ static void *caldav_load_calendar(void *void_data)
                ast_log(LOG_ERROR, "Could not allocate space for fetching events for calendar: %s\n", cal->name);
                pvt = unref_caldav(pvt);
                ao2_unlock(cal);
+               ast_calendar_config_release();
                return NULL;
        }
 
@@ -572,10 +647,11 @@ static void *caldav_load_calendar(void *void_data)
                ast_log(LOG_ERROR, "Couldn't allocate string field space for calendar: %s\n", cal->name);
                pvt = unref_caldav(pvt);
                ao2_unlock(cal);
+               ast_calendar_config_release();
                return NULL;
        }
 
-       for (v = ast_variable_browse(ast_calendar_config, cal->name); v; v = v->next) {
+       for (v = ast_variable_browse(cfg, cal->name); v; v = v->next) {
                if (!strcasecmp(v->name, "url")) {
                        ast_string_field_set(pvt, url, v->value);
                } else if (!strcasecmp(v->name, "user")) {
@@ -585,6 +661,8 @@ static void *caldav_load_calendar(void *void_data)
                }
        }
 
+       ast_calendar_config_release();
+
        if (ast_strlen_zero(pvt->url)) {
                ast_log(LOG_WARNING, "No URL was specified for CalDAV calendar '%s' - skipping.\n", cal->name);
                pvt = unref_caldav(pvt);
@@ -608,8 +686,9 @@ static void *caldav_load_calendar(void *void_data)
        }
 
        pvt->session = ne_session_create(pvt->uri.scheme, pvt->uri.host, pvt->uri.port);
+       ne_redirect_register(pvt->session);
        ne_set_server_auth(pvt->session, auth_credentials, pvt);
-       if (!strncasecmp(pvt->uri.scheme, "https", sizeof(pvt->uri.scheme))) {
+       if (!strcasecmp(pvt->uri.scheme, "https")) {
                ne_ssl_trust_default_ca(pvt->session);
                ne_ssl_set_verify(pvt->session, verify_cert, NULL);
        }
@@ -669,7 +748,10 @@ static int unload_module(void)
        return 0;
 }
 
-AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Asterisk CalDAV Calendar Integration",
-               .load = load_module,
-               .unload = unload_module,
-       );
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "Asterisk CalDAV Calendar Integration",
+       .support_level = AST_MODULE_SUPPORT_EXTENDED,
+       .load = load_module,
+       .unload = unload_module,
+       .load_pri = AST_MODPRI_DEVSTATE_PLUGIN,
+       .requires = "res_calendar",
+);