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