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