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