add calendar after update event
[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-2017 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 or has no attendee");
165             return;
166         }
167
168         $periods = $this->getBlockingPeriods($_event, array(
169             'from'  => $_event->dtstart,
170             'until' => $_event->dtstart->getClone()->addMonth(2)
171         ));
172
173         $fbInfo = $this->getFreeBusyInfo($periods, $_event->attendee, $ignoreUIDs);
174         
175         if (count($fbInfo) > 0) {
176             $busyException = new Calendar_Exception_AttendeeBusy();
177             $busyException->setFreeBusyInfo($fbInfo);
178             
179             Calendar_Model_Attender::resolveAttendee($_event->attendee, /* resolve_display_containers = */ false);
180             $busyException->setEvent($_event);
181             
182             throw $busyException;
183         }
184         
185         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
186             . " Free/busy check: no conflict found");
187     }
188     
189     /**
190      * add one record
191      *
192      * @param   Tinebase_Record_Interface $_record
193      * @param   bool                      $_checkBusyConflicts
194      * @return  Tinebase_Record_Interface
195      * @throws  Tinebase_Exception_AccessDenied
196      * @throws  Tinebase_Exception_Record_Validation
197      */
198     public function create(Tinebase_Record_Interface $_record, $_checkBusyConflicts = FALSE, $skipEvent = false)
199     {
200         try {
201             $db = $this->_backend->getAdapter();
202             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
203             
204             $this->_inspectEvent($_record, $skipEvent);
205             
206             // we need to resolve groupmembers before free/busy checking
207             Calendar_Model_Attender::resolveGroupMembers($_record->attendee);
208             
209             if ($_checkBusyConflicts) {
210                 // ensure that all attendee are free
211                 $this->checkBusyConflicts($_record);
212             }
213             
214             $sendNotifications = $this->_sendNotifications;
215             $this->_sendNotifications = FALSE;
216             
217             $createdEvent = parent::create($_record);
218             
219             $this->_sendNotifications = $sendNotifications;
220             
221             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
222         } catch (Exception $e) {
223             Tinebase_TransactionManager::getInstance()->rollBack();
224             throw $e;
225         }
226         
227         // send notifications
228         if ($this->_sendNotifications && $_record->mute != 1) {
229             $this->doSendNotifications($createdEvent, Tinebase_Core::getUser(), 'created');
230         }        
231
232         return $createdEvent;
233     }
234     
235     /**
236      * inspect creation of one record (after create)
237      *
238      * @param   Tinebase_Record_Interface $_createdRecord
239      * @param   Tinebase_Record_Interface $_record
240      * @return  void
241      */
242     protected function _inspectAfterCreate($_createdRecord, Tinebase_Record_Interface $_record)
243     {
244         $this->_saveAttendee($_record, $_createdRecord);
245     }
246     
247     /**
248      * deletes a recur series
249      *
250      * @param  Calendar_Model_Event $_recurInstance
251      * @return void
252      */
253     public function deleteRecurSeries($_recurInstance)
254     {
255         $baseEvent = $this->getRecurBaseEvent($_recurInstance);
256         $this->delete($baseEvent->getId());
257     }
258     
259     /**
260      * Gets all entries
261      *
262      * @param string $_orderBy Order result by
263      * @param string $_orderDirection Order direction - allowed are ASC and DESC
264      * @throws Tinebase_Exception_InvalidArgument
265      * @return Tinebase_Record_RecordSet
266      */
267     public function getAll($_orderBy = 'id', $_orderDirection = 'ASC') 
268     {
269         throw new Tinebase_Exception_NotImplemented('not implemented');
270     }
271
272     /**
273      * returns period filter expressing blocking times caused by given event
274      *
275      * @param  Calendar_Model_Event         $event
276      * @param  array                        $checkPeriod array(
277             "from"  => DateTime  (defaults to dtstart)
278             "until" => DateTime  (defaults to dtstart + 2 years)
279             "max"   => Integer   (defaults to 25)
280         )
281      * @return Calendar_Model_EventFilter
282      */
283     public function getBlockingPeriods($event, $checkPeriod = array())
284     {
285         $eventSet = new Tinebase_Record_RecordSet('Calendar_Model_Event', array($event));
286
287         if (! empty($event->rrule)) {
288             try {
289                 $eventSet->merge($this->getRecurExceptions($event, true));
290             } catch (Tinebase_Exception_NotFound $e) {
291                 // it's ok, event is not exising yet so we don't have exceptions as well
292             }
293
294             $from = isset($checkPeriod['from']) ? $checkPeriod['from'] : clone $event->dtstart;
295             $until = isset($checkPeriod['until']) ? $checkPeriod['until'] : $from->getClone()->addMonth(24);
296             Calendar_Model_Rrule::mergeRecurrenceSet($eventSet, $from, $until);
297         }
298
299         $periodFilters = array();
300         foreach ($eventSet as $candidate) {
301             if ($candidate->transp != Calendar_Model_Event::TRANSP_TRANSP) {
302                 $periodFilters[] = array(
303                     'field' => 'period',
304                     'operator' => 'within',
305                     'value' => array(
306                         'from' => $candidate->dtstart,
307                         'until' => $candidate->dtend,
308                     ),
309                 );
310             }
311         }
312
313         $filter = new Calendar_Model_EventFilter($periodFilters, Tinebase_Model_Filter_FilterGroup::CONDITION_OR);
314
315         return $filter;
316     }
317
318     /**
319      * returns conflicting periods
320      *
321      * @param Calendar_Model_EventFilter $periodCandidates
322      * @param Calendar_Model_EventFilter $conflictCriteria
323      * @param bool                       $getAll
324      * @return array
325      */
326     public function getConflictingPeriods($periodCandidates, $conflictCriteria, $getAll=false)
327     {
328         $conflictFilter = clone $conflictCriteria;
329         $conflictFilter->addFilterGroup($periodCandidates);
330         $conflictCandidates = $this->search($conflictFilter);
331
332         $from = $until = false;
333         foreach ($periodCandidates as $periodFilter) {
334             $period = $periodFilter->getValue();
335             $from = $from ? min($from, $period['from']) : $period['from'];
336             $until = $until ?  max($until, $period['until']) : $period['until'];
337         }
338         Calendar_Model_Rrule::mergeRecurrenceSet($conflictCandidates, $from, $until);
339
340         $conflicts = array();
341         foreach ($periodCandidates as $periodFilter) {
342             $period = $periodFilter->getValue();
343
344             foreach($conflictCandidates as $event) {
345                 if ($event->dtstart->isEarlier($period['until']) && $event->dtend->isLater($period['from'])) {
346                     $conflicts[] = array(
347                         'from' => $period['from'],
348                         'until' => $period['until'],
349                         'event' => $event
350                     );
351
352                     if (! $getAll) {
353                         break;
354                     }
355                 }
356             }
357         }
358
359         return $conflicts;
360     }
361
362     /**
363      * @param Tinebase_Record_RecordSet $_records
364      * @return array
365      */
366     public function mergeFreeBusyInfo(Tinebase_Record_RecordSet $_records)
367     {
368         $_records->sort('dtstart');
369         $result = array();
370         /** @var Calendar_Model_Event $event */
371         foreach($_records as $event) {
372             foreach($result as $key => &$period) {
373                 if ($event->dtstart->isEarlierOrEquals($period['dtend'])) {
374                     if ($event->dtend->isLaterOrEquals($period['dtstart'])) {
375                         if ($event->dtstart->isEarlier($period['dtstart'])) {
376                             $period['dtstart'] = clone $event->dtstart;
377                         }
378                         if ($event->dtend->isLater($period['dtend'])) {
379                             $period['dtend'] = clone $event->dtend;
380                         }
381                         continue 2;
382                     } else {
383                         throw new Tinebase_Exception_UnexpectedValue('record set sort by dtstart did not work!');
384                     }
385                 }
386             }
387             $result[] = array(
388                 'dtstart' => $event->dtstart,
389                 'dtend' => $event->dtend
390             );
391         }
392
393         return $result;
394     }
395
396     /**
397      * returns freebusy information for given period and given attendee
398      * 
399      * @todo merge overlapping events to one freebusy entry
400      * 
401      * @param  Calendar_Model_EventFilter                           $_periods
402      * @param  Tinebase_Record_RecordSet                            $_attendee
403      * @param  array                                                $_ignoreUIDs
404      * @return Tinebase_Record_RecordSet of Calendar_Model_FreeBusy
405      */
406     public function getFreeBusyInfo($_periods, $_attendee, $_ignoreUIDs = array())
407     {
408         $fbInfoSet = new Tinebase_Record_RecordSet('Calendar_Model_FreeBusy');
409         
410         // map groupmembers to users
411         $attendee = clone $_attendee;
412         $groupmembers = $attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
413         $groupmembers->user_type = Calendar_Model_Attender::USERTYPE_USER;
414         /*$groups = $attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUP);
415         $attendee->removeRecords($groups);
416         /** @var Calendar_Model_Attender $group *
417         foreach($groups as $group) {
418             $group = Tinebase_Group::getInstance()->getGroupById($group->user_id);
419
420             // fetch list only if list_id is not NULL, otherwise we get back an empty list object
421             if (!empty($group->list_id)) {
422                 $contactList = Addressbook_Controller_List::getInstance()->get($group->list_id);
423                 foreach ($contactList->members as $member) {
424                     $attendee->addRecord(new Calendar_Model_Attender(array(
425                         'user_id' => $member,
426                         'user_type' => Calendar_Model_Attender::USERTYPE_USER
427                     ), true));
428                 }
429             }
430         }*/
431         
432         $conflictCriteria = new Calendar_Model_EventFilter(array(
433             array('field' => 'attender', 'operator' => 'in',     'value' => $attendee),
434             array('field' => 'transp',   'operator' => 'equals', 'value' => Calendar_Model_Event::TRANSP_OPAQUE)
435         ));
436
437         $conflictingPeriods = $this->getConflictingPeriods($_periods, $conflictCriteria, true);
438
439         // create a typemap
440         $typeMap = array();
441         foreach ($attendee as $attender) {
442             if (! isset($typeMap[$attender['user_type']])) {
443                 $typeMap[$attender['user_type']] = array();
444             }
445             if (is_object($attender['user_id'])) {
446                 $attender['user_id'] = $attender['user_id']->getId();
447             }
448             $typeMap[$attender['user_type']][$attender['user_id']] = array();
449         }
450         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . ' ' . __LINE__
451             . ' value: ' . print_r($typeMap, true));
452
453         // fetch resources to get freebusy type
454         // NOTE: we could add busy_type to attendee later
455         if (isset($typeMap[Calendar_Model_Attender::USERTYPE_RESOURCE])) {
456             $resources = Calendar_Controller_Resource::getInstance()->getMultiple(array_keys($typeMap[Calendar_Model_Attender::USERTYPE_RESOURCE]), true);
457         }
458
459         $processedEvents = array();
460
461         foreach ($conflictingPeriods as $conflictingPeriod) {
462             $event = $conflictingPeriod['event'];
463
464             // one event may conflict multiple periods
465             if (in_array($event, $processedEvents)) {
466                 continue;
467             }
468
469             $processedEvents[] = $event;
470
471             // skip events with ignoreUID
472             if (in_array($event->uid, $_ignoreUIDs)) {
473                 continue;
474             }
475
476             // map groupmembers to users
477             $groupmembers = $event->attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
478             $groupmembers->user_type = Calendar_Model_Attender::USERTYPE_USER;
479
480             foreach ($event->attendee as $attender) {
481                 // skip declined/transp events
482                 if ($attender->status == Calendar_Model_Attender::STATUS_DECLINED ||
483                     $attender->transp == Calendar_Model_Event::TRANSP_TRANSP) {
484                     continue;
485                 }
486
487                 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]))) {
488                     $type = Calendar_Model_FreeBusy::FREEBUSY_BUSY;
489
490                     if ($attender->user_type == Calendar_Model_Attender::USERTYPE_RESOURCE) {
491                         $resource = $resources->getById($attender->user_id);
492                         if ($resource) {
493                             $type = $resource->busy_type;
494                         }
495                     }
496
497                     $fbInfo = new Calendar_Model_FreeBusy(array(
498                         'user_type' => $attender->user_type,
499                         'user_id'   => $attender->user_id,
500                         'dtstart'   => clone $event->dtstart,
501                         'dtend'     => clone $event->dtend,
502                         'type'      => $type,
503                     ), true);
504                     
505                     if ($event->{Tinebase_Model_Grants::GRANT_READ}) {
506                         $fbInfo->event = clone $event;
507                         unset($fbInfo->event->attendee);
508                     }
509                     
510                     //$typeMap[$attender->user_type][$attender->user_id][] = $fbInfo;
511                     $fbInfoSet->addRecord($fbInfo);
512                 }
513             }
514         }
515         
516         return $fbInfoSet;
517     }
518
519     /**
520      * update future constraint exdates of a single event
521      *
522      * @param $_record
523      */
524     public function setConstraintsExdates($_record)
525     {
526         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
527             . ' event has rrule constrains, calculating exdates');
528
529         $exdates = is_array($_record->exdate) ? $_record->exdate : array();
530
531         // own event should not trigger constraints conflicts
532         if ($_record->rrule_constraints && $_record->rrule_constraints instanceof Calendar_Model_EventFilter) {
533             $constraints = clone $_record->rrule_constraints;
534             $constraints->addFilter(new Tinebase_Model_Filter_Text('uid', 'not', $_record->uid));
535
536             $constrainExdatePeriods = $this->getConflictingPeriods($this->getBlockingPeriods($_record), $constraints);
537             foreach ($constrainExdatePeriods as $constrainExdatePeriod) {
538                 $exdates[] = $constrainExdatePeriod['from'];
539             }
540         }
541
542         $_record->exdate = array_unique($exdates);
543     }
544
545     /**
546      * update all future constraints exdates
547      */
548     public function updateConstraintsExdates()
549     {
550         // find all future recur events with constraints (ignoring ACL's)
551         $constraintsEventIds = $this->_backend->search(new Calendar_Model_EventFilter(array(
552             array('field' => 'period', 'operator' => 'within', 'value' => array(
553                 'from' => Tinebase_DateTime::now(),
554                 'until' => Tinebase_DateTime::now()->addMonth(24)
555             )),
556             array('field' => 'rrule_contraints', 'operator' => 'notnull', 'value' => NULL)
557         )), NULL, 'id');
558
559         // update each
560         foreach ($constraintsEventIds as $constraintsEventId) {
561             try {
562                 $event = $this->_backend->get($constraintsEventId);
563                 $this->setConstraintsExdates($event);
564                 // NOTE: touch also updates
565                 $this->_touch($event);
566             } catch (Exception $e) {
567                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
568                     . " cannot update constraints exdates for event {$constraintsEventId}: " . $e->getMessage());
569                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
570                     . " cannot update constraints exdates for event {$constraintsEventId}: " . $e);
571             }
572         }
573     }
574
575     /**
576      * get list of records
577      *
578      * @param Tinebase_Model_Filter_FilterGroup             $_filter
579      * @param Tinebase_Model_Pagination                     $_pagination
580      * @param bool                                          $_getRelations
581      * @param boolean                                       $_onlyIds
582      * @param string                                        $_action for right/acl check
583      * @return Tinebase_Record_RecordSet|array
584      */
585     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
586     {
587         $events = parent::search($_filter, $_pagination, $_getRelations, $_onlyIds, $_action);
588         if (! $_onlyIds) {
589             $this->_freeBusyCleanup($events, $_action);
590         }
591         
592         return $events;
593     }
594     
595     /**
596      * Returns a set of records identified by their id's
597      *
598      * @param   array $_ids       array of record identifiers
599      * @param   bool  $_ignoreACL don't check acl grants
600      * @return  Tinebase_Record_RecordSet of $this->_modelName
601      */
602     public function getMultiple($_ids, $_ignoreACL = false)
603     {
604         $events = parent::getMultiple($_ids, $_ignoreACL = false);
605         if ($_ignoreACL !== true) {
606             $this->_freeBusyCleanup($events, 'get');
607         }
608         
609         return $events;
610     }
611     
612     /**
613      * cleanup search results (freebusy)
614      * 
615      * @param Tinebase_Record_RecordSet $_events
616      * @param string $_action
617      */
618     protected function _freeBusyCleanup(Tinebase_Record_RecordSet $_events, $_action)
619     {
620         foreach ($_events as $event) {
621             $doFreeBusyCleanup = $event->doFreeBusyCleanup();
622             if ($doFreeBusyCleanup && $_action !== 'get') {
623                 $_events->removeRecord($event);
624             }
625         }
626     }
627
628     /**
629      * @param Calendar_Model_Event $_event with
630      *   attendee to find free timeslot for
631      *   dtstart, dtend -> to calculate duration
632      *   rrule optional
633      * @param array $_options
634      *  'from'         datetime (optional, defaults event->dtstart) from where to start searching
635      *  'until'        datetime (optional, defaults 2 years) until when to giveup searching
636      *  'constraints'  array    (optional, defaults to 8-20 'FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR') array of timespecs to limit the search with
637      *     timespec:
638      *       dtstart,
639      *       dtend,
640      *       rrule ... for example "work days" -> 'FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR'
641      * @return Tinebase_Record_RecordSet record set of event sugestions
642      * @throws Tinebase_Exception_NotImplemented
643      */
644     public function searchFreeTime($_event, $_options)
645     {
646         $functionTime = time();
647
648         // validate $_event, originator_tz will be validated by setTimezone() call
649         if (!isset($_event->dtstart) || !$_event->dtstart instanceof Tinebase_DateTime) {
650             throw new Tinebase_Exception_UnexpectedValue('dtstart needs to be set');
651         }
652         if (!isset($_event->dtstart) || !$_event->dtstart instanceof Tinebase_DateTime) {
653             throw new Tinebase_Exception_UnexpectedValue('dtend needs to be set');
654         }
655         if (!isset($_event->attendee) || !$_event->attendee instanceof Tinebase_Record_RecordSet ||
656                 $_event->attendee->count() < 1) {
657             throw new Tinebase_Exception_UnexpectedValue('attendee needs to be set and contain at least one attendee');
658         }
659
660         if (empty($_event->originator_tz)) {
661             $_event->originator_tz = Tinebase_Core::getUserTimezone();
662         }
663
664         $from = isset($_options['from']) ? ($_options['from'] instanceof Tinebase_DateTime ? $_options['from'] :
665             new Tinebase_DateTime($_options['from'])) : clone $_event->dtstart;
666         $until = isset($_options['until']) ? ($_options['until'] instanceof Tinebase_DateTime ? $_options['until'] :
667             new Tinebase_DateTime($_options['until'])) : $_event->dtend->getClone()->addYear(2);
668
669         $currentFrom = $from->getClone();
670         $currentUntil = $from->getClone()->addDay(6)->setTime(23, 59, 59);
671         if ($currentUntil->isLater($until)) {
672             $currentUntil = clone $until;
673         }
674         $durationSec = (int)$_event->dtend->getTimestamp() - (int)$_event->dtstart->getTimestamp();
675         $constraints = new Tinebase_Record_RecordSet('Calendar_Model_Event', array());
676         $exceptions = new Tinebase_Record_RecordSet('Calendar_Model_Event', array());
677
678         if (isset($_options['constraints'])) {
679             foreach ($_options['constraints'] as $constraint) {
680                 if (!isset($constraint['dtstart']) || !isset($constraint['dtend'])) {
681                     // LOG
682                     continue;
683                 }
684                 $constraint['uid'] = Tinebase_Record_Abstract::generateUID();
685                 $event = new Calendar_Model_Event(array(), true);
686                 $event->setFromJsonInUsersTimezone($constraint);
687                 $event->originator_tz = $_event->originator_tz;
688                 $constraints->addRecord($event);
689             }
690         }
691
692         if ($constraints->count() === 0) {
693             //here the timezone will come from the getClone, not need to set it
694             $constraints->addRecord(new Calendar_Model_Event(
695                 array(
696                     'uid'           => Tinebase_Record_Abstract::generateUID(),
697                     'dtstart'       => $currentFrom->getClone()->setHour(8)->setMinute(0)->setSecond(0),
698                     'dtend'         => $currentFrom->getClone()->setHour(20)->setMinute(0)->setSecond(0),
699                     'rrule'         => 'FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR',
700                     'originator_tz' => $_event->originator_tz
701                 ), true)
702             );
703         }
704
705         do {
706             if (time() - $functionTime > 23) {
707                 $exception = new Calendar_Exception_AttendeeBusy();
708                 $exception->setEvent(new Calendar_Model_Event(array('dtend' => $currentFrom), true));
709                 throw $exception;
710             }
711
712             $currentConstraints = clone $constraints;
713             Calendar_Model_Rrule::mergeRecurrenceSet($currentConstraints, $currentFrom, $currentUntil);
714             $currentConstraints->sort('dtstart');
715             $remove = array();
716
717             // sort out constraints that do not fit the rrule
718             if (!empty($_event->rrule)) {
719                 /** @var Calendar_Model_Event $event */
720                 foreach ($currentConstraints as $event) {
721                     $recurEvent = clone $_event;
722                     $recurEvent->uid = Tinebase_Record_Abstract::generateUID();
723                     $recurEvent->dtstart = $event->dtstart->getClone()->subDay(1);
724                     if ($_event->is_all_day_event) {
725                         $recurEvent->dtend = $event->dtend->getClone()->subDay(1);
726                     } else {
727                         $recurEvent->dtend = $recurEvent->dtstart->getClone()->addSecond($durationSec);
728                     }
729                     if (null === ($recurEvent = Calendar_Model_Rrule::computeNextOccurrence($recurEvent, $exceptions, $event->dtstart))
730                             || $recurEvent->dtstart->isLater($event->dtend)) {
731                         $remove[] = $event;
732                     }
733                 }
734                 foreach($remove as $event) {
735                     $currentConstraints->removeRecord($event);
736                 }
737             }
738
739             if ($currentConstraints->count() > 0) {
740                 $periods = array();
741                 /** @var Calendar_Model_Event $event */
742                 foreach ($currentConstraints as $event) {
743                     $periods[] = array(
744                         'field' => 'period',
745                         'operator' => 'within',
746                         'value' => array(
747                             'from' => $event->dtstart,
748                             'until' => $event->dtend
749                         ),
750                     );
751                 }
752
753                 $busySlots = $this->getFreeBusyInfo(new Calendar_Model_EventFilter($periods, Tinebase_Model_Filter_FilterGroup::CONDITION_OR), $_event->attendee);
754                 $busySlots = $this->mergeFreeBusyInfo($busySlots);
755
756                 /** @var Calendar_Model_Event $event */
757                 foreach ($currentConstraints as $event) {
758                     if ($event->dtend->isEarlierOrEquals($currentFrom)) {
759                         continue;
760                     }
761
762                     if ($_event->is_all_day_event) {
763                         $durationSec = (int)$event->dtend->getTimestamp() - (int)$event->dtstart->getTimestamp();
764                     }
765
766                     $constraintStart = (int)$event->dtstart->getTimestamp();
767                     if ($constraintStart < (int)$currentFrom->getTimestamp()) {
768                         $constraintStart = (int)$currentFrom->getTimestamp();
769                     }
770                     $constraintEnd = (int)$event->dtend->getTimestamp();
771                     if ($constraintEnd > (int)$currentUntil->getTimestamp()) {
772                         $constraintEnd = (int)$currentUntil->getTimestamp();
773                     }
774                     $lastBusyEnd = $constraintStart;
775                     $remove = array();
776                     /** @var Calendar_Model_FreeBusy $busy */
777                     foreach ($busySlots as $key => $busy) {
778                         $busyStart = (int)$busy['dtstart']->getTimestamp();
779                         $busyEnd = (int)$busy['dtend']->getTimestamp();
780
781                         if ($busyEnd < $constraintStart) {
782                             $remove[] = $key;
783                             continue;
784                         }
785
786                         if ($busyStart > ($constraintEnd - $durationSec)) {
787                             $lastBusyEnd = $busyEnd;
788                             break;
789                         }
790
791                         if (($lastBusyEnd + $durationSec) <= $busyStart) {
792                             // check between $lastBusyEnd and $busyStart
793                             $result = $this->_tryForFreeSlot($_event, $lastBusyEnd, $busyStart, $durationSec, $until);
794                             if ($result->count() > 0) {
795                                 if ($_event->is_all_day_event) {
796                                     $result->getFirstRecord()->dtstart = $_event->dtstart;
797                                     $result->getFirstRecord()->dtend = $_event->dtend;
798                                 }
799                                 return $result;
800                             }
801                         }
802                         $lastBusyEnd = $busyEnd;
803                     }
804                     foreach ($remove as $key) {
805                         unset($busySlots[$key]);
806                     }
807
808                     if (($lastBusyEnd + $durationSec) <= $constraintEnd) {
809                         // check between $lastBusyEnd and $constraintEnd
810                         $result = $this->_tryForFreeSlot($_event, $lastBusyEnd, $constraintEnd, $durationSec, $until);
811                         if ($result->count() > 0) {
812                             if ($_event->is_all_day_event) {
813                                 $result->getFirstRecord()->dtstart = $_event->dtstart;
814                                 $result->getFirstRecord()->dtend = $_event->dtend;
815                             }
816                             return $result;
817                         }
818                     }
819                 }
820             }
821
822             $currentFrom->addDay(7)->setTime(0, 0, 0);
823             $currentUntil->addDay(7);
824             if ($currentUntil->isLater($until)) {
825                 $currentUntil = clone $until;
826             }
827         } while ($until->isLater($currentFrom));
828
829         return new Tinebase_Record_RecordSet('Calendar_Model_Event', array());
830     }
831
832     protected function _tryForFreeSlot(Calendar_Model_Event $_event, $_startSec, $_endSec, $_durationSec, Tinebase_DateTime $_until)
833     {
834         $event = new Calendar_Model_Event(array(
835             'uid'           => Tinebase_Record_Abstract::generateUID(),
836             'dtstart'       => new Tinebase_DateTime($_startSec),
837             'dtend'         => new Tinebase_DateTime($_startSec + $_durationSec),
838             'originator_tz' => $_event->originator_tz,
839         ), true);
840         $result = new Tinebase_Record_RecordSet('Calendar_Model_Event', array($event));
841
842         if (!empty($_event->rrule)) {
843             $event->rrule = $_event->rrule;
844             do {
845                 $until = $event->dtstart->getClone()->addMonth(2);
846                 if ($until->isLater($_until)) {
847                     $until = $_until;
848                 }
849                 $periods = $this->getBlockingPeriods($event, array(
850                     'from'  => $event->dtstart,
851                     'until' => $until
852                 ));
853                 $busySlots = $this->getFreeBusyInfo($periods, $_event->attendee);
854                 $event->dtstart->addMinute(15);
855                 $event->dtend->addMinute(15);
856             } while($busySlots->count() > 0 && $event->dtend->getTimestamp() <= $_endSec && $event->dtend->isEarlierOrEquals($_until));
857
858             if ($busySlots->count() > 0) {
859                 $result->removeAll();
860             } else {
861                 $event->dtstart->subMinute(15);
862                 $event->dtend->subMinute(15);
863             }
864         }
865
866         return $result;
867     }
868     
869     /**
870      * update one record
871      *
872      * @param   Tinebase_Record_Interface $_record
873      * @param   bool                      $_checkBusyConflicts
874      * @param   string                    $range
875      * @return  Tinebase_Record_Interface
876      * @throws  Tinebase_Exception_AccessDenied
877      * @throws  Tinebase_Exception_Record_Validation
878      */
879     public function update(Tinebase_Record_Interface $_record, $_checkBusyConflicts = FALSE, $range = Calendar_Model_Event::RANGE_THIS, $skipEvent = false)
880     {
881         /** @var Calendar_Model_Event $_record */
882         try {
883             $db = $this->_backend->getAdapter();
884             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
885             
886             $sendNotifications = $this->sendNotifications(FALSE);
887
888             $event = $this->get($_record->getId());
889             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
890                     .' Going to update the following event. rawdata: ' . print_r($event->toArray(), true));
891
892             //NOTE we check via get(full rights) here whereas _updateACLCheck later checks limited rights from search
893             if ($this->_doContainerACLChecks === FALSE || $event->hasGrant(Tinebase_Model_Grants::GRANT_EDIT)) {
894                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " updating event: {$_record->id} (range: {$range})");
895                 
896                 // we need to resolve groupmembers before free/busy checking
897                 Calendar_Model_Attender::resolveGroupMembers($_record->attendee);
898                 $this->_inspectEvent($_record, $skipEvent);
899                
900                 if ($_checkBusyConflicts) {
901                     if ($event->isRescheduled($_record) ||
902                         count(array_diff($_record->attendee->user_id, $event->attendee->user_id)) > 0
903                     ) {
904                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
905                             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
906                                 . " Ensure that all attendee are free with free/busy check ... ");
907                         }
908                         $this->checkBusyConflicts($_record);
909                     } else {
910                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
911                             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
912                                 . " Skipping free/busy check because event has not been rescheduled and no new attender has been added");
913                         }
914                     }
915                 }
916
917                 parent::update($_record);
918                 
919             } else if ($_record->attendee instanceof Tinebase_Record_RecordSet) {
920                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
921                     . " user has no editGrant for event: {$_record->id}, updating attendee status with valid authKey only");
922                 foreach ($_record->attendee as $attender) {
923                     if ($attender->status_authkey) {
924                         $this->attenderStatusUpdate($_record, $attender, $attender->status_authkey);
925                     }
926                 }
927             }
928
929             if ($_record->isRecurException() && in_array($range, array(Calendar_Model_Event::RANGE_ALL, Calendar_Model_Event::RANGE_THISANDFUTURE))) {
930                 $this->_updateExdateRange($_record, $range, $event);
931             }
932
933             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
934         } catch (Exception $e) {
935             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Rolling back because: ' . $e);
936             Tinebase_TransactionManager::getInstance()->rollBack();
937             $this->sendNotifications($sendNotifications);
938             throw $e;
939         }
940         
941         $updatedEvent = $this->get($event->getId());
942         
943         // send notifications
944         $this->sendNotifications($sendNotifications);
945         if ($this->_sendNotifications && $_record->mute != 1) {
946             $this->doSendNotifications($updatedEvent, Tinebase_Core::getUser(), 'changed', $event);
947         }
948         return $updatedEvent;
949     }
950     
951     /**
952      * inspect update of one record (after update)
953      *
954      * @param   Tinebase_Record_Interface $updatedRecord   the just updated record
955      * @param   Tinebase_Record_Interface $record          the update record
956      * @param   Tinebase_Record_Interface $currentRecord   the current record (before update)
957      * @return  void
958      */
959     protected function _inspectAfterUpdate($updatedRecord, $record, $currentRecord)
960     {
961         $this->_saveAttendee($record, $currentRecord, $record->isRescheduled($currentRecord));
962         // need to save new attendee set in $updatedRecord for modlog
963         $updatedRecord->attendee = clone($record->attendee);
964
965         Tinebase_Record_PersistentObserver::getInstance()->fireEvent(new Calendar_Event_InspectEventAfterUpdate([
966             'observable' => $updatedRecord
967         ]));
968     }
969     
970     /**
971      * update range of events starting with given recur exception
972      * 
973      * @param Calendar_Model_Event $exdate
974      * @param string $range
975      */
976     protected function _updateExdateRange($exdate, $range, $oldExdate)
977     {
978         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
979             . ' Updating events (range: ' . $range . ') belonging to recur exception event ' . $exdate->getId());
980         
981         $baseEvent = $this->getRecurBaseEvent($exdate);
982         /** @var Tinebase_Record_Diff $diff */
983         $diff = $oldExdate->diff($exdate);
984         
985         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
986             . ' Exdate diff: ' . print_r($diff->toArray(), TRUE));
987         
988         if ($range === Calendar_Model_Event::RANGE_ALL) {
989             $events = $this->getRecurExceptions($baseEvent);
990             $events->addRecord($baseEvent);
991             $this->_applyExdateDiffToRecordSet($exdate, $diff, $events);
992         } else if ($range === Calendar_Model_Event::RANGE_THISANDFUTURE) {
993             $nextRegularRecurEvent = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $exdate->dtstart);
994             
995             if ($nextRegularRecurEvent == $baseEvent) {
996                 // NOTE if a fist instance exception takes place before the
997                 //      series would start normally, $nextOccurence is the
998                 //      baseEvent of the series. As createRecurException can't
999                 //      deal with this situation we update whole series here
1000                 $this->_updateExdateRange($exdate, Calendar_Model_Event::RANGE_ALL, $oldExdate);
1001             } else if ($nextRegularRecurEvent !== NULL && ! $nextRegularRecurEvent->dtstart->isEarlier($exdate->dtstart)) {
1002                 $this->_applyDiff($nextRegularRecurEvent, $diff, $exdate, FALSE);
1003                 
1004                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1005                     . ' Next recur exception event at: ' . $nextRegularRecurEvent->dtstart->toString());
1006                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1007                     . ' ' . print_r($nextRegularRecurEvent->toArray(), TRUE));
1008                 
1009                 $nextRegularRecurEvent->mute = $exdate->mute;
1010                 $newBaseEvent = $this->createRecurException($nextRegularRecurEvent, FALSE, TRUE);
1011                 // @todo this should be done by createRecurException
1012                 $exdatesOfNewBaseEvent = $this->getRecurExceptions($newBaseEvent);
1013                 $this->_applyExdateDiffToRecordSet($exdate, $diff, $exdatesOfNewBaseEvent);
1014             } else {
1015                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1016                     . ' No upcoming occurrences found.');
1017             }
1018         }
1019     }
1020
1021     protected function _applyDTStartToBaseEventRRULE($_dtstart, $_baseEvent)
1022     {
1023         /** @var Calendar_Model_Rrule $rrule */
1024         /*$rrule = $_baseEvent->rrule;
1025         if (! $rrule instanceof Calendar_Model_Rrule) {
1026             $rrule = new Calendar_Model_Rrule($rrule);
1027         }
1028
1029         switch($rrule->freq)
1030         {
1031             case Calendar_Model_Rrule::FREQ_DAILY
1032         }
1033
1034         if ($rrule->freq == Calendar_Model_Rrule::FREQ_WEEKLY) {
1035
1036         }
1037
1038         if ($rrule->freq == Calendar_Model_Rrule::FREQ_MONTHLY) {
1039             if (!empty($rrule->byday)) {
1040
1041             } elseif(!empty($rrule->bymonthday)) {
1042
1043             }
1044         }
1045
1046         if ($rrule->freq == Calendar_Model_Rrule::FREQ_YEARLY) {
1047             // bymonthday + bymonth
1048         }
1049
1050         switch($rrule->byday)
1051         {
1052
1053         }*/
1054     }
1055     
1056     /**
1057      * apply exdate diff to a recordset of events
1058      * 
1059      * @param Calendar_Model_Event $exdate
1060      * @param Tinebase_Record_Diff $diff
1061      * @param Tinebase_Record_RecordSet $events
1062      */
1063     protected function _applyExdateDiffToRecordSet($exdate, $diff, $events)
1064     {
1065         // make sure baseEvent gets updated first to circumvent concurrency conflicts
1066         $events->sort('recurdid', 'ASC');
1067
1068         foreach ($events as $event) {
1069             if ($event->getId() === $exdate->getId()) {
1070                 // skip the exdate
1071                 continue;
1072             }
1073             $this->_applyDiff($event, $diff, $exdate, FALSE);
1074             $this->update($event);
1075         }
1076     }
1077     
1078     /**
1079      * merge updates from exdate into event
1080      * 
1081      * @param Calendar_Model_Event $event
1082      * @param Tinebase_Record_Diff $diff
1083      * @param Calendar_Model_Event $exdate
1084      * @param boolean $overwriteMods
1085      * 
1086      * @todo is $overwriteMods needed?
1087      */
1088     protected function _applyDiff($event, $diff, $exdate, $overwriteMods = TRUE)
1089     {
1090         if (! $overwriteMods) {
1091             $recentChanges = Tinebase_Timemachine_ModificationLog::getInstance()->getModifications('Calendar', $event, NULL, 'Sql', $exdate->creation_time)->filter('change_type', Tinebase_Timemachine_ModificationLog::UPDATED);
1092             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1093                 . ' Recent changes (since ' . $exdate->creation_time->toString() . '): ' . print_r($recentChanges->toArray(), TRUE));
1094         } else {
1095             $recentChanges = new Tinebase_Record_RecordSet('Tinebase_Model_ModificationLog');
1096         }
1097
1098         $changedAttributes = Tinebase_Timemachine_ModificationLog::getModifiedAttributes($recentChanges);
1099         $diffIgnore = array('organizer', 'seq', 'last_modified_by', 'last_modified_time', 'dtstart', 'dtend');
1100         foreach ($diff->diff as $key => $newValue) {
1101             if ($key === 'attendee') {
1102                 if (in_array($key, $changedAttributes)) {
1103                     $attendeeDiff = $diff->diff['attendee'];
1104                     if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1105                         . ' Attendee diff: ' . print_r($attendeeDiff->toArray(), TRUE));
1106                     foreach ($attendeeDiff['added'] as $attenderToAdd) {
1107                         $attenderToAdd->setId(NULL);
1108                         $event->attendee->addRecord($attenderToAdd);
1109                     }
1110                     foreach ($attendeeDiff['removed'] as $attenderToRemove) {
1111                         $attenderInCurrentSet = Calendar_Model_Attender::getAttendee($event->attendee, $attenderToRemove);
1112                         if ($attenderInCurrentSet) {
1113                             $event->attendee->removeRecord($attenderInCurrentSet);
1114                         }
1115                     }
1116                 } else {
1117                     // remove ids of new attendee
1118                     $attendee = clone($exdate->attendee);
1119                     foreach ($attendee as $attender) {
1120                         if (! $event->attendee->getById($attender->getId())) {
1121                             $attender->setId(NULL);
1122                         }
1123                     }
1124                     $event->attendee = $attendee;
1125                 }
1126             } else if (! in_array($key, $diffIgnore) && ! in_array($key, $changedAttributes)) {
1127                 $event->{$key} = $exdate->{$key};
1128             } else {
1129                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1130                     . ' Ignore / recently changed: ' . $key);
1131             }
1132         }
1133         
1134         if ((isset($diff->diff['dtstart']) || array_key_exists('dtstart', $diff->diff)) || (isset($diff->diff['dtend']) || array_key_exists('dtend', $diff->diff))) {
1135             $this->_applyTimeDiff($event, $exdate);
1136         }
1137     }
1138     
1139     /**
1140      * update multiple records
1141      * 
1142      * @param   Tinebase_Model_Filter_FilterGroup $_filter
1143      * @param   array $_data
1144      * @return  integer number of updated records
1145      */
1146     public function updateMultiple($_filter, $_data)
1147     {
1148         $this->_checkRight('update');
1149         $this->checkFilterACL($_filter, 'update');
1150         
1151         // get only ids
1152         $ids = $this->_backend->search($_filter, NULL, TRUE);
1153         
1154         foreach ($ids as $eventId) {
1155             $event = $this->get($eventId);
1156             foreach ($_data as $field => $value) {
1157                 $event->$field = $value;
1158             }
1159             
1160             $this->update($event);
1161         }
1162         
1163         return count($ids);
1164     }
1165     
1166     /**
1167      * Deletes a set of records.
1168      * 
1169      * If one of the records could not be deleted, no record is deleted
1170      * 
1171      * @param   array $_ids array of record identifiers
1172      * @param   string $range
1173      * @return  NULL
1174      * @throws Tinebase_Exception_NotFound|Tinebase_Exception
1175      */
1176     public function delete($_ids, $range = Calendar_Model_Event::RANGE_THIS)
1177     {
1178         if ($_ids instanceof $this->_modelName) {
1179             $_ids = (array)$_ids->getId();
1180         }
1181         
1182         $records = $this->_backend->getMultiple((array) $_ids);
1183         
1184         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1185             . " Deleting " . count($records) . ' with range ' . $range . ' ...');
1186         
1187         foreach ($records as $record) {
1188             if ($record->isRecurException() && in_array($range, array(Calendar_Model_Event::RANGE_ALL, Calendar_Model_Event::RANGE_THISANDFUTURE))) {
1189                 $this->_deleteExdateRange($record, $range);
1190             }
1191             
1192             try {
1193                 $db = $this->_backend->getAdapter();
1194                 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
1195                 
1196                 // delete if delete grant is present
1197                 if ($this->_doContainerACLChecks === FALSE || $record->hasGrant(Tinebase_Model_Grants::GRANT_DELETE)) {
1198                     // NOTE delete needs to update sequence otherwise iTIP based protocolls ignore the delete
1199                     $record->status = Calendar_Model_Event::STATUS_CANCELED;
1200                     $this->_touch($record);
1201                     if ($record->isRecurException()) {
1202                         try {
1203                             $baseEvent = $this->getRecurBaseEvent($record);
1204                             $this->_touch($baseEvent);
1205                         } catch (Tinebase_Exception_NotFound $tnfe) {
1206                             // base Event might be gone already
1207                             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1208                                 . " BaseEvent of exdate {$record->uid} to delete not found ");
1209
1210                         }
1211                     }
1212                     parent::delete($record);
1213                 }
1214                 
1215                 // otherwise update status for user to DECLINED
1216                 else if ($record->attendee instanceof Tinebase_Record_RecordSet) {
1217                     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");
1218                     $ownContact = Tinebase_Core::getUser()->contact_id;
1219                     foreach ($record->attendee as $attender) {
1220                         if ($attender->user_id == $ownContact && in_array($attender->user_type, array(Calendar_Model_Attender::USERTYPE_USER, Calendar_Model_Attender::USERTYPE_GROUPMEMBER))) {
1221                             $attender->status = Calendar_Model_Attender::STATUS_DECLINED;
1222                             $this->attenderStatusUpdate($record, $attender, $attender->status_authkey);
1223                         }
1224                     }
1225                 }
1226                 
1227                 // increase display container content sequence for all attendee of deleted event
1228                 if ($record->attendee instanceof Tinebase_Record_RecordSet) {
1229                     foreach ($record->attendee as $attender) {
1230                         $this->_increaseDisplayContainerContentSequence($attender, $record, Tinebase_Model_ContainerContent::ACTION_DELETE);
1231                     }
1232                 }
1233                 
1234                 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1235             } catch (Exception $e) {
1236                 Tinebase_TransactionManager::getInstance()->rollBack();
1237                 throw $e;
1238             }
1239         }
1240     }
1241     
1242     /**
1243      * delete range of events starting with given recur exception
1244      *
1245      * NOTE: if exdate is persistent, it will not be deleted by this function
1246      *       but by the original call of delete
1247      *
1248      * @param Calendar_Model_Event $exdate
1249      * @param string $range
1250      */
1251     protected function _deleteExdateRange($exdate, $range)
1252     {
1253         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1254             . ' Deleting events (range: ' . $range . ') belonging to recur exception event ' . $exdate->getId());
1255         
1256         $baseEvent = $this->getRecurBaseEvent($exdate);
1257         
1258         if ($range === Calendar_Model_Event::RANGE_ALL) {
1259             $this->deleteRecurSeries($exdate);
1260         } else if ($range === Calendar_Model_Event::RANGE_THISANDFUTURE) {
1261             $nextRegularRecurEvent = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $exdate->dtstart);
1262             
1263             if ($nextRegularRecurEvent == $baseEvent) {
1264                 // NOTE if a fist instance exception takes place before the
1265                 //      series would start normally, $nextOccurence is the
1266                 //      baseEvent of the series. As createRecurException can't
1267                 //      deal with this situation we delete whole series here
1268                 $this->_deleteExdateRange($exdate, Calendar_Model_Event::RANGE_ALL);
1269             } else {
1270                 $this->createRecurException($nextRegularRecurEvent, TRUE, TRUE);
1271             }
1272         }
1273     }
1274     
1275     /**
1276      * updates a recur series
1277      *
1278      * @param  Calendar_Model_Event $_recurInstance
1279      * @param  bool                 $_checkBusyConflicts
1280      * @return Calendar_Model_Event
1281      */
1282     public function updateRecurSeries($_recurInstance, $_checkBusyConflicts = FALSE)
1283     {
1284         $baseEvent = $this->getRecurBaseEvent($_recurInstance);
1285
1286         $originalDtStart = $_recurInstance->getOriginalDtStart();
1287         $originalDtStart->setTimezone(Tinebase_DateTime::TIMEZONE_UTC);
1288         $dtStart = $_recurInstance->dtstart;
1289         if (! $dtStart instanceof Tinebase_DateTime) {
1290             $dtStart = new Tinebase_DateTime($dtStart);
1291         }
1292         $dtStart->setTimezone(Tinebase_DateTime::TIMEZONE_UTC);
1293         $dtEnd = $_recurInstance->dtend;
1294         if (! $dtEnd instanceof Tinebase_DateTime) {
1295             $dtEnd = new Tinebase_DateTime($dtEnd);
1296         }
1297         $dtEnd->setTimezone(Tinebase_DateTime::TIMEZONE_UTC);
1298
1299         if ($originalDtStart->compare($dtStart) !== 0 ||
1300             (($orgDiff = $baseEvent->dtend->diff($baseEvent->dtstart)) &&
1301                 ($newDiff = $dtEnd->diff($dtStart)) &&
1302                 (
1303                     $orgDiff->days !== $newDiff->days ||
1304                     $orgDiff->h !== $newDiff->h ||
1305                     $orgDiff->m !== $newDiff->m ||
1306                     $orgDiff->s !== $newDiff->s
1307                 )
1308             )) {
1309             if (strpos($baseEvent->rrule->byday, ',') !== false ||
1310                 strpos($baseEvent->rrule->bymonthday, ',') !== false ) {
1311                 throw new Tinebase_Exception_UnexpectedValue('dont change complex stuff like that');
1312             }
1313         }
1314
1315         // replace baseEvent with adopted instance
1316         $newBaseEvent = clone $_recurInstance;
1317         $newBaseEvent->setId($baseEvent->getId());
1318         unset($newBaseEvent->recurid);
1319         $newBaseEvent->exdate = $baseEvent->exdate;
1320         
1321         $this->_applyTimeDiff($newBaseEvent, $_recurInstance, $baseEvent);
1322         
1323         return $this->update($newBaseEvent, $_checkBusyConflicts);
1324     }
1325     
1326     /**
1327      * apply time diff
1328      * 
1329      * @param Calendar_Model_Event $newEvent
1330      * @param Calendar_Model_Event $fromEvent
1331      * @param Calendar_Model_Event $baseEvent
1332      */
1333     protected function _applyTimeDiff($newEvent, $fromEvent, $baseEvent = NULL)
1334     {
1335         if (! $baseEvent) {
1336             $baseEvent = $newEvent;
1337         }
1338         
1339         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1340             . ' New event: ' . print_r($newEvent->toArray(), TRUE));
1341         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1342             . ' From event: ' . print_r($fromEvent->toArray(), TRUE));
1343         
1344         // compute time diff (NOTE: if the $fromEvent is the baseEvent, it has no recurid)
1345         $originalDtStart = $fromEvent->recurid ? new Tinebase_DateTime(substr($fromEvent->recurid, -19), 'UTC') : clone $baseEvent->dtstart;
1346         
1347         $dtstartDiff = $originalDtStart->diff($fromEvent->dtstart);
1348         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1349             . " Dtstart diff: " . $dtstartDiff->format('%H:%M:%i'));
1350         $eventDuration = $fromEvent->dtstart->diff($fromEvent->dtend);
1351         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1352             . " Duration diff: " . $dtstartDiff->format('%H:%M:%i'));
1353         
1354         $newEvent->dtstart = clone $baseEvent->dtstart;
1355         $newEvent->dtstart->add($dtstartDiff);
1356         
1357         $newEvent->dtend = clone $newEvent->dtstart;
1358         $newEvent->dtend->add($eventDuration);
1359     }
1360     
1361     /**
1362      * creates an exception instance of a recurring event
1363      *
1364      * NOTE: deleting persistent exceptions is done via a normal delete action
1365      *       and handled in the deleteInspection
1366      * 
1367      * @param  Calendar_Model_Event  $_event
1368      * @param  bool                  $_deleteInstance
1369      * @param  bool                  $_allFollowing
1370      * @param  bool                  $_checkBusyConflicts
1371      * @return Calendar_Model_Event  exception Event | updated baseEvent
1372      * 
1373      * @todo replace $_allFollowing param with $range
1374      * @deprecated replace with create/update/delete
1375      */
1376     public function createRecurException($_event, $_deleteInstance = FALSE, $_allFollowing = FALSE, $_checkBusyConflicts = FALSE)
1377     {
1378         $baseEvent = $this->getRecurBaseEvent($_event);
1379         
1380         if ($baseEvent->last_modified_time != $_event->last_modified_time) {
1381             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
1382                 . " It is not allowed to create recur instance if it is clone of base event");
1383             throw new Tinebase_Timemachine_Exception_ConcurrencyConflict('concurrency conflict!');
1384         }
1385
1386 //        // Maybe Later
1387 //        // exdates needs to stay in baseEvents container
1388 //        if ($_event->container_id != $baseEvent->container_id) {
1389 //            throw new Calendar_Exception_ExdateContainer();
1390 //        }
1391
1392         // check if this is an exception to the first occurence
1393         if ($baseEvent->getId() == $_event->getId()) {
1394             if ($_allFollowing) {
1395                 throw new Exception('please edit or delete complete series!');
1396             }
1397             // NOTE: if the baseEvent gets a time change, we can't compute the recurdid w.o. knowing the original dtstart
1398             $recurid = $baseEvent->setRecurId($baseEvent->getId());
1399             unset($baseEvent->recurid);
1400             $_event->recurid = $recurid;
1401         }
1402         
1403         // just do attender status update if user has no edit grant
1404         if ($this->_doContainerACLChecks && !$baseEvent->{Tinebase_Model_Grants::GRANT_EDIT}) {
1405             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1406                 . " user has no editGrant for event: '{$baseEvent->getId()}'. Only creating exception for attendee status");
1407             if ($_event->attendee instanceof Tinebase_Record_RecordSet) {
1408                 foreach ($_event->attendee as $attender) {
1409                     if ($attender->status_authkey) {
1410                         $exceptionAttender = $this->attenderStatusCreateRecurException($_event, $attender, $attender->status_authkey, $_allFollowing);
1411                     }
1412                 }
1413             }
1414             
1415             if (! isset($exceptionAttender)) {
1416                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG) && $_event->attendee instanceof Tinebase_Record_RecordSet) {
1417                     Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " Failed to update attendee: " . print_r($_event->attendee->toArray(), true));
1418                 }
1419                 throw new Tinebase_Exception_AccessDenied('Failed to update attendee, status authkey might be missing');
1420             }
1421             
1422             return $this->get($exceptionAttender->cal_event_id);
1423         }
1424         
1425         // NOTE: recurid is computed by rrule recur computations and therefore is already part of the event.
1426         if (empty($_event->recurid)) {
1427             throw new Exception('recurid must be present to create exceptions!');
1428         }
1429         
1430         // we do notifications ourself
1431         $sendNotifications = $this->sendNotifications(FALSE);
1432         
1433         // EDIT for baseEvent is checked above, CREATE, DELETE for recur exceptions is implied with it
1434         $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
1435         
1436         $db = $this->_backend->getAdapter();
1437         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
1438         
1439         $exdate = new Tinebase_DateTime(substr($_event->recurid, -19));
1440         $exdates = is_array($baseEvent->exdate) ? $baseEvent->exdate : array();
1441         $originalDtstart = $_event->getOriginalDtStart();
1442         $originalEvent = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $originalDtstart);
1443         
1444         if ($_allFollowing != TRUE) {
1445             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
1446                 . " Adding exdate for: '{$_event->recurid}'");
1447             
1448             array_push($exdates, $exdate);
1449             $baseEvent->exdate = $exdates;
1450             $updatedBaseEvent = $this->update($baseEvent, FALSE);
1451             
1452             if ($_deleteInstance == FALSE) {
1453                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
1454                     . " Creating persistent exception for: '{$_event->recurid}'");
1455                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
1456                     . " Recur exception: " . print_r($_event->toArray(), TRUE));
1457
1458                 $_event->base_event_id = $baseEvent->getId();
1459                 $_event->setId(NULL);
1460                 unset($_event->rrule);
1461                 unset($_event->exdate);
1462             
1463                 foreach (array('attendee', 'notes', 'alarms') as $prop) {
1464                     if ($_event->{$prop} instanceof Tinebase_Record_RecordSet) {
1465                         $_event->{$prop}->setId(NULL);
1466                     }
1467                 }
1468                 
1469                 $originalDtstart = $_event->getOriginalDtStart();
1470                 $dtStartHasDiff = $originalDtstart->compare($_event->dtstart) != 0; // php52 compat
1471                 
1472                 if (! $dtStartHasDiff) {
1473                     $attendees = $_event->attendee;
1474                     unset($_event->attendee);
1475                 }
1476                 $note = $_event->notes;
1477                 unset($_event->notes);
1478                 $persistentExceptionEvent = $this->create($_event, $_checkBusyConflicts);
1479                 
1480                 if (! $dtStartHasDiff) {
1481                     // we save attendee seperatly to preserve their attributes
1482                     if ($attendees instanceof Tinebase_Record_RecordSet) {
1483                         $attendees->cal_event_id = $persistentExceptionEvent->getId();
1484                         $calendar = Tinebase_Container::getInstance()->getContainerById($_event->container_id);
1485                         foreach ($attendees as $attendee) {
1486                             $this->_createAttender($attendee, $_event, TRUE, $calendar);
1487                             $this->_increaseDisplayContainerContentSequence($attendee, $persistentExceptionEvent, Tinebase_Model_ContainerContent::ACTION_CREATE);
1488                         }
1489                     }
1490                 }
1491                 
1492                 // @todo save notes and add a update note -> what was updated? -> modlog is also missing
1493                 $persistentExceptionEvent = $this->get($persistentExceptionEvent->getId());
1494             }
1495             
1496         } else {
1497             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " shorten recur series for/to: '{$_event->recurid}'");
1498                 
1499             // split past/future exceptions
1500             $pastExdates = array();
1501             $futureExdates = array();
1502             foreach($exdates as $exdate) {
1503                 $exdate->isLater($_event->dtstart) ? $futureExdates[] = $exdate : $pastExdates[] = $exdate;
1504             }
1505             
1506             $persistentExceptionEvents = $this->getRecurExceptions($_event);
1507             $pastPersistentExceptionEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
1508             $futurePersistentExceptionEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
1509             foreach ($persistentExceptionEvents as $persistentExceptionEvent) {
1510                 $persistentExceptionEvent->getOriginalDtStart()->isLater($_event->dtstart) ?
1511                     $futurePersistentExceptionEvents->addRecord($persistentExceptionEvent) :
1512                     $pastPersistentExceptionEvents->addRecord($persistentExceptionEvent);
1513             }
1514             
1515             // update baseEvent
1516             $rrule = Calendar_Model_Rrule::getRruleFromString($baseEvent->rrule);
1517             if (isset($rrule->count)) {
1518                 // get all occurences and find the split
1519                 
1520                 $exdate = $baseEvent->exdate;
1521                 $baseEvent->exdate = NULL;
1522                     //$baseCountOccurrence = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $baseEvent->rrule_until, $baseCount);
1523                 $recurSet = Calendar_Model_Rrule::computeRecurrenceSet($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $baseEvent->dtstart, $baseEvent->rrule_until);
1524                 $baseEvent->exdate = $exdate;
1525                 
1526                 $originalDtstart = $_event->getOriginalDtStart();
1527                 foreach($recurSet as $idx => $rInstance) {
1528                     if ($rInstance->dtstart >= $originalDtstart) break;
1529                 }
1530                 
1531                 $rrule->count = $idx+1;
1532             } else {
1533                 $lastBaseOccurence = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $_event->getOriginalDtStart()->subSecond(1), -1);
1534                 $rrule->until = $lastBaseOccurence ? $lastBaseOccurence->getOriginalDtStart() : $baseEvent->dtstart;
1535             }
1536             $baseEvent->rrule = (string) $rrule;
1537             $baseEvent->exdate = $pastExdates;
1538
1539             // NOTE: we don't want implicit attendee updates
1540             //$updatedBaseEvent = $this->update($baseEvent, FALSE);
1541             $this->_inspectEvent($baseEvent);
1542             $updatedBaseEvent = parent::update($baseEvent);
1543             
1544             if ($_deleteInstance == TRUE) {
1545                 // delete all future persistent events
1546                 $this->delete($futurePersistentExceptionEvents->getId());
1547             } else {
1548                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " create new recur series for/at: '{$_event->recurid}'");
1549                 
1550                 // NOTE: in order to move exceptions correctly in time we need to find out the original dtstart
1551                 //       and create the new baseEvent with this time. A following update also updates its exceptions
1552                 $originalDtstart = new Tinebase_DateTime(substr($_event->recurid, -19));
1553                 $adoptedDtstart = clone $_event->dtstart;
1554                 $dtStartHasDiff = $adoptedDtstart->compare($originalDtstart) != 0; // php52 compat
1555                 $eventLength = $_event->dtstart->diff($_event->dtend);
1556                 
1557                 $_event->dtstart = clone $originalDtstart;
1558                 $_event->dtend = clone $originalDtstart;
1559                 $_event->dtend->add($eventLength);
1560                 
1561                 // adopt count
1562                 if (isset($rrule->count)) {
1563                     $baseCount = $rrule->count;
1564                     $rrule = Calendar_Model_Rrule::getRruleFromString($_event->rrule);
1565                     $rrule->count = $rrule->count - $baseCount;
1566                     $_event->rrule = (string) $rrule;
1567                 }
1568                 
1569                 $_event->setId(Tinebase_Record_Abstract::generateUID());
1570                 $_event->uid = $futurePersistentExceptionEvents->uid = Tinebase_Record_Abstract::generateUID();
1571                 $_event->setId(Tinebase_Record_Abstract::generateUID());
1572                 $futurePersistentExceptionEvents->setRecurId($_event->getId());
1573                 unset($_event->recurid);
1574                 unset($_event->base_event_id);
1575                 foreach(array('attendee', 'notes', 'alarms') as $prop) {
1576                     if ($_event->{$prop} instanceof Tinebase_Record_RecordSet) {
1577                         $_event->{$prop}->setId(NULL);
1578                     }
1579                 }
1580                 $_event->exdate = $futureExdates;
1581
1582                 $attendees = $_event->attendee; unset($_event->attendee);
1583                 $note = $_event->notes; unset($_event->notes);
1584                 $persistentExceptionEvent = $this->create($_event, $_checkBusyConflicts && $dtStartHasDiff);
1585                 
1586                 // we save attendee separately to preserve their attributes
1587                 if ($attendees instanceof Tinebase_Record_RecordSet) {
1588                     foreach($attendees as $attendee) {
1589                         $this->_createAttender($attendee, $persistentExceptionEvent, true);
1590                     }
1591                 }
1592                 
1593                 // @todo save notes and add a update note -> what was updated? -> modlog is also missing
1594                 
1595                 $persistentExceptionEvent = $this->get($persistentExceptionEvent->getId());
1596                 
1597                 foreach($futurePersistentExceptionEvents as $futurePersistentExceptionEvent) {
1598                     $this->update($futurePersistentExceptionEvent, FALSE);
1599                 }
1600                 
1601                 if ($dtStartHasDiff) {
1602                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " new recur series has adpted dtstart -> update to adopt exceptions'");
1603                     $persistentExceptionEvent->dtstart = clone $adoptedDtstart;
1604                     $persistentExceptionEvent->dtend = clone $adoptedDtstart;
1605                     $persistentExceptionEvent->dtend->add($eventLength);
1606                     
1607                     $persistentExceptionEvent = $this->update($persistentExceptionEvent, $_checkBusyConflicts);
1608                 }
1609             }
1610         }
1611         
1612         Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1613         
1614         // restore original notification handling
1615         $this->sendNotifications($sendNotifications);
1616         $notificationAction = $_deleteInstance ? 'deleted' : 'changed';
1617         $notificationEvent = $_deleteInstance ? $_event : $persistentExceptionEvent;
1618         
1619         // restore acl
1620         $this->doContainerACLChecks($doContainerACLChecks);
1621         
1622         // send notifications
1623         if ($this->_sendNotifications && $_event->mute != 1) {
1624             // NOTE: recur exception is a fake event from client. 
1625             //       this might lead to problems, so we wrap the calls
1626             try {
1627                 if (count($_event->attendee) > 0) {
1628                     $_event->attendee->bypassFilters = TRUE;
1629                 }
1630                 $_event->created_by = $baseEvent->created_by;
1631                 
1632                 $this->doSendNotifications($notificationEvent, Tinebase_Core::getUser(), $notificationAction, $originalEvent);
1633             } catch (Exception $e) {
1634                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getTraceAsString());
1635                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " could not send notification {$e->getMessage()}");
1636             }
1637         }
1638         
1639         return $_deleteInstance ? $updatedBaseEvent : $persistentExceptionEvent;
1640     }
1641     
1642     /**
1643      * (non-PHPdoc)
1644      * @see Tinebase_Controller_Record_Abstract::get()
1645      */
1646     public function get($_id, $_containerId = NULL, $_getRelatedData = TRUE, $_getDeleted = FALSE)
1647     {
1648         if (preg_match('/^fakeid(.*)\/(.*)/', $_id, $matches)) {
1649             $baseEvent = $this->get($matches[1]);
1650             $exceptions = $this->getRecurExceptions($baseEvent);
1651             $originalDtStart = new Tinebase_DateTime($matches[2]);
1652
1653             $exdates = $exceptions->getOriginalDtStart();
1654             $exdate = array_search($originalDtStart, $exdates);
1655
1656             return $exdate !== false ? $exceptions[$exdate] :
1657                 Calendar_Model_Rrule::computeNextOccurrence($baseEvent, $exceptions, $originalDtStart);
1658         } else {
1659             return parent::get($_id, $_containerId, $_getRelatedData, $_getDeleted);
1660         }
1661     }
1662     
1663     /**
1664      * returns base event of a recurring series
1665      *
1666      * @param  Calendar_Model_Event $_event
1667      * @return Calendar_Model_Event
1668      */
1669     public function getRecurBaseEvent($_event)
1670     {
1671         $baseEventId = $_event->base_event_id ?: $_event->id;
1672
1673         if (! $baseEventId) {
1674             throw new Tinebase_Exception_NotFound('base event of a recurring series not found');
1675         }
1676
1677         // make sure we have a 'fully featured' event
1678         return $this->get($baseEventId);
1679     }
1680
1681     /**
1682      * returns all persistent recur exceptions of recur series
1683      * 
1684      * NOTE: deleted instances are saved in the base events exception property
1685      * NOTE: returns all exceptions regardless of current filters and access restrictions
1686      * 
1687      * @param  Calendar_Model_Event        $_event
1688      * @param  boolean                     $_fakeDeletedInstances
1689      * @param  Calendar_Model_EventFilter  $_eventFilter
1690      * @return Tinebase_Record_RecordSet of Calendar_Model_Event
1691      */
1692     public function getRecurExceptions($_event, $_fakeDeletedInstances = FALSE, $_eventFilter = NULL)
1693     {
1694         $baseEventId = $_event->base_event_id ?: $_event->id;
1695
1696         $exceptionFilter = new Calendar_Model_EventFilter(array(
1697             array('field' => 'base_event_id', 'operator' => 'equals',  'value' => $baseEventId),
1698             array('field' => 'recurid',       'operator' => 'notnull', 'value' => NULL)
1699         ));
1700         
1701         if ($_eventFilter instanceof Calendar_Model_EventFilter) {
1702             $exceptionFilter->addFilterGroup($_eventFilter);
1703         }
1704         
1705         $exceptions = $this->_backend->search($exceptionFilter);
1706         
1707         if ($_fakeDeletedInstances) {
1708             $baseEvent = $this->getRecurBaseEvent($_event);
1709             $this->fakeDeletedExceptions($baseEvent, $exceptions);
1710         }
1711         
1712         $exceptions->exdate = NULL;
1713         $exceptions->rrule = NULL;
1714         $exceptions->rrule_until = NULL;
1715         
1716         return $exceptions;
1717     }
1718
1719     /**
1720      * add exceptions events for deleted instances
1721      *
1722      * @param Calendar_Model_Event $baseEvent
1723      * @param Tinebase_Record_RecordSet $exceptions
1724      */
1725     public function fakeDeletedExceptions($baseEvent, $exceptions)
1726     {
1727         $eventLength = $baseEvent->dtstart->diff($baseEvent->dtend);
1728
1729         // compute remaining exdates
1730         $deletedInstanceDtStarts = array_diff(array_unique((array) $baseEvent->exdate), $exceptions->getOriginalDtStart());
1731
1732         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1733             ' Faking ' . count($deletedInstanceDtStarts) . ' deleted exceptions');
1734
1735         foreach((array) $deletedInstanceDtStarts as $deletedInstanceDtStart) {
1736             $fakeEvent = clone $baseEvent;
1737             $fakeEvent->setId(NULL);
1738
1739             $fakeEvent->dtstart = clone $deletedInstanceDtStart;
1740             $fakeEvent->dtend = clone $deletedInstanceDtStart;
1741             $fakeEvent->dtend->add($eventLength);
1742             $fakeEvent->is_deleted = TRUE;
1743             $fakeEvent->setRecurId($baseEvent->getId());
1744             $fakeEvent->rrule = null;
1745
1746             $exceptions->addRecord($fakeEvent);
1747         }
1748     }
1749
1750    /**
1751     * adopt alarm time to next occurrence for recurring events
1752     *
1753     * @param Tinebase_Record_Abstract $_record
1754     * @param Tinebase_Model_Alarm $_alarm
1755     * @param bool $_nextBy {instance|time} set recurr alarm to next from given instance or next by current time
1756     * @return void
1757     */
1758     public function adoptAlarmTime(Tinebase_Record_Abstract $_record, Tinebase_Model_Alarm $_alarm, $_nextBy = 'time')
1759     {
1760         if ($_record->rrule) {
1761             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1762                  ' Adopting alarm time for next recur occurrence (by ' . $_nextBy . ')');
1763             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
1764                  ' ' . print_r($_record->toArray(), TRUE));
1765             
1766             if ($_nextBy == 'time') {
1767                 // NOTE: this also finds instances running right now
1768                 $from = Tinebase_DateTime::now();
1769             
1770             } else {
1771                 $recurid = $_alarm->getOption('recurid');
1772                 $instanceStart = $recurid ? new Tinebase_DateTime(substr($recurid, -19)) : clone $_record->dtstart;
1773                 $eventLength = $_record->dtstart->diff($_record->dtend);
1774                 
1775                 $instanceStart->setTimezone($_record->originator_tz);
1776                 $from = $instanceStart->add($eventLength);
1777                 $from->setTimezone('UTC');
1778             }
1779             
1780             // compute next
1781             $exceptions = $this->getRecurExceptions($_record);
1782             $nextOccurrence = Calendar_Model_Rrule::computeNextOccurrence($_record, $exceptions, $from);
1783             
1784             if ($nextOccurrence === NULL) {
1785                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
1786                     ' Recur series is over, no more alarms pending');
1787             } else {
1788                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1789                     ' Found next occurrence, adopting alarm to dtstart ' . $nextOccurrence->dtstart->toString());
1790             }
1791             
1792             // save recurid so we know for which recurrance the alarm is for
1793             $_alarm->setOption('recurid', isset($nextOccurrence) ? $nextOccurrence->recurid : NULL);
1794             
1795             $_alarm->sent_status = $nextOccurrence ? Tinebase_Model_Alarm::STATUS_PENDING : Tinebase_Model_Alarm::STATUS_SUCCESS;
1796             $_alarm->sent_message = $nextOccurrence ?  '' : 'Nothing to send, series is over';
1797             
1798             $eventStart = $nextOccurrence ? clone $nextOccurrence->dtstart : clone $_record->dtstart;
1799         } else {
1800             $eventStart = clone $_record->dtstart;
1801         }
1802         
1803         // save minutes before / compute it for custom alarms
1804         $minutesBefore = $_alarm->minutes_before == Tinebase_Model_Alarm::OPTION_CUSTOM 
1805             ? ($_record->dtstart->getTimestamp() - $_alarm->alarm_time->getTimestamp()) / 60 
1806             : $_alarm->minutes_before;
1807         $minutesBefore = round($minutesBefore);
1808         
1809         $_alarm->setOption('minutes_before', $minutesBefore);
1810         $_alarm->alarm_time = $eventStart->subMinute($minutesBefore);
1811         
1812         if ($_record->rrule && $_alarm->sent_status == Tinebase_Model_Alarm::STATUS_PENDING && $_alarm->alarm_time < $_alarm->sent_time) {
1813             $this->adoptAlarmTime($_record, $_alarm, 'instance');
1814         }
1815         
1816         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
1817             ' alarm: ' . print_r($_alarm->toArray(), true));
1818     }
1819     
1820     /****************************** overwritten functions ************************/
1821     
1822     /**
1823      * restore original alarm time of recurring events
1824      * 
1825      * @param Tinebase_Record_Interface $_record
1826      * @return void
1827      */
1828     protected function _inspectAlarmGet(Tinebase_Record_Interface $_record)
1829     {
1830         foreach ($_record->alarms as $alarm) {
1831             if ($recurid = $alarm->getOption('recurid')) {
1832                 $alarm->alarm_time = clone $_record->dtstart;
1833                 $alarm->alarm_time->subMinute((int) $alarm->getOption('minutes_before'));
1834             }
1835         }
1836         
1837         parent::_inspectAlarmGet($_record);
1838     }
1839     
1840     /**
1841      * adopt alarm time to next occurance for recurring events
1842      * 
1843      * @param Tinebase_Record_Interface $_record
1844      * @param Tinebase_Model_Alarm $_alarm
1845      * @param bool $_nextBy {instance|time} set recurr alarm to next from given instance or next by current time
1846      * @return void
1847      * @throws Tinebase_Exception_InvalidArgument
1848      */
1849     protected function _inspectAlarmSet(Tinebase_Record_Interface $_record, Tinebase_Model_Alarm $_alarm, $_nextBy = 'time')
1850     {
1851         parent::_inspectAlarmSet($_record, $_alarm);
1852         $this->adoptAlarmTime($_record, $_alarm, 'time');
1853     }
1854     
1855     /**
1856      * inspect update of one record
1857      * 
1858      * @param   Tinebase_Record_Interface $_record      the update record
1859      * @param   Tinebase_Record_Interface $_oldRecord   the current persistent record
1860      * @return  void
1861      */
1862     protected function _inspectBeforeUpdate($_record, $_oldRecord)
1863     {
1864         // if dtstart of an event changes, we update the originator_tz, alarm times
1865         if (! $_oldRecord->dtstart->equals($_record->dtstart)) {
1866             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' dtstart changed -> adopting organizer_tz');
1867             $_record->originator_tz = Tinebase_Core::getUserTimezone();
1868             if (! empty($_record->rrule)) {
1869                 $diff = $_oldRecord->dtstart->diff($_record->dtstart);
1870                 $this->_updateRecurIdOfExdates($_record, $diff);
1871             }
1872         }
1873         
1874         // delete recur exceptions if update is no longer a recur series
1875         if (! empty($_oldRecord->rrule) && empty($_record->rrule)) {
1876             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' deleting recur exceptions as event is no longer a recur series');
1877             $this->_backend->delete($this->getRecurExceptions($_record));
1878         }
1879         
1880         // touch base event of a recur series if an persistent exception changes
1881         if ($_record->recurid) {
1882             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' touch base event of a persistent exception');
1883             $baseEvent = $this->getRecurBaseEvent($_record);
1884             $this->_touch($baseEvent, TRUE);
1885         }
1886
1887         if (empty($_record->recurid) && ! empty($_record->rrule) && (string)$_record->rrule == (string)$_oldRecord->rrule && $_record->dtstart->compare($_oldRecord->dtstart, 'Y-m-d') !== 0) {
1888             // we are in a base event & dtstart changed the date & the rrule was not changed
1889             // if easy rrule, try to adapt
1890             $rrule = $_record->rrule;
1891             if (! $rrule instanceof Calendar_Model_Rrule) {
1892                 $rrule = new Calendar_Model_Rrule($rrule);
1893             } else {
1894                 $rrule = clone $rrule;
1895             }
1896
1897             switch($rrule->freq)
1898             {
1899                 case Calendar_Model_Rrule::FREQ_WEEKLY:
1900                     // only do simple bydays
1901                     if (empty($rrule->byday) || !isset(Calendar_Model_Rrule::$WEEKDAY_MAP[$rrule->byday])) {
1902                         break;
1903                     }
1904
1905                     // check old dtstart matches byday, if not, we abort
1906                     if (strtolower($_oldRecord->dtstart->format('D')) !== Calendar_Model_Rrule::$WEEKDAY_MAP[$rrule->byday]) {
1907                         break;
1908                     }
1909
1910                     $rrule->byday = Calendar_Model_Rrule::$WEEKDAY_MAP_REVERSE[strtolower($_record->dtstart->format('D'))];
1911                     $_record->rrule = $rrule;
1912                     break;
1913
1914                 case Calendar_Model_Rrule::FREQ_MONTHLY:
1915                     // if there is no day specification, nothing to do
1916                     if (empty($rrule->byday) && empty($rrule->bymonthday)) {
1917                         break;
1918                     }
1919                     // only do simple rules
1920                     if (!empty($rrule->byday) && (strpos(',', $rrule->byday) !== false)) {
1921                         break;
1922                     }
1923                     if (!empty($rrule->bymonthday) && strpos(',', $rrule->bymonthday) !== false) {
1924                         break;
1925                     }
1926                     if (!empty($rrule->byday) && !empty($rrule->bymonthday)) {
1927                         break;
1928                     }
1929
1930                     if (!empty($rrule->byday)) {
1931                         $bydayPrefix = intval($rrule->byday);
1932                         $byday = substr($rrule->byday, -2);
1933
1934                         // if we dont have a quantifier we abort
1935                         if ($bydayPrefix === 0) {
1936                             break;
1937                         }
1938
1939                         // check old dtstart matches byday, if not we abort
1940                         if (strtolower($_oldRecord->dtstart->format('D')) !== Calendar_Model_Rrule::$WEEKDAY_MAP[$byday]) {
1941                             break;
1942                         }
1943
1944                         $dtstartJ = $_oldRecord->dtstart->format('j');
1945                         // check old dtstart matches bydayPrefix, if not we abort
1946                         if ($bydayPrefix === -1) {
1947                             if ($_oldRecord->dtstart->format('t') - $dtstartJ > 6) {
1948                                 break;
1949                             }
1950                         } else {
1951                             if ($dtstartJ - (($bydayPrefix-1)*7) > 6 || $dtstartJ - (($bydayPrefix-1)*7) < 1) {
1952                                 break;
1953                             }
1954                         }
1955
1956                         if ($_record->dtstart->format('j') > 28 || ($bydayPrefix === -1 && $_record->dtstart->format('t') - $_record->dtstart->format('j') < 7)) {
1957                             // keep -1 => last X
1958                             $prefix = '-1';
1959                         } else {
1960                             $prefix = floor(($_record->dtstart->format('j') - 1) / 7) + 1;
1961                         }
1962
1963                         $rrule->byday = $prefix . Calendar_Model_Rrule::$WEEKDAY_MAP_REVERSE[strtolower($_record->dtstart->format('D'))];
1964                         $_record->rrule = $rrule;
1965
1966                     } else {
1967
1968                         // check old dtstart matches bymonthday, if not we abort
1969                         if ($_oldRecord->dtstart->format('j') != $rrule->bymonthday) {
1970                             break;
1971                         }
1972
1973                         $rrule->bymonthday = $_record->dtstart->format('j');
1974                         $_record->rrule = $rrule;
1975                     }
1976
1977                     break;
1978
1979                 case Calendar_Model_Rrule::FREQ_YEARLY:
1980                     // only do simple rules
1981                     if (! empty($rrule->byday) || empty($rrule->bymonth) || empty($rrule->bymonthday) || strpos($rrule->bymonth, ',') !== false ||
1982                         strpos($rrule->bymonthday, ',') !== false ||
1983                         // check old dtstart matches the date
1984                         $_oldRecord->dtstart->format('j') != $rrule->bymonthday || $_oldRecord->dtstart->format('n') != $rrule->bymonth
1985                     ) {
1986                         break;
1987                     }
1988
1989                     $rrule->bymonthday = $_record->dtstart->format('j');
1990                     $rrule->bymonth = $_record->dtstart->format('n');
1991                     $_record->rrule = $rrule;
1992
1993                     break;
1994             }
1995         }
1996     }
1997     
1998     /**
1999      * update exdates and recurids if dtstart of an recurevent changes
2000      * 
2001      * @param Calendar_Model_Event $_record
2002      * @param DateInterval $diff
2003      */
2004     protected function _updateRecurIdOfExdates($_record, $diff)
2005     {
2006         // update exceptions
2007         $exceptions = $this->getRecurExceptions($_record);
2008         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' dtstart of a series changed -> adopting '. count($exceptions) . ' recurid(s)');
2009         $exdates = array();
2010         foreach ($exceptions as $exception) {
2011             $exception->recurid = new Tinebase_DateTime(substr($exception->recurid, -19));
2012             Calendar_Model_Rrule::addUTCDateDstFix($exception->recurid, $diff, $_record->originator_tz);
2013             $exdates[] = $exception->recurid;
2014             
2015             $exception->setRecurId($_record->getId());
2016             $this->_backend->update($exception);
2017         }
2018         
2019         $_record->exdate = $exdates;
2020     }
2021     
2022     /**
2023      * inspect before create/update
2024      * 
2025      * @TODO move stuff from other places here
2026      * @param   Calendar_Model_Event $_record      the record to inspect
2027      */
2028     protected function _inspectEvent($_record, $skipEvent = false)
2029     {
2030         $_record->uid = $_record->uid ? $_record->uid : Tinebase_Record_Abstract::generateUID();
2031         $_record->organizer = $_record->organizer ? $_record->organizer : Tinebase_Core::getUser()->contact_id;
2032         $_record->transp = $_record->transp ? $_record->transp : Calendar_Model_Event::TRANSP_OPAQUE;
2033
2034         $this->_inspectOriginatorTZ($_record);
2035
2036         if ($_record->hasExternalOrganizer()) {
2037             // assert calendarUser as attendee. This is important to keep the event in the loop via its displaycontianer(s)
2038             try {
2039                 $container = Tinebase_Container::getInstance()->getContainerById($_record->container_id);
2040                 $owner = $container->getOwner();
2041                 $calendarUserId = Addressbook_Controller_Contact::getInstance()->getContactByUserId($owner, true)->getId();
2042             } catch (Exception $e) {
2043                 $container = NULL;
2044                 $calendarUserId = Tinebase_Core::getUser()->contact_id;
2045             }
2046             
2047             $attendee = $_record->assertAttendee(new Calendar_Model_Attender(array(
2048                 'user_type'    => Calendar_Model_Attender::USERTYPE_USER,
2049                 'user_id'      => $calendarUserId
2050             )), false, false, true);
2051             
2052             if ($attendee && $container instanceof Tinebase_Model_Container) {
2053                 $attendee->displaycontainer_id = $container->getId();
2054             }
2055             
2056             if (! $container instanceof Tinebase_Model_Container || $container->type == Tinebase_Model_Container::TYPE_PERSONAL) {
2057                 // move into special (external users) container
2058                 $container = Calendar_Controller::getInstance()->getInvitationContainer($_record->resolveOrganizer());
2059                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2060                     . ' Setting container_id to ' . $container->getId() . ' for external organizer ' . $_record->organizer->email);
2061                 $_record->container_id = $container->getId();
2062             }
2063             
2064         }
2065         
2066         if ($_record->is_all_day_event) {
2067             // harmonize datetimes of all day events
2068             $_record->setTimezone($_record->originator_tz);
2069             if (! $_record->dtend) {
2070                 $_record->dtend = clone $_record->dtstart;
2071                 $_record->dtend->setTime(23,59,59);
2072             }
2073             $_record->dtstart->setTime(0,0,0);
2074             $_record->dtend->setTime(23,59,59);
2075             $_record->setTimezone('UTC');
2076         }
2077         $_record->setRruleUntil();
2078         
2079         if ($_record->rrule instanceof Calendar_Model_Rrule) {
2080             $_record->rrule->normalize($_record);
2081         }
2082
2083         if ($_record->isRecurException()) {
2084             $_record->rrule = NULL;
2085             $_record->rrule_constraints = NULL;
2086
2087 //            // Maybe Later
2088 //            $baseEvent = $this->getRecurBaseEvent($_record);
2089 //            // exdates needs to stay in baseEvents container
2090 //            if($_record->container_id != $baseEvent->container_id) {
2091 //                throw new Calendar_Exception_ExdateContainer();
2092 //            }
2093         }
2094
2095         // inspect rrule_constraints
2096         if ($_record->rrule_constraints) {
2097             $this->setConstraintsExdates($_record);
2098         }
2099
2100         if (!$skipEvent) {
2101             Tinebase_Record_PersistentObserver::getInstance()->fireEvent(new Calendar_Event_InspectEvent(array(
2102                 'observable' => $_record
2103             )));
2104         }
2105     }
2106
2107     /**
2108      * checks/sets originator timezone
2109      *
2110      * @param $record
2111      * @throws Tinebase_Exception_Record_Validation
2112      */
2113     protected function _inspectOriginatorTZ($record)
2114     {
2115         $record->originator_tz = $record->originator_tz ? $record->originator_tz : Tinebase_Core::getUserTimezone();
2116
2117         try {
2118             new DateTimeZone($record->originator_tz);
2119         } catch (Exception $e) {
2120             throw new Tinebase_Exception_Record_Validation('Bad Timezone: ' . $record->originator_tz);
2121         }
2122     }
2123     
2124     /**
2125      * inspects delete action
2126      *
2127      * @param array $_ids
2128      * @return array of ids to actually delete
2129      */
2130     protected function _inspectDelete(array $_ids) {
2131         $events = $this->_backend->getMultiple($_ids);
2132         
2133         foreach ($events as $event) {
2134             
2135             // implicitly delete persistent recur instances of series
2136             if (! empty($event->rrule)) {
2137                 $exceptionIds = $this->getRecurExceptions($event)->getId();
2138                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2139                     . ' Implicitly deleting ' . (count($exceptionIds) - 1 ) . ' persistent exception(s) for recurring series with uid' . $event->uid);
2140                 $_ids = array_merge($_ids, $exceptionIds);
2141             }
2142
2143             Tinebase_Record_PersistentObserver::getInstance()->fireEvent(new Calendar_Event_InspectDeleteEvent([
2144                 'observable' => $event,
2145                 'deletedIds' => $_ids
2146             ]));
2147         }
2148         
2149         $this->_deleteAlarmsForIds($_ids);
2150         
2151         return array_unique($_ids);
2152     }
2153     
2154     /**
2155      * redefine required grants for get actions
2156      * 
2157      * @param Tinebase_Model_Filter_FilterGroup $_filter
2158      * @param string $_action get|update
2159      */
2160     public function checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
2161     {
2162         $hasGrantsFilter = FALSE;
2163         foreach($_filter->getAclFilters() as $aclFilter) {
2164             if ($aclFilter instanceof Calendar_Model_GrantFilter) {
2165                 $hasGrantsFilter = TRUE;
2166                 break;
2167             }
2168         }
2169         
2170         if (! $hasGrantsFilter && $this->_doContainerACLChecks) {
2171             // force a grant filter
2172             // NOTE: actual grants are set via setRequiredGrants later
2173             $grantsFilter = $_filter->createFilter('grants', 'in', '@setRequiredGrants');
2174             $_filter->addFilter($grantsFilter);
2175         }
2176         
2177         parent::checkFilterACL($_filter, $_action);
2178         
2179         if ($_action == 'get') {
2180             $_filter->setRequiredGrants(array(
2181                 Tinebase_Model_Grants::GRANT_FREEBUSY,
2182                 Tinebase_Model_Grants::GRANT_READ,
2183                 Tinebase_Model_Grants::GRANT_ADMIN,
2184             ));
2185         }
2186     }
2187     
2188     /**
2189      * check grant for action (CRUD)
2190      *
2191      * @param Tinebase_Record_Interface $_record
2192      * @param string $_action
2193      * @param boolean $_throw
2194      * @param string $_errorMessage
2195      * @param Tinebase_Record_Interface $_oldRecord
2196      * @return boolean
2197      * @throws Tinebase_Exception_AccessDenied
2198      * 
2199      * @todo use this function in other create + update functions
2200      * @todo invent concept for simple adding of grants (plugins?) 
2201      */
2202     protected function _checkGrant($_record, $_action, $_throw = TRUE, $_errorMessage = 'No Permission.', $_oldRecord = NULL)
2203     {
2204         if (    ! $this->_doContainerACLChecks 
2205             // admin grant includes all others (only if class is PUBLIC)
2206             ||  (! empty($this->class) && $this->class === Calendar_Model_Event::CLASS_PUBLIC 
2207                 && $_record->container_id && Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADMIN))
2208             // external invitations are in a spechial invitaion calendar. only attendee can see it via displaycal
2209             ||  $_record->hasExternalOrganizer()
2210         ) {
2211             return true;
2212         }
2213         
2214         switch ($_action) {
2215             case 'get':
2216                 // NOTE: free/busy is not a read grant!
2217                 $hasGrant = $_record->hasGrant(Tinebase_Model_Grants::GRANT_READ);
2218                 if (! $hasGrant) {
2219                     $_record->doFreeBusyCleanup();
2220                 }
2221                 break;
2222             case 'create':
2223                 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADD);
2224                 break;
2225             case 'update':
2226                 $hasGrant = (bool) $_oldRecord->hasGrant(Tinebase_Model_Grants::GRANT_EDIT);
2227                 
2228                 if ($_oldRecord->container_id != $_record->container_id) {
2229                     $hasGrant &= Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADD)
2230                                  && $_oldRecord->hasGrant(Tinebase_Model_Grants::GRANT_DELETE);
2231                 }
2232                 break;
2233             case 'delete':
2234                 $hasGrant = (bool) $_record->hasGrant(Tinebase_Model_Grants::GRANT_DELETE);
2235                 break;
2236             case 'sync':
2237                 $hasGrant = (bool) $_record->hasGrant(Tinebase_Model_Grants::GRANT_SYNC);
2238                 break;
2239             case 'export':
2240                 $hasGrant = (bool) $_record->hasGrant(Tinebase_Model_Grants::GRANT_EXPORT);
2241                 break;
2242         }
2243         
2244         if (! $hasGrant) {
2245             if ($_throw) {
2246                 throw new Tinebase_Exception_AccessDenied($_errorMessage);
2247             } else {
2248                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2249                     . ' No permissions to ' . $_action . ' in container ' . $_record->container_id);
2250             }
2251         }
2252         
2253         return $hasGrant;
2254     }
2255     
2256     /**
2257      * touches (sets seq, last_modified_time and container content sequence) given event
2258      * 
2259      * @param  $_event
2260      * @return void
2261      */
2262     protected function _touch($_event, $_setModifier = FALSE)
2263     {
2264         $_event->last_modified_time = Tinebase_DateTime::now();
2265         $_event->seq = (int)$_event->seq + 1;
2266         if ($_setModifier) {
2267             $_event->last_modified_by = Tinebase_Core::getUser()->getId();
2268         }
2269         
2270         $this->_backend->update($_event);
2271         
2272         $this->_increaseContainerContentSequence($_event, Tinebase_Model_ContainerContent::ACTION_UPDATE);
2273     }
2274     
2275     /**
2276      * increase container content sequence
2277      *
2278      * @param Tinebase_Record_Interface $_record
2279      * @param string $action
2280      */
2281     protected function _increaseContainerContentSequence(Tinebase_Record_Interface $record, $action = NULL)
2282     {
2283         parent::_increaseContainerContentSequence($record, $action);
2284         
2285         if ($record->attendee instanceof Tinebase_Record_RecordSet) {
2286             $updatedContainerIds = array($record->container_id);
2287             foreach ($record->attendee as $attender) {
2288                 if (isset($attender->displaycontainer_id) && ! in_array($attender->displaycontainer_id, $updatedContainerIds)) {
2289                     Tinebase_Container::getInstance()->increaseContentSequence($attender->displaycontainer_id, $action, $record->getId());
2290                     $updatedContainerIds[] = $attender->displaycontainer_id;
2291                 }
2292             }
2293         }
2294     }
2295     
2296     
2297     /****************************** attendee functions ************************/
2298     
2299     /**
2300      * creates an attender status exception of a recurring event series
2301      * 
2302      * NOTE: Recur exceptions are implicitly created
2303      *
2304      * @param  Calendar_Model_Event    $_recurInstance
2305      * @param  Calendar_Model_Attender $_attender
2306      * @param  string                  $_authKey
2307      * @param  bool                    $_allFollowing
2308      * @return Calendar_Model_Attender updated attender
2309      */
2310     public function attenderStatusCreateRecurException($_recurInstance, $_attender, $_authKey, $_allFollowing = FALSE)
2311     {
2312         try {
2313             $db = $this->_backend->getAdapter();
2314             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
2315             
2316             $baseEvent = $this->getRecurBaseEvent($_recurInstance);
2317             $baseEventAttendee = Calendar_Model_Attender::getAttendee($baseEvent->attendee, $_attender);
2318             
2319             if ($baseEvent->getId() == $_recurInstance->getId()) {
2320                 // exception to the first occurence
2321                 $_recurInstance->setRecurId($baseEvent->getId());
2322             }
2323             
2324             // NOTE: recurid is computed by rrule recur computations and therefore is already part of the event.
2325             if (empty($_recurInstance->recurid)) {
2326                 throw new Exception('recurid must be present to create exceptions!');
2327             }
2328             
2329             try {
2330                 // check if we already have a persistent exception for this event
2331                 $eventInstance = $this->_backend->getByProperty($_recurInstance->recurid, $_property = 'recurid');
2332                 
2333                 // NOTE: the user must exist (added by someone with appropriate rights by createRecurException)
2334                 $exceptionAttender = Calendar_Model_Attender::getAttendee($eventInstance->attendee, $_attender);
2335                 if (! $exceptionAttender) {
2336                     throw new Tinebase_Exception_AccessDenied('not an attendee');
2337                 }
2338                 
2339                 
2340                 if ($exceptionAttender->status_authkey != $_authKey) {
2341                     // NOTE: it might happen, that the user set her status from the base event without knowing about 
2342                     //       an existing exception. In this case the base event authkey is also valid
2343                     if (! $baseEventAttendee || $baseEventAttendee->status_authkey != $_authKey) {
2344                         throw new Tinebase_Exception_AccessDenied('Attender authkey mismatch');
2345                     }
2346                 }
2347                 
2348             } catch (Tinebase_Exception_NotFound $e) {
2349                 // otherwise create it implicilty
2350                 
2351                 // check if this intance takes place
2352                 if (in_array($_recurInstance->dtstart, (array)$baseEvent->exdate)) {
2353                     throw new Tinebase_Exception_AccessDenied('Event instance is deleted and may not be recreated via status setting!');
2354                 }
2355                 
2356                 if (! $baseEventAttendee) {
2357                     throw new Tinebase_Exception_AccessDenied('not an attendee');
2358                 }
2359                 
2360                 if ($baseEventAttendee->status_authkey != $_authKey) {
2361                     throw new Tinebase_Exception_AccessDenied('Attender authkey mismatch');
2362                 }
2363                 
2364                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " creating recur exception for a exceptional attendee status");
2365                 
2366                 $doContainerAclChecks = $this->doContainerACLChecks(FALSE);
2367                 $sendNotifications = $this->sendNotifications(FALSE);
2368                 
2369                 // NOTE: the user might have no edit grants, so let's be carefull
2370                 $diff = $baseEvent->dtstart->diff($baseEvent->dtend);
2371                 
2372                 $baseEvent->dtstart = new Tinebase_DateTime(substr($_recurInstance->recurid, -19), 'UTC');
2373                 $baseEvent->dtend   = clone $baseEvent->dtstart;
2374                 $baseEvent->dtend->add($diff);
2375
2376                 $baseEvent->base_event_id = $baseEvent->id;
2377                 $baseEvent->id = $_recurInstance->id;
2378                 $baseEvent->recurid = $_recurInstance->recurid;
2379
2380                 $attendee = $baseEvent->attendee;
2381                 unset($baseEvent->attendee);
2382                 
2383                 $eventInstance = $this->createRecurException($baseEvent, FALSE, $_allFollowing);
2384                 $eventInstance->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
2385                 $this->doContainerACLChecks($doContainerAclChecks);
2386                 $this->sendNotifications($sendNotifications);
2387                 
2388                 foreach ($attendee as $attender) {
2389                     $attender->setId(NULL);
2390                     $attender->cal_event_id = $eventInstance->getId();
2391                     
2392                     $attender = $this->_backend->createAttendee($attender);
2393                     $eventInstance->attendee->addRecord($attender);
2394                     $this->_increaseDisplayContainerContentSequence($attender, $eventInstance, Tinebase_Model_ContainerContent::ACTION_CREATE);
2395                 }
2396                 
2397                 $exceptionAttender = Calendar_Model_Attender::getAttendee($eventInstance->attendee, $_attender);
2398             }
2399             
2400             $exceptionAttender->status = $_attender->status;
2401             $exceptionAttender->transp = $_attender->transp;
2402             $eventInstance->alarms     = clone $_recurInstance->alarms;
2403             $eventInstance->alarms->setId(NULL);
2404             
2405             $updatedAttender = $this->attenderStatusUpdate($eventInstance, $exceptionAttender, $exceptionAttender->status_authkey);
2406             
2407             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
2408         } catch (Exception $e) {
2409             Tinebase_TransactionManager::getInstance()->rollBack();
2410             throw $e;
2411         }
2412         
2413         return $updatedAttender;
2414     }
2415     
2416     /**
2417      * updates an attender status of a event
2418      * 
2419      * @param  Calendar_Model_Event    $_event
2420      * @param  Calendar_Model_Attender $_attender
2421      * @param  string                  $_authKey
2422      * @return Calendar_Model_Attender updated attender
2423      */
2424     public function attenderStatusUpdate(Calendar_Model_Event $_event, Calendar_Model_Attender $_attender, $_authKey)
2425     {
2426         try {
2427             $event = $this->get($_event->getId());
2428             
2429             if (! $event->attendee) {
2430                 throw new Tinebase_Exception_NotFound('Could not find any attendee of event.');
2431             }
2432             
2433             if (($currentAttender = Calendar_Model_Attender::getAttendee($event->attendee, $_attender)) == null) {
2434                 throw new Tinebase_Exception_NotFound('Could not find attender in event.');
2435             }
2436             
2437             $updatedAttender = clone $currentAttender;
2438             
2439             if ($currentAttender->status_authkey !== $_authKey) {
2440                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
2441                     Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " no permissions to update status for {$currentAttender->user_type} {$currentAttender->user_id}");
2442                 return $updatedAttender;
2443             }
2444             
2445             Calendar_Controller_Alarm::enforceACL($_event, $event);
2446             
2447             $currentAttenderDisplayContainerId = $currentAttender->displaycontainer_id instanceof Tinebase_Model_Container ? 
2448                 $currentAttender->displaycontainer_id->getId() : 
2449                 $currentAttender->displaycontainer_id;
2450             
2451             $attenderDisplayContainerId = $_attender->displaycontainer_id instanceof Tinebase_Model_Container ? 
2452                 $_attender->displaycontainer_id->getId() : 
2453                 $_attender->displaycontainer_id;
2454             
2455             // check if something what can be set as user has changed
2456             if ($currentAttender->status == $_attender->status &&
2457                 $currentAttenderDisplayContainerId  == $attenderDisplayContainerId   &&
2458                 $currentAttender->transp            == $_attender->transp            &&
2459                 ! Calendar_Controller_Alarm::hasUpdates($_event, $event)
2460             ) {
2461                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
2462                     Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__ . "no status change -> do nothing");
2463                 return $updatedAttender;
2464             }
2465             
2466             $updatedAttender->status              = $_attender->status;
2467             $updatedAttender->displaycontainer_id = isset($_attender->displaycontainer_id) ? $_attender->displaycontainer_id : $updatedAttender->displaycontainer_id;
2468             $updatedAttender->transp              = isset($_attender->transp) ? $_attender->transp : Calendar_Model_Event::TRANSP_OPAQUE;
2469             
2470             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
2471                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2472                     . " update attender status to {$_attender->status} for {$currentAttender->user_type} {$currentAttender->user_id}");
2473                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2474                     . ' set alarm_ack_time / alarm_snooze_time: ' . $updatedAttender->alarm_ack_time . ' / ' . $updatedAttender->alarm_snooze_time);
2475             }
2476             
2477             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
2478             
2479             $updatedAttender = $this->_backend->updateAttendee($updatedAttender);
2480             if ($_event->alarms instanceof Tinebase_Record_RecordSet) {
2481                 foreach($_event->alarms as $alarm) {
2482                     $this->_inspectAlarmSet($event, $alarm);
2483                 }
2484                 
2485                 Tinebase_Alarm::getInstance()->setAlarmsOfRecord($_event);
2486             }
2487
2488             $event->attendee->removeRecord($currentAttender);
2489             $event->attendee->addRecord($updatedAttender);
2490             
2491             $this->_increaseDisplayContainerContentSequence($updatedAttender, $event);
2492
2493             Tinebase_Record_PersistentObserver::getInstance()->fireEvent(new Calendar_Event_InspectEvent(array(
2494                 'observable' => $event
2495             )));
2496
2497             $this->_touch($event, true);
2498
2499             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
2500         } catch (Exception $e) {
2501             Tinebase_TransactionManager::getInstance()->rollBack();
2502             throw $e;
2503         }
2504         
2505         // send notifications
2506         if ($currentAttender->status != $updatedAttender->status && $this->_sendNotifications && $_event->mute != 1) {
2507             $updatedEvent = $this->get($event->getId());
2508             $this->doSendNotifications($updatedEvent, Tinebase_Core::getUser(), 'changed', $event);
2509         }
2510         
2511         return $updatedAttender;
2512     }
2513     
2514     /**
2515      * saves all attendee of given event
2516      * 
2517      * NOTE: This function is executed in a create/update context. As such the user
2518      *       has edit/update the event and can do anything besides status settings of attendee
2519      * 
2520      * @todo add support for resources
2521      * 
2522      * @param Calendar_Model_Event $_event
2523      * @param Calendar_Model_Event $_currentEvent
2524      * @param bool                 $_isRescheduled event got rescheduled reset all attendee status
2525      */
2526     protected function _saveAttendee($_event, $_currentEvent = NULL, $_isRescheduled = FALSE)
2527     {
2528         if (! $_event->attendee instanceof Tinebase_Record_RecordSet) {
2529             $_event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
2530         }
2531         
2532         Calendar_Model_Attender::resolveEmailOnlyAttendee($_event);
2533         
2534         $_event->attendee->cal_event_id = $_event->getId();
2535         
2536         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " About to save attendee for event {$_event->id} ");
2537         
2538         $currentAttendee = $_currentEvent->attendee;
2539         
2540         $diff = $currentAttendee->getMigration($_event->attendee->getArrayOfIds());
2541
2542         $calendar = Tinebase_Container::getInstance()->getContainerById($_event->container_id);
2543         
2544         // delete attendee
2545         $this->_backend->deleteAttendee($diff['toDeleteIds']);
2546         foreach ($diff['toDeleteIds'] as $deleteAttenderId) {
2547             $idx = $currentAttendee->getIndexById($deleteAttenderId);
2548             if ($idx !== FALSE) {
2549                 $currentAttenderToDelete = $currentAttendee[$idx];
2550                 $this->_increaseDisplayContainerContentSequence($currentAttenderToDelete, $_event, Tinebase_Model_ContainerContent::ACTION_DELETE);
2551             }
2552         }
2553         
2554         // create/update attendee
2555         foreach ($_event->attendee as $attender) {
2556             $attenderId = $attender->getId();
2557             $idx = ($attenderId) ? $currentAttendee->getIndexById($attenderId) : FALSE;
2558             
2559             if ($idx !== FALSE) {
2560                 $currentAttender = $currentAttendee[$idx];
2561                 $this->_updateAttender($attender, $currentAttender, $_event, $_isRescheduled, $calendar);
2562             } else {
2563                 $this->_createAttender($attender, $_event, FALSE, $calendar);
2564             }
2565         }
2566     }
2567
2568     /**
2569      * creates a new attender
2570      * 
2571      * @param Calendar_Model_Attender  $attender
2572      * @param Tinebase_Model_Container $_calendar
2573      * @param boolean $preserveStatus
2574      * @param Tinebase_Model_Container $calendar
2575      */
2576     protected function _createAttender(Calendar_Model_Attender $attender, Calendar_Model_Event $event, $preserveStatus = FALSE, Tinebase_Model_Container $calendar = NULL)
2577     {
2578         // apply defaults
2579         $attender->user_type         = isset($attender->user_type) ? $attender->user_type : Calendar_Model_Attender::USERTYPE_USER;
2580         $attender->cal_event_id      =  $event->getId();
2581         $calendar = ($calendar) ? $calendar : Tinebase_Container::getInstance()->getContainerById($event->container_id);
2582         
2583         $userAccountId = $attender->getUserAccountId();
2584         
2585         // generate auth key
2586         if (! $attender->status_authkey) {
2587             $attender->status_authkey = Tinebase_Record_Abstract::generateUID();
2588         }
2589         
2590         // attach to display calendar if attender has/is a useraccount
2591         if ($userAccountId) {
2592             if ($calendar->type == Tinebase_Model_Container::TYPE_PERSONAL && Tinebase_Container::getInstance()->hasGrant($userAccountId, $calendar, Tinebase_Model_Grants::GRANT_ADMIN)) {
2593                 // if attender has admin grant to (is owner of) personal physical container, this phys. cal also gets displ. cal
2594                 $attender->displaycontainer_id = $calendar->getId();
2595             } else if ($attender->displaycontainer_id && $userAccountId == Tinebase_Core::getUser()->getId() && Tinebase_Container::getInstance()->hasGrant($userAccountId, $attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_ADMIN)) {
2596                 // allow user to set his own displ. cal
2597                 $attender->displaycontainer_id = $attender->displaycontainer_id;
2598             } else {
2599                 $displayCalId = self::getDefaultDisplayContainerId($userAccountId);
2600                 $attender->displaycontainer_id = $displayCalId;
2601             }
2602
2603         } else if ($attender->user_type === Calendar_Model_Attender::USERTYPE_RESOURCE) {
2604             $resource = Calendar_Controller_Resource::getInstance()->get($attender->user_id);
2605             $attender->displaycontainer_id = $resource->container_id;
2606         }
2607         
2608         if ($attender->displaycontainer_id) {
2609             // check if user is allowed to set status
2610             if (! $preserveStatus && ! Tinebase_Core::getUser()->hasGrant($attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_EDIT)) {
2611                 if ($attender->user_type === Calendar_Model_Attender::USERTYPE_RESOURCE) {
2612                     //If resource has an default status use this
2613                     $attender->status = isset($resource->status) ? $resource->status : Calendar_Model_Attender::STATUS_NEEDSACTION;
2614                 } else {
2615                     $attender->status = Calendar_Model_Attender::STATUS_NEEDSACTION;
2616                 }
2617             }
2618         }
2619
2620         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " New attender: " . print_r($attender->toArray(), TRUE));
2621
2622         Tinebase_Timemachine_ModificationLog::getInstance()->setRecordMetaData($attender, 'create');
2623         $this->_backend->createAttendee($attender);
2624         $this->_increaseDisplayContainerContentSequence($attender, $event, Tinebase_Model_ContainerContent::ACTION_CREATE);
2625     }
2626     
2627         
2628     /**
2629      * returns the default calendar
2630      * 
2631      * @return Tinebase_Model_Container
2632      */
2633     public function getDefaultCalendar()
2634     {
2635         return Tinebase_Container::getInstance()->getDefaultContainer($this->_applicationName, NULL, Calendar_Preference::DEFAULTCALENDAR);
2636     }
2637     
2638     /**
2639      * returns default displayContainer id of given attendee
2640      *
2641      * @param string $userAccountId
2642      */
2643     public static function getDefaultDisplayContainerId($userAccountId)
2644     {
2645         $userAccountId = Tinebase_Model_User::convertUserIdToInt($userAccountId);
2646         $displayCalId = Tinebase_Core::getPreference('Calendar')->getValueForUser(Calendar_Preference::DEFAULTCALENDAR, $userAccountId);
2647         
2648         try {
2649             // assert that displaycal is of type personal
2650             $container = Tinebase_Container::getInstance()->getContainerById($displayCalId);
2651             if ($container->type != Tinebase_Model_Container::TYPE_PERSONAL) {
2652                 $displayCalId = NULL;
2653             }
2654         } catch (Exception $e) {
2655             $displayCalId = NULL;
2656         }
2657         
2658         if (! isset($displayCalId)) {
2659             $containers = Tinebase_Container::getInstance()->getPersonalContainer($userAccountId, 'Calendar_Model_Event', $userAccountId, 0, true);
2660             if ($containers->count() > 0) {
2661                 $displayCalId = $containers->getFirstRecord()->getId();
2662             }
2663         }
2664         
2665         return $displayCalId;
2666     }
2667     
2668     /**
2669      * increases content sequence of attender display container
2670      * 
2671      * @param Calendar_Model_Attender $attender
2672      * @param Calendar_Model_Event $event
2673      * @param string $action
2674      */
2675     protected function _increaseDisplayContainerContentSequence($attender, $event, $action = Tinebase_Model_ContainerContent::ACTION_UPDATE)
2676     {
2677         if ($event->container_id === $attender->displaycontainer_id || empty($attender->displaycontainer_id)) {
2678             // no need to increase sequence
2679             return;
2680         }
2681         
2682         Tinebase_Container::getInstance()->increaseContentSequence($attender->displaycontainer_id, $action, $event->getId());
2683     }
2684     
2685     /**
2686      * updates an attender
2687      * 
2688      * @param Calendar_Model_Attender  $attender
2689      * @param Calendar_Model_Attender  $currentAttender
2690      * @param Calendar_Model_Event     $event
2691      * @param bool                     $isRescheduled event got rescheduled reset all attendee status
2692      * @param Tinebase_Model_Container $calendar
2693      */
2694     protected function _updateAttender($attender, $currentAttender, $event, $isRescheduled, $calendar = NULL)
2695     {
2696         $userAccountId = $currentAttender->getUserAccountId();
2697
2698         // update display calendar if attender has/is a useraccount
2699         if ($userAccountId) {
2700             if ($calendar->type == Tinebase_Model_Container::TYPE_PERSONAL && Tinebase_Container::getInstance()->hasGrant($userAccountId, $calendar, Tinebase_Model_Grants::GRANT_ADMIN)) {
2701                 // if attender has admin grant to personal physical container, this phys. cal also gets displ. cal
2702                 $attender->displaycontainer_id = $calendar->getId();
2703             } else if ($userAccountId == Tinebase_Core::getUser()->getId() && Tinebase_Container::getInstance()->hasGrant($userAccountId, $attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_ADMIN)) {
2704                 // allow user to set his own displ. cal
2705                 $attender->displaycontainer_id = $attender->displaycontainer_id;
2706             } else {
2707                 $attender->displaycontainer_id = $currentAttender->displaycontainer_id;
2708             }
2709         }
2710
2711         // reset status if user has no right and authkey is wrong
2712         if ($attender->displaycontainer_id) {
2713             if (! Tinebase_Core::getUser()->hasGrant($attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_EDIT)
2714                     && $attender->status_authkey != $currentAttender->status_authkey) {
2715
2716                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
2717                     . ' Wrong authkey, resetting status (' . $attender->status . ' -> ' . $currentAttender->status . ')');
2718                 $attender->status = $currentAttender->status;
2719             }
2720         }
2721
2722         // reset all status but calUser on reschedule except resources (Resources might have a configured default value)
2723         if ($isRescheduled && !$attender->isSame($this->getCalendarUser())) {
2724             if ($attender->user_type === Calendar_Model_Attender::USERTYPE_RESOURCE) {
2725                 //If resource has a default status reset to this
2726                 $resource = Calendar_Controller_Resource::getInstance()->get($attender->user_id);
2727                 $attender->status = isset($resource->status) ? $resource->status : Calendar_Model_Attender::STATUS_NEEDSACTION;
2728             } else {
2729                 $attender->status = Calendar_Model_Attender::STATUS_NEEDSACTION;
2730             }
2731             $attender->transp = null;
2732         }
2733
2734         // preserve old authkey
2735         $attender->status_authkey = $currentAttender->status_authkey;
2736
2737         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
2738             . " Updating attender: " . print_r($attender->toArray(), TRUE));
2739
2740
2741         Tinebase_Timemachine_ModificationLog::getInstance()->setRecordMetaData($attender, 'update', $currentAttender);
2742         Tinebase_Timemachine_ModificationLog::getInstance()->writeModLog($attender, $currentAttender, get_class($attender), $this->_getBackendType(), $attender->getId());
2743         $this->_backend->updateAttendee($attender);
2744         
2745         if ($attender->displaycontainer_id !== $currentAttender->displaycontainer_id) {
2746             $this->_increaseDisplayContainerContentSequence($currentAttender, $event, Tinebase_Model_ContainerContent::ACTION_DELETE);
2747             $this->_increaseDisplayContainerContentSequence($attender, $event, Tinebase_Model_ContainerContent::ACTION_CREATE);
2748         } else {
2749             $this->_increaseDisplayContainerContentSequence($attender, $event);
2750         }
2751     }
2752     
2753     /**
2754      * event handler for group updates
2755      * 
2756      * @param Tinebase_Model_Group $_group
2757      * @return void
2758      */
2759     public function onUpdateGroup($_groupId)
2760     {
2761         $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
2762         
2763         $filter = new Calendar_Model_EventFilter(array(
2764             array('field' => 'attender', 'operator' => 'equals', 'value' => array(
2765                 'user_type' => Calendar_Model_Attender::USERTYPE_GROUP,
2766                 'user_id'   => $_groupId
2767             )),
2768             array('field' => 'period', 'operator' => 'within', 'value' => array(
2769                 'from'  => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
2770                 'until' => Tinebase_DateTime::now()->addYear(100)->get(Tinebase_Record_Abstract::ISO8601LONG))
2771             )
2772         ));
2773         $events = $this->search($filter, new Tinebase_Model_Pagination(), FALSE, FALSE);
2774         
2775         foreach($events as $event) {
2776             try {
2777                 if (! $event->rrule) {
2778                     // update non recurring futrue events
2779                     Calendar_Model_Attender::resolveGroupMembers($event->attendee);
2780                     $this->update($event);
2781                 } else {
2782                     // update thisandfuture for recurring events
2783                     $nextOccurrence = Calendar_Model_Rrule::computeNextOccurrence($event, $this->getRecurExceptions($event), Tinebase_DateTime::now());
2784                     Calendar_Model_Attender::resolveGroupMembers($nextOccurrence->attendee);
2785                     
2786                     if ($nextOccurrence->dtstart != $event->dtstart) {
2787                         $this->createRecurException($nextOccurrence, FALSE, TRUE);
2788                     } else {
2789                         $this->update($nextOccurrence);
2790                     }
2791                 }
2792             } catch (Exception $e) {
2793                 Tinebase_Core::getLogger()->NOTICE(__METHOD__ . '::' . __LINE__ . " could not update attendee");
2794             }
2795         }
2796         
2797         $this->doContainerACLChecks($doContainerACLChecks);
2798     }
2799     
2800     /****************************** alarm functions ************************/
2801     
2802     /**
2803      * send an alarm
2804      *
2805      * @param  Tinebase_Model_Alarm $_alarm
2806      * @return void
2807      * 
2808      * NOTE: the given alarm is raw and has not passed _inspectAlarmGet
2809      *  
2810      * @todo throw exception on error
2811      */
2812     public function sendAlarm(Tinebase_Model_Alarm $_alarm) 
2813     {
2814         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " About to send alarm " . print_r($_alarm->toArray(), TRUE));
2815         
2816         $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
2817
2818         try {
2819             $event = $this->get($_alarm->record_id);
2820             $event->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm', array($_alarm));
2821             $this->_inspectAlarmGet($event);
2822         } catch (Exception $e) {
2823             $this->doContainerACLChecks($doContainerACLChecks);
2824             throw($e);
2825         }
2826
2827         $this->doContainerACLChecks($doContainerACLChecks);
2828
2829         if ($event->rrule) {
2830             $recurid = $_alarm->getOption('recurid');
2831             
2832             // adopts the (referenced) alarm and sets alarm time to next occurance
2833             parent::_inspectAlarmSet($event, $_alarm);
2834             $this->adoptAlarmTime($event, $_alarm, 'instance');
2835             
2836             // sent_status might have changed in adoptAlarmTime()
2837             if ($_alarm->sent_status !== Tinebase_Model_Alarm::STATUS_PENDING) {
2838                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2839                     . ' Not sending alarm for event at ' . $event->dtstart->toString() . ' with status ' . $_alarm->sent_status);
2840                 return;
2841             }
2842             
2843             if ($recurid) {
2844                 // NOTE: In case of recuring events $event is always the baseEvent,
2845                 //       so we might need to adopt event time to recur instance.
2846                 $diff = $event->dtstart->diff($event->dtend);
2847                 
2848                 $event->dtstart = new Tinebase_DateTime(substr($recurid, -19));
2849                 
2850                 $event->dtend = clone $event->dtstart;
2851                 $event->dtend->add($diff);
2852             }
2853             
2854             if ($event->exdate && in_array($event->dtstart, $event->exdate)) {
2855                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2856                     . " Not sending alarm because instance at " . $event->dtstart->toString() . ' is an exception.');
2857                 return;
2858             }
2859         }
2860         
2861         Calendar_Controller_EventNotifications::getInstance()->doSendNotifications($event, Tinebase_Core::getUser(), 'alarm', NULL, array('alarm' => $_alarm));
2862     }
2863     
2864     /**
2865      * send notifications 
2866      * 
2867      * @param Tinebase_Record_Interface  $_event
2868      * @param Tinebase_Model_FullUser    $_updater
2869      * @param String                     $_action
2870      * @param Tinebase_Record_Interface  $_oldEvent
2871      * @param Array                      $_additionalData
2872      */
2873     public function doSendNotifications(Tinebase_Record_Interface $_event, Tinebase_Model_FullUser $_updater, $_action, Tinebase_Record_Interface $_oldEvent = NULL, array $_additionalData = array())
2874     {
2875         Tinebase_ActionQueue::getInstance()->queueAction('Calendar.sendEventNotifications', 
2876             $_event, 
2877             $_updater,
2878             $_action, 
2879             $_oldEvent ? $_oldEvent : NULL
2880         );
2881     }
2882
2883     public function compareCalendars($cal1, $cal2, $from, $until)
2884     {
2885         $matchingEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
2886         $changedEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
2887         $missingEventsInCal1 = new Tinebase_Record_RecordSet('Calendar_Model_Event');
2888         $missingEventsInCal2 = new Tinebase_Record_RecordSet('Calendar_Model_Event');
2889         $cal2EventIdsAlreadyProcessed = array();
2890         
2891         while ($from->isEarlier($until)) {
2892     
2893             $endWeek = $from->getClone()->addWeek(1);
2894             
2895             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2896                     . ' Comparing period ' . $from . ' - ' . $endWeek);
2897     
2898             // get all events from cal1+cal2 for the week
2899             $cal1Events = $this->_getEventsForPeriodAndCalendar($cal1, $from, $endWeek);
2900             $cal1EventsClone = clone $cal1Events;
2901             $cal2Events = $this->_getEventsForPeriodAndCalendar($cal2, $from, $endWeek);
2902             $cal2EventsClone = clone $cal2Events;
2903             
2904             $from->addWeek(1);
2905             if (count($cal1Events) == 0 && count($cal2Events) == 0) {
2906                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2907                         . ' No events found');
2908                 continue;
2909             }
2910     
2911             foreach ($cal1Events as $event) {
2912                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2913                     . ' Checking event "' . $event->summary . '" ' . $event->dtstart . ' - ' . $event->dtend);
2914                 
2915                 if ($event->container_id != $cal1) {
2916                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2917                             . ' Event is in another calendar - skip');
2918                     $cal1Events->removeRecord($event);
2919                     continue;
2920                 }
2921                 
2922                 $summaryMatch = $cal2Events->filter('summary', $event->summary);
2923                 if (count($summaryMatch) > 0) {
2924                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2925                         . " Found " . count($summaryMatch) . ' events with matching summaries');
2926                     
2927                     $dtStartMatch = $summaryMatch->filter('dtstart', $event->dtstart);
2928                     if (count($dtStartMatch) > 0) {
2929                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2930                             . " Found " . count($summaryMatch) . ' events with matching dtstarts and summaries');
2931                         
2932                         $matchingEvents->merge($dtStartMatch);
2933                         // remove from cal1+cal2
2934                         $cal1Events->removeRecord($event);
2935                         $cal2Events->removeRecords($dtStartMatch);
2936                         $cal2EventIdsAlreadyProcessed = array_merge($cal2EventIdsAlreadyProcessed, $dtStartMatch->getArrayOfIds());
2937                     } else {
2938                         $changedEvents->merge($summaryMatch);
2939                         $cal1Events->removeRecord($event);
2940                         $cal2Events->removeRecords($summaryMatch);
2941                         $cal2EventIdsAlreadyProcessed = array_merge($cal2EventIdsAlreadyProcessed, $summaryMatch->getArrayOfIds());
2942                     }
2943                 }
2944             }
2945             
2946             // add missing events
2947             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2948                 . " Found " . count($cal1Events) . ' events missing in cal2');
2949             $missingEventsInCal2->merge($cal1Events);
2950             
2951             // compare cal2 -> cal1 and add events as missing from cal1 that we did not detect before
2952             foreach ($cal2EventsClone as $event) {
2953                 if (in_array($event->getId(), $cal2EventIdsAlreadyProcessed)) {
2954                     continue;
2955                 }
2956                 if ($event->container_id != $cal2) {
2957                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2958                             . ' Event is in another calendar - skip');
2959                     continue;
2960                 }
2961                 
2962                 $missingEventsInCal1->addRecord($event);
2963             }
2964         }
2965         
2966         $result = array(
2967             'matching'      => $matchingEvents,
2968             'changed'       => $changedEvents,
2969             'missingInCal1' => $missingEventsInCal1,
2970             'missingInCal2' => $missingEventsInCal2,
2971         );
2972         return $result;
2973     }
2974     
2975     protected function _getEventsForPeriodAndCalendar($calendarId, $from, $until)
2976     {
2977         $filter = new Calendar_Model_EventFilter(array(
2978             array('field' => 'period', 'operator' => 'within', 'value' =>
2979                 array("from" => $from, "until" => $until)
2980             ),
2981             array('field' => 'container_id', 'operator' => 'equals', 'value' => $calendarId),
2982         ));
2983     
2984         $events = Calendar_Controller_Event::getInstance()->search($filter);
2985         Calendar_Model_Rrule::mergeAndRemoveNonMatchingRecurrences($events, $filter);
2986         return $events;
2987     }
2988     
2989     /**
2990      * add calendar owner as attendee if not already set
2991      * 
2992      * @param string $calendarId
2993      * @param Tinebase_DateTime $from
2994      * @param Tinebase_DateTime $until
2995      * @param boolean $dry run
2996      * 
2997      * @return number of updated events
2998      */
2999     public function repairAttendee($calendarId, $from, $until, $dry = false)
3000     {
3001         $container = Tinebase_Container::getInstance()->getContainerById($calendarId);
3002         if ($container->type !== Tinebase_Model_Container::TYPE_PERSONAL) {
3003             throw new Calendar_Exception('Only allowed for personal containers!');
3004         }
3005         if ($container->owner_id !== Tinebase_Core::getUser()->getId()) {
3006             throw new Calendar_Exception('Only allowed for own containers!');
3007         }
3008         
3009         $updateCount = 0;
3010         while ($from->isEarlier($until)) {
3011             $endWeek = $from->getClone()->addWeek(1);
3012             
3013             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
3014                     . ' Repairing period ' . $from . ' - ' . $endWeek);
3015             
3016             
3017             // TODO we need to detect events with DECLINED/DELETED attendee
3018             $events = $this->_getEventsForPeriodAndCalendar($calendarId, $from, $endWeek);
3019             
3020             $from->addWeek(1);
3021             
3022             if (count($events) == 0) {
3023                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
3024                         . ' No events found');
3025                 continue;
3026             }
3027             
3028             foreach ($events as $event) {
3029                 // add attendee if not already set
3030                 if ($event->isRecurInstance()) {
3031                     // TODO get base event
3032                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
3033                             . ' Skip recur instance ' . $event->toShortString());
3034                     continue;
3035                 }
3036                 
3037                 $ownAttender = Calendar_Model_Attender::getOwnAttender($event->attendee);
3038                 if (! $ownAttender) {
3039                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
3040                             . ' Add missing attender to event ' . $event->toShortString());
3041                     
3042                     $attender = new Calendar_Model_Attender(array(
3043                         'user_type' => Calendar_Model_Attender::USERTYPE_USER,
3044                         'user_id'   => Tinebase_Core::getUser()->contact_id,
3045                         'status'    => Calendar_Model_Attender::STATUS_ACCEPTED
3046                     ));
3047                     $event->attendee->addRecord($attender);
3048                     if (! $dry) {
3049                         $this->update($event);
3050                     }
3051                     $updateCount++;
3052                 }
3053             }
3054         }
3055         
3056         return $updateCount;
3057     }
3058
3059     public function sendTentativeNotifications()
3060     {
3061         $eventNotificationController = Calendar_Controller_EventNotifications::getInstance();
3062         $calConfig = Calendar_Config::getInstance();
3063         if (true !== $calConfig->{Calendar_Config::TENTATIVE_NOTIFICATIONS}
3064                 ->{Calendar_Config::TENTATIVE_NOTIFICATIONS_ENABLED}) {
3065             return;
3066         }
3067
3068         $days = $calConfig->{Calendar_Config::TENTATIVE_NOTIFICATIONS}->{Calendar_Config::TENTATIVE_NOTIFICATIONS_DAYS};
3069         $additionalFilters = $calConfig->{Calendar_Config::TENTATIVE_NOTIFICATIONS}
3070             ->{Calendar_Config::TENTATIVE_NOTIFICATIONS_FILTER};
3071
3072         $filter = array(
3073             array('field' => 'period', 'operator' => 'within', 'value' => array(
3074                 'from'  => Tinebase_DateTime::now(),
3075                 'until' => Tinebase_DateTime::now()->addDay($days)
3076             )),
3077             array('field' => 'status', 'operator' => 'equals', 'value' => Calendar_Model_Event::STATUS_TENTATIVE)
3078         );
3079
3080         if (null !== $additionalFilters) {
3081             $filter = array_merge($filter, $additionalFilters);
3082         }
3083
3084         $filter = new Calendar_Model_EventFilter($filter);
3085         foreach ($this->search($filter) as $event) {
3086             $eventNotificationController->doSendNotifications($event, null, 'tentative');
3087         }
3088     }
3089
3090     /**
3091      * returns active fixed calendars for users (combines config and preference)
3092      *
3093      * @return array
3094      * @throws Tinebase_Exception_NotFound
3095      */
3096     public function getFixedCalendarIds()
3097     {
3098         $fixedCalendars = (array) Calendar_Config::getInstance()->get(Calendar_Config::FIXED_CALENDARS);
3099
3100         // add fixed calendars from user preference
3101         $fixedCalendarsPref = Tinebase_Core::getPreference('Calendar')->getValue(Calendar_Preference::FIXED_CALENDARS);
3102         if (is_array($fixedCalendarsPref)) {
3103             foreach ($fixedCalendarsPref as $container) {
3104                 if (isset($container['id'])) {
3105                     $fixedCalendars[] = $container['id'];
3106                 }
3107             }
3108         }
3109
3110         return $fixedCalendars;
3111     }
3112 }