822 lines
30 KiB
PHP
822 lines
30 KiB
PHP
<?php
|
|
/**
|
|
* The Kronolith_Driver_Ical class implements the Kronolith_Driver API for
|
|
* iCalendar data.
|
|
*
|
|
* Possible driver parameters:
|
|
* - url: The location of the remote calendar.
|
|
* - proxy: A hash with HTTP proxy information.
|
|
* - user: The user name for HTTP Basic Authentication.
|
|
* - password: The password for HTTP Basic Authentication.
|
|
*
|
|
* Copyright 2004-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file COPYING for license information (GPL). If you
|
|
* did not receive this file, see http://www.horde.org/licenses/gpl.
|
|
*
|
|
* @author Chuck Hagenbuch <chuck@horde.org>
|
|
* @author Jan Schneider <jan@horde.org>
|
|
* @package Kronolith
|
|
*/
|
|
class Kronolith_Driver_Ical extends Kronolith_Driver
|
|
{
|
|
/**
|
|
* Cache events as we fetch them to avoid fetching or parsing the same
|
|
* event twice.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_cache = array();
|
|
|
|
/**
|
|
* HTTP client object.
|
|
*
|
|
* @var Horde_Http_Client
|
|
*/
|
|
protected $_client;
|
|
|
|
/**
|
|
* A list of DAV support levels.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_davSupport;
|
|
|
|
/**
|
|
* The Horde_Perms permissions mask matching the CalDAV ACL.
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected $_permission;
|
|
|
|
/**
|
|
* Selects a calendar as the currently opened calendar.
|
|
*
|
|
* @param string $calendar A calendar identifier.
|
|
*/
|
|
public function open($calendar)
|
|
{
|
|
parent::open($calendar);
|
|
$this->_client = null;
|
|
$this->_permission = 0;
|
|
unset($this->_davSupport);
|
|
}
|
|
|
|
/**
|
|
* Returns the background color of the current calendar.
|
|
*
|
|
* @return string The calendar color.
|
|
*/
|
|
public function backgroundColor()
|
|
{
|
|
return $GLOBALS['calendar_manager']->getEntry(Kronolith::ALL_REMOTE_CALENDARS, $this->calendar)
|
|
? $GLOBALS['calendar_manager']->getEntry(Kronolith::ALL_REMOTE_CALENDARS, $this->calendar)->background()
|
|
: '#dddddd';
|
|
}
|
|
|
|
public function listAlarms($date, $fullevent = false)
|
|
{
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Lists all events in the time range, optionally restricting results to
|
|
* only events with alarms.
|
|
*
|
|
* @param Horde_Date $startDate The start of range date.
|
|
* @param Horde_Date $endDate The end of date range.
|
|
* @param array $options Additional options:
|
|
* - show_recurrence: (boolean) Return every instance of a recurring
|
|
* event?
|
|
* DEFAULT: false (Only return recurring events once
|
|
* inside $startDate - $endDate range)
|
|
* - has_alarm: (boolean) Only return events with alarms.
|
|
* DEFAULT: false (Return all events)
|
|
* - json: (boolean) Store the results of the event's toJson()
|
|
* method?
|
|
* DEFAULT: false
|
|
* - cover_dates: (boolean) Add the events to all days that they
|
|
* cover?
|
|
* DEFAULT: true
|
|
* - hide_exceptions: (boolean) Hide events that represent exceptions to
|
|
* a recurring event.
|
|
* DEFAULT: false (Do not hide exception events)
|
|
* - fetch_tags: (boolean) Fetch tags for all events.
|
|
* DEFAULT: false (Do not fetch event tags)
|
|
*
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
protected function _listEvents(Horde_Date $startDate = null,
|
|
Horde_Date $endDate = null,
|
|
array $options = array())
|
|
{
|
|
if ($this->isCalDAV()) {
|
|
try {
|
|
return $this->_listCalDAVEvents(
|
|
$startDate, $endDate, $options['show_recurrence'],
|
|
$options['has_alarm'], $options['json'],
|
|
$options['cover_dates'], $options['hide_exceptions']);
|
|
} catch (Kronolith_Exception $e) {
|
|
// Fall back to regular ICS downloads. At least Nextcloud
|
|
// advertises calendars as CalDAV capable, but then denying
|
|
// CalDAV requests.
|
|
$this->_davSupport = false;
|
|
}
|
|
}
|
|
return $this->_listWebDAVEvents(
|
|
$startDate, $endDate, $options['show_recurrence'],
|
|
$options['has_alarm'], $options['json'],
|
|
$options['cover_dates'], $options['hide_exceptions']);
|
|
}
|
|
|
|
/**
|
|
* Lists all events in the time range, optionally restricting results to
|
|
* only events with alarms.
|
|
*
|
|
* @param Horde_Date $startInterval Start of range date object.
|
|
* @param Horde_Date $endInterval End of range data object.
|
|
* @param boolean $showRecurrence Return every instance of a recurring
|
|
* event? If false, will only return
|
|
* recurring events once inside the
|
|
* $startDate - $endDate range.
|
|
* @param boolean $hasAlarm Only return events with alarms?
|
|
* @param boolean $json Store the results of the events'
|
|
* toJson() method?
|
|
* @param boolean $coverDates Whether to add the events to all days
|
|
* that they cover.
|
|
* $param boolean $hideExceptions Hide events that represent exceptions
|
|
* to a recurring event.
|
|
*
|
|
* @return array Events in the given time range.
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
protected function _listWebDAVEvents(
|
|
$startDate = null, $endDate = null, $showRecurrence = false,
|
|
$hasAlarm = false, $json = false, $coverDates = true,
|
|
$hideExceptions = false
|
|
)
|
|
{
|
|
$ical = $this->getRemoteCalendar();
|
|
|
|
if (is_null($startDate)) {
|
|
$startDate = new Horde_Date(array('mday' => 1,
|
|
'month' => 1,
|
|
'year' => 0000));
|
|
}
|
|
if (is_null($endDate)) {
|
|
$endDate = new Horde_Date(array('mday' => 31,
|
|
'month' => 12,
|
|
'year' => 9999));
|
|
}
|
|
|
|
$startDate = clone $startDate;
|
|
$startDate->hour = $startDate->min = $startDate->sec = 0;
|
|
$endDate = clone $endDate;
|
|
$endDate->hour = 23;
|
|
$endDate->min = $endDate->sec = 59;
|
|
|
|
$results = array();
|
|
$this->_processComponents(
|
|
$results, $ical, $startDate, $endDate, $showRecurrence, $json,
|
|
$coverDates, $hideExceptions
|
|
);
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Lists all events in the time range, optionally restricting results to
|
|
* only events with alarms.
|
|
*
|
|
* @param Horde_Date $startInterval Start of range date object.
|
|
* @param Horde_Date $endInterval End of range data object.
|
|
* @param boolean $showRecurrence Return every instance of a recurring
|
|
* event? If false, will only return
|
|
* recurring events once inside the
|
|
* $startDate - $endDate range.
|
|
* @param boolean $hasAlarm Only return events with alarms?
|
|
* @param boolean $json Store the results of the events'
|
|
* toJson() method?
|
|
* @param boolean $coverDates Whether to add the events to all days
|
|
* that they cover.
|
|
* $param boolean $hideExceptions Hide events that represent exceptions
|
|
* to a recurring event.
|
|
*
|
|
* @return array Events in the given time range.
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
protected function _listCalDAVEvents(
|
|
$startDate = null, $endDate = null, $showRecurrence = false,
|
|
$hasAlarm = false, $json = false, $coverDates = true,
|
|
$hideExceptions = false
|
|
)
|
|
{
|
|
if (!is_null($startDate)) {
|
|
$startDate = clone $startDate;
|
|
$startDate->hour = $startDate->min = $startDate->sec = 0;
|
|
}
|
|
if (!is_null($endDate)) {
|
|
$endDate = clone $endDate;
|
|
$endDate->hour = 23;
|
|
$endDate->min = $endDate->sec = 59;
|
|
}
|
|
|
|
/* Build report query. */
|
|
$xml = new XMLWriter();
|
|
$xml->openMemory();
|
|
$xml->setIndent(true);
|
|
$xml->startDocument();
|
|
$xml->startElementNS('C', 'calendar-query', 'urn:ietf:params:xml:ns:caldav');
|
|
$xml->writeAttribute('xmlns:D', 'DAV:');
|
|
$xml->startElement('D:prop');
|
|
$xml->writeElement('D:getetag');
|
|
$xml->startElement('C:calendar-data');
|
|
$xml->startElement('C:comp');
|
|
$xml->writeAttribute('name', 'VCALENDAR');
|
|
$xml->startElement('C:comp');
|
|
$xml->writeAttribute('name', 'VEVENT');
|
|
$xml->endElement();
|
|
$xml->endElement();
|
|
$xml->endElement();
|
|
$xml->endElement();
|
|
$xml->startElement('C:filter');
|
|
$xml->startElement('C:comp-filter');
|
|
$xml->writeAttribute('name', 'VCALENDAR');
|
|
$xml->startElement('C:comp-filter');
|
|
$xml->writeAttribute('name', 'VEVENT');
|
|
if (!is_null($startDate) ||
|
|
!is_null($endDate)) {
|
|
$xml->startElement('C:time-range');
|
|
if (!is_null($startDate)) {
|
|
$xml->writeAttribute('start', $startDate->toiCalendar());
|
|
}
|
|
if (!is_null($endDate)) {
|
|
$xml->writeAttribute('end', $endDate->toiCalendar());
|
|
}
|
|
}
|
|
$xml->endDocument();
|
|
|
|
$url = $this->_getUrl();
|
|
list($response, $events) = $this->_request('REPORT', $url, $xml,
|
|
array('Depth' => 1));
|
|
if (!$events->children('DAV:')->response) {
|
|
return array();
|
|
}
|
|
if (isset($response['headers']['content-location'])) {
|
|
$path = $response['headers']['content-location'];
|
|
} else {
|
|
$parsedUrl = parse_url($url);
|
|
$path = $parsedUrl['path'];
|
|
}
|
|
|
|
$results = array();
|
|
foreach ($events->children('DAV:')->response as $response) {
|
|
if (!$response->children('DAV:')->propstat) {
|
|
continue;
|
|
}
|
|
$ical = new Horde_Icalendar();
|
|
try {
|
|
$ical->parsevCalendar($response->children('DAV:')->propstat->prop->children('urn:ietf:params:xml:ns:caldav')->{'calendar-data'});
|
|
} catch (Horde_Icalendar_Exception $e) {
|
|
throw new Kronolith_Exception($e);
|
|
}
|
|
$this->_processComponents(
|
|
$results, $ical, $startDate, $endDate, $showRecurrence, $json,
|
|
$coverDates, $hideExceptions,
|
|
trim(str_replace($path, '', $response->href), '/')
|
|
);
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Processes the components of a Horde_Icalendar container into an event
|
|
* list.
|
|
*
|
|
* @param array $results Gets filled with the events in the
|
|
* given time range.
|
|
* @param Horde_Icalendar $ical An Horde_Icalendar container.
|
|
* @param Horde_Date $startInterval Start of range date.
|
|
* @param Horde_Date $endInterval End of range date.
|
|
* @param boolean $showRecurrence Return every instance of a recurring
|
|
* event? If false, will only return
|
|
* recurring events once inside the
|
|
* $startDate - $endDate range.
|
|
* @param boolean $json Store the results of the events'
|
|
* toJson() method?
|
|
* @param boolean $coverDates Whether to add the events to all days
|
|
* that they cover.
|
|
* $param boolean $hideExceptions Hide events that represent exceptions
|
|
* to a recurring event.
|
|
* @param string $id Enforce a certain event id (not UID).
|
|
*
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
protected function _processComponents(
|
|
&$results, $ical, $startDate, $endDate, $showRecurrence, $json,
|
|
$coverDates, $hideExceptions, $id = null
|
|
)
|
|
{
|
|
$components = $ical->getComponents();
|
|
$events = array();
|
|
$count = count($components);
|
|
$exceptions = array();
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$component = $components[$i];
|
|
if ($component->getType() == 'vEvent') {
|
|
try {
|
|
$event = new Kronolith_Event_Ical($this, $component);
|
|
} catch (Kronolith_Exception $e) {
|
|
Horde::log(
|
|
sprintf(
|
|
'Failed parse event from remote calendar: url = "%s"',
|
|
$this->calendar
|
|
),
|
|
'INFO'
|
|
);
|
|
continue;
|
|
}
|
|
$event->permission = $this->getPermission();
|
|
// Force string so JSON encoding is consistent across drivers.
|
|
$event->id = $id ? $id : 'ical' . $i;
|
|
|
|
/* Catch RECURRENCE-ID attributes which mark single recurrence
|
|
* instances. */
|
|
try {
|
|
$recurrence_id = $component->getAttribute('RECURRENCE-ID');
|
|
if (is_int($recurrence_id) &&
|
|
is_string($uid = $component->getAttribute('UID')) &&
|
|
is_int($seq = $component->getAttribute('SEQUENCE'))) {
|
|
$exceptions[$uid][$seq] = $recurrence_id;
|
|
if ($hideExceptions) {
|
|
continue;
|
|
}
|
|
$event->id .= '/' . $recurrence_id;
|
|
}
|
|
} catch (Horde_Icalendar_Exception $e) {}
|
|
|
|
/* Ignore events out of the period. */
|
|
$recurs = $event->recurs();
|
|
if (
|
|
/* Starts after the period. */
|
|
($endDate && $event->start->compareDateTime($endDate) > 0) ||
|
|
/* End before the period and doesn't recur. */
|
|
($startDate && !$recurs &&
|
|
$event->end->compareDateTime($startDate) < 0)) {
|
|
continue;
|
|
}
|
|
|
|
if ($recurs && $startDate) {
|
|
// Fixed end date? Check if end is before start period.
|
|
if ($event->recurrence->hasRecurEnd() &&
|
|
$event->recurrence->recurEnd->compareDateTime($startDate) < 0) {
|
|
continue;
|
|
} elseif ($endDate) {
|
|
$next = $event->recurrence->nextRecurrence($startDate);
|
|
if ($next == false || $next->compareDateTime($endDate) > 0) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
$events[] = $event;
|
|
}
|
|
}
|
|
|
|
/* Loop through all explicitly defined recurrence intances and create
|
|
* exceptions for those in the event with the matching recurrence. */
|
|
foreach ($events as $key => $event) {
|
|
if ($event->recurs() &&
|
|
isset($exceptions[$event->uid][$event->sequence])) {
|
|
$timestamp = $exceptions[$event->uid][$event->sequence];
|
|
$events[$key]->recurrence->addException(date('Y', $timestamp), date('m', $timestamp), date('d', $timestamp));
|
|
}
|
|
Kronolith::addEvents($results, $event, $startDate, $endDate,
|
|
$showRecurrence, $json, $coverDates);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @throws Kronolith_Exception
|
|
* @throws Horde_Exception_NotFound
|
|
*/
|
|
public function getEvent($eventId = null)
|
|
{
|
|
if (!$eventId) {
|
|
$event = new Kronolith_Event_Ical($this);
|
|
$event->permission = $this->getPermission();
|
|
return $event;
|
|
}
|
|
|
|
if ($this->isCalDAV()) {
|
|
if (preg_match('/(.*)-(\d+)$/', $eventId, $matches)) {
|
|
$eventId = $matches[1];
|
|
//$recurrenceId = $matches[2];
|
|
}
|
|
$url = trim($this->_getUrl(), '/') . '/' . $eventId;
|
|
try {
|
|
$response = $this->_getClient($url)->request('GET');
|
|
} catch (Horde_Dav_Exception $e) {
|
|
throw new Kronolith_Exception($e);
|
|
}
|
|
if ($response['statusCode'] == 200) {
|
|
$ical = new Horde_Icalendar();
|
|
try {
|
|
$ical->parsevCalendar($response['body']);
|
|
} catch (Horde_Icalendar_Exception $e) {
|
|
throw new Kronolith_Exception($e);
|
|
}
|
|
$results = array();
|
|
$this->_processComponents($results, $ical, null, null, false,
|
|
false, false, false, $eventId);
|
|
$event = reset(reset($results));
|
|
if (!$event) {
|
|
throw new Horde_Exception_NotFound(_("Event not found"));
|
|
}
|
|
return $event;
|
|
}
|
|
}
|
|
|
|
$eventId = str_replace('ical', '', $eventId);
|
|
$ical = $this->getRemoteCalendar();
|
|
$components = $ical->getComponents();
|
|
if (isset($components[$eventId]) &&
|
|
$components[$eventId]->getType() == 'vEvent') {
|
|
$event = new Kronolith_Event_Ical($this, $components[$eventId]);
|
|
$event->permission = $this->getPermission();
|
|
$event->id = 'ical' . $eventId;
|
|
return $event;
|
|
}
|
|
|
|
throw new Horde_Exception_NotFound(_("Event not found"));
|
|
}
|
|
|
|
/**
|
|
* Updates an existing event in the backend.
|
|
*
|
|
* @param Kronolith_Event $event The event to save.
|
|
*
|
|
* @return string The event id.
|
|
* @throws Horde_Mime_Exception
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
protected function _updateEvent(Kronolith_Event $event)
|
|
{
|
|
$response = $this->_saveEvent($event);
|
|
if (!in_array($response['statusCode'], array(200, 204))) {
|
|
Horde::log(sprintf('Failed to update event on remote calendar: url = "%s", status = %s',
|
|
$response['url'], $response['statusCode']), 'INFO');
|
|
throw new Kronolith_Exception(_("The event could not be updated on the remote server."));
|
|
}
|
|
return $event->id;
|
|
}
|
|
|
|
/**
|
|
* Adds an event to the backend.
|
|
*
|
|
* @param Kronolith_Event $event The event to save.
|
|
*
|
|
* @return string The event id.
|
|
* @throws Horde_Mime_Exception
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
protected function _addEvent(Kronolith_Event $event)
|
|
{
|
|
if (!$event->uid) {
|
|
$event->uid = (string)new Horde_Support_Uuid;
|
|
}
|
|
if (!$event->id) {
|
|
$event->id = $event->uid . '.ics';
|
|
}
|
|
|
|
$response = $this->_saveEvent($event);
|
|
if (!in_array($response['statusCode'], array(200, 201, 204))) {
|
|
Horde::log(sprintf('Failed to create event on remote calendar: status = %s',
|
|
$response['statusCode']), 'INFO');
|
|
throw new Kronolith_Exception(_("The event could not be added to the remote server."));
|
|
}
|
|
return $event->id;
|
|
}
|
|
|
|
/**
|
|
* Updates an existing event in the backend.
|
|
*
|
|
* @param Kronolith_Event $event The event to save.
|
|
*
|
|
* @return string The event id.
|
|
* @throws Horde_Mime_Exception
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
protected function _saveEvent($event)
|
|
{
|
|
$ical = new Horde_Icalendar();
|
|
$ical->addComponent($event->toiCalendar($ical));
|
|
|
|
$url = trim($this->_getUrl(), '/') . '/' . $event->id;
|
|
try {
|
|
return $this->_getClient($url)
|
|
->request(
|
|
'PUT',
|
|
'',
|
|
$ical->exportvCalendar(),
|
|
array('Content-Type' => 'text/calendar')
|
|
);
|
|
} catch (Horde_Dav_Exception $e) {
|
|
Horde::log($e, 'INFO');
|
|
throw new Kronolith_Exception($e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes an event.
|
|
*
|
|
* @param string $eventId The ID of the event to delete.
|
|
* @param boolean $silent Don't send notifications, used when deleting
|
|
* events in bulk from maintenance tasks.
|
|
*
|
|
* @throws Kronolith_Exception
|
|
* @throws Horde_Exception_NotFound
|
|
* @throws Horde_Mime_Exception
|
|
*/
|
|
protected function _deleteEvent($eventId, $silent = false)
|
|
{
|
|
/* Fetch the event for later use. */
|
|
if ($eventId instanceof Kronolith_Event) {
|
|
$event = $eventId;
|
|
$eventId = $event->id;
|
|
} else {
|
|
$event = $this->getEvent($eventId);
|
|
}
|
|
|
|
if (!$this->isCalDAV()) {
|
|
throw new Kronolith_Exception(_("Deleting events is not supported with this remote calendar."));
|
|
}
|
|
|
|
if (preg_match('/(.*)-(\d+)$/', $eventId)) {
|
|
throw new Kronolith_Exception(_("Cannot delete exceptions (yet)."));
|
|
}
|
|
|
|
$url = trim($this->_getUrl(), '/') . '/' . $eventId;
|
|
try {
|
|
$response = $this->_getClient($url)->request('DELETE');
|
|
} catch (Horde_Dav_Exception $e) {
|
|
Horde::log($e, 'INFO');
|
|
throw new Kronolith_Exception($e);
|
|
}
|
|
if (!in_array($response['statusCode'], array(200, 202, 204))) {
|
|
Horde::log(sprintf('Failed to delete event from remote calendar: url = "%s", status = %s',
|
|
$url, $response['statusCode']), 'INFO');
|
|
throw new Kronolith_Exception(_("The event could not be deleted from the remote server."));
|
|
}
|
|
|
|
return $event;
|
|
}
|
|
|
|
/**
|
|
* Fetches a remote calendar into the cache and return the data.
|
|
*
|
|
* @param boolean $cache Whether to return data from the cache.
|
|
*
|
|
* @return Horde_Icalendar The calendar data.
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
public function getRemoteCalendar($cache = true)
|
|
{
|
|
$url = $this->_getUrl();
|
|
$cacheOb = $GLOBALS['injector']->getInstance('Horde_Cache');
|
|
$cacheVersion = 2;
|
|
$signature = 'kronolith_remote_' . $cacheVersion . '_' . $url . '_' . serialize($this->_params);
|
|
if ($cache) {
|
|
$calendar = $cacheOb->get($signature, 3600);
|
|
if ($calendar) {
|
|
$calendar = unserialize($calendar);
|
|
if (!is_object($calendar)) {
|
|
throw new Kronolith_Exception($calendar);
|
|
}
|
|
return $calendar;
|
|
}
|
|
}
|
|
|
|
try {
|
|
$response = $this->_getClient($url)->request('GET');
|
|
if ($response['statusCode'] != 200) {
|
|
throw new Horde_Dav_Exception('Request Failed', $response['statusCode']);
|
|
}
|
|
} catch (Horde_Dav_Exception $e) {
|
|
Horde::log(
|
|
sprintf('Failed to retrieve remote calendar: url = "%s", status = %s',
|
|
$url, $e->getCode()),
|
|
'INFO'
|
|
);
|
|
$error = sprintf(
|
|
_("Could not open %s: %s"),
|
|
$url, $e->getMessage()
|
|
);
|
|
if ($cache) {
|
|
$cacheOb->set($signature, serialize($error));
|
|
}
|
|
throw new Kronolith_Exception($error, $e->getCode());
|
|
}
|
|
|
|
/* Log fetch at DEBUG level. */
|
|
Horde::log(
|
|
sprintf('Retrieved remote calendar for %s: url = "%s"',
|
|
$GLOBALS['registry']->getAuth(), $url),
|
|
'DEBUG'
|
|
);
|
|
|
|
$ical = new Horde_Icalendar();
|
|
try {
|
|
$ical->parsevCalendar($response['body']);
|
|
} catch (Horde_Icalendar_Exception $e) {
|
|
if ($cache) {
|
|
$cacheOb->set($signature, serialize($e->getMessage()));
|
|
}
|
|
throw new Kronolith_Exception($e);
|
|
}
|
|
|
|
if ($cache) {
|
|
$cacheOb->set($signature, serialize($ical));
|
|
}
|
|
|
|
return $ical;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the remote calendar is a CalDAV server, and propagates
|
|
* the $_davSupport propery with the server's DAV capabilities.
|
|
*
|
|
* @return boolean True if the remote calendar is a CalDAV server.
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
public function isCalDAV()
|
|
{
|
|
if (isset($this->_davSupport)) {
|
|
return $this->_davSupport
|
|
? in_array('calendar-access', $this->_davSupport)
|
|
: false;
|
|
}
|
|
|
|
$client = $this->_getClient($this->_getUrl());
|
|
try {
|
|
$this->_davSupport = $client->options();
|
|
} catch (Horde_Dav_Exception $e) {
|
|
if ($e->getCode() != 405) {
|
|
Horde::log($e, 'INFO');
|
|
}
|
|
$this->_davSupport = false;
|
|
return false;
|
|
}
|
|
|
|
if (!$this->_davSupport) {
|
|
$this->_davSupport = false;
|
|
return false;
|
|
}
|
|
|
|
if (!in_array('calendar-access', $this->_davSupport)) {
|
|
return false;
|
|
}
|
|
|
|
/* Check if this URL is a collection. */
|
|
try {
|
|
$properties = $client->propfind(
|
|
'',
|
|
array('{DAV:}resourcetype', '{DAV:}current-user-privilege-set')
|
|
);
|
|
} catch (\Sabre\DAV\Exception $e) {
|
|
Horde::log($e, 'INFO');
|
|
return false;
|
|
} catch (Horde_Dav_Exception $e) {
|
|
Horde::log($e, 'INFO');
|
|
return false;
|
|
}
|
|
|
|
if (!$properties['{DAV:}resourcetype']->is('{DAV:}collection')) {
|
|
throw new Kronolith_Exception(_("The remote server URL does not point to a CalDAV directory."));
|
|
}
|
|
|
|
/* Read ACLs. */
|
|
if (!empty($properties['{DAV:}current-user-privilege-set'])) {
|
|
$privileges = $properties['{DAV:}current-user-privilege-set'];
|
|
if ($privileges->has('{DAV:}read')) {
|
|
/* GET access. */
|
|
$this->_permission |= Horde_Perms::SHOW;
|
|
$this->_permission |= Horde_Perms::READ;
|
|
}
|
|
if ($privileges->has('{DAV:}write') ||
|
|
$privileges->has('{DAV:}write-content')) {
|
|
/* PUT access. */
|
|
$this->_permission |= Horde_Perms::EDIT;
|
|
}
|
|
if ($privileges->has('{DAV:}unbind')) {
|
|
/* DELETE access. */
|
|
$this->_permission |= Horde_Perms::DELETE;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns the permissions for the current calendar.
|
|
*
|
|
* @return integer A Horde_Perms permission bit mask.
|
|
*/
|
|
public function getPermission()
|
|
{
|
|
if ($this->isCalDAV()) {
|
|
return $this->_permission;
|
|
}
|
|
return Horde_Perms::SHOW | Horde_Perms::READ;
|
|
}
|
|
|
|
/**
|
|
* Sends a CalDAV request.
|
|
*
|
|
* @param string $method A request method.
|
|
* @param string $url A request URL.
|
|
* @param XMLWriter $xml An XMLWriter object with the body content.
|
|
* @param array $headers A hash with additional request headers.
|
|
*
|
|
* @return array The Horde_Http_Response object and the parsed
|
|
* SimpleXMLElement results.
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
protected function _request($method, $url, XMLWriter $xml = null,
|
|
array $headers = array())
|
|
{
|
|
try {
|
|
$response = $this->_getClient($url)
|
|
->request($method,
|
|
'',
|
|
$xml ? $xml->outputMemory() : null,
|
|
array_merge(array('Cache-Control' => 'no-cache',
|
|
'Pragma' => 'no-cache',
|
|
'Content-Type' => 'application/xml'),
|
|
$headers));
|
|
} catch (Horde_Dav_Exception $e) {
|
|
Horde::log($e, 'INFO');
|
|
throw new Kronolith_Exception($e);
|
|
}
|
|
if ($response['statusCode'] != 207) {
|
|
throw new Kronolith_Exception(_("Unexpected response from remote server."));
|
|
}
|
|
libxml_use_internal_errors(true);
|
|
try {
|
|
$xml = new SimpleXMLElement($response['body']);
|
|
} catch (Exception $e) {
|
|
throw new Kronolith_Exception($e);
|
|
}
|
|
return array($response, $xml);
|
|
}
|
|
|
|
/**
|
|
* Returns the URL of this calendar.
|
|
*
|
|
* Does any necessary trimming and URL scheme fixes on the user-provided
|
|
* calendar URL.
|
|
*
|
|
* @return string The URL of this calendar.
|
|
*/
|
|
protected function _getUrl()
|
|
{
|
|
$url = trim($this->calendar);
|
|
if (strpos($url, 'http') !== 0) {
|
|
$url = str_replace(array('webcal://', 'webdav://', 'webdavs://'),
|
|
array('http://', 'http://', 'https://'),
|
|
$url);
|
|
}
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Returns a configured, cached DAV client.
|
|
*
|
|
* @param string $uri The base URI for any requests.
|
|
*
|
|
* @return Horde_Dav_Client A DAV client.
|
|
*/
|
|
protected function _getClient($uri)
|
|
{
|
|
$hordeOptions = array(
|
|
'request.timeout' => isset($this->_params['timeout'])
|
|
? $this->_params['timeout']
|
|
: 5
|
|
);
|
|
$sabreOptions = array('baseUri' => $uri);
|
|
if (!empty($this->_params['user'])) {
|
|
$hordeOptions['request.username'] = $sabreOptions['userName'] = $this->_params['user'];
|
|
$hordeOptions['request.password'] = $sabreOptions['password'] = $this->_params['password'];
|
|
}
|
|
$sabreOptions['client'] = $GLOBALS['injector']
|
|
->getInstance('Horde_Core_Factory_HttpClient')
|
|
->create($hordeOptions);
|
|
|
|
$this->_client = new Horde_Dav_Client($sabreOptions);
|
|
|
|
return $this->_client;
|
|
}
|
|
|
|
}
|