758e3f631103b41fdc6cf0b1291f6d57b1af960b
[tine20] / tine20 / Calendar / Controller / Event.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Calendar
6  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
7  * @author      Cornelius Weiss <c.weiss@metaways.de>
8  * @copyright   Copyright (c) 2010-2012 Metaways Infosystems GmbH (http://www.metaways.de)
9  */
10
11 /**
12  * Calendar Event Controller
13  * 
14  * In the calendar application, the container grants concept is slightly extended:
15  *  1. GRANTS for events are not only based on the events "calendar" (technically 
16  *     a container) but additionally a USER gets implicit grants for an event if 
17  *     he is ATTENDER (+READ GRANT) or ORGANIZER (+READ,EDIT GRANT).
18  *  2. ATTENDER which are invited to a certain "event" can assign the "event" to
19  *     one of their personal calenders as "display calendar" (technically personal 
20  *     containers they are admin of). The "display calendar" of an ATTENDER is
21  *     stored in the attendee table.  Each USER has a default calendar, as 
22  *     PREFERENCE,  all invitations are assigned to.
23  *  3. The "effective GRANT" a USER has on an event (read/update/delete/...) is the 
24  *     maximum GRANT of the following sources: 
25  *      - container: GRANT the USER has to the calender of the event
26  *      - implicit:  Additional READ GRANT for an attender and READ,EDIT
27  *                   GRANT for the organizer.
28 *       - inherited: FREEBUSY, READ, PRIVATE, SYNC, EXPORT can be inherited
29 *                    from the GRANTS USER has to the a display calendar
30  * 
31  * When Applying/Asuring grants, we have to deal with two differnt situations:
32  *  A: Check: Check individual grants on a event (record) basis.
33  *            This is required for create/update/delete actions and done by 
34  *            this controllers _checkGrant method.
35  *  B: Seach: From the grants perspective this is a multi step process
36  *            1. fetch all records with appropriate grants from backend
37  *            2. cleanup records user has only free/busy grant for
38  * 
39  *  NOTE: To empower the client for enabling/disabling of actions based on the 
40  *        grants a user has to an event, we need to compute the "effective GRANT"
41  *        for read/search operations.
42  *                  
43  * Case A is not critical, as the amount of data is low. 
44  * Case B however is the hard one, as lots of events and calendars may be
45  * involved.
46  * 
47  * NOTE: the backend always fetches full records for grant calculations.
48  *       searching ids only does not hlep with performance
49  * 
50  * @package Calendar
51  */
52 class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract implements Tinebase_Controller_Alarm_Interface
53 {
54     /**
55      * @var boolean
56      * 
57      * just set is_delete=1 if record is going to be deleted
58      */
59     protected $_purgeRecords = FALSE;
60     
61     /**
62      * send notifications?
63      *
64      * @var boolean
65      */
66     protected $_sendNotifications = TRUE;
67     
68     /**
69      * @var Calendar_Controller_Event
70      */
71     private static $_instance = NULL;
72     
73     /**
74      * the constructor
75      *
76      * don't use the constructor. use the singleton 
77      */
78     private function __construct()
79     {
80         $this->_applicationName = 'Calendar';
81         $this->_modelName       = 'Calendar_Model_Event';
82         $this->_backend         = new Calendar_Backend_Sql();
83     }
84
85     /**
86      * don't clone. Use the singleton.
87      */
88     private function __clone() 
89     {
90         
91     }
92     
93     /**
94      * singleton
95      *
96      * @return Calendar_Controller_Event
97      */
98     public static function getInstance() 
99     {
100         if (self::$_instance === NULL) {
101             self::$_instance = new Calendar_Controller_Event();
102         }
103         return self::$_instance;
104     }
105     
106     /**
107      * checks if all attendee of given event are not busy for given event
108      * 
109      * @param Calendar_Model_Event $_event
110      * @return void
111      * @throws Calendar_Exception_AttendeeBusy
112      */
113     public function checkBusyConflicts($_event)
114     {
115         $ignoreUIDs = !empty($_event->uid) ? array($_event->uid) : array();
116         
117         // 
118         if ($_event->transp == Calendar_Model_Event::TRANSP_TRANSP || count($_event->attendee) < 1) {
119             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
120                 . " Skipping free/busy check because event is transparent");
121             return;
122         }
123         
124         $eventSet = new Tinebase_Record_RecordSet('Calendar_Model_Event', array($_event));
125         
126         if (! empty($_event->rrule)) {
127             $checkUntil = clone $_event->dtstart;
128             $checkUntil->add(1, Tinebase_DateTime::MODIFIER_MONTH);
129             Calendar_Model_Rrule::mergeRecurrenceSet($eventSet, $_event->dtstart, $checkUntil);
130         }
131         
132         $periods = array();
133         foreach ($eventSet as $event) {
134             $periods[] = array('from' => $event->dtstart, 'until' => $event->dtend);
135         }
136         
137         $fbInfo = $this->getFreeBusyInfo($periods, $_event->attendee, $ignoreUIDs);
138         
139         if (count($fbInfo) > 0) {
140             $busyException = new Calendar_Exception_AttendeeBusy();
141             $busyException->setFreeBusyInfo($fbInfo);
142             
143             Calendar_Model_Attender::resolveAttendee($_event->attendee, FALSE);
144             $busyException->setEvent($_event);
145             
146             throw $busyException;
147         }
148         
149         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
150             . " Free/busy check: no conflict found");
151     }
152     
153     /**
154      * add one record
155      *
156      * @param   Tinebase_Record_Interface $_record
157      * @param   bool                      $_checkBusyConflicts
158      * @return  Tinebase_Record_Interface
159      * @throws  Tinebase_Exception_AccessDenied
160      * @throws  Tinebase_Exception_Record_Validation
161      */
162     public function create(Tinebase_Record_Interface $_record, $_checkBusyConflicts = FALSE)
163     {
164         try {
165             $db = $this->_backend->getAdapter();
166             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
167             
168             $this->_inspectEvent($_record);
169             
170             // we need to resolve groupmembers before free/busy checking
171             Calendar_Model_Attender::resolveGroupMembers($_record->attendee);
172             
173             if ($_checkBusyConflicts) {
174                 // ensure that all attendee are free
175                 $this->checkBusyConflicts($_record);
176             }
177             
178             $sendNotifications = $this->_sendNotifications;
179             $this->_sendNotifications = FALSE;
180             
181             $createdEvent = parent::create($_record);
182             
183             $this->_sendNotifications = $sendNotifications;
184             
185             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
186         } catch (Exception $e) {
187             Tinebase_TransactionManager::getInstance()->rollBack();
188             throw $e;
189         }
190         
191         // send notifications
192         if ($this->_sendNotifications) {
193             $this->doSendNotifications($createdEvent, Tinebase_Core::getUser(), 'created');
194         }
195         
196         return $createdEvent;
197     }
198     
199     /**
200      * inspect creation of one record (after create)
201      *
202      * @param   Tinebase_Record_Interface $_createdRecord
203      * @param   Tinebase_Record_Interface $_record
204      * @return  void
205      */
206     protected function _inspectAfterCreate($_createdRecord, Tinebase_Record_Interface $_record)
207     {
208         $this->_saveAttendee($_record, $_createdRecord);
209     }
210     
211     /**
212      * deletes a recur series
213      *
214      * @param  Calendar_Model_Event $_recurInstance
215      * @return void
216      */
217     public function deleteRecurSeries($_recurInstance)
218     {
219         $baseEvent = $this->getRecurBaseEvent($_recurInstance);
220         $this->delete($baseEvent->getId());
221     }
222     
223     /**
224      * Gets all entries
225      *
226      * @param string $_orderBy Order result by
227      * @param string $_orderDirection Order direction - allowed are ASC and DESC
228      * @throws Tinebase_Exception_InvalidArgument
229      * @return Tinebase_Record_RecordSet
230      */
231     public function getAll($_orderBy = 'id', $_orderDirection = 'ASC') 
232     {
233         throw new Tinebase_Exception_NotImplemented('not implemented');
234     }
235     
236     /**
237      * returns freebusy information for given period and given attendee
238      * 
239      * @todo merge overlapping events to one freebusy entry
240      * 
241      * @param  array of array with from and until                   $_periods
242      * @param  Tinebase_Record_RecordSet of Calendar_Model_Attender $_attendee
243      * @param  array of UIDs                                        $_ignoreUIDs
244      * @return Tinebase_Record_RecordSet of Calendar_Model_FreeBusy
245      */
246     public function getFreeBusyInfo($_periods, $_attendee, $_ignoreUIDs = array())
247     {
248         $fbInfoSet = new Tinebase_Record_RecordSet('Calendar_Model_FreeBusy');
249         
250         // map groupmembers to users
251         $attendee = clone $_attendee;
252         $attendee->addIndices(array('user_type'));
253         $groupmembers = $attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
254         $groupmembers->user_type = Calendar_Model_Attender::USERTYPE_USER;
255         
256         // base filter data
257         $filterData = array(
258             array('field' => 'attender', 'operator' => 'in',     'value' => $_attendee),
259             array('field' => 'transp',   'operator' => 'equals', 'value' => Calendar_Model_Event::TRANSP_OPAQUE)
260         );
261         
262         // add all periods to filterdata
263         $periodFilters = array();
264         foreach ($_periods as $period) {
265             $periodFilters[] = array(
266                 'field' => 'period', 
267                 'operator' => 'within', 
268                 'value' => array(
269                     'from' => $period['from'], 
270                     'until' => $period['until']
271             ));
272         }
273         $filterData[] = array('condition' => 'OR', 'filters' => $periodFilters);
274         
275         // finaly create filter
276         $filter = new Calendar_Model_EventFilter($filterData);
277         
278         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . ' ' . __LINE__
279             . ' free/busy fitler: ' . print_r($filter->toArray(), true));
280         
281         $events = $this->search($filter, new Tinebase_Model_Pagination(), FALSE, FALSE);
282         
283         foreach ($_periods as $period) {
284             Calendar_Model_Rrule::mergeRecurrenceSet($events, $period['from'], $period['until']);
285         }
286         
287         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . ' ' . __LINE__
288             . ' value: ' . print_r($events->toArray(), true));
289         
290         // create a typemap
291         $typeMap = array();
292         foreach($attendee as $attender) {
293             if (! (isset($typeMap[$attender['user_type']]) || array_key_exists($attender['user_type'], $typeMap))) {
294                 $typeMap[$attender['user_type']] = array();
295             }
296             
297             $typeMap[$attender['user_type']][$attender['user_id']] = array();
298         }
299         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . ' ' . __LINE__
300             . ' value: ' . print_r($typeMap, true));
301         
302         // generate freeBusyInfos
303         foreach ($events as $event) {
304             // skip events with ignoreUID
305             if (in_array($event->uid, $_ignoreUIDs)) {
306                 continue;
307             }
308             
309             // check if event is conflicting one of the given periods
310             $conflicts = FALSE;
311             foreach($_periods as $period) {
312                 if ($event->dtstart->isEarlier($period['until']) && $event->dtend->isLater($period['from'])) {
313                     $conflicts = TRUE;
314                     break;
315                 }
316             }
317             if (! $conflicts) {
318                 continue;
319             }
320             
321             // map groupmembers to users
322             $event->attendee->addIndices(array('user_type'));
323             $groupmembers = $event->attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
324             $groupmembers->user_type = Calendar_Model_Attender::USERTYPE_USER;
325         
326             foreach ($event->attendee as $attender) {
327                 // skip declined/transp events
328                 if ($attender->status == Calendar_Model_Attender::STATUS_DECLINED ||
329                     $attender->transp == Calendar_Model_Event::TRANSP_TRANSP) {
330                     continue;
331                 }
332                 
333                 if ((isset($typeMap[$attender->user_type]) || array_key_exists($attender->user_type, $typeMap)) && (isset($typeMap[$attender->user_type][$attender->user_id]) || array_key_exists($attender->user_id, $typeMap[$attender->user_type]))) {
334                     $fbInfo = new Calendar_Model_FreeBusy(array(
335                         'user_type' => $attender->user_type,
336                         'user_id'   => $attender->user_id,
337                         'dtstart'   => clone $event->dtstart,
338                         'dtend'     => clone $event->dtend,
339                         'type'      => Calendar_Model_FreeBusy::FREEBUSY_BUSY,
340                     ), true);
341                     
342                     if ($event->{Tinebase_Model_Grants::GRANT_READ}) {
343                         $fbInfo->event = clone $event;
344                         unset($fbInfo->event->attendee);
345                     }
346                     
347                     //$typeMap[$attender->user_type][$attender->user_id][] = $fbInfo;
348                     $fbInfoSet->addRecord($fbInfo);
349                 }
350             }
351         }
352         
353         return $fbInfoSet;
354     }
355     
356     /**
357      * get list of records
358      *
359      * @param Tinebase_Model_Filter_FilterGroup|optional    $_filter
360      * @param Tinebase_Model_Pagination|optional            $_pagination
361      * @param bool                                          $_getRelations
362      * @param boolean                                       $_onlyIds
363      * @param string                                        $_action for right/acl check
364      * @return Tinebase_Record_RecordSet|array
365      */
366     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
367     {
368         $events = parent::search($_filter, $_pagination, $_getRelations, $_onlyIds, $_action);
369         if (! $_onlyIds) {
370             $this->_freeBusyCleanup($events, $_action);
371         }
372         
373         return $events;
374     }
375     
376     /**
377      * Returns a set of records identified by their id's
378      *
379      * @param   array $_ids       array of record identifiers
380      * @param   bool  $_ignoreACL don't check acl grants
381      * @return  Tinebase_Record_RecordSet of $this->_modelName
382      */
383     public function getMultiple($_ids, $_ignoreACL = false)
384     {
385         $events = parent::getMultiple($_ids, $_ignoreACL = false);
386         if ($_ignoreACL !== true) {
387             $this->_freeBusyCleanup($events, 'get');
388         }
389         
390         return $events;
391     }
392     
393     /**
394      * cleanup search results (freebusy)
395      * 
396      * @param Tinebase_Record_RecordSet $_events
397      * @param string $_action
398      */
399     protected function _freeBusyCleanup(Tinebase_Record_RecordSet $_events, $_action)
400     {
401         foreach ($_events as $event) {
402             $doFreeBusyCleanup = $event->doFreeBusyCleanup();
403             if ($doFreeBusyCleanup && $_action !== 'get') {
404                 $_events->removeRecord($event);
405             }
406         }
407     }
408     
409     /**
410      * returns freeTime (suggestions) for given period of given attendee
411      * 
412      * @param  Tinebase_DateTime                                            $_from
413      * @param  Tinebase_DateTime                                            $_until
414      * @param  Tinebase_Record_RecordSet of Calendar_Model_Attender $_attendee
415      * 
416      * ...
417      */
418     public function searchFreeTime($_from, $_until, $_attendee/*, $_constains, $_mode*/)
419     {
420         $fbInfoSet = $this->getFreeBusyInfo(array(array('from' => $_from, 'until' => $_until)), $_attendee);
421         
422 //        $fromTs = $_from->getTimestamp();
423 //        $untilTs = $_until->getTimestamp();
424 //        $granularity = 1800;
425 //        
426 //        // init registry of granularity
427 //        $eventRegistry = array_combine(range($fromTs, $untilTs, $granularity), array_fill(0, ceil(($untilTs - $fromTs)/$granularity)+1, ''));
428 //        
429 //        foreach ($fbInfoSet as $fbInfo) {
430 //            $startIdx = $fromTs + $granularity * floor(($fbInfo->dtstart->getTimestamp() - $fromTs) / $granularity);
431 //            $endIdx = $fromTs + $granularity * ceil(($fbInfo->dtend->getTimestamp() - $fromTs) / $granularity);
432 //            
433 //            for ($idx=$startIdx; $idx<=$endIdx; $idx+=$granularity) {
434 //                //$eventRegistry[$idx][] = $fbInfo;
435 //                $eventRegistry[$idx] .= '.';
436 //            }
437 //        }
438         
439         //print_r($eventRegistry);
440     }
441     
442     /**
443      * update one record
444      *
445      * @param   Tinebase_Record_Interface $_record
446      * @param   bool                      $_checkBusyConflicts
447      * @param   string                    $range
448      * @return  Tinebase_Record_Interface
449      * @throws  Tinebase_Exception_AccessDenied
450      * @throws  Tinebase_Exception_Record_Validation
451      */
452     public function update(Tinebase_Record_Interface $_record, $_checkBusyConflicts = FALSE, $range = Calendar_Model_Event::RANGE_THIS)
453     {
454         try {
455             $db = $this->_backend->getAdapter();
456             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
457             
458             $sendNotifications = $this->sendNotifications(FALSE);
459             
460             $event = $this->get($_record->getId());
461             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .' Going to update the following event. rawdata: ' . print_r($event->toArray(), true));
462             //NOTE we check via get(full rights) here whereas _updateACLCheck later checks limited rights from search
463             if ($this->_doContainerACLChecks === FALSE || $event->hasGrant(Tinebase_Model_Grants::GRANT_EDIT)) {
464                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " updating event: {$_record->id} (range: {$range})");
465                 
466                 // we need to resolve groupmembers before free/busy checking
467                 Calendar_Model_Attender::resolveGroupMembers($_record->attendee);
468                 $this->_inspectEvent($_record);
469                
470                 if ($_checkBusyConflicts) {
471                     if ($event->isRescheduled($_record) ||
472                            count(array_diff($_record->attendee->user_id, $event->attendee->user_id)) > 0
473                        ) {
474                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
475                             . " Ensure that all attendee are free with free/busy check ... ");
476                         $this->checkBusyConflicts($_record);
477                     } else {
478                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
479                             . " Skipping free/busy check because event has not been rescheduled and no new attender has been added");
480                     }
481                 }
482                 
483                 parent::update($_record);
484                 
485             } else if ($_record->attendee instanceof Tinebase_Record_RecordSet) {
486                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
487                     . " user has no editGrant for event: {$_record->id}, updating attendee status with valid authKey only");
488                 foreach ($_record->attendee as $attender) {
489                     if ($attender->status_authkey) {
490                         $this->attenderStatusUpdate($_record, $attender, $attender->status_authkey);
491                     }
492                 }
493             }
494             
495             if ($_record->isRecurException() && in_array($range, array(Calendar_Model_Event::RANGE_ALL, Calendar_Model_Event::RANGE_THISANDFUTURE))) {
496                 $this->_updateExdateRange($_record, $range, $event);
497             }
498             
499             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
500         } catch (Exception $e) {
501             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Rolling back because: ' . $e);
502             Tinebase_TransactionManager::getInstance()->rollBack();
503             $this->sendNotifications($sendNotifications);
504             throw $e;
505         }
506         
507         $updatedEvent = $this->get($event->getId());
508         
509         // send notifications
510         $this->sendNotifications($sendNotifications);
511         if ($this->_sendNotifications) {
512             $this->doSendNotifications($updatedEvent, Tinebase_Core::getUser(), 'changed', $event);
513         }
514         return $updatedEvent;
515     }
516     
517     /**
518      * inspect update of one record (after update)
519      *
520      * @param   Tinebase_Record_Interface $updatedRecord   the just updated record
521      * @param   Tinebase_Record_Interface $record          the update record
522      * @param   Tinebase_Record_Interface $currentRecord   the current record (before update)
523      * @return  void
524      */
525     protected function _inspectAfterUpdate($updatedRecord, $record, $currentRecord)
526     {
527         $this->_saveAttendee($record, $currentRecord, $record->isRescheduled($currentRecord));
528         // need to save new attendee set in $updatedRecord for modlog
529         $updatedRecord->attendee = clone($record->attendee);
530     }
531     
532     /**
533      * update range of events starting with given recur exception
534      * 
535      * @param Calendar_Model_Event $exdate
536      * @param string $range
537      */
538     protected function _updateExdateRange($exdate, $range, $oldExdate)
539     {
540         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
541             . ' Updating events (range: ' . $range . ') belonging to recur exception event ' . $exdate->getId());
542         
543         $baseEvent = $this->getRecurBaseEvent($exdate);
544         $diff = $oldExdate->diff($exdate);
545         
546         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
547             . ' Exdate diff: ' . print_r($diff->toArray(), TRUE));
548         
549         if ($range === Calendar_Model_Event::RANGE_ALL) {
550             $events = $this->getRecurExceptions($baseEvent);
551             $events->addRecord($baseEvent);
552             $this->_applyExdateDiffToRecordSet($exdate, $diff, $events);
553         } else if ($range === Calendar_Model_Event::RANGE_THISANDFUTURE) {
554             $nextRegularRecurEvent = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $exdate->dtstart);
555             
556             if ($nextRegularRecurEvent == $baseEvent) {
557                 // NOTE if a fist instance exception takes place before the
558                 //      series would start normally, $nextOccurence is the
559                 //      baseEvent of the series. As createRecurException can't
560                 //      deal with this situation we update whole series here
561                 $this->_updateExdateRange($exdate, Calendar_Model_Event::RANGE_ALL, $oldExdate);
562             } else if ($nextRegularRecurEvent !== NULL && ! $nextRegularRecurEvent->dtstart->isEarlier($exdate->dtstart)) {
563                 $this->_applyDiff($nextRegularRecurEvent, $diff, $exdate, FALSE);
564                 
565                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
566                     . ' Next recur exception event at: ' . $nextRegularRecurEvent->dtstart->toString());
567                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
568                     . ' ' . print_r($nextRegularRecurEvent->toArray(), TRUE));
569                 
570                 $newBaseEvent = $this->createRecurException($nextRegularRecurEvent, FALSE, TRUE);
571                 // @todo this should be done by createRecurException
572                 $exdatesOfNewBaseEvent = $this->getRecurExceptions($newBaseEvent);
573                 $this->_applyExdateDiffToRecordSet($exdate, $diff, $exdatesOfNewBaseEvent);
574             } else {
575                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
576                     . ' No upcoming occurrences found.');
577             }
578         }
579     }
580     
581     /**
582      * apply exdate diff to a recordset of events
583      * 
584      * @param Calendar_Model_Event $exdate
585      * @param Tinebase_Record_Diff $diff
586      * @param Tinebase_Record_RecordSet $events
587      */
588     protected function _applyExdateDiffToRecordSet($exdate, $diff, $events)
589     {
590         foreach ($events as $event) {
591             if ($event->getId() === $exdate->getId()) {
592                 // skip the exdate
593                 continue;
594             }
595             $this->_applyDiff($event, $diff, $exdate, FALSE);
596             $this->update($event);
597         }
598     }
599     
600     /**
601      * merge updates from exdate into event
602      * 
603      * @param Calendar_Model_Event $event
604      * @param Tinebase_Record_Diff $diff
605      * @param Calendar_Model_Event $exdate
606      * @param boolean $overwriteMods
607      * 
608      * @todo is $overwriteMods needed?
609      */
610     protected function _applyDiff($event, $diff, $exdate, $overwriteMods = TRUE)
611     {
612         if (! $overwriteMods) {
613             $recentChanges = Tinebase_Timemachine_ModificationLog::getInstance()->getModifications('Calendar', $event, NULL, 'Sql', $exdate->creation_time);
614             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
615                 . ' Recent changes (since ' . $exdate->creation_time->toString() . '): ' . print_r($recentChanges->toArray(), TRUE));
616         } else {
617             $recentChanges = new Tinebase_Record_RecordSet('Tinebase_Model_ModificationLog');
618         }
619         
620         $diffIgnore = array('organizer', 'seq', 'last_modified_by', 'last_modified_time', 'dtstart', 'dtend');
621         foreach ($diff->diff as $key => $newValue) {
622             if ($key === 'attendee') {
623                 if (in_array($key, $recentChanges->modified_attribute)) {
624                     $attendeeDiff = $diff->diff['attendee'];
625                     if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
626                         . ' Attendee diff: ' . print_r($attendeeDiff->toArray(), TRUE));
627                     foreach ($attendeeDiff['added'] as $attenderToAdd) {
628                         $attenderToAdd->setId(NULL);
629                         $event->attendee->addRecord($attenderToAdd);
630                     }
631                     foreach ($attendeeDiff['removed'] as $attenderToRemove) {
632                         $attenderInCurrentSet = Calendar_Model_Attender::getAttendee($event->attendee, $attenderToRemove);
633                         $event->attendee->removeRecord($attenderInCurrentSet);
634                     }
635                 } else {
636                     // remove ids of new attendee
637                     $attendee = clone($exdate->attendee);
638                     foreach ($attendee as $attender) {
639                         if (! $event->attendee->getById($attender->getId())) {
640                             $attender->setId(NULL);
641                         }
642                     }
643                     $event->attendee = $attendee;
644                 }
645             } else if (! in_array($key, $diffIgnore) && ! in_array($key, $recentChanges->modified_attribute)) {
646                 $event->{$key} = $exdate->{$key};
647             } else {
648                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
649                     . ' Ignore / recently changed: ' . $key);
650             }
651         }
652         
653         if ((isset($diff->diff['dtstart']) || array_key_exists('dtstart', $diff->diff)) || (isset($diff->diff['dtend']) || array_key_exists('dtend', $diff->diff))) {
654             $this->_applyTimeDiff($event, $exdate);
655         }
656     }
657     
658     /**
659      * update multiple records
660      * 
661      * @param   Tinebase_Model_Filter_FilterGroup $_filter
662      * @param   array $_data
663      * @return  integer number of updated records
664      */
665     public function updateMultiple($_filter, $_data)
666     {
667         $this->_checkRight('update');
668         $this->checkFilterACL($_filter, 'update');
669         
670         // get only ids
671         $ids = $this->_backend->search($_filter, NULL, TRUE);
672         
673         foreach ($ids as $eventId) {
674             $event = $this->get($eventId);
675             foreach ($_data as $field => $value) {
676                 $event->$field = $value;
677             }
678             
679             $this->update($event);
680         }
681         
682         return count($ids);
683     }
684     
685     /**
686      * Deletes a set of records.
687      * 
688      * If one of the records could not be deleted, no record is deleted
689      * 
690      * @param   array $_ids array of record identifiers
691      * @param   string $range
692      * @return  NULL
693      * @throws Tinebase_Exception_NotFound|Tinebase_Exception
694      */
695     public function delete($_ids, $range = Calendar_Model_Event::RANGE_THIS)
696     {
697         if ($_ids instanceof $this->_modelName) {
698             $_ids = (array)$_ids->getId();
699         }
700         
701         $records = $this->_backend->getMultiple((array) $_ids);
702         
703         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
704             . " Deleting " . count($records) . ' with range ' . $range . ' ...');
705         
706         foreach ($records as $record) {
707             if ($record->isRecurException() && in_array($range, array(Calendar_Model_Event::RANGE_ALL, Calendar_Model_Event::RANGE_THISANDFUTURE))) {
708                 $this->_deleteExdateRange($record, $range);
709             }
710             
711             try {
712                 $db = $this->_backend->getAdapter();
713                 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
714                 
715                 // delete if delete grant is present
716                 if ($this->_doContainerACLChecks === FALSE || $record->hasGrant(Tinebase_Model_Grants::GRANT_DELETE)) {
717                     // NOTE delete needs to update sequence otherwise iTIP based protocolls ignore the delete
718                     $record->status = Calendar_Model_Event::STATUS_CANCELED;
719                     $this->_touch($record);
720                     parent::delete($record);
721                 }
722                 
723                 // otherwise update status for user to DECLINED
724                 else if ($record->attendee instanceof Tinebase_Record_RecordSet) {
725                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " user has no deleteGrant for event: " . $record->id . ", updating own status to DECLINED only");
726                     $ownContact = Tinebase_Core::getUser()->contact_id;
727                     foreach ($record->attendee as $attender) {
728                         if ($attender->user_id == $ownContact && in_array($attender->user_type, array(Calendar_Model_Attender::USERTYPE_USER, Calendar_Model_Attender::USERTYPE_GROUPMEMBER))) {
729                             $attender->status = Calendar_Model_Attender::STATUS_DECLINED;
730                             $this->attenderStatusUpdate($record, $attender, $attender->status_authkey);
731                         }
732                     }
733                 }
734                 
735                 // increase display container content sequence for all attendee of deleted event
736                 if ($record->attendee instanceof Tinebase_Record_RecordSet) {
737                     foreach ($record->attendee as $attender) {
738                         $this->_increaseDisplayContainerContentSequence($attender, $record, Tinebase_Model_ContainerContent::ACTION_DELETE);
739                     }
740                 }
741                 
742                 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
743             } catch (Exception $e) {
744                 Tinebase_TransactionManager::getInstance()->rollBack();
745                 throw $e;
746             }
747         }
748     }
749     
750     /**
751      * delete range of events starting with given recur exception
752      * 
753      * @param Calendar_Model_Event $exdate
754      * @param string $range
755      */
756     protected function _deleteExdateRange($exdate, $range)
757     {
758         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
759             . ' Deleting events (range: ' . $range . ') belonging to recur exception event ' . $exdate->getId());
760         
761         $baseEvent = $this->getRecurBaseEvent($exdate);
762         
763         if ($range === Calendar_Model_Event::RANGE_ALL) {
764             $this->deleteRecurSeries($exdate);
765         } else if ($range === Calendar_Model_Event::RANGE_THISANDFUTURE) {
766             $nextRegularRecurEvent = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $exdate->dtstart);
767             
768             if ($nextRegularRecurEvent == $baseEvent) {
769                 // NOTE if a fist instance exception takes place before the
770                 //      series would start normally, $nextOccurence is the
771                 //      baseEvent of the series. As createRecurException can't
772                 //      deal with this situation we delete whole series here
773                 $this->_deleteExdateRange($exdate, Calendar_Model_Event::RANGE_ALL);
774             } else {
775                 $this->createRecurException($nextRegularRecurEvent, TRUE, TRUE);
776             }
777         }
778     }
779     
780     /**
781      * updates a recur series
782      *
783      * @param  Calendar_Model_Event $_recurInstance
784      * @param  bool                 $_checkBusyConflicts
785      * @return Calendar_Model_Event
786      */
787     public function updateRecurSeries($_recurInstance, $_checkBusyConflicts = FALSE)
788     {
789         $baseEvent = $this->getRecurBaseEvent($_recurInstance);
790         
791         // replace baseEvent with adopted instance
792         $newBaseEvent = clone $_recurInstance;
793         $newBaseEvent->setId($baseEvent->getId());
794         unset($newBaseEvent->recurid);
795         $newBaseEvent->exdate = $baseEvent->exdate;
796         
797         $this->_applyTimeDiff($newBaseEvent, $_recurInstance, $baseEvent);
798         
799         return $this->update($newBaseEvent, $_checkBusyConflicts);
800     }
801     
802     /**
803      * apply time diff
804      * 
805      * @param Calendar_Model_Event $newEvent
806      * @param Calendar_Model_Event $fromEvent
807      * @param Calendar_Model_Event $baseEvent
808      */
809     protected function _applyTimeDiff($newEvent, $fromEvent, $baseEvent = NULL)
810     {
811         if (! $baseEvent) {
812             $baseEvent = $newEvent;
813         }
814         
815         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
816             . ' New event: ' . print_r($newEvent->toArray(), TRUE));
817         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
818             . ' From event: ' . print_r($fromEvent->toArray(), TRUE));
819         
820         // compute time diff (NOTE: if the $fromEvent is the baseEvent, it has no recurid)
821         $originalDtStart = $fromEvent->recurid ? new Tinebase_DateTime(substr($fromEvent->recurid, -19), 'UTC') : clone $baseEvent->dtstart;
822         
823         $dtstartDiff = $originalDtStart->diff($fromEvent->dtstart);
824         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
825             . " Dtstart diff: " . $dtstartDiff->format('%H:%M:%i'));
826         $eventDuration = $fromEvent->dtstart->diff($fromEvent->dtend);
827         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
828             . " Duration diff: " . $dtstartDiff->format('%H:%M:%i'));
829         
830         $newEvent->dtstart = clone $baseEvent->dtstart;
831         $newEvent->dtstart->add($dtstartDiff);
832         
833         $newEvent->dtend = clone $newEvent->dtstart;
834         $newEvent->dtend->add($eventDuration);
835     }
836     
837     /**
838      * creates an exception instance of a recurring event
839      *
840      * NOTE: deleting persistent exceptions is done via a normal delete action
841      *       and handled in the deleteInspection
842      * 
843      * @param  Calendar_Model_Event  $_event
844      * @param  bool                  $_deleteInstance
845      * @param  bool                  $_allFollowing
846      * @param  bool                  $_checkBusyConflicts
847      * @return Calendar_Model_Event  exception Event | updated baseEvent
848      * 
849      * @todo replace $_allFollowing param with $range
850      * @deprecated replace with create/update/delete
851      */
852     public function createRecurException($_event, $_deleteInstance = FALSE, $_allFollowing = FALSE, $_checkBusyConflicts = FALSE)
853     {
854         $baseEvent = $this->getRecurBaseEvent($_event);
855         
856         if ($baseEvent->last_modified_time != $_event->last_modified_time) {
857             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
858                 . " It is not allowed to create recur instance if it is clone of base event");
859             throw new Tinebase_Timemachine_Exception_ConcurrencyConflict('concurrency conflict!');
860         }
861         
862         // check if this is an exception to the first occurence
863         if ($baseEvent->getId() == $_event->getId()) {
864             if ($_allFollowing) {
865                 throw new Exception('please edit or delete complete series!');
866             }
867             // NOTE: if the baseEvent gets a time change, we can't compute the recurdid w.o. knowing the original dtstart
868             $recurid = $baseEvent->setRecurId();
869             unset($baseEvent->recurid);
870             $_event->recurid = $recurid;
871         }
872         
873         // just do attender status update if user has no edit grant
874         if ($this->_doContainerACLChecks && !$baseEvent->{Tinebase_Model_Grants::GRANT_EDIT}) {
875             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
876                 . " user has no editGrant for event: '{$baseEvent->getId()}'. Only creating exception for attendee status");
877             if ($_event->attendee instanceof Tinebase_Record_RecordSet) {
878                 foreach ($_event->attendee as $attender) {
879                     if ($attender->status_authkey) {
880                         $exceptionAttender = $this->attenderStatusCreateRecurException($_event, $attender, $attender->status_authkey, $_allFollowing);
881                     }
882                 }
883             }
884             
885             if (! isset($exceptionAttender)) {
886                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG) && $_event->attendee instanceof Tinebase_Record_RecordSet) {
887                     Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " Failed to update attendee: " . print_r($_event->attendee->toArray(), true));
888                 }
889                 throw new Tinebase_Exception_AccessDenied('Failed to update attendee, status authkey might be missing');
890             }
891             
892             return $this->get($exceptionAttender->cal_event_id);
893         }
894         
895         // NOTE: recurid is computed by rrule recur computations and therefore is already part of the event.
896         if (empty($_event->recurid)) {
897             throw new Exception('recurid must be present to create exceptions!');
898         }
899         
900         // we do notifications ourself
901         $sendNotifications = $this->sendNotifications(FALSE);
902         
903         // EDIT for baseEvent is checked above, CREATE, DELETE for recur exceptions is implied with it
904         $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
905         
906         $db = $this->_backend->getAdapter();
907         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
908         
909         $exdate = new Tinebase_DateTime(substr($_event->recurid, -19));
910         $exdates = is_array($baseEvent->exdate) ? $baseEvent->exdate : array();
911         $originalDtstart = $_event->getOriginalDtStart();
912         $originalEvent = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $originalDtstart);
913         
914         if ($_allFollowing != TRUE) {
915             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " adding exdate for: '{$_event->recurid}'");
916             
917             array_push($exdates, $exdate);
918             $baseEvent->exdate = $exdates;
919             $updatedBaseEvent = $this->update($baseEvent, FALSE);
920             
921             if ($_deleteInstance == FALSE) {
922                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " Creating persistent exception for: '{$_event->recurid}'");
923                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " Recur exception: " . print_r($_event->toArray(), TRUE));
924                 
925                 $_event->setId(NULL);
926                 unset($_event->rrule);
927                 unset($_event->exdate);
928             
929                 foreach (array('attendee', 'notes', 'alarms') as $prop) {
930                     if ($_event->{$prop} instanceof Tinebase_Record_RecordSet) {
931                         $_event->{$prop}->setId(NULL);
932                     }
933                 }
934                 
935                 $originalDtstart = $_event->getOriginalDtStart();
936                 $dtStartHasDiff = $originalDtstart->compare($_event->dtstart) != 0; // php52 compat
937                 
938                 if (! $dtStartHasDiff) {
939                     $attendees = $_event->attendee;
940                     unset($_event->attendee);
941                 }
942                 $note = $_event->notes;
943                 unset($_event->notes);
944                 $persistentExceptionEvent = $this->create($_event, $_checkBusyConflicts);
945                 
946                 if (! $dtStartHasDiff) {
947                     // we save attendee seperatly to preserve their attributes
948                     if ($attendees instanceof Tinebase_Record_RecordSet) {
949                         $attendees->cal_event_id = $persistentExceptionEvent->getId();
950                         $calendar = Tinebase_Container::getInstance()->getContainerById($_event->container_id);
951                         foreach ($attendees as $attendee) {
952                             $this->_createAttender($attendee, $_event, TRUE, $calendar);
953                             $this->_increaseDisplayContainerContentSequence($attendee, $persistentExceptionEvent, Tinebase_Model_ContainerContent::ACTION_CREATE);
954                         }
955                     }
956                 }
957                 
958                 // @todo save notes and add a update note -> what was updated? -> modlog is also missing
959                 $persistentExceptionEvent = $this->get($persistentExceptionEvent->getId());
960             }
961             
962         } else {
963             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " shorten recur series for/to: '{$_event->recurid}'");
964                 
965             // split past/future exceptions
966             $pastExdates = array();
967             $futureExdates = array();
968             foreach($exdates as $exdate) {
969                 $exdate->isLater($_event->dtstart) ? $futureExdates[] = $exdate : $pastExdates[] = $exdate;
970             }
971             
972             $persistentExceptionEvents = $this->getRecurExceptions($_event);
973             $pastPersistentExceptionEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
974             $futurePersistentExceptionEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
975             foreach ($persistentExceptionEvents as $persistentExceptionEvent) {
976                 $persistentExceptionEvent->dtstart->isLater($_event->dtstart) ? $futurePersistentExceptionEvents->addRecord($persistentExceptionEvent) : $pastPersistentExceptionEvents->addRecord($persistentExceptionEvent);
977             }
978             
979             // update baseEvent
980             $rrule = Calendar_Model_Rrule::getRruleFromString($baseEvent->rrule);
981             if (isset($rrule->count)) {
982                 // get all occurences and find the split
983                 
984                 $exdate = $baseEvent->exdate;
985                 $baseEvent->exdate = NULL;
986                     //$baseCountOccurrence = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $baseEvent->rrule_until, $baseCount);
987                 $recurSet = Calendar_Model_Rrule::computeRecurrenceSet($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $baseEvent->dtstart, $baseEvent->rrule_until);
988                 $baseEvent->exdate = $exdate;
989                 
990                 $originalDtstart = $_event->getOriginalDtStart();
991                 foreach($recurSet as $idx => $rInstance) {
992                     if ($rInstance->dtstart >= $originalDtstart) break;
993                 }
994                 
995                 $rrule->count = $idx+1;
996             } else {
997                 $lastBaseOccurence = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $_event->getOriginalDtStart()->subSecond(1), -1);
998                 $rrule->until = $lastBaseOccurence ? $lastBaseOccurence->getOriginalDtStart() : $baseEvent->dtstart;
999             }
1000             $baseEvent->rrule = (string) $rrule;
1001             $baseEvent->exdate = $pastExdates;
1002             
1003             // NOTE: we don't want implicit attendee updates
1004             //$updatedBaseEvent = $this->update($baseEvent, FALSE);
1005             $this->_inspectEvent($baseEvent);
1006             $updatedBaseEvent = parent::update($baseEvent);
1007             
1008             if ($_deleteInstance == TRUE) {
1009                 // delete all future persistent events
1010                 $this->delete($futurePersistentExceptionEvents->getId());
1011             } else {
1012                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " create new recur series for/at: '{$_event->recurid}'");
1013                 
1014                 // NOTE: in order to move exceptions correctly in time we need to find out the original dtstart
1015                 //       and create the new baseEvent with this time. A following update also updates its exceptions
1016                 $originalDtstart = new Tinebase_DateTime(substr($_event->recurid, -19));
1017                 $adoptedDtstart = clone $_event->dtstart;
1018                 $dtStartHasDiff = $adoptedDtstart->compare($originalDtstart) != 0; // php52 compat
1019                 $eventLength = $_event->dtstart->diff($_event->dtend);
1020                 
1021                 $_event->dtstart = clone $originalDtstart;
1022                 $_event->dtend = clone $originalDtstart;
1023                 $_event->dtend->add($eventLength);
1024                 
1025                 // adopt count
1026                 if (isset($rrule->count)) {
1027                     $baseCount = $rrule->count;
1028                     $rrule = Calendar_Model_Rrule::getRruleFromString($_event->rrule);
1029                     $rrule->count = $rrule->count - $baseCount;
1030                     $_event->rrule = (string) $rrule;
1031                 }
1032                 
1033                 $_event->setId(NULL);
1034                 $_event->uid = $futurePersistentExceptionEvents->uid = Tinebase_Record_Abstract::generateUID();
1035                 $futurePersistentExceptionEvents->setRecurId();
1036                 unset($_event->recurid);
1037                 foreach(array('attendee', 'notes', 'alarms') as $prop) {
1038                     if ($_event->{$prop} instanceof Tinebase_Record_RecordSet) {
1039                         $_event->{$prop}->setId(NULL);
1040                     }
1041                 }
1042                 $_event->exdate = $futureExdates;
1043                 $futurePersistentExceptionEvents->setRecurId();
1044                 
1045                 $attendees = $_event->attendee; unset($_event->attendee);
1046                 $note = $_event->notes; unset($_event->notes);
1047                 $persistentExceptionEvent = $this->create($_event, $_checkBusyConflicts && $dtStartHasDiff);
1048                 
1049                 // we save attendee seperatly to preserve their attributes
1050                 if ($attendees instanceof Tinebase_Record_RecordSet) {
1051                     $attendees->cal_event_id = $persistentExceptionEvent->getId();
1052                     
1053                     foreach($attendees as $attendee) {
1054                         if (! $attendee->status_authkey) {
1055                             // new invitations
1056                             $attendee->status_authkey = Tinebase_Record_Abstract::generateUID();
1057                         }
1058                         $this->_backend->createAttendee($attendee);
1059                         $this->_increaseDisplayContainerContentSequence($attendee, $persistentExceptionEvent, Tinebase_Model_ContainerContent::ACTION_CREATE);
1060                     }
1061                 }
1062                 
1063                 // @todo save notes and add a update note -> what was updated? -> modlog is also missing
1064                 
1065                 $persistentExceptionEvent = $this->get($persistentExceptionEvent->getId());
1066                 
1067                 foreach($futurePersistentExceptionEvents as $futurePersistentExceptionEvent) {
1068                     $this->update($futurePersistentExceptionEvent, FALSE);
1069                 }
1070                 
1071                 if ($dtStartHasDiff) {
1072                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " new recur series has adpted dtstart -> update to adopt exceptions'");
1073                     $persistentExceptionEvent->dtstart = clone $adoptedDtstart;
1074                     $persistentExceptionEvent->dtend = clone $adoptedDtstart;
1075                     $persistentExceptionEvent->dtend->add($eventLength);
1076                     
1077                     $persistentExceptionEvent = $this->update($persistentExceptionEvent, $_checkBusyConflicts);
1078                 }
1079             }
1080         }
1081         
1082         Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1083         
1084         // restore original notification handling
1085         $this->sendNotifications($sendNotifications);
1086         $notificationAction = $_deleteInstance ? 'deleted' : 'changed';
1087         $notificationEvent = $_deleteInstance ? $_event : $persistentExceptionEvent;
1088         
1089         // restore acl
1090         $this->doContainerACLChecks($doContainerACLChecks);
1091         
1092         // send notifications
1093         if ($this->_sendNotifications) {
1094             // NOTE: recur exception is a fake event from client. 
1095             //       this might lead to problems, so we wrap the calls
1096             try {
1097                 if (count($_event->attendee) > 0) {
1098                     $_event->attendee->bypassFilters = TRUE;
1099                 }
1100                 $_event->created_by = $baseEvent->created_by;
1101                 
1102                 $this->doSendNotifications($notificationEvent, Tinebase_Core::getUser(), $notificationAction, $originalEvent);
1103             } catch (Exception $e) {
1104                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getTraceAsString());
1105                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " could not send notification {$e->getMessage()}");
1106             }
1107         }
1108         
1109         return $_deleteInstance ? $updatedBaseEvent : $persistentExceptionEvent;
1110     }
1111     
1112     /**
1113      * (non-PHPdoc)
1114      * @see Tinebase_Controller_Record_Abstract::get()
1115      */
1116     public function get($_id, $_containerId = NULL, $_getRelatedData = TRUE)
1117     {
1118         if (preg_match('/^fakeid/', $_id)) {
1119             // get base event when trying to fetch a non-persistent recur instance
1120             return $this->getRecurBaseEvent(new Calendar_Model_Event(array('uid' => substr(str_replace('fakeid', '', $_id), 0, 40))), TRUE);
1121         } else {
1122             return parent::get($_id, $_containerId, $_getRelatedData);
1123         }
1124     }
1125     
1126     /**
1127      * returns base event of a recurring series
1128      *
1129      * @param  Calendar_Model_Event $_event
1130      * @return Calendar_Model_Event
1131      */
1132     public function getRecurBaseEvent($_event)
1133     {
1134         $possibleBaseEventIds = $this->_backend->search(new Calendar_Model_EventFilter(array(
1135             array('field' => 'uid',     'operator' => 'equals', 'value' => $_event->uid),
1136             array('field' => 'recurid', 'operator' => 'isnull', 'value' => NULL)
1137         )), NULL, TRUE);
1138         
1139         if (count($possibleBaseEventIds) > 0) {
1140             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ .
1141                 ' Got multiple possible base events: ' . print_r($possibleBaseEventIds, true));
1142         }
1143         $baseEventId = array_value(0, $possibleBaseEventIds);
1144         
1145         if (! $baseEventId) {
1146             throw new Tinebase_Exception_NotFound('base event of a recurring series not found');
1147         }
1148         
1149         // make sure we have a 'fully featured' event
1150         return $this->get($baseEventId);
1151     }
1152
1153    /**
1154     * lookup existing event by uid
1155     *
1156     * @param  Calendar_Model_Event $_event
1157     * @return Calendar_Model_Event|NULL
1158     * 
1159     * @todo also add more criteria for lookup (recurid, ...)
1160     * @todo sophisticated reccurring event handling
1161     */
1162     public function lookupExistingEvent($_event)
1163     {
1164         $events = $this->_backend->search(new Calendar_Model_EventFilter(array(
1165             array('field' => 'uid',     'operator' => 'equals', 'value' => $_event->uid),
1166             //array('field' => 'recurid', 'operator' => 'isnull', 'value' => NULL)
1167         )));
1168         
1169         $event = $events->filter(Tinebase_Model_Grants::GRANT_READ, TRUE)->getFirstRecord();
1170     
1171         // make sure we have a 'fully featured' event
1172         return ($event !== NULL) ? $this->get($event->getId()) : NULL;
1173     }
1174     
1175     /**
1176      * returns all persistent recur exceptions of recur series identified by uid of given event
1177      * 
1178      * NOTE: deleted instances are saved in the base events exception property
1179      * NOTE: returns all exceptions regardless of current filters and access restrictions
1180      * 
1181      * @param  Calendar_Model_Event        $_event
1182      * @param  boolean                     $_fakeDeletedInstances
1183      * @param  Calendar_Model_EventFilter  $_eventFilter
1184      * @return Tinebase_Record_RecordSet of Calendar_Model_Event
1185      */
1186     public function getRecurExceptions($_event, $_fakeDeletedInstances = FALSE, $_eventFilter = NULL)
1187     {
1188         $exceptionFilter = new Calendar_Model_EventFilter(array(
1189             array('field' => 'uid',     'operator' => 'equals',  'value' => $_event->uid),
1190             array('field' => 'recurid', 'operator' => 'notnull', 'value' => NULL)
1191         ));
1192         
1193         if ($_eventFilter instanceof Calendar_Model_EventFilter) {
1194             $exceptionFilter->addFilterGroup($_eventFilter);
1195         }
1196         
1197         $exceptions = $this->_backend->search($exceptionFilter);
1198         
1199         if ($_fakeDeletedInstances) {
1200             $baseEvent = $this->getRecurBaseEvent($_event);
1201             $eventLength = $baseEvent->dtstart->diff($baseEvent->dtend);
1202
1203             // compute remaining exdates
1204             $deletedInstanceDtStarts = array_diff(array_unique((array) $baseEvent->exdate), $exceptions->getOriginalDtStart());
1205             foreach((array) $deletedInstanceDtStarts as $deletedInstanceDtStart) {
1206                 $fakeEvent = clone $baseEvent;
1207                 $fakeEvent->setId(NULL);
1208                 
1209                 $fakeEvent->dtstart = clone $deletedInstanceDtStart;
1210                 $fakeEvent->dtend = clone $deletedInstanceDtStart;
1211                 $fakeEvent->dtend->add($eventLength);
1212                 $fakeEvent->is_deleted = TRUE;
1213                 $fakeEvent->setRecurId();
1214                 
1215                 $exceptions->addRecord($fakeEvent);
1216             }
1217         }
1218         
1219         $exceptions->exdate = NULL;
1220         $exceptions->rrule = NULL;
1221         $exceptions->rrule_until = NULL;
1222         
1223         return $exceptions;
1224     }
1225     
1226    /**
1227     * adopt alarm time to next occurrence for recurring events
1228     *
1229     * @param Tinebase_Record_Abstract $_record
1230     * @param Tinebase_Model_Alarm $_alarm
1231     * @param bool $_nextBy {instance|time} set recurr alarm to next from given instance or next by current time
1232     * @return void
1233     */
1234     public function adoptAlarmTime(Tinebase_Record_Abstract $_record, Tinebase_Model_Alarm $_alarm, $_nextBy = 'time')
1235     {
1236         if ($_record->rrule) {
1237             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1238                  ' Adopting alarm time for next recur occurrence (by ' . $_nextBy . ')');
1239             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
1240                  ' ' . print_r($_record->toArray(), TRUE));
1241             
1242             if ($_nextBy == 'time') {
1243                 // NOTE: this also finds instances running right now
1244                 $from = Tinebase_DateTime::now();
1245             
1246             } else {
1247                 $recurid = $_alarm->getOption('recurid');
1248                 $instanceStart = $recurid ? new Tinebase_DateTime(substr($recurid, -19)) : clone $_record->dtstart;
1249                 $eventLength = $_record->dtstart->diff($_record->dtend);
1250                 
1251                 $instanceStart->setTimezone($_record->originator_tz);
1252                 $from = $instanceStart->add($eventLength);
1253                 $from->setTimezone('UTC');
1254             }
1255             
1256             // compute next
1257             $exceptions = $this->getRecurExceptions($_record);
1258             $nextOccurrence = Calendar_Model_Rrule::computeNextOccurrence($_record, $exceptions, $from);
1259             
1260             if ($nextOccurrence === NULL) {
1261                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
1262                     ' Recur series is over, no more alarms pending');
1263             } else {
1264                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1265                     ' Found next occurrence, adopting alarm to dtstart ' . $nextOccurrence->dtstart->toString());
1266             }
1267             
1268             // save recurid so we know for which recurrance the alarm is for
1269             $_alarm->setOption('recurid', isset($nextOccurrence) ? $nextOccurrence->recurid : NULL);
1270             
1271             $_alarm->sent_status = $nextOccurrence ? Tinebase_Model_Alarm::STATUS_PENDING : Tinebase_Model_Alarm::STATUS_SUCCESS;
1272             $_alarm->sent_message = $nextOccurrence ?  '' : 'Nothing to send, series is over';
1273             
1274             $eventStart = $nextOccurrence ? clone $nextOccurrence->dtstart : $_record->dtstart;
1275         } else {
1276             $eventStart = clone $_record->dtstart;
1277         }
1278         
1279         // save minutes before / compute it for custom alarms
1280         $minutesBefore = $_alarm->minutes_before == Tinebase_Model_Alarm::OPTION_CUSTOM 
1281             ? ($_record->dtstart->getTimestamp() - $_alarm->alarm_time->getTimestamp()) / 60 
1282             : $_alarm->minutes_before;
1283         $minutesBefore = round($minutesBefore);
1284         
1285         $_alarm->setOption('minutes_before', $minutesBefore);
1286         $_alarm->alarm_time = $eventStart->subMinute($minutesBefore);
1287         
1288         if ($_record->rrule && $_alarm->sent_status == Tinebase_Model_Alarm::STATUS_PENDING && $_alarm->alarm_time < $_alarm->sent_time) {
1289             $this->adoptAlarmTime($_record, $_alarm, 'instance');
1290         }
1291     }
1292     
1293     /****************************** overwritten functions ************************/
1294     
1295     /**
1296      * restore original alarm time of recurring events
1297      * 
1298      * @param Tinebase_Record_Abstract $_record
1299      * @return void
1300      */
1301     protected function _inspectAlarmGet(Tinebase_Record_Abstract $_record)
1302     {
1303         foreach ($_record->alarms as $alarm) {
1304             if ($recurid = $alarm->getOption('recurid')) {
1305                 $alarm->alarm_time = clone $_record->dtstart;
1306                 $alarm->alarm_time->subMinute((int) $alarm->getOption('minutes_before'));
1307             }
1308         }
1309         
1310         parent::_inspectAlarmGet($_record);
1311     }
1312     
1313     /**
1314      * adopt alarm time to next occurance for recurring events
1315      * 
1316      * @param Tinebase_Record_Abstract $_record
1317      * @param Tinebase_Model_Alarm $_alarm
1318      * @param bool $_nextBy {instance|time} set recurr alarm to next from given instance or next by current time
1319      * @return void
1320      * @throws Tinebase_Exception_InvalidArgument
1321      */
1322     protected function _inspectAlarmSet(Tinebase_Record_Abstract $_record, Tinebase_Model_Alarm $_alarm, $_nextBy = 'time')
1323     {
1324         parent::_inspectAlarmSet($_record, $_alarm);
1325         $this->adoptAlarmTime($_record, $_alarm, 'time');
1326     }
1327     
1328     /**
1329      * inspect update of one record
1330      * 
1331      * @param   Tinebase_Record_Interface $_record      the update record
1332      * @param   Tinebase_Record_Interface $_oldRecord   the current persistent record
1333      * @return  void
1334      */
1335     protected function _inspectBeforeUpdate($_record, $_oldRecord)
1336     {
1337         // if dtstart of an event changes, we update the originator_tz, alarm times
1338         if (! $_oldRecord->dtstart->equals($_record->dtstart)) {
1339             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' dtstart changed -> adopting organizer_tz');
1340             $_record->originator_tz = Tinebase_Core::get(Tinebase_Core::USERTIMEZONE);
1341             if (! empty($_record->rrule)) {
1342                 $diff = $_oldRecord->dtstart->diff($_record->dtstart);
1343                 $this->_updateRecurIdOfExdates($_record, $diff);
1344             }
1345         }
1346         
1347         // delete recur exceptions if update is no longer a recur series
1348         if (! empty($_oldRecord->rrule) && empty($_record->rrule)) {
1349             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' deleting recur exceptions as event is no longer a recur series');
1350             $this->_backend->delete($this->getRecurExceptions($_record));
1351         }
1352         
1353         // touch base event of a recur series if an persistent exception changes
1354         if ($_record->recurid) {
1355             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' touch base event of a persistent exception');
1356             $baseEvent = $this->getRecurBaseEvent($_record);
1357             $this->_touch($baseEvent, TRUE);
1358         }
1359     }
1360     
1361     /**
1362      * update exdates and recurids if dtstart of an recurevent changes
1363      * 
1364      * @param Calendar_Model_Event $_record
1365      * @param DateInterval $diff
1366      */
1367     protected function _updateRecurIdOfExdates($_record, $diff)
1368     {
1369         // update exceptions
1370         $exceptions = $this->getRecurExceptions($_record);
1371         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' dtstart of a series changed -> adopting '. count($exceptions) . ' recurid(s)');
1372         $exdates = array();
1373         foreach ($exceptions as $exception) {
1374             $exception->recurid = new Tinebase_DateTime(substr($exception->recurid, -19));
1375             Calendar_Model_Rrule::addUTCDateDstFix($exception->recurid, $diff, $_record->originator_tz);
1376             $exdates[] = $exception->recurid;
1377             
1378             $exception->setRecurId();
1379             $this->_backend->update($exception);
1380         }
1381         
1382         $_record->exdate = $exdates;
1383     }
1384     
1385     /**
1386      * inspect before create/update
1387      * 
1388      * @TODO move stuff from other places here
1389      * @param   Calendar_Model_Event $_record      the record to inspect
1390      */
1391     protected function _inspectEvent($_record)
1392     {
1393         $_record->uid = $_record->uid ? $_record->uid : Tinebase_Record_Abstract::generateUID();
1394         $_record->originator_tz = $_record->originator_tz ? $_record->originator_tz : Tinebase_Core::get(Tinebase_Core::USERTIMEZONE);
1395         $_record->organizer = $_record->organizer ? $_record->organizer : Tinebase_Core::getUser()->contact_id;
1396         $_record->transp = $_record->transp ? $_record->transp : Calendar_Model_Event::TRANSP_OPAQUE;
1397         
1398         // external organizer (iTIP)
1399         if (! $_record->resolveOrganizer()->account_id && count($_record->attendee) > 1) {
1400             $ownAttendee = Calendar_Model_Attender::getOwnAttender($_record->attendee);
1401             $_record->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender', $ownAttendee ? array($ownAttendee) : array());
1402         }
1403         
1404         if ($_record->is_all_day_event) {
1405             // harmonize datetimes of all day events
1406             $_record->setTimezone($_record->originator_tz);
1407             if (! $_record->dtend) {
1408                 $_record->dtend = clone $_record->dtstart;
1409                 $_record->dtend->setTime(23,59,59);
1410             }
1411             $_record->dtstart->setTime(0,0,0);
1412             $_record->dtend->setTime(23,59,59);
1413             $_record->setTimezone('UTC');
1414         }
1415         $_record->setRruleUntil();
1416         
1417         if ($_record->rrule instanceof Calendar_Model_Rrule) {
1418             $_record->rrule->normalize($_record);
1419         }
1420         
1421         if ($_record->isRecurException() && $_record->rrule !== NULL) {
1422             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1423                 . ' Removing invalid rrule from recur exception: ' . $_record->rrule);
1424             $_record->rrule = NULL;
1425         }
1426     }
1427     
1428     /**
1429      * inspects delete action
1430      *
1431      * @param array $_ids
1432      * @return array of ids to actually delete
1433      */
1434     protected function _inspectDelete(array $_ids) {
1435         $events = $this->_backend->getMultiple($_ids);
1436         
1437         foreach ($events as $event) {
1438             
1439             // implicitly delete persistent recur instances of series
1440             if (! empty($event->rrule)) {
1441                 $exceptionIds = $this->getRecurExceptions($event)->getId();
1442                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1443                     . ' Implicitly deleting ' . (count($exceptionIds) - 1 ) . ' persistent exception(s) for recurring series with uid' . $event->uid);
1444                 $_ids = array_merge($_ids, $exceptionIds);
1445             }
1446         }
1447         
1448         $this->_deleteAlarmsForIds($_ids);
1449         
1450         return array_unique($_ids);
1451     }
1452     
1453     /**
1454      * redefine required grants for get actions
1455      * 
1456      * @param Tinebase_Model_Filter_FilterGroup $_filter
1457      * @param string $_action get|update
1458      */
1459     public function checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
1460     {
1461         $hasGrantsFilter = FALSE;
1462         foreach($_filter->getAclFilters() as $aclFilter) {
1463             if ($aclFilter instanceof Calendar_Model_GrantFilter) {
1464                 $hasGrantsFilter = TRUE;
1465                 break;
1466             }
1467         }
1468         
1469         if (! $hasGrantsFilter) {
1470             // force a grant filter
1471             // NOTE: actual grants are set via setRequiredGrants later
1472             $grantsFilter = $_filter->createFilter('grants', 'in', '@setRequiredGrants');
1473             $_filter->addFilter($grantsFilter);
1474         }
1475         
1476         parent::checkFilterACL($_filter, $_action);
1477         
1478         if ($_action == 'get') {
1479             $_filter->setRequiredGrants(array(
1480                 Tinebase_Model_Grants::GRANT_FREEBUSY,
1481                 Tinebase_Model_Grants::GRANT_READ,
1482                 Tinebase_Model_Grants::GRANT_ADMIN,
1483             ));
1484         }
1485     }
1486     
1487     /**
1488      * check grant for action (CRUD)
1489      *
1490      * @param Tinebase_Record_Interface $_record
1491      * @param string $_action
1492      * @param boolean $_throw
1493      * @param string $_errorMessage
1494      * @param Tinebase_Record_Interface $_oldRecord
1495      * @return boolean
1496      * @throws Tinebase_Exception_AccessDenied
1497      * 
1498      * @todo use this function in other create + update functions
1499      * @todo invent concept for simple adding of grants (plugins?) 
1500      */
1501     protected function _checkGrant($_record, $_action, $_throw = TRUE, $_errorMessage = 'No Permission.', $_oldRecord = NULL)
1502     {
1503         if (    ! $this->_doContainerACLChecks 
1504             // admin grant includes all others (only if class is PUBLIC)
1505             ||  (! empty($this->class) && $this->class === Calendar_Model_Event::CLASS_PUBLIC 
1506                 && $_record->container_id && Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADMIN))
1507         ) {
1508             return TRUE;
1509         }
1510
1511         switch ($_action) {
1512             case 'get':
1513                 // NOTE: free/busy is not a read grant!
1514                 $hasGrant = $_record->hasGrant(Tinebase_Model_Grants::GRANT_READ);
1515                 if (! $hasGrant) {
1516                     $_record->doFreeBusyCleanup();
1517                 }
1518                 break;
1519             case 'create':
1520                 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADD);
1521                 break;
1522             case 'update':
1523                 $hasGrant = (bool) $_oldRecord->hasGrant(Tinebase_Model_Grants::GRANT_EDIT);
1524                 
1525                 if ($_oldRecord->container_id != $_record->container_id) {
1526                     $hasGrant &= Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADD)
1527                                  && $_oldRecord->hasGrant(Tinebase_Model_Grants::GRANT_DELETE);
1528                 }
1529                 break;
1530             case 'delete':
1531                 $hasGrant = (bool) $_record->hasGrant(Tinebase_Model_Grants::GRANT_DELETE);
1532                 break;
1533             case 'sync':
1534                 $hasGrant = (bool) $_record->hasGrant(Tinebase_Model_Grants::GRANT_SYNC);
1535                 break;
1536             case 'export':
1537                 $hasGrant = (bool) $_record->hasGrant(Tinebase_Model_Grants::GRANT_EXPORT);
1538                 break;
1539         }
1540         
1541         if (! $hasGrant) {
1542             if ($_throw) {
1543                 throw new Tinebase_Exception_AccessDenied($_errorMessage);
1544             } else {
1545                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 'No permissions to ' . $_action . ' in container ' . $_record->container_id);
1546             }
1547         }
1548         
1549         return $hasGrant;
1550     }
1551     
1552     /**
1553      * touches (sets seq and last_modified_time) given event
1554      * 
1555      * @param  $_event
1556      * @return void
1557      */
1558     protected function _touch($_event, $_setModifier = FALSE)
1559     {
1560         $_event->last_modified_time = Tinebase_DateTime::now();
1561         //$_event->last_modified_time = Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG);
1562         $_event->seq = (int)$_event->seq + 1;
1563         if ($_setModifier) {
1564             $_event->last_modified_by = Tinebase_Core::getUser()->getId();
1565         }
1566         
1567         
1568         $this->_backend->update($_event);
1569     }
1570     
1571     /****************************** attendee functions ************************/
1572     
1573     /**
1574      * creates an attender status exception of a recurring event series
1575      * 
1576      * NOTE: Recur exceptions are implicitly created
1577      *
1578      * @param  Calendar_Model_Event    $_recurInstance
1579      * @param  Calendar_Model_Attender $_attender
1580      * @param  string                  $_authKey
1581      * @param  bool                    $_allFollowing
1582      * @return Calendar_Model_Attender updated attender
1583      */
1584     public function attenderStatusCreateRecurException($_recurInstance, $_attender, $_authKey, $_allFollowing = FALSE)
1585     {
1586         try {
1587             $db = $this->_backend->getAdapter();
1588             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
1589             
1590             $baseEvent = $this->getRecurBaseEvent($_recurInstance);
1591             $baseEventAttendee = Calendar_Model_Attender::getAttendee($baseEvent->attendee, $_attender);
1592             
1593             if ($baseEvent->getId() == $_recurInstance->getId()) {
1594                 // exception to the first occurence
1595                 $_recurInstance->setRecurId();
1596             }
1597             
1598             // NOTE: recurid is computed by rrule recur computations and therefore is already part of the event.
1599             if (empty($_recurInstance->recurid)) {
1600                 throw new Exception('recurid must be present to create exceptions!');
1601             }
1602             
1603             try {
1604                 // check if we already have a persistent exception for this event
1605                 $eventInstance = $this->_backend->getByProperty($_recurInstance->recurid, $_property = 'recurid');
1606                 
1607                 // NOTE: the user must exist (added by someone with appropriate rights by createRecurException)
1608                 $exceptionAttender = Calendar_Model_Attender::getAttendee($eventInstance->attendee, $_attender);
1609                 if (! $exceptionAttender) {
1610                     throw new Tinebase_Exception_AccessDenied('not an attendee');
1611                 }
1612                 
1613                 
1614                 if ($exceptionAttender->status_authkey != $_authKey) {
1615                     // NOTE: it might happen, that the user set her status from the base event without knowing about 
1616                     //       an existing exception. In this case the base event authkey is also valid
1617                     if (! $baseEventAttendee || $baseEventAttendee->status_authkey != $_authKey) {
1618                         throw new Tinebase_Exception_AccessDenied('Attender authkey mismatch');
1619                     }
1620                 }
1621                 
1622             } catch (Tinebase_Exception_NotFound $e) {
1623                 // otherwise create it implicilty
1624                 
1625                 // check if this intance takes place
1626                 if (in_array($_recurInstance->dtstart, (array)$baseEvent->exdate)) {
1627                     throw new Tinebase_Exception_AccessDenied('Event instance is deleted and may not be recreated via status setting!');
1628                 }
1629                 
1630                 if (! $baseEventAttendee) {
1631                     throw new Tinebase_Exception_AccessDenied('not an attendee');
1632                 }
1633                 
1634                 if ($baseEventAttendee->status_authkey != $_authKey) {
1635                     throw new Tinebase_Exception_AccessDenied('Attender authkey mismatch');
1636                 }
1637                 
1638                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " creating recur exception for a exceptional attendee status");
1639                 
1640                 $doContainerAclChecks = $this->doContainerACLChecks(FALSE);
1641                 $sendNotifications = $this->sendNotifications(FALSE);
1642                 
1643                 // NOTE: the user might have no edit grants, so let's be carefull
1644                 $diff = $baseEvent->dtstart->diff($baseEvent->dtend);
1645                 
1646                 $baseEvent->dtstart = new Tinebase_DateTime(substr($_recurInstance->recurid, -19), 'UTC');
1647                 $baseEvent->dtend   = clone $baseEvent->dtstart;
1648                 $baseEvent->dtend->add($diff);
1649                 
1650                 $baseEvent->id = $_recurInstance->id;
1651                 $baseEvent->recurid = $_recurInstance->recurid;
1652                 
1653                 $attendee = $baseEvent->attendee;
1654                 unset($baseEvent->attendee);
1655                 
1656                 $eventInstance = $this->createRecurException($baseEvent, FALSE, $_allFollowing);
1657                 $eventInstance->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
1658                 $this->doContainerACLChecks($doContainerAclChecks);
1659                 $this->sendNotifications($sendNotifications);
1660                 
1661                 foreach ($attendee as $attender) {
1662                     $attender->setId(NULL);
1663                     $attender->cal_event_id = $eventInstance->getId();
1664                     
1665                     $attender = $this->_backend->createAttendee($attender);
1666                     $eventInstance->attendee->addRecord($attender);
1667                     $this->_increaseDisplayContainerContentSequence($attender, $eventInstance, Tinebase_Model_ContainerContent::ACTION_CREATE);
1668                 }
1669                 
1670                 $exceptionAttender = Calendar_Model_Attender::getAttendee($eventInstance->attendee, $_attender);
1671             }
1672             
1673             $exceptionAttender->status = $_attender->status;
1674             $exceptionAttender->transp = $_attender->transp;
1675             $eventInstance->alarms     = clone $_recurInstance->alarms;
1676             $eventInstance->alarms->setId(NULL);
1677             
1678             $updatedAttender = $this->attenderStatusUpdate($eventInstance, $exceptionAttender, $exceptionAttender->status_authkey);
1679             
1680             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1681         } catch (Exception $e) {
1682             Tinebase_TransactionManager::getInstance()->rollBack();
1683             throw $e;
1684         }
1685         
1686         return $updatedAttender;
1687     }
1688     
1689     /**
1690      * updates an attender status of a event
1691      * 
1692      * @param  Calendar_Model_Event    $_event
1693      * @param  Calendar_Model_Attender $_attender
1694      * @param  string                  $_authKey
1695      * @return Calendar_Model_Attender updated attender
1696      */
1697     public function attenderStatusUpdate(Calendar_Model_Event $_event, Calendar_Model_Attender $_attender, $_authKey)
1698     {
1699         try {
1700             $event = $this->get($_event->getId());
1701             
1702             if (! $event->attendee) {
1703                 throw new Tinebase_Exception_NotFound('Could not find any attendee of event.');
1704             }
1705             
1706             if (($currentAttender = Calendar_Model_Attender::getAttendee($event->attendee, $_attender)) == null) {
1707                 throw new Tinebase_Exception_NotFound('Could not find attender in event.');
1708             }
1709             
1710             $updatedAttender = clone $currentAttender;
1711             
1712             if ($currentAttender->status_authkey !== $_authKey) {
1713                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
1714                     Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " no permissions to update status for {$currentAttender->user_type} {$currentAttender->user_id}");
1715                 return $updatedAttender;
1716             }
1717             
1718             Calendar_Controller_Alarm::enforceACL($_event, $event);
1719             
1720             $currentAttenderDisplayContainerId = $currentAttender->displaycontainer_id instanceof Tinebase_Model_Container ? 
1721                 $currentAttender->displaycontainer_id->getId() : 
1722                 $currentAttender->displaycontainer_id;
1723             
1724             $attenderDisplayContainerId = $_attender->displaycontainer_id instanceof Tinebase_Model_Container ? 
1725                 $_attender->displaycontainer_id->getId() : 
1726                 $_attender->displaycontainer_id;
1727             
1728             // check if something what can be set as user has changed
1729             if ($currentAttender->status == $_attender->status &&
1730                 $currentAttenderDisplayContainerId  == $attenderDisplayContainerId   &&
1731                 $currentAttender->transp            == $_attender->transp            &&
1732                 ! Calendar_Controller_Alarm::hasUpdates($_event, $event)
1733             ) {
1734                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
1735                     Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__ . "no status change -> do nothing");
1736                 return $updatedAttender;
1737             }
1738             
1739             $updatedAttender->status              = $_attender->status;
1740             $updatedAttender->displaycontainer_id = isset($_attender->displaycontainer_id) ? $_attender->displaycontainer_id : $updatedAttender->displaycontainer_id;
1741             $updatedAttender->transp              = isset($_attender->transp) ? $_attender->transp : Calendar_Model_Event::TRANSP_OPAQUE;
1742             
1743             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
1744                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1745                     . " update attender status to {$_attender->status} for {$currentAttender->user_type} {$currentAttender->user_id}");
1746                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1747                     . ' set alarm_ack_time / alarm_snooze_time: ' . $updatedAttender->alarm_ack_time . ' / ' . $updatedAttender->alarm_snooze_time);
1748             }
1749             
1750             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1751             
1752             $updatedAttender = $this->_backend->updateAttendee($updatedAttender);
1753             if ($_event->alarms instanceof Tinebase_Record_RecordSet) {
1754                 foreach($_event->alarms as $alarm) {
1755                     $this->_inspectAlarmSet($event, $alarm);
1756                 }
1757                 
1758                 Tinebase_Alarm::getInstance()->setAlarmsOfRecord($_event);
1759             }
1760             
1761             $this->_increaseDisplayContainerContentSequence($updatedAttender, $event);
1762
1763             if ($currentAttender->status != $updatedAttender->status) {
1764                 $this->_touch($event, TRUE);
1765             }
1766             
1767             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1768         } catch (Exception $e) {
1769             Tinebase_TransactionManager::getInstance()->rollBack();
1770             throw $e;
1771         }
1772         
1773         // send notifications
1774         if ($currentAttender->status != $updatedAttender->status && $this->_sendNotifications) {
1775             $updatedEvent = $this->get($event->getId());
1776             $this->doSendNotifications($updatedEvent, Tinebase_Core::getUser(), 'changed', $event);
1777         }
1778         
1779         return $updatedAttender;
1780     }
1781     
1782     /**
1783      * saves all attendee of given event
1784      * 
1785      * NOTE: This function is executed in a create/update context. As such the user
1786      *       has edit/update the event and can do anything besides status settings of attendee
1787      * 
1788      * @todo add support for resources
1789      * 
1790      * @param Calendar_Model_Event $_event
1791      * @param Calendar_Model_Event $_currentEvent
1792      * @param bool                 $_isRescheduled event got rescheduled reset all attendee status
1793      */
1794     protected function _saveAttendee($_event, $_currentEvent = NULL, $_isRescheduled = FALSE)
1795     {
1796         if (! $_event->attendee instanceof Tinebase_Record_RecordSet) {
1797             $_event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
1798         }
1799         
1800         Calendar_Model_Attender::resolveEmailOnlyAttendee($_event);
1801         
1802         $_event->attendee->cal_event_id = $_event->getId();
1803         
1804         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " About to save attendee for event {$_event->id} ");
1805         
1806         $currentAttendee = $_currentEvent->attendee;
1807         
1808         $diff = $currentAttendee->getMigration($_event->attendee->getArrayOfIds());
1809
1810         $calendar = Tinebase_Container::getInstance()->getContainerById($_event->container_id);
1811         
1812         // delete attendee
1813         $this->_backend->deleteAttendee($diff['toDeleteIds']);
1814         foreach ($diff['toDeleteIds'] as $deleteAttenderId) {
1815             $idx = $currentAttendee->getIndexById($deleteAttenderId);
1816             if ($idx !== FALSE) {
1817                 $currentAttenderToDelete = $currentAttendee[$idx];
1818                 $this->_increaseDisplayContainerContentSequence($currentAttenderToDelete, $_event, Tinebase_Model_ContainerContent::ACTION_DELETE);
1819             }
1820         }
1821         
1822         // create/update attendee
1823         foreach ($_event->attendee as $attender) {
1824             $attenderId = $attender->getId();
1825             $idx = ($attenderId) ? $currentAttendee->getIndexById($attenderId) : FALSE;
1826             
1827             if ($idx !== FALSE) {
1828                 $currentAttender = $currentAttendee[$idx];
1829                 $this->_updateAttender($attender, $currentAttender, $_event, $_isRescheduled, $calendar);
1830             } else {
1831                 $this->_createAttender($attender, $_event, FALSE, $calendar);
1832             }
1833         }
1834     }
1835
1836     /**
1837      * creates a new attender
1838      * 
1839      * @param Calendar_Model_Attender  $attender
1840      * @param Tinebase_Model_Container $_calendar
1841      * @param boolean $preserveStatus
1842      * @param Tinebase_Model_Container $calendar
1843      */
1844     protected function _createAttender(Calendar_Model_Attender $attender, Calendar_Model_Event $event, $preserveStatus = FALSE, Tinebase_Model_Container $calendar = NULL)
1845     {
1846         // apply defaults
1847         $attender->user_type         = isset($attender->user_type) ? $attender->user_type : Calendar_Model_Attender::USERTYPE_USER;
1848         $calendar = ($calendar) ? $calendar : Tinebase_Container::getInstance()->getContainerById($event->container_id);
1849         
1850         $userAccountId = $attender->getUserAccountId();
1851         
1852         // reset status if not a contact or my account
1853         if (! $preserveStatus 
1854             && ($attender->user_type == Calendar_Model_Attender::USERTYPE_GROUP 
1855                 || $userAccountId && $userAccountId != Tinebase_Core::getUser()->getId()
1856             )) {
1857             $attender->status = Calendar_Model_Attender::STATUS_NEEDSACTION;
1858         }
1859         
1860         // generate auth key
1861         if (! $attender->status_authkey) {
1862             $attender->status_authkey = Tinebase_Record_Abstract::generateUID();
1863         }
1864         
1865         // attach to display calendar if attender has/is a useraccount
1866         if ($userAccountId) {
1867             if ($calendar->type == Tinebase_Model_Container::TYPE_PERSONAL && Tinebase_Container::getInstance()->hasGrant($userAccountId, $calendar, Tinebase_Model_Grants::GRANT_ADMIN)) {
1868                 // if attender has admin grant to personal physical container, this phys. cal also gets displ. cal
1869                 $attender->displaycontainer_id = $calendar->getId();
1870             } else if ($attender->displaycontainer_id && $userAccountId == Tinebase_Core::getUser()->getId() && Tinebase_Container::getInstance()->hasGrant($userAccountId, $attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_ADMIN)) {
1871                 // allow user to set his own displ. cal
1872                 $attender->displaycontainer_id = $attender->displaycontainer_id;
1873             } else {
1874                 $displayCalId = self::getDefaultDisplayContainerId($userAccountId);
1875                 $attender->displaycontainer_id = $displayCalId;
1876             }
1877         }
1878         
1879         if ($attender->user_type === Calendar_Model_Attender::USERTYPE_RESOURCE) {
1880             $resource = Calendar_Controller_Resource::getInstance()->get($attender->user_id);
1881             $attender->displaycontainer_id = $resource->container_id;
1882             
1883             // check if user is allowed to set status
1884             if (! Tinebase_Core::getUser()->hasGrant($attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_EDIT)) {
1885                 $attender->status = Calendar_Model_Attender::STATUS_NEEDSACTION;
1886             }
1887         }
1888         
1889         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " New attender: " . print_r($attender->toArray(), TRUE));
1890         
1891         Tinebase_Timemachine_ModificationLog::getInstance()->setRecordMetaData($attender, 'create');
1892         $this->_backend->createAttendee($attender);
1893         $this->_increaseDisplayContainerContentSequence($attender, $event, Tinebase_Model_ContainerContent::ACTION_CREATE);
1894     }
1895     
1896     /**
1897      * returns default displayContainer id of given attendee
1898      *
1899      * @param string $userAccountId
1900      */
1901     public static function getDefaultDisplayContainerId($userAccountId)
1902     {
1903         $userAccountId = Tinebase_Model_User::convertUserIdToInt($userAccountId);
1904         $displayCalId = Tinebase_Core::getPreference('Calendar')->getValueForUser(Calendar_Preference::DEFAULTCALENDAR, $userAccountId);
1905         
1906         try {
1907             // assert that displaycal is of type personal
1908             $container = Tinebase_Container::getInstance()->getContainerById($displayCalId);
1909             if ($container->type != Tinebase_Model_Container::TYPE_PERSONAL) {
1910                 $displayCalId = NULL;
1911             }
1912         } catch (Exception $e) {
1913             $displayCalId = NULL;
1914         }
1915         
1916         if (! isset($displayCalId)) {
1917             $containers = Tinebase_Container::getInstance()->getPersonalContainer($userAccountId, 'Calendar_Model_Event', $userAccountId, 0, true);
1918             if ($containers->count() > 0) {
1919                 $displayCalId = $containers->getFirstRecord()->getId();
1920             }
1921         }
1922         
1923         return $displayCalId;
1924     }
1925     
1926     /**
1927      * increases content sequence of attender display container
1928      * 
1929      * @param Calendar_Model_Attender $attender
1930      * @param Calendar_Model_Event $event
1931      * @param string $action
1932      */
1933     protected function _increaseDisplayContainerContentSequence($attender, $event, $action = Tinebase_Model_ContainerContent::ACTION_UPDATE)
1934     {
1935         if ($event->container_id === $attender->displaycontainer_id || empty($attender->displaycontainer_id)) {
1936             // no need to increase sequence
1937             return;
1938         }
1939         
1940         Tinebase_Container::getInstance()->increaseContentSequence($attender->displaycontainer_id, $action, $event->getId());
1941     }
1942     
1943     /**
1944      * updates an attender
1945      * 
1946      * @param Calendar_Model_Attender  $attender
1947      * @param Calendar_Model_Attender  $currentAttender
1948      * @param Calendar_Model_Event     $event
1949      * @param bool                     $isRescheduled event got rescheduled reset all attendee status
1950      * @param Tinebase_Model_Container $calendar
1951      */
1952     protected function _updateAttender($attender, $currentAttender, $event, $isRescheduled, $calendar = NULL)
1953     {
1954         $userAccountId = $currentAttender->getUserAccountId();
1955         
1956         // reset status if attender != currentuser and wrong authkey
1957         if ($userAccountId != Tinebase_Core::getUser()->getId()) {
1958             
1959             if ($isRescheduled) {
1960                 $attender->status = Calendar_Model_Attender::STATUS_NEEDSACTION;
1961                 $attender->transp = null;
1962             }
1963             
1964             else if ($attender->status_authkey != $currentAttender->status_authkey) {
1965                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1966                     . ' Wrong authkey, resetting status (' . $attender->status . ' -> ' . $currentAttender->status . ')');
1967                 $attender->status = $currentAttender->status;
1968             }
1969         }
1970         
1971         // preserve old authkey
1972         $attender->status_authkey = $currentAttender->status_authkey;
1973         
1974         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1975             . " Updating attender: " . print_r($attender->toArray(), TRUE));
1976         
1977         // update display calendar if attender has/is a useraccount
1978         if ($userAccountId) {
1979             if ($calendar->type == Tinebase_Model_Container::TYPE_PERSONAL && Tinebase_Container::getInstance()->hasGrant($userAccountId, $calendar, Tinebase_Model_Grants::GRANT_ADMIN)) {
1980                 // if attender has admin grant to personal physical container, this phys. cal also gets displ. cal
1981                 $attender->displaycontainer_id = $calendar->getId();
1982             } else if ($userAccountId == Tinebase_Core::getUser()->getId() && Tinebase_Container::getInstance()->hasGrant($userAccountId, $attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_ADMIN)) {
1983                 // allow user to set his own displ. cal
1984                 $attender->displaycontainer_id = $attender->displaycontainer_id;
1985             } else {
1986                 $attender->displaycontainer_id = $currentAttender->displaycontainer_id;
1987             }
1988         }
1989         
1990         Tinebase_Timemachine_ModificationLog::getInstance()->setRecordMetaData($attender, 'update', $currentAttender);
1991         Tinebase_Timemachine_ModificationLog::getInstance()->writeModLog($attender, $currentAttender, get_class($attender), $this->_getBackendType(), $attender->getId());
1992         $this->_backend->updateAttendee($attender);
1993         
1994         if ($attender->displaycontainer_id !== $currentAttender->displaycontainer_id) {
1995             $this->_increaseDisplayContainerContentSequence($currentAttender, $event, Tinebase_Model_ContainerContent::ACTION_DELETE);
1996             $this->_increaseDisplayContainerContentSequence($attender, $event, Tinebase_Model_ContainerContent::ACTION_CREATE);
1997         } else {
1998             $this->_increaseDisplayContainerContentSequence($attender, $event);
1999         }
2000     }
2001     
2002     /**
2003      * event handler for group updates
2004      * 
2005      * @param Tinebase_Model_Group $_group
2006      * @return void
2007      */
2008     public function onUpdateGroup($_groupId)
2009     {
2010         $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
2011         
2012         $filter = new Calendar_Model_EventFilter(array(
2013             array('field' => 'attender', 'operator' => 'equals', 'value' => array(
2014                 'user_type' => Calendar_Model_Attender::USERTYPE_GROUP,
2015                 'user_id'   => $_groupId
2016             )),
2017             array('field' => 'period', 'operator' => 'within', 'value' => array(
2018                 'from'  => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
2019                 'until' => Tinebase_DateTime::now()->addYear(100)->get(Tinebase_Record_Abstract::ISO8601LONG))
2020             )
2021         ));
2022         $events = $this->search($filter, new Tinebase_Model_Pagination(), FALSE, FALSE);
2023         
2024         foreach($events as $event) {
2025             try {
2026                 if (! $event->rrule) {
2027                     // update non recurring futrue events
2028                     Calendar_Model_Attender::resolveGroupMembers($event->attendee);
2029                     $this->update($event);
2030                 } else {
2031                     // update thisandfuture for recurring events
2032                     $nextOccurrence = Calendar_Model_Rrule::computeNextOccurrence($event, $this->getRecurExceptions($event), Tinebase_DateTime::now());
2033                     Calendar_Model_Attender::resolveGroupMembers($nextOccurrence->attendee);
2034                     
2035                     if ($nextOccurrence->dtstart != $event->dtstart) {
2036                         $this->createRecurException($nextOccurrence, FALSE, TRUE);
2037                     } else {
2038                         $this->update($nextOccurrence);
2039                     }
2040                 }
2041             } catch (Exception $e) {
2042                 Tinebase_Core::getLogger()->NOTICE(__METHOD__ . '::' . __LINE__ . " could not update attendee");
2043             }
2044         }
2045         
2046         $this->doContainerACLChecks($doContainerACLChecks);
2047     }
2048     
2049     /****************************** alarm functions ************************/
2050     
2051     /**
2052      * send an alarm
2053      *
2054      * @param  Tinebase_Model_Alarm $_alarm
2055      * @return void
2056      * 
2057      * NOTE: the given alarm is raw and has not passed _inspectAlarmGet
2058      *  
2059      * @todo throw exception on error
2060      */
2061     public function sendAlarm(Tinebase_Model_Alarm $_alarm) 
2062     {
2063         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " About to send alarm " . print_r($_alarm->toArray(), TRUE));
2064         
2065         $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
2066         
2067         $event = $this->get($_alarm->record_id);
2068         $event->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm', array($_alarm));
2069         $this->_inspectAlarmGet($event);
2070         
2071         $this->doContainerACLChecks($doContainerACLChecks);
2072         
2073         if ($event->rrule) {
2074             $recurid = $_alarm->getOption('recurid');
2075             
2076             // adopts the (referenced) alarm and sets alarm time to next occurance
2077             parent::_inspectAlarmSet($event, $_alarm);
2078             $this->adoptAlarmTime($event, $_alarm, 'instance');
2079             
2080             // sent_status might have changed in adoptAlarmTime()
2081             if ($_alarm->sent_status !== Tinebase_Model_Alarm::STATUS_PENDING) {
2082                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2083                     . ' Not sending alarm for event at ' . $event->dtstart->toString() . ' with status ' . $_alarm->sent_status);
2084                 return;
2085             }
2086             
2087             if ($recurid) {
2088                 // NOTE: In case of recuring events $event is always the baseEvent,
2089                 //       so we might need to adopt event time to recur instance.
2090                 $diff = $event->dtstart->diff($event->dtend);
2091                 
2092                 $event->dtstart = new Tinebase_DateTime(substr($recurid, -19));
2093                 
2094                 $event->dtend = clone $event->dtstart;
2095                 $event->dtend->add($diff);
2096             }
2097             
2098             if ($event->exdate && in_array($event->dtstart, $event->exdate)) {
2099                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2100                     . " Not sending alarm because instance at " . $event->dtstart->toString() . ' is an exception.');
2101                 return;
2102             }
2103         }
2104         
2105         Calendar_Controller_EventNotifications::getInstance()->doSendNotifications($event, Tinebase_Core::getUser(), 'alarm', NULL, $_alarm);
2106     }
2107     
2108     /**
2109      * send notifications 
2110      * 
2111      * @param Calendar_Model_Event       $_event
2112      * @param Tinebase_Model_FullAccount $_updater
2113      * @param Sting                      $_action
2114      * @param Calendar_Model_Event       $_oldEvent
2115      * @return void
2116      */
2117     public function doSendNotifications($_event, $_updater, $_action, $_oldEvent=NULL)
2118     {
2119         Tinebase_ActionQueue::getInstance()->queueAction('Calendar.sendEventNotifications', 
2120             $_event, 
2121             $_updater,
2122             $_action, 
2123             $_oldEvent ? $_oldEvent : NULL
2124         );
2125     }
2126 }