183cf0ab3360ec19f7d73e8e0d1c9a7327b71a3b
[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     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|optional    $_filter
168      * @param Tinebase_Model_Pagination|optional            $_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_Record_Interface $_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                 $exceptionAttendee->displaycontainer_id = $_attendee->displaycontainer_id;
510                 Calendar_Controller_Event::getInstance()->attenderStatusUpdate($exception, $exceptionAttendee, $exceptionAttendee->status_authkey);
511             }
512         }
513         
514         return $this->get($_event->getId());
515     }
516     
517     /**
518      * update multiple records
519      * 
520      * @param   Tinebase_Model_Filter_FilterGroup $_filter
521      * @param   array $_data
522      * @return  integer number of updated records
523      */
524     public function updateMultiple($_what, $_data)
525     {
526         throw new Tinebase_Exception_NotImplemented('Calendar_Conroller_MSEventFacade::updateMultiple not yet implemented');
527     }
528     
529     /**
530      * Deletes a set of records.
531      * 
532      * If one of the records could not be deleted, no record is deleted
533      * 
534      * @param   array array of record identifiers
535      * @return  Tinebase_Record_RecordSet
536      */
537     public function delete($_ids)
538     {
539         $ids = array_unique((array)$_ids);
540         $events = $this->getMultiple($ids);
541         
542         foreach ($events as $event) {
543             if ($event->exdate !== null) {
544                 foreach ($event->exdate as $exception) {
545                     $exceptionId = $exception->getId();
546                     if ($exceptionId) {
547                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
548                             . ' Found exdate to be deleted (id: ' . $exceptionId . ')');
549                         array_unshift($ids, $exceptionId);
550                     }
551                 }
552             }
553         }
554         
555         $this->_eventController->delete($ids);
556         return $events;
557     }
558     
559     /**
560      * get and resolve all alarms of given record(s)
561      * 
562      * @param  Tinebase_Record_Interface|Tinebase_Record_RecordSet $_record
563      */
564     public function getAlarms($_record)
565     {
566         $events = $_record instanceof Tinebase_Record_RecordSet ? $_record->getClone(true) : new Tinebase_Record_RecordSet('Calendar_Model_Event', array($_record));
567         
568         foreach($events as $event) {
569             if ($event->exdate instanceof Tinebase_Record_RecordSet) {
570 //                 $event->exdate->addIndices(array('is_deleted'));
571                 $events->merge($event->exdate->filter('is_deleted', 0));
572             }
573         }
574         
575         $this->_eventController->getAlarms($events);
576     }
577     
578     /**
579      * set displaycontainer for given attendee 
580      * 
581      * @param Calendar_Model_Event    $_event
582      * @param string                  $_container
583      * @param Calendar_Model_Attender $_attendee    defaults to calendarUser
584      */
585     public function setDisplaycontainer($_event, $_container, $_attendee = NULL)
586     {
587         if ($_event->exdate instanceof Tinebase_Record_RecordSet) {
588             foreach ($_event->exdate as $idx => $exdate) {
589                 self::setDisplaycontainer($exdate, $_container, $_attendee);
590             }
591         }
592         
593         $attendeeRecord = Calendar_Model_Attender::getAttendee($_event->attendee, $_attendee ? $_attendee : $this->getCalendarUser());
594         
595         if ($attendeeRecord) {
596             $attendeeRecord->displaycontainer_id = $_container;
597         }
598     }
599     
600     /**
601      * sets current calendar user
602      * 
603      * @param Calendar_Model_Attender $_calUser
604      * @return Calendar_Model_Attender oldUser
605      */
606     public function setCalendarUser(Calendar_Model_Attender $_calUser)
607     {
608         if (! in_array($_calUser->user_type, array(Calendar_Model_Attender::USERTYPE_USER, Calendar_Model_Attender::USERTYPE_GROUPMEMBER))) {
609             throw new Tinebase_Exception_UnexpectedValue('Calendar user must be a contact');
610         }
611         $oldUser = $this->_calendarUser;
612         $this->_calendarUser = $_calUser;
613         $this->_eventController->setCalendarUser($_calUser);
614         
615         return $oldUser;
616     }
617     
618     /**
619      * get current calendar user
620      * 
621      * @return Calendar_Model_Attender
622      */
623     public function getCalendarUser()
624     {
625         return $this->_calendarUser;
626     }
627     
628     /**
629      * set current event filter for exdate computations
630      * 
631      * @param  Calendar_Model_EventFilter
632      * @return Calendar_Model_EventFilter
633      */
634     public function setEventFilter($_filter)
635     {
636         $oldFilter = $this->_eventFilter;
637         
638         if ($_filter !== NULL) {
639             if (! $_filter instanceof Calendar_Model_EventFilter) {
640                 throw new Tinebase_Exception_UnexpectedValue('not a valid filter');
641             }
642             $this->_eventFilter = clone $_filter;
643             
644             $periodFilters = $this->_eventFilter->getFilter('period', TRUE, TRUE);
645             foreach((array) $periodFilters as $periodFilter) {
646                 $periodFilter->setDisabled();
647             }
648         } else {
649             $this->_eventFilter = NULL;
650         }
651         
652         return $oldFilter;
653     }
654     
655     /**
656      * get current event filter
657      * 
658      * @return Calendar_Model_EventFilter
659      */
660     public function getEventFilter()
661     {
662         return $this->_eventFilter;
663     }
664     
665     /**
666      * filters given eventset for events with matching dtstart
667      * 
668      * @param Tinebase_Record_RecordSet $_events
669      * @param array                     $_dtstarts
670      */
671     protected function _filterEventsByDTStarts($_events, $_dtstarts)
672     {
673         $filteredSet = new Tinebase_Record_RecordSet('Calendar_Model_Event');
674         $allDTStarts = $_events->getOriginalDtStart();
675         
676         $existingIdxs = array_intersect($allDTStarts, $_dtstarts);
677         
678         foreach($existingIdxs as $idx => $dtstart) {
679             $filteredSet->addRecord($_events[$idx]);
680         }
681         
682         return $filteredSet;
683     }
684
685     protected function _resolveData($events) {
686         $eventSet = $events instanceof Tinebase_Record_RecordSet
687             ? $events->getClone(true)
688             : new Tinebase_Record_RecordSet('Calendar_Model_Event', array($events));
689
690         // get recur exceptions
691         foreach($eventSet as $event) {
692             if ($event->rrule && !$event->exdate instanceof Tinebase_Record_RecordSet) {
693                 $exdates = $this->_eventController->getRecurExceptions($event, TRUE, $this->getEventFilter());
694                 $event->exdate = $exdates;
695                 $eventSet->merge($exdates);
696             }
697         }
698
699         $this->_eventController->getAlarms($eventSet);
700         Tinebase_FileSystem_RecordAttachments::getInstance()->getMultipleAttachmentsOfRecords($eventSet);
701     }
702
703     /**
704      * converts a tine20 event to an iTIP event
705      * 
706      * @param  Calendar_Model_Event $_event - must have exceptions, alarms & attachements resovled
707      * @return Calendar_Model_Event 
708      */
709     protected function _toiTIP($_event)
710     {
711         $events = $_event instanceof Tinebase_Record_RecordSet
712             ? $_event
713             : new Tinebase_Record_RecordSet('Calendar_Model_Event', array($_event));
714
715         foreach ($events as $idx => $event) {
716             // get exdates
717             if ($event->getId() && $event->rrule) {
718                 $this->_toiTIP($event->exdate);
719             }
720
721             $this->_filterAttendeeWithoutEmail($event);
722             
723             $CUAttendee = Calendar_Model_Attender::getAttendee($event->attendee, $this->_calendarUser);
724             $isOrganizer = $event->isOrganizer($this->_calendarUser);
725             
726             // apply perspective
727             if ($CUAttendee && !$isOrganizer) {
728                 $event->transp = $CUAttendee->transp ? $CUAttendee->transp : $event->transp;
729             }
730             
731             if ($event->alarms instanceof Tinebase_Record_RecordSet) {
732                 foreach($event->alarms as $alarm) {
733                     if (! Calendar_Model_Attender::isAlarmForAttendee($this->_calendarUser, $alarm, $event)) {
734                         $event->alarms->removeRecord($alarm);
735                     }
736                 }
737             }
738         }
739         
740         return $_event;
741     }
742     
743     /**
744      * filter out attendee w.o. email
745      * 
746      * @param Calendar_Model_Event $event
747      */
748     protected function _filterAttendeeWithoutEmail($event)
749     {
750         if (! $event->attendee instanceof Tinebase_Record_RecordSet) {
751             return;
752         }
753         
754         foreach ($event->attendee as $attender) {
755             $cacheId = $attender->user_type . $attender->user_id;
756             
757             // value is in array and true
758             if (isset(self::$_attendeeEmailCache[$cacheId])) {
759                 continue;
760             }
761             
762             // add value to cache if not existing already
763             if (!array_key_exists($cacheId, self::$_attendeeEmailCache)) {
764                 $this->_fillResolvedAttendeeCache($event);
765                 
766                 self::$_attendeeEmailCache[$cacheId] = !!$attender->getEmail();
767                 
768                 // limit class cache to 100 entries
769                 if (count(self::$_attendeeEmailCache) > 100) {
770                     array_shift(self::$_attendeeEmailCache);
771                 }
772             }
773             
774             // remove entry if value is not true => attender has no email address
775             if (!self::$_attendeeEmailCache[$cacheId]) {
776                 $event->attendee->removeRecord($attender);
777             }
778         }
779     }
780
781     /**
782      * re add attendee w.o. email
783      * 
784      * @param Calendar_Model_Event $event
785      */
786     protected function _addAttendeeWithoutEmail($event, $currentEvent)
787     {
788         if (! $currentEvent->attendee instanceof Tinebase_Record_RecordSet) {
789             return;
790         }
791         $this->_fillResolvedAttendeeCache($currentEvent);
792         
793         if (! $event->attendee instanceof Tinebase_Record_RecordSet) {
794             $event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
795         }
796         foreach ($currentEvent->attendee->getEmail() as $idx => $email) {
797             if (! $email) {
798                 $event->attendee->addRecord($currentEvent->attendee[$idx]);
799             }
800         }
801     }
802     
803     /**
804      * this fills the resolved attendee cache without changing the event attendee recordset
805      * 
806      * @param Calendar_Model_Event $event
807      */
808     protected function _fillResolvedAttendeeCache($event)
809     {
810         if (! $event->attendee instanceof Tinebase_Record_RecordSet) {
811             return;
812         }
813         
814         Calendar_Model_Attender::fillResolvedAttendeesCache($event->attendee);
815     }
816     
817     /**
818      * converts an iTIP event to a tine20 event
819      * 
820      * @param Calendar_Model_Event $_event
821      * @param Calendar_Model_Event $_currentEvent (not iTIP!)
822      */
823     protected function _fromiTIP($_event, $_currentEvent)
824     {
825         if (! $_event->rrule) {
826             $_event->exdate = NULL;
827         }
828         
829         if ($_event->exdate instanceof Tinebase_Record_RecordSet) {
830             
831             try{
832                 $currExdates = $this->_eventController->getRecurExceptions($_event, TRUE);
833                 $this->getAlarms($currExdates);
834                 $currClientExdates = $this->_eventController->getRecurExceptions($_event, TRUE, $this->getEventFilter());
835                 $this->getAlarms($currClientExdates);
836             } catch (Tinebase_Exception_NotFound $e) {
837                 $currExdates = NULL;
838                 $currClientExdates = NULL; 
839             }
840             
841             foreach ($_event->exdate as $idx => $exdate) {
842                 try {
843                     $this->_prepareException($_event, $exdate);
844                 } catch (Exception $e){}
845
846                 $currExdate = $currExdates instanceof Tinebase_Record_RecordSet ? $currExdates->filter('recurid', $exdate->recurid)->getFirstRecord() : NULL;
847                 
848                 
849                 if ($exdate->is_deleted) {
850                     // reset implicit filter fallouts and mark as don't touch (seq = -1)
851                     $currClientExdate = $currClientExdates instanceof Tinebase_Record_RecordSet ? $currClientExdates->filter('recurid', $exdate->recurid)->getFirstRecord() : NULL;
852                     if ($currClientExdate && $currClientExdate->is_deleted) {
853                         $_event->exdate[$idx] = $currExdate;
854                         $currExdate->seq = -1;
855                         continue;
856                     }
857                 }
858                 $this->_fromiTIP($exdate, $currExdate ? $currExdate : clone $_currentEvent);
859             }
860         }
861         
862         // assert organizer
863         $_event->organizer = $_event->organizer ?: ($_currentEvent->organizer ?: $this->_calendarUser->user_id);
864
865         $this->_addAttendeeWithoutEmail($_event, $_currentEvent);
866         
867         $CUAttendee = Calendar_Model_Attender::getAttendee($_event->attendee, $this->_calendarUser);
868         $currentCUAttendee  = Calendar_Model_Attender::getAttendee($_currentEvent->attendee, $this->_calendarUser);
869         $isOrganizer = $_event->isOrganizer($this->_calendarUser);
870         
871         // remove perspective 
872         if ($CUAttendee && !$isOrganizer) {
873             $CUAttendee->transp = $_event->transp;
874             $_event->transp = $_currentEvent->transp ? $_currentEvent->transp : $_event->transp;
875         }
876         
877         // apply changes to original alarms
878         $_currentEvent->alarms  = $_currentEvent->alarms instanceof Tinebase_Record_RecordSet ? $_currentEvent->alarms : new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
879         $_event->alarms  = $_event->alarms instanceof Tinebase_Record_RecordSet ? $_event->alarms : new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
880         
881         foreach($_currentEvent->alarms as $currentAlarm) {
882             if (Calendar_Model_Attender::isAlarmForAttendee($this->_calendarUser, $currentAlarm)) {
883                 $alarmUpdate = Calendar_Controller_Alarm::getMatchingAlarm($_event->alarms, $currentAlarm);
884                 
885                 if ($alarmUpdate) {
886                     // we could map the alarm => save ack & snooze options
887                     if ($dtAck = Calendar_Controller_Alarm::getAcknowledgeTime($alarmUpdate)) {
888                         Calendar_Controller_Alarm::setAcknowledgeTime($currentAlarm, $dtAck, $this->getCalendarUser()->user_id);
889                     }
890                     if ($dtSnooze = Calendar_Controller_Alarm::getSnoozeTime($alarmUpdate)) {
891                         Calendar_Controller_Alarm::setSnoozeTime($currentAlarm, $dtSnooze, $this->getCalendarUser()->user_id);
892                     }
893                     $_event->alarms->removeRecord($alarmUpdate);
894                 } else {
895                     // alarm is to be skiped/deleted
896                     if (! $currentAlarm->getOption('attendee')) {
897                         Calendar_Controller_Alarm::skipAlarm($currentAlarm, $this->_calendarUser);
898                     } else {
899                         $_currentEvent->alarms->removeRecord($currentAlarm);
900                     }
901                 }
902             }
903         }
904         if (! $isOrganizer) {
905             $_event->alarms->setOption('attendee', Calendar_Controller_Alarm::attendeeToOption($this->_calendarUser));
906         }
907         $_event->alarms->merge($_currentEvent->alarms);
908
909         // assert organizer for personal calendars to be calendar owner
910         if ($this->_currentEventFacadeContainer && $this->_currentEventFacadeContainer->getId() == $_event->container_id
911             && $this->_currentEventFacadeContainer->type == Tinebase_Model_Container::TYPE_PERSONAL
912             && !$_event->hasExternalOrganizer() ) {
913
914             $_event->organizer = $this->_calendarUser->user_id;
915         }
916         // in MS world only cal_user can do status updates
917         if ($CUAttendee) {
918             $CUAttendee->status_authkey = $currentCUAttendee ? $currentCUAttendee->status_authkey : NULL;
919         }
920     }
921     
922     /**
923      * computes an returns the migration for event exceptions
924      * 
925      * @param Tinebase_Record_RecordSet $_currentPersistentExceptions
926      * @param Tinebase_Record_RecordSet $_newPersistentExceptions
927      */
928     protected function _getExceptionsMigration($_currentPersistentExceptions, $_newPersistentExceptions)
929     {
930         $migration = array();
931         
932         // add indices and sort to speedup things
933         $_currentPersistentExceptions->addIndices(array('dtstart'))->sort('dtstart');
934         $_newPersistentExceptions->addIndices(array('dtstart'))->sort('dtstart');
935         
936         // get dtstarts
937         $currDtStart = $_currentPersistentExceptions->getOriginalDtStart();
938         $newDtStart = $_newPersistentExceptions->getOriginalDtStart();
939         
940         // compute migration in terms of dtstart
941         $toDeleteDtStart = array_diff($currDtStart, $newDtStart);
942         $toCreateDtStart = array_diff($newDtStart, $currDtStart);
943         $toUpdateDtSTart = array_intersect($currDtStart, $newDtStart);
944         
945         $migration['toDelete'] = $this->_filterEventsByDTStarts($_currentPersistentExceptions, $toDeleteDtStart);
946         $migration['toCreate'] = $this->_filterEventsByDTStarts($_newPersistentExceptions, $toCreateDtStart);
947         $migration['toUpdate'] = $this->_filterEventsByDTStarts($_newPersistentExceptions, $toUpdateDtSTart);
948         
949         // get ids for toUpdate
950         $idxIdMap = $this->_filterEventsByDTStarts($_currentPersistentExceptions, $toUpdateDtSTart)->getId();
951         $migration['toUpdate']->setByIndices('id', $idxIdMap, /* $skipMissing = */ true);
952         
953         // filter exceptions marked as don't touch 
954         foreach ($migration['toUpdate'] as $toUpdate) {
955             if ($toUpdate->seq === -1) {
956                 $migration['toUpdate']->removeRecord($toUpdate);
957             }
958         }
959         
960         return $migration;
961     }
962     
963     /**
964      * prepares an exception instance for persistence
965      * 
966      * @param  Calendar_Model_Event $_baseEvent
967      * @param  Calendar_Model_Event $_exception
968      * @return void
969      * @throws Tinebase_Exception_InvalidArgument
970      */
971     protected function _prepareException(Calendar_Model_Event $_baseEvent, Calendar_Model_Event $_exception)
972     {
973         if (! $_baseEvent->uid) {
974             throw new Tinebase_Exception_InvalidArgument('base event has no uid');
975         }
976         
977         if ($_exception->is_deleted == false) {
978             $_exception->container_id = $_baseEvent->container_id;
979         }
980         $_exception->uid = $_baseEvent->uid;
981         $_exception->base_event_id = $_baseEvent->getId();
982         $_exception->recurid = $_baseEvent->uid . '-' . $_exception->getOriginalDtStart()->format(Tinebase_Record_Abstract::ISO8601LONG);
983         
984         // NOTE: we always refetch the base event as it might be touched in the meantime
985         $currBaseEvent = $this->_eventController->get($_baseEvent, null, false);
986         $_exception->last_modified_time = $currBaseEvent->last_modified_time;
987     }
988 }