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