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