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