33 #include <libical/ical.h>
34 #include <ne_session.h>
36 #include <ne_request.h>
38 #include <ne_redirect.h>
39 #include <libxml/xmlreader.h>
48 static void *caldav_load_calendar(
void *data);
49 static void *unref_caldav(
void *obj);
54 .description =
"CalDAV calendars",
56 .load_calendar = caldav_load_calendar,
57 .unref_calendar = unref_caldav,
58 .write_event = caldav_write_event,
73 static void caldav_destructor(
void *obj)
77 ast_debug(1,
"Destroying pvt for CalDAV calendar %s\n", pvt->owner->
name);
79 ne_session_destroy(pvt->session);
81 ne_uri_free(&pvt->uri);
89 static void *unref_caldav(
void *obj)
97 static int fetch_response_reader(
void *data,
const char *block,
size_t len)
99 struct ast_str **response = data;
105 memcpy(tmp, block, len);
113 static int auth_credentials(
void *userdata,
const char *realm,
int attempts,
char *username,
char *secret)
118 ast_log(LOG_WARNING,
"Invalid username or password for CalDAV calendar '%s'\n", pvt->owner->
name);
122 ne_strnzcpy(username, pvt->user, NE_ABUFSIZ);
123 ne_strnzcpy(secret, pvt->secret, NE_ABUFSIZ);
128 static int debug_response_handler(
void *userdata, ne_request *req,
const ne_status *st)
130 if (st->code < 200 || st->code > 299) {
131 if (st->code == 401) {
132 ast_debug(1,
"Got a 401 from the server but we expect this to happen when authenticating, %d: %s\n", st->code, st->reason_phrase);
134 ast_debug(1,
"Unexpected response from server, %d: %s\n", st->code, st->reason_phrase);
141 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)
149 ast_log(LOG_ERROR,
"There is no private!\n");
154 ast_log(LOG_ERROR,
"Could not allocate memory for response.\n");
158 snprintf(buf,
sizeof(buf),
"%s%s", pvt->uri.path, subdir ?
ast_str_buffer(subdir) :
"");
160 req = ne_request_create(pvt->session, method, buf);
161 ne_add_response_body_reader(req, debug_response_handler, fetch_response_reader, &response);
163 ne_add_request_header(req,
"Content-type", ast_strlen_zero(content_type) ?
"text/xml" : content_type);
164 ne_add_request_header(req,
"Depth",
"1");
166 ret = ne_request_dispatch(req);
167 ne_request_destroy(req);
170 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));
181 struct ast_str *body = NULL, *response = NULL, *subdir = NULL;
182 icalcomponent *calendar, *icalevent;
183 icaltimezone *utc = icaltimezone_get_utc_timezone();
187 ast_log(LOG_WARNING,
"No event passed!\n");
191 if (!(event->
start && event->
end)) {
192 ast_log(LOG_WARNING,
"The event must contain a start and an end\n");
197 ast_log(LOG_ERROR,
"Could not allocate memory for request!\n");
201 pvt =
event->owner->tech_pvt;
203 if (ast_strlen_zero(event->uid)) {
204 unsigned short val[8];
206 for (x = 0; x < 8; x++) {
207 val[x] = ast_random();
210 (
unsigned)val[0], (
unsigned)val[1], (
unsigned)val[2],
211 (
unsigned)val[3], (
unsigned)val[4], (
unsigned)val[5],
212 (
unsigned)val[6], (
unsigned)val[7]);
215 calendar = icalcomponent_new(ICAL_VCALENDAR_COMPONENT);
216 icalcomponent_add_property(calendar, icalproperty_new_version(
"2.0"));
217 icalcomponent_add_property(calendar, icalproperty_new_prodid(
"-//Digium, Inc.//res_caldav//EN"));
219 icalevent = icalcomponent_new(ICAL_VEVENT_COMPONENT);
220 icalcomponent_add_property(icalevent, icalproperty_new_dtstamp(icaltime_current_time_with_zone(utc)));
221 icalcomponent_add_property(icalevent, icalproperty_new_uid(event->uid));
222 icalcomponent_add_property(icalevent, icalproperty_new_dtstart(icaltime_from_timet_with_zone(event->
start, 0, utc)));
223 icalcomponent_add_property(icalevent, icalproperty_new_dtend(icaltime_from_timet_with_zone(event->
end, 0, utc)));
224 if (!ast_strlen_zero(event->organizer)) {
225 icalcomponent_add_property(icalevent, icalproperty_new_organizer(event->organizer));
227 if (!ast_strlen_zero(event->summary)) {
228 icalcomponent_add_property(icalevent, icalproperty_new_summary(event->summary));
230 if (!ast_strlen_zero(event->description)) {
231 icalcomponent_add_property(icalevent, icalproperty_new_description(event->description));
233 if (!ast_strlen_zero(event->location)) {
234 icalcomponent_add_property(icalevent, icalproperty_new_location(event->location));
236 if (!ast_strlen_zero(event->categories)) {
237 icalcomponent_add_property(icalevent, icalproperty_new_categories(event->categories));
240 icalcomponent_add_property(icalevent, icalproperty_new_priority(event->
priority));
244 case AST_CALENDAR_BS_BUSY:
245 icalcomponent_add_property(icalevent, icalproperty_new_status(ICAL_STATUS_CONFIRMED));
248 case AST_CALENDAR_BS_BUSY_TENTATIVE:
249 icalcomponent_add_property(icalevent, icalproperty_new_status(ICAL_STATUS_TENTATIVE));
253 icalcomponent_add_property(icalevent, icalproperty_new_status(ICAL_STATUS_NONE));
256 icalcomponent_add_component(calendar, icalevent);
258 ast_str_append(&body, 0,
"%s", icalcomponent_as_ical_string(calendar));
259 ast_str_set(&subdir, 0,
"%s%s.ics", pvt->url[strlen(pvt->url) - 1] ==
'/' ?
"" :
"/", event->uid);
261 if ((response = caldav_request(pvt,
"PUT", body, subdir,
"text/calendar"))) {
279 static struct ast_str *caldav_get_events_between(
struct caldav_pvt *pvt, time_t start_time, time_t end_time)
281 struct ast_str *body, *response;
282 icaltimezone *utc = icaltimezone_get_utc_timezone();
283 icaltimetype start, end;
284 const char *start_str, *end_str;
287 ast_log(LOG_ERROR,
"Could not allocate memory for body of request!\n");
291 start = icaltime_from_timet_with_zone(start_time, 0, utc);
292 end = icaltime_from_timet_with_zone(end_time, 0, utc);
293 start_str = icaltime_as_ical_string(start);
294 end_str = icaltime_as_ical_string(end);
301 "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
302 "<C:calendar-query xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n"
304 " <C:calendar-data>\n"
305 " <C:expand start=\"%s\" end=\"%s\"/>\n"
306 " </C:calendar-data>\n"
309 " <C:comp-filter name=\"VCALENDAR\">\n"
310 " <C:comp-filter name=\"VEVENT\">\n"
311 " <C:time-range start=\"%s\" end=\"%s\"/>\n"
312 " </C:comp-filter>\n"
313 " </C:comp-filter>\n"
315 "</C:calendar-query>\n", start_str, end_str, start_str, end_str);
317 response = caldav_request(pvt,
"REPORT", body, NULL, NULL);
327 static time_t icalfloat_to_timet(icaltimetype time)
333 tm.
tm_mon = time.month - 1;
352 static void caldav_add_event(icalcomponent *comp,
struct icaltime_span *span,
void *data)
356 icaltimezone *utc = icaltimezone_get_utc_timezone();
358 icalcomponent *valarm;
360 struct icaltriggertype trigger;
362 if (!(pvt && pvt->owner)) {
363 ast_log(LOG_ERROR,
"Require a private structure with an owner\n");
368 ast_log(LOG_ERROR,
"Could not allocate an event!\n");
372 start = icalcomponent_get_dtstart(comp);
373 end = icalcomponent_get_dtend(comp);
375 event->start = icaltime_get_tzid(start) ? span->start : icalfloat_to_timet(start);
376 event->end = icaltime_get_tzid(end) ? span->end : icalfloat_to_timet(end);
377 event->busy_state = span->is_busy ? AST_CALENDAR_BS_BUSY : AST_CALENDAR_BS_FREE;
379 if ((prop = icalcomponent_get_first_property(comp, ICAL_SUMMARY_PROPERTY))) {
383 if ((prop = icalcomponent_get_first_property(comp, ICAL_DESCRIPTION_PROPERTY))) {
387 if ((prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY))) {
391 if ((prop = icalcomponent_get_first_property(comp, ICAL_LOCATION_PROPERTY))) {
395 if ((prop = icalcomponent_get_first_property(comp, ICAL_CATEGORIES_PROPERTY))) {
399 if ((prop = icalcomponent_get_first_property(comp, ICAL_PRIORITY_PROPERTY))) {
400 event->priority = icalvalue_get_integer(icalproperty_get_value(prop));
403 if ((prop = icalcomponent_get_first_property(comp, ICAL_UID_PROPERTY))) {
406 ast_log(LOG_WARNING,
"No UID found, but one is required. Generating, but updates may not be accurate\n");
407 if (!ast_strlen_zero(event->summary)) {
410 char tmp[AST_TIME_T_LEN];
417 for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
418 prop; prop = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY)) {
422 if (!(attendee =
ast_calloc(1,
sizeof(*attendee)))) {
426 data = icalproperty_get_attendee(prop);
427 if (ast_strlen_zero(data)) {
439 if (!(valarm = icalcomponent_get_first_component(comp, ICAL_VALARM_COMPONENT))) {
445 if (!(prop = icalcomponent_get_first_property(valarm, ICAL_TRIGGER_PROPERTY))) {
446 ast_log(LOG_WARNING,
"VALARM has no TRIGGER, skipping!\n");
452 trigger = icalproperty_get_trigger(prop);
454 if (icaltriggertype_is_null_trigger(trigger)) {
455 ast_log(LOG_WARNING,
"Bad TRIGGER for VALARM, skipping!\n");
461 if (!icaltime_is_null_time(trigger.time)) {
462 tmp = icaltime_convert_to_zone(trigger.time, utc);
463 event->alarm = icaltime_as_timet_with_zone(tmp, utc);
467 tmp = icaltime_add(start, trigger.duration);
468 event->alarm = icaltime_as_timet_with_zone(tmp, icaltime_get_timezone(start));
485 static const xmlChar *caldav_node_localname = BAD_CAST
"calendar-data";
486 static const xmlChar *caldav_node_nsuri = BAD_CAST
"urn:ietf:params:xml:ns:caldav";
488 static void handle_start_element(xmlTextReaderPtr reader,
struct xmlstate *
state)
490 const xmlChar *localname = xmlTextReaderConstLocalName(reader);
491 const xmlChar *uri = xmlTextReaderConstNamespaceUri(reader);
493 if (!xmlStrEqual(localname, caldav_node_localname) || !xmlStrEqual(uri, caldav_node_nsuri)) {
497 state->in_caldata = 1;
501 static void handle_end_element(xmlTextReaderPtr reader,
struct xmlstate *state)
503 struct icaltimetype start, end;
504 icaltimezone *utc = icaltimezone_get_utc_timezone();
507 const xmlChar *localname = xmlTextReaderConstLocalName(reader);
508 const xmlChar *uri = xmlTextReaderConstNamespaceUri(reader);
510 if (!xmlStrEqual(localname, caldav_node_localname) || !xmlStrEqual(uri, caldav_node_nsuri)) {
514 state->in_caldata = 0;
520 start = icaltime_from_timet_with_zone(state->start, 0, utc);
521 end = icaltime_from_timet_with_zone(state->end, 0, utc);
524 for (iter = icalcomponent_get_first_component(comp, ICAL_VEVENT_COMPONENT);
526 iter = icalcomponent_get_next_component(comp, ICAL_VEVENT_COMPONENT))
528 icalcomponent_foreach_recurrence(iter, start, end, caldav_add_event, state->pvt);
531 icalcomponent_free(comp);
534 static void handle_characters(xmlTextReaderPtr reader,
struct xmlstate *state)
538 if (!state->in_caldata) {
542 text = xmlTextReaderValue(reader);
549 static void parse_error_handler(
void *arg,
const char *msg,
550 xmlParserSeverities severity, xmlTextReaderLocatorPtr locator)
553 case XML_PARSER_SEVERITY_VALIDITY_WARNING:
554 case XML_PARSER_SEVERITY_WARNING:
555 ast_log(LOG_WARNING,
"While parsing CalDAV response at line %d: %s\n",
556 xmlTextReaderLocatorLineNumber(locator),
559 case XML_PARSER_SEVERITY_VALIDITY_ERROR:
560 case XML_PARSER_SEVERITY_ERROR:
562 ast_log(LOG_ERROR,
"While parsing CalDAV response at line %d: %s\n",
563 xmlTextReaderLocatorLineNumber(locator),
569 static int update_caldav(
struct caldav_pvt *pvt)
574 xmlTextReaderPtr reader;
581 end = now.tv_sec + 60 * pvt->owner->
timeframe;
582 if (!(response = caldav_get_events_between(pvt, start, end))) {
594 reader = xmlReaderForMemory(
604 xmlTextReaderSetErrorHandler(reader, parse_error_handler, NULL);
606 res = xmlTextReaderRead(reader);
608 int node_type = xmlTextReaderNodeType(reader);
610 case XML_READER_TYPE_ELEMENT:
611 handle_start_element(reader, &state);
613 case XML_READER_TYPE_END_ELEMENT:
614 handle_end_element(reader, &state);
616 case XML_READER_TYPE_TEXT:
617 case XML_READER_TYPE_CDATA:
618 handle_characters(reader, &state);
623 res = xmlTextReaderRead(reader);
625 xmlFreeTextReader(reader);
631 ast_free(state.cdata);
636 static int verify_cert(
void *userdata,
int failures,
const ne_ssl_certificate *cert)
642 static void *caldav_load_calendar(
void *void_data)
651 ast_log(LOG_ERROR,
"You must enable calendar support for res_caldav to load\n");
655 if (ao2_trylock(cal)) {
656 if (cal->unloading) {
657 ast_log(LOG_WARNING,
"Unloading module, load_calendar cancelled.\n");
659 ast_log(LOG_WARNING,
"Could not lock calendar, aborting!\n");
665 if (!(pvt = ao2_alloc(
sizeof(*pvt), caldav_destructor))) {
666 ast_log(LOG_ERROR,
"Could not allocate caldav_pvt structure for calendar: %s\n", cal->
name);
674 ast_log(LOG_ERROR,
"Could not allocate space for fetching events for calendar: %s\n", cal->
name);
675 pvt = unref_caldav(pvt);
682 ast_log(LOG_ERROR,
"Couldn't allocate string field space for calendar: %s\n", cal->
name);
683 pvt = unref_caldav(pvt);
689 for (v = ast_variable_browse(cfg, cal->
name); v; v = v->
next) {
690 if (!strcasecmp(v->
name,
"url")) {
692 }
else if (!strcasecmp(v->
name,
"user")) {
694 }
else if (!strcasecmp(v->
name,
"secret")) {
701 if (ast_strlen_zero(pvt->url)) {
702 ast_log(LOG_WARNING,
"No URL was specified for CalDAV calendar '%s' - skipping.\n", cal->
name);
703 pvt = unref_caldav(pvt);
708 if (ne_uri_parse(pvt->url, &pvt->uri) || pvt->uri.host == NULL || pvt->uri.path == NULL) {
709 ast_log(LOG_WARNING,
"Could not parse url '%s' for CalDAV calendar '%s' - skipping.\n", pvt->url, cal->
name);
710 pvt = unref_caldav(pvt);
715 if (pvt->uri.scheme == NULL) {
716 pvt->uri.scheme =
"http";
719 if (pvt->uri.port == 0) {
720 pvt->uri.port = ne_uri_defaultport(pvt->uri.scheme);
723 pvt->session = ne_session_create(pvt->uri.scheme, pvt->uri.host, pvt->uri.port);
724 ne_redirect_register(pvt->session);
725 ne_set_server_auth(pvt->session, auth_credentials, pvt);
726 if (!strcasecmp(pvt->uri.scheme,
"https")) {
727 ne_ssl_trust_default_ca(pvt->session);
728 ne_ssl_set_verify(pvt->session, verify_cert, NULL);
733 ast_mutex_init(&refreshlock);
743 struct timespec ts = {0,};
745 ts.tv_sec = tv.tv_sec + (60 * pvt->owner->
refresh);
747 ast_mutex_lock(&refreshlock);
748 while (!pvt->owner->unloading) {
749 if (ast_cond_timedwait(&pvt->owner->unload, &refreshlock, &ts) == ETIMEDOUT) {
753 ast_mutex_unlock(&refreshlock);
755 if (pvt->owner->unloading) {
756 ast_debug(10,
"Skipping refresh since we got a shutdown signal\n");
768 static int load_module(
void)
779 static int unload_module(
void)
786 AST_MODULE_INFO(
ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER,
"Asterisk CalDAV Calendar Integration",
787 .support_level = AST_MODULE_SUPPORT_EXTENDED,
789 .unload = unload_module,
791 .requires =
"res_calendar",
struct ast_variable * next
Asterisk locking-related definitions:
int ast_calendar_register(struct ast_calendar_tech *tech)
Register a new calendar technology.
Asterisk main include file. File version handling, generic pbx functions.
int ast_time_t_to_string(time_t time, char *buf, size_t length)
Converts to a string representation of a time_t as decimal seconds since the epoch. Returns -1 on failure, zero otherwise.
char * ast_str_buffer(const struct ast_str *buf)
Returns the string buffer within the ast_str buf.
#define ao2_callback(c, flags, cb_fn, arg)
ao2_callback() is a generic function that applies cb_fn() to all objects in a container, as described below.
Structure for variables, used for configurations and for channel variables.
int ast_str_append(struct ast_str **buf, ssize_t max_len, const char *fmt,...)
Append to a thread local dynamic string.
enum ast_calendar_busy_state busy_state
struct timeval ast_tvnow(void)
Returns current timeval. Meant to replace calls to gettimeofday().
#define AST_DECLARE_STRING_FIELDS(field_list)
Declare the fields needed in a structure.
#define ast_strdup(str)
A wrapper for strdup()
int ast_str_set(struct ast_str **buf, ssize_t max_len, const char *fmt,...)
Set a dynamic string using variable arguments.
void ast_calendar_merge_events(struct ast_calendar *cal, struct ao2_container *new_events)
Add an event to the list of events for a calendar.
Configuration File Parser.
General Asterisk PBX channel definitions.
#define ast_string_field_init(x, size)
Initialize a field pool and fields.
#define ao2_ref(o, delta)
Reference/unreference an object and return the old refcount.
struct ao2_container * ast_calendar_event_container_alloc(void)
Allocate an astobj2 container for ast_calendar_event objects.
#define AST_STRING_FIELD(name)
Declare a string field.
A general API for managing calendar events with Asterisk.
static struct stasis_rest_handlers events
REST handler for /api-docs/events.json.
#define ast_malloc(len)
A wrapper for malloc()
#define ast_debug(level,...)
Log a DEBUG message.
const ast_string_field name
struct ast_calendar_event * ast_calendar_event_alloc(struct ast_calendar *cal)
Allocate an astobj2 ast_calendar_event object.
#define AST_LIST_INSERT_TAIL(head, elm, field)
Appends a list entry to the tail of a list.
Support for dynamic strings.
#define ast_calloc(num, len)
A wrapper for calloc()
const struct ast_config * ast_calendar_config_acquire(void)
Grab and lock pointer to the calendar config (read only)
Module has failed to load, may be in an inconsistent state.
struct ast_calendar_event * ast_calendar_unref_event(struct ast_calendar_event *event)
Unreference an ast_calendar_event.
structure to hold users read from users.conf
#define ast_string_field_build(x, field, fmt, args...)
Set a field to a complex (built) value.
void ast_str_reset(struct ast_str *buf)
Reset the content of a dynamic string. Useful before a series of ast_str_append.
void ast_calendar_unregister(struct ast_calendar_tech *tech)
Unregister a new calendar technology.
size_t ast_str_strlen(const struct ast_str *buf)
Returns the current length of the string stored within buf.
struct timeval ast_mktime(struct ast_tm *const tmp, const char *zone)
Timezone-independent version of mktime(3).
Individual calendaring technology data.
Asterisk calendar structure.
void ast_calendar_config_release(void)
Release the calendar config.
#define ASTERISK_GPL_KEY
The text the key() function should return.
Asterisk module definitions.
#define ast_string_field_free_memory(x)
free all memory - to be called before destroying the object
Structure for mutex and tracking information.
#define ast_str_create(init_len)
Create a malloc'ed dynamic length string.
#define ast_string_field_set(x, field, data)
Set a field to a simple string value.
#define ao2_link(container, obj)
Add an object to a container.