mirror of https://github.com/asterisk/asterisk
This commit add Calendaring support to Asterisk for iCalendar, CalDAV, and MS Exchange calendars. Exchange support has only been tested on Exchange Server 2k3 and does not support forms-based authentication at this time (patches *very* welcome). Exchange support is also currently missing the ability to return a list of a meting's attendees (again, patches are very, very welcome). Features include: Querying a calendar for events over a specific time range Checking a calendar's busy status via the dialplan Writing calendar events via the dialplan (CalDAV and Exchange only) Handling calendar event notifications through the dialplan (closes issue #14771) Tested by: lmadsen, twilson, Shivaprakash Review: https://reviewboard.asterisk.org/r/58 git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@197738 65c4cc65-6c06-0410-ace0-fbb531ad65f3certified/1.8.6
parent
08f0ec4e8e
commit
71a3a2ebf6
@ -0,0 +1,80 @@
|
||||
;[calendar1]
|
||||
;type = ical ; type of calendar--currently supported: ical, caldav, or exchange
|
||||
;url = https://example.com/home/jdoe/Calendar/ ; URL to shared calendar (Zimbra example)
|
||||
;user = jdoe ; web username
|
||||
;secret = supersecret ; web password
|
||||
;refresh = 15 ; refresh calendar every n minutes
|
||||
;timeframe = 60 ; number of minutes of calendar data to pull for each refresh period
|
||||
; ; should always be >= refresh
|
||||
;
|
||||
; You can set up res_icalendar to execute a call upon an upcoming busy status
|
||||
; The following fields are available from the ${CALENDAR_EVENT(<field>)} dialplan function:
|
||||
;
|
||||
; summary : The VEVENT Summary property or Exchange subject
|
||||
; description : The text description of the vent
|
||||
; organizer : The organizer of the event
|
||||
; location : The location field of the event
|
||||
; calendar : The name of the calendar tied to the event
|
||||
; uid : The unique ID for this event
|
||||
; start : Start time of the event
|
||||
; end : The end time of the event
|
||||
; busystate : 0=FREE, 1=TENTATIVE, 2=BUSY
|
||||
;
|
||||
;autoreminder = 10 ; Override event-defined reminder before each busy status (in mins)
|
||||
;
|
||||
;channel = SIP/60001 ; Channel to dial
|
||||
;context = default ; Context to connect to on answer
|
||||
;extension = 123 ; Extension to connect to on answer
|
||||
;
|
||||
; or
|
||||
;
|
||||
;app = Playback ; Application to execute on answer (instead of context/extension)
|
||||
;appdata = tt-weasels ; Data part of application to execute on answer
|
||||
;
|
||||
;waittime = 30 ; How long to wait for an answer
|
||||
|
||||
;[calendar2]
|
||||
;type = exchange ; type of calendar--currently supported: ical, caldav, or exchange
|
||||
;url = https://example.com/exchange/jdoe ; URL to MS Exchange OWA for user (usually includes exchange/user)
|
||||
;user = jdoe ; Exchange username
|
||||
;secret = mysecret ; Exchange password
|
||||
;refresh = 15 ; refresh calendar every n minutes
|
||||
;timeframe = 60 ; number of minutes of calendar data to pull for each refresh period
|
||||
; ; should always be >= refresh
|
||||
;
|
||||
; You can set up res_icalendar to execute a call upon an upcoming busy status
|
||||
;autoreminder = 10 ; Override event-defined reminder before each busy status (in mins)
|
||||
;
|
||||
;channel = SIP/1234 ; Channel to dial
|
||||
;context = default ; Context to connect to on answer
|
||||
;extension = 1234 ; Extension to connect to on answer
|
||||
;
|
||||
; or
|
||||
;
|
||||
;app = Playback ; Application to execute on answer (instead of context/extension)
|
||||
;appdata = tt-weasels ; Data part of application to execute on answer
|
||||
;
|
||||
;waittime = 30 ; How long to wait for an answer
|
||||
|
||||
;[calendar3]
|
||||
;type = caldav ; type of calendar--currently supported: ical, caldav, or exchange
|
||||
;url = https://www.google.com/calendar/dav/username@gmail.com/events/ ; Main GMail calendar (the trailing slash is significant!)
|
||||
;user = jdoe@gmail.com ; username
|
||||
;secret = mysecret ; password
|
||||
;refresh = 15 ; refresh calendar every n minutes
|
||||
;timeframe = 60 ; number of minutes of calendar data to pull for each refresh period
|
||||
; ; should always be >= refresh
|
||||
;
|
||||
; You can set up res_icalendar to execute a call upon an upcoming busy status
|
||||
;autoreminder = 10 ; Override event-defined reminder before each busy status (in mins)
|
||||
;
|
||||
;channel = SIP/1234 ; Channel to dial
|
||||
;context = default ; Context to connect to on answer
|
||||
;extension = 1234 ; Extension to connect to on answer
|
||||
;
|
||||
; or
|
||||
;
|
||||
;app = Playback ; Application to execute on answer (instead of context/extension)
|
||||
;appdata = tt-weasels ; Data part of application to execute on answer
|
||||
;
|
||||
;waittime = 30 ; How long to wait for an answer
|
@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Asterisk -- An open source telephony toolkit.
|
||||
*
|
||||
* Copyright (C) 2008 - 2009, Digium, Inc.
|
||||
*
|
||||
* Terry Wilson <twilson@digium.com>
|
||||
*
|
||||
* See http://www.asterisk.org for more information about
|
||||
* the Asterisk project. Please do not directly contact
|
||||
* any of the maintainers of this project for assistance;
|
||||
* the project provides a web site, mailing lists and IRC
|
||||
* channels for your use.
|
||||
*
|
||||
* This program is free software, distributed under the terms of
|
||||
* the GNU General Public License Version 2. See the LICENSE file
|
||||
* at the top of the source tree.
|
||||
*/
|
||||
|
||||
#ifndef _ASTERISK_CALENDAR_H
|
||||
#define _ASTERISK_CALENDAR_H
|
||||
|
||||
#include "asterisk.h"
|
||||
#include "asterisk/stringfields.h"
|
||||
#include "asterisk/config.h"
|
||||
#include "asterisk/linkedlists.h"
|
||||
#include "asterisk/lock.h"
|
||||
|
||||
/*! \file calendar.h
|
||||
* \brief A general API for managing calendar events with Asterisk
|
||||
*
|
||||
* \author Terry Wilson <twilson@digium.com>
|
||||
*
|
||||
* \note This API implements an abstraction for handling different calendaring
|
||||
* technologies in Asterisk. The services provided by the API are a dialplan
|
||||
* function to query whether or not a calendar is busy at the present time, a
|
||||
* adialplan function to query specific information about events in a time range,
|
||||
* a devicestate provider, and notification of calendar events through execution
|
||||
* of dialplan apps or dialplan logic at a specific context and extension. The
|
||||
* information available through the CALENDAR_EVENT() dialplan function are:
|
||||
*
|
||||
* SUMMARY, DESCRIPTION, ORGANIZER, LOCATION
|
||||
* CALENDAR, UID, START, END, and BUSYSTATE
|
||||
*
|
||||
* BUSYSTATE can have the values 0 (free), 1 (tentatively busy), or 2 (busy)
|
||||
*
|
||||
* Usage
|
||||
* All calendaring configuration data is located in calendar.conf and is only read
|
||||
* directly by the Calendaring API. Each calendar technology resource must register
|
||||
* a load_calendar callback which will be passed an ast_calendar_load_data structure.
|
||||
* The load_calendar callback function should then set the values it needs from this
|
||||
* cfg, load the calendar data, and then loop updating the calendar data and events
|
||||
* baesd on the refresh interval in the ast_calendar object. Each call to
|
||||
* the load_calendar callback will be will run in its own thread.
|
||||
*
|
||||
* Updating events involves creating an astobj2 container of new events and passing
|
||||
* it to the API through ast_calendar_merge_events.
|
||||
*
|
||||
* Calendar technology resource modules must also register an unref_calendar callback
|
||||
* which will only be called when the resource module calls ast_calendar_unregister()
|
||||
* to unregister that module's calendar type (usually done in module_unload())
|
||||
*/
|
||||
|
||||
extern struct ast_config *calendar_config;
|
||||
|
||||
struct ast_calendar;
|
||||
struct ast_calendar_event;
|
||||
|
||||
/*! \brief Individual calendaring technology data */
|
||||
struct ast_calendar_tech {
|
||||
const char *type;
|
||||
const char *description;
|
||||
const char *module;
|
||||
int (* is_busy)(struct ast_calendar *calendar); /*!< Override default busy determination */
|
||||
void *(* load_calendar)(void *data); /*!< Create private structure, add calendar events, etc. */
|
||||
void *(* unref_calendar)(void *obj); /*!< Function to be called to free the private structure */
|
||||
int (* write_event)(struct ast_calendar_event *event); /*!< Function for writing an event to the calendar */
|
||||
AST_LIST_ENTRY(ast_calendar_tech) list;
|
||||
};
|
||||
|
||||
enum ast_calendar_busy_state {
|
||||
AST_CALENDAR_BS_FREE = 0,
|
||||
AST_CALENDAR_BS_BUSY_TENTATIVE,
|
||||
AST_CALENDAR_BS_BUSY,
|
||||
};
|
||||
|
||||
struct ast_calendar_attendee {
|
||||
char *data;
|
||||
AST_LIST_ENTRY(ast_calendar_attendee) next;
|
||||
};
|
||||
|
||||
/* \brief Calendar events */
|
||||
struct ast_calendar_event {
|
||||
AST_DECLARE_STRING_FIELDS(
|
||||
AST_STRING_FIELD(summary);
|
||||
AST_STRING_FIELD(description);
|
||||
AST_STRING_FIELD(organizer);
|
||||
AST_STRING_FIELD(location);
|
||||
AST_STRING_FIELD(uid);
|
||||
);
|
||||
struct ast_calendar *owner; /*!< The calendar that owns this event */
|
||||
time_t start; /*!< Start of event (UTC) */
|
||||
time_t end; /*!< End of event (UTC) */
|
||||
time_t alarm; /*!< Time for event notification */
|
||||
enum ast_calendar_busy_state busy_state; /*!< The busy status of the event */
|
||||
int notify_sched; /*!< The sched for event notification */
|
||||
int bs_start_sched; /*!< The sched for changing the device state at the start of an event */
|
||||
int bs_end_sched; /*!< The sched for changing the device state at the end of an event */
|
||||
AST_LIST_HEAD_NOLOCK(attendees, ast_calendar_attendee) attendees;
|
||||
};
|
||||
|
||||
/*! \brief Asterisk calendar structure */
|
||||
struct ast_calendar {
|
||||
const struct ast_calendar_tech *tech;
|
||||
void *tech_pvt;
|
||||
AST_DECLARE_STRING_FIELDS(
|
||||
AST_STRING_FIELD(name); /*!< Name from config file [name] */
|
||||
AST_STRING_FIELD(notify_channel); /*!< Channel to use for notification */
|
||||
AST_STRING_FIELD(notify_context); /*!< Optional context to execute from for notification */
|
||||
AST_STRING_FIELD(notify_extension); /*!< Optional extension to execute from for notification */
|
||||
AST_STRING_FIELD(notify_app); /*!< Optional dialplan app to execute for notification */
|
||||
AST_STRING_FIELD(notify_appdata); /*!< Optional arguments for dialplan app */
|
||||
);
|
||||
int autoreminder; /*!< If set, override any calendar_tech specific notification times and use this time (in mins) */
|
||||
int notify_waittime; /*!< Maxiumum time to allow for a notification attempt */
|
||||
int refresh; /*!< When to refresh the calendar events */
|
||||
int timeframe; /*!< Span (in mins) of calendar data to pull with each request */
|
||||
pthread_t thread; /*!< The thread that the calendar is loaded/updated in */
|
||||
ast_cond_t unload;
|
||||
int unloading:1;
|
||||
int pending_deletion:1;
|
||||
struct ao2_container *events; /*!< The events that are known at this time */
|
||||
};
|
||||
|
||||
/*! \brief Register a new calendar technology
|
||||
*
|
||||
* \param tech calendar technology to register
|
||||
*
|
||||
* \retval 0 success
|
||||
* \retval -1 failure
|
||||
*/
|
||||
int ast_calendar_register(struct ast_calendar_tech *tech);
|
||||
|
||||
/*! \brief Unregister a new calendar technology
|
||||
*
|
||||
* \param tech calendar technology to unregister
|
||||
*
|
||||
* \retval 0 success
|
||||
* \retva -1 failure
|
||||
*/
|
||||
void ast_calendar_unregister(struct ast_calendar_tech *tech);
|
||||
|
||||
/*! \brief Allocate an astobj2 ast_calendar_event object
|
||||
*
|
||||
* \param cal calendar to allocate an event for
|
||||
*
|
||||
* \return a new, initialized calendar event
|
||||
*/
|
||||
struct ast_calendar_event *ast_calendar_event_alloc(struct ast_calendar *cal);
|
||||
|
||||
/*! \brief Allocate an astobj2 container for ast_calendar_event objects
|
||||
*
|
||||
* \return a new event container
|
||||
*/
|
||||
struct ao2_container *ast_calendar_event_container_alloc(void);
|
||||
|
||||
/*! \brief Add an event to the list of events for a calendar
|
||||
*
|
||||
* \param cal calendar containing the events to be merged
|
||||
* \param new_events an oa2 container of events to be merged into cal->events
|
||||
*/
|
||||
void ast_calendar_merge_events(struct ast_calendar *cal, struct ao2_container *new_events);
|
||||
|
||||
/*! \brief Unreference an ast_calendar_event
|
||||
*
|
||||
* \param event event to unref
|
||||
*
|
||||
* \return NULL
|
||||
*/
|
||||
struct ast_calendar_event *ast_calendar_unref_event(struct ast_calendar_event *event);
|
||||
|
||||
/*! \brief Remove all events from calendar
|
||||
*
|
||||
* \param cal calendar whose events need to be cleared
|
||||
*/
|
||||
void ast_calendar_clear_events(struct ast_calendar *cal);
|
||||
|
||||
#endif /* _ASTERISK_CALENDAR_H */
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,7 @@
|
||||
{
|
||||
global:
|
||||
ast_*;
|
||||
calendar_config;
|
||||
local:
|
||||
*;
|
||||
};
|
@ -0,0 +1,675 @@
|
||||
/*
|
||||
* Asterisk -- An open source telephony toolkit.
|
||||
*
|
||||
* Copyright (C) 2008 - 2009, Digium, Inc.
|
||||
*
|
||||
* Terry Wilson <twilson@digium.com>
|
||||
*
|
||||
* See http://www.asterisk.org for more information about
|
||||
* the Asterisk project. Please do not directly contact
|
||||
* any of the maintainers of this project for assistance;
|
||||
* the project provides a web site, mailing lists and IRC
|
||||
* channels for your use.
|
||||
*
|
||||
* This program is free software, distributed under the terms of
|
||||
* the GNU General Public License Version 2. See the LICENSE file
|
||||
* at the top of the source tree.
|
||||
*/
|
||||
|
||||
/*! \file
|
||||
* \brief Resource for handling iCalnedar calendars
|
||||
*/
|
||||
|
||||
/*** MODULEINFO
|
||||
<depend>neon</depend>
|
||||
<depend>ical</depend>
|
||||
<depend>libxml2</depend>
|
||||
***/
|
||||
#include "asterisk.h"
|
||||
|
||||
ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
|
||||
|
||||
#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 <libxml/xmlmemory.h>
|
||||
#include <libxml/parser.h>
|
||||
|
||||
#include "asterisk/module.h"
|
||||
#include "asterisk/calendar.h"
|
||||
#include "asterisk/lock.h"
|
||||
#include "asterisk/config.h"
|
||||
#include "asterisk/astobj2.h"
|
||||
|
||||
static void *caldav_load_calendar(void *data);
|
||||
static void *unref_caldav(void *obj);
|
||||
static int caldav_write_event(struct ast_calendar_event *event);
|
||||
|
||||
static struct ast_calendar_tech caldav_tech = {
|
||||
.type = "caldav",
|
||||
.description = "CalDAV calendars",
|
||||
.module = AST_MODULE,
|
||||
.load_calendar = caldav_load_calendar,
|
||||
.unref_calendar = unref_caldav,
|
||||
.write_event = caldav_write_event,
|
||||
};
|
||||
|
||||
struct caldav_pvt {
|
||||
AST_DECLARE_STRING_FIELDS(
|
||||
AST_STRING_FIELD(url);
|
||||
AST_STRING_FIELD(user);
|
||||
AST_STRING_FIELD(secret);
|
||||
);
|
||||
struct ast_calendar *owner;
|
||||
ne_uri uri;
|
||||
ne_session *session;
|
||||
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;
|
||||
|
||||
ast_debug(1, "Destroying pvt for CalDAV calendar %s\n", pvt->owner->name);
|
||||
if (pvt->session) {
|
||||
ne_session_destroy(pvt->session);
|
||||
}
|
||||
ast_string_field_free_memory(pvt);
|
||||
|
||||
ao2_callback(pvt->events, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, cb_true, NULL);
|
||||
|
||||
ao2_ref(pvt->events, -1);
|
||||
}
|
||||
|
||||
static void *unref_caldav(void *obj)
|
||||
{
|
||||
struct caldav_pvt *pvt = obj;
|
||||
|
||||
ao2_ref(pvt, -1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int fetch_response_reader(void *data, const char *block, size_t len)
|
||||
{
|
||||
struct ast_str **response = data;
|
||||
unsigned char *tmp;
|
||||
|
||||
if (!(tmp = ast_malloc(len + 1))) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(tmp, block, len);
|
||||
tmp[len] = '\0';
|
||||
ast_str_append(response, 0, "%s", tmp);
|
||||
ast_free(tmp);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int auth_credentials(void *userdata, const char *realm, int attempts, char *username, char *secret)
|
||||
{
|
||||
struct caldav_pvt *pvt = userdata;
|
||||
|
||||
if (attempts > 1) {
|
||||
ast_log(LOG_WARNING, "Invalid username or password for CalDAV calendar '%s'\n", pvt->owner->name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ne_strnzcpy(username, pvt->user, NE_ABUFSIZ);
|
||||
ne_strnzcpy(secret, pvt->secret, NE_ABUFSIZ);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
ne_request *req;
|
||||
int ret;
|
||||
char buf[1000];
|
||||
|
||||
if (!pvt) {
|
||||
ast_log(LOG_ERROR, "There is no private!\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!(response = ast_str_create(512))) {
|
||||
ast_log(LOG_ERROR, "Could not allocate memory for response.\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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_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);
|
||||
|
||||
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));
|
||||
ast_free(response);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static int caldav_write_event(struct ast_calendar_event *event)
|
||||
{
|
||||
struct ast_str *body = NULL, *response = NULL, *subdir = NULL;
|
||||
icalcomponent *calendar, *icalevent;
|
||||
icaltimezone *utc = icaltimezone_get_utc_timezone();
|
||||
int ret = -1;
|
||||
|
||||
if (!event) {
|
||||
ast_log(LOG_WARNING, "No event passed!\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!(event->start && event->end)) {
|
||||
ast_log(LOG_WARNING, "The event must contain a start and an end\n");
|
||||
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");
|
||||
goto write_cleanup;
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
calendar = icalcomponent_new(ICAL_VCALENDAR_COMPONENT);
|
||||
icalcomponent_add_property(calendar, icalproperty_new_version("2.0"));
|
||||
icalcomponent_add_property(calendar, icalproperty_new_prodid("-//Digium, Inc.//res_caldav//EN"));
|
||||
|
||||
icalevent = icalcomponent_new(ICAL_VEVENT_COMPONENT);
|
||||
icalcomponent_add_property(icalevent, icalproperty_new_dtstamp(icaltime_current_time_with_zone(utc)));
|
||||
icalcomponent_add_property(icalevent, icalproperty_new_uid(event->uid));
|
||||
icalcomponent_add_property(icalevent, icalproperty_new_dtstart(icaltime_from_timet_with_zone(event->start, 0, utc)));
|
||||
icalcomponent_add_property(icalevent, icalproperty_new_dtend(icaltime_from_timet_with_zone(event->end, 0, utc)));
|
||||
if (!ast_strlen_zero(event->organizer)) {
|
||||
icalcomponent_add_property(icalevent, icalproperty_new_organizer(event->organizer));
|
||||
}
|
||||
if (!ast_strlen_zero(event->summary)) {
|
||||
icalcomponent_add_property(icalevent, icalproperty_new_summary(event->summary));
|
||||
}
|
||||
if (!ast_strlen_zero(event->description)) {
|
||||
icalcomponent_add_property(icalevent, icalproperty_new_description(event->description));
|
||||
}
|
||||
if (!ast_strlen_zero(event->location)) {
|
||||
icalcomponent_add_property(icalevent, icalproperty_new_location(event->location));
|
||||
}
|
||||
|
||||
switch (event->busy_state) {
|
||||
case AST_CALENDAR_BS_BUSY:
|
||||
icalcomponent_add_property(icalevent, icalproperty_new_status(ICAL_STATUS_CONFIRMED));
|
||||
break;
|
||||
|
||||
case AST_CALENDAR_BS_BUSY_TENTATIVE:
|
||||
icalcomponent_add_property(icalevent, icalproperty_new_status(ICAL_STATUS_TENTATIVE));
|
||||
break;
|
||||
|
||||
default:
|
||||
icalcomponent_add_property(icalevent, icalproperty_new_status(ICAL_STATUS_NONE));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
response = caldav_request(event->owner->tech_pvt, "PUT", body, subdir, "text/calendar");
|
||||
|
||||
ret = 0;
|
||||
|
||||
write_cleanup:
|
||||
if (body) {
|
||||
ast_free(body);
|
||||
}
|
||||
if (response) {
|
||||
ast_free(response);
|
||||
}
|
||||
if (subdir) {
|
||||
ast_free(subdir);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static struct ast_str *caldav_get_events_between(struct caldav_pvt *pvt, time_t start_time, time_t end_time)
|
||||
{
|
||||
struct ast_str *body, *response;
|
||||
icaltimezone *utc = icaltimezone_get_utc_timezone();
|
||||
icaltimetype start, end;
|
||||
const char *start_str, *end_str;
|
||||
|
||||
if (!(body = ast_str_create(512))) {
|
||||
ast_log(LOG_ERROR, "Could not allocate memory for body of request!\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
start = icaltime_from_timet_with_zone(start_time, 0, utc);
|
||||
end = icaltime_from_timet_with_zone(end_time, 0, utc);
|
||||
start_str = icaltime_as_ical_string(start);
|
||||
end_str = icaltime_as_ical_string(end);
|
||||
|
||||
/* If I was really being efficient, I would store a collection of event URIs and etags,
|
||||
* first doing a query of just the etag and seeing if anything had changed. If it had,
|
||||
* then I would do a request for each of the events that had changed, and only bother
|
||||
* updating those. Oh well. */
|
||||
ast_str_append(&body, 0,
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
|
||||
"<C:calendar-query xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n"
|
||||
" <D:prop>\n"
|
||||
" <C:calendar-data>\n"
|
||||
" <C:expand start=\"%s\" end=\"%s\"/>\n"
|
||||
" </C:calendar-data>\n"
|
||||
" </D:prop>\n"
|
||||
" <C:filter>\n"
|
||||
" <C:comp-filter name=\"VCALENDAR\">\n"
|
||||
" <C:comp-filter name=\"VEVENT\">\n"
|
||||
" <C:time-range start=\"%s\" end=\"%s\"/>\n"
|
||||
" </C:comp-filter>\n"
|
||||
" </C:comp-filter>\n"
|
||||
" </C:filter>\n"
|
||||
"</C:calendar-query>\n", start_str, end_str, start_str, end_str);
|
||||
|
||||
response = caldav_request(pvt, "REPORT", body, NULL, NULL);
|
||||
ast_free(body);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static void caldav_add_event(icalcomponent *comp, struct icaltime_span *span, void *data)
|
||||
{
|
||||
struct caldav_pvt *pvt = data;
|
||||
struct ast_calendar_event *event;
|
||||
icaltimezone *utc = icaltimezone_get_utc_timezone();
|
||||
icaltimetype start, end, tmp;
|
||||
icalcomponent *valarm;
|
||||
icalproperty *prop;
|
||||
struct icaltriggertype trigger;
|
||||
|
||||
if (!(pvt && pvt->owner)) {
|
||||
ast_log(LOG_ERROR, "Require a private structure with an owner\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(event = ast_calendar_event_alloc(pvt->owner))) {
|
||||
ast_log(LOG_ERROR, "Could not allocate an event!\n");
|
||||
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;
|
||||
|
||||
default:
|
||||
event->busy_state = 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));
|
||||
}
|
||||
|
||||
if ((prop = icalcomponent_get_first_property(comp, ICAL_DESCRIPTION_PROPERTY))) {
|
||||
ast_string_field_set(event, description, icalproperty_get_value_as_string(prop));
|
||||
}
|
||||
|
||||
if ((prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY))) {
|
||||
ast_string_field_set(event, organizer, icalproperty_get_value_as_string(prop));
|
||||
}
|
||||
|
||||
if ((prop = icalcomponent_get_first_property(comp, ICAL_LOCATION_PROPERTY))) {
|
||||
ast_string_field_set(event, location, icalproperty_get_value_as_string(prop));
|
||||
}
|
||||
|
||||
if ((prop = icalcomponent_get_first_property(comp, ICAL_UID_PROPERTY))) {
|
||||
ast_string_field_set(event, uid, icalproperty_get_value_as_string(prop));
|
||||
} else {
|
||||
ast_log(LOG_WARNING, "No UID found, but one is required. Generating, but updates may not be acurate\n");
|
||||
if (!ast_strlen_zero(event->summary)) {
|
||||
ast_string_field_set(event, uid, event->summary);
|
||||
} else {
|
||||
char tmp[100];
|
||||
snprintf(tmp, sizeof(tmp), "%lu", event->start);
|
||||
ast_string_field_set(event, uid, tmp);
|
||||
}
|
||||
}
|
||||
|
||||
/* Get the attendees */
|
||||
for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
|
||||
prop; prop = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY)) {
|
||||
struct ast_calendar_attendee *attendee;
|
||||
const char *data;
|
||||
|
||||
if (!(attendee = ast_calloc(1, sizeof(*attendee)))) {
|
||||
event = ast_calendar_unref_event(event);
|
||||
return;
|
||||
}
|
||||
data = icalproperty_get_attendee(prop);
|
||||
if (!ast_strlen_zero(data)) {
|
||||
attendee->data = ast_strdup(data);;
|
||||
AST_LIST_INSERT_TAIL(&event->attendees, attendee, next);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Only set values for alarm based on VALARM. Can be overriden in main/calendar.c by autoreminder
|
||||
* therefore, go ahead and add events even if their is no VALARM or it is malformed
|
||||
* Currently we are only getting the first VALARM and are handling repitition in main/calendar.c from calendar.conf */
|
||||
if (!(valarm = icalcomponent_get_first_component(comp, ICAL_VALARM_COMPONENT))) {
|
||||
ao2_link(pvt->events, event);
|
||||
event = ast_calendar_unref_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(prop = icalcomponent_get_first_property(valarm, ICAL_TRIGGER_PROPERTY))) {
|
||||
ast_log(LOG_WARNING, "VALARM has no TRIGGER, skipping!\n");
|
||||
ao2_link(pvt->events, event);
|
||||
event = ast_calendar_unref_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
trigger = icalproperty_get_trigger(prop);
|
||||
|
||||
if (icaltriggertype_is_null_trigger(trigger)) {
|
||||
ast_log(LOG_WARNING, "Bad TRIGGER for VALARM, skipping!\n");
|
||||
ao2_link(pvt->events, event);
|
||||
event = ast_calendar_unref_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!icaltime_is_null_time(trigger.time)) { /* This is an absolute time */
|
||||
tmp = icaltime_convert_to_zone(trigger.time, utc);
|
||||
event->alarm = icaltime_as_timet_with_zone(tmp, utc);
|
||||
} else { /* Offset from either dtstart or dtend */
|
||||
/* 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);
|
||||
}
|
||||
|
||||
ao2_link(pvt->events, event);
|
||||
event = ast_calendar_unref_event(event);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
struct xmlstate {
|
||||
int in_caldata;
|
||||
struct caldav_pvt *pvt;
|
||||
struct ast_str *cdata;
|
||||
time_t start;
|
||||
time_t end;
|
||||
};
|
||||
|
||||
static void handle_start_element(void *data, const xmlChar *fullname, const xmlChar **atts)
|
||||
{
|
||||
struct xmlstate *state = data;
|
||||
|
||||
if (!xmlStrcasecmp(fullname, BAD_CAST "C:calendar-data")) {
|
||||
state->in_caldata = 1;
|
||||
ast_str_reset(state->cdata);
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_end_element(void *data, const xmlChar *name)
|
||||
{
|
||||
struct xmlstate *state = data;
|
||||
struct icaltimetype start, end;
|
||||
icaltimezone *utc = icaltimezone_get_utc_timezone();
|
||||
icalcomponent *iter;
|
||||
icalcomponent *comp;
|
||||
|
||||
if (xmlStrcasecmp(name, BAD_CAST "C:calendar-data")) {
|
||||
return;
|
||||
}
|
||||
|
||||
state->in_caldata = 0;
|
||||
if (!(state->cdata && ast_str_strlen(state->cdata))) {
|
||||
return;
|
||||
}
|
||||
/* XXX Parse the calendar blurb for recurrence events in the time range,
|
||||
* create an event, and add it to pvt->events */
|
||||
start = icaltime_from_timet_with_zone(state->start, 0, utc);
|
||||
end = icaltime_from_timet_with_zone(state->end, 0, utc);
|
||||
comp = icalparser_parse_string(ast_str_buffer(state->cdata));
|
||||
|
||||
for (iter = icalcomponent_get_first_component(comp, ICAL_VEVENT_COMPONENT);
|
||||
iter;
|
||||
iter = icalcomponent_get_next_component(comp, ICAL_VEVENT_COMPONENT))
|
||||
{
|
||||
icalcomponent_foreach_recurrence(iter, start, end, caldav_add_event, state->pvt);
|
||||
}
|
||||
|
||||
icalcomponent_free(comp);
|
||||
}
|
||||
|
||||
static void handle_characters(void *data, const xmlChar *ch, int len)
|
||||
{
|
||||
struct xmlstate *state = data;
|
||||
xmlChar *tmp;
|
||||
|
||||
if (!state->in_caldata) {
|
||||
return;
|
||||
}
|
||||
|
||||
tmp = xmlStrndup(ch, len);
|
||||
ast_str_append(&state->cdata, 0, "%s", (char *)tmp);
|
||||
xmlFree(tmp);
|
||||
}
|
||||
|
||||
static int update_caldav(struct caldav_pvt *pvt)
|
||||
{
|
||||
struct timeval now = ast_tvnow();
|
||||
time_t start, end;
|
||||
struct ast_str *response;
|
||||
xmlSAXHandler saxHandler;
|
||||
struct xmlstate state = {
|
||||
.in_caldata = 0,
|
||||
.pvt = pvt
|
||||
};
|
||||
|
||||
start = now.tv_sec;
|
||||
end = now.tv_sec + 60 * pvt->owner->timeframe;
|
||||
if (!(response = caldav_get_events_between(pvt, start, end))) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!(state.cdata = ast_str_create(512))) {
|
||||
ast_free(response);
|
||||
return -1;
|
||||
}
|
||||
|
||||
state.start = start;
|
||||
state.end = end;
|
||||
|
||||
memset(&saxHandler, 0, sizeof(saxHandler));
|
||||
saxHandler.startElement = handle_start_element;
|
||||
saxHandler.endElement = handle_end_element;
|
||||
saxHandler.characters = handle_characters;
|
||||
|
||||
xmlSAXUserParseMemory(&saxHandler, &state, ast_str_buffer(response), ast_str_strlen(response));
|
||||
|
||||
ast_calendar_merge_events(pvt->owner, pvt->events);
|
||||
|
||||
ast_free(response);
|
||||
ast_free(state.cdata);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int verify_cert(void *userdata, int failures, const ne_ssl_certificate *cert)
|
||||
{
|
||||
/* Verify all certs */
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void *caldav_load_calendar(void *void_data)
|
||||
{
|
||||
struct caldav_pvt *pvt;
|
||||
struct ast_variable *v;
|
||||
struct ast_calendar *cal = void_data;
|
||||
ast_mutex_t refreshlock;
|
||||
|
||||
if (!(cal && calendar_config)) {
|
||||
ast_log(LOG_ERROR, "You must enable calendar support for res_caldav to load\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (ao2_trylock(cal)) {
|
||||
if (cal->unloading) {
|
||||
ast_log(LOG_WARNING, "Unloading module, load_calendar cancelled.\n");
|
||||
} else {
|
||||
ast_log(LOG_WARNING, "Could not lock calendar, aborting!\n");
|
||||
}
|
||||
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);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pvt->owner = cal;
|
||||
|
||||
if (!(pvt->events = ast_calendar_event_container_alloc())) {
|
||||
ast_log(LOG_ERROR, "Could not allocate space for fetching events for calendar: %s\n", cal->name);
|
||||
pvt = unref_caldav(pvt);
|
||||
ao2_unlock(cal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (ast_string_field_init(pvt, 32)) {
|
||||
ast_log(LOG_ERROR, "Couldn't allocate string field space for calendar: %s\n", cal->name);
|
||||
pvt = unref_caldav(pvt);
|
||||
ao2_unlock(cal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (v = ast_variable_browse(calendar_config, 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")) {
|
||||
ast_string_field_set(pvt, user, v->value);
|
||||
} else if (!strcasecmp(v->name, "secret")) {
|
||||
ast_string_field_set(pvt, secret, v->value);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
ao2_unlock(cal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (ne_uri_parse(pvt->url, &pvt->uri) || pvt->uri.host == NULL || pvt->uri.path == NULL) {
|
||||
ast_log(LOG_WARNING, "Could not parse url '%s' for CalDAV calendar '%s' - skipping.\n", pvt->url, cal->name);
|
||||
pvt = unref_caldav(pvt);
|
||||
ao2_unlock(cal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (pvt->uri.scheme == NULL) {
|
||||
pvt->uri.scheme = "http";
|
||||
}
|
||||
|
||||
if (pvt->uri.port == 0) {
|
||||
pvt->uri.port = ne_uri_defaultport(pvt->uri.scheme);
|
||||
}
|
||||
|
||||
pvt->session = ne_session_create(pvt->uri.scheme, pvt->uri.host, pvt->uri.port);
|
||||
ne_set_server_auth(pvt->session, auth_credentials, pvt);
|
||||
if (!strncasecmp(pvt->uri.scheme, "https", sizeof(pvt->uri.scheme))) {
|
||||
ne_ssl_trust_default_ca(pvt->session);
|
||||
ne_ssl_set_verify(pvt->session, verify_cert, NULL);
|
||||
}
|
||||
|
||||
cal->tech_pvt = pvt;
|
||||
|
||||
ast_mutex_init(&refreshlock);
|
||||
|
||||
/* Load it the first time */
|
||||
update_caldav(pvt);
|
||||
|
||||
ao2_unlock(cal);
|
||||
|
||||
/* The only writing from another thread will be if unload is true */
|
||||
for (;;) {
|
||||
struct timeval tv = ast_tvnow();
|
||||
struct timespec ts = {0,};
|
||||
|
||||
ts.tv_sec = tv.tv_sec + (60 * pvt->owner->refresh);
|
||||
|
||||
ast_mutex_lock(&refreshlock);
|
||||
while (!pvt->owner->unloading) {
|
||||
if (ast_cond_timedwait(&pvt->owner->unload, &refreshlock, &ts) == ETIMEDOUT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ast_mutex_unlock(&refreshlock);
|
||||
|
||||
if (pvt->owner->unloading) {
|
||||
ast_debug(10, "Skipping refresh since we got a shutdown signal\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ast_debug(10, "Refreshing after %d minute timeout\n", pvt->owner->refresh);
|
||||
|
||||
update_caldav(pvt);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int load_module(void)
|
||||
{
|
||||
ne_sock_init();
|
||||
if (ast_calendar_register(&caldav_tech)) {
|
||||
ne_sock_exit();
|
||||
return AST_MODULE_LOAD_DECLINE;
|
||||
}
|
||||
|
||||
return AST_MODULE_LOAD_SUCCESS;
|
||||
}
|
||||
|
||||
static int unload_module(void)
|
||||
{
|
||||
ast_calendar_unregister(&caldav_tech);
|
||||
ne_sock_exit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Asterisk CalDAV Calendar Integration",
|
||||
.load = load_module,
|
||||
.unload = unload_module,
|
||||
);
|
@ -0,0 +1,757 @@
|
||||
/*
|
||||
* Asterisk -- An open source telephony toolkit.
|
||||
*
|
||||
* Copyright (C) 2008 - 2009, Digium, Inc.
|
||||
*
|
||||
* Terry Wilson <twilson@digium.com>
|
||||
*
|
||||
* See http://www.asterisk.org for more information about
|
||||
* the Asterisk project. Please do not directly contact
|
||||
* any of the maintainers of this project for assistance;
|
||||
* the project provides a web site, mailing lists and IRC
|
||||
* channels for your use.
|
||||
*
|
||||
* This program is free software, distributed under the terms of
|
||||
* the GNU General Public License Version 2. See the LICENSE file
|
||||
* at the top of the source tree.
|
||||
*/
|
||||
|
||||
/*! \file
|
||||
* \brief Resource for handling iCalnedar calendars
|
||||
*/
|
||||
|
||||
/*** MODULEINFO
|
||||
<depend>neon</depend>
|
||||
<depend>ical</depend>
|
||||
<depend>iksemel</depend>
|
||||
***/
|
||||
#include "asterisk.h"
|
||||
|
||||
ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
|
||||
|
||||
#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 <iksemel.h>
|
||||
|
||||
#include "asterisk/module.h"
|
||||
#include "asterisk/calendar.h"
|
||||
#include "asterisk/lock.h"
|
||||
#include "asterisk/config.h"
|
||||
#include "asterisk/astobj2.h"
|
||||
|
||||
static void *exchangecal_load_calendar(void *data);
|
||||
static void *unref_exchangecal(void *obj);
|
||||
static int exchangecal_write_event(struct ast_calendar_event *event);
|
||||
|
||||
static struct ast_calendar_tech exchangecal_tech = {
|
||||
.type = "exchange",
|
||||
.description = "MS Exchange calendars",
|
||||
.module = AST_MODULE,
|
||||
.load_calendar = exchangecal_load_calendar,
|
||||
.unref_calendar = unref_exchangecal,
|
||||
.write_event = exchangecal_write_event,
|
||||
};
|
||||
|
||||
struct exchangecal_pvt {
|
||||
AST_DECLARE_STRING_FIELDS(
|
||||
AST_STRING_FIELD(url);
|
||||
AST_STRING_FIELD(user);
|
||||
AST_STRING_FIELD(secret);
|
||||
);
|
||||
struct ast_calendar *owner;
|
||||
ne_uri uri;
|
||||
ne_session *session;
|
||||
struct ao2_container *events;
|
||||
};
|
||||
|
||||
struct xmlstate {
|
||||
char tag[80];
|
||||
int in_response;
|
||||
int in_propstat;
|
||||
int in_prop;
|
||||
void *ptr;
|
||||
struct exchangecal_pvt *pvt;
|
||||
};
|
||||
|
||||
static int parse_tag(void *data, char *name, char **atts, int type)
|
||||
{
|
||||
struct xmlstate *state = data;
|
||||
char *tmp;
|
||||
|
||||
if ((tmp = strchr(name, ':'))) {
|
||||
tmp++;
|
||||
} else {
|
||||
return IKS_HOOK;
|
||||
}
|
||||
|
||||
ast_copy_string(state->tag, tmp, sizeof(state->tag));
|
||||
|
||||
switch (type) {
|
||||
case IKS_OPEN:
|
||||
if (!strcasecmp(state->tag, "response")) {
|
||||
struct ast_calendar_event *event;
|
||||
|
||||
state->in_response = 1;
|
||||
if (!(event = ast_calendar_event_alloc(state->pvt->owner))) {
|
||||
return IKS_NOMEM;
|
||||
}
|
||||
state->ptr = event;
|
||||
} else if (!strcasecmp(state->tag, "propstat")) {
|
||||
state->in_propstat = 1;
|
||||
} else if (!strcasecmp(state->tag, "prop")) {
|
||||
state->in_prop = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case IKS_CLOSE:
|
||||
if (!strcasecmp(state->tag, "response")) {
|
||||
struct ao2_container *events = state->pvt->events;
|
||||
struct ast_calendar_event *event = state->ptr;
|
||||
|
||||
state->in_response = 0;
|
||||
if (ast_strlen_zero(event->uid)) {
|
||||
ast_log(LOG_ERROR, "This event has no UID, something has gone wrong\n");
|
||||
event = ast_calendar_unref_event(event);
|
||||
return IKS_HOOK;
|
||||
}
|
||||
ao2_link(events, event);
|
||||
event = ast_calendar_unref_event(event);
|
||||
} else if (!strcasecmp(state->tag, "propstat")) {
|
||||
state->in_propstat = 0;
|
||||
} else if (!strcasecmp(state->tag, "prop")) {
|
||||
state->in_prop = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return IKS_OK;
|
||||
}
|
||||
|
||||
return IKS_OK;
|
||||
}
|
||||
|
||||
static time_t mstime_to_time_t(char *mstime)
|
||||
{
|
||||
char *read, *write;
|
||||
icaltimetype tt;
|
||||
for (read = write = mstime; *read; read++) {
|
||||
if (*read == '.') {
|
||||
*write++ = 'Z';
|
||||
*write = '\0';
|
||||
break;
|
||||
}
|
||||
if (*read == '-' || *read == ':')
|
||||
continue;
|
||||
*write = *read;
|
||||
write++;
|
||||
}
|
||||
|
||||
tt = icaltime_from_string(mstime);
|
||||
return icaltime_as_timet(tt);
|
||||
}
|
||||
|
||||
static enum ast_calendar_busy_state msbusy_to_bs(const char *msbusy)
|
||||
{
|
||||
if (!strcasecmp(msbusy, "FREE")) {
|
||||
return AST_CALENDAR_BS_FREE;
|
||||
} else if (!strcasecmp(msbusy, "TENTATIVE")) {
|
||||
return AST_CALENDAR_BS_BUSY_TENTATIVE;
|
||||
} else {
|
||||
return AST_CALENDAR_BS_BUSY;
|
||||
}
|
||||
}
|
||||
|
||||
static int parse_cdata(void *data, char *value, size_t len)
|
||||
{
|
||||
char *str;
|
||||
struct xmlstate *state = data;
|
||||
struct ast_calendar_event *event = state->ptr;
|
||||
|
||||
|
||||
str = ast_skip_blanks(value);
|
||||
|
||||
if (str == value + len)
|
||||
return IKS_OK;
|
||||
|
||||
if (!(str = ast_calloc(1, len + 1))) {
|
||||
return IKS_NOMEM;
|
||||
}
|
||||
memcpy(str, value, len);
|
||||
if (!(state->in_response && state->in_propstat && state->in_prop)) {
|
||||
ast_free(str);
|
||||
return IKS_OK;
|
||||
}
|
||||
/* We use ast_string_field_build here because libiksemel is parsing CDATA with < as
|
||||
* new elements which is a bit odd and shouldn't happen */
|
||||
if (!strcasecmp(state->tag, "subject")) {
|
||||
ast_string_field_build(event, summary, "%s%s", event->summary, str);
|
||||
} else if (!strcasecmp(state->tag, "location")) {
|
||||
ast_string_field_build(event, location, "%s%s", event->location, str);
|
||||
} else if (!strcasecmp(state->tag, "uid")) {
|
||||
ast_string_field_build(event, uid, "%s%s", event->location, str);
|
||||
} else if (!strcasecmp(state->tag, "organizer")) {
|
||||
ast_string_field_build(event, organizer, "%s%s", event->organizer, str);
|
||||
} else if (!strcasecmp(state->tag, "textdescription")) {
|
||||
ast_string_field_build(event, description, "%s%s", event->description, str);
|
||||
} else if (!strcasecmp(state->tag, "dtstart")) {
|
||||
event->start = mstime_to_time_t(str);
|
||||
} else if (!strcasecmp(state->tag, "dtend")) {
|
||||
event->end = mstime_to_time_t(str);
|
||||
} else if (!strcasecmp(state->tag, "busystatus")) {
|
||||
event->busy_state = msbusy_to_bs(str);
|
||||
} else if (!strcasecmp(state->tag, "reminderoffset")) {
|
||||
/*XXX Currently we rely on event->start being set first which means we rely on the response order
|
||||
* which technically should be fine since the query returns in the order we ask for, but ... */
|
||||
event->alarm = event->start - atoi(str);
|
||||
}
|
||||
|
||||
ast_free(str);
|
||||
return IKS_OK;
|
||||
}
|
||||
|
||||
static int cb_true(void *user_data, void *arg, int flags)
|
||||
{
|
||||
return CMP_MATCH;
|
||||
}
|
||||
|
||||
static void exchangecal_destructor(void *obj)
|
||||
{
|
||||
struct exchangecal_pvt *pvt = obj;
|
||||
|
||||
ast_debug(1, "Destroying pvt for Exchange calendar %s\n", pvt->owner->name);
|
||||
if (pvt->session) {
|
||||
ne_session_destroy(pvt->session);
|
||||
}
|
||||
ast_string_field_free_memory(pvt);
|
||||
|
||||
ao2_callback(pvt->events, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, cb_true, NULL);
|
||||
|
||||
ao2_ref(pvt->events, -1);
|
||||
}
|
||||
|
||||
static void *unref_exchangecal(void *obj)
|
||||
{
|
||||
struct exchangecal_pvt *pvt = obj;
|
||||
|
||||
ao2_ref(pvt, -1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* It is very important to use the return value of this function as a realloc could occur */
|
||||
static struct ast_str *generate_exchange_uuid(struct ast_str *uid)
|
||||
{
|
||||
unsigned short val[8];
|
||||
int x;
|
||||
|
||||
for (x = 0; x < 8; x++) {
|
||||
val[x] = ast_random();
|
||||
}
|
||||
ast_str_set(&uid, 0, "%04x%04x-%04x-%04x-%04x-%04x%04x%04x", val[0], val[1], val[2], val[3], val[4], val[5], val[6], val[7]);
|
||||
|
||||
return uid;
|
||||
}
|
||||
|
||||
static int is_valid_uuid(struct ast_str *uid)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (ast_str_strlen(uid) != 36) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < ast_str_strlen(uid); i++) {
|
||||
if (i == 8 || i == 13 || i == 18 || i == 23) {
|
||||
if (ast_str_buffer(uid)[i] != '-') {
|
||||
return 0;
|
||||
}
|
||||
} else if (!((ast_str_buffer(uid)[i] > 47 && ast_str_buffer(uid)[i] < 58) || (ast_str_buffer(uid)[i] > 96 && ast_str_buffer(uid)[i] < 103))) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static struct ast_str *xml_encode_str(struct ast_str *dst, const char *src)
|
||||
{
|
||||
const char *tmp;
|
||||
char buf[7];
|
||||
|
||||
for (tmp = src; *tmp; tmp++) {
|
||||
switch (*tmp) {
|
||||
case '\"':
|
||||
strcpy(buf, """);
|
||||
break;
|
||||
|
||||
case '\'':
|
||||
strcpy(buf, "'");
|
||||
break;
|
||||
|
||||
case '&':
|
||||
strcpy(buf, "&");
|
||||
break;
|
||||
|
||||
case '<':
|
||||
strcpy(buf, "<");
|
||||
break;
|
||||
|
||||
case '>':
|
||||
strcpy(buf, ">");
|
||||
break;
|
||||
|
||||
default:
|
||||
sprintf(buf, "%c", *tmp);
|
||||
}
|
||||
|
||||
ast_str_append(&dst, 0, "%s", buf);
|
||||
}
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
static struct ast_str *epoch_to_exchange_time(struct ast_str *dst, time_t epoch)
|
||||
{
|
||||
icaltimezone *utc = icaltimezone_get_utc_timezone();
|
||||
icaltimetype tt = icaltime_from_timet_with_zone(epoch, 0, utc);
|
||||
char tmp[30];
|
||||
int i;
|
||||
|
||||
ast_copy_string(tmp, icaltime_as_ical_string(tt), sizeof(tmp));
|
||||
for (i = 0; tmp[i]; i++) {
|
||||
ast_str_append(&dst, 0, "%c", tmp[i]);
|
||||
if (i == 3 || i == 5)
|
||||
ast_str_append(&dst, 0, "%c", '-');
|
||||
if (i == 10 || i == 12)
|
||||
ast_str_append(&dst, 0, "%c", ':');
|
||||
if (i == 14)
|
||||
ast_str_append(&dst, 0, "%s", ".000");
|
||||
}
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
static struct ast_str *bs_to_exchange_bs(struct ast_str *dst, enum ast_calendar_busy_state bs)
|
||||
{
|
||||
switch (bs) {
|
||||
case AST_CALENDAR_BS_BUSY:
|
||||
ast_str_set(&dst, 0, "%s", "BUSY");
|
||||
break;
|
||||
|
||||
case AST_CALENDAR_BS_BUSY_TENTATIVE:
|
||||
ast_str_set(&dst, 0, "%s", "TENTATIVE");
|
||||
break;
|
||||
|
||||
default:
|
||||
ast_str_set(&dst, 0, "%s", "FREE");
|
||||
}
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
static int fetch_response_reader(void *data, const char *block, size_t len)
|
||||
{
|
||||
struct ast_str **response = data;
|
||||
unsigned char *tmp;
|
||||
|
||||
if (!(tmp = ast_malloc(len + 1))) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(tmp, block, len);
|
||||
tmp[len] = '\0';
|
||||
ast_str_append(response, 0, "%s", tmp);
|
||||
ast_free(tmp);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int auth_credentials(void *userdata, const char *realm, int attempts, char *username, char *secret)
|
||||
{
|
||||
struct exchangecal_pvt *pvt = userdata;
|
||||
|
||||
if (attempts > 1) {
|
||||
ast_log(LOG_WARNING, "Invalid username or password for Exchange calendar '%s'\n", pvt->owner->name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ne_strnzcpy(username, pvt->user, NE_ABUFSIZ);
|
||||
ne_strnzcpy(secret, pvt->secret, NE_ABUFSIZ);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct ast_str *exchangecal_request(struct exchangecal_pvt *pvt, const char *method, struct ast_str *req_body, struct ast_str *subdir)
|
||||
{
|
||||
struct ast_str *response;
|
||||
ne_request *req;
|
||||
int ret;
|
||||
char buf[1000];
|
||||
|
||||
if (!pvt) {
|
||||
ast_log(LOG_ERROR, "There is no private!\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!(response = ast_str_create(512))) {
|
||||
ast_log(LOG_ERROR, "Could not allocate memory for response.\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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_set_request_body_buffer(req, ast_str_buffer(req_body), ast_str_strlen(req_body));
|
||||
ne_add_request_header(req, "Content-type", "text/xml");
|
||||
|
||||
ret = ne_request_dispatch(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));
|
||||
ast_free(response);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static int exchangecal_write_event(struct ast_calendar_event *event)
|
||||
{
|
||||
struct ast_str *body = NULL, *response = NULL, *subdir = NULL;
|
||||
struct ast_str *uid = NULL, *summary = NULL, *description = NULL, *organizer = NULL,
|
||||
*location = NULL, *start = NULL, *end = NULL, *busystate = NULL;
|
||||
int ret = -1;
|
||||
|
||||
if (!event) {
|
||||
ast_log(LOG_WARNING, "No event passed!\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!(event->start && event->end)) {
|
||||
ast_log(LOG_WARNING, "The event must contain a start and an end\n");
|
||||
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");
|
||||
goto write_cleanup;
|
||||
}
|
||||
|
||||
if (!(uid = ast_str_create(32)) ||
|
||||
!(summary = ast_str_create(32)) ||
|
||||
!(description = ast_str_create(32)) ||
|
||||
!(organizer = ast_str_create(32)) ||
|
||||
!(location = ast_str_create(32)) ||
|
||||
!(start = ast_str_create(32)) ||
|
||||
!(end = ast_str_create(32)) ||
|
||||
!(busystate = ast_str_create(32))) {
|
||||
ast_log(LOG_ERROR, "Unable to allocate memory for request values\n");
|
||||
goto write_cleanup;
|
||||
}
|
||||
|
||||
if (ast_strlen_zero(event->uid)) {
|
||||
uid = generate_exchange_uuid(uid);
|
||||
} else {
|
||||
ast_str_set(&uid, 36, "%s", event->uid);
|
||||
}
|
||||
|
||||
if (!is_valid_uuid(uid)) {
|
||||
ast_log(LOG_WARNING, "An invalid uid was provided, you may leave this field blank to have one generated for you\n");
|
||||
goto write_cleanup;
|
||||
}
|
||||
|
||||
summary = xml_encode_str(summary, event->summary);
|
||||
description = xml_encode_str(description, event->description);
|
||||
organizer = xml_encode_str(organizer, event->organizer);
|
||||
location = xml_encode_str(location, event->location);
|
||||
start = epoch_to_exchange_time(start, event->start);
|
||||
end = epoch_to_exchange_time(end, event->end);
|
||||
busystate = bs_to_exchange_bs(busystate, event->busy_state);
|
||||
|
||||
ast_str_append(&body, 0,
|
||||
"<?xml version=\"1.0\"?>\n"
|
||||
"<a:propertyupdate\n"
|
||||
" xmlns:a=\"DAV:\"\n"
|
||||
" xmlns:e=\"http://schemas.microsoft.com/exchange/\"\n"
|
||||
" xmlns:mapi=\"http://schemas.microsoft.com/mapi/\"\n"
|
||||
" xmlns:mapit=\"http://schemas.microsoft.com/mapi/proptag/\"\n"
|
||||
" xmlns:x=\"xml:\" xmlns:cal=\"urn:schemas:calendar:\"\n"
|
||||
" xmlns:dt=\"uuid:%s/\"\n" /* uid */
|
||||
" xmlns:header=\"urn:schemas:mailheader:\"\n"
|
||||
" xmlns:mail=\"urn:schemas:httpmail:\"\n"
|
||||
">\n"
|
||||
" <a:set>\n"
|
||||
" <a:prop>\n"
|
||||
" <a:contentclass>urn:content-classes:appointment</a:contentclass>\n"
|
||||
" <e:outlookmessageclass>IPM.Appointment</e:outlookmessageclass>\n"
|
||||
" <mail:subject>%s</mail:subject>\n" /* summary */
|
||||
" <mail:description>%s</mail:description>\n" /* description */
|
||||
" <header:to>%s</header:to>\n" /* organizer */
|
||||
" <cal:location>%s</cal:location>\n" /* location */
|
||||
" <cal:dtstart dt:dt=\"dateTime.tz\">%s</cal:dtstart>\n" /* start */
|
||||
" <cal:dtend dt:dt=\"dateTime.tz\">%s</cal:dtend>\n" /* end */
|
||||
" <cal:instancetype dt:dt=\"int\">0</cal:instancetype>\n"
|
||||
" <cal:busystatus>%s</cal:busystatus>\n" /* busy_state (BUSY, FREE, BUSY_TENTATIVE) */
|
||||
" <cal:meetingstatus>CONFIRMED</cal:meetingstatus>\n"
|
||||
" <cal:alldayevent dt:dt=\"boolean\">0</cal:alldayevent>\n" /* XXX need to add event support for all day events */
|
||||
" <cal:responserequested dt:dt=\"boolean\">0</cal:responserequested>\n"
|
||||
" <mapi:finvited dt:dt=\"boolean\">1</mapi:finvited>\n"
|
||||
" </a:prop>\n"
|
||||
" </a:set>\n"
|
||||
"</a:propertyupdate>\n",
|
||||
ast_str_buffer(uid), ast_str_buffer(summary), ast_str_buffer(description), ast_str_buffer(organizer), ast_str_buffer(location), ast_str_buffer(start), ast_str_buffer(end), ast_str_buffer(busystate));
|
||||
ast_verb(0, "\n\n%s\n\n", ast_str_buffer(body));
|
||||
ast_str_set(&subdir, 0, "/Calendar/%s.eml", ast_str_buffer(uid));
|
||||
|
||||
response = exchangecal_request(event->owner->tech_pvt, "PROPPATCH", body, subdir);
|
||||
|
||||
ret = 0;
|
||||
write_cleanup:
|
||||
if (uid) {
|
||||
ast_free(uid);
|
||||
}
|
||||
if (summary) {
|
||||
ast_free(summary);
|
||||
}
|
||||
if (description) {
|
||||
ast_free(description);
|
||||
}
|
||||
if (organizer) {
|
||||
ast_free(organizer);
|
||||
}
|
||||
if (location) {
|
||||
ast_free(location);
|
||||
}
|
||||
if (start) {
|
||||
ast_free(start);
|
||||
}
|
||||
if (end) {
|
||||
ast_free(end);
|
||||
}
|
||||
if (busystate) {
|
||||
ast_free(busystate);
|
||||
}
|
||||
if (body) {
|
||||
ast_free(body);
|
||||
}
|
||||
if (response) {
|
||||
ast_free(response);
|
||||
}
|
||||
if (subdir) {
|
||||
ast_free(subdir);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
static struct ast_str *exchangecal_get_events_between(struct exchangecal_pvt *pvt, time_t start_time, time_t end_time)
|
||||
{
|
||||
struct ast_str *body, *response;
|
||||
char start[80], end[80];
|
||||
struct timeval tv = {0,};
|
||||
struct ast_tm tm;
|
||||
|
||||
tv.tv_sec = start_time;
|
||||
ast_localtime(&tv, &tm, "UTC");
|
||||
ast_strftime(start, sizeof(start), "%Y/%m/%d %T", &tm);
|
||||
|
||||
tv.tv_sec = end_time;
|
||||
ast_localtime(&tv, &tm, "UTC");
|
||||
ast_strftime(end, sizeof(end), "%Y/%m/%d %T", &tm);
|
||||
|
||||
if (!(body = ast_str_create(512))) {
|
||||
ast_log(LOG_ERROR, "Could not allocate memory for body of request!\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ast_str_append(&body, 0,
|
||||
"<?xml version=\"1.0\"?>\n"
|
||||
"<g:searchrequest xmlns:g=\"DAV:\">\n"
|
||||
" <g:sql> SELECT \"urn:schemas:calendar:location\", \"urn:schemas:httpmail:subject\",\n"
|
||||
" \"urn:schemas:calendar:dtstart\", \"urn:schemas:calendar:dtend\",\n"
|
||||
" \"urn:schemas:calendar:busystatus\", \"urn:schemas:calendar:instancetype\",\n"
|
||||
" \"urn:schemas:calendar:uid\", \"urn:schemas:httpmail:textdescription\",\n"
|
||||
" \"urn:schemas:calendar:organizer\", \"urn:schemas:calendar:reminderoffset\"\n"
|
||||
" FROM Scope('SHALLOW TRAVERSAL OF \"%s/Calendar\"')\n"
|
||||
" WHERE NOT \"urn:schemas:calendar:instancetype\" = 1\n"
|
||||
" AND \"DAV:contentclass\" = 'urn:content-classes:appointment'\n"
|
||||
" AND NOT (\"urn:schemas:calendar:dtend\" < '%s'\n"
|
||||
" OR \"urn:schemas:calendar:dtstart\" > '%s')\n"
|
||||
" ORDER BY \"urn:schemas:calendar:dtstart\" ASC\n"
|
||||
" </g:sql>\n"
|
||||
"</g:searchrequest>\n", pvt->url, start, end);
|
||||
|
||||
ast_debug(5, "Request:\n%s\n", ast_str_buffer(body));
|
||||
response = exchangecal_request(pvt, "SEARCH", body, NULL);
|
||||
ast_debug(5, "Response:\n%s\n", ast_str_buffer(response));
|
||||
ast_free(body);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static int update_exchangecal(struct exchangecal_pvt *pvt)
|
||||
{
|
||||
struct xmlstate state;
|
||||
struct timeval now = ast_tvnow();
|
||||
time_t start, end;
|
||||
struct ast_str *response;
|
||||
iksparser *p;
|
||||
|
||||
state.pvt = pvt;
|
||||
start = now.tv_sec;
|
||||
end = now.tv_sec + 60 * pvt->owner->timeframe;
|
||||
if (!(response = exchangecal_get_events_between(pvt, start, end))) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
p = iks_sax_new(&state, parse_tag, parse_cdata);
|
||||
iks_parse(p, ast_str_buffer(response), ast_str_strlen(response), 1);
|
||||
ast_calendar_merge_events(pvt->owner, pvt->events);
|
||||
ast_free(response);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void *exchangecal_load_calendar(void *void_data)
|
||||
{
|
||||
struct exchangecal_pvt *pvt;
|
||||
struct ast_variable *v;
|
||||
struct ast_calendar *cal = void_data;
|
||||
ast_mutex_t refreshlock;
|
||||
|
||||
if (!(cal && calendar_config)) {
|
||||
ast_log(LOG_ERROR, "You must enable calendar support for res_exchangecal to load\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (ao2_trylock(cal)) {
|
||||
if (cal->unloading) {
|
||||
ast_log(LOG_WARNING, "Unloading module, load_calendar cancelled.\n");
|
||||
} else {
|
||||
ast_log(LOG_WARNING, "Could not lock calendar, aborting!\n");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!(pvt = ao2_alloc(sizeof(*pvt), exchangecal_destructor))) {
|
||||
ast_log(LOG_ERROR, "Could not allocate exchangecal_pvt structure for calendar: %s\n", cal->name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pvt->owner = cal;
|
||||
|
||||
if (!(pvt->events = ast_calendar_event_container_alloc())) {
|
||||
ast_log(LOG_ERROR, "Could not allocate space for fetching events for calendar: %s\n", cal->name);
|
||||
pvt = unref_exchangecal(pvt);
|
||||
ao2_unlock(cal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (ast_string_field_init(pvt, 32)) {
|
||||
ast_log(LOG_ERROR, "Couldn't allocate string field space for calendar: %s\n", cal->name);
|
||||
pvt = unref_exchangecal(pvt);
|
||||
ao2_unlock(cal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (v = ast_variable_browse(calendar_config, 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")) {
|
||||
ast_string_field_set(pvt, user, v->value);
|
||||
} else if (!strcasecmp(v->name, "secret")) {
|
||||
ast_string_field_set(pvt, secret, v->value);
|
||||
}
|
||||
}
|
||||
|
||||
if (ast_strlen_zero(pvt->url)) {
|
||||
ast_log(LOG_WARNING, "No URL was specified for Exchange calendar '%s' - skipping.\n", cal->name);
|
||||
pvt = unref_exchangecal(pvt);
|
||||
ao2_unlock(cal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (ne_uri_parse(pvt->url, &pvt->uri) || pvt->uri.host == NULL || pvt->uri.path == NULL) {
|
||||
ast_log(LOG_WARNING, "Could not parse url '%s' for Exchange calendar '%s' - skipping.\n", pvt->url, cal->name);
|
||||
pvt = unref_exchangecal(pvt);
|
||||
ao2_unlock(cal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (pvt->uri.scheme == NULL) {
|
||||
pvt->uri.scheme = "http";
|
||||
}
|
||||
|
||||
if (pvt->uri.port == 0) {
|
||||
pvt->uri.port = ne_uri_defaultport(pvt->uri.scheme);
|
||||
}
|
||||
|
||||
pvt->session = ne_session_create(pvt->uri.scheme, pvt->uri.host, pvt->uri.port);
|
||||
ne_set_server_auth(pvt->session, auth_credentials, pvt);
|
||||
if (!strncasecmp(pvt->uri.scheme, "https", sizeof(pvt->uri.scheme))) {
|
||||
ne_ssl_trust_default_ca(pvt->session);
|
||||
}
|
||||
|
||||
cal->tech_pvt = pvt;
|
||||
|
||||
ast_mutex_init(&refreshlock);
|
||||
|
||||
/* Load it the first time */
|
||||
update_exchangecal(pvt);
|
||||
|
||||
ao2_unlock(cal);
|
||||
|
||||
/* The only writing from another thread will be if unload is true */
|
||||
for (;;) {
|
||||
struct timeval tv = ast_tvnow();
|
||||
struct timespec ts = {0,};
|
||||
|
||||
ts.tv_sec = tv.tv_sec + (60 * pvt->owner->refresh);
|
||||
|
||||
ast_mutex_lock(&refreshlock);
|
||||
while (!pvt->owner->unloading) {
|
||||
if (ast_cond_timedwait(&pvt->owner->unload, &refreshlock, &ts) == ETIMEDOUT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ast_mutex_unlock(&refreshlock);
|
||||
|
||||
if (pvt->owner->unloading) {
|
||||
ast_debug(10, "Skipping refresh since we got a shutdown signal\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ast_debug(10, "Refreshing after %d minute timeout\n", pvt->owner->refresh);
|
||||
|
||||
update_exchangecal(pvt);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int load_module(void)
|
||||
{
|
||||
ne_sock_init();
|
||||
if (ast_calendar_register(&exchangecal_tech)) {
|
||||
ne_sock_exit();
|
||||
return AST_MODULE_LOAD_DECLINE;
|
||||
}
|
||||
|
||||
return AST_MODULE_LOAD_SUCCESS;
|
||||
}
|
||||
|
||||
static int unload_module(void)
|
||||
{
|
||||
ast_calendar_unregister(&exchangecal_tech);
|
||||
ne_sock_exit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Asterisk MS Exchange Calendar Integration",
|
||||
.load = load_module,
|
||||
.unload = unload_module,
|
||||
);
|
@ -0,0 +1,470 @@
|
||||
/*
|
||||
* Asterisk -- An open source telephony toolkit.
|
||||
*
|
||||
* Copyright (C) 2008 - 2009, Digium, Inc.
|
||||
*
|
||||
* Terry Wilson <twilson@digium.com>
|
||||
*
|
||||
* See http://www.asterisk.org for more information about
|
||||
* the Asterisk project. Please do not directly contact
|
||||
* any of the maintainers of this project for assistance;
|
||||
* the project provides a web site, mailing lists and IRC
|
||||
* channels for your use.
|
||||
*
|
||||
* This program is free software, distributed under the terms of
|
||||
* the GNU General Public License Version 2. See the LICENSE file
|
||||
* at the top of the source tree.
|
||||
*/
|
||||
|
||||
/*! \file
|
||||
* \brief Resource for handling iCalnedar calendars
|
||||
*/
|
||||
|
||||
/*** MODULEINFO
|
||||
<depend>neon</depend>
|
||||
<depend>ical</depend>
|
||||
***/
|
||||
#include "asterisk.h"
|
||||
|
||||
ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
|
||||
|
||||
#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 "asterisk/module.h"
|
||||
#include "asterisk/calendar.h"
|
||||
#include "asterisk/lock.h"
|
||||
#include "asterisk/config.h"
|
||||
#include "asterisk/astobj2.h"
|
||||
|
||||
static void *ical_load_calendar(void *data);
|
||||
static void *unref_icalendar(void *obj);
|
||||
|
||||
static struct ast_calendar_tech ical_tech = {
|
||||
.type = "ical",
|
||||
.module = AST_MODULE,
|
||||
.description = "iCalendar .ics calendars",
|
||||
.load_calendar = ical_load_calendar,
|
||||
.unref_calendar = unref_icalendar,
|
||||
};
|
||||
|
||||
struct icalendar_pvt {
|
||||
AST_DECLARE_STRING_FIELDS(
|
||||
AST_STRING_FIELD(url);
|
||||
AST_STRING_FIELD(user);
|
||||
AST_STRING_FIELD(secret);
|
||||
);
|
||||
struct ast_calendar *owner;
|
||||
ne_uri uri;
|
||||
ne_session *session;
|
||||
icalcomponent *data;
|
||||
struct ao2_container *events;
|
||||
};
|
||||
|
||||
static int cb_true(void *user_data, void *arg, int flags)
|
||||
{
|
||||
return CMP_MATCH;
|
||||
}
|
||||
|
||||
static void icalendar_destructor(void *obj)
|
||||
{
|
||||
struct icalendar_pvt *pvt = obj;
|
||||
|
||||
ast_debug(1, "Destroying pvt for iCalendar %s\n", pvt->owner->name);
|
||||
if (pvt->session) {
|
||||
ne_session_destroy(pvt->session);
|
||||
}
|
||||
if (pvt->data) {
|
||||
icalcomponent_free(pvt->data);
|
||||
}
|
||||
ast_string_field_free_memory(pvt);
|
||||
|
||||
ao2_callback(pvt->events, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, cb_true, NULL);
|
||||
|
||||
ao2_ref(pvt->events, -1);
|
||||
}
|
||||
|
||||
static void *unref_icalendar(void *obj)
|
||||
{
|
||||
struct icalendar_pvt *pvt = obj;
|
||||
|
||||
ao2_ref(pvt, -1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int fetch_response_reader(void *data, const char *block, size_t len)
|
||||
{
|
||||
struct ast_str **response = data;
|
||||
unsigned char *tmp;
|
||||
|
||||
if (!(tmp = ast_malloc(len + 1))) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(tmp, block, len);
|
||||
tmp[len] = '\0';
|
||||
ast_str_append(response, 0, "%s", tmp);
|
||||
ast_free(tmp);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int auth_credentials(void *userdata, const char *realm, int attempts, char *username, char *secret)
|
||||
{
|
||||
struct icalendar_pvt *pvt = userdata;
|
||||
|
||||
if (attempts > 1) {
|
||||
ast_log(LOG_WARNING, "Invalid username or password for iCalendar '%s'\n", pvt->owner->name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ne_strnzcpy(username, pvt->user, NE_ABUFSIZ);
|
||||
ne_strnzcpy(secret, pvt->secret, NE_ABUFSIZ);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static icalcomponent *fetch_icalendar(struct icalendar_pvt *pvt)
|
||||
{
|
||||
int ret;
|
||||
struct ast_str *response;
|
||||
ne_request *req;
|
||||
icalcomponent *comp = NULL;
|
||||
|
||||
if (!pvt) {
|
||||
ast_log(LOG_ERROR, "There is no private!\n");
|
||||
}
|
||||
|
||||
if (!(response = ast_str_create(512))) {
|
||||
ast_log(LOG_ERROR, "Could not allocate memory for response.\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
req = ne_request_create(pvt->session, "GET", pvt->uri.path);
|
||||
ne_add_response_body_reader(req, ne_accept_2xx, fetch_response_reader, &response);
|
||||
|
||||
ret = ne_request_dispatch(req);
|
||||
if (ret != NE_OK || !ast_str_strlen(response)) {
|
||||
ast_log(LOG_WARNING, "Unable to retrieve iCalendar '%s' from '%s': %s\n", pvt->owner->name, pvt->url, ne_get_error(pvt->session));
|
||||
ast_free(response);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!ast_strlen_zero(ast_str_buffer(response))) {
|
||||
comp = icalparser_parse_string(ast_str_buffer(response));
|
||||
}
|
||||
ast_free(response);
|
||||
|
||||
return comp;
|
||||
}
|
||||
|
||||
static void icalendar_add_event(icalcomponent *comp, struct icaltime_span *span, void *data)
|
||||
{
|
||||
struct icalendar_pvt *pvt = data;
|
||||
struct ast_calendar_event *event;
|
||||
icaltimezone *utc = icaltimezone_get_utc_timezone();
|
||||
icaltimetype start, end, tmp;
|
||||
icalcomponent *valarm;
|
||||
icalproperty *prop;
|
||||
struct icaltriggertype trigger;
|
||||
|
||||
if (!(pvt && pvt->owner)) {
|
||||
ast_log(LOG_ERROR, "Require a private structure with an ownenr\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(event = ast_calendar_event_alloc(pvt->owner))) {
|
||||
ast_log(LOG_ERROR, "Could not allocate an event!\n");
|
||||
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;
|
||||
|
||||
default:
|
||||
event->busy_state = 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));
|
||||
}
|
||||
|
||||
if ((prop = icalcomponent_get_first_property(comp, ICAL_DESCRIPTION_PROPERTY))) {
|
||||
ast_string_field_set(event, description, icalproperty_get_value_as_string(prop));
|
||||
}
|
||||
|
||||
if ((prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY))) {
|
||||
ast_string_field_set(event, organizer, icalproperty_get_value_as_string(prop));
|
||||
}
|
||||
|
||||
if ((prop = icalcomponent_get_first_property(comp, ICAL_LOCATION_PROPERTY))) {
|
||||
ast_string_field_set(event, location, icalproperty_get_value_as_string(prop));
|
||||
}
|
||||
|
||||
if ((prop = icalcomponent_get_first_property(comp, ICAL_UID_PROPERTY))) {
|
||||
ast_string_field_set(event, uid, icalproperty_get_value_as_string(prop));
|
||||
} else {
|
||||
ast_log(LOG_WARNING, "No UID found, but one is required. Generating, but updates may not be acurate\n");
|
||||
if (!ast_strlen_zero(event->summary)) {
|
||||
ast_string_field_set(event, uid, event->summary);
|
||||
} else {
|
||||
char tmp[100];
|
||||
snprintf(tmp, sizeof(tmp), "%lu", event->start);
|
||||
ast_string_field_set(event, uid, tmp);
|
||||
}
|
||||
}
|
||||
|
||||
/* Get the attendees */
|
||||
for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
|
||||
prop; prop = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY)) {
|
||||
struct ast_calendar_attendee *attendee;
|
||||
const char *data;
|
||||
|
||||
if (!(attendee = ast_calloc(1, sizeof(*attendee)))) {
|
||||
event = ast_calendar_unref_event(event);
|
||||
return;
|
||||
}
|
||||
data = icalproperty_get_attendee(prop);
|
||||
if (!ast_strlen_zero(data)) {
|
||||
attendee->data = ast_strdup(data);;
|
||||
AST_LIST_INSERT_TAIL(&event->attendees, attendee, next);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Only set values for alarm based on VALARM. Can be overriden in main/calendar.c by autoreminder
|
||||
* therefore, go ahead and add events even if their is no VALARM or it is malformed
|
||||
* Currently we are only getting the first VALARM and are handling repitition in main/calendar.c from calendar.conf */
|
||||
if (!(valarm = icalcomponent_get_first_component(comp, ICAL_VALARM_COMPONENT))) {
|
||||
ao2_link(pvt->events, event);
|
||||
event = ast_calendar_unref_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(prop = icalcomponent_get_first_property(valarm, ICAL_TRIGGER_PROPERTY))) {
|
||||
ast_log(LOG_WARNING, "VALARM has no TRIGGER, skipping!\n");
|
||||
ao2_link(pvt->events, event);
|
||||
event = ast_calendar_unref_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
trigger = icalproperty_get_trigger(prop);
|
||||
|
||||
if (icaltriggertype_is_null_trigger(trigger)) {
|
||||
ast_log(LOG_WARNING, "Bad TRIGGER for VALARM, skipping!\n");
|
||||
ao2_link(pvt->events, event);
|
||||
event = ast_calendar_unref_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!icaltime_is_null_time(trigger.time)) { /* This is an absolute time */
|
||||
tmp = icaltime_convert_to_zone(trigger.time, utc);
|
||||
event->alarm = icaltime_as_timet_with_zone(tmp, utc);
|
||||
} else { /* Offset from either dtstart or dtend */
|
||||
/* 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);
|
||||
}
|
||||
|
||||
ao2_link(pvt->events, event);
|
||||
event = ast_calendar_unref_event(event);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static void icalendar_update_events(struct icalendar_pvt *pvt)
|
||||
{
|
||||
struct icaltimetype start_time, end_time;
|
||||
icalcomponent *iter;
|
||||
|
||||
if (!pvt) {
|
||||
ast_log(LOG_ERROR, "iCalendar is NULL\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pvt->owner) {
|
||||
ast_log(LOG_ERROR, "iCalendar is an orphan!\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pvt->data) {
|
||||
ast_log(LOG_ERROR, "The iCalendar has not been parsed!\n");
|
||||
return;
|
||||
}
|
||||
|
||||
start_time = icaltime_current_time_with_zone(icaltimezone_get_utc_timezone());
|
||||
end_time = icaltime_current_time_with_zone(icaltimezone_get_utc_timezone());
|
||||
end_time.second += pvt->owner->timeframe * 60;
|
||||
icaltime_normalize(end_time);
|
||||
|
||||
for (iter = icalcomponent_get_first_component(pvt->data, ICAL_VEVENT_COMPONENT);
|
||||
iter;
|
||||
iter = icalcomponent_get_next_component(pvt->data, ICAL_VEVENT_COMPONENT))
|
||||
{
|
||||
icalcomponent_foreach_recurrence(iter, start_time, end_time, icalendar_add_event, pvt);
|
||||
}
|
||||
|
||||
ast_calendar_merge_events(pvt->owner, pvt->events);
|
||||
}
|
||||
|
||||
static void *ical_load_calendar(void *void_data)
|
||||
{
|
||||
struct icalendar_pvt *pvt;
|
||||
struct ast_variable *v;
|
||||
struct ast_calendar *cal = void_data;
|
||||
ast_mutex_t refreshlock;
|
||||
|
||||
if (!(cal && calendar_config)) {
|
||||
ast_log(LOG_ERROR, "You must enable calendar support for res_icalendar to load\n");
|
||||
return NULL;
|
||||
}
|
||||
if (ao2_trylock(cal)) {
|
||||
if (cal->unloading) {
|
||||
ast_log(LOG_WARNING, "Unloading module, load_calendar cancelled.\n");
|
||||
} else {
|
||||
ast_log(LOG_WARNING, "Could not lock calendar, aborting!\n");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!(pvt = ao2_alloc(sizeof(*pvt), icalendar_destructor))) {
|
||||
ast_log(LOG_ERROR, "Could not allocate icalendar_pvt structure for calendar: %s\n", cal->name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pvt->owner = cal;
|
||||
|
||||
if (!(pvt->events = ast_calendar_event_container_alloc())) {
|
||||
ast_log(LOG_ERROR, "Could not allocate space for fetching events for calendar: %s\n", cal->name);
|
||||
pvt = unref_icalendar(pvt);
|
||||
ao2_unlock(cal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (ast_string_field_init(pvt, 32)) {
|
||||
ast_log(LOG_ERROR, "Couldn't allocate string field space for calendar: %s\n", cal->name);
|
||||
pvt = unref_icalendar(pvt);
|
||||
ao2_unlock(cal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (v = ast_variable_browse(calendar_config, 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")) {
|
||||
ast_string_field_set(pvt, user, v->value);
|
||||
} else if (!strcasecmp(v->name, "secret")) {
|
||||
ast_string_field_set(pvt, secret, v->value);
|
||||
}
|
||||
}
|
||||
|
||||
if (ast_strlen_zero(pvt->url)) {
|
||||
ast_log(LOG_WARNING, "No URL was specified for iCalendar '%s' - skipping.\n", cal->name);
|
||||
pvt = unref_icalendar(pvt);
|
||||
ao2_unlock(cal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (ne_uri_parse(pvt->url, &pvt->uri) || pvt->uri.host == NULL || pvt->uri.path == NULL) {
|
||||
ast_log(LOG_WARNING, "Could not parse url '%s' for iCalendar '%s' - skipping.\n", pvt->url, cal->name);
|
||||
pvt = unref_icalendar(pvt);
|
||||
ao2_unlock(cal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (pvt->uri.scheme == NULL) {
|
||||
pvt->uri.scheme = "http";
|
||||
}
|
||||
|
||||
if (pvt->uri.port == 0) {
|
||||
pvt->uri.port = ne_uri_defaultport(pvt->uri.scheme);
|
||||
}
|
||||
|
||||
pvt->session = ne_session_create(pvt->uri.scheme, pvt->uri.host, pvt->uri.port);
|
||||
ne_set_server_auth(pvt->session, auth_credentials, pvt);
|
||||
if (!strncasecmp(pvt->uri.scheme, "https", sizeof(pvt->uri.scheme))) {
|
||||
ne_ssl_trust_default_ca(pvt->session);
|
||||
}
|
||||
|
||||
cal->tech_pvt = pvt;
|
||||
|
||||
ast_mutex_init(&refreshlock);
|
||||
|
||||
/* Load it the first time */
|
||||
if (!(pvt->data = fetch_icalendar(pvt))) {
|
||||
ast_log(LOG_WARNING, "Unable to parse iCalendar '%s'\n", cal->name);
|
||||
}
|
||||
|
||||
icalendar_update_events(pvt);
|
||||
|
||||
ao2_unlock(cal);
|
||||
|
||||
/* The only writing from another thread will be if unload is true */
|
||||
for(;;) {
|
||||
struct timeval tv = ast_tvnow();
|
||||
struct timespec ts = {0,};
|
||||
|
||||
ts.tv_sec = tv.tv_sec + (60 * pvt->owner->refresh);
|
||||
|
||||
ast_mutex_lock(&refreshlock);
|
||||
while (!pvt->owner->unloading) {
|
||||
if (ast_cond_timedwait(&pvt->owner->unload, &refreshlock, &ts) == ETIMEDOUT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ast_mutex_unlock(&refreshlock);
|
||||
|
||||
if (pvt->owner->unloading) {
|
||||
ast_debug(10, "Skipping refresh since we got a shutdown signal\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ast_debug(10, "Refreshing after %d minute timeout\n", pvt->owner->refresh);
|
||||
|
||||
if (!(pvt->data = fetch_icalendar(pvt))) {
|
||||
ast_log(LOG_WARNING, "Unable to parse iCalendar '%s'\n", pvt->owner->name);
|
||||
continue;
|
||||
}
|
||||
|
||||
icalendar_update_events(pvt);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int load_module(void)
|
||||
{
|
||||
ne_sock_init();
|
||||
if (ast_calendar_register(&ical_tech)) {
|
||||
ne_sock_exit();
|
||||
return AST_MODULE_LOAD_DECLINE;
|
||||
}
|
||||
|
||||
return AST_MODULE_LOAD_SUCCESS;
|
||||
}
|
||||
|
||||
static int unload_module(void)
|
||||
{
|
||||
ast_calendar_unregister(&ical_tech);
|
||||
ne_sock_exit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Asterisk iCalendar .ics file integration",
|
||||
.load = load_module,
|
||||
.unload = unload_module,
|
||||
);
|
Loading…
Reference in new issue