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