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