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         $baseVevent = null;
363         foreach ($vcalendar->VEVENT as $vevent) {
364             if (! isset($vevent->{'RECURRENCE-ID'})) {
365                 $this->_convertVevent($vevent, $event);
366                 $baseVevent = $vevent;
367                 
368                 break;
369             }
370         }
371
372         // if we have found no VEVENT component something went wrong, lets stop here
373         if (! $baseVevent) {
374             throw new Tinebase_Exception_UnexpectedValue('no main VEVENT component found in VCALENDAR');
375         }
376         
377         // TODO only do this for events with rrule?
378         // if (! empty($event->rrule)) {
379         $this->_parseEventExceptions($event, $vcalendar, $baseVevent);
380         $event->isValid(true);
381         
382         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
383             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' data ' . print_r($event->toArray(), true));
384         
385         return $event;
386     }
387     
388     /**
389      * parse event exceptions and add them to Tine 2.0 event record
390      * 
391      * @param  Calendar_Model_Event                $event
392      * @param  \Sabre\VObject\Component\VCalendar  $vcalendar
393      * @param  \Sabre\VObject\Component\VCalendar  $baseVevent
394      */
395     protected function _parseEventExceptions(Calendar_Model_Event $event, \Sabre\VObject\Component\VCalendar $vcalendar, $baseVevent = null)
396     {
397         $oldExdates = $event->exdate instanceof Tinebase_Record_RecordSet ? $event->exdate->filter('is_deleted', false) : new Tinebase_Record_RecordSet('Calendar_Model_Event');
398         
399         foreach ($vcalendar->VEVENT as $vevent) {
400             if (isset($vevent->{'RECURRENCE-ID'}) && $event->uid == $vevent->UID) {
401                 $recurException = $this->_getRecurException($oldExdates, $vevent);
402                 
403                 // initialize attendee with attendee from base events for new exceptions
404                 // this way we can keep attendee extra values like groupmember type
405                 // attendees which do not attend to the new exception will be removed in _convertVevent
406                 if (! $recurException->attendee instanceof Tinebase_Record_RecordSet) {
407                     $recurException->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
408                     foreach ($event->attendee as $attendee) {
409                         $recurException->attendee->addRecord(new Calendar_Model_Attender(array(
410                             'user_id'   => $attendee->user_id,
411                             'user_type' => $attendee->user_type,
412                             'role'      => $attendee->role,
413                             'status'    => $attendee->status
414                         )));
415                     }
416                 }
417                 
418                 if ($baseVevent) {
419                     $this->_adaptBaseEventProperties($vevent, $baseVevent);
420                 }
421                 
422                 $this->_convertVevent($vevent, $recurException);
423                 
424                 if (! $event->exdate instanceof Tinebase_Record_RecordSet) {
425                     $event->exdate = new Tinebase_Record_RecordSet('Calendar_Model_Event');
426                 }
427                 $event->exdate->addRecord($recurException);
428             }
429         }
430     }
431     
432     /**
433      * adapt X-MOZ-LASTACK / X-MOZ-SNOOZE-TIME from base vevent
434      * 
435      * @see 0009396: alarm_ack_time and alarm_snooze_time are not updated
436      */
437     protected function _adaptBaseEventProperties($vevent, $baseVevent)
438     {
439         $propertiesToAdapt = array('X-MOZ-LASTACK', 'X-MOZ-SNOOZE-TIME');
440         
441         foreach ($propertiesToAdapt as $property) {
442             if (isset($baseVevent->{$property})) {
443                 $vevent->{$property} = $baseVevent->{$property};
444             }
445         }
446     }
447     
448     /**
449      * convert VCALENDAR to Tinebase_Record_RecordSet of Calendar_Model_Event
450      * 
451      * @param  mixed  $blob  the vcalendar to parse
452      * @return Tinebase_Record_RecordSet
453      */
454     public function toTine20RecordSet($blob)
455     {
456         $vcalendar = self::getVObject($blob);
457         
458         $result = new Tinebase_Record_RecordSet('Calendar_Model_Event');
459         
460         foreach ($vcalendar->VEVENT as $vevent) {
461             if (! isset($vevent->{'RECURRENCE-ID'})) {
462                 $event = new Calendar_Model_Event();
463                 $this->_convertVevent($vevent, $event);
464                 if (! empty($event->rrule)) {
465                     $this->_parseEventExceptions($event, $vcalendar);
466                 }
467                 $result->addRecord($event);
468             }
469         }
470         
471         return $result;
472     }
473     
474     /**
475      * returns VObject of input data
476      * 
477      * @param   mixed  $blob
478      * @return  \Sabre\VObject\Component\VCalendar
479      */
480     public static function getVObject($blob)
481     {
482         if ($blob instanceof \Sabre\VObject\Component\VCalendar) {
483             return $blob;
484         }
485         
486         if (is_resource($blob)) {
487             $blob = stream_get_contents($blob);
488         }
489         
490         $vcalendar = self::readVCalBlob($blob);
491         
492         return $vcalendar;
493     }
494     
495     /**
496      * reads vcal blob and tries to repair some parsing problems that Sabre has
497      * 
498      * @param string $blob
499      * @param integer $failcount
500      * @param integer $spacecount
501      * @param integer $lastBrokenLineNumber
502      * @param array $lastLines
503      * @throws Sabre\VObject\ParseException
504      * @return Sabre\VObject\Component\VCalendar
505      * 
506      * @see 0006110: handle iMIP messages from outlook
507      * 
508      * @todo maybe we can remove this when #7438 is resolved
509      */
510     public static function readVCalBlob($blob, $failcount = 0, $spacecount = 0, $lastBrokenLineNumber = 0, $lastLines = array())
511     {
512         // convert to utf-8
513         $blob = mbConvertTo($blob);
514         
515         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
516             ' ' . $blob);
517         
518         try {
519             $vcalendar = \Sabre\VObject\Reader::read($blob);
520         } catch (Sabre\VObject\ParseException $svpe) {
521             // NOTE: we try to repair\Sabre\VObject\Reader as it fails to detect followup lines that do not begin with a space or tab
522             if ($failcount < 10 && preg_match(
523                 '/Invalid VObject, line ([0-9]+) did not follow the icalendar\/vcard format/', $svpe->getMessage(), $matches
524             )) {
525                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
526                     ' ' . $svpe->getMessage() .
527                     ' lastBrokenLineNumber: ' . $lastBrokenLineNumber);
528                 
529                 $brokenLineNumber = $matches[1] - 1 + $spacecount;
530                 
531                 if ($lastBrokenLineNumber === $brokenLineNumber) {
532                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
533                         ' Try again: concat this line to previous line.');
534                     $lines = $lastLines;
535                     $brokenLineNumber--;
536                     // increase spacecount because one line got removed
537                     $spacecount++;
538                 } else {
539                     $lines = preg_split('/[\r\n]*\n/', $blob);
540                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
541                         ' Concat next line to this one.');
542                     $lastLines = $lines; // for retry
543                 }
544                 $lines[$brokenLineNumber] .= $lines[$brokenLineNumber + 1];
545                 unset($lines[$brokenLineNumber + 1]);
546                 
547                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
548                     ' failcount: ' . $failcount .
549                     ' brokenLineNumber: ' . $brokenLineNumber .
550                     ' spacecount: ' . $spacecount);
551                 
552                 $vcalendar = self::readVCalBlob(implode("\n", $lines), $failcount + 1, $spacecount, $brokenLineNumber, $lastLines);
553             } else {
554                 throw $svpe;
555             }
556         }
557         
558         return $vcalendar;
559     }
560     
561     /**
562      * get METHOD of current VCALENDAR or supplied blob
563      * 
564      * @param  string  $blob
565      * @return string|NULL
566      */
567     public function getMethod($blob = NULL)
568     {
569         if ($this->_method) {
570             return $this->_method;
571         }
572         
573         if ($blob !== NULL) {
574             $vcalendar = self::getVObject($blob);
575             return $vcalendar->METHOD;
576         }
577         
578         return null;
579     }
580
581     /**
582      * find a matching exdate or return an empty event record
583      * 
584      * @param  Tinebase_Record_RecordSet        $oldExdates
585      * @param  \Sabre\VObject\Component\VEvent  $vevent
586      * @return Calendar_Model_Event
587      */
588     protected function _getRecurException(Tinebase_Record_RecordSet $oldExdates,Sabre\VObject\Component\VEvent $vevent)
589     {
590         $exDate = clone $vevent->{'RECURRENCE-ID'}->getDateTime();
591         $exDate->setTimeZone(new DateTimeZone('UTC'));
592         $exDateString = $exDate->format('Y-m-d H:i:s');
593         
594         foreach ($oldExdates as $id => $oldExdate) {
595             if ($exDateString == substr((string) $oldExdate->recurid, -19)) {
596                 unset($oldExdates[$id]);
597                 
598                 return $oldExdate;
599             }
600         }
601         
602         return new Calendar_Model_Event();
603     }
604     
605     /**
606      * get attendee array for given contact
607      * 
608      * @param  \Sabre\VObject\Property\ICalendar\CalAddress  $calAddress  the attendee row from the vevent object
609      * @return array
610      */
611     protected function _getAttendee(\Sabre\VObject\Property\ICalendar\CalAddress $calAddress)
612     {
613         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
614             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' attendee ' . $calAddress->serialize());
615         
616         if (isset($calAddress['CUTYPE']) && in_array($calAddress['CUTYPE']->getValue(), array('INDIVIDUAL', Calendar_Model_Attender::USERTYPE_GROUP, Calendar_Model_Attender::USERTYPE_RESOURCE))) {
617             $type = $calAddress['CUTYPE']->getValue() == 'INDIVIDUAL' ? Calendar_Model_Attender::USERTYPE_USER : $calAddress['CUTYPE']->getValue();
618         } else {
619             $type = Calendar_Model_Attender::USERTYPE_USER;
620         }
621         
622         if (isset($calAddress['ROLE']) && in_array($calAddress['ROLE']->getValue(), array(Calendar_Model_Attender::ROLE_OPTIONAL, Calendar_Model_Attender::ROLE_REQUIRED))) {
623             $role = $calAddress['ROLE']->getValue();
624         } else {
625             $role = Calendar_Model_Attender::ROLE_REQUIRED;
626         }
627         
628         if (isset($calAddress['PARTSTAT']) && in_array($calAddress['PARTSTAT']->getValue(), array(
629             Calendar_Model_Attender::STATUS_ACCEPTED,
630             Calendar_Model_Attender::STATUS_DECLINED,
631             Calendar_Model_Attender::STATUS_NEEDSACTION,
632             Calendar_Model_Attender::STATUS_TENTATIVE
633         ))) {
634             $status = $calAddress['PARTSTAT']->getValue();
635         } else {
636             $status = Calendar_Model_Attender::STATUS_NEEDSACTION;
637         }
638         
639         if (!empty($calAddress['EMAIL'])) {
640             $email = $calAddress['EMAIL']->getValue();
641         } else {
642             if (!preg_match('/(?P<protocol>mailto:|urn:uuid:)(?P<email>.*)/i', $calAddress->getValue(), $matches)) {
643                 throw new Tinebase_Exception_UnexpectedValue('invalid attendee provided: ' . $calAddress->getValue());
644             }
645             $email = $matches['email'];
646         }
647         
648         $fullName = isset($calAddress['CN']) ? $calAddress['CN']->getValue() : $email;
649         
650         if (preg_match('/(?P<firstName>\S*) (?P<lastNameName>\S*)/', $fullName, $matches)) {
651             $firstName = $matches['firstName'];
652             $lastName  = $matches['lastNameName'];
653         } else {
654             $firstName = null;
655             $lastName  = $fullName;
656         }
657
658         $attendee = array(
659             'userType'  => $type,
660             'firstName' => $firstName,
661             'lastName'  => $lastName,
662             'partStat'  => $status,
663             'role'      => $role,
664             'email'     => $email
665         );
666         
667         return $attendee;
668     }
669     
670     /**
671      * parse VEVENT part of VCALENDAR
672      * 
673      * @param  \Sabre\VObject\Component\VEvent  $vevent  the VEVENT to parse
674      * @param  Calendar_Model_Event             $event   the Tine 2.0 event to update
675      */
676     protected function _convertVevent(\Sabre\VObject\Component\VEvent $vevent, Calendar_Model_Event $event)
677     {
678         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
679             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' vevent ' . $vevent->serialize());
680         
681         $newAttendees = array();
682         
683         // unset supported fields
684         foreach ($this->_supportedFields as $field) {
685             if ($field == 'alarms') {
686                 $event->$field = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
687             } else {
688                 $event->$field = null;
689             }
690         }
691         
692         foreach ($vevent->children() as $property) {
693             switch ($property->name) {
694                 case 'CREATED':
695                 case 'DTSTAMP':
696                     // do nothing
697                     break;
698                     
699                 case 'LAST-MODIFIED':
700                     $event->last_modified_time = new Tinebase_DateTime($property->getValue());
701                     break;
702                 
703                 case 'ATTENDEE':
704                     $newAttendee = $this->_getAttendee($property);
705                     if ($newAttendee) {
706                         $newAttendees[] = $newAttendee;
707                     }
708                     break;
709                     
710                 case 'CLASS':
711                     if (in_array($property->getValue(), array(Calendar_Model_Event::CLASS_PRIVATE, Calendar_Model_Event::CLASS_PUBLIC))) {
712                         $event->class = $property->getValue();
713                     } else {
714                         $event->class = Calendar_Model_Event::CLASS_PUBLIC;
715                     }
716                     
717                     break;
718                     
719                 case 'STATUS':
720                     if (in_array($property->getValue(), array(Calendar_Model_Event::STATUS_CONFIRMED, Calendar_Model_Event::STATUS_TENTATIVE, Calendar_Model_Event::STATUS_CANCELED))) {
721                         $event->status = $property->getValue();
722                     } else {
723                         $event->status = Calendar_Model_Event::STATUS_CONFIRMED;
724                     }
725                     break;
726                     
727                 case 'DTEND':
728                     
729                     if (isset($property['VALUE']) && strtoupper($property['VALUE']) == 'DATE') {
730                         // all day event
731                         $event->is_all_day_event = true;
732                         $dtend = $this->_convertToTinebaseDateTime($property, TRUE);
733                         
734                         // whole day events ends at 23:59:59 in Tine 2.0 but 00:00 the next day in vcalendar
735                         $dtend->subSecond(1);
736                     } else {
737                         $event->is_all_day_event = false;
738                         $dtend = $this->_convertToTinebaseDateTime($property);
739                     }
740                     
741                     $event->dtend = $dtend;
742                     
743                     break;
744                     
745                 case 'DTSTART':
746                     if (isset($property['VALUE']) && strtoupper($property['VALUE']) == 'DATE') {
747                         // all day event
748                         $event->is_all_day_event = true;
749                         $dtstart = $this->_convertToTinebaseDateTime($property, TRUE);
750                     } else {
751                         $event->is_all_day_event = false;
752                         $dtstart = $this->_convertToTinebaseDateTime($property);
753                     }
754                     
755                     $event->originator_tz = $dtstart->getTimezone()->getName();
756                     $event->dtstart = $dtstart;
757                     
758                     break;
759                     
760                 case 'SEQUENCE':
761                     $event->seq = $property->getValue();
762                     break;
763                     
764                 case 'DESCRIPTION':
765                 case 'LOCATION':
766                 case 'UID':
767                 case 'SUMMARY':
768                     $key = strtolower($property->name);
769                     $event->$key = $property->getValue();
770                     break;
771                     
772                 case 'ORGANIZER':
773                     $email = null;
774                     
775                     if (!empty($property['EMAIL'])) {
776                         $email = $property['EMAIL'];
777                     } elseif (preg_match('/mailto:(?P<email>.*)/i', $property->getValue(), $matches)) {
778                         $email = $matches['email'];
779                     }
780                     
781                     if ($email !== null) {
782                         // it's not possible to change the organizer by spec
783                         if (empty($event->organizer)) {
784                             $name = isset($property['CN']) ? $property['CN']->getValue() : $email;
785                             $contact = Calendar_Model_Attender::resolveEmailToContact(array(
786                                 'email'     => $email,
787                                 'lastName'  => $name,
788                             ));
789                         
790                             $event->organizer = $contact->getId();
791                         }
792                         
793                         // Lightning attaches organizer ATTENDEE properties to ORGANIZER property and does not add an ATTENDEE for the organizer
794                         if (isset($property['PARTSTAT'])) {
795                             $newAttendees[] = $this->_getAttendee($property);
796                         }
797                     }
798                     
799                     break;
800
801                 case 'RECURRENCE-ID':
802                     // original start of the event
803                     $event->recurid = $this->_convertToTinebaseDateTime($property);
804                     
805                     // convert recurrence id to utc
806                     $event->recurid->setTimezone('UTC');
807                     
808                     break;
809                     
810                 case 'RRULE':
811                     $rruleString = $property->getValue();
812                     
813                     // convert date format
814                     $rruleString = preg_replace_callback('/UNTIL=([\dTZ]+)(?=;?)/', function($matches) {
815                         if (strlen($matches[1]) < 10) {
816                             $dtUntil = date_create($matches[1], new DateTimeZone ((string) Tinebase_Core::get(Tinebase_Core::USERTIMEZONE)));
817                             $dtUntil->setTimezone(new DateTimeZone('UTC'));
818                         } else {
819                             $dtUntil = date_create($matches[1]);
820                         }
821                         
822                         return 'UNTIL=' . $dtUntil->format(Tinebase_Record_Abstract::ISO8601LONG);
823                     }, $rruleString);
824
825                     // remove additional days from BYMONTHDAY property (BYMONTHDAY=11,15 => BYMONTHDAY=11)
826                     $rruleString = preg_replace('/(BYMONTHDAY=)([\d]+),([,\d]+)/', '$1$2', $rruleString);
827                     
828                     $event->rrule = $rruleString;
829                     
830                     // process exceptions
831                     if (isset($vevent->EXDATE)) {
832                         $exdates = new Tinebase_Record_RecordSet('Calendar_Model_Event');
833                         
834                         foreach ($vevent->EXDATE as $exdate) {
835                             foreach ($exdate->getDateTimes() as $exception) {
836                                 if (isset($exdate['VALUE']) && strtoupper($exdate['VALUE']) == 'DATE') {
837                                     $recurid = new Tinebase_DateTime($exception->format(Tinebase_Record_Abstract::ISO8601LONG), (string) Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
838                                 } else {
839                                     $recurid = new Tinebase_DateTime($exception->format(Tinebase_Record_Abstract::ISO8601LONG), $exception->getTimezone());
840                                 }
841                                 $recurid->setTimezone(new DateTimeZone('UTC'));
842                                 
843                                 $eventException = new Calendar_Model_Event(array(
844                                     'recurid'    => $recurid,
845                                     'is_deleted' => true
846                                 ));
847                         
848                                 $exdates->addRecord($eventException);
849                             }
850                         }
851                     
852                         $event->exdate = $exdates;
853                     }
854                     
855                     break;
856                     
857                 case 'TRANSP':
858                     if (in_array($property->getValue(), array(Calendar_Model_Event::TRANSP_OPAQUE, Calendar_Model_Event::TRANSP_TRANSP))) {
859                         $event->transp = $property->getValue();
860                     } else {
861                         $event->transp = Calendar_Model_Event::TRANSP_TRANSP;
862                     }
863                     
864                     break;
865                     
866                 case 'UID':
867                     // it's not possible to change the uid by spec
868                     if (!empty($event->uid)) {
869                         continue;
870                     }
871                     
872                     $event->uid = $property->getValue();
873                 
874                     break;
875                     
876                 case 'VALARM':
877                     foreach ($property as $valarm) {
878                         
879                         if ($valarm->ACTION == 'NONE') {
880                             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
881                                 . ' We can\'t cope with action NONE: iCal 6.0 sends default alarms in the year 1976 with action NONE. Skipping alarm.');
882                             continue;
883                         }
884                         
885                         # TRIGGER:-PT15M
886                         if (is_string($valarm->TRIGGER->getValue()) && $valarm->TRIGGER instanceof Sabre\VObject\Property\ICalendar\Duration) {
887                             $valarm->TRIGGER->add('VALUE', 'DURATION');
888                         }
889                         
890                         $trigger = is_object($valarm->TRIGGER['VALUE']) ? $valarm->TRIGGER['VALUE'] : (is_object($valarm->TRIGGER['RELATED']) ? $valarm->TRIGGER['RELATED'] : NULL);
891                         
892                         if ($trigger === NULL) {
893                             // added Trigger/Related for eM Client alarms
894                             // 2014-01-03 - Bullshit, why don't we have testdata for emclient alarms?
895                             //              this alarm handling should be refactored, the logic is scrambled
896                             // @see 0006110: handle iMIP messages from outlook
897                             // @todo fix 0007446: handle broken alarm in outlook invitation message
898                             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
899                                 . ' Alarm has no TRIGGER value. Skipping it.');
900                             continue;
901                         }
902                         
903                         switch (strtoupper($trigger->getValue())) {
904                             # TRIGGER;VALUE=DATE-TIME:20111031T130000Z
905                             case 'DATE-TIME':
906                                 $alarmTime = new Tinebase_DateTime($valarm->TRIGGER->getValue());
907                                 $alarmTime->setTimezone('UTC');
908                                 
909                                 $alarm = new Tinebase_Model_Alarm(array(
910                                     'alarm_time'        => $alarmTime,
911                                     'minutes_before'    => 'custom',
912                                     'model'             => 'Calendar_Model_Event'
913                                 ));
914                                 
915                                 $event->alarms->addRecord($alarm);
916                                 
917                                 break;
918                                 
919                             # TRIGGER;VALUE=DURATION:-PT1H15M
920                             case 'DURATION':
921                             default:
922                                 $alarmTime = $this->_convertToTinebaseDateTime($vevent->DTSTART);
923                                 $alarmTime->setTimezone('UTC');
924                                 
925                                 preg_match('/(?P<invert>[+-]?)(?P<spec>P.*)/', $valarm->TRIGGER->getValue(), $matches);
926                                 $duration = new DateInterval($matches['spec']);
927                                 $duration->invert = !!($matches['invert'] === '-');
928
929                                 $alarm = new Tinebase_Model_Alarm(array(
930                                     'alarm_time'        => $alarmTime->add($duration),
931                                     'minutes_before'    => ($duration->format('%d') * 60 * 24) + ($duration->format('%h') * 60) + ($duration->format('%i')),
932                                     'model'             => 'Calendar_Model_Event'
933                                 ));
934                                 
935                                 $event->alarms->addRecord($alarm);
936                                 
937                                 break;
938                         }
939                     }
940                     
941                     break;
942                     
943                 case 'CATEGORIES':
944                     $event->tags = Tinebase_Model_Tag::resolveTagNameToTag($property->getParts(), 'Calendar');
945                     break;
946                     
947                 case 'X-MOZ-LASTACK':
948                     $lastAck = $this->_convertToTinebaseDateTime($property);
949                     break;
950                     
951                 case 'X-MOZ-SNOOZE-TIME':
952                     $snoozeTime = $this->_convertToTinebaseDateTime($property);
953                     break;
954                     
955                 default:
956                     // thunderbird saves snooze time for recurring event occurrences in properties with names like this -
957                     // we just assume that the event/recur series has only one snooze time 
958                     if (preg_match('/^X-MOZ-SNOOZE-TIME-[0-9]+$/', $property->name)) {
959                         $snoozeTime = $this->_convertToTinebaseDateTime($property);
960                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
961                             . ' Found snooze time for recur occurrence: ' . $snoozeTime->toString());
962                     }
963                     break;
964             }
965         }
966         
967         // merge old and new attendee
968         Calendar_Model_Attender::emailsToAttendee($event, $newAttendees);
969         
970         if (($ownAttendee = Calendar_Model_Attender::getOwnAttender($event->attendee)) !== null) {
971             if (isset($lastAck)) {
972                 $ownAttendee->alarm_ack_time = $lastAck;
973             }
974             if (isset($snoozeTime)) {
975                 $ownAttendee->alarm_snooze_time = $snoozeTime;
976             }
977         }
978         
979         if (empty($event->seq)) {
980             $event->seq = 0;
981         }
982         
983         if (empty($event->class)) {
984             $event->class = Calendar_Model_Event::CLASS_PUBLIC;
985         }
986         
987         // convert all datetime fields to UTC
988         $event->setTimezone('UTC');
989     }
990     
991     /**
992      * get datetime from sabredav datetime property (user TZ is fallback)
993      * 
994      * @param  Sabre\VObject\Property  $dateTimeProperty
995      * @param  boolean                 $_useUserTZ
996      * @return Tinebase_DateTime
997      * 
998      * @todo try to guess some common timezones
999      */
1000     protected function _convertToTinebaseDateTime(\Sabre\VObject\Property $dateTimeProperty, $_useUserTZ = FALSE)
1001     {
1002         $defaultTimezone = date_default_timezone_get();
1003         date_default_timezone_set((string) Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
1004         
1005         if ($dateTimeProperty instanceof Sabre\VObject\Property\ICalendar\DateTime) {
1006             $dateTime = $dateTimeProperty->getDateTime();
1007             $tz = ($_useUserTZ || (isset($dateTimeProperty['VALUE']) && strtoupper($dateTimeProperty['VALUE']) == 'DATE')) ? 
1008                 (string) Tinebase_Core::get(Tinebase_Core::USERTIMEZONE) : 
1009                 $dateTime->getTimezone();
1010             
1011             $result = new Tinebase_DateTime($dateTime->format(Tinebase_Record_Abstract::ISO8601LONG), $tz);
1012         } else {
1013             $result = new Tinebase_DateTime($dateTimeProperty->getValue());
1014         }
1015         
1016         date_default_timezone_set($defaultTimezone);
1017         
1018         return $result;
1019     }
1020 }