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