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