76182c112850343c9cb69a4574e70c310d3704ca
[tine20] / tine20 / Calendar / Convert / Event / VCalendar / Abstract.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Calendar
6  * @subpackage  Frontend
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Lars Kneschke <l.kneschke@metaways.de>
9  * @copyright   Copyright (c) 2011-2014 Metaways Infosystems GmbH (http://www.metaways.de)
10  *
11  */
12
13 /**
14  * abstract class to convert a single event (repeating with exceptions) to/from VCalendar
15  *
16  * @package     Calendar
17  * @subpackage  Convert
18  */
19 class Calendar_Convert_Event_VCalendar_Abstract extends Tinebase_Convert_VCalendar_Abstract implements Tinebase_Convert_Interface
20 {
21     public static $cutypeMap = array(
22         Calendar_Model_Attender::USERTYPE_USER          => 'INDIVIDUAL',
23         Calendar_Model_Attender::USERTYPE_GROUPMEMBER   => 'INDIVIDUAL',
24         Calendar_Model_Attender::USERTYPE_GROUP         => 'GROUP',
25         Calendar_Model_Attender::USERTYPE_RESOURCE      => 'RESOURCE',
26     );
27     
28     protected $_modelName = 'Calendar_Model_Event';
29
30     /**
31      * value of METHOD property
32      * @var string
33      */
34     protected $_method;
35     
36     /**
37      * convert Tinebase_Record_RecordSet to Sabre\VObject\Component
38      *
39      * @param  Tinebase_Record_RecordSet  $_records
40      * @return Sabre\VObject\Component
41      */
42     public function fromTine20RecordSet(Tinebase_Record_RecordSet $_records)
43     {
44         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
45             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Events: ' . print_r($_records->toArray(), true));
46         
47         // required vcalendar fields
48         $version = Tinebase_Application::getInstance()->getApplicationByName('Calendar')->version;
49         
50         $vcalendar = new \Sabre\VObject\Component\VCalendar(array(
51             'PRODID'   => "-//tine20.com//Tine 2.0 Calendar V$version//EN",
52             'VERSION'  => '2.0',
53             'CALSCALE' => 'GREGORIAN'
54         ));
55         
56         if (isset($this->_method)) {
57             $vcalendar->add('METHOD', $this->_method);
58         }
59         
60         $originatorTz = $_records->getFirstRecord() ? $_records->getFirstRecord()->originator_tz : NULL;
61         if (empty($originatorTz)) {
62             throw new Tinebase_Exception_Record_Validation('originator_tz needed for conversion to Sabre\VObject\Component');
63         }
64         
65         $vcalendar->add(new Sabre_VObject_Component_VTimezone($originatorTz));
66         
67         foreach ($_records as $_record) {
68             $this->_convertCalendarModelEvent($vcalendar, $_record);
69             
70             if ($_record->exdate instanceof Tinebase_Record_RecordSet) {
71                 $_record->exdate->addIndices(array('is_deleted'));
72                 $eventExceptions = $_record->exdate->filter('is_deleted', false);
73                 
74                 foreach ($eventExceptions as $eventException) {
75                     $this->_convertCalendarModelEvent($vcalendar, $eventException, $_record);
76                 }
77                 
78             }
79         }
80         
81         $this->_afterFromTine20Model($vcalendar);
82         
83         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
84             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' card ' . $vcalendar->serialize());
85         
86         return $vcalendar;
87     }
88
89     /**
90      * convert Calendar_Model_Event to Sabre\VObject\Component
91      *
92      * @param  Calendar_Model_Event  $_record
93      * @return Sabre\VObject\Component
94      */
95     public function fromTine20Model(Tinebase_Record_Abstract $_record)
96     {
97         $_records = new Tinebase_Record_RecordSet(get_class($_record), array($_record), true, false);
98         
99         return $this->fromTine20RecordSet($_records);
100     }
101     
102     /**
103      * convert calendar event to Sabre\VObject\Component
104      * 
105      * @param  \Sabre\VObject\Component\VCalendar $vcalendar
106      * @param  Calendar_Model_Event               $_event
107      * @param  Calendar_Model_Event               $_mainEvent
108      */
109     protected function _convertCalendarModelEvent(\Sabre\VObject\Component\VCalendar $vcalendar, Calendar_Model_Event $_event, Calendar_Model_Event $_mainEvent = null)
110     {
111         // clone the event and change the timezone
112         $event = clone $_event;
113         $event->setTimezone($event->originator_tz);
114         
115         $lastModifiedDateTime = $_event->last_modified_time ? $_event->last_modified_time : $_event->creation_time;
116         if (! $event->creation_time instanceof Tinebase_DateTime) {
117             throw new Tinebase_Exception_Record_Validation('creation_time needed for conversion to Sabre\VObject\Component');
118         }
119         
120         $vevent = $vcalendar->create('VEVENT', array(
121             'CREATED'       => $_event->creation_time->getClone()->setTimezone('UTC'),
122             'LAST-MODIFIED' => $lastModifiedDateTime->getClone()->setTimezone('UTC'),
123             'DTSTAMP'       => Tinebase_DateTime::now(),
124             'UID'           => $event->uid,
125             'SEQUENCE'      => $event->seq
126         ));
127
128         if ($event->isRecurException()) {
129             $originalDtStart = $_event->getOriginalDtStart()->setTimezone($_event->originator_tz);
130             
131             $recurrenceId = $vevent->add('RECURRENCE-ID', $originalDtStart);
132             
133             if ($_mainEvent && $_mainEvent->is_all_day_event == true) {
134                 $recurrenceId['VALUE'] = 'DATE';
135             }
136         }
137         
138         // dtstart and dtend
139         $dtstart = $vevent->add('DTSTART', $_event->dtstart->getClone()->setTimezone($event->originator_tz));
140         
141         if ($event->is_all_day_event == true) {
142             $dtstart['VALUE'] = 'DATE';
143             
144             // whole day events ends at 23:59:(00|59) in Tine 2.0 but 00:00 the next day in vcalendar
145             $event->dtend->addSecond($event->dtend->get('s') == 59 ? 1 : 0);
146             $event->dtend->addMinute($event->dtend->get('i') == 59 ? 1 : 0);
147             
148             $dtend = $vevent->add('DTEND', $event->dtend);
149             $dtend['VALUE'] = 'DATE';
150         } else {
151             $dtend = $vevent->add('DTEND', $event->dtend);
152         }
153         
154         // auto status for deleted events
155         if ($event->is_deleted) {
156             $event->status = Calendar_Model_Event::STATUS_CANCELED;
157         }
158         
159         // event organizer
160         if (!empty($event->organizer)) {
161             $organizerContact = $event->resolveOrganizer();
162
163             if ($organizerContact instanceof Addressbook_Model_Contact && !empty($organizerContact->email)) {
164                 $organizer = $vevent->add(
165                     'ORGANIZER', 
166                     'mailto:' . $organizerContact->email, 
167                     array('CN' => $organizerContact->n_fileas, 'EMAIL' => $organizerContact->email)
168                 );
169             }
170         }
171         
172         $this->_addEventAttendee($vevent, $event);
173         
174         $optionalProperties = array(
175             'class',
176             'status',
177             'description',
178             'geo',
179             'location',
180             'priority',
181             'summary',
182             'transp',
183             'url'
184         );
185         
186         foreach ($optionalProperties as $property) {
187             if (!empty($event->$property)) {
188                 $vevent->add(strtoupper($property), $event->$property);
189             }
190         }
191         
192         // categories
193         if (!isset($event->tags)) {
194             $event->tags = Tinebase_Tags::getInstance()->getTagsOfRecord($event);
195         }
196         if(isset($event->tags) && count($event->tags) > 0) {
197             $vevent->add('CATEGORIES', (array) $event->tags->name);
198         }
199         
200         // repeating event properties
201         if ($event->rrule) {
202             if ($event->is_all_day_event == true) {
203                 $vevent->add('RRULE', preg_replace_callback('/UNTIL=([\d :-]{19})(?=;?)/', function($matches) {
204                     $dtUntil = new Tinebase_DateTime($matches[1]);
205                     $dtUntil->setTimezone((string) Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
206                     
207                     return 'UNTIL=' . $dtUntil->format('Ymd');
208                 }, $event->rrule));
209             } else {
210                 $vevent->add('RRULE', preg_replace('/(UNTIL=)(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/', '$1$2$3$4T$5$6$7Z', $event->rrule));
211             }
212             
213             if ($event->exdate instanceof Tinebase_Record_RecordSet) {
214                 $event->exdate->addIndices(array('is_deleted'));
215                 $deletedEvents = $event->exdate->filter('is_deleted', true);
216                 
217                 foreach ($deletedEvents as $deletedEvent) {
218                     $dateTime = $deletedEvent->getOriginalDtStart();
219
220                     $exdate = $vevent->add('EXDATE');
221                     
222                     if ($event->is_all_day_event == true) {
223                         $dateTime->setTimezone($event->originator_tz);
224                         $exdate['VALUE'] = 'DATE';
225                     }
226                     
227                     $exdate->setValue($dateTime);
228                 }
229             }
230         }
231         
232         $ownAttendee = Calendar_Model_Attender::getOwnAttender($event->attendee);
233         
234         if ($event->alarms instanceof Tinebase_Record_RecordSet) {
235             $mozLastAck = NULL;
236             $mozSnooze = NULL;
237             
238             foreach ($event->alarms as $alarm) {
239                 $valarm = $vcalendar->create('VALARM');
240                 $valarm->add('ACTION', 'DISPLAY');
241                 $valarm->add('DESCRIPTION', $event->summary);
242                 
243                 if ($dtack = Calendar_Controller_Alarm::getAcknowledgeTime($alarm)) {
244                     $valarm->add('ACKNOWLEDGED', $dtack->getClone()->setTimezone('UTC')->format('Ymd\\THis\\Z'));
245                     $mozLastAck = $dtack > $mozLastAck ? $dtack : $mozLastAck;
246                 }
247                 
248                 if ($dtsnooze = Calendar_Controller_Alarm::getSnoozeTime($alarm)) {
249                     $mozSnooze = $dtsnooze > $mozSnooze ? $dtsnooze : $mozSnooze;
250                 }
251                 if (is_numeric($alarm->minutes_before)) {
252                     if ($event->dtstart == $alarm->alarm_time) {
253                         $periodString = 'PT0S';
254                     } else {
255                         $interval = $event->dtstart->diff($alarm->alarm_time);
256                         $periodString = sprintf('%sP%s%s%s%s',
257                             $interval->format('%r'),
258                             $interval->format('%d') > 0 ? $interval->format('%dD') : null,
259                             ($interval->format('%h') > 0 || $interval->format('%i') > 0) ? 'T' : null,
260                             $interval->format('%h') > 0 ? $interval->format('%hH') : null,
261                             $interval->format('%i') > 0 ? $interval->format('%iM') : null
262                         );
263                     }
264                     # TRIGGER;VALUE=DURATION:-PT1H15M
265                     $trigger = $valarm->add('TRIGGER', $periodString);
266                     $trigger['VALUE'] = "DURATION";
267                 } else {
268                     # TRIGGER;VALUE=DATE-TIME:...
269                     $trigger = $valarm->add('TRIGGER', $alarm->alarm_time->getClone()->setTimezone('UTC')->format('Ymd\\THis\\Z'));
270                     $trigger['VALUE'] = "DATE-TIME";
271                 }
272                 
273                 $vevent->add($valarm);
274             }
275             
276             if ($mozLastAck instanceof DateTime) {
277                 $vevent->add('X-MOZ-LASTACK', $mozLastAck->getClone()->setTimezone('UTC'), array('VALUE' => 'DATE-TIME'));
278             }
279             
280             if ($mozSnooze instanceof DateTime) {
281                 $vevent->add('X-MOZ-SNOOZE-TIME', $mozSnooze->getClone()->setTimezone('UTC'), array('VALUE' => 'DATE-TIME'));
282             }
283         }
284         
285         $baseUrl = Tinebase_Core::getHostname() . "/webdav/Calendar/records/Calendar_Model_Event/{$event->getId()}/";
286         
287         if ($event->attachments instanceof Tinebase_Record_RecordSet) {
288             foreach ($event->attachments as $attachment) {
289                 $attach = $vcalendar->createProperty('ATTACH', "{$baseUrl}{$attachment->name}", array(
290                     'MANAGED-ID' => $attachment->hash,
291                     'FMTTYPE'    => $attachment->contenttype,
292                     'SIZE'       => $attachment->size,
293                     'FILENAME'   => $attachment->name
294                 ), 'TEXT');
295                 
296                 $vevent->add($attach);
297             }
298             
299         }
300         
301         $vcalendar->add($vevent);
302     }
303     
304     /**
305      * add event attendee to VEVENT object 
306      * 
307      * @param \Sabre\VObject\Component\VEvent $vevent
308      * @param Calendar_Model_Event            $event
309      */
310     protected function _addEventAttendee(\Sabre\VObject\Component\VEvent $vevent, Calendar_Model_Event $event)
311     {
312         if (empty($event->attendee)) {
313             return;
314         }
315         
316         Calendar_Model_Attender::resolveAttendee($event->attendee, FALSE, $event);
317         
318         foreach($event->attendee as $eventAttendee) {
319             $attendeeEmail = $eventAttendee->getEmail();
320             
321             $parameters = array(
322                 'CN'       => $eventAttendee->getName(),
323                 'CUTYPE'   => Calendar_Convert_Event_VCalendar_Abstract::$cutypeMap[$eventAttendee->user_type],
324                 'PARTSTAT' => $eventAttendee->status,
325                 'ROLE'     => "{$eventAttendee->role}-PARTICIPANT",
326                 'RSVP'     => 'FALSE'
327             );
328             if (strpos($attendeeEmail, '@') !== false) {
329                 $parameters['EMAIL'] = $attendeeEmail;
330             }
331             $vevent->add('ATTENDEE', (strpos($attendeeEmail, '@') !== false ? 'mailto:' : 'urn:uuid:') . $attendeeEmail, $parameters);
332         }
333     }
334
335     /**
336      * set the METHOD for the generated VCALENDAR
337      *
338      * @param  string  $_method  the method
339      */
340     public function setMethod($method)
341     {
342         $this->_method = $method;
343     }
344     
345     /**
346      * converts vcalendar to Calendar_Model_Event
347      * 
348      * @param  mixed                 $_blob    the VCALENDAR to parse
349      * @param  Calendar_Model_Event  $_record  update existing event
350      * @param  array                 $options  array of options
351      * @return Calendar_Model_Event
352      */
353     public function toTine20Model($blob, Tinebase_Record_Abstract $_record = null, $options = array())
354     {
355         $vcalendar = self::getVObject($blob);
356         
357         // contains the VCALENDAR any VEVENTS
358         if (! isset($vcalendar->VEVENT)) {
359             throw new Tinebase_Exception_UnexpectedValue('no vevents found');
360         }
361         
362         // update a provided record or create a new one
363         if ($_record instanceof Calendar_Model_Event) {
364             $event = $_record;
365         } else {
366             $event = new Calendar_Model_Event(null, false);
367         }
368         
369         if (isset($vcalendar->METHOD)) {
370             $this->setMethod($vcalendar->METHOD);
371         }
372         
373         $baseVevent = $this->_findMainEvent($vcalendar);
374         
375         if (! $baseVevent) {
376             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
377                 . ' No main VEVENT found');
378             
379             if (! $_record && count($vcalendar->VEVENT) > 0) {
380                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
381                     . ' Convert recur exception without existing event using first VEVENT');
382                 $this->_convertVevent($vcalendar->VEVENT[0], $event, $options);
383             }
384         } else {
385             $this->_convertVevent($baseVevent, $event, $options);
386         }
387         
388         // TODO only do this for events with rrule?
389         // if (! empty($event->rrule)) {
390         $this->_parseEventExceptions($event, $vcalendar, $baseVevent, $options);
391         
392         $event->isValid(true);
393         
394         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
395             . ' Event: ' . print_r($event->toArray(), true));
396         
397         return $event;
398     }
399     
400     /**
401      * find the main event - the main event has no RECURRENCE-ID
402      * 
403      * @param \Sabre\VObject\Component\VCalendar $vcalendar
404      * @return \Sabre\VObject\Component\VCalendar | null
405      */
406     protected function _findMainEvent(\Sabre\VObject\Component\VCalendar $vcalendar)
407     {
408         foreach ($vcalendar->VEVENT as $vevent) {
409             if (! isset($vevent->{'RECURRENCE-ID'})) {
410                 return $vevent;
411             }
412         }
413         
414         return null;
415     }
416     
417     /**
418      * parse event exceptions and add them to Tine 2.0 event record
419      * 
420      * @param  Calendar_Model_Event                $event
421      * @param  \Sabre\VObject\Component\VCalendar  $vcalendar
422      * @param  \Sabre\VObject\Component\VCalendar  $baseVevent
423      * @param  array                               $options
424      */
425     protected function _parseEventExceptions(Calendar_Model_Event $event, \Sabre\VObject\Component\VCalendar $vcalendar, $baseVevent = null, $options = array())
426     {
427         if (! $event->exdate instanceof Tinebase_Record_RecordSet) {
428             $event->exdate = new Tinebase_Record_RecordSet('Calendar_Model_Event');
429         }
430         $recurExceptions =  $event->exdate->filter('is_deleted', false);
431         
432         foreach ($vcalendar->VEVENT as $vevent) {
433             if (isset($vevent->{'RECURRENCE-ID'}) && $event->uid == $vevent->UID) {
434                 $recurException = $this->_getRecurException($recurExceptions, $vevent);
435                 
436                 // initialize attendee with attendee from base events for new exceptions
437                 // this way we can keep attendee extra values like groupmember type
438                 // attendees which do not attend to the new exception will be removed in _convertVevent
439                 if (! $recurException->attendee instanceof Tinebase_Record_RecordSet) {
440                     $recurException->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
441                     foreach ($event->attendee as $attendee) {
442                         $recurException->attendee->addRecord(new Calendar_Model_Attender(array(
443                             'user_id'   => $attendee->user_id,
444                             'user_type' => $attendee->user_type,
445                             'role'      => $attendee->role,
446                             'status'    => $attendee->status
447                         )));
448                     }
449                 }
450                 
451                 // initialize attachments from base event as clients may skip parameters like
452                 // name and contentytpe and we can't backward relove them from managedId
453                 if ($event->attachments instanceof Tinebase_Record_RecordSet && 
454                         ! $recurException->attachments instanceof Tinebase_Record_RecordSet) {
455                     $recurException->attachments = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
456                     foreach ($event->attachments as $attachment) {
457                         $recurException->attachments->addRecord(new Tinebase_Model_Tree_Node(array(
458                             'name'         => $attachment->name,
459                             'type'         => Tinebase_Model_Tree_Node::TYPE_FILE,
460                             'contenttype'  => $attachment->contenttype,
461                             'hash'         => $attachment->hash,
462                         ), true));
463                     }
464                 }
465                 
466                 if ($baseVevent) {
467                     $this->_adaptBaseEventProperties($vevent, $baseVevent);
468                 }
469                 
470                 $this->_convertVevent($vevent, $recurException, $options);
471                 
472                 if (! $recurException->getId()) {
473                     $event->exdate->addRecord($recurException);
474                 }
475             }
476         }
477     }
478     
479     /**
480      * adapt X-MOZ-LASTACK / X-MOZ-SNOOZE-TIME from base vevent
481      * 
482      * @see 0009396: alarm_ack_time and alarm_snooze_time are not updated
483      */
484     protected function _adaptBaseEventProperties($vevent, $baseVevent)
485     {
486         $propertiesToAdapt = array('X-MOZ-LASTACK', 'X-MOZ-SNOOZE-TIME');
487         
488         foreach ($propertiesToAdapt as $property) {
489             if (isset($baseVevent->{$property})) {
490                 $vevent->{$property} = $baseVevent->{$property};
491             }
492         }
493     }
494     
495     /**
496      * template method
497      * 
498      * implement if client has support for sending attachments
499      * 
500      * @param Calendar_Model_Event          $event
501      * @param Tinebase_Record_RecordSet     $attachments
502      */
503     protected function _manageAttachmentsFromClient($event, $attachments) {}
504     
505     /**
506      * convert VCALENDAR to Tinebase_Record_RecordSet of Calendar_Model_Event
507      * 
508      * @param  mixed  $blob  the vcalendar to parse
509      * @param  array  $options
510      * @return Tinebase_Record_RecordSet
511      */
512     public function toTine20RecordSet($blob, $options = array())
513     {
514         $vcalendar = self::getVObject($blob);
515         
516         $result = new Tinebase_Record_RecordSet('Calendar_Model_Event');
517         
518         foreach ($vcalendar->VEVENT as $vevent) {
519             if (! isset($vevent->{'RECURRENCE-ID'})) {
520                 $event = new Calendar_Model_Event();
521                 $this->_convertVevent($vevent, $event, $options);
522                 if (! empty($event->rrule)) {
523                     $this->_parseEventExceptions($event, $vcalendar, $options);
524                 }
525                 $result->addRecord($event);
526             }
527         }
528         
529         return $result;
530     }
531     
532     /**
533      * get METHOD of current VCALENDAR or supplied blob
534      * 
535      * @param  string  $blob
536      * @return string|NULL
537      */
538     public function getMethod($blob = NULL)
539     {
540         if ($this->_method) {
541             return $this->_method;
542         }
543         
544         if ($blob !== NULL) {
545             $vcalendar = self::getVObject($blob);
546             return $vcalendar->METHOD;
547         }
548         
549         return null;
550     }
551
552     /**
553      * find a matching exdate or return an empty event record
554      * 
555      * @param  Tinebase_Record_RecordSet        $oldExdates
556      * @param  \Sabre\VObject\Component\VEvent  $vevent
557      * @return Calendar_Model_Event
558      */
559     protected function _getRecurException(Tinebase_Record_RecordSet $oldExdates,Sabre\VObject\Component\VEvent $vevent)
560     {
561         $exDate = clone $vevent->{'RECURRENCE-ID'}->getDateTime();
562         $exDate->setTimeZone(new DateTimeZone('UTC'));
563         $exDateString = $exDate->format('Y-m-d H:i:s');
564         
565         foreach ($oldExdates as $id => $oldExdate) {
566             if ($exDateString == substr((string) $oldExdate->recurid, -19)) {
567                 return $oldExdate;
568             }
569         }
570         
571         return new Calendar_Model_Event();
572     }
573     
574     /**
575      * get attendee array for given contact
576      * 
577      * @param  \Sabre\VObject\Property\ICalendar\CalAddress  $calAddress  the attendee row from the vevent object
578      * @return array
579      */
580     protected function _getAttendee(\Sabre\VObject\Property\ICalendar\CalAddress $calAddress)
581     {
582         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
583             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' attendee ' . $calAddress->serialize());
584         
585         if (isset($calAddress['CUTYPE']) && in_array($calAddress['CUTYPE']->getValue(), array('INDIVIDUAL', Calendar_Model_Attender::USERTYPE_GROUP, Calendar_Model_Attender::USERTYPE_RESOURCE))) {
586             $type = $calAddress['CUTYPE']->getValue() == 'INDIVIDUAL' ? Calendar_Model_Attender::USERTYPE_USER : $calAddress['CUTYPE']->getValue();
587         } else {
588             $type = Calendar_Model_Attender::USERTYPE_USER;
589         }
590         
591         if (isset($calAddress['ROLE']) && in_array($calAddress['ROLE']->getValue(), array(Calendar_Model_Attender::ROLE_OPTIONAL, Calendar_Model_Attender::ROLE_REQUIRED))) {
592             $role = $calAddress['ROLE']->getValue();
593         } else {
594             $role = Calendar_Model_Attender::ROLE_REQUIRED;
595         }
596         
597         if (isset($calAddress['PARTSTAT']) && in_array($calAddress['PARTSTAT']->getValue(), array(
598             Calendar_Model_Attender::STATUS_ACCEPTED,
599             Calendar_Model_Attender::STATUS_DECLINED,
600             Calendar_Model_Attender::STATUS_NEEDSACTION,
601             Calendar_Model_Attender::STATUS_TENTATIVE
602         ))) {
603             $status = $calAddress['PARTSTAT']->getValue();
604         } else {
605             $status = Calendar_Model_Attender::STATUS_NEEDSACTION;
606         }
607         
608         if (!empty($calAddress['EMAIL'])) {
609             $email = $calAddress['EMAIL']->getValue();
610         } else {
611             if (! preg_match('/(?P<protocol>mailto:|urn:uuid:)(?P<email>.*)/i', $calAddress->getValue(), $matches)) {
612                 if (preg_match(Tinebase_Mail::EMAIL_ADDRESS_REGEXP, $calAddress->getValue())) {
613                     $email = $calAddress->getValue();
614                 } else {
615                     if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) 
616                         Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' invalid attendee provided: ' . $calAddress->getValue());
617                     return null;
618                 }
619             } else {
620                 $email = $matches['email'];
621             }
622         }
623         
624         $fullName = isset($calAddress['CN']) ? $calAddress['CN']->getValue() : $email;
625         
626         if (preg_match('/(?P<firstName>\S*) (?P<lastNameName>\S*)/', $fullName, $matches)) {
627             $firstName = $matches['firstName'];
628             $lastName  = $matches['lastNameName'];
629         } else {
630             $firstName = null;
631             $lastName  = $fullName;
632         }
633
634         $attendee = array(
635             'userType'  => $type,
636             'firstName' => $firstName,
637             'lastName'  => $lastName,
638             'partStat'  => $status,
639             'role'      => $role,
640             'email'     => $email
641         );
642         
643         return $attendee;
644     }
645     
646     /**
647      * parse VEVENT part of VCALENDAR
648      * 
649      * @param  \Sabre\VObject\Component\VEvent  $vevent  the VEVENT to parse
650      * @param  Calendar_Model_Event             $event   the Tine 2.0 event to update
651      * @param  array                            $options
652      */
653     protected function _convertVevent(\Sabre\VObject\Component\VEvent $vevent, Calendar_Model_Event $event, $options)
654     {
655         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
656             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' vevent ' . $vevent->serialize());
657         
658         $newAttendees = array();
659         $shortenedFields = array();
660         $attachments = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
661         $event->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
662         
663         foreach ($vevent->children() as $property) {
664             switch ($property->name) {
665                 case 'CREATED':
666                 case 'DTSTAMP':
667                     if (! isset($options[self::OPTION_USE_SERVER_MODLOG]) || $options[self::OPTION_USE_SERVER_MODLOG] !== true) {
668                         $event->{$property->name == 'CREATED' ? 'creation_time' : 'last_modified_time'} = $this->_convertToTinebaseDateTime($property);
669                     }
670                     break;
671                     
672                 case 'LAST-MODIFIED':
673                     $event->last_modified_time = new Tinebase_DateTime($property->getValue());
674                     break;
675                 
676                 case 'ATTENDEE':
677                     $newAttendee = $this->_getAttendee($property);
678                     if ($newAttendee) {
679                         $newAttendees[] = $newAttendee;
680                     }
681                     break;
682                     
683                 case 'CLASS':
684                     if (in_array($property->getValue(), array(Calendar_Model_Event::CLASS_PRIVATE, Calendar_Model_Event::CLASS_PUBLIC))) {
685                         $event->class = $property->getValue();
686                     } else {
687                         $event->class = Calendar_Model_Event::CLASS_PUBLIC;
688                     }
689                     
690                     break;
691                     
692                 case 'STATUS':
693                     if (in_array($property->getValue(), array(Calendar_Model_Event::STATUS_CONFIRMED, Calendar_Model_Event::STATUS_TENTATIVE, Calendar_Model_Event::STATUS_CANCELED))) {
694                         $event->status = $property->getValue();
695                     } else {
696                         $event->status = Calendar_Model_Event::STATUS_CONFIRMED;
697                     }
698                     break;
699                     
700                 case 'DTEND':
701                     
702                     if (isset($property['VALUE']) && strtoupper($property['VALUE']) == 'DATE') {
703                         // all day event
704                         $event->is_all_day_event = true;
705                         $dtend = $this->_convertToTinebaseDateTime($property, TRUE);
706                         
707                         // whole day events ends at 23:59:59 in Tine 2.0 but 00:00 the next day in vcalendar
708                         $dtend->subSecond(1);
709                     } else {
710                         $event->is_all_day_event = false;
711                         $dtend = $this->_convertToTinebaseDateTime($property);
712                     }
713                     
714                     $event->dtend = $dtend;
715                     
716                     break;
717                     
718                 case 'DTSTART':
719                     if (isset($property['VALUE']) && strtoupper($property['VALUE']) == 'DATE') {
720                         // all day event
721                         $event->is_all_day_event = true;
722                         $dtstart = $this->_convertToTinebaseDateTime($property, TRUE);
723                     } else {
724                         $event->is_all_day_event = false;
725                         $dtstart = $this->_convertToTinebaseDateTime($property);
726                     }
727                     
728                     $event->originator_tz = $dtstart->getTimezone()->getName();
729                     $event->dtstart = $dtstart;
730                     
731                     break;
732                     
733                 case 'SEQUENCE':
734                     if (! isset($options[self::OPTION_USE_SERVER_MODLOG]) || $options[self::OPTION_USE_SERVER_MODLOG] !== true) {
735                         $event->seq = $property->getValue();
736                     }
737                     break;
738                     
739                 case 'DESCRIPTION':
740                 case 'LOCATION':
741                 case 'UID':
742                 case 'SUMMARY':
743                     $key = strtolower($property->name);
744                     $value = $property->getValue();
745                     switch ($key) {
746                         case 'summary':
747                         case 'location':
748                             if (strlen($value) > 255) {
749                                 $shortenedFields[$key] = $value;
750                                 $endPos = strpos($value, "\n") !== false && strpos($value, "\n") < 255 
751                                     ? strpos($value, "\n") 
752                                     : 255;
753                                 if (extension_loaded('mbstring')) {
754                                     $value = mb_substr($value, 0, $endPos, 'UTF-8');
755                                 } else {
756                                     $value = Tinebase_Core::filterInputForDatabase(substr($value, 0, $endPos));
757                                 }
758                             }
759                             break;
760                     }
761                     $event->$key = $value;
762                     
763                     break;
764                     
765                 case 'ORGANIZER':
766                     $email = null;
767                     
768                     if (!empty($property['EMAIL'])) {
769                         $email = $property['EMAIL'];
770                     } elseif (preg_match('/mailto:(?P<email>.*)/i', $property->getValue(), $matches)) {
771                         $email = $matches['email'];
772                     }
773                     
774                     if ($email !== null) {
775                         // it's not possible to change the organizer by spec
776                         if (empty($event->organizer)) {
777                             $name = isset($property['CN']) ? $property['CN']->getValue() : $email;
778                             $contact = Calendar_Model_Attender::resolveEmailToContact(array(
779                                 'email'     => $email,
780                                 'lastName'  => $name,
781                             ));
782                         
783                             $event->organizer = $contact->getId();
784                         }
785                         
786                         // Lightning attaches organizer ATTENDEE properties to ORGANIZER property and does not add an ATTENDEE for the organizer
787                         if (isset($property['PARTSTAT'])) {
788                             $newAttendees[] = $this->_getAttendee($property);
789                         }
790                     }
791                     
792                     break;
793
794                 case 'RECURRENCE-ID':
795                     // original start of the event
796                     $event->recurid = $this->_convertToTinebaseDateTime($property);
797                     
798                     // convert recurrence id to utc
799                     $event->recurid->setTimezone('UTC');
800                     
801                     break;
802                     
803                 case 'RRULE':
804                     $rruleString = $property->getValue();
805                     
806                     // convert date format
807                     $rruleString = preg_replace_callback('/UNTIL=([\dTZ]+)(?=;?)/', function($matches) {
808                         if (strlen($matches[1]) < 10) {
809                             $dtUntil = date_create($matches[1], new DateTimeZone ((string) Tinebase_Core::get(Tinebase_Core::USERTIMEZONE)));
810                             $dtUntil->setTimezone(new DateTimeZone('UTC'));
811                         } else {
812                             $dtUntil = date_create($matches[1]);
813                         }
814                         
815                         return 'UNTIL=' . $dtUntil->format(Tinebase_Record_Abstract::ISO8601LONG);
816                     }, $rruleString);
817
818                     // remove additional days from BYMONTHDAY property (BYMONTHDAY=11,15 => BYMONTHDAY=11)
819                     $rruleString = preg_replace('/(BYMONTHDAY=)([\d]+),([,\d]+)/', '$1$2', $rruleString);
820                     
821                     $event->rrule = $rruleString;
822                     
823                     // process exceptions
824                     if (isset($vevent->EXDATE)) {
825                         $exdates = new Tinebase_Record_RecordSet('Calendar_Model_Event');
826                         
827                         foreach ($vevent->EXDATE as $exdate) {
828                             foreach ($exdate->getDateTimes() as $exception) {
829                                 if (isset($exdate['VALUE']) && strtoupper($exdate['VALUE']) == 'DATE') {
830                                     $recurid = new Tinebase_DateTime($exception->format(Tinebase_Record_Abstract::ISO8601LONG), (string) Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
831                                 } else {
832                                     $recurid = new Tinebase_DateTime($exception->format(Tinebase_Record_Abstract::ISO8601LONG), $exception->getTimezone());
833                                 }
834                                 $recurid->setTimezone(new DateTimeZone('UTC'));
835                                 
836                                 $eventException = new Calendar_Model_Event(array(
837                                     'recurid'    => $recurid,
838                                     'is_deleted' => true
839                                 ));
840                         
841                                 $exdates->addRecord($eventException);
842                             }
843                         }
844                     
845                         $event->exdate = $exdates;
846                     }
847                     
848                     break;
849                     
850                 case 'TRANSP':
851                     if (in_array($property->getValue(), array(Calendar_Model_Event::TRANSP_OPAQUE, Calendar_Model_Event::TRANSP_TRANSP))) {
852                         $event->transp = $property->getValue();
853                     } else {
854                         $event->transp = Calendar_Model_Event::TRANSP_TRANSP;
855                     }
856                     
857                     break;
858                     
859                 case 'UID':
860                     // it's not possible to change the uid by spec
861                     if (!empty($event->uid)) {
862                         continue;
863                     }
864                     
865                     $event->uid = $property->getValue();
866                 
867                     break;
868                     
869                 case 'VALARM':
870                     $this->_parseAlarm($event, $property, $vevent);
871                     break;
872                     
873                 case 'CATEGORIES':
874                     $tags = Tinebase_Model_Tag::resolveTagNameToTag($property->getParts(), 'Calendar');
875                     if (! isset($event->tags)) {
876                         $event->tags = $tags;
877                     } else {
878                         $event->tags->merge($tags);
879                     }
880                     break;
881                     
882                 case 'ATTACH':
883                     $name = (string) $property['FILENAME'];
884                     $managedId = (string) $property['MANAGED-ID'];
885                     $value = (string) $property['VALUE'];
886                     $attachment = NULL;
887                     $readFromURL = false;
888                     $url = '';
889                     
890                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
891                         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' attachment found: ' . $name . ' ' . $managedId);
892                     
893                     if ($managedId) {
894                         $attachment = $event->attachments instanceof Tinebase_Record_RecordSet ?
895                             $event->attachments->filter('hash', $property['MANAGED-ID'])->getFirstRecord() :
896                             NULL;
897                         
898                         // NOTE: we might miss a attachment here for the following reasons
899                         //       1. client reuses a managed id (we are server):
900                         //          We havn't observerd this yet. iCal client reuse manged id's
901                         //          from base events in exceptions but this is covered as we 
902                         //          initialize new exceptions with base event attachments
903                         //          
904                         //          When a client reuses a managed id it's not clear yet if
905                         //          this managed id needs to be in the same series/calendar/server
906                         //
907                         //          As we use the object hash the managed id might be used in the 
908                         //          same files with different names. We need to evaluate the name
909                         //          (if attached) in this case as well.
910                         //       
911                         //       2. server send his managed id (we are client)
912                         //          * we need to download the attachment (here?)
913                         //          * we need to have a mapping externalid / internalid (where?)
914                         
915                         if (! $attachment) {
916                             $readFromURL = true;
917                             $url = $property->getValue();
918                         } else {
919                             $attachments->addRecord($attachment);
920                         }
921                     } elseif('URI' === $value) {
922                         /*
923                          * ATTACH;VALUE=URI:https://ical.familienservice.de/calendars/__uids__/0AA0
924  3A3B-F7B6-459A-AB3E-4726E53637D0/dropbox/4971F93F-8657-412B-841A-A0FD913
925  9CD61.dropbox/Canada.png
926                          */
927                         $readFromURL = true;
928                         $url = $property->getValue();
929                         $name = parse_url($url, PHP_URL_PATH);
930                         $name = pathinfo($name, PATHINFO_BASENAME);
931                     }
932                     // base64
933                     else {
934                         // @TODO: implement (check if add / update / update is needed)
935                         if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
936                                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' attachment found that could not be imported due to missing managed id');
937                     }
938                     
939                     if($readFromURL) {
940                         if (preg_match('#^(https?://)(.*)$#', str_replace(array("\n","\r"), '', $url), $matches)) {
941                             // we are client and found an external hosted attachment that we need to import
942                             $user = Tinebase_Core::getUser();
943                             $userCredentialCache = Tinebase_Core::get(Tinebase_Core::USERCREDENTIALCACHE);
944                             $stream = fopen($matches[1] . $userCredentialCache->username . ':' . $userCredentialCache->password . '@' . $matches[2], 'r');
945                             $attachment = new Tinebase_Model_Tree_Node(array(
946                                 'name'         => $name,
947                                 'type'         => Tinebase_Model_Tree_Node::TYPE_FILE,
948                                 'contenttype'  => (string) $property['FMTTYPE'],
949                                 'tempFile'     => $stream,
950                                 ), true);
951                             $attachments->addRecord($attachment);
952                         } else {
953                             if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
954                                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' attachment found with malformed url: '.$url. ' '.$name. ' '.$managedId);
955                         }
956                     }
957                     break;
958                     
959                 case 'X-MOZ-LASTACK':
960                     $lastAck = $this->_convertToTinebaseDateTime($property);
961                     break;
962                     
963                 case 'X-MOZ-SNOOZE-TIME':
964                     $snoozeTime = $this->_convertToTinebaseDateTime($property);
965                     break;
966                     
967                 default:
968                     // thunderbird saves snooze time for recurring event occurrences in properties with names like this -
969                     // we just assume that the event/recur series has only one snooze time 
970                     if (preg_match('/^X-MOZ-SNOOZE-TIME-[0-9]+$/', $property->name)) {
971                         $snoozeTime = $this->_convertToTinebaseDateTime($property);
972                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
973                             . ' Found snooze time for recur occurrence: ' . $snoozeTime->toString());
974                     }
975                     break;
976             }
977         }
978         
979         foreach ($shortenedFields as $key => $value) {
980             $event->description = "--------\n$key: $value";
981         }
982         
983         if (isset($lastAck)) {
984             Calendar_Controller_Alarm::setAcknowledgeTime($event->alarms, $lastAck);
985         }
986         if (isset($snoozeTime)) {
987             Calendar_Controller_Alarm::setSnoozeTime($event->alarms, $snoozeTime);
988         }
989         
990         // merge old and new attendee
991         Calendar_Model_Attender::emailsToAttendee($event, $newAttendees);
992         
993         if (empty($event->seq)) {
994             $event->seq = 1;
995         }
996         
997         if (empty($event->class)) {
998             $event->class = Calendar_Model_Event::CLASS_PUBLIC;
999         }
1000         
1001         $this->_manageAttachmentsFromClient($event, $attachments);
1002         
1003         if (empty($event->dtend)) {
1004             // TODO find out duration (see TRIGGER DURATION)
1005 //             if (isset($vevent->DURATION)) {
1006 //             }
1007             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1008                     . ' Got event without dtend. Assuming 30 minutes duration');
1009             $event->dtend = clone $event->dtstart;
1010             $event->dtend->addMinute(30);
1011         }
1012         
1013         // convert all datetime fields to UTC
1014         $event->setTimezone('UTC');
1015     }
1016 }