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