Overview

Packages

  • Kronolith
  • None

Classes

  • Kronolith
  • Kronolith_Ajax_Application
  • Kronolith_Ajax_Imple_ContactAutoCompleter
  • Kronolith_Ajax_Imple_Embed
  • Kronolith_Ajax_Imple_TagActions
  • Kronolith_Ajax_Imple_TagAutoCompleter
  • Kronolith_Api
  • Kronolith_Calendar
  • Kronolith_Calendar_External
  • Kronolith_Calendar_External_Tasks
  • Kronolith_Calendar_Holiday
  • Kronolith_Calendar_Internal
  • Kronolith_Calendar_Remote
  • Kronolith_Calendar_Resource
  • Kronolith_Calendars_Base
  • Kronolith_Calendars_Default
  • Kronolith_Calendars_Kolab
  • Kronolith_Day
  • Kronolith_Driver
  • Kronolith_Driver_Holidays
  • Kronolith_Driver_Horde
  • Kronolith_Driver_Ical
  • Kronolith_Driver_Kolab
  • Kronolith_Driver_Mock
  • Kronolith_Driver_Resource
  • Kronolith_Driver_Sql
  • Kronolith_Event
  • Kronolith_Event_Holidays
  • Kronolith_Event_Horde
  • Kronolith_Event_Ical
  • Kronolith_Event_Kolab
  • Kronolith_Event_Resource
  • Kronolith_Event_Sql
  • Kronolith_Exception
  • Kronolith_Factory_Calendars
  • Kronolith_Factory_Geo
  • Kronolith_Form_CreateCalendar
  • Kronolith_Form_CreateResource
  • Kronolith_Form_CreateResourceGroup
  • Kronolith_Form_DeleteCalendar
  • Kronolith_Form_DeleteResource
  • Kronolith_Form_DeleteResourceGroup
  • Kronolith_Form_EditCalendar
  • Kronolith_Form_EditRemoteCalendar
  • Kronolith_Form_EditResource
  • Kronolith_Form_EditResourceGroup
  • Kronolith_Form_SubscribeRemoteCalendar
  • Kronolith_Form_UnsubscribeRemoteCalendar
  • Kronolith_FreeBusy
  • Kronolith_FreeBusy_View
  • Kronolith_FreeBusy_View_Day
  • Kronolith_FreeBusy_View_Month
  • Kronolith_FreeBusy_View_Week
  • Kronolith_FreeBusy_View_Workweek
  • Kronolith_Geo_Base
  • Kronolith_Geo_Mysql
  • Kronolith_Geo_Sql
  • Kronolith_LoginTasks_SystemTask_Upgrade
  • Kronolith_LoginTasks_Task_PurgeEvents
  • Kronolith_Notification_Listener_AjaxStatus
  • Kronolith_Resource
  • Kronolith_Resource_Base
  • Kronolith_Resource_Group
  • Kronolith_Resource_Single
  • Kronolith_Storage
  • Kronolith_Storage_Kolab
  • Kronolith_Storage_Sql
  • Kronolith_Tagger
  • Kronolith_Test
  • Kronolith_View_Day
  • Kronolith_View_DeleteEvent
  • Kronolith_View_EditEvent
  • Kronolith_View_Event
  • Kronolith_View_ExportEvent
  • Kronolith_View_Month
  • Kronolith_View_Week
  • Kronolith_View_WorkWeek
  • Kronolith_View_Year
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * The Kronolith_Driver_Ical class implements the Kronolith_Driver API for
  4:  * iCalendar data.
  5:  *
  6:  * Possible driver parameters:
  7:  * - url:      The location of the remote calendar.
  8:  * - proxy:    A hash with HTTP proxy information.
  9:  * - user:     The user name for HTTP Basic Authentication.
 10:  * - password: The password for HTTP Basic Authentication.
 11:  *
 12:  * Copyright 2004-2012 Horde LLC (http://www.horde.org/)
 13:  *
 14:  * See the enclosed file COPYING for license information (GPL). If you
 15:  * did not receive this file, see http://www.horde.org/licenses/gpl.
 16:  *
 17:  * @author  Chuck Hagenbuch <chuck@horde.org>
 18:  * @author  Jan Schneider <jan@horde.org>
 19:  * @package Kronolith
 20:  */
 21: class Kronolith_Driver_Ical extends Kronolith_Driver
 22: {
 23:     /**
 24:      * Cache events as we fetch them to avoid fetching or parsing the same
 25:      * event twice.
 26:      *
 27:      * @var array
 28:      */
 29:     protected $_cache = array();
 30: 
 31:     /**
 32:      * HTTP client object.
 33:      *
 34:      * @var Horde_Http_Client
 35:      */
 36:     protected $_client;
 37: 
 38:     /**
 39:      * A list of DAV support levels.
 40:      *
 41:      * @var array
 42:      */
 43:     protected $_davSupport;
 44: 
 45:     /**
 46:      * The Horde_Perms permissions mask matching the CalDAV ACL.
 47:      *
 48:      * @var integer
 49:      */
 50:     protected $_permission;
 51: 
 52:     /**
 53:      * Selects a calendar as the currently opened calendar.
 54:      *
 55:      * @param string $calendar  A calendar identifier.
 56:      */
 57:     public function open($calendar)
 58:     {
 59:         parent::open($calendar);
 60:         $this->_client = null;
 61:         unset($this->_davSupport, $this->_permission);
 62:     }
 63: 
 64:     /**
 65:      * Returns the background color of the current calendar.
 66:      *
 67:      * @return string  The calendar color.
 68:      */
 69:     public function backgroundColor()
 70:     {
 71:         return empty($GLOBALS['all_remote_calendars'][$this->calendar])
 72:             ? '#dddddd'
 73:             : $GLOBALS['all_remote_calendars'][$this->calendar]->background();
 74:     }
 75: 
 76:     public function listAlarms($date, $fullevent = false)
 77:     {
 78:         return array();
 79:     }
 80: 
 81:     /**
 82:      * Lists all events in the time range, optionally restricting results to
 83:      * only events with alarms.
 84:      *
 85:      * @param Horde_Date $startInterval  Start of range date object.
 86:      * @param Horde_Date $endInterval    End of range data object.
 87:      * @param boolean $showRecurrence    Return every instance of a recurring
 88:      *                                   event? If false, will only return
 89:      *                                   recurring events once inside the
 90:      *                                   $startDate - $endDate range.
 91:      * @param boolean $hasAlarm          Only return events with alarms?
 92:      * @param boolean $json              Store the results of the events'
 93:      *                                   toJson() method?
 94:      * @param boolean $coverDates        Whether to add the events to all days
 95:      *                                   that they cover.
 96:      *
 97:      * @return array  Events in the given time range.
 98:      * @throws Kronolith_Exception
 99:      */
100:     public function listEvents($startDate = null, $endDate = null,
101:                                $showRecurrence = false, $hasAlarm = false,
102:                                $json = false, $coverDates = true)
103:     {
104:         if ($this->isCalDAV()) {
105:             return $this->_listCalDAVEvents($startDate, $endDate,
106:                                             $showRecurrence, $hasAlarm,
107:                                             $json, $coverDates);
108:         }
109:         return $this->_listWebDAVEvents($startDate, $endDate,
110:                                         $showRecurrence, $hasAlarm,
111:                                         $json, $coverDates);
112:     }
113: 
114:     /**
115:      * Lists all events in the time range, optionally restricting results to
116:      * only events with alarms.
117:      *
118:      * @param Horde_Date $startInterval  Start of range date object.
119:      * @param Horde_Date $endInterval    End of range data object.
120:      * @param boolean $showRecurrence    Return every instance of a recurring
121:      *                                   event? If false, will only return
122:      *                                   recurring events once inside the
123:      *                                   $startDate - $endDate range.
124:      * @param boolean $hasAlarm          Only return events with alarms?
125:      * @param boolean $json              Store the results of the events'
126:      *                                   toJson() method?
127:      * @param boolean $coverDates        Whether to add the events to all days
128:      *                                   that they cover.
129:      *
130:      * @return array  Events in the given time range.
131:      * @throws Kronolith_Exception
132:      */
133:     protected function _listWebDAVEvents($startDate = null, $endDate = null,
134:                                          $showRecurrence = false,
135:                                          $hasAlarm = false, $json = false,
136:                                          $coverDates = true)
137:     {
138:         $ical = $this->getRemoteCalendar();
139: 
140:         if (is_null($startDate)) {
141:             $startDate = new Horde_Date(array('mday' => 1,
142:                                               'month' => 1,
143:                                               'year' => 0000));
144:         }
145:         if (is_null($endDate)) {
146:             $endDate = new Horde_Date(array('mday' => 31,
147:                                             'month' => 12,
148:                                             'year' => 9999));
149:         }
150: 
151:         $startDate = clone $startDate;
152:         $startDate->hour = $startDate->min = $startDate->sec = 0;
153:         $endDate = clone $endDate;
154:         $endDate->hour = 23;
155:         $endDate->min = $endDate->sec = 59;
156: 
157:         $results = array();
158:         $this->_processComponents($results, $ical, $startDate, $endDate,
159:                                   $showRecurrence, $json, $coverDates);
160: 
161:         return $results;
162:     }
163: 
164:     /**
165:      * Lists all events in the time range, optionally restricting results to
166:      * only events with alarms.
167:      *
168:      * @param Horde_Date $startInterval  Start of range date object.
169:      * @param Horde_Date $endInterval    End of range data object.
170:      * @param boolean $showRecurrence    Return every instance of a recurring
171:      *                                   event? If false, will only return
172:      *                                   recurring events once inside the
173:      *                                   $startDate - $endDate range.
174:      * @param boolean $hasAlarm          Only return events with alarms?
175:      * @param boolean $json              Store the results of the events'
176:      *                                   toJson() method?
177:      * @param boolean $coverDates        Whether to add the events to all days
178:      *                                   that they cover.
179:      *
180:      * @return array  Events in the given time range.
181:      * @throws Kronolith_Exception
182:      */
183:     protected function _listCalDAVEvents($startDate = null, $endDate = null,
184:                                          $showRecurrence = false,
185:                                          $hasAlarm = false, $json = false,
186:                                          $coverDates = true)
187:     {
188:         if (!is_null($startDate)) {
189:             $startDate = clone $startDate;
190:             $startDate->hour = $startDate->min = $startDate->sec = 0;
191:         }
192:         if (!is_null($endDate)) {
193:             $endDate = clone $endDate;
194:             $endDate->hour = 23;
195:             $endDate->min = $endDate->sec = 59;
196:         }
197: 
198:         /* Build report query. */
199:         $xml = new XMLWriter();
200:         $xml->openMemory();
201:         $xml->setIndent(true);
202:         $xml->startDocument();
203:         $xml->startElementNS('C', 'calendar-query', 'urn:ietf:params:xml:ns:caldav');
204:         $xml->writeAttribute('xmlns:D', 'DAV:');
205:         $xml->startElement('D:prop');
206:         $xml->writeElement('D:getetag');
207:         $xml->startElement('C:calendar-data');
208:         $xml->startElement('C:comp');
209:         $xml->writeAttribute('name', 'VCALENDAR');
210:         $xml->startElement('C:comp');
211:         $xml->writeAttribute('name', 'VEVENT');
212:         $xml->endElement();
213:         $xml->endElement();
214:         $xml->endElement();
215:         $xml->endElement();
216:         $xml->startElement('C:filter');
217:         $xml->startElement('C:comp-filter');
218:         $xml->writeAttribute('name', 'VCALENDAR');
219:         $xml->startElement('C:comp-filter');
220:         $xml->writeAttribute('name', 'VEVENT');
221:         if (!is_null($startDate) ||
222:             !is_null($endDate)) {
223:             $xml->startElement('C:time-range');
224:             if (!is_null($startDate)) {
225:                 $xml->writeAttribute('start', $startDate->toiCalendar());
226:             }
227:             if (!is_null($endDate)) {
228:                 $xml->writeAttribute('end', $endDate->toiCalendar());
229:             }
230:         }
231:         $xml->endDocument();
232: 
233:         $url = $this->_getUrl();
234:         list($response, $events) = $this->_request('REPORT', $url, $xml,
235:                                           array('Depth' => 1));
236:         if (!$events->children('DAV:')->response) {
237:             return array();
238:         }
239:         if (!($path = $response->getHeader('content-location'))) {
240:             $parsedUrl = parse_url($url);
241:             $path = $parsedUrl['path'];
242:         }
243: 
244:         $results = array();
245:         foreach ($events->children('DAV:')->response as $response) {
246:             if (!$response->children('DAV:')->propstat) {
247:                 continue;
248:             }
249:             $ical = new Horde_Icalendar();
250:             try {
251:                 $result = $ical->parsevCalendar($response->children('DAV:')->propstat->prop->children('urn:ietf:params:xml:ns:caldav')->{'calendar-data'});
252:             } catch (Horde_Icalendar_Exception $e) {
253:                 throw new Kronolith_Exception($e);
254:             }
255:             $this->_processComponents($results, $ical, $startDate, $endDate,
256:                                       $showRecurrence, $json, $coverDates,
257:                                       trim(str_replace($path, '', $response->href), '/'));
258:         }
259: 
260:         return $results;
261:     }
262: 
263:     /**
264:      * Processes the components of a Horde_Icalendar container into an event
265:      * list.
266:      *
267:      * @param array $results             Gets filled with the events in the
268:      *                                   given time range.
269:      * @param Horde_Icalendar $ical      An Horde_Icalendar container.
270:      * @param Horde_Date $startInterval  Start of range date.
271:      * @param Horde_Date $endInterval    End of range date.
272:      * @param boolean $showRecurrence    Return every instance of a recurring
273:      *                                   event? If false, will only return
274:      *                                   recurring events once inside the
275:      *                                   $startDate - $endDate range.
276:      * @param boolean $json              Store the results of the events'
277:      *                                   toJson() method?
278:      * @param boolean $coverDates        Whether to add the events to all days
279:      *                                   that they cover.
280:      * @param string $id                 Enforce a certain event id (not UID).
281:      *
282:      * @throws Kronolith_Exception
283:      */
284:     protected function _processComponents(&$results, $ical, $startDate,
285:                                           $endDate, $showRecurrence, $json,
286:                                           $coverDates, $id = null)
287:     {
288:         $components = $ical->getComponents();
289:         $events = array();
290:         $count = count($components);
291:         $exceptions = array();
292:         for ($i = 0; $i < $count; $i++) {
293:             $component = $components[$i];
294:             if ($component->getType() == 'vEvent') {
295:                 $event = new Kronolith_Event_Ical($this);
296:                 $event->status = Kronolith::STATUS_FREE;
297:                 $event->permission = $this->getPermission();
298:                 $event->fromDriver($component);
299:                 // Force string so JSON encoding is consistent across drivers.
300:                 $event->id = $id ? $id : 'ical' . $i;
301: 
302:                 /* Catch RECURRENCE-ID attributes which mark single recurrence
303:                  * instances. */
304:                 try {
305:                     $recurrence_id = $component->getAttribute('RECURRENCE-ID');
306:                     if (is_int($recurrence_id) &&
307:                         is_string($uid = $component->getAttribute('UID')) &&
308:                         is_int($seq = $component->getAttribute('SEQUENCE'))) {
309:                         $exceptions[$uid][$seq] = $recurrence_id;
310:                         $event->id .= '/' . $recurrence_id;
311:                     }
312:                 } catch (Horde_Icalendar_Exception $e) {}
313: 
314:                 /* Ignore events out of the period. */
315:                 if (
316:                     /* Starts after the period. */
317:                     ($endDate && $event->start->compareDateTime($endDate) > 0) ||
318:                     /* End before the period and doesn't recur. */
319:                     ($startDate && !$event->recurs() &&
320:                      $event->end->compareDateTime($startDate) < 0) ||
321:                     /* Recurs and ... */
322:                     ($startDate && $event->recurs() &&
323:                       /* ... has a recurrence end before the period. */
324:                       ($event->recurrence->hasRecurEnd() &&
325:                        $event->recurrence->recurEnd->compareDateTime($startDate) < 0))) {
326:                     continue;
327:                 }
328: 
329:                 $events[] = $event;
330:             }
331:         }
332: 
333:         /* Loop through all explicitly defined recurrence intances and create
334:          * exceptions for those in the event with the matching recurrence. */
335:         foreach ($events as $key => $event) {
336:             if ($event->recurs() &&
337:                 isset($exceptions[$event->uid][$event->sequence])) {
338:                 $timestamp = $exceptions[$event->uid][$event->sequence];
339:                 $events[$key]->recurrence->addException(date('Y', $timestamp), date('m', $timestamp), date('d', $timestamp));
340:             }
341:             Kronolith::addEvents($results, $event, $startDate, $endDate,
342:                                  $showRecurrence, $json, $coverDates);
343:         }
344:     }
345: 
346:     /**
347:      * @throws Kronolith_Exception
348:      * @throws Horde_Exception_NotFound
349:      */
350:     public function getEvent($eventId = null)
351:     {
352:         if (!$eventId) {
353:             $event = new Kronolith_Event_Ical($this);
354:             $event->permission = $this->getPermission();
355:             return $event;
356:         }
357: 
358:         if ($this->isCalDAV()) {
359:             if (preg_match('/(.*)-(\d+)$/', $eventId, $matches)) {
360:                 $eventId = $matches[1];
361:                 $recurrenceId = $matches[2];
362:             }
363:             $url = trim($this->_getUrl(), '/') . '/' . $eventId;
364:             $response = $this->_getClient()->get($url);
365:             if ($response->code == 200) {
366:                 $ical = new Horde_Icalendar();
367:                 try {
368:                     $ical->parsevCalendar($response->getBody());
369:                 } catch (Horde_Icalendar_Exception $e) {
370:                     throw new Kronolith_Exception($e);
371:                 }
372:                 $results = array();
373:                 $this->_processComponents($results, $ical, null, null, false,
374:                                           false, false, $eventId);
375:                 $event = reset(reset($results));
376:                 if (!$event) {
377:                     throw new Horde_Exception_NotFound(_("Event not found"));
378:                 }
379:                 return $event;
380:             }
381:         }
382: 
383:         $eventId = str_replace('ical', '', $eventId);
384:         $ical = $this->getRemoteCalendar();
385:         $components = $ical->getComponents();
386:         if (isset($components[$eventId]) &&
387:             $components[$eventId]->getType() == 'vEvent') {
388:             $event = new Kronolith_Event_Ical($this);
389:             $event->status = Kronolith::STATUS_FREE;
390:             $event->permission = $this->getPermission();
391:             $event->fromDriver($components[$eventId]);
392:             $event->id = 'ical' . $eventId;
393:             return $event;
394:         }
395: 
396:         throw new Horde_Exception_NotFound(_("Event not found"));
397:     }
398: 
399:     /**
400:      * Updates an existing event in the backend.
401:      *
402:      * @param Kronolith_Event $event  The event to save.
403:      *
404:      * @return string  The event id.
405:      * @throws Horde_Mime_Exception
406:      * @throws Kronolith_Exception
407:      */
408:     protected function _updateEvent(Kronolith_Event $event)
409:     {
410:         $response = $this->_saveEvent($event);
411:         if (!in_array($response->code, array(200, 204))) {
412:             Horde::logMessage(sprintf('Failed to update event on remote calendar: url = "%s", status = %s',
413:                                       $url, $response->code), 'INFO');
414:             throw new Kronolith_Exception(_("The event could not be updated on the remote server."));
415:         }
416:         return $event->id;
417:     }
418: 
419:     /**
420:      * Adds an event to the backend.
421:      *
422:      * @param Kronolith_Event $event  The event to save.
423:      *
424:      * @return string  The event id.
425:      * @throws Horde_Mime_Exception
426:      * @throws Kronolith_Exception
427:      */
428:     protected function _addEvent(Kronolith_Event $event)
429:     {
430:         if (!$event->uid) {
431:             $event->uid = (string)new Horde_Support_Uuid;
432:         }
433:         if (!$event->id) {
434:             $event->id = $event->uid . '.ics';
435:         }
436: 
437:         $response = $this->_saveEvent($event);
438:         if (!in_array($response->code, array(200, 201, 204))) {
439:             Horde::logMessage(sprintf('Failed to create event on remote calendar: status = %s',
440:                                       $response->code), 'INFO');
441:             throw new Kronolith_Exception(_("The event could not be added to the remote server."));
442:         }
443:         return $event->id;
444:     }
445: 
446:     /**
447:      * Updates an existing event in the backend.
448:      *
449:      * @param Kronolith_Event $event  The event to save.
450:      *
451:      * @return string  The event id.
452:      * @throws Horde_Mime_Exception
453:      * @throws Kronolith_Exception
454:      */
455:     protected function _saveEvent($event)
456:     {
457:         $ical = new Horde_Icalendar();
458:         $ical->addComponent($event->toiCalendar($ical));
459: 
460:         $url = trim($this->_getUrl(), '/') . '/' . $event->id;
461:         try {
462:             $response = $this->_getClient()->put($url, $ical->exportvCalendar(), array('Content-Type' => 'text/calendar'));
463:         } catch (Horde_Http_Exception $e) {
464:             Horde::logMessage($e, 'INFO');
465:             throw new Kronolith_Exception($e);
466:         }
467:         return $response;
468:     }
469: 
470:     /**
471:      * Deletes an event.
472:      *
473:      * @param string $eventId  The ID of the event to delete.
474:      * @param boolean $silent  Don't send notifications, used when deleting
475:      *                         events in bulk from maintenance tasks.
476:      *
477:      * @throws Kronolith_Exception
478:      * @throws Horde_Exception_NotFound
479:      * @throws Horde_Mime_Exception
480:      */
481:     public function deleteEvent($eventId, $silent = false)
482:     {
483:         if (!$this->isCalDAV()) {
484:             throw new Kronolith_Exception(_("Deleting events is not supported with this remote calendar."));
485:         }
486: 
487:         if (preg_match('/(.*)-(\d+)$/', $eventId, $matches)) {
488:             throw new Kronolith_Exception(_("Cannot delete exceptions (yet)."));
489:         }
490: 
491:         $url = trim($this->_getUrl(), '/') . '/' . $eventId;
492:         try {
493:             $response = $this->_getClient()->delete($url);
494:         } catch (Horde_Http_Exception $e) {
495:             Horde::logMessage($e, 'INFO');
496:             throw new Kronolith_Exception($e);
497:         }
498:         if (!in_array($response->code, array(200, 202, 204))) {
499:             Horde::logMessage(sprintf('Failed to delete event from remote calendar: url = "%s", status = %s',
500:                                       $url, $response->code), 'INFO');
501:             throw new Kronolith_Exception(_("The event could not be deleted from the remote server."));
502:         }
503:     }
504: 
505:     /**
506:      * Fetches a remote calendar into the cache and return the data.
507:      *
508:      * @param boolean $cache  Whether to return data from the cache.
509:      *
510:      * @return Horde_Icalendar  The calendar data.
511:      * @throws Kronolith_Exception
512:      */
513:     public function getRemoteCalendar($cache = true)
514:     {
515:         $url = $this->_getUrl();
516:         $cacheOb = $GLOBALS['injector']->getInstance('Horde_Cache');
517:         $cacheVersion = 2;
518:         $signature = 'kronolith_remote_'  . $cacheVersion . '_' . $url . '_' . serialize($this->_params);
519:         if ($cache) {
520:             $calendar = $cacheOb->get($signature, 3600);
521:             if ($calendar) {
522:                 $calendar = unserialize($calendar);
523:                 if (!is_object($calendar)) {
524:                     throw new Kronolith_Exception($calendar);
525:                 }
526:                 return $calendar;
527:             }
528:         }
529: 
530:         $http = $this->_getClient();
531:         try {
532:             $response = $http->get($url);
533:         } catch (Horde_Http_Exception $e) {
534:             Horde::logMessage($e, 'INFO');
535:             if ($cache) {
536:                 $cacheOb->set($signature, serialize($e->getMessage()));
537:             }
538:             throw new Kronolith_Exception($e);
539:         }
540:         if ($response->code != 200) {
541:             Horde::logMessage(sprintf('Failed to retrieve remote calendar: url = "%s", status = %s',
542:                                       $url, $response->code), 'INFO');
543:             $error = sprintf(_("Could not open %s."), $url);
544:             $body = $response->getBody();
545:             if ($body) {
546:                 $error .= ' ' . _("This is what the server said:")
547:                     . ' ' . Horde_String::truncate(strip_tags($body));
548:             }
549:             if ($cache) {
550:                 $cacheOb->set($signature, serialize($error));
551:             }
552:             throw new Kronolith_Exception($error, $response->code);
553:         }
554: 
555:         /* Log fetch at DEBUG level. */
556:         Horde::logMessage(sprintf('Retrieved remote calendar for %s: url = "%s"',
557:                                   $GLOBALS['registry']->getAuth(), $url), 'DEBUG');
558: 
559:         $data = $response->getBody();
560:         $ical = new Horde_Icalendar();
561:         try {
562:             $result = $ical->parsevCalendar($data);
563:         } catch (Horde_Icalendar_Exception $e) {
564:             if ($cache) {
565:                 $cacheOb->set($signature, serialize($e->getMessage()));
566:             }
567:             throw new Kronolith_Exception($e);
568:         }
569: 
570:         if ($cache) {
571:             $cacheOb->set($signature, serialize($ical));
572:         }
573: 
574:         return $ical;
575:     }
576: 
577:     /**
578:      * Returns whether the remote calendar is a CalDAV server, and propagates
579:      * the $_davSupport propery with the server's DAV capabilities.
580:      *
581:      * @return boolean  True if the remote calendar is a CalDAV server.
582:      * @throws Kronolith_Exception
583:      */
584:     public function isCalDAV()
585:     {
586:         if (isset($this->_davSupport)) {
587:             return $this->_davSupport
588:                 ? in_array('calendar-access', $this->_davSupport)
589:                 : false;
590:         }
591: 
592:         $url = $this->_getUrl();
593:         $http = $this->_getClient();
594:         try {
595:             $response = $http->request('OPTIONS', $url);
596:         } catch (Horde_Http_Exception $e) {
597:             Horde::logMessage($e, 'INFO');
598:             return false;
599:         }
600:         if ($response->code != 200) {
601:             $this->_davSupport = false;
602:             return false;
603:         }
604: 
605:         if ($dav = $response->getHeader('dav')) {
606:             /* Check for DAV support. */
607:             if (is_array($dav)) {
608:                 $dav = implode (',', $dav);
609:             }
610:             $this->_davSupport = preg_split('/,\s*/', $dav);
611:             if (!in_array('3', $this->_davSupport)) {
612:                 Horde::logMessage(sprintf('The remote server at %s doesn\'t support an WebDAV protocol version 3.', $url), 'WARN');
613:             }
614:             if (!in_array('calendar-access', $this->_davSupport)) {
615:                 return false;
616:             }
617: 
618:             /* Check if this URL is a collection. */
619:             $xml = new XMLWriter();
620:             $xml->openMemory();
621:             $xml->startDocument();
622:             $xml->startElement('propfind');
623:             $xml->writeAttribute('xmlns', 'DAV:');
624:             $xml->startElement('prop');
625:             $xml->writeElement('resourcetype');
626:             $xml->writeElement('current-user-privilege-set');
627:             $xml->endDocument();
628:             list(, $properties) = $this->_request('PROPFIND', $url, $xml,
629:                                                   array('Depth' => 0));
630:             if (!$properties->children('DAV:')->response->propstat->prop->resourcetype->collection) {
631:                 throw new Kronolith_Exception(_("The remote server URL does not point to a CalDAV directory."));
632:             }
633: 
634:             /* Read ACLs. */
635:             if ($properties->children('DAV:')->response->propstat->prop->{'current-user-privilege-set'}) {
636:                 foreach ($properties->children('DAV:')->response->propstat->prop->{'current-user-privilege-set'}->privilege as $privilege) {
637:                     if ($privilege->all) {
638:                         $this->_permission = Horde_Perms::ALL;
639:                         break;
640:                     } elseif ($privilege->read) {
641:                         /* GET access. */
642:                         $this->_permission |= Horde_Perms::SHOW;
643:                         $this->_permission |= Horde_Perms::READ;
644:                     } elseif ($privilege->write || $privilege->{'write-content'}) {
645:                         /* PUT access. */
646:                         $this->_permission |= Horde_Perms::EDIT;
647:                     } elseif ($privilege->unbind) {
648:                         /* DELETE access. */
649:                         $this->_permission |= Horde_Perms::DELETE;
650:                     }
651:                 }
652:             }
653: 
654:             return true;
655:         }
656: 
657:         $this->_davSupport = false;
658:         return false;
659:     }
660: 
661:     /**
662:      * Returns the permissions for the current calendar.
663:      *
664:      * @return integer  A Horde_Perms permission bit mask.
665:      */
666:     public function getPermission()
667:     {
668:         if ($this->isCalDAV()) {
669:             return $this->_permission;
670:         }
671:         return Horde_Perms::SHOW | Horde_Perms::READ;
672:     }
673: 
674:     /**
675:      * Sends a CalDAV request.
676:      *
677:      * @param string $method  A request method.
678:      * @param string $url     A request URL.
679:      * @param XMLWriter $xml  An XMLWriter object with the body content.
680:      * @param array $headers  A hash with additional request headers.
681:      *
682:      * @return array  The Horde_Http_Response object and the parsed
683:      *                SimpleXMLElement results.
684:      * @throws Kronolith_Exception
685:      */
686:     protected function _request($method, $url, XMLWriter $xml = null,
687:                                 array $headers = array())
688:     {
689:         try {
690:             $response = $this->_getClient()
691:                 ->request($method,
692:                           $url,
693:                           $xml ? $xml->outputMemory() : null,
694:                           array_merge(array('Cache-Control' => 'no-cache',
695:                                             'Pragma' => 'no-cache',
696:                                             'Content-Type' => 'application/xml'),
697:                                       $headers));
698:         } catch (Horde_Http_Exception $e) {
699:             Horde::logMessage($e, 'INFO');
700:             throw new Kronolith_Exception($e);
701:         }
702:         if ($response->code != 207) {
703:             throw new Kronolith_Exception(_("Unexpected response from remote server."));
704:         }
705:         libxml_use_internal_errors(true);
706:         try {
707:             $body = $response->getBody();
708:             $xml = new SimpleXMLElement($body);
709:         } catch (Exception $e) {
710:             throw new Kronolith_Exception($e);
711:         }
712:         return array($response, $xml);
713:     }
714: 
715:     /**
716:      * Returns the URL of this calendar.
717:      *
718:      * Does any necessary trimming and URL scheme fixes on the user-provided
719:      * calendar URL.
720:      *
721:      * @return string  The URL of this calendar.
722:      */
723:     protected function _getUrl()
724:     {
725:         $url = trim($this->calendar);
726:         if (strpos($url, 'http') !== 0) {
727:             $url = str_replace(array('webcal://', 'webdav://', 'webdavs://'),
728:                                array('http://', 'http://', 'https://'),
729:                                $url);
730:         }
731:         return $url;
732:     }
733: 
734:     /**
735:      * Returns a configured, cached HTTP client.
736:      *
737:      * @return Horde_Http_Client  A HTTP client.
738:      */
739:     protected function _getClient()
740:     {
741:         $options = array('request.timeout' => isset($this->_params['timeout'])
742:                                               ? $this->_params['timeout']
743:                                               : 5);
744:         if (!empty($this->_params['user'])) {
745:             $options['request.username'] = $this->_params['user'];
746:             $options['request.password'] = $this->_params['password'];
747:         }
748: 
749:         $this->_client = $GLOBALS['injector']
750:             ->getInstance('Horde_Core_Factory_HttpClient')
751:             ->create($options);
752: 
753:         return $this->_client;
754:     }
755: 
756: }
757: 
API documentation generated by ApiGen