784c4c7624e45ee77e46fe94726c47de7eeda920
[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' => 'uid',     'operator' => 'in',      'value' => $events->uid),
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(); // uid => baseEvent
249         $exceptionSets = array(); // uid => exceptions
250         $exceptionMap = array(); // idx => event
251
252         foreach($events as $event) {
253             if ($event->rrule) {
254                 $eventUid = $event->uid;
255                 $baseEventMap[$eventUid] = $event;
256                 $exceptionSets[$eventUid] = new Tinebase_Record_RecordSet('Calendar_Model_Event');
257             } else if ($event->recurid) {
258                 $exceptionMap[] = $event;
259             }
260         }
261
262         foreach($exceptionMap as $exception) {
263             $exceptionUid = $exception->uid;
264             $baseEvent = array_key_exists($exceptionUid, $baseEventMap) ? $baseEventMap[$exceptionUid] : false;
265             if ($baseEvent) {
266                 $exceptionSet = $exceptionSets[$exceptionUid];
267                 $exceptionSet->addRecord($exception);
268                 $events->removeRecord($exception);
269             }
270         }
271
272         foreach($baseEventMap as $uid => $baseEvent) {
273             $exceptionSet = $exceptionSets[$uid];
274             $this->_eventController->fakeDeletedExceptions($baseEvent, $exceptionSet);
275             $baseEvent->exdate = $exceptionSet;
276         }
277
278         return $events;
279     }
280     
281    /**
282      * (non-PHPdoc)
283      * @see Calendar_Controller_Event::lookupExistingEvent()
284      */
285     public function lookupExistingEvent($_event)
286     {
287         $event = $this->_eventController->lookupExistingEvent($_event);
288
289         if ($event) {
290             $this->_resolveData($event);
291             return $this->_toiTIP($event);
292         }
293     }
294     
295     /*************** add / update / delete *****************/    
296
297     /**
298      * add one record
299      *
300      * @param   Calendar_Model_Event $_event
301      * @return  Calendar_Model_Event
302      * @throws  Tinebase_Exception_AccessDenied
303      * @throws  Tinebase_Exception_Record_Validation
304      */
305     public function create(Tinebase_Record_Interface $_event)
306     {
307         if ($_event->recurid) {
308             throw new Tinebase_Exception_UnexpectedValue('recur event instances must be saved as part of the base event');
309         }
310         
311         $this->_fromiTIP($_event, new Calendar_Model_Event(array(), TRUE));
312         
313         $exceptions = $_event->exdate;
314         $_event->exdate = NULL;
315         
316         $_event->assertAttendee($this->getCalendarUser());
317         $savedEvent = $this->_eventController->create($_event);
318         
319         if ($exceptions instanceof Tinebase_Record_RecordSet) {
320             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
321                 . ' About to create ' . count($exceptions) . ' exdates for event ' . $_event->summary . ' (' . $_event->dtstart . ')');
322             
323             foreach ($exceptions as $exception) {
324                 $exception->assertAttendee($this->getCalendarUser());
325                 $this->_prepareException($savedEvent, $exception);
326                 $this->_eventController->createRecurException($exception, !!$exception->is_deleted);
327             }
328         }
329
330         $this->_resolveData($savedEvent);
331         return $this->_toiTIP($savedEvent);
332     }
333     
334     /**
335      * update one record
336      * 
337      * NOTE: clients might send their original (creation) data w.o. our adoptions for update
338      *       therefore we need reapply them
339      *       
340      * @param   Calendar_Model_Event $_event
341      * @param   bool                 $_checkBusyConflicts
342      * @return  Calendar_Model_Event
343      * @throws  Tinebase_Exception_AccessDenied
344      * @throws  Tinebase_Exception_Record_Validation
345      */
346     public function update(Tinebase_Record_Interface $_event, $_checkBusyConflicts = FALSE)
347     {
348         if ($_event->recurid) {
349             throw new Tinebase_Exception_UnexpectedValue('recur event instances must be saved as part of the base event');
350         }
351         $currentOriginEvent = $this->_eventController->get($_event->getId());
352         $this->_fromiTIP($_event, $currentOriginEvent);
353         
354         $_event->assertAttendee($this->getCalendarUser());
355         
356         $exceptions = $_event->exdate instanceof Tinebase_Record_RecordSet ? $_event->exdate : new Tinebase_Record_RecordSet('Calendar_Model_Event');
357         $exceptions->addIndices(array('is_deleted'));
358         
359         $currentPersistentExceptions = $_event->rrule ? $this->_eventController->getRecurExceptions($_event, FALSE) : new Tinebase_Record_RecordSet('Calendar_Model_Event');
360         $newPersistentExceptions = $exceptions->filter('is_deleted', 0);
361         
362         $migration = $this->_getExceptionsMigration($currentPersistentExceptions, $newPersistentExceptions);
363         
364         $this->_eventController->delete($migration['toDelete']->getId());
365         
366         // NOTE: we need to exclude the toCreate exdates here to not confuse computations in createRecurException!
367         $_event->exdate = array_diff($exceptions->getOriginalDtStart(), $migration['toCreate']->getOriginalDtStart());
368         $updatedBaseEvent = $this->_eventController->update($_event, $_checkBusyConflicts);
369         
370         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
371             . ' Found ' . count($migration['toCreate']) . ' exceptions to create and ' . count($migration['toUpdate']) . ' to update.');
372         
373         foreach ($migration['toCreate'] as $exception) {
374             $exception->assertAttendee($this->getCalendarUser());
375             $this->_prepareException($updatedBaseEvent, $exception);
376             $this->_eventController->createRecurException($exception, !!$exception->is_deleted);
377         }
378
379         $updatedExceptions = array();
380         foreach ($migration['toUpdate'] as $exception) {
381
382             if (in_array($exception->getId(),$updatedExceptions )) {
383                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' '
384                     . ' Exdate ' . $exception->getId() . ' already updated');
385                 continue;
386             }
387
388             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' '
389                 . ' Update exdate ' . $exception->getId() . ' at ' . $exception->dtstart->toString());
390             
391             $exception->assertAttendee($this->getCalendarUser());
392             $this->_prepareException($updatedBaseEvent, $exception);
393             $this->_addStatusAuthkeyForOwnAttender($exception);
394             
395             // skip concurrency check here by setting the seq of the current record
396             $currentException = $currentPersistentExceptions->getById($exception->getId());
397             $exception->seq = $currentException->seq;
398             
399             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
400                 . ' Updating exception: ' . print_r($exception->toArray(), TRUE));
401             $this->_eventController->update($exception, $_checkBusyConflicts);
402             $updatedExceptions[] = $exception->getId();
403         }
404         
405         // NOTE: we need to refetch here, otherwise eTag fail's as exception updates change baseEvents seq
406         return $this->get($updatedBaseEvent->getId());
407     }
408     
409     /**
410      * add status_authkey for own attender
411      * 
412      * @param Calendar_Model_Event $event
413      */
414     protected function _addStatusAuthkeyForOwnAttender($event)
415     {
416         if (! $event->attendee instanceof Tinebase_Record_RecordSet) {
417             return;
418         }
419         $ownAttender = Calendar_Model_Attender::getOwnAttender($event->attendee);
420         if ($ownAttender) {
421             $currentEvent = $this->_eventController->get($event->id);
422             $currentAttender = Calendar_Model_Attender::getAttendee($currentEvent->attendee, $ownAttender);
423             if ($currentAttender) {
424                 $ownAttender->status_authkey = $currentAttender->status_authkey;
425             } else {
426                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
427                     . ' currentAttender not found in currentEvent: ' . print_r($currentEvent->toArray(), true));
428             }
429         }
430     }
431     
432     /**
433      * asserts correct event filter and calendar user in MSEventFacade
434      * 
435      * NOTE: this is nessesary as MSEventFacade is a singleton and in some operations (e.g. move) there are 
436      *       multiple instances of self
437      */
438     public function assertEventFacadeParams(Tinebase_Model_Container $container, $setEventFilter=true)
439     {
440         if (!$this->_currentEventFacadeContainer ||
441              $this->_currentEventFacadeContainer->getId() !== $container->getId()
442         ) {
443             $this->_currentEventFacadeContainer = $container;
444
445             try {
446                 $calendarUserId = $container->type == Tinebase_Model_Container::TYPE_PERSONAL ?
447                 Addressbook_Controller_Contact::getInstance()->getContactByUserId($container->getOwner(), true)->getId() :
448                 Tinebase_Core::getUser()->contact_id;
449             } catch (Exception $e) {
450                 $calendarUserId = Calendar_Controller_MSEventFacade::getCurrentUserContactId();
451             }
452             
453             $calendarUser = new Calendar_Model_Attender(array(
454                 'user_type' => Calendar_Model_Attender::USERTYPE_USER,
455                 'user_id'   => $calendarUserId,
456             ));
457             
458
459             $this->setCalendarUser($calendarUser);
460
461             if ($setEventFilter) {
462                 $eventFilter = new Calendar_Model_EventFilter(array(
463                     array('field' => 'container_id', 'operator' => 'equals', 'value' => $container->getId())
464                 ));
465                 $this->setEventFilter($eventFilter);
466             }
467         }
468     }
469     
470     /**
471      * updates an attender status of a event
472      *
473      * @param  Calendar_Model_Event    $_event
474      * @param  Calendar_Model_Attender $_attendee
475      * @return Calendar_Model_Event    updated event
476      */
477     public function attenderStatusUpdate($_event, $_attendee)
478     {
479         if ($_event->recurid) {
480             throw new Tinebase_Exception_UnexpectedValue('recur event instances must be saved as part of the base event');
481         }
482         
483         $exceptions = $_event->exdate instanceof Tinebase_Record_RecordSet ? $_event->exdate : new Tinebase_Record_RecordSet('Calendar_Model_Event');
484         $_event->exdate = $exceptions->getOriginalDtStart();
485         
486         // update base event status
487         $attendeeFound = Calendar_Model_Attender::getAttendee($_event->attendee, $_attendee);
488         if (!isset($attendeeFound)) {
489             throw new Tinebase_Exception_UnexpectedValue('not an attendee');
490         }
491         $attendeeFound->displaycontainer_id = $_attendee->displaycontainer_id;
492         Calendar_Controller_Event::getInstance()->attenderStatusUpdate($_event, $attendeeFound, $attendeeFound->status_authkey);
493         
494         // update exceptions
495         foreach($exceptions as $exception) {
496             // do not attempt to set status of an deleted instance
497             if ($exception->is_deleted) continue;
498             
499             $exceptionAttendee = Calendar_Model_Attender::getAttendee($exception->attendee, $_attendee);
500             
501             if (! $exception->getId()) {
502                 if (! $exceptionAttendee) {
503                     // set user status to DECLINED
504                     $exceptionAttendee = clone $attendeeFound;
505                     $exceptionAttendee->status = Calendar_Model_Attender::STATUS_DECLINED;
506                 }
507                 $exceptionAttendee->displaycontainer_id = $_attendee->displaycontainer_id;
508                 Calendar_Controller_Event::getInstance()->attenderStatusCreateRecurException($exception, $exceptionAttendee, $exceptionAttendee->status_authkey);
509             } else {
510                 if (! $exceptionAttendee) {
511                     // we would need to find out the users authkey to decline him -> not allowed!?
512                     if (!isset($attendeeFound)) {
513                         throw new Tinebase_Exception_UnexpectedValue('not an attendee');
514                     }
515                 }
516                 $exceptionAttendee->displaycontainer_id = $_attendee->displaycontainer_id;
517                 Calendar_Controller_Event::getInstance()->attenderStatusUpdate($exception, $exceptionAttendee, $exceptionAttendee->status_authkey);
518             }
519         }
520         
521         return $this->get($_event->getId());
522     }
523     
524     /**
525      * update multiple records
526      * 
527      * @param   Tinebase_Model_Filter_FilterGroup $_filter
528      * @param   array $_data
529      * @return  integer number of updated records
530      */
531     public function updateMultiple($_what, $_data)
532     {
533         throw new Tinebase_Exception_NotImplemented('Calendar_Conroller_MSEventFacade::updateMultiple not yet implemented');
534     }
535     
536     /**
537      * Deletes a set of records.
538      * 
539      * If one of the records could not be deleted, no record is deleted
540      * 
541      * @param   array array of record identifiers
542      * @return  Tinebase_Record_RecordSet
543      */
544     public function delete($_ids)
545     {
546         $ids = array_unique((array)$_ids);
547         $events = $this->getMultiple($ids);
548         
549         foreach ($events as $event) {
550             if ($event->exdate !== null) {
551                 foreach ($event->exdate as $exception) {
552                     $exceptionId = $exception->getId();
553                     if ($exceptionId) {
554                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
555                             . ' Found exdate to be deleted (id: ' . $exceptionId . ')');
556                         array_unshift($ids, $exceptionId);
557                     }
558                 }
559             }
560         }
561         
562         $this->_eventController->delete($ids);
563         return $events;
564     }
565     
566     /**
567      * get and resolve all alarms of given record(s)
568      * 
569      * @param  Tinebase_Record_Interface|Tinebase_Record_RecordSet $_record
570      */
571     public function getAlarms($_record)
572     {
573         $events = $_record instanceof Tinebase_Record_RecordSet ? $_record->getClone(true) : new Tinebase_Record_RecordSet('Calendar_Model_Event', array($_record));
574         
575         foreach($events as $event) {
576             if ($event->exdate instanceof Tinebase_Record_RecordSet) {
577 //                 $event->exdate->addIndices(array('is_deleted'));
578                 $events->merge($event->exdate->filter('is_deleted', 0));
579             }
580         }
581         
582         $this->_eventController->getAlarms($events);
583     }
584     
585     /**
586      * set displaycontainer for given attendee 
587      * 
588      * @param Calendar_Model_Event    $_event
589      * @param string                  $_container
590      * @param Calendar_Model_Attender $_attendee    defaults to calendarUser
591      */
592     public function setDisplaycontainer($_event, $_container, $_attendee = NULL)
593     {
594         if ($_event->exdate instanceof Tinebase_Record_RecordSet) {
595             foreach ($_event->exdate as $idx => $exdate) {
596                 self::setDisplaycontainer($exdate, $_container, $_attendee);
597             }
598         }
599         
600         $attendeeRecord = Calendar_Model_Attender::getAttendee($_event->attendee, $_attendee ? $_attendee : $this->getCalendarUser());
601         
602         if ($attendeeRecord) {
603             $attendeeRecord->displaycontainer_id = $_container;
604         }
605     }
606     
607     /**
608      * sets current calendar user
609      * 
610      * @param Calendar_Model_Attender $_calUser
611      * @return Calendar_Model_Attender oldUser
612      */
613     public function setCalendarUser(Calendar_Model_Attender $_calUser)
614     {
615         if (! in_array($_calUser->user_type, array(Calendar_Model_Attender::USERTYPE_USER, Calendar_Model_Attender::USERTYPE_GROUPMEMBER))) {
616             throw new Tinebase_Exception_UnexpectedValue('Calendar user must be a contact');
617         }
618         $oldUser = $this->_calendarUser;
619         $this->_calendarUser = $_calUser;
620         $this->_eventController->setCalendarUser($_calUser);
621         
622         return $oldUser;
623     }
624     
625     /**
626      * get current calendar user
627      * 
628      * @return Calendar_Model_Attender
629      */
630     public function getCalendarUser()
631     {
632         return $this->_calendarUser;
633     }
634     
635     /**
636      * set current event filter for exdate computations
637      * 
638      * @param  Calendar_Model_EventFilter
639      * @return Calendar_Model_EventFilter
640      */
641     public function setEventFilter($_filter)
642     {
643         $oldFilter = $this->_eventFilter;
644         
645         if ($_filter !== NULL) {
646             if (! $_filter instanceof Calendar_Model_EventFilter) {
647                 throw new Tinebase_Exception_UnexpectedValue('not a valid filter');
648             }
649             $this->_eventFilter = clone $_filter;
650             
651             $periodFilters = $this->_eventFilter->getFilter('period', TRUE, TRUE);
652             foreach((array) $periodFilters as $periodFilter) {
653                 $periodFilter->setDisabled();
654             }
655         } else {
656             $this->_eventFilter = NULL;
657         }
658         
659         return $oldFilter;
660     }
661     
662     /**
663      * get current event filter
664      * 
665      * @return Calendar_Model_EventFilter
666      */
667     public function getEventFilter()
668     {
669         return $this->_eventFilter;
670     }
671     
672     /**
673      * filters given eventset for events with matching dtstart
674      * 
675      * @param Tinebase_Record_RecordSet $_events
676      * @param array                     $_dtstarts
677      */
678     protected function _filterEventsByDTStarts($_events, $_dtstarts)
679     {
680         $filteredSet = new Tinebase_Record_RecordSet('Calendar_Model_Event');
681         $allDTStarts = $_events->getOriginalDtStart();
682         
683         $existingIdxs = array_intersect($allDTStarts, $_dtstarts);
684         
685         foreach($existingIdxs as $idx => $dtstart) {
686             $filteredSet->addRecord($_events[$idx]);
687         }
688         
689         return $filteredSet;
690     }
691
692     protected function _resolveData($events) {
693         $eventSet = $events instanceof Tinebase_Record_RecordSet
694             ? $events->getClone(true)
695             : new Tinebase_Record_RecordSet('Calendar_Model_Event', array($events));
696
697         // get recur exceptions
698         foreach($eventSet as $event) {
699             if ($event->rrule && !$event->exdate instanceof Tinebase_Record_RecordSet) {
700                 $exdates = $this->_eventController->getRecurExceptions($event, TRUE, $this->getEventFilter());
701                 $event->exdate = $exdates;
702                 $eventSet->merge($exdates);
703             }
704         }
705
706         $this->_eventController->getAlarms($eventSet);
707         Tinebase_FileSystem_RecordAttachments::getInstance()->getMultipleAttachmentsOfRecords($eventSet);
708     }
709
710     /**
711      * converts a tine20 event to an iTIP event
712      * 
713      * @param  Calendar_Model_Event $_event - must have exceptions, alarms & attachements resovled
714      * @return Calendar_Model_Event 
715      */
716     protected function _toiTIP($_event)
717     {
718         $events = $_event instanceof Tinebase_Record_RecordSet
719             ? $_event
720             : new Tinebase_Record_RecordSet('Calendar_Model_Event', array($_event));
721
722         foreach ($events as $idx => $event) {
723             // get exdates
724             if ($event->getId() && $event->rrule) {
725                 $this->_toiTIP($event->exdate);
726             }
727
728             $this->_filterAttendeeWithoutEmail($event);
729             
730             $CUAttendee = Calendar_Model_Attender::getAttendee($event->attendee, $this->_calendarUser);
731             $isOrganizer = $event->isOrganizer($this->_calendarUser);
732             
733             // apply perspective
734             if ($CUAttendee && !$isOrganizer) {
735                 $event->transp = $CUAttendee->transp ? $CUAttendee->transp : $event->transp;
736             }
737             
738             if ($event->alarms instanceof Tinebase_Record_RecordSet) {
739                 foreach($event->alarms as $alarm) {
740                     if (! Calendar_Model_Attender::isAlarmForAttendee($this->_calendarUser, $alarm, $event)) {
741                         $event->alarms->removeRecord($alarm);
742                     }
743                 }
744             }
745         }
746         
747         return $_event;
748     }
749     
750     /**
751      * filter out attendee w.o. email
752      * 
753      * @param Calendar_Model_Event $event
754      */
755     protected function _filterAttendeeWithoutEmail($event)
756     {
757         if (! $event->attendee instanceof Tinebase_Record_RecordSet) {
758             return;
759         }
760         
761         foreach ($event->attendee as $attender) {
762             $cacheId = $attender->user_type . $attender->user_id;
763             
764             // value is in array and true
765             if (isset(self::$_attendeeEmailCache[$cacheId])) {
766                 continue;
767             }
768             
769             // add value to cache if not existing already
770             if (!array_key_exists($cacheId, self::$_attendeeEmailCache)) {
771                 $this->_fillResolvedAttendeeCache($event);
772                 
773                 self::$_attendeeEmailCache[$cacheId] = !!$attender->getEmail();
774                 
775                 // limit class cache to 100 entries
776                 if (count(self::$_attendeeEmailCache) > 100) {
777                     array_shift(self::$_attendeeEmailCache);
778                 }
779             }
780             
781             // remove entry if value is not true => attender has no email address
782             if (!self::$_attendeeEmailCache[$cacheId]) {
783                 $event->attendee->removeRecord($attender);
784             }
785         }
786     }
787
788     /**
789      * re add attendee w.o. email
790      * 
791      * @param Calendar_Model_Event $event
792      */
793     protected function _addAttendeeWithoutEmail($event, $currentEvent)
794     {
795         if (! $currentEvent->attendee instanceof Tinebase_Record_RecordSet) {
796             return;
797         }
798         $this->_fillResolvedAttendeeCache($currentEvent);
799         
800         if (! $event->attendee instanceof Tinebase_Record_RecordSet) {
801             $event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
802         }
803         foreach ($currentEvent->attendee->getEmail() as $idx => $email) {
804             if (! $email) {
805                 $event->attendee->addRecord($currentEvent->attendee[$idx]);
806             }
807         }
808     }
809     
810     /**
811      * this fills the resolved attendee cache without changing the event attendee recordset
812      * 
813      * @param Calendar_Model_Event $event
814      */
815     protected function _fillResolvedAttendeeCache($event)
816     {
817         if (! $event->attendee instanceof Tinebase_Record_RecordSet) {
818             return;
819         }
820         
821         Calendar_Model_Attender::fillResolvedAttendeesCache($event->attendee);
822     }
823     
824     /**
825      * converts an iTIP event to a tine20 event
826      * 
827      * @param Calendar_Model_Event $_event
828      * @param Calendar_Model_Event $_currentEvent (not iTIP!)
829      */
830     protected function _fromiTIP($_event, $_currentEvent)
831     {
832         if (! $_event->rrule) {
833             $_event->exdate = NULL;
834         }
835         
836         if ($_event->exdate instanceof Tinebase_Record_RecordSet) {
837             
838             try{
839                 $currExdates = $this->_eventController->getRecurExceptions($_event, TRUE);
840                 $this->getAlarms($currExdates);
841                 $currClientExdates = $this->_eventController->getRecurExceptions($_event, TRUE, $this->getEventFilter());
842                 $this->getAlarms($currClientExdates);
843             } catch (Tinebase_Exception_NotFound $e) {
844                 $currExdates = NULL;
845                 $currClientExdates = NULL; 
846             }
847             
848             foreach ($_event->exdate as $idx => $exdate) {
849                 try {
850                     $this->_prepareException($_event, $exdate);
851                 } catch (Exception $e){}
852
853                 $currExdate = $currExdates instanceof Tinebase_Record_RecordSet ? $currExdates->filter('recurid', $exdate->recurid)->getFirstRecord() : NULL;
854                 
855                 
856                 if ($exdate->is_deleted) {
857                     // reset implicit filter fallouts and mark as don't touch (seq = -1)
858                     $currClientExdate = $currClientExdates instanceof Tinebase_Record_RecordSet ? $currClientExdates->filter('recurid', $exdate->recurid)->getFirstRecord() : NULL;
859                     if ($currClientExdate && $currClientExdate->is_deleted) {
860                         $_event->exdate[$idx] = $currExdate;
861                         $currExdate->seq = -1;
862                         continue;
863                     }
864                 }
865                 $this->_fromiTIP($exdate, $currExdate ? $currExdate : clone $_currentEvent);
866             }
867         }
868         
869         // assert organizer
870         $_event->organizer = $_event->organizer ?: ($_currentEvent->organizer ?: $this->_calendarUser->user_id);
871
872         $this->_addAttendeeWithoutEmail($_event, $_currentEvent);
873         
874         $CUAttendee = Calendar_Model_Attender::getAttendee($_event->attendee, $this->_calendarUser);
875         $currentCUAttendee  = Calendar_Model_Attender::getAttendee($_currentEvent->attendee, $this->_calendarUser);
876         $isOrganizer = $_event->isOrganizer($this->_calendarUser);
877         
878         // remove perspective 
879         if ($CUAttendee && !$isOrganizer) {
880             $CUAttendee->transp = $_event->transp;
881             $_event->transp = $_currentEvent->transp ? $_currentEvent->transp : $_event->transp;
882         }
883         
884         // apply changes to original alarms
885         $_currentEvent->alarms  = $_currentEvent->alarms instanceof Tinebase_Record_RecordSet ? $_currentEvent->alarms : new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
886         $_event->alarms  = $_event->alarms instanceof Tinebase_Record_RecordSet ? $_event->alarms : new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
887         
888         foreach($_currentEvent->alarms as $currentAlarm) {
889             if (Calendar_Model_Attender::isAlarmForAttendee($this->_calendarUser, $currentAlarm)) {
890                 $alarmUpdate = Calendar_Controller_Alarm::getMatchingAlarm($_event->alarms, $currentAlarm);
891                 
892                 if ($alarmUpdate) {
893                     // we could map the alarm => save ack & snooze options
894                     if ($dtAck = Calendar_Controller_Alarm::getAcknowledgeTime($alarmUpdate)) {
895                         Calendar_Controller_Alarm::setAcknowledgeTime($currentAlarm, $dtAck, $this->getCalendarUser()->user_id);
896                     }
897                     if ($dtSnooze = Calendar_Controller_Alarm::getSnoozeTime($alarmUpdate)) {
898                         Calendar_Controller_Alarm::setSnoozeTime($currentAlarm, $dtSnooze, $this->getCalendarUser()->user_id);
899                     }
900                     $_event->alarms->removeRecord($alarmUpdate);
901                 } else {
902                     // alarm is to be skiped/deleted
903                     if (! $currentAlarm->getOption('attendee')) {
904                         Calendar_Controller_Alarm::skipAlarm($currentAlarm, $this->_calendarUser);
905                     } else {
906                         $_currentEvent->alarms->removeRecord($currentAlarm);
907                     }
908                 }
909             }
910         }
911         if (! $isOrganizer) {
912             $_event->alarms->setOption('attendee', Calendar_Controller_Alarm::attendeeToOption($this->_calendarUser));
913         }
914         $_event->alarms->merge($_currentEvent->alarms);
915
916         // assert organizer for personal calendars to be calendar owner
917         if ($this->_currentEventFacadeContainer && $this->_currentEventFacadeContainer->getId() == $_event->container_id
918             && $this->_currentEventFacadeContainer->type == Tinebase_Model_Container::TYPE_PERSONAL
919             && !$_event->hasExternalOrganizer() ) {
920
921             $_event->organizer = $this->_calendarUser->user_id;
922         }
923         // in MS world only cal_user can do status updates
924         if ($CUAttendee) {
925             $CUAttendee->status_authkey = $currentCUAttendee ? $currentCUAttendee->status_authkey : NULL;
926         }
927     }
928     
929     /**
930      * computes an returns the migration for event exceptions
931      * 
932      * @param Tinebase_Record_RecordSet $_currentPersistentExceptions
933      * @param Tinebase_Record_RecordSet $_newPersistentExceptions
934      */
935     protected function _getExceptionsMigration($_currentPersistentExceptions, $_newPersistentExceptions)
936     {
937         $migration = array();
938         
939         // add indices and sort to speedup things
940         $_currentPersistentExceptions->addIndices(array('dtstart'))->sort('dtstart');
941         $_newPersistentExceptions->addIndices(array('dtstart'))->sort('dtstart');
942         
943         // get dtstarts
944         $currDtStart = $_currentPersistentExceptions->getOriginalDtStart();
945         $newDtStart = $_newPersistentExceptions->getOriginalDtStart();
946         
947         // compute migration in terms of dtstart
948         $toDeleteDtStart = array_diff($currDtStart, $newDtStart);
949         $toCreateDtStart = array_diff($newDtStart, $currDtStart);
950         $toUpdateDtSTart = array_intersect($currDtStart, $newDtStart);
951         
952         $migration['toDelete'] = $this->_filterEventsByDTStarts($_currentPersistentExceptions, $toDeleteDtStart);
953         $migration['toCreate'] = $this->_filterEventsByDTStarts($_newPersistentExceptions, $toCreateDtStart);
954         $migration['toUpdate'] = $this->_filterEventsByDTStarts($_newPersistentExceptions, $toUpdateDtSTart);
955         
956         // get ids for toUpdate
957         $idxIdMap = $this->_filterEventsByDTStarts($_currentPersistentExceptions, $toUpdateDtSTart)->getId();
958         $migration['toUpdate']->setByIndices('id', $idxIdMap, /* $skipMissing = */ true);
959         
960         // filter exceptions marked as don't touch 
961         foreach ($migration['toUpdate'] as $toUpdate) {
962             if ($toUpdate->seq === -1) {
963                 $migration['toUpdate']->removeRecord($toUpdate);
964             }
965         }
966         
967         return $migration;
968     }
969     
970     /**
971      * prepares an exception instance for persistence
972      * 
973      * @param  Calendar_Model_Event $_baseEvent
974      * @param  Calendar_Model_Event $_exception
975      * @return void
976      * @throws Tinebase_Exception_InvalidArgument
977      */
978     protected function _prepareException(Calendar_Model_Event $_baseEvent, Calendar_Model_Event $_exception)
979     {
980         if (! $_baseEvent->uid) {
981             throw new Tinebase_Exception_InvalidArgument('base event has no uid');
982         }
983         
984         if ($_exception->is_deleted == false) {
985             $_exception->container_id = $_baseEvent->container_id;
986         }
987         $_exception->uid = $_baseEvent->uid;
988         $_exception->recurid = $_baseEvent->uid . '-' . $_exception->getOriginalDtStart()->format(Tinebase_Record_Abstract::ISO8601LONG);
989         
990         // NOTE: we always refetch the base event as it might be touched in the meantime
991         $currBaseEvent = $this->_eventController->get($_baseEvent, null, false);
992         $_exception->last_modified_time = $currBaseEvent->last_modified_time;
993     }
994 }