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)
12 * Calendar Event Controller
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
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
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.
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
47 * NOTE: the backend always fetches full records for grant calculations.
48 * searching ids only does not hlep with performance
52 class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract implements Tinebase_Controller_Alarm_Interface
57 * just set is_delete=1 if record is going to be deleted
59 protected $_purgeRecords = FALSE;
66 protected $_sendNotifications = TRUE;
69 * @see Tinebase_Controller_Record_Abstract
73 protected $_resolveCustomFields = TRUE;
76 * @var Calendar_Model_Attender
78 protected $_calendarUser = NULL;
81 * @var Calendar_Controller_Event
83 private static $_instance = NULL;
88 * don't use the constructor. use the singleton
90 private function __construct()
92 $this->_applicationName = 'Calendar';
93 $this->_modelName = 'Calendar_Model_Event';
94 $this->_backend = new Calendar_Backend_Sql();
97 $this->setCalendarUser(new Calendar_Model_Attender(array(
98 'user_type' => Calendar_Model_Attender::USERTYPE_USER,
99 'user_id' => Calendar_Controller_MSEventFacade::getCurrentUserContactId()
104 * don't clone. Use the singleton.
106 private function __clone()
114 * @return Calendar_Controller_Event
116 public static function getInstance()
118 if (self::$_instance === NULL) {
119 self::$_instance = new Calendar_Controller_Event();
121 return self::$_instance;
125 * sets current calendar user
127 * @param Calendar_Model_Attender $_calUser
128 * @return Calendar_Model_Attender oldUser
130 public function setCalendarUser(Calendar_Model_Attender $_calUser)
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');
135 $oldUser = $this->_calendarUser;
136 $this->_calendarUser = $_calUser;
142 * get current calendar user
144 * @return Calendar_Model_Attender
146 public function getCalendarUser()
148 return $this->_calendarUser;
152 * checks if all attendee of given event are not busy for given event
154 * @param Calendar_Model_Event $_event
156 * @throws Calendar_Exception_AttendeeBusy
158 public function checkBusyConflicts($_event)
160 $ignoreUIDs = !empty($_event->uid) ? array($_event->uid) : array();
162 if ($_event->transp == Calendar_Model_Event::TRANSP_TRANSP || count($_event->attendee) < 1) {
163 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
164 . " Skipping free/busy check because event is transparent");
168 $eventSet = new Tinebase_Record_RecordSet('Calendar_Model_Event', array($_event));
170 if (! empty($_event->rrule)) {
171 $checkUntil = clone $_event->dtstart;
172 $checkUntil->add(1, Tinebase_DateTime::MODIFIER_MONTH);
173 Calendar_Model_Rrule::mergeRecurrenceSet($eventSet, $_event->dtstart, $checkUntil);
177 foreach ($eventSet as $event) {
178 $periods[] = array('from' => $event->dtstart, 'until' => $event->dtend);
181 $fbInfo = $this->getFreeBusyInfo($periods, $_event->attendee, $ignoreUIDs);
183 if (count($fbInfo) > 0) {
184 $busyException = new Calendar_Exception_AttendeeBusy();
185 $busyException->setFreeBusyInfo($fbInfo);
187 Calendar_Model_Attender::resolveAttendee($_event->attendee, /* resolve_display_containers = */ false);
188 $busyException->setEvent($_event);
190 throw $busyException;
193 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
194 . " Free/busy check: no conflict found");
200 * @param Tinebase_Record_Interface $_record
201 * @param bool $_checkBusyConflicts
202 * @return Tinebase_Record_Interface
203 * @throws Tinebase_Exception_AccessDenied
204 * @throws Tinebase_Exception_Record_Validation
206 public function create(Tinebase_Record_Interface $_record, $_checkBusyConflicts = FALSE)
209 $db = $this->_backend->getAdapter();
210 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
212 $this->_inspectEvent($_record);
214 // we need to resolve groupmembers before free/busy checking
215 Calendar_Model_Attender::resolveGroupMembers($_record->attendee);
217 if ($_checkBusyConflicts) {
218 // ensure that all attendee are free
219 $this->checkBusyConflicts($_record);
222 $sendNotifications = $this->_sendNotifications;
223 $this->_sendNotifications = FALSE;
225 $createdEvent = parent::create($_record);
227 $this->_sendNotifications = $sendNotifications;
229 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
230 } catch (Exception $e) {
231 Tinebase_TransactionManager::getInstance()->rollBack();
235 // send notifications
236 if ($this->_sendNotifications && $_record->mute != 1) {
237 $this->doSendNotifications($createdEvent, Tinebase_Core::getUser(), 'created');
240 return $createdEvent;
244 * inspect creation of one record (after create)
246 * @param Tinebase_Record_Interface $_createdRecord
247 * @param Tinebase_Record_Interface $_record
250 protected function _inspectAfterCreate($_createdRecord, Tinebase_Record_Interface $_record)
252 $this->_saveAttendee($_record, $_createdRecord);
256 * deletes a recur series
258 * @param Calendar_Model_Event $_recurInstance
261 public function deleteRecurSeries($_recurInstance)
263 $baseEvent = $this->getRecurBaseEvent($_recurInstance);
264 $this->delete($baseEvent->getId());
270 * @param string $_orderBy Order result by
271 * @param string $_orderDirection Order direction - allowed are ASC and DESC
272 * @throws Tinebase_Exception_InvalidArgument
273 * @return Tinebase_Record_RecordSet
275 public function getAll($_orderBy = 'id', $_orderDirection = 'ASC')
277 throw new Tinebase_Exception_NotImplemented('not implemented');
281 * returns freebusy information for given period and given attendee
283 * @todo merge overlapping events to one freebusy entry
285 * @param array of array with from and until $_periods
286 * @param Tinebase_Record_RecordSet of Calendar_Model_Attender $_attendee
287 * @param array of UIDs $_ignoreUIDs
288 * @return Tinebase_Record_RecordSet of Calendar_Model_FreeBusy
290 public function getFreeBusyInfo($_periods, $_attendee, $_ignoreUIDs = array())
292 $fbInfoSet = new Tinebase_Record_RecordSet('Calendar_Model_FreeBusy');
294 // map groupmembers to users
295 $attendee = clone $_attendee;
296 $attendee->addIndices(array('user_type'));
297 $groupmembers = $attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
298 $groupmembers->user_type = Calendar_Model_Attender::USERTYPE_USER;
302 array('field' => 'attender', 'operator' => 'in', 'value' => $_attendee),
303 array('field' => 'transp', 'operator' => 'equals', 'value' => Calendar_Model_Event::TRANSP_OPAQUE)
306 // add all periods to filterdata
307 $periodFilters = array();
308 foreach ($_periods as $period) {
309 $periodFilters[] = array(
311 'operator' => 'within',
313 'from' => $period['from'],
314 'until' => $period['until']
317 $filterData[] = array('condition' => 'OR', 'filters' => $periodFilters);
319 // finaly create filter
320 $filter = new Calendar_Model_EventFilter($filterData);
322 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . ' ' . __LINE__
323 . ' free/busy filter: ' . print_r($filter->toArray(), true));
325 $events = $this->search($filter, new Tinebase_Model_Pagination(), FALSE, FALSE);
327 foreach ($_periods as $period) {
328 Calendar_Model_Rrule::mergeRecurrenceSet($events, $period['from'], $period['until']);
331 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . ' ' . __LINE__
332 . ' value: ' . print_r($events->toArray(), true));
336 foreach ($attendee as $attender) {
337 if (! (isset($typeMap[$attender['user_type']]) || array_key_exists($attender['user_type'], $typeMap))) {
338 $typeMap[$attender['user_type']] = array();
341 $typeMap[$attender['user_type']][$attender['user_id']] = array();
343 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . ' ' . __LINE__
344 . ' value: ' . print_r($typeMap, true));
346 // generate freeBusyInfos
347 foreach ($events as $event) {
348 // skip events with ignoreUID
349 if (in_array($event->uid, $_ignoreUIDs)) {
353 // check if event is conflicting one of the given periods
355 foreach($_periods as $period) {
356 if ($event->dtstart->isEarlier($period['until']) && $event->dtend->isLater($period['from'])) {
365 // map groupmembers to users
366 $event->attendee->addIndices(array('user_type'));
367 $groupmembers = $event->attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
368 $groupmembers->user_type = Calendar_Model_Attender::USERTYPE_USER;
370 foreach ($event->attendee as $attender) {
371 // skip declined/transp events
372 if ($attender->status == Calendar_Model_Attender::STATUS_DECLINED ||
373 $attender->transp == Calendar_Model_Event::TRANSP_TRANSP) {
377 if ((isset($typeMap[$attender->user_type]) || array_key_exists($attender->user_type, $typeMap)) && (isset($typeMap[$attender->user_type][$attender->user_id]) || array_key_exists($attender->user_id, $typeMap[$attender->user_type]))) {
378 $fbInfo = new Calendar_Model_FreeBusy(array(
379 'user_type' => $attender->user_type,
380 'user_id' => $attender->user_id,
381 'dtstart' => clone $event->dtstart,
382 'dtend' => clone $event->dtend,
383 'type' => Calendar_Model_FreeBusy::FREEBUSY_BUSY,
386 if ($event->{Tinebase_Model_Grants::GRANT_READ}) {
387 $fbInfo->event = clone $event;
388 unset($fbInfo->event->attendee);
391 //$typeMap[$attender->user_type][$attender->user_id][] = $fbInfo;
392 $fbInfoSet->addRecord($fbInfo);
401 * get list of records
403 * @param Tinebase_Model_Filter_FilterGroup|optional $_filter
404 * @param Tinebase_Model_Pagination|optional $_pagination
405 * @param bool $_getRelations
406 * @param boolean $_onlyIds
407 * @param string $_action for right/acl check
408 * @return Tinebase_Record_RecordSet|array
410 public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
412 $events = parent::search($_filter, $_pagination, $_getRelations, $_onlyIds, $_action);
414 $this->_freeBusyCleanup($events, $_action);
421 * Returns a set of records identified by their id's
423 * @param array $_ids array of record identifiers
424 * @param bool $_ignoreACL don't check acl grants
425 * @return Tinebase_Record_RecordSet of $this->_modelName
427 public function getMultiple($_ids, $_ignoreACL = false)
429 $events = parent::getMultiple($_ids, $_ignoreACL = false);
430 if ($_ignoreACL !== true) {
431 $this->_freeBusyCleanup($events, 'get');
438 * cleanup search results (freebusy)
440 * @param Tinebase_Record_RecordSet $_events
441 * @param string $_action
443 protected function _freeBusyCleanup(Tinebase_Record_RecordSet $_events, $_action)
445 foreach ($_events as $event) {
446 $doFreeBusyCleanup = $event->doFreeBusyCleanup();
447 if ($doFreeBusyCleanup && $_action !== 'get') {
448 $_events->removeRecord($event);
454 * returns freeTime (suggestions) for given period of given attendee
456 * @param Tinebase_DateTime $_from
457 * @param Tinebase_DateTime $_until
458 * @param Tinebase_Record_RecordSet of Calendar_Model_Attender $_attendee
462 public function searchFreeTime($_from, $_until, $_attendee/*, $_constains, $_mode*/)
464 $fbInfoSet = $this->getFreeBusyInfo(array(array('from' => $_from, 'until' => $_until)), $_attendee);
466 // $fromTs = $_from->getTimestamp();
467 // $untilTs = $_until->getTimestamp();
468 // $granularity = 1800;
470 // // init registry of granularity
471 // $eventRegistry = array_combine(range($fromTs, $untilTs, $granularity), array_fill(0, ceil(($untilTs - $fromTs)/$granularity)+1, ''));
473 // foreach ($fbInfoSet as $fbInfo) {
474 // $startIdx = $fromTs + $granularity * floor(($fbInfo->dtstart->getTimestamp() - $fromTs) / $granularity);
475 // $endIdx = $fromTs + $granularity * ceil(($fbInfo->dtend->getTimestamp() - $fromTs) / $granularity);
477 // for ($idx=$startIdx; $idx<=$endIdx; $idx+=$granularity) {
478 // //$eventRegistry[$idx][] = $fbInfo;
479 // $eventRegistry[$idx] .= '.';
483 //print_r($eventRegistry);
489 * @param Tinebase_Record_Interface $_record
490 * @param bool $_checkBusyConflicts
491 * @param string $range
492 * @return Tinebase_Record_Interface
493 * @throws Tinebase_Exception_AccessDenied
494 * @throws Tinebase_Exception_Record_Validation
496 public function update(Tinebase_Record_Interface $_record, $_checkBusyConflicts = FALSE, $range = Calendar_Model_Event::RANGE_THIS)
499 $db = $this->_backend->getAdapter();
500 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
502 $sendNotifications = $this->sendNotifications(FALSE);
504 $event = $this->get($_record->getId());
505 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
506 .' Going to update the following event. rawdata: ' . print_r($event->toArray(), true));
508 //NOTE we check via get(full rights) here whereas _updateACLCheck later checks limited rights from search
509 if ($this->_doContainerACLChecks === FALSE || $event->hasGrant(Tinebase_Model_Grants::GRANT_EDIT)) {
510 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " updating event: {$_record->id} (range: {$range})");
512 // we need to resolve groupmembers before free/busy checking
513 Calendar_Model_Attender::resolveGroupMembers($_record->attendee);
514 $this->_inspectEvent($_record);
516 if ($_checkBusyConflicts) {
517 if ($event->isRescheduled($_record) ||
518 count(array_diff($_record->attendee->user_id, $event->attendee->user_id)) > 0
520 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
521 . " Ensure that all attendee are free with free/busy check ... ");
522 $this->checkBusyConflicts($_record);
524 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
525 . " Skipping free/busy check because event has not been rescheduled and no new attender has been added");
529 parent::update($_record);
531 } else if ($_record->attendee instanceof Tinebase_Record_RecordSet) {
532 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
533 . " user has no editGrant for event: {$_record->id}, updating attendee status with valid authKey only");
534 foreach ($_record->attendee as $attender) {
535 if ($attender->status_authkey) {
536 $this->attenderStatusUpdate($_record, $attender, $attender->status_authkey);
541 if ($_record->isRecurException() && in_array($range, array(Calendar_Model_Event::RANGE_ALL, Calendar_Model_Event::RANGE_THISANDFUTURE))) {
542 $this->_updateExdateRange($_record, $range, $event);
545 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
546 } catch (Exception $e) {
547 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Rolling back because: ' . $e);
548 Tinebase_TransactionManager::getInstance()->rollBack();
549 $this->sendNotifications($sendNotifications);
553 $updatedEvent = $this->get($event->getId());
555 // send notifications
556 $this->sendNotifications($sendNotifications);
557 if ($this->_sendNotifications && $_record->mute != 1) {
558 $this->doSendNotifications($updatedEvent, Tinebase_Core::getUser(), 'changed', $event);
560 return $updatedEvent;
564 * inspect update of one record (after update)
566 * @param Tinebase_Record_Interface $updatedRecord the just updated record
567 * @param Tinebase_Record_Interface $record the update record
568 * @param Tinebase_Record_Interface $currentRecord the current record (before update)
571 protected function _inspectAfterUpdate($updatedRecord, $record, $currentRecord)
573 $this->_saveAttendee($record, $currentRecord, $record->isRescheduled($currentRecord));
574 // need to save new attendee set in $updatedRecord for modlog
575 $updatedRecord->attendee = clone($record->attendee);
579 * update range of events starting with given recur exception
581 * @param Calendar_Model_Event $exdate
582 * @param string $range
584 protected function _updateExdateRange($exdate, $range, $oldExdate)
586 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
587 . ' Updating events (range: ' . $range . ') belonging to recur exception event ' . $exdate->getId());
589 $baseEvent = $this->getRecurBaseEvent($exdate);
590 $diff = $oldExdate->diff($exdate);
592 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
593 . ' Exdate diff: ' . print_r($diff->toArray(), TRUE));
595 if ($range === Calendar_Model_Event::RANGE_ALL) {
596 $events = $this->getRecurExceptions($baseEvent);
597 $events->addRecord($baseEvent);
598 $this->_applyExdateDiffToRecordSet($exdate, $diff, $events);
599 } else if ($range === Calendar_Model_Event::RANGE_THISANDFUTURE) {
600 $nextRegularRecurEvent = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $exdate->dtstart);
602 if ($nextRegularRecurEvent == $baseEvent) {
603 // NOTE if a fist instance exception takes place before the
604 // series would start normally, $nextOccurence is the
605 // baseEvent of the series. As createRecurException can't
606 // deal with this situation we update whole series here
607 $this->_updateExdateRange($exdate, Calendar_Model_Event::RANGE_ALL, $oldExdate);
608 } else if ($nextRegularRecurEvent !== NULL && ! $nextRegularRecurEvent->dtstart->isEarlier($exdate->dtstart)) {
609 $this->_applyDiff($nextRegularRecurEvent, $diff, $exdate, FALSE);
611 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
612 . ' Next recur exception event at: ' . $nextRegularRecurEvent->dtstart->toString());
613 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
614 . ' ' . print_r($nextRegularRecurEvent->toArray(), TRUE));
616 $nextRegularRecurEvent->mute = $exdate->mute;
617 $newBaseEvent = $this->createRecurException($nextRegularRecurEvent, FALSE, TRUE);
618 // @todo this should be done by createRecurException
619 $exdatesOfNewBaseEvent = $this->getRecurExceptions($newBaseEvent);
620 $this->_applyExdateDiffToRecordSet($exdate, $diff, $exdatesOfNewBaseEvent);
622 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
623 . ' No upcoming occurrences found.');
629 * apply exdate diff to a recordset of events
631 * @param Calendar_Model_Event $exdate
632 * @param Tinebase_Record_Diff $diff
633 * @param Tinebase_Record_RecordSet $events
635 protected function _applyExdateDiffToRecordSet($exdate, $diff, $events)
637 // make sure baseEvent gets updated first to circumvent concurrency conflicts
638 $events->sort('recurdid', 'ASC');
640 foreach ($events as $event) {
641 if ($event->getId() === $exdate->getId()) {
645 $this->_applyDiff($event, $diff, $exdate, FALSE);
646 $this->update($event);
651 * merge updates from exdate into event
653 * @param Calendar_Model_Event $event
654 * @param Tinebase_Record_Diff $diff
655 * @param Calendar_Model_Event $exdate
656 * @param boolean $overwriteMods
658 * @todo is $overwriteMods needed?
660 protected function _applyDiff($event, $diff, $exdate, $overwriteMods = TRUE)
662 if (! $overwriteMods) {
663 $recentChanges = Tinebase_Timemachine_ModificationLog::getInstance()->getModifications('Calendar', $event, NULL, 'Sql', $exdate->creation_time);
664 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
665 . ' Recent changes (since ' . $exdate->creation_time->toString() . '): ' . print_r($recentChanges->toArray(), TRUE));
667 $recentChanges = new Tinebase_Record_RecordSet('Tinebase_Model_ModificationLog');
670 $diffIgnore = array('organizer', 'seq', 'last_modified_by', 'last_modified_time', 'dtstart', 'dtend');
671 foreach ($diff->diff as $key => $newValue) {
672 if ($key === 'attendee') {
673 if (in_array($key, $recentChanges->modified_attribute)) {
674 $attendeeDiff = $diff->diff['attendee'];
675 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
676 . ' Attendee diff: ' . print_r($attendeeDiff->toArray(), TRUE));
677 foreach ($attendeeDiff['added'] as $attenderToAdd) {
678 $attenderToAdd->setId(NULL);
679 $event->attendee->addRecord($attenderToAdd);
681 foreach ($attendeeDiff['removed'] as $attenderToRemove) {
682 $attenderInCurrentSet = Calendar_Model_Attender::getAttendee($event->attendee, $attenderToRemove);
683 if ($attenderInCurrentSet) {
684 $event->attendee->removeRecord($attenderInCurrentSet);
688 // remove ids of new attendee
689 $attendee = clone($exdate->attendee);
690 foreach ($attendee as $attender) {
691 if (! $event->attendee->getById($attender->getId())) {
692 $attender->setId(NULL);
695 $event->attendee = $attendee;
697 } else if (! in_array($key, $diffIgnore) && ! in_array($key, $recentChanges->modified_attribute)) {
698 $event->{$key} = $exdate->{$key};
700 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
701 . ' Ignore / recently changed: ' . $key);
705 if ((isset($diff->diff['dtstart']) || array_key_exists('dtstart', $diff->diff)) || (isset($diff->diff['dtend']) || array_key_exists('dtend', $diff->diff))) {
706 $this->_applyTimeDiff($event, $exdate);
711 * update multiple records
713 * @param Tinebase_Model_Filter_FilterGroup $_filter
714 * @param array $_data
715 * @return integer number of updated records
717 public function updateMultiple($_filter, $_data)
719 $this->_checkRight('update');
720 $this->checkFilterACL($_filter, 'update');
723 $ids = $this->_backend->search($_filter, NULL, TRUE);
725 foreach ($ids as $eventId) {
726 $event = $this->get($eventId);
727 foreach ($_data as $field => $value) {
728 $event->$field = $value;
731 $this->update($event);
738 * Deletes a set of records.
740 * If one of the records could not be deleted, no record is deleted
742 * @param array $_ids array of record identifiers
743 * @param string $range
745 * @throws Tinebase_Exception_NotFound|Tinebase_Exception
747 public function delete($_ids, $range = Calendar_Model_Event::RANGE_THIS)
749 if ($_ids instanceof $this->_modelName) {
750 $_ids = (array)$_ids->getId();
753 $records = $this->_backend->getMultiple((array) $_ids);
755 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
756 . " Deleting " . count($records) . ' with range ' . $range . ' ...');
758 foreach ($records as $record) {
759 if ($record->isRecurException() && in_array($range, array(Calendar_Model_Event::RANGE_ALL, Calendar_Model_Event::RANGE_THISANDFUTURE))) {
760 $this->_deleteExdateRange($record, $range);
764 $db = $this->_backend->getAdapter();
765 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
767 // delete if delete grant is present
768 if ($this->_doContainerACLChecks === FALSE || $record->hasGrant(Tinebase_Model_Grants::GRANT_DELETE)) {
769 // NOTE delete needs to update sequence otherwise iTIP based protocolls ignore the delete
770 $record->status = Calendar_Model_Event::STATUS_CANCELED;
771 $this->_touch($record);
772 if ($record->isRecurException()) {
774 $baseEvent = $this->getRecurBaseEvent($record);
775 $this->_touch($baseEvent);
776 } catch (Tinebase_Exception_NotFound $tnfe) {
777 // base Event might be gone already
778 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
779 . " BaseEvent of exdate {$record->uid} to delete not found ");
783 parent::delete($record);
786 // otherwise update status for user to DECLINED
787 else if ($record->attendee instanceof Tinebase_Record_RecordSet) {
788 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " user has no deleteGrant for event: " . $record->id . ", updating own status to DECLINED only");
789 $ownContact = Tinebase_Core::getUser()->contact_id;
790 foreach ($record->attendee as $attender) {
791 if ($attender->user_id == $ownContact && in_array($attender->user_type, array(Calendar_Model_Attender::USERTYPE_USER, Calendar_Model_Attender::USERTYPE_GROUPMEMBER))) {
792 $attender->status = Calendar_Model_Attender::STATUS_DECLINED;
793 $this->attenderStatusUpdate($record, $attender, $attender->status_authkey);
798 // increase display container content sequence for all attendee of deleted event
799 if ($record->attendee instanceof Tinebase_Record_RecordSet) {
800 foreach ($record->attendee as $attender) {
801 $this->_increaseDisplayContainerContentSequence($attender, $record, Tinebase_Model_ContainerContent::ACTION_DELETE);
805 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
806 } catch (Exception $e) {
807 Tinebase_TransactionManager::getInstance()->rollBack();
814 * delete range of events starting with given recur exception
816 * NOTE: if exdate is persistent, it will not be deleted by this function
817 * but by the original call of delete
819 * @param Calendar_Model_Event $exdate
820 * @param string $range
822 protected function _deleteExdateRange($exdate, $range)
824 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
825 . ' Deleting events (range: ' . $range . ') belonging to recur exception event ' . $exdate->getId());
827 $baseEvent = $this->getRecurBaseEvent($exdate);
829 if ($range === Calendar_Model_Event::RANGE_ALL) {
830 $this->deleteRecurSeries($exdate);
831 } else if ($range === Calendar_Model_Event::RANGE_THISANDFUTURE) {
832 $nextRegularRecurEvent = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $exdate->dtstart);
834 if ($nextRegularRecurEvent == $baseEvent) {
835 // NOTE if a fist instance exception takes place before the
836 // series would start normally, $nextOccurence is the
837 // baseEvent of the series. As createRecurException can't
838 // deal with this situation we delete whole series here
839 $this->_deleteExdateRange($exdate, Calendar_Model_Event::RANGE_ALL);
841 $this->createRecurException($nextRegularRecurEvent, TRUE, TRUE);
847 * updates a recur series
849 * @param Calendar_Model_Event $_recurInstance
850 * @param bool $_checkBusyConflicts
851 * @return Calendar_Model_Event
853 public function updateRecurSeries($_recurInstance, $_checkBusyConflicts = FALSE)
855 $baseEvent = $this->getRecurBaseEvent($_recurInstance);
857 // replace baseEvent with adopted instance
858 $newBaseEvent = clone $_recurInstance;
859 $newBaseEvent->setId($baseEvent->getId());
860 unset($newBaseEvent->recurid);
861 $newBaseEvent->exdate = $baseEvent->exdate;
863 $this->_applyTimeDiff($newBaseEvent, $_recurInstance, $baseEvent);
865 return $this->update($newBaseEvent, $_checkBusyConflicts);
871 * @param Calendar_Model_Event $newEvent
872 * @param Calendar_Model_Event $fromEvent
873 * @param Calendar_Model_Event $baseEvent
875 protected function _applyTimeDiff($newEvent, $fromEvent, $baseEvent = NULL)
878 $baseEvent = $newEvent;
881 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
882 . ' New event: ' . print_r($newEvent->toArray(), TRUE));
883 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
884 . ' From event: ' . print_r($fromEvent->toArray(), TRUE));
886 // compute time diff (NOTE: if the $fromEvent is the baseEvent, it has no recurid)
887 $originalDtStart = $fromEvent->recurid ? new Tinebase_DateTime(substr($fromEvent->recurid, -19), 'UTC') : clone $baseEvent->dtstart;
889 $dtstartDiff = $originalDtStart->diff($fromEvent->dtstart);
890 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
891 . " Dtstart diff: " . $dtstartDiff->format('%H:%M:%i'));
892 $eventDuration = $fromEvent->dtstart->diff($fromEvent->dtend);
893 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
894 . " Duration diff: " . $dtstartDiff->format('%H:%M:%i'));
896 $newEvent->dtstart = clone $baseEvent->dtstart;
897 $newEvent->dtstart->add($dtstartDiff);
899 $newEvent->dtend = clone $newEvent->dtstart;
900 $newEvent->dtend->add($eventDuration);
904 * creates an exception instance of a recurring event
906 * NOTE: deleting persistent exceptions is done via a normal delete action
907 * and handled in the deleteInspection
909 * @param Calendar_Model_Event $_event
910 * @param bool $_deleteInstance
911 * @param bool $_allFollowing
912 * @param bool $_checkBusyConflicts
913 * @return Calendar_Model_Event exception Event | updated baseEvent
915 * @todo replace $_allFollowing param with $range
916 * @deprecated replace with create/update/delete
918 public function createRecurException($_event, $_deleteInstance = FALSE, $_allFollowing = FALSE, $_checkBusyConflicts = FALSE)
920 $baseEvent = $this->getRecurBaseEvent($_event);
922 if ($baseEvent->last_modified_time != $_event->last_modified_time) {
923 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
924 . " It is not allowed to create recur instance if it is clone of base event");
925 throw new Tinebase_Timemachine_Exception_ConcurrencyConflict('concurrency conflict!');
929 // // exdates needs to stay in baseEvents container
930 // if ($_event->container_id != $baseEvent->container_id) {
931 // throw new Calendar_Exception_ExdateContainer();
934 // check if this is an exception to the first occurence
935 if ($baseEvent->getId() == $_event->getId()) {
936 if ($_allFollowing) {
937 throw new Exception('please edit or delete complete series!');
939 // NOTE: if the baseEvent gets a time change, we can't compute the recurdid w.o. knowing the original dtstart
940 $recurid = $baseEvent->setRecurId($baseEvent->getId());
941 unset($baseEvent->recurid);
942 $_event->recurid = $recurid;
945 // just do attender status update if user has no edit grant
946 if ($this->_doContainerACLChecks && !$baseEvent->{Tinebase_Model_Grants::GRANT_EDIT}) {
947 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
948 . " user has no editGrant for event: '{$baseEvent->getId()}'. Only creating exception for attendee status");
949 if ($_event->attendee instanceof Tinebase_Record_RecordSet) {
950 foreach ($_event->attendee as $attender) {
951 if ($attender->status_authkey) {
952 $exceptionAttender = $this->attenderStatusCreateRecurException($_event, $attender, $attender->status_authkey, $_allFollowing);
957 if (! isset($exceptionAttender)) {
958 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG) && $_event->attendee instanceof Tinebase_Record_RecordSet) {
959 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " Failed to update attendee: " . print_r($_event->attendee->toArray(), true));
961 throw new Tinebase_Exception_AccessDenied('Failed to update attendee, status authkey might be missing');
964 return $this->get($exceptionAttender->cal_event_id);
967 // NOTE: recurid is computed by rrule recur computations and therefore is already part of the event.
968 if (empty($_event->recurid)) {
969 throw new Exception('recurid must be present to create exceptions!');
972 // we do notifications ourself
973 $sendNotifications = $this->sendNotifications(FALSE);
975 // EDIT for baseEvent is checked above, CREATE, DELETE for recur exceptions is implied with it
976 $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
978 $db = $this->_backend->getAdapter();
979 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
981 $exdate = new Tinebase_DateTime(substr($_event->recurid, -19));
982 $exdates = is_array($baseEvent->exdate) ? $baseEvent->exdate : array();
983 $originalDtstart = $_event->getOriginalDtStart();
984 $originalEvent = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $originalDtstart);
986 if ($_allFollowing != TRUE) {
987 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
988 . " Adding exdate for: '{$_event->recurid}'");
990 array_push($exdates, $exdate);
991 $baseEvent->exdate = $exdates;
992 $updatedBaseEvent = $this->update($baseEvent, FALSE);
994 if ($_deleteInstance == FALSE) {
995 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
996 . " Creating persistent exception for: '{$_event->recurid}'");
997 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
998 . " Recur exception: " . print_r($_event->toArray(), TRUE));
1000 $_event->base_event_id = $baseEvent->getId();
1001 $_event->setId(NULL);
1002 unset($_event->rrule);
1003 unset($_event->exdate);
1005 foreach (array('attendee', 'notes', 'alarms') as $prop) {
1006 if ($_event->{$prop} instanceof Tinebase_Record_RecordSet) {
1007 $_event->{$prop}->setId(NULL);
1011 $originalDtstart = $_event->getOriginalDtStart();
1012 $dtStartHasDiff = $originalDtstart->compare($_event->dtstart) != 0; // php52 compat
1014 if (! $dtStartHasDiff) {
1015 $attendees = $_event->attendee;
1016 unset($_event->attendee);
1018 $note = $_event->notes;
1019 unset($_event->notes);
1020 $persistentExceptionEvent = $this->create($_event, $_checkBusyConflicts);
1022 if (! $dtStartHasDiff) {
1023 // we save attendee seperatly to preserve their attributes
1024 if ($attendees instanceof Tinebase_Record_RecordSet) {
1025 $attendees->cal_event_id = $persistentExceptionEvent->getId();
1026 $calendar = Tinebase_Container::getInstance()->getContainerById($_event->container_id);
1027 foreach ($attendees as $attendee) {
1028 $this->_createAttender($attendee, $_event, TRUE, $calendar);
1029 $this->_increaseDisplayContainerContentSequence($attendee, $persistentExceptionEvent, Tinebase_Model_ContainerContent::ACTION_CREATE);
1034 // @todo save notes and add a update note -> what was updated? -> modlog is also missing
1035 $persistentExceptionEvent = $this->get($persistentExceptionEvent->getId());
1039 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " shorten recur series for/to: '{$_event->recurid}'");
1041 // split past/future exceptions
1042 $pastExdates = array();
1043 $futureExdates = array();
1044 foreach($exdates as $exdate) {
1045 $exdate->isLater($_event->dtstart) ? $futureExdates[] = $exdate : $pastExdates[] = $exdate;
1048 $persistentExceptionEvents = $this->getRecurExceptions($_event);
1049 $pastPersistentExceptionEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
1050 $futurePersistentExceptionEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
1051 foreach ($persistentExceptionEvents as $persistentExceptionEvent) {
1052 $persistentExceptionEvent->dtstart->isLater($_event->dtstart) ? $futurePersistentExceptionEvents->addRecord($persistentExceptionEvent) : $pastPersistentExceptionEvents->addRecord($persistentExceptionEvent);
1056 $rrule = Calendar_Model_Rrule::getRruleFromString($baseEvent->rrule);
1057 if (isset($rrule->count)) {
1058 // get all occurences and find the split
1060 $exdate = $baseEvent->exdate;
1061 $baseEvent->exdate = NULL;
1062 //$baseCountOccurrence = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $baseEvent->rrule_until, $baseCount);
1063 $recurSet = Calendar_Model_Rrule::computeRecurrenceSet($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $baseEvent->dtstart, $baseEvent->rrule_until);
1064 $baseEvent->exdate = $exdate;
1066 $originalDtstart = $_event->getOriginalDtStart();
1067 foreach($recurSet as $idx => $rInstance) {
1068 if ($rInstance->dtstart >= $originalDtstart) break;
1071 $rrule->count = $idx+1;
1073 $lastBaseOccurence = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $_event->getOriginalDtStart()->subSecond(1), -1);
1074 $rrule->until = $lastBaseOccurence ? $lastBaseOccurence->getOriginalDtStart() : $baseEvent->dtstart;
1076 $baseEvent->rrule = (string) $rrule;
1077 $baseEvent->exdate = $pastExdates;
1079 // NOTE: we don't want implicit attendee updates
1080 //$updatedBaseEvent = $this->update($baseEvent, FALSE);
1081 $this->_inspectEvent($baseEvent);
1082 $updatedBaseEvent = parent::update($baseEvent);
1084 if ($_deleteInstance == TRUE) {
1085 // delete all future persistent events
1086 $this->delete($futurePersistentExceptionEvents->getId());
1088 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " create new recur series for/at: '{$_event->recurid}'");
1090 // NOTE: in order to move exceptions correctly in time we need to find out the original dtstart
1091 // and create the new baseEvent with this time. A following update also updates its exceptions
1092 $originalDtstart = new Tinebase_DateTime(substr($_event->recurid, -19));
1093 $adoptedDtstart = clone $_event->dtstart;
1094 $dtStartHasDiff = $adoptedDtstart->compare($originalDtstart) != 0; // php52 compat
1095 $eventLength = $_event->dtstart->diff($_event->dtend);
1097 $_event->dtstart = clone $originalDtstart;
1098 $_event->dtend = clone $originalDtstart;
1099 $_event->dtend->add($eventLength);
1102 if (isset($rrule->count)) {
1103 $baseCount = $rrule->count;
1104 $rrule = Calendar_Model_Rrule::getRruleFromString($_event->rrule);
1105 $rrule->count = $rrule->count - $baseCount;
1106 $_event->rrule = (string) $rrule;
1109 $_event->setId(Tinebase_Record_Abstract::generateUID());
1110 $_event->uid = $futurePersistentExceptionEvents->uid = Tinebase_Record_Abstract::generateUID();
1111 $_event->setId(Tinebase_Record_Abstract::generateUID());
1112 $futurePersistentExceptionEvents->setRecurId($_event->getId());
1113 unset($_event->recurid);
1114 unset($_event->base_event_id);
1115 foreach(array('attendee', 'notes', 'alarms') as $prop) {
1116 if ($_event->{$prop} instanceof Tinebase_Record_RecordSet) {
1117 $_event->{$prop}->setId(NULL);
1120 $_event->exdate = $futureExdates;
1122 $attendees = $_event->attendee; unset($_event->attendee);
1123 $note = $_event->notes; unset($_event->notes);
1124 $persistentExceptionEvent = $this->create($_event, $_checkBusyConflicts && $dtStartHasDiff);
1126 // we save attendee separately to preserve their attributes
1127 if ($attendees instanceof Tinebase_Record_RecordSet) {
1128 foreach($attendees as $attendee) {
1129 $this->_createAttender($attendee, $persistentExceptionEvent, true);
1133 // @todo save notes and add a update note -> what was updated? -> modlog is also missing
1135 $persistentExceptionEvent = $this->get($persistentExceptionEvent->getId());
1137 foreach($futurePersistentExceptionEvents as $futurePersistentExceptionEvent) {
1138 $this->update($futurePersistentExceptionEvent, FALSE);
1141 if ($dtStartHasDiff) {
1142 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " new recur series has adpted dtstart -> update to adopt exceptions'");
1143 $persistentExceptionEvent->dtstart = clone $adoptedDtstart;
1144 $persistentExceptionEvent->dtend = clone $adoptedDtstart;
1145 $persistentExceptionEvent->dtend->add($eventLength);
1147 $persistentExceptionEvent = $this->update($persistentExceptionEvent, $_checkBusyConflicts);
1152 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1154 // restore original notification handling
1155 $this->sendNotifications($sendNotifications);
1156 $notificationAction = $_deleteInstance ? 'deleted' : 'changed';
1157 $notificationEvent = $_deleteInstance ? $_event : $persistentExceptionEvent;
1160 $this->doContainerACLChecks($doContainerACLChecks);
1162 // send notifications
1163 if ($this->_sendNotifications && $_event->mute != 1) {
1164 // NOTE: recur exception is a fake event from client.
1165 // this might lead to problems, so we wrap the calls
1167 if (count($_event->attendee) > 0) {
1168 $_event->attendee->bypassFilters = TRUE;
1170 $_event->created_by = $baseEvent->created_by;
1172 $this->doSendNotifications($notificationEvent, Tinebase_Core::getUser(), $notificationAction, $originalEvent);
1173 } catch (Exception $e) {
1174 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getTraceAsString());
1175 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " could not send notification {$e->getMessage()}");
1179 return $_deleteInstance ? $updatedBaseEvent : $persistentExceptionEvent;
1184 * @see Tinebase_Controller_Record_Abstract::get()
1186 public function get($_id, $_containerId = NULL, $_getRelatedData = TRUE)
1188 if (preg_match('/^fakeid/', $_id)) {
1189 // get base event when trying to fetch a non-persistent recur instance
1190 return $this->getRecurBaseEvent(new Calendar_Model_Event(array('uid' => substr(str_replace('fakeid', '', $_id), 0, 40))), TRUE);
1192 return parent::get($_id, $_containerId, $_getRelatedData);
1197 * returns base event of a recurring series
1199 * @param Calendar_Model_Event $_event
1200 * @return Calendar_Model_Event
1202 public function getRecurBaseEvent($_event)
1204 $baseEventId = $_event->base_event_id ?: $_event->id;
1206 if (! $baseEventId) {
1207 throw new Tinebase_Exception_NotFound('base event of a recurring series not found');
1210 // make sure we have a 'fully featured' event
1211 return $this->get($baseEventId);
1215 * returns all persistent recur exceptions of recur series identified by uid of given event
1217 * NOTE: deleted instances are saved in the base events exception property
1218 * NOTE: returns all exceptions regardless of current filters and access restrictions
1220 * @param Calendar_Model_Event $_event
1221 * @param boolean $_fakeDeletedInstances
1222 * @param Calendar_Model_EventFilter $_eventFilter
1223 * @return Tinebase_Record_RecordSet of Calendar_Model_Event
1225 public function getRecurExceptions($_event, $_fakeDeletedInstances = FALSE, $_eventFilter = NULL)
1227 $baseEventId = $_event->base_event_id ?: $_event->id;
1229 $exceptionFilter = new Calendar_Model_EventFilter(array(
1230 array('field' => 'base_event_id', 'operator' => 'equals', 'value' => $baseEventId),
1233 if ($_eventFilter instanceof Calendar_Model_EventFilter) {
1234 $exceptionFilter->addFilterGroup($_eventFilter);
1237 $exceptions = $this->_backend->search($exceptionFilter);
1239 if ($_fakeDeletedInstances) {
1240 $baseEvent = $this->getRecurBaseEvent($_event);
1241 $this->fakeDeletedExceptions($baseEvent, $exceptions);
1244 $exceptions->exdate = NULL;
1245 $exceptions->rrule = NULL;
1246 $exceptions->rrule_until = NULL;
1252 * add exceptions events for deleted instances
1254 * @param Calendar_Model_Event $baseEvent
1255 * @param Tinebase_Record_RecordSet $exceptions
1257 public function fakeDeletedExceptions($baseEvent, $exceptions)
1259 $eventLength = $baseEvent->dtstart->diff($baseEvent->dtend);
1261 // compute remaining exdates
1262 $deletedInstanceDtStarts = array_diff(array_unique((array) $baseEvent->exdate), $exceptions->getOriginalDtStart());
1264 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1265 ' Faking ' . count($deletedInstanceDtStarts) . ' deleted exceptions');
1267 foreach((array) $deletedInstanceDtStarts as $deletedInstanceDtStart) {
1268 $fakeEvent = clone $baseEvent;
1269 $fakeEvent->setId(NULL);
1271 $fakeEvent->dtstart = clone $deletedInstanceDtStart;
1272 $fakeEvent->dtend = clone $deletedInstanceDtStart;
1273 $fakeEvent->dtend->add($eventLength);
1274 $fakeEvent->is_deleted = TRUE;
1275 $fakeEvent->setRecurId($baseEvent->getId());
1276 $fakeEvent->rrule = null;
1278 $exceptions->addRecord($fakeEvent);
1283 * adopt alarm time to next occurrence for recurring events
1285 * @param Tinebase_Record_Abstract $_record
1286 * @param Tinebase_Model_Alarm $_alarm
1287 * @param bool $_nextBy {instance|time} set recurr alarm to next from given instance or next by current time
1290 public function adoptAlarmTime(Tinebase_Record_Abstract $_record, Tinebase_Model_Alarm $_alarm, $_nextBy = 'time')
1292 if ($_record->rrule) {
1293 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1294 ' Adopting alarm time for next recur occurrence (by ' . $_nextBy . ')');
1295 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
1296 ' ' . print_r($_record->toArray(), TRUE));
1298 if ($_nextBy == 'time') {
1299 // NOTE: this also finds instances running right now
1300 $from = Tinebase_DateTime::now();
1303 $recurid = $_alarm->getOption('recurid');
1304 $instanceStart = $recurid ? new Tinebase_DateTime(substr($recurid, -19)) : clone $_record->dtstart;
1305 $eventLength = $_record->dtstart->diff($_record->dtend);
1307 $instanceStart->setTimezone($_record->originator_tz);
1308 $from = $instanceStart->add($eventLength);
1309 $from->setTimezone('UTC');
1313 $exceptions = $this->getRecurExceptions($_record);
1314 $nextOccurrence = Calendar_Model_Rrule::computeNextOccurrence($_record, $exceptions, $from);
1316 if ($nextOccurrence === NULL) {
1317 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
1318 ' Recur series is over, no more alarms pending');
1320 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1321 ' Found next occurrence, adopting alarm to dtstart ' . $nextOccurrence->dtstart->toString());
1324 // save recurid so we know for which recurrance the alarm is for
1325 $_alarm->setOption('recurid', isset($nextOccurrence) ? $nextOccurrence->recurid : NULL);
1327 $_alarm->sent_status = $nextOccurrence ? Tinebase_Model_Alarm::STATUS_PENDING : Tinebase_Model_Alarm::STATUS_SUCCESS;
1328 $_alarm->sent_message = $nextOccurrence ? '' : 'Nothing to send, series is over';
1330 $eventStart = $nextOccurrence ? clone $nextOccurrence->dtstart : clone $_record->dtstart;
1332 $eventStart = clone $_record->dtstart;
1335 // save minutes before / compute it for custom alarms
1336 $minutesBefore = $_alarm->minutes_before == Tinebase_Model_Alarm::OPTION_CUSTOM
1337 ? ($_record->dtstart->getTimestamp() - $_alarm->alarm_time->getTimestamp()) / 60
1338 : $_alarm->minutes_before;
1339 $minutesBefore = round($minutesBefore);
1341 $_alarm->setOption('minutes_before', $minutesBefore);
1342 $_alarm->alarm_time = $eventStart->subMinute($minutesBefore);
1344 if ($_record->rrule && $_alarm->sent_status == Tinebase_Model_Alarm::STATUS_PENDING && $_alarm->alarm_time < $_alarm->sent_time) {
1345 $this->adoptAlarmTime($_record, $_alarm, 'instance');
1348 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
1349 ' alarm: ' . print_r($_alarm->toArray(), true));
1352 /****************************** overwritten functions ************************/
1355 * restore original alarm time of recurring events
1357 * @param Tinebase_Record_Abstract $_record
1360 protected function _inspectAlarmGet(Tinebase_Record_Abstract $_record)
1362 foreach ($_record->alarms as $alarm) {
1363 if ($recurid = $alarm->getOption('recurid')) {
1364 $alarm->alarm_time = clone $_record->dtstart;
1365 $alarm->alarm_time->subMinute((int) $alarm->getOption('minutes_before'));
1369 parent::_inspectAlarmGet($_record);
1373 * adopt alarm time to next occurance for recurring events
1375 * @param Tinebase_Record_Abstract $_record
1376 * @param Tinebase_Model_Alarm $_alarm
1377 * @param bool $_nextBy {instance|time} set recurr alarm to next from given instance or next by current time
1379 * @throws Tinebase_Exception_InvalidArgument
1381 protected function _inspectAlarmSet(Tinebase_Record_Abstract $_record, Tinebase_Model_Alarm $_alarm, $_nextBy = 'time')
1383 parent::_inspectAlarmSet($_record, $_alarm);
1384 $this->adoptAlarmTime($_record, $_alarm, 'time');
1388 * inspect update of one record
1390 * @param Tinebase_Record_Interface $_record the update record
1391 * @param Tinebase_Record_Interface $_oldRecord the current persistent record
1394 protected function _inspectBeforeUpdate($_record, $_oldRecord)
1396 // if dtstart of an event changes, we update the originator_tz, alarm times
1397 if (! $_oldRecord->dtstart->equals($_record->dtstart)) {
1398 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' dtstart changed -> adopting organizer_tz');
1399 $_record->originator_tz = Tinebase_Core::getUserTimezone();
1400 if (! empty($_record->rrule)) {
1401 $diff = $_oldRecord->dtstart->diff($_record->dtstart);
1402 $this->_updateRecurIdOfExdates($_record, $diff);
1406 // delete recur exceptions if update is no longer a recur series
1407 if (! empty($_oldRecord->rrule) && empty($_record->rrule)) {
1408 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' deleting recur exceptions as event is no longer a recur series');
1409 $this->_backend->delete($this->getRecurExceptions($_record));
1412 // touch base event of a recur series if an persistent exception changes
1413 if ($_record->recurid) {
1414 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' touch base event of a persistent exception');
1415 $baseEvent = $this->getRecurBaseEvent($_record);
1416 $this->_touch($baseEvent, TRUE);
1421 * update exdates and recurids if dtstart of an recurevent changes
1423 * @param Calendar_Model_Event $_record
1424 * @param DateInterval $diff
1426 protected function _updateRecurIdOfExdates($_record, $diff)
1428 // update exceptions
1429 $exceptions = $this->getRecurExceptions($_record);
1430 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' dtstart of a series changed -> adopting '. count($exceptions) . ' recurid(s)');
1432 foreach ($exceptions as $exception) {
1433 $exception->recurid = new Tinebase_DateTime(substr($exception->recurid, -19));
1434 Calendar_Model_Rrule::addUTCDateDstFix($exception->recurid, $diff, $_record->originator_tz);
1435 $exdates[] = $exception->recurid;
1437 $exception->setRecurId($_record->getId());
1438 $this->_backend->update($exception);
1441 $_record->exdate = $exdates;
1445 * inspect before create/update
1447 * @TODO move stuff from other places here
1448 * @param Calendar_Model_Event $_record the record to inspect
1450 protected function _inspectEvent($_record)
1452 $_record->uid = $_record->uid ? $_record->uid : Tinebase_Record_Abstract::generateUID();
1453 $_record->organizer = $_record->organizer ? $_record->organizer : Tinebase_Core::getUser()->contact_id;
1454 $_record->transp = $_record->transp ? $_record->transp : Calendar_Model_Event::TRANSP_OPAQUE;
1456 $this->_inspectOriginatorTZ($_record);
1458 if ($_record->hasExternalOrganizer()) {
1459 // assert calendarUser as attendee. This is important to keep the event in the loop via its displaycontianer(s)
1461 $container = Tinebase_Container::getInstance()->getContainerById($_record->container_id);
1462 $owner = $container->getOwner();
1463 $calendarUserId = Addressbook_Controller_Contact::getInstance()->getContactByUserId($owner, true)->getId();
1464 } catch (Exception $e) {
1466 $calendarUserId = Tinebase_Core::getUser()->contact_id;
1469 $attendee = $_record->assertAttendee(new Calendar_Model_Attender(array(
1470 'user_type' => Calendar_Model_Attender::USERTYPE_USER,
1471 'user_id' => $calendarUserId
1472 )), false, false, true);
1474 if ($attendee && $container instanceof Tinebase_Model_Container) {
1475 $attendee->displaycontainer_id = $container->getId();
1478 if (! $container instanceof Tinebase_Model_Container || $container->type == Tinebase_Model_Container::TYPE_PERSONAL) {
1479 // move into special (external users) container
1480 $container = Calendar_Controller::getInstance()->getInvitationContainer($_record->resolveOrganizer());
1481 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1482 . ' Setting container_id to ' . $container->getId() . ' for external organizer ' . $_record->organizer->email);
1483 $_record->container_id = $container->getId();
1488 if ($_record->is_all_day_event) {
1489 // harmonize datetimes of all day events
1490 $_record->setTimezone($_record->originator_tz);
1491 if (! $_record->dtend) {
1492 $_record->dtend = clone $_record->dtstart;
1493 $_record->dtend->setTime(23,59,59);
1495 $_record->dtstart->setTime(0,0,0);
1496 $_record->dtend->setTime(23,59,59);
1497 $_record->setTimezone('UTC');
1499 $_record->setRruleUntil();
1501 if ($_record->rrule instanceof Calendar_Model_Rrule) {
1502 $_record->rrule->normalize($_record);
1505 if ($_record->isRecurException()) {
1506 $baseEvent = $this->getRecurBaseEvent($_record);
1508 // remove invalid rrules
1509 if ($_record->rrule !== NULL) {
1510 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1511 . ' Removing invalid rrule from recur exception: ' . $_record->rrule);
1512 $_record->rrule = NULL;
1516 // // exdates needs to stay in baseEvents container
1517 // if($_record->container_id != $baseEvent->container_id) {
1518 // throw new Calendar_Exception_ExdateContainer();
1524 * checks/sets originator timezone
1527 * @throws Tinebase_Exception_Record_Validation
1529 protected function _inspectOriginatorTZ($record)
1531 $record->originator_tz = $record->originator_tz ? $record->originator_tz : Tinebase_Core::getUserTimezone();
1534 new DateTimeZone($record->originator_tz);
1535 } catch (Exception $e) {
1536 throw new Tinebase_Exception_Record_Validation('Bad Timezone: ' . $record->originator_tz);
1541 * inspects delete action
1543 * @param array $_ids
1544 * @return array of ids to actually delete
1546 protected function _inspectDelete(array $_ids) {
1547 $events = $this->_backend->getMultiple($_ids);
1549 foreach ($events as $event) {
1551 // implicitly delete persistent recur instances of series
1552 if (! empty($event->rrule)) {
1553 $exceptionIds = $this->getRecurExceptions($event)->getId();
1554 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1555 . ' Implicitly deleting ' . (count($exceptionIds) - 1 ) . ' persistent exception(s) for recurring series with uid' . $event->uid);
1556 $_ids = array_merge($_ids, $exceptionIds);
1560 $this->_deleteAlarmsForIds($_ids);
1562 return array_unique($_ids);
1566 * redefine required grants for get actions
1568 * @param Tinebase_Model_Filter_FilterGroup $_filter
1569 * @param string $_action get|update
1571 public function checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
1573 $hasGrantsFilter = FALSE;
1574 foreach($_filter->getAclFilters() as $aclFilter) {
1575 if ($aclFilter instanceof Calendar_Model_GrantFilter) {
1576 $hasGrantsFilter = TRUE;
1581 if (! $hasGrantsFilter) {
1582 // force a grant filter
1583 // NOTE: actual grants are set via setRequiredGrants later
1584 $grantsFilter = $_filter->createFilter('grants', 'in', '@setRequiredGrants');
1585 $_filter->addFilter($grantsFilter);
1588 parent::checkFilterACL($_filter, $_action);
1590 if ($_action == 'get') {
1591 $_filter->setRequiredGrants(array(
1592 Tinebase_Model_Grants::GRANT_FREEBUSY,
1593 Tinebase_Model_Grants::GRANT_READ,
1594 Tinebase_Model_Grants::GRANT_ADMIN,
1600 * check grant for action (CRUD)
1602 * @param Tinebase_Record_Interface $_record
1603 * @param string $_action
1604 * @param boolean $_throw
1605 * @param string $_errorMessage
1606 * @param Tinebase_Record_Interface $_oldRecord
1608 * @throws Tinebase_Exception_AccessDenied
1610 * @todo use this function in other create + update functions
1611 * @todo invent concept for simple adding of grants (plugins?)
1613 protected function _checkGrant($_record, $_action, $_throw = TRUE, $_errorMessage = 'No Permission.', $_oldRecord = NULL)
1615 if ( ! $this->_doContainerACLChecks
1616 // admin grant includes all others (only if class is PUBLIC)
1617 || (! empty($this->class) && $this->class === Calendar_Model_Event::CLASS_PUBLIC
1618 && $_record->container_id && Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADMIN))
1619 // external invitations are in a spechial invitaion calendar. only attendee can see it via displaycal
1620 || $_record->hasExternalOrganizer()
1627 // NOTE: free/busy is not a read grant!
1628 $hasGrant = $_record->hasGrant(Tinebase_Model_Grants::GRANT_READ);
1630 $_record->doFreeBusyCleanup();
1634 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADD);
1637 $hasGrant = (bool) $_oldRecord->hasGrant(Tinebase_Model_Grants::GRANT_EDIT);
1639 if ($_oldRecord->container_id != $_record->container_id) {
1640 $hasGrant &= Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADD)
1641 && $_oldRecord->hasGrant(Tinebase_Model_Grants::GRANT_DELETE);
1645 $hasGrant = (bool) $_record->hasGrant(Tinebase_Model_Grants::GRANT_DELETE);
1648 $hasGrant = (bool) $_record->hasGrant(Tinebase_Model_Grants::GRANT_SYNC);
1651 $hasGrant = (bool) $_record->hasGrant(Tinebase_Model_Grants::GRANT_EXPORT);
1657 throw new Tinebase_Exception_AccessDenied($_errorMessage);
1659 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1660 . ' No permissions to ' . $_action . ' in container ' . $_record->container_id);
1668 * touches (sets seq, last_modified_time and container content sequence) given event
1673 protected function _touch($_event, $_setModifier = FALSE)
1675 $_event->last_modified_time = Tinebase_DateTime::now();
1676 $_event->seq = (int)$_event->seq + 1;
1677 if ($_setModifier) {
1678 $_event->last_modified_by = Tinebase_Core::getUser()->getId();
1681 $this->_backend->update($_event);
1683 $this->_increaseContainerContentSequence($_event, Tinebase_Model_ContainerContent::ACTION_UPDATE);
1687 * increase container content sequence
1689 * @param Tinebase_Record_Interface $_record
1690 * @param string $action
1692 protected function _increaseContainerContentSequence(Tinebase_Record_Interface $record, $action = NULL)
1694 parent::_increaseContainerContentSequence($record, $action);
1696 if ($record->attendee instanceof Tinebase_Record_RecordSet) {
1697 $updatedContainerIds = array($record->container_id);
1698 foreach ($record->attendee as $attender) {
1699 if (isset($attender->displaycontainer_id) && ! in_array($attender->displaycontainer_id, $updatedContainerIds)) {
1700 Tinebase_Container::getInstance()->increaseContentSequence($attender->displaycontainer_id, $action, $record->getId());
1701 $updatedContainerIds[] = $attender->displaycontainer_id;
1708 /****************************** attendee functions ************************/
1711 * creates an attender status exception of a recurring event series
1713 * NOTE: Recur exceptions are implicitly created
1715 * @param Calendar_Model_Event $_recurInstance
1716 * @param Calendar_Model_Attender $_attender
1717 * @param string $_authKey
1718 * @param bool $_allFollowing
1719 * @return Calendar_Model_Attender updated attender
1721 public function attenderStatusCreateRecurException($_recurInstance, $_attender, $_authKey, $_allFollowing = FALSE)
1724 $db = $this->_backend->getAdapter();
1725 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
1727 $baseEvent = $this->getRecurBaseEvent($_recurInstance);
1728 $baseEventAttendee = Calendar_Model_Attender::getAttendee($baseEvent->attendee, $_attender);
1730 if ($baseEvent->getId() == $_recurInstance->getId()) {
1731 // exception to the first occurence
1732 $_recurInstance->setRecurId($baseEvent->getId());
1735 // NOTE: recurid is computed by rrule recur computations and therefore is already part of the event.
1736 if (empty($_recurInstance->recurid)) {
1737 throw new Exception('recurid must be present to create exceptions!');
1741 // check if we already have a persistent exception for this event
1742 $eventInstance = $this->_backend->getByProperty($_recurInstance->recurid, $_property = 'recurid');
1744 // NOTE: the user must exist (added by someone with appropriate rights by createRecurException)
1745 $exceptionAttender = Calendar_Model_Attender::getAttendee($eventInstance->attendee, $_attender);
1746 if (! $exceptionAttender) {
1747 throw new Tinebase_Exception_AccessDenied('not an attendee');
1751 if ($exceptionAttender->status_authkey != $_authKey) {
1752 // NOTE: it might happen, that the user set her status from the base event without knowing about
1753 // an existing exception. In this case the base event authkey is also valid
1754 if (! $baseEventAttendee || $baseEventAttendee->status_authkey != $_authKey) {
1755 throw new Tinebase_Exception_AccessDenied('Attender authkey mismatch');
1759 } catch (Tinebase_Exception_NotFound $e) {
1760 // otherwise create it implicilty
1762 // check if this intance takes place
1763 if (in_array($_recurInstance->dtstart, (array)$baseEvent->exdate)) {
1764 throw new Tinebase_Exception_AccessDenied('Event instance is deleted and may not be recreated via status setting!');
1767 if (! $baseEventAttendee) {
1768 throw new Tinebase_Exception_AccessDenied('not an attendee');
1771 if ($baseEventAttendee->status_authkey != $_authKey) {
1772 throw new Tinebase_Exception_AccessDenied('Attender authkey mismatch');
1775 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " creating recur exception for a exceptional attendee status");
1777 $doContainerAclChecks = $this->doContainerACLChecks(FALSE);
1778 $sendNotifications = $this->sendNotifications(FALSE);
1780 // NOTE: the user might have no edit grants, so let's be carefull
1781 $diff = $baseEvent->dtstart->diff($baseEvent->dtend);
1783 $baseEvent->dtstart = new Tinebase_DateTime(substr($_recurInstance->recurid, -19), 'UTC');
1784 $baseEvent->dtend = clone $baseEvent->dtstart;
1785 $baseEvent->dtend->add($diff);
1787 $baseEvent->base_event_id = $baseEvent->id;
1788 $baseEvent->id = $_recurInstance->id;
1789 $baseEvent->recurid = $_recurInstance->recurid;
1791 $attendee = $baseEvent->attendee;
1792 unset($baseEvent->attendee);
1794 $eventInstance = $this->createRecurException($baseEvent, FALSE, $_allFollowing);
1795 $eventInstance->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
1796 $this->doContainerACLChecks($doContainerAclChecks);
1797 $this->sendNotifications($sendNotifications);
1799 foreach ($attendee as $attender) {
1800 $attender->setId(NULL);
1801 $attender->cal_event_id = $eventInstance->getId();
1803 $attender = $this->_backend->createAttendee($attender);
1804 $eventInstance->attendee->addRecord($attender);
1805 $this->_increaseDisplayContainerContentSequence($attender, $eventInstance, Tinebase_Model_ContainerContent::ACTION_CREATE);
1808 $exceptionAttender = Calendar_Model_Attender::getAttendee($eventInstance->attendee, $_attender);
1811 $exceptionAttender->status = $_attender->status;
1812 $exceptionAttender->transp = $_attender->transp;
1813 $eventInstance->alarms = clone $_recurInstance->alarms;
1814 $eventInstance->alarms->setId(NULL);
1816 $updatedAttender = $this->attenderStatusUpdate($eventInstance, $exceptionAttender, $exceptionAttender->status_authkey);
1818 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1819 } catch (Exception $e) {
1820 Tinebase_TransactionManager::getInstance()->rollBack();
1824 return $updatedAttender;
1828 * updates an attender status of a event
1830 * @param Calendar_Model_Event $_event
1831 * @param Calendar_Model_Attender $_attender
1832 * @param string $_authKey
1833 * @return Calendar_Model_Attender updated attender
1835 public function attenderStatusUpdate(Calendar_Model_Event $_event, Calendar_Model_Attender $_attender, $_authKey)
1838 $event = $this->get($_event->getId());
1840 if (! $event->attendee) {
1841 throw new Tinebase_Exception_NotFound('Could not find any attendee of event.');
1844 if (($currentAttender = Calendar_Model_Attender::getAttendee($event->attendee, $_attender)) == null) {
1845 throw new Tinebase_Exception_NotFound('Could not find attender in event.');
1848 $updatedAttender = clone $currentAttender;
1850 if ($currentAttender->status_authkey !== $_authKey) {
1851 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
1852 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " no permissions to update status for {$currentAttender->user_type} {$currentAttender->user_id}");
1853 return $updatedAttender;
1856 Calendar_Controller_Alarm::enforceACL($_event, $event);
1858 $currentAttenderDisplayContainerId = $currentAttender->displaycontainer_id instanceof Tinebase_Model_Container ?
1859 $currentAttender->displaycontainer_id->getId() :
1860 $currentAttender->displaycontainer_id;
1862 $attenderDisplayContainerId = $_attender->displaycontainer_id instanceof Tinebase_Model_Container ?
1863 $_attender->displaycontainer_id->getId() :
1864 $_attender->displaycontainer_id;
1866 // check if something what can be set as user has changed
1867 if ($currentAttender->status == $_attender->status &&
1868 $currentAttenderDisplayContainerId == $attenderDisplayContainerId &&
1869 $currentAttender->transp == $_attender->transp &&
1870 ! Calendar_Controller_Alarm::hasUpdates($_event, $event)
1872 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
1873 Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__ . "no status change -> do nothing");
1874 return $updatedAttender;
1877 $updatedAttender->status = $_attender->status;
1878 $updatedAttender->displaycontainer_id = isset($_attender->displaycontainer_id) ? $_attender->displaycontainer_id : $updatedAttender->displaycontainer_id;
1879 $updatedAttender->transp = isset($_attender->transp) ? $_attender->transp : Calendar_Model_Event::TRANSP_OPAQUE;
1881 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
1882 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1883 . " update attender status to {$_attender->status} for {$currentAttender->user_type} {$currentAttender->user_id}");
1884 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1885 . ' set alarm_ack_time / alarm_snooze_time: ' . $updatedAttender->alarm_ack_time . ' / ' . $updatedAttender->alarm_snooze_time);
1888 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1890 $updatedAttender = $this->_backend->updateAttendee($updatedAttender);
1891 if ($_event->alarms instanceof Tinebase_Record_RecordSet) {
1892 foreach($_event->alarms as $alarm) {
1893 $this->_inspectAlarmSet($event, $alarm);
1896 Tinebase_Alarm::getInstance()->setAlarmsOfRecord($_event);
1899 $this->_increaseDisplayContainerContentSequence($updatedAttender, $event);
1901 if ($currentAttender->status != $updatedAttender->status) {
1902 $this->_touch($event, TRUE);
1905 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1906 } catch (Exception $e) {
1907 Tinebase_TransactionManager::getInstance()->rollBack();
1911 // send notifications
1912 if ($currentAttender->status != $updatedAttender->status && $this->_sendNotifications && $_event->mute != 1) {
1913 $updatedEvent = $this->get($event->getId());
1914 $this->doSendNotifications($updatedEvent, Tinebase_Core::getUser(), 'changed', $event);
1917 return $updatedAttender;
1921 * saves all attendee of given event
1923 * NOTE: This function is executed in a create/update context. As such the user
1924 * has edit/update the event and can do anything besides status settings of attendee
1926 * @todo add support for resources
1928 * @param Calendar_Model_Event $_event
1929 * @param Calendar_Model_Event $_currentEvent
1930 * @param bool $_isRescheduled event got rescheduled reset all attendee status
1932 protected function _saveAttendee($_event, $_currentEvent = NULL, $_isRescheduled = FALSE)
1934 if (! $_event->attendee instanceof Tinebase_Record_RecordSet) {
1935 $_event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
1938 Calendar_Model_Attender::resolveEmailOnlyAttendee($_event);
1940 $_event->attendee->cal_event_id = $_event->getId();
1942 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " About to save attendee for event {$_event->id} ");
1944 $currentAttendee = $_currentEvent->attendee;
1946 $diff = $currentAttendee->getMigration($_event->attendee->getArrayOfIds());
1948 $calendar = Tinebase_Container::getInstance()->getContainerById($_event->container_id);
1951 $this->_backend->deleteAttendee($diff['toDeleteIds']);
1952 foreach ($diff['toDeleteIds'] as $deleteAttenderId) {
1953 $idx = $currentAttendee->getIndexById($deleteAttenderId);
1954 if ($idx !== FALSE) {
1955 $currentAttenderToDelete = $currentAttendee[$idx];
1956 $this->_increaseDisplayContainerContentSequence($currentAttenderToDelete, $_event, Tinebase_Model_ContainerContent::ACTION_DELETE);
1960 // create/update attendee
1961 foreach ($_event->attendee as $attender) {
1962 $attenderId = $attender->getId();
1963 $idx = ($attenderId) ? $currentAttendee->getIndexById($attenderId) : FALSE;
1965 if ($idx !== FALSE) {
1966 $currentAttender = $currentAttendee[$idx];
1967 $this->_updateAttender($attender, $currentAttender, $_event, $_isRescheduled, $calendar);
1969 $this->_createAttender($attender, $_event, FALSE, $calendar);
1975 * creates a new attender
1977 * @param Calendar_Model_Attender $attender
1978 * @param Tinebase_Model_Container $_calendar
1979 * @param boolean $preserveStatus
1980 * @param Tinebase_Model_Container $calendar
1982 protected function _createAttender(Calendar_Model_Attender $attender, Calendar_Model_Event $event, $preserveStatus = FALSE, Tinebase_Model_Container $calendar = NULL)
1985 $attender->user_type = isset($attender->user_type) ? $attender->user_type : Calendar_Model_Attender::USERTYPE_USER;
1986 $attender->cal_event_id = $event->getId();
1987 $calendar = ($calendar) ? $calendar : Tinebase_Container::getInstance()->getContainerById($event->container_id);
1989 $userAccountId = $attender->getUserAccountId();
1991 // generate auth key
1992 if (! $attender->status_authkey) {
1993 $attender->status_authkey = Tinebase_Record_Abstract::generateUID();
1996 // attach to display calendar if attender has/is a useraccount
1997 if ($userAccountId) {
1998 if ($calendar->type == Tinebase_Model_Container::TYPE_PERSONAL && Tinebase_Container::getInstance()->hasGrant($userAccountId, $calendar, Tinebase_Model_Grants::GRANT_ADMIN)) {
1999 // if attender has admin grant to personal physical container, this phys. cal also gets displ. cal
2000 $attender->displaycontainer_id = $calendar->getId();
2001 } else if ($attender->displaycontainer_id && $userAccountId == Tinebase_Core::getUser()->getId() && Tinebase_Container::getInstance()->hasGrant($userAccountId, $attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_ADMIN)) {
2002 // allow user to set his own displ. cal
2003 $attender->displaycontainer_id = $attender->displaycontainer_id;
2005 $displayCalId = self::getDefaultDisplayContainerId($userAccountId);
2006 $attender->displaycontainer_id = $displayCalId;
2009 } else if ($attender->user_type === Calendar_Model_Attender::USERTYPE_RESOURCE) {
2010 $resource = Calendar_Controller_Resource::getInstance()->get($attender->user_id);
2011 $attender->displaycontainer_id = $resource->container_id;
2014 if ($attender->displaycontainer_id) {
2015 // check if user is allowed to set status
2016 if (! $preserveStatus && ! Tinebase_Core::getUser()->hasGrant($attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_EDIT)) {
2017 $attender->status = Calendar_Model_Attender::STATUS_NEEDSACTION;
2021 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " New attender: " . print_r($attender->toArray(), TRUE));
2023 Tinebase_Timemachine_ModificationLog::getInstance()->setRecordMetaData($attender, 'create');
2024 $this->_backend->createAttendee($attender);
2025 $this->_increaseDisplayContainerContentSequence($attender, $event, Tinebase_Model_ContainerContent::ACTION_CREATE);
2030 * returns the default calendar
2032 * @return Tinebase_Model_Container
2034 public function getDefaultCalendar()
2036 return Tinebase_Container::getInstance()->getDefaultContainer($this->_applicationName, NULL, Calendar_Preference::DEFAULTCALENDAR);
2040 * returns default displayContainer id of given attendee
2042 * @param string $userAccountId
2044 public static function getDefaultDisplayContainerId($userAccountId)
2046 $userAccountId = Tinebase_Model_User::convertUserIdToInt($userAccountId);
2047 $displayCalId = Tinebase_Core::getPreference('Calendar')->getValueForUser(Calendar_Preference::DEFAULTCALENDAR, $userAccountId);
2050 // assert that displaycal is of type personal
2051 $container = Tinebase_Container::getInstance()->getContainerById($displayCalId);
2052 if ($container->type != Tinebase_Model_Container::TYPE_PERSONAL) {
2053 $displayCalId = NULL;
2055 } catch (Exception $e) {
2056 $displayCalId = NULL;
2059 if (! isset($displayCalId)) {
2060 $containers = Tinebase_Container::getInstance()->getPersonalContainer($userAccountId, 'Calendar_Model_Event', $userAccountId, 0, true);
2061 if ($containers->count() > 0) {
2062 $displayCalId = $containers->getFirstRecord()->getId();
2066 return $displayCalId;
2070 * increases content sequence of attender display container
2072 * @param Calendar_Model_Attender $attender
2073 * @param Calendar_Model_Event $event
2074 * @param string $action
2076 protected function _increaseDisplayContainerContentSequence($attender, $event, $action = Tinebase_Model_ContainerContent::ACTION_UPDATE)
2078 if ($event->container_id === $attender->displaycontainer_id || empty($attender->displaycontainer_id)) {
2079 // no need to increase sequence
2083 Tinebase_Container::getInstance()->increaseContentSequence($attender->displaycontainer_id, $action, $event->getId());
2087 * updates an attender
2089 * @param Calendar_Model_Attender $attender
2090 * @param Calendar_Model_Attender $currentAttender
2091 * @param Calendar_Model_Event $event
2092 * @param bool $isRescheduled event got rescheduled reset all attendee status
2093 * @param Tinebase_Model_Container $calendar
2095 protected function _updateAttender($attender, $currentAttender, $event, $isRescheduled, $calendar = NULL)
2097 $userAccountId = $currentAttender->getUserAccountId();
2099 // update display calendar if attender has/is a useraccount
2100 if ($userAccountId) {
2101 if ($calendar->type == Tinebase_Model_Container::TYPE_PERSONAL && Tinebase_Container::getInstance()->hasGrant($userAccountId, $calendar, Tinebase_Model_Grants::GRANT_ADMIN)) {
2102 // if attender has admin grant to personal physical container, this phys. cal also gets displ. cal
2103 $attender->displaycontainer_id = $calendar->getId();
2104 } else if ($userAccountId == Tinebase_Core::getUser()->getId() && Tinebase_Container::getInstance()->hasGrant($userAccountId, $attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_ADMIN)) {
2105 // allow user to set his own displ. cal
2106 $attender->displaycontainer_id = $attender->displaycontainer_id;
2108 $attender->displaycontainer_id = $currentAttender->displaycontainer_id;
2112 // reset status if user has no right and authkey is wrong
2113 if ($attender->displaycontainer_id) {
2114 if (! Tinebase_Core::getUser()->hasGrant($attender->displaycontainer_id, Tinebase_Model_Grants::GRANT_EDIT)
2115 && $attender->status_authkey != $currentAttender->status_authkey) {
2117 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
2118 . ' Wrong authkey, resetting status (' . $attender->status . ' -> ' . $currentAttender->status . ')');
2119 $attender->status = $currentAttender->status;
2123 // reset all status but calUser on reschedule
2124 if ($isRescheduled && !$attender->isSame($this->getCalendarUser())) {
2125 $attender->status = Calendar_Model_Attender::STATUS_NEEDSACTION;
2126 $attender->transp = null;
2129 // preserve old authkey
2130 $attender->status_authkey = $currentAttender->status_authkey;
2132 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
2133 . " Updating attender: " . print_r($attender->toArray(), TRUE));
2136 Tinebase_Timemachine_ModificationLog::getInstance()->setRecordMetaData($attender, 'update', $currentAttender);
2137 Tinebase_Timemachine_ModificationLog::getInstance()->writeModLog($attender, $currentAttender, get_class($attender), $this->_getBackendType(), $attender->getId());
2138 $this->_backend->updateAttendee($attender);
2140 if ($attender->displaycontainer_id !== $currentAttender->displaycontainer_id) {
2141 $this->_increaseDisplayContainerContentSequence($currentAttender, $event, Tinebase_Model_ContainerContent::ACTION_DELETE);
2142 $this->_increaseDisplayContainerContentSequence($attender, $event, Tinebase_Model_ContainerContent::ACTION_CREATE);
2144 $this->_increaseDisplayContainerContentSequence($attender, $event);
2149 * event handler for group updates
2151 * @param Tinebase_Model_Group $_group
2154 public function onUpdateGroup($_groupId)
2156 $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
2158 $filter = new Calendar_Model_EventFilter(array(
2159 array('field' => 'attender', 'operator' => 'equals', 'value' => array(
2160 'user_type' => Calendar_Model_Attender::USERTYPE_GROUP,
2161 'user_id' => $_groupId
2163 array('field' => 'period', 'operator' => 'within', 'value' => array(
2164 'from' => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
2165 'until' => Tinebase_DateTime::now()->addYear(100)->get(Tinebase_Record_Abstract::ISO8601LONG))
2168 $events = $this->search($filter, new Tinebase_Model_Pagination(), FALSE, FALSE);
2170 foreach($events as $event) {
2172 if (! $event->rrule) {
2173 // update non recurring futrue events
2174 Calendar_Model_Attender::resolveGroupMembers($event->attendee);
2175 $this->update($event);
2177 // update thisandfuture for recurring events
2178 $nextOccurrence = Calendar_Model_Rrule::computeNextOccurrence($event, $this->getRecurExceptions($event), Tinebase_DateTime::now());
2179 Calendar_Model_Attender::resolveGroupMembers($nextOccurrence->attendee);
2181 if ($nextOccurrence->dtstart != $event->dtstart) {
2182 $this->createRecurException($nextOccurrence, FALSE, TRUE);
2184 $this->update($nextOccurrence);
2187 } catch (Exception $e) {
2188 Tinebase_Core::getLogger()->NOTICE(__METHOD__ . '::' . __LINE__ . " could not update attendee");
2192 $this->doContainerACLChecks($doContainerACLChecks);
2195 /****************************** alarm functions ************************/
2200 * @param Tinebase_Model_Alarm $_alarm
2203 * NOTE: the given alarm is raw and has not passed _inspectAlarmGet
2205 * @todo throw exception on error
2207 public function sendAlarm(Tinebase_Model_Alarm $_alarm)
2209 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " About to send alarm " . print_r($_alarm->toArray(), TRUE));
2211 $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
2214 $event = $this->get($_alarm->record_id);
2215 $event->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm', array($_alarm));
2216 $this->_inspectAlarmGet($event);
2217 } catch (Exception $e) {
2218 $this->doContainerACLChecks($doContainerACLChecks);
2222 $this->doContainerACLChecks($doContainerACLChecks);
2224 if ($event->rrule) {
2225 $recurid = $_alarm->getOption('recurid');
2227 // adopts the (referenced) alarm and sets alarm time to next occurance
2228 parent::_inspectAlarmSet($event, $_alarm);
2229 $this->adoptAlarmTime($event, $_alarm, 'instance');
2231 // sent_status might have changed in adoptAlarmTime()
2232 if ($_alarm->sent_status !== Tinebase_Model_Alarm::STATUS_PENDING) {
2233 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2234 . ' Not sending alarm for event at ' . $event->dtstart->toString() . ' with status ' . $_alarm->sent_status);
2239 // NOTE: In case of recuring events $event is always the baseEvent,
2240 // so we might need to adopt event time to recur instance.
2241 $diff = $event->dtstart->diff($event->dtend);
2243 $event->dtstart = new Tinebase_DateTime(substr($recurid, -19));
2245 $event->dtend = clone $event->dtstart;
2246 $event->dtend->add($diff);
2249 if ($event->exdate && in_array($event->dtstart, $event->exdate)) {
2250 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2251 . " Not sending alarm because instance at " . $event->dtstart->toString() . ' is an exception.');
2256 Calendar_Controller_EventNotifications::getInstance()->doSendNotifications($event, Tinebase_Core::getUser(), 'alarm', NULL, $_alarm);
2260 * send notifications
2262 * @param Calendar_Model_Event $_event
2263 * @param Tinebase_Model_FullAccount $_updater
2264 * @param Sting $_action
2265 * @param Calendar_Model_Event $_oldEvent
2266 * @param Array $_additionalRecipients
2269 public function doSendNotifications($_event, $_updater, $_action, $_oldEvent = NULL)
2271 Tinebase_ActionQueue::getInstance()->queueAction('Calendar.sendEventNotifications',
2275 $_oldEvent ? $_oldEvent : NULL
2279 public function compareCalendars($cal1, $cal2, $from, $until)
2281 $matchingEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
2282 $changedEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
2283 $missingEventsInCal1 = new Tinebase_Record_RecordSet('Calendar_Model_Event');
2284 $missingEventsInCal2 = new Tinebase_Record_RecordSet('Calendar_Model_Event');
2285 $cal2EventIdsAlreadyProcessed = array();
2287 while ($from->isEarlier($until)) {
2289 $endWeek = $from->getClone()->addWeek(1);
2291 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2292 . ' Comparing period ' . $from . ' - ' . $endWeek);
2294 // get all events from cal1+cal2 for the week
2295 $cal1Events = $this->_getEventsForPeriodAndCalendar($cal1, $from, $endWeek);
2296 $cal1EventsClone = clone $cal1Events;
2297 $cal2Events = $this->_getEventsForPeriodAndCalendar($cal2, $from, $endWeek);
2298 $cal2EventsClone = clone $cal2Events;
2301 if (count($cal1Events) == 0 && count($cal2Events) == 0) {
2302 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2303 . ' No events found');
2307 foreach ($cal1Events as $event) {
2308 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2309 . ' Checking event "' . $event->summary . '" ' . $event->dtstart . ' - ' . $event->dtend);
2311 if ($event->container_id != $cal1) {
2312 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2313 . ' Event is in another calendar - skip');
2314 $cal1Events->removeRecord($event);
2318 $summaryMatch = $cal2Events->filter('summary', $event->summary);
2319 if (count($summaryMatch) > 0) {
2320 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2321 . " Found " . count($summaryMatch) . ' events with matching summaries');
2323 $dtStartMatch = $summaryMatch->filter('dtstart', $event->dtstart);
2324 if (count($dtStartMatch) > 0) {
2325 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2326 . " Found " . count($summaryMatch) . ' events with matching dtstarts and summaries');
2328 $matchingEvents->merge($dtStartMatch);
2329 // remove from cal1+cal2
2330 $cal1Events->removeRecord($event);
2331 $cal2Events->removeRecords($dtStartMatch);
2332 $cal2EventIdsAlreadyProcessed = array_merge($cal2EventIdsAlreadyProcessed, $dtStartMatch->getArrayOfIds());
2334 $changedEvents->merge($summaryMatch);
2335 $cal1Events->removeRecord($event);
2336 $cal2Events->removeRecords($summaryMatch);
2337 $cal2EventIdsAlreadyProcessed = array_merge($cal2EventIdsAlreadyProcessed, $summaryMatch->getArrayOfIds());
2342 // add missing events
2343 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2344 . " Found " . count($cal1Events) . ' events missing in cal2');
2345 $missingEventsInCal2->merge($cal1Events);
2347 // compare cal2 -> cal1 and add events as missing from cal1 that we did not detect before
2348 foreach ($cal2EventsClone as $event) {
2349 if (in_array($event->getId(), $cal2EventIdsAlreadyProcessed)) {
2352 if ($event->container_id != $cal2) {
2353 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2354 . ' Event is in another calendar - skip');
2358 $missingEventsInCal1->addRecord($event);
2363 'matching' => $matchingEvents,
2364 'changed' => $changedEvents,
2365 'missingInCal1' => $missingEventsInCal1,
2366 'missingInCal2' => $missingEventsInCal2,
2371 protected function _getEventsForPeriodAndCalendar($calendarId, $from, $until)
2373 $filter = new Calendar_Model_EventFilter(array(
2374 array('field' => 'period', 'operator' => 'within', 'value' =>
2375 array("from" => $from, "until" => $until)
2377 array('field' => 'container_id', 'operator' => 'equals', 'value' => $calendarId),
2380 $events = Calendar_Controller_Event::getInstance()->search($filter);
2381 Calendar_Model_Rrule::mergeAndRemoveNonMatchingRecurrences($events, $filter);
2386 * add calendar owner as attendee if not already set
2388 * @param string $calendarId
2389 * @param Tinebase_DateTime $from
2390 * @param Tinebase_DateTime $until
2391 * @param boolean $dry run
2393 * @return number of updated events
2395 public function repairAttendee($calendarId, $from, $until, $dry = false)
2397 $container = Tinebase_Container::getInstance()->getContainerById($calendarId);
2398 if ($container->type !== Tinebase_Model_Container::TYPE_PERSONAL) {
2399 throw new Calendar_Exception('Only allowed for personal containers!');
2401 if ($container->owner_id !== Tinebase_Core::getUser()->getId()) {
2402 throw new Calendar_Exception('Only allowed for own containers!');
2406 while ($from->isEarlier($until)) {
2407 $endWeek = $from->getClone()->addWeek(1);
2409 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2410 . ' Repairing period ' . $from . ' - ' . $endWeek);
2413 // TODO we need to detect events with DECLINED/DELETED attendee
2414 $events = $this->_getEventsForPeriodAndCalendar($calendarId, $from, $endWeek);
2418 if (count($events) == 0) {
2419 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2420 . ' No events found');
2424 foreach ($events as $event) {
2425 // add attendee if not already set
2426 if ($event->isRecurInstance()) {
2427 // TODO get base event
2428 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2429 . ' Skip recur instance ' . $event->toShortString());
2433 $ownAttender = Calendar_Model_Attender::getOwnAttender($event->attendee);
2434 if (! $ownAttender) {
2435 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2436 . ' Add missing attender to event ' . $event->toShortString());
2438 $attender = new Calendar_Model_Attender(array(
2439 'user_type' => Calendar_Model_Attender::USERTYPE_USER,
2440 'user_id' => Tinebase_Core::getUser()->contact_id,
2441 'status' => Calendar_Model_Attender::STATUS_ACCEPTED
2443 $event->attendee->addRecord($attender);
2445 $this->update($event);
2452 return $updateCount;