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