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