0010058: vevent with lots of exdates leads to alarm saving failure
[tine20] / tine20 / Calendar / Controller / MSEventFacade.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Calendar
6  * @subpackage  Controller
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Cornelius Weiss <c.weiss@metaways.de>
9  * @copyright   Copyright (c) 2010 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * Facade for Calendar_Controller_Event
14  * 
15  * Adopts Tine 2.0 internal event representation to the iTIP (RFC 5546) representations
16  * 
17  * In iTIP event exceptions are tranfered together/supplement with/to their baseEvents.
18  * So with this facade event exceptions are part of the baseEvent and stored in their exdate property:
19  * -> Tinebase_Record_RecordSet Calendar_Model_Event::exdate
20  * 
21  * deleted recur event instances (fall outs) have the property:
22  * -> Calendar_Model_Event::is_deleted set to TRUE (MSEvents)
23  * 
24  * when creating/updating events, make sure to have the original start time (ExceptionStartTime)
25  * of recur event instances stored in the property:
26  * -> Calendar_Model_Event::recurid
27  * 
28  * In iTIP Event handling is based on the perspective of a certain user. This user is the 
29  * current user per default, but can be switched with
30  * Calendar_Controller_MSEventFacade::setCalendarUser(Calendar_Model_Attender $_calUser)
31  * 
32  * @package     Calendar
33  * @subpackage  Controller
34  */
35 class Calendar_Controller_MSEventFacade implements Tinebase_Controller_Record_Interface
36 {
37     /**
38      * @var Calendar_Controller_Event
39      */
40     protected $_eventController = NULL;
41     
42     /**
43      * @var Calendar_Model_Attender
44      */
45     protected $_calendarUser = NULL;
46     
47     /**
48      * @var Calendar_Model_EventFilter
49      */
50     protected $_eventFilter = NULL;
51     
52     /**
53      * @var Calendar_Controller_MSEventFacade
54      */
55     private static $_instance = NULL;
56     
57     /**
58      * the constructor
59      *
60      * don't use the constructor. use the singleton 
61      */
62     private function __construct() {
63         $this->_eventController = Calendar_Controller_Event::getInstance();
64         
65         // set default CU
66         $this->setCalendarUser(new Calendar_Model_Attender(array(
67             'user_type' => Calendar_Model_Attender::USERTYPE_USER,
68             'user_id'   => Tinebase_Core::getUser()->contact_id
69         )));
70     }
71
72     /**
73      * don't clone. Use the singleton.
74      */
75     private function __clone() 
76     {
77         
78     }
79     
80     /**
81      * singleton
82      *
83      * @return Calendar_Controller_MSEventFacade
84      */
85     public static function getInstance() 
86     {
87         if (self::$_instance === NULL) {
88             self::$_instance = new Calendar_Controller_MSEventFacade();
89         }
90         return self::$_instance;
91     }
92     
93     /**
94      * get by id
95      *
96      * @param string $_id
97      * @return Calendar_Model_Event
98      * @throws  Tinebase_Exception_AccessDenied
99      */
100     public function get($_id)
101     {
102         $event = $this->_eventController->get($_id);
103         
104         return $this->_toiTIP($event);
105     }
106     
107     /**
108      * Returns a set of events identified by their id's
109      * 
110      * @param   array array of record identifiers
111      * @return  Tinebase_Record_RecordSet of Calendar_Model_Event
112      */
113     public function getMultiple($_ids)
114     {
115         $events = $this->_eventController->getMultiple($_ids);
116         
117         return $this->_toiTIP($events);
118     }  
119     
120     /**
121      * Gets all entries
122      *
123      * @param string $_orderBy Order result by
124      * @param string $_orderDirection Order direction - allowed are ASC and DESC
125      * @throws Tinebase_Exception_InvalidArgument
126      * @return Tinebase_Record_RecordSet of Calendar_Model_Event
127      */
128     public function getAll($_orderBy = 'id', $_orderDirection = 'ASC')
129     {
130         $events = $this->_eventController->getAll($_orderBy, $_orderDirection);
131         
132         return $this->_toiTIP($events);
133     }
134     
135     /**
136      * get list of records
137      *
138      * @param Tinebase_Model_Filter_FilterGroup|optional    $_filter
139      * @param Tinebase_Model_Pagination|optional            $_pagination
140      * @param bool                                          $_getRelations
141      * @param boolean                                       $_onlyIds
142      * @param string                                        $_action for right/acl check
143      * @return Tinebase_Record_RecordSet|array
144      */
145     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
146     {
147         $eventIds = $this->_getEventIds($_filter, $_action);
148         
149         if ($_pagination instanceof Tinebase_Model_Pagination) {
150             $numEvents = count($eventIds);
151             
152             $offset = min($_pagination->start, $numEvents);
153             $length = min($_pagination->limit, $offset+$numEvents);
154             
155             $eventIds = array_slice($eventIds, $offset, $length);
156         }
157         
158         if (! $_onlyIds) {
159             
160             $events =  $this->_eventController->search(new Calendar_Model_EventFilter(array(
161                 array('field' => 'id', 'operator' => 'in', 'value' => $eventIds)
162             )), NULL, FALSE, FALSE, $_action);
163             
164             // NOTE: it would be correct to wrap this with the search filter, BUT
165             //       this breaks webdasv as it fetches its events with a search id OR uid.
166             //       ActiveSync sets its syncfilter generically so it's not problem either
167 //             $oldFilter = $this->setEventFilter($_filter);
168             $events = $this->_toiTIP($events);
169 //             $this->setEventFilter($oldFilter);
170         }
171         
172         return $_onlyIds ? $eventIds : $events;
173     }
174     
175     /**
176      * Gets total count of search with $_filter
177      * 
178      * NOTE: we don't count exceptions where the user has no access to base event here
179      *       so the result might not be precise
180      *       
181      * @param Tinebase_Model_Filter_FilterGroup $_filter
182      * @param string $_action for right/acl check
183      * @return int
184      */
185     public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get') 
186     {
187         $eventIds = $this->_getEventIds($_filter, $_action);
188         
189         return count ($eventIds);
190     }
191     
192     /**
193      * fetches all eventids for given filter
194      * 
195      * @param Tinebase_Model_Filter_FilterGroup $_filter
196      * @param string                            $action
197      */
198     protected function _getEventIds($_filter, $_action)
199     {
200         if (! $_filter instanceof Calendar_Model_EventFilter) {
201             $_filter = new Calendar_Model_EventFilter();
202         }
203         
204         $recurIdFilter = new Tinebase_Model_Filter_Text('recurid', 'isnull', null);
205         $_filter->addFilter($recurIdFilter);
206         $baseEventIds = $this->_eventController->search($_filter, NULL, FALSE, TRUE, $_action);
207         $_filter->removeFilter($recurIdFilter);
208
209         $baseEventUIDs =  $this->_eventController->search(new Calendar_Model_EventFilter(array(
210             array('field' => 'id', 'operator' => 'in', 'value' => $baseEventIds)
211         )), NULL, FALSE, 'uid', $_action);
212         
213         // add exceptions where the user has no access to the base event as baseEvents
214         $uidFilter = new Tinebase_Model_Filter_Text('uid', 'notin', $baseEventUIDs);
215         $recurIdFilter = new Tinebase_Model_Filter_Text('recurid', 'notnull', null);
216         $_filter->addFilter($uidFilter);
217         $_filter->addFilter($recurIdFilter);
218         $baselessExceptionIds = $this->_eventController->search($_filter, NULL, FALSE, TRUE, $_action);
219         $_filter->removeFilter($uidFilter);
220         $_filter->removeFilter($recurIdFilter);
221         
222         return array_unique(array_merge($baseEventIds, $baselessExceptionIds));
223     }
224     
225    /**
226      * (non-PHPdoc)
227      * @see Calendar_Controller_Event::lookupExistingEvent()
228      */
229     public function lookupExistingEvent($_event)
230     {
231         $event = $this->_eventController->lookupExistingEvent($_event);
232         
233         return $event? $this->_toiTIP($event) : NULL;
234     }
235     
236     /*************** add / update / delete *****************/    
237
238     /**
239      * add one record
240      *
241      * @param   Calendar_Model_Event $_event
242      * @return  Calendar_Model_Event
243      * @throws  Tinebase_Exception_AccessDenied
244      * @throws  Tinebase_Exception_Record_Validation
245      */
246     public function create(Tinebase_Record_Interface $_event)
247     {
248         if ($_event->recurid) {
249             throw new Tinebase_Exception_UnexpectedValue('recur event instances must be saved as part of the base event');
250         }
251         
252         $this->_fromiTIP($_event, new Calendar_Model_Event(array(), TRUE));
253         
254         $exceptions = $_event->exdate;
255         $_event->exdate = NULL;
256         
257         $_event->assertCurrentUserAsAttendee();
258         $savedEvent = $this->_eventController->create($_event);
259         
260         if ($exceptions instanceof Tinebase_Record_RecordSet) {
261             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
262                 . ' About to create ' . count($exceptions) . ' exdates for event ' . $_event->summary . ' (' . $_event->dtstart . ')');
263             
264             foreach ($exceptions as $exception) {
265                 $exception->assertCurrentUserAsAttendee();
266                 $this->_prepareException($savedEvent, $exception);
267                 $this->_eventController->createRecurException($exception, !!$exception->is_deleted);
268             }
269         }
270         
271         return $this->_toiTIP($savedEvent);
272     }
273     
274     /**
275      * update one record
276      * 
277      * NOTE: clients might send their original (creation) data w.o. our adoptions for update
278      *       therefore we need reapply them
279      *       
280      * @param   Calendar_Model_Event $_event
281      * @param   bool                 $_checkBusyConflicts
282      * @return  Calendar_Model_Event
283      * @throws  Tinebase_Exception_AccessDenied
284      * @throws  Tinebase_Exception_Record_Validation
285      */
286     public function update(Tinebase_Record_Interface $_event, $_checkBusyConflicts = FALSE)
287     {
288         if ($_event->recurid) {
289             throw new Tinebase_Exception_UnexpectedValue('recur event instances must be saved as part of the base event');
290         }
291         $currentOriginEvent = $this->_eventController->get($_event->getId());
292         $this->_fromiTIP($_event, $currentOriginEvent);
293         
294         $_event->assertCurrentUserAsAttendee(TRUE, TRUE);
295         
296         $exceptions = $_event->exdate instanceof Tinebase_Record_RecordSet ? $_event->exdate : new Tinebase_Record_RecordSet('Calendar_Model_Event');
297         $exceptions->addIndices(array('is_deleted'));
298         
299         $currentPersistentExceptions = $_event->rrule ? $this->_eventController->getRecurExceptions($_event, FALSE) : new Tinebase_Record_RecordSet('Calendar_Model_Event');
300         $newPersistentExceptions = $exceptions->filter('is_deleted', 0);
301         
302         $migration = $this->_getExceptionsMigration($currentPersistentExceptions, $newPersistentExceptions);
303         
304         $this->_eventController->delete($migration['toDelete']->getId());
305         
306         
307         // NOTE: we need to exclude the toCreate exdates here to not confuse computations in createRecurException!
308         $_event->exdate = array_diff($exceptions->getOriginalDtStart(), $migration['toCreate']->getOriginalDtStart());
309         $updatedBaseEvent = $this->_eventController->update($_event, $_checkBusyConflicts);
310         
311         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
312             . ' Found ' . count($migration['toCreate']) . ' exceptions to create and ' . count($migration['toUpdate']) . ' to update.');
313         
314         foreach ($migration['toCreate'] as $exception) {
315             $exception->assertCurrentUserAsAttendee(TRUE, TRUE);
316             $this->_prepareException($updatedBaseEvent, $exception);
317             $this->_eventController->createRecurException($exception, !!$exception->is_deleted);
318         }
319         
320         foreach ($migration['toUpdate'] as $exception) {
321             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' '
322                 . ' Update exdate ' . $exception->getId() . ' at ' . $exception->dtstart->toString());
323             
324             $exception->assertCurrentUserAsAttendee(TRUE, TRUE);
325             $this->_prepareException($updatedBaseEvent, $exception);
326             $this->_addStatusAuthkeyForOwnAttender($exception);
327             
328             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
329                 . ' Updating exception: ' . print_r($exception->toArray(), TRUE));
330             $this->_eventController->update($exception, $_checkBusyConflicts);
331         }
332         
333         // NOTE: we need to refetch here, otherwise eTag fail's as exception updates change baseEvents seq
334         return $this->get($updatedBaseEvent->getId());
335     }
336     
337     /**
338      * add status_authkey for own attender
339      * 
340      * @param Calendar_Model_Event $event
341      */
342     protected function _addStatusAuthkeyForOwnAttender($event)
343     {
344         if (! $event->attendee instanceof Tinebase_Record_RecordSet) {
345             return;
346         }
347         $ownAttender = Calendar_Model_Attender::getOwnAttender($event->attendee);
348         if ($ownAttender) {
349             $currentEvent = $this->_eventController->get($event->id);
350             $currentAttender = Calendar_Model_Attender::getAttendee($currentEvent->attendee, $ownAttender);
351             $ownAttender->status_authkey = $currentAttender->status_authkey;
352         }
353     }
354     
355     /**
356      * updates an attender status of a event
357      *
358      * @param  Calendar_Model_Event    $_event
359      * @param  Calendar_Model_Attender $_attendee
360      * @return Calendar_Model_Event    updated event
361      */
362     public function attenderStatusUpdate($_event, $_attendee)
363     {
364         if ($_event->recurid) {
365             throw new Tinebase_Exception_UnexpectedValue('recur event instances must be saved as part of the base event');
366         }
367         
368         $exceptions = $_event->exdate instanceof Tinebase_Record_RecordSet ? $_event->exdate : new Tinebase_Record_RecordSet('Calendar_Model_Event');
369         $_event->exdate = $exceptions->getOriginalDtStart();
370         
371         // update base event status
372         $attendeeFound = Calendar_Model_Attender::getAttendee($_event->attendee, $_attendee);
373         if (!isset($attendeeFound)) {
374             throw new Tinebase_Exception_UnexpectedValue('not an attendee');
375         }
376         $attendeeFound->displaycontainer_id = $_attendee->displaycontainer_id;
377         Calendar_Controller_Event::getInstance()->attenderStatusUpdate($_event, $attendeeFound, $attendeeFound->status_authkey);
378         
379         // update exceptions
380         foreach($exceptions as $exception) {
381             // do not attempt to set status of an deleted instance
382             if ($exception->is_deleted) continue;
383             
384             $exceptionAttendee = Calendar_Model_Attender::getAttendee($exception->attendee, $_attendee);
385             
386             if (! $exception->getId()) {
387                 if (! $exceptionAttendee) {
388                     // set user status to DECLINED
389                     $exceptionAttendee = clone $attendeeFound;
390                     $exceptionAttendee->status = Calendar_Model_Attender::STATUS_DECLINED;
391                 }
392                 $exceptionAttendee->displaycontainer_id = $_attendee->displaycontainer_id;
393                 Calendar_Controller_Event::getInstance()->attenderStatusCreateRecurException($exception, $exceptionAttendee, $exceptionAttendee->status_authkey);
394             } else {
395                 if (! $exceptionAttendee) {
396                     // we would need to find out the users authkey to decline him -> not allowed!?
397                     if (!isset($attendeeFound)) {
398                         throw new Tinebase_Exception_UnexpectedValue('not an attendee');
399                     }
400                 }
401                 $exceptionAttendee->displaycontainer_id = $_attendee->displaycontainer_id;
402                 Calendar_Controller_Event::getInstance()->attenderStatusUpdate($exception, $exceptionAttendee, $exceptionAttendee->status_authkey);
403             }
404         }
405         
406         return $this->get($_event->getId());
407     }
408     
409     /**
410      * update multiple records
411      * 
412      * @param   Tinebase_Model_Filter_FilterGroup $_filter
413      * @param   array $_data
414      * @return  integer number of updated records
415      */
416     public function updateMultiple($_what, $_data)
417     {
418         throw new Tinebase_Exception_NotImplemented('Calendar_Conroller_MSEventFacade::updateMultiple not yet implemented');
419     }
420     
421     /**
422      * Deletes a set of records.
423      * 
424      * If one of the records could not be deleted, no record is deleted
425      * 
426      * @param   array array of record identifiers
427      * @return  Tinebase_Record_RecordSet
428      */
429     public function delete($_ids)
430     {
431         $ids = array_unique((array)$_ids);
432         $events = $this->getMultiple($ids);
433         
434         foreach ($events as $event) {
435             if ($event->exdate !== null) {
436                 foreach ($event->exdate as $exception) {
437                     $exceptionId = $exception->getId();
438                     if ($exceptionId) {
439                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
440                             . ' Found exdate to be deleted (id: ' . $exceptionId . ')');
441                         $ids[] = $exceptionId;
442                     }
443                 }
444             }
445         }
446         
447         $this->_eventController->delete($ids);
448         return $events;
449     }
450     
451     /**
452      * get and resolve all alarms of given record(s)
453      * 
454      * @param  Tinebase_Record_Interface|Tinebase_Record_RecordSet $_record
455      */
456     public function getAlarms($_record)
457     {
458         $events = $_record instanceof Tinebase_Record_RecordSet ? $_record : new Tinebase_Record_RecordSet('Calendar_Model_Event', array($_record));
459         
460         foreach($events as $event) {
461             if ($event->exdate instanceof Tinebase_Record_RecordSet) {
462 //                 $event->exdate->addIndices(array('is_deleted'));
463                 $events->merge($event->exdate->filter('is_deleted', 0));
464             }
465         }
466         
467         $this->_eventController->getAlarms($events);
468     }
469     
470     /**
471      * set displaycontainer for given attendee 
472      * 
473      * @param Calendar_Model_Event    $_event
474      * @param string                  $_container
475      * @param Calendar_Model_Attender $_attendee    defaults to calendarUser
476      */
477     public function setDisplaycontainer($_event, $_container, $_attendee = NULL)
478     {
479         if ($_event->exdate instanceof Tinebase_Record_RecordSet) {
480             foreach ($_event->exdate as $idx => $exdate) {
481                 self::setDisplaycontainer($exdate, $_container, $_attendee);
482             }
483         }
484         
485         $attendeeRecord = Calendar_Model_Attender::getAttendee($_event->attendee, $_attendee ? $_attendee : $this->getCalendarUser());
486         
487         if ($attendeeRecord) {
488             $attendeeRecord->displaycontainer_id = $_container;
489         }
490     }
491     
492     /**
493      * sets current calendar user
494      * 
495      * @param Calendar_Model_Attender $_calUser
496      * @return Calendar_Model_Attender oldUser
497      */
498     public function setCalendarUser(Calendar_Model_Attender $_calUser)
499     {
500         $oldUser = $this->_calendarUser;
501         $this->_calendarUser = $_calUser;
502         
503         return $oldUser;
504     }
505     
506     /**
507      * get current calendar user
508      * 
509      * @return Calendar_Model_Attender
510      */
511     public function getCalendarUser()
512     {
513         return $this->_calendarUser;
514     }
515     
516     /**
517      * set current event filter for exdate computations
518      * 
519      * @param  Calendar_Model_EventFilter
520      * @return Calendar_Model_EventFilter
521      */
522     public function setEventFilter($_filter)
523     {
524         $oldFilter = $this->_eventFilter;
525         
526         if ($_filter !== NULL) {
527             if (! $_filter instanceof Calendar_Model_EventFilter) {
528                 throw new Tinebase_Exception_UnexpectedValue('not a valid filter');
529             }
530             $this->_eventFilter = clone $_filter;
531             
532             $periodFilters = $this->_eventFilter->getFilter('period', TRUE, TRUE);
533             foreach((array) $periodFilters as $periodFilter) {
534                 $periodFilter->setDisabled();
535             }
536         } else {
537             $this->_eventFilter = NULL;
538         }
539         
540         return $oldFilter;
541     }
542     
543     /**
544      * get current event filter
545      * 
546      * @return Calendar_Model_EventFilter
547      */
548     public function getEventFilter()
549     {
550         return $this->_eventFilter;
551     }
552     
553     /**
554      * filters given eventset for events with matching dtstart
555      * 
556      * @param Tinebase_Record_RecordSet $_events
557      * @param array                     $_dtstarts
558      */
559     protected function _filterEventsByDTStarts($_events, $_dtstarts)
560     {
561         $filteredSet = new Tinebase_Record_RecordSet('Calendar_Model_Event');
562         $allDTStarts = $_events->getOriginalDtStart();
563         
564         $existingIdxs = array_intersect($allDTStarts, $_dtstarts);
565         
566         foreach($existingIdxs as $idx => $dtstart) {
567             $filteredSet->addRecord($_events[$idx]);
568         }
569         
570         return $filteredSet;
571     }
572
573     /**
574      * converts a tine20 event to an iTIP event
575      * 
576      * @param  Calendar_Model_Event $_event
577      * @return Calendar_Model_Event 
578      */
579     protected function _toiTIP($_event)
580     {
581         if ($_event instanceof Tinebase_Record_RecordSet) {
582             foreach ($_event as $idx => $event) {
583                 try {
584                     $_event[$idx] = $this->_toiTIP($event);
585                 } catch (Tinebase_Exception_AccessDenied $ade) {
586                     // if we don't have permissions for the exdates, this is likely a freebusy info only -> remove from set
587                     $_event->removeRecord($event);
588                 } catch (Exception $e) {
589                     $event->exdate = new Tinebase_Record_RecordSet('Calendar_Model_Event');
590                 }
591             }
592             
593             return $_event;
594         }
595         
596         // get exdates
597         if ($_event->getId() && $_event->rrule) {
598             $_event->exdate = $this->_eventController->getRecurExceptions($_event, TRUE, $this->getEventFilter());
599             $this->getAlarms($_event);
600             
601             foreach ($_event->exdate as $exdate) {
602                 $this->_toiTIP($exdate);
603             }
604         }
605         
606         $this->_filterAttendeeWithoutEmail($_event);
607         
608         // get alarms for baseEvents w.o. exdate
609         if (! $_event->isRecurException() && ! $_event->exdate) {
610             $this->getAlarms($_event);
611         }
612         
613         $CUAttendee = Calendar_Model_Attender::getAttendee($_event->attendee, $this->_calendarUser);
614         $isOrganizer = $_event->isOrganizer($this->_calendarUser);
615         
616         // apply perspective
617         if ($CUAttendee && !$isOrganizer) {
618             $_event->transp = $CUAttendee->transp ? $CUAttendee->transp : $_event->transp;
619         }
620         
621         if ($_event->alarms instanceof Tinebase_Record_RecordSet) {
622             foreach($_event->alarms as $alarm) {
623                 if (! Calendar_Model_Attender::isAlarmForAttendee($this->_calendarUser, $alarm, $_event)) {
624                     $_event->alarms->removeRecord($alarm);
625                 }
626             }
627         }
628         
629         return $_event;
630     }
631     
632     /**
633      * filter out attendee w.o. email
634      * 
635      * @param Calendar_Model_Event $event
636      */
637     protected function _filterAttendeeWithoutEmail($event)
638     {
639         $this->_fillResolvedAttendeeCache($event);
640         
641         $filteredAttendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
642         foreach ($event->attendee->getEmail() as $idx => $email) {
643             if ($email) {
644                 $filteredAttendee->addRecord($event->attendee[$idx]);
645             }
646         }
647         $event->attendee = $filteredAttendee;
648     }
649
650     /**
651      * re add attendee w.o. email
652      * 
653      * @param Calendar_Model_Event $event
654      */
655     protected function _addAttendeeWithoutEmail($event, $currentEvent)
656     {
657         if (! $currentEvent->attendee instanceof Tinebase_Record_RecordSet) {
658             return;
659         }
660         
661         $this->_fillResolvedAttendeeCache($currentEvent);
662         
663         if (! $event->attendee instanceof Tinebase_Record_RecordSet) {
664             $event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
665         }
666         foreach ($currentEvent->attendee->getEmail() as $idx => $email) {
667             if (! $email) {
668                 $event->attendee->addRecord($currentEvent->attendee[$idx]);
669             }
670         }
671     }
672     
673     /**
674      * this fills the resolved attendee cache without changing the event attendee recordset
675      * 
676      * @param Calendar_Model_Event $event
677      */
678     protected function _fillResolvedAttendeeCache($event)
679     {
680         $attendeeClone = clone $event->attendee;
681         Calendar_Model_Attender::resolveAttendee($attendeeClone, FALSE);
682     }
683     
684     /**
685      * converts an iTIP event to a tine20 event
686      * 
687      * @param Calendar_Model_Event $_event
688      * @param Calendar_Model_Event $_currentEvent (not iTIP!)
689      */
690     protected function _fromiTIP($_event, $_currentEvent)
691     {
692         if (! $_event->rrule) {
693             $_event->exdate = NULL;
694         }
695         
696         if ($_event->exdate instanceof Tinebase_Record_RecordSet) {
697             
698             try{
699                 $currExdates = $this->_eventController->getRecurExceptions($_event, TRUE);
700                 $this->getAlarms($currExdates);
701                 $currClientExdates = $this->_eventController->getRecurExceptions($_event, TRUE, $this->getEventFilter());
702                 $this->getAlarms($currClientExdates);
703             } catch (Tinebase_Exception_NotFound $e) {
704                 $currExdates = NULL;
705                 $currClientExdates = NULL; 
706             }
707             
708             foreach ($_event->exdate as $idx => $exdate) {
709                 try {
710                     $this->_prepareException($_event, $exdate);
711                 } catch (Exception $e){}
712
713                 $currExdate = $currExdates instanceof Tinebase_Record_RecordSet ? $currExdates->filter('recurid', $exdate->recurid)->getFirstRecord() : NULL;
714                 
715                 
716                 if ($exdate->is_deleted) {
717                     // reset implicit filter fallouts and mark as don't touch (seq = -1)
718                     $currClientExdate = $currClientExdates instanceof Tinebase_Record_RecordSet ? $currClientExdates->filter('recurid', $exdate->recurid)->getFirstRecord() : NULL;
719                     if ($currClientExdate && $currClientExdate->is_deleted) {
720                         $_event->exdate[$idx] = $currExdate;
721                         $currExdate->seq = -1;
722                         continue;
723                     }
724                 }
725                 $this->_fromiTIP($exdate, $currExdate ? $currExdate : clone $_currentEvent);
726             }
727         }
728         
729         // assert organizer
730         $_event->organizer = $_event->organizer ?: ($_currentEvent->organizer ?: Tinebase_Core::getUser()->contact_id);
731         
732         $this->_addAttendeeWithoutEmail($_event, $_currentEvent);
733         
734         $CUAttendee = Calendar_Model_Attender::getAttendee($_event->attendee, $this->_calendarUser);
735         $currentCUAttendee  = Calendar_Model_Attender::getAttendee($_currentEvent->attendee, $this->_calendarUser);
736         $isOrganizer = $_event->isOrganizer($this->_calendarUser);
737         
738         // remove perspective 
739         if ($CUAttendee && !$isOrganizer) {
740             $CUAttendee->transp = $_event->transp;
741             $_event->transp = $_currentEvent->transp ? $_currentEvent->transp : $_event->transp;
742         }
743         
744         // apply changes to original alarms
745         $_currentEvent->alarms  = $_currentEvent->alarms instanceof Tinebase_Record_RecordSet ? $_currentEvent->alarms : new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
746         $_event->alarms  = $_event->alarms instanceof Tinebase_Record_RecordSet ? $_event->alarms : new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
747         
748         foreach($_currentEvent->alarms as $currentAlarm) {
749             if (Calendar_Model_Attender::isAlarmForAttendee($this->_calendarUser, $currentAlarm)) {
750                 $alarmUpdate = Calendar_Controller_Alarm::getMatchingAlarm($_event->alarms, $currentAlarm);
751                 
752                 if ($alarmUpdate) {
753                     // we could map the alarm => save ack & snooze options
754                     if ($dtAck = Calendar_Controller_Alarm::getAcknowledgeTime($alarmUpdate)) {
755                         Calendar_Controller_Alarm::setAcknowledgeTime($currentAlarm, $dtAck, $this->getCalendarUser()->user_id);
756                     }
757                     if ($dtSnooze = Calendar_Controller_Alarm::getSnoozeTime($alarmUpdate)) {
758                         Calendar_Controller_Alarm::setSnoozeTime($currentAlarm, $dtSnooze, $this->getCalendarUser()->user_id);
759                     }
760                     $_event->alarms->removeRecord($alarmUpdate);
761                 } else {
762                     // alarm is to be skiped/deleted
763                     if (! $currentAlarm->getOption('attendee')) {
764                         Calendar_Controller_Alarm::skipAlarm($currentAlarm, $this->_calendarUser);
765                     } else {
766                         $_currentEvent->alarms->removeRecord($currentAlarm);
767                     }
768                 }
769             }
770         }
771         if (! $isOrganizer) {
772             $_event->alarms->setOption('attendee', Calendar_Controller_Alarm::attendeeToOption($this->_calendarUser));
773         }
774         $_event->alarms->merge($_currentEvent->alarms);
775         
776         // in MS world only cal_user can do status updates
777         if ($CUAttendee) {
778             $CUAttendee->status_authkey = $currentCUAttendee ? $currentCUAttendee->status_authkey : NULL;
779         }
780     }
781     
782     /**
783      * computes an returns the migration for event exceptions
784      * 
785      * @param Tinebase_Record_RecordSet $_currentPersistentExceptions
786      * @param Tinebase_Record_RecordSet $_newPersistentExceptions
787      */
788     protected function _getExceptionsMigration($_currentPersistentExceptions, $_newPersistentExceptions)
789     {
790         $migration = array();
791         
792         // add indices and sort to speedup things
793         $_currentPersistentExceptions->addIndices(array('dtstart'))->sort('dtstart');
794         $_newPersistentExceptions->addIndices(array('dtstart'))->sort('dtstart');
795         
796         // get dtstarts
797         $currDtStart = $_currentPersistentExceptions->getOriginalDtStart();
798         $newDtStart = $_newPersistentExceptions->getOriginalDtStart();
799         
800         // compute migration in terms of dtstart
801         $toDeleteDtStart = array_diff($currDtStart, $newDtStart);
802         $toCreateDtStart = array_diff($newDtStart, $currDtStart);
803         $toUpdateDtSTart = array_intersect($currDtStart, $newDtStart);
804         
805         $migration['toDelete'] = $this->_filterEventsByDTStarts($_currentPersistentExceptions, $toDeleteDtStart);
806         $migration['toCreate'] = $this->_filterEventsByDTStarts($_newPersistentExceptions, $toCreateDtStart);
807         $migration['toUpdate'] = $this->_filterEventsByDTStarts($_newPersistentExceptions, $toUpdateDtSTart);
808         
809         // get ids for toUpdate
810         $idxIdMap = $this->_filterEventsByDTStarts($_currentPersistentExceptions, $toUpdateDtSTart)->getId();
811         try {
812             $migration['toUpdate']->setByIndices('id', $idxIdMap);
813         } catch (Tinebase_Exception_Record_NotDefined $ternd) {
814             // some debugging for 0008182: event with lots of exceptions breaks calendar sync
815             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($idxIdMap, TRUE));
816             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($migration['toUpdate']->toArray(), TRUE));
817             throw $ternd;
818         }
819         
820         // filter exceptions marked as don't touch 
821         foreach ($migration['toUpdate'] as $toUpdate) {
822             if ($toUpdate->seq === -1) {
823                 $migration['toUpdate']->removeRecord($toUpdate);
824             }
825         }
826         
827         return $migration;
828     }
829     
830     /**
831      * prepares an exception instance for persistence
832      * 
833      * @param  Calendar_Model_Event $_baseEvent
834      * @param  Calendar_Model_Event $_exception
835      * @return void
836      * @throws Tinebase_Exception_InvalidArgument
837      */
838     protected function _prepareException(Calendar_Model_Event $_baseEvent, Calendar_Model_Event $_exception)
839     {
840         if (! $_baseEvent->uid) {
841             throw new Tinebase_Exception_InvalidArgument('base event has no uid');
842         }
843         
844         if ($_exception->is_deleted == false) {
845             $_exception->container_id = $_baseEvent->container_id;
846         }
847         $_exception->uid = $_baseEvent->uid;
848         $_exception->recurid = $_baseEvent->uid . '-' . $_exception->getOriginalDtStart()->format(Tinebase_Record_Abstract::ISO8601LONG);
849         
850         // NOTE: we always refetch the base event as it might be touched in the meantime
851         $currBaseEvent = $this->_eventController->get($_baseEvent, null, false);
852         $_exception->last_modified_time = $currBaseEvent->last_modified_time;
853     }
854 }