1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20:
21: class Kronolith_Driver_Ical extends Kronolith_Driver
22: {
23: 24: 25: 26: 27: 28:
29: protected $_cache = array();
30:
31: 32: 33: 34: 35:
36: protected $_client;
37:
38: 39: 40: 41: 42:
43: protected $_davSupport;
44:
45: 46: 47: 48: 49:
50: protected $_permission;
51:
52: 53: 54: 55: 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: 66: 67: 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: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 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: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 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: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 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:
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: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 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:
300: $event->id = $id ? $id : 'ical' . $i;
301:
302: 303:
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:
315: if (
316:
317: ($endDate && $event->start->compareDateTime($endDate) > 0) ||
318:
319: ($startDate && !$event->recurs() &&
320: $event->end->compareDateTime($startDate) < 0) ||
321:
322: ($startDate && $event->recurs() &&
323:
324: ($event->recurrence->hasRecurEnd() &&
325: $event->recurrence->recurEnd->compareDateTime($startDate) < 0))) {
326: continue;
327: }
328:
329: $events[] = $event;
330: }
331: }
332:
333: 334:
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: 348: 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: 401: 402: 403: 404: 405: 406: 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: 421: 422: 423: 424: 425: 426: 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: 448: 449: 450: 451: 452: 453: 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: 472: 473: 474: 475: 476: 477: 478: 479: 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: 507: 508: 509: 510: 511: 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:
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: 579: 580: 581: 582: 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:
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:
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:
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:
642: $this->_permission |= Horde_Perms::SHOW;
643: $this->_permission |= Horde_Perms::READ;
644: } elseif ($privilege->write || $privilege->{'write-content'}) {
645:
646: $this->_permission |= Horde_Perms::EDIT;
647: } elseif ($privilege->unbind) {
648:
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: 663: 664: 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: 676: 677: 678: 679: 680: 681: 682: 683: 684: 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: 717: 718: 719: 720: 721: 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: 736: 737: 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: