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