0011542: Event exception update looses attendee
[tine20] / tine20 / ActiveSync / Controller / Calendar.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     ActiveSync
6  * @subpackage  Controller
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Cornelius Weiss <c.weiss@metaways.de>
10  */
11
12 /**
13  * controller events class
14  * 
15  * @package     ActiveSync
16  * @subpackage  Controller
17  */
18 class ActiveSync_Controller_Calendar extends ActiveSync_Controller_Abstract implements Syncroton_Data_IDataCalendar
19 {
20     /**
21      * available filters
22      * 
23      * @var array
24      */
25     protected $_filterArray = array(
26         Syncroton_Command_Sync::FILTER_2_WEEKS_BACK,
27         Syncroton_Command_Sync::FILTER_1_MONTH_BACK,
28         Syncroton_Command_Sync::FILTER_3_MONTHS_BACK,
29         Syncroton_Command_Sync::FILTER_6_MONTHS_BACK
30     );
31     
32     /**
33      * mapping of attendee status
34      *
35      * NOTE: not surjektive
36      * @var array
37      */
38     protected $_attendeeStatusMapping = array(
39         Syncroton_Model_EventAttendee::ATTENDEE_STATUS_UNKNOWN       => Calendar_Model_Attender::STATUS_NEEDSACTION,
40         Syncroton_Model_EventAttendee::ATTENDEE_STATUS_TENTATIVE     => Calendar_Model_Attender::STATUS_TENTATIVE,
41         Syncroton_Model_EventAttendee::ATTENDEE_STATUS_ACCEPTED      => Calendar_Model_Attender::STATUS_ACCEPTED,
42         Syncroton_Model_EventAttendee::ATTENDEE_STATUS_DECLINED      => Calendar_Model_Attender::STATUS_DECLINED,
43         //self::ATTENDEE_STATUS_NOTRESPONDED  => Calendar_Model_Attender::STATUS_NEEDSACTION
44     );
45     
46     /**
47      * mapping of attendee status in meeting response
48      * @var array
49      */
50     protected $_meetingResponseAttendeeStatusMapping = array(
51         Syncroton_Model_MeetingResponse::RESPONSE_ACCEPTED    => Calendar_Model_Attender::STATUS_ACCEPTED,
52         Syncroton_Model_MeetingResponse::RESPONSE_TENTATIVE   => Calendar_Model_Attender::STATUS_TENTATIVE,
53         Syncroton_Model_MeetingResponse::RESPONSE_DECLINED    => Calendar_Model_Attender::STATUS_DECLINED,
54     );
55     
56     /**
57      * mapping of busy status
58      *
59      * NOTE: not surjektive
60      * @var array
61      */
62     protected $_busyStatusMapping = array(
63         Syncroton_Model_Event::BUSY_STATUS_FREE      => Calendar_Model_Attender::STATUS_DECLINED,
64         Syncroton_Model_Event::BUSY_STATUS_TENATTIVE => Calendar_Model_Attender::STATUS_TENTATIVE,
65         Syncroton_Model_Event::BUSY_STATUS_BUSY      => Calendar_Model_Attender::STATUS_ACCEPTED
66     );
67     
68     /**
69      * mapping of attendee types
70      * 
71      * NOTE: recources need extra handling!
72      * @var array
73      */
74     protected $_attendeeTypeMapping = array(
75         Syncroton_Model_EventAttendee::ATTENDEE_TYPE_REQUIRED => Calendar_Model_Attender::ROLE_REQUIRED,
76         Syncroton_Model_EventAttendee::ATTENDEE_TYPE_OPTIONAL => Calendar_Model_Attender::ROLE_OPTIONAL,
77         Syncroton_Model_EventAttendee::ATTENDEE_TYPE_RESOURCE => Calendar_Model_Attender::USERTYPE_RESOURCE
78     );
79     
80     /**
81      * mapping of recur types
82      *
83      * NOTE: not surjektive
84      * @var array
85      */
86     protected $_recurTypeMapping = array(
87         Syncroton_Model_EventRecurrence::TYPE_DAILY          => Calendar_Model_Rrule::FREQ_DAILY,
88         Syncroton_Model_EventRecurrence::TYPE_WEEKLY         => Calendar_Model_Rrule::FREQ_WEEKLY,
89         Syncroton_Model_EventRecurrence::TYPE_MONTHLY        => Calendar_Model_Rrule::FREQ_MONTHLY,
90         Syncroton_Model_EventRecurrence::TYPE_MONTHLY_DAYN   => Calendar_Model_Rrule::FREQ_MONTHLY,
91         Syncroton_Model_EventRecurrence::TYPE_YEARLY         => Calendar_Model_Rrule::FREQ_YEARLY,
92         Syncroton_Model_EventRecurrence::TYPE_YEARLY_DAYN    => Calendar_Model_Rrule::FREQ_YEARLY,
93     );
94     
95     /**
96      * mapping of weekdays
97      * 
98      * NOTE: ActiveSync uses a bitmask
99      * @var array
100      */
101     protected $_recurDayMapping = array(
102         Calendar_Model_Rrule::WDAY_SUNDAY       => Syncroton_Model_EventRecurrence::RECUR_DOW_SUNDAY,
103         Calendar_Model_Rrule::WDAY_MONDAY       => Syncroton_Model_EventRecurrence::RECUR_DOW_MONDAY,
104         Calendar_Model_Rrule::WDAY_TUESDAY      => Syncroton_Model_EventRecurrence::RECUR_DOW_TUESDAY,
105         Calendar_Model_Rrule::WDAY_WEDNESDAY    => Syncroton_Model_EventRecurrence::RECUR_DOW_WEDNESDAY,
106         Calendar_Model_Rrule::WDAY_THURSDAY     => Syncroton_Model_EventRecurrence::RECUR_DOW_THURSDAY,
107         Calendar_Model_Rrule::WDAY_FRIDAY       => Syncroton_Model_EventRecurrence::RECUR_DOW_FRIDAY,
108         Calendar_Model_Rrule::WDAY_SATURDAY     => Syncroton_Model_EventRecurrence::RECUR_DOW_SATURDAY
109     );
110     
111     /**
112      * trivial mapping
113      *
114      * @var array
115      */
116     protected $_mapping = array(
117         //'Timezone'          => 'timezone',
118         'allDayEvent'       => 'is_all_day_event',
119         //'BusyStatus'        => 'transp',
120         //'OrganizerName'     => 'organizer',
121         //'OrganizerEmail'    => 'organizer',
122         //'DtStamp'           => 'last_modified_time',  // not used outside from Tine 2.0
123         'endTime'           => 'dtend',
124         'location'          => 'location',
125         'reminder'          => 'alarms',
126         //'Sensitivity'       => 'class',
127         'subject'           => 'summary',
128         'body'              => 'description',
129         'startTime'         => 'dtstart',
130         //'UID'               => 'uid',             // not used outside from Tine 2.0
131         //'MeetingStatus'     => 'status_id',
132         'attendees'         => 'attendee',
133         'categories'        => 'tags',
134         'recurrence'        => 'rrule',
135         'exceptions'        => 'exdate',
136     );
137     
138     /**
139      * name of Tine 2.0 backend application
140      * 
141      * @var string
142      */
143     protected $_applicationName     = 'Calendar';
144     
145     /**
146      * name of Tine 2.0 model to use
147      * 
148      * @var string
149      */
150     protected $_modelName           = 'Event';
151     
152     /**
153      * type of the default folder
154      *
155      * @var int
156      */
157     protected $_defaultFolderType   = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR;
158     
159     /**
160      * default container for new entries
161      * 
162      * @var string
163      */
164     protected $_defaultFolder       = ActiveSync_Preference::DEFAULTCALENDAR;
165     
166     /**
167      * type of user created folders
168      *
169      * @var int
170      */
171     protected $_folderType          = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED;
172     
173     /**
174      * name of property which defines the filterid for different content classes
175      * 
176      * @var string
177      */
178     protected $_filterProperty      = 'calendarfilterId';
179     
180     /**
181      * name of the contentcontoller class
182      * 
183      * @var string
184      */
185     protected $_contentControllerName = 'Calendar_Controller_MSEventFacade';
186     
187     protected $_defaultContainerPreferenceName = Calendar_Preference::DEFAULTCALENDAR;
188     
189     /**
190      * list of devicetypes with wrong busy status default (0 = FREE)
191      * 
192      * @var array
193      */
194     protected $_devicesWithWrongBusyStatusDefault = array(
195         'samsunggti9100', // Samsung Galaxy S-2
196         'samsunggtn7000', // Samsung Galaxy Note 
197         'samsunggti9300', // Samsung Galaxy S-3
198     );
199     
200     /**
201      * (non-PHPdoc)
202      * @see ActiveSync_Controller_Abstract::__construct()
203      */
204     public function __construct(Syncroton_Model_IDevice $_device, DateTime $_syncTimeStamp)
205     {
206         parent::__construct($_device, $_syncTimeStamp);
207         
208         $this->_contentController->setEventFilter($this->_getContentFilter(0));
209     }
210     
211     /**
212      * (non-PHPdoc)
213      * @see Syncroton_Data_IDataCalendar::setAttendeeStatus()
214      */
215     public function setAttendeeStatus(Syncroton_Model_MeetingResponse $response)
216     {
217         $event = $instance = $this->_contentController->get($response->requestId);
218         $method = 'attenderStatusUpdate';
219         
220         if ($response->instanceId instanceof DateTime) {
221             $instance = $event->exdate->filter('recurid', $event->uid . '-' . $response->instanceId->format(Tinebase_Record_Abstract::ISO8601LONG))->getFirstRecord();
222             if (! $instance) {
223                 $exceptions = $event->exdate;
224                 $event->exdate = $exceptions->getOriginalDtStart();
225                 
226                 $instance = Calendar_Model_Rrule::computeNextOccurrence($event, $exceptions, new Tinebase_DateTime($response->instanceId));
227             }
228             
229             $method = 'attenderStatusCreateRecurException';
230         }
231         
232         $attendee = Calendar_Model_Attender::getOwnAttender($instance->attendee);
233         if (! $attendee) {
234             throw new Syncroton_Exception_Status_MeetingResponse("party crushing not allowed", Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST);
235         }
236         $attendee->status = $this->_meetingResponseAttendeeStatusMapping[$response->userResponse];
237         
238         Calendar_Controller_Event::getInstance()->$method($instance, $attendee, $attendee->status_authkey);
239         
240         // return id of calendar event
241         return $response->requestId;
242     }
243     
244     /**
245      * (non-PHPdoc)
246      * @see ActiveSync_Controller_Abstract::toSyncrotonModel()
247      * @todo handle BusyStatus
248      */
249     public function toSyncrotonModel($entry, array $options = array())
250     {
251         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(
252             __METHOD__ . '::' . __LINE__ . " calendar data " . print_r($entry->toArray(), true));
253         
254         $syncrotonEvent = new Syncroton_Model_Event();
255         
256         foreach ($this->_mapping as $syncrotonProperty => $tine20Property) {
257             if (empty($entry->$tine20Property) && $entry->$tine20Property != '0' || count($entry->$tine20Property) === 0) {
258                 continue;
259             }
260             
261             switch($tine20Property) {
262                 case 'alarms':
263                     $entry->$tine20Property->sort('alarm_time');
264                     $alarm = $entry->alarms->getFirstRecord();
265                     
266                     if($alarm instanceof Tinebase_Model_Alarm) {
267                         // NOTE: option minutes_before is always calculated by Calendar_Controller_Event::_inspectAlarmSet
268                         $minutesBefore = (int) $alarm->getOption('minutes_before');
269                         
270                         // avoid negative alarms which may break phones
271                         if ($minutesBefore >= 0) {
272                             $syncrotonEvent->$syncrotonProperty = $minutesBefore;
273                         }
274                     }
275                     
276                     break;
277                     
278                 case 'attendee':
279                     if ($this->_device->devicetype === Syncroton_Model_Device::TYPE_IPHONE &&
280                         $entry->container_id       !== $this->_getDefaultContainerId()) {
281                         
282                         continue;
283                     }
284                     
285                     // fill attendee cache
286                     Calendar_Model_Attender::resolveAttendee($entry->$tine20Property, FALSE);
287                     
288                     $attendees = array();
289                 
290                     foreach($entry->$tine20Property as $attenderObject) {
291                         $attendee = new Syncroton_Model_EventAttendee();
292                         $attendee->name = $attenderObject->getName();
293                         $attendee->email = $attenderObject->getEmail();
294                         
295                         $acsType = array_search($attenderObject->role, $this->_attendeeTypeMapping);
296                         $attendee->attendeeType = $acsType ? $acsType : Syncroton_Model_EventAttendee::ATTENDEE_TYPE_REQUIRED;
297             
298                         $acsStatus = array_search($attenderObject->status, $this->_attendeeStatusMapping);
299                         $attendee->attendeeStatus = $acsStatus ? $acsStatus : Syncroton_Model_EventAttendee::ATTENDEE_STATUS_UNKNOWN;
300                         
301                         $attendees[] = $attendee;
302                     }
303                     
304                     $syncrotonEvent->$syncrotonProperty = $attendees;
305                     
306                     // set own status
307                     if (($ownAttendee = Calendar_Model_Attender::getOwnAttender($entry->attendee)) !== null && ($busyType = array_search($ownAttendee->status, $this->_busyStatusMapping)) !== false) {
308                         $syncrotonEvent->busyStatus = $busyType;
309                     }
310                     
311                     break;
312                     
313                 case 'description':
314                     $syncrotonEvent->$syncrotonProperty = new Syncroton_Model_EmailBody(array(
315                         'type' => Syncroton_Model_EmailBody::TYPE_PLAINTEXT,
316                         'data' => $entry->$tine20Property
317                     ));
318                 
319                     break;
320                 
321                 case 'dtend':
322                     if($entry->$tine20Property instanceof DateTime) {
323                         if ($entry->is_all_day_event == true) {
324                             // whole day events ends at 23:59:59 in Tine 2.0 but 00:00 the next day in AS
325                             $dtend = clone $entry->$tine20Property;
326                             $dtend->addSecond($dtend->get('s') == 59 ? 1 : 0);
327                             $dtend->addMinute($dtend->get('i') == 59 ? 1 : 0);
328
329                             $syncrotonEvent->$syncrotonProperty = $dtend;
330                         } else {
331                             $syncrotonEvent->$syncrotonProperty = $entry->$tine20Property;
332                         }
333                     }
334                     
335                     break;
336                     
337                 case 'dtstart':
338                     if($entry->$tine20Property instanceof DateTime) {
339                         $syncrotonEvent->$syncrotonProperty = $entry->$tine20Property;
340                     }
341                     
342                     break;
343                     
344                 case 'exdate':
345                     // handle exceptions of repeating events
346                     if($entry->$tine20Property instanceof Tinebase_Record_RecordSet && $entry->$tine20Property->count() > 0) {
347                         $exceptions = array();
348                     
349                         foreach ($entry->exdate as $exdate) {
350                             $exception = new Syncroton_Model_EventException();
351                             
352                             // send the Deleted element only, when needed
353                             // HTC devices ignore the value(0 or 1) of the Deleted element
354                             if ((int)$exdate->is_deleted === 1) { 
355                                 $exception->deleted        = 1;
356                             }
357                             $exception->exceptionStartTime = $exdate->getOriginalDtStart();
358                             
359                             if ((int)$exdate->is_deleted === 0) {
360                                 $exceptionSyncrotonEvent = $this->toSyncrotonModel($exdate);
361                                 foreach ($exception->getProperties() as $property) {
362                                     if (isset($exceptionSyncrotonEvent->$property)) {
363                                         $exception->$property = $exceptionSyncrotonEvent->$property;
364                                     }
365                                 }
366                                 unset($exceptionSyncrotonEvent);
367                             }
368                             
369                             $exceptions[] = $exception;
370                         }
371                         
372                         $syncrotonEvent->$syncrotonProperty = $exceptions;
373                     }
374                     
375                     break;
376                     
377                 case 'rrule':
378                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
379                         __METHOD__ . '::' . __LINE__ . " calendar rrule " . $entry->$tine20Property);
380                         
381                     $rrule = Calendar_Model_Rrule::getRruleFromString($entry->$tine20Property);
382                     
383                     $recurrence = new Syncroton_Model_EventRecurrence();
384                     
385                     // required fields
386                     switch($rrule->freq) {
387                         case Calendar_Model_Rrule::FREQ_DAILY:
388                             $recurrence->type = Syncroton_Model_EventRecurrence::TYPE_DAILY;
389                             
390                             break;
391                     
392                         case Calendar_Model_Rrule::FREQ_WEEKLY:
393                             $recurrence->type      = Syncroton_Model_EventRecurrence::TYPE_WEEKLY;
394                             $recurrence->dayOfWeek = $this->_convertDayToBitMask($rrule->byday);
395                             
396                             break;
397                     
398                         case Calendar_Model_Rrule::FREQ_MONTHLY:
399                             if(!empty($rrule->bymonthday)) {
400                                 $recurrence->type       = Syncroton_Model_EventRecurrence::TYPE_MONTHLY;
401                                 $recurrence->dayOfMonth = $rrule->bymonthday;
402                             } else {
403                                 $weekOfMonth = (int) substr($rrule->byday, 0, -2);
404                                 $weekOfMonth = ($weekOfMonth == -1) ? 5 : $weekOfMonth;
405                                 $dayOfWeek   = substr($rrule->byday, -2);
406                     
407                                 $recurrence->type        = Syncroton_Model_EventRecurrence::TYPE_MONTHLY_DAYN;
408                                 $recurrence->weekOfMonth = $weekOfMonth;
409                                 $recurrence->dayOfWeek   = $this->_convertDayToBitMask($dayOfWeek);
410                             }
411                             
412                             break;
413                     
414                         case Calendar_Model_Rrule::FREQ_YEARLY:
415                             if(!empty($rrule->bymonthday)) {
416                                 $recurrence->type        = Syncroton_Model_EventRecurrence::TYPE_YEARLY;
417                                 $recurrence->dayOfMonth  = $rrule->bymonthday;
418                                 $recurrence->monthOfYear = $rrule->bymonth;
419                             } else {
420                                 $weekOfMonth = (int) substr($rrule->byday, 0, -2);
421                                 $weekOfMonth = ($weekOfMonth == -1) ? 5 : $weekOfMonth;
422                                 $dayOfWeek   = substr($rrule->byday, -2);
423                     
424                                 $recurrence->type        = Syncroton_Model_EventRecurrence::TYPE_YEARLY_DAYN;
425                                 $recurrence->weekOfMonth = $weekOfMonth;
426                                 $recurrence->dayOfWeek   = $this->_convertDayToBitMask($dayOfWeek);
427                                 $recurrence->monthOfYear = $rrule->bymonth;
428                             }
429                             
430                             break;
431                     }
432                     
433                     // required field
434                     $recurrence->interval = $rrule->interval ? $rrule->interval : 1;
435                     
436                     if($rrule->count) {
437                         $recurrence->occurrences = $rrule->count;
438                     } else if($rrule->until instanceof DateTime) {
439                         $recurrence->until = $rrule->until;
440                     }
441                     
442                     $syncrotonEvent->$syncrotonProperty = $recurrence;
443                     
444                     break;
445                     
446                 case 'tags':
447                     $syncrotonEvent->$syncrotonProperty = $entry->$tine20Property->name;;
448                     
449                     break;
450                     
451                 default:
452                     $syncrotonEvent->$syncrotonProperty = $entry->$tine20Property;
453                     
454                     break;
455             }
456         }
457         
458         $timeZoneConverter = ActiveSync_TimezoneConverter::getInstance(
459             Tinebase_Core::getLogger(),
460             Tinebase_Core::get(Tinebase_Core::CACHE)
461         );
462         
463         $syncrotonEvent->timezone = $timeZoneConverter->encodeTimezone(Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
464         
465         $syncrotonEvent->meetingStatus = 1;
466         $syncrotonEvent->sensitivity = 0;
467         $syncrotonEvent->dtStamp = $entry->creation_time;
468         $syncrotonEvent->uID = $entry->uid;
469         
470         $this->_addOrganizer($syncrotonEvent, $entry);
471         
472         return $syncrotonEvent;
473     }
474     
475     /**
476      * convert string of days (TU,TH) to bitmask used by ActiveSync
477      *  
478      * @param $_days
479      * @return int
480      */
481     protected function _convertDayToBitMask($_days)
482     {
483         $daysArray = explode(',', $_days);
484         
485         $result = 0;
486         
487         foreach($daysArray as $dayString) {
488             $result = $result + $this->_recurDayMapping[$dayString];
489         }
490         
491         return $result;
492     }
493     
494     /**
495      * convert bitmask used by ActiveSync to string of days (TU,TH) 
496      *  
497      * @param int $_days
498      * @return string
499      */
500     protected function _convertBitMaskToDay($_days)
501     {
502         $daysArray = array();
503         
504         for($bitmask = 1; $bitmask <= Syncroton_Model_EventRecurrence::RECUR_DOW_SATURDAY; $bitmask = $bitmask << 1) {
505             $dayMatch = $_days & $bitmask;
506             if($dayMatch === $bitmask) {
507                 $daysArray[] = array_search($bitmask, $this->_recurDayMapping);
508             }
509         }
510         $result = implode(',', $daysArray);
511         
512         return $result;
513     }
514     
515     /**
516      * (non-PHPdoc)
517      * @see ActiveSync_Controller_Abstract::toTineModel()
518      */
519     public function toTineModel(Syncroton_Model_IEntry $data, $entry = null)
520     {
521         if ($entry instanceof Calendar_Model_Event) {
522             $event = $entry;
523         } else {
524             $event = new Calendar_Model_Event(array(), true);
525         }
526
527         if ($data instanceof Syncroton_Model_Event) {
528             $data->copyFieldsFromParent();
529         }
530         
531         // Update seq to entries seq to prevent concurrent update
532         $event->seq = $entry['seq'];
533         
534         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(
535             __METHOD__ . '::' . __LINE__ . " Event before mapping: " . print_r($event->toArray(), true));
536         
537         foreach ($this->_mapping as $syncrotonProperty => $tine20Property) {
538             if (! isset($data->$syncrotonProperty)) {
539                 if ($tine20Property === 'description' && $this->_device->devicetype == Syncroton_Model_Device::TYPE_IPHONE) {
540                     // @see #8230: added alarm to event on iOS 6.1 -> description removed
541                     // this should be removed when Tine 2.0 / Syncroton supports ghosted properties
542                     if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(
543                         __METHOD__ . '::' . __LINE__ . ' Unsetting description');
544                     unset($event->$tine20Property);
545                 } else {
546                     if ($tine20Property === 'attendee' && $entry &&
547                         $this->_device->devicetype === Syncroton_Model_Device::TYPE_IPHONE &&
548                         $entry->container_id       !== $this->_getDefaultContainerId()) {
549                             // keep attendees as the are / they were not sent to the device before
550                     } else {
551                         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(
552                             __METHOD__ . '::' . __LINE__ . ' Removing ' . $tine20Property);
553                         $event->$tine20Property = null;
554                     }
555                 }
556                 continue;
557             }
558             
559             switch ($tine20Property) {
560                 case 'alarms':
561                     // handled after switch statement
562                     
563                     break;
564                     
565                 case 'attendee':
566                     if ($entry && 
567                         $this->_device->devicetype === Syncroton_Model_Device::TYPE_IPHONE &&
568                         $entry->container_id       !== $this->_getDefaultContainerId()) {
569                             // keep attendees as the are / they were not sent to the device before
570                             continue;
571                     }
572
573                     $newAttendees = array();
574                     
575                     foreach($data->$syncrotonProperty as $attendee) {
576                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
577                             __METHOD__ . '::' . __LINE__ . " attendee email " . $attendee->email);
578                         
579                         if(isset($attendee->attendeeType) && (isset($this->_attendeeTypeMapping[$attendee->attendeeType]) || array_key_exists($attendee->attendeeType, $this->_attendeeTypeMapping))) {
580                             $role = $this->_attendeeTypeMapping[$attendee->attendeeType];
581                         } else {
582                             $role = Calendar_Model_Attender::ROLE_REQUIRED;
583                         }
584                         
585                         // AttendeeStatus send only on repsonse
586                         if (preg_match('/(?P<firstName>\S*) (?P<lastNameName>\S*)/', $attendee->name, $matches)) {
587                             $firstName = $matches['firstName'];
588                             $lastName  = $matches['lastNameName'];
589                         } else {
590                             $firstName = null;
591                             $lastName  = $attendee->name;
592                         }
593                         
594                         // @todo handle resources
595                         $newAttendees[] = array(
596                             'userType'  => Calendar_Model_Attender::USERTYPE_USER,
597                             'firstName' => $firstName,
598                             'lastName'  => $lastName,
599                             #'partStat'  => $status,
600                             'role'      => $role,
601                             'email'     => $attendee->email
602                         );
603                     }
604                     
605                     Calendar_Model_Attender::emailsToAttendee($event, $newAttendees);
606                     
607                     break;
608
609                 case 'exdate':
610                     // handle exceptions from recurrence
611                     $exdates = new Tinebase_Record_RecordSet('Calendar_Model_Event');
612                     $oldExdates = $event->exdate instanceof Tinebase_Record_RecordSet ? $event->exdate : new Tinebase_Record_RecordSet('Calendar_Model_Event');
613                     
614                     foreach ($data->$syncrotonProperty as $exception) {
615                         $eventException = $this->_getRecurException($oldExdates, $exception);
616
617                         if ($exception->deleted == 0) {
618                             $eventException = $this->toTineModel($exception, $eventException);
619                             $eventException->last_modified_time = new Tinebase_DateTime($this->_syncTimeStamp);
620                         }
621
622                         $eventException->is_deleted = (bool) $exception->deleted;
623                         $eventException->seq = $entry['seq'];
624                         $exdates->addRecord($eventException);
625                     }
626                     
627                     $event->$tine20Property = $exdates;
628                     
629                     break;
630                     
631                 case 'description':
632                     // @todo check $data->$fieldName->Type and convert to/from HTML if needed
633                     if ($data->$syncrotonProperty instanceof Syncroton_Model_EmailBody) {
634                         $event->$tine20Property = $data->$syncrotonProperty->data;
635                     } else {
636                         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(
637                             __METHOD__ . '::' . __LINE__ . ' Removing description.');
638                         $event->$tine20Property = null;
639                     }
640                 
641                     break;
642                     
643                 case 'rrule':
644                     // handle recurrence
645                     if ($data->$syncrotonProperty instanceof Syncroton_Model_EventRecurrence && isset($data->$syncrotonProperty->type)) {
646                         $rrule = new Calendar_Model_Rrule();
647                     
648                         switch ($data->$syncrotonProperty->type) {
649                             case Syncroton_Model_EventRecurrence::TYPE_DAILY:
650                                 $rrule->freq = Calendar_Model_Rrule::FREQ_DAILY;
651                                 
652                                 break;
653                     
654                             case Syncroton_Model_EventRecurrence::TYPE_WEEKLY:
655                                 $rrule->freq  = Calendar_Model_Rrule::FREQ_WEEKLY;
656                                 $rrule->byday = $this->_convertBitMaskToDay($data->$syncrotonProperty->dayOfWeek);
657                                 
658                                 break;
659                                  
660                             case Syncroton_Model_EventRecurrence::TYPE_MONTHLY:
661                                 $rrule->freq       = Calendar_Model_Rrule::FREQ_MONTHLY;
662                                 $rrule->bymonthday = $data->$syncrotonProperty->dayOfMonth;
663                                 
664                                 break;
665                                  
666                             case Syncroton_Model_EventRecurrence::TYPE_MONTHLY_DAYN:
667                                 $rrule->freq = Calendar_Model_Rrule::FREQ_MONTHLY;
668                     
669                                 $week   = $data->$syncrotonProperty->weekOfMonth;
670                                 $day    = $data->$syncrotonProperty->dayOfWeek;
671                                 $byDay  = $week == 5 ? -1 : $week;
672                                 $byDay .= $this->_convertBitMaskToDay($day);
673                     
674                                 $rrule->byday = $byDay;
675                                 
676                                 break;
677                                  
678                             case Syncroton_Model_EventRecurrence::TYPE_YEARLY:
679                                 $rrule->freq       = Calendar_Model_Rrule::FREQ_YEARLY;
680                                 $rrule->bymonth    = $data->$syncrotonProperty->monthOfYear;
681                                 $rrule->bymonthday = $data->$syncrotonProperty->dayOfMonth;
682                                 
683                                 break;
684                                  
685                             case Syncroton_Model_EventRecurrence::TYPE_YEARLY_DAYN:
686                                 $rrule->freq    = Calendar_Model_Rrule::FREQ_YEARLY;
687                                 $rrule->bymonth = $data->$syncrotonProperty->monthOfYear;
688                     
689                                 $week = $data->$syncrotonProperty->weekOfMonth;
690                                 $day  = $data->$syncrotonProperty->dayOfWeek;
691                                 $byDay  = $week == 5 ? -1 : $week;
692                                 $byDay .= $this->_convertBitMaskToDay($day);
693                     
694                                 $rrule->byday = $byDay;
695                                 
696                                 break;
697                         }
698                         
699                         $rrule->interval = isset($data->$syncrotonProperty->interval) ? $data->$syncrotonProperty->interval : 1;
700                     
701                         if(isset($data->$syncrotonProperty->occurrences)) {
702                             $rrule->count = $data->$syncrotonProperty->occurrences;
703                             $rrule->until = null;
704                         } else if(isset($data->$syncrotonProperty->until)) {
705                             $rrule->count = null;
706                             $rrule->until = new Tinebase_DateTime($data->$syncrotonProperty->until);
707                         } else {
708                             $rrule->count = null;
709                             $rrule->until = null;
710                         }
711                         
712                         $event->rrule = $rrule;
713                     }
714                     
715                     break;
716                     
717                     
718                 default:
719                     if ($data->$syncrotonProperty instanceof DateTime) {
720                         $event->$tine20Property = new Tinebase_DateTime($data->$syncrotonProperty);
721                     } else {
722                         $event->$tine20Property = $data->$syncrotonProperty;
723                     }
724                     
725                     break;
726             }
727         }
728         
729         // whole day events ends at 23:59:59 in Tine 2.0 but 00:00 the next day in AS
730         if (isset($event->is_all_day_event) && $event->is_all_day_event == 1) {
731             $event->dtend->subSecond(1);
732         }
733         
734         // decode timezone data
735         if (isset($data->timezone)) {
736             $timeZoneConverter = ActiveSync_TimezoneConverter::getInstance(
737                 Tinebase_Core::getLogger(),
738                 Tinebase_Core::get(Tinebase_Core::CACHE)
739             );
740         
741             try {
742                 $timezone = $timeZoneConverter->getTimezone(
743                     $data->timezone,
744                     Tinebase_Core::get(Tinebase_Core::USERTIMEZONE)
745                 );
746                 $event->originator_tz = $timezone;
747             } catch (ActiveSync_TimezoneNotFoundException $e) {
748                 Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . " timezone data not found " . $data->timezone);
749                 $event->originator_tz = Tinebase_Core::get(Tinebase_Core::USERTIMEZONE);
750             }
751         
752             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
753                     __METHOD__ . '::' . __LINE__ . " timezone data " . $event->originator_tz);
754         }
755         
756         $this->_handleAlarms($data, $event);
757         
758         $this->_handleBusyStatus($data, $event);
759         
760         // event should be valid now
761         $event->isValid();
762         
763         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
764             __METHOD__ . '::' . __LINE__ . " eventData " . print_r($event->toArray(), true));
765
766         return $event;
767     }
768     
769     /**
770      * handle alarms / Reminder
771      * 
772      * @param SimpleXMLElement $xmlData
773      * @param Calendar_Model_Event $event
774      */
775     protected function _handleAlarms($data, $event)
776     {
777         // NOTE: existing alarms are already filtered for CU by MSEF
778         $event->alarms = $event->alarms instanceof Tinebase_Record_RecordSet ? $event->alarms : new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
779         $event->alarms->sort('alarm_time');
780         
781         $currentAlarm = $event->alarms->getFirstRecord();
782         $alarm = NULL;
783         
784         if (isset($data->reminder)) {
785             $dtstart = clone $event->dtstart;
786             
787             $alarm = new Tinebase_Model_Alarm(array(
788                 'alarm_time'        => $dtstart->subMinute($data->reminder),
789                 'minutes_before'    => in_array($data->reminder, array(0, 5, 15, 30, 60, 120, 720, 1440, 2880)) ? $data->reminder : 'custom',
790                 'model'             => 'Calendar_Model_Event'
791             ));
792             
793             $alarmUpdate = Calendar_Controller_Alarm::getMatchingAlarm($event->alarms, $alarm);
794             if (!$alarmUpdate) {
795                 // alarm not existing -> add it
796                 $event->alarms->addRecord($alarm);
797                 
798                 if ($currentAlarm) {
799                     // ActiveSync supports one alarm only -> current got deleted
800                     $event->alarms->removeRecord($currentAlarm);
801                 }
802             }
803         } else if ($currentAlarm) {
804             // current alarm got removed
805             $event->alarms->removeRecord($currentAlarm);
806         }
807     }
808
809     /**
810      * find a matching exdate or return an empty event record
811      *
812      * @param  Tinebase_Record_RecordSet        $oldExdates
813      * @param  Syncroton_Model_EventException   $sevent
814      * @return Calendar_Model_Event
815      */
816     protected function _getRecurException(Tinebase_Record_RecordSet $oldExdates, Syncroton_Model_EventException $sevent)
817     {
818         // we need to use the user timezone here if this is a DATE (like this: RECURRENCE-ID;VALUE=DATE:20140429)
819         $originalDtStart = new Tinebase_DateTime($sevent->exceptionStartTime);
820
821         foreach ($oldExdates as $id => $oldExdate) {
822             if ($originalDtStart == $oldExdate->getOriginalDtStart()) {
823                 return $oldExdate;
824             }
825         }
826
827         return new Calendar_Model_Event(array(
828             'recurid'    => $originalDtStart,
829         ));
830     }
831
832     /**
833      * append organizer name and email
834      *
835      * @param Syncroton_Model_Event $syncrotonEvent
836      * @param Calendar_Model_Event $event
837      */
838     protected function _addOrganizer(Syncroton_Model_Event $syncrotonEvent, Calendar_Model_Event $event)
839     {
840         $organizer = NULL;
841         
842         if(! empty($event->organizer)) {
843             try {
844                 $organizer = $event->resolveOrganizer();
845             } catch (Tinebase_Exception_AccessDenied $tead) {
846                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " " . $tead);
847             }
848         }
849     
850         if ($organizer instanceof Addressbook_Model_Contact) {
851             $organizerName = $organizer->n_fileas;
852             $organizerEmail = $organizer->getPreferedEmailAddress();
853         } else {
854             // set the current account as organizer
855             // if organizer is not set, you can not edit the event on the Motorola Milestone
856             $organizerName = Tinebase_Core::getUser()->accountFullName;
857             $organizerEmail = Tinebase_Core::getUser()->accountEmailAddress;
858         }
859     
860         $syncrotonEvent->organizerName = $organizerName;
861         if ($organizerEmail) {
862             $syncrotonEvent->organizerEmail = $organizerEmail;
863         }
864     }
865     
866     /**
867      * set status of own attender depending on BusyStatus
868      * 
869      * @param SimpleXMLElement $xmlData
870      * @param Calendar_Model_Event $event
871      * 
872      * @todo move detection of special handling / device type to device library
873      */
874     protected function _handleBusyStatus($data, $event)
875     {
876         if (! isset($data->busyStatus)) {
877             return;
878         }
879         
880         $ownAttender = Calendar_Model_Attender::getOwnAttender($event->attendee);
881         if ($ownAttender === NULL) {
882             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
883                 . ' No own attender found.');
884             return;
885         }
886         
887         $busyStatus = $data->busyStatus;
888         if (in_array(strtolower($this->_device->devicetype), $this->_devicesWithWrongBusyStatusDefault)) {
889             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
890                 . ' Device uses a bad default setting. BUSY and FREE are mapped to ACCEPTED.');
891             $busyStatusMapping = array(
892                 Syncroton_Model_Event::BUSY_STATUS_BUSY      => Calendar_Model_Attender::STATUS_ACCEPTED,
893                 Syncroton_Model_Event::BUSY_STATUS_TENATTIVE => Calendar_Model_Attender::STATUS_TENTATIVE,
894                 Syncroton_Model_Event::BUSY_STATUS_FREE      => Calendar_Model_Attender::STATUS_ACCEPTED
895             );
896         } else {
897             $busyStatusMapping = $this->_busyStatusMapping;
898         }
899         
900         if (isset($busyStatusMapping[$busyStatus])) {
901             $ownAttender->status = $busyStatusMapping[$busyStatus];
902         } else {
903             $ownAttender->status = Calendar_Model_Attender::STATUS_NEEDSACTION;
904         }
905     }
906     
907     /**
908      * convert contact from xml to Calendar_Model_EventFilter
909      *
910      * @param SimpleXMLElement $_data
911      * @return array
912      */
913     protected function _toTineFilterArray(SimpleXMLElement $_data)
914     {
915         $xmlData = $_data->children('uri:Calendar');
916         
917         $filterArray = array();
918         
919         foreach($this->_mapping as $fieldName => $field) {
920             if(isset($xmlData->$fieldName)) {
921                 switch ($field) {
922                     case 'dtend':
923                     case 'dtstart':
924                         $value = new Tinebase_DateTime((string)$xmlData->$fieldName);
925                         break;
926                         
927                     default:
928                         $value = (string)$xmlData->$fieldName;
929                         break;
930                         
931                 }
932                 $filterArray[] = array(
933                     'field'     => $field,
934                     'operator'  => 'equals',
935                     'value'     => $value
936                 );
937             }
938         }
939         
940         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " filterData " . print_r($filterArray, true));
941         
942         return $filterArray;
943     }
944     
945     /**
946      * return contentfilter array
947      * 
948      * @param  int $_filterType
949      * @return Tinebase_Model_Filter_FilterGroup
950      */
951     protected function _getContentFilter($_filterType)
952     {
953         $filter = parent::_getContentFilter($_filterType);
954         
955         // no persistent filter set -> add default filter
956         // NOTE: we use attender+status as devices always show declined events
957         if ($filter->isEmpty()) {
958             $attendeeFilter = $filter->createFilter('attender', 'equals', array(
959                 'user_type'    => Calendar_Model_Attender::USERTYPE_USER,
960                 'user_id'      => Tinebase_Core::getUser()->contact_id,
961             ));
962             $statusFilter = $filter->createFilter('attender_status', 'notin', array(
963                 Calendar_Model_Attender::STATUS_DECLINED
964             ));
965             $containerFilter = $filter->createFilter('container_id', 'equals', array(
966                 'path' => '/personal/' . Tinebase_Core::getUser()->getId()
967             ));
968             
969             $filter->addFilter($attendeeFilter);
970             $filter->addFilter($statusFilter);
971             $filter->addFilter($containerFilter);
972         }
973         
974         if (in_array($_filterType, $this->_filterArray)) {
975             switch($_filterType) {
976                 case Syncroton_Command_Sync::FILTER_2_WEEKS_BACK:
977                     $from = Tinebase_DateTime::now()->subWeek(2);
978                     break;
979                 case Syncroton_Command_Sync::FILTER_1_MONTH_BACK:
980                     $from = Tinebase_DateTime::now()->subMonth(2);
981                     break;
982                 case Syncroton_Command_Sync::FILTER_3_MONTHS_BACK:
983                     $from = Tinebase_DateTime::now()->subMonth(3);
984                     break;
985                 case Syncroton_Command_Sync::FILTER_6_MONTHS_BACK:
986                     $from = Tinebase_DateTime::now()->subMonth(6);
987                     break;
988             }
989         } else {
990             // don't return more than the previous 6 months
991             $from = Tinebase_DateTime::now()->subMonth(6);
992         }
993         
994         // next 10 years
995         $to = Tinebase_DateTime::now()->addYear(10);
996         
997         // remove all 'old' period filters
998         $filter->removeFilter('period');
999         
1000         // add period filter
1001         $filter->addFilter(new Calendar_Model_PeriodFilter('period', 'within', array(
1002             'from'  => $from,
1003             'until' => $to
1004         )));
1005         
1006         return $filter;
1007     }
1008
1009     /**
1010      * 
1011      * @return int     Syncroton_Command_Sync::FILTER...
1012      */
1013     public function getMaxFilterType()
1014     {
1015         return ActiveSync_Config::getInstance()->get(ActiveSync_Config::MAX_FILTER_TYPE_CALENDAR);
1016     }
1017
1018     /**
1019      * NOTE: calendarFilter is based on contentFilter for ActiveSync
1020      *
1021      * @param $folderId
1022      */
1023     protected function _assertContentControllerParams($folderId)
1024     {
1025         try {
1026             $container = Tinebase_Container::getInstance()->getContainerById($folderId);
1027         } catch (Exception $e) {
1028             $containerId = Tinebase_Core::getPreference('ActiveSync')->{$this->_defaultFolder};
1029             $container = Tinebase_Container::getInstance()->getContainerById($containerId);
1030         }
1031         Calendar_Controller_MSEventFacade::getInstance()->assertEventFacadeParams($container, false);
1032     }
1033 }