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