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