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