Merge branch '2014.11-develop' into 2015.07
[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     protected static $_attendeeEmailCache = array();
58     
59     /**
60      * the constructor
61      *
62      * don't use the constructor. use the singleton 
63      */
64     private function __construct()
65     {
66         $this->_eventController = Calendar_Controller_Event::getInstance();
67         
68         // set default CU
69         $this->setCalendarUser(new Calendar_Model_Attender(array(
70             'user_type' => Calendar_Model_Attender::USERTYPE_USER,
71             'user_id'   => self::getCurrentUserContactId()
72         )));
73     }
74
75     /**
76      * don't clone. Use the singleton.
77      */
78     private function __clone() 
79     {
80         
81     }
82     
83     /**
84      * singleton
85      *
86      * @return Calendar_Controller_MSEventFacade
87      */
88     public static function getInstance() 
89     {
90         if (self::$_instance === NULL) {
91             self::$_instance = new Calendar_Controller_MSEventFacade();
92         }
93         return self::$_instance;
94     }
95     
96     /**
97      * get user contact id
98      * - NOTE: creates a new user contact on the fly if it did not exist before
99      * 
100      * @return string
101      */
102     public static function getCurrentUserContactId()
103     {
104         if (empty(Tinebase_Core::getUser()->contact_id)) {
105             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
106             . ' Creating user contact for ' . Tinebase_Core::getUser()->accountDisplayName . ' on the fly ...');
107             $contact = Admin_Controller_User::getInstance()->createOrUpdateContact(Tinebase_Core::getUser());
108             Tinebase_Core::getUser()->contact_id = $contact->getId();
109             Tinebase_User::getInstance()->updateUserInSqlBackend(Tinebase_Core::getUser());
110         }
111         
112         return Tinebase_Core::getUser()->contact_id;
113     }
114     
115     /**
116      * get by id
117      *
118      * @param string $_id
119      * @return Calendar_Model_Event
120      * @throws  Tinebase_Exception_AccessDenied
121      */
122     public function get($_id)
123     {
124         $event = $this->_eventController->get($_id);
125         $this->_resolveData($event);
126         
127         return $this->_toiTIP($event);
128     }
129     
130     /**
131      * Returns a set of events identified by their id's
132      * 
133      * @param   array array of record identifiers
134      * @return  Tinebase_Record_RecordSet of Calendar_Model_Event
135      */
136     public function getMultiple($_ids)
137     {
138         $filter = new Calendar_Model_EventFilter(array(
139             array('field' => 'id', 'operator' => 'in', 'value' => $_ids)
140         ));
141         return $this->search($filter);
142     }
143     
144     /**
145      * Gets all entries
146      *
147      * @param string $_orderBy Order result by
148      * @param string $_orderDirection Order direction - allowed are ASC and DESC
149      * @throws Tinebase_Exception_InvalidArgument
150      * @return Tinebase_Record_RecordSet of Calendar_Model_Event
151      */
152     public function getAll($_orderBy = 'id', $_orderDirection = 'ASC')
153     {
154         $filter = new Calendar_Model_EventFilter();
155         $pagination = new Tinebase_Model_Pagination(array(
156             'sort' => $_orderBy,
157             'dir'  => $_orderDirection
158         ));
159         return $this->search($filter, $pagination);
160     }
161     
162     /**
163      * get list of records
164      *
165      * @param Tinebase_Model_Filter_FilterGroup|optional    $_filter
166      * @param Tinebase_Model_Pagination|optional            $_pagination
167      * @param bool                                          $_getRelations
168      * @param boolean                                       $_onlyIds
169      * @param string                                        $_action for right/acl check
170      * @return Tinebase_Record_RecordSet|array
171      */
172     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
173     {
174         $events = $this->_getEvents($_filter, $_action);
175
176         if ($_pagination instanceof Tinebase_Model_Pagination && ($_pagination->start || $_pagination->limit) ) {
177             $eventIds = $events->id;
178             $numEvents = count($eventIds);
179             
180             $offset = min($_pagination->start, $numEvents);
181             $length = min($_pagination->limit, $offset+$numEvents);
182             
183             $eventIds = array_slice($eventIds, $offset, $length);
184             $eventSlice = new Tinebase_Record_RecordSet('Calendar_Model_Event');
185             foreach($eventIds as $eventId) {
186                 $eventSlice->addRecord($events->getById($eventId));
187             }
188             $events = $eventSlice;
189         }
190         
191         if (! $_onlyIds) {
192             // NOTE: it would be correct to wrap this with the search filter, BUT
193             //       this breaks webdasv as it fetches its events with a search id OR uid.
194             //       ActiveSync sets its syncfilter generically so it's not problem either
195 //             $oldFilter = $this->setEventFilter($_filter);
196             $events = $this->_toiTIP($events);
197 //             $this->setEventFilter($oldFilter);
198         }
199         
200         return $_onlyIds ? $events->id : $events;
201     }
202     
203     /**
204      * Gets total count of search with $_filter
205      * 
206      * NOTE: we don't count exceptions where the user has no access to base event here
207      *       so the result might not be precise
208      *       
209      * @param Tinebase_Model_Filter_FilterGroup $_filter
210      * @param string $_action for right/acl check
211      * @return int
212      */
213     public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get') 
214     {
215         $eventIds = $this->_getEvents($_filter, $_action);
216         
217         return count ($eventIds);
218     }
219     
220     /**
221      * fetches all events and sorts exceptions into exdate prop for given filter
222      * 
223      * @param Tinebase_Model_Filter_FilterGroup $_filter
224      * @param string                            $action
225      */
226     protected function _getEvents($_filter, $_action)
227     {
228         if (! $_filter instanceof Calendar_Model_EventFilter) {
229             $_filter = new Calendar_Model_EventFilter();
230         }
231
232         $events = $this->_eventController->search($_filter, NULL, FALSE, FALSE, $_action);
233
234         // if an id filter is set, we need to fetch exceptions in a second query
235         if ($_filter->getFilter('id', true, true)) {
236             $events->merge($this->_eventController->search(new Calendar_Model_EventFilter(array(
237                 array('field' => 'base_event_id', 'operator' => 'in',      'value' => $events->id),
238                 array('field' => 'id',            'operator' => 'notin',   'value' => $events->id),
239                 array('field' => 'recurid',       'operator' => 'notnull', 'value' => null),
240             )), NULL, FALSE, FALSE, $_action));
241         }
242
243         $this->_eventController->getAlarms($events);
244         Tinebase_FileSystem_RecordAttachments::getInstance()->getMultipleAttachmentsOfRecords($events);
245
246         $baseEventMap = array(); // id => baseEvent
247         $exceptionSets = array(); // id => exceptions
248         $exceptionMap = array(); // idx => event
249
250         foreach($events as $event) {
251             if ($event->rrule) {
252                 $eventId = $event->id;
253                 $baseEventMap[$eventId] = $event;
254                 $exceptionSets[$eventId] = new Tinebase_Record_RecordSet('Calendar_Model_Event');
255             } else if ($event->recurid) {
256                 $exceptionMap[] = $event;
257             }
258         }
259
260         foreach($exceptionMap as $exception) {
261             $baseEventId = $exception->base_event_id;
262             $baseEvent = array_key_exists($baseEventId, $baseEventMap) ? $baseEventMap[$baseEventId] : false;
263             if ($baseEvent) {
264                 $exceptionSet = $exceptionSets[$baseEventId];
265                 $exceptionSet->addRecord($exception);
266                 $events->removeRecord($exception);
267             }
268         }
269
270         foreach($baseEventMap as $id => $baseEvent) {
271             $exceptionSet = $exceptionSets[$id];
272             $this->_eventController->fakeDeletedExceptions($baseEvent, $exceptionSet);
273             $baseEvent->exdate = $exceptionSet;
274         }
275
276         return $events;
277     }
278
279     /*************** add / update / delete *****************/    
280
281     /**
282      * add one record
283      *
284      * @param   Calendar_Model_Event $_event
285      * @return  Calendar_Model_Event
286      * @throws  Tinebase_Exception_AccessDenied
287      * @throws  Tinebase_Exception_Record_Validation
288      */
289     public function create(Tinebase_Record_Interface $_event)
290     {
291         if ($_event->recurid) {
292             throw new Tinebase_Exception_UnexpectedValue('recur event instances must be saved as part of the base event');
293         }
294         
295         $this->_fromiTIP($_event, new Calendar_Model_Event(array(), TRUE));
296         
297         $exceptions = $_event->exdate;
298         $_event->exdate = NULL;
299         
300         $_event->assertAttendee($this->getCalendarUser());
301         $savedEvent = $this->_eventController->create($_event);
302         
303         if ($exceptions instanceof Tinebase_Record_RecordSet) {
304             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
305                 . ' About to create ' . count($exceptions) . ' exdates for event ' . $_event->summary . ' (' . $_event->dtstart . ')');
306             
307             foreach ($exceptions as $exception) {
308                 $exception->assertAttendee($this->getCalendarUser());
309                 $this->_prepareException($savedEvent, $exception);
310                 $this->_eventController->createRecurException($exception, !!$exception->is_deleted);
311             }
312         }
313
314         // NOTE: exdate creation changes baseEvent, so we need to refetch it here
315         return $this->get($savedEvent->getId());
316     }
317     
318     /**
319      * update one record
320      * 
321      * NOTE: clients might send their original (creation) data w.o. our adoptions for update
322      *       therefore we need reapply them
323      *       
324      * @param   Calendar_Model_Event $_event
325      * @param   bool                 $_checkBusyConflicts
326      * @return  Calendar_Model_Event
327      * @throws  Tinebase_Exception_AccessDenied
328      * @throws  Tinebase_Exception_Record_Validation
329      */
330     public function update(Tinebase_Record_Interface $_event, $_checkBusyConflicts = FALSE)
331     {
332         if ($_event->recurid) {
333             throw new Tinebase_Exception_UnexpectedValue('recur event instances must be saved as part of the base event');
334         }
335         $currentOriginEvent = $this->_eventController->get($_event->getId());
336         $this->_fromiTIP($_event, $currentOriginEvent);
337         
338         $_event->assertAttendee($this->getCalendarUser());
339         
340         $exceptions = $_event->exdate instanceof Tinebase_Record_RecordSet ? $_event->exdate : new Tinebase_Record_RecordSet('Calendar_Model_Event');
341         $exceptions->addIndices(array('is_deleted'));
342         
343         $currentPersistentExceptions = $_event->rrule ? $this->_eventController->getRecurExceptions($_event, FALSE) : new Tinebase_Record_RecordSet('Calendar_Model_Event');
344         $newPersistentExceptions = $exceptions->filter('is_deleted', 0);
345         
346         $migration = $this->_getExceptionsMigration($currentPersistentExceptions, $newPersistentExceptions);
347
348         $this->_eventController->delete($migration['toDelete']->getId());
349         
350         // NOTE: we need to exclude the toCreate exdates here to not confuse computations in createRecurException!
351         $_event->exdate = array_diff($exceptions->getOriginalDtStart(), $migration['toCreate']->getOriginalDtStart());
352         $updatedBaseEvent = $this->_eventController->update($_event, $_checkBusyConflicts);
353         
354         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
355             . ' Found ' . count($migration['toCreate']) . ' exceptions to create and ' . count($migration['toUpdate']) . ' to update.');
356         
357         foreach ($migration['toCreate'] as $exception) {
358             $exception->assertAttendee($this->getCalendarUser());
359             $this->_prepareException($updatedBaseEvent, $exception);
360             $this->_eventController->createRecurException($exception, !!$exception->is_deleted);
361         }
362
363         $updatedExceptions = array();
364         foreach ($migration['toUpdate'] as $exception) {
365
366             if (in_array($exception->getId(),$updatedExceptions )) {
367                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' '
368                     . ' Exdate ' . $exception->getId() . ' already updated');
369                 continue;
370             }
371
372             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' '
373                 . ' Update exdate ' . $exception->getId() . ' at ' . $exception->dtstart->toString());
374             
375             $exception->assertAttendee($this->getCalendarUser());
376             $this->_prepareException($updatedBaseEvent, $exception);
377             $this->_addStatusAuthkeyForOwnAttender($exception);
378             
379             // skip concurrency check here by setting the seq of the current record
380             $currentException = $currentPersistentExceptions->getById($exception->getId());
381             $exception->seq = $currentException->seq;
382             
383             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
384                 . ' Updating exception: ' . print_r($exception->toArray(), TRUE));
385             $this->_eventController->update($exception, $_checkBusyConflicts);
386             $updatedExceptions[] = $exception->getId();
387         }
388         
389         // NOTE: we need to refetch here, otherwise eTag fail's as exception updates change baseEvents seq
390         return $this->get($updatedBaseEvent->getId());
391     }
392     
393     /**
394      * add status_authkey for own attender
395      * 
396      * @param Calendar_Model_Event $event
397      */
398     protected function _addStatusAuthkeyForOwnAttender($event)
399     {
400         if (! $event->attendee instanceof Tinebase_Record_RecordSet) {
401             return;
402         }
403         $ownAttender = Calendar_Model_Attender::getOwnAttender($event->attendee);
404         if ($ownAttender) {
405             $currentEvent = $this->_eventController->get($event->id);
406             $currentAttender = Calendar_Model_Attender::getAttendee($currentEvent->attendee, $ownAttender);
407             $ownAttender->status_authkey = $currentAttender->status_authkey;
408         }
409     }
410     
411     protected $_currentEventFacadeContainer;
412     
413     /**
414      * asserts correct event filter and calendar user in MSEventFacade
415      * 
416      * NOTE: this is nessesary as MSEventFacade is a singleton and in some operations (e.g. move) there are 
417      *       multiple instances of self
418      */
419     public function assertEventFacadeParams(Tinebase_Model_Container $container, $setEventFilter=true)
420     {
421         if (!$this->_currentEventFacadeContainer ||
422              $this->_currentEventFacadeContainer->getId() !== $container->getId()
423         ) {
424             $this->_currentEventFacadeContainer = $container;
425
426             try {
427                 $calendarUserId = $container->type == Tinebase_Model_Container::TYPE_PERSONAL ?
428                 Addressbook_Controller_Contact::getInstance()->getContactByUserId($container->getOwner(), true)->getId() :
429                 Tinebase_Core::getUser()->contact_id;
430             } catch (Exception $e) {
431                 $calendarUserId = Calendar_Controller_MSEventFacade::getCurrentUserContactId();
432             }
433             
434             $calendarUser = new Calendar_Model_Attender(array(
435                 'user_type' => Calendar_Model_Attender::USERTYPE_USER,
436                 'user_id'   => $calendarUserId,
437             ));
438             
439
440             $this->setCalendarUser($calendarUser);
441
442             if ($setEventFilter) {
443                 $eventFilter = new Calendar_Model_EventFilter(array(
444                     array('field' => 'container_id', 'operator' => 'equals', 'value' => $container->getId())
445                 ));
446                 $this->setEventFilter($eventFilter);
447             }
448         }
449     }
450     
451     /**
452      * updates an attender status of a event
453      *
454      * @param  Calendar_Model_Event    $_event
455      * @param  Calendar_Model_Attender $_attendee
456      * @return Calendar_Model_Event    updated event
457      */
458     public function attenderStatusUpdate($_event, $_attendee)
459     {
460         if ($_event->recurid) {
461             throw new Tinebase_Exception_UnexpectedValue('recur event instances must be saved as part of the base event');
462         }
463         
464         $exceptions = $_event->exdate instanceof Tinebase_Record_RecordSet ? $_event->exdate : new Tinebase_Record_RecordSet('Calendar_Model_Event');
465         $_event->exdate = $exceptions->getOriginalDtStart();
466         
467         // update base event status
468         $attendeeFound = Calendar_Model_Attender::getAttendee($_event->attendee, $_attendee);
469         if (!isset($attendeeFound)) {
470             throw new Tinebase_Exception_UnexpectedValue('not an attendee');
471         }
472         $attendeeFound->displaycontainer_id = $_attendee->displaycontainer_id;
473         Calendar_Controller_Event::getInstance()->attenderStatusUpdate($_event, $attendeeFound, $attendeeFound->status_authkey);
474         
475         // update exceptions
476         foreach($exceptions as $exception) {
477             // do not attempt to set status of an deleted instance
478             if ($exception->is_deleted) continue;
479             
480             $exceptionAttendee = Calendar_Model_Attender::getAttendee($exception->attendee, $_attendee);
481             
482             if (! $exception->getId()) {
483                 if (! $exceptionAttendee) {
484                     // set user status to DECLINED
485                     $exceptionAttendee = clone $attendeeFound;
486                     $exceptionAttendee->status = Calendar_Model_Attender::STATUS_DECLINED;
487                 }
488                 $exceptionAttendee->displaycontainer_id = $_attendee->displaycontainer_id;
489                 Calendar_Controller_Event::getInstance()->attenderStatusCreateRecurException($exception, $exceptionAttendee, $exceptionAttendee->status_authkey);
490             } else {
491                 if (! $exceptionAttendee) {
492                     // we would need to find out the users authkey to decline him -> not allowed!?
493                     if (!isset($attendeeFound)) {
494                         throw new Tinebase_Exception_UnexpectedValue('not an attendee');
495                     }
496                 }
497                 $exceptionAttendee->displaycontainer_id = $_attendee->displaycontainer_id;
498                 Calendar_Controller_Event::getInstance()->attenderStatusUpdate($exception, $exceptionAttendee, $exceptionAttendee->status_authkey);
499             }
500         }
501         
502         return $this->get($_event->getId());
503     }
504     
505     /**
506      * update multiple records
507      * 
508      * @param   Tinebase_Model_Filter_FilterGroup $_filter
509      * @param   array $_data
510      * @return  integer number of updated records
511      */
512     public function updateMultiple($_what, $_data)
513     {
514         throw new Tinebase_Exception_NotImplemented('Calendar_Conroller_MSEventFacade::updateMultiple not yet implemented');
515     }
516     
517     /**
518      * Deletes a set of records.
519      * 
520      * If one of the records could not be deleted, no record is deleted
521      * 
522      * @param   array array of record identifiers
523      * @return  Tinebase_Record_RecordSet
524      */
525     public function delete($_ids)
526     {
527         $ids = array_unique((array)$_ids);
528         $events = $this->getMultiple($ids);
529         
530         foreach ($events as $event) {
531             if ($event->exdate !== null) {
532                 foreach ($event->exdate as $exception) {
533                     $exceptionId = $exception->getId();
534                     if ($exceptionId) {
535                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
536                             . ' Found exdate to be deleted (id: ' . $exceptionId . ')');
537                         array_unshift($ids, $exceptionId);
538                     }
539                 }
540             }
541         }
542         
543         $this->_eventController->delete($ids);
544         return $events;
545     }
546     
547     /**
548      * Delete Attendees from event
549      *
550      * @param Calendar_Model_Event $_event
551      * @param Calendar_Model_Event $_iMIP
552      *
553      */
554     public function deleteAttendees($_event, $_iMIP) {
555
556         foreach ($_iMIP->attendee as $attendeeToDelete) {
557             $attenderToRemove = Calendar_Model_Attender::getAttendee($_event->attendee, $attendeeToDelete);
558             if ($attenderToRemove) {
559                 $_event->attendee->removeRecord($attenderToRemove);
560             }
561         }
562         $_event->last_modified_time = $_iMIP->last_modified_time;
563         $this->update($_event);
564         return $_event;
565     }
566
567     /**
568      * get and resolve all alarms of given record(s)
569      * 
570      * @param  Tinebase_Record_Interface|Tinebase_Record_RecordSet $_record
571      */
572     public function getAlarms($_record)
573     {
574         $events = $_record instanceof Tinebase_Record_RecordSet ? $_record->getClone(true) : new Tinebase_Record_RecordSet('Calendar_Model_Event', array($_record));
575         
576         foreach($events as $event) {
577             if ($event->exdate instanceof Tinebase_Record_RecordSet) {
578 //                 $event->exdate->addIndices(array('is_deleted'));
579                 $events->merge($event->exdate->filter('is_deleted', 0));
580             }
581         }
582         
583         $this->_eventController->getAlarms($events);
584     }
585     
586     /**
587      * set displaycontainer for given attendee 
588      * 
589      * @param Calendar_Model_Event    $_event
590      * @param string                  $_container
591      * @param Calendar_Model_Attender $_attendee    defaults to calendarUser
592      */
593     public function setDisplaycontainer($_event, $_container, $_attendee = NULL)
594     {
595         if ($_event->exdate instanceof Tinebase_Record_RecordSet) {
596             foreach ($_event->exdate as $idx => $exdate) {
597                 self::setDisplaycontainer($exdate, $_container, $_attendee);
598             }
599         }
600         
601         $attendeeRecord = Calendar_Model_Attender::getAttendee($_event->attendee, $_attendee ? $_attendee : $this->getCalendarUser());
602         
603         if ($attendeeRecord) {
604             $attendeeRecord->displaycontainer_id = $_container;
605         }
606     }
607     
608     /**
609      * sets current calendar user
610      * 
611      * @param Calendar_Model_Attender $_calUser
612      * @return Calendar_Model_Attender oldUser
613      */
614     public function setCalendarUser(Calendar_Model_Attender $_calUser)
615     {
616         if (! in_array($_calUser->user_type, array(Calendar_Model_Attender::USERTYPE_USER, Calendar_Model_Attender::USERTYPE_GROUPMEMBER))) {
617             throw new Tinebase_Exception_UnexpectedValue('Calendar user must be a contact');
618         }
619         $oldUser = $this->_calendarUser;
620         $this->_calendarUser = $_calUser;
621         $this->_eventController->setCalendarUser($_calUser);
622         
623         return $oldUser;
624     }
625     
626     /**
627      * get current calendar user
628      * 
629      * @return Calendar_Model_Attender
630      */
631     public function getCalendarUser()
632     {
633         return $this->_calendarUser;
634     }
635     
636     /**
637      * set current event filter for exdate computations
638      * 
639      * @param  Calendar_Model_EventFilter
640      * @return Calendar_Model_EventFilter
641      */
642     public function setEventFilter($_filter)
643     {
644         $oldFilter = $this->_eventFilter;
645         
646         if ($_filter !== NULL) {
647             if (! $_filter instanceof Calendar_Model_EventFilter) {
648                 throw new Tinebase_Exception_UnexpectedValue('not a valid filter');
649             }
650             $this->_eventFilter = clone $_filter;
651             
652             $periodFilters = $this->_eventFilter->getFilter('period', TRUE, TRUE);
653             foreach((array) $periodFilters as $periodFilter) {
654                 $periodFilter->setDisabled();
655             }
656         } else {
657             $this->_eventFilter = NULL;
658         }
659         
660         return $oldFilter;
661     }
662     
663     /**
664      * get current event filter
665      * 
666      * @return Calendar_Model_EventFilter
667      */
668     public function getEventFilter()
669     {
670         return $this->_eventFilter;
671     }
672     
673     /**
674      * filters given eventset for events with matching dtstart
675      * 
676      * @param Tinebase_Record_RecordSet $_events
677      * @param array                     $_dtstarts
678      */
679     protected function _filterEventsByDTStarts($_events, $_dtstarts)
680     {
681         $filteredSet = new Tinebase_Record_RecordSet('Calendar_Model_Event');
682         $allDTStarts = $_events->getOriginalDtStart();
683         
684         $existingIdxs = array_intersect($allDTStarts, $_dtstarts);
685         
686         foreach($existingIdxs as $idx => $dtstart) {
687             $filteredSet->addRecord($_events[$idx]);
688         }
689         
690         return $filteredSet;
691     }
692
693     protected function _resolveData($events) {
694         $eventSet = $events instanceof Tinebase_Record_RecordSet
695             ? $events->getClone(true)
696             : new Tinebase_Record_RecordSet('Calendar_Model_Event', array($events));
697
698         // get recur exceptions
699         foreach($eventSet as $event) {
700             if ($event->rrule && !$event->exdate instanceof Tinebase_Record_RecordSet) {
701                 $exdates = $this->_eventController->getRecurExceptions($event, TRUE, $this->getEventFilter());
702                 $event->exdate = $exdates;
703                 $eventSet->merge($exdates);
704             }
705         }
706
707         $this->_eventController->getAlarms($eventSet);
708         Tinebase_FileSystem_RecordAttachments::getInstance()->getMultipleAttachmentsOfRecords($eventSet);
709     }
710
711     /**
712      * converts a tine20 event to an iTIP event
713      * 
714      * @param  Calendar_Model_Event $_event - must have exceptions, alarms & attachements resovled
715      * @return Calendar_Model_Event 
716      */
717     protected function _toiTIP($_event)
718     {
719         $events = $_event instanceof Tinebase_Record_RecordSet
720             ? $_event
721             : new Tinebase_Record_RecordSet('Calendar_Model_Event', array($_event));
722
723         foreach ($events as $idx => $event) {
724             // get exdates
725             if ($event->getId() && $event->rrule) {
726                 $this->_toiTIP($event->exdate);
727             }
728
729             $this->_filterAttendeeWithoutEmail($event);
730             
731             $CUAttendee = Calendar_Model_Attender::getAttendee($event->attendee, $this->_calendarUser);
732             $isOrganizer = $event->isOrganizer($this->_calendarUser);
733             
734             // apply perspective
735             if ($CUAttendee && !$isOrganizer) {
736                 $event->transp = $CUAttendee->transp ? $CUAttendee->transp : $event->transp;
737             }
738             
739             if ($event->alarms instanceof Tinebase_Record_RecordSet) {
740                 foreach($event->alarms as $alarm) {
741                     if (! Calendar_Model_Attender::isAlarmForAttendee($this->_calendarUser, $alarm, $event)) {
742                         $event->alarms->removeRecord($alarm);
743                     }
744                 }
745             }
746         }
747         
748         return $_event;
749     }
750     
751     /**
752      * filter out attendee w.o. email
753      * 
754      * @param Calendar_Model_Event $event
755      */
756     protected function _filterAttendeeWithoutEmail($event)
757     {
758         if (! $event->attendee instanceof Tinebase_Record_RecordSet) {
759             return;
760         }
761         
762         foreach ($event->attendee as $attender) {
763             $cacheId = $attender->user_type . $attender->user_id;
764             
765             // value is in array and true
766             if (isset(self::$_attendeeEmailCache[$cacheId])) {
767                 continue;
768             }
769             
770             // add value to cache if not existing already
771             if (!array_key_exists($cacheId, self::$_attendeeEmailCache)) {
772                 $this->_fillResolvedAttendeeCache($event);
773                 
774                 self::$_attendeeEmailCache[$cacheId] = !!$attender->getEmail();
775                 
776                 // limit class cache to 100 entries
777                 if (count(self::$_attendeeEmailCache) > 100) {
778                     array_shift(self::$_attendeeEmailCache);
779                 }
780             }
781             
782             // remove entry if value is not true => attender has no email address
783             if (!self::$_attendeeEmailCache[$cacheId]) {
784                 $event->attendee->removeRecord($attender);
785             }
786         }
787     }
788
789     /**
790      * re add attendee w.o. email
791      * 
792      * @param Calendar_Model_Event $event
793      */
794     protected function _addAttendeeWithoutEmail($event, $currentEvent)
795     {
796         if (! $currentEvent->attendee instanceof Tinebase_Record_RecordSet) {
797             return;
798         }
799         $this->_fillResolvedAttendeeCache($currentEvent);
800         
801         if (! $event->attendee instanceof Tinebase_Record_RecordSet) {
802             $event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
803         }
804         foreach ($currentEvent->attendee->getEmail() as $idx => $email) {
805             if (! $email) {
806                 $event->attendee->addRecord($currentEvent->attendee[$idx]);
807             }
808         }
809     }
810     
811     /**
812      * this fills the resolved attendee cache without changing the event attendee recordset
813      * 
814      * @param Calendar_Model_Event $event
815      */
816     protected function _fillResolvedAttendeeCache($event)
817     {
818         if (! $event->attendee instanceof Tinebase_Record_RecordSet) {
819             return;
820         }
821         
822         Calendar_Model_Attender::fillResolvedAttendeesCache($event->attendee);
823     }
824     
825     /**
826      * converts an iTIP event to a tine20 event
827      * 
828      * @param Calendar_Model_Event $_event
829      * @param Calendar_Model_Event $_currentEvent (not iTIP!)
830      */
831     protected function _fromiTIP($_event, $_currentEvent)
832     {
833         if (! $_event->rrule) {
834             $_event->exdate = NULL;
835         }
836         
837         if ($_event->exdate instanceof Tinebase_Record_RecordSet) {
838             
839             try{
840                 $currExdates = $this->_eventController->getRecurExceptions($_event, TRUE);
841                 $this->getAlarms($currExdates);
842                 $currClientExdates = $this->_eventController->getRecurExceptions($_event, TRUE, $this->getEventFilter());
843                 $this->getAlarms($currClientExdates);
844             } catch (Tinebase_Exception_NotFound $e) {
845                 $currExdates = NULL;
846                 $currClientExdates = NULL; 
847             }
848             
849             foreach ($_event->exdate as $idx => $exdate) {
850                 try {
851                     $this->_prepareException($_event, $exdate);
852                 } catch (Exception $e){}
853
854                 $currExdate = $currExdates instanceof Tinebase_Record_RecordSet ? $currExdates->filter('recurid', $exdate->recurid)->getFirstRecord() : NULL;
855                 
856                 
857                 if ($exdate->is_deleted) {
858                     // reset implicit filter fallouts and mark as don't touch (seq = -1)
859                     $currClientExdate = $currClientExdates instanceof Tinebase_Record_RecordSet ? $currClientExdates->filter('recurid', $exdate->recurid)->getFirstRecord() : NULL;
860                     if ($currClientExdate && $currClientExdate->is_deleted) {
861                         $_event->exdate[$idx] = $currExdate;
862                         $currExdate->seq = -1;
863                         continue;
864                     }
865                 }
866                 $this->_fromiTIP($exdate, $currExdate ? $currExdate : clone $_currentEvent);
867             }
868         }
869         
870         // assert organizer
871         $_event->organizer = $_event->organizer ?: ($_currentEvent->organizer ?: $this->_calendarUser->user_id);
872
873         $this->_addAttendeeWithoutEmail($_event, $_currentEvent);
874         
875         $CUAttendee = Calendar_Model_Attender::getAttendee($_event->attendee, $this->_calendarUser);
876         $currentCUAttendee  = Calendar_Model_Attender::getAttendee($_currentEvent->attendee, $this->_calendarUser);
877         $isOrganizer = $_event->isOrganizer($this->_calendarUser);
878         
879         // remove perspective 
880         if ($CUAttendee && !$isOrganizer) {
881             $CUAttendee->transp = $_event->transp;
882             $_event->transp = $_currentEvent->transp ? $_currentEvent->transp : $_event->transp;
883         }
884         
885         // apply changes to original alarms
886         $_currentEvent->alarms  = $_currentEvent->alarms instanceof Tinebase_Record_RecordSet ? $_currentEvent->alarms : new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
887         $_event->alarms  = $_event->alarms instanceof Tinebase_Record_RecordSet ? $_event->alarms : new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
888         
889         foreach($_currentEvent->alarms as $currentAlarm) {
890             if (Calendar_Model_Attender::isAlarmForAttendee($this->_calendarUser, $currentAlarm)) {
891                 $alarmUpdate = Calendar_Controller_Alarm::getMatchingAlarm($_event->alarms, $currentAlarm);
892                 
893                 if ($alarmUpdate) {
894                     // we could map the alarm => save ack & snooze options
895                     if ($dtAck = Calendar_Controller_Alarm::getAcknowledgeTime($alarmUpdate)) {
896                         Calendar_Controller_Alarm::setAcknowledgeTime($currentAlarm, $dtAck, $this->getCalendarUser()->user_id);
897                     }
898                     if ($dtSnooze = Calendar_Controller_Alarm::getSnoozeTime($alarmUpdate)) {
899                         Calendar_Controller_Alarm::setSnoozeTime($currentAlarm, $dtSnooze, $this->getCalendarUser()->user_id);
900                     }
901                     $_event->alarms->removeRecord($alarmUpdate);
902                 } else {
903                     // alarm is to be skiped/deleted
904                     if (! $currentAlarm->getOption('attendee')) {
905                         Calendar_Controller_Alarm::skipAlarm($currentAlarm, $this->_calendarUser);
906                     } else {
907                         $_currentEvent->alarms->removeRecord($currentAlarm);
908                     }
909                 }
910             }
911         }
912         if (! $isOrganizer) {
913             $_event->alarms->setOption('attendee', Calendar_Controller_Alarm::attendeeToOption($this->_calendarUser));
914         }
915         $_event->alarms->merge($_currentEvent->alarms);
916
917         // assert organizer for personal calendars to be calendar owner
918         if ($this->_currentEventFacadeContainer && $this->_currentEventFacadeContainer->getId() == $_event->container_id
919             && $this->_currentEventFacadeContainer->type == Tinebase_Model_Container::TYPE_PERSONAL
920             && !$_event->hasExternalOrganizer() ) {
921
922             $_event->organizer = $this->_calendarUser->user_id;
923         }
924         // in MS world only cal_user can do status updates
925         if ($CUAttendee) {
926             $CUAttendee->status_authkey = $currentCUAttendee ? $currentCUAttendee->status_authkey : NULL;
927         }
928     }
929     
930     /**
931      * computes an returns the migration for event exceptions
932      * 
933      * @param Tinebase_Record_RecordSet $_currentPersistentExceptions
934      * @param Tinebase_Record_RecordSet $_newPersistentExceptions
935      */
936     protected function _getExceptionsMigration($_currentPersistentExceptions, $_newPersistentExceptions)
937     {
938         $migration = array();
939         
940         // add indices and sort to speedup things
941         $_currentPersistentExceptions->addIndices(array('dtstart'))->sort('dtstart');
942         $_newPersistentExceptions->addIndices(array('dtstart'))->sort('dtstart');
943         
944         // get dtstarts
945         $currDtStart = $_currentPersistentExceptions->getOriginalDtStart();
946         $newDtStart = $_newPersistentExceptions->getOriginalDtStart();
947         
948         // compute migration in terms of dtstart
949         $toDeleteDtStart = array_diff($currDtStart, $newDtStart);
950         $toCreateDtStart = array_diff($newDtStart, $currDtStart);
951         $toUpdateDtSTart = array_intersect($currDtStart, $newDtStart);
952         
953         $migration['toDelete'] = $this->_filterEventsByDTStarts($_currentPersistentExceptions, $toDeleteDtStart);
954         $migration['toCreate'] = $this->_filterEventsByDTStarts($_newPersistentExceptions, $toCreateDtStart);
955         $migration['toUpdate'] = $this->_filterEventsByDTStarts($_newPersistentExceptions, $toUpdateDtSTart);
956         
957         // get ids for toUpdate
958         $idxIdMap = $this->_filterEventsByDTStarts($_currentPersistentExceptions, $toUpdateDtSTart)->getId();
959         $migration['toUpdate']->setByIndices('id', $idxIdMap, /* $skipMissing = */ true);
960         
961         // filter exceptions marked as don't touch 
962         foreach ($migration['toUpdate'] as $toUpdate) {
963             if ($toUpdate->seq === -1) {
964                 $migration['toUpdate']->removeRecord($toUpdate);
965             }
966         }
967         
968         return $migration;
969     }
970     
971     /**
972      * prepares an exception instance for persistence
973      * 
974      * @param  Calendar_Model_Event $_baseEvent
975      * @param  Calendar_Model_Event $_exception
976      * @return void
977      * @throws Tinebase_Exception_InvalidArgument
978      */
979     protected function _prepareException(Calendar_Model_Event $_baseEvent, Calendar_Model_Event $_exception)
980     {
981         if (! $_baseEvent->uid) {
982             throw new Tinebase_Exception_InvalidArgument('base event has no uid');
983         }
984         
985         if ($_exception->is_deleted == false) {
986             $_exception->container_id = $_baseEvent->container_id;
987         }
988         $_exception->uid = $_baseEvent->uid;
989         $_exception->base_event_id = $_baseEvent->getId();
990         $_exception->recurid = $_baseEvent->uid . '-' . $_exception->getOriginalDtStart()->format(Tinebase_Record_Abstract::ISO8601LONG);
991         
992         // NOTE: we always refetch the base event as it might be touched in the meantime
993         $currBaseEvent = $this->_eventController->get($_baseEvent, null, false);
994         $_exception->last_modified_time = $currBaseEvent->last_modified_time;
995     }
996 }